日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

View觸發機制API實現GestureDetector?OverScroller詳解_Android

作者:陳序猿_Android ? 更新時間: 2022-12-21 編程語言

前言

前一篇文章講了View的觸發反饋機制的原理,對于一個自定義View而言,手勢的處理都是重寫onTouchEvent函數,或者通過setOnTouchEventListener方法捕捉手勢。但是手勢的處理,如滑動、觸摸、雙擊等檢測對應的檢測也并不是那么簡單,自己一個個造輪子也過于麻煩,萬幸的是google早已經給開發者提供了手勢捕捉的類- GestureDetector。通過這個類我們可以識別很多的手勢,主要是通過他的onTouchEvent(event)方法完成了不同手勢的識別。雖然他能識別手勢,但是不同的手勢要怎么處理,應該是提供給程序員實現的。

GestureDetector

GestureDetector 中一共有三種主要的回調接口 ,OnGestureListenerOnDoubleTapListener、OnContextClickListener

這三個接口的方法如下。

public interface OnGestureListener {
        boolean onDown(MotionEvent e);
        void onShowPress(MotionEvent e);
        boolean onSingleTapUp(MotionEvent e);
        boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
        void onLongPress(MotionEvent e);
        boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
}
public interface OnDoubleTapListener {
    boolean onSingleTapConfirmed(MotionEvent e);
    boolean onDoubleTap(MotionEvent e);
    boolean onDoubleTapEvent(MotionEvent e);
}
public interface OnContextClickListener {
    boolean onContextClick(MotionEvent e);
}

GestureDetector 使用

GestureDector 負責監聽手勢,而 OnDoubleTapListener、OnGestureListener 用于開發者自己去處理對應手勢的反饋

package com.example.androidtemp.view;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.widget.OverScroller;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
public class TouchView extends View implements GestureDetector.OnGestureListener,GestureDetector.OnDoubleTapListener{
    private static final String TAG = "TouchView";
    GestureDetector gestureDetector = null;
    public TouchView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        gestureDetector = new GestureDetector(context,this);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return gestureDetector.onTouchEvent(event);
    }
    @Override
    public boolean onSingleTapConfirmed(MotionEvent e) {
        Log.i(TAG, "onSingleTapConfirmed: ");
        return false;
    }
    @Override
    public boolean onDoubleTap(MotionEvent e) {
        Log.i(TAG, "onDoubleTap: ");
        return false;
    }
    @Override
    public boolean onDoubleTapEvent(MotionEvent e) {
        Log.i(TAG, "onDoubleTapEvent: ");
        return false;
    }
    @Override
    public boolean onDown(MotionEvent e) {
        Log.d(TAG, "onDown: ");
        return true;
    }
    @Override
    public void onShowPress(MotionEvent e) {
        Log.i(TAG, "onShowPress: ");
    }
    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        Log.i(TAG, "onSingleTapUp: ");
        return false;
    }
    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        Log.i(TAG, "onScroll: ");
        return false;
    }
    @Override
    public void onLongPress(MotionEvent e) {
        Log.i(TAG, "onLongPress: ");
    }
    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        Log.i(TAG, "onFling: ");
        return false;
    }
}

onDown方法

onDown 方法是在ACTION_DOWN 事件時被調用的,其的返回值決定了View是否消費該事件,一般我們肯定是需要消費該事件的,因此其值為true.

public boolean onDown() {
    return true;
}

onShowPress方法

@Override
public void onShowPress(MotionEvent e) {
    //進行控件顏色的改變或其他一些動作
}

onShowPress 是用戶按下時的一種回調,主要作用是用于給用戶一種按壓下的狀態,可以在該回調中讓控件顏色改變或進行一些動作。需要注意的是,onShowPress 方法不是立即回調的,在手指觸碰后,在100ms左右后才會回調。在這100ms內如果手指抬起或滾動,該回調方法不會被觸發。在前一篇文章View事件分發機制 中提到過自定義View 默認的super.onTouchEvent 實現中,按壓狀態也是有一個預按壓狀態的檢測,此處的onShowPress的回調機制也是同理。

onLongPress 方法

用于檢測長按事件的,即手指按下后不抬起,在一段時間后會觸發該事件。

@Override 
public void onLongPress(MotionEvent e) {
}

onLongPress 回調被觸發前 onShowPress 一定會被觸發。

需要注意的是 onLongPress一旦被觸發,其他事件都不會被觸發了。

不過,onLongPress事件可以被禁止使用,通過如下代碼設置,即不會觸發長按事件

gestureDetector.setIsLongpressEnabled(false);

onSingleTapUp 方法

@Override
public boolean onSingleTapUp(MotionEvent e) {
    return false;
}

onSingleTapUP的返回值不是太重要,不過一般消費了就還是返回ture吧。

onSingleTapUp的意思顧名思義,即在 手指抬起時觸發,不過他跟一般的onClick、以及onSingleTapConfirmed有一定區別

單擊事件觸發:

GCS: onSingleTapUp
GCS: onClick
GCS: onSingleTapConfirmed
類型 觸發次數 摘要
onSingleTapUp 1 單擊抬起
onSingleTapConfirmed 1 單擊確認
onClick 1 單擊事件

雙擊事件觸發:

onSingleTapUp
onClick
onDoubleTap 
onClick
類型 觸發次數 摘要
onSingleTapUp 1 在雙擊的第一次抬起時觸發
onSingleTapConfirmed 0 雙擊發生時不會觸發。
onClick 2 在雙擊事件時觸發兩次。

可以看出來這三個事件還是有所不同的,根據自己實際需要進行使用即可

onScroll

@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float 
        distanceY) {
    return true;
}

onScroll 方法是用于監聽手指的滑動的,e1是第一次ACTION_DOWN的事件,e2是當前滾動事件。distanceX、distanceY記錄了手指在x、y軸滑動的距離。

需要注意的時,該滑動距離記錄的是上次滑動回調與這次回調之間的距離差值。且還有一個有意思的注意事項,該差值是 lastEvent-curEvent 得到的,這與正常的邏輯行為不太一致,不過google就這樣干了,所以當我們在計算滑動偏移量時需要對 distanceX、distancesY進行一個 相減的操作而不是相加。

onFling

public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
                       float velocityY) {
    return true;
}

用戶手指在屏幕快速滑動后,在抬起時(ACTION_UP)觸發該事件。

Fling 中文直接翻譯過來就是一扔、拋、甩,最常見的場景就是在 ListView 或者 RecyclerView 上快速滑動時手指抬起后它還會滾動一段時間才會停止。onFling 就是檢測這種手勢的。

四個參數的介紹如下

參數 簡介
e1 手指按下時的 Event。
e2 手指抬起時的 Event。
velocityX 在 X 軸上的運動速度(像素/秒)。
velocityY 在 Y 軸上的運動速度(像素/秒)。

利用 velocityX、velocityY 參數可以實現一個具有一定初速度的滑動,之后該速度隨著滑動衰減,直到停止。

一般onFling 可以結合 OverScroller 實現一個均勻減速的滑動效果。

overScroller的用法在后方介紹。

onSingleTapConfirmed 和onDoubleTap

public boolean onSingleTapConfirmed(MotionEvent e) {
    return false;
}
public boolean onDoubleTap(MotionEvent e) {
    return false;
}
public boolean onDoubleTapEvent(MotionEvent e) {
    return false;
}

onSingleTapConfirmed用于監聽單擊事件,而onDoubleTap用于監聽雙擊事件。這兩個回調函數是互斥的。

onSingleTapConfigrmed的調用是延遲的,其在 手指按下300ms后觸發。

onSingleTapConfigrmed 適合于在 既檢測單擊事件也檢測雙擊時間時使用。

但是如果只是檢測單擊事件,onSingleTapUp更合適,onSingleTapConfigrmed會讓用戶明顯感覺到延遲。

需要注意的是 onDoubleTap 事件并不是第二次抬起時觸發的,而是第二次手觸摸到屏幕時即(第二次ACTION_DOWN)事件時就會觸發該事件,如果要保證在第二次抬起時才觸發該事件,就需要使用onDoubleTapEvent方法了

onDoubleTapEvent

@Override
public boolean onDoubleTapEvent(MotionEvent e) {
    Log.i(TAG, "onDoubleTapEvent: event:" + e.getActionMasked());
    switch (e.getActionMasked()) {
        case MotionEvent.ACTION_UP:
            Log.i(TAG, "onDoubleTapEvent: ACTION_UP");
            break;
    }
    return true;
}

雙擊時,onDoubleTapEvent 將會在onDoubleTap 后觸發.

雙擊觸發日志:

TouchView: onDown: 
TouchView: onSingleTapUp: 
TouchView: onDoubleTap: 
TouchView: onDoubleTapEvent: event:0(ACTION_DOWN)
TouchView: onDown: 
TouchView: onDoubleTapEvent: event:2(ACTION_MOVE)
TouchView: onDoubleTapEvent: event:2(ACTION_MOVE)
TouchView: onDoubleTapEvent: event:1(ACTION_UP)
TouchView: onDoubleTapEvent: ACTION_UP

需要注意的是不論是雙擊還是單擊,只要按下長時間未動且未抬起,都會觸發onLongPress。

第二次按下后常按再抬起日志

TouchView: onDown: 
TouchView: onSingleTapUp: 
TouchView: onDoubleTap: 
TouchView: onDoubleTapEvent: event:0
TouchView: onDown: 
TouchView: onDoubleTapEvent: event:2
TouchView: onDoubleTapEvent: event:2
TouchView: onDoubleTapEvent: event:2
TouchView: onShowPress: 
TouchView: onDoubleTapEvent: event:2
TouchView: onDoubleTapEvent: event:2
TouchView: onDoubleTapEvent: event:2
TouchView: onLongPress: 
ouchView: onDoubleTapEvent: event:1
TouchView: onDoubleTapEvent: ACTION_UP

OverScroller

onFling 方法中,曾說過 使用velocityX ,velocityY 兩個參數可以實現 View的滑動效果.

public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
                       float velocityY) {
    return true;
}

示例

此處用一個可拖拉滑動的小圓球作為示例.

scroll效果圖

Fling效果圖

代碼如下

package com.example.androidtemp.view
import android.view.View
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.util.AttributeSet
import android.util.Log
import android.view.GestureDetector
import android.view.MotionEvent
import android.widget.OverScroller
import kotlin.math.max
import kotlin.math.min
private const val TAG = "SmallBallView"
class SmallBallView(context: Context?, attrs:AttributeSet?) :View(context,attrs) ,GestureDetector.OnGestureListener{
    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
    private val BALL_DIAMETER_SIZE = 100 //球直徑長度
    private var originOffsetX = 0f
    private var originOffsetY = 0f
    private var offsetX = 0f
    private var offsetY = 0f
    private val gestureDetector = GestureDetector(this.context,this)
    private val scroller = OverScroller(this.context)
    override fun onTouchEvent(event: MotionEvent): Boolean {
        return gestureDetector.onTouchEvent(event);
    }
    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        originOffsetX = (w - BALL_DIAMETER_SIZE)/2f
        originOffsetY = (h - BALL_DIAMETER_SIZE)/2f
    }
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        // 偏移
        canvas.translate(offsetX,offsetY)
        //中間位置畫個圓
        canvas.drawArc(originOffsetX,originOffsetY,originOffsetX + BALL_DIAMETER_SIZE.toFloat(),originOffsetY + BALL_DIAMETER_SIZE.toFloat(),0f,360f,false,paint)
    }
    override fun onDown(e: MotionEvent?): Boolean = true
    override fun onShowPress(e: MotionEvent?) {}
    override fun onSingleTapUp(e: MotionEvent?): Boolean {
        return false
    }
    override fun onLongPress(e: MotionEvent?) {}
    override fun onScroll(
        e1: MotionEvent?,
        e2: MotionEvent?,
        distanceX: Float,
        distanceY: Float
    ): Boolean  {
        Log.i(TAG, "onScroll: ")
        offsetX -= distanceX
        offsetY -= distanceY
        //移動不能超過圓的一半
        offsetX = min(offsetX,width.toFloat()/2)
        offsetX = max(offsetX,-width.toFloat()/2)
        //移動不能超過圓的一半
        offsetY = min(offsetY,height.toFloat()/2)
        offsetY = max(offsetY,-height.toFloat()/2)
        invalidate()
        return true;
    }
    override fun onFling(
        e1: MotionEvent?,
        e2: MotionEvent?,
        velocityX: Float,
        velocityY: Float
    ): Boolean {
        //限制滑動不能超過一小圓的一半
        scroller.fling(offsetX.toInt(),offsetY.toInt(),velocityX.toInt(),velocityY.toInt(),-width/2,width/2,-height/2,height/2)
        postOnAnimation(scrollerRunnable)
        return true;
    }
    private val scrollerRunnable = object :Runnable {
        override fun run() {
            if (scroller.computeScrollOffset()) {
                offsetX = scroller.currX.toFloat()
                offsetY = scroller.currY.toFloat()
                invalidate()
                postOnAnimation(this)
            }
        }
    }
}

OverScroller方法介紹

  • fling 方法
public void fling(int startX, int startY, int velocityX, int velocityY,
            int minX, int maxX, int minY, int maxY) {
    fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, 0, 0);
}
public void fling(int startX, int startY, int velocityX, int velocityY,
            int minX, int maxX, int minY, int maxY, int overX, int overY) {
    //實現邏輯省略,有興趣的可以自己去看代碼
}
參數 簡介
startX、startY 開始滑動的X(Y)軸位置
velocityX、velocityY 在 X(Y) 軸上的運動速度(像素/秒)。
minX、maxX 滑動時X軸的兩個邊界值,滑動時一旦到達邊界值,則立刻停止
minY、maxY 滑動時Y軸的兩個邊界值,滑動時一旦到達邊界值,則立刻停止
overX、overY 在滑動時,可超出的滑動值,可超過邊界值,不過超過邊界值后,又會重新滑動回來
  • startScroll 方法

startScroll的滾動默認以一種粘性液體的效果進行滾動。

public void startScroll(int startX, int startY, int dx, int dy) {
    startScroll(startX, startY, dx, dy, DEFAULT_DURATION);//DEFAULT_DURATION 250 ms
}
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
    mMode = SCROLL_MODE;
    mScrollerX.startScroll(startX, dx, duration);
    mScrollerY.startScroll(startY, dy, duration);
}
參數 簡介
startX、startY 開始滑動的X(Y)軸位置
dx、dy 滾動到達的目標位置
duration 滾動花費時間(單位ms),如果不指定默認時250ms

原文鏈接:https://juejin.cn/post/7167720906540711966

欄目分類
最近更新