1. 源码下载
喜欢的话,别忘了点个关注,还有给个 Github 右上角的小星星吧。
源码下载地址,代码会根据不断更新。
Flutter 仿生微信(目录)
上一篇:Flutter 仿生微信(4):我的页面搭建
下一篇:未完待续
PS:最近有点忙,更新的比较慢。
2. 思路
结合上一篇文章,我们在滑动到指定高度时,执行隐藏还是展示扫一扫页面的 Header。
这里我们在滑动结束的时候,加一个过渡动画,让页面更加平滑一点。
- 动画分析
在滑动停止时,有两种状态,隐藏扫一扫页面和展示扫一扫页面。所以我们需要两种动画,向上隐藏扫一扫动画,向下展示扫一扫动画。
- 动画创建
动画比较简单,我们只需要创建一个
向上隐藏扫一扫动画:animation.begin = _topY,animation.end = 0。
向下展示扫一扫动画:animation.begin = _topY,animation.end = _screenHeight - 64。
- 动画执行
在滑动停止时,根据当前需要的状态初始化对应的动画,并执行动画。
- 页面效果
我们创建的是
- 动画结束
我们监听动画状态,当动画结束时。将页面复位到对应的位置(隐藏或者展示),并更新 _hideTop 的值,防止页面逻辑出错。
- 黄色警告条
这里有一个黄色的警告条,说明约束有问题,再来检查一下滑动过程中,页面位移处理。
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 {
}
}
}
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 的大小不会改变,这样就解决了黄色警告条的问题。