今日份分享:Flutter自定義之旋轉木馬
先上圖,帶你回到童年時光:
效果分析
-
子佈局按照圓形順序放置且平分角度
-
子佈局旋轉、支援手勢滑動旋轉、快速滑動抬手繼續旋轉、自動旋轉
-
支援X軸旋轉
-
支援前後縮放子佈局(起始角度為前,相對位置為後,最前面最大,反而越小)
-
多個佈局疊加時前面遮擋後面
效果難點問題
- Flutter如何實現控制元件佈局達到3D效果?
- Flutter如何實現子控制元件旋轉、自動旋轉、手勢滑動時關聯子控制元件旋轉滾動?快速滑動抬手繼續旋轉滾動?
- Flutter如何實現多個佈局疊加時前面遮擋後面?
1.子佈局按照圓形順序放置且平分角度
如上圖所示:
如上圖所示(參考系:最下方為0度,逆時針旋轉角度增加
第一個點
解:根據已知條件列方程式
x2=width/2+sin(a)*R
y2=height/2+cos(a)*R
第二個點
解:根據已知條件列方程式①
① x=width/2-sin(b)*R
y=height/2-cos(b)*R
因為b=a-180,所以帶入①方程得:
② x=width/2-sin(a-180)*R
y=height/2-cos(a-180)*R
又因為sin(k*360+a)=sin(a),所以②方式可以修改為:
③ x=width/2-sin(180+a)*R
y=height/2-cos(180+a)*R
又又因為 sin(180+a)=-sin(a),cos(180+a)=-cosa 帶入③方程式得:
④ x=width/2+sin(a)*R
y=height/2+cos(a)*R
由上面2點計算得,每個子佈局的中心點座標公式統一為:
x=width/2+sin(a)*R
y=height/2+cos(a)*R
以上所用三角函式公式表:
通過上面計算得出子控制元件的位置公式後,開始我們的程式碼。
實現子控制元件按照圓形佈局及平分角度程式碼如下:
//所有子控制元件的位置資料
//count:子控制元件數量;
//startAngle:開始角度預設為0;
//rotateAngle:偏轉角度預設為0;
List<Point> _childPointList({Size size = Size.zero}) {
List<Point> childPointList = [];
double averageAngle = 360 / count;
double radius = size.width / 2 - childWidth / 2;
for (int i = 0; i < count; i++) {
/********************子佈局角度*****************/
double angle = startAngle + averageAngle * i + rotateAngle;
//子佈局中心點座標
var centerX = size.width / 2 + sin(radian(angle)) * radius;
var centerY = size.height / 2 + cos(radian(angle)) * radius;
childPointList.add(Point(
centerX,
centerY,
childWidth,
childHeight,
centerX - childWidth / 2,
centerY - childHeight / 2,
centerX + childWidth / 2,
centerY + childHeight / 2,
1,
angle,
i,
));
}
return childPointList;
}
///角度轉弧度
///弧度 =度數 * (π / 180)
///度數 =弧度 * (180 / π)
double radian(double angle) {
return angle * pi / 180;
}
2.子佈局如何旋轉?自動旋轉?支援手勢滑動旋轉?快速滑動抬手繼續旋轉?
子佈局如何旋轉
所謂的旋轉就是所有的子佈局繞著圓形移動,佈局一旦移動就代表中間位置改變,根據上面我們計算的子佈局位置的公式來看:
中心點座標
x=width/2+sin(a)*R
y=height/2+cos(a)*R
因為width
和R
都是已知並且定下來的尺寸,所以說,想要改變中心點座標,只需修改 角度a就可以了。要想達到旋轉效果的話就是讓所有的子佈局都同時移動相同的角度即可。
子佈局原始角度值:
double angle = startAngle + averageAngle * i;
我們可以在此基礎上加上一個可變的角度值,通過改變這個值,所有的子佈局都會同時加上此值同時移動了位置。如下:
double angle = startAngle + averageAngle * i + rotateAngle;
其中 rotateAngle 就是可變的值。改變這個值就能讓佈局動起來
自動旋轉
同理,我們只要搞個定時器,週期性修改這個rotateAngle
值,並setState(() {})
重新整理下,看起來就自動旋轉了。
支援手勢滑動旋轉
大家已經知道通過修改rotateAngle
值去實現旋轉,那麼支援手勢滑動旋轉顧名思義就是通過手勢修改這個rotateAngle值就OK,那麼手勢處理Flutter提供了GestureDetector
元件,這個元件功能很強大,這裡面我們使用了他的幾個回撥方法。
本次實現直接使用水平滑動監聽,大家如果想相容豎直滑動可以自己嘗試修改就可以。
GestureDetector(
///水平滑動按下
onHorizontalDragDown: (DragDownDetails details) {...},
///水平滑動開始
onHorizontalDragStart: (DragStartDetails details) {
//記錄拖動開始時當前的選擇角度值
downAngle = rotateAngle;
//記錄拖動開始時的x座標
downX = details.globalPosition.dx;
},
///水平滑動中
onHorizontalDragUpdate: (DragUpdateDetails details) {
//滑動中X座標值
var updateX = details.globalPosition.dx;
//計算當前旋轉角度值並重新整理
rotateAngle = (downX - updateX) * slipRatio + downAngle;
if (mounted) setState(() {});
},
///水平滑動結束
onHorizontalDragEnd: (DragEndDetails details) {...},
///滑動取消
onHorizontalDragCancel: () {...},
behavior: HitTestBehavior.opaque,//deferToChild translucent
child: xxx,
);
快速滑動抬手繼續旋轉
抬手還能繼續旋轉,也就是當我們快速滑動抬手的時候只要繼續修改旋轉角度值rotateAngle
就可以達到繼續旋轉的效果。當我們抬手的時候我們可以拿到什麼呢?
例如:當我們騎著小黃單車在大路上快速的蹬著腳蹬子然後停止蹬,你的小黃已當時的速度飛馳在這個大路上,由於地面的摩擦力的影響,速度會越來越小,最後停止。
///水平滑動結束
onHorizontalDragEnd: (DragEndDetails details) {
//x方向上每秒速度的畫素數
velocityX = details.velocity.pixelsPerSecond.dx;
_controller.reset();
_controller.forward();
},
//動畫設定rotateAngle
_controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: 1000),
);
animation = CurvedAnimation(
parent: _controller,
curve: Curves.linearToEaseOut,
);
animation = new Tween<double>(begin: 1, end: 0).animate(animation)
..addListener(() {
//當前速度
var velocity = animation.value * -velocityX;
var offsetX = radius != 0 ? velocity * 5 / (2 * pi * radius) : velocity;
rotateAngle += offsetX;
setState(() => {});
})
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
rotateAngle = rotateAngle % 360;
_startRotateTimer();
}
});
3.支援X軸旋轉
上圖是X軸方向檢視旋轉切面圖,按照x軸旋轉所有的x座標都是相同的,y值從上往下不斷增加。因為繞著X軸旋轉時,X座標是不變的,Y座標值改變,當旋轉了a角度時,現在的Y座標如圖所示為
Y座標旋轉後=height/2+y*cos(a) y值我們已經在上面計算過了,y=cos(a)*R
所以最終的計算公式是:
Y座標值=height/2+cos(a)*R*cos(a)
cos(a)在a=[0,90]區間時對應的值是1-0 即是 a=0度時cos(a)=1,就是原始狀態(與Y軸平行),a=90度時cos(a)=0,就是與Y軸垂直準狀態。所以我們可以設定a在0-90之間即可。
4.支援前後縮放子佈局(起始角度為前,相對位置為後,最前面最大,反而越小)
上圖為cos餘弦曲線圖。0
度和360
度最大 ,180
度最小,剛好與我們設計的初始值從0開始,然後逆時針繞一圈角度從0-360
度。
所以縮放比scale計算公式可以寫為:
var scale = (1 - minScale) / 2 * (1 + cos(radian(angle - startAngle))) + minScale;
minScale
為最小縮放比,為了讓縮放有個極限值,即 scale範圍為:(minScale,1)
5.多個佈局疊加時前面遮擋後面
從視覺感受,靠近前面的佈局應該遮擋後面的佈局,在Android當中bringToFront()
方法可以讓佈局置於前面,Flutter沒有提供此方法,我們該如何處理這種情況呢?
Flutter提供一個Stack佈局,也叫層疊式佈局,當我們新增子佈局到Stack佈局中時,後面新增的會遮住前面新增的,所以只要我們在新增子佈局的時候按照由後到前來新增即可。話說怎麼知道是前是後呢?
知道實現思路現在要解決的問題是:
如何區分前與後?有什麼條件可以區分?
考慮中...
1、根據座標值?Y座標小就是後面,Y座標大就是前面?
答案是不一定;因為當我啟動角度不是0的時候,比如是90度,那麼最右面是前面,最左邊是後面,這個時候是X座標的大小區分前後關係,所以說單獨使用座標值的大小來決定前後關係是不對的。
2、根據前大後小原則?根據縮放值排序來新增子佈局?
答案是可行;因為我們已經實現了前面的佈局縮放值是1,後面的縮放值越來越小,而且我們已經處理了啟動角度問題,所以根據縮放值來實現是可行的。
///通過縮放值進行排序,從小到大
childPointList.sort((a, b) {
return a.scale.compareTo(b.scale);
});
///遍歷新增子佈局
Stack(
children: childPointList.map(
(Point point) {
return Positioned(
width: point.width,
left: point.left,
top: point.top,
child: this.widget.children[point.index]);
},
).toList(),
),
通過上面方式即可實現前後遮擋效果了。
小知識點
Flutter 之Stack
元件Stack
一個可以疊加子控制元件的佈局,這裡主要講一下Positioned
,其他使用方式可以看下官網說明。
Positioned({
Key key,
this.left,
this.top,
this.right,
this.bottom,
this.width,
this.height,
@required Widget child,
})
使用Positioned
控制Widget的位置,通過Positioned可以隨意擺放一個元件,有點像絕對佈局。其中left
、top
、right
、bottom
分別代表離Stack左、上、右、底四邊的距離。
Flutter之LayoutBuilder 元件
有時我們希望根據元件的大小確認元件的外觀,比如豎屏的時候上下展示,橫屏的時候左右展示,通過LayoutBuilder元件可以獲取父元件的約束尺寸。
附:github連結:https://github.com/yixiaolunhui/my_flutter
原文連結:https://www.jianshu.com/p/4512486fc52b
文末
您的點贊收藏就是對我最大的鼓勵!
歡迎關注我,分享Android乾貨,交流Android技術。
對文章有何見解,或者有何技術問題,歡迎在評論區一起留言討論!