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.relativeLa开发者_如何学Pythonyout1);
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);
}
}
});
}
}
You can use composite pattern to be able to add more than one TouchDelegate
to the View
. Steps:
- Create
TouchDelegateComposite
(no matter what view you'll pass as an argument, it's used just to get the Context) - Create necessary
TouchDelegates
and add them to composite 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; } }
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.
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
}
}
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
}
}
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);
}
});
}
}
this seemed to be working for me http://cyrilmottier.com/2012/02/16/listview-tips-tricks-5-enlarged-touchable-areas/
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!
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.
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
}
}
}
}
I implemented a simple solution from the link that Brendan Weinstein listed above - the static method was incredibly clean and tidy compared to all other solutions. Padding for me simply doesnt work.
My use case was increasing the touch area of 2 small buttons to improve UX.
I have a MyUserInterfaceManager
class, where i insert the static function from the youtube video;
public static void changeViewsTouchArea(final View newTouchArea,
final View viewToChange) {
newTouchArea.post(new Runnable() {
@Override
public void run() {
Rect rect = new Rect(0,0, newTouchArea.getWidth(), newTouchArea.getHeight());
newTouchArea.setTouchDelegate(new TouchDelegate(rect, viewToChange));
}
});
}
Then in XML i have the following code (per imagebutton);
<!-- constrain touch area to button / view!-->
<FrameLayout
android:id="@+id/btn_a_touch_area"
android:layout_width="@dimen/large_touch_area"
android:layout_height="@dimen/large_touch_area"
/>
<ImageButton
android:id="@+id/btn_id_here"
android:layout_width="@dimen/button_size"
android:layout_height="@dimen/button_size" />
The in the onCreateView(...)
method of my fragment i called the method per View
that needs the touch area modified (after binding both Views
!);
MyUserInterfaceManager.changeViewsTouchArea(buttonATouchArea, buttonA);
buttonA.setOnClickListener(v -> {
// add event code here
});
This solution means i can explicitly design and see the touch area in layout files - a must have to ensure im not getting too close to other View
touch areas (compared to the "calculate and add pixels" methods recommended by google and others).
精彩评论