Flutter 仿生微信(5):我的頁面上拉下拉動畫

1. 源碼下載

喜歡的話,別忘了點個關(guān)注,還有給個 Github 右上角的小星星吧。

源碼下載地址,代碼會根據(jù)不斷更新。

Flutter 仿生微信(目錄)
上一篇:Flutter 仿生微信(4):我的頁面搭建
下一篇:未完待續(xù)

PS:最近有點忙,更新的比較慢。

2. 思路

結(jié)合上一篇文章,我們在滑動到指定高度時,執(zhí)行隱藏還是展示掃一掃頁面的 Header。
這里我們在滑動結(jié)束的時候,加一個過渡動畫,讓頁面更加平滑一點。

  • 動畫分析

在滑動停止時,有兩種狀態(tài),隱藏掃一掃頁面和展示掃一掃頁面。所以我們需要兩種動畫,向上隱藏掃一掃動畫,向下展示掃一掃動畫。

  • 動畫創(chuàng)建

動畫比較簡單,我們只需要創(chuàng)建一個 <double> 類型動畫,畢竟只是更改 _topY 的值。

向上隱藏掃一掃動畫:animation.begin = _topY,animation.end = 0。
向下展示掃一掃動畫:animation.begin = _topY,animation.end = _screenHeight - 64。

  • 動畫執(zhí)行

在滑動停止時,根據(jù)當(dāng)前需要的狀態(tài)初始化對應(yīng)的動畫,并執(zhí)行動畫。

  • 頁面效果

我們創(chuàng)建的是 <double> 類型動畫,我們只需要監(jiān)聽動畫的值。并在值改變時,用這個去更新 _topY。

  • 動畫結(jié)束

我們監(jiān)聽動畫狀態(tài),當(dāng)動畫結(jié)束時。將頁面復(fù)位到對應(yīng)的位置(隱藏或者展示),并更新 _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);
        },
      ),
    );
  }

  // 滑動結(jié)束
  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;
      }
    });
  }

  // 執(zhí)行動畫
  Future _startAnimation() async {
    try {
      await _controller.forward(from: 0).orCancel;
    } on TickerCanceled {

    }
  }
}
FM Weixin Animate.gif

4. 源碼分析

4.1 隱藏展示邏輯

  // 滑動結(jié)束
  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,我們展示掃一掃頁面,否則復(fù)原頁面。
上拉松手時,上拉超過100,我們隱藏掃一掃頁面,否則復(fù)原頁面。

4.2 動畫創(chuàng)建

  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;
      }
    });
  }

  // 執(zhí)行動畫
  Future _startAnimation() async {
    try {
      await _controller.forward(from: 0).orCancel;
    } on TickerCanceled {

    }
  }

我們先創(chuàng)建 AnimationController 和 Animation,在動畫初始化的時候就監(jiān)聽他的變化,以及動畫執(zhí)行狀態(tài)。在動畫的值改變時進行頁面的動效展示,在動畫結(jié)束時按照預(yù)期的狀態(tài)對頁面進行復(fù)位。

5. 黃色警告條

這里是后續(xù)補充的,就單獨寫一下好了。

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 設(shè)置的 bottom = 0,top = _offsetY,隨著高度越來越低,整個 FMMineContent 的高度也會越來越小。導(dǎo)致設(shè)置的 Item 高度無法正常展示,出現(xiàn)約束沖突。

這里我們讓 top,bottom 一同下移,保證了 FMMineContent 的大小不會改變,這樣就解決了黃色警告條的問題。

FM Weixin Warning Clear.gif
Flutter 仿生微信(目錄)
上一篇:Flutter 仿生微信(4):我的頁面搭建
下一篇:未完待續(xù)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容