Flutter高級(jí)玩法Flow位置自定義
前言
Flow布局是一個(gè)超級(jí)強(qiáng)大的布局,但應(yīng)該很少有人用,因?yàn)槿胧值拈T檻還是有的
Flow的屬性很簡單,只有FlowDelegate類型的delegate和組件列表children,
可能很多人看到delegate就揮揮手:臣妾做不到,今天就來掰扯一下這個(gè)FlowDelegate.
class Flow extends MultiChildRenderObjectWidget {
Flow({
Key key,
@required this.delegate,
List<Widget> children = const <Widget>[],
}) : assert(delegate != null),


第一幕、開場(chǎng)-演員入臺(tái)
1. 展示舞臺(tái)
我們的第一個(gè)舞臺(tái)是一個(gè)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出場(chǎng)
FlowDemo中使用Flow組件,包含四個(gè)box
四個(gè)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出場(chǎng)
Flow布局需要一個(gè)FlowDelegate類型的delegate對(duì)象
但是Flutter中并沒有其實(shí)現(xiàn)類,所以想玩Flow,只有一條路:自定義
class _Delegate extends FlowDelegate {
@override
void paintChildren(FlowPaintingContext context) {
}
@override
bool shouldRepaint(FlowDelegate oldDelegate) {
return true;
}
}
4. paintChildren方法和FlowPaintingContext對(duì)象
paintChildren顧名思義是用來畫孩子的
FlowPaintingContext也就是繪制的上下文,即繪制的信息
那就輕輕的瞄一眼FlowPaintingContext里面有啥吧:
一共有四個(gè)東西: size、childCount、getChildSize、paintChild
---->[源碼:flutter/lib/src/rendering/flow.dart:23]----
abstract class FlowPaintingContext {
Size get size;//父親尺寸
int get childCount;//孩子個(gè)數(shù)
Size getChildSize(int i);//第i個(gè)孩子尺寸
//繪制孩子
void paintChild(int i, { Matrix4 transform, double opacity = 1.0 });
}
接下來用代碼測(cè)試一下這幾個(gè)屬性看看,不出所料
默認(rèn)是繪制在父容器的左上角。
class _Delegate extends FlowDelegate {
@override
void paintChildren(FlowPaintingContext context) {
print("父容器尺寸:${context.size}");
print("孩子個(gè)數(shù):${context.childCount}");
for(int i=0;i<context.childCount;i++){
print("第$i個(gè)孩子尺寸:${context.getChildSize(i)}");
}
}


第二幕、排兵布陣
前面只是將組件排在了左上角,那如何對(duì)進(jìn)行其他排布呢?
1. paintChild與Matrix4
在paintChild時(shí)可以傳入transform的Matrix4對(duì)象進(jìn)行變換
在這里基本上只用了Matrix4的平移translationValues功能,至于Matrix4的具體用法,那又是一個(gè)故事了 這里讓黃色的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)在讓四個(gè)組件排布在父親的四角,如下:

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布局的封裝
如果需要一個(gè)排布四角的組件,可以基于上面的Delegate做一個(gè)組件
雖然用處很有限,但原來了解一下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布局
其實(shí)可以看出,F(xiàn)low的核心就是根據(jù)信息來計(jì)算位置
所以,所有的布局都可以通過Flow進(jìn)行實(shí)現(xiàn)。
除此之外對(duì)應(yīng)一些特定情況的布局,使用Flow會(huì)非常簡單,比如:

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)行定位,而動(dòng)畫的本質(zhì)是若干個(gè)變動(dòng)的數(shù)字
那么兩者自然是郎才女貌,情投意合
1.圓形布局 + 旋轉(zhuǎn)
前面圓形布局靠的是計(jì)算某個(gè)組件偏轉(zhuǎn)的角度
那么想要實(shí)現(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時(shí)傳入角度,在offsetX、offsetY 時(shí)加上角度就行了
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.圓形布局 + 偏移
能實(shí)現(xiàn)出來我還是蠻激動(dòng)的。定義了menu為中間的組件
children為周圍的組件,點(diǎn)擊中間組件,執(zhí)行動(dòng)畫,
在進(jìn)行定位時(shí),讓offsetX和offsetY乘以分率后加半徑,這樣就會(huì)向中心靠攏,
反之?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;
}
}
另外可以對(duì)周圍的組件排布進(jìn)行設(shè)計(jì),可以是半圓弧收方放、
四分之一圓弧收方、甚至是指定角度弧排列
周圍的組件也可以進(jìn)行透明度的漸變,這些都是可以優(yōu)化的點(diǎn)
這里就不再說了,跟你們一些空間,各位可以自行優(yōu)化。
布局重在定位,而Flow是定位之王,我的位置我做主。好了,這篇就到這里吧,更多關(guān)于Flutter Flow位置自定義的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android創(chuàng)建淡入淡出動(dòng)畫的詳解
大家好,本篇文章主要講的是Android創(chuàng)建淡入淡出動(dòng)畫的詳解,感興趣的同學(xué)趕快來看一看吧,對(duì)你有幫助的話記得收藏一下,方便下次瀏覽2021-12-12
Android中使用TabHost 與 Fragment 制作頁面切換效果
這篇文章主要介紹了Android中使用TabHost 與 Fragment 制作頁面切換效果的相關(guān)資料,需要的朋友可以參考下2016-03-03
Android中ViewFlipper的使用及設(shè)置動(dòng)畫效果實(shí)例詳解
這篇文章主要介紹了Android中ViewFlipper的使用及設(shè)置動(dòng)畫效果的方法,以實(shí)例形式較為詳細(xì)的分析了ViewFlipper的功能、原理及設(shè)置與使用技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-10-10
Android開發(fā)之搜索框SearchView用法示例
這篇文章主要介紹了Android開發(fā)之搜索框SearchView用法,結(jié)合實(shí)例形式分析了Android搜索框SearchView的基本功能、用法及相關(guān)操作注意事項(xiàng),需要的朋友可以參考下2019-03-03
Android高級(jí)開發(fā)之性能優(yōu)化典范
本文從電量,視圖,內(nèi)存三個(gè)性能方面的知識(shí)點(diǎn)給大家介紹android高級(jí)開發(fā)之性能優(yōu)化的相關(guān)知識(shí),希望對(duì)大家有所幫助2016-05-05
Flutter實(shí)現(xiàn)視頻壓縮功能的示例代碼
移動(dòng)應(yīng)用程序中,視頻占用了大量的存儲(chǔ)空間和帶寬,這在一定程度上影響了應(yīng)用程序的性能和用戶體驗(yàn),所以本文為大家準(zhǔn)備了Flutter實(shí)現(xiàn)視頻壓縮的方法,需要的可以參考一下2023-06-06
android實(shí)現(xiàn)session保持簡要概述及實(shí)現(xiàn)
其實(shí)sesion在瀏覽器和web服務(wù)器直接是通過一個(gè)叫做name為sessionid的cookie來傳遞的,所以只要在每次數(shù)據(jù)請(qǐng)求時(shí)保持sessionid是同一個(gè)不變就可以用到web的session了,感興趣的你可以參考下本文或許對(duì)你有所幫助2013-03-03
Android 創(chuàng)建/驗(yàn)證/刪除桌面快捷方式(已測(cè)試可用)
桌面快捷方式的出現(xiàn)方便了用戶操作,在某些程度上提高了用戶體驗(yàn),接下來將介紹下Android創(chuàng)建/驗(yàn)證/刪除桌面快捷方式的實(shí)現(xiàn)思路及代碼,感興趣的朋友可以了解下,或許本文可以幫助到你2013-02-02

