一个相似于QQ侧滑菜单的功能,支持从上、下、左、右四个方法打开菜单栏。能够经过自定义transform实现更加炫酷的动效!
先上效果图:git
Github地址:github.com/yumi0629/Sl…github
使用方法:bash
SlideStack(
child: SlideContainer(
key: _slideKey,
child: Container(
/// widget mian.
),
slideDirection: SlideDirection.top,
onSlide: onSlide,
drawerSize: maxSlideDistance,
transform: transform,
),
drawer: Container(
/// widget drawer.
),
);
复制代码
slideDirection
属性用来控制菜单从哪一个方法打开;调用key.currentState.openOrClose()
方法能够手动打开或关闭菜单;配合transform属性和滑动过程当中返回的监听值,能够在动画过程当中为布局添加各类个样的变换。框架
用Flutter实现这样的一个效果其实很简单,300行代码足矣。侧滑菜单的实现其实就是上层布局随着用户手势,更改自身的位置,从而让底层菜单栏展现出来。明白了这么一个过程以后,一切就都好办了。
基本思路:上下两层布局用Stack组合,上层布局须要支持手势,下层布局只须要是一个普通布局就能够了。因此难点就是,上层布局如何支持手势?关于Flutter中的手势能够看下这篇文章:解析Flutter中的手势控制Gestures,了解一下GestureRecognizer是什么。固然,咱们实现简单的侧滑功能并不须要这么复杂,由于没有涉及到滑动冲突,咱们只需使用系统自带的HorizontalDragGestureRecognizer
类就能够了。上层布局每一帧的变换进度使用AnimationController
来控制,其回调中的value值可让咱们很方便的就获取到动画的进度值。ide
首先,咱们给咱们的自定义布局注册手势监听Recognizer,_registerGestureRecognizer()
方法在布局的initState()
方法中执行:函数
final Map<Type, GestureRecognizerFactory> gestures =
<Type, GestureRecognizerFactory>{};
void _registerGestureRecognizer() {
if (isSlideVertical) {
gestures[VerticalDragGestureRecognizer] =
createGestureRecognizer<VerticalDragGestureRecognizer>(
() => VerticalDragGestureRecognizer());
} else {
gestures[HorizontalDragGestureRecognizer] =
createGestureRecognizer<HorizontalDragGestureRecognizer>(
() => HorizontalDragGestureRecognizer());
}
}
GestureRecognizerFactoryWithHandlers<T>
createGestureRecognizer<T extends DragGestureRecognizer>(
GestureRecognizerFactoryConstructor<T> constructor) =>
GestureRecognizerFactoryWithHandlers<T>(
constructor,
(T instance) {
instance
..onStart = handleDragStart
..onUpdate = handleDragUpdate
..onEnd = handleDragEnd;
},
);
复制代码
咱们有了Recognizer,怎么跟用户的手势绑定起来呢?这里用到了AnimationController
和Ticker
类。布局
AnimationController animationController;
Ticker fingerTicker;
@override
void initState() {
animationController =
AnimationController(vsync: this, duration: widget.autoSlideDuration)
..addListener(() {
······
// 刷新上层布局位置
setState(() {});
});
fingerTicker = createTicker((_) {
······
// 更具用户手势移动位置,更新animationController.value
animationController.value = ······;
});
_registerGestureRecognizer();
super.initState();
}
复制代码
很明显,用户的手势滑动时会产生一个滑动值,咱们将这个滑动值进行计算,再赋值给animationController.value;同时计算出上层布局须要的偏移量,经过调用setState(() {});
刷新上层布局位置。动画
因此,build函数的返回值就很好定义了,由于有手势,咱们最外层包裹一个RawGestureDetector
,而后将咱们在Step 1中注册的gestures传进去,表示这个控件以后将会接收垂直/水平方向的gestures。由于上层布局涉及到位置的移动,所以咱们选择使用Transform来构建。每次用户手指滑动时,产生一个dragValue,经过该值计算出控件应该偏移的值,咱们将其保存为containerOffset,将这个containerOffset传给Transform,setState时就会产生页面上的移动视觉效果了。ui
@override
Widget build(BuildContext context) => RawGestureDetector(
gestures: gestures,
child: Transform.translate(
offset: isSlideVertical
? Offset(
0.0,
containerOffset,
)
: Offset(
containerOffset,
0.0,
),
child: _getContainer(),
),
);
复制代码
到目前为止,大体的实现框架已经出来了,接下来就是计算部分了。
首先,咱们的containerOffset其实就是dragValue,很好理解。this
double get containerOffset => dragValue;
复制代码
其次是滑动(动画)的进度,很简单,dragValue / maxDragDistance
,也就是拖动距离/总距离(Drawer的宽度/高度)
。
fingerTicker = createTicker((_) {
animationController.value = dragValue / maxDragDistance;
});
复制代码
这里有人可能会有一个疑问了,我根据dragValue,直接算出了containerOffset,而后让上层控件移动位置,整个过程不久OK了嘛,还要什么AnimationController干吗?确实,animationController只是起到了一个记录做用。咱们之因此要用到animationController,一是能够经过AnimationController将拖动进度返回给最外层的父控件,还有一个缘由是,能够经过animationController去快速完成/取消滑动动做。
AnimationController好处都有啥,看下面:
void openOrClose() {
final AnimationStatus status = animationController.status;
final bool isOpen = status == AnimationStatus.completed || status == AnimationStatus.forward;
animationController.fling(velocity: isOpen ? -2.0 : 2.0);
}
void _completeSlide() => animationController.forward().then((_) {
if (widget.onSlideCompleted != null) widget.onSlideCompleted();
});
void _cancelSlide() => animationController.reverse().then((_) {
if (widget.onSlideCanceled != null) widget.onSlideCanceled();
});
复制代码
咱们能够很方便的经过AnimationController提供的API,在用户拖动到一半,或者说用户点击了某个按钮来打开/关闭菜单时,快速地完成打开/关闭操做,而不是手动的不停的刷新containerOffset。因此说,AnimationController是一个未雨绸缪的设计,由于这不是一个单纯地布局跟着用户手势动就OK了的控件,咱们须要一个控制器来自由地控制布局的位置。
实际使用中,咱们常常会碰到一个问题,就是用户的手指并无彻底滑动到maxDragDistance这个值,可能化到一半就中止了。那么咱们的上层控件应该怎么作呢?将布局位置定位在用户手势中止的地方明显是不友好的。QQ侧滑菜单的解决方案是:用户手指超过了某个边界值则自动完成打开操做;若未达到边界值,则取消这个打开操做:
handleDragEnd
方法,这个方法在Step 1中注册GestureRecognizer时,咱们将其传入了Recognizer的onEnd回调监听中,
minAutoSlideDragVelocity
就是咱们定义的这个边界值:
void handleDragUpdate(DragUpdateDetails details) {
if (dragValue >
widget.minAutoSlideDragVelocity) {
_completeSlide();
} else if (dragValue <
widget.minAutoSlideDragVelocity) {
_cancelSlide();
}
fingerTicker.stop();
}
复制代码
这个很简单,以前已经提到了,使用Stack布局时最简单的方法了:
class SlideStack extends StatefulWidget {
/// The main widget.
final SlideContainer child;
/// The drawer hidden below.
final Widget drawer;
const SlideStack({
@required this.child,
@required this.drawer,
}) : super();
@override
State<StatefulWidget> createState() => _StackState();
}
class _StackState extends State<SlideStack> {
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
widget.drawer,
widget.child,
],
);
}
}
复制代码
到此为止,咱们已经完成了90%的工做了,接下来就是修饰一些细节了,咱们添加一些属性,让侧滑菜单体验更加友好。这部分具体的请看 源码 。
shadowBlurRadius
和shadowSpreadRadius
属性;dragDampening
,这个参数在咱们作List滑动的时候很常见,布局的实际移动距离,跟用户手指的移动距离每每是不一致的,咱们能够经过这个阻尼系数来控制;transform
,咱们上面的实现都只是将上层布局进行了平移,若是须要实现效果图1中的平移+缩小效果,须要添加自定义的transform。之因此没有将缩小效果包裹进控件,是由于我但愿控件的形变能够更为灵活,你们能够从外部去控制,而不是直接写死。并且我已经经过AnimationController将动画进度暴露出来了,经过动画进度能够很方便的进行各类你想要的transform。onSlideStarted
、onSlideCompleted
、onSlideCanceled
、onSlide
。