HarmonyOS自定義控件之速度檢測VelocityDetector
一般在涉及到滾動的場景時,我們會用到速度檢測。比如列表滑動時,我們需要拿到手指抬起時的瞬時速度,來做慣性滾動。又比如在滾動翻頁時,我們要根據手指速度來判斷是否翻到下一頁還是繼續保持當頁。
接下來我們就來看看HarmonyOS中的VelocityDetector如何使用。
使用方法
VelocityDetector使用起來還是比較簡單的,主要是分為以下幾步:
- 獲取VelocityDetector實例
- 為VelocityDetector添加TouchEvent
- 計算速度
- 獲取計算后的速度
- 清除已添加的event
獲取實例
通過obtainInstance函數獲取實例:
- VelocityDetector detector = VelocityDetector.obtainInstance();
添加TouchEvent
在控件的TouchEventListener內調用addEvent函數:
- component.setTouchEventListener(new TouchEventListener() {
- @Override
- public boolean onTouchEvent(Component component, TouchEvent ev) {
- detector.addEvent(ev);
- return true;
- }
- });
計算速度
一般情況下,我們需要在手指抬起時計算速度,因為我們需要的是手指抬起后的速度值。因此我們可以在TouchEvent.PRIMARY_POINT_UP時調用calculateCurrentVelocity函數來計算速度:
- 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函數有兩個重載:
- void calculateCurrentVelocity(int units);
- void calculateCurrentVelocity(int units, float maxVxVelocity, float maxVyVelocity)
其中:
- units為單位,1代表像素/毫秒,1000代表像素/秒,以此類推。一般情況下我們都傳1000,獲取的速度代表手指每秒移動多少像素
- maxVxVelocity為橫向最大速度為多少,比如慣性滾動時,如果我們不希望滾動過快,可以設置一個最大速度
- maxVyVelocity為縱向最大速度為多少,比如慣性滾動時,如果我們不希望滾動過快,可以設置一個最大速度
獲取速度
在計算速度之后就能直接獲取速度值了:
- float velocityY = detector.getVerticalVelocity();
- float velocityX = detector.getHorizontalVelocity();
- // 或者獲取速度數組,下標0為橫向速度,下標1為縱向速度
- float[] velocity = detector.getVelocity();
獲取到的速度可能是正值也可能是負值,正負值代表了速度的方向,這個大家可以通過日志自行實驗一下。
清除
最后,我們需要清除前面添加的TouchEvent,為新一輪的事件做準備,避免舊的TouchEvent影響了后續的速度計算。這里我們在獲取到速度后或者CANCEL事件中,就可以調用clear函數:
- if (ev.getAction() == TouchEvent.PRIMARY_POINT_UP) {
- ...
- float[] velocity = detector.getVelocity();
- ...
- detector.clear();
- }
- if (ev.getAction() == TouchEvent.CANCEL) {
- detector.clear();
- }
總結
VelocityDetector目前只能獲取一個手指的速度,在多點觸控的情況下,暫時沒法獲取其他手指的速度。
到此我們就獲取到了手指抬起時的速度了,至于怎么利用這個速度,后續會在慣性滾動相關的文章中講述。接下來我們再來分析一下VelocityDetector存在什么問題。
問題
首先我們來了解一下VelocityDetector的基本原理:
我們通過addEvent將TouchEvent傳遞給VelocityDetector,然后通過calculateCurrentVelocity來計算速度,在這個過程中,VelocityDetector基本上就是通過TouchEvent拿到手指的坐標,然后通過移動距離以及時間來計算速度。當然內部算法遠比說的復雜,但是我們只需要記住一個關鍵變量即可:移動距離。
TouchEvent有兩個函數可以拿到手指坐標來計算距離:getPointerPosition與getPointerScreenPosition。VelocityDetector究竟用的哪一個呢?我們可以通過如下代碼來實驗:
- @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);
- }
- ......
- };
- }
在手指移動過程中,日志如下:
- 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獲取的坐標是相對于父控件的,而不是屏幕的左上角,那么根據getPointerPosition的描述我們有理由猜測:
當被監聽的控件,在手指移動過程中,不斷的改變自己的位置,那么通過getPointerPosition獲取的手指坐標會加上控件的位移量,導致滑動距離計算偏離預期。
下面我們來實驗一下。在父布局中,子控件監聽觸摸事件,通過getPointerPosition獲取手指坐標并計算MOVE與DOWN中坐標的差,并使用setComponentPosition與坐標差改變子控件的位置。
然后我們打印getPointerPosition獲取的y坐標,getPointerScreenPosition獲取的y坐標,以及移動距離,代碼如下:
- 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
可以發現通過getPointerPosition計算出來deltaY是忽大忽小而不是線性增加的,并且與getPointerScreenPosition計算的deltaScreenY對比可以發現,deltaY等于deltaScreenY減去上一次的deltaY。也就證明了:通過getPointerPosition獲取的手指坐標會加上該控件的位移量。
那么這對VelocityDetector有什么影響呢?VelocityDetector計算速度有一個重要的因素就是距離,在這種情況下距離忽大忽小,就會導致速度計算出來的值會小于正常速度,甚至于正負值都不太一樣。
總結一下:當一個控件在該控件的觸摸事件內,改變了自己相對于父控件的位置,那么通過VelocityDetector獲取的速度就會出現誤差。能影響控件位置的函數有setTop(在實驗中setTop未能改變控件的位置,還不確定是為什么)、setContentPosition、setComponentPosition,甚至還包括setTranslationY、setTranslationX。并且如果在該控件的觸摸事件內,父控件改變了位置,也會產生此問題。
在這種情況下,觸摸事件內計算距離的問題好解決,不使用getPointerPosition直接使用getPointerScreenPosition即可。但是VelocityDetector的問題如何解決呢?兩個辦法:代理法與偏移法。
代理法
通過一個TouchEventProxy,內部維護一個TouchEvent,并將其getPointerPosition實現轉發至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) {
- // 轉發至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();
- }
- }
使用起來也很簡單:
- TouchEventProxy proxy = new TouchEventProxy();
- @Override
- public boolean onTouchEvent(Component component, TouchEvent ev) {
- proxy.setEvent(ev);
- detector.addEvent(proxy);
- ......
- return true;
- }
位移法
通過反射TouchEvent發現,其內部含有能設置偏移量的函數,該函數會影響getPointerPosition的值。那么我們就可以在觸摸事件內,對比getPointerPosition與getPointerScreenPosition的差,并通過函數設置偏移,強制使坐標同步。這里只提供位移法的可行并驗證過的思路,代碼大家可以自行嘗試。
對比
既然有方法可以修復速度的問題,那么我們就可以對比修復前與修復后的速度,到底有多少差距。我們定義兩個VelocityDetector實例,一個add代理,一個add原始的event,然后同時獲取速度來看看:
- 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.
慢一點上滑:
- 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
總結
可以發現上滑過程采樣越少(慢速突然變快的情況)兩個速度越接近,但是在下滑過程中,如果速度比較慢甚至會得到一個方向相反的速度。