自定义View

来源:互联网 发布:毛孔收缩精华液知乎 编辑:程序博客网 时间:2024/06/08 06:12

自定义View解析

我们很经常是会使用View,但是系统系统的View毕竟是有限的,可能无法满足我们的需求,所以自定义View就显得十分有必要了。那么如何实现一个自定义View呢?想想,我们在画图干什么?要绘制的图有多大?画的图画在哪里?然后画成怎么样?在Android自定义View中的步骤也是这样。凭借着onMeasure(),onLayout(),onDraw()这三种方法进行绘画。
先看看view的结构,可以发现它是继承自ViewGroup的
这里写图片描述
再来看看View的绘制流程
这里写图片描述
西面我们一步一步来分析

1. onMeasure()

measure就是测量的意思,该方法主要就是用来测量视图的大小。View绘制流程会从ViewRoot的performTraversals()方法开始(ViewRoot不是一个View,而是一个Handler)。然后在其内部调用View的Measure()方法。而Measure()方法接受两个参数,分别是widthMeasureSpec和heighMeasureSpec,这两个参数用来确定视图的宽度和高度的规格大小。

在测量中,我们还需要理解MeasureSpec,它是View的一个内部类。
MeasureSpec的值由记录大小的specSize和记录规格的specMode共同组成的,
specMode一共有三种类型,如下所示:
1. EXACTLY
表示父视图希望子视图的大小应该是由specSize的值来决定的。
2. AT_MOST
父视图指定一个可用大小的SpecSize,View大小不能大于这个值
3. UNSPECIFIED
父视图对View没有任何限制。这种情况一般用于系统内部。

而widthMeasureSpec和heighMeasureSpec的值是父视图通过计算然后传递给子视图的。说明父视图一定程度上决定子视图的大小。而根视图的specSize都等于windowSize,意味着会根视图会充满全屏。

介绍完了MeasureSpec相关内容,接下来来看看View的measure方法

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {    if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT ||            widthMeasureSpec != mOldWidthMeasureSpec ||            heightMeasureSpec != mOldHeightMeasureSpec) {        mPrivateFlags &= ~MEASURED_DIMENSION_SET;        if (ViewDebug.TRACE_HIERARCHY) {            ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_MEASURE);        }        // 这里调用了onMeasure方法        onMeasure(widthMeasureSpec, heightMeasureSpec);        if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) {            throw new IllegalStateException("onMeasure() did not set the"                    + " measured dimension by calling"                    + " setMeasuredDimension()");        }        mPrivateFlags |= LAYOUT_REQUIRED;    }    mOldWidthMeasureSpec = widthMeasureSpec;    mOldHeightMeasureSpec = heightMeasureSpec;}

可以发现measure方法是final的,我们没有办法改写这一个方法,但是我们发现在该方法中调用了onMeasure方法,这里才是设施View大小的地方
那么下面来看看View的onMeasure方法,

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

代码很简单,通过getDefaultSize方法来或许大小。接下来看看该方法

public static int getDefaultSize(int size, int measureSpec) {        int result = size;        int specMode = MeasureSpec.getMode(measureSpec);        int specSize = MeasureSpec.getSize(measureSpec);        switch (specMode) {        case MeasureSpec.UNSPECIFIED:            result = size;            break;        case MeasureSpec.AT_MOST:        case MeasureSpec.EXACTLY:            result = specSize;            break;        }        return result;    }

可以看出,View的高和宽都是又measureSpec决定的。这里可以发现,只要重写onMeasure方法就能确定View的大小。

当然,一个界面的展示会设计多次measure,因为一个布局中会包含多个子视图,每个视图都要经历一次measure过程。所以ViewGroup定义了一个measureChildren()的方法,来测量子视图的大小

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);        }    }}

从代码中可以看到,首先是获取所有子视图,然后在调用measureChild的方法测量

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);}

可以看到这里通过getChildMeasureSpec的方法计算大小,然后在最后调用measure方法,之后就是前面介绍的一样了。那么我们绘制View的第一步就解决了

2.onLayout()

measure过程后,视图的大小就已经测量好了。接下来就是确定视图的位置了。onLayout方法的作用就是在于这里。
先来看看View的layout方法

public void layout(int l, int t, int r, int b) {      int oldL = mLeft;      int oldT = mTop;      int oldB = mBottom;      int oldR = mRight;      //    boolean changed = setFrame(l, t, r, b);      if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {          if (ViewDebug.TRACE_HIERARCHY) {              ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);          }          onLayout(changed, l, t, r, b);          mPrivateFlags &= ~LAYOUT_REQUIRED;          if (mOnLayoutChangeListeners != null) {              ArrayList<OnLayoutChangeListener> listenersCopy =                      (ArrayList<OnLayoutChangeListener>) 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 &= ~FORCE_LAYOUT;  }  

这里重点是关注changed这个变量。setFrame方法是用来判断视图大小是否发生变化,用来确定当前视图是否要进行重绘。之后会调用onLayout这个方法。
这里的onLayout方法是一个空方法,因为它的实现和具体布局有关。那么我们通过FrameLayout的onLayout方法来看看是怎么实现的。

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {        layoutChildren(left, top, right, bottom, false /* no force left gravity */);    }    void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {        final int count = getChildCount();        // 获取父内边距的padding值        final int parentLeft = getPaddingLeftWithForeground();        final int parentRight = right - left - getPaddingRightWithForeground();        final int parentTop = getPaddingTopWithForeground();        final int parentBottom = bottom - top - getPaddingBottomWithForeground();        // 遍历子View,确定子View的属性。        for (int i = 0; i < count; i++) {            final View child = getChildAt(i);            if (child.getVisibility() != GONE) {                final LayoutParams lp = (LayoutParams) child.getLayoutParams();                final int width = child.getMeasuredWidth();                final int height = child.getMeasuredHeight();                int childLeft;                int childTop;                int gravity = lp.gravity;                if (gravity == -1) {                    gravity = DEFAULT_CHILD_GRAVITY;                }                final int layoutDirection = getLayoutDirection();                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);                final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {                    case Gravity.CENTER_HORIZONTAL:                        childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +                        lp.leftMargin - lp.rightMargin;                        break;                    case Gravity.RIGHT:                        if (!forceLeftGravity) {                            childLeft = parentRight - width - lp.rightMargin;                            break;                        }                    case Gravity.LEFT:                    default:                        childLeft = parentLeft + lp.leftMargin;                }                switch (verticalGravity) {                    case Gravity.TOP:                        childTop = parentTop + lp.topMargin;                        break;                    case Gravity.CENTER_VERTICAL:                        childTop = parentTop + (parentBottom - parentTop - height) / 2 +                        lp.topMargin - lp.bottomMargin;                        break;                    case Gravity.BOTTOM:                        childTop = parentBottom - height - lp.bottomMargin;                        break;                    default:                        childTop = parentTop + lp.topMargin;                }                对View进行布局                child.layout(childLeft, childTop, childLeft + width, childTop + height);            }        }    }

步骤可以分为
1、获取父View的内边距padding的值
2、遍历子View,处理子View的layout_gravity属性、根据View测量后的宽和高、父View的padding值、来确定子View的布局参数,
3、调用child.layout方法,对子View进行布局

可以发现,View通过layout方法确认自己的在父视图中的位置,ViewGroup通过onLayout方法确定View在视图的位置。

在onLayout()过程结束后,我们就可以通过getWidth()和getHeight()方法来获取视图的宽高了。上面onLayout()中使用了getMeasureWidth()方法。那么他们有什么区别呢?
首先getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才能获取到。另外,getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的,而getWidth()方法中的值则是通过视图右边的坐标减去左边的坐标计算出来的。

绘制View的第二部也完成了。

3.onDraw()

measure和layout结束后,接下来就进入视图的绘制过程了。顾名思义Draw就是绘画的意思
那么我们来看看onDraw方法

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;        // 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;        }         ... ...    }

可以看到,绘制过程一共分为6步。
1. 绘制背景,这里就是通过在XML设置background属性获取的颜色或者图片,或者是通过setBackgroundResource设置的颜色或者图片
3. 绘制内容,这里动用了onDraw方法,这是个空方法,每个视图的内容都不同,所以这功能交给子类完成
4. 绘制子视图,这里的dispatchDraw也是个空方法,不过在ViewGroup中就有该方法的绘制代码
6. 绘制滚动条等,任何一个视图都是有滚动条的,只是一般情况下我们都没有让它显示出来而已

那么只用重写onDraw方法,自定义view就完成了。

原创粉丝点击