Home » Android » android – How To Use Multiple TouchDelegate

android – How To Use Multiple TouchDelegate

Posted by: admin May 14, 2020 Leave a comment

Questions:

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

}

}

How to&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;
        }
    
    }
    

Answer:

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.

Answer:

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

}

Answer:

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

Answer:

this seemed to be working for me http://cyrilmottier.com/2012/02/16/listview-tips-tricks-5-enlarged-touchable-areas/

Answer:

Ok i guess nobody provides the real answer to make solution of it and make it easy.Lately i had same issue and reading all above i just had no clue how just to make it work.But finally i did it.First thing to keep in your mind!Lets pretend you have one whole layout which is holding your two small buttons which area must be expanded,so you MUST make another layout inside your main layout and put another button to your newly created layout so in that case with static method you can give touch delegate to 2 buttons at the same time.Now more deeply and step by step into code!
first you surely just find the view of your MAINLAYOUT and Button like this.(this layout will hold our first button)

RelativeLayout mymainlayout = (RelativeLayout)findViewById(R.id.mymainlayout)
Button mybutoninthislayout = (Button)findViewById(R.id.mybutton)

ok we done finding the main layout and its button view which will hold everything its just our onCreate() displaying layout but you have to find in case to use it later.Ok what next?We create another RelativeLayout inside our main layout which width and height is on your taste(this newly created layout will hold our second button)

RelativeLayout myinnerlayout = (RelativeLayout)findViewById(R.id.myinnerlayout)
Button mybuttoninsideinnerlayout = (Button)findViewById(R.id.mysecondbutton)

ok we done finding views so we can now just copy and paste the code of our guy who firstly answered your question.Just copy that code inside your main activity.

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

Now how to use this method and make it work?here is how!
on your onCreate() method paste the next code snippet

  expandTouchArea(mymainlayout,mybutoninthislayout,60);
  expandTouchArea(myinnerlayout, mybuttoninsideinnerlayout,60);

Explanation on what we did in this snipped.We took our created static method named expandTouchArea() and gave 3 arguments.
1st argument-The layout which holds the button which area must be expanded
2nd argument – actual button to expand the area of it
3rd argument – the area in pixels of how much we want the button area to be expanded!
ENJOY!

Answer:

I had the same issue: Trying to add multiple TouchDelegates for different LinearLayouts that route touch events to separate Switches respectively, in one layout.

For details please refer to this question asked by me and my answer.

What I found is: Once I enclose the LinearLayouts each by another LinearLayout, respectively, the second (end every other successive) TouchDelegate starts to work as expected.

So this might help the OP to create a working solution to his problem.
I don’t have a satisfying explanation on why it behaves like this, though.

Answer:

I copy the code of TouchDelegate and made some alter. Now it can support multi Views regardless of whether thoese views had common parents

class MyTouchDelegate: TouchDelegate {
private var mDelegateViews = ArrayList<View>()

private var mBoundses = ArrayList<Rect>()

private var mSlopBoundses = ArrayList<Rect>()

private var mDelegateTargeted: Boolean = false

val ABOVE = 1
val BELOW = 2
val TO_LEFT = 4
val TO_RIGHT = 8

private var mSlop: Int = 0

constructor(context: Context): super(Rect(), View(context)) {
    mSlop = ViewConfiguration.get(context).scaledTouchSlop
}

fun addTouchDelegate(delegateView: View, bounds: Rect) {
    val slopBounds = Rect(bounds)
    slopBounds.inset(-mSlop, -mSlop)
    mDelegateViews.add(delegateView)
    mSlopBoundses.add(slopBounds)
    mBoundses.add(Rect(bounds))
}

var targetIndex = -1

override fun onTouchEvent(event: MotionEvent): Boolean {
    val x = event.x.toInt()
    val y = event.y.toInt()
    var sendToDelegate = false
    var hit = true
    var handled = false


    when (event.action) {
        MotionEvent.ACTION_DOWN -> {
            targetIndex = -1

            for ((index, item) in mBoundses.withIndex()) {
                if (item.contains(x, y)) {
                    mDelegateTargeted = true
                    targetIndex = index
                    sendToDelegate = true
                }
            }
        }
        MotionEvent.ACTION_UP, MotionEvent.ACTION_MOVE -> {
            sendToDelegate = mDelegateTargeted
            if (sendToDelegate) {
                val slopBounds = mSlopBoundses[targetIndex]
                if (!slopBounds.contains(x, y)) {
                    hit = false
                }
            }
        }
        MotionEvent.ACTION_CANCEL -> {
            sendToDelegate = mDelegateTargeted
            mDelegateTargeted = false
        }
    }
    if (sendToDelegate) {
        val delegateView = mDelegateViews[targetIndex]

        if (hit) {
            // Offset event coordinates to be inside the target view
            event.setLocation((delegateView.width / 2).toFloat(), (delegateView.height / 2).toFloat())
        } else {
            // Offset event coordinates to be outside the target view (in case it does
            // something like tracking pressed state)
            val slop = mSlop
            event.setLocation((-(slop * 2)).toFloat(), (-(slop * 2)).toFloat())
        }
        handled = delegateView.dispatchTouchEvent(event)
    }
    return handled
}

}

use it like this

  fun expandTouchArea(viewList: List<View>, touchSlop: Int) {


    val rect = Rect()

    viewList.forEach {
        it.getHitRect(rect)
        if (rect.left == rect.right && rect.top == rect.bottom) {
            postDelay(Runnable { expandTouchArea(viewList, touchSlop) }, 200)
            return
        }
        rect.top -= touchSlop
        rect.left -= touchSlop
        rect.right += touchSlop
        rect.bottom += touchSlop

        val parent = it.parent as? View
        if (parent != null) {
            val parentDelegate = parent.touchDelegate
            if (parentDelegate != null) {
                (parentDelegate as? MyTouchDelegate)?.addTouchDelegate(it, rect)
            } else {
                val touchDelegate = MyTouchDelegate(this)
                touchDelegate.addTouchDelegate(it, rect)
                parent.touchDelegate = touchDelegate
            }
        }
    }
}