Flutter高級玩法Flow位置自定義
前言
Flow布局是一個超級強大的布局,但應(yīng)該很少有人用,因為入手的門檻還是有的
Flow的屬性很簡單,只有FlowDelegate類型的delegate
和組件列表children
,
可能很多人看到delegate就揮揮手:臣妾做不到
,今天就來掰扯一下這個FlowDelegate.
class Flow extends MultiChildRenderObjectWidget { Flow({ Key key, @required this.delegate, List<Widget> children = const <Widget>[], }) : assert(delegate != null),
第一幕、開場-演員入臺
1. 展示舞臺
我們的第一個舞臺是一個200*200的灰色 box,由FlowDemo組件出當(dāng)主角
void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: Scaffold( appBar: AppBar(), body: Center(child: HomePage()), )); } } class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Container( width: 200, height: 200, color: Colors.grey.withAlpha(66), alignment: Alignment.center, child: FlowDemo(), ); } }
2. Flow出場
FlowDemo中使用Flow組件,包含四個box
四個box變成依次是60.0(紅), 50.0(黃), 40.0(藍(lán)), 30.0(綠)
class FlowDemo extends StatelessWidget { final sides = [60.0, 50.0, 40.0, 30.0]; final colors = [Colors.red,Colors.yellow,Colors.blue,Colors.green]; @override Widget build(BuildContext context) { return Flow( delegate: _Delegate(), children: sides.map((e) => _buildItem(e)).toList(), ); } Widget _buildItem(double e) { return Container( width: e, alignment: Alignment.center, height: e, color: colors[sides.indexOf(e)], child: Text('$e'), ); } }
3. FlowDelegate出場
Flow布局需要一個FlowDelegate類型的delegate對象
但是Flutter中并沒有其實現(xiàn)類,所以想玩Flow,只有一條路:自定義
class _Delegate extends FlowDelegate { @override void paintChildren(FlowPaintingContext context) { } @override bool shouldRepaint(FlowDelegate oldDelegate) { return true; } }
4. paintChildren方法和FlowPaintingContext對象
paintChildren
顧名思義是用來畫孩子的
FlowPaintingContext
也就是繪制的上下文,即繪制的信息
那就輕輕的瞄一眼FlowPaintingContext里面有啥吧:
一共有四個東西: size、childCount、getChildSize、paintChild
---->[源碼:flutter/lib/src/rendering/flow.dart:23]---- abstract class FlowPaintingContext { Size get size;//父親尺寸 int get childCount;//孩子個數(shù) Size getChildSize(int i);//第i個孩子尺寸 //繪制孩子 void paintChild(int i, { Matrix4 transform, double opacity = 1.0 }); }
接下來用代碼測試一下這幾個屬性看看,不出所料
默認(rèn)是繪制在父容器的左上角。
class _Delegate extends FlowDelegate { @override void paintChildren(FlowPaintingContext context) { print("父容器尺寸:${context.size}"); print("孩子個數(shù):${context.childCount}"); for(int i=0;i<context.childCount;i++){ print("第$i個孩子尺寸:${context.getChildSize(i)}"); } }
第二幕、排兵布陣
前面只是將組件排在了左上角,那如何對進(jìn)行其他排布呢?
1. paintChild與Matrix4
在paintChild
時可以傳入transform的Matrix4對象進(jìn)行變換
在這里基本上只用了Matrix4的平移translationValues功能,至于Matrix4的具體用法,那又是一個故事了
這里讓黃色的box移到右上角,即X方向平移(父寬-己寬):
@override void paintChildren(FlowPaintingContext context) { var size = context.size; for (int i = 0; i < context.childCount; i++) { if (i == 1) { var tr = context.getChildSize(i); context.paintChild(i, transform: Matrix4.translationValues(size.width - tr.width, 0, 0.0)); } else { context.paintChild(i); } } }
現(xiàn)在讓四個組件排布在父親的四角,如下:
class _AngleDelegate extends FlowDelegate { Matrix4 m4; @override void paintChildren(FlowPaintingContext context) { var size = context.size; for (int i = 0; i < context.childCount; i++) { var cSize = context.getChildSize(i); if (i == 1) { m4 = Matrix4.translationValues(size.width - cSize.width, 0, 0.0); } else if (i == 2) { m4 = Matrix4.translationValues(0, size.height - cSize.height, 0.0); } else if (i == 3) { m4 = Matrix4.translationValues(size.width - cSize.width, size.height - cSize.height, 0.0); } context.paintChild(i, transform: m4); } } @override bool shouldRepaint(FlowDelegate oldDelegate) { return true; } }
2. Flow布局的封裝
如果需要一個排布四角的組件,可以基于上面的Delegate做一個組件
雖然用處很有限,但原來了解一下Flow還是挺好的。
class AngleFlow extends StatelessWidget { final List<Widget> children; AngleFlow({@required this.children}) : assert(children.length == 4); @override Widget build(BuildContext context) { return Flow( delegate: _AngleDelegate(), children: children, ); } } class _AngleDelegate extends FlowDelegate { Matrix4 m4; @override void paintChildren(FlowPaintingContext context) { var size = context.size; for (int i = 0; i < context.childCount; i++) { var cSize = context.getChildSize(i); if (i == 1) { m4 = Matrix4.translationValues(size.width - cSize.width, 0, 0.0); } else if (i == 2) { m4 = Matrix4.translationValues(0, size.height - cSize.height, 0.0); } else if (i == 3) { m4 = Matrix4.translationValues( size.width - cSize.width, size.height - cSize.height, 0.0); } context.paintChild(i, transform: m4); } } @override bool shouldRepaint(FlowDelegate oldDelegate) { return true; } }
3. 圓形的Flow布局
其實可以看出,F(xiàn)low的核心就是根據(jù)信息來計算位置
所以,所有的布局都可以通過Flow進(jìn)行實現(xiàn)。
除此之外對應(yīng)一些特定情況的布局,使用Flow會非常簡單,比如:
class CircleFlow extends StatelessWidget { final List<Widget> children; CircleFlow({@required this.children}); @override Widget build(BuildContext context) { return Flow( delegate: _CircleFlowDelegate(), children: children, ); } } class _CircleFlowDelegate extends FlowDelegate { @override //繪制孩子的方法 void paintChildren(FlowPaintingContext context) { double radius = context.size.shortestSide / 2; var count = context.childCount; var perRad = 2 * pi / count; for (int i = 0; i < count; i++) { print(i); var cSizeX = context.getChildSize(i).width / 2; var cSizeY = context.getChildSize(i).height / 2; var offsetX = (radius - cSizeX) * cos(i * perRad) + radius; var offsetY = (radius - cSizeY) * sin(i * perRad) + radius; context.paintChild(i, transform: Matrix4.translationValues( offsetX - cSizeX, offsetY - cSizeY, 0.0)); } } @override bool shouldRepaint(FlowDelegate oldDelegate) { return true; } }
第三幕、當(dāng)Flow遇到Animation
全面說Flow最重要的就是進(jìn)行定位,而動畫的本質(zhì)是若干個變動的數(shù)字
那么兩者自然是郎才女貌,情投意合
1.圓形布局 + 旋轉(zhuǎn)
前面圓形布局靠的是計算某個組件偏轉(zhuǎn)的角度
那么想要實現(xiàn)旋轉(zhuǎn)是非常簡單的,由于有角度的狀態(tài),所以StatefulWidget
class CircleFlow extends StatefulWidget { final List<Widget> children; CircleFlow({@required this.children}); @override _CircleFlowState createState() => _CircleFlowState(); } class _CircleFlowState extends State<CircleFlow> with SingleTickerProviderStateMixin { AnimationController _controller; double rad = 0.0; @override void initState() { _controller = AnimationController(duration: Duration(milliseconds: 3000), vsync: this) ..addListener(() => setState(() => rad = _controller.value*pi*2)); _controller.forward(); super.initState(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Flow( delegate: _CircleFlowDelegate(rad), children: widget.children, ); } }
在構(gòu)造_CircleFlowDelegate時傳入角度,在offsetX、offsetY 時加上角度就行了
class _CircleFlowDelegate extends FlowDelegate { final double rad; _CircleFlowDelegate(this.rad); @override //繪制孩子的方法 void paintChildren(FlowPaintingContext context) { double radius = context.size.shortestSide / 2; var count = context.childCount; var perRad = 2 * pi / count ; for (int i = 0; i < count; i++) { print(i); var cSizeX = context.getChildSize(i).width / 2; var cSizeY = context.getChildSize(i).height / 2; var offsetX = (radius - cSizeX) * cos(i * perRad+ rad) + radius; var offsetY = (radius - cSizeY) * sin(i * perRad+ rad) + radius; context.paintChild(i, transform: Matrix4.translationValues( offsetX - cSizeX, offsetY - cSizeY, 0.0)); } } @override bool shouldRepaint(FlowDelegate oldDelegate) { return true; } }
2.圓形布局 + 偏移
能實現(xiàn)出來我還是蠻激動的。定義了menu為中間的組件
children為周圍的組件,點擊中間組件,執(zhí)行動畫,
在進(jìn)行定位時,讓offsetX和offsetY乘以分率后加半徑,這樣就會向中心靠攏,
反之?dāng)U散,我取名為BurstFlow,意為綻放
class BurstFlow extends StatefulWidget { final List<Widget> children; final Widget menu; BurstFlow({@required this.children, @required this.menu}); @override _BurstFlowState createState() => _BurstFlowState(); } class _BurstFlowState extends State<BurstFlow> with SingleTickerProviderStateMixin { AnimationController _controller; double _rad = 0.0; bool _closed = true; @override void initState() { _controller = AnimationController(duration: Duration(milliseconds: 1000), vsync: this) ..addListener(() => setState(() => _rad = (_closed ? (_controller.value) :1- _controller.value))) ..addStatusListener((status) { if (status == AnimationStatus.completed) { _closed = !_closed; } }); super.initState(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Flow( delegate: _CircleFlowDelegate(_rad), children: [ ...widget.children, InkWell( onTap: () { _controller.reset(); _controller.forward(); }, child: widget.menu) ], ); } } class _CircleFlowDelegate extends FlowDelegate { final double rad; _CircleFlowDelegate(this.rad); @override //繪制孩子的方法 void paintChildren(FlowPaintingContext context) { double radius = context.size.shortestSide / 2; var count = context.childCount - 1; var perRad = 2 * pi / count; for (int i = 0; i < count; i++) { print(i); var cSizeX = context.getChildSize(i).width / 2; var cSizeY = context.getChildSize(i).height / 2; var offsetX = rad * (radius - cSizeX) * cos(i * perRad) + radius; var offsetY = rad * (radius - cSizeY) * sin(i * perRad) + radius; context.paintChild(i, transform: Matrix4.translationValues( offsetX - cSizeX, offsetY - cSizeY, 0.0)); } context.paintChild(context.childCount - 1, transform: Matrix4.translationValues( radius - context.getChildSize(context.childCount - 1).width / 2, radius - context.getChildSize(context.childCount - 1).height / 2, 0.0)); } @override bool shouldRepaint(FlowDelegate oldDelegate) { return true; } }
另外可以對周圍的組件排布進(jìn)行設(shè)計,可以是半圓弧收方放、
四分之一圓弧收方、甚至是指定角度弧排列
周圍的組件也可以進(jìn)行透明度的漸變,這些都是可以優(yōu)化的點
這里就不再說了,跟你們一些空間,各位可以自行優(yōu)化。
布局重在定位,而Flow是定位之王,我的位置我做主。好了,這篇就到這里吧,更多關(guān)于Flutter Flow位置自定義的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android中使用TabHost 與 Fragment 制作頁面切換效果
這篇文章主要介紹了Android中使用TabHost 與 Fragment 制作頁面切換效果的相關(guān)資料,需要的朋友可以參考下2016-03-03Android中ViewFlipper的使用及設(shè)置動畫效果實例詳解
這篇文章主要介紹了Android中ViewFlipper的使用及設(shè)置動畫效果的方法,以實例形式較為詳細(xì)的分析了ViewFlipper的功能、原理及設(shè)置與使用技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-10-10Android開發(fā)之搜索框SearchView用法示例
這篇文章主要介紹了Android開發(fā)之搜索框SearchView用法,結(jié)合實例形式分析了Android搜索框SearchView的基本功能、用法及相關(guān)操作注意事項,需要的朋友可以參考下2019-03-03android實現(xiàn)session保持簡要概述及實現(xiàn)
其實sesion在瀏覽器和web服務(wù)器直接是通過一個叫做name為sessionid的cookie來傳遞的,所以只要在每次數(shù)據(jù)請求時保持sessionid是同一個不變就可以用到web的session了,感興趣的你可以參考下本文或許對你有所幫助2013-03-03Android 創(chuàng)建/驗證/刪除桌面快捷方式(已測試可用)
桌面快捷方式的出現(xiàn)方便了用戶操作,在某些程度上提高了用戶體驗,接下來將介紹下Android創(chuàng)建/驗證/刪除桌面快捷方式的實現(xiàn)思路及代碼,感興趣的朋友可以了解下,或許本文可以幫助到你2013-02-02