好久以前羣裏有人懸賞實現這個功能,由於較忙因此沒接,趁這幾天沒事把它實現出來。android
Tip:爲了不線的重疊致使混亂,這裏刻意偏離了一些像素,若是很差理解,能夠對比代碼。
由於沒有設計圖,因此開發時我劃分了一個基本塊(如黑色),尺寸爲
寬度: 1 * quarter : 屏幕寬度/4
高度 : blockHeight : 40
根部View爲一個Stack。
複製代碼
這裏的實現是按當時的開發順序而不是Stack層級順序
全部滾動組件的滾動處理都由我們自行處理。
複製代碼
首先咱們在根部寫一個stack,而後寫左上角那個最容易的 黑色方塊。git
Container(
color: Colors.white,
alignment: Alignment.center,
width: quarter,height: blockHeight,
child: Text('編輯',style: TextStyle(color: Colors.black),),
),
複製代碼
以後咱們實現頂部的tag,這裏是一個橫向的listview ,代碼:github
Positioned(
left: quarter, //注意這裏,要左邊空出一個 quarter 避免黑色區域遮擋
child: buildTags(),
),
複製代碼
ListView(
controller: tagController,
physics: NeverScrollableScrollPhysics(),
scrollDirection: Axis.horizontal,
children: List.generate(titles.length, (index){
return Container(
color: Colors.white,
width: quarter,height: blockHeight,
alignment: Alignment.center,
child: Text('${titles[index]}'),
);
}),
)
複製代碼
接着實現左側黃色區域(股票代碼) ,這是一個縱向的listview, 代碼:bash
Widget buildStockName(Size size){
return Container(
color: Colors.white,
margin: EdgeInsets.only(top: blockHeight), //上方要空一個 blockheight 避免黑色遮擋
width: quarter,height: size.height - blockHeight,
child: ListView.builder(
controller: stockNameController,
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.all(0),
itemCount: 50,
itemBuilder: (ctx,index){
return Container(
width:quarter,height: blockHeight,
alignment: Alignment.center,
child: Text('No.600$index'),);
}),
);
}
複製代碼
最後咱們實現最底層的紫色和粉色區域,首先咱們先把它倆做爲一個widget看待,並寫在stack的第一個位置,框架
代碼結構以下:ide
Container(
padding: MediaQuery.of(context).padding,
color: Colors.white,
width: size.width,height: size.height,
child: Stack(
children: <Widget>[
///粉色 紫色
buildBottomPart(size),
///黑色
Container(
color: Colors.white,
alignment: Alignment.center,
width: quarter,height: blockHeight,
child: Text('編輯',style: TextStyle(color: Colors.black),),
),
///藍色
Positioned(
left: quarter,
child: buildTags(),
),
///黃色
buildStockName(size),
],
),
)
複製代碼
粉色區域和紫色區域的總寬度是:源碼分析
quarter * 4 + titles.length * quarte
複製代碼
外層包裹一個SingleChildScrollView方便咱們滾動處理,代碼以下:佈局
buildBottomPart(Size size){
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: rightController,
physics: NeverScrollableScrollPhysics(),
child: Container(
margin: EdgeInsets.only(top: blockHeight),
padding: EdgeInsets.only(left: quarter),
width: quarter*4+titles.length*quarter,height: size.height,
child: Row(
children: <Widget>[
///紫色
Container(
width: miniPageWidth,height: size.height,
color: Colors.red,
child: buildLeftDetail(),
),
///粉色
Container(
width: titles.length*quarter,height: size.height,
color: Colors.blue,
child: buildStockDetail(),
),
],
),
),
);
}
複製代碼
紫色和粉絲內部的item很簡單(折線圖是我瞎畫的,不要隨意聯想),這裏不作贅述,示意圖和代碼以下:post
紫色示意圖:優化
return Container(
width: quarter * 3,height: blockHeight,
child: Row(
children: <Widget>[
LineChart(quarter*2,blockHeight),
Container(
alignment: Alignment.center,
color: Colors.lightBlueAccent,
width: quarter,height: blockHeight,
child: Text('分時圖'),
)
],
),
);
複製代碼
粉色示意圖:
item 代碼:
return Container(
width: quarter * titles.length,height: blockHeight,
child: stockDetail(index),
);
複製代碼
Widget stockDetail(int index){
return ListView(
//controller: detailHorController,
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
padding: EdgeInsets.all(0),
scrollDirection: Axis.horizontal,
children: List.generate(titles.length, (index){
return Container(
color: index % 2 == 0 ? Colors.yellow : Colors.purple,
width:quarter,height: blockHeight,
alignment: Alignment.center,
child: Text('$index.2%'),);
}),
);
}
複製代碼
咱們發現,紫色區域和粉色區域對應的tag是不同的,因此咱們要更新一下藍色區域(tag)的代碼,並先設置一個flag標識是紫色仍是粉色顯示。
bool chartShow = false //紫色區域是否顯示
複製代碼
更新後的 tag 代碼:
Widget buildTags(){
return Container(
width: quarter*3, height: blockHeight,
child: chartShow ? //注意這裏, 用於切換顯示
Row(
children: <Widget>[
Container(width: quarter*2,height: blockHeight,alignment: Alignment.center,
child: Text('分時圖'),),
Container(width: quarter*1,height: blockHeight,alignment: Alignment.center,
child: Text('漲幅'),),
],
)
: ListView(
controller: tagController,
physics: NeverScrollableScrollPhysics(),
scrollDirection: Axis.horizontal,
children: List.generate(titles.length, (index){
return Container(
color: Colors.white,
width: quarter,height: blockHeight,
alignment: Alignment.center,
child: Text('${titles[index]}'),
);
}),
),
);
}
複製代碼
接下來就是麻煩的手勢處理了,上面咱們將全部的滾動widget的屬性設置爲不滾動,並分別跟他們傳入了scrollController。 以下:
//控制底層橫向滾動
ScrollController rightController;
//控制右側股票詳情(粉色區域)
ScrollController stockVerticalController;
//控制左側股票名稱
ScrollController stockNameController;
//控制頂部tag 橫向滾動
ScrollController tagController;
//控制圖表頁的縱向滾動(紫色區域)
ScrollController chartController;
複製代碼
在這以前,咱們先定義一個枚舉來標識滑動方向:
enum SlideDirection{
Left,Right,Up,Down
}
SlideDirection slideDirection;
複製代碼
以後咱們在根佈局Stack外層包裹一個gestureDetector組件,用於手勢處理,加上以前寫的UI佈局,總體代碼以下:
GestureDetector(
//處理手勢的三個方法
onPanStart: handleStart,
onPanEnd: handleEnd,
onPanUpdate: handleUpdate,
child: Container(
padding: MediaQuery.of(context).padding,
color: Colors.white,
width: size.width,height: size.height,
child: Stack(
children: <Widget>[
///bottom part
buildBottomPart(size),
///left top
Container(
color: Colors.white,
alignment: Alignment.center,
width: quarter,height: blockHeight,
child: Text('編輯',style: TextStyle(color: Colors.black),),
),
///right top detail tag
Positioned(
left: quarter,
child: buildTags(),
),
///left stock name
buildStockName(size),
],
),
),
),
複製代碼
三個方法咱們一個一個來。
當咱們的手指第一次接觸屏幕時,這個方法會被調用,只要保持手指不離屏(或者未被取消)那麼,這個方法只會調用一次。
Offset lastPos; //咱們記錄一下手指的位置
handleStart(DragStartDetails details){
lastPos = details.globalPosition;
}
複製代碼
當咱們觸摸屏幕,並開始移動的時候,這個方法變回持續性調用。這個方法有點長,我將解釋寫在註釋裏,方便閱讀。
handleUpdate(DragUpdateDetails details){
//這裏有點像android 原生了,咱們先根據滑動位置來判斷方向
if((details.globalPosition.dx - lastPos.dx).abs() > (details.globalPosition.dy - lastPos.dy).abs()){
///橫向滑動
if(details.globalPosition.dx > lastPos.dx){
//向右
slideDirection = SlideDirection.Right;
}else{
//向左
slideDirection = SlideDirection.Left;
}
}else{
///縱向滑動
if(details.globalPosition.dy > lastPos.dy){
//向下
slideDirection = SlideDirection.Down;
}else{
//向上
slideDirection = SlideDirection.Up;
}
}
//以後咱們記錄滑動的距離,這裏的滑動距離是上次點到當前點的距離,不是總距離
double disV = (details.globalPosition.dy - lastPos.dy).abs();
double disH = (details.globalPosition.dx - lastPos.dx).abs();
//而後咱們根據滑動方向來驅動哪些滑動組件
switch(slideDirection){
case SlideDirection.Left:
//向左滑動時,咱們要保證不能滑動超出最大尺寸(其實不作這個處理也沒事它會滾回來)
//rightController.position.maxScrollExtent是當前可滾動的最大尺寸
if(rightController.offset < rightController.position.maxScrollExtent){
rightController.jumpTo(rightController.offset + disH);
if(!chartShow){
//若是是粉色區域顯示,咱們才滑動頂部tag
//由於紫色區域的tag是不須要滾動的
tagController.jumpTo(tagController.offset + disH);
}
}
break;
case SlideDirection.Right:
//向右滑動
if(rightController.offset > quarter*3){
//粉色區域顯示時
if((rightController.offset - disH) < quarter*3){
//經過這個判斷,咱們要確保用戶快速fling時,不會把紫色區域滑出來
rightController.jumpTo(quarter*3);
}else{
//普通向右滑動
rightController.jumpTo(rightController.offset - disH);
//同上
if(!chartShow){
tagController.jumpTo(tagController.offset - disH);
}
}
}else if(rightController.offset != 0 && rightController.offset <= quarter*3){
//當用戶在粉色初始區域時繼續向右滑動,咱們要營造一個阻尼效果(這裏簡單處理一下),
rightController.jumpTo(rightController.offset - disH/3);
}
break;
case SlideDirection.Up:
//向上滑動
if(stockVerticalController.offset < stockVerticalController.position.maxScrollExtent){
//股票名字,粉色和紫色向上滑動
stockVerticalController.jumpTo(stockVerticalController.offset+disV);
stockNameController.jumpTo(stockNameController.offset+disV);
chartController.jumpTo(stockNameController.offset+disV);
}
break;
case SlideDirection.Down:
//股票名字,粉色和紫色向下滑動
if(stockVerticalController.offset > stockVerticalController.position.minScrollExtent){
stockVerticalController.jumpTo(stockVerticalController.offset-disV);
stockNameController.jumpTo(stockNameController.offset-disV);
chartController.jumpTo(stockNameController.offset-disV);
}
break;
}
//記錄一下當前滑動位置
lastPos = details.globalPosition;
}
複製代碼
當咱們滑動後,手指離屏時,會調用且只調用一次這個方法,這是咱們就須要對粉色和紫色的顯隱作處理(回彈效果),代碼以下:
bool rightAnimated = false;//滾動動畫是否運行
bool chartShow = false;//紫色區域是否顯示
handleEnd(DragEndDetails details){
//首先咱們只處理橫向滾動
if(slideDirection == SlideDirection.Left || slideDirection == SlideDirection.Right){
//紫色和粉色的切換是須要動畫來作的,在這以前,咱們要確保上一次的動畫完成,
if(!rightAnimated &&rightController.offset != 0 && rightController.offset < quarter *3){
if((quarter*3 - rightController.offset) > quarter/2 && details.velocity.pixelsPerSecond.dx > 500){
//當用戶滾動時,紫色區域顯示出的寬度大於 quarter/2,切用戶滑動速度大於 500時,咱們就要作切換了
//details.velocity.pixelsPerSecond.dx 橫向 像素/每秒
rightAnimated = true;
rightController.animateTo(0.0, duration: Duration(milliseconds: 300), curve: Curves.ease)
.then((value){
rightAnimated = false;
setState(() {
chartShow = true;
});
});
}else{
//若是不符合上面的條件,咱們再滾回粉色顯示區域
rightAnimated = true;
rightController.animateTo(quarter*3, duration: Duration(milliseconds: 50), curve: Curves.ease)
.then((value){
rightAnimated = false;
setState(() {
chartShow = false;
});
});
}
}
}
}
複製代碼
至此咱們整個功能就實現了,實際上還有不少能夠優化的地方,這裏就交給你們探索一下吧。
若是以爲對你有幫助,就點個贊和star吧 ~ 謝謝 :)
複製代碼
Bedrock——基於MVVM+Provider的Flutter快速開發框架