Prevent Small Hops in Drag Upon Touch Up/End in Flutter - 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();
},
),
],
);
}
}

Related

Randomly place animated gifs inside of container in 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 :)

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

How to properly use curve's value to animate a widget?

Minimal reproducible code:
class _MyPageState extends State<MyPage> {
double _dx1 = 0;
double _dx2 = 0;
final Duration _duration = Duration(seconds: 1);
final Curve _curve = Curves.linear;
void _play() {
final width = MediaQuery.of(context).size.width;
_dx1 = width;
var i = 0;
Timer.periodic(Duration(milliseconds: 1), (timer) {
if (i > 1000) {
timer.cancel();
} else {
setState(() {
_dx2 = _curve.transform(i / 1000) * width;
i++;
});
}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: _play,
child: Icon(Icons.play_arrow),
),
body: SizedBox.expand(
child: Stack(
children: [
AnimatedPositioned(
left: _dx1,
duration: _duration,
curve: _curve,
child: _box,
),
Positioned(
left: _dx2,
top: 60,
child: _box,
),
],
),
),
);
}
Container get _box => Container(width: 50, height: 50, color: Colors.red);
}
Output:
As you can see my second custom animated box doesn't catch up with the first default AnimatedPositioned widget.
Note: This can easily be done using Tween and AnimationController but I just want to know why I'm unable to correctly use Curve's value to match the default behavior.
Assumption: The callback is periodically called for every millisecond.
Expected Result: After 1 second, i = 1000;
The assumption is wrong. Add the below code to verify:
void _play() {
...
var i = 0;
final start = DateTime.now().millisecondsSinceEpoch;
Timer.periodic(Duration(milliseconds: 1), (timer) {
final elapse = DateTime.now().millisecondsSinceEpoch - start;
print('elapse = $elapse');
print('i = $i');
print('ticks = ${timer.tick}');
print('######################');
...
});
}
On my pc the last value is:
elapse = 14670
i = 1001
ticks = 14670
######################
So this implies that it took 14 seconds on my PC for 1001 callbacks. That's not what we were expecting. What we can infer from this is that some callbacks are missed and i does not reflect the time elapsed.
However, the value we need is timer.tick. Quoting the docs
If a periodic timer with a non-zero duration is delayed too much, so more than one tick should have happened, all but the last tick in the past are considered "missed", and no callback is invoked for them. The tick count reflects the number of durations that have passed and not the number of callback invocations that have happened.
So, tick tells us the number of periods that have passed. The below code will catch up with the AnimatedPositioned
void _play() {
final width = MediaQuery.of(context).size.width;
_dx1 = width;
final period = Duration(milliseconds: 1);
Timer.periodic(period, (timer) {
final elapsed = timer.tick * period.inMilliseconds;
if (elapsed > _duration.inMilliseconds) {
timer.cancel();
} else {
setState(() {
_dx2 = _curve.transform(elapsed / _duration.inMilliseconds) * width;
});
}
});
}
Now, you might see some stutter where our box might be a bit ahead or behind the AnimatedPositioned, this is because we use Timer but the AnimatedPositioned uses Ticker. The difference is Timer.periodic is driven by the Duration we passed as period, but Ticker is driven by SchedulerBinding.scheduleFrameCallback. So, the instant the value _dx2 is updated and the instant the frame is rendered on the screen might not be the same. Add the fact that some callbacks are missed!

Left and right to cancel or confirm slider flutter

I need to use a slider which slides both to the left to cancel and right to confirm
this is the desired slider
I couldn't find a way to do it, is there any way to do it ?
You can achieve it by using a Slider and customizing it.
...
double _currentSliderValue = 5;
Slider customSlider() {
return Slider(
value: _currentSliderValue,
min: 0,
max: 10,
divisions: 10,
onChanged: (double value) {
setState(() {
_currentSliderValue = value;
});
if (_currentSliderValue == 0) // Decline
else if (_currentSliderValue == 10) // Accept
else // Nothing
},
);
}
The UI can be achieved by including the customSlider() as a child of a Row widget as follows (didn't try it but it should be the right path):
Row declineOrAcceptSlider() {
return Row(children: [
Text("Decline"),
customSlider(),
Text("Accept")
], mainAxisAlignment: MainAxisAlignment.spacedEvenly);
}
Use Gesture Detector this
Example :
#override
Widget build(BuildContext context) {
String swipeDirection;
return GestureDetector(
onPanUpdate: (details) {
swipeDirection = details.delta.dx < 0 ? 'left' : 'right';
},
onPanEnd: (details) {
if (swipeDirection == 'left') {
//handle swipe left event
}
if (swipeDirection == 'right') {
//handle swipe right event
}
},
child: //child widget
);
}

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),
],
),
);
}),
),
);
}
}