จัดการเหตุการณ์การแตะใน ViewGroup

ลองใช้วิธีแบบ Compose
Jetpack Compose เป็นชุดเครื่องมือ UI ที่แนะนำสำหรับ Android ดูข้อมูลเกี่ยวกับท่าทางสัมผัสในฟีเจอร์ช่วยเขียน

การจัดการเหตุการณ์สัมผัสใน ViewGroup ต้องใช้ความระมัดระวังเป็นพิเศษ เนื่องจากโดยทั่วไปแล้ว ViewGroup มักจะมีองค์ประกอบย่อยที่กำหนดเป้าหมายสำหรับเหตุการณ์สัมผัสที่แตกต่างกัน กับ ViewGroup เอง หากต้องการให้แน่ใจว่าแต่ละมุมมองได้รับเหตุการณ์การแตะที่ตั้งใจไว้สำหรับมุมมองนั้นอย่างถูกต้อง ให้ลบล้างเมธอด onInterceptTouchEvent()

สกัดกั้นเหตุการณ์การแตะใน ViewGroup

ระบบจะเรียกใช้เมธอด onInterceptTouchEvent() ทุกครั้งที่ตรวจพบการโต้ตอบแบบสัมผัสบนพื้นผิวของ ViewGroup รวมถึงบนพื้นผิวขององค์ประกอบย่อย หาก onInterceptTouchEvent()แสดงผลเป็น true แสดงว่า MotionEvent ถูกสกัดกั้น ซึ่งหมายความว่าระบบจะไม่ส่งต่อไปยังองค์ประกอบย่อย แต่จะส่งไปยังเมธอด onTouchEvent() ขององค์ประกอบหลักแทน

onInterceptTouchEvent() วิธีนี้ช่วยให้ผู้ปกครองมีโอกาสได้เห็นการโต้ตอบแบบสัมผัส ก่อนที่หน้าจอสำหรับเด็กจะเห็น หากคุณส่งคืน true จาก onInterceptTouchEvent() มุมมองขององค์ประกอบย่อยที่เคยจัดการการโต้ตอบแบบสัมผัสจะได้รับ ACTION_CANCEL และระบบจะส่งเหตุการณ์จากจุดนั้นเป็นต้นไปไปยังเมธอด onTouchEvent() ของผู้ปกครอง เพื่อจัดการตามปกติ onInterceptTouchEvent() ยังส่งคืน false และ สอดแนมเหตุการณ์ขณะที่เหตุการณ์เดินทางลงไปตามลำดับชั้นการแสดงผลไปยังเป้าหมายปกติ ซึ่งจัดการ เหตุการณ์ด้วย onTouchEvent() ของตัวเองได้ด้วย

ในข้อมูลโค้ดต่อไปนี้ คลาส MyViewGroup จะขยาย ViewGroup MyViewGroup มีมุมมองย่อยหลายรายการ หากคุณลากนิ้วผ่านหน้าจอสำหรับเด็กในแนวนอน หน้าจอสำหรับเด็กจะไม่ได้รับการโต้ตอบแบบสัมผัสอีกต่อไป และ MyViewGroup จะจัดการการโต้ตอบแบบสัมผัส โดยการเลื่อนเนื้อหา อย่างไรก็ตาม หากคุณแตะปุ่มในหน้าจอสำหรับเด็กหรือเลื่อนหน้าจอสำหรับเด็กในแนวตั้ง ผู้ปกครองจะไม่สกัดกั้นการโต้ตอบแบบสัมผัสเหล่านั้นเนื่องจากหน้าจอสำหรับเด็กเป็นเป้าหมายที่ต้องการ ในกรณีดังกล่าว onInterceptTouchEvent() จะแสดงผล false และระบบจะไม่เรียกใช้ onTouchEvent() ของคลาส MyViewGroup

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 เพื่อเข้าถึง ระยะทาง ความเร็ว และเวลาที่ใช้กันทั่วไปในระบบ Android ได้

"ความคลาดเคลื่อนในการสัมผัส" หมายถึงระยะทางเป็นพิกเซลที่การสัมผัสของผู้ใช้สามารถเลื่อนไปได้ก่อนที่ระบบจะตีความท่าทางสัมผัสเป็นการเลื่อน โดยปกติแล้วจะใช้ Touch Slop เพื่อป้องกันการเลื่อนโดยไม่ตั้งใจเมื่อผู้ใช้ กำลังดำเนินการสัมผัสอื่นๆ เช่น การแตะองค์ประกอบบนหน้าจอ

ViewConfiguration อีก 2 วิธีที่ใช้กันโดยทั่วไปคือ 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 คือ _delegate 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 ในเทรด UI ซึ่งจะช่วยให้มั่นใจได้ว่าองค์ประกอบระดับบนสุดจะวางองค์ประกอบย่อยก่อนเรียกใช้เมธอด getHitRect() เมธอด getHitRect() จะรับสี่เหลี่ยมผืนผ้าของ Hit (หรือพื้นที่ที่แตะได้) ขององค์ประกอบย่อยในพิกัดขององค์ประกอบหลัก
  • ค้นหาImageButtonมุมมองขององค์ประกอบย่อยและเรียก getHitRect() เพื่อรับ ขอบเขตของพื้นที่ที่แตะได้ขององค์ประกอบย่อย
  • ขยายขอบเขตของสี่เหลี่ยมผืนผ้าที่ตรวจพบของมุมมองย่อย ImageButton
  • สร้างอินสแตนซ์ของ TouchDelegate โดยส่งผ่านสี่เหลี่ยมผืนผ้าของ Hit ที่ขยายแล้วและ ImageButton มุมมองย่อยเป็นพารามิเตอร์
  • ตั้งค่า TouchDelegate ในมุมมองหลักเพื่อให้ระบบกำหนดเส้นทางการแตะภายในขอบเขตของ 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);
                }
            }
        });
    }
}