Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How To Use Multiple TouchDelegate

i have two ImageButtons, each inside a RelativeLayout and these two RelativeLayouts are in another RelativeLayout, i want to set TouchDelegate for each ImageButton. If normally i add TouchDelegate to each ImageButton and it's parent RelativeLayout then just one ImageButton works properly, Another one doesn't extend it's clicking area. So PLease help me on how to use TouchDelegate in both ImageButtons. If it's not possible then what can be a effective way to extend the clicking area of a view? Thanks in advance ........

Here is my xml code:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout android:id="@+id/FrameContainer"
android:layout_width="fill_parent" android:layout_height="fill_parent"
xmlns:android="http://schemas.android.com/apk/res/android">
<RelativeLayout android:layout_alignParentLeft="true"
    android:id="@+id/relativeLayout3" android:layout_width="wrap_content"
    android:layout_height="wrap_content">
    <RelativeLayout android:layout_alignParentLeft="true"
        android:id="@+id/relativeLayout1" android:layout_width="113dip"
        android:layout_height="25dip">
        <ImageButton android:id="@+id/tutorial1"
            android:layout_width="wrap_content" android:layout_height="wrap_content"
            android:background="@null" android:src="@drawable/tutorial" />
    </RelativeLayout>
    <RelativeLayout android:layout_alignParentLeft="true"
        android:id="@+id/relativeLayout2" android:layout_width="113dip"
        android:layout_height="25dip" android:layout_marginLeft="100dip">
        <ImageButton android:id="@+id/tutorial2"
            android:layout_width="wrap_content" android:layout_height="wrap_content"
            android:background="@null" android:src="@drawable/tutorial"
            android:layout_marginLeft="50dip" />
    </RelativeLayout>
</RelativeLayout>

My Activity class :

import android.app.Activity;
import android.graphics.Rect;
import android.os.Bundle;
import android.view.TouchDelegate;
import android.view.View;
import android.widget.ImageButton;
import android.widget.Toast;

public class TestTouchDelegate extends Activity {
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    View mParent1 = findViewById(R.id.relativeLayout1);
    mParent1.post(new Runnable() {
        @Override
        public void run() {
            Rect bounds1 = new Rect();
            ImageButton mTutorialButton1 = (ImageButton) findViewById(R.id.tutorial1);
            mTutorialButton1.setEnabled(true);
            mTutorialButton1.setOnClickListener(new View.OnClickListener() {
                public void onClick(View view) {
                    Toast.makeText(TestTouchDelegate.this, "Test TouchDelegate 1", Toast.LENGTH_SHORT).show();
                }
            });

            mTutorialButton1.getHitRect(bounds1);
            bounds1.right += 50;
            TouchDelegate touchDelegate1 = new TouchDelegate(bounds1, mTutorialButton1);

            if (View.class.isInstance(mTutorialButton1.getParent())) {
                ((View) mTutorialButton1.getParent()).setTouchDelegate(touchDelegate1);
            }
        }
    });

    //View mParent = findViewById(R.id.FrameContainer);
    View mParent2 = findViewById(R.id.relativeLayout2);
    mParent2.post(new Runnable() {
        @Override
        public void run() {
            Rect bounds2 = new Rect();
            ImageButton mTutorialButton2 = (ImageButton) findViewById(R.id.tutorial2);
            mTutorialButton2.setEnabled(true);
            mTutorialButton2.setOnClickListener(new View.OnClickListener() {
                public void onClick(View view) {
                    Toast.makeText(TestTouchDelegate.this, "Test TouchDelegate 2", Toast.LENGTH_SHORT).show();
                }
            });

            mTutorialButton2.getHitRect(bounds2);
            bounds2.left += 50;
            TouchDelegate touchDelegate2 = new TouchDelegate(bounds2, mTutorialButton2);

            if (View.class.isInstance(mTutorialButton2.getParent())) {
                ((View) mTutorialButton2.getParent()).setTouchDelegate(touchDelegate2);
            }
        }
    });

}

}

like image 321
Junaid Avatar asked Jul 23 '11 07:07

Junaid


5 Answers

You can use composite pattern to be able to add more than one TouchDelegate to the View. Steps:

  1. Create TouchDelegateComposite (no matter what view you'll pass as an argument, it's used just to get the Context)
  2. Create necessary TouchDelegates and add them to composite
  3. Add composite to view as they recommend here (via view.post(new Runnable))

    public class TouchDelegateComposite extends TouchDelegate {
    
        private final List<TouchDelegate> delegates = new ArrayList<TouchDelegate>();
        private static final Rect emptyRect = new Rect();
    
        public TouchDelegateComposite(View view) {
            super(emptyRect, view);
        }
    
        public void addDelegate(TouchDelegate delegate) {
            if (delegate != null) {
                delegates.add(delegate);
            }
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            boolean res = false;
            float x = event.getX();
            float y = event.getY();
            for (TouchDelegate delegate : delegates) {
                event.setLocation(x, y);
                res = delegate.onTouchEvent(event) || res;
            }
            return res;
        }
    
    }
    
like image 99
need1milliondollars Avatar answered Oct 20 '22 00:10

need1milliondollars


There is only supposed to be one touch delegate for each view. The documentation for getTouchDelegate() in the View class reads:

"Gets the TouchDelegate for this View."

There is only to be one TouchDelegate. To use only one TouchDelegate per view, you can wrap each touchable view within a view with dimensions reflecting what you would like to be touchable. An android developer at square gives an example of how you can do this for multiple Views using just one static method (http://www.youtube.com/watch?v=jF6Ad4GYjRU&t=37m4s):

  public static void expandTouchArea(final View bigView, final View smallView, final int extraPadding) {
bigView.post(new Runnable() {
    @Override
    public void run() {
        Rect rect = new Rect();
        smallView.getHitRect(rect);
        rect.top -= extraPadding;
        rect.left -= extraPadding;
        rect.right += extraPadding;
        rect.bottom += extraPadding;
        bigView.setTouchDelegate(new TouchDelegate(rect, smallView));
    }
});

}

Let's say that you do not want to clutter your view hierarchy. There are two other options I can think of. You can define the bounds of what is touchable inside the touchable view and make sure to pass all touchevents to that child view from respective parent views. Or you can override getHitRect() for the touchable view. The former will quickly clutter your code and make it difficult to understand, so the latter is the better way forward. You want to go with overriding getHitRect.

Where mPadding is the amount of extra area you want to be touchable around your view, you could use something like the following:

    @Override
public void getHitRect(Rect outRect) {
    outRect.set(getLeft() - mPadding, getTop() - mPadding, getRight() + mPadding, getTop() + mPadding);
}

If you use code like the above you'll have to consider what touchable views are nearby. The touchable area of the View that is highest on the stack could overlap on top of another View.

Another similar option would be to just change the padding of the touchable view. I dislike this as a solution because it can become difficult to keep track of how Views are being resized.

like image 26
Brendan Weinstein Avatar answered Oct 19 '22 23:10

Brendan Weinstein


Kotlin version of @need1milliondollars's answer:

class TouchDelegateComposite(view: View) : TouchDelegate(Rect(), view) {

    private val delegates: MutableList<TouchDelegate> = ArrayList()

    fun addDelegate(delegate: TouchDelegate) {
        delegates.add(delegate)
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        var res = false
        val x = event.x
        val y = event.y
        for (delegate in delegates) {
            event.setLocation(x, y)
            res = delegate.onTouchEvent(event) || res
        }
        return res
    }
}
like image 41
ThomasW Avatar answered Oct 20 '22 00:10

ThomasW


Fully working Kotlin extension function which allows for multiple views to increase their touch target by the same amount:

// Example of usage
parentLayout.increaseHitAreaForViews(views = *arrayOf(myFirstView, mySecondView, myThirdView))

/*
 * Use this function if a parent view contains more than one view that
 * needs to increase its touch target hit area.
 *
 * Call this on the parent view
 */
fun View.increaseHitAreaForViews(@DimenRes radiusIncreaseDpRes: Int = R.dimen.touch_target_default_radius_increase, vararg views: View) {
    val increasedRadiusPixels = resources.getDimensionPixelSize(radiusIncreaseDpRes)
    val touchDelegateComposite = TouchDelegateComposite(this)
    post {
        views.forEach { view ->
            val rect = Rect()
            view.getHitRect(rect)
            rect.top -= increasedRadiusPixels
            rect.left -= increasedRadiusPixels
            rect.bottom += increasedRadiusPixels
            rect.right += increasedRadiusPixels
            touchDelegateComposite.addDelegate(TouchDelegate(rect, view))
        }
        touchDelegate = touchDelegateComposite
    }
}

class TouchDelegateComposite(view: View) : TouchDelegate(Rect(), view) {
    private val delegates = mutableListOf<TouchDelegate>()

    fun addDelegate(delegate: TouchDelegate) {
        delegates.add(delegate)
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        var res = false
        for (delegate in delegates) {
            event.setLocation(event.x, event.y)
            res = delegate.onTouchEvent(event) || res
        }
        return res
    }
}
like image 25
w3bshark Avatar answered Oct 20 '22 00:10

w3bshark


To make your code working you need to decrease left border of bounds2, and not increase it.

bounds2.left -= 50;

After playing around with TouchDelegate, I came to the code below, which works for me all the time on any Android version. The trick is to extend area guarantied after layout is called.

public class ViewUtils {

    public static final void extendTouchArea(final View view, 
            final int padding) {

        view.getViewTreeObserver().addOnGlobalLayoutListener(
                new OnGlobalLayoutListener() {

            @Override
            @SuppressWarnings("deprecation")
            public void onGlobalLayout() {
                final Rect touchArea = new Rect();
                view.getHitRect(touchArea);
                touchArea.top -= padding;
                touchArea.bottom += padding;
                touchArea.left -= padding;
                touchArea.right += padding;

                final TouchDelegate touchDelegate = 
                    new TouchDelegate(touchArea, view);
                final View parent = (View) view.getParent();
                parent.setTouchDelegate(touchDelegate);

                view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
            }
        });
    }

}
like image 45
sergej shafarenka Avatar answered Oct 20 '22 01:10

sergej shafarenka