Flutter Go 源码分析(六)

  1. PageReveal(用于操作nextPage执行滑动动画)
    PageReveal主要是利用ClipOval组件对该widget进行圆形裁剪,从而和滑动手势关联起来实现滑动动画。
final double revealPercent;
  final Widget child;

  PageReveal({
    this.revealPercent,
    this.child
  });

  @override
  Widget build(BuildContext context) {
    return ClipOval(
      clipper: new CircleRevealClipper(revealPercent),//自定义剪裁路径 
      child: child,
    );
  }

通过CircleRevealClipper继承于CustomClipper重写getClip方法自定义剪裁路径:

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);
  }
  1. PagerIndicator指示器
Widget build(BuildContext context) {

    List<PageBubble> bubbles = [];
    for(var i = 0; i < viewModel.pages.length; ++i ){
      final page = viewModel.pages[i];

      var percentActive;

      if(i == viewModel.activeIndex){//滑到当前的index
        percentActive = 1.0 - viewModel.slidePercent;
      } else if (i == viewModel.activeIndex - 1 && viewModel.slideDirection == SlideDirection.leftToRight){//从左往右滑动 而且是当前index-1 
        percentActive = viewModel.slidePercent;
      } else if (i == viewModel.activeIndex + 1 && viewModel.slideDirection == SlideDirection.rightToLeft){//从右往左滑动 而且是当前index+1
        percentActive = viewModel.slidePercent;
      }else {//其他请求 不进行变化
        percentActive = 0.0;
      }
      //isHollow 是否是未滑到的index 当前index 的后面
      bool isHollow = i > viewModel.activeIndex || (i == viewModel.activeIndex && viewModel.slideDirection == SlideDirection.leftToRight);



      bubbles.add(
        new  PageBubble(//指示器原点
          viewModel: new PageBubbleViewModel(
              page.iconAssetPath,//icon imamge路径
              page.color,//页面颜色
              isHollow,//isHollow 是否是未滑到的index 当前index 的后面
              percentActive,//滑动百分百
          ),
        ),
      );
    }

    final bubbleWidth = 55.0 ;
    final baseTranslation = ((viewModel.pages.length * bubbleWidth) / 2) - (bubbleWidth / 2) ;
    var translation = baseTranslation - (viewModel.activeIndex * bubbleWidth);

    if (viewModel.slideDirection == SlideDirection.leftToRight){
        translation = bubbleWidth * viewModel.slidePercent + translation;
    }else if (viewModel.slideDirection == SlideDirection.rightToLeft){
        translation = bubbleWidth * viewModel.slidePercent - translation;
    }

    return new Column(
      children: <Widget>[
        new Expanded(child: new Container()),
        new Transform(
          transform: new Matrix4.translationValues(0, 0.0, 0.0),
          child: new Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: bubbles,
          ),
        ),
      ],
    );
  }
}
  • PageBubble指示圆点
Widget build(BuildContext context) {
    return new Container(
      width: 55.0,
      height: 65.0,
      child: new Center(
        child: new Container(
          width: lerpDouble(20.0,45.0,viewModel.activePercent),//根据比例缩放
          height: lerpDouble(20.0,45.0,viewModel.activePercent),//根据比例缩放
          decoration: new BoxDecoration(
            shape: BoxShape.circle,
            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(负责滑动-->手势)
    一些初始化方法:
final canDragLeftToRight;
  final canDragRightToLeft;

  final StreamController<SlideUpdate> slideUpdateStream;


  PageDragger({
    this.canDragLeftToRight,//是否能从左往右滑动
    this.canDragRightToLeft,//是否能从右往左滑动
    this.slideUpdateStream,//监听器
  });

_PageDraggerState用来监听组件GestureDetector滑动相关信息:

static const FULL_TRANSTITION_PX = 300.0;//滑动最大距离

  Offset dragStart;//开始滑动的点
  SlideDirection slideDirection;//滑动方向
  double slidePercent = 0.0;//滑动百分百

  onDragStart(DragStartDetails details){//滑动开始
    dragStart = details.globalPosition;
  }

  onDragUpdate(DragUpdateDetails details) {//滑动更新
    if (dragStart != null) {
      print(details.globalPosition);
      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){//clamp :如果参数位于最小数值和最大数值之间的数值范围内,则该函数将返回参数值。如果参数大于范围,该函数将返回最大数值。如果参数小于范围,该函数将返回最小数值,通过这个函数为变量的赋值设置了取值范围
      slidePercent = (dx / FULL_TRANSTITION_PX).abs().clamp(0.0, 1.0);
      } else {
        slidePercent = 0.0;
      }
      widget.slideUpdateStream.add(//发送正在滑动监听数据
          new SlideUpdate(
          UpdateType.dragging,
          slideDirection,
          slidePercent
      ));
    }
  }

  onDragEnd(DragEndDetails details){//滑动结束
    widget.slideUpdateStream.add(//发送滑动结束消息
      new SlideUpdate(
      UpdateType.doneDragging,
      SlideDirection.none,
      0.0,
      )
    );

    dragStart = null;
  }
  1. FourthPageState处理监听相关
StreamController<SlideUpdate> slideUpdateStream;////滑动监听 controller
  AnimatedPageDragger animatedPageDragger;//动画执行者

  int activeIndex = 0;//当前序号
  SlideDirection slideDirection = SlideDirection.none;
  int nextPageIndex = 0;//下一个序号
  int waitingNextPageIndex = -1;

  double slidePercent = 0.0;//滑动到下一页的百分百

  FourthPageState() {
    slideUpdateStream = new StreamController<SlideUpdate>();//滑动监听 controller

    slideUpdateStream.stream.listen((SlideUpdate event) {//监听
      if (mounted) {
        setState(() {
          if (event.updateType == UpdateType.dragging) {//手指滑动 进行滑动动画
            slideDirection = event.direction;
            slidePercent = event.slidePercent;

            if (slideDirection == SlideDirection.leftToRight) {//从左往右滑动
              nextPageIndex = activeIndex - 1;
            } else if (slideDirection == SlideDirection.rightToLeft) {//从右往右滑动
              nextPageIndex = activeIndex + 1;
            } else {
              nextPageIndex = activeIndex;
            }
          } else if (event.updateType == UpdateType.doneDragging) {//手指滑动完成 进行下一步动画(跳转下个页面 或者返回 0.5界限)
            if (slidePercent > 0.5) {//跳转下个页面的动画
              animatedPageDragger = new AnimatedPageDragger(
                slideDirection: slideDirection,
                transitionGoal: TransitionGoal.open,
                slidePercent: slidePercent,
                slideUpdateStream: slideUpdateStream,
                vsync: this,
              );
            } else {//返回滑动前的页面动画
              animatedPageDragger = new AnimatedPageDragger(
                slideDirection: slideDirection,
                transitionGoal: TransitionGoal.close,
                slidePercent: slidePercent,
                slideUpdateStream: slideUpdateStream,
                vsync: this,
              );

              waitingNextPageIndex = activeIndex;
            }

            animatedPageDragger.run();//滑动完 完成接下来的动画
          } else if (event.updateType == UpdateType.animating) {//动画进行中 进行百分百刷新
            slideDirection = event.direction;
            slidePercent = event.slidePercent;
          } else if (event.updateType == UpdateType.doneAnimating) {//动画完成  置空百分百数据 销毁animatedPageDragger
            if (waitingNextPageIndex != -1) {
              nextPageIndex = waitingNextPageIndex;
              waitingNextPageIndex = -1;
            } else {
              activeIndex = nextPageIndex;
            }

            slideDirection = SlideDirection.none;
            slidePercent = 0.0;

            animatedPageDragger.dispose();//销毁animatedPageDragger
          }
        });
      }
    });
  }

这里要说一下AnimatedPageDragger这个类,它是用来处理滑动完成是进行跳转下个页面或者返回上个页面的动画执行的。

static const PERCENT_PER_MILLISECOND = 0.005;//每毫秒的百分百

  final slideDirection;//滑动方向
  final transitionGoal;//动画类型 open: 跳转下个页面   close: 返回当前页面

  AnimationController completionAnimationController;//动画监听controller

  AnimatedPageDragger({
      this.slideDirection,
      this.transitionGoal,
      slidePercent,
      StreamController<SlideUpdate> slideUpdateStream,
      TickerProvider vsync,
  }) {//一些初始化操作
    final startSlidePercent = slidePercent;
    var endSlidePercent;
    var duration;

    if ( transitionGoal == TransitionGoal.open){//如果是跳转下个页面
      endSlidePercent = 1.0;

      final slideRemaining = 1.0 - slidePercent;

      duration = new Duration(
        milliseconds: (slideRemaining / PERCENT_PER_MILLISECOND).round()
      );

    } else {//返回当前页面
      endSlidePercent = 0.0;
      duration = new Duration(
          milliseconds: (slidePercent / PERCENT_PER_MILLISECOND).round()
      );
    }

    completionAnimationController = new AnimationController(
        duration: duration,
        vsync: vsync
    )
    ..addListener((){//监听
       slidePercent = lerpDouble( //lerpDouble(begin.height, end.height, t), t是百分百 在 begin 和end之间
          startSlidePercent,
          endSlidePercent,
          completionAnimationController.value
      );

      slideUpdateStream.add(//发送动画执行监听
        new SlideUpdate(
        UpdateType.animating,
        slideDirection,
        slidePercent,
        )
      );

    })

    ..addStatusListener((AnimationStatus status){//动画执行状态发生改变

      if(status == AnimationStatus.completed){
        slideUpdateStream.add(//发送动画完成监听
        new SlideUpdate(
          UpdateType.doneAnimating,
          slideDirection,
          endSlidePercent,
          )
        );
      }

    });

  }

全在注释里。

结语

到这里,整个fluttergo项目的大体框架和实现思路基本都搞清楚了,以上分析仅是我个人理解,如有不对的地方欢迎指正。
最后再次感谢fluttergo项目开发人员的无私开源。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352