HarmonyOS自定義控件之速度檢測(cè)VelocityDetector
想了解更多內(nèi)容,請(qǐng)?jiān)L問(wèn):
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)
一般在涉及到滾動(dòng)的場(chǎng)景時(shí),我們會(huì)用到速度檢測(cè)。比如列表滑動(dòng)時(shí),我們需要拿到手指抬起時(shí)的瞬時(shí)速度,來(lái)做慣性滾動(dòng)。又比如在滾動(dòng)翻頁(yè)時(shí),我們要根據(jù)手指速度來(lái)判斷是否翻到下一頁(yè)還是繼續(xù)保持當(dāng)頁(yè)。
接下來(lái)我們就來(lái)看看HarmonyOS中的VelocityDetector如何使用。
使用方法
VelocityDetector使用起來(lái)還是比較簡(jiǎn)單的,主要是分為以下幾步:
- 獲取VelocityDetector實(shí)例
- 為VelocityDetector添加TouchEvent
- 計(jì)算速度
- 獲取計(jì)算后的速度
- 清除已添加的event
獲取實(shí)例
通過(guò)obtainInstance函數(shù)獲取實(shí)例:
- VelocityDetector detector = VelocityDetector.obtainInstance();
添加TouchEvent
在控件的TouchEventListener內(nèi)調(diào)用addEvent函數(shù):
- component.setTouchEventListener(new TouchEventListener() {
- @Override
- public boolean onTouchEvent(Component component, TouchEvent ev) {
- detector.addEvent(ev);
- return true;
- }
- });
計(jì)算速度
一般情況下,我們需要在手指抬起時(shí)計(jì)算速度,因?yàn)槲覀冃枰氖鞘种柑鸷蟮乃俣戎?。因此我們可以在TouchEvent.PRIMARY_POINT_UP時(shí)調(diào)用calculateCurrentVelocity函數(shù)來(lái)計(jì)算速度:
- static final int MAX_VELOCITY = 10000;
- @Override
- public boolean onTouchEvent(Component component, TouchEvent ev) {
- detector.addEvent(ev);
- if (ev.getAction() == TouchEvent.PRIMARY_POINT_UP) {
- detector.calculateCurrentVelocity(1000, MAX_VELOCITY, MAX_VELOCITY);
- }
- return true;
- }
calculateCurrentVelocity函數(shù)有兩個(gè)重載:
- void calculateCurrentVelocity(int units);
- void calculateCurrentVelocity(int units, float maxVxVelocity, float maxVyVelocity)
其中:
- units為單位,1代表像素/毫秒,1000代表像素/秒,以此類推。一般情況下我們都傳1000,獲取的速度代表手指每秒移動(dòng)多少像素
- maxVxVelocity為橫向最大速度為多少,比如慣性滾動(dòng)時(shí),如果我們不希望滾動(dòng)過(guò)快,可以設(shè)置一個(gè)最大速度
- maxVyVelocity為縱向最大速度為多少,比如慣性滾動(dòng)時(shí),如果我們不希望滾動(dòng)過(guò)快,可以設(shè)置一個(gè)最大速度
獲取速度
在計(jì)算速度之后就能直接獲取速度值了:
- float velocityY = detector.getVerticalVelocity();
- float velocityX = detector.getHorizontalVelocity();
- // 或者獲取速度數(shù)組,下標(biāo)0為橫向速度,下標(biāo)1為縱向速度
- float[] velocity = detector.getVelocity();
獲取到的速度可能是正值也可能是負(fù)值,正負(fù)值代表了速度的方向,這個(gè)大家可以通過(guò)日志自行實(shí)驗(yàn)一下。
清除
最后,我們需要清除前面添加的TouchEvent,為新一輪的事件做準(zhǔn)備,避免舊的TouchEvent影響了后續(xù)的速度計(jì)算。這里我們?cè)讷@取到速度后或者CANCEL事件中,就可以調(diào)用clear函數(shù):
- if (ev.getAction() == TouchEvent.PRIMARY_POINT_UP) {
- ...
- float[] velocity = detector.getVelocity();
- ...
- detector.clear();
- }
- if (ev.getAction() == TouchEvent.CANCEL) {
- detector.clear();
- }
總結(jié)
VelocityDetector目前只能獲取一個(gè)手指的速度,在多點(diǎn)觸控的情況下,暫時(shí)沒(méi)法獲取其他手指的速度。
到此我們就獲取到了手指抬起時(shí)的速度了,至于怎么利用這個(gè)速度,后續(xù)會(huì)在慣性滾動(dòng)相關(guān)的文章中講述。接下來(lái)我們?cè)賮?lái)分析一下VelocityDetector存在什么問(wèn)題。
問(wèn)題
首先我們來(lái)了解一下VelocityDetector的基本原理:
我們通過(guò)addEvent將TouchEvent傳遞給VelocityDetector,然后通過(guò)calculateCurrentVelocity來(lái)計(jì)算速度,在這個(gè)過(guò)程中,VelocityDetector基本上就是通過(guò)TouchEvent拿到手指的坐標(biāo),然后通過(guò)移動(dòng)距離以及時(shí)間來(lái)計(jì)算速度。當(dāng)然內(nèi)部算法遠(yuǎn)比說(shuō)的復(fù)雜,但是我們只需要記住一個(gè)關(guān)鍵變量即可:移動(dòng)距離。
TouchEvent有兩個(gè)函數(shù)可以拿到手指坐標(biāo)來(lái)計(jì)算距離:getPointerPosition與getPointerScreenPosition。VelocityDetector究竟用的哪一個(gè)呢?我們可以通過(guò)如下代碼來(lái)實(shí)驗(yàn):
- @Override
- public boolean onTouchEvent(Component component, TouchEvent ev) {
- detector.addEvent(cloneEvent(ev));
- return true;
- }
- private TouchEvent cloneEvent(TouchEvent event) {
- return new TouchEvent() {
- @Override
- public int getIndex() {
- System.out.println(TAG + "getIndex");
- return event.getIndex();
- }
- @Override
- public MmiPoint getPointerPosition(int i) {
- System.out.println(TAG + "getPointerPosition");
- return event.getPointerPosition(i);
- }
- @Override
- public MmiPoint getPointerScreenPosition(int i) {
- System.out.println(TAG + "getPointerScreenPosition");
- return event.getPointerScreenPosition(i);
- }
- ......
- };
- }
在手指移動(dòng)過(guò)程中,日志如下:
- 08-04 17:14:09.296 24871-24871/com.ryan.ohos.parallaxlayout I System.out: ParallaxLayout TouchEvent: getIndex
- 08-04 17:14:09.296 24871-24871/com.ryan.ohos.parallaxlayout I System.out: ParallaxLayout TouchEvent: getPointerPosition
- 08-04 17:14:09.297 24871-24871/com.ryan.ohos.parallaxlayout I System.out: ParallaxLayout TouchEvent: getIndex
- 08-04 17:14:09.297 24871-24871/com.ryan.ohos.parallaxlayout I System.out: ParallaxLayout TouchEvent: getPointerPosition
- 08-04 17:14:09.469 24871-24871/com.ryan.ohos.parallaxlayout I System.out: ParallaxLayout TouchEvent: getIndex
- ....
答案很明顯,VelocityDetector使用的是getPointerPosition。getPointerPosition獲取的坐標(biāo)是相對(duì)于父控件的,而不是屏幕的左上角,那么根據(jù)getPointerPosition的描述我們有理由猜測(cè):
當(dāng)被監(jiān)聽(tīng)的控件,在手指移動(dòng)過(guò)程中,不斷的改變自己的位置,那么通過(guò)getPointerPosition獲取的手指坐標(biāo)會(huì)加上控件的位移量,導(dǎo)致滑動(dòng)距離計(jì)算偏離預(yù)期。
下面我們來(lái)實(shí)驗(yàn)一下。在父布局中,子控件監(jiān)聽(tīng)觸摸事件,通過(guò)getPointerPosition獲取手指坐標(biāo)并計(jì)算MOVE與DOWN中坐標(biāo)的差,并使用setComponentPosition與坐標(biāo)差改變子控件的位置。
然后我們打印getPointerPosition獲取的y坐標(biāo),getPointerScreenPosition獲取的y坐標(biāo),以及移動(dòng)距離,代碼如下:
- Component child = getComponentAt(1);
- child.setTouchEventListener(this);
- int top = child.getTop();
- @Override
- public boolean onTouchEvent(Component component, TouchEvent ev) {
- float y = getY(ev);
- float screenY = getScreenY(ev);
- switch (ev.getAction()) {
- case TouchEvent.PRIMARY_POINT_DOWN:
- downY = y;
- downScreenY = screenY;
- break;
- case TouchEvent.POINT_MOVE:
- float deltaY = y - downY;
- float deltaScreenY = screenY - downScreenY;
- System.out.println(TAG + "y: " + y + " screenY: " + screenY + ", deltaY: " + deltaY + " deltaScreenY" + deltaScreenY);
- moveChildren((int) deltaY);
- break;
- }
- return true;
- }
- private void moveChildren(int deltaY) {
- child.setComponentPosition(0, top + deltaY, child.getWidth(), top + deltaY + child.getHeight());
- }
日志如下:
- y: 1206.0348, screenY: 1905.0348, deltaY: -13.782349, deltaScreenY: -13.782349
- y: 1095.856, screenY: 1781.856, deltaY: -123.96118, deltaScreenY: -136.96118
- y: 1204.7794, screenY: 1780.7794, deltaY: -15.03772, deltaScreenY: -138.03772
- y: 1041.5786, screenY: 1725.5786, deltaY: -178.23853, deltaScreenY: -193.23853
- y: 1094.1056, screenY: 1615.1056, deltaY: -125.71155, deltaScreenY: -303.71155
- y: 972.12244, screenY: 1546.1224, deltaY: -247.6947, deltaScreenY: -372.6947
- y: 1066.4863, screenY: 1518.4863, deltaY: -153.33081, deltaScreenY: -400.3308
- y: 917.2855, screenY: 1463.2855, deltaY: -302.53162, deltaScreenY: -455.53162
- y: 1024.8671, screenY: 1421.8671, deltaY: -194.95007, deltaScreenY: -496.95007
- y: 875.4486, screenY: 1380.4486, deltaY: -344.36853, deltaScreenY: -538.3685
- y: 941.0178, screenY: 1296.0178, deltaY: -278.79932, deltaScreenY: -622.7993
- y: 821.4109, screenY: 1242.4109, deltaY: -398.40625, deltaScreenY: -676.40625
- y: 883.01855, screenY: 1184.0186, deltaY: -336.79858, deltaScreenY: -734.7986
- y: 817.2832, screenY: 1180.2832, deltaY: -402.53394, deltaScreenY: -738.53394
- y: 834.93787, screenY: 1131.9379, deltaY: -384.87927, deltaScreenY: -786.8793
可以發(fā)現(xiàn)通過(guò)getPointerPosition計(jì)算出來(lái)deltaY是忽大忽小而不是線性增加的,并且與getPointerScreenPosition計(jì)算的deltaScreenY對(duì)比可以發(fā)現(xiàn),deltaY等于deltaScreenY減去上一次的deltaY。也就證明了:通過(guò)getPointerPosition獲取的手指坐標(biāo)會(huì)加上該控件的位移量。
那么這對(duì)VelocityDetector有什么影響呢?VelocityDetector計(jì)算速度有一個(gè)重要的因素就是距離,在這種情況下距離忽大忽小,就會(huì)導(dǎo)致速度計(jì)算出來(lái)的值會(huì)小于正常速度,甚至于正負(fù)值都不太一樣。
總結(jié)一下:當(dāng)一個(gè)控件在該控件的觸摸事件內(nèi),改變了自己相對(duì)于父控件的位置,那么通過(guò)VelocityDetector獲取的速度就會(huì)出現(xiàn)誤差。能影響控件位置的函數(shù)有setTop(在實(shí)驗(yàn)中setTop未能改變控件的位置,還不確定是為什么)、setContentPosition、setComponentPosition,甚至還包括setTranslationY、setTranslationX。并且如果在該控件的觸摸事件內(nèi),父控件改變了位置,也會(huì)產(chǎn)生此問(wèn)題。
在這種情況下,觸摸事件內(nèi)計(jì)算距離的問(wèn)題好解決,不使用getPointerPosition直接使用getPointerScreenPosition即可。但是VelocityDetector的問(wèn)題如何解決呢?兩個(gè)辦法:代理法與偏移法。
代理法
通過(guò)一個(gè)TouchEventProxy,內(nèi)部維護(hù)一個(gè)TouchEvent,并將其getPointerPosition實(shí)現(xiàn)轉(zhuǎn)發(fā)至TouchEvent的getPointerScreenPosition中。
- public class TouchEventProxy extends TouchEvent {
- private TouchEvent event;
- public void setEvent(TouchEvent event) {
- this.event = event;
- }
- @Override
- public int getAction() {
- return event.getAction();
- }
- @Override
- public int getIndex() {
- return event.getIndex();
- }
- @Override
- public long getStartTime() {
- return event.getStartTime();
- }
- @Override
- public int getPhase() {
- return event.getPhase();
- }
- @Override
- public MmiPoint getPointerPosition(int i) {
- // 轉(zhuǎn)發(fā)至getPointerScreenPosition
- return event.getPointerScreenPosition(i);
- }
- @Override
- public void setScreenOffset(float v, float v1) {
- event.setScreenOffset(v, v1);
- }
- @Override
- public MmiPoint getPointerScreenPosition(int i) {
- return event.getPointerScreenPosition(i);
- }
- @Override
- public int getPointerCount() {
- return event.getPointerCount();
- }
- @Override
- public int getPointerId(int i) {
- return event.getPointerId(i);
- }
- @Override
- public float getForce(int i) {
- return event.getForce(i);
- }
- @Override
- public float getRadius(int i) {
- return event.getRadius(i);
- }
- @Override
- public int getSourceDevice() {
- return event.getSourceDevice();
- }
- @Override
- public String getDeviceId() {
- return event.getDeviceId();
- }
- @Override
- public int getInputDeviceId() {
- return event.getInputDeviceId();
- }
- @Override
- public long getOccurredTime() {
- return event.getOccurredTime();
- }
- }
使用起來(lái)也很簡(jiǎn)單:
- TouchEventProxy proxy = new TouchEventProxy();
- @Override
- public boolean onTouchEvent(Component component, TouchEvent ev) {
- proxy.setEvent(ev);
- detector.addEvent(proxy);
- ......
- return true;
- }
位移法
通過(guò)反射TouchEvent發(fā)現(xiàn),其內(nèi)部含有能設(shè)置偏移量的函數(shù),該函數(shù)會(huì)影響getPointerPosition的值。那么我們就可以在觸摸事件內(nèi),對(duì)比getPointerPosition與getPointerScreenPosition的差,并通過(guò)函數(shù)設(shè)置偏移,強(qiáng)制使坐標(biāo)同步。這里只提供位移法的可行并驗(yàn)證過(guò)的思路,代碼大家可以自行嘗試。
對(duì)比
既然有方法可以修復(fù)速度的問(wèn)題,那么我們就可以對(duì)比修復(fù)前與修復(fù)后的速度,到底有多少差距。我們定義兩個(gè)VelocityDetector實(shí)例,一個(gè)add代理,一個(gè)add原始的event,然后同時(shí)獲取速度來(lái)看看:
- VelocityDetector detector1 = VelocityDetector.obtainInstance();
- VelocityDetector detector2 = VelocityDetector.obtainInstance();
- TouchEventProxy proxy = new TouchEventProxy();
- @Override
- public boolean onTouchEvent(Component component, TouchEvent ev) {
- proxy.setEvent(ev);
- detector1.addEvent(ev);
- detector2.addEvent(proxy);
- float y = getY(ev);
- switch (ev.getAction()) {
- case TouchEvent.PRIMARY_POINT_DOWN:
- downY = y;
- break;
- case TouchEvent.POINT_MOVE:
- float deltaY = y - downY;
- moveChildren((int) deltaY);
- break;
- case TouchEvent.PRIMARY_POINT_UP:
- detector1.calculateCurrentVelocity(1000);
- detector2.calculateCurrentVelocity(1000);
- System.out.println(TAG + "detector1: " + detector1.getVerticalVelocity() + ", detector2: " + detector2.getVerticalVelocity());
- detector1.clear();
- detector2.clear();
- break;
- }
- return true;
- }
快速上滑:
- 08-05 09:36:29.004 1846-1846/? I System.out: ParallaxLayout TouchEvent: detector1: -5332.0, detector2: -9285.
慢一點(diǎn)上滑:
- 08-05 09:35:39.065 1846-1846/? I System.out: ParallaxLayout TouchEvent: detector1: -1003.0, detector2: -3560.0
先慢速最后快速上滑:
- 08-05 09:37:04.066 1846-1846/? I System.out: ParallaxLayout TouchEvent: detector1: -4176.0, detector2: -4491.0
快速下滑:
- 08-05 09:39:44.785 1846-1846/? I System.out: ParallaxLayout TouchEvent: detector1: 1955.0, detector2: 6660.0
慢速下滑:
- 08-05 09:40:32.813 1846-1846/? I System.out: ParallaxLayout TouchEvent: detector1: 907.0, detector2: 3835.0
先慢速最后快速下滑:
- 08-05 09:39:15.739 1846-1846/? I System.out: ParallaxLayout TouchEvent: detector1: -784.0, detector2: 1937.0
總結(jié)
可以發(fā)現(xiàn)上滑過(guò)程采樣越少(慢速突然變快的情況)兩個(gè)速度越接近,但是在下滑過(guò)程中,如果速度比較慢甚至?xí)玫揭粋€(gè)方向相反的速度。
想了解更多內(nèi)容,請(qǐng)?jiān)L問(wèn):
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)