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

6,321 阅读11分钟

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

1:写在前面:

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

2.选几张图镇楼:

新手级2-ok.png

---

一、入门级布局1:

1.出题

测试1.png


2.思路

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

测试1.png


3.解题

测试1-ok.png

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---添加测试背景色

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

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.出题

微信条.png


2.思路

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

分析1.png

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

分析1.png


3.解题

测试2-ok.png

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

//正常文字
var commonStyle = TextStyle(color: Colors.black, fontSize: 18);
//灰色较小文字
var infoStyle = TextStyle(color: Color(0xff999999), fontSize: 13);
//左边头像
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也是一堆废话,封装一下吧

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

//以前全加加Pading:-----------------
Padding(
  child: headImg3,
  padding: EdgeInsets.all(5),
),

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上走一波

新手级1.png


2.分析

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

新手级1.png


3.解题

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

新手级1-阶段.png

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

新手级1-ok.png

//较大文字
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.png


2.分析:还是先打块:

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

新手级2.png


3.解题

新手级2-ok.png

////////////////////////-----------------测试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
//条目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条目(左图),变相的多条目...,
当然你可以随意控制怎么玩,比如每隔两个插入一个(右图),注意:插入的条目不算总数里

separated.png

//在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.先天交互天赋的控件
Switch Slider Checkbox TextField SnackBar BottomNavigationBar
OutlineButton FlatButton RaisedButton IconButton FloatingActionButton 等...

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

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

GestureDetector.png

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.源码追踪:
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测试代码
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");
  },
);
点了一下,控制台输出:
I/flutter (27114): onPanDownOffset(205.5, 384.5)
I/flutter (27114): onTapUpOffset(205.5, 384.5)
I/flutter (27114): onTap

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

4.测试2:三大小白

顾名思义...不多说

this.onDoubleTap,----双击----void Function()----
this.onLongPress,----长按----void Function()----
this.onLongPressUp,----长按松开----void Function()----
var ctrl_test2 = GestureDetector(
    child: box,
    onDoubleTap: () {
      print("onDoubleTap");
    },
    onLongPress: () {
      print("onLongPress");
    },
    onLongPressUp: () {
      print("onLongPressUp");
    });

5.测试3:战场双龙(只给一条,另一条类比)
 this.onVerticalDragDown,----竖直拖动按下----Function(DragDownDetails details)----
 this.onVerticalDragStart,----竖直拖动开始----Function(DragStartDetails details)----
  this.onVerticalDragUpdate,----竖直拖动更新----Function(DragUpdateDetails details)----
  this.onVerticalDragEnd,----竖直拖动结束----Function(DragEndDetails details)----
  this.onVerticalDragCancel,----竖直拖动取消----Function()----
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---");
    });
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小球数据承载类:
class Draw {
  double x;
  double y;
  Color color;
  Draw(this.x, this.y, this.color);
}

1.2:准备Canvas绘板

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

生成球.gif

//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.数据的变动与渲染(交互)
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的时候加点就行了

绘图.gif

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

3.画线

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

画线.gif

//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;
  }
}
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(() {});
      },
    );
  }
}

八、关于跳转

页面跳转与关闭.gif

跳转方式1:加routes
    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:直接开控件
Navigator.push(context,MaterialPageRoute(builder: (bu) => ClockPage()));

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

好了,今天就到这里


后记:捷文规范

1.本文成长记录及勘误表
项目源码日期备注
V0.1-github2018-12-20Flutter第5天--布局实例+操作交互
2.更多关于我
笔名QQ微信爱好
张风捷特烈1981462002zdl1994328语言
我的github我的简书我的掘金个人网站
3.声明

1----本文由张风捷特烈原创,转载请注明
2----欢迎广大编程爱好者共同交流
3----个人能力有限,如有不正之处欢迎大家批评指证,必定虚心改正
4----看到这里,我在此感谢你的喜欢与支持


icon_wx_200.png