The Flutter implements arbitrary TAB switching effects

Handles and responds to touch effects

We can do this with a GestureDetector

GestureDetector(/// gesture touch move start, here we can record the start of the touch point, used to judge the move ratio and the animation's initial point onHorizontalDragStart: OnStart, /// This generates a TAB toggle effect, which can be customized by the user, and the effect code is in the Delegate class. OnUpdate, /// the current TAB onHorizontalDragEnd: onEnd, child: child,);Copy the code

Touch the start

OnStart (DragStartDetails details) {dragStart = details.globalPosition; . }Copy the code

Touch moving

onUpdate(DragUpdateDetails details) { if (dragStart ! = null) {/// SlideDirection SlideDirection; [0, 1] double slidePercent = 0.0; /// Final newPosition = details. GlobalPosition; Final dx = newposition.dx - dragstart. dx; final dx = newposition.dx - dragstart. dx; final dx = newposition.dx - dragstart. dx; SlidePercent = (dx/FULL_TRANSITION_PX).abs().clamp(0.0, 1.0).todouble (); if (dx > 0) { slideDirection = SlideDirection.leftToRight; } else if (dx < 0) { slideDirection = SlideDirection.rightToLeft; } else { slideDirection = SlideDirection.none; slidePercent = 0; }... }}Copy the code

Animation processing

After the touch gesture is over, we start animation processing. The animation is divided into two parts, one is the animation that slides successfully to switch to the next TAB, and the other is the animation that slides failed (for example, the sliding distance is very small and there is no need to jump to the next page). Here, the value is the sliding ratio of touch gesture and the value of Animation. The two values are the same, so that there can be a coherent Animation effect.

onAnimatedStart({SlideUpdate slideUpdate}) { Duration duration; _isSlideSuccess = value >= slideSuccessProportion; If (_isSlideSuccess) {final slideRemaining = 1.0-value; // Success if (_isSlideSuccess) {final slideRemaining = 1.0-value; Duration = duration (milliseconds: (slideRemaining/PERCENT_PER_MILLISECOND).round()); _animationController.duration = duration; Run forward to 1 / / / animation, animation after switching the current TAB to the next page TAB _animationController. Forward (the from: value). WhenComplete (() = > animationCompleted ()); $milliseconds = duration (milliseconds: (value/PERCENT_PER_MILLISECOND).round()); _animationController.duration = duration; / / / will be animated value back to 0. _animationController. Reverse (from: value); }}Copy the code

Effect customization

Here we use the AnyTabDelegate abstract class, which we can inherit to implement any effect. The biggest benefit of this is the separation of UI and logical processing.

Abstract class AnyTabDelegate {/// TAB List<Widget> tabs; AnyTabDelegate({@required this.tabs}); int get length => tabs.length; Build Widget build(BuildContext context, /// current TAB page int activeIndex, /// next page int nextPageIndex, /// the value of the animation is the value of the gesture touch and the value of the animation. Animation Animation, /// the initial point of touch, Offset startingOffset,); }Copy the code

CircularAnyTabDelegate is implemented in CircularAnyTabDelegate. ClipOval is used to crop the TAB to be displayed on the next page. If percentage is passed in 0, the TAB will not be displayed at all.

class CircularAnyTabDelegate extends AnyTabDelegate {
  CircularAnyTabDelegate({@required List<Widget> tabs})
      : assert(tabs != null && tabs.length > 0),
        super(tabs: tabs);

  @override
  Widget build(BuildContext context, int activeIndex, int nextPageIndex,
      Animation animation, Offset startingOffset) {
    return Stack(
      children: [
        tabs[activeIndex],
        ClipOval(
          clipper: CircularClipper(
            percentage: animation.value,
            offset: startingOffset,
          ),
          child: tabs[nextPageIndex],
        )
      ],
    );
  }
}
Copy the code

Scroll down to the code for CircularClipper.

Class CircularClipper extends CustomClipper<Rect> {/// / percentage, 0-> 1,1 => all shows final double percentage; /// final Offset Offset; const CircularClipper({this.percentage = 0, this.offset = Offset.zero}); @override Rect getClip(Size Size) {double maxValue = maxLength(Size, Size); offset) * percentage; return Rect.fromLTRB(-maxValue + offset.dx, -maxValue + offset.dy, maxValue + offset.dx, maxValue + offset.dy); } @override bool shouldReclip(CircularClipper oldClipper) { return percentage ! = oldClipper.percentage || offset ! = oldClipper.offset; } / / / | 1 | 2 / / / / / / -- -- -- -- -- -- -- -- -- April / / / / / / 3 | | / / / rectangular interior point to the edge of the maximum distance calculation, here we put the rectangle is divided into four pieces, Double maxLength(Size Size, Offset Offset) {double centerX = size.width / 2; double centerX = size.width / 2; double centerY = size.height / 2; if (offset.dx < centerX && offset.dy < centerY) { ///1 return getEdge(size.width - offset.dx, size.height - offset.dy); } else if (offset.dx > centerX && offset.dy < centerY) { ///2 return getEdge(offset.dx, size.height - offset.dy); } else if (offset.dx < centerX && offset.dy > centerY) { ///3 return getEdge(size.width - offset.dx, offset.dy); } else { ///4 return getEdge(offset.dx, offset.dy); } } double getEdge(double width, double height) { return sqrt(pow(width, 2) + pow(height, 2)); }}Copy the code

The results are as follows:

The code address

The Demo address

The logical handling of touch here refers to Ali’s implementation of the Flutter – Go TAB.