自定义View

Posted 李过饰非

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自定义View相关的知识,希望对你有一定的参考价值。

一、了解ViewRoot和DecorView

1.ViewRoot

从源码可以看出ViewRoot是ViewParent的实现类


public final class ViewRoot extends Handler implements ViewParent,

ViewRoot对应于的ViewRootImp也是ViewParent的实现类

public final class ViewRootImpl implements ViewParent,View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks 

我们知道View有三大流程(measure->layout->draw),都是通过ViewRoot完成的。内部通过performalTraversals()方法依次向下传递的。

2.View的三大流程

measure

measure决定了View的宽高,当measure完成后,我们可以通过以下代码获取view的测量宽度、测量高度,


        t.getMeasuredHeight();
        t.getMeasuredWidth();

注意:我们不可以在Activity的onCreate方法中对一个view使用上述代码获取宽度,因为在执行Activity的onCreate()方法时measure还没测量好,需要在前面调用以下代码来


    t.measure(0, 0);

这里的0是MeasureSpec,下面后介绍到。还有种获取宽度、高度的方式,如下:


    t.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
             width = t.getMeasuredWidth(); 
             height = t.getMeasuredHeight();
             t.getViewTreeObserver().removeGlobalOnLayoutListener(this);
        }
    }) ;

其实方法有很多种,我们也可以通过view.post()将获取宽度、高度的代码放到一个任务队列的末尾、重写onWindowFocusChanged()方法等。

继续分析,View的measure是又measure()方法完成的。查看源码


     public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
            boolean optical = isLayoutModeOptical(this);
            if (optical != isLayoutModeOptical(mParent)) {
                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);
            }
            if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
                    widthMeasureSpec != mOldWidthMeasureSpec ||
                    heightMeasureSpec != mOldHeightMeasureSpec) {

            // first clears the measured dimension flag
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

            resolveRtlPropertiesIfNeeded();

            // measure ourselves, this should set the measured dimension flag back
            onMeasure(widthMeasureSpec, heightMeasureSpec);

            // 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("onMeasure() did not set the"
                        + " measured dimension by calling"
                        + " setMeasuredDimension()");
            }

            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
        }

        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;
    }

这是一个final方法,因此子类不可以重写,但是我们从源码中可以看到有如下代码


         // measure ourselves, this should set the measured dimension flag back
         onMeasure(widthMeasureSpec, heightMeasureSpec);

再看onMeasure()


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

由此可以看出最后是通过setMeasuredDimension()方法进行设置宽度、高度的。再看看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;
    }

这个方法比较简单,其中specSize是测量后的大小,然后通过swich根据specMode来设置不同的值,当specMode为AT_MOST、EXACTLY时,返回specSize。而当specMode是UNSPECIFIED时,返回的就直接是getSuggestedMinimumHeight()的值。


    protected int getSuggestedMinimumHeight() {
        return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());

    }

根据背景是否为null,如果为null,则返回mMinHeight,mMinHeight默认为0。写到这,突然想到PoupuWindow的使用,不知道有没有童鞋遇到过明明代码没错但是就是不出现效果,非得设置个背景才有效,即使背景什么都没有,不知道是不是也是上面的原因,回头研究下。。。

layout

layout决定了View的四个点的坐标和位置信息,此时获取的宽度高度是真正宽度高度,并且我们可以获取left、right、top、bottom值,直接通过getWidth()、getHeight()获取宽度、高度。
源码如下:


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

由上面代码可以看出上述代码先是调用了下面这行代码用来确定四个顶点的位置


    boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

然后调用了onLayout()方法,而在onLayout()方法中,我们发现只是一个空实现,因此需要我们子类去重写


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

这里我们查看一下RelativeLayout的onLayout方法,看其具体实现


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //  The layout has actually already been performed and the positions
        //  cached.  Apply the cached values to the children.
        final int count = getChildCount();

        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                RelativeLayout.LayoutParams st =
                        (RelativeLayout.LayoutParams) child.getLayoutParams();
                child.layout(st.mLeft, st.mTop, st.mRight, st.mBottom);
            }
        }
    }

相对布局的onLayout()方法的代码比较简单,首先是获取布局子view的个数,然后for遍历,只要不是gone的都是依次获取布局参数,然后调用子view的layout()方法,子view又调用setFrame(l, t, r, b),因此相对布局的每一个子view都是重叠的。

draw

draw完成了View的显示过程,只有在draw完成之后才会才屏幕上东西。
在View的draw()方法的实现我们可以看到如下:


        /*
         * 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)
         */

翻译过来就是:

  1. 画背景
  2. 有需要的话画layers
  3. 画内容
  4. 画字view
  5. 有需要的话画edges和报存layers
  6. 画装饰

DecorView

DecorView是顶级View,一般包括一个竖直方向的LineayLayout,主要包括标题栏和内容。标题栏一般是主题设置的,内容部分是在代码中体现的


    setContentView(R.layout.activity_main);

DecorView其实就是一个FrameLayout,View的事件都会经过它,然后再传递给子view

二、MeasureSpec

1.MeasureSpec是什么

MeasureSpec是一个32位的二进制数,高2位代表测量模式specMode,低30位代表测量大小specMode
整个类比较简单,源码如下


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

        /**
         * Measure specification mode: The parent has not imposed any constraint
         * on the child. It can be whatever size it wants.
         */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has determined an exact size
         * for the child. The child is going to be given those bounds regardless
         * of how big it wants to be.
         */
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        /**
         * Measure specification mode: The child can be as large as it wants up
         * to the specified size.
         */
        public static final int AT_MOST     = 2 << MODE_SHIFT;

        /**
         * Creates a measure specification based on the supplied size and mode.
         *
         * The mode must always be one of the following:
         * <ul>
         *  <li>{@link android.view.View.MeasureSpec#UNSPECIFIED}</li>
         *  <li>{@link android.view.View.MeasureSpec#EXACTLY}</li>
         *  <li>{@link android.view.View.MeasureSpec#AT_MOST}</li>
         * </ul>
         *
         * <p><strong>Note:</strong> On API level 17 and lower, makeMeasureSpec‘s
         * implementation was such that the order of arguments did not matter
         * and overflow in either value could impact the resulting MeasureSpec.
         * {@link android.widget.RelativeLayout} was affected by this bug.
         * Apps targeting API levels greater than 17 will get the fixed, more strict
         * behavior.</p>
         *
         * @param size the size of the measure specification
         * @param mode the mode of the measure specification
         * @return the measure specification based on size and mode
         */
        public static int makeMeasureSpec(int size, int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }

        /**
         * Extracts the mode from the supplied measure specification.
         *
         * @param measureSpec the measure specification to extract the mode from
         * @return {@link android.view.View.MeasureSpec#UNSPECIFIED},
         *         {@link android.view.View.MeasureSpec#AT_MOST} or
         *         {@link android.view.View.MeasureSpec#EXACTLY}
         */
        public static int getMode(int measureSpec) {
            return (measureSpec & MODE_MASK);
        }

        /**
         * Extracts the size from the supplied measure specification.
         *
         * @param measureSpec the measure specification to extract the size from
         * @return the size in pixels defined in the supplied measure specification
         */
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }

        static int adjust(int measureSpec, int delta) {
            return makeMeasureSpec(getSize(measureSpec + delta), getMode(measureSpec));
        }

        /**
         * Returns a String representation of the specified measure
         * specification.
         *
         * @param measureSpec the measure specification to convert to a String
         * @return a String with the following format: "MeasureSpec: MODE SIZE"
         */
        public static String toString(int measureSpec) {
            int mode = getMode(measureSpec);
            int size = getSize(measureSpec);

            StringBuilder sb = new StringBuilder("MeasureSpec: ");

            if (mode == UNSPECIFIED)
                sb.append("UNSPECIFIED ");
            else if (mode == EXACTLY)
                sb.append("EXACTLY ");
            else if (mode == AT_MOST)
                sb.append("AT_MOST ");
            else
                sb.append(mode).append(" ");

            sb.append(size);
            return sb.toString();
        }
    }

其中比较重要的是specMode。

2.specMode

specMode有三个值分别是:

  1. AT_MOST:这个值表示父容器指定了可用大小,view的大小不可以大于该指定大小,这个对应于LayoutParams的wrap_content。这个模式处理起来稍麻烦点,因为需要我们自行测量。
  2. EXACTLY:父容器已经知道子view 的精确大小,这时候的view 的大小就是specSize。
  3. UNSPECIFIED:父容器不对view有任何限制,这个模式用的比较少。

3.LayoutParams

上面提到到AT_MOST的时候该模式对应于LayoutParams的wrap_content,那么说明LayoutParams也会影响view的测量的。在对View进行测量的时候,系统会将LayoutParams在父容器的约束下转换成MeasureSpec.
总的来说:对于普通View的MeasureSpec由父容器的MeasureSpec和LayoutParams决定,而对应顶级View来说,因为没有父容器,故由窗口的大小和自身LayoutParams决定。而一旦MeasureSpec确定,就可以对其测量了。这里提到MesureSpec是又父容器的MeasureSpec和LayoutParams决定的,就从源码看看。


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

这是ViewGroup的measureChild方法,该方法调用了getChildMeasureSpec()方法,并且将父容器的MeasureSpec传递过去然后再返回子view的MeasureSpec。


        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 = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

从源码可以看出,首先通过父容器的MeasureSpec来switch判断,例如,如果父容器的MeasureSpec是EXACTLY,而自view的LayoutParams是WRAP_CONTENT,那么


             resultSize = size;
             resultMode = MeasureSpec.AT_MOST;

三、实例

理论说的比较多,上代码,


    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        tools:context=".MainActivity" >

         <com.lw.viewdemo.MyView 
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:background="#000"
             />

    </LinearLayout>

注意:这里的父布局的宽度、高度我使用的是wrap_content,而MyView的宽度、高度使用的都是是match_parent。


    public class CircleView extends View {

        private int mColor = Color.BLUE ;
        private Paint mPaint = new Paint(Paint.DEV_KERN_TEXT_FLAG) ;
        public CircleView(Context context) {
            super(context);
            init();
        }


        public CircleView(Context context, AttributeSet attrs) {
            super(context, attrs);
            init();
        }
        private void init() {
            mPaint.setColor(mColor);
        }

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
            int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
            int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
            int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
             if (widthSpecMode == MeasureSpec.AT_MOST
                        && heightSpecMode == MeasureSpec.AT_MOST) {
                    setMeasuredDimension(200, 200);
                } else if (widthSpecMode == MeasureSpec.AT_MOST) {
                    setMeasuredDimension(200, heightSpecSize);
                } else if (heightSpecMode == MeasureSpec.AT_MOST) {
                    setMeasuredDimension(widthSpecSize, 200);
                }
        }

        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            int paddingLeft = getPaddingLeft();
            int paddingRight = getPaddingRight();
            int paddingBottom = getPaddingBottom();
            int paddingTop = getPaddingTop();
            int width = getWidth(); 
            width = width - paddingLeft - paddingRight ;
            int height = getHeight() ;
            height = height - paddingTop - paddingBottom;
            int radius = Math.min(width/2, height/2); 
            canvas.drawCircle(paddingLeft + width / 2 , paddingTop + height / 2 , radius, mPaint);
        }

    }

在onMeasure()方法中判断子view的测量模式,如果模式为AT_MOST的话,我们需要自己手动为其指定高度和宽度,而根据前面我们分析getChildMeasureSpec()方法中可以知道,我们这里对应的是滴一个else if语句块。因此此时onDraw()画出来的是200dp。其实从源码可以看出,子父容器为AT_MOST的条件下,子view是WRAP_CONTENT和是MATCH_PARENT没什么区别。当然,若是父容器是EXACTLY的话,两者是有区别的。然后就是setMeasuredDimension()方法调用,这个方法之前在分析源码的时候看到过了,默认会调用这个方法的,作用就是设置宽度高度。


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

由于代码比较简单,就不上传了,需要的留言。。。
OK,这篇简单的分析了自定义view的基本知识和一个小demo,下篇将会继续学习自定义View。

以上是关于自定义View的主要内容,如果未能解决你的问题,请参考以下文章

VSCode自定义代码片段——CSS选择器

VSCode自定义代码片段6——CSS选择器

VSCode自定义代码片段(vue主模板)

VSCode自定义代码片段——声明函数

VSCode自定义代码片段——.vue文件的模板

VSCode自定义代码片段——git命令操作一个完整流程