ViewGroup'taki dokunma etkinliklerini yönetme

ViewGroup içinde dokunma etkinliklerinin ele alınması özel bir özen gösterir. Çünkü ViewGroup için, ViewGroup öğesinin kendisinden farklı dokunma etkinliklerinin hedefi olan alt öğeler olması sık karşılaşılan bir durumdur. Her görünümün ilgili dokunma etkinliklerini doğru şekilde aldığından emin olmak için onInterceptTouchEvent() yöntemini geçersiz kılın.

ViewGroup'taki dokunma etkinliklerine müdahale et

onInterceptTouchEvent() yöntemi, alt yüzeyler de dahil olmak üzere ViewGroup yüzeyinde bir dokunma etkinliği algılandığında çağrılır. onInterceptTouchEvent(), true değerini döndürürse MotionEvent yeterli olur. Diğer bir deyişle, alt öğeye değil, üst öğenin onTouchEvent() yöntemine iletilir.

onInterceptTouchEvent() yöntemi, ebeveyne dokunma etkinliklerini alt öğelerinden önce görme fırsatı sağlar. onInterceptTouchEvent() ürününden true öğesini döndürürseniz daha önce dokunma etkinliklerini işleyen alt görünüm ACTION_CANCEL alır ve bu noktadan itibaren etkinlikler, normal işleme için üst öğenin onTouchEvent() yöntemine gönderilir. onInterceptTouchEvent() ayrıca, görünüm hiyerarşisinde aşağı doğru inen ve etkinlikleri kendi onTouchEvent() değerleriyle işleyen normal hedeflerine doğru ilerlerken false değerini döndürebilir ve etkinlikleri gizlice izleyebilir.

Aşağıdaki snippet'te MyViewGroup sınıfı ViewGroup kapsamını genişletir. MyViewGroup birden çok alt görünüm içeriyor. Parmağınızı bir çocuk görünümünde yatay olarak sürüklerseniz alt görünüm bundan böyle dokunma etkinliklerini almaz ve MyViewGroup, içeriğini kaydırarak dokunma etkinliklerini işler. Bununla birlikte, alt öğe görünümündeki düğmelere dokunur veya alt görünümü dikey olarak kaydırırsanız üst öğe, amaçlanan hedef çocuk olduğundan bu dokunma etkinliklerine müdahale etmez. Bu durumlarda, onInterceptTouchEvent() false değerini döndürür ve MyViewGroup sınıfının onTouchEvent() çağrılmaz.

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 öğesinin bir requestDisallowInterceptTouchEvent() yöntemi de sağladığını unutmayın. Çocuk, üst veya üst öğelerinin onInterceptTouchEvent() ile dokunma etkinliklerine müdahale etmesini istemediğinde ViewGroup bu yöntemi çağırır.

ACTION_OUTSIDE etkinliklerini işle

Bir ViewGroup, ACTION_OUTSIDE içeren bir MotionEvent alırsa etkinlik varsayılan olarak alt öğelerine gönderilmez. MotionEvent öğesini ACTION_OUTSIDE ile işlemek için dispatchTouchEvent(MotionEvent event) öğesini geçersiz kılarak uygun View öğesine gönderin veya ilgili Window.Callback öğesinde (örneğin, Activity) işleyin.

ViewConfiguration sabitlerini kullan

Önceki snippet, mTouchSlop adlı değişkeni başlatmak için geçerli ViewConfiguration öğesini kullanır. Android sistemi tarafından kullanılan ortak mesafeler, hızlar ve sürelere erişmek için ViewConfiguration sınıfını kullanabilirsiniz.

"Dokunma eğimi", hareket kaydırma olarak yorumlanmadan önce kullanıcının dokunuşuyla gezinebileceği olan mesafeyi piksel cinsinden belirtir. Dokunma eğimi, genellikle kullanıcı ekrandaki öğelere dokunma gibi başka bir dokunma işlemi gerçekleştirirken yanlışlıkla kaydırma yapılmasını önlemek için kullanılır.

Yaygın olarak kullanılan diğer iki ViewConfiguration yöntemi getScaledMinimumFlingVelocity() ve getScaledMaximumFlingVelocity() yöntemleridir. Bu yöntemler, saniyede piksel cinsinden ölçülen bir hızlı kaydırma başlatmak için sırasıyla minimum ve maksimum hızı döndürür. Örneğin:

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.
    }
}

Çocuk görünümünün dokunulabilir alanını genişletme

Android, ebeveynin çocuk görünümünün dokunulabilir alanını çocuğun sınırlarını aşmasına olanak tanımak için TouchDelegate sınıfını sağlar. Bu, çocuğun küçük olmasına rağmen daha geniş bir temas alanına ihtiyacı olduğunda faydalıdır. Bu yaklaşımı, çocuğun dokunulabileceği bölgeyi küçültmek için de kullanabilirsiniz.

Aşağıdaki örnekte ImageButton, _yetki verilmiş görünüm_, yani dokunma alanını genişlettiği alt öğedir. Düzen dosyasını aşağıda görebilirsiniz:

<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>

Aşağıdaki snippet şu görevleri tamamlar:

  • Üst görünümü alır ve kullanıcı arayüzü iş parçacığında bir Runnable yayınlar. Bu, üst yayıncının getHitRect() yöntemini çağırmadan önce alt öğelerini ortaya koyduğundan emin olur. getHitRect() yöntemi, alt öğenin isabet dikdörtgenini (veya dokunulabilir alanını) üst öğenin koordinatlarında alır.
  • ImageButton alt görünümünü bulur ve alt öğenin dokunulabilir alanının sınırlarını almak için getHitRect() öğesini çağırır.
  • ImageButton alt görünümünün isabet dikdörtgeninin sınırlarını genişletir.
  • Genişletilmiş isabet dikdörtgeni ve ImageButton alt görünümünü parametre olarak geçirerek bir TouchDelegate örneğini somutlaştırır.
  • Üst görünümde TouchDelegate öğesini, dokunma yetkisi sınırları içindeki dokunmaların alt öğeye yönlendirileceği şekilde ayarlar.

ImageButton alt görünümü için dokunma yetkisi verme yetkisi bulunan üst görünüm, tüm dokunma etkinliklerini alır. Dokunma etkinliği, alt yayıncının isabet dikdörtgeni içinde gerçekleşirse üst düzey, dokunma etkinliğini işlenmesi için alt tabloya iletir.

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);
                }
            }
        });
    }
}