Flutter | 超簡單仿微信QQ側滑菜單組件

相信你們對於這種需求確定不陌生:git

側滑出菜單,在Flutter 當中,這種需求怎麼實現?github

看一下實現的效果:微信

需求分析

老套路,先分析一下需求:less

  1. 首先能夠滑出菜單
  2. 菜單滑動到必定距離徹底滑出,未達到距離回滾
  3. 菜單數量、樣式隨意定製
  4. 菜單點擊回調
  5. 菜單展開時,點擊 item 收回菜單(見QQ)

代碼實現

需求明瞭之後就能夠寫代碼了。ide

1. 首先能夠滑出菜單

最基本的,菜單要能滑的出來,咱們思考一下,如何能在屏幕外面放置 Widget,而且還能滑動?函數

基本上不到一分鐘,相信你們都能想出來答案:ScrollView,沒錯,也就只有 ScrollView 知足咱們的需求。測試

說幹就幹:ui

SingleChildScrollView(
  physics: ClampingScrollPhysics(),
  scrollDirection: Axis.horizontal,
  controller: _controller,
  child: Row(children: children),
)

---------------
// 第一個 Widget,寬度爲屏幕的寬度
SizedBox(
  width: screenWidth,
  child: child,
),
複製代碼
  1. 首先把 ScrollView 滑動的位置改成橫向
  2. 把滑動的效果改成 ClampingScrollPhysics,不然 iOS 會有回彈的效果
  3. 設置一個 controller,用於監聽滑動距離
  4. 設置child 爲Row,而且第一個 Widget 充滿橫向屏幕,這樣後續的菜單就在屏幕外了

2. 菜單滑動到必定距離徹底滑出,未達到距離回滾

這個效果就須要監聽滑動距離和手勢了。this

若是滑動距離大於全部 menu 寬度的 1/4,那就全都滑出來,若是不到的話,就回滾回去。spa

那就要判斷一下手是否已經離開屏幕,在這個時候判斷距離。

原本想着套一個 Gesture,可是發現不行,問了一下大佬們,用了 Listener

代碼以下:

Listener(
  onPointerUp: (d) {
    if (_controller.offset < (screenWidth / 5) * menu.length / 4) {
      _controller.animateTo(0, duration: Duration(milliseconds: 100), curve: Curves.linear);
    } else {
      _controller.animateTo(menu.length * (screenWidth / 5), duration: Duration(milliseconds: 100), curve: Curves.linear);
    }
  },
  child: SingleChildScrollView(
    physics: ClampingScrollPhysics(),
    scrollDirection: Axis.horizontal,
    controller: _controller,
    child: Row(children: children),
  ),
)
複製代碼

很簡單,就是在 手擡起 的時候判斷了一下距離,而後調用了 animteTo 方法。

3. 菜單數量、樣式隨意定製

這個其實很簡單,讓「用戶」來傳入就行了,

我只須要控制 menu 的寬度。

因而我把 Container 的參數都扒了下來,封裝成了一個組件 SlideMenuItem

class SlideMenuItem extends StatelessWidget {
  SlideMenuItem({
    Key key,
    @required this.child,
    this.alignment,
    this.padding,
    Color color,
    Decoration decoration,
    this.foregroundDecoration,
    this.height,
    BoxConstraints constraints,
    this.margin,
    this.transform,
    @required this.onTap,
  })  : assert(child != null),
        assert(margin == null || margin.isNonNegative),
        assert(padding == null || padding.isNonNegative),
        assert(decoration == null || decoration.debugAssertIsValid()),
        assert(constraints == null || constraints.debugAssertIsValid()),
        assert(
            color == null || decoration == null,
            'Cannot provide both a color and a decoration\n'
            'The color argument is just a shorthand for "decoration: new BoxDecoration(color: color)".'),
        decoration =
            decoration ?? (color != null ? BoxDecoration(color: color) : null),
        constraints = (height != null)
            ? constraints?.tighten(height: height) ??
                BoxConstraints.tightFor(height: height)
            : constraints,
        super(key: key);

  final BoxConstraints constraints;
  final Decoration decoration;
  final AlignmentGeometry alignment;
  final EdgeInsets padding;
  final Decoration foregroundDecoration;
  final EdgeInsets margin;
  final Matrix4 transform;
  final Widget child;
  final double height;
  final GestureTapCallback onTap;

  @override
  Widget build(BuildContext context) {
    return Container(
      child: child,
      alignment: alignment,
      constraints: constraints,
      decoration: decoration,
      padding: padding,
      width: screenWidth / 5,
      height: height,
      foregroundDecoration: foregroundDecoration,
      margin: margin,
      transform: transform,
    );
  }
}
複製代碼

這麼長的代碼,其實就 「width」 和 「onTap」 是本身寫的。

4. 菜單點擊回調

這裏有個小問題:把 Menu 單獨封裝成了一個組件,那如何在點擊 menu 的時候把 menu 收回去?

基於這個問題,在建立整個 SlideItem 的時候,經過構造函數把每個 menu 都添加上了 GestureDetector,而後在 onTap() 回調中調用 menu 的 onTap() 方法,再調用 dismiss() 方法來收回 menu。

代碼以下:

addAll(menu
       .map((w) => GestureDetector(
         child: w,
         onTap: (){
           w.onTap();
           dismiss();
         },
       ))
       .toList());
複製代碼

5. 菜單展開時,點擊 item 收回菜單

也就是 菜單展開時,點擊了 item 的話,要先收回菜單。QQ 就是如此。

這裏有一個知識點,咱們設置的點擊事件默認是不會命中透明組件的,因此要給第一個默認佔滿屏幕寬度的 Widget 加上一個屬性:behavior: HitTestBehavior.opaque

完整的代碼以下:

GestureDetector(
  behavior: HitTestBehavior.opaque,
  onTap: (){
    if(_controller.offset != 0){
      dismiss();
    }else{
      onTap();
    }
  },
  child: SizedBox(
    width: screenWidth,
    child: child,
  ),
)
複製代碼

其中 behavior 有三個值:

  • deferToChild:子組件會一個接一個的進行命中測試,若是子組件中有測試經過的,則當前組件經過,這就意味着,若是指針事件做用於子組件上時,其父級組件也確定能夠收到該事件。
  • opaque:在命中測試時,將當前組件當成不透明處理(即便自己是透明的),最終的效果至關於當前Widget的整個區域都是點擊區域。
  • translucent:當點擊組件透明區域時,能夠對自身邊界內及底部可視區域都進行命中測試,這意味着點擊頂部組件透明區域時,頂部組件和底部組件均可以接收到事件。

總結

引用羣裏一大佬的話:

不要把問題複雜化。

其實對於這種效果,咱們仔細的想一分鐘,幾乎都能想出來解決方案的。並且實現起來也很簡單。

原本想封裝成一個 ListView 的,後來感受沒什麼必要,單獨封裝成一個 Item 也足夠用了。

代碼已傳到GitHub:github.com/wanglu1209/…

另我我的建立了一個「Flutter 交流羣」,能夠添加我我的微信 「17610912320」來入羣。

推薦閱讀:

Flutter | 1.9 全新組件 ToggleButtons

Flutter | 一個超級酷炫的登陸頁是怎樣煉成的

Flutter | WReorderList 一個能夠指定兩個item互換位置的組件

Flutter | 如何實現一個水波紋擴散效果的 Widget

相關文章
相關標籤/搜索