Android之自定义View学习(二)

Stella981
• 阅读 833

Android学习系列

Android之Room学习

Android之自定义View学习(一)

Android之自定义View学习(二)

目录

  • Android学习系列

  • Android之Room学习

  • Android之自定义View学习(一)

  • Android之自定义View学习(二)

  • Android之自定义View学习(二)

  • 前言

  • 2. 自定义View初体验

  • 2.1 View类简介

  • 2.2 自定义View构造函数

  • 2.3 绘制自定义View

  • 2.3.1 测量View

  • 2.3.1.1 MeasureSpec

  • 2.3.1.2 DecorView

  • 2.3.1.3 onMeasure

  • 2.3.2 布局View

  • 2.3.3 绘制View

  • 2.3.4 绘制自定义View总结

  • 其他学习分享系列

  • 数据结构与算法系列

  • 数据结构与算法之哈希表

  • 数据结构与算法之跳跃表

  • 数据结构与算法之字典树

  • 数据结构与算法之2-3树

  • 数据结构与算法之平衡二叉树

  • 数据结构与算法之十大经典排序

  • 数据结构与算法之二分查找三模板

Android之自定义View学习(二)

前言

在上一节当中,博主介绍了布局加载的流程以及布局加载的源码,今天主要介绍一下View的工作原理和源码。

2. 自定义View初体验

2.1 View类简介

  • 直观上:视图上的各种控件包括布局都是一种View
  • 代码上:View类是Android所有组件控件间接或者直接的父类

借用红黑联盟网站上的一张图,下图为View的继承关系图,红色控件为常用控件

Android之自定义View学习(二)

自定义View,顾名思义,就是写自己所需要的控件。下面开始View第一步。

2.2 自定义View构造函数

观看控件的源码,控件都是或直接或间接的继承了View类进行操作。因此第一步继承View类,并继承四种构造函数如下。

public class MyTestView extends View {
    public static String TAG = "View";
    
    //第一类,MyTestView在代码中创建
    public MyTestView(Context context) {
        super(context);
    }
    
    //第二类,MyTestView在.xml的布局文件中创建
    //自定义属性从AttributeSet传入
    public MyTestView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    //第三类,MyTestView有自定义style属性时调用
    public MyTestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    //第四类,MyTestView设置自定义style resource文件时调用
    public MyTestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }
}

2.3 绘制自定义View

绘制自定义View主要用到以下函数

public class MyTestView extends View {
    ......
    @Override
    //用于被内部调用测量视图大小
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        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);
    }

    @Override
    //用于被内部调用绘制视图样式
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

    }
}

2.3.1 测量View

View系统的绘制流程会从ViewRoot(源码位置是ViewRootImpl.java)的performTraversals()方法中开始调用performMeasure(),进而调用View的measure()方法。

然后onMeasure方法在源码中被measure方法调用,用来测量自定义View的大小,并且在measure方法注解中提及,想要自定义View必须要重写onMeasure方法。

同时,onMeasure方法的参数widthMeasureSpecheightMeasureSpec也有着重要意义,这两个参数是从MeasureSpec得到。

2.3.1.1 MeasureSpec

MeasureSpec封装了父类给子类的布局要求,widthMeasureSpecheightMeasureSpec即对宽度、高度的要求。

MeasureSpec实质上是一个32位int值,由测量模式SpecMode和测量模式下的大小值SpecSize组成,高两位是测量模式,有三种模式,低30位是测量模式下的大小值。

UNSPECIFIED

父类不指定尺寸也不限制尺寸,随便继承的子类如何定义尺寸。

EXACTLY

父类指定子类具体尺寸

AT_MOST

父类指定子类的最大尺寸

定义如下:

 public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /** @hide */
        @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
        @Retention(RetentionPolicy.SOURCE)
        public @interface MeasureSpecMode {}

        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        public static final int EXACTLY     = 1 << MODE_SHIFT;

        public static final int AT_MOST     = 2 << MODE_SHIFT;
         ......
 }
2.3.1.2 DecorView

正常情况下,我们直接按照MeasureSpec来进行指定即可。对于一般View,确实好像没有问题,但是仔细思考一下,那么XML中指定属性match_parentwrap_content从哪儿得到的?LinearLayout是个ViewGroupViewGroup继承于View,当Activity启动时,最开始的根视图是谁呢,这个根视图又是如何获得宽高的呢?

这时候就要引入LayoutParamsDecorView的概念。

首先LayoutParams比较简单,就是布局所需要的宽高设置。

ViewGroup.java
    
public static class LayoutParams {
    ......
    @SuppressWarnings({"UnusedDeclaration"})
    @Deprecated
    public static final int FILL_PARENT = -1;

    public static final int MATCH_PARENT = -1;

    public static final int WRAP_CONTENT = -2;
    ......
}

因此对于一般View,它的宽高是由父容器的MeasureSpec 和自身 LayoutParams一起决定的。关于XML中标签指定宽高的问题已经解决了。

那么进入第二个问题,根视图是谁的呢?它是如何获取宽高呢?

最顶层也就是最外层的根视图我们称之为DecorView.

如下图,Activity是一个PhoneWindow实例,其布局形式即为DecorViewDecorView是一个FrameLayout布局,有标题栏(ActionBar)和内容视图(ContentView, 也就是每个活动我们所调用的函数SetContentView)
Android之自定义View学习(二)

那么现在来看,DecorView又是如何获得宽高的呢?

ViewRootImplmeasureHierarchy中有着如下一段代码,就是DecorViewMeasureSpec的赋值。

childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

再来看下getRootMeasureSpec的实现:

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

可以看出,三种模式,且都和windowSize有关,即可以理解为根视图/最外层视图的宽高是由窗口尺寸和自身LayoutParams决定的。

我们可以得出结论:

  • 对于一般视图View,它的宽高是由父容器的MeasureSpec 和自身 LayoutParams一起决定的。
  • 对于根视图DecorView,它的宽高是由窗口尺寸和自身 LayoutParams一起决定的。
2.3.1.3 onMeasure

根据getDefaultSize函数来给定高度或者宽度的大小,然后使用setMeasuredDimension函数来指定自定义View的(尺寸)高度、宽度

getSuggestedMinimumWidth用来获取内容或者背景尺寸二者中的较大值。

          public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
            boolean optical = isLayoutModeOptical(this);
            if (optical != isLayoutModeOptical(mParent)) {
                //父类子类不同种View 则调整宽高
                Insets insets = getOpticalInsets();
                int oWidth  = insets.left + insets.right;
                int oHeight = insets.top  + insets.bottom;
                widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
                heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
            }

            // Suppress sign extension for the low bytes
            long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
            if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

            final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;

            // Optimize layout by avoiding an extra EXACTLY pass when the view is
            // already measured as the correct size. In API 23 and below, this
            // extra pass is required to make LinearLayout re-distribute weight.

            //宽高是否发生了变化
            final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
                    || heightMeasureSpec != mOldHeightMeasureSpec;
            
           //是否是EXACT模式
           final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
                && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
           
           //匹配
            final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
                    && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
            final boolean needsLayout = specChanged
                    && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);

            if (forceLayout || needsLayout) {
                // first clears the measured dimension flag
                mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

                resolveRtlPropertiesIfNeeded();

                int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
                if (cacheIndex < 0 || sIgnoreMeasureCache) {
                    // measure ourselves, this should set the measured dimension flag back
                    onMeasure(widthMeasureSpec, heightMeasureSpec);
                    mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
                } else {
                    long value = mMeasureCache.valueAt(cacheIndex);
                    // Casting a long to int drops the high 32 bits, no mask needed
                    setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                    mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
                }

                // flag not set, setMeasuredDimension() was not invoked, we raise
                // an exception to warn the developer
                if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
                    throw new IllegalStateException("View with id " + getId() + ": "
                            + getClass().getName() + "#onMeasure() did not set the"
                            + " measured dimension by calling"
                            + " setMeasuredDimension()");
                }

                mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
            }

            mOldWidthMeasureSpec = widthMeasureSpec;
            mOldHeightMeasureSpec = heightMeasureSpec;

            mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
                    (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
    }

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

   protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
       //如果当前View与父类View不是同种View
        if (optical != isLayoutModeOptical(mParent)) {
            //不同就要调整测量值大小
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;
            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }
    //直接赋值
    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;
        //打上标识,已经测量该View的大小
        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }

    //获取默认大小
    //根据三种模式调整再次调整大小
    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;
    }
    //获取内容或者背景尺寸二者中的较大值
    protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

上述过程就是一个单一View的测量过程,当然对于ViewGroup来说其包含多个子View,因此在ViewGroup的源码中有measureChildren来测量子View的尺寸。

   protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            //子View的测量过程
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

    protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();
        //getChildMeasureSpec方法,大致过程也是设置size和mode
        //其中与LayoutParams.MATCH_PARENT以及 LayoutParams.WRAP_CONTENT进行匹配判断
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

2.3.2 布局View

测量好View的大小之后,performTraversals继续会调用performLayout方法,进而调用Viewlayout方法,然后onLayout方法在源码中被layout方法调用,用来在视图中给View布局。因此,该步骤主要是给予View在布局中的位置。

   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) {
            //如果发生了变化,确定View在布局中的位置
            onLayout(changed, l, t, r, b);
            ......
        }
    ......
    }

setOptionalFramesetFrame函数的主要目的就是给View确定位置并判断位置是否变化,setOptionalFrame内核也是调用setFrame函数,因此直接分析setFrame源码。

   protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;

        if (DBG) {
            Log.d(VIEW_LOG_TAG, this + " View.setFrame(" + left + "," + top + ","
                    + right + "," + bottom + ")");
        }
        //判断位置是否变化,变化则需要重新绘制
        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            changed = true;

            // Remember our drawn bit
            int drawn = mPrivateFlags & PFLAG_DRAWN;
            
            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;
            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
            ......
        }
        return changed;
    }

对于我们来说,我们仅需在代码中重写onLayout()函数,如下。

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    }

接着,我们追溯到View的onLayout源码,发现…

    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

???小朋友你是否有很多问号,突然想了想,好像确实应该为空,因为自定义View的位置本该就由其父布局决定,即父布局(一般继承ViewGroup)决定其子View布局(一般继承View类)。那就到ViewGroup里一探究竟吧。

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

    @Override
    protected abstract void onLayout(boolean changed,
            int l, int t, int r, int b);

然而貌似问号越来越多,ViewGroup的layout最关键的部分还是调用其父类Viewlayout函数(是的,ViewGroup的父类也是View)来确定自身位置。而ViewGrouponLayout函数却是一个抽象方法??

仔细考虑一下,ViewGrouponLayout()函数是抽象方法是正确的,因为每个ViewGroup都有着自己的独特布局,如LinearLayoutRelativeLayout等等对于子View的布局规则是不同的,所以写成抽象方法后方便后来继承者自定义自己的布局规则。

接下来看看LinearLayoutonLayout实现

LinearLayout有两种布局形式,以其中一种作为例子分析。

为了好理解布局中一些属性(如margin、padding等),从浏览器中抠出一张图仅供参考。

Android之自定义View学习(二)

@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);//横向线性布局
        }
    }

    /**
     * Position the children during a layout pass if the orientation of this
     * LinearLayout is set to {@link #VERTICAL}.
     *
     * @see #getOrientation()
     * @see #setOrientation(int)
     * @see #onLayout(boolean, int, int, int, int)
     * @param left
     * @param top
     * @param right
     * @param bottom
     */
    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;//内容部分要用宽度去除内填充的长度
        //获得子View数量 getVirtualChildCount()调用的就是getChildCount()
        final int count = getVirtualChildCount();
        
        //对齐方式,通过改变子ViewTop值
        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;
        }
        //循环遍历子View
        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);//子View的Top是基于上一个的,nullChild的值为0
            } else if (child.getVisibility() != GONE) {//这就解释了为什么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;
                //确定子View位置,并判断是否变化
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);//下一个子View的Top位置

                i += getChildrenSkipCount(child, i);
            }
        }
    }

最后做一个小小的实践。
布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<com.example.myviewlearning.TestLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher"/>
</com.example.myviewlearning.TestLayout>

TestLayout.java如下:

public class TestLayout extends ViewGroup {

    public TestLayout(Context context) {
        super(context);
    }

    public TestLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public TestLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public TestLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }
    //上述四类和继承View的自定义View相同
    
    //注意一个顺序,在Measure测量以前,getMeasureWidth和getMeasureHeight两个函数返回值为0
    //这里只给了一个子View
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int childCount = getChildCount();
        //循环遍历子View
        for(int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);//测量子View
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (getChildCount() > 0) {
           View childView = getChildAt(0);
           childView.layout(0, 0, childView.getMeasuredWidth() + 100, childView.getMeasuredHeight() + 100);//给子View安排位置
            Log.d("View Size", "Width: "+ Integer.toString(childView.getWidth()) + " height:" + Integer.toString(childView.getHeight()));
            Log.d("View MeasuredSize", "MeasureWidth: "+ Integer.toString(childView.getMeasuredWidth()) + " height:" + Integer.toString(childView.getMeasuredHeight()));
        }
    }
}

onLayout运行结束后,我们可以通过getWidthgetHeight方法来获取子View的高和宽。

注意:

childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());//给子View安排位置

一般来说childView.getWidth()childView.getMeasuredWidth()会相同,原因是上述给子View布局的这句代码。

getWidth() = childView.getMeasuredWidth() - 0(就是 right - left);

但是实际上两者意义是不同的。

childView.getWidth()主要是用来表示当前childView在该布局中的宽度,只有在layout过程过后才有值。

childView.getMeasuredWidth()主要是用来测量视图本身的大小,在measure之后即可获取到值。

2.3.3 绘制View

测量好View(measure),给View布局好位置(layout),ViewRoot中会继续执行调用performDraw

private void performDraw() {
    ......
    try {
        boolean canUseAsync = draw(fullRedrawNeeded);
        ......
        }
    ......
}

performDraw调用自身的draw方法,在drawSoftware中创建出一个Canvas对象进行一些基本绘制(如背景颜色)并且真正的调用View类中的draw方法,传入创建的Canvas对象。

ViewRootImpl.java
    
private boolean draw(boolean fullRedrawNeeded) {
    .......
            if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
                    scalingRequired, dirty, surfaceInsets)) {
                return false;
            }
        }
    }
......
}

  private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty, Rect surfaceInsets) {

        // Draw with software renderer.
        final Canvas canvas;
        ......
        try {
            if (DEBUG_ORIENTATION || DEBUG_DRAW) {
                Log.v(mTag, "Surface " + surface + " drawing to bitmap w="
                        + canvas.getWidth() + ", h=" + canvas.getHeight());
                //canvas.drawARGB(255, 255, 0, 0);
            }

            if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
                canvas.drawColor(0, PorterDuff.Mode.CLEAR);
            }

            ......
            try {
                canvas.translate(-xoff, -yoff);
                if (mTranslator != null) {
                    mTranslator.translateCanvas(canvas);
                }
                canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
                attachInfo.mSetIgnoreDirtyState = false;

                mView.draw(canvas);

                drawAccessibilityFocusedDrawableIfNeeded(canvas);
            } finally {
                if (!attachInfo.mSetIgnoreDirtyState) {
                    // Only clear the flag if it was not set during the mView.draw() call
                    attachInfo.mIgnoreDirtyState = false;
                }
            }
        } finally {
            ......
        }
        return true;
    }

然后调用View的draw()方法来执行具体的开始绘制(draw)View,最后onDraw方法被View类中的draw方法调用进行内容绘制,内容绘制也是最关键的一步。

绘制过程主要分六步骤,其中第二、第五步骤相对使用较少。

    View.java
    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. 绘制背景
         *      2. 保存当前canvas,非必须
         *      3. 绘制View的内容
         *      4. 绘制子View
         *      5. 绘制边缘、阴影等效果,非必须
         *      6. 绘制装饰,如滚动条等
         */

        // 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);
            
            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);

            .......
            // we're done...
            return;
        }
        ......
    }

  private void drawBackground(Canvas canvas) {
        final Drawable background = mBackground;
        if (background == null) {
            return;
        }

        setBackgroundBounds();
        ......
    }

    void setBackgroundBounds() {
        if (mBackgroundSizeChanged && mBackground != null) {
            mBackground.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
            mBackgroundSizeChanged = false;
            rebuildOutline();
        }
    }

从代码中看出,第一步,背景的绘制实际上调用了一个Drawable对象mBackground进行背景绘制,然后根据layout过程确定的视图位置(mLeft mRight mTop mBottom)来设置背景的绘制区域,之后再调用onDraw方法来完成背景的绘制工作。

而这个mBackground对象其实就是在XML中通过android:background属性设置的图片或颜色。当然也可以在代码中通过setBackgroundColor()setBackgroundResource()等方法进行赋值。

跳过第二步,来到第三步骤,调用onDraw方法对View内容进行绘制,但是有了onLayout经验,这里onDraw方法同样需要被重写。

第四步,进行子View绘制处理,当然对于View来说,dispatchDraw是空方法,因为没有子View,但是对于ViewGroup来说,dispatchDraw还是比较复杂的。

跳过第五步,来到最后一步,对视图的滚动条进行装饰,从这里其实就可以看出,其实所有的控件都是有着自己的滚动条的,只不过被隐藏了起来。

最后同样来个小实践。
布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/main_layout"
    tools:context=".MainActivity">

    <com.example.myviewlearning.MyTestView
        android:layout_height="match_parent"
        android:layout_width="match_parent"
        android:background="#000000"/>


</LinearLayout>

onDraw()重写如下:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        final int paddingLeft = getPaddingLeft();
        final int paddingRight = getPaddingRight();
        final int paddingTop = getPaddingTop();
        final int paddingBottom = getPaddingBottom();
        int width = getWidth() - paddingLeft - paddingRight;
        int height = getHeight() - paddingTop - paddingBottom;
        int radius = Math.min(width, height)/2;
        
           //画圆
        canvas.drawCircle(paddingLeft + width/2, paddingTop + height/2, radius, mPaint);
        
        //设置text和textColor
        mPaint.setTextSize(88);
        String text = "Hi, my view";
        mPaint.setColor(Color.WHITE);
        canvas.drawText(text,width/3, height/2, mPaint);

    }

2.3.4 绘制自定义View总结

到此为止,我们就明白了,要想能够自定义一个View,少不了这三个步骤。

测量------>布局------>绘制

其他学习分享系列

数据结构与算法系列

数据结构与算法之哈希表

数据结构与算法之跳跃表

数据结构与算法之字典树

数据结构与算法之2-3树

数据结构与算法之平衡二叉树

数据结构与算法之十大经典排序

数据结构与算法之二分查找三模板

如有兴趣可以关注我的微信公众号,每周带你学一点算法与数据结构。
Android之自定义View学习(二)

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
Android So动态加载 优雅实现与原理分析
背景:漫品Android客户端集成适配转换功能(基于目标识别(So库35M)和人脸识别库(5M)),导致apk体积50M左右,为优化客户端体验,决定实现So文件动态加载.!(https://oscimg.oschina.net/oscnet/00d1ff90e4b34869664fef59e3ec3fdd20b.png)点击上方“蓝字”关注我
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Wesley13 Wesley13
3年前
35岁是技术人的天花板吗?
35岁是技术人的天花板吗?我非常不认同“35岁现象”,人类没有那么脆弱,人类的智力不会说是35岁之后就停止发展,更不是说35岁之后就没有机会了。马云35岁还在教书,任正非35岁还在工厂上班。为什么技术人员到35岁就应该退役了呢?所以35岁根本就不是一个问题,我今年已经37岁了,我发现我才刚刚找到自己的节奏,刚刚上路。
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这