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

相关文章
相关标签/搜索