最近須要實現一個小需求包含以下功能點: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