مدیریت رویدادهای لمسی در
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 فراخوانی نمیشود.
کاتلین
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. ... } }
جاوا
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 عبارتند از
getScaledMinimumFlingVelocity()
و
getScaledMaximumFlingVelocity().
این روشها بهترتیب حداقل و حداکثر سرعت را برای شروع پرتاب اندازهگیریشده برحسب پیکسل در ثانیه برمیگردانند. برای مثال:
کاتلین
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. } }
جاوا
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 _نمای نماینده_ است—یعنی فرزندی که والد ناحیه لمس آن را گسترش میدهد. فایل چیدمان در اینجا آمده است:
<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، همه رویدادهای لمسی را دریافت میکند. اگر رویداد لمسی در مستطیل ضربه فرزند رخ دهد، والد
رویداد لمسی را برای رسیدگی به فرزند منتقل میکند.
کاتلین
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) } } } }
جاوا
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); } } }); } }