Last active
January 25, 2021 08:40
-
-
Save Zfinix/5345cb16c952a7b109ce59084cb4c14c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import 'dart:math' as math; | |
| import 'dart:ui' as ui; | |
| import 'package:flutter/gestures.dart'; | |
| import 'package:flutter/material.dart'; | |
| import 'package:flutter/rendering.dart'; | |
| void main() => runApp( | |
| MaterialApp( | |
| theme: ThemeData.dark(), | |
| debugShowCheckedModeBanner: false, | |
| home: Playground(), | |
| ), | |
| ); | |
| class Playground extends StatefulWidget { | |
| @override | |
| _PlaygroundState createState() => _PlaygroundState(); | |
| } | |
| class _PlaygroundState extends State<Playground> with TickerProviderStateMixin { | |
| @override | |
| Widget build(BuildContext context) { | |
| return Scaffold( | |
| backgroundColor: Color(0xFF070813), | |
| body: Center( | |
| child: SlideButton( | |
| vsync: this, | |
| child: Text("SLIDE TO SEND"), | |
| onSlide: () { | |
| print("onChanged"); | |
| }, | |
| onTap: () { | |
| print("onTap"); | |
| }, | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| class SlideButton extends SingleChildRenderObjectWidget { | |
| SlideButton({ | |
| Key key, | |
| this.child, | |
| this.onTap, | |
| this.onSlide, | |
| @required this.vsync, | |
| }) : super(key: key, child: child); | |
| final Widget child; | |
| final VoidCallback onTap; | |
| final VoidCallback onSlide; | |
| final TickerProvider vsync; | |
| @override | |
| RenderSlideButton createRenderObject(BuildContext context) { | |
| return RenderSlideButton(onTap: onTap, onSlide: onSlide, vsync: vsync); | |
| } | |
| @override | |
| void updateRenderObject( | |
| BuildContext context, covariant RenderSlideButton renderObject) { | |
| renderObject | |
| ..onTap = onTap | |
| ..onSlide = onSlide | |
| ..vsync = vsync; | |
| } | |
| } | |
| class RenderSlideButton extends RenderProxyBox { | |
| RenderSlideButton({ | |
| RenderBox child, | |
| VoidCallback onTap, | |
| VoidCallback onSlide, | |
| TickerProvider vsync, | |
| }) : _onTap = onTap, | |
| _onSlide = onSlide, | |
| _vsync = vsync, | |
| super(child) { | |
| final physics = BouncingScrollPhysics(); | |
| drag = HorizontalDragGestureRecognizer() | |
| ..minFlingVelocity = physics.minFlingVelocity | |
| ..maxFlingVelocity = physics.maxFlingVelocity | |
| ..minFlingDistance = physics.dragStartDistanceMotionThreshold | |
| ..onStart = _onDragStart | |
| ..onUpdate = _onDragUpdate | |
| ..onCancel = _onDragCancel | |
| ..onEnd = _onDragEnd; | |
| } | |
| DragGestureRecognizer drag; | |
| AnimationController slideController; | |
| TickerProvider _vsync; | |
| set vsync(TickerProvider vsync) { | |
| assert(vsync != null); | |
| if (vsync == _vsync) { | |
| return; | |
| } | |
| _vsync = vsync; | |
| slideController.resync(_vsync); | |
| } | |
| VoidCallback _onTap; | |
| set onTap(VoidCallback onTap) { | |
| if (_onTap == onTap) { | |
| return; | |
| } | |
| _onTap = onTap; | |
| } | |
| VoidCallback _onSlide; | |
| set onSlide(VoidCallback onSlide) { | |
| if (_onSlide == onSlide) { | |
| return; | |
| } | |
| _onSlide = onSlide; | |
| } | |
| @override | |
| void attach(PipelineOwner owner) { | |
| super.attach(owner); | |
| slideController = AnimationController.unbounded( | |
| value: 0.0, | |
| vsync: _vsync, | |
| duration: Duration(milliseconds: 350), | |
| )..addListener(markNeedsPaint); | |
| } | |
| @override | |
| void detach() { | |
| slideController.removeListener(markNeedsPaint); | |
| super.detach(); | |
| } | |
| void _onDragStart(DragStartDetails details) { | |
| slideController.animateTo(size.width * .05).whenCompleteOrCancel(() { | |
| WidgetsBinding.instance.addPostFrameCallback((timeStamp) { | |
| _onTap?.call(); | |
| }); | |
| }); | |
| } | |
| void _onDragUpdate(DragUpdateDetails details) { | |
| slideController.value += details.primaryDelta; | |
| } | |
| void _onDragCancel() { | |
| slideController.value = 0.0; | |
| } | |
| void _onDragEnd(DragEndDetails details) { | |
| final threshold = size.width / 4; | |
| if (slideController.value > threshold) { | |
| slideController.animateTo(size.width).whenCompleteOrCancel(() { | |
| _onDragCancel(); | |
| WidgetsBinding.instance.addPostFrameCallback((timeStamp) { | |
| _onSlide?.call(); | |
| }); | |
| }); | |
| return; | |
| } | |
| slideController.animateBack(0.0).whenCompleteOrCancel(_onDragCancel); | |
| } | |
| @override | |
| bool hitTestSelf(ui.Offset position) => true; | |
| @override | |
| bool get isRepaintBoundary => true; | |
| @override | |
| void handleEvent(PointerEvent event, covariant BoxHitTestEntry entry) { | |
| if (event is PointerDownEvent) { | |
| drag.addPointer(event); | |
| } | |
| } | |
| @override | |
| void performLayout() { | |
| final effectiveConstraints = constraints.enforce(BoxConstraints( | |
| minHeight: 40, | |
| maxHeight: 80, | |
| maxWidth: 400, | |
| )); | |
| size = effectiveConstraints.biggest; | |
| if (child != null) { | |
| child.layout(constraints, parentUsesSize: true); | |
| } else {} | |
| } | |
| @override | |
| void paint(PaintingContext context, Offset offset) { | |
| final canvas = context.canvas; | |
| final value = math.max(0.0, slideController.value); | |
| final t = interpolate(inputMax: size.width)(value); | |
| final bounds = RRect.fromRectAndRadius(offset & size, Radius.circular(18)); | |
| canvas.clipRRect(bounds); | |
| canvas.drawRRect( | |
| bounds, | |
| Paint() | |
| ..color = Color.lerp(Color(0xFF141224), Color(0xFF5A01CB), | |
| Curves.decelerate.transform(t)), | |
| ); | |
| if (child != null) { | |
| context.paintChild( | |
| child, | |
| size.center(Offset(offset.dx - 80, offset.dy - 10)).translate(value, 0), | |
| ); | |
| } | |
| } | |
| } | |
| // https://stackoverflow.com/a/55088673/8236404 | |
| double Function(double input) interpolate({ | |
| double inputMin = 0, | |
| double inputMax = 1, | |
| double outputMin = 0, | |
| double outputMax = 1, | |
| }) { | |
| //range check | |
| if (inputMin == inputMax) { | |
| print("Warning: Zero input range"); | |
| return null; | |
| } | |
| if (outputMin == outputMax) { | |
| print("Warning: Zero output range"); | |
| return null; | |
| } | |
| //check reversed input range | |
| var reverseInput = false; | |
| final oldMin = math.min(inputMin, inputMax); | |
| final oldMax = math.max(inputMin, inputMax); | |
| if (oldMin != inputMin) { | |
| reverseInput = true; | |
| } | |
| //check reversed output range | |
| var reverseOutput = false; | |
| final newMin = math.min(outputMin, outputMax); | |
| final newMax = math.max(outputMin, outputMax); | |
| if (newMin != outputMin) { | |
| reverseOutput = true; | |
| } | |
| // Hot-rod the most common case. | |
| if (!reverseInput && !reverseOutput) { | |
| final dNew = newMax - newMin; | |
| final dOld = oldMax - oldMin; | |
| return (double x) { | |
| return ((x - oldMin) * dNew / dOld) + newMin; | |
| }; | |
| } | |
| return (double x) { | |
| double portion; | |
| if (reverseInput) { | |
| portion = (oldMax - x) * (newMax - newMin) / (oldMax - oldMin); | |
| } else { | |
| portion = (x - oldMin) * (newMax - newMin) / (oldMax - oldMin); | |
| } | |
| double result; | |
| if (reverseOutput) { | |
| result = newMax - portion; | |
| } else { | |
| result = portion + newMin; | |
| } | |
| return result; | |
| }; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment