詳解Flutter手游操縱桿移動(dòng)的原理與實(shí)現(xiàn)
前言
上一篇介紹了手勢(shì)在畫布上的應(yīng)用,那么手勢(shì)與繪制畫布究竟能摩擦出怎樣的火花呢,本篇文章將為你詳解手游中操縱桿移動(dòng)角色的的原理與實(shí)現(xiàn)過(guò)程。
基本思路
確定操縱桿區(qū)域,確定點(diǎn)擊時(shí)手勢(shì)響應(yīng)區(qū)域,當(dāng)手指滑動(dòng)操縱桿時(shí),計(jì)算出當(dāng)前的手指位置與當(dāng)前操縱桿圓心偏移弧度,從而確定當(dāng)前角色的移動(dòng)方向。接下來(lái)就一步一步實(shí)現(xiàn)吧。
繪制
繪制操縱桿的靜態(tài)圖形,玩過(guò)手游應(yīng)該知道操縱桿基本構(gòu)成由底部圓和手指移動(dòng)圓球組成,手指移動(dòng)的圓球圍繞底圓進(jìn)行360°旋轉(zhuǎn)從而控制角色朝不同方向移動(dòng)。
靜態(tài)效果
操縱桿的核心是由兩個(gè)圓形組成,代碼也非常簡(jiǎn)單。
繪制代碼:
// 底圓 canvas.drawCircle( Offset(0,0), bgR, _paint ..style = PaintingStyle.fill ..color = Colors.blue.withOpacity(0.2)); _paint.color = color; _paint.style = PaintingStyle.stroke; /// 手勢(shì)小圓 canvas.drawCircle( Offset(0,0), bgR / 3, _paint ..style = PaintingStyle.fill ..color = Colors.blue.withOpacity(0.9));
添加手勢(shì)交互 GestureDetector
大概思路:
當(dāng)點(diǎn)擊可觸控區(qū)域,將操縱桿移動(dòng)到當(dāng)前手指按下的位置,移動(dòng)手指,根據(jù)手指位置坐標(biāo)和按下時(shí)圓心位置坐標(biāo)計(jì)算偏移角度得出手指相對(duì)于底圓的坐標(biāo)點(diǎn),松開手指,操縱桿進(jìn)行復(fù)位回到初始位置。
手勢(shì)組件
return GestureDetector( child: CustomPaint( size: size, painter: JoyStickPainter( offset: _offset, offsetCenter: _offsetCenter, listenable: Listenable.merge([_offset, _offsetCenter])), ), // 按下 onPanDown: down, // 移動(dòng) onPanUpdate: update, // 抬起 onPanEnd: reset, );
備注:上篇文章介紹了,手指觸控屏幕的坐標(biāo)點(diǎn)永遠(yuǎn)都是以左上角為原點(diǎn)的,為了方便理解和計(jì)算,我們同樣也需要將手指的坐標(biāo)的原點(diǎn)進(jìn)行偏移到畫布中央和畫布保持一致,所以這里我們通過(guò)手勢(shì)獲取的坐標(biāo)點(diǎn)之后需要進(jìn)行偏移。
不管手指點(diǎn)擊、移動(dòng)、還是抬起都要通知畫布進(jìn)行更新,這里使用ValueNotifier<Offset>
通知坐標(biāo)點(diǎn)更新。
ValueNotifier<Offset> _offset = ValueNotifier(Offset.zero);
點(diǎn)擊交互 down: 當(dāng)用戶點(diǎn)擊可觸控區(qū)域,將大圓和小圓移動(dòng)至手指點(diǎn)擊的位置。
因?yàn)榈讏A在點(diǎn)擊之后抬起之前都是處于靜止?fàn)顟B(tài),當(dāng)移動(dòng)手指只有小圓移動(dòng),所以這里用兩個(gè)坐標(biāo)來(lái)保存底圓的圓心,和小圓的圓心,當(dāng)點(diǎn)擊時(shí),底圓和小圓的中心是一致的,所以這里當(dāng)點(diǎn)擊時(shí)同時(shí)更新兩個(gè)圓心位置。
down(DragDownDetails details) { Offset offset = details.localPosition; _offsetCenter.value = offset.translate(-size.width / 2, -size.height / 2); _offset.value = offset.translate(-size.width / 2, -size.height / 2); }
這里需要注意的是,當(dāng)我們的手指點(diǎn)擊在可觸控區(qū)域邊界距離小于底圓半徑時(shí),需要控制圓心位置的x軸和y軸距離可觸控區(qū)域邊界距離大于等于底圓半徑。
如果不控制邊界點(diǎn)擊時(shí),操縱桿會(huì)偏離出觸控區(qū)域
所以這里最好在點(diǎn)擊時(shí)可以加一個(gè)邊界處理,上下左右加一個(gè)邊界控制。
if (offset.dx > size.width - bgR) { offset = Offset(size.width - bgR, offset.dy); } if (offset.dx < bgR) { offset = Offset(bgR, offset.dy); } if (offset.dy > size.height - bgR) { offset = Offset(offset.dx, size.height - bgR); } if (offset.dy < bgR) { offset = Offset(offset.dx, bgR); }
之后再點(diǎn)擊邊界時(shí)就不會(huì)出界了。
移動(dòng)交互 update: 當(dāng)用戶移動(dòng)手指時(shí),小圓根據(jù)手指在底圓內(nèi)部進(jìn)行移動(dòng)。
手指移動(dòng)是操縱桿的核心交互邏輯。
思路: 當(dāng)手指點(diǎn)擊之后移動(dòng)離開圓心,計(jì)算當(dāng)前坐標(biāo)點(diǎn)以當(dāng)前底圓圓心為原點(diǎn)的偏移弧度,通過(guò)反正切函數(shù)atan2(y,x)
可以得出當(dāng)前坐標(biāo)針對(duì)x軸向右為正,y軸向下為正的偏移弧度α
,默認(rèn)范圍 [-pi]-[pi]
, 為了方便理解計(jì)算,這里我們將得到的角度+pi
轉(zhuǎn)換為 0-2pi,角度范圍:0-360°
。見(jiàn)下圖:
Offset
類里的direction(y,x)
方法就是通過(guò)atan2
方法計(jì)算當(dāng)前坐標(biāo)的偏移弧度。
/// The angle of this offset as radians clockwise from the positive x-axis, in /// the range -[pi] to [pi], assuming positive values of the x-axis go to the /// right and positive values of the y-axis go down. double get direction => math.atan2(dy, dx);
角色移動(dòng)的關(guān)鍵就是通過(guò)得出的偏移弧度來(lái)進(jìn)行不同方向的移動(dòng)。
核心代碼:
/// 手指移動(dòng)坐標(biāo) var offsetTranslate = offset.value; /// 操縱桿圓心坐標(biāo) var offsetTranslateCenter = offsetCenter.value; /// 計(jì)算當(dāng)前位置坐標(biāo)點(diǎn) 左半?yún)^(qū)域 X為負(fù)數(shù) double x = offsetTranslateCenter.dx - offsetTranslate.dx; /// y軸 下半?yún)^(qū)域 Y為負(fù)數(shù) double y = offsetTranslateCenter.dy - offsetTranslate.dy; /// 反正切函數(shù) 通過(guò)此函數(shù)可以計(jì)算出此坐標(biāo)旋轉(zhuǎn)的弧度 為正 代表X軸逆時(shí)針旋轉(zhuǎn)的角度 為負(fù) 順時(shí)針旋轉(zhuǎn)角度 /// 范圍 [-pi] - [pi] double ata = atan2(y, x); /// 默認(rèn)坐標(biāo)系范圍為-pi - pi 順時(shí)針旋轉(zhuǎn)坐標(biāo)系180度 變?yōu)?0 - 2*pi; var thta = ata + pi; print("angle ${(180 / pi * thta).toInt()}");
這里手指移動(dòng)分為2種情況,手指在底圓內(nèi)部和手指在底圓外部。見(jiàn)下圖:
當(dāng)手指在底圓內(nèi)部,我們可以直接使用當(dāng)前手指?jìng)鬟f的坐標(biāo)計(jì)算。
當(dāng)手指移動(dòng)到底圓外部,我們需要控制小圓的圓形坐標(biāo)不能跑到底圓的外部,控制小圓 不能超過(guò)底圓的的范圍,所以,這里需要進(jìn)行計(jì)算當(dāng)前手指的坐標(biāo)距離底圓圓心的距離有沒(méi)有超過(guò)底圓半徑,如果超出,需要計(jì)算小圓的臨界坐標(biāo)值。
有了偏移弧度α
,我們就可以通過(guò)三角函數(shù)計(jì)算出上面x1,y1的坐標(biāo)點(diǎn)
,也就是當(dāng)前手指控制小圓圓心的臨界坐標(biāo)。
核心代碼:
/// 當(dāng)前手指坐標(biāo)距離底圓圓心長(zhǎng)度 var r = sqrt(pow(x, 2) + pow(y, 2)); if (r > bgR) { var dx = bgR * cos(thta) + offsetTranslateCenter.dx; // x軸坐標(biāo)點(diǎn) var dy = bgR * sin(thta) + offsetTranslateCenter.dy; // y軸坐標(biāo)點(diǎn) offsetTranslate = Offset(dx, dy); }
松開交互 reset: 當(dāng)用戶點(diǎn)擊可觸控區(qū)域,將大圓和小圓移動(dòng)至手指點(diǎn)擊的位置。
將兩個(gè)圓的圓心回歸坐標(biāo)系原點(diǎn)。
reset(DragEndDetails details) { _offset.value = Offset.zero; _offsetCenter.value = Offset.zero; }
注意的是,當(dāng)點(diǎn)擊和松開時(shí),當(dāng)前角色都是不動(dòng)的,只有當(dāng)移動(dòng)時(shí)才傳遞角度值賦給角色進(jìn)行移動(dòng),所以當(dāng)這里需要判斷當(dāng)前手指觸控點(diǎn)和底圓圓心是否重合,如果重合表示當(dāng)前角色處于靜止?fàn)顟B(tài)。因?yàn)槟J(rèn)不作處理,弧度獲取的是pi
,所以這里需要特殊處理一下。 這里我們需要將獲取的弧度值傳遞出去,如果當(dāng)前處于靜止?fàn)顟B(tài),將弧度設(shè)為負(fù)數(shù),因?yàn)槲覀兊幕《确秶?code>0-2pi,移動(dòng)狀態(tài)中不可能為負(fù)。
if (x == 0 && y == 0) { onAngle?.call(-1); } else { onAngle?.call(thta); }
為了方便展示效果,我加了坐標(biāo)軸輔助,這樣看起來(lái)更直觀一些。
最終效果:
通過(guò)當(dāng)前獲取的弧度值即可傳遞給角色進(jìn)行移動(dòng)。
總結(jié)
本篇文章主要介紹了操縱桿如何向角色傳遞有效信息從而控制角色移動(dòng),其實(shí)操縱桿的實(shí)現(xiàn)邏輯并不復(fù)雜,主要難點(diǎn)集中在手指移動(dòng)計(jì)算偏移弧度哪里,還有就是小圓球的邊界處理,掌握了這兩點(diǎn),也就掌握了核心邏輯。
到此這篇關(guān)于詳解Flutter手游操縱桿移動(dòng)的原理與實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)Flutter手游操縱桿移動(dòng)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android 第三方應(yīng)用接入微信平臺(tái)研究情況分享(一)
微信平臺(tái)開放后倒是挺火的,許多第三方應(yīng)用都想試下接入微信這個(gè)平臺(tái),畢竟可以利用微信建立起來(lái)的關(guān)系鏈來(lái)拓展自己的應(yīng)用還是挺不錯(cuò)的 最近由于實(shí)習(xí)需要也在研究這個(gè)東西,這里把我的整個(gè)研究情況給出來(lái)2013-01-01Android中AlertDialog 點(diǎn)擊按鈕后不關(guān)閉對(duì)話框的功能
本篇文章主要介紹了Android中AlertDialog 點(diǎn)擊按鈕后不關(guān)閉對(duì)話框的功能,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2017-04-04android動(dòng)態(tài)壁紙調(diào)用的簡(jiǎn)單實(shí)例
動(dòng)態(tài)壁紙的實(shí)現(xiàn)其實(shí)就是在Activity中調(diào)用動(dòng)態(tài)壁紙服務(wù),通過(guò)綁定服務(wù)得到IWallpaperService,調(diào)用該接口中的attach函數(shù)實(shí)現(xiàn)壁紙的調(diào)用。2013-06-06android自定義控件實(shí)現(xiàn)簡(jiǎn)易時(shí)間軸(2)
這篇文章主要為大家詳細(xì)介紹了android自定義控件實(shí)現(xiàn)簡(jiǎn)易時(shí)間軸的第二篇,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01Android之使用Android-query框架開發(fā)實(shí)戰(zhàn)(二)
這篇文章主要介紹了Android之使用Android-query框架開發(fā)實(shí)戰(zhàn)(二)的相關(guān)資料,需要的朋友可以參考下2015-10-10解決Android Studio4.1沒(méi)有Gsonfomat插件,Plugin “GsonFormat” is inco
這篇文章主要介紹了解決Android Studio4.1沒(méi)有Gsonfomat插件,Plugin “GsonFormat” is incompatible (supported only in IntelliJ IDEA)的問(wèn)題 ,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2020-12-12Kotlin中Lambda表達(dá)式與高階函數(shù)使用分析講解
lambda 本質(zhì)上是可以傳遞給函數(shù)的一小段代碼,Kotlin 與 Java 中的 Lambda 有一定的區(qū)別,除了對(duì) lambda 的全面支持外,還有內(nèi)聯(lián)函數(shù)等簡(jiǎn)潔高效的特性。下面我們來(lái)仔細(xì)看一下2022-12-12Android自定義view系列之99.99%實(shí)現(xiàn)QQ側(cè)滑刪除效果實(shí)例代碼詳解
這篇文章給大家介紹android自定義view系列之99.99%實(shí)現(xiàn)QQ側(cè)滑刪除效果,本文介紹的非常詳細(xì),具有參考借鑒價(jià)值,需要的朋友參考下吧2016-09-09Android自定義HorizontalScrollView打造超強(qiáng)Gallery效果
這篇文章主要介紹了Android自定義HorizontalScrollView打造圖片橫向滑動(dòng)效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-05-05