《Android开发艺术探索》之View的工作原理(五)

175 阅读10分钟

                                                                      第四章  View的工作原理
View是Android中视觉的呈现。为了更好地自定义View,需要掌握View的底层工作原理,比如View的测量流程measure、布局流程layout和绘制流程draw。View常见的回调方法也是必须掌握的,比如构造方法onAttach,onVisibilityChanged,onDetach。有滑动效果的自定义View需要解决滑动冲突。总的来说,自定义View有几种固定的类型,View或者ViewGroup,有的直接重写原生控件
(一)初识ViewRoot和DecorView
1.1 ViewRoot

ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程都是通过ViewRoot来完成的。在ActivityThread中,当Activity被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立联系。

root = new ViewRootImpl(view.getContext(),display);
root.setView(view,wparams,panelParentView);

        View的绘制流程:从ViewRoot的perfromTraversals方法开始,他通过measure,layout和draw三个过程才能将View画出来,measure测量view宽高,layout确定view在父容器的位置,draw将view绘制在屏幕上。依次调用perfromMeasure,perfromLayout,perfromDraw,他们分别完成顶级View的measure,layout和draw。
onMeasure完成从父容器传递到子元素的过程,接着子元素会重复父容器的measure过程,如此反复的完成了整个View树的遍历,measur过程决定了View的宽高,通过getMeasureWidth和getMeasureHeight来获取View测量后的高宽;layout过程决定了view的四个顶点的坐标和实际View的宽高,一般View测量后的宽高等同于View最终的宽高;draw决定了View的显示,draw方法完成了后view会显示在屏幕上。

                                      
1.2 DecorView
顶级View DecorView,一般情况下他内部会包含一个竖直方向的LinearLayout,这里面有上下两部分,上面是标题栏,下面是内容。可用通过setContentView将所设置的布局文件就是放在内容中。
得到content:ViewGroup content = findviewbyid(android.R.id.content);得到设置的View:content.getChildAt(0).DeaorView其实就是一个FrameLayout,View层事件都先经过DecorView,然后传递给View。

                                                            
(二)理解MeasureSpec
MeasureSpec“测量规格””测量说明书”,其受父容器的影响,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后再根据这个measureSpec来测量出View的宽高。测量高宽不应定等于最终高宽。
2.1.MeasureSpec
MeasureSpec代表一个32位int值,高两位代表SpecMode(测量模式),低30位代表SpecSize(规格大小)。
MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,并提供SpecMode和SpecSize的打包和解包方法。SpecMode分为三类:UNSPECIFIED:无限制,要多大给多大;EXACTLY:父容器已经检测出View所需要的精度大小(SpecSize所指定的值),对应于LayoutParams中的match_parent和具体的数值两种。AT_MOST:父容器指定了一个可用大小(SpecSize),view的大小不能大于这个值,它对应于LayoutParams中wrap_content。
2.2.MeasureSpec 和 LayoutParams 的对应关系
在view测量的时候,系统会将layoutparams在父容器的约束下转换成对应的MeasureSpec,然后再根据这个MeasureSpec来确定view测量后的宽高。
MeasureSpec不是唯一由layoutparams决定的,layoutparams需要和父容器一起决定view的MeasureSpec,从而进一步决定view的宽高。顶级view(DecorView)和普通的view的MeasureSpec转换过程:前者由窗口自身尺寸和自身的layoutparams来决定,后者由父容器的MeasureSpec和自身的layoutparams来决定,还和View的Margin、Padding有关。
layoutparams参数包括:
LayouParams.MATCH_PARENT:精确模式,大小就是窗口的大小
LayouParams.WRAP_CONTENT:最大模式,大小不定,但是不能超出屏幕的大小
固定大小(比如100dp):精确模式,大小为LayoutParams中指定的大小
2.3总结
当View采用固定宽/高的时候,不管父容器的MeasureSpec是什么,View 的MeasureSpee都是精确模式,那么View也是精准模式并且其大小是父容器的剩余空间;当View是match_parent时,要么等于父容器剩余空间,要么不大于父容器剩余空间;当View的宽/高是wrap_content时,不管父容器的模式是精准还是最大化,View的模式总是最大化,并且大小不能超过父容器的剩余空间。

          
只要提供父容器的MeasureSpec和子元素的LayoutParams,就可以快速地确定出子元素的MeasureSpec了。有了 MeasureSpec就可以进一步确定出子元素测量后的大小了。
(三)View的工作流程
View的工作流程主要是指measure、layout、draw这三大流程,即测量、布局和绘制,其中measure确定View的测量宽/高,layout确定View的最终宽/高和四个顶点的位置,而draw将View绘制到屏幕上。
3.1.measure过程
如果只是一个原始的View,那么通过measure方法就可以完成了其测量过程,如果是一个ViewGroup,除了完成自己的测量过程外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个流程。
3.1.1.View的measure过程
在View的measure方法中(final类型的方法,子类不能重写此方法)去调用View的onMesure方法:

    @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//1.setMeasuredDimension会设置View宽/高的测量值
        setMeasuredDimension(
                getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
//3.getSuggestedMinimumWidth:如果View没有设置背景,那么返回android:minwidth这个属性所指定的值,这个值可以为0:如果View设置了背景,则返回 android:minwidth和背景Drawable的原始宽度这两者中的最大值.
}

//2.getDefaultSize返回的大小就是mesurESpec中的specSize,而这个specSize就是view测量后的大小,View最终的大小是在layout阶段确定的,但一般情况两者相同。
    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的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent。
原因:表4-1和代码说明了。如果View在布局中使用wrap_content,那么它的specMode是AT_MOST模式,在这种模式下,它的宽/高等于 specSize;表4-1说明View的specSize是parentSize,而parentSize是父容器中目前可以使用的大小,View的宽/高就等于父容器当前剩余的空间大小,这种效果和在布局中使用match_parent完全一致。
如何解决:在重写的onMeasure方法中指定默认的内部高/宽。TextView、ImageView的wrap_content对onMeasure方法做了特殊处理。
3.1.2.ViewGroup的measure过程
对于ViewGroup而言,除了完成自己的measure过程以外,还会遍历去调用所有子元素的measure方法,各个子元素再通归去执行这个过程。和View不同的是,ViewGroup是一个抽象类,因此它没有重写View的onMeasure方法,但是它提供了一个叫measureChildren,在ViewGroup的measure时,会对每一个子元素调用measureChild进行测量。

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

 protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        //1.取出子元素的LayoutParams
        final LayoutParams lp = child.getLayoutParams();
        //2.getChidMeasureSpec来创建子元素的MeasureSpec
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
//3.getChildMeasureSpec根据父容器的MeasureSpec同时结合view本身来layoutparams来确定子元素的MesureSpec
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);
        //4.MeasureSpec直接传递给View的measure方法来进行测量
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

3.1.3.场景一:LinearLayout测量过程分析ViewGroup的measure过程
步骤一:调用onMeasure方法;判断布局是水平还是竖直;根据布局方向选择measureVertical或者measureHorizontal方法;
步骤二:进入measureVertical方法,遍历子元素并对每一个子元素执行measureChildBeforeLayout方法,这个方法内部会调用子元素的measure方法;mTotalLength这个变量来存储LinearLayout在竖直方向上的高度。
步骤三:当子元素测量完毕之后,LinearLayout会根据子元素的情况来测量自己的大小,若高度采用的是match_parent或者具体值,那么他的绘制过程和View一致,若采用warp_content,那么它的高度是所有的子元素所占用的高度+竖直方向上的Padding。
3.1.4.场景二:解决View测算的获取错误问题
场景:Activity已启动的时候做一件任务,这一件任务需要获取某个View的宽/高,在onCreate、onStart、onResume中均无法正确得View的宽/高信息,这是因为View的measure过程和Activity的生命周期方法不是同步执行的,因此无法保证Activiy执行了onCreate、onStart、onResume时某个Vicw已经完毕了,如果View还没有测量完毕,那么获得的宽/高就是0。如何解决?
四种解决方案:
方案一:Activity/View#onWindowFocusChanged:View已经初始化完毕了,宽/高已经准备好了,这个时候去获取宽/高是没问题的。

 public void onWindowFocusChanged(boolean hasWindowFocus) {
        InputMethodManager imm = InputMethodManager.peekInstance();
        if (!hasFocus) {
             int width = view.getMeasureWidth();
             int height = view.getMeasureHeight();
        }
}

       方案二:view.post(runnable):通过post可以将一个runnable投递到消息队列,然后等到Lopper调用runnable的时候,View也就初始化好了,典型代码如下:

   @Override
    protected void onStart() {
        super.onStart();
        mTextView.post(new Runnable() {
            @Override
            public void run() {
                int width = mTextView.getMeasuredWidth();
                int height = mTextView.getMeasuredHeight();
            }
        });
    }

       方案三:ViewTreeObserver:使用ViewTreeObserver的众多回调可以完成这个功能,比如使用OnGlobalLayoutListener这个接口,当View树的状态发生改变或者View树内部的View的可见性发生改变,onGlobalLayout方法就会回调。

    @Override
    protected void onStart() {
        super.onStart();
        ViewTreeObserver observer = mTextView.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener(){
            @Override
            public void onGlobalLayout() {
                mTextView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                int width = mTextView.getMeasuredWidth();
                int height = mTextView.getMeasuredHeight();
            }
        });
    }

       方案四:view.measure(int widthMeasureSpec , int heightMeasureSpec):
针对match_parent,无法测算;
针对具体的数值,宽/高都是100dp,代码:

  int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.
EXACTLY);
  int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.
EXACTLY);
  mTextView.measure(widthMeasureSpec,heightMeasureSpec);

        针对wrap_content:
View的尺寸是三十位的二进制表示,也就是说最大是30个1(2^30-1),也就是(1<30-1),在最大的模式下,我们用View理论上能支持最大值去构造MwasureSpec是合理的。

int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.
AT_MOST);
 int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.
AT_MOST);
mTextView.measure(widthMeasureSpec,heightMeasureSpec);

3.2.layout过程
Layout的作用是ViewGroup用来确定子元素的, 当ViewGroup的位置被确认之后,他的layout就会去遍历所有子元素并且调用onLayout方法,在layout方法中onLayou又被调用,layout方法确定了View本身的位置,而onLayout方法则会确定所有子元素的位置,先看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;
        //setFrame方法来设定View的四个顶点的位置,四个顶点一旦确定,那么View在父容器的位置也就确定了
        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;
    }

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

   void layoutVertical(int left, int top, int right, int bottom) {
        ...
        final int count = getVirtualChildCount();
        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {
                //width和height实际上就是子元素测量宽高
                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方法来为子元素指定对应的位置,仅仅是调用元素的layout方法而已,这样的父容器在layout方法中完成自己的定位以后,就通过onLayout方法去调用,子元素又会通过自己的layout方法来确定自己的位置。
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

                i += getChildrenSkipCount(child, i);
            }
        }
    }
   
    //width和height实际上就是子元素测量宽高
    private void setChildFrame(View child, int left, int top, int width, int height) {        
        child.layout(left, top, left + width, top + height);
    }

       场景:View的测量宽高和最终宽高有什么区别?
回答:根据源码,getWidth返回的刚好是View测量的测量宽度,getHeight是测量高度,在View的默认实现中,View的测量宽高和最终的是一样的,只不过一个是measure过程,一个是layout过程,而最终形成的是layout过程,即两者的赋值时机不同,测量宽高的赋值时机,稍微早一些。一般相等。eg:View的最终宽高总是比测量大于100px。

    public void layout(int l,int t,int r, int b){
        super.layout(l,t,t+100,b+100);
    }

       此外,View需要多次measure才能确定自己的测量宽高。这种两者也有可能不一致。
3.3.draw过程
Draw的作用是将View绘制到屏幕上面,主要有以下几个步骤:1.绘制背景background.draw(canvas);2.绘制自己(onDraw);3.绘制children(dispatchDraw);4.绘制装饰(onDrawScrollBars)。

   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会遍历调用所有子元素的draw方法,draw时间一层层传递了下去。
            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
4.1.自定义View的分类

自定义View没有什么具体的分类,不过可以从特性大致的分为4类:
1.继承View重写onDraw方法
重写了绘制,自己实现某些图形,原生控件已经满足不了你了,重写onDraw方法,采用这个方式需要自身支持warp_content,并且padding也要自己处理。
2.继承ViewGroup派生出来的Layout
重写容器,实现自定义布局。 除了LinearLayout、RelativeLayout、FrameLayout外,重新定义布局,像是几种View组合在一起,可合理使用测量、布局,并处理好子元素。
3.继承特定的View(比如TextView)
重写原生的View,较为容易实现,不需要自己支持wrap_content和padding等。
4.继承特定的ViewGroup(比如LinearLayout)
重写容器,本质相同,在事件分发时用的多,方法二更接近于底层。
4.2.自定义View需知
若不注意,会导致内存泄漏。具体注意事项:
1.让View支持warp_content
直接继承View或ViewGroup控件,需要在onMeasure中对wrap_content进行特殊处理;
2.如果有必要,让View支持Padding
直接继承View控件的需要在draw方法中处理padding;另外,继承自ViewGroup的空间需要在onMeasure和onLayout中考虑padding和子元素的margin。
3.尽量不要在View中使用Handler
View内部本身提供了post系列方法,完全可以替代Handler的作用。
4.View中如果有线程和动画,需要及时停止View#onDetachedFromWindow
不停止这个线程或者动画,容易导致内存溢出,当此View的Activity退出或者当前View被remove时,View的onDetachedFromWindow会被调用,与之相对应的是onAttachedToWindow。
5.View代用滑动嵌套情形时,需要处理好滑动冲突
若有滑动冲突,及时处理。
4.3.自定义View示例
4.3.1.继承View重写onDraw方法

任务:绘制一个简单的圆。需要注意很多细节,实现的过程中需要考虑(1)warp_content;(2)padding;(3)为其提供自定义属性。因为直接继承自View和ViewGroup的控件不对wrap_content和padding做特殊处理,需要自行处理。
问题一:wrap_content不起作用,与match_parent没有任何区别。
回答一:onMeasure方法中指定wrap_content的默认宽/高,在下面代码中我们指定为200px,分析宽高的MeasureSpec,分析模式,设定默认值。
问题二:padding不起作用的问题。
回答二:onDraw方法中做一定修改,在绘制的时候考虑到View四周的空白即可,圆心和半径都要考虑到。
问题三:如何添加自定义属性?
回答三:第一步:在values目录下面创建自定义属性的xml,比如attrs_circle_view.xml

<resources>
    <declare-styleable name="CirecleView">
        <attr name="circle_color" format="color" />
    </declare-styleable>
    <!--定义了格式为color的属性circle_color-->
</resources>

      第二步,在View的构造方法里解析自定义属性的值并做相应处理。参考下面代码中的构造方法;
第三步:在布局文件中使用自定义属性。app:circle_color="@color/colorPrimary"
4.3.2.继承View重写onDraw方法的代码实现

/**
 * 实现了一个具有圆形效果的自定义View,它会在自己的中心点以宽高的最小值为直径绘制一个红色的施实心圆。
 * TODO: document your custom view class.
 */
public class CirecleView extends View {
    //
    private int mColor = Color.RED;
    private Paint mpaint = new Paint(Paint.ANTI_ALIAS_FLAG);

    public CirecleView(Context context) {
        super(context);
        init();
    }

    public CirecleView(Context context, AttributeSet attrs) {
        this(context,attrs,0);
    }

    public CirecleView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        //加载自定义属性集合CircleView
        TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.CirecleView);
        //解析CircleView属性集合中的circle_color属性,它的id刚才已经定义了
        mColor = a.getColor(R.styleable.CirecleView_circle_color,Color.RED);
        //解析完成后,通过recycle来释放资源
        a.recycle();
        init();
    }

    private void init() {
        // Load attributes
        mpaint.setColor(mColor);
    }

    //解决wrap_content不起作用的问题
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, 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(200,200);
        }else if(widthSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(200,heightSpecSize);
        }else if(heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthMeasureSpec,200);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // TODO: consider storing these as member variables to reduce
        // allocations per draw cycle.
//        int width = getWidth();
//        int height = getHeight();
//        int radius = Math.min(width, height) / 2;
//        canvas.drawCircle(width/2,height/2,radius,mpaint);
        //解决padding不起作用的问题
        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);
        //Padding 为内边框,指该控件内部内容,如文本/图片距离该控件的边距
//        Margin 为外边框,指该控件距离边父控件的边距
    }
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ffffff"
    android:orientation="vertical">
    <com.example.hzk.p202.CirecleView
        android:id="@+id/circleView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="20dp"
        app:circle_color="@color/colorPrimary"
        android:padding="10dp"
        android:background="#000000"
        />
<!--直接引用该布局即可-->
</LinearLayout>

4.3.3.继承ViewGroup派生特殊的Layout
任务:类似于ViewPager,或者说水平方向的线性布局LinearLayout,它内部的子元素可以水平滑动并且子元素元素内部可以竖直滑动。
解决:onMeasure方法中,首先判断是否有子元素,若果没有子元素就直接把自己的宽高设置为零,如果高采用了wrap_content,那么高度就是第一个元素的高度。如果宽采用了wrap_content,那么宽度就是所有子元素的宽度之和。不规范之处有两点:1.没有元素时不应该把宽高设置为零,应该根据LayoutParams的宽高来处理; 2.未考虑它的Padding和子元素的margin。
onLayout方法,首先遍历所有子元素,若果不是处于GONE状态下,通过Layout将其放在合适的位置上,位置是从左往右的,但是仍然没有考虑padding和子元素的margin,这个也不是很规范。
另外考虑滑动冲突的外部拦截事件处理onInterceptTouchEvent和点击事件处理onTouchEvent。
部分代码如下:

import android.content.Context;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
/**
 * Created by hzk on 2019/6/17.
 */
public class HorizontalScrollViewEx extends ViewGroup {
    private int childrenSize;
    private int mChildWidth;
    public HorizontalScrollViewEx(Context context) {
        super(context);
    }	
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int measureWidth = 0;
        int measureHeight = 0;
        //首先判断是否有子元素
        final int childrenCount = getChildCount();
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        //如果没有子元素就直接把自己的宽高设置为零
        if (childrenCount == 0) {
            setMeasuredDimension(0, 0);
        } else if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            //如果高采用了wrap_content,那么高度就是第一个元素的高度。
            measureWidth = childView.getMeasuredWidth() * childrenCount;
            //如果宽采用了wrap_content,那么宽度就是所有子元素的宽度之和。
            measureHeight = childView.getMeasuredHeight();
            setMeasuredDimension(measureWidth,measureHeight);
        }else if(heightSpecMode == MeasureSpec.AT_MOST){
            final View childView = getChildAt(0);
            measureHeight = childView.getMeasuredHeight();
            setMeasuredDimension(widthSpecSize,measureHeight);
        }else if(widthSpecMode == MeasureSpec.AT_MOST){
            final View childView = getChildAt(0);
            measureWidth = childView.getMeasuredWidth() * childrenCount;
            setMeasuredDimension(measureWidth,heightSpecSize);
        }
//        不规范之处有两点:1.没有元素时不应该把宽高设置为零,应该根据LayoutParams的宽高来处理;
//        2.未考虑它的Padding和子元素的margin。
    }
//    首先遍历所有子元素,若果不是处于GONE状态下,通过Layout将其放在合适的位置上,位置是从左往右的,
//    但是仍然没有考虑padding和子元素的margin,这个也不是很规范,
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childleft = 0;
        final int childCount = getChildCount();
        childrenSize = childCount;
        for(int i= 0 ;i<childCount;i++){
            final View childView = getChildAt(i);
            if(childView.getVisibility()!=View.GONE){
                final int childWidth = childView.getMeasuredWidth();
                mChildWidth = childWidth;
                childView.layout(childleft,0,childleft+childWidth,childView.getMeasuredHeight());
                childleft+=childWidth;
            }
        }
    }
    //解决滑动冲突的外部拦截事件处理
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return super.onInterceptTouchEvent(ev);
    }
    //点击事件处理
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return super.onTouchEvent(event);
    }
}

        4.4自定义View的思想
首先掌握基本功,比如View的弹性滑动、滑动冲突、绘制原理;掌握之后,自定义View对其分类并选择合适的实现思路;最后是积累经验。