在本文中,我们将给您介绍关于Viewgroupmeasurechild的详细内容,此外,我们还将为您提供关于Andriod从源码的角度详解View,ViewGroup的Touch事件的分发机制、And
在本文中,我们将给您介绍关于Viewgroup measure child的详细内容,此外,我们还将为您提供关于Andriod 从源码的角度详解 View,ViewGroup 的 Touch 事件的分发机制、Android - 自定义控件 - 继承 View 与 ViewGroup 的初步理解、Android View&ViewGroup相关类基础关系图、Android ViewParent是否保证是Viewgroup?的知识。
本文目录一览:- Viewgroup measure child
- Andriod 从源码的角度详解 View,ViewGroup 的 Touch 事件的分发机制
- Android - 自定义控件 - 继承 View 与 ViewGroup 的初步理解
- Android View&ViewGroup相关类基础关系图
- Android ViewParent是否保证是Viewgroup?
Viewgroup measure child
理解 MeasureSpec:
- spec(规格要求,分为 width spec 和 height spec) 是一个 int 型的 ** 组合值 **,包括 spec mode 和 size 两个值;
- MeasureSpec 是产生 spec 的工具;
- child 的 spec 是由其自身的布局参数 (layout_width,layout_height 等)以及 parent 的 spec 共同决定的
以宽度计算为例:
childDimension_代表_layout_width;
-
如果 parent 的 spec mode 是 exactly (即 parent 的宽度是确定的值):
- 如果 child 布局参数 layout_width 是 一个具体的值(大于 0), 那么 child 的 spec mode 就是 exactly, 而且 size 是_childDimension_;
- 如果 child 布局参数 layout_width 是 match_content, 那么 child 的 spec mode 就是 exactly, 而且 size 是 parent 的 size
- 如果 child 布局参数 layout_width 是 wrap_content, 那么 child 的 spec mode 就是 at_most, 而且 size 是 parent 的 size (不同的布局容器策略会不同,这里以默认的 ViewGroup 为例);
-
如果 parent 的 spec mode 是 exactly (即 parent 的宽度是确定的值): * 看代码..
-
如果 parent 的 spec mode 是 UNSPECIFIED (即 parent 的宽度是确定的值): * 看代码..
参数解释:
- spec 是这个 vg 的 parent 为此 vg 赋予的 MeasureSpec;
- padding 是此 vg 的 padding;
- childDimension 是 child 的 layout 模式 (wrap_content,即 - 2、match_parent,即 - 1) 或具体的值;
返回值是为 child 产生的 MeasureSpec
//ViewGroup.java
* Does the hard part of measureChildren: figuring out the MeasureSpec to
* pass to a particular child. This method figures out the right MeasureSpec
* for one dimension (height or width) of one child view.
*
* The goal is to combine information from our MeasureSpec with the
* LayoutParams of the child to get the best possible results. For example,
* if the this view knows its size (because its MeasureSpec has a mode of
* EXACTLY), and the child has indicated in its LayoutParams that it wants
* to be the same size as the parent, the parent should ask the child to
* layout given an exact size.
*
* @param spec The requirements for this view
* @param padding The padding of this view for the current dimension and
* margins, if applicable
* @param childDimension How big the child wants to be in the current
* dimension
* @return a MeasureSpec integer for the child
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can''t be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can''t be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
Andriod 从源码的角度详解 View,ViewGroup 的 Touch 事件的分发机制
ViewGroup 的事件分发机制
我们用手指去触摸 Android 手机屏幕,就会产生一个触摸事件,但是这个触摸事件在底层是怎么分发的呢?这个我还真不知道,这里涉及到操作硬件(手机屏幕)方面的知识,也就是 Linux 内核方面的知识,我也没有了解过这方面的东西,所以我们可能就往上层来分析分析,我们知道 Android 中负责与用户交互,与用户操作紧密相关的四大组件之一是 Activity, 所以我们有理由相信 Activity 中存在分发事件的方法,这个方法就是 dispatchTouchEvent (), 我们先看其源码吧
[java] view plaincopy
public boolean dispatchTouchEvent(MotionEvent ev) {
// 如果是按下状态就调用 onUserInteraction () 方法,onUserInteraction () 方法
// 是个空的方法, 我们直接跳过这里看下面的实现
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//getWindow ().superDispatchTouchEvent (ev) 返回 false,这个事件就交给 Activity
// 来处理, Activity 的 onTouchEvent () 方法直接返回了 false
return onTouchEvent(ev);
}
这个方法中我们还是比较关心 getWindow () 的 superDispatchTouchEvent () 方法,getWindow () 返回当前 Activity 的顶层窗口 Window 对象,我们直接看 Window API 的 superDispatchTouchEvent () 方法
[java] view plaincopy
/**
* Used by custom windows, such as Dialog, to pass the touch screen event
* further down the view hierarchy. Application developers should
* not need to implement or call this.
*
*/
public abstract boolean superDispatchTouchEvent(MotionEvent event);
这个是个抽象方法,所以我们直接找到其子类来看看 superDispatchTouchEvent () 方法的具体逻辑实现,Window 的唯一子类是 PhoneWindow, 我们就看看 PhoneWindow 的 superDispatchKeyEvent () 方法
[java] view plaincopy
public boolean superDispatchKeyEvent(KeyEvent event) {
return mDecor.superDispatchKeyEvent(event);
}
里面直接调用 DecorView 类的 superDispatchKeyEvent 方法,或许很多人不了解 DecorView 这个类,DecorView 是 PhoneWindow 的一个 final 的内部类并且继承 FrameLayout 的,也是 Window 界面的最顶层的 View 对象,这是什么意思呢?别着急,我们接着往下看
我们先新建一个项目,取名 AndroidTouchEvent,然后直接用模拟器运行项目, MainActivity 的布局文件为
[html] view plaincopy
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:text="@string/hello_world" />
</RelativeLayout>
利用 hierarchyviewer 工具来查看下 MainActivity 的 View 的层次结构,如下图
我们看到最顶层就是 PhoneWindow$DecorView,接着 DecorView 下面有一个 LinearLayout, LinearLayout 下面有两个 FrameLayout
上面那个 FrameLayout 是用来显示标题栏的,这个 Demo 中是一个 TextView, 当然我们还可以定制我们的标题栏,利用 getWindow ().setFeatureInt (Window.FEATURE_CUSTOM_TITLE,R.layout.XXX); xxx 就是我们自定义标题栏的布局 XML 文件
下面的 FrameLayout 是用来装载 ContentView 的,也就是我们在 Activity 中利用 setContentView () 方法设置的 View,现在我们知道了,原来我们利用 setContentView () 设置 Activity 的 View 的外面还嵌套了这么多的东西
我们来理清下思路,Activity 的最顶层窗体是 PhoneWindow, 而 PhoneWindow 的最顶层 View 是 DecorView,接下来我们就看 DecorView 类的 superDispatchTouchEvent () 方法
[java] view plaincopy
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
在里面调用了父类 FrameLayout 的 dispatchTouchEvent () 方法,而 FrameLayout 中并没有 dispatchTouchEvent () 方法,所以我们直接看 ViewGroup 的 dispatchTouchEvent () 方法
[java] view plaincopy
/**
* {@inheritDoc}
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
final float xf = ev.getX();
final float yf = ev.getY();
final float scrolledXFloat = xf + mScrollX;
final float scrolledYFloat = yf + mScrollY;
final Rect frame = mTempRect;
// 这个值默认是 false, 然后我们可以通过 requestDisallowInterceptTouchEvent (boolean disallowIntercept) 方法
// 来改变 disallowIntercept 的值
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
// 这里是 ACTION_DOWN 的处理逻辑
if (action == MotionEvent.ACTION_DOWN) {
// 清除 mMotionTarget, 每次 ACTION_DOWN 都很设置 mMotionTarget 为 null
if (mMotionTarget != null) {
mMotionTarget = null;
}
//disallowIntercept 默认是 false, 就看 ViewGroup 的 onInterceptTouchEvent () 方法
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
ev.setAction(MotionEvent.ACTION_DOWN);
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
// 遍历其子 View
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
// 如果该子 View 是 VISIBLE 或者该子 View 正在执行动画, 表示该 View 才
// 可以接受到 Touch 事件
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
// 获取子 View 的位置范围
child.getHitRect(frame);
// 如 Touch 到屏幕上的点在该子 View 上面
if (frame.contains(scrolledXInt, scrolledYInt)) {
// offset the event to the view''s coordinate system
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
// 调用该子 View 的 dispatchTouchEvent () 方法
if (child.dispatchTouchEvent(ev)) {
// 如果 child.dispatchTouchEvent (ev) 返回 true 表示
// 该事件被消费了,设置 mMotionTarget 为该子 View
mMotionTarget = child;
// 直接返回 true
return true;
}
// The event didn''t get handled, try the next view.
// Don''t reset the event''s location, it''s not
// necessary here.
}
}
}
}
}
// 判断是否为 ACTION_UP 或者 ACTION_CANCEL
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
if (isUpOrCancel) {
// 如果是 ACTION_UP 或者 ACTION_CANCEL, 将 disallowIntercept 设置为默认的 false
// 假如我们调用了 requestDisallowInterceptTouchEvent () 方法来设置 disallowIntercept 为 true
// 当我们抬起手指或者取消 Touch 事件的时候要将 disallowIntercept 重置为 false
// 所以说上面的 disallowIntercept 默认在我们每次 ACTION_DOWN 的时候都是 false
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// The event wasn''t an ACTION_DOWN, dispatch it to our target if
// we have one.
final View target = mMotionTarget;
//mMotionTarget 为 null 意味着没有找到消费 Touch 事件的 View, 所以我们需要调用 ViewGroup 父类的
//dispatchTouchEvent () 方法,也就是 View 的 dispatchTouchEvent () 方法
if (target == null) {
// We don''t have a target, this means we''re handling the
// event as a regular view.
ev.setLocation(xf, yf);
if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
}
return super.dispatchTouchEvent(ev);
}
// 这个 if 里面的代码 ACTION_DOWN 不会执行,只有 ACTION_MOVE
//ACTION_UP 才会走到这里, 假如在 ACTION_MOVE 或者 ACTION_UP 拦截的
//Touch 事件, 将 ACTION_CANCEL 派发给 target,然后直接返回 true
// 表示消费了此 Touch 事件
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
ev.setAction(MotionEvent.ACTION_CANCEL);
ev.setLocation(xc, yc);
if (!target.dispatchTouchEvent(ev)) {
}
// clear the target
mMotionTarget = null;
// Don''t dispatch this event to our own view, because we already
// saw it when intercepting; we just want to give the following
// event to the normal onTouchEvent().
return true;
}
if (isUpOrCancel) {
mMotionTarget = null;
}
// finally offset the event to the target''s coordinate system and
// dispatch the event.
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setLocation(xc, yc);
if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
mMotionTarget = null;
}
// 如果没有拦截 ACTION_MOVE, ACTION_DOWN 的话,直接将 Touch 事件派发给 target
return target.dispatchTouchEvent(ev);
}
这个方法相对来说还是蛮长,不过所有的逻辑都写在一起,看起来比较方便,接下来我们就具体来分析一下
我们点击屏幕上面的 TextView 来看看 Touch 是如何分发的,先看看 ACTION_DOWN
在 DecorView 这一层会直接调用 ViewGroup 的 dispatchTouchEvent (), 先看 18 行,每次 ACTION_DOWN 都会将 mMotionTarget 设置为 null, mMotionTarget 是什么?我们先不管,继续看代码,走到 25 行, disallowIntercept 默认为 false,我们再看 ViewGroup 的 onInterceptTouchEvent () 方法
[java] view plaincopy
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
直接返回 false, 继续往下看,循环遍历 DecorView 里面的 Child,从上面的 MainActivity 的层次结构图我们可以看出,DecorView 里面只有一个 Child 那就是 LinearLayout, 第 43 行判断 Touch 的位置在不在 LinnearLayout 上面,这是毫无疑问的,所以直接跳到 51 行, 调用 LinearLayout 的 dispatchTouchEvent () 方法,LinearLayout 也没有 dispatchTouchEvent () 这个方法,所以也是调用 ViewGroup 的 dispatchTouchEvent () 方法,所以这个方法卡在 51 行没有继续下去,而是去先执行 LinearLayout 的 dispatchTouchEvent ()
LinearLayout 调用 dispatchTouchEvent () 的逻辑跟 DecorView 是一样的,所以也是遍历 LinearLayout 的两个 FrameLayout,判断 Touch 的是哪个 FrameLayout,很明显是下面那个,调用下面那个 FrameLayout 的 dispatchTouchEvent (), 所以 LinearLayout 的 dispatchTouchEvent () 卡在 51 也没继续下去
继续调用 FrameLayout 的 dispatchTouchEvent () 方法,和上面一样的逻辑,下面的 FrameLayout 也只有一个 Child,就是 RelativeLayout,FrameLayout 的 dispatchTouchEvent () 继续卡在 51 行,先执行 RelativeLayout 的 dispatchTouchEvent () 方法
执行 RelativeLayout 的 dispatchTouchEvent () 方法逻辑还是一样的,循环遍历 RelativeLayout 里面的孩子,里面只有一个 TextView, 所以这里就调用 TextView 的 dispatchTouchEvent (), TextView 并没有 dispatchTouchEvent () 这个方法,于是找 TextView 的父类 View,在看 View 的 dispatchTouchEvent () 的方法之前,我们先理清下上面这些 ViewGroup 执行 dispatchTouchEvent () 的思路,我画了一张图帮大家理清下(这里没有画出 onInterceptTouchEvent()方法)
上面的 ViewGroup 的 Touch 事件分发就告一段落先,因为这里要调用 TextView(也就是 View)的 dispatchTouchEvent () 方法,所以我们先分析 View 的 dispatchTouchEvent () 方法在将上面的继续下去
View 的 Touch 事件分发机制
我们还是先看 View 的 dispatchTouchEvent () 方法的源码
[java] view plaincopy
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
在这个方法里面,先进行了一个判断
第一个条件 mOnTouchListener 就是我们调用 View 的 setTouchListener () 方法设置的
第二个条件是判断 View 是否为 enabled 的, View 一般都是 enabled, 除非你手动设置为 disabled
第三个条件就是 OnTouchListener 接口的 onTouch () 方法的返回值了,如果调用了 setTouchListener () 设置 OnTouchListener,并且 onTouch () 方法返回 true,View 的 dispatchTouchEvent () 方法就直接返回 true, 否则就执行 View 的 onTouchEvent () 并返回 View 的 onTouchEvent () 的值
现在你了解了 View 的 onTouchEvent () 方法和 onTouch () 的关系了吧,为什么 Android 提供了处理 Touch 事件 onTouchEvent () 方法还要增加一个 OnTouchListener 接口呢?我觉得 OnTouchListener 接口是对处理 Touch 事件的屏蔽和扩展作用吧,屏蔽作用我就不举例介绍了,看上面的源码就知道了,我就说下扩展吧,比如我们要打印 View 的 Touch 的点的坐标,我们可以自定义一个 View 如下
[java] view plaincopy
public class CustomView extends View {
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("tag", "X 的坐标 = " + event.getX() + " Y 的坐标 = " + event.getY());
return super.onTouchEvent(event);
}
}
也可以直接对 View 设置 OnTouchListener 接口,在 return 的时候调用下 v.onTouchEvent ()
[java] view plaincopy
view.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.i("tag", "X 的坐标 = " + event.getX() + " Y 的坐标 = " + event.getY());
return v.onTouchEvent(event);
}
});
这样子也实现了我们所需要的功能,所以我认为 OnTouchListener 是对 onTouchEvent () 方法的一个屏蔽和扩展作用,假如你有不一样的理解,你也可以告诉我下,这里就不纠结这个了。
我们再看 View 的 onTouchEvent () 方法
[java] view plaincopy
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
// 如果设置了 Touch 代理,就交给代理来处理,mTouchDelegate 默认是 null
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
// 如果 View 是 clickable 或者 longClickable 的 onTouchEvent 就返回 true, 否则返回 false
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
if ((mPrivateFlags & PRESSED) != 0 || prepressed) {
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (!mHasPerformedLongPress) {
removeLongPressCallback();
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
mPrivateFlags |= PRESSED;
refreshDrawableState();
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
mUnsetPressedState.run();
}
removeTapCallback();
}
break;
case MotionEvent.ACTION_DOWN:
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPrivateFlags |= PREPRESSED;
mHasPerformedLongPress = false;
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
break;
case MotionEvent.ACTION_CANCEL:
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
removeTapCallback();
break;
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();
// 当手指在 View 上面滑动超过 View 的边界,
int slop = mTouchSlop;
if ((x < 0 - slop) || (x >= getWidth() + slop) ||
(y < 0 - slop) || (y >= getHeight() + slop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PRESSED) != 0) {
removeLongPressCallback();
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
}
}
break;
}
return true;
}
return false;
}
这个方法也是比较长的,我们先看第 4 行,如果一个 View 是 disabled, 并且该 View 是 Clickable 或者 longClickable, onTouchEvent () 就不执行下面的代码逻辑直接返回 true, 表示该 View 就一直消费 Touch 事件,如果一个 enabled 的 View, 并且是 clickable 或者 longClickable 的,onTouchEvent () 会执行下面的代码逻辑并返回 true,综上,一个 clickable 或者 longclickable 的 View 是一直消费 Touch 事件的,而一般的 View 既不是 clickable 也不是 longclickable 的 (即不会消费 Touch 事件,只会执行 ACTION_DOWN 而不会执行 ACTION_MOVE 和 ACTION_UP) Button 是 clickable 的,可以消费 Touch 事件,但是我们可以通过 setClickable () 和 setLongClickable () 来设置 View 是否为 clickable 和 longClickable。当然还可以通过重写 View 的 onTouchEvent () 方法来控制 Touch 事件的消费与否
我们在看 57 行的 ACTION_DOWN, 新建一个 CheckForTap,我们看看 CheckForTap 是什么
[java] view plaincopy
private final class CheckForTap implements Runnable {
public void run() {
mPrivateFlags &= ~PREPRESSED;
mPrivateFlags |= PRESSED;
refreshDrawableState();
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
postCheckForLongClick(ViewConfiguration.getTapTimeout());
}
}
}
原来是个 Runnable 对象,然后使用 Handler 的 post 方法延时 ViewConfiguration.getTapTimeout () 执行 CheckForTap 的 run () 方法,在 run 方法中先判断 view 是否 longClickable 的,一般的 View 都是 false, postCheckForLongClick (ViewConfiguration.getTapTimeout ()) 这段代码就是执行长按的逻辑的代码,只有当我们设置为 longClickble 才会去执行 postCheckForLongClick (ViewConfiguration.getTapTimeout ()),这里我就不介绍了
由于考虑到文章篇幅的问题,我就不继续分析 View 的长按事件和点击事件了,在这里我直接得出结论吧
长按事件是在 ACTION_DOWN 中执行,点击事件是在 ACTION_UP 中执行,要想执行长按事件,这个 View 必须是 longclickable 的, 也许你会纳闷,一般的 View 不是 longClickable 为什么也会执行长按事件呢?我们要执行长按事件必须要调用 setOnLongClickListener () 设置 OnLongClickListener 接口,我们看看这个方法的源码
[java] view plaincopy
public void setOnLongClickListener(OnLongClickListener l) {
if (!isLongClickable()) {
setLongClickable(true);
}
mOnLongClickListener = l;
}
看到没有,如果这个 View 不是 longClickable 的,我们就调用 setLongClickable (true) 方法设置为 longClickable 的,所以才会去执行长按方法 onLongClick ();
要想执行点击事件,这个 View 就必须要消费 ACTION_DOWN 和 ACTION_MOVE 事件,并且没有设置 OnLongClickListener 的情况下,如果设置了 OnLongClickListener 的情况下,需要 onLongClick () 返回 false 才能执行到 onClick () 方法,也许你又会纳闷,一般的 View 默认是不消费 touch 事件的,这不是和你上面说的相违背嘛,我们要向执行点击事件必须要调用 setOnClickListener () 来设置 OnClickListener 接口,我们看看这个方法的源码就知道了
[java] view plaincopy
public void setOnClickListener(OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
mOnClickListener = l;
}
所以说一个 enable 的并且是 clickable 的 View 是一直消费 touch 事件的,所以才会执行到 onClick()方法
对于 View 的 Touch 事件的分发机制算是告一段落了,从上面我们可以得出 TextView 的 dispatchTouchEvent () 的返回 false 的,即不消费 Touch 事件。我们就要往上看 RelativeLayout 的 dispatchTouchEvent () 方法的 51 行,由于 TextView.dispatchTouchEvent () 为 false, 导致 mMotionTarget 没有被赋值,还是 null, 继续往下走执行 RelativeLayout 的 dispatchTouchEvent () 方法,来到第 84 行, 判断 target 是否为 null,这个 target 就是 mMotionTarget,满足条件,执行 92 行的 super.dispatchTouchEvent (ev) 代码并返回, 这里调用的是 RelativeLayout 父类 View 的 dispatchTouchEvent () 方法,由于 RelativeLayout 没有设置 onTouchListener, 所以这里直接调用 RelativeLayout (其实就是 View, 因为 RelativeLayout 没有重写 onTouchEvent ()) 的 onTouchEvent () 方法 由于 RelativeLayout 既不是 clickable 的也是 longClickable 的,所以其 onTouchEvent () 方法 false, RelativeLayout 的 dispatchTouchEvent () 也是返回 false, 这里就执行完了 RelativeLayout 的 dispatchTouchEvent () 方法
继续执行 FrameLayout 的 dispatchTouchEvent () 的第 51 行,由于 RelativeLayout.dispatchTouchEvent () 返回的是 false, 跟上面的逻辑是一样的, 也是执行到 92 行的 super.dispatchTouchEvent (ev) 代码并返回,然后执行 FrameLayout 的 onTouchEvent () 方法,而 FrameLayout 的 onTouchEvent () 也是返回 false, 所以 FrameLayout 的 dispatchTouchEvent () 方法返回 false, 执行完毕 FrameLayout 的 dispatchTouchEvent () 方法
在上面的我就不分析了,大家自行分析一下,跟上面的逻辑是一样的,我直接画了个图来帮大家理解下(这里没有画出 onInterceptTouchEvent()方法)
所以我们点击屏幕上面的 TextView 的事件分发流程是上图那个样子的,表示 Activity 的 View 都不消费 ACTION_DOWN 事件,所以就不能在触发 ACTION_MOVE, ACTION_UP 等事件了,具体是为什么?我还不太清楚,毕竟从 Activity 到 TextView 这一层是分析不出来的,估计是在底层实现的。
但如果将 TextView 换成 Button,流程是不是还是这个样子呢?答案不是,我们来分析分析一下,如果是 Button , Button 是一个 clickable 的 View,onTouchEvent () 返回 true, 表示他一直消费 Touch 事件,所以 Button 的 dispatchTouchEvent () 方法返回 true, 回到 RelativeLayout 的 dispatchTouchEvent () 方法的 51 行,满足条件,进入到 if 方法体,设置 mMotionTarget 为 Button,然后直接返回 true, RelativeLayout 的 dispatchTouchEvent () 方法执行完毕, 不会调用到 RelativeLayout 的 onTouchEvent () 方法
然后到 FrameLayout 的 dispatchTouchEvent () 方法的 51 行,由于 RelativeLayout.dispatchTouchEvent () 返回 true, 满足条件,进入 if 方法体,设置 mMotionTarget 为 RelativeLayout,注意下,这里的 mMotionTarget 跟 RelativeLayout 的 dispatchTouchEvent () 方法的 mMotionTarget 不是同一个哦,因为他们是不同的方法中的,然后返回 true
同理 FrameLayout 的 dispatchTouchEvent () 也是返回 true, DecorView 的 dispatchTouchEvent () 方法也返回 true, 还是画一个流程图(这里没有画出 onInterceptTouchEvent()方法)给大家理清下
从上面的流程图得出一个结论,Touch 事件是从顶层的 View 一直往下分发到手指按下的最里面的 View,如果这个 View 的 onTouchEvent () 返回 false, 即不消费 Touch 事件,这个 Touch 事件就会向上找父布局调用其父布局的 onTouchEvent () 处理,如果这个 View 返回 true, 表示消费了 Touch 事件,就不调用父布局的 onTouchEvent ()
接下来我们用一个自定义的 ViewGroup 来替换 RelativeLayout, 自定义 ViewGroup 代码如下
[java] view plaincopy
package com.example.androidtouchevent;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.RelativeLayout;
public class CustomLayout extends RelativeLayout {
public CustomLayout(Context context, AttributeSet attrs) {
super(context, attrs, 0);
}
public CustomLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
}
}
我们就重写了 onInterceptTouchEvent (), 返回 true, RelativeLayout 默认是返回 false, 然后再 CustomLayout 布局中加一个 Button , 如下图
我们这次不从 DecorView 的 dispatchTouchEvent () 分析了,直接从 CustomLayout 的 dispatchTouchEvent () 分析
我们先看 ACTION_DOWN 来到 25 行,由于我们重写了 onInterceptTouchEvent () 返回 true, 所以不走这个 if 里面,直接往下看代码,来到 84 行, target 为 null, 所以进入 if 方法里面,直接调用 super.dispatchTouchEvent () 方法, 也就是 View 的 dispatchTouchEvent () 方法,而在 View 的 dispatchTouchEvent () 方法中是直接调用 View 的 onTouchEvent () 方法,但是 CustomLayout 重写了 onTouchEvent (),所以这里还是调用 CustomLayout 的 onTouchEvent (), 这个方法返回 false, 不消费 Touch 事件,所以不会在触发 ACTION_MOVE,ACTION_UP 等事件了,这里我再画一个流程图吧(含有 onInterceptTouchEvent () 方法的)
好了,就分析到这里吧,差不多分析完了,还有一种情况没有分析到,例如我将 CustomLayout 的代码改成下面的情形,Touch 事件又是怎么分发的呢?我这里就不带大家分析了
[java] view plaincopy
package com.example.androidtouchevent;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.RelativeLayout;
public class CustomLayout extends RelativeLayout {
public CustomLayout(Context context, AttributeSet attrs) {
super(context, attrs, 0);
}
public CustomLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(ev.getAction() == MotionEvent.ACTION_MOVE){
return true;
}
return super.onInterceptTouchEvent(ev);
}
}
这篇文章的篇幅有点长,如果你想了解 Touch 事件的分发机制,你一定要认真看完,下面来总结一下吧
1.Activity 的最顶层 Window 是 PhoneWindow,PhoneWindow 的最顶层 View 是 DecorView
2. 一个 clickable 或者 longClickable 的 View 会永远消费 Touch 事件,不管他是 enabled 还是 disabled 的
3.View 的长按事件是在 ACTION_DOWN 中执行,要想执行长按事件该 View 必须是 longClickable 的,并且不能产生 ACTION_MOVE
4.View 的点击事件是在 ACTION_UP 中执行,想要执行点击事件的前提是消费了 ACTION_DOWN 和 ACTION_MOVE, 并且没有设置 OnLongClickListener 的情况下,如设置了 OnLongClickListener 的情况,则必须使 onLongClick () 返回 false
5. 如果 View 设置了 onTouchListener 了,并且 onTouch () 方法返回 true,则不执行 View 的 onTouchEvent () 方法,也表示 View 消费了 Touch 事件,返回 false 则继续执行 onTouchEvent ()
6.Touch 事件是从最顶层的 View 一直分发到手指 touch 的最里层的 View, 如果最里层 View 消费了 ACTION_DOWN 事件(设置 onTouchListener,并且 onTouch () 返回 true 或者 onTouchEvent () 方法返回 true)才会触发 ACTION_MOVE,ACTION_UP 的发生,如果某个 ViewGroup 拦截了 Touch 事件,则 Touch 事件交给 ViewGroup 处理
7.Touch 事件的分发过程中,如果消费了 ACTION_DOWN, 而在分发 ACTION_MOVE 的时候,某个 ViewGroup 拦截了 Touch 事件,就像上面那个自定义 CustomLayout,则会将 ACTION_CANCEL 分发给该 ViewGroup 下面的 Touch 到的 View, 然后将 Touch 事件交给 ViewGroup 处理,并返回 true
Android - 自定义控件 - 继承 View 与 ViewGroup 的初步理解
继承 View 需要走的流程是:
1. 构造实例化, public ChildView (Context context, @Nullable AttributeSet attrs)
2. 测量自身的高和宽 onMeasure-->setMeasuredDimension (宽,高)
3.onDraw 绘制,需要 X 轴,Y 轴
继承 ViewGroup 需要走的流程是:
1. 构造实例化, public ChildView (Context context, @Nullable AttributeSet attrs)
2.onMeasure 测量子控件的高和宽,子控件.measure (宽,高),而自己的高和宽交给父控件去测量,因为我是父控件的子控件
3.onLayout 给子控件排版指定位置
布局文件,指定自定义类:
<!-- 继承View 与 继承ViewGroup的初步理解 -->
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:myswitch="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="400dp"
android:layout_height="500dp">
<!-- 爷爷类,爷爷类有一个孩子(爸爸类) -->
<custom.view.upgrade.view_viewgroup_theory.GrandpaViewGroup
android:layout_width="300dp"
android:layout_height="300dp"
android:background="#3300cc"
android:layout_centerInParent="true"
> <!--
虽然android:layout_centerInParent="true"属性可以去居中
但这是Android RelativeLayout 对爷爷类进行了居中的排版固定位置处理
-->
<!-- 爸爸类,爸爸类有一个孩子(孩子类) -->
<custom.view.upgrade.view_viewgroup_theory.FatherViewGroup
android:layout_width="200dp"
android:layout_height="200dp"
android:background="#00ccff">
<!-- 孩子类目前不包含孩子 -->
<custom.view.upgrade.view_viewgroup_theory.ChildView
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#cc3300"/>
</custom.view.upgrade.view_viewgroup_theory.FatherViewGroup>
</custom.view.upgrade.view_viewgroup_theory.GrandpaViewGroup>
</RelativeLayout>
爷爷类,GrandpaViewGroup:
package custom.view.upgrade.view_viewgroup_theory;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
/**
* 爷爷类,爷爷类有自己的孩子(爸爸类) ----> 爸爸类有自己的孩子(孩子类)
*/
public class GrandpaViewGroup extends ViewGroup {
private static final String TAG = GrandpaViewGroup.class.getSimpleName();
/**
* 此两个参数的构造方法,用于在xml布局指定初始化,并传入xml中的所有属性
* @param context
* @param attrs
*/
public GrandpaViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
// 爸爸类
private View fatherViewGroup;
/**
* 当xml布局完成加载后,调用此方法
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate();
// 获取子控件(爸爸类)
fatherViewGroup = getChildAt(0);
}
/**
* 由于当前是ViewGroup所以测量子控件的宽和高
* ,如果当前是View就是此类自己的高和宽
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 测量子控件(爸爸类)的宽和高
// 宽和高就是 爸爸类在xml布局中设置的值
fatherViewGroup.measure(fatherViewGroup.getLayoutParams().width, fatherViewGroup.getLayoutParams().height);
// 想获取测量后子控件(爸爸类)的高和宽,是无法获取到的,因为子控件(爸爸类)是ViewGroup,拿到测量后的高和宽需要 View-->setMeasuredDimension()
// 测试下:子控件(爸爸类)的高和宽
Log.d(TAG, "fatherViewGroup.getMeasuredWidth():" + fatherViewGroup.getMeasuredWidth() +
" fatherViewGroup.getMeasuredHeight():" + fatherViewGroup.getMeasuredHeight());
}
/**
* 排版 指定位置,只给子控件指定位置的
* @param changed 当发生改变
* @param l 左边线距离左边距离,注意:值是由父控件Layout后,处理了逻辑后传递过来的
* @param t 上边线距离上边距离,注意:值是由父控件Layout后,处理了逻辑后传递过来的
* @param r 右边线距离左边距离,注意:值是由父控件Layout后,处理了逻辑后传递过来的
* @param b 底边线距离上边距离,注意:值是由父控件Layout后,处理了逻辑后传递过来的
*
* 父控件必须排版了layout此类的位置,此onLayout方法才会执行,否则不执行
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 测试下:获取自身当前GrandpaViewGroup测量后的宽和高
int measuredWidth = getMeasuredWidth();
int measuredHeight = getMeasuredHeight();
Log.d(TAG, "measuredWidth:" + measuredWidth + " measuredHeight:" + measuredHeight);
// 给子控件(爸爸类)指定位置
// Y轴与X轴移动计算一样,X计算:移动X轴方向,得到自身(GrandpaViewGroup爷爷类)宽度的一半 减 子控件(爸爸类)的一半就居中了
int fatherL = (measuredWidth / 2) - (fatherViewGroup.getLayoutParams().width / 2);
int fatherT = (measuredHeight / 2) - (fatherViewGroup.getLayoutParams().height / 2);
// L位置增加了,R位置也需要增加
int fatherR = fatherViewGroup.getLayoutParams().width + fatherL;
int fatherB = fatherViewGroup.getLayoutParams().height + fatherT;
fatherViewGroup.layout(fatherL, fatherT, fatherR, fatherB);
}
/**
* 为什么继承了ViewGroup就不需要onDraw绘制了,因为绘制都是在View中处理
* 继承了ViewGroup需要做好测量-->排版固定位置就好了,绘制是交给View去处理
*/
}
爸爸类,FatherViewGroup:
package custom.view.upgrade.view_viewgroup_theory;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
/**
* 爸爸类,爸爸类有自己的孩子
*/
public class FatherViewGroup extends ViewGroup {
private static final String TAG = FatherViewGroup.class.getSimpleName();
/**
* 此两个参数的构造方法,用于在xml布局指定初始化,并传入xml中的所有属性
* @param context
* @param attrs
*/
public FatherViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
// 子控件(孩子类)
private View childView;
/**
* 当xml布局完成加载后,调用此方法
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate();
childView = getChildAt(0);
}
private int w;
private int h;
/**
* 测量子控件(孩子类)的高和宽
* @param widthMeasureSpec 可以转变为模式,也可以转变为父类给当前自己传递过来的宽
* @param heightMeasureSpec 可以转变为模式,也可以转变为父类给当前自己传递过来的高
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 可以获取模式
MeasureSpec.getMode(widthMeasureSpec);
MeasureSpec.getMode(heightMeasureSpec);
/**
* 可以获取父类(爷爷类)在测量方法--->
* fatherViewGroup.measure(fatherViewGroup.getLayoutParams()
* .width, fatherViewGroup.getLayoutParams().height);
* 传递过来的高和宽
*/
w = MeasureSpec.getSize(widthMeasureSpec);
h = MeasureSpec.getSize(heightMeasureSpec);
Log.d(TAG, " w:" + w + " h:" + h);
// 开始测量子控件(孩子类)
// 宽高都是孩子类在xml布局设置的宽高
childView.measure(childView.getLayoutParams().width, childView.getLayoutParams().height);
// 测试下:子控件(孩子类)的高和宽
/**
* 注意:为什么在这里又可以获取到子控件的宽和高呢?
* 因为子控件是View 并且这个View在测量方法中执行了 setMeasuredDimension(w, h);
*/
Log.d(TAG, "childView.getMeasuredWidth():" + childView.getMeasuredWidth() +
" childView.getMeasuredHeight():" + childView.getMeasuredHeight());
}
/**
* 排版 指定位置,只给子控件指定位置的
* @param changed 当发生改变
* @param l 左边线距离左边距离
* @param t 上边线距离上边距离
* @param r 右边线距离左边距离
* @param b 底边线距离上边距离
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 测试下:获取自身当前FatherViewGroup测量后的宽和高
// 注意:在这里无法获取,还不知道是什么原因!!!,
// 爷爷类可以获取到是因为爷爷类的父类控件是系统的RelativeLayout
int measuredWidth = getMeasuredWidth();
int measuredHeight = getMeasuredHeight();
Log.d(TAG, "爸爸类 >>> measuredWidth:" + measuredWidth + " measuredHeight:" + measuredHeight);
// 给子控件(爸爸类)指定位置
// Y轴与X轴移动计算一样,X计算:移动X轴方向,得到自身(FatherViewGroup爸爸类)宽度的一半 减 子控件(孩子类)的一本就居中了
int childL = (w / 2) - (childView.getMeasuredWidth() / 2);
int childT = (h / 2) - (childView.getMeasuredHeight() / 2);
// L位置增加了,R位置也需要增加
int childR = childView.getMeasuredWidth() + childL;
int childB = childView.getMeasuredHeight() + childT;
childView.layout(childL, childT, childR, childB);
}
/**
* 为什么继承了ViewGroup就不需要onDraw绘制了,因为绘制都是在View中处理
* 继承了ViewGroup需要做好测量-->排版固定位置就好了,绘制是交给View去处理
*/
}
孩子类,ChildView:
package custom.view.upgrade.view_viewgroup_theory;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
/**
* 孩子类,孩子类暂时还没有自己的孩子,所有是继承View
*/
public class ChildView extends View {
private static final String CONTEXT = "child";
// 创建画笔把文字画到画布中去
private Paint mPaint;
private Rect rect;
/**
* 此两个参数的构造方法,用于在xml布局指定初始化,并传入xml中的所有属性
* @param context
* @param attrs
*/
public ChildView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint();
// 画笔防锯齿
mPaint.setAntiAlias(true);
// 画笔白色
mPaint.setColor(Color.WHITE);
mPaint.setTextSize(40);
rect = new Rect();
}
/**
* 此高宽是父控件(爸爸类)传递过来的高和宽
*/
private int w;
private int h;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
w = MeasureSpec.getSize(widthMeasureSpec);
h = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(w, h);
}
/**
* 绘制的方法
* @param canvas 画布
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 拿到自身宽度的一半 减 文字宽度的一半
mPaint.getTextBounds(CONTEXT, 0, CONTEXT.length(), rect);
int x = (w / 2) - (rect.width() / 2);
int y = (h / 2) - (rect.height() / 2) + rect.height();
canvas.drawText(CONTEXT, x, y , mPaint);
}
}
效果图:
Android View&ViewGroup相关类基础关系图
一个很乱的树形结构,以View的直接子类集合红色方框为跟节点。
Android ViewParent是否保证是Viewgroup?
是否存在View.getParent()返回不属于ViewGroup类型的对象的实际情况?或者我可以安全地投射它而不首先检查它的类型,如下面的代码示例中所示?
if (getParent() == null){
throw new IllegalStateException("View does not have a parent,it cannot be rootview!");
}
ViewGroup parent = (ViewGroup) getParent();
– 在普通应用程序中,您通常了解父母是或可以 – 所以您可以跳过检查
关于Viewgroup measure child的介绍已经告一段落,感谢您的耐心阅读,如果想了解更多关于Andriod 从源码的角度详解 View,ViewGroup 的 Touch 事件的分发机制、Android - 自定义控件 - 继承 View 与 ViewGroup 的初步理解、Android View&ViewGroup相关类基础关系图、Android ViewParent是否保证是Viewgroup?的相关信息,请在本站寻找。
本文标签: