flutter仿微信底部圖示漸變

今天你摸魚了嗎發表於2020-04-13

先看效果圖

flutter仿微信底部圖示漸變

實現思路

在flutter中,如果想實現上面的頁面切換效果,必然會想到pageView。pageView的controller可以監聽到pageView的滾動事件,也可以獲取pageView滾動的位置,所以我們在滾動事件中根據位置去改變對應的圖示顏色就可以實現了。

改變圖示顏色

圖示是從微信中提取出來的,都是webp格式的圖片。要改變圖片顏色可以使用ImageIcon這個元件。

ImageIcon會把一張圖片變成單色圖片,所以只要圖片沒有多色的要求,就可以用這個元件。

既然能改變顏色了,我們也需要知道pageView滾動的時候究竟要改什麼顏色。從一個頁面滾動到另一個頁面的過程中,顏色都是線性漸變的,要獲取這個過程中的顏色可以使用flutter的Color類提供的lerp方法,作用是獲取兩種顏色之間的線性差值

flutter仿微信底部圖示漸變
裡面有3個引數,a和b都是顏色,t是夾在0到1之間的,當t為0時返回a,當t為1時返回b

也就是在滾動事件中,計算出 t ,根據 t 改變圖示顏色就可以實現上面的效果了。

pageController.addListener(() {
      int currentPage = pageController.page.toInt();
      //當前頁面的page是double型別的, 把它減去當前頁面的int型別就可以得出當前頁面到下一個頁面的偏移量了
      double t = pageController.page - currentPage;
      //根據上一次的頁面位置獲得方向
      if (lastPage <= pageController.page) {
        //向右滑動時currentPage是當前頁
        //從當前頁過渡到下一頁
        streamController.sink.add(StreamModel(
            timeline: t, index: currentPage, gotoIndex: currentPage + 1));
      } else {
        //向左滑動時currentPage是上一頁
        //從當前頁過渡到上一頁
        streamController.sink.add(StreamModel(
            timeline: t, index: currentPage + 1, gotoIndex: currentPage));
      }
      lastPage = pageController.page;
    });
複製程式碼

上面程式碼中currentPage的值舉個例子:當前page是1,要滑動到2,那麼它的值是1.11...1.21...這樣一直到2,所以在這個過程中currentPage是當前頁。如果當前page是4,要滑動到3的時候,它的值是3.99...3.81...這樣一直到3,在這個過程中currentPage就是上一頁了。

t 的計算就更簡單了,1.11-1=0.11,3.99-3=0.99 .....

管理圖示顏色

因為我是用了自帶的底部導航BottomNavigationBar,在pageController的滾動事件中改變圖示顏色太麻煩了,所以用了Stream來管理圖示的狀態。使用Stream建立一個多訂閱的管道,讓所有圖示都訂閱它,然後在滑動事件中把需要的資料都傳送給所有圖示。

需要的資料:

class StreamModel {
  const StreamModel({this.timeline, this.index, this.gotoIndex});
  
  final double timeline;
  final int index;
  final int gotoIndex;
}
複製程式碼

圖示元件

構造方法設定一個index,方便判斷圖示是哪個。

使用StreamBuilder包住要改變顏色的元件,並且繫結從建構函式設定的StreamController。

在StreamBuilder中根據pageView滾動事件傳進來的引數控制圖示顏色。

class BottomNavIcon extends StatelessWidget {
    final StreamController<StreamModel> streamController;
    final int index;
    final String img;
     final String title;
     final double fontSize;
     Color _color;
     Color _activeColor;
     final bool isActive;
   BottomNavIcon(this.title, this.img, this.index,
      {@required this.streamController,
     this.isActive = false,
     this.fontSize = 18.0,
     Color color = Colors.grey,
      Color activeColor = Colors.blue}) {
    _color = isActive ? activeColor : color;
    _activeColor = isActive ? color : activeColor;
   }
   @override
   Widget build(BuildContext context) {
    return StreamBuilder(
        stream: streamController.stream,
        builder: (BuildContext context, AsyncSnapshot snapshot) {
          final StreamModel data = snapshot.data;
          double t = 0.0;
          if (data != null) {
            //開始的index
            if (data.index == index) {
              t = data.index > data.gotoIndex
                  ? data.timeline
                  : 1.0 - data.timeline;
              print("this${data.index}:${t}");
            }
            //結束的index
            if (data.gotoIndex == index) {
              t = data.index > data.gotoIndex
                  ? 1.0 - data.timeline //開始的index大於結束的index方向向左
                  : data.timeline; //小於方向向右
              //過渡到的圖示顏色的插值超過0.6時, 個人感覺當前顏色和結束的哪個顏色相差太多,
              //所以超過0.6時恢復預設顏色
              t = t >= 0.6 ? 1 : t;
              print("goto${data.gotoIndex}:${t}");
            }
          }
          if (t > 0.0 && t < 1.0) {
            //color.lerp 獲取兩種顏色之間的線性插值
            return Column(
              children: <Widget>[
                ImageIcon(AssetImage(this.img),
                    color: Color.lerp(_color, _activeColor, t)),
                Text(title,
                    style: TextStyle(
                        fontSize: fontSize,
                        color: Color.lerp(_color, _activeColor, t))),
              ],
            );
          }
          return Column(
            children: <Widget>[
              ImageIcon(AssetImage(this.img),
                  color:
                      Color.fromRGBO(_color.red, _color.green, _color.blue, 1)),
              Text(title,
                  style: TextStyle(
                     fontSize: fontSize,
                   color: Color.fromRGBO(
                          _color.red, _color.green, _color.blue, 1))),
            ],
          );
        });
     }
   }
複製程式碼

圖示的顏色都是當前的(index == data.index)漸漸變淺,要滾動到(index==data.gotoIndex)的圖示顏色漸深

建立多訂閱的管道(Stream)

final StreamController<StreamModel> streamController =
    StreamController.broadcast();
複製程式碼

載入圖示

for (int i = 0; i < pages.length; i++) {
      TabBarModel model = pages[i];
      bars.add(
        BottomNavigationBarItem(
          icon: BottomNavIcon(
            model.title,
            'assets/images/tabbar_' + model.icon + '_c.webp',
            i,
            streamController: streamController,
          ),
          activeIcon: BottomNavIcon(
            model.title,
            'assets/images/tabbar_' + model.icon + '_s.webp',
            i,
            streamController: streamController,
            isActive: true,
          ),
          title: Center(),
        ),
      );
    }
複製程式碼

上面程式碼的title為Center的原因是已經在圖示元件中建立了一個顯示標題的元件,方便一起設定顏色。這裡就不需要了,但是它的title不允許為null,所以隨便給它一個高寬都是0的元件

結語

其實這個效果和微信的不是一模一樣,微信的應該是選中圖示疊加到預設圖示上面。預設圖示顏色線性漸變,選中圖示透明度漸變。flutter實現這個用自帶的BottomNavigationBar估計不行,可能需要自定義一個底部導航。

第一次寫技術文章,感覺有點亂,所以貼下完整的程式碼地址:

相關文章