扩大点击热区
# padding方式
适合图片等dp或px为单位的view,
不适合textview、button这种以或含sp计算单位的view。
# TouchDelegate方式
# 作用
假设有两个View,分别是v1、v2,可以通过v1.setTouchDelegate(new TouchDelegate(bounds, v2))来委派触摸事件,其中bounds是一个Rect。执行该这个方法后,v1中bounds区域的触摸事件TouchEvent将会传递给v2。
# 实现API
/**
* 扩展点击区域的范围
*
* @param view 需要扩展的元素,此元素必需要有父级元素
* @param expendSize 需要扩展的尺寸
*/
private fun expendTouchArea(view: View?, expendSize: Int) {
expendTouchArea(view, expendSize, expendSize, expendSize, expendSize)
}
private fun expendTouchArea(view: View?, left: Int, top: Int, right: Int, bottom: Int) {
if (view == null) {
return
}
val parentView = view.parent as View
parentView.post {
val rect = Rect()
view.getHitRect(rect) // 如果太早执行本函数,会获取rect失败,因为此时UI界面尚未开始绘制,无法获得正确的坐标
rect.left -= left
rect.top -= top
rect.right += right
rect.bottom += bottom
parentView.touchDelegate = TouchDelegate(rect, view)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 实现介绍
在Android的UI开发中,经常遇到部分控件UI过小 对用户来说不好点击,此时 我们可能通过在外部包裹一层FrameLayout或者是给控件设置padding的方式来扩大 控件的区域大小。
事实是,在系统已经提供了扩大控件点击区域判断范围的代理方式,我们看下View类的源码
class View {
/**
* The delegate to handle touch events that are physically in this view
* but should be handled by another view.
*/
private TouchDelegate mTouchDelegate = null;
public boolean onTouchEvent(MotionEvent event) {
//...
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
}
/**
* Sets the TouchDelegate for this View.
*/
public void setTouchDelegate(TouchDelegate delegate) {
mTouchDelegate = delegate;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
从源码中可以看到如果设置了TouchDelegate,touchEvent会优先交给TouchDelegate来处理。
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.view;
import android.graphics.Rect;
/**
* Helper class to handle situations where you want a view to have a larger touch area than its
* actual view bounds. The view whose touch area is changed is called the delegate view. This
* class should be used by an ancestor of the delegate. To use a TouchDelegate, first create an
* instance that specifies the bounds that should be mapped to the delegate and the delegate
* view itself.
* <p>
* The ancestor should then forward all of its touch events received in its
* {@link android.view.View#onTouchEvent(MotionEvent)} to {@link #onTouchEvent(MotionEvent)}.
* </p>
*/
public class TouchDelegate {
/**
private View mDelegateView;
private Rect mBounds;
private boolean mDelegateTargeted;
public TouchDelegate(Rect bounds, View delegateView) {
mBounds = bounds;
mDelegateView = delegateView;
}
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
boolean sendToDelegate = false;
boolean hit = true;
boolean handled = false;
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mDelegateTargeted = mBounds.contains(x, y);
sendToDelegate = mDelegateTargeted;
break;
//...
}
if (sendToDelegate) {
final View delegateView = mDelegateView;
if (hit) {
// Offset event coordinates to be inside the target view
event.setLocation(delegateView.getWidth() / 2, delegateView.getHeight() / 2);
} else {
// Offset event coordinates to be outside the target view (in case it does
// something like tracking pressed state)
int slop = mSlop;
event.setLocation(-(slop * 2), -(slop * 2));
}
handled = delegateView.dispatchTouchEvent(event);
}
return handled;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
从源码中 可以看到,创建TouchDelegate 需要传入一个Rect(left,top,right,bottom) 和delegateView, onTouchEvent触发时,会通过这个Rect来判断点击事件是否落在区域内,如果是 则转发给代理view来处理该事件。
但这个自定义的View并不是完美的,还存在以下问题:
- 必须保证parent足够大,如果自定义的范围超出parent的大小,则超出的那部分无效。
- 一个parent只能设置一个触摸委派,设置多个时,只有最后设置的child有效。如果希望一个view能设置多个委派,需要再自定义parent
总而言之,要触发委派,必须保证parent接收到了触摸事件,并且落在了你定义的范围内。
编辑 (opens new window)
上次更新: 2023/10/05, 00:59:59