touch事件传递的三个重要方法
dispatchTouchEvent(MotionEvent ev)方法用于事件的分发,当事件传递给当前View时,会被调用,返回值受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件
/**
* ViewGroup
* @param ev
* @return
*/
public boolean dispatchTouchEvent(MotionEvent ev){
....//其他处理,在此不管
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;
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
child.getHitRect(frame);
if (frame.contains(scrolledXInt, scrolledYInt)) {
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
if (child.dispatchTouchEvent(ev)) {
mMotionTarget = child;
return true;
}
}
}
}
...//其他处理,在此不管
}
/**
* View
* @param ev
* @return
*/
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
由上面源码可以看出,当点击事件产生后,这个ViewGroup中的dispatchTouchEvent会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true,就表示要拦截当前事件,然后把事件交给这个ViewGroup处理,即它的onTouchEvent方法被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回false,就表示它 不拦截当前事件,然后继续传递给它的子元素,直至事件被最终处理。
一般情况下,我们不该在普通View内重写dispatchTouchEvent方法,因为它并不执行分发逻辑。当Touch事件到达View时,我们该做的就是是否在onTouchEvent事件中处理它。
流程分析
当一个Touch事件(触摸事件为例)到达根节点,即Acitivty的ViewGroup时,它会依次下发,下发的过程是调用子View(ViewGroup)的dispatchTouchEvent方法实现的。简单来说,就是ViewGroup遍历它包含着的子View,调用每个View的dispatchTouchEvent方法,而当子View为ViewGroup时,又会通过调用ViwGroup的dispatchTouchEvent方法继续调用其内部的View的dispatchTouchEvent方法。上述例子中的消息下发顺序是这样的:①-②-⑤-⑥-⑦-③-④。dispatchTouchEvent方法只负责事件的分发,它拥有boolean类型的返回值,当返回为true时,顺序下发会中断。在上述例子中如果⑤的dispatchTouchEvent返回结果为true,那么⑥-⑦-③-④将都接收不到本次Touch事件。
事件传递的相关结论:
- 同一个事件序列在手指接触到离开的整个过程以down事件开始,中间含有不定的move事件,最后以up事件结束,但有些特殊情况会没有up事件,需具体分析。
- 一个事件序列只能被一个View拦截消耗,同一个事件序列中的事件不能分别由两个View同时处理,但可以通过onTouchEvent强行传递给其他View处理。
- 某个View一旦决定拦截,同一个事件序列的onInterceptTouchEvent不会再被调用,只会调用一次。
- ViewGroup默认不拦截任何事件。因为Android源码中ViewGroup的onInterceptTouchEvent方法默认返回false
- View没有onEnterceptTouchEvent方法,一旦接收到点击事件,那么它的onTouchEvent方法就会被调用,
- View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false)。
- View的enable属性不影响onTouchEvent的默认返回值。
- onClick触发的前提是当前View是可点击的,且它收到了down和up的事件
- 通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程。
View的绘制流程
View的工作流程主要指measure、layout、draw三大流程,即测量,布局,和绘制,measure是确定View的测量宽高,layout是确定View的最终宽/高和四个顶点的位置,draw是将View绘制到屏幕上。
measure
measure过程的核心方法: measure() - onMeasure() - setMeasuredDimension()
measure调用onMeasure,onMeasure调用setMeasureDimension,measure,setMeasureDimension是final类型,view的子类不需要重写,onMeasure在view的子类中重写。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {
// first clears the measured dimension flag
mPrivateFlags &= ~MEASURED_DIMENSION_SET;
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_MEASURE);
}
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
// flag not set, setMeasuredDimension() was not invoked, we raise
// an exception to warn the developer
if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) {
throw new IllegalStateException("onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
}
mPrivateFlags |= LAYOUT_REQUIRED;
}
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
}
View的measure方法中会去调用View的onMeasure方法,因此只需要看onMeasure的实现即可。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
setMeasuredDimension方法会设置View的宽高度的测量值,我们可以看getDefaultSize这个方法即可。
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
MeasureSpec有三种模式分别是UNSPECIFIED, EXACTLY和AT_MOST。
EXACTLY表示父视图希望子视图的大小应该是由specSize的值来决定的,系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。
AT_MOST表示子视图最多只能是specSize中指定的大小,开发人员应该尽可能小得去设置这个视图,并且保证不会超过specSize。系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。
UNSPECIFIED表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制。这种情况比较少见,不太会用到。
layout
measure过程确定视图的大小,而layout过程确定视图的位置。loyout是从view的layout方法开始的:
public void layout(int l, int t, int r, int b) {
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
}
函数中参数l、t、r、b是指view的左、上、右、底的位置,这几个参数是父视图传入的,而根视图中参数是由performTraversals()方法传入的。
layout中调用了onLayout方法,在view中onLayout方法是一个空函数,他需要其子类实现。
我们来看一下LinearLayout的onlayout实现:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical();
} else {
layoutHorizontal();
}
}
其中的layoutHorizontal()为:
void More ...layoutHorizontal(int left, int top, int right, int bottom) {
1579 final boolean isLayoutRtl = isLayoutRtl();
1580 final int paddingTop = mPaddingTop;
1581
1582 int childTop;
1583 int childLeft;
1584
1585 // Where bottom of child should go
1586 final int height = bottom - top;
1587 int childBottom = height - mPaddingBottom;
1588
1589 // Space available for child
1590 int childSpace = height - paddingTop - mPaddingBottom;
1591
1592 final int count = getVirtualChildCount();
1593
1594 final int majorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
1595 final int minorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
1596
1597 final boolean baselineAligned = mBaselineAligned;
1598
1599 final int[] maxAscent = mMaxAscent;
1600 final int[] maxDescent = mMaxDescent;
1601
1602 final int layoutDirection = getLayoutDirection();
1603 switch (Gravity.getAbsoluteGravity(majorGravity, layoutDirection)) {
1604 case Gravity.RIGHT:
1605 // mTotalLength contains the padding already
1606 childLeft = mPaddingLeft + right - left - mTotalLength;
1607 break;
1608
1609 case Gravity.CENTER_HORIZONTAL:
1610 // mTotalLength contains the padding already
1611 childLeft = mPaddingLeft + (right - left - mTotalLength) / 2;
1612 break;
1613
1614 case Gravity.LEFT:
1615 default:
1616 childLeft = mPaddingLeft;
1617 break;
1618 }
1619
1620 int start = 0;
1621 int dir = 1;
1622 //In case of RTL, start drawing from the last child.
1623 if (isLayoutRtl) {
1624 start = count - 1;
1625 dir = -1;
1626 }
1627
1628 for (int i = 0; i < count; i++) {
1629 int childIndex = start + dir * i;
1630 final View child = getVirtualChildAt(childIndex);
1631
1632 if (child == null) {
1633 childLeft += measureNullChild(childIndex);
1634 } else if (child.getVisibility() != GONE) {
1635 final int childWidth = child.getMeasuredWidth();
1636 final int childHeight = child.getMeasuredHeight();
1637 int childBaseline = -1;
1638
1639 final LinearLayout.LayoutParams lp =
1640 (LinearLayout.LayoutParams) child.getLayoutParams();
1641
1642 if (baselineAligned && lp.height != LayoutParams.MATCH_PARENT) {
1643 childBaseline = child.getBaseline();
1644 }
1645
1646 int gravity = lp.gravity;
1647 if (gravity < 0) {
1648 gravity = minorGravity;
1649 }
1650
1651 switch (gravity & Gravity.VERTICAL_GRAVITY_MASK) {
1652 case Gravity.TOP:
1653 childTop = paddingTop + lp.topMargin;
1654 if (childBaseline != -1) {
1655 childTop += maxAscent[INDEX_TOP] - childBaseline;
1656 }
1657 break;
1658
1659 case Gravity.CENTER_VERTICAL:
1660 // Removed support for baseline alignment when layout_gravity or
1661 // gravity == center_vertical. See bug #1038483.
1662 // Keep the code around if we need to re-enable this feature
1663 // if (childBaseline != -1) {
1664 // // Align baselines vertically only if the child is smaller than us
1665 // if (childSpace - childHeight > 0) {
1666 // childTop = paddingTop + (childSpace / 2) - childBaseline;
1667 // } else {
1668 // childTop = paddingTop + (childSpace - childHeight) / 2;
1669 // }
1670 // } else {
1671 childTop = paddingTop + ((childSpace - childHeight) / 2)
1672 + lp.topMargin - lp.bottomMargin;
1673 break;
1674
1675 case Gravity.BOTTOM:
1676 childTop = childBottom - childHeight - lp.bottomMargin;
1677 if (childBaseline != -1) {
1678 int descent = child.getMeasuredHeight() - childBaseline;
1679 childTop -= (maxDescent[INDEX_BOTTOM] - descent);
1680 }
1681 break;
1682 default:
1683 childTop = paddingTop;
1684 break;
1685 }
1686
1687 if (hasDividerBeforeChildAt(childIndex)) {
1688 childLeft += mDividerWidth;
1689 }
1690
1691 childLeft += lp.leftMargin;
1692 setChildFrame(child, childLeft + getLocationOffset(child), childTop,
1693 childWidth, childHeight);
1694 childLeft += childWidth + lp.rightMargin +
1695 getNextLocationOffset(child);
1696
1697 i += getChildrenSkipCount(child, childIndex);
1698 }
1699 }
1700 }
1701
1702 private void More ...setChildFrame(View child, int left, int top, int width, int height) {
1703 child.layout(left, top, left + width, top + height);
1704 }
注意到上面代码中末尾的setChildFrame方法,可以看出,layout是一个自上而下的过程,先设置父视图位置,在循环子视图,父视图位置一定程度上决定了子视图位置。
draw
所有视图最终都是调用View的draw方法进行绘制。 在自定义视图中, 也不应该复写该方法, 而是复写onDraw()方法进行绘制, 如果自定义的视图确实要复写该方法,先调用super.draw()完成系统的绘制,再进行自定义的绘制。
onDraw()方法默认是空实现,自定义绘制过程需要复写方法,绘制自身的内容。
dispatchDraw()发起对子视图的绘制,在View中默认为空实现,ViewGroup复写了dispatchDraw()来对其子视图进行绘制。自定义的ViewGroup不应该对dispatchDraw()进行复写。
drow方法有六个步骤:
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
draw方法:
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
final Drawable background = mBackground;
if (background != null) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if (mBackgroundSizeChanged) {
background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
}
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
// we're done...
return;
}
/*
* Here we do the full fledged routine...
* (this is an uncommon case where speed matters less,
* this is why we repeat some of the tests that have been
* done above)
*/
boolean drawTop = false;
boolean drawBottom = false;
boolean drawLeft = false;
boolean drawRight = false;
float topFadeStrength = 0.0f;
float bottomFadeStrength = 0.0f;
float leftFadeStrength = 0.0f;
float rightFadeStrength = 0.0f;
// Step 2, save the canvas' layers
int paddingLeft = mPaddingLeft;
final boolean offsetRequired = isPaddingOffsetRequired();
if (offsetRequired) {
paddingLeft += getLeftPaddingOffset();
}
int left = mScrollX + paddingLeft;
int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
int top = mScrollY + getFadeTop(offsetRequired);
int bottom = top + getFadeHeight(offsetRequired);
if (offsetRequired) {
right += getRightPaddingOffset();
bottom += getBottomPaddingOffset();
}
final ScrollabilityCache scrollabilityCache = mScrollCache;
final float fadeHeight = scrollabilityCache.fadingEdgeLength;
int length = (int) fadeHeight;
// clip the fade length if top and bottom fades overlap
// overlapping fades produce odd-looking artifacts
if (verticalEdges && (top + length > bottom - length)) {
length = (bottom - top) / 2;
}
// also clip horizontal fades if necessary
if (horizontalEdges && (left + length > right - length)) {
length = (right - left) / 2;
}
if (verticalEdges) {
topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));
drawTop = topFadeStrength * fadeHeight > 1.0f;
bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));
drawBottom = bottomFadeStrength * fadeHeight > 1.0f;
}
if (horizontalEdges) {
leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));
drawLeft = leftFadeStrength * fadeHeight > 1.0f;
rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));
drawRight = rightFadeStrength * fadeHeight > 1.0f;
}
saveCount = canvas.getSaveCount();
int solidColor = getSolidColor();
if (solidColor == 0) {
final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;
if (drawTop) {
canvas.saveLayer(left, top, right, top + length, null, flags);
}
if (drawBottom) {
canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
}
if (drawLeft) {
canvas.saveLayer(left, top, left + length, bottom, null, flags);
}
if (drawRight) {
canvas.saveLayer(right - length, top, right, bottom, null, flags);
}
} else {
scrollabilityCache.setFadeColor(solidColor);
}
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
final Paint p = scrollabilityCache.paint;
final Matrix matrix = scrollabilityCache.matrix;
final Shader fade = scrollabilityCache.shader;
if (drawTop) {
matrix.setScale(1, fadeHeight * topFadeStrength);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
canvas.drawRect(left, top, right, top + length, p);
}
if (drawBottom) {
matrix.setScale(1, fadeHeight * bottomFadeStrength);
matrix.postRotate(180);
matrix.postTranslate(left, bottom);
fade.setLocalMatrix(matrix);
canvas.drawRect(left, bottom - length, right, bottom, p);
}
if (drawLeft) {
matrix.setScale(1, fadeHeight * leftFadeStrength);
matrix.postRotate(-90);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
canvas.drawRect(left, top, left + length, bottom, p);
}
if (drawRight) {
matrix.setScale(1, fadeHeight * rightFadeStrength);
matrix.postRotate(90);
matrix.postTranslate(right, top);
fade.setLocalMatrix(matrix);
canvas.drawRect(right - length, top, right, bottom, p);
}
canvas.restoreToCount(saveCount);
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
}
可以看到,第一步是从第20行代码开始的,这一步的作用是对视图的背景进行绘制。这里会先得到一个mBGDrawable对象,然后根据layout过程确定的视图位置来设置背景的绘制区域,之后再调用Drawable的draw()方法来完成背景的绘制工作。那么这个mBGDrawable对象是从哪里来的呢?其实就是在XML中通过android:background属性设置的图片或颜色。当然你也可以在代码中通过setBackgroundColor()、setBackgroundResource()等方法进行赋值。
接下来的第三步是在第150行执行的,这一步的作用是对视图的内容进行绘制。可以看到,这里去调用了一下onDraw()方法,那么onDraw()方法里又写了什么代码呢?进去一看你会发现,原来又是个空方法啊。其实也可以理解,因为每个视图的内容部分肯定都是各不相同的,这部分的功能交给子类来去实现也是理所当然的。
第三步完成之后紧接着会执行第四步,这一步的作用是对当前视图的所有子视图进行绘制。但如果当前的视图没有子视图,那么也就不需要进行绘制了。因此你会发现View中的dispatchDraw()方法又是一个空方法,而ViewGroup的dispatchDraw()方法中就会有具体的绘制代码。
以上都执行完后就会进入到第六步,也是最后一步,这一步的作用是对视图的滚动条进行绘制。那么你可能会奇怪,当前的视图又不一定是ListView或者ScrollView,为什么要绘制滚动条呢?其实不管是Button也好,TextView也好,任何一个视图都是有滚动条的,只是一般情况下我们都没有让它显示出来而已。绘制滚动条的代码逻辑也比较复杂,这里就不再贴出来了,因为我们的重点是第三步过程。
通过以上流程分析,相信大家已经知道,View是不会帮我们绘制内容部分的,因此需要每个视图根据想要展示的内容来自行绘制。如果你去观察TextView、ImageView等类的源码,你会发现它们都有重写onDraw()这个方法,并且在里面执行了相当不少的绘制逻辑。绘制的方式主要是借助Canvas这个类,它会作为参数传入到onDraw()方法中,供给每个视图使用。Canvas这个类的用法非常丰富,基本可以把它当成一块画布。