管理 ViewGroup 中的觸控事件

處理觸控事件中的 ViewGroup特別照顧 因為 ViewGroup 包含具有不同目標的子項是很常見的情況 不是 ViewGroup 本身的觸控事件。為了確保每項資料檢視正確收到 指定的觸控事件,則會覆寫 onInterceptTouchEvent() 方法。

攔截 ViewGroup 中的觸控事件

偵測到觸控事件時,系統就會呼叫 onInterceptTouchEvent() 方法 ViewGroup 的表面,包括其子項的表面。如果 onInterceptTouchEvent() 會傳回 trueMotionEvent ,表示該物件不會傳遞至子項,而是傳送至 onTouchEvent() 方法。

onInterceptTouchEvent() 方法可讓家長查看觸控事件 如果從 onInterceptTouchEvent() 退貨:true, 先前處理觸控事件的子項檢視畫面會收到 ACTION_CANCEL, 之後的事件會傳送到父項的 onTouchEvent() 方法 以便照常處理onInterceptTouchEvent() 也可以傳回 false 和 監控事件向下瀏覽檢視區塊階層至其平時的目標,從而處理 以及自己的 onTouchEvent() 的活動

在以下程式碼片段中,MyViewGroup 類別會擴充 ViewGroupMyViewGroup 包含多個子檢視畫面。在孩童檢視模式上拖曳手指 子項檢視畫面不會再擷取觸控事件,而 MyViewGroup 會處理觸控事件 就會觸發這類事件不過,如果您在孩童檢視畫面中輕觸按鈕,或捲動子項, 垂直檢視畫面,父項不會攔截這些觸控事件,因為子項是必要的 目標。在這種情況下,onInterceptTouchEvent() 會傳回 false,而 系統不會呼叫 MyViewGroup 類別的 onTouchEvent()

Kotlin

class MyViewGroup @JvmOverloads constructor(
        context: Context,
        private val mTouchSlop: Int = ViewConfiguration.get(context).scaledTouchSlop
) : ViewGroup(context) {
    ...
    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        // This method only determines whether you want to intercept the motion.
        // If this method returns true, onTouchEvent is called and you can do
        // the actual scrolling there.
        return when (ev.actionMasked) {
            // Always handle the case of the touch gesture being complete.
            MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
                // Release the scroll.
                mIsScrolling = false
                false // Don't intercept the touch event. Let the child handle it.
            }
            MotionEvent.ACTION_MOVE -> {
                if (mIsScrolling) {
                    // You're currently scrolling, so intercept the touch event.
                    true
                } else {

                    // If the user drags their finger horizontally more than the
                    // touch slop, start the scroll.

                    // Left as an exercise for the reader.
                    val xDiff: Int = calculateDistanceX(ev)

                    // Touch slop is calculated using ViewConfiguration constants.
                    if (xDiff > mTouchSlop) {
                        // Start scrolling!
                        mIsScrolling = true
                        true
                    } else {
                        false
                    }
                }
            }
            ...
            else -> {
                // In general, don't intercept touch events. The child view
                // handles them.
                false
            }
        }
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        // Here, you actually handle the touch event. For example, if the action
        // is ACTION_MOVE, scroll this container. This method is only called if
        // the touch event is intercepted in onInterceptTouchEvent.
        ...
    }
}

Java

public class MyViewGroup extends ViewGroup {

    private int mTouchSlop;
    ...
    ViewConfiguration vc = ViewConfiguration.get(view.getContext());
    mTouchSlop = vc.getScaledTouchSlop();
    ...
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        // This method only determines whether you want to intercept the motion.
        // If this method returns true, onTouchEvent is called and you can do
        // the actual scrolling there.

        final int action = MotionEventCompat.getActionMasked(ev);

        // Always handle the case of the touch gesture being complete.
        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
            // Release the scroll.
            mIsScrolling = false;
            return false; // Don't intercept touch event. Let the child handle it.
        }

        switch (action) {
            case MotionEvent.ACTION_MOVE: {
                if (mIsScrolling) {
                    // You're currently scrolling, so intercept the touch event.
                    return true;
                }

                // If the user drags their finger horizontally more than the
                // touch slop, start the scroll.

                // Left as an exercise for the reader.
                final int xDiff = calculateDistanceX(ev);

                // Touch slop is calculated using ViewConfiguration constants.
                if (xDiff > mTouchSlop) {
                    // Start scrolling.
                    mIsScrolling = true;
                    return true;
                }
                break;
            }
            ...
        }

        // In general, don't intercept touch events. The child view handles them.
        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        // Here, you actually handle the touch event. For example, if the
        // action is ACTION_MOVE, scroll this container. This method is only
        // called if the touch event is intercepted in onInterceptTouchEvent.
        ...
    }
}

請注意,ViewGroup 也提供 requestDisallowInterceptTouchEvent() 方法。如果子項不需要父項及其父項,ViewGroup 會呼叫這個方法 祖系,透過 onInterceptTouchEvent() 攔截觸控事件。

處理 ACTION_OUTSIDE 事件

如果 ViewGroup 收到包含 MotionEvent ACTION_OUTSIDE, 事件預設不會分派給其子項如要處理 MotionEvent ACTION_OUTSIDE,任一覆寫值 dispatchTouchEvent(MotionEvent event) 派出至適當的 View 處理 Window.Callback—用於 例如 Activity

使用 ViewConfiguration 常數

上述程式碼片段使用目前的 ViewConfiguration 初始化變數 名為 mTouchSlop。您可以使用 ViewConfiguration 類別存取 常見的距離、速度和時間。

「觸控筆」是指使用者的輕觸手勢在手勢結束之前的距離 (以像素為單位) 會解讀為捲動通常用於防止使用者意外捲動 目前正在執行其他觸控操作,例如輕觸螢幕上的元素。

以下是另外兩種常用的 ViewConfiguration 方法: getScaledMinimumFlingVelocity()getScaledMaximumFlingVelocity()。 這些方法分別傳回最小和最大速率,以啟動測量的快速滑過量 以每秒像素為單位例如:

Kotlin

private val vc: ViewConfiguration = ViewConfiguration.get(context)
private val mSlop: Int = vc.scaledTouchSlop
private val mMinFlingVelocity: Int = vc.scaledMinimumFlingVelocity
private val mMaxFlingVelocity: Int = vc.scaledMaximumFlingVelocity
...
MotionEvent.ACTION_MOVE -> {
    ...
    val deltaX: Float = motionEvent.rawX - mDownX
    if (Math.abs(deltaX) > mSlop) {
        // A swipe occurs, do something.
    }
    return false
}
...
MotionEvent.ACTION_UP -> {
    ...
    if (velocityX in mMinFlingVelocity..mMaxFlingVelocity && velocityY < velocityX) {
        // The criteria are satisfied, do something.
    }
}

Java

ViewConfiguration vc = ViewConfiguration.get(view.getContext());
private int mSlop = vc.getScaledTouchSlop();
private int mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
private int mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
...
case MotionEvent.ACTION_MOVE: {
    ...
    float deltaX = motionEvent.getRawX() - mDownX;
    if (Math.abs(deltaX) > mSlop) {
        // A swipe occurs, do something.
    }
...
case MotionEvent.ACTION_UP: {
    ...
    } if (mMinFlingVelocity <= velocityX && velocityX <= mMaxFlingVelocity
            && velocityY < velocityX) {
        // The criteria are satisfied, do something.
    }
}

展開子項檢視畫面的可觸控區域

Android 提供 只需使用 TouchDelegate 類別即可 可讓父項將子項檢視畫面的可觸控區域延伸至子項邊界之外。這個 如果孩子必須小,但需要更大的觸控區域,那麼這項功能就非常實用。您還可以使用 縮短孩子觸控區域。

在以下範例中 ImageButton是_委派代表 view_—也就是由父項延伸的觸控區域子項。以下是版面配置檔案:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/parent_layout"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     tools:context=".MainActivity" >

     <ImageButton android:id="@+id/button"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:background="@null"
          android:src="@drawable/icon" />
</RelativeLayout>

下列程式碼片段已完成這些工作:

  • 取得父母檢視模式並張貼 Runnable 發出相關的要求這可確保父項在呼叫 getHitRect() 方法。getHitRect() 方法會取得子項的命中矩形 (或 可輕觸區域)。
  • 找出 ImageButton 子項檢視畫面並呼叫 getHitRect(),取得 兒童可觸控區域的邊界。
  • 擴充 ImageButton 子項檢視區塊命中矩形的邊界。
  • TouchDelegate 例項化,並傳入已展開的點擊矩形和 ImageButton 子項檢視畫面做為參數。
  • 設定父項檢視畫面的 TouchDelegate,以便在觸控委派代表內部輕觸 上下限會轉送到子項。

其容量為 ImageButton 子項檢視畫面的觸控委派,以及父項檢視畫面 接收所有觸控事件。如果觸控事件在子項的點擊矩形內發生,系統會為父項 將觸控事件傳送給子項進行處理。

Kotlin

public class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Post in the parent's message queue to make sure the parent lays out
        // its children before you call getHitRect().
        findViewById<View>(R.id.parent_layout).post {
            // The bounds for the delegate view, which is an ImageButton in this
            // example.
            val delegateArea = Rect()
            val myButton = findViewById<ImageButton>(R.id.button).apply {
                isEnabled = true
                setOnClickListener {
                    Toast.makeText(
                            this@MainActivity,
                            "Touch occurred within ImageButton touch region.",
                            Toast.LENGTH_SHORT
                    ).show()
                }

                // The hit rectangle for the ImageButton.
                getHitRect(delegateArea)
            }

            // Extend the touch area of the ImageButton beyond its bounds on the
            // right and bottom.
            delegateArea.right += 100
            delegateArea.bottom += 100

            // Set the TouchDelegate on the parent view so that touches within
            // the touch delegate bounds are routed to the child.
            (myButton.parent as? View)?.apply {
                // Instantiate a TouchDelegate. "delegateArea" is the bounds in
                // local coordinates of the containing view to be mapped to the
                // delegate view. "myButton" is the child view that receives
                // motion events.
                touchDelegate = TouchDelegate(delegateArea, myButton)
            }
        }
    }
}

Java

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // Get the parent view.
        View parentView = findViewById(R.id.parent_layout);

        parentView.post(new Runnable() {
            // Post in the parent's message queue to make sure the parent lays
            // out its children before you call getHitRect().
            @Override
            public void run() {
                // The bounds for the delegate view, which is an ImageButton in
                // this example.
                Rect delegateArea = new Rect();
                ImageButton myButton = (ImageButton) findViewById(R.id.button);
                myButton.setEnabled(true);
                myButton.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        Toast.makeText(MainActivity.this,
                                "Touch occurred within ImageButton touch region.",
                                Toast.LENGTH_SHORT).show();
                    }
                });

                // The hit rectangle for the ImageButton.
                myButton.getHitRect(delegateArea);

                // Extend the touch area of the ImageButton beyond its bounds on
                // the right and bottom.
                delegateArea.right += 100;
                delegateArea.bottom += 100;

                // Instantiate a TouchDelegate. "delegateArea" is the bounds in
                // local coordinates of the containing view to be mapped to the
                // delegate view. "myButton" is the child view that receives
                // motion events.
                TouchDelegate touchDelegate = new TouchDelegate(delegateArea,
                        myButton);

                // Set the TouchDelegate on the parent view so that touches
                // within the touch delegate bounds are routed to the child.
                if (View.class.isInstance(myButton.getParent())) {
                    ((View) myButton.getParent()).setTouchDelegate(touchDelegate);
                }
            }
        });
    }
}