关于Android TouchDelegate源码解析
关于Android TouchDelegate源码解析
android.view.TouchDelegate是用来扩大View的触摸点击区域的。
用法很简单,套路是:
比如
mButton = new CheckBox(getContext());
Rect bounds = new Rect(0, 0, viewBound.getMeasuredWidth(), viewBound.getMeasuredHeight());
TouchDelegate delegate = new TouchDelegate(bounds, mButton);
viewBound.setTouchDelegate(delegate);
这样就可以扩大mButton的触摸点击区域了,将它的触摸区域设成viewBound的区域,也就是说点viewBound的任何地方都等同于点mButton。
既然是这样那我也可以设置另外一块和mButton毫无交集区域作为viewBound的点击范围。
public class TouchDelegateLayout extends FrameLayout {
public TouchDelegateLayout(Context context) {
super(context);
init();
}
public TouchDelegateLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public TouchDelegateLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private CheckBox mButton;
private void init() {
mButton = new CheckBox(getContext());
mButton.setText(Click Anywhere On Screen);
addView(mButton, new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, Gravity.CENTER));
}
/*
* TouchDelegate is applied to this view (parent) to delegate all touches
* within the specified rectangle to the CheckBox (child). Here, the rectangle
* is the entire size of this parent view.
*
* This must be done after the view has measured itself so we know how big to make the rect,
* thus we've chosen to add the delegate in onMeasure()
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//Apply the whole area of this view as the delegate area
Rect bounds = new Rect(0, 0, getMeasuredWidth()/3, getMeasuredHeight()/3);
TouchDelegate delegate = new TouchDelegate(bounds, mButton);
setTouchDelegate(delegate);
}
}
如上,点击FrameLayout的左上角是可以让mButton选中的。为什么可以实现,TouchDelegate为我们做了什么?
public class TouchDelegate {
/**
* View that should receive forwarded touch events
*/
private View mDelegateView;
/**
* Bounds in local coordinates of the containing view that should be mapped to the delegate
* view. This rect is used for initial hit testing.
*/
private Rect mBounds;
/**
* mBounds inflated to include some slop. This rect is to track whether the motion events
* should be considered to be be within the delegate view.
*/
private Rect mSlopBounds;
/**
* True if the delegate had been targeted on a down event (intersected mBounds).
*/
private boolean mDelegateTargeted;
/**
* The touchable region of the View extends above its actual extent.
*/
public static final int ABOVE = 1;
/**
* The touchable region of the View extends below its actual extent.
*/
public static final int BELOW = 2;
/**
* The touchable region of the View extends to the left of its
* actual extent.
*/
public static final int TO_LEFT = 4;
/**
* The touchable region of the View extends to the right of its
* actual extent.
*/
public static final int TO_RIGHT = 8;
private int mSlop;
/**
* Constructor
*
* @param bounds Bounds in local coordinates of the containing view that should be mapped to
* the delegate view
* @param delegateView The view that should receive motion events
*/
public TouchDelegate(Rect bounds, View delegateView) {
mBounds = bounds;
mSlop = ViewConfiguration.get(delegateView.getContext()).getScaledTouchSlop();
mSlopBounds = new Rect(bounds);
mSlopBounds.inset(-mSlop, -mSlop);
mDelegateView = delegateView;
}
/**
* Will forward touch events to the delegate view if the event is within the bounds
* specified in the constructor.
*
* @param event The touch event to forward
* @return True if the event was forwarded to the delegate, false otherwise.
*/
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.getAction()) {
case MotionEvent.ACTION_DOWN:
Rect bounds = mBounds;
if (bounds.contains(x, y)) {
mDelegateTargeted = true;
sendToDelegate = true;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_MOVE:
sendToDelegate = mDelegateTargeted;
if (sendToDelegate) {
Rect slopBounds = mSlopBounds;
if (!slopBounds.contains(x, y)) {
hit = false;
}
}
break;
case MotionEvent.ACTION_CANCEL:
sendToDelegate = mDelegateTargeted;
mDelegateTargeted = false;
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;
}
}
可以看出TouchDelegate仅仅是一个普通的不能再普通的java类而已。代码不多,就一个构造方法和一个onTouchEvent方法,而且这个onTouchEvent方法不是重写的,仅仅是自己定义的方法取这么个名字。那这个方法在哪调用的呢?
是在View.java里的
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
里面调用的,看到这里大家明白了,原来点击FrameLayout势必会调用到View.java里的onTouchEvent里来,然后又设了代理TouchDelegate,所有势必会调用TouchDelegate里的onTouchEvent方法。
现在我们来看此方法怎么做到的。
这是它的构造方法
public TouchDelegate(Rect bounds, View delegateView) {
//把Rect点击区域赋值
mBounds = bounds;
//拿到android定义的touch边界值
mSlop = ViewConfiguration.get(delegateView.getContext()).getScaledTouchSlop();
//初始化touch边界值的Rect区域,初始化时直接用传过来的rect
mSlopBounds = new Rect(bounds);
//跟进代码里,则个方法意思是让这个Rect区域变宽点,可以看源码注释
mSlopBounds.inset(-mSlop, -mSlop);
//需要代理的view,上面里例子就是FrameLayout里的mButton
mDelegateView = delegateView;
}
OK,现在来看onTouchEvent
//注意这里变量的命名,很规范,让人一看就明白什么意思
public boolean onTouchEvent(MotionEvent event) {
int x = (int)event.getX();
int y = (int)event.getY();
//是否发生event事件给需要代理的view
boolean sendToDelegate = false;
//是否点击在需代理的view上,这里不好翻译,大致意思看后面
boolean hit = true;
//是否已处理
boolean handled = false;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Rect bounds = mBounds;
//down事件包含在rect区域里,要发event事件给需代理的view
if (bounds.contains(x, y)) {
mDelegateTargeted = true;
sendToDelegate = true;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_MOVE:
sendToDelegate = mDelegateTargeted;
if (sendToDelegate) {
Rect slopBounds = mSlopBounds;
//用ScaleTouchSlop扩大的区域是否包含了event的x y坐标,hit默认为true,默认包含
if (!slopBounds.contains(x, y)) {
hit = false;
}
}
break;
case MotionEvent.ACTION_CANCEL:
sendToDelegate = mDelegateTargeted;
mDelegateTargeted = false;
break;
}
if (sendToDelegate) {
final View delegateView = mDelegateView;
if (hit) {
// Offset event coordinates to be inside the target view
//这里重设了event的坐标,刚开始不明白setLocation用法,特别是跟到源码里看就更不明白了,发现源码里有个offsetLocation(x - oldX, y - oldY)方法,这个方法实现了ViewGroup中的childView上的touchEvent事件的x y坐标是相对于自身的左上角为00的边界。
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;
//上面英文解释也很清楚,是为了追踪preesed状态的,就是当move事件一直移动还没up前移出了设定的rect点击区域的时候需要重写设置event的坐标
event.setLocation(-(slop * 2), -(slop * 2));
}
handled = delegateView.dispatchTouchEvent(event);
}
return handled;
}
Ok,这里就分析差不多了,至少我弄明白了。
然后说说MotionEvent里的offsetLocation方法,在ViewGroup中用来将childView的touchEvent的坐标偏移成相对自身左上角为0点的起始坐标的。
引用篇博客http://blog.csdn.net/bigconvience/article/details/26391743
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
/*直接对MotionEvent进行坐标变换,将MotionEvent传递下去*/
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event);
/*回复MotionEvent*/
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
主要是上面先偏移event.offsetLocation(offsetX, offsetY);
然后再偏移回来event.offsetLocation(-offsetX, -offsetY);
中间让childView自己去分发TouchEvent
handled = child.dispatchTouchEvent(event);
所以到这里整个TouchDelegate类里的所有疑问都理清楚了,完全可以自己实现一个类似的类了,不错!
主要是觉得这个类简单但是可以学习的东西很多,遂连夜记录下来!共勉
用户评论