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