自定义View

在实际开发中,我们仅仅了解常用的原生控件的使用方法是往往不够的,因为它是无法作出很复杂的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_MOSTUNSPECIFIED
dp/pxEXACTLY/childSizeEXACTLY/childSizeEXACTLY/childSize
MATCH_PARENTEXACTLY/childSizeAT_MOST/parentSizeUNSPECIFIED/0
WRAP_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


标题基于SpringBoot+Vue的学生交流互助平台研究AI更换标题第1章引言介绍学生交流互助平台的研究背景、意义、现状、方法与创新点。1.1研究背景与意义分析学生交流互助平台在当前教育环境下的需求及其重要性。1.2国内外研究现状综述国内外在学生交流互助平台方面的研究进展与实践应用。1.3研究方法与创新点概述本研究采用的方法论、技术路线及预期的创新成果。第2章相关理论阐述SpringBoot与Vue框架的理论基础及在学生交流互助平台中的应用。2.1SpringBoot框架概述介绍SpringBoot框架的核心思想、特点及优势。2.2Vue框架概述阐述Vue框架的基本原理、组件化开发思想及与前端的交互机制。2.3SpringBoot与Vue的整合应用探讨SpringBoot与Vue在学生交流互助平台中的整合方式及优势。第3章平台需求分析深入分析学生交流互助平台的功能需求、非功能需求及用户体验要求。3.1功能需求分析详细阐述平台的各项功能需求,如用户管理、信息交流、互助学习等。3.2非功能需求分析对平台的性能、安全性、可扩展性等非功能需求进行分析。3.3用户体验要求从用户角度出发,提出平台在易用性、美观性等方面的要求。第4章平台设计与实现具体描述学生交流互助平台的架构设计、功能实现及前后端交互细节。4.1平台架构设计给出平台的整体架构设计,包括前后端分离、微服务架构等思想的应用。4.2功能模块实现详细阐述各个功能模块的实现过程,如用户登录注册、信息发布与查看、在线交流等。4.3前后端交互细节介绍前后端数据交互的方式、接口设计及数据传输过程中的安全问题。第5章平台测试与优化对平台进行全面的测试,发现并解决潜在问题,同时进行优化以提高性能。5.1测试环境与方案介绍测试环境的搭建及所采用的测试方案,包括单元测试、集成测试等。5.2测试结果分析对测试结果进行详细分析,找出问题的根源并
内容概要:本文详细介绍了一个基于灰狼优化算法(GWO)优化的卷积双向长短期记忆神经网络(CNN-BiLSTM)融合注意力机制的多变量多步时间序列预测项目。该项目旨在解决传统时序预测方法难以捕捉非线性、复杂时序依赖关系的问题,通过融合CNN的空间特征提取、BiLSTM的时序建模能力及注意力机制的动态权重调节能力,实现对多变量多步时间序列的精准预测。项目不仅涵盖了数据预处理、模型构建与训练、性能评估,还包括了GUI界面的设计与实现。此外,文章还讨论了模型的部署、应用领域及其未来改进方向。 适合人群:具备一定编程基础,特别是对深度学习、时间序列预测及优化算法有一定了解的研发人员和数据科学家。 使用场景及目标:①用于智能电网负荷预测、金融市场多资产价格预测、环境气象多参数预报、智能制造设备状态监测与预测维护、交通流量预测与智慧交通管理、医疗健康多指标预测等领域;②提升多变量多步时间序列预测精度,优化资源调度和风险管控;③实现自动化超参数优化,降低人工调参成本,提高模型训练效率;④增强模型对复杂时序数据特征的学习能力,促进智能决策支持应用。 阅读建议:此资源不仅提供了详细的代码实现和模型架构解析,还深入探讨了模型优化和实际应用中的挑战与解决方案。因此,在学习过程中,建议结合理论与实践,逐步理解各个模块的功能和实现细节,并尝试在自己的项目中应用这些技术和方法。同时,注意数据预处理的重要性,合理设置模型参数与网络结构,控制多步预测误差传播,防范过拟合,规划计算资源与训练时间,关注模型的可解释性和透明度,以及持续更新与迭代模型,以适应数据分布的变化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值