最近须要实现一个小需求包含以下功能点:1. 点击某个区域,高亮此区域,其余地方灰度显示;2. 高亮的同时,底部弹出菜单按钮;3. 点击菜单按钮执行相应操做,点击灰度地方高亮和底部弹出菜单消失。以下图所示:html
刚开始考虑到使用BottomSheet来作,可是BottomSheet弹出后,其余地方不会高亮,后来又想到是否可使用CustomPainter画出来,后面发现比较难以实现。接着就网上搜索了一下有没有相似方案,发现了的确有人作了很是相似的东西,参考这里。git
看了一遍以后发现思路很是简单(PS:我作的时候彻底没有往这方面想,多是刚接触Flutter思路想法尚未转过来吧~_~),因此咱们的主要思路就是,获取咱们点击的区域(咱们这里是BankCardBox Widget)Widget,拿到这个BankCardBox Widget传到新的页面,同时在新的页面咱们要保证这个Widget的位置要和原来屏幕上面的位置是同样的,这样在新页面其余地方设置透明度,达到咱们须要的效果 --- 即点击屏幕区域,高亮此区域,而且其余地方置灰。基于此,咱们主要须要作如下几件事:github
首先咱们想到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
上面咱们实现了这个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,
),
),
);
}
复制代码
点击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,
);
}
复制代码
点击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,
),
),
),
],
),
),
);
}
}
复制代码
主要是这种思路,使用Widgets tree包裹获取子Widget的大小和位置,使用了PageRouteBuilder来实现路由效果,GestureDetector检测点击区域等等。源码。ide