<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
開發過程中常見這樣的需求,頁面中有幾個按鈕,使用者首次進入時需要對這幾個按鈕高亮展示並加上文字提示。常見的一種方案是找UI切圖,那如何完全使用程式碼來實現呢?
就以Flutter原始Demo頁面為例,如果我們需要對中間展示區域以及右下角按鈕進行一個引導提示。
我們需要做到的效果是除了紅色框內的Widget
,其餘部分要蓋上一層半透明黑色浮層,相當於是全螢幕浮層,紅色區域鏤空。
首先是黑色浮層,這個比較容易,Flutter中的Overlay
可以輕易實現,它可以浮在任意的Widget
之上,包括Dialog
。
那麼如何鏤空呢?
一種思路是首先拿到對應的Widget
與其寬高和xy偏移量,然後在Overlay
中先鋪一層浮層後,把該Widget
在Overlay
的對應位置中再繪製一遍。也就是說該Widget
存在兩份,一份是原本的Widget
,另一份是在Overlay
之上又繪製一層,並且不會被浮層所覆蓋,即為高亮。這是一種思路,但如果你需要進行引導提示的Widget
自身有透明度,那麼這個方案就略有問題,因為你的浮層即為半透明,那麼使用者就可以穿過頂層的Widget
看到下面的內容,略有瑕疵。
那麼另一種思路就是我們不去在Overlay
之上蓋上另一個克隆Widget
,而是將Overlay
半透明黑色塗層對應位置進行鏤空即可,就不存在任何問題了。
既然需要鏤空,我們需要了解一下Flutter中的圖層混合模式概念
在畫布上繪製形狀或影象時,可以使用不同的演演算法來混合畫素,每個演演算法都存在兩個輸入,即源(正在繪製的影象 src)和目標(要合成源影象的影象 dst)
我們把半透明黑色塗層 和 需要進行高亮的Widget 理解為src和dst。
接下來我們通過下面的圖例可知,如果我們需要實現鏤空效果,需要的混合模式為SrcOut
或DstOut
,因為他們的混合模式為一個源展示,且該源與另一個源有非透明畫素交匯部分完全剔除。
Flutter中為我們提供了ColorFiltered
,這是一個官方為我們封裝的一個以Color作為源的混合模式Widget。其接收兩個引數,colorFilter
和child
,前者我們可以理解為上述的src
,後者則為dst
。
下面以一段簡單的程式碼說明
class TestColorFilteredPage extends StatelessWidget { const TestColorFilteredPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return ColorFiltered( colorFilter: const ColorFilter.mode(Colors.yellow, BlendMode.srcOut), child: Stack( children: [ Positioned.fill( child: Container( color: Colors.transparent, )), Positioned( top: 100, left: 100, child: Container( color: Colors.black, height: 100, width: 100, )) ], ), ); } }
效果:
可以看到作為src
的colorFiler
除了與作為dst
的Stack
有非透明畫素交匯的地方被鏤空了,其他地方均正常顯示。
此處需要說明一下,作為dst
的child
,要實現蒙版的效果,必須要與src
有所交匯,所以Stack
中使用了透明的Positioned.fill
填充,之所以要用透明色,是因為我們使用的混合模式srcOut
的演演算法會剔除非透明畫素互動部分
上述部分思路已經足夠支援我們寫出想要的效果了,接下來我們來進行實現
首先我需要拿到對應Widget
的key
,就可以拿到對應的寬高與xy偏移量
RenderObject? promptRenderObject = promptWidgetKey.currentContext?.findRenderObject(); double widgetHeight = promptRenderObject?.paintBounds.height ?? 0; double widgetWidth = promptRenderObject?.paintBounds.width ?? 0; double widgetTop = 0; double widgetLeft = 0; if (promptRenderObject is RenderBox) { Offset offset = promptRenderObject.localToGlobal(Offset.zero); widgetTop = offset.dy; widgetLeft = offset.dx; }
lastOverlay = OverlayEntry(builder: (ctx) { return GestureDetector( onTap: () { // 點選後移除當前展示的overlay _removeCurrentOverlay(); // 準備展示下一個overlay _prepareToPromptSingleWidget(); }, child: Stack( children: [ Positioned.fill( child: ColorFiltered( colorFilter: ColorFilter.mode( Colors.black.withOpacity(0.7), BlendMode.srcOut), child: Stack( children: [ // 透明色填充背景,作為蒙版 Positioned.fill( child: Container( color: Colors.transparent, )), // 鏤空區域 Positioned( left: l, top: t, child: Container( width: w, height: h, decoration: decoration ?? const BoxDecoration(color: Colors.black), )), ], ), )), // 文字提示,需要放在ColorFiltered的外層 Positioned( left: l - 40, top: t - 40, child: Material( color: Colors.transparent, child: Text( tips, style: const TextStyle(fontSize: 14, color: Colors.white), ), )) ], ), ); }); Overlay.of(context)?.insert(lastOverlay!);
其中的文字偏移量,可以自己通過程式碼來設定,展示在中心,或者判斷位置跟隨Widget
展示均可,此處不再贅述。
最後我們把Overlay
新增到螢幕上展示即可。
這裡我將邏輯封裝在靜態工具類中,鑑於單個頁面可能會有不止一個引導Widget
,所以對於這個靜態工具類,我們需要傳入需要進行高亮引導的Widget
和提示語的集合。
class PromptItem { GlobalKey promptWidgetKey; String promptTips; PromptItem(this.promptWidgetKey, this.promptTips); }
class PromptBuilder { static List<PromptItem> toPromptWidgetKeys = []; static OverlayEntry? lastOverlay; static promptToWidgets(List<PromptItem> widgetKeys) { toPromptWidgetKeys = widgetKeys; _prepareToPromptSingleWidget(); } static _prepareToPromptSingleWidget() async { if (toPromptWidgetKeys.isEmpty) { return; } PromptItem promptItem = toPromptWidgetKeys.removeAt(0); RenderObject? promptRenderObject = promptItem.promptWidgetKey.currentContext?.findRenderObject(); double widgetHeight = promptRenderObject?.paintBounds.height ?? 0; double widgetWidth = promptRenderObject?.paintBounds.width ?? 0; double widgetTop = 0; double widgetLeft = 0; if (promptRenderObject is RenderBox) { Offset offset = promptRenderObject.localToGlobal(Offset.zero); widgetTop = offset.dy; widgetLeft = offset.dx; } if (widgetHeight != 0 && widgetWidth != 0 && widgetTop != 0 && widgetLeft != 0) { _buildNextPromptOverlay( promptItem.promptWidgetKey.currentContext!, widgetWidth, widgetHeight, widgetLeft, widgetTop, null, promptItem.promptTips); } } static _buildNextPromptOverlay(BuildContext context, double w, double h, double l, double t, Decoration? decoration, String tips) { _removeCurrentOverlay(); lastOverlay = OverlayEntry(builder: (ctx) { return GestureDetector( onTap: () { // 點選後移除當前展示的overlay _removeCurrentOverlay(); // 準備展示下一個overlay _prepareToPromptSingleWidget(); }, child: Stack( children: [ Positioned.fill( child: ColorFiltered( colorFilter: ColorFilter.mode( Colors.black.withOpacity(0.7), BlendMode.srcOut), child: Stack( children: [ // 透明色填充背景,作為蒙版 Positioned.fill( child: Container( color: Colors.transparent, )), // 鏤空區域 Positioned( left: l, top: t, child: Container( width: w, height: h, decoration: decoration ?? const BoxDecoration(color: Colors.black), )), ], ), )), // 文字提示,需要放在ColorFiltered的外層 Positioned( left: l - 40, top: t - 40, child: Material( color: Colors.transparent, child: Text( tips, style: const TextStyle(fontSize: 14, color: Colors.white), ), )) ], ), ); }); Overlay.of(context)?.insert(lastOverlay!); } static _removeCurrentOverlay() { if (lastOverlay != null) { lastOverlay!.remove(); lastOverlay = null; } } }
class MyHomePage extends StatefulWidget { const MyHomePage({Key? key, required this.title}) : super(key: key); final String title; @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver { int _counter = 0; GlobalKey centerWidgetKey = GlobalKey(); GlobalKey bottomWidgetKey = GlobalKey(); void _incrementCounter() { setState(() { _counter++; }); } @override void initState() { super.initState(); // 頁面展示時進行prompt繪製,在此新增observer監聽等待渲染完成後掛載prompt WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { List<PromptItem> prompts = []; prompts.add(PromptItem(centerWidgetKey, "這是中心Widget")); prompts.add(PromptItem(bottomWidgetKey, "這是底部Button")); PromptBuilder.promptToWidgets(prompts); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, // 需要高亮展示的widget,需要宣告其GlobalKey key: centerWidgetKey, children: <Widget>[ const Text( 'You have pushed the button this many times:', ), Text( '$_counter', style: Theme.of(context).textTheme.headline4, ), ], ), ), floatingActionButton: FloatingActionButton( // 需要高亮展示的widget,需要宣告其GlobalKey key: bottomWidgetKey, onPressed: _incrementCounter, tooltip: 'Increment', child: const Icon(Icons.add), ), // This trailing comma makes auto-formatting nicer for build methods. ); } }
本文僅總結程式碼實現思路,對於具體細節並未處理,可以在PromptItem
和PromptBuilder
進行更多的屬性宣告以更加靈活的展示prompt,比如圓角等引數。有任何問題歡迎大家隨時討論。
最後附上github地址:github.com/slowguy/flu…
以上就是Flutter使用Overlay與ColorFiltered新手引導實現範例的詳細內容,更多關於Flutter使用Overlay ColorFiltered的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45