阿里 Flutter-go 項目拆解筆記(七)

Flutter-go 項目地址是:https://github.com/alibaba/flutter-go

上文 我們分析了 第三個 Tab 頁面,主要分析了 組件的收藏的實現,EventBus,sqflite 的使用

這篇文章主要拆解 第四個Tab頁面(關於手冊)。對應的welcome_page.dart文件的路徑如下:'package:flutter_go/views /welcome_page/welcome_page.dart';

下圖是整理後的collection_page.dart文件主要的內容:

頁面切換實現

老實說,沒用 Flutter 做過項目,直接來閱讀源碼還是有點吃力的,理解錯了歡迎指出。

fourth_page.dart的佈局中,我們可以看到children是由Page、PageReveal、PageIndicator、PageDragger幾個Widget組成的。那麼我們就來分析這幾個的Widget的實現,瞭解他們的作用是什麼。

Page 組件分析

作用是:承載每個頁面。
Page組件中使用了Stack組件,用於在右上角顯示go GitHub按鈕。
Page組件的childrenContainer、PositionedContainer用於展示 每個頁面 的內容Positioned主要顯示右上角go GitHub按鈕。

每個頁面 的實現分析:

Transform可以在其子Widget繪製時對其應用一個矩陣變換(transformation)
這裏的實現主要就是在children集合中添加三個Transform組件,一個用於 頂部圖片 的動畫,一個用於 中間標題文字 的動畫,一個用於 描述文字 的動畫。

/// 這裏只貼出了頂部圖片的變換代碼,更多代碼請查看源碼
Transform(
    // 參數1:x 軸的移動方向,參數2:y 軸的移動方向,參數3:z 軸的移動方向
    transform: Matrix4.translationValues(
        0.0, 50.0 * (1.0 - percentVisible), 0.0),
    child: Padding(
      padding: EdgeInsets.only(top: 20.0, bottom: 10.0),
      /// 頂部圖片
      child: Image.asset(viewModel.heroAssetPath,
          width: 160.0, height: 160.0),
    ),
),

/// 標題的實現也是類似
/// 描述文本的實現同上

go GitHub 按鈕的實現分析:

使用了RaisedButton.icon組件,該組件的作用是:可生成一個帶有icon的按鈕。而 半圓角的矩形邊框 是使用RoundedRectangleBorder實現的

  /// 回到首頁按鈕,Github 按鈕
  Widget creatButton(
      BuildContext context, String txt, IconData iconName, String type) {
    return RaisedButton.icon(
        onPressed: () async {
          if (type == 'start') {
            await SpUtil.getInstance()
              ..putBool(SharedPreferencesKeys.showWelcome, false);
          /// 跳轉首頁
            _goHomePage(context);
          } else if (type == 'goGithub') {
        /// 進入 Flutter-go  GitHub 首頁
            Application.router.navigateTo(context,
                '${Routes.webViewPage}?title=${Uri.encodeComponent(txt)} Doc&&url=${Uri.encodeComponent("https://github.com/alibaba/flutter-go")}');
          }
        },
        elevation: 10.0,
        color: Colors.black26,
        // shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(20.0))),
        //如果不手動設置icon和text顏色,則默認使用foregroundColor顏色
        icon: Icon(iconName, color: Colors.white, size: 14.0),
        label: Text(
          txt,
          maxLines: 1,
          style: TextStyle(
              color: Colors.white, fontSize: 14, fontWeight: FontWeight.w700),
        ));
  }

PageReveal 組件分析

PageReveal 作用是實現頁面Page裁剪 效果。在實現過程中繼承了CustomClipperCustomClipper重寫的方法getClip根據需要露出的百分比revealPercentchild進行裁剪,返回了Rect,但是在CircleRevealClipper的外層嵌套了ClipOvalClipOval是使用橢圓來剪輯其子對象的Widget。實現源碼如下:

 @override
  Widget build(BuildContext context) {
    return ClipOval(
      clipper: new CircleRevealClipper(revealPercent),
      // 這裏的 child 是傳入的 page
      child: child,
    );
  }
}

class CircleRevealClipper extends CustomClipper<Rect>{

  // 顯示的百分比
  final double revealPercent;


  CircleRevealClipper(
    this.revealPercent
  );

  @override
  Rect getClip(Size size) {

    final epicenter = new Offset(size.width / 2, size.height * 0.9);

    double theta = atan(epicenter.dy / epicenter.dx);
    final distanceToCorner = epicenter.dy / sin(theta);

    final radius = distanceToCorner * revealPercent;
    final diameter = 2 * radius;

    return new Rect.fromLTWH(epicenter.dx - radius, epicenter.dy - radius, diameter, diameter);
  }

  @override
  bool shouldReclip(CustomClipper<Rect> oldClipper) {
    return true;
  }

}

PagerIndicator 組件分析

底部的指示器實現,在源碼中可以看出指示器的實現也使用了Matrix4.translationValues動畫,指示器的實現主要是由PageBubble Widget 集合組成。而PageBubble的實現如下:

 @override
  Widget build(BuildContext context) {
    return new Container(
      width: 55.0,
      height: 65.0,
      child: new Center(
        child: new Container(
          // 寬度在(20.0,45.0)線性插值兩個數字之間變換
          width: lerpDouble(20.0,45.0,viewModel.activePercent),
         // 高度在(20.0,45.0)線性插值兩個數字之間變換
          height: lerpDouble(20.0,45.0,viewModel.activePercent),
          decoration: new BoxDecoration(
            shape: BoxShape.circle,
          // isHollow 是否顯示圓圈
          // i > viewModel.activeIndex 從右向左滑動 返回 true
          // i == viewModel.activeIndex && viewModel.slideDirection == SlideDirection.leftToRight) 從左向右滑動 返回 true
          //  bool isHollow = i > viewModel.activeIndex || (i == viewModel.activeIndex && viewModel.slideDirection == SlideDirection.leftToRight);
          // isHollow表示圓點對應的頁碼是否大於當前頁碼,如果大於的話顯示空心,否則顯示實心
            color: viewModel.isHollow
                ? const Color(0x88FFFFFF).withAlpha(0x88 * viewModel.activePercent.round())
                : const Color(0x88FFFFFF),
            border: new Border.all(
              color: viewModel.isHollow
                  ? const Color(0x88FFFFFF).withAlpha((0x88 * (1.0 - viewModel.activePercent)).round())
                  : Colors.transparent,
              width: 3.0,
            ),
          ),
          // 指示器圖片
          child: new Opacity(
            opacity: viewModel.activePercent,
            child: Image.asset(
              viewModel.iconAssetPath,
              color: viewModel.color,
            ),
          ),
        ),
      ),
    );
  }

PageDragger 組件分析

PageDragger主要用來接收觸摸事件,然後根據觸摸事件來進行對應的操作。首先是在FourthPage的構造方法中創建了一個Stream流的事件監聽。關於Stream介紹可以查看 官方文檔 或者這篇博文 Flutter響應式編程 - Stream

FourthPage的構造方法僞代碼如下:

...
slideUpdateStream = new StreamController<SlideUpdate>();
// 開始監聽
slideUpdateStream.stream.listen((SlideUpdate event) {
    ...
}
...

在創建了slideUpdateStream之後將其傳遞個PageDragger構造方法,如下:

 new PageDragger(
          canDragLeftToRight: activeIndex > 0,
          canDragRightToLeft: activeIndex < pages.length - 1,
          slideUpdateStream: this.slideUpdateStream,
        )

在構造方法中控制了左右滑動的邊界,而slideUpdateStream就是用於監聽觸摸事件的。那麼在這裏是如何去監聽觸摸事件的呢?

PageDraggerbuild實現中可以看出是通過監聽了水平滑動來實現對應的操作

@override
  Widget build(BuildContext context) {
    // 水平觸摸監聽
    return GestureDetector(
      onHorizontalDragStart: onDragStart ,
      onHorizontalDragUpdate: onDragUpdate ,
      onHorizontalDragEnd: onDragEnd ,
    );
  }

我們可以看其中的一個方法onDragUpdate的實現:

// 正在拖拽
  onDragUpdate(DragUpdateDetails details) {
    if (dragStart != null) {
      final newPosition = details.globalPosition;
      final dx = dragStart.dx - newPosition.dx;
      // 滑動方向
      if (dx > 0 && widget.canDragRightToLeft) {
        slideDirection = SlideDirection.rightToLeft;
      } else if (dx < 0 && widget.canDragLeftToRight) {
        slideDirection = SlideDirection.leftToRight;
      } else {
        slideDirection = SlideDirection.none;
      }
      // 滑動的百分比
      if (slideDirection != SlideDirection.none){
      slidePercent = (dx / FULL_TRANSTITION_PX).abs().clamp(0.0, 1.0);
      } else {
        slidePercent = 0.0;
      }
      // 添加 stream 數據
      widget.slideUpdateStream.add(
          new SlideUpdate(
          UpdateType.dragging,
          slideDirection,
          slidePercent
      ));
    }
  }

在上面的代碼可以看到slideUpdateStream通過add方法添加了一個SlideUpdate對象。

所以Stream的使用方式可以分爲如下步驟:

  1. 創建slideUpdateStream = new StreamController<SlideUpdate>();
  2. 開啓監聽slideUpdateStream.stream.listen((SlideUpdate event) {}
  3. 添加被監聽的對象slideUpdateStream.add(new SlideUpdate())

該效果實現總結

  1. 創建page裝載每個頁面
  2. 通過PageReveal去裁剪頁面
  3. 根據PageDragger的滑動百分比來控制顯示哪個頁面

點擊右上角 Github 跳轉

屬於跳轉詳情頁面,在下一篇跳轉詳情中介紹。

參考文章

FlutterPageReveal:翻頁動畫

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章