Flutter - 上下文彈出菜單

最近須要實現一個小需求包含以下功能點:1. 點擊某個區域,高亮此區域,其餘地方灰度顯示;2. 高亮的同時,底部彈出菜單按鈕;3. 點擊菜單按鈕執行相應操做,點擊灰度地方高亮和底部彈出菜單消失。以下圖所示:html

剛開始考慮到使用BottomSheet來作,可是BottomSheet彈出後,其餘地方不會高亮,後來又想到是否可使用CustomPainter畫出來,後面發現比較難以實現。接着就網上搜索了一下有沒有相似方案,發現了的確有人作了很是相似的東西,參考這裏git

看了一遍以後發現思路很是簡單(PS:我作的時候徹底沒有往這方面想,多是剛接觸Flutter思路想法尚未轉過來吧~_~),因此咱們的主要思路就是,獲取咱們點擊的區域(咱們這裏是BankCardBox Widget)Widget,拿到這個BankCardBox Widget傳到新的頁面,同時在新的頁面咱們要保證這個Widget的位置要和原來屏幕上面的位置是同樣的,這樣在新頁面其餘地方設置透明度,達到咱們須要的效果 --- 即點擊屏幕區域,高亮此區域,而且其餘地方置灰。基於此,咱們主要須要作如下幾件事:github

1. 獲取原屏幕頁面上面的BankCardBox Widget的位置和大小,保證打開新屏幕頁面後徹底覆蓋以前的BankCardBox Widget;

首先咱們想到Flutter的UI渲染是一個Widgets tree,那麼tree的特性使得一個節點能夠經過context很輕易的拿到它的字節點的相關信息,因此咱們這裏若是須要獲取Widget的位置,咱們何不把這個Widget經過一個Stateful Widget包裹起來,而後經過Global key拿到這個Widget的位置,這樣咱們編碼以下:api

class FocusedMenuHolder extends StatefulWidget {
  final Widget child,menuContent;


  const FocusedMenuHolder({Key key, @required this.child,@required this.menuContent});

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

class _FocusedMenuHolderState extends State<FocusedMenuHolder> {
  GlobalKey containerKey = GlobalKey();
  Offset childOffset = Offset(0, 0);
  Size childSize;

  getOffset() {
    RenderBox renderBox = containerKey.currentContext.findRenderObject();
    Size size = renderBox.size;
    Offset offset = renderBox.localToGlobal(Offset.zero);
    setState(() {
      this.childOffset = Offset(offset.dx, offset.dy);
      childSize = size;
    });
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
        key: containerKey,
        onLongPress: () async {
          getOffset();
        },
        child: widget.child);
  }
}
複製代碼

能夠看到,Stateful Widget裏面的child屬性就是咱們須要包裹的Widget,menuContent就是咱們點擊Widget時候須要在底部彈出的菜單按鈕。咱們在這裏是經過getOffset方法拿到Widget的位置和大小的。markdown

2. 包裹每一個BankCardBox;

上面咱們實現了這個Stateful Widget,接着咱們就能夠經過它來包裹咱們的BankCardBox Widget了。咱們經過ListView.builder方法構建了一個卡片列表,卡片列表的每一個卡片就是咱們的BandCardBox。app

@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: CommonWidget.appBar(
        context,
        'Cards',
        Icons.arrow_back,
        Colors.black,
      ),
      body: Container(
        margin: EdgeInsets.all(8.0),
        height: SizeConfig().screenHeight * .7,
        child: ListView.builder(
          shrinkWrap: true,
          itemBuilder: (context, index) {
            BankCard card = cards[index];
            return FocusedMenuHolder(
              child: BankCardBox(
                cardType: card.cardBrand,
                cardNum: card.cardNumber,
              ),
              menuContent: _buildMenuItems(card),
            );
          },
          itemCount: cards.length,
        ),
      ),
    );
  }
複製代碼

3. 獲取BankCardBox Widget跳轉新頁面;

點擊BankCardBox Widget以後,跳轉到新頁面,這裏咱們爲了實現菜單彈出的效果,咱們不用傳統的MaterialPageRoute,使用PageRouteBuilder來實現這個路由。less

@override
  Widget build(BuildContext context) {
    return GestureDetector(
      key: containerKey,
      onTap: () async {
        getOffset();
        await Navigator.push(
          context,
          PageRouteBuilder(
            transitionDuration: Duration(milliseconds: 100),
            pageBuilder: (context, animation, secondaryAnimation) {
              animation = Tween(begin: 0.0, end: 1.0).animate(animation);
              return FadeTransition(
                opacity: animation,
                child: FocusedMenuDetails(
                  menuContent: widget.menuContent,
                  child: widget.child,
                  childOffset: childOffset,
                  childSize: childSize,
                ),
              );
            },
            fullscreenDialog: true,
            opaque: false,
          ),
        );
      },
      child: widget.child,
    );
  }
複製代碼

4. 彈出菜單新頁面實現

點擊BankCardBox以後,咱們跳轉到新頁面,新頁面實現以下,總體上使用Stack佈局,使得彈出菜單展現在底部,BankCardBox Widget根據傳入的位置和大小布局到指定的位置,而且使用Backdrop Filter來調節頁面的透明度。同時咱們使用GestureDetector來實現點擊其餘地方pop當前彈出頁面。async

import 'dart:ui';

import 'package:flutter/material.dart';

import '../../shared.dart';

class FocusedMenuDetails extends StatelessWidget {
  final Offset childOffset;
  final Size childSize;
  final Widget menuContent;
  final Widget child;

  const FocusedMenuDetails({
    Key key,
    @required this.menuContent,
    @required this.childOffset,
    @required this.childSize,
    @required this.child,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final sw = SizeConfig().screenWidth;
    final sh = SizeConfig().screenHeight;

    return Scaffold(
      backgroundColor: Colors.transparent,
      body: Container(
        child: Stack(
          fit: StackFit.expand,
          children: [
            GestureDetector(
              onTap: () {
                Navigator.pop(context);
              },
              child: BackdropFilter(
                filter: ImageFilter.blur(sigmaX: 4, sigmaY: 4),
                child: Container(
                  color: Colors.black.withOpacity(0.3),
                ),
              ),
            ),
            Positioned(
              bottom: 20.0,
              left: 15.0,
              child: TweenAnimationBuilder(
                duration: Duration(milliseconds: 200),
                builder: (BuildContext context, value, Widget child) {
                  return Transform.scale(
                    scale: value,
                    alignment: Alignment.center,
                    child: child,
                  );
                },
                tween: Tween(begin: 0.0, end: 1.0),
                child: Container(
                  width: sw - 30.0,
                  height: sh * .2,
                  decoration: BoxDecoration(
                      color: Colors.transparent,
                      borderRadius:
                          const BorderRadius.all(Radius.circular(5.0)),
                      boxShadow: [
                        const BoxShadow(
                            color: Colors.black38,
                            blurRadius: 10,
                            spreadRadius: 1)
                      ]),
                  child: ClipRRect(
                    borderRadius: const BorderRadius.all(Radius.circular(5.0)),
                    child: menuContent,
                  ),
                ),
              ),
            ),
            Positioned(
              top: childOffset.dy,
              left: childOffset.dx,
              child: AbsorbPointer(
                absorbing: true,
                child: Container(
                  decoration: BoxDecoration(
                    color: Colors.white,
                    borderRadius: BorderRadius.all(Radius.circular(8.0)),
                  ),
                  width: childSize.width,
                  height: childSize.height,
                  child: child,
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
複製代碼

5. 總結。

主要是這種思路,使用Widgets tree包裹獲取子Widget的大小和位置,使用了PageRouteBuilder來實現路由效果,GestureDetector檢測點擊區域等等。源碼ide

相關文章
相關標籤/搜索