Flutter 仿生微信(5):我的页面上拉下拉动画

1. 源码下载

喜欢的话,别忘了点个关注,还有给个 Github 右上角的小星星吧。

源码下载地址,代码会根据不断更新。

Flutter 仿生微信(目录)
上一篇:Flutter 仿生微信(4):我的页面搭建
下一篇:未完待续

PS:最近有点忙,更新的比较慢。

2. 思路

结合上一篇文章,我们在滑动到指定高度时,执行隐藏还是展示扫一扫页面的 Header。
这里我们在滑动结束的时候,加一个过渡动画,让页面更加平滑一点。

  • 动画分析

在滑动停止时,有两种状态,隐藏扫一扫页面和展示扫一扫页面。所以我们需要两种动画,向上隐藏扫一扫动画,向下展示扫一扫动画。

  • 动画创建

动画比较简单,我们只需要创建一个 <double> 类型动画,毕竟只是更改 _topY 的值。

向上隐藏扫一扫动画:animation.begin = _topY,animation.end = 0。
向下展示扫一扫动画:animation.begin = _topY,animation.end = _screenHeight - 64。

  • 动画执行

在滑动停止时,根据当前需要的状态初始化对应的动画,并执行动画。

  • 页面效果

我们创建的是 <double> 类型动画,我们只需要监听动画的值。并在值改变时,用这个去更新 _topY。

  • 动画结束

我们监听动画状态,当动画结束时。将页面复位到对应的位置(隐藏或者展示),并更新 _hideTop 的值,防止页面逻辑出错。

FM Weixin Animate.gif
  • 黄色警告条

这里有一个黄色的警告条,说明约束有问题,再来检查一下滑动过程中,页面位移处理。

FM Weixin Warning Clear.gif

3. 示例代码

FMMine.dart

import 'dart:async';
import 'dart:ui';

import 'package:FMWeixinApp/mine/mine/body/FMMineBody.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';

class FMMine extends StatefulWidget {
  @override
  FMMineState createState()=> FMMineState();
}

class FMMineState extends State <FMMine> with SingleTickerProviderStateMixin {

  final StreamController<double> _streamController = StreamController();

  double _topY = 0;
  bool _hideTop = true;

  final double _contentHeight = window.physicalSize.height / 2.0 - 64;

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return RawGestureDetector(
      gestures: <Type, GestureRecognizerFactory>{
        PanGestureRecognizer : GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
              ()=>PanGestureRecognizer(),
              (PanGestureRecognizer instace){
                instace
                ..onStart = (details) {

                }
                ..onUpdate = (details) {
                  print('update');
                  _streamController.sink.add(
                      _topY += details.delta.dy * (_hideTop ? 0.5 : 0.2 )
                  );
                }
                ..onEnd = (details) {
                  print('end');
                  _didHideTopWhenEndPanning();
                }
                ..onCancel = (){
                  print('cancel');
                }
                ..onDown = (details){
                  print('down');
                };
              },
        ),
      },
      child: StreamBuilder<double>(
        stream: _streamController.stream,
        initialData: _topY,
        builder: (context, snapShot){
          // print('topY $_topY');
          return FMMineBody(_topY);
        },
      ),
    );
  }

  // 滑动结束
  void _didHideTopWhenEndPanning(){
    if (!_hideTop) {
      if (_topY < _contentHeight - 100) {
        _hideTopWhenEndPaning();
      } else {
        _showTopWhenEndPanning();
      }
    } else {
      if (_topY > 200) {
        _showTopWhenEndPanning();
      } else {
        _hideTopWhenEndPaning();
      }
    }
  }

  // 隐藏 Header 动画
  void _hideTopWhenEndPaning(){
    _initAnimation(true);
    _startAnimation();
  }

  // 展示 Header 动画
  void _showTopWhenEndPanning(){
    _initAnimation(false);
    _startAnimation();
  }

  Animation <double> _animation;
  AnimationController _controller;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();

    _controller = new AnimationController(vsync: this, duration: Duration(milliseconds: 300));
  }

  @override
  void  dispose(){
    _controller?.dispose();
    super.dispose();
  }

  // 初始化动画
  void _initAnimation(isHide){
    _animation = Tween<double>(
      begin: _topY,
      end: isHide ? 0 : _contentHeight,
    ).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Curves.easeIn,
      ),
    )..addListener(() {
      _streamController.sink.add(
          _topY = _animation.value
      );
    })..addStatusListener((status) {
      if (status == AnimationStatus.completed){
        _streamController.sink.add(
            _topY = isHide ? 0 : _contentHeight
        );
        _hideTop = isHide;
      }
    });
  }

  // 执行动画
  Future _startAnimation() async {
    try {
      await _controller.forward(from: 0).orCancel;
    } on TickerCanceled {

    }
  }
}
FM Weixin Animate.gif

4. 源码分析

4.1 隐藏展示逻辑

  // 滑动结束
  void _didHideTopWhenEndPanning(){
    if (!_hideTop) {
      if (_topY < _contentHeight - 100) {
        _hideTopWhenEndPaning();
      } else {
        _showTopWhenEndPanning();
      }
    } else {
      if (_topY > 200) {
        _showTopWhenEndPanning();
      } else {
        _hideTopWhenEndPaning();
      }
    }
  }

  // 隐藏 Header 动画
  void _hideTopWhenEndPaning(){
    _initAnimation(true);
    _startAnimation();
  }

  // 展示 Header 动画
  void _showTopWhenEndPanning(){
    _initAnimation(false);
    _startAnimation();
  }

下拉松手时,下拉超过200,我们展示扫一扫页面,否则复原页面。
上拉松手时,上拉超过100,我们隐藏扫一扫页面,否则复原页面。

4.2 动画创建

  Animation <double> _animation;
  AnimationController _controller;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();

    _controller = new AnimationController(vsync: this, duration: Duration(milliseconds: 300));
  }

  @override
  void  dispose(){
    _controller?.dispose();
    super.dispose();
  }

  // 初始化动画
  void _initAnimation(isHide){
    _animation = Tween<double>(
      begin: _topY,
      end: isHide ? 0 : _contentHeight,
    ).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Curves.easeIn,
      ),
    )..addListener(() {
      _streamController.sink.add(
          _topY = _animation.value
      );
    })..addStatusListener((status) {
      if (status == AnimationStatus.completed){
        _streamController.sink.add(
            _topY = isHide ? 0 : _contentHeight
        );
        _hideTop = isHide;
      }
    });
  }

  // 执行动画
  Future _startAnimation() async {
    try {
      await _controller.forward(from: 0).orCancel;
    } on TickerCanceled {

    }
  }

我们先创建 AnimationController 和 Animation,在动画初始化的时候就监听他的变化,以及动画执行状态。在动画的值改变时进行页面的动效展示,在动画结束时按照预期的状态对页面进行复位。

5. 黄色警告条

这里是后续补充的,就单独写一下好了。

FMMineBody.dart

import 'package:FMWeixinApp/mine/mine/content/FMMineContent.dart';
import 'package:FMWeixinApp/mine/mine/top/FMMineTopView.dart';
import 'package:FMWeixinApp/tools/FMColor.dart';
import 'package:flutter/material.dart';

class FMMineBody extends StatelessWidget {
  double _offsetY = 0;
  FMMineBody(this._offsetY);

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Stack(
      children: [
        Container(color: FMColors.wx_gray,),
        Positioned(
          top: 0,
          left: 0,
          right: 0,
          height: _offsetY,
          child: FMMineTopView(),
        ),
        Positioned(
          top: _offsetY,
          left: 0,
          right: 0,
          bottom: -_offsetY,
          child: FMMineContent(),
        ),
      ],
    );
  }
}

我们之前给 FMMineContent 设置的 bottom = 0,top = _offsetY,随着高度越来越低,整个 FMMineContent 的高度也会越来越小。导致设置的 Item 高度无法正常展示,出现约束冲突。

这里我们让 top,bottom 一同下移,保证了 FMMineContent 的大小不会改变,这样就解决了黄色警告条的问题。

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

推荐阅读更多精彩内容