Flutter 自定義View——仿同花順自選股列表

吉哈達發表於2020-07-14

前言

很久之前群裡有人懸賞實現這個功能,因為較忙所以沒接,趁這幾天沒事把它實現出來。

目標

Flutter 自定義View——仿同花順自選股列表

佈局草圖

Flutter 自定義View——仿同花順自選股列表

說明

Tip:為了避免線的重疊導致混亂,這裡刻意偏離了一些畫素,如果不好理解,可以對比程式碼。

因為沒有設計圖,所以開發時我劃分了一個基本塊(如黑色),尺寸為

寬度: 1 * quarter : 螢幕寬度/4
高度 : blockHeight : 40

根部View為一個Stack。
複製程式碼

結構圖

Flutter 自定義View——仿同花順自選股列表

程式碼實現

這裡的實現是按當時的開發順序而不是Stack層級順序

所有滾動元件的滾動處理都由我們們自行處理。
複製程式碼

黑色區域

首先我們在根部寫一個stack,然後寫左上角那個最容易的 黑色方塊。

              Container(
                color: Colors.white,
                alignment: Alignment.center,
                width: quarter,height: blockHeight,
                child: Text('編輯',style: TextStyle(color: Colors.black),),
              ),
複製程式碼

藍色區域

之後我們實現頂部的tag,這裡是一個橫向的listview ,程式碼:

              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, 程式碼:

  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的第一個位置,

程式碼結構如下:

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很簡單(折線圖是我瞎畫的,不要隨意聯想),這裡不做贅述,示意圖和程式碼如下:

紫色示意圖:

Flutter 自定義View——仿同花順自選股列表

        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('分時圖'),
              )
            ],
          ),
        );
複製程式碼

粉色示意圖:

Flutter 自定義View——仿同花順自選股列表

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),
            ],
          ),
        ),
      ),
複製程式碼

三個方法我們一個一個來。

handleStart

當我們的手指第一次接觸螢幕時,這個方法會被呼叫,只要保持手指不離屏(或者未被取消)那麼,這個方法只會呼叫一次。

  Offset lastPos; //我們記錄一下手指的位置

  handleStart(DragStartDetails details){
    lastPos = details.globalPosition;

  }
複製程式碼

handleUpdate

當我們觸控螢幕,並開始移動的時候,這個方法變回持續性呼叫。這個方法有點長,我將解釋寫在註釋裡,方便閱讀。

  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;
  }
複製程式碼

handleEnd

當我們滑動後,手指離屏時,會呼叫且只呼叫一次這個方法,這是我們就需要對粉色和紫色的顯隱做處理(回彈效果),程式碼如下:

  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吧 ~ 謝謝  :)
複製程式碼

DEMO

Demo

推薦

Bedrock——基於MVVM+Provider的Flutter快速開發框架

Flutter——PageView的PageController原始碼分析筆記

Flutter—Android混合開發之下載安裝的實現

相關文章