該文已授權公眾號 「碼個蛋」,轉載請指明出處
在 Flutter
中,自帶手勢監聽的目前為止好像只有按鈕部件和一些 chip
部件,例如 Text
等部件需要實現手勢監聽,就需要借助帶有監聽事件的部件來實現了,這節我們會講下 InkWell
和 GestureDetector
來實現手勢的監聽。
InkWell
在前面的一些例子中,小伙伴應該看到了好幾次 InkWell
這個部件,通過它我們可以實現對一些手勢的監聽,并實現 MD
的水波紋效果,舉個簡單的一個例子
InkWell(
child: Text('點我...點我...我能響應點擊手勢'),
onTap: () => print('啊...我被點擊了...')
),
那么當點擊 Text
的時候就會響應點擊事件,控制臺輸出日志
我們還是老套路,分析下源碼。Ctrl
點擊 InkWell
來查看源碼(Android Studio
的操作,別的我不懂喔...),然后,「嗯...除了構造函數怎么什么都沒有???」那只能看它的父類 InkResponse
了,在那之前,我們看下 InkWell
的說明
/// A rectangular area of a [Material] that responds to touch.
InkWell
是在 MaterialDesign
風格下的一個用來響應觸摸的矩形區域(注意加粗的文字,1.如果不是 MD
風格的部件下,你是不能用這個來做點擊響應的;2.InkWell
是一塊矩形區域,如果你要的是圓形區域,8 好意思,不行!)
/// The [InkWell] widget must have a [Material] widget as an ancestor. The /// [Material] widget is where the ink reactions are actually painted. This /// matches the material design premise wherein the [Material] is what is /// actually reacting to touches by spreading ink.```
InkWell
必須要有一個 Material
風格的部件作為錨點,巴拉巴拉巴拉....再次強調必須要在 MD
風格下使用。
接下來看下 InkResponse
吧
InkResponse
const InkResponse({
Key key,
this.child, // 需要監聽的子部件
// 一個 `GestureTapCallback` 類型參數,看下 `GestureTapCallback` 的定義,
// `typedef GestureTapCallback = void Function();` 就是簡單的無參無返回類型參數
// 監聽手指點擊事件
this.onTap,
// 一個 `GestureTapDownCallback` 類型參數,需要 `TapDownDetails` 類型參數,
// `TapDownDetails` 里面有個 `Offset` 參數用于記錄點擊的位置,監聽手指點擊屏幕的事件
this.onTapDown,
// 同 `onTap` 表示點擊事件取消監聽
this.onTapCancel,
// 同 `onTap` 表示雙擊事件監聽
this.onDoubleTap,
// 一個 `GestureLongPressCallback` 類型參數,也是無參無返回值,表示長按的監聽
this.onLongPress,
// 監聽高亮的變化,返回 `true` 表示往高亮變化,`false` 相反
this.onHighlightChanged,
// 是否需要裁剪區域,`InkWell` 該值為 `true`,會根據 `highlightShape` 裁剪
this.containedInkWell = false,
// 高亮的外形,`InkWell` 該值設置成 `BoxShape.rectangle`,所以是個矩形區域
this.highlightShape = BoxShape.circle,
this.radius, // 手指點下去的時候,出現水波紋的半徑
this.borderRadius, // 點擊時候外圈陰影的圓角半徑
this.customBorder,
this.highlightColor, // 高亮顏色
this.splashColor, // 手指點下生成的水波顏色
this.splashFactory, // 兩個值 `InkRipple.splashFactory` 和 `InkSplash.splashFactory`
this.enableFeedback = true, // 檢測到手勢是否有反饋
this.excludeFromSemantics = false,
})
所以一些簡單的觸摸事件直接通過 InkWell
或者 InkResponse
就能夠實現,但是面臨一些比較復雜的手勢,就有點不太夠用了,我們需要通過 GestureDector
來進行處理
GestureDector
GestureDetector
也是一個部件,主要實現對各種手勢動作的監聽,其監聽事件查看下面的表格
回調方法 | 回調描述 |
---|---|
onTapDown |
點擊屏幕的手勢觸碰到屏幕時候觸發 |
onTapUp |
點擊屏幕抬手后觸發,點擊結束 |
onTap |
點擊事件已經完成的時候觸發,和 onTapUp 幾乎同時 |
onTapCancel |
點擊未完成,被其它手勢取代的時候觸發 |
onDoubleTap |
雙擊屏幕的時候觸發 |
onLongPress |
長按屏幕的時候觸發 |
onLongPressUp |
長按屏幕后抬手觸發 |
onVerticalDragDown |
觸碰到屏幕,可能發生垂直方向移動觸發,onVerticalDrag 系列事件不會同 onHorizontalDrag 系列事件同時發生 ,如果發生了 onVerticalDrag 則接下來如何變化移動,都不會觸發 onHorizontalDrag 事件,除非取消后重新觸發。判斷兩者的關鍵是準備滑動的意圖,先發生橫向滑動則觸發 onHorizontalDrag 事件,否則 onVerticalDrag 事件。 |
onVerticalDragStart |
觸碰到屏幕,并開始發生垂直方向的移動觸發 |
onVerticalDragUpdate |
垂直方向移動的距離變化觸發 |
onVerticalDragEnd |
抬手取消垂直方向移動的時候觸發 |
onVerticalDragCancel |
觸發 onVerticalDragDown 但是沒有完成整個 onVerticalDrag 事件觸發 |
onHorizontalDrag 系列介紹省略同上... |
|
onPanDown |
觸碰到屏幕,準備滑動的時候觸發,onPan 系列回調不可和 onVerticalDrag 或者 onHorizontalDrag 系列回調同時設置 |
onPanStart |
觸碰到屏幕,并開始滑動時候觸發 |
onPanUpdate |
滑動位置發生改變的時候觸發 |
onPanEnd |
滑動完成并抬手的時候觸發 |
onPanCancel |
觸發 onPanDown 但是沒有完成整個 onPan 事件觸發 |
onScaleStart |
兩個手指之間建立聯絡點觸發,初始縮放比例為 1.0 |
onScaleUpdate |
手指距離發生變化,縮放比例也跟隨變化觸發 |
onScaleEnd |
手指抬起,至間的聯絡斷開時候觸發 |
還有 onForcePress
系列事件,這個是根據對屏幕的擠壓力度進行觸發,需要達到某些定值才能觸發。GestureDetector
有個 behavior
屬性用于設置手勢監聽過程中的表現形式
-
deferToChild
默認值,觸摸到child
的范圍才會觸發手勢,空白處不會觸發 -
opaque
不透明模式,防止background widget
接收到手勢 -
translucent
半透明模式,剛好同opaque
相反,允許background widget
接收到手勢
介紹完了手勢,那就可以實際操練起來了,比如,實現一個跟隨手指運動的小方塊,先看下效果圖
簡單的分析下,通過 Positioned
來設置小方塊的位置,根據 GestureDetector
的 onPanUpdate
修改 Positioned
的 left
和 top
值,當 onPanEnd
或者 onPanCancel
的時候設置為原點,那么就可以有如圖的效果了
class GestureDemoPage extends StatefulWidget {
@override
_GestureDemoPageState createState() => _GestureDemoPageState();
}
class _GestureDemoPageState extends State<GestureDemoPage> {
double left = 0.0;
double top = 0.0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Gesture Demo'),
),
body: Stack(
alignment: Alignment.center,
children: <Widget>[
Positioned(child: Container(width: 50.0, height: 50.0, color: Colors.red), left: left, top: top),
GestureDetector(
behavior: HitTestBehavior.translucent,
child: Container(
color: Colors.transparent,
width: MediaQuery.of(context).size.width - 10,
height: MediaQuery.of(context).size.height),
onPanDown: (details) {
setState(() {
left = details.globalPosition.dx;
top = details.globalPosition.dy;
});
},
onPanUpdate: (details) {
setState(() {
left = details.globalPosition.dx;
top = details.globalPosition.dy;
});
},
onPanCancel: () {
setState(() {
left = 0.0;
top = 0.0;
});
},
onPanEnd: (details) {
setState(() {
left = 0.0;
top = 0.0;
});
},
)
],
));
}
}
如果說要實現一個放大縮小的方塊,就可以通過 onScaleUpdate
中獲取到的 details.scale
來設置方塊的寬高即可。這個比較簡單就留給小伙伴們自己實現效果了。
該部分代碼查看 gesture_main.dart
文件
Animation 動畫
Flutter
的 Animation
是個抽象類,具體的實現需要看其子類 AnimationController
,在這之前,先了解下 Animation
的一些方法和介紹。
addListener
/removeListener
添加的監聽用于監聽值的變化,remove
用于停止監聽addStatusListener
/removeStatusListener
添加動畫狀態變化的監聽,remove
停止監聽,Animation
的狀態有 4 種:dismissed
動畫初始狀態,反向運動結束狀態,forward
動畫正向運動狀態,reverse
動畫反向運動狀態,completed
動畫正向運動結束狀態。-
drive
方法用于連接動畫,例如官方舉的例子,因為AnimationController
是其子類,所以也擁有該方法Animation<Alignment> _alignment1 = _controller.drive( AlignmentTween( begin: Alignment.topLeft, end: Alignment.topRight, ), );
上面的例子將
AnimationController
和AlignmentTween
結合成一個Animation<Alignment>
動畫,當然drive
可以結合多個動畫,例如Animation<Alignment> _alignment3 = _controller .drive(CurveTween(curve: Curves.easeIn)) .drive(AlignmentTween( begin: Alignment.topLeft, end: Alignment.topRight, ));
因為 Animation
是抽象類,所以具體的還是需要通過 AnimationController
來實現。
AnimationController
AnimationController({
double value, // 設置初始的值
this.duration, // 動畫的時長
this.debugLabel, // 主要是用于 `toString` 方法中輸出信息
this.lowerBound = 0.0, // 最小范圍
this.upperBound = 1.0, // 最大范圍
// AnimationController 結束時候的行為,有 `normal` 和 `preserve` 兩個值可選
this.animationBehavior = AnimationBehavior.normal,
// 這個屬性可以通過 with `SingleTickerProviderStateMixin`
// 或者 `TickerProviderStateMixin` 引入到 `State`,通過 `this` 指定
@required TickerProvider vsync,
})
AnimationController
控制動畫的方法有這么幾個
-
forward
啟動動畫,和上面提到的forward
狀態不一樣 -
reverse
方向啟動動畫 -
repeat
重復使動畫運行 -
stop
停止動畫 -
reset
重置動畫
大概了解了 AnimationController
,接下來通過一個實際的小例子來加深下印象,例如實現如下效果,點擊開始動畫,結束后再點擊反向動畫
class _AnimationDemoPageState extends State<AnimationDemoPage> with TickerProviderStateMixin {
AnimationController _animationController;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this, duration: Duration(milliseconds: 1000), lowerBound: 28.0, upperBound: 50.0);
// 當動畫值發生變化的時候,重繪下 icon
_animationController.addListener(() {
setState(() {});
});
}
@override
void dispose() {
// 一定要釋放資源
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Animation Demo'),
),
body: Center(
child: IconButton(
icon: Icon(Icons.android, color: Colors.green[500], size: _animationController.value),
onPressed: () {
// 根據狀態執行不同動畫運動方式
if (_animationController.status == AnimationStatus.completed)
_animationController.reverse();
else if (_animationController.status == AnimationStatus.dismissed)
_animationController.forward();
}),
),
);
}
}
那么如果要實現無限動畫呢,那就可以通過 addStatusListener
監聽動畫的狀態來執行,修改代碼,在 initState
增加如下代碼
_animationController.addStatusListener((status) {
if (_animationController.status == AnimationStatus.completed)
_animationController.reverse(); // 正向結束后開始反向
else if (_animationController.status == AnimationStatus.dismissed)
_animationController.forward(); // 反向結束后開始正向
});
_animationController.forward(); // 啟動動畫
把 Center
的 child
替換成一個 Icon
,因為上面已經啟動了動畫,所以不需要再用點擊去啟動了,運行后就會無限放大縮小循環跑了。
在這個例子中,通過設置 AnimationController
的 lowerBound
和 upperBound
實現了動畫的變化范圍,接下來,將通過 Tween
來實現動畫的變化范圍。先看下 Tween
的一些介紹。
Tween
/// A linear interpolation between a beginning and ending value. /// /// [Tween] is useful if you want to interpolate across a range. /// /// To use a [Tween] object with an animation, call the [Tween] object's /// [animate] method and pass it the [Animation] object that you want to /// modify. /// /// You can chain [Tween] objects together using the [chain] method, so that a /// single [Animation] object is configured by multiple [Tween] objects called /// in succession. This is different than calling the [animate] method twice, /// which results in two separate [Animation] objects, each configured with a /// single [Tween].
Tween
是一個線性插值(如果要修改運動的插值,可以通過 CurveTween
來修改),所以在線性變化的時候很有用
通過調用 Tween
的 animate
方法生成一個 Animation
(animate
一般傳入 AnimationController
)
還可以通過 chain
方法將多個 Tween
結合到一起,這樣就不需要多次去調用 Tween
的 animate
方法來生成動畫了,多次調用 animate
相當于使用了兩個分開的動畫來完成效果,但是 chain
結合到一起就是一個動畫過程
那么對前面的動畫進行一些修改,通過 Tween
來控制值的變化
class _AnimationDemoPageState extends State<AnimationDemoPage> with TickerProviderStateMixin {
AnimationController _animationController;
Animation _scaleAnimation; // 動畫實例,用于修改值的大小
@override
void initState() {
super.initState();
_animationController = AnimationController(vsync: this, duration: Duration(milliseconds: 1000)); // 不通過 `lowerBound` 和 `upperBound` 設置范圍,改用 `Tween`
// 當動畫值發生變化的時候,重繪下 icon
_animationController.addListener(() {
setState(() {});
});
_animationController.addStatusListener((status) {
if (_animationController.status == AnimationStatus.completed)
_animationController.reverse();
else if (_animationController.status == AnimationStatus.dismissed)
_animationController.forward();
});
// 通過 `Tween` 的 `animate` 生成一個 Animation
// 再通過 Animation.value 進行值的修改
_scaleAnimation = Tween(begin: 28.0, end: 50.0).animate(_animationController);
_animationController.forward();
}
@override
void dispose() {
// 一定要釋放資源
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Animation Demo'),
),
body: Center(
// 通過動畫返回的值,修改圖標的大小
child: Icon(Icons.favorite, color: Colors.red, size: _scaleAnimation.value),
),
);
}
}
再次運行,還是能過達到之前的效果,那么很多小伙伴肯定會問了,「**,加了那么多代碼,效果還是和以前的一樣,還不如不加...」好吧,我無法反駁,但是如果要實現多個動畫呢,那么使用 Tween
就有優勢了,比如我們讓圖標大小變化的同時,顏色和位置也發生變化,只通過 AnimationController
要怎么實現? 又比如說,運動的方式要先加速后減速,那只通過 AnimationController
要如何實現?這些問題通過 Tween
就會非常方便解決,直接上代碼
class _AnimationDemoPageState extends State<AnimationDemoPage> with TickerProviderStateMixin {
AnimationController _animationController;
Animation _scaleAnimation; // 用于控制圖標大小
Animation<Color> _colorAnimation; // 控制圖標顏色
Animation<Offset> _positionAnimation; // 控制圖標位置
@override
void initState() {
super.initState();
_animationController = AnimationController(vsync: this, duration: Duration(milliseconds: 2000));
// 當動畫值發生變化的時候,重繪下 icon
_animationController.addListener(() {
setState(() {});
});
_animationController.addStatusListener((status) {
if (_animationController.status == AnimationStatus.completed)
_animationController.reverse();
else if (_animationController.status == AnimationStatus.dismissed) _animationController.forward();
});
// 通過 `chain` 結合 `CurveTween` 修改動畫的運動方式,曲線類型可自行替換
_scaleAnimation =
Tween(begin: 28.0, end: 50.0).chain(CurveTween(curve: Curves.decelerate)).animate(_animationController);
_colorAnimation = ColorTween(begin: Colors.red[200], end: Colors.red[900])
.chain(CurveTween(curve: Curves.easeIn))
.animate(_animationController);
_positionAnimation = Tween(begin: Offset(100, 100), end: Offset(300, 300))
.chain(CurveTween(curve: Curves.bounceInOut))
.animate(_animationController);
_animationController.forward(); // 啟動動畫
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Animation Demo'),
),
body: Stack(
children: <Widget>[
Positioned(
child: Icon(Icons.favorite, color: _colorAnimation.value, size: _scaleAnimation.value),
left: _positionAnimation.value.dx,
top: _positionAnimation.value.dy,
)
],
),
);
}
}
那么最后的效果圖
當然,Flutter
中已經實現的 Tween
還有很多,包括 BorderTween
、TextStyleTween
、ThemeDataTween
..等等,實現的方式都是類似的,小伙伴們可以自己慢慢看。
AnimationWidget
在上面的例子中,都是通過 addListener
監聽動畫值變化,然后通過 setState
方法來實現刷新效果。那么 Flutter
也提供了一個部件 AnimationWidget
來實現動畫部件,就不需要一直監聽了,還是實現上面的例子
class RunningHeart extends AnimatedWidget {
final List<Animation> animations; // 傳入動畫列表
final AnimationController animationController; // 控制動畫
RunningHeart({this.animations, this.animationController})
// 對傳入的參數進行限制(當然你也可以不做限制)
: assert(animations.length == 3),
assert(animations[0] is Animation<Color>),
assert(animations[1] is Animation<double>),
assert(animations[2] is Animation<Offset>),
super(listenable: animationController);
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
// 之前的 animation 都通過 animations 參數傳入到 `AnimationWidget`
child: Icon(Icons.favorite, color: animations[0].value, size: animations[1].value),
left: animations[2].value.dx,
top: animations[2].value.dy,
)
],
);
}
}
其實內部返回的部件和前面的是一樣的
接著對 _AnimationDemoPageState
類進行修改,注釋 initState
中的 _animationController.addListener
所有內容,然后將 body
屬性替換成新建的 RunningHeart
部件,記得傳入的動畫列表的順序
body: RunningHeart(
animations: [_colorAnimation, _scaleAnimation, _positionAnimation],
animationController: _animationController,
)
這樣就實現了剛才一樣的效果,并且沒有一直調用 setState
來刷新。
該部分代碼查看 animation_main.dart
文件
StaggeredAnimations
Flutter
還提供了交錯動畫,聽名字就可以知道,是按照時間軸,進行不同的動畫,并且由同個AnimationController
進行控制。因為沒有找到好的例子,原諒我直接搬官方的例子來講,官方交錯動畫 demo
在繼續看之前,先了解下 Interval
/// An [Interval] can be used to delay an animation. For example, a six second /// animation that uses an [Interval] with its [begin] set to 0.5 and its [end] /// set to 1.0 will essentially become a three-second animation that starts /// three seconds later.
Interval
用來延遲動畫,例如一個時長 6s 的動畫,通過 Interval
設置其 begin
參數為 0.5,end
參數設置為 1.0,那么這個動畫就會變成 3s 的動畫,并且開始的時間延遲了 3s。
了解 Interval
功能后,就可以看下實例了,當然我們不和官方的 demo 一樣,中間加個旋轉動畫
class StaggeredAnim extends StatelessWidget {
final AnimationController controller;
final Animation<double> opacity;
final Animation<double> width;
final Animation<double> height;
final Animation<EdgeInsets> padding;
final Animation<BorderRadius> border;
final Animation<Color> color;
final Animation<double> rotate;
StaggeredAnim({Key key, this.controller}):
// widget 透明度
opacity = Tween(begin: 0.0, end: 1.0)
.animate(CurvedAnimation(parent: controller, curve: Interval(0.0, 0.1, curve: Curves.ease))),
// widget 寬
width = Tween(begin: 50.0, end: 150.0)
.animate(CurvedAnimation(parent: controller, curve: Interval(0.1, 0.250, curve: Curves.ease))),
// widget 高
height = Tween(begin: 50.0, end: 150.0)
.animate(CurvedAnimation(parent: controller, curve: Interval(0.25, 0.375, curve: Curves.ease))),
// widget 底部距離
padding = EdgeInsetsTween(begin: const EdgeInsets.only(top: 150.0), end: const EdgeInsets.only(top: .0))
.animate(CurvedAnimation(parent: controller, curve: Interval(0.25, 0.375, curve: Curves.ease))),
// widget 旋轉
rotate = Tween(begin: 0.0, end: 0.25)
.animate(CurvedAnimation(parent: controller, curve: Interval(0.375, 0.5, curve: Curves.ease))),
// widget 外形
border = BorderRadiusTween(begin: BorderRadius.circular(5.0), end: BorderRadius.circular(75.0))
.animate(CurvedAnimation(parent: controller, curve: Interval(0.5, 0.75, curve: Curves.ease))),
// widget 顏色
color = ColorTween(begin: Colors.blue, end: Colors.orange)
.animate(CurvedAnimation(parent: controller, curve: Interval(0.75, 1.0, curve: Curves.ease))),
super(key: key);
Widget _buildAnimWidget(BuildContext context, Widget child) {
return Container(
padding: padding.value,
alignment: Alignment.center,
// 旋轉變化
child: RotationTransition(
turns: rotate, // turns 表示當前動畫的值 * 360° 角度
child: Opacity(
opacity: opacity.value, // 透明度變化
child: Container(
width: width.value, // 寬度變化
height: height.value, // 高度變化
decoration: BoxDecoration(
color: color.value, // 顏色變化
border: Border.all(color: Colors.indigo[300], width: 3.0),
borderRadius: border.value), // 外形變化
),
),
),
);
}
@override
Widget build(BuildContext context) {
// AnimatedBuilder 繼承 AnimationWidget,用來快速構建動畫部件
return AnimatedBuilder(animation: controller, builder: _buildAnimWidget);
}
}
然后修改 body
的參數,設置成我們的動畫,當點擊的時候就會啟動動畫
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: _playAnim,
child: Center(
// 定義一個外層圈,能夠使動畫顯眼點
child: Container(
width: 300,
height: 300,
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.1), border: Border.all(color: Colors.black.withOpacity(0.5))),
child: StaggeredAnim(controller: _controller),
),
),
)
看下最后的效果吧
該部分代碼查看 staggered_animation_main.dart
文件
結束前,我們再講一種比較簡單的 Hreo
動畫,用來過渡用。
Hero
通過指定 Hero
中的 tag
,在切換的時候 Hero
會尋找相同的 tag
,并實現動畫,具體的實現邏輯,這里可以推薦一篇文章 談一談Flutter中的共享元素動畫Hero,里面寫的很詳細,就不造車輪了。當然這邊還是得提供個簡單的 demo 的,替換前面的 body
參數
body: Container(
alignment: Alignment.center,
child: InkWell(
child: Hero(
tag: 'hero_tag', // 這里指定 tag
child: Image.asset('images/ali.jpg', width: 100.0, height: 100.0),
),
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => HeroPage())),
),
)
然后創建 HeroPage
界面,當然也可以是個 Dialog
,只要通過路由實現即可
class HeroPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
alignment: Alignment.center,
child: InkWell(
child: Hero(tag: 'hero_tag', child: Image.asset('images/ali.jpg', width: 200.0, height: 200.0)),
onTap: () => Navigator.pop(context),
),
),
);
}
}
看下最后的效果圖:
該部分代碼查看 animation_main.dart
文件
這一部分講的比較多,小伙伴可以慢慢消化,下節我會盡量填下之前留下的狀態管理的坑。
最后代碼的地址還是要的:
文章中涉及的代碼:demos
基于郭神
cool weather
接口的一個項目,實現BLoC
模式,實現狀態管理:flutter_weather一個課程(當時買了想看下代碼規范的,代碼更新會比較慢,雖然是跟著課上的一些寫代碼,但是還是做了自己的修改,很多地方看著不舒服,然后就改成自己的實現方式了):flutter_shop
如果對你有幫助的話,記得給個 Star,先謝過,你的認可就是支持我繼續寫下去的動力~