本人主要在知乎上發佈相關Flutter文章,知乎瞭解下:android
https://www.zhihu.com/people/qiang-fu-5-67/activities
git
咱們來實戰剖析下「企鵝電競」直播欄下怎麼實現:github
代碼我儘可能寫的你們淺顯易懂,不裝逼,很少嗶嗶,點關注不迷路。bash
企鵝電競app截圖:app
下面看下最終實現的效果圖:less
佈局解析成下面一個簡單的圖例:ide
總體佈局結構以下:佈局
咱們把內容部分拆分紅了幾個方法體:ui
_buildReminder()實現提醒內容區域==>預約按鈕那一行
_buildRecommedList()實現橫向的推薦列表==>吸金榜,周禮榜,真愛榜
_buildContentImageText()實現直播推薦下方的網格列表中的一個單元格內容
複製代碼
在實踐過程當中用了好幾個實現方式,this
其中一種是ScrollView方式,在Flutter中經過CustomScrollView實現,小部件使用SliveFixedExtendList和SliverGrid。
這種實現方式寫完才發現,尼瑪grid不支持自定義title,不像Android中recycleView那麼好,這尼瑪就和android中的GridView大差不差,另外Fluterr中也有GridView,也是網格佈局。
而後推翻了這個實現方式。
另外一箇中實現方式也差很少,也是gridView沒法添加水平title,我實現的是ListView嵌套GridView,可是這裏要注意的是,你須要禁用GridView滑動,不然會和ListView滑動衝突。
全部滾動組件都有一個叫physics的屬性,咱們增長一個
physics: new NeverScrollableScrollPhysics(),//禁用滾動
複製代碼
這個是一開始實現的方案。
最後仍是使用ListView實現吧,圖片效果網格部分列表怎麼實現呢?使用一個標題加4個網格單元當成一個item實現就好了。
BoxDecoration:描述如何繪製容器,Container與BoxDecoration配合來裝飾 background, border, or shadow。
new Center(
child: new Container(
width: 50.0,
height: 50.0,
decoration: new BoxDecoration(
//背景色
color: const Color(0xff7c94b6),
//沒有圖片的小夥,註釋掉image這個,用color背景也是能夠看效果的
image: new DecorationImage(
image: new ExactAssetImage('images/mozi.jpeg'),
fit: BoxFit.cover,
),
//shape類型:rectangle|circle
shape: BoxShape.rectangle,
//邊框顏色
border: new Border.all(
color: Colors.red,//邊框顏色
width: 2.0,//邊框寬度
),
),
),
)
複製代碼
主要是在容器中使用BoxDecoration進行繪製,若是不指定borderRadius 那麼容器就是一個矩形。
若是設置shape參數,BoxShape.rectangle:矩形,BoxShape.circle:圓形
咱們經過改變borderRadius值來變化shape的弧度。
咱們能夠BoxDecoration的屬性borderRadius中配置一個邊界半徑:
borderRadius: new BorderRadius.all(new Radius.circular(15.0))
複製代碼
Radius.circular構造一個圓的半徑。
下面咱們實現下「預約按鈕區域」:
///預訂按鈕區域
new Container(
//設置容器邊距
padding: const EdgeInsets.only(top: 21.0, left: 20.0),
child: new Container(
//容器中小部件居中
alignment: Alignment.center,
//設置小部件距離容器的邊距
padding: const EdgeInsets.fromLTRB(14.0, 7.0, 14.0, 7.0),
//設置裝飾器
decoration: new BoxDecoration(
//設置邊界
border: new Border.all(
color: Colors.black38,
width: 1.0,
),
//設置邊界半徑
borderRadius: new BorderRadius.all(new Radius.circular(50.0)),
),
child: new Row(
//按鈕和預訂文字水平排列顯示
children: <Widget>[
new Icon(Icons.timer, color: Colors.black38, size: 12.0),
new Text(
"預訂",
style: new TextStyle(fontSize: 13.0),
)
],
),
),
)
複製代碼
一樣的,推薦列表中用戶頭像也是一樣的道理:
new Container(
//外邊距,若是用padding的話頭像會變形
margin: new EdgeInsets.symmetric(horizontal: 10.0),
//須要定容器寬高,不然CircleAvatar裁剪出來的圖片很小
width: 40.6,
height: 40.6,
//添加一個邊框
decoration: new BoxDecoration(
shape: BoxShape.circle,
//設置邊框顏色
border: new Border.all(
width: 1.0,
color: Colors.yellow,
)),
child: new CircleAvatar(//圓角頭像小部件
radius: 5.0,
//AssetBundleImageProvider
backgroundImage: new AssetImage(
assetName,//動態傳入進來,如'images/chenhe.jpg'
),
),
)
複製代碼
new Center(
child: new Stack(
children: <Widget>[
widget1,
widget2,
widget3,
......
],
),
)
複製代碼
上面這段代碼顯示出來的小部件都疊加在一塊兒,默認對齊方式是左上角,Stack控件自己包含全部不定位的子控件,
咱們能夠經過Stack的alignment讓內部全部的子控件對齊方式改變。
在作上面企鵝電競效果的時候,Expanded啊,Container啊,等等控件,內部各類屬性用了遍,沒法讓其餘小部件在Stack內部進行定位到某個位置。就一個鼠標點擊的操做,Stack源碼中有告訴咱們哪一個控件能夠對Stack內部的小部件進行定位。
Positioned代碼的註釋:
A widget that controls where a child of a [Stack] is positioned.
經過子控件的top、right、bottom和left屬性將它們定位在Stack控件不一樣位置處。
代碼樣例以下:
new Container(
width: double.infinity,
height: double.infinity,
child: new Stack(
children: <Widget>[
new Container(
width: 100.0,
height: 80.0,
color: Colors.green,
),
new Container(
width: 50.0,
height: 50.0,
color: Colors.orangeAccent,
),
new Positioned(
right: 150.0,
bottom: 280.0,
child: new Container(
width: 100.0,
height: 100.0,
color: Colors.lime,
)),
new Positioned(
right: 50.0,
bottom: 100.0,
child: new Container(
width: 60.0,
height: 60.0,
color: Colors.deepPurpleAccent,
)),
],
),
)
複製代碼
截圖效果中有一個抽獎中的tag背景,我拿到企鵝電競app內的小圖標,它是反過來的,我懶得轉方向,考慮程序怎麼旋轉,順便讓你們瞭解下怎麼旋轉這個圖片。
RotatedBox:旋轉內部小部件,上面的圖片咱們須要順時針旋轉2次便可。
const RotatedBox({
Key key,
@required this.quarterTurns,
Widget child,
}) : assert(quarterTurns != null),
super(key: key, child: child);
複製代碼
quarterTurns這個屬性值表明的是:旋轉的次數;每旋轉一次走順時針方向的四分之一;
咱們經過以下代碼實現了,抽獎中的tag效果:
new Stack(
alignment: Alignment.center,
children: <Widget>[
//RotatedBox:旋轉內部小部件;
//quarterTurns:旋轉的次數;每旋轉一次走順時針方向的四分之一;
new RotatedBox(quarterTurns: 2,child: new Image.asset('images/battle_status_bg_yellow.9.png',width: 45.0,height: 20.0,fit: BoxFit.fill,),),
new Text('抽獎中',style: new TextStyle(fontSize: 9.0,color: Colors.black),),
],
)
複製代碼
1.入口無狀態小部件來一波,瞭解下:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: new Text('企鵝電競佈局實戰篇一'),
),
body: new ListView.builder(
itemBuilder: (BuildContext context, int index) {
return index == 0 ? buildHeader() : buildContent(index);
},
itemCount: 3,
),
),
);
}
}
複製代碼
2.來構建下列表頭部內容:
Widget buildHeader() {
return new Container(
alignment: Alignment.topLeft,
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
//佈局輪播圖區域,本篇只講簡單的佈局,不作輪播圖介紹
//設置寬度最大,高度150像素,裁剪方式:居中裁剪
new Image.asset(
'images/lake.jpg',
width: double.infinity,
height: 126.6,
fit: BoxFit.cover,
),
_buildReminder(),
_buildRecommendList(),
],
),
);
}
複製代碼
_buildReminder()和_buildRecommendList()這兩個有了上面的講解本身就能夠實現了,最下面我會附上github代碼地址。
3.構建網格列表body體,瞭解下:
Widget buildContent(int index) {
return new Column(
children: <Widget>[
new Container(
margin: const EdgeInsets.all(15.0),
child: new Row(
children: <Widget>[
//強制子類填充可用空間==match_parent
new Expanded(
child: new Text(
'直播推薦',
textAlign: TextAlign.left,
style: new TextStyle(
fontSize: 15.0,
fontWeight: FontWeight.w500,
fontFamily: 'Roboto',
),
)),
new Expanded(
child: new Text(
'刷新',
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: 12.0,
color: const Color.fromARGB(255, 136, 136, 153)),
))
],
),
),
new Row(
children: <Widget>[
//強制填充剩餘空間
new Expanded(
child: new Container(
margin: const EdgeInsets.only(right: 1.5),
child: _buildContentImageText(
'images/zhubo01.jpg', '新進主播,多多關注', 'Dae-安格', 16.6),
)),
new Expanded(
child: new Container(
margin: const EdgeInsets.only(left: 1.5),
child: _buildContentImageText(
'images/zhubo02.jpeg', '國服李白,瞭解一下', 'EL-溜神', 52.1),
),
),
],
),
new Row(
children: <Widget>[
new Expanded(
child: new Container(
margin: const EdgeInsets.only(right: 1.5),
child: _buildContentImageText(
'images/zhubo03.jpeg', '貂蟬帶你五殺', '呂布別走\(^o^)/~', 5.9),
)),
new Expanded(
child: new Container(
margin: const EdgeInsets.only(left: 1.5),
child: _buildContentImageText(
'images/zhubo04.jpeg', '國服最騷香香', '國服最騷香香', 11.1),
))
],
)
],
);
}
複製代碼
上述代碼中Expanded做用是:填充剩餘可用空間==Match_parent
網格列表body體,佈局結構:
Column{
Container子widget1,<外邊距,child{
Row{
Expanded包裹一個Text文本(直播),
Expanded包裹一個Text文本(刷新)
}
}>
Row子widget2,------>_buildContentImageText()
Row子widget3 ------->_buildContentImageText()
}
複製代碼
_buildContentImageText()方法主要針對Stack的使用
Widget _buildContentImageText(
String asserPath, String desc, String username, double onlinePopulation) {
return new Container(
alignment: Alignment.center,
child: new Column(
children: <Widget>[
new Stack(
children: <Widget>[
//封面圖
new Image.asset(
asserPath,
fit: BoxFit.cover,
),
//抽獎標識
new Container(
alignment: Alignment.topRight,
padding: const EdgeInsets.only(top: 5.0),
child: new Stack(
alignment: Alignment.center,
children: <Widget>[
//RotatedBox:旋轉內部小部件;
//quarterTurns:旋轉的次數;每旋轉一次走順時針方向的四分之一;
new RotatedBox(
quarterTurns: 2,
child: new Image.asset(
'images/battle_status_bg_yellow.9.png',//Tag背景圖片
width: 45.0,
height: 20.0,
fit: BoxFit.fill,
),
),
new Text(
'抽獎中',
style: new TextStyle(fontSize: 9.0, color: Colors.black),
),
],
),
),
//用戶名和人氣值
new Positioned(
//控制[Stack]子部件位置的小部件
left: 15.0,
right: 11.0,
bottom: 7.0,
child: new Row(
children: <Widget>[
//填充剩餘空間
new Expanded(
child: new Text(
username,
textAlign: TextAlign.left,
style: new TextStyle(
fontSize: 8.0,
color: Colors.white,
),
)),
new Expanded(
child: new Text(
'$onlinePopulation 萬人氣',
textAlign: TextAlign.right,
style: new TextStyle(fontSize: 8.0, color: Colors.white),
))
],
),
),
],
),
//網格圖片下面的文字介紹
new Container(
margin: const EdgeInsets.only(top: 7.0, bottom: 16.0),
child: new Text(
desc,
style: new TextStyle(
color: Colors.black,
fontSize: 12.0,
fontWeight: FontWeight.w500,
),
),
)
],
),
);
}
複製代碼