Flutter第5天--佈局例項+操作互動

張風捷特烈發表於2018-12-20

今天調料十足,保證新鮮美味----2018-12-20

1:寫在前面:

每個佈局的實現方案都有很多,我只是選擇自己認為較好的佈局方案
對於非常複雜的佈局,建議先打草稿,再進行顏色塊模擬,最後再寫控制元件
有留白的地方Expanded+flex(以下我所說的flex就是Row+Column的總成)會有很好的適應性

2.選幾張圖鎮樓:

新手級2-ok.png

-- -
Flutter第5天--佈局例項+操作互動
Flutter第5天--佈局例項+操作互動

一、入門級佈局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
Flutter第5天--佈局例項+操作互動
Flutter第5天--佈局例項+操作互動
//條目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-github 2018-12-20 Flutter第5天--佈局例項+操作互動
2.更多關於我
筆名 QQ 微信 愛好
張風捷特烈 1981462002 zdl1994328 語言
我的github 我的簡書 我的掘金 個人網站
3.宣告

1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援


icon_wx_200.png

相關文章