一個相似於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屬性和滑動過程當中返回的監聽值,能夠在動畫過程當中爲佈局添加各類個樣的變換。markdown
用Flutter實現這樣的一個效果其實很簡單,300行代碼足矣。側滑菜單的實現其實就是上層佈局隨着用戶手勢,更改自身的位置,從而讓底層菜單欄展現出來。明白了這麼一個過程以後,一切就都好辦了。
基本思路:上下兩層佈局用Stack組合,上層佈局須要支持手勢,下層佈局只須要是一個普通佈局就能夠了。因此難點就是,上層佈局如何支持手勢?關於Flutter中的手勢能夠看下這篇文章:解析Flutter中的手勢控制Gestures,瞭解一下GestureRecognizer是什麼。固然,咱們實現簡單的側滑功能並不須要這麼複雜,由於沒有涉及到滑動衝突,咱們只需使用系統自帶的HorizontalDragGestureRecognizer
類就能夠了。上層佈局每一幀的變換進度使用AnimationController
來控制,其回調中的value值可讓咱們很方便的就獲取到動畫的進度值。框架
首先,咱們給咱們的自定義佈局註冊手勢監聽Recognizer,_registerGestureRecognizer()
方法在佈局的initState()
方法中執行:ide
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(() {});
刷新上層佈局位置。oop
因此,build函數的返回值就很好定義了,由於有手勢,咱們最外層包裹一個RawGestureDetector
,而後將咱們在Step 1中註冊的gestures傳進去,表示這個控件以後將會接收垂直/水平方向的gestures。由於上層佈局涉及到位置的移動,所以咱們選擇使用Transform來構建。每次用戶手指滑動時,產生一個dragValue,經過該值計算出控件應該偏移的值,咱們將其保存爲containerOffset,將這個containerOffset傳給Transform,setState時就會產生頁面上的移動視覺效果了。佈局
@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,很好理解。動畫
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
。