本篇文章已授權(quán)微信公眾號 guolin_blog (郭霖)獨(dú)家發(fā)布
寫在前面
Flutter 是 Google推出并開源的移動應(yīng)用開發(fā)框架,主打跨平臺、高保真、高性能。開發(fā)者可以通過 Dart語言開發(fā) App,一套代碼同時運(yùn)行在 iOS 和 Android平臺。
Flutter官網(wǎng):https://flutter-io.cn
抖音,英文名TikTok,一款火遍全球的短視頻App。在玩抖音的日子里,最令我感到舒服的就是抖音的手勢交互,加上近期都在進(jìn)行Flutter方面的學(xué)習(xí),因此就產(chǎn)生了使用Flutter來仿寫TikTok手勢交互的想法。
來看看實(shí)現(xiàn)的效果:
demo下載:
GestureDetector以及Transform
既然是手勢交互,那么就必然要檢測手勢,在Flutter中提供了GestureDetector
來幫助開發(fā)者,并提供了多個回
調(diào)來處理手勢。
Property/Callback | Description |
---|---|
onTapDown | 用戶每次和屏幕交互時都會被調(diào)用 |
onTapUp | 用戶停止觸摸屏幕時觸發(fā) |
onTap | 短暫觸摸屏幕時觸發(fā) |
onTapCancel | 用戶觸摸了屏幕,但是沒有完成Tap的動作時觸發(fā) |
onDoubleTap | 用戶在短時間內(nèi)觸摸了屏幕兩次 |
onLongPress | 用戶觸摸屏幕時間超過500ms時觸發(fā) |
onVerticalDragDown | 當(dāng)一個觸摸點(diǎn)開始跟屏幕交互,同時在垂直方向上移動時觸發(fā) |
onVerticalDragStart | 當(dāng)觸摸點(diǎn)開始在垂直方向上移動時觸發(fā) |
onVerticalDragUpdate | 屏幕上的觸摸點(diǎn)位置每次改變時,都會觸發(fā)這個回調(diào) |
onVerticalDragEnd | 當(dāng)用戶停止移動,這個拖拽操作就被認(rèn)為是完成了,就會觸發(fā)這個回調(diào) |
onVerticalDragCancel | 用戶突然停止拖拽時觸發(fā) |
onHorizontalDragDown | 當(dāng)一個觸摸點(diǎn)開始跟屏幕交互,同時在水平方向上移動時觸發(fā) |
onHorizontalDragStart | 當(dāng)觸摸點(diǎn)開始在水平方向上移動時觸發(fā) |
onHorizontalDragUpdate | 屏幕上的觸摸點(diǎn)位置每次改變時,都會觸發(fā)這個回調(diào) |
onHorizontalDragEnd | 水平拖拽結(jié)束時觸發(fā) |
onHorizontalDragCancel | onHorizontalDragDown沒有成功完成時觸發(fā) |
onPanDown | 當(dāng)觸摸點(diǎn)開始跟屏幕交互時觸發(fā) |
onPanStart | 當(dāng)觸摸點(diǎn)開始移動時觸發(fā) |
onPanUpdate | 屏幕上的觸摸點(diǎn)位置每次改變時,都會觸發(fā)這個回調(diào) |
onPanEnd | pan操作完成時觸發(fā) |
onScaleStart | 觸摸點(diǎn)開始跟屏幕交互時觸發(fā),同時會建立一個焦點(diǎn)為1.0 |
onScaleUpdate | 跟屏幕交互時觸發(fā),同時會標(biāo)示一個新的焦點(diǎn) |
onScaleEnd | 觸摸點(diǎn)不再跟屏幕有任何交互,同時也表示這個scale手勢完成 |
GestureDetector
并不會監(jiān)聽上面所有的手勢,只有傳入的callbacks非空時,才會監(jiān)聽。所以,如果你想要禁用某個手勢時,可以給對應(yīng)的callback傳null。
本文主要關(guān)注的的是拖動相關(guān)的,比如onPanXX
、onHorizontalDragXX
、onVerticalDragXX
等等回調(diào)事件。
Transform可以在其子Widget繪制時對其應(yīng)用一個矩陣變換(transformation),Matrix4是一個4D矩陣,通過它我們可以實(shí)現(xiàn)各種矩陣操作。
Container(
color: Colors.black,
child: new Transform(
alignment: Alignment.topRight, //相對于坐標(biāo)系原點(diǎn)的對齊方式
transform: new Matrix4.skewY(0.3), //沿Y軸傾斜0.3弧度
child: new Container(
padding: const EdgeInsets.all(8.0),
color: Colors.deepOrange,
child: const Text('Apartment for rent!'),
),
),
);
效果如下:
在Flutter中提供了一些封裝好的transform效果供開發(fā)者選擇,比如:平移(translate)、旋轉(zhuǎn)(rotate)、縮放(scale)。
在了解了這兩點(diǎn)之后,我們來逐步分解前文的效果。
交互分解
首先,需要明確的是這些交互效果其實(shí)都是通過檢測手指的滑動,得到一個x坐標(biāo)或者y坐標(biāo)的偏移量,然后配合Transform進(jìn)行各種不同的變換,明白了這一點(diǎn),想做到這樣的效果并不難。
- 首頁的交互
Gif :https://media.giphy.com/media/iFgOIQ7abZx0i98MCA/giphy.gif
這里的交互都是橫向的滑動,因此這里主要處理onHorizontalDragXX
相關(guān)的事件。
然后來看看首頁的布局:
Left:拍攝頁 Middle:主頁 Right:用戶頁
外層是一個GestureDetector
用于處理整個頁面的手勢,里面用的是一個Stack
,類似于Android中的FrameLayout
,它包含3個Transform
的子Widget。
這里選取拍攝頁(left)來具體談?wù)?
通過觀察可以發(fā)現(xiàn),隨著偏移量的改變,這里其實(shí)包含兩個變化:1.縮放 2. 前景色透明度。
縮放可以直接采用前文提到的Transform.scale
,前景色可以用foregroundDecoration
通過改變Color的透明度來達(dá)到效果,看看實(shí)現(xiàn):
/// 左側(cè)Widget
///
/// 通過 [Transform.scale] 進(jìn)行根據(jù) [offsetX] 縮放
/// 最小 0.88 最大為 1
Transform buildLeftPage(double screenWidth) {
return Transform.scale(
scale: 0.88 + 0.12 * offsetX / screenWidth < 0.88 ? 0.88 : 0.88 + 0.12 * offsetX / screenWidth,
child: Container(
child: Image.asset(
"assets/left.png",
fit: BoxFit.fill,
),
foregroundDecoration: BoxDecoration(
color: Color.fromRGBO(0, 0, 0, 1 - (offsetX / screenWidth)),
),
),
);
}
當(dāng)我們的手指在橫向移動的時候,記錄下偏移總量offsetX
,然后通過setState進(jìn)行更新。
onHorizontalDragUpdate: (details) {
// 控制 offsetX 的值在 -screenWidth 到 screenWidth 之間
if (offsetX + details.delta.dx >= screenWidth) {
setState(() {
offsetX = screenWidth;
});
} else if (offsetX + details.delta.dx <= -screenWidth) {
setState(() {
offsetX = -screenWidth;
});
} else {
setState(() {
offsetX += details.delta.dx;
});
}
}
通過setState更新偏移量offsetX之后,F(xiàn)lutter便會重新渲染視圖,從而達(dá)到上圖的效果。
- Hero動畫
Gif:https://media.giphy.com/media/Q7LdOFFBM4HB8xGSpg/giphy.gif
Flutter提供了Hero動畫來實(shí)現(xiàn)這樣的過渡效果。Hero指的是可以在路由(頁面)之間“飛行”的widget,簡單來說Hero動畫就是在路由切換時,有一個共享的Widget可以在新舊路由間切換,由于共享的Widget在新舊路由頁面上的位置、外觀可能有所差異,所以在路由切換時會逐漸過渡,這樣就會產(chǎn)生一個Hero動畫。
/// tiktok_page.dart
Widget build(BuildContext context) {
return Hero(
tag: "detail",
//child
)
)
/// detail_page.dart
Widget build(BuildContext context) {
return Hero(
tag: "detail",
// child
)
}
保證tag一致就可以了。
- 詳情頁的交互
Gif :https://media.giphy.com/media/h4HMGtcLLcQQmqTZTI/giphy.gif
跟首頁一樣的思路,只是這里的手勢是垂直方向。
布局同樣是GestureDetector
加上Stack
再配合Transform.translate
。
Hero(
tag: "detail",
child: GestureDetector(
onVerticalDragUpdate: (details){
// dy 不超過 -screenHeight * 0.6
dy += details.delta.dy;
if ((dy < 0 && dy.abs() > screenHeight * 0.6)) {
dy = -screenHeight * 0.6;
} else {
setState(() {});
}
},
child: Stack(
children: <Widget>[
Image.asset(
"assets/detail.png",
fit: BoxFit.fitWidth,
width: screenWidth,
height: screenHeight,
),
Transform.translate(
offset: Offset(0, dy + screenHeight),
child: Container(
height: screenHeight * 0.6,
child: GestureDetector(
onTap: () {},
child: Image.asset(
"assets/comment.png",),
)
),
),
],
),
),
);
在手指離開屏幕時,根據(jù)偏移利用動畫進(jìn)行調(diào)整。
onVerticalDragEnd: (_){
// 滑動截止時,根據(jù) dy 判斷是展開還是回縮
if (dy < 0) {
if (!isCommentShow && dy.abs() > screenHeight * 0.2) {
if (dy.abs() > screenHeight * 0.2) {
animateToTop(screenHeight);
} else {
animateToBottom(screenHeight);
}
} else {
if (dy.abs() > screenHeight * 0.4) {
animateToTop(screenHeight);
} else {
animateToBottom(screenHeight);
}
}
}
},
寫在最后
總的來說,這些交互都是依靠著對手勢的檢測做到的,相比于Android,F(xiàn)lutter有著一切都是Widget的概念,
GestureDetector
以及Hero
都是Widget而且提供了很多回調(diào)函數(shù),再配合數(shù)據(jù)驅(qū)動UI和Flutter優(yōu)秀的渲染機(jī)制,減輕了開發(fā)者進(jìn)行手勢交互的難度。
Github地址:https://github.com/ditclear/tiktok_gestures
如果本文對你有幫助,請點(diǎn)贊支持。
參考資料:
- Flutter實(shí)戰(zhàn):https://book.flutterchina.club/
- 解析Flutter中的手勢控制Gestures:http://www.lxweimin.com/p/228b2d043bca
==================== 分割線 ======================
如果你想了解更多關(guān)于MVVM、Flutter、響應(yīng)式編程方面的知識,歡迎關(guān)注我。
你可以在以下地方找到我:
簡書:http://www.lxweimin.com/u/117f1cf0c556
掘金:https://juejin.im/user/582d601d2e958a0069bbe687
Github: https://github.com/ditclear