Flutter学习笔记

Flutter一个仿“探探”的左右滑动选择喜欢/不喜欢的控件

2020-04-27  本文已影响0人  王俏

一个仿“探探”的左右滑动选择喜欢/不喜欢的控件

教程:用Flutter实现一个仿“探探”的左右滑动选择喜欢/不喜欢的效果

代码实现SlideContainer和SlideStack

import 'package:flutter/gestures.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';
import 'package:async/async.dart'
/// Container that can be slid.
///
/// Will automatically finish the slide animation when the drag gesture ends.
class SlideContainer extends StatefulWidget {
  final Widget child;
  final double slideDistance;

  final double rotateRate;

  final Duration reShowDuration;

  final double minAutoSlideDragVelocity;

  final VoidCallback onSlideStarted;

  final VoidCallback onSlideCompleted;

  final VoidCallback onSlideCanceled;

  final SlideChanged<double, SlideDirection> onSlide;

  SlideContainer({
    Key key,
    @required this.child,
    @required this.slideDistance,
    this.rotateRate = 0.25,
    this.minAutoSlideDragVelocity = 600.0,
    this.reShowDuration,
    this.onSlideStarted,
    this.onSlideCompleted,
    this.onSlideCanceled,
    this.onSlide,
  })  : assert(child != null),
        assert(rotateRate != null),
        assert(minAutoSlideDragVelocity != null),
        assert(reShowDuration != null),
        super(key: key);

  @override
  ContainerState createState() => ContainerState();
}

class ContainerState extends State<SlideContainer>
    with TickerProviderStateMixin {
  final Map<Type, GestureRecognizerFactory> gestures =
      <Type, GestureRecognizerFactory>{};

  RestartableTimer timer;

  // User's finger move value.
  double dragValue = 0.0;

  // How long should the container move.
  double dragTarget = 0.0;
  bool isFirstDragFrame;
  AnimationController animationController;
  Ticker fingerTicker;

  double get maxDragDistance => widget.slideDistance;

  double get minAutoSlideDistance => maxDragDistance * 0.5;

  // The translation offset of the container.(decides the position of the container)
  double get containerOffset =>
      animationController.value *
      maxDragDistance *
      (1.0 + widget.rotateRate) *
      dragTarget.sign;

  set containerOffset(double value) {
    containerOffset = value;
  }

  @override
  void initState() {
    animationController =
        AnimationController(vsync: this, duration: widget.reShowDuration)
          ..addListener(() {
            if (widget.onSlide != null)
              widget.onSlide(animationController.value, slideDirection);
            setState(() {});
          });

    fingerTicker = createTicker((_) {
      if ((dragValue - dragTarget).abs() <= 1.0) {
        dragTarget = dragValue;
      } else {
        dragTarget += (dragValue - dragTarget);
      }
      animationController.value = dragTarget.abs() / maxDragDistance;
    });

    _registerGestureRecognizer();

    super.initState();
  }

  @override
  void dispose() {
    animationController?.dispose();
    fingerTicker?.dispose();
    timer?.cancel();
    super.dispose();
  }

  GestureRecognizerFactoryWithHandlers<T>
      createGestureRecognizer<T extends DragGestureRecognizer>(
              GestureRecognizerFactoryConstructor<T> constructor) =>
          GestureRecognizerFactoryWithHandlers<T>(
            constructor,
            (T instance) {
              instance
                ..onStart = handleDragStart
                ..onUpdate = handleDragUpdate
                ..onEnd = handleDragEnd;
            },
          );

  void _registerGestureRecognizer() {
    gestures[HorizontalDragGestureRecognizer] =
        createGestureRecognizer<HorizontalDragGestureRecognizer>(
            () => HorizontalDragGestureRecognizer());
  }

  double getVelocity(DragEndDetails details) =>
      details.velocity.pixelsPerSecond.dx;

  double getDelta(DragUpdateDetails details) => details.delta.dx;

  void reShow() {
    setState(() {
      animationController.value = 0.0;
    });
  }

  void _startTimer() {
    if (timer == null) {
      timer = RestartableTimer(widget.reShowDuration, reShow);
    } else {
      timer.reset();
    }
  }

  void _completeSlide() => animationController.forward().then((_) {
        if (widget.onSlideCompleted != null) widget.onSlideCompleted();
        _startTimer();
      });

  void _cancelSlide() => animationController.reverse().then((_) {
        if (widget.onSlideCanceled != null) widget.onSlideCanceled();
      });

  void handleDragStart(DragStartDetails details) {
    isFirstDragFrame = true;
    dragValue = animationController.value * maxDragDistance * dragTarget.sign;
    dragTarget = dragValue;
    fingerTicker.start();
    if (widget.onSlideStarted != null) widget.onSlideStarted();
  }

  void handleDragUpdate(DragUpdateDetails details) {
    if (isFirstDragFrame) {
      isFirstDragFrame = false;
      return;
    }
    dragValue = (dragValue + getDelta(details))
        .clamp(-maxDragDistance, maxDragDistance);

    if (slideDirection == SlideDirection.left) {
      dragValue = dragValue.clamp(-maxDragDistance, 0.0);
    } else if (slideDirection == SlideDirection.right) {
      dragValue = dragValue.clamp(0.0, maxDragDistance);
    }
  }

  void handleDragEnd(DragEndDetails details) {
    if (getVelocity(details) * dragTarget.sign >
        widget.minAutoSlideDragVelocity) {
      _completeSlide();
    } else if (getVelocity(details) * dragTarget.sign <
        -widget.minAutoSlideDragVelocity) {
      _cancelSlide();
    } else {
      dragTarget.abs() > minAutoSlideDistance
          ? _completeSlide()
          : _cancelSlide();
    }
    fingerTicker.stop();
  }

  SlideDirection get slideDirection =>
      dragValue.isNegative ? SlideDirection.left : SlideDirection.right;

  double get rotation => animationController.value * widget.rotateRate;

  Matrix4 get transformMatrix => slideDirection == SlideDirection.left
      ? (Matrix4.rotationZ(rotation)..invertRotation())
      : (Matrix4.rotationZ(rotation));

  Widget _getContainer() {
    return Transform(
      child: Card(
        child: widget.child,
      ),
      transform: transformMatrix,
      alignment: FractionalOffset.center,
    );
  }

  @override
  Widget build(BuildContext context) => RawGestureDetector(
        gestures: gestures,
        child: Transform.translate(
          offset: Offset(
            containerOffset,
            0.0,
          ),
          child: _getContainer(),
        ),
      );
}



enum SlideDirection {
  left,
  right,
}

typedef SlideChanged<double, SlideDirection> = void Function(
    double value, SlideDirection value2);

class SlideStack extends StatefulWidget {
  /// The main widget.
  final Widget child;

  /// The widget hidden below.
  final Widget below;

  final double slideDistance;

  final double rotateRate;
  final double scaleRate;

  final Duration scaleDuration;

  /// If the drag gesture is fast enough, it will auto complete the slide.
  final double minAutoSlideDragVelocity;

  /// Called when the drawer starts to open.
  final VoidCallback onSlideStarted;

  /// Called when the drawer is full opened.
  final VoidCallback onSlideCompleted;

  /// Called when the drag gesture is canceled (the container goes back to the starting position).
  final VoidCallback onSlideCanceled;

  final VoidCallback refreshBelow;

  /// Called each time when the slide gesture is active.
  ///
  /// returns the position of the drawer between 0.0 and 1.0 (depends on the progress of animation).
  ///

  final SlideChanged<double, SlideDirection> onSlide;

  const SlideStack({
    Key key,
    @required this.child,
    @required this.below,
    @required this.slideDistance,
    this.rotateRate = 0.25,
    this.scaleRate = 1.08,
    this.scaleDuration = const Duration(milliseconds: 250),
    this.minAutoSlideDragVelocity = 600.0,
    this.onSlideStarted,
    this.onSlideCompleted,
    this.onSlideCanceled,
    this.onSlide,
    this.refreshBelow,
  })  : assert(child != null),
        assert(minAutoSlideDragVelocity != null),
        assert(scaleDuration != null),
        super(key: key);

  @override
  State<StatefulWidget> createState() => _StackState();
}

class _StackState extends State<SlideStack> with TickerProviderStateMixin {
  double position = 0.0;
  double elevation = 0.0;
  AnimationController controller;
  Animation<double> animation;

  @override
  void initState() {
    super.initState();
    controller = new AnimationController(
      vsync: this,
      duration: widget.scaleDuration,
    )
      ..addListener(() {
        setState(() {});
      })
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          controller
              .animateTo(0.0, duration: widget.scaleDuration)
              .whenCompleteOrCancel(() {
            elevation = 0.0;
            if (widget.refreshBelow != null) widget.refreshBelow();
            setState(() {});
          });
        }
      });
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  void onSlide(value, direction) {
    if (widget.onSlide != null) widget.onSlide(value, direction);
    controller.value = value;
    setState(() {});
  }

  void onSlideStarted() {
    if (widget.onSlideStarted != null) widget.onSlideStarted();
    elevation = 1.0;
    setState(() {});
  }

  void onSlideCanceled() {
    if (widget.onSlideCanceled != null) widget.onSlideCanceled();
    elevation = 0.0;
    setState(() {});
  }

  double get scale => 1 + controller.value * (widget.scaleRate - 1.0);

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        Positioned.fill(
            child: Transform.scale(
          scale: scale,
          child: Card(
            elevation: elevation,
            child: widget.below,
          ),
        )),
        Positioned.fill(
            child: SlideContainer(
          child: widget.child,
          slideDistance: widget.slideDistance,
          rotateRate: widget.rotateRate,
          minAutoSlideDragVelocity: widget.minAutoSlideDragVelocity,
          reShowDuration: widget.scaleDuration,
          onSlideStarted: onSlideStarted,
          onSlideCompleted: widget.onSlideCompleted,
          onSlideCanceled: onSlideCanceled,
          onSlide: onSlide,
        )),
      ],
    );
  }
}

使用

import 'package:flutter/material.dart';
import 'package:flutter_ui/draglike/drag_like_stack.dart';
import 'package:oktoast/oktoast.dart';

class Girl {
  final String description;
  final String asset;

  Girl(this.description, this.asset);
}

final List<Girl> girls = [
  Girl('Sliding to the left means dislike', 'images/girl01.png'),
  Girl('slipping to the right means expressing love', 'images/girl02.png'),
  Girl('Hope you like', 'images/girl03.png')
];

class DragLikePage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => DragLikeState();
}

class DragLikeState extends State<DragLikePage> with TickerProviderStateMixin {
  AnimationController controller;

  int aboveIndex = 0;
  int belowIndex = 1;

  final double bottomHeight = 100.0;
  final double defaultIconSize = 30.0;
  final Color defaultIconColor =
      Color.lerp(Color(0xFFFF80AB), Color(0xFFC51162), 0.0);

  double position = 0.0;
  SlideDirection slideDirection;

  double get leftIconSize => slideDirection == SlideDirection.left
      ? defaultIconSize * (1 + position * 0.8)
      : defaultIconSize;

  double get rightIconSize => slideDirection == SlideDirection.right
      ? defaultIconSize * (1 + position * 0.8)
      : defaultIconSize;

  Color get leftIconColor => slideDirection == SlideDirection.left
      ? Color.lerp(Color(0xFFFF80AB), Color(0xFFC51162), position)
      : defaultIconColor;

  Color get rightIconColor => slideDirection == SlideDirection.right
      ? Color.lerp(Color(0xFFFF80AB), Color(0xFFC51162), position)
      : defaultIconColor;

  void setAboveIndex() {
    if (aboveIndex < girls.length - 1) {
      aboveIndex++;
    } else {
      aboveIndex = 0;
    }
  }

  void setBelowIndex() {
    if (belowIndex < girls.length - 1) {
      belowIndex++;
    } else {
      belowIndex = 0;
    }
  }

  void onSlide(double position, SlideDirection direction) {
    setState(() {
      this.position = position;
      this.slideDirection = direction;
    });
  }

  void onSlideCompleted() {
    controller.forward();
    String isLike =
        (slideDirection == SlideDirection.left) ? 'dislike' : 'like';
    showToast('You $isLike this !',duration: const Duration(milliseconds: 1500));
    setAboveIndex();
  }

  @override
  void initState() {
    super.initState();
    controller = new AnimationController(
      vsync: this,
      duration: Duration(milliseconds: 250),
    )
      ..addListener(() {
        setState(() {
          if (position != 0) {
            position = 1 - controller.value;
          }
        });
      })
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          controller.reset();
        }
      });
  }

  @override
  void dispose() {
    super.dispose();
    controller.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Drag to choose like or dislike'),
      ),
      body: Container(
        child: Column(
          children: <Widget>[
            Expanded(
              child: Container(
                padding: EdgeInsets.all(20.0),
                child: _buildCard(),
              ),
            ),
            _buildBottom(),
          ],
        ),
      ),
    );
  }

  Widget _buildCard() {
    return Stack(
      children: <Widget>[
        _buildBackground(),
        Positioned(
          child: SlideStack(
            child: _buildChooseView(girls[aboveIndex]),
            below: _buildChooseView(girls[belowIndex]),
            slideDistance: MediaQuery.of(context).size.width - 40.0,
            onSlide: onSlide,
            onSlideCompleted: onSlideCompleted,
            refreshBelow: setBelowIndex,
            rotateRate: 0.4,
          ),
          left: 10.0,
          top: 20.0,
          bottom: 40.0,
          right: 10.0,
        ),
      ],
    );
  }

  Widget _buildChooseView(Girl girl) {
    return Stack(
      children: <Widget>[
        Positioned(
          child: Image.asset(
            girl.asset,
            fit: BoxFit.cover,
          ),
          left: 35.0,
          right: 35.0,
          top: 20.0,
          bottom: 50.0,
        ),
        Positioned(
          child: Text(
            girl.description,
            maxLines: 1,
            overflow: TextOverflow.ellipsis,
            textAlign: TextAlign.center,
          ),
          left: 10.0,
          right: 10.0,
          bottom: 10.0,
        ),
      ],
    );
  }

  Stack _buildBackground() {
    return Stack(
      children: <Widget>[
        Positioned(
          child: Card(
            child: Text('test'),
          ),
          left: 40.0,
          top: 40.0,
          bottom: 10.0,
          right: 40.0,
        ),
        Positioned(
          child: Card(
            child: Text('test'),
          ),
          left: 30.0,
          top: 30.0,
          bottom: 20.0,
          right: 30.0,
        ),
        Positioned(
          child: Card(
            child: Text('test'),
          ),
          left: 20.0,
          top: 30.0,
          bottom: 30.0,
          right: 20.0,
        ),
      ],
    );
  }

  Widget _buildBottom() {
    return SizedBox(
      height: bottomHeight,
      child: Container(
        padding: EdgeInsets.all(20.0),
        child: Row(
          children: <Widget>[
            Expanded(
                child: Icon(
              Icons.favorite_border,
              size: leftIconSize,
              color: leftIconColor,
            )),
            Expanded(
                child: Icon(
              Icons.favorite,
              size: rightIconSize,
              color: rightIconColor,
            ))
          ],
        ),
      ),
    );
  }
}


上一篇下一篇

猜你喜欢

热点阅读