Flutter第5天--布局实例+操作交互

今天调料十足,保证新鲜美味----2018-12-20

1:写在前面:

每个布局的实现方案都有很多,我只是选择自己认为较好的布局方案
对于非常复杂的布局,建议先打草稿,再进行颜色块模拟,最后再写控件
有留白的地方Expanded+flex(以下我所说的flex就是Row+Column的总成)会有很好的适应性

2.选几张图镇楼:

-

一、入门级布局1:

1.出题


2.思路

很容易看出,三个块水平排列,两端靠边,Row逃不掉了,中间很容易想到Expanded
这样中间的部分自动尺寸,而且留白很多,基本上不会造成溢出,对不同屏幕适应性更好
三个部件写完后,用个Container套一下给内边距就行了(边距的多少,就不纠结了,演示而已)


3.解题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var rowLine = Row(
children: <Widget>[
Icon(
Icons.extension,
color: Colors.blue,
),
Expanded(
child: Padding(
padding: EdgeInsets.only(left: 20),
child: Text(
"好友微视",
style: TextStyle(fontSize: 18),
),
)),
Icon(Icons.arrow_forward)
],
);

var test1 = Container(color: Colors.white, padding: EdgeInsets.all(15), child: rowLine);

二、入门级布局2:

[番外]:小封装1—添加测试背景色

实在要吐槽:想加个背景色想加一下麻烦死了…我是在受不了,封装一下方法

1
2
3
4
5
6
7
8
9
10
11
bg(Widget w, [Color color]) {
return Container(color: color ?? randomARGB(), child: w);
}
Color randomARGB(){
Random random = new Random();
int r = 30 + random.nextInt(200);
int g = 30 + random.nextInt(200);
int b = 30 + random.nextInt(200);
int a = 50 + random.nextInt(200);
return Color.fromARGB(a, r, g, b);
}

1.出题


2.思路

有了上面的指引,相信下面的应该难不倒你:
三个Row,中间用Column,模式基本同上,达到这步应该很简单

这里暂停一下,为了说明flex布局的轴,对于Column而言,主轴是纵向
交错轴横向,默认交错轴是center,所以呈现了上面的效果,我们只需要轻轻地:
crossAxisAlignment: CrossAxisAlignment.start,就完成雏形了,剩下的小修小补一下


3.解题

写文字的style真心烦,抽取一下吧

1
2
3
4
//正常文字
var commonStyle = TextStyle(color: Colors.black, fontSize: 18);
//灰色较小文字
var infoStyle = TextStyle(color: Color(0xff999999), fontSize: 13);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//左边头像
var headImg = Image.asset(
"images/icon_gql.jpg", width: 45, height: 45,
);

//中间的信息
var center2 = Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text( "心如止水", style: commonStyle,),
Text( "《应龙》--张风捷特烈 一游小池两岁月,洗却凡世几闲尘。时逢雷霆风会雨,应乘扶摇化入云。",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: infoStyle,
textAlign: TextAlign.start,
)
],
);

//尾部的时间+图标
var end2 = Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("06:45",style: infoStyle),
Icon(Icons.visibility_off,size: 20,color: Color(0xff999999),
)
],
);

//整行的内容
var rowLine2 = Row(
children: <Widget>[
Padding(child: headImg, padding: EdgeInsets.all(5)),
Expanded(child: Padding(padding: EdgeInsets.all(5), child: center2)),
end2
],
);

//包裹一下,收工
var test2 = Container(
height: 70,
color: Colors.white,
padding: EdgeInsets.all(5),
child: rowLine2);

三、新级级别布局1

[番外]:小封装2

好吧,我又要了:感觉加个padding也是一堆废话,封装一下吧

1
2
3
4
5
6
7
8
9
padding只要函数包一下就好:--看起来要比以前那一坨好多了
pd(Text("创世神"), l: 5)//只加左边距
pda(Text("创世神"),5)//全加边距

//以前全加加Pading:-----------------
Padding(
child: headImg3,
padding: EdgeInsets.all(5),
),
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pd(Widget w, {double l, double t, double r, double b}) {
return Padding(
child: w,
padding: EdgeInsets.fromLTRB(l ?? 0, t ?? 0, r ?? 0, b ?? 0),
);
}

//全部padding
pda(Widget w, double a) {
return Padding(
child: w,
padding: EdgeInsets.all(a),
);
}

//水平、竖直的两个padding
pdhv(Widget w, {double h, double v}) {
return Padding(
child: w,
padding: EdgeInsets.fromLTRB(h ?? 0, v ?? 0, h ?? 0, v ?? 0),
);
}

1.出题:(来玩掘金吧~)

这是网页掘金的主页栏,是我喜欢的风格,现在flutter上走一波


2.分析

有了前两个的经验,这种样式应该难不倒你,区块划分如下:
也许有新手不知道从哪入手,那就画个Container,填个色,这是从0到1质变,然后就是+1的量变了
我比较喜欢卡片。所以这个用Card包一下吧,三块一目了然


3.解题

也许你不知道一个布局有多大,你可以用上面的bg函数包裹一下,如下:

背景有助于你的排布,最后当然要把背景去掉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
//较大文字
var bigStyle = TextStyle(color: Colors.black, fontSize: 20, fontWeight: FontWeight.bold);
//btn文字
var btnStyle = TextStyle(color: Color(0xffffffff), fontSize: 13);

////////////////////////-----------------测试3--------------------------------
//左边头像
var headImg3 = Image.asset("images/icon_90.png", width: 50, height: 50,);

//中间的信息
var center3 = Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("张风捷特烈",style: bigStyle),
Row(children: <Widget>[
Icon(Icons.next_week, size: 15),
pd(Text("创世神 | 无"), l: 5)
],
),
Row(children: <Widget>[
Icon(Icons.keyboard, size: 15),
pd(Text("海的彼岸有我未曾见证的风采"), l: 5)
],
),
],
);

//尾部的
var end3 = Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Row(children: <Widget>[
Icon(Icons.language,size: 15,),
Icon(Icons.local_pharmacy, size: 15),
Icon(Icons.person_pin_circle, size: 15)
],
),
bg(pdhv(
Text("编辑",style: btnStyle,), h: 10, v: 3), Colors.blueAccent),
],
);

var rowLine3 = Row(
children: <Widget>[
pda(headImg3, 5),
Expanded(child: pda(center3,5)),
pda(end3, 10),
],
);

var test3 = Card(
child: Container(
height: 95,
color: Colors.white,
padding: EdgeInsets.all(5),
child: rowLine3));

四、新手级别布局(2)

1.出题:还拿掘金来玩吧

这个稍微复杂了一丢丢


2.分析:还是先打块:

分块的方式有很多,你喜欢怎么打就这么打,你可以看出行,也可以看成列
外部是个Column,头,身,尾。身是一个Row,文字两行是Column,头,尾都是Row


3.解题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
////////////////////////-----------------测试4--------------------------------
var line1_4 = Row(
children: <Widget>[
Image.asset("images/icon_90.png", width: 20, height: 20),
Expanded( child: pd(Text("张风捷特烈"), l: 5),),
Text("Flutter/Dart", style: infoStyle,)
],
);

var center_right = Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text("Flutter第4天--基础控件(下)+Flex布局详解", style: littelStyle, maxLines: 2,),
pd(Text(
"1.2:优雅地查看:图片的适应模式--BoxFit1.3:优雅地查看:颜色混合模式--colorBlendMode",
style: infoStyle, maxLines: 2,overflow: TextOverflow.ellipsis),t:5),
],
);

//中间的信息
var center4 = Row(
children: <Widget>[Expanded(child: pda(center_right, 5)),
Image.asset("images/wy_300x200.jpg", width: 80,height: 80,fit: BoxFit.fitHeight)
],
);

var end4 = Row(
children: <Widget>[
Icon(Icons.grade,color: Colors.green,size: 20,),
Text("1000W",style: infoStyle,),
pd(Icon(Icons.tag_faces,color:Colors.lightBlueAccent, size: 20),l:15,r:5),
Text("2000W",style: infoStyle),
],
);

var item4 = Column(children: <Widget>[line1_4, Expanded(child: center4), end4]);

var test4 = Card(
child: Container(
height: 160,
color: Colors.white,
padding: EdgeInsets.all(10),
child: item4));

经过这四个,可以看出,大块是小块组合的,一点点拼总能拼出来,
所以遇到复杂界面不要怕,一点一点分块,最后一点一点拼合,就能搞定
几个小例子就这样吧,好好消化一下


五:ListView的测试

条目有了,此时不测试ListView更待何时?
当然现在还只是静态的,你可以将需要的字段抽取出来封装成函数
然后再动态获取数据填充视图(打算放在最后一天说,这里用静态页面测试)


1.ListView.builder
条目2 条目4
1
2
3
4
5
6
7
8
//条目2
var test5 = ListView.builder(
itemCount: 30,
itemBuilder: (BuildContext context, int index) {
return
Column(children: <Widget>[test2,Divider(height:1)],);
},
);

2.ListView.separated

这个多一个separatorBuilder,类型和itemBuilder一毛一样
也就是在某些位置,插入东西分割(常用的是分割线),看下图:
我在index=1的条目下面插入了test2条目(左图),变相的多条目…,
当然你可以随意控制怎么玩,比如每隔两个插入一个(右图),注意:插入的条目不算总数里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//在index=1下插入
var test6 = ListView.separated(
itemBuilder: (ctx, i) {
return Column(
children: <Widget>[test4],
);
},
separatorBuilder: (ctx, i) {
return Column(children: <Widget>[i==1?test2:Container()],
);
},
itemCount: 40);

//每隔两个插入
var test6 = ListView.separated(
itemBuilder: (ctx, i) {
return Column(
children: <Widget>[test4],
);
},
separatorBuilder: (ctx, i) {
return Column(
children: <Widget>[(i+1 ) % 2== 0 ? test2 : Container()],
);
},
itemCount: 40);

六、操作交互:

Bit世界的三大要素:数据(m),界面(v),交互(c或p),
一个项目讲白了,就是围绕这三个转,说谁更重要的都是废话
没有数据的是空壳标本,没有交互的是植物人,没有界面的那时白日做梦...
Flutter的交互感觉好奇葩…也许是一切节Widget的思想驱使吧,还是包一下

1.先天交互天赋的控件
1
2
Switch Slider Checkbox TextField SnackBar BottomNavigationBar
OutlineButton FlatButton RaisedButton IconButton FloatingActionButton 等...

2.没有先天天赋怎么办?—GestureDetector给你光环加持

看一下源码:好吧,挺多的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
GestureDetector({
Key key,
this.child,

this.onTap,----点击----Function()---
this.onTapDown,----按下:Function(TapDownDetails details)---
this.onTapUp,---- 抬起:Function(TapUpDetails details)----
this.onTapCancel,----取消(onTap无法触发时):Function()----

this.onDoubleTap,----双击----void Function()----
this.onLongPress,----长按----void Function()----
this.onLongPressUp,----长按松开----void Function()----

this.onVerticalDragDown,----竖直拖动按下----Function(DragDownDetails details)----
this.onVerticalDragStart,----竖直拖动开始----Function(DragStartDetails details)----
this.onVerticalDragUpdate,----竖直拖动更新----Function(DragUpdateDetails details)----
this.onVerticalDragEnd,----竖直拖动结束----Function(DragEndDetails details)----
this.onVerticalDragCancel,----竖直拖动取消----Function()----

this.onHorizontalDragDown,
this.onHorizontalDragStart,
this.onHorizontalDragUpdate,
this.onHorizontalDragEnd,
this.onHorizontalDragCancel,

this.onPanDown,
this.onPanStart,
this.onPanUpdate,
this.onPanEnd,
this.onPanCancel,

this.onScaleStart,
this.onScaleUpdate,
this.onScaleEnd,
this.behavior,
this.excludeFromSemantics = false

3.测试1:四大战将
3.1.源码追踪:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
this.onTap,----点击----Function()---
this.onTapDown,----按下:Function(TapDownDetails details)---
this.onTapUp,---- 抬起:Function(TapUpDetails details)
this.onTapCancel,----取消(onTap无法触发时):Function()----

---->[源码追踪:onTapDown]
final GestureTapDownCallback onTapDown;

---->[源码追踪:GestureTapDownCallback]
typedef GestureTapDownCallback = void Function(TapDownDetails details);

---->[源码追踪:TapDownDetails]
class TapDownDetails {
/// Creates details for a [GestureTapDownCallback].
///
/// The [globalPosition] argument must not be null.
TapDownDetails({ this.globalPosition = Offset.zero })
: assert(globalPosition != null);

---->[源码追踪:Offset]
class Offset extends OffsetBase {
/// Creates an offset. The first argument sets [dx], the horizontal component,
/// and the second sets [dy], the vertical component.
const Offset(double dx, double dy) : super(dx, dy);

//好吧,搞了半天就是落点嘛...

3.2测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var box = Container(
width: 100,
height: 100,
color: Colors.lightBlueAccent,
);

var ctrl_test = GestureDetector(
child: box,
onTap: () {
print("onTap");
},
onTapDown: (d) {
print("onPanDown" + d.globalPosition.toString());
},
onTapUp: (d) {
print("onTapUp" + d.globalPosition.toString());
},
onTapCancel: () {
print("onTapUp");
},
);
1
2
3
4
5
6
7
点了一下,控制台输出:
I/flutter (27114): onPanDownOffset(205.5, 384.5)
I/flutter (27114): onTapUpOffset(205.5, 384.5)
I/flutter (27114): onTap

可见坐标是相对于屏幕顶点的
onTapCancel

4.测试2:三大小白

顾名思义…不多说

1
2
3
this.onDoubleTap,----双击----void Function()----
this.onLongPress,----长按----void Function()----
this.onLongPressUp,----长按松开----void Function()----
1
2
3
4
5
6
7
8
9
10
11
var ctrl_test2 = GestureDetector(
child: box,
onDoubleTap: () {
print("onDoubleTap");
},
onLongPress: () {
print("onLongPress");
},
onLongPressUp: () {
print("onLongPressUp");
});

5.测试3:战场双龙(只给一条,另一条类比)
1
2
3
4
5
this.onVerticalDragDown,----竖直拖动按下----Function(DragDownDetails details)----
this.onVerticalDragStart,----竖直拖动开始----Function(DragStartDetails details)----
this.onVerticalDragUpdate,----竖直拖动更新----Function(DragUpdateDetails details)----
this.onVerticalDragEnd,----竖直拖动结束----Function(DragEndDetails details)----
this.onVerticalDragCancel,----竖直拖动取消----Function()----
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var ctrl_test3 = GestureDetector(
child: box,
onVerticalDragDown: (d) {
print("onVerticalDragDown---" + d.globalPosition.toString());
},
onVerticalDragStart: (d) {
print("onVerticalDragStart---" + d.globalPosition.toString());
},
onVerticalDragUpdate: (d) {
print("onVerticalDragUpdate---" + d.globalPosition.toString());
},

onVerticalDragCancel: () {
print("onVerticalDragCancel---");
});
1
2
3
4
5
6
I/flutter ( 4994): onVerticalDragDown---Offset(182.5, 384.8)
I/flutter ( 4994): onVerticalDragStart---Offset(182.5, 384.8)
I/flutter ( 4994): onVerticalDragUpdate---Offset(182.5, 390.2)
I/flutter ( 4994): onVerticalDragUpdate---Offset(181.8, 402.2)
I/flutter ( 4994): onVerticalDragUpdate---Offset(180.8, 420.5)
I/flutter ( 4994): onVerticalDragUpdate---Offset(181.2, 443.5)

七、交互操作小案例

1:点击生成小球

canvas画出的CustomPaint大小神奇般的是0,导致GestureDetector不起作用
没办法,只能曲线救国,GestureDetector包住全部,在减去偏移量
小球的绘制就不分析了,就是收集球,再画出来,如果第二天的文章会了,这都是小菜

1.1小球数据承载类:
1
2
3
4
5
6
class Draw {
double x;
double y;
Color color;
Draw(this.x, this.y, this.color);
}

1.2:准备Canvas绘板

drawGrid绘制网格见第二篇(其实没有也无所谓,我比较喜欢)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//Canvas绘版
class CanvasView extends CustomPainter {
BuildContext context;
Paint mPaint;
CanvasView(this.context) {
mPaint = new Paint();
}

@override
void paint(Canvas canvas, Size size) {
balls.forEach((ball) {
drawBall(canvas, ball);
});
var winSize = MediaQuery.of(context).size;
drawGrid(canvas, winSize);
}

@override
bool shouldRepaint(CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
return true;
}

//绘制小球
void drawBall(Canvas canvas, Draw ball) {
mPaint.color = ball.color;
canvas.drawCircle(Offset(ball.x, ball.y), 10, mPaint);
}
}

1.3.数据的变动与渲染(交互)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
var balls = []; //小球合集
class CanvasPage extends StatefulWidget {
CanvasPage({Key key, this.title}) : super(key: key);

final String title;

@override
_CanvasPageState createState() => _CanvasPageState();
}

class _CanvasPageState extends State<CanvasPage> {
@override
Widget build(BuildContext context) {
var appBar = AppBar(
title: Text("张风捷特烈"),
);
var barTopHeight = MediaQueryData.fromWindow(window).padding.top;
print(barTopHeight);

var scf = Scaffold(
appBar: appBar,
body: CustomPaint(
painter: CanvasView(context),
));

return GestureDetector(
child: scf,
onTapDown: (d) {
var pos = d.globalPosition;
balls.add(new Draw(pos.dx,
pos.dy - appBar.preferredSize.height - barTopHeight, randomRGB()));
print(balls.length);
setState(() {});
},
);
}
}

2.onPanUpdate测试

实现起来还是很简单的,onPanUpdate的时候加点就行了

1
2
3
4
onPanUpdate: (d) {
var pos = d.globalPosition;
balls.add(new Draw(pos.dx,
pos.dy - appBar.preferredSize.height - barTopHeight, randomARGB()));

3.画线

好吧,这个比较搓,不过测试了onPanDownonPanUpdateonPanEnd
Flutter的canvas用的怪怪的,无法记录前次的绘制,要实现自由绘制,看来只能拼点了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//Canvas绘版
class CanvasView extends CustomPainter {
BuildContext context;
Paint mPaint;
double _downX;
double _downY;
double _upX;
double _upY;
CanvasView(this.context, this._downX, this._downY, this._upX, this._upY) {
mPaint = new Paint()
..strokeWidth = 10
..strokeCap = StrokeCap.round;
}
@override
void paint(Canvas canvas, Size size) {
var winSize = MediaQuery.of(context).size;
drawGrid(canvas, winSize);
print("_downX:$_downX,_downY:$_downY");
canvas.drawLine(Offset(_downX, _downY), Offset(_upX, _upY), mPaint);
}

@override
bool shouldRepaint(CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
return true;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class CanvasPage extends StatefulWidget {
CanvasPage({Key key, this.title}) : super(key: key);

final String title;

@override
_CanvasPageState createState() => _CanvasPageState();
}

class _CanvasPageState extends State<CanvasPage> {
var _downX;
var _downY;
var _upX;
var _upY;

@override
Widget build(BuildContext context) {
var appBar = AppBar(
title: Text("张风捷特烈"),
);
var barTopHeight = MediaQueryData.fromWindow(window).padding.top;

var scf = Scaffold(
appBar: appBar,
body: CustomPaint(
painter: CanvasView(context, _downX, _downY, _upX, _upY),
));

return GestureDetector(
child: scf,
onPanDown: (d) {
_downX = d.globalPosition.dx;
_downY =
d.globalPosition.dy - appBar.preferredSize.height - barTopHeight;
},
onPanUpdate: (d) {
_upX = d.globalPosition.dx;
_upY = d.globalPosition.dy - appBar.preferredSize.height - barTopHeight;
setState(() {});
},
onPanEnd: (d) {
_downX = -10.0;
_downY = -10.0;
_upX = -10.0;
_upY = -10.0;
setState(() {});
},
);
}
}

八、关于跳转

跳转方式1:加routes
1
2
3
4
5
6
7
8
9
10
11
12
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.lightBlue,
),
home: new CanvasPage(),
routes: <String, WidgetBuilder> {
'/clock': (BuildContext context) => ClockPage(),
},);

//跳转方法:
Navigator.of(context).pushNamed('/clock');

跳转方式2:直接开控件
1
Navigator.push(context,MaterialPageRoute(builder: (bu) => ClockPage()));

关闭方式:
1
Navigator.pop(context);
要说flutter的方便之处,那就是布局是对象,这有多爽:
1
2
3
4
5
1.Android时候写xml,如果一个布局文件你想要其中的一部分,这就尴尬了:  
cv一下,删删改改,有时id有联系就更尴尬了。
2.虽然安卓的xml相比于Java代码布局的简洁性,复用性高很多,但仍有局限性。
3.而flutter布局是对象,你可以用变量来记录它,随用随取。
4.Flutter的flex布局让布局的适应性变得很强,虽然Android的约束布局也可以,但略显繁杂

好了,今天就到这里

张风捷特烈 wechat
-------------本文结束感谢您的阅读 @张风捷特烈-------------
0%