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

1. 源码下载

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

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

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

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

2. 思路

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

  • 动画分析

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

  • 动画创建

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

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

  • 动画执行

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

  • 页面效果

我们创建的是 类型动画,我们只需要监听动画的值。并在值改变时,用这个去更新 _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  with SingleTickerProviderStateMixin {

  final StreamController _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: {
        PanGestureRecognizer : GestureRecognizerFactoryWithHandlers(
              ()=>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(
        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  _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(
      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  _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(
      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):我的页面搭建
下一篇:未完待续

你可能感兴趣的:(Flutter 仿生微信(5):我的页面上拉下拉动画)