Recently, I need to implement a small requirement, which includes the following function points: 1. Click a region, highlight this region, and display gray elsewhere; 2. 2. While highlighting, a menu button pops up at the bottom; 3. Click the menu button to perform the corresponding operation, click the gray highlight and the popup menu at the bottom to disappear. As shown below:

At first, I thought about using BottomSheet, but after BottomSheet pops up, other places will not be highlighted. Later, I thought whether I could use CustomPainter to draw it, but LATER I found it difficult to achieve. Then I searched the Internet to see if there was a similar solution. I found someone who did something very similar. See here.

After reading it, the idea is very simple (PS: I didn’t think about this at all, maybe I just got into the idea of Flutter and haven’t turned it around yet, so our main idea is to get the region that we click on (in this case BankCardBox Widget) Take the BankCardBox Widget and upload it to the new page. On the new page, we need to make sure that the Widget is positioned the same as it was on the top of the original screen. In this way, we can set the transparency on the rest of the new page to achieve the desired effect — click on the screen area to highlight this area. And ash everywhere else. Based on this, we mainly need to do the following things:

1. Obtain the position and size of the BankCardBox Widget on the original screen page to ensure that the previous BankCardBox Widget is completely overwritten after opening the new screen page;

Firstly, we think that the UI rendering of Flutter is a Widgets tree. The tree feature makes it easy for a node to get information about its byte points through the context. Therefore, if we need to get the location of the Widget, Why don’t we make this Widget with a Stateful widgets, then through Global key had a position of the Widget, so we code is as follows:

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); }}Copy the code

The child attribute in the Stateful Widget was the Widget that was packaged, and the menuContent was the menu button that was popped up at the bottom when the Widget was clicked. Here we get the location and size of the Widget using the getOffset method.

2. Wrap each BankCardBox;

We implemented the Stateful Widget, which we could then use to wrap our BankCardBox Widget. We use the ListView.builder method to build a list of cards, and each card in the list is our BandCardBox.

  @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,
        ),
      ),
    );
  }
Copy the code

3. Get BankCardBox Widget to jump to a new page;

After clicking the BankCardBox Widget, jump to the new page. In order to achieve the effect of menu pop-up, we use PageRouteBuilder instead of the traditional MaterialPageRoute to implement this route.

@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, ); }Copy the code

4. The new page of the pop-up menu is realized

After clicking BankCardBox, we jump to the new page. The new page is implemented as follows. The overall Stack layout is used so that the pop-up menu is displayed at the bottom. The Transparency of the page was adjusted using the Backdrop Filter. At the same time we use GestureDetector to achieve clicking elsewhere to pop the current popup page.

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: 1, sigmaY: 1), child: Container(color: color.black. WithOpacity (0.3),),),), 21. 15.0, child: TweenAnimationBuilder(duration: duration (milliseconds: 200), Builder: (BuildContext context, value, Widget child) { return Transform.scale( scale: value, alignment: Alignment.center, child: }, tween: tween (begin: 0.0, end: 1.0), child: Container(width: sw-30.0, height: sh *.2, decoration: BoxDecoration(color: color.transparent, borderRadius: const borderRadius. All (Radius. Circular (5.0)), boxShadow: [ const BoxShadow( color: Colors.black38, blurRadius: 10, spreadRadius: 1) ]), child: ClipRRect( borderRadius: 21. Const borderradio.all (radio.circular (5.0)), child: menuContent,),),),), top: childOffset. Dy, left: childOffset.dx, child: AbsorbPointer( absorbing: true, child: Container( decoration: BoxDecoration( color: Color.white, borderRadius: Borderradius.all (radius.circular (8.0)),), width: childsize.width, height: childSize.height, child: child, ), ), ), ], ), ), ); }}Copy the code

5. Summary.

The main idea is to use Widgets Tree packages to get the size and location of child Widgets, PageRouteBuilder to implement routing effects, GestureDetector to detect click areas, and so on. The source code.