1. 源碼下載
喜歡的話贯溅,別忘了點(diǎn)個(gè)關(guān)注霹娄,還有給個(gè) Github 右上角的小星星吧单默。
源碼下載地址绕辖,代碼會(huì)根據(jù)不斷更新胯府。
Flutter 仿生微信(目錄)
上一篇:Flutter 仿生微信(3):發(fā)現(xiàn)頁面搭建
下一篇:Flutter 仿生微信(5):我的頁面上拉下拉動(dòng)畫
2. 思路
- 頁面層級(jí)
我的頁面可以下拉出現(xiàn)掃一掃功能捌肴,所以頁面應(yīng)該分為三個(gè)層級(jí)偏螺。
第一層是手勢層父腕,捕捉用戶滑動(dòng)操作椅邓,并更新頁面位置柠逞,展示上方掃一掃頁面。
第二層是掃一掃景馁,停在頁面最上方板壮,根據(jù)下拉動(dòng)作逐漸展示。
第三層是我的頁面合住,展示個(gè)人信息以及一些功能绰精。
- 手勢層
這里要捕捉到用戶手勢,所以最后邊一層使用 RawGestureDetector聊疲,然后手勢用 PanGestureRecognizer茬底,捕捉用戶的滑動(dòng),以及滑動(dòng)結(jié)束操作获洲≮灞恚滑動(dòng)時(shí)我們按照 0.5 的阻尼系數(shù)進(jìn)行主頁面移動(dòng),當(dāng)滑動(dòng)停止時(shí)贡珊,根據(jù)頁面高度進(jìn)行判斷最爬,顯示掃一掃還是我的頁面。
- 動(dòng)態(tài)刷新
由于 flutter 頻繁使用 setState 會(huì)變得很卡頓门岔,所以我們減少 setState 的使用爱致,這里使用 StreamBuilder 來處理動(dòng)態(tài)更新。
- 我的頁面寒随、掃一掃頁面
這倆頁面就是簡單的 Widget 堆疊了糠悯,由于底層捕獲了滑動(dòng)手勢帮坚,這里我的頁面就使用 Column 來處理了。
- TODO
onEnd 事件后互艾,過渡動(dòng)畫后邊在做试和。
滑動(dòng)過程中,底部有約束問題導(dǎo)致的黃色條纫普。
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> {
final StreamController<double> _streamController = StreamController();
double _topY = 0;
bool _hideTop = true;
final double _screenHeight = window.physicalSize.height / 2.0;
@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');
_streamController.sink.add(
_topY = _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);
},
),
);
}
double _didHideTopWhenEndPanning(){
if (!_hideTop) {
if (_topY < _screenHeight - 100 - 64) {
_hideTopWhenEndPaning();
} else {
_showTopWhenEndPanning();
}
} else {
if (_topY > 200) {
_showTopWhenEndPanning();
} else {
_hideTopWhenEndPaning();
}
}
return _topY;
}
void _hideTopWhenEndPaning(){
_topY = 0;
_hideTop = true;
}
void _showTopWhenEndPanning(){
_topY = _screenHeight - 64;
_hideTop = false;
}
}
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: 0,
child: FMMineContent(),
),
],
);
}
}
FMMineTopView.dart
import 'package:FMWeixinApp/tools/FMColor.dart';
import 'package:flutter/material.dart';
class FMMineTopView extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Stack(
children: [
Container(
color: FMColors.wx_gray,
),
],
);
}
}
FMMineContent.dart
import 'package:FMWeixinApp/find/item/FMFindItem.dart';
import 'package:FMWeixinApp/find/model/FMFindModel.dart';
import 'package:FMWeixinApp/mine/mine/item/FMMineHeader.dart';
import 'package:flutter/material.dart';
class FMMineContent extends StatelessWidget{
List <FMFindModel> _models = [];
List <Widget> _items = [];
@override
Widget build(BuildContext context) {
// TODO: implement build
_initModels();
_intItems();
return Column(
children: _items,
);
}
void _initModels(){
_models.add(FMFindModel('', '', 'divid'));
_models.add(FMFindModel('assets/images/mine/mine_pay.png', '支付', 'function'));
_models.add(FMFindModel('', '', 'divid'));
_models.add(FMFindModel('assets/images/mine/mine_collection.png', '收藏', ''));
_models.add(FMFindModel('assets/images/mine/mine_album.png', '相冊(cè)', ''));
_models.add(FMFindModel('assets/images/mine/mine_wallet.png', '卡包', ''));
_models.add(FMFindModel('assets/images/mine/mine_face.png', '表情', ''));
_models.add(FMFindModel('', '', 'divid'));
_models.add(FMFindModel('assets/images/mine/mine_setting.png', '設(shè)置', ''));
}
void _intItems(){
_items.add(
Container(
height: 200,
child: FMMineHeader(),
),
);
_models.forEach((model) {
if (model.type == 'divid') {
_items.add(Padding(padding: EdgeInsets.only(top: 10),));
} else {
_items.add(
Container(
height: 60.0,
child: FMFindItem(
model: model,
onTap: (model){
print('${model.title}');
},
),
),
);
}
});
}
}
4. 源碼分析
4.1 手勢層
- 事件捕捉
使用 RawGestureDetector阅悍,手勢設(shè)置為 PanGestureRecognizer,捕捉 onUpdate, onEnd 兩個(gè)事件昨稼。
- onUpdate
滑動(dòng)時(shí)會(huì)一直走這個(gè)回調(diào)函數(shù)节视,會(huì)返回 offset,我們這里只使用 details.delta.dy假栓,根據(jù) y 的偏移量 * 0.5 的阻尼系數(shù)寻行,來下拉整個(gè) FMMineContent。
- onEnd
滑動(dòng)結(jié)束后但指,判斷當(dāng)前 FMMineContent.topY 的值寡痰。分為兩種情況:
正常情況下拉,F(xiàn)MMineContent.topY > 200棋凳,認(rèn)為下拉成功,收起 FMMineContent连躏,展示 FMMineTopView剩岳。
展示 FMMineTopView 時(shí),上拉入热,F(xiàn)MMineContent.topY < (screen.height - 200)拍棕,認(rèn)為上拉成功,收起 FMMineTopView勺良,展示 FMMineContent绰播。
4.2 頁面刷新
由于使用 setState 來做動(dòng)態(tài)刷新非常浪費(fèi)性能,我們這里使用 Stream 來做刷新操作尚困。
創(chuàng)建 StreamController蠢箩,并設(shè)置需要改變的變量,這里我們定義一個(gè) double _topY事甜。
創(chuàng)建 StreamBuilder
StreamBuilder<double>(
stream: _streamController.stream,
initialData: _topY,
builder: (context, snapShot){
return FMMineBody(_topY);
}
這樣當(dāng) StreamController 改變 _topY 時(shí)谬泌,這里就會(huì)監(jiān)聽到,從而刷新控件逻谦。
- 改變 _topY
..onUpdate = (details) {
print('update');
_streamController.sink.add(
_topY += details.delta.dy * 0.5
);
}
在 onUpdate 中掌实,我們使用 StreamController 對(duì) _topY 進(jìn)行改變。
- 整體分析
StreamController創(chuàng)建 -> StreamBuilder創(chuàng)建 -> StreamController改變_topY -> StreamBuilder刷新控件邦马。