自定义View

    xiaoxiao2021-03-25  95

    在实际开发中,我们仅仅了解常用的原生控件的使用方法是往往不够的,因为它是无法作出很复杂的View,这时就需要自定义View。一般自定义View的步骤就有那么几步:

    1、View的测量过程(onMeasure)

    2、布局过程(onLayout)

    3、绘制过程(onDraw)

    了解View和ViewGroup

    View:代表用户界面组件基本构建块,view在屏幕中占据了一个矩形区域,而且负责相关绘制和事件处理,如常用的组件(buttom、TextView...)其父类都是View

    ViewGroup:它也是View的子类,ViewGroup是所有布局父类,它可以包含其它view或者viewGroup

    下面看一下相关时序图

    说明:顶层视图代表应用程序窗口的视图对象(DecorView类型的对象),ViewRoot对应的是ViewRootImpl类

    说明:下面用()加数字表示对应上面第几步,如(1)则表示第1步

    (9)获取顶层视图decor【DecorView类型的对象】

    经过(11)(12)(13)这几步将decor传递给ViewRoot

    经过上面步骤就实现了将ViewRoot和DecorView建立了关联;在(13)中,ViewRoot类的成员函数setView会调用ViewRoot类的另外一个成员函数requestLayout来请求 对顶层视图decor 作第一次布局以及显示。

    顶层视图decor的三大流程

    我们就从ViewRoot类的成员函数requestLayout开始,分析顶层视图decor的三大流程,如下图所示:

    (5)调用ViewRootImpl类的performTraversals方法会依次调用performMeasure→performLayout→performDraw方法来完成顶层视图decor的测量(measure)、布局(layout)和绘制过程(draw)。

    View的测量过程 (measure)

    上图(9)则遍历每一个子View,被调用子View的measure方法(10)继续开始进行子View的测量过程(measure)

    View类型的ViewGroup和非ViewGroup类型的View的测量过程是不同的?

             非ViewGroup类型的View通过onMeasure方法就完成了其测量过程

            ViewGroup类型的View除了通过onMeasure方法就完成自身的测量过程外,还要在onMeasure方法中完成遍历子View的measure方法,各个子View再去递归执行这个流程。

    非ViewGroup类型的View的测量过程如图:

    (1)执行View中的measure方法,该方法是一个final方法,这就意味着子类不能从写该方法,measure方法会调用View类的onMeasure方法,onMeasure方法:

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }

    从上面的代码就对应上图中3、4、5、6、7步,先来看第3步对应的View类的getSuggestedMinimumWidth方法的源码:

    protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); }从getSuggestedMinimumWidth的代码可以看出,当View没有设置背景,那么getSuggestedMinimumWidth方法的返回值为mMinWidth,而mMinWidth对应于android:minWidth属性指定的值,即getSuggestedMinimumWidth方法的返回值为android: minWidth属性指定的值,如果没有设置android: minWidth属性,则mMinWidth默认为0;如果View设置了背景,则getSuggestedMinimumWidth方法的返回值为max(mMinWidth, mBackground.getMinimumWidth()),下面先来看看Drawable类中getMinimumWidth

    法的源码:

    public int getMinimumWidth() { final int intrinsicWidth = getIntrinsicWidth(); return intrinsicWidth > 0 ? intrinsicWidth : 0; }有上面的代码可知getMinimumWidth返回的是View的背景的原始宽度,如果View的背景没有原始宽度,就返回0。

    现在来总结一下getSuggestedMinimumWidth方法的逻辑,当View没有设置背景时,getSuggestedMinimumWidth方法的返回值为android: minWidth属性指定的值,这个值可以为0;当View设置了背景时,getSuggestedMinimumWidth方法的返回值为android: minWidth属性指定的值与View的背景的最小宽度中的最大值。

    现在我们来看一下最关键的View类的getDefaultSize方法的源代码(对应第4步):

    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.AT_MOST和MeasureSpec.EXACTLY测量模式,getDefaultSize直接返回测量后的值(即父View通过measure方法传递过来的测量值);对于MeasureSpec.UNSPECIFIED测量模式,一般用于系统内部的测量过程,getDefaultSize返回值为getSuggestedMinimumWidth方法的返回值。

    第7步中View类的setMeasuredDimension方法调用了第8步中View类的setMeasuredDimensionRaw方法,setMeasuredDimensionRaw方法的源码:

    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) { mMeasuredWidth = measuredWidth; mMeasuredHeight = measuredHeight; mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET; }

    有上面的代码可知,View测量后的宽高被保存到View类的成员变量mMeasuredWidth和mMeasuredHeight中了,通过View类的getMeasuredWidth方法和getMeasuredHeight方法获取的就是mMeasuredWidth和mMeasuredHeight的值,需要注意的是,在某些极端情况下,系统可能需要多次measure才能确定最终的测量宽高,在这种情况下,在onMeasure方法中拿到的测量宽高很可能是不准确的,一个好的习惯是在onLayout方法中去获取View最终的测量宽高。上面只是说在自定义View中什么时机获取最终的测量宽高,那在Activity中什么时机获取View的测量宽高呢?有如下四种方法

    1 在Activity/View#onWindowFocusChanged方法中获取 2 在Activity中的onStart方法中执行View.post获取 3 通过ViewTreeObserver获取 4 通过手动执行View.measure获取有如下几点需要注意:

    1>直接继承View的自定义控件需要重写onMeasure方法并且设置wrap_content时的自身大小,否者在布局中使用wrap_content就相当于使用math_parent,具体原因会在下一节进行说明。

    2> 在自定义View时可以通过重写onMeasure方法设置View测量大小,这样的话你就抛弃了父容器通过measure方法传进来建议测量值MeasureSpec。

    ViewGroup类型的View的测量过程

    先通过如下的时序图,整体的看一下测量过程:

    ViewGroup并没有定义其自身测量的具体过程(即没有onMeasure方法),这是因为ViewGroup是一个抽象类,其测量过程的onMeasure方法需要各个子类去具体实现,所以上面展示了LinearLayout测量过程图

    对于上面的步骤进行解析一下,第1步执行View类中的measure方法,该方法是一个final方法,这就意味着子类不能从写该方法,measure方法会调用LinearLayout类的onMeasure方法,onMeasure方法的实现代码如下所示:

    @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mOrientation == VERTICAL) { measureVertical(widthMeasureSpec, heightMeasureSpec); } else { measureHorizontal(widthMeasureSpec, heightMeasureSpec); } }我们现在只分析当LinearLayout的方向是垂直方向的情况,此时会执行LinearLayout类的measureVertical方法,代码如下(由于measureVertical方法的代码比较长,下面只展示我们关心的逻辑代码):

    // See how tall everyone is. Also remember max width. for (int i = 0; i < count; ++i) { final View child = getVirtualChildAt(i); // Determine how big this child would like to be. If this or // previous children have given a weight, then we allow it to // use all available space (and we will shrink things later // if needed). ...... measureChildBeforeLayout( child, i, widthMeasureSpec, 0, heightMeasureSpec, totalWeight == 0 ? mTotalLength : 0); if (oldHeight != Integer.MIN_VALUE) { lp.height = oldHeight; } final int childHeight = child.getMeasuredHeight(); final int totalLength = mTotalLength; mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin + lp.bottomMargin + getNextLocationOffset(child)); ...... } ...... // Add in our padding mTotalLength += mPaddingTop + mPaddingBottom; int heightSize = mTotalLength; // Check against our minimum height heightSize = Math.max(heightSize, getSuggestedMinimumHeight()); // Reconcile our calculated size with the heightMeasureSpec int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0); heightSize = heightSizeAndState & MEASURED_SIZE_MASK; ...... setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), heightSizeAndState); .....由上半部分的代码可知LinearLayout类的measureVertical方法会遍历每一个子元素并且执行LinearLayout类的measureChildBeforeLayout方法对子元素进行测量,LinearLayout类的 measureChildBeforeLayout方法内部会执行 子元素的measure方法。在代码中,变量mTotalLength会是用来存放LinearLayout在竖直方向上的当前高度,每遍历一个子元素,mTotalLength就会增加,增加的部分主要包括子元素自身的高度、子元素在竖直方向上的margin。当测量完所有子元素时,LinearLayout会根据子元素的情况测量自身的大小,针对竖直的LinearLayout而言,它在水平方向的测量过程遵循View的测量过程,在竖直方向上的测量过程和View有所不同,具体来说是指,如果它的布局中高度采用的是math_content或者具体数值,那么它的测量过程与View一致,如果它的布局中高度采用的是wrap_content,那么它的高度是所有子元素所占用的高度总和,但是仍然不能超过父容器的剩余空间,这个过程对应与resolveSizeAndState的源码: public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) { final int specMode = MeasureSpec.getMode(measureSpec); final int specSize = MeasureSpec.getSize(measureSpec); final int result; switch (specMode) { case MeasureSpec.AT_MOST: if (specSize < size) { result = specSize | MEASURED_STATE_TOO_SMALL; } else { result = size; } break; case MeasureSpec.EXACTLY: result = specSize; break; case MeasureSpec.UNSPECIFIED: default: result = size; } return result | (childMeasuredState & MEASURED_STATE_MASK); }下面我们来看一看LinearLayout类的measureChildBeforeLayout方法是如何对子元素进行测量,该方法的第4个和第6个参数分别代表在水平方向和垂直方向上LinearLayout已经被其他子元素占据的长度,measureChildBeforeLayout的源码如下: void measureChildBeforeLayout(View child, int childIndex, int widthMeasureSpec, int totalWidth, int heightMeasureSpec, int totalHeight) { measureChildWithMargins(child, widthMeasureSpec, totalWidth, heightMeasureSpec, totalHeight); }

    LinearLayout类的measureChildBeforeLayout方法会调用ViewGroup类的measureChildWithMargins方法,measureChildWithMargins方法的源码如下:

    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }ViewGroup类的measureChildWithMargins方法会调用子元素的measure方法对子元素进行测量, 在对子元素测量之前先会 通过调用ViewGroup类的getChildMeasureSpec方法得到子元素宽高的MeasureSpec,从传给ViewGroup类的getChildMeasureSpec方法的第二个参数可知,子元素MeasureSpec的创建与父容器的MeasureSpec、父容器的padding、子元素的margin和兄弟元素占用的长度有关。ViewGroup类的getChildMeasureSpec方法代码如下所示: 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; } //noinspection ResourceType return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }

    ViewGroup三种测量模式

    ViewGroup类的getChildMeasureSpec方法的逻辑可以通过下表来说明,注意,表中的parentSize是指父容器目前可使用的大小

    childLayoutParams/parentSpecModeEXACTLYAT_MOSTUNSPECIFIEDdp/pxEXACTLY/childSizeEXACTLY/childSizeEXACTLY/childSizeMATCH_PARENTEXACTLY/childSizeAT_MOST/parentSizeUNSPECIFIED/0WRAP_CONTENTAT_MOST/parentSizeAT_MOST/parentSizeUNSPECIFIED/0 EXACTLY:表示设置了精确的值,一般当childView设置其宽、高为精确值、match_parent时,ViewGroup会将其设置为EXACTLY;

    AT_MOST:表示子布局被限制在一个最大值内,一般当childView设置其宽、高为wrap_content时,ViewGroup会将其设置为AT_MOST;

    UNSPECIFIED:表示子布局想要多大就多大,一般出现在AadapterView的item的heightMode中、ScrollView的childView的heightMode中;此种模式比较少见

    ViewGroup类的getChildMeasureSpec方法返回子元素宽高的MeasureSpec,然后将子元素宽高的MeasureSpec作为measure方法的参数。

    到此为止,非ViewGroup类型的View的测量过程和ViewGroup类型的View的测量过程已经分析完毕,进行如下总结:

    1> 父View会遍历测量每一个子View(通常使用ViewGroup类的measureChildWithMargins方法),然后调用子View的measure方法并且将测量后的宽高作为measure方法的参数,但是这只是父View的建议值,子View可以通过继承onMeasure来改变测量值。

    2> 非ViewGroup类型的View自身的测量是在非ViewGroup类型的View的onMeasure方法中进行测量的

    3> ViewGroup类型的View自身的测量是在ViewGroup类型的View的onMeasure方法中进行测量的

    4>直接继承ViewGroup的自定义控件需要重写onMeasure方法并且设置wrap_content时的自身大小,否者在布局中使用wrap_content就相当于使用math_parent,具体原因通过上面的表格可以说明。

    View的布局过程(layout)

    decor的三大流程图的第16步会遍历每一个子元素,并且调用子元素的layout方法,继而开始进行子元素的布局过程。layout过程比measure过程简单多了,layout方法用来确定View本身的位置,而onLayout方法用来确定所有子元素的位置。ViewGroup类型的View和非ViewGroup类型的View的布局过程是不同的,非ViewGroup类型的View通过layout方法就完成了其布局过程,而ViewGroup类型的View除了通过layout方法就完成自身的布局过程外,还要调用onLayout方法去遍历子元素并且调用子元素的layout方法,各个子View再去递归执行这个流程。

    非ViewGroup类型的View的布局过程

    先通过如下的时序图,整体的看一下布局过程:

    对上面的时序图进行一下解析,第1步执行View类的layout方法,代码如下:

    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); 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; }如果isLayoutModeOptical()返回true,那么就会执行setOpticalFrame()方法,否则会执行setFrame()方法。并且setOpticalFrame()内部会调用setFrame(),所以无论如何都会执行setFrame()方法;第2步layout方法调用View类的setFrame方法,部分我们感兴趣的源码如下:

    protected boolean setFrame(int left, int top, int right, int bottom) { boolean changed = false; if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) { changed = true; int oldWidth = mRight - mLeft; int oldHeight = mBottom - mTop; int newWidth = right - left; int newHeight = bottom - top; boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight); // Invalidate our old position invalidate(sizeChanged); mLeft = left; mTop = top; mRight = right; mBottom = bottom; if (sizeChanged) { sizeChange(newWidth, newHeight, oldWidth, oldHeight); } } return changed; }由上面的源码可知,setFrame方法是用来设定View的四个顶点的位置,即初始化mLeft、mTop、mRight、mBottom这四个值,View的四个顶点一旦确定,那么View在父容器中的位置也就确定了;第3步layout方法接着调用View类的onLayout方法,这个方法的作用是用来确定子元素的位置,由于非ViewGroup类型的View没有子元素,所以View类的onLayout方法为空。

    ViewGroup类型的View的布局过程

    先通过如下的时序图,整体的看一下布局过程:

    上面其实是LinearLayout的布局时序图,因为ViewGroup的onLayout方法是抽象方法,所以就选择了ViewGroup的子类LinearLayout进行分析。对上面的时序图进行一下解析,第1步执行ViewGroup类的layout方法,该方法是一个final方法,即子类无法重写该方法,源代码如下:第1步执行ViewGroup类的layout方法,该方法是一个final方法,即子类无法重写该方法,源代码如下:

    @Override public final void layout(int l, int t, int r, int b) { if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) { if (mTransition != null) { mTransition.layoutChange(this); } super.layout(l, t, r, b); } else { // record the fact that we noop'd it; request layout when transition finishes mLayoutCalledWhileSuppressed = true; } }第2步ViewGroup类的layout方法会调用View类的layout方法,第3步View类的layout方法调用View类的setFrame方法,这两步与上面讨论 非ViewGroup类型的View的布局过程的第1、2步相同,这里就不在赘叙,直接看第4步View类的layout方法调用LinearLayout类的onLayout方法,源代码如下: @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (mOrientation == VERTICAL) { layoutVertical(l, t, r, b); } else { layoutHorizontal(l, t, r, b); } }我们现在只分析当LinearLayout的方向是垂直方向的情况,此时会执行LinearLayout类的layoutVertical方法,代码如下: void layoutVertical(int left, int top, int right, int bottom) { final int paddingLeft = mPaddingLeft; int childTop; int childLeft; // Where right end of child should go final int width = right - left; int childRight = width - mPaddingRight; // Space available for child int childSpace = width - paddingLeft - mPaddingRight; final int count = getVirtualChildCount(); final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK; switch (majorGravity) { case Gravity.BOTTOM: // mTotalLength contains the padding already childTop = mPaddingTop + bottom - top - mTotalLength; break; // mTotalLength contains the padding already case Gravity.CENTER_VERTICAL: childTop = mPaddingTop + (bottom - top - mTotalLength) / 2; break; case Gravity.TOP: default: childTop = mPaddingTop; break; } for (int i = 0; i < count; i++) { final View child = getVirtualChildAt(i); if (child == null) { childTop += measureNullChild(i); } else if (child.getVisibility() != GONE) { final int childWidth = child.getMeasuredWidth(); final int childHeight = child.getMeasuredHeight(); final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); int gravity = lp.gravity; if (gravity < 0) { gravity = minorGravity; } final int layoutDirection = getLayoutDirection(); final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection); switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { case Gravity.CENTER_HORIZONTAL: childLeft = paddingLeft + ((childSpace - childWidth) / 2) + lp.leftMargin - lp.rightMargin; break; case Gravity.RIGHT: childLeft = childRight - childWidth - lp.rightMargin; break; case Gravity.LEFT: default: childLeft = paddingLeft + lp.leftMargin; break; } if (hasDividerBeforeChildAt(i)) { childTop += mDividerHeight; } childTop += lp.topMargin; setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight); childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child); i += getChildrenSkipCount(child, i); } } }可以看到LinearLayout类的onLayout方法会遍历每一个子元素,然后调用LinearLayout类的setChildFrame方法,setChildFrame方法会调用子元素的layout方法来对子元素进行布局,setChildFrame方法的源码如下: private void setChildFrame(View child, int left, int top, int width, int height) { child.layout(left, top, left + width, top + height); }

    View的绘制过程(draw)

    decor的三大流程图的第23步会遍历每一个子View,并且调用子元素的draw方法,继而开始进行子View的绘制过程。先通过如下的时序图,整体的看一下绘制过程:

    上面其实是LinearLayout的绘制时序图,因为View的onDraw方法是空方法,所以就选择了ViewGroup的子类LinearLayout进行分析。

    LinearLayout的绘制过程遵循如下几步:

    1> 绘制背景

    2> 绘制自己(绘制分割线)

    3> 绘制子View(dispatchDraw)

    4> 绘制前景

    Android中是通过View类的draw方法来实现上面的4步,源码如下所示:

    /** * 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; } ..... }从这个方法的注释可以知道,当自定义View并且需要绘制时,应该重写View类的onDraw方法而不要重写View类的draw方法,如果你需要重写draw方法,必须在重写时调用父类的draw方法。上面的代码很明显的验证了View绘制过程的4步。由于View类无法确定自己是否有子元素,所以View类的dispatchDraw方法是空方法,那么我们就来看看ViewGroup类的dispatchDraw方法的源码(由于该方法的源码太长了,因此我只展示我们感兴趣的部分代码): @Override protected void dispatchDraw(Canvas canvas) { boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode); final int childrenCount = mChildrenCount; final View[] children = mChildren; ...... boolean more = false; final long drawingTime = getDrawingTime(); if (usingRenderNodeProperties) canvas.insertReorderBarrier(); final int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size(); int transientIndex = transientCount != 0 ? 0 : -1; // Only use the preordered list if not HW accelerated, since the HW pipeline will do the // draw reordering internally final ArrayList<View> preorderedList = usingRenderNodeProperties ? null : buildOrderedChildList(); final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); for (int i = 0; i < childrenCount; i++) { while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) { final View transientChild = mTransientViews.get(transientIndex); if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE || transientChild.getAnimation() != null) { more |= drawChild(canvas, transientChild, drawingTime); } transientIndex++; if (transientIndex >= transientCount) { transientIndex = -1; } } int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i; final View child = (preorderedList == null) ? children[childIndex] : preorderedList.get(childIndex); if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { more |= drawChild(canvas, child, drawingTime); } } ...... }ViewGroup类的dispatchDraw方法会遍历每一个子元素,然后调用ViewGroup类的drawChild方法对子元素进行绘制,ViewGroup类的drawChild方法源码如下 protected boolean drawChild(Canvas canvas, View child, long drawingTime) { return child.draw(canvas, this, drawingTime); } 与View生命周期相关的常用的回调方法

    onFocusChanged(boolean, int, android.graphics.Rect):该方法在当前View获得或失去焦点时被回调。

    onWindowFocusChanged(boolean):该方法在包含当前View的window获得或失去焦点时被回调。

    onAttachedToWindow():该方法在当前View被附到一个window上时被回调。

    onDetachedFromWindow():该方法在当前View从一个window上分离时被回调。

    onVisibilityChanged(View, int):该方法在当前View或其祖先的可见性改变时被调用。

    onWindowVisibilityChanged(int):该方法在包含当前View的window可见性改变时被回调。

    自定义View实例

    自定义View的分类标准不唯一,而我把自定义View分为3类

    1> 通过继承View或者ViewGroup实现自定义View

    2> 通过继承已有的控件实现自定义View

    3> 通过组合实现自定义View

    我在下面只针对1>来实现自定义View,因为2>和3>相对于1>就比较简单了。

    通过继承View实现环状进度条

    实现上面效果代码

    根据上面对非ViewGrop类型View三大流程的分析,第一步就是测量,由于是继承View类的,因此如果想要支持wrap_content属性,就必须重写onMeasure方法,如下所示(可以当做模板代码):

    @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(mWidth, mHeight); } else if (widthSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(mWidth, heightSpecSize); } else if (heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(widthSpecSize, mHeight); } else { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } }第二步就是进行布局,由于非ViewGrop类型View自身的布局在View类的layout方法中已经实现,而onLayout方法是用来对子View进行布局的,所以对于非ViewGrop类型View就不用考虑布局的实现。

    第三步就是进行绘制,由于非ViewGrop类型View没有子View,所以不用考虑对子View的绘制,因此只要重写View类的onDraw方法对自身进行绘制即可,代码如下:

    @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawArc(new RectF(200,200, mWidth + 200, mHeight + 200), 0, currentValue, false, paint); }

    下面是自定义View完整代码:

    public class SimpleView extends View { private int mWidth = 300; private int mHeight = 300; private Paint paint = null; private float currentValue = 0; public SimpleView(Context context, AttributeSet attrs) { super(context, attrs); paint = new Paint(Paint.ANTI_ALIAS_FLAG); paint.setColor(Color.BLUE); paint.setStyle(Paint.Style.STROKE);//设置画笔为线条模式 paint.setStrokeWidth(10); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawArc(new RectF(200,200, mWidth + 200, mHeight + 200), 0, currentValue, false, paint); } public void startAnim(){ ValueAnimator valueAnimator = ValueAnimator.ofFloat(0,180); valueAnimator.setDuration(3000); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { currentValue = (float) animation.getAnimatedValue(); postInvalidate(); } }); valueAnimator.start(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){ setMeasuredDimension(mWidth,mHeight); }else if(widthSpecMode == MeasureSpec.AT_MOST){ setMeasuredDimension(mWidth,heightSpecSize); }else if (heightSpecMode == MeasureSpec.AT_MOST){ setMeasuredDimension(widthSpecSize,mHeight); }else{ super.onMeasure(widthMeasureSpec,heightMeasureSpec); } } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); } }xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <com.hh.person.customview.widget.SimpleView android:id="@+id/custom_view" android:layout_width="500dp" android:layout_height="500dp" android:background="#f0f" /> </LinearLayout>MAinActivity public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); SimpleView simpleView = (SimpleView) findViewById(R.id.custom_view); simpleView.startAnim(); } }

    通过继承ViewGroup实现流式布局(FlowLayout)

    效果图

    根据自定义三部曲来走

    第一步先进行测量

    //测量ViewGroup大小 @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); int maxWidth = 0; //记录最大的宽 int sumHeight = 0; //记录子View在垂直方向累加的高 // 记录每一行的宽度 int lineWidth = 0; //记录一行中最高子View的高度 int lineHeight = 0; // 获取子View总数 int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { View childView = getChildAt(i); // 测量子View的宽和高 measureChild(childView, widthMeasureSpec, heightMeasureSpec); // 得到子View的外边距 MarginLayoutParams lp = (MarginLayoutParams)childView.getLayoutParams(); // 子View占据的宽度 int childWidth = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; // 子View占据的高度 int childHeight = childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; //换行情况 if (lineWidth + childWidth > widthSpecSize - getPaddingLeft() - getPaddingRight()) { // 对比得到最大的宽度 maxWidth = Math.max(maxWidth, lineWidth); sumHeight += lineHeight; // 累加高度 // 重置lineWidth和lineHeight值 lineWidth = childWidth; lineHeight = childHeight; }else{ //同行显示 // 叠加行宽 lineWidth += childWidth; // 得到当前行最大的高度 lineHeight = Math.max(lineHeight, childHeight); } // 最后一个控件 if (i == childCount - 1) { maxWidth = Math.max(lineWidth, maxWidth); sumHeight += lineHeight; } } //宽高测量都是最大值模式(wrap_content) if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(maxWidth + getPaddingLeft() + getPaddingRight(), sumHeight + getPaddingTop() + getPaddingBottom()); } else if (widthSpecMode == MeasureSpec.AT_MOST) { //宽测量模式 setMeasuredDimension(maxWidth + getPaddingLeft() + getPaddingRight(), heightSpecSize); } else if (heightSpecMode == MeasureSpec.AT_MOST) { //高测量模式 setMeasuredDimension(widthSpecSize, sumHeight + getPaddingTop() + getPaddingBottom()); } else { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } } 第二部子View布局 @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { //重新对子View布局时,一定要清掉之前的数据,否则数据就有问题 mAllViews.clear(); mLineHeight.clear(); // 当前ViewGroup的宽度 int width = getWidth(); int lineWidth = 0; int lineHeight = 0; List<View> lineViews = new ArrayList<>(); int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { View childView = getChildAt(i); MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams(); int childWidth = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; int childHeight = childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; // 换行 if (childWidth + lineWidth > width - getPaddingLeft() - getPaddingRight()) { // 记录LineHeight mLineHeight.add(lineHeight); // 记录当前行的Views mAllViews.add(lineViews); // 重置lineWidth和lineHeight lineWidth = 0; lineHeight = childHeight; // 重置lineViews集合 lineViews = new ArrayList<>(); } lineWidth += childWidth; lineHeight = Math.max(lineHeight, childHeight); lineViews.add(childView); } // 处理最后一行 mLineHeight.add(lineHeight); mAllViews.add(lineViews); // 设置子View的位置 int left = getPaddingLeft(); int top = getPaddingTop(); // 行数 int lineNum = mAllViews.size(); for (int i = 0; i < lineNum; i++){ // 当前行的所有的View lineViews = mAllViews.get(i); lineHeight = mLineHeight.get(i); //对应lineViews当前行的行高 for (int j = 0; j < lineViews.size(); j++){ View childView = lineViews.get(j); // 判断child的状态 if (childView.getVisibility() == View.GONE){ continue; } MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams(); int lc = left + lp.leftMargin; int tc = top + lp.topMargin; int rc = lc + childView.getMeasuredWidth(); int bc = tc + childView.getMeasuredHeight(); // 为子View进行布局 childView.layout(lc, tc, rc, bc); left += childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; } left = getPaddingLeft() ; top += lineHeight ; } }第三步就是进行绘制,由于设计的流式布局不需要对自己进行绘制,所以不用考虑绘制。

    完整源码

    public class SimpleViewGroup extends ViewGroup { public SimpleViewGroup(Context context, AttributeSet attrs) { super(context, attrs); } //测量ViewGroup大小 @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); int maxWidth = 0; //记录最大的宽 int sumHeight = 0; //记录子View在垂直方向累加的高 // 记录每一行的宽度 int lineWidth = 0; //记录一行中最高子View的高度 int lineHeight = 0; // 获取子View总数 int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { View childView = getChildAt(i); // 测量子View的宽和高 measureChild(childView, widthMeasureSpec, heightMeasureSpec); // 得到子View的外边距 MarginLayoutParams lp = (MarginLayoutParams)childView.getLayoutParams(); // 子View占据的宽度 int childWidth = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; // 子View占据的高度 int childHeight = childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; //换行情况 if (lineWidth + childWidth > widthSpecSize - getPaddingLeft() - getPaddingRight()) { // 对比得到最大的宽度 maxWidth = Math.max(maxWidth, lineWidth); sumHeight += lineHeight; // 累加高度 // 重置lineWidth和lineHeight值 lineWidth = childWidth; lineHeight = childHeight; }else{ //同行显示 // 叠加行宽 lineWidth += childWidth; // 得到当前行最大的高度 lineHeight = Math.max(lineHeight, childHeight); } // 最后一个控件 if (i == childCount - 1) { maxWidth = Math.max(lineWidth, maxWidth); sumHeight += lineHeight; } } //宽高测量都是最大值模式(wrap_content) if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(maxWidth + getPaddingLeft() + getPaddingRight(), sumHeight + getPaddingTop() + getPaddingBottom()); } else if (widthSpecMode == MeasureSpec.AT_MOST) { //宽测量模式 setMeasuredDimension(maxWidth + getPaddingLeft() + getPaddingRight(), heightSpecSize); } else if (heightSpecMode == MeasureSpec.AT_MOST) { //高测量模式 setMeasuredDimension(widthSpecSize, sumHeight + getPaddingTop() + getPaddingBottom()); } else { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } } //存储所有的View private List<List<View>> mAllViews = new ArrayList<>(); //每一行的高度 private List<Integer> mLineHeight = new ArrayList<>(); @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { //重新对子View布局时,一定要清掉之前的数据,否则数据就有问题 mAllViews.clear(); mLineHeight.clear(); // 当前ViewGroup的宽度 int width = getWidth(); int lineWidth = 0; int lineHeight = 0; List<View> lineViews = new ArrayList<>(); int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { View childView = getChildAt(i); MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams(); int childWidth = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; int childHeight = childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; // 换行 if (childWidth + lineWidth > width - getPaddingLeft() - getPaddingRight()) { // 记录LineHeight mLineHeight.add(lineHeight); // 记录当前行的Views mAllViews.add(lineViews); // 重置lineWidth和lineHeight lineWidth = 0; lineHeight = childHeight; // 重置lineViews集合 lineViews = new ArrayList<>(); } lineWidth += childWidth; lineHeight = Math.max(lineHeight, childHeight); lineViews.add(childView); } // 处理最后一行 mLineHeight.add(lineHeight); mAllViews.add(lineViews); // 设置子View的位置 int left = getPaddingLeft(); int top = getPaddingTop(); // 行数 int lineNum = mAllViews.size(); for (int i = 0; i < lineNum; i++){ // 当前行的所有的View lineViews = mAllViews.get(i); lineHeight = mLineHeight.get(i); //对应lineViews当前行的行高 for (int j = 0; j < lineViews.size(); j++){ View childView = lineViews.get(j); // 判断child的状态 if (childView.getVisibility() == View.GONE){ continue; } MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams(); int lc = left + lp.leftMargin; int tc = top + lp.topMargin; int rc = lc + childView.getMeasuredWidth(); int bc = tc + childView.getMeasuredHeight(); // 为子View进行布局 childView.layout(lc, tc, rc, bc); left += childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; } left = getPaddingLeft() ; top += lineHeight ; } } /** * 与当前ViewGroup对应的LayoutParams */ @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(), attrs); } }

    xml

    <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <com.hh.person.customview.widget.SimpleViewGroup android:id="@+id/custom_vg" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#555" android:paddingLeft="12dp" android:paddingTop="20dp" android:paddingRight="12dp" android:paddingBottom="10dp"/> </LinearLayout>

    MainActivity

    public class MainActivity extends Activity { private String[] mVals = new String[] { "夏朝", "商朝", "西周、东周(春秋、战国)","秦朝", "西汉、新朝、东汉", "曹魏、蜀汉、孙吴", "西晋、东晋", "前赵(汉赵)、成汉、前凉、后赵、" + "前燕、前秦、后秦、后燕、西秦、后凉、南凉、南燕、西凉、胡夏、北燕、" + "北凉、冉魏、西燕、西蜀","【南朝】宋、齐、梁、陈 【北朝】东魏、西魏、北齐、北周", "隋朝", "唐朝","后梁、后唐、后晋、后汉、后周、前蜀、后蜀、杨吴、" + "南唐、吴越、闽国、马楚、南汉、南平、北汉","北宋、南宋", "辽国", "大理", "西夏","金","元朝","明朝","清朝" }; private SimpleViewGroup simpleViewGroup; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.activity_main); simpleViewGroup = (SimpleViewGroup) findViewById(R.id.custom_vg); LayoutInflater mInflater = LayoutInflater.from(this); for (int i = 0; i < mVals.length; i++) { TextView tv = (TextView) mInflater.inflate(R.layout.tv,simpleViewGroup, false); tv.setText(mVals[i]); simpleViewGroup.addView(tv); } } }

    参考博文:http://www.qingpingshan.com/rjbc/az/121048.html

    转载请注明原文地址: https://ju.6miu.com/read-13605.html

    最新回复(0)