Randomly place animated gifs inside of container in Flutter - flutter

I want to randomly place animated gifs inside of a sized container. My problem is that the gifs are always placed outside of the container
This is the widget which gives me the functionality to generate a gridview and place gifs at specific positions inside of it.
class MultiGifPage extends StatefulWidget {
final Map<int, GifImage> gifs;
final Map<int, Matrix4> positions;
MultiGifPage({required this.gifs, required this.positions});
_MultiGifPageState createState() => _MultiGifPageState(this.gifs, this.positions);
}
class _MultiGifPageState extends State<MultiGifPage> with TickerProviderStateMixin {
Map<int, GifImage> gifs;
Map<int, Matrix4> positions;
_MultiGifPageState(this.gifs, this.positions);
Random random = new Random();
#override
Widget build(BuildContext context) {
return Scaffold(
body: GridView.count(
crossAxisCount: (MediaQuery.of(context).size.width / 100).round(),
children: List.generate(gifs.length, (index) {
return Container(child: gifs[index], transform: positions[index]);
}),
),
);
}
}
Here you can see where I am trying to use the other functionality:
class _EqualsWidget extends State<EqualsWidget> with TickerProviderStateMixin {
late GifImage _gif = GifImage(
image: AssetImage("assets/images/growing_plant_transparent.gif"),
controller: gif_Controller_tree);
int gif_index = 0;
Map<int, GifImage> _list = new Map();
Map<int, Matrix4> positions = new Map();
Random random = new Random();
late Size size = new Size(0, 0);
late double xAxis = 0;
late double yAxis = 0;
late GlobalKey equal_key = new GlobalKey();
#override
void initState() {
super.initState();
//print('INSTANCE: WidgetsBinding.instance.addPostFrameCallback(_afterLayout)}');
equal_key = new GlobalKey();
size = new Size(0, 0);
xAxis = 0;
yAxis = 0;
WidgetsBinding.instance?.addPostFrameCallback(_afterLayout);
}
_afterLayout(_) {
_getWidgetInfo();
Timer.periodic(Duration(milliseconds: 1000), (timer) {
FlutterGifController controller = new FlutterGifController(vsync: this);
controller.animateTo(89, duration: Duration(milliseconds: 1000));
_list.addAll({
gif_index: GifImage(
image: AssetImage("assets/images/growing_plant_transparent.gif"),
controller: controller)
});
double x = random.nextDouble() * (xAxis + size.width);
double y = random.nextDouble() * (yAxis + size.height);
positions.addAll({gif_index: Matrix4.translationValues(x, y, 0)});
++gif_index;
setState(() {});
});
}
void _getWidgetInfo() {
final RenderBox renderBox =
equal_key.currentContext?.findRenderObject() as RenderBox;
size = renderBox.size;
print('Size: ${size.width}, ${size.height}');
final Offset offset = renderBox.localToGlobal(Offset.zero);
print('Offset: ${offset.dx}, ${offset.dy}');
print(
'Position: ${(offset.dx + size.width) / 2}, ${(offset.dy + size.height) / 2}');
xAxis = offset.dx;
print(xAxis);
yAxis = offset.dy;
}
Widget equalText(
String typeTxt, double convert, String endTxt, width, height, gif) {
var formatter = NumberFormat.decimalPattern('vi_VN');
return Container(
key: equal_key,
decoration: BoxDecoration(border: Border.all(color: Colors.black)),
height: height / 6,
width: width / 2,
child: Center(
child: Column(children: [
Expanded(child: MultiGifPage(gifs: _list, positions: positions)),
Text(
typeTxt +
' ${formatter.format((_endSum * convert).round())} ' +
endTxt,
style: TextStyle(fontFamily: Config.FONT),
)
])),
);
}
}
I have also tried a simpler version where I just use the width / 2 height / 6 of the container but that also does not work. I just want that the Container is filled with the gifs and I hav no clue how to do it and would really appreciate some help :)

Related

Prevent Small Hops in Drag Upon Touch Up/End in Flutter

I have built a custom slider and have been using GestureDetector with onHorizontalDragUpdate to report drag changes, update the UI and value.
However, when a user lifts their finger, there can sometimes be a small, unintentional hop/drag, enough to adjust the value on the slider and reduce accuracy. How can I stop this occuring?
I have considered adding a small delay to prevent updates if the drag hasn't moved for a tiny period and assessing the primaryDelta, but unsure if this would be fit for purpose or of there is a more routine common practive to prevent this.
--
Example of existing drag logic I am using. The initial drag data is from onHorizontalDragUpdate in _buildThumb. When the slider is rebuilt, the track size and thumb position is calculated in the LayoutBuilder and then the value is calculated based on the thumb position.
double valueForPosition({required double min, required double max}) {
double posIncrements = ((max) / (_divisions));
double posIncrement = (_thumbPosX / (posIncrements));
double incrementVal =
(increment) * (posIncrement + widget.minimumValue).round() +
(widget.minimumValue - widget.minimumValue.truncate());
return incrementVal.clamp(widget.minimumValue, widget.maximumValue);
}
double thumbPositionForValue({required double min, required double max}) {
return (max / (widget.maximumValue - widget.minimumValue - 1)) *
(value - widget.minimumValue - 1);
}
double trackWidthForValue({
required double min,
required double max,
required double thumbPosition,
}) {
return (thumbPosition + (_thumbTouchZoneWidth / 2))
.clamp(min, max)
.toDouble();
}
bool isDragging = false;
bool isSnapping = false;
Widget _buildSlider() {
return SizedBox(
height: _contentHeight,
child: LayoutBuilder(
builder: (context, constraints) {
double minThumbPosX = -(_thumbTouchZoneWidth - _thumbWidth) / 2;
double maxThumbPosX =
constraints.maxWidth - (_thumbTouchZoneWidth / 2);
if (isDragging) {
_thumbPosX = _thumbPosX.clamp(minThumbPosX, maxThumbPosX);
value = valueForPosition(min: minThumbPosX, max: maxThumbPosX);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
widget.onChanged(value);
});
} else {
_thumbPosX = thumbPositionForValue(
min: minThumbPosX,
max: maxThumbPosX,
);
}
double minTrackWidth = 0;
double maxTrackWidth = constraints.maxWidth;
double trackWidth = 0;
if (isDragging) {
trackWidth = (_thumbPosX + (_thumbTouchZoneWidth / 2))
.clamp(_thumbWidth, constraints.maxWidth);
} else {
trackWidth = trackWidthForValue(
min: minTrackWidth,
max: maxTrackWidth,
thumbPosition: _thumbPosX,
);
}
return Stack(
alignment: Alignment.centerLeft,
clipBehavior: Clip.none,
children: [
_buildLabels(),
_buildInactiveTrack(),
Positioned(
width: trackWidth,
child: _buildActiveTrack(),
),
Positioned(
left: _thumbPosX,
child: _buildThumb(),
),
],
);
},
),
);
}
Widget _buildThumb() {
return GestureDetector(
behavior: HitTestBehavior.opaque,
dragStartBehavior: DragStartBehavior.down,
onHorizontalDragUpdate: (details) {
setState(() {
_thumbPosX += details.delta.dx;
isDragging = true;
});
},
child: // Thumb UI
);
}
Updated: I make a little adjustment by adding a delay state and lastChangedTime.
If the user stops dragging for a short period (3 sec), the slider will be locked until the next new value is updated + a short delay (1.5 sec)
I follow your train of thought and make a simple example from Slider widget.
Is the result act like your expected? (You can adjust the Duration to any number)
DartPad: https://dartpad.dev/?id=95f2bd6d004604b3c37f27dd2852cb31
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({super.key});
#override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
double _currentSliderValue = 20;
DateTime lastChangedTime = DateTime.now();
bool isDalying = false;
#override
Widget build(BuildContext context) {
return Column(
children: [
Text(_currentSliderValue.toString()),
const SizedBox(height: 30),
Slider(
value: _currentSliderValue,
max: 100,
label: _currentSliderValue.round().toString(),
onChanged: (double value) async {
if (isDalying) {
await Future.delayed(
Duration(milliseconds: 1500),
() => isDalying = false,
);
} else {
if (DateTime.now().difference(lastChangedTime) >
Duration(seconds: 3)) {
isDalying = true;
} else {
setState(() {
_currentSliderValue = value;
});
}
}
lastChangedTime = DateTime.now();
},
),
],
);
}
}

Dynamically layout widgets based on the size/position of other widgets (sub-widgets)

I'd like to draw a graph of nodes and edges. The graph should appear like a forest of trees. To simplify my question, let's focus on a tree, which should be drawn like this:
I'm not in search of the algorithm, which computes positions of nodes on the drawing plane. Recursively computing sizes and positions of visual representations of a nodes / subtrees using depth first tree traversal is trivial.
I'm in search of a flutter implementation:
I suppose, a Stack and Positioned widgets would be fine for placing nodes.
But how do I get the rendered size / dimension of a widget and recursively the dimension of a subtree?
And how do I get this dimensions when the code is about to place nodes / subtrees using Positioned on Stack?
Could you please provide an example or a recipe?
Update 2022-09-25
As Randal Schwartz and PixelToast pointed out, boxy is a great solution to meet my current goal. Great solution created by #PixelToast!
Nevertheless, I'll keep the question open, in case someone posts details regarding the rendering / measuring process.
Unfortunately these kinds of layouts are not possible in Flutter without lots of boilerplate and a custom RenderObject. I am the author of the Boxy package which makes the process of creating one much simpler.
Here is a working solution:
class TreeNode {
const TreeNode(this.widget, [this.children = const []]);
final Widget widget;
final List<TreeNode> children;
Iterable<Widget> get allWidgets =>
[widget].followedBy(children.expand((e) => e.allWidgets));
}
class TreeView extends StatelessWidget {
const TreeView({
required this.root,
required this.verticalSpacing,
required this.horizontalSpacing,
super.key,
});
final TreeNode root;
final double verticalSpacing;
final double horizontalSpacing;
#override
Widget build(BuildContext context) {
return CustomBoxy(
delegate: _TreeViewBoxy(
root: root,
verticalSpacing: verticalSpacing,
horizontalSpacing: horizontalSpacing,
),
children: [...root.allWidgets],
);
}
}
class _TreeViewBoxy extends BoxyDelegate {
_TreeViewBoxy({
required this.root,
required this.verticalSpacing,
required this.horizontalSpacing,
});
final TreeNode root;
final double verticalSpacing;
final double horizontalSpacing;
#override
Size layout() {
var index = 0;
Size visit(TreeNode node, Offset offset) {
final nodeIndex = index++;
final child = children[nodeIndex];
final size = child.layout(const BoxConstraints());
final Size subtreeSize;
if (node.children.isEmpty) {
subtreeSize = size;
} else {
var width = 0.0;
var height = 0.0;
var x = 0.0;
final y = offset.dy + child.size.height + verticalSpacing;
for (final child in node.children) {
final childSize = visit(child, Offset(offset.dx + x, y));
height = max(height, childSize.height);
width += childSize.width;
x += childSize.width + horizontalSpacing;
}
width += (node.children.length - 1) * horizontalSpacing;
subtreeSize = Size(
max(width, size.width),
size.height + height + verticalSpacing,
);
}
child.position(
offset +
Offset(
subtreeSize.width / 2 - child.size.width / 2,
0,
),
);
return subtreeSize;
}
return visit(root, Offset.zero);
}
#override
void paint() {
var index = 0;
void paintLines(TreeNode node) {
final nodeOffset = children[index++].rect.bottomCenter;
for (final child in node.children) {
final childOffset = children[index].rect.topCenter;
canvas.drawPath(
Path()
..moveTo(nodeOffset.dx, nodeOffset.dy)
..cubicTo(
nodeOffset.dx,
nodeOffset.dy + verticalSpacing,
childOffset.dx,
childOffset.dy - verticalSpacing,
childOffset.dx,
childOffset.dy,
),
Paint()
..style = PaintingStyle.stroke
..strokeWidth = 3.0,
);
paintLines(child);
}
}
paintLines(root);
}
#override
bool shouldRelayout(_TreeViewBoxy oldDelegate) =>
root != oldDelegate.root ||
verticalSpacing != oldDelegate.verticalSpacing ||
horizontalSpacing != oldDelegate.horizontalSpacing;
}
The full example can be found here: https://gist.github.com/PixelToast/3739dee678ee1b19e4d299c0025794b9
You want the Boxy package in the Pub. There's even an example of a hierarchical set of widgets being displayed in the Examples.
This partial solution layouts Nodes like so:
Unfortunately, drawing edges might not be possible. Nevertheless...
Structure of nodes:
Node AA = Node( title: 'AA' );
Node ABA = Node( title: 'ABA' );
Node ABB = Node( title: 'ABB' );
Node ABC = Node( title: 'ABC' );
Node AB = Node( title: 'AB', children: [ ABA, ABB, ABC ] );
Node AC = Node( title: 'AC' );
Node AD = Node( title: 'AD' );
Node root = Node( title: 'A', children: [ AA, AB, AC, AD ] );
Calling the layout:
TreeViewSample( root: root )
TreeViewSample:
import 'package:flutter/material.dart';
class Node {
Node({required this.title, this.children = const []});
String title = '-';
List<Node> children = [];
}
/**
* Layouts Nodes like a tree, root at the top of the drawing area
*/
class TreeViewSample extends StatefulWidget {
TreeViewSample({Key? key, required this.root}) : super(key: key);
Node root;
#override
State<TreeViewSample> createState() => _TreeViewSampleState();
}
class _TreeViewSampleState extends State<TreeViewSample> {
TextStyle style = TextStyle(fontSize: 20, color: Colors.grey);
#override
Widget build(BuildContext context) {
if ( widget.root.children.isEmpty ) {
return Container(
margin: EdgeInsets.all(20.0),
child: Text(widget.root.title, style: style));
} else {
return Container(
margin: EdgeInsets.all(20.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Container(child: Text(widget.root.title, style: style)),
SizedBox(height: 5),
Container(
decoration: BoxDecoration(
border: Border(top: BorderSide(color: Colors.white))),
margin: EdgeInsets.all(10.0),
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
...widget.root.children
.map((e) => TreeViewSample(root: e))
])),
]));
}
}
}

How to animate a widget by non-linear path?

I have a floating widget which I want to animate moving across the screen on a Bezier path.
I already have a path var which changes according to drag and fling gestures (predefined final offset).
How do I make the widget follow such a path?
Edit: as per request here's the code.
It's mostly a fork of https://pub.dev/packages/pip_view package.
You can look at the _onPadEnd, which is where I'm triggering the animation. Just trying to see how to make it follow a Bezier path.
import 'package:flutter/material.dart';
import 'dismiss_keyboard.dart';
import 'constants.dart';
import 'dart:developer';
import 'dart:math' hide log;
import 'package:arc_animator/arc_animator.dart'; //unused import
double sqrtNegSup(double val){ return val.isNegative ? -(sqrt(-val)) : sqrt(val); }
Map<PIPViewCorner, Offset> _calculateOffsets({Size spaceSize, Size widgetSize, EdgeInsets windowPadding,}) {
Offset getOffsetForCorner(PIPViewCorner corner) {
final spacing = 16;
final left = spacing + windowPadding.left;
final top = spacing + windowPadding.top;
final right = spaceSize.width - widgetSize.width - windowPadding.right - spacing;
final bottom = spaceSize.height - widgetSize.height - windowPadding.bottom - spacing;
switch (corner) {
case PIPViewCorner.topLeft: return Offset(left, top);
case PIPViewCorner.topRight: return Offset(right, top);
case PIPViewCorner.bottomLeft: return Offset(left, bottom);
case PIPViewCorner.bottomRight: return Offset(right, bottom);
default: throw Exception('Not implemented.');
}
}
final corners = PIPViewCorner.values;
final Map<PIPViewCorner, Offset> offsets = {};
for (final corner in corners) {
offsets[corner] = getOffsetForCorner(corner);
}
return offsets;
}
enum PIPViewCorner {
topLeft,
topRight,
bottomLeft,
bottomRight,
}
class _CornerDistance {
final PIPViewCorner corner;
final double distance;
_CornerDistance({this.corner, this.distance,});
}
class PIPView extends StatefulWidget {
final PIPViewCorner initialCorner;
final double floatingWidth;
final double floatingHeight;
final bool avoidKeyboard;
final Widget Function(BuildContext context, bool isFloating, ) builder;
const PIPView({
Key key, #required this.builder,
this.initialCorner = PIPViewCorner.topRight,
this.floatingWidth,
this.floatingHeight,
this.avoidKeyboard = true,
}) : super(key: key);
#override PIPViewState createState() => PIPViewState();
static PIPViewState of(BuildContext context) {return context.findAncestorStateOfType<PIPViewState>();}
}
/// pan functions correspond to callbacks of dragging gestures
/// those functions will update the last drag and offset vars accordingly
///
/// animations are performed independently
/// and are triggered when toggling or dragging is concluded
class PIPViewState extends State<PIPView> with TickerProviderStateMixin {
Widget _bottomView;
AnimationController _toggleFloatingAnimationController; //animation for toggling PIP
AnimationController _dragAnimationController; //animation for dragging PIP
PIPViewCorner _corner;
Offset _dragOffset = Offset.zero;
bool _isDragging = false;
Map<PIPViewCorner, Offset> _offsets = {};
//screenspace
double width;
double height;
//values for refining fling engagement (see usages)
double baseAdditionOfFlingVelocity = 0.75;
double baseMultiplierOfFlingVelocity = 2;
//values for refining fling duration (see usages)
int minimumDuration = 730;
int maximumDuration = 1280;
double durationMultiplier = 1.10;
bool _isAnimating() { return _toggleFloatingAnimationController.isAnimating || _dragAnimationController.isAnimating; }
#override void initState() {
super.initState();
_corner = widget.initialCorner;
_toggleFloatingAnimationController = AnimationController(duration: defaultAnimationDuration, vsync: this,);
_dragAnimationController = AnimationController(duration: defaultAnimationDuration, vsync: this,);
}
void _updateCornersOffsets({ Size spaceSize, Size widgetSize, EdgeInsets windowPadding,}) {_offsets = _calculateOffsets(spaceSize: spaceSize, widgetSize: widgetSize, windowPadding: windowPadding,);}
_CornerDistance _calculateNearestCornerAndDistance({Offset offset, Map<PIPViewCorner, Offset> offsets,}) {
_CornerDistance calculateDistance(PIPViewCorner corner) {
final distance = offsets[corner].translate(-offset.dx, -offset.dy,).distanceSquared;
return _CornerDistance(corner: corner, distance: distance,);
}
final distances = PIPViewCorner.values.map(calculateDistance).toList();
distances.sort((cd0, cd1) => cd0.distance.compareTo(cd1.distance));
return _CornerDistance(corner:distances.first.corner,distance:distances.first.distance);
}
_CornerDistance _calculateNearestCornerAndDistanceWithFling({Offset offsetWithFling, Offset offsetWithoutFling, Map<PIPViewCorner, Offset> offsets,}) {
_CornerDistance calculateDistance(PIPViewCorner corner) {
final distance = offsets[corner].translate(-offsetWithFling.dx, -offsetWithFling.dy,).distanceSquared;
return _CornerDistance(corner: corner, distance: distance,);
}
final distances = PIPViewCorner.values.map(calculateDistance).toList();
distances.sort((cd0, cd1) => cd0.distance.compareTo(cd1.distance));
final actualDistance = offsets[distances.first.corner].translate(-offsetWithoutFling.dx, -offsetWithoutFling.dy).distanceSquared;
return _CornerDistance(corner:distances.first.corner, distance: actualDistance);
}
#override Widget build(BuildContext context) {
final mediaQuery = MediaQuery.of(context);
var windowPadding = mediaQuery.padding;
if (widget.avoidKeyboard) {windowPadding += mediaQuery.viewInsets;}
final isFloating = _bottomView != null;
return LayoutBuilder(
builder: (context, constraints) {
width = constraints.maxWidth;
height = constraints.maxHeight;
double floatingWidth = widget.floatingWidth;
double floatingHeight = widget.floatingHeight;
if (floatingWidth == null && floatingHeight != null) {floatingWidth = width / height * floatingHeight;}
floatingWidth ??= 100.0;
if (floatingHeight == null) {floatingHeight = height / width * floatingWidth;}
final floatingWidgetSize = Size(floatingWidth, floatingHeight);
final fullWidgetSize = Size(width, height);
_updateCornersOffsets(spaceSize: fullWidgetSize, widgetSize: floatingWidgetSize, windowPadding: windowPadding,);
final calculatedOffset = _offsets[_corner];
// BoxFit.cover
final widthRatio = floatingWidth / width;
final heightRatio = floatingHeight / height;
final scaledDownScale = widthRatio > heightRatio
? floatingWidgetSize.width / fullWidgetSize.width
: floatingWidgetSize.height / fullWidgetSize.height;
return Stack( children: <Widget>[
if (isFloating) Navigator(onGenerateRoute: (settings) {return MaterialPageRoute(builder: (_) {return _bottomView;});},),
AnimatedBuilder(
animation: Listenable.merge([_toggleFloatingAnimationController, _dragAnimationController,]),
builder: (context, child) {
final animationCurve = CurveTween(curve: Curves.fastLinearToSlowEaseIn,);
// final animationArc =
//region assign corresponding keys of controller and animation
final dragAnimationValue= animationCurve.transform(_dragAnimationController.value,);
final toggleFloatingAnimationValue = animationCurve.transform(_toggleFloatingAnimationController.value,);
//endregion
final floatingOffset = _isDragging ? _dragOffset : Tween<Offset>(begin: _dragOffset, end: calculatedOffset,)
.transform(_dragAnimationController.isAnimating ? dragAnimationValue : toggleFloatingAnimationValue);
final borderRadius = Tween<double>(begin: 0, end: 10,).transform(toggleFloatingAnimationValue);
final width = Tween<double>(begin: fullWidgetSize.width, end: floatingWidgetSize.width,).transform(toggleFloatingAnimationValue);
final height = Tween<double>(begin: fullWidgetSize.height, end: floatingWidgetSize.height,).transform(toggleFloatingAnimationValue);
final scale = Tween<double>(begin: 1, end: scaledDownScale,).transform(toggleFloatingAnimationValue);
return Positioned(
left: floatingOffset.dx,
top: floatingOffset.dy,
child: GestureDetector(
onPanStart: isFloating ? _onPanStart : null,
onPanUpdate: isFloating ? _onPanUpdate : null,
onPanCancel: isFloating ? _onPanCancel : null,
onPanEnd: isFloating ? _onPanEnd : null, //pass floatingWidgetSize
onTap: isFloating ? stopFloating : null,
child: Material(
elevation: 10,
borderRadius: BorderRadius.circular(borderRadius),
child: Container(
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(borderRadius),
),
width: width,
height: height,
child: Transform.scale(
scale: scale,
child: OverflowBox(
maxHeight: fullWidgetSize.height,
maxWidth: fullWidgetSize.width,
child: IgnorePointer(
ignoring: isFloating,
child: child,
),
),
),
),
),
),
);
},
child: Builder(builder: (context) => widget.builder(context, isFloating),),
),
],);
},
);
}
//region padEndLogs
void logFlingDetails(DragEndDetails details){
// details.velocity.pixelsPerSecond.translate(translateX, translateY)
log(
"fling details"
+"\n dx sqrt: " + sqrtNegSup(details.velocity.pixelsPerSecond.dx).toInt().toString()
+"\n dy sqrt: " + sqrtNegSup(details.velocity.pixelsPerSecond.dy).toInt().toString()
+"\n direction: " + details.velocity.pixelsPerSecond.direction.toString()
);
}
void logOffsetDetails(Offset _off, bool beforeApplyingFling){
log(
(beforeApplyingFling ? "offset details BEFORE applying fling params" : "offset details AFTER applying fling params")
+"\n dx: " + _off.dx.toInt().toString()
+"\n dy: " + _off.dy.toInt().toString()
+"\n direction: " + _off.direction.toString()
);
}
void logCornerCalculationDetails(_CornerDistance cornerDistanceDetails, bool withFling){
log(
(withFling?"cornerDistanceDetails AFTER fling":"cornerDistanceDetails BEFORE fling")
+"\n nearest corner: " + cornerDistanceDetails.corner.toString()
+"\n distance: " + cornerDistanceDetails.distance.toInt().toString()
);
}
//endregion
//region toggles & gestures
/*toggle pip on, arg is page over which it will be presented*/ void presentBelow(Widget widget) {
if (_isAnimating() || _bottomView != null) return;
dismissKeyboard(context);
setState(() {_bottomView = widget;});
_toggleFloatingAnimationController.forward();
}
/*toggle pip off, triggered on tap*/ void stopFloating() {
if (_isAnimating() || _bottomView == null) return;
dismissKeyboard(context);
_toggleFloatingAnimationController.reverse().whenCompleteOrCancel(() {if (mounted) {setState(() {_bottomView = null;});}});
}
void _onPanCancel() {
if (!_isDragging) return;
setState(() {
_dragAnimationController.value = 0;
_dragOffset = Offset.zero;
_isDragging = false;
});
}
void _onPanStart(DragStartDetails details) {
if (_isAnimating()) return;
setState(() {_dragOffset = _offsets[_corner]; _isDragging = true;});
}
void _onPanUpdate(DragUpdateDetails details) {
if (!_isDragging) return;
setState(() {_dragOffset = _dragOffset.translate(details.delta.dx, details.delta.dy,);});
}
void _onPanEnd(DragEndDetails details) {
if (!_isDragging) return;
//region fling values
double minimum = min(width, height);
double flingDx = sqrtNegSup(details.velocity.pixelsPerSecond.dx) * ((width / minimum) + baseAdditionOfFlingVelocity) * baseMultiplierOfFlingVelocity;
double flingDy = sqrtNegSup(details.velocity.pixelsPerSecond.dy) * ((height / minimum) + baseAdditionOfFlingVelocity) * baseMultiplierOfFlingVelocity;
//endregion
Offset offsetWithFling = _dragOffset.translate(flingDx, flingDy);
final nearestCornerToUse = _calculateNearestCornerAndDistanceWithFling(offsetWithFling: offsetWithFling,offsetWithoutFling: _dragOffset,offsets: _offsets );
setState(() { _corner = nearestCornerToUse.corner; _isDragging = false; });
//region duration values
int newDuration = (sqrt(nearestCornerToUse.distance)*durationMultiplier).toInt();
if(newDuration<minimumDuration) newDuration=minimumDuration;
if(newDuration>maximumDuration) newDuration=maximumDuration;
//endregion
log("new duration is: "+newDuration.toString());
_dragAnimationController.duration = Duration(milliseconds: newDuration.toInt());
Path path = Path();
path.moveTo(_dragOffset.dx, _dragOffset.dy);
path.quadraticBezierTo(flingDx, flingDy, _offsets[nearestCornerToUse.corner].dx, _offsets[nearestCornerToUse.corner].dy);
double jDx = calculate(path, _dragAnimationController).dx;
double jDy = calculate(path, _dragAnimationController).dy;
_dragAnimationController.forward().whenCompleteOrCancel(() { _dragAnimationController.value = 0; _dragOffset = Offset.zero; });
}
//endregion
Offset calculate(Path path, value) {
PathMetrics pathMetrics = path.computeMetrics();
PathMetric pathMetric = pathMetrics.elementAt(0);
value = pathMetric.length * value;
Tangent pos = pathMetric.getTangentForOffset(value);
return pos.position;
}
}```
[1]: https://pub.dev/packages/pip_view

Flutter Rotate CupertinoPicker

Is there any way to rotate the CupertinoPicker in Flutter by 90 degrees? So that you can pick horizontally and not vertically. Transform.rotate is not an option because then the width of the Picker is limited to the height of the parent widget. Or is there any good way to force the cupertino picker to be bigger than its parent widget?
How about the RotatedBox widget?
RotatedBox(
quarterTurns: 1,
child: CupertinoPicker(...),
)
Unlike Transform, which applies a transform just prior to painting, this object applies its rotation prior to layout, which means the entire rotated box consumes only as much space as required by the rotated child.
So I found 2 solutions. First you can use a RotatedBox. Thx to josxha for that idea. 2. solution: Make a complete custom picker. So if anyone has the same problem you can use my custom picker. The code is a total mess please dont judge lmao.
class CustomPicker extends StatefulWidget {
CustomPicker(
{#required double this.width,
#required double this.height,
#required double this.containerWidth,
#required double this.containerHeight,
#required double this.gapScaleFactor,
#required List<Widget> this.childrenW,
Function(int) this.onSnap});
double width;
double height;
double containerWidth;
double containerHeight;
double gapScaleFactor;
List<Widget> childrenW;
Function(int) onSnap;
_CustomPicker createState() => _CustomPicker(width, height, containerWidth,
containerHeight, gapScaleFactor, childrenW, onSnap);
}
class _CustomPicker extends State<CustomPicker>
with SingleTickerProviderStateMixin {
AnimationController controller;
double width;
double height;
double containerWidth;
double containerHeight;
double gapScaleFactor;
double currentScrollX = 0;
double oldAnimScrollX = 0;
double animDistance = 0;
int currentSnap = 0;
List<Widget> childrenW;
List<Positioned> scrollableContainer = [];
final Function(int) onSnap;
int currentPos;
_CustomPicker(
double this.width,
double this.height,
double this.containerWidth,
double this.containerHeight,
double this.gapScaleFactor,
List<Widget> this.childrenW,
Function(int) this.onSnap) {
initController();
init();
}
void initController() {
controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: 200),
lowerBound: 0,
upperBound: 1,
)..addListener(() {
setState(() {
currentScrollX = oldAnimScrollX + controller.value * animDistance;
init();
});
});
}
void init() {
scrollableContainer.clear();
if (currentScrollX < 0) {
currentScrollX = 0;
}
double scrollableLength =
(containerWidth + containerWidth * gapScaleFactor) *
(childrenW.length) -
containerWidth * gapScaleFactor;
if (currentScrollX > scrollableLength - containerWidth) {
currentScrollX = scrollableLength - containerWidth;
}
for (int i = 0; i < childrenW.length; i++) {
double leftPos = width / 2 -
containerWidth / 2 -
currentScrollX +
containerWidth * i +
containerWidth * gapScaleFactor * i;
double mid = width / 2 - containerWidth / 2;
double topPos = containerHeight *
0.9 *
((leftPos - mid).abs() / scrollableLength) /
2;
scrollableContainer.add(Positioned(
//calculate X position
left: leftPos,
top: topPos,
child: Container(
height: containerHeight -
containerHeight *
0.9 *
((leftPos - mid).abs() / scrollableLength),
width: containerWidth -
containerWidth *
0.9 *
((leftPos - mid).abs() / scrollableLength),
child: childrenW[i],
)));
}
}
void lookForSnappoint() {
double distance = 1000000;
double animVal = 0;
int index = -2032;
for (int i = 0; i < scrollableContainer.length; i++) {
double snappoint = width / 2 - containerWidth / 2;
double currentLeftPos = width / 2 -
containerWidth / 2 -
currentScrollX +
containerWidth * i +
containerWidth * gapScaleFactor * i;
if ((currentLeftPos - snappoint).abs() < distance) {
distance = (currentLeftPos - snappoint).abs();
animVal = currentLeftPos - snappoint;
index = i;
}
}
animDistance = animVal;
oldAnimScrollX = currentScrollX;
controller.reset();
controller.forward();
this.onSnap(index);
}
#override
Widget build(BuildContext context) {
return Container(
width: widget.width,
height: widget.height,
child: GestureDetector(
onPanUpdate: (DragUpdateDetails dragUpdateDetails) {
setState(() {
this.currentScrollX -= dragUpdateDetails.delta.dx;
init();
});
},
onPanEnd: (DragEndDetails dragEndDetails) {
lookForSnappoint();
},
behavior: HitTestBehavior.translucent,
child: LayoutBuilder(builder: (context, constraint) {
return Container(
child: Stack(
children: <Widget>[
Stack(children: scrollableContainer),
],
),
);
}),
),
);
}
}

How to animate a path in flutter?

I'd like to achieve the path animation effect as seen over here :
This animation (I couldn't include it because the gif is too big)
I only want to achieve the path on the map animation, I know I need to use a stacked, place my map, then use a Painter to paint such path, but how can I animate it ?
I know this question has an accepted answer, but I'd like to show an alternate solution to this problem.
First of all, creating a custom path from individual points is not optimal for the following:
calculating the length of each segment is not trivial
animating the steps evenly at small increments is difficult and resource-heavy
does not work with quadratic / bezier segments
Just like in the good old Android there is this path tracing method, so does a very similar PathMetrics exist in Flutter.
Building upon the accepted answer of this question, here is a much more generic way of animating any path.
So given a path and an animation percent, we need to extract a path from the start until that percent:
Path createAnimatedPath(
Path originalPath,
double animationPercent,
) {
// ComputeMetrics can only be iterated once!
final totalLength = originalPath
.computeMetrics()
.fold(0.0, (double prev, PathMetric metric) => prev + metric.length);
final currentLength = totalLength * animationPercent;
return extractPathUntilLength(originalPath, currentLength);
}
So now I only need to extract a path until a given length (not the percent). We need to combine all existing paths until a certain distance. Then add to this existing path some part of the last path segment.
Doing that is pretty straightforward.
Path extractPathUntilLength(
Path originalPath,
double length,
) {
var currentLength = 0.0;
final path = new Path();
var metricsIterator = originalPath.computeMetrics().iterator;
while (metricsIterator.moveNext()) {
var metric = metricsIterator.current;
var nextLength = currentLength + metric.length;
final isLastSegment = nextLength > length;
if (isLastSegment) {
final remainingLength = length - currentLength;
final pathSegment = metric.extractPath(0.0, remainingLength);
path.addPath(pathSegment, Offset.zero);
break;
} else {
// There might be a more efficient way of extracting an entire path
final pathSegment = metric.extractPath(0.0, metric.length);
path.addPath(pathSegment, Offset.zero);
}
currentLength = nextLength;
}
return path;
}
The rest of the code required to an entire example:
void main() => runApp(
new MaterialApp(
home: new AnimatedPathDemo(),
),
);
class AnimatedPathPainter extends CustomPainter {
final Animation<double> _animation;
AnimatedPathPainter(this._animation) : super(repaint: _animation);
Path _createAnyPath(Size size) {
return Path()
..moveTo(size.height / 4, size.height / 4)
..lineTo(size.height, size.width / 2)
..lineTo(size.height / 2, size.width)
..quadraticBezierTo(size.height / 2, 100, size.width, size.height);
}
#override
void paint(Canvas canvas, Size size) {
final animationPercent = this._animation.value;
print("Painting + ${animationPercent} - ${size}");
final path = createAnimatedPath(_createAnyPath(size), animationPercent);
final Paint paint = Paint();
paint.color = Colors.amberAccent;
paint.style = PaintingStyle.stroke;
paint.strokeWidth = 10.0;
canvas.drawPath(path, paint);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
class AnimatedPathDemo extends StatefulWidget {
#override
_AnimatedPathDemoState createState() => _AnimatedPathDemoState();
}
class _AnimatedPathDemoState extends State<AnimatedPathDemo>
with SingleTickerProviderStateMixin {
AnimationController _controller;
void _startAnimation() {
_controller.stop();
_controller.reset();
_controller.repeat(
period: Duration(seconds: 5),
);
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(title: const Text('Animated Paint')),
body: SizedBox(
height: 300,
width: 300,
child: new CustomPaint(
painter: new AnimatedPathPainter(_controller),
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _startAnimation,
child: new Icon(Icons.play_arrow),
),
);
}
#override
void initState() {
super.initState();
_controller = new AnimationController(
vsync: this,
);
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
}
I created a library for this: drawing_animation
You just have to provide the Path objects to the widget:
Resulting in this image output: imgur
import 'package:drawing_animation/drawing_animation.dart';
//...
List<Paths> dottedPathArray = ...;
bool run = true;
//...
AnimatedDrawing.paths(
this.dottedPathArray,
run: this.run,
animationOrder: PathOrders.original,
duration: new Duration(seconds: 2),
lineAnimation: LineAnimation.oneByOne,
animationCurve: Curves.linear,
onFinish: () => setState(() {
this.run = false;
}),
)),
You don't actually need a Stack; you could use a foregroundPainter over the map image. To animate a CustomPainter pass the AnimationController into its constructor and also to the super constructor. In paint use the value of the animation to decide how much of the path the draw. For example, if value is 0.25, draw just the first 25% of the path.
class AnimatedPainter extends CustomPainter {
final Animation<double> _animation;
AnimatedPainter(this._animation) : super(repaint: _animation);
#override
void paint(Canvas canvas, Size size) {
// _animation.value has a value between 0.0 and 1.0
// use this to draw the first X% of the path
}
#override
bool shouldRepaint(AnimatedPainter oldDelegate) {
return true;
}
}
class PainterDemo extends StatefulWidget {
#override
PainterDemoState createState() => new PainterDemoState();
}
class PainterDemoState extends State<PainterDemo>
with SingleTickerProviderStateMixin {
AnimationController _controller;
#override
void initState() {
super.initState();
_controller = new AnimationController(
vsync: this,
);
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
void _startAnimation() {
_controller.stop();
_controller.reset();
_controller.repeat(
period: Duration(seconds: 5),
);
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(title: const Text('Animated Paint')),
body: new CustomPaint(
foregroundPainter: new AnimatedPainter(_controller),
child: new SizedBox(
// doesn't have to be a SizedBox - could be the Map image
width: 200.0,
height: 200.0,
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _startAnimation,
child: new Icon(Icons.play_arrow),
),
);
}
}
void main() {
runApp(
new MaterialApp(
home: new PainterDemo(),
),
);
}
Presumably you will have a list of coordinates that define the path. Assuming some list of points you'd draw the complete path with something like:
if (points.isEmpty) return;
Path path = Path();
Offset origin = points[0];
path.moveTo(origin.dx, origin.dy);
for (Offset o in points) {
path.lineTo(o.dx, o.dy);
}
canvas.drawPath(
path,
Paint()
..color = Colors.orange
..style = PaintingStyle.stroke
..strokeWidth = 4.0,
);
When value is less than 1.0 you need to devise a way to draw less than 100% of the path. For example, when value is 0.25, you might only add the first quarter of the points to the path. If your path consisted of relatively few points, you'd probably get the smoothest animation if you calculated the total length of the path and drew just the first segments of the path that added up to a quarter of the total length.