Android艺术探索第四 view的自定义
一.初见View
-
View的层级关系(Veiw到底分成几层,自定义view是从那一层开始绘制的)
R:Veiw树的结构如下 ,自定义View是从DecorView开始的;DecorView是View树的最顶层View,
-
View树的遍历过程
-
父布局对View绘制模式的影响 (P182表)
4.为什么父view为wrap_parent时,继承的view布局为warp_parent行不通达不到预期效果?
5.如果在activity启动时获取某个view的尺寸,怎么获取?
onWindowsFocusChanges方法时获取
6.getwidth()和getMeasurewidth()有什么区别? -
Touch事件分析 包含2个一个是 dispatchTouchEvent 和OnTouchEvent();
-
如何处理滑动冲突
二. MeasureSpec
1.由来:在View的测量过程中系统会将View的LayoutParams根据父容器所施加的规格转换成对应的MeasureSpec,然后偶再根据这个MeasureSpec来测量Veiw的宽高;
2.MeasureSpec是一个32位的int值,可以通过getMode和getSize来获取SpecMode和SpecSize;
3.SpecSize的有3种类型:
|无限制类型unspecified |精确类型exactly |至多型at_most |
| 无限制 |对应match_parent和精确数值 |对应于wrap_content |
2.1MeasureSpec和Layoutparams对应关系
4.顶层DecorView的MeasureSpec的产生,在ViewRootImpl中
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
由上可以得出 :
Layoutparmas.match_parent | wrap_parent | 精确(50dp) |
---|---|---|
精确模式,大小就是窗口大小 | 最大模式 at_most | 精确模式,大小就是制定大小 |
5.普通View的产生,由于View的measure是由ViewGrop传递而来
/**
* 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); // 一般来说这是父View传进来的
int specSize = MeasureSpec.getSize(spec); // 同上
int size = Math.max(0, specSize - padding); //去掉padding得到内容的大小
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
// childDimension是layout文件里设置的大小:可以是MATCH_PARENT, WRAP_CONTENT或者具体大小, 代码中分别对三种做不同的处理
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
//如果childDimension是具体大小,那么就按照这个大小来, Mode为Exactly,这样子View就知道要按照填写的大小来draw
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
// 子View填的是Match_parent, 那么父View就给子view自己的size(去掉padding), 然后告诉子view这是Exactly的大小
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.
// 子View 填的是wrap_Content,那么父View就告诉子View自己的size(去掉padding), 然后告诉子View, 你最多有这么多, 你自己决定大小
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.
// 如果子View是Match_parent,但是父View是告诉子View, 你最多有size这么多, 那么就告诉子View,你最多有父View的size
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 = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
从上面的方法不难理解,他的代码主要是阐述根据父容器的MeasureSpec同时结合View本身的Layoutparams来确定子元素的MeasureSpec,列表如下
三. View的工作流程
View的工作流程主要是指Measure layout draw三个流程,即测量布局和绘制
3.1 View的测量
1.测量代码主要如下
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
这段代码主要是确定View的测量后的值
/**
* Utility to return a default size. Uses the supplied size if the
* MeasureSpec imposed no constraints. Will get larger if allowed
* by the MeasureSpec.
*
* @param size Default size for this view
* @param measureSpec Constraints imposed by the parent
* @return The size this view should be.
*/
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;
}
由上面的代码可知在at_most和exactly的模式下,返回的view的specsize就是测量后的大小,(测量后的大小!=view的最终大小,最终大小是在layout时确定的),而unspecified,这种情况下,由上面代码可知
result=size,即宽高分别为传入值,那么这个传入值的方法是什么呢,往下看
/**
* Returns the suggested minimum height that the view should use. This
* returns the maximum of the view's minimum height
* and the background's minimum height
* ({@link android.graphics.drawable.Drawable#getMinimumHeight()}).
* <p>
* When being used in {@link #onMeasure(int, int)}, the caller should still
* ensure the returned height is within the requirements of the parent.
*
* @return The suggested minimum height of the view.
*/
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
/**
* Returns the suggested minimum width that the view should use. This
* returns the maximum of the view's minimum width
* and the background's minimum width
* ({@link android.graphics.drawable.Drawable#getMinimumWidth()}).
* <p>
* When being used in {@link #onMeasure(int, int)}, the caller should still
* ensure the returned width is within the requirements of the parent.
*
* @return The suggested minimum width of the view.
*/
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
从上面代码可以看出 如果view没有设置背景,view宽度对应xml里面的Android:minwidth属性所对应的值,如果没有指定即为0;如果有设置背景则取二者的较大值,
综合上面2段代码来看,也就很好的解释了开头的问题4,直接继承属性为wrap_parent的view的自定义控件需要重写onMeasure并设置其自身大小,因为如果父view为wrap,那么他的Specmode就是at_most,其子view如果是wrap属性,那么子veiw大小就是父控件的大小,和子view使用match_parent无异;
2 ViewGroup的测量
其实一开始我以为ViewGroup和View的测量代码是一样的都是调用onMeasure去完成,后来发现虽然逻辑上它的确是遍历所有子元素Measure方法然后子元素递归执行这个过程,但是实际上有点区别..由于
Viewgroup是一个抽象类,里面没有Onmeasure方法,他把测量工作交给了各个子类去实现,原因也是因为不同的布局是有不同的计算方式
/**
* Ask all of the children of this view to measure themselves, taking into
* account both the MeasureSpec requirements for this view and its padding.
* We skip children that are in the GONE state The heavy lifting is done in
* getChildMeasureSpec.
*
* @param widthMeasureSpec The width requirements for this view
* @param heightMeasureSpec The height requirements for this view
*/
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
/**
* Ask one of the children of this view to measure itself, taking into
* account both the MeasureSpec requirements for this view and its padding.
* The heavy lifting is done in getChildMeasureSpec.
*
* @param child The child to measure
* @param parentWidthMeasureSpec The width requirements for this view
* @param parentHeightMeasureSpec The height requirements for this view
*/
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
3.2view的布局过程
1.先普及下坐标概念
View获取自身宽高
getHeight():获取View自身高度
getWidth():获取View自身宽度
View自身坐标
通过如下方法可以获得View到其父控件(ViewGroup)的距离:
getTop():获取View自身顶边到其父布局顶边的距离
getLeft():获取View自身左边到其父布局左边的距离
getRight():获取View自身右边到其父布局左边的距离
getBottom():获取View自身底边到其父布局顶边的距离
MotionEvent提供的方法
我们看上图那个深蓝色的点,假设就是我们触摸的点,我们知道无论是View还是ViewGroup,最终的点击事件都会由onTouchEvent(MotionEvent event)方法来处理,MotionEvent也提供了各种获取焦点坐标的方法:
getX():获取点击事件距离控件左边的距离,即视图坐标
getY():获取点击事件距离控件顶边的距离,即视图坐标
getRawX():获取点击事件距离整个屏幕左边距离,即绝对坐标
getRawY():获取点击事件距离整个屏幕顶边的的距离,即绝对坐标
2.layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup位置被确定后,他会遍历所有的子元素并调用其layout方法,layout确定veiw本身的位子,onLayout确定所有子元素的位子
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}
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;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
在第12行通过setFrame设定View的四个顶点位置然后在调用onlayout方法去确定子Veiw的在Veiw中的位子;
3.getwidth()和getMeasurewidth()有什么区别?
前者是最终的宽高,后者是测量的宽高,后者形成早于前者,如果一般默认情况下二者的确是相等的,但是如果我们自定义时人为更改,就会比测量的多出100
@Override
public void layout(@Px int l, @Px int t, @Px int r, @Px int b) {
super.layout(l, t, r+100, b+100);
}
3.3View的绘制过程
draw过程的作用是将view绘制在屏幕上面
1.draw的步骤遵循以下几步
- 绘制背景 background.draw(canvas)
- 绘制自己 ondraw
- 绘制Children dispatchdraw
- 绘制装饰 ondrawScrollbars
源码里面其实写的也很清楚
/**
* Manually render this view (and all of its children) to the given Canvas.
* The view must have already done a full layout before this function is
* called. When implementing a view, implement
* {@link #onDraw(android.graphics.Canvas)} instead of overriding this method.
* If you do need to override this method, call the superclass version.
*
* @param canvas The Canvas to which the View is rendered.
*/
@CallSuper
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) {
drawBackground(canvas);
}
// 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);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// we're done...
return;
}
四.自定义Veiw的分类
1.继承View重新Ondraw()方法
2.继承viewGroup派生特殊的layout
3.继承特定的某个view
4.继承特定的viewGroup(LinearLayout)