View的工作原理之自定义View

前面的四篇系列文章,从源码开始详细的分析了View的Measure过程。学以致用,这篇文章就记录一下,学完View的Measure过程之后,自己自定义View的一些收获。本文讲解的是普通View的自定义,ViewGroup的自定义将在下篇讲解。

       创建一个Android应用工程,新建一个类MyView继承自View。重写它的三个构造方法及onDraw方法:

    public class MyView extends View {
        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        int mColor;
        public MyView(Context context) {
            super(context);
        }
     
        public MyView(Context context, @Nullable AttributeSet attrs) {
            this(context,attrs,0);
        }
     
        public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.MyView);
            mColor = a.getColor(R.styleable.MyView_rect_color,Color.RED);
            a.recycle();
        }
     
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            float width = getMeasuredWidth();
            float height = getMeasuredHeight();
            paint.setColor(mColor);
     
            canvas.drawRect(0,0,width,height,paint);
        }
    }

       这里来看一下代码,首先是需要重写的三个构造方法,这是每个自定义View都必须重写的。在第三个构造方法里面,拿到了一个自定义的属性。那就来说说,该怎么自定义属性。

      android原生View给我们定义了很多属性,基本上能满足平常的开发工作。但是有时候面对一些特殊的需求,可能需要用到自定义属性,那么这就可以使用到自定义属性了。实现自定义属性的步骤是,首先在res/values文件夹下,新建一个命名为atts的,类型为value的xml文件,然后添加如下内容:

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <declare-styleable name="MyView">
            <attr name="rect_color" format="color" />
        </declare-styleable>
    </resources>

        先看declare-styleable标签,它后面有个name的属性,这个属性的值,就表示declare-styleable标签下面的自定义属性,适用于哪个时刻View,这里看到,它用于我们自定义的MyView。然后declare-styleable标签里面是attr标签,这个标签里面的name属性,就是View属性名,format属性的值就是这个View属性的值的类型,比如这里format的是color,表示颜色类型。 然后 xml文件定义好之后,回到代码中,第三个构造方法里面,通过TypedArray这个类来获得自定义的属性的值,最后别忘了需要调用recycle方法。到这就可以拿到自定义View的值了,这些属性值一般是在View的measure、layout、draw过程中用到。这里还有重要的一点,就是第二个构造方法需要改为调用第三个构造方法。

      接着我们看到onDraw方法,在这方法里面,将拿到的自定义属性里面的颜色值给到画笔pain,这里注意到,在pain初始化的时候我们给到的是实心画笔。然后根据View的宽高,画出一个矩形。这就是最简单的自定义View了。把我们自定义的View应用于布局文件,就可以看到效果了。

    <com.junxu.customview.MyView
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content" />

        这里发现一个问题,那就是不管我们把MyView这个View的layout_width和layout_height的值设置成wrap_content还是match_parent,这个View都会占满全屏。不管我们把这个View放在LinearLayout还是RelativeLayout或者是放在布局文件的根结点,都是一样。 那我们需要来找一下原因,前面的系列文章,我们已经知道了,getMeasuredWidth方法获取View的测量宽度(height的情况一样),而这个测量宽度怎么来的呢?我们知道,不管父容器是哪种类型,最终都会调用我们的View的onMeasure方法,因为我们没有重写这个方法,所以最终调用的是View的onMeasure方法,在第三篇文章中,我们分析了它的源码,得出了这样的结论:不管父容器的测量模式是什么,默认情况下,子元素的测量大小,都是父容器剩余的大小。这样我们就清楚为什么会有这种情况的发生了。

        基于这种情况,那我们在重写onMeasure方法的时候,就需要处理这种情况了。如何处理呢?首先,这种情况下,View的大小是父容器剩余的空间,这在View设置layout_width(或者layout_height)为match_parent的时候,是符合要求的。所以我们需要做的就是,对于View设置layout_width(或者layout_height)为wrap_content的时候,我们需要给View一个符合要求的值,wrap_content表示要求包裹住内容,正常做法是,根据View的内容,做到把内容正确展示就好,当然有自己的需求也可以加上,总之给一个合理的大小即可。在我们这里例子中,我是直接给了一个确定值,以图方便。

     @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
            int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
            int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
            int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
            Log.d("widthMeasureSpec",""+widthMeasureSpec);
            Log.d("heightMeasureSpec",""+heightMeasureSpec);
     
            Log.d("width_mode",""+widthSpecMode);
            Log.d("width_size",""+widthSpecSize);
            Log.d("height_mode",""+heightSpecMode);
            Log.d("height_size",""+heightSpecSize);
            if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT &&
                    getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
                setMeasuredDimension(200, 200);
            }
            else if(getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT){
                setMeasuredDimension(200, heightSpecSize);
            }else if(getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT){
                setMeasuredDimension(widthSpecSize, 200);
            }else{
                setMeasuredDimension(widthSpecSize,heightSpecSize);
            }
        }

        当我重写了View的onMeasure方法之后,再将View的宽高设置为wrap_content之后,View就不再是占满全屏了。

        我们来看父容器对View的margin值和padding值的处理。如果是直接把这个View放在布局文件的根布局,那么它的父容器是DecorView,前面文章中我们了解过了DecorView的子View的measure是在FrameLayout的onMeasure中,那这时候就看FrameLayout的onMeasure方法。而如果这个View的父容器是LinearLayout或者RelativeLayout,那么就看这两种父容器对应的onMeasure方法。在三种父容器的onMeasure中,我们都看到了它们构建子View的测量规格值的时候,都处理了margin,当我们在我们的View中添加margin属性的时候,也确实生效了。但是在设置padding的时候,却没有生效,这说明了,父容器构建子View的测量规格值的时候没有考虑到子View的padding值。至于在父容器中,我们看到的以padding命名的变量值(如:mPaddingTop等),既然不是子View的padding,那我猜测可能是父容器自己的padding,因为没找到mPaddingTop等值的赋值代码,所以没有办法给出准确答案。

       由此,我们得知,在自定义View的时候,我们还得处理View的padding值。这个我是在onDraw中进行了处理,当然,在onMeasure中处理也是可以的,看个人喜好。

     @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            float width = getMeasuredWidth();
            float height = getMeasuredHeight();
            int paddingLeft = getPaddingLeft();
            int paddingTop = getPaddingTop();
            int paddingRight = getPaddingRight();
            int paddingBottom = getPaddingBottom();
            paint.setColor(mColor);
            canvas.drawRect(paddingLeft,paddingTop,width-paddingRight,height-paddingBottom,paint);
          
        }

          总结一下,其实步骤就是:

    继承自View类,重写三个构造方法,需要自定义属性的话,创建xml文件,编写新属性,然后在构造方法中获得自定义属性的值,这些自定义的属性值,根据自己的需求在measure,layout,draw过程中使用。
    重写onMeasure方法,根据自己的需求来处理layout_width和layout_height不同值的情况。
    重写onDraw方法,处理padding的值。

     在普通View的自定义中,我没有重写onLayout方法,这是用于定位的方法。其实也简单,就是根据自己的需求,看如何定位,现在没有这需求,就没做这一步,这里就不记录了。到这,普通View的自定义就讲解完成了。

 
---------------------
作者:林序
来源:CSDN
原文:https://blog.csdn.net/lin962792501/article/details/84890758
版权声明:本文为博主原创文章,转载请附上博文链接!

posted @ 2019-06-12 18:25  天涯海角路  阅读(170)  评论(0)    收藏  举报