在Android中如何使用TouchDelegate来增加视图的点击目标大小?请提供示例。

90

我的理解是,当你的视图太小以至于很难触摸时,你应该使用TouchDelegate来增加该视图的可点击区域。

然而,在谷歌上搜索使用示例会得到多个人提出问题,但很少有答案。

有人知道设置触摸代理的正确方法吗?例如,将其可点击区域在每个方向上增加4像素。


3
这里有一篇关于如何使用TouchDelegate的好文章:http://groups.google.com/group/android-developers/browse_thread/thread/178af525ab84b371 - Mason Lee
好的教程:扩大可触摸区域 - Thierry Roy
13个回答

107

我向 Google 的一位朋友询问并得到了帮助,他们帮我弄清了如何使用 TouchDelegate。这是我们想出来的方法:

final View parent = (View) delegate.getParent();
parent.post( new Runnable() {
    // Post in the parent's message queue to make sure the parent
    // lays out its children before we call getHitRect()
    public void run() {
        final Rect r = new Rect();
        delegate.getHitRect(r);
        r.top -= 4;
        r.bottom += 4;
        parent.setTouchDelegate( new TouchDelegate( r , delegate));
    }
});

18
如果一个屏幕上有两个按钮需要进行这样的操作,那该怎么办?如果再次设置触摸代理,则会擦除先前的触摸代理。 - cottonBallPaws
2
这个对我没用... 我猜委托是我正在尝试添加TouchDelegate的视图。 - Kevin Parker
20
我曾遇到同样的问题。不使用TouchDelegates,我使用一个继承自按钮的类。在这个类中,我重写了getHitRect方法。您可以将点击矩形扩展到父视图的大小。以下是代码: public void getHitRect(Rect outRect) { outRect.set(getLeft() - mPadding, getTop() - mPadding, getRight() + mPadding, getTop() + mPadding); } - Brendan Weinstein
1
这个解决方案安全吗?如果发布的可运行文件在布局发生之前被执行会怎样?无论如何,这里的依赖关系太多了,过于依赖预期的行为——这是不具有未来性的。 - Zsolt Safrany
8
根据@ZsoltSafrany的建议,更安全的方法是通过OnGlobalLayoutListener,并在onGlobalLayout()中分配委托。 - greg7gkb
显示剩余5条评论

22
我能够通过在一个屏幕上使用多个视图(复选框)来实现这一点,主要参考了这篇博客文章。基本上,您需要将emmby的解决方案应用到每个按钮及其父级上。
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));
        }
   });
}

在我的情况下,我有一个带有复选框覆盖的图像视图网格,并按如下方式调用该方法:
CheckBox mCheckBox = (CheckBox) convertView.findViewById(R.id.checkBox1);
final ImageView imageView = (ImageView) convertView.findViewById(R.id.imageView1);

// Increase checkbox clickable area
expandTouchArea(imageView, mCheckBox, 100);

对我来说运作得非常好。


12

这个解决方案是由@BrendanWeinstein在评论中发布的。

你可以重写你的ViewgetHitRect(Rect)方法(如果你正在扩展一个),而不是发送一个 TouchDelegate

public class MyView extends View {  //NOTE: any other View can be used here

    /* a lot of required methods */

    @Override
    public void getHitRect(Rect r) {
        super.getHitRect(r); //get hit Rect of current View

        if(r == null) {
            return;
        }

        /* Manipulate with rect as you wish */
        r.top -= 10;
    }
}

有趣的想法。但这是否意味着视图无法从布局中创建?不,等等——如果我设计一个新的视图类并在布局中使用它。那可能行得通。值得一试。 - Martin
11
@Martin,那是正确的想法。不幸的是,自ICS以来,dispatchTouchEvent不再调用getHitRect。http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.4_r1/android/view/ViewGroup.java#1834 覆盖getHitRect只适用于Gingerbread及以下版本。 - Brendan Weinstein

6

Emmby的方法对我不起作用,但经过一些小改动后就可以了:

private void initApplyButtonOnClick() {
    mApplyButton.setOnClickListener(onApplyClickListener);
    final View parent = (View)mApplyButton.getParent();
    parent.post(new Runnable() {

        @Override
        public void run() {
            final Rect hitRect = new Rect();
            parent.getHitRect(hitRect);
            hitRect.right = hitRect.right - hitRect.left;
            hitRect.bottom = hitRect.bottom - hitRect.top;
            hitRect.top = 0;
            hitRect.left = 0;
            parent.setTouchDelegate(new TouchDelegate(hitRect , mApplyButton));         
        }
    });
}

也许这可以节省某人的时间。

这是我用过的可行方法。它实际上使整个父元素发送点击事件到子元素。甚至不必是直接父元素(也可以是父元素的父元素)。 - android developer

如果您只有一个视图要委托,那可能会起作用。但如果您有50个视图要增强触摸区域呢?

就像在这个截图中:https://plus.google.com/u/0/b/112127498414857833804/112127498414857833804/posts

此时,TouchDelegate仍然是合适的解决方案吗?
- Martin

4
根据@Mason Lee的评论,这解决了我的问题。我的项目有相对布局和一个按钮。因此父对象是->布局,子对象是->按钮。
以下是一个Google链接示例: google code 万一他非常有价值的回答被删除,我在此放置他的回答。

I was recently asked about how to use a TouchDelegate. I was a bit rusty myself on this and I couldn't find any good documentation on it. Here's the code I wrote after a little trial and error. touch_delegate_view is a simple RelativeLayout with the id touch_delegate_root. I defined with a single, child of the layout, the button delegated_button. In this example I expand the clickable area of the button to 200 pixels above the top of my button.

public class TouchDelegateSample extends Activity {

  Button mButton;   
  @Override   
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.touch_delegate_view);
    mButton = (Button)findViewById(R.id.delegated_button);
    View parent = findViewById(R.id.touch_delegate_root);

    // post a runnable to the parent view's message queue so its run after
    // the view is drawn
    parent.post(new Runnable() {
      @Override
      public void run() {
        Rect delegateArea = new Rect();
        Button delegate = TouchDelegateSample.this.mButton;
        delegate.getHitRect(delegateArea);
        delegateArea.top -= 200;
        TouchDelegate expandedArea = new TouchDelegate(delegateArea, delegate);
        // give the delegate to an ancestor of the view we're delegating the
        // area to
        if (View.class.isInstance(delegate.getParent())) {
          ((View)delegate.getParent()).setTouchDelegate(expandedArea);
        }
      }
    });   
  } 
}

Cheers, Justin Android Team @ Google

我的建议是:如果您想扩展对象的左侧,可以使用负数值;如果您想扩展对象的右侧,可以使用正数值。同样的规则也适用于上下方向。


1
这里的关键似乎是一个按钮 - 我有一种感觉,TouchDelegate对于多个按钮来说是无用的。你能确认一下吗? - Martin

2

虽然有点晚了,但经过大量研究,我现在正在使用:

/**
 * Expand the given child View's touchable area by the given padding, by
 * setting a TouchDelegate on the given ancestor View whenever its layout
 * changes.
 */*emphasized text*
public static void expandTouchArea(final View ancestorView,
    final View childView, final Rect padding) {

    ancestorView.getViewTreeObserver().addOnGlobalLayoutListener(
        new OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                TouchDelegate delegate = null;

                if (childView.isShown()) {
                    // Get hitRect in parent's coordinates
                    Rect hitRect = new Rect();
                    childView.getHitRect(hitRect);

                    // Translate to ancestor's coordinates
                    int ancestorLoc[] = new int[2];
                    ancestorView.getLocationInWindow(ancestorLoc);

                    int parentLoc[] = new int[2];
                    ((View)childView.getParent()).getLocationInWindow(
                        parentLoc);

                    int xOffset = parentLoc[0] - ancestorLoc[0];
                    hitRect.left += xOffset;
                    hitRect.right += xOffset;

                    int yOffset = parentLoc[1] - ancestorLoc[1];
                    hitRect.top += yOffset;
                    hitRect.bottom += yOffset;

                    // Add padding
                    hitRect.top -= padding.top;
                    hitRect.bottom += padding.bottom;
                    hitRect.left -= padding.left;
                    hitRect.right += padding.right;

                    delegate = new TouchDelegate(hitRect, childView);
                }

                ancestorView.setTouchDelegate(delegate);
            }
        });
}

这种解决方案比被接受的方案更好,因为它允许在任何祖先视图上设置TouchDelegate,而不仅仅是父视图。与被接受的方案不同的是,每当祖先视图的布局发生变化时,它还会更新TouchDelegate。

1

因为我不喜欢等待布局传递以获取TouchDelegate矩形的新大小的想法,所以我选择了另一种解决方案:

public class TouchSizeIncreaser extends FrameLayout {
    public TouchSizeIncreaser(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        return true;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        final View child = getChildAt(0);
        if(child != null) {
            child.onTouchEvent(event);
        }
        return true;
    }
}

然后,在一个布局中:

<ch.tutti.ui.util.TouchSizeIncreaser
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:padding="10dp">

    <Spinner
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"/>
</ch.tutti.ui.util.TouchSizeIncreaser>

这个想法是TouchSizeIncreaser FrameLayout将包装Spinner(可以是任何子视图),并将捕获的所有触摸事件转发到子视图中。它适用于点击,即使在其边界外点击,下拉列表也会打开,但其他更复杂的情况可能会有影响。

1

我在Google Repo中找到的一个使用TouchDelegate的解决方案是这样实现的:

/**
 * Ensures that the touchable area of [view] equal [minTouchTarget] by expanding the touch area
 * of a view beyond its actual view bounds. This adapter can be used expand the touchable area of a
 * view when other options (adding padding, for example) may not be available.
 *
 * Usage:
 * <ImageView
 *     ...
 *     app:ensureMinTouchArea="@{@dimen/min_touch_target}"
 *
 * @param view The view whose touch area may be expanded
 * @param minTouchTarget The minimum touch area expressed dimen resource
 */
@BindingAdapter("ensureMinTouchArea")
fun addTouchDelegate(view: View, minTouchTarget: Float) {
    val parent = view.parent as View
    parent.post {
        val delegate = Rect()
        view.getHitRect(delegate)

        val metrics = view.context.resources.displayMetrics
        val height = ceil(delegate.height() / metrics.density)
        val width = ceil(delegate.width() / metrics.density)
        val minTarget = minTouchTarget / metrics.density
        var extraSpace = 0
        if (height < minTarget) {
            extraSpace = (minTarget.toInt() - height.toInt()) / 2
            delegate.apply {
                top -= extraSpace
                bottom += extraSpace
            }
        }

        if (width < minTarget) {
            extraSpace = (minTarget.toInt() - width.toInt()) / 2
            delegate.apply {
                left -= extraSpace
                right += extraSpace
            }
        }

        parent.touchDelegate = TouchDelegate(delegate, view)
    }
}

1

给那个组件(按钮)添加填充不是更好的想法吗?


1

这会使视图变大。如果您在按钮上方有一些不可点击的文本,并且希望利用该文本区域来提高按钮的点击准确性,该怎么办呢?

就像这个屏幕截图中所示:https://plus.google.com/u/0/b/112127498414857833804/112127498414857833804/posts
- Martin

0
在大多数情况下,您可以将需要更大触摸区域的视图包装在另一个无头视图(人工透明视图)中,并向包装器视图添加填充/边距,并将点击/触摸事件附加到包装器视图而不是原始视图,从而使其具有更大的触摸区域。

这样做不会在视图周围创建无法用于其他任何事情的空白空间吗?如果您想在按钮附近显示信息文本并使用文本区域增强按钮单击区域,该怎么办? - Martin
@Martin,这取决于你如何做。而且应该很容易做到你想要的任何事情 - 只需在所有这些视图的顶部包装任何您想要的内容(无论您想要多少个视图)并为该区域分配一个点击事件即可。 - inteist

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接