Android 自定义View

来源:互联网 发布:传奇怪物补丁算法 编辑:程序博客网 时间:2024/06/07 10:48

自定义View

代码

public class MyView extends View {    private final static int REQUEST_DRAW = 0;    private final static int REQUEST_LAYOUT = 1;    private Paint mPaint;    private int mCount = 0;    private Rect mBounds;    //不能这样定义,因为此时Thread.sleep(1000);休眠的是主线程,所以很可能导致ANR,就算没有导致ANR,    //比如此时主界面有一个button,那么该button永远也接受不到点击事件,因为点击事件的处理也是在main线程,但是main线程一直在这里阻塞了    /*private Handler anoHandler=new Handler(){        @Override        public void handleMessage(Message msg) {            while (true) {                try {                    mCount++;                    if (mCount == 100) {                        break;                    }                    Thread.sleep(1000);                    invalidate();                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        }    };*/    private Handler handler = new Handler() {        @Override        public void handleMessage(Message msg) {            switch (msg.what) {                case REQUEST_DRAW:                    invalidate();//只会调用onDraw                    break;                case REQUEST_LAYOUT:                    requestLayout();//会重新调用整个流程,onMeasure,onLayout,onDraw                    break;                default:                    break;            }        }    };    public MyView(Context context) {        this(context, null);    }    public MyView(Context context, AttributeSet attrs) {        super(context, attrs);        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);        mPaint.setTextSize(30);        mBounds = new Rect();        new Thread() {            @Override            public void run() {                while (true) {                    try {                        mCount++;                        if (mCount == 100) {                            break;                        }                        Thread.sleep(1000);                        if (mCount == 10) {                            handler.sendEmptyMessage(REQUEST_LAYOUT);//此时变成两位数,所以需要重新测量布局                        } else {                            //注意这里不能直接调用invalidate,控件在main线程,同时控件是线程不安全的,所以不能在子线程中刷新view,所以只能通过handler发消息到main线程                            //或者可以直接使用postInvalidate();实际上postInvalidate()内部实现使用了handler,原理还是发消息到main线程,让main线程去刷新view                            handler.sendEmptyMessage(REQUEST_DRAW);//否则只是重绘view,不需要重新测量布局                        }                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                }            }        }.start();    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        int widthSize = MeasureSpec.getSize(widthMeasureSpec);        int widthMode = MeasureSpec.getMode(widthMeasureSpec);        int heightSize = MeasureSpec.getSize(heightMeasureSpec);        int heightMode = MeasureSpec.getMode(heightMeasureSpec);        String text = String.valueOf(mCount);        mPaint.getTextBounds(text, 0, text.length(), mBounds);        //文字的宽度,为了支持padding,必须加上-左右padding        //为什么不用mBounds.width()测量文字宽,而是用measureText方法,通过实际发现,mBounds.width()测量宽度的结果并不准确        int textWidth = (int) mPaint.measureText(text) + getPaddingLeft() + getPaddingRight();        //文字的高度,为了支持padding,必须加上-上下padding        int textHeigth = mBounds.height() + getPaddingTop() + getPaddingBottom();        //如果设置了背景图片或者view中设置了android:minWidth,android:minHeight属性,那么defWidth等于getSuggestedMinimumWidth/getSuggestedMinimumHeight        //否则就是文字的宽度/高度,注意为了支持padding,宽度必须加上上下左右padding        int defWidth = getSuggestedMinimumWidth() == 0 ? textWidth : getSuggestedMinimumWidth();        int defHeight = getSuggestedMinimumHeight() == 0 ? textHeigth : getSuggestedMinimumHeight();        //如果是EXACTLY,设置了具体数值/match_parent,就使用父view传递下来的widthSize/heightSize,否则如果是wrap_content,AT_MOST就使用defWidth/defHeight        int width = (widthMode == MeasureSpec.EXACTLY) ? widthSize : defWidth;        int heigth = (heightMode == MeasureSpec.EXACTLY) ? heightSize : defHeight;        setMeasuredDimension(width, heigth);        //实际上以上的实现,基本跟这句等效,只是如果view设置的是wrap_content,模式为AT_MOST,那么最后view的大小得到的将是父view传递下来的大小        //而上面的代码中对wrap_content,模式为AT_MOST进行了自己的实现        //super.onMeasure(widthMeasureSpec, heightMeasureSpec);    }    @Override    protected void onDraw(Canvas canvas) {        //super.onDraw(canvas); 因为父类view的onDraw没有任何实现,如果继承自button等,就必须调用super.onDraw(canvas)        mPaint.setColor(Color.BLUE);        mPaint.setStyle(Paint.Style.FILL);        //画一个长方新蓝色背景        canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);        String text = String.valueOf(mCount);        mPaint.setColor(Color.YELLOW);        mPaint.getTextBounds(text, 0, text.length(), mBounds);        int textWidth = (int) mPaint.measureText(text);        int textHeigth = mBounds.height();        //在view的中间画数字        canvas.drawText(text, getMeasuredWidth() / 2 - textWidth / 2, getMeasuredHeight() / 2 + textHeigth / 2, mPaint);    }}


效果

<lbb.mytest.demo.MyView        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_gravity="center"        android:minHeight="50dp"        android:minWidth="50dp"/>
实际显示:中间的数字一直在更新。

此时width设置为wrap_content,但是设置了minWidth。所以该view最后的大小就是50dp

 <lbb.mytest.demo.MyView        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_gravity="center"/>
实际显示:中间的数字一直在更新

此时width设置为wrap_content,但是没有设置背景图片也没有设置minWidth,同时没有设置padding,所以最后宽度就是文字的实际宽度,在数字累加到10的时候,会重新测量一次。



invalidate:  刷新view,调用OnDraw,不可以在子线程中调用
postInvalidate:刷新view,调用OnDraw,可以在子线程中直接刷新,内部使用handler发消息到main线程
requestLayout:  重新测量布局绘制view,调用过程onMeasure,onLayout,onDraw


自定义ViewGroup

代码

对《Android 手把手教您自定义ViewGroup》  中的例子做了优化,让它支持margin,同时增加了自身的绘制过程。    

package lbb.mytest.demo;import android.content.Context;import android.graphics.Canvas;import android.graphics.Paint;import android.util.AttributeSet;import android.view.View;import android.view.ViewGroup;/** * Created by liaobinbin on 2015/10/19. */public class MyViewGroup extends ViewGroup {    private Paint paint;    public MyViewGroup(Context context) {        this(context, null);    }    public MyViewGroup(Context context, AttributeSet attrs) {        super(context, attrs);        paint = new Paint();        //setPadding(0, 0, 0, 0);//忽略padding的影响    }    //为了支持margin,默认情况下ViewGroup的generateLayoutParams方法返回的是LayoutParams对象,只能获取layout_width和layout_height。    //但是MarginLayoutParams(context,attrs)会额外获取margin参数,padding默认是支持的。    //generateLayoutParams在inflate过程中被调用,未每个子view生成layoutparam。setContentView内部其实也是调用inflate函数的    @Override    public LayoutParams generateLayoutParams(AttributeSet attrs) {        return new MarginLayoutParams(getContext(), attrs);    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        int widthSize = MeasureSpec.getSize(widthMeasureSpec);        int widthMode = MeasureSpec.getMode(widthMeasureSpec);        int heightSize = MeasureSpec.getSize(heightMeasureSpec);        int heightMode = MeasureSpec.getMode(heightMeasureSpec);        //必须先手动测量一次viewgroup内所有子view,否则子view的getMeasuredWidth/getMeasuredHeight都是0        measureChildren(widthMeasureSpec, heightMeasureSpec);        int width = 0;        if (widthMode == MeasureSpec.EXACTLY) {            width = widthSize;//如果设置了具体数值或者使用了match_parent,那么使用父控件传下来的size        } else { //如果是wrap_content(AT_MOST),那么就自己计算width            int tWidth = 0, bWidth = 0;            for (int i = 0; i < getChildCount(); i++) {                View view = getChildAt(i);                MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();                if (i == 0 || i == 1) {                    tWidth += view.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;                } else {                    bWidth += view.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;                }            }            width += getPaddingLeft() + getPaddingRight() + Math.max(tWidth, bWidth);//上下排最大值+padding作为width            if (width > widthSize) {//如果计算出来的值比父控件给的size还要大,那就说明已经超过了最大值。                width = widthSize;            }        }        int height = 0;        if (heightMode == MeasureSpec.EXACTLY) {            height = heightSize;        } else {            int lHeight = 0, rHeight = 0;            for (int i = 0; i < getChildCount(); i++) {                View view = getChildAt(i);                MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();                if (i == 0 || i == 2) {                    lHeight += view.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;                } else {                    rHeight += view.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;                }            }            height += getPaddingTop() + getPaddingBottom() + Math.max(lHeight, rHeight);            if (height > heightSize) {                height = heightSize;            }        }        setMeasuredDimension(width, height);    }    @Override    protected void onLayout(boolean changed, int l, int t, int r, int b) {        int startX = 0, startY = 0;        for (int i = 0; i < getChildCount(); i++) {            View view = getChildAt(i);            MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();//取得子view的layoutparam参数            switch (i) {                case 0:                    startX = getPaddingLeft() + lp.leftMargin;                    startY = getPaddingTop() + lp.topMargin;                    break;                case 1:                    startX = getMeasuredWidth() - getPaddingRight() - lp.rightMargin - view.getMeasuredWidth();                    startY = getPaddingTop() + lp.topMargin;                    break;                case 2:                    startX = getPaddingLeft() + lp.leftMargin;                    startY = getMeasuredHeight() - getPaddingBottom() - lp.bottomMargin - view.getMeasuredHeight();                    break;                case 3:                    startX = getMeasuredWidth() - getPaddingRight() - lp.rightMargin - view.getMeasuredWidth();                    startY = getMeasuredHeight() - getPaddingBottom() - lp.bottomMargin - view.getMeasuredHeight();                    break;                default:                    break;            }            view.layout(startX, startY, startX + view.getMeasuredWidth(), startY + view.getMeasuredHeight());        }    }    //测量,布局完毕之后,就到了绘制过程    @Override    protected void onDraw(Canvas canvas) {        //super.onDraw(canvas);        paint.setColor(getResources().getColor(android.R.color.darker_gray));        paint.setStyle(Paint.Style.FILL);        canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), paint);//在viewgroup的最外层绘制一个长方形作为背景        paint.setColor(getResources().getColor(R.color.background));//#FF00FFE1        paint.setStyle(Paint.Style.FILL);        int left = getPaddingLeft();        int right = getMeasuredWidth() - getPaddingRight();        int top = getPaddingTop();        int buttom = getMeasuredHeight() - getPaddingBottom();        canvas.drawRect(left, top, right, buttom, paint);//viewgroup去掉padding绘制一个长方形作为背景    }}
主界面

<?xml version="1.0" encoding="utf-8"?><LinearLayout    xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:orientation="vertical"    android:padding="5dp">    <lbb.mytest.demo.MyViewGroup        android:layout_width="match_parent"        android:layout_height="300dp"        android:layout_marginTop="10dp"        android:background="#00000000"        android:padding="30dp">        <Button            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:layout_margin="10dp"            android:background="#fff944"            android:text="11"/>        <Button            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:background="#00ff00"            android:text="22"/>        <Button            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:background="#ff0000"            android:text="11"/>        <lbb.mytest.demo.MyView            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:padding="10dp"/>    </lbb.mytest.demo.MyViewGroup></LinearLayout>


实际显示,MyView支持padding,MyViewGroup完美支持margin

MyViewGroup中measureChildren(widthMeasureSpec, heightMeasureSpec);会去测量每个子view,所以当测量myview的时候,自然会去执行myView的onMeasure,这个时候测量出来的结果自然是文字的宽高加上padding了。


分析

1、ViewGroup的职责是啥?

ViewGroup相当于一个放置View的容器,并且我们在写布局xml的时候,会告诉容器(凡是以layout为开头的属性,都是为用于告诉容器的),我们的宽度(layout_width)、高度(layout_height)、对齐方式(layout_gravity)等;当然还有margin等;于是乎,ViewGroup的职能为:给childView计算出建议的宽和高和测量模式 ;决定childView的位置;为什么只是建议的宽和高,而不是直接确定呢,别忘了childView宽和高可以设置为wrap_content,这样只有childView才能计算出自己的宽和高。

2、View的职责是啥?

View的职责,根据测量模式和ViewGroup给出的建议的宽和高,计算出自己的宽和高;同时还有个更重要的职责是:在ViewGroup为其指定的区域内绘制自己的形态。

3、ViewGroup和LayoutParams之间的关系?

大家可以回忆一下,当在LinearLayout中写childView的时候,可以写layout_gravity,layout_weight属性;在RelativeLayout中的childView有layout_centerInParent属性,却没有layout_gravity,layout_weight,这是为什么呢?这是因为每个ViewGroup需要指定一个LayoutParams,用于确定支持childView支持哪些属性,比如LinearLayout指定LinearLayout.LayoutParams等。如果大家去看LinearLayout的源码,会发现其内部定义了LinearLayout.LayoutParams,在此类中,你可以发现weight和gravity的身影。


首先可以去参考《Android View绘制流程》《Android LayoutInflater原理分析

1. 为什么要去重写generateLayoutParams方法才能支持margin?

因为不管viewgroup通过inflate加载还是setcontentview(内部其实还是通过inflate)加载,都会去解析xml中的资源,然后会为每个view设置它的layoutparam,这个layoutparam是通过它的父viewgroup调用generateLayoutParams方法得到的,默认情况下这个方法返回的是LayoutParams对象,只能获取layout_width,layout_height。如果要支持margin,那么generateLayoutParams方法就必须返回MarginLayoutParams对象。

ViewGroup.LayoutParams params = null;params = root.generateLayoutParams(attrs);if (!attachToRoot) {temp.setLayoutParams(params);        }//inflate的是根View,也是generateLayoutParams这个带attrs参数的重载方法。

    final View view = createViewFromTag(parent, name, context, attrs);    final ViewGroup viewGroup = (ViewGroup) parent;    final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);    rInflateChildren(parser, view, attrs, true);    viewGroup.addView(view, params);
在inflate视图的时候,递归解析子视图时,调用了viewgroup的generateLayoutParams(attrs)方法,并把该layoutparam绑定给了该view

public LayoutParams generateLayoutParams(AttributeSet attrs) {return new LayoutParams(getContext(), attrs);}public LayoutParams(Context c, AttributeSet attrs) {                       TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);                       setBaseAttributes(a,R.styleable.ViewGroup_Layout_layout_width,R.styleable.ViewGroup_Layout_layout_height);                       a.recycle();                }
默认情况下LayoutParams对象只会获取,layout_width和layout_height这两个参数。


    @Override    public LayoutParams generateLayoutParams(AttributeSet attrs) {        return new MarginLayoutParams(getContext(), attrs);    }    public MarginLayoutParams(Context c, AttributeSet attrs) {            super();            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout);            setBaseAttributes(a,                    R.styleable.ViewGroup_MarginLayout_layout_width,                    R.styleable.ViewGroup_MarginLayout_layout_height);            int margin = a.getDimensionPixelSize(                    com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1);            if (margin >= 0) {                leftMargin = margin;                topMargin = margin;                rightMargin= margin;                bottomMargin = margin;            }.............}
所以如果要能获取margin参数,那么必须重写generateLayoutParams方法

另一方面为什么LinearLayout等支持,因为他们实现了自己的测量过程,已经在测量过程中计算了margin。可以看到LinearLayout的内部类LayoutParams

public static class LayoutParams extends ViewGroup.MarginLayoutParams。继承了MarginLayoutParams所以可以获取margin


2. 为什么说默认是支持padding的?

viewgroup测量循环测量子view大小,measureChildren(widthMeasureSpec, heightMeasureSpec);可以看到里面把padding考虑进去了

    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);            }        }    }    /**     * Ask one of the children of this view to measure itself, taking into     * account both the MeasureSpec requirements for this view and its padding.     * The heavy lifting is done in getChildMeasureSpec.     *     * @param child The child to measure     * @param parentWidthMeasureSpec The width requirements for this view     * @param parentHeightMeasureSpec The height requirements for this view     */    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);    }

可以看到measureChild中使用了padding,其实也是用了layoutparam,但是layoutparam只取了了layout_width和layout_height,当然不支持margin了。


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://如果viewgroup是EXACTLY            if (childDimension >= 0) { //如果子view设置的是具体数值                resultSize = childDimension; //具体数值                resultMode = MeasureSpec.EXACTLY; //EXACTLY            } else if (childDimension == LayoutParams.MATCH_PARENT) { //如果子view设置的是MATCH_PARENT                // Child wants to be our size. So be it.                resultSize = size; //父view的大小                resultMode = MeasureSpec.EXACTLY; //EXACTLY            } else if (childDimension == LayoutParams.WRAP_CONTENT) { //如果子view设置的是WRAP_CONTENT                // Child wants to determine its own size. It can't be                // bigger than us.                resultSize = size; //父view的大小                resultMode = MeasureSpec.AT_MOST; //AT_MOST            }            break;        // Parent has imposed a maximum size on us        case MeasureSpec.AT_MOST: //如果viewgroup是AT_MOST            if (childDimension >= 0) { //如果子view设置的是具体数值                // Child wants a specific size... so be it                resultSize = childDimension;  //具体数值                resultMode = MeasureSpec.EXACTLY; //EXACTLY            } else if (childDimension == LayoutParams.MATCH_PARENT) { //如果子view设置的是MATCH_PARENT                // Child wants to be our size, but our size is not fixed.                // Constrain child to not be bigger than us.                resultSize = size; //父view的大小                resultMode = MeasureSpec.AT_MOST;  //AT_MOST            } else if (childDimension == LayoutParams.WRAP_CONTENT) { //如果子view设置的是WRAP_CONTENT                // Child wants to determine its own size. It can't be                // bigger than us.                resultSize = size; //父view的大小                resultMode = MeasureSpec.AT_MOST; //AT_MOST            }            break;}    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));    }    //现在测量子view了,其实viewgroup自身的大小也是这里的。        protected int getSuggestedMinimumWidth() {        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());//背景图片大小    }        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; //如果是UNSPECIFIED,那么使用的getSuggestedMinimumWidth,很可能就是背景图片大小了。            break;        case MeasureSpec.AT_MOST:        case MeasureSpec.EXACTLY:            result = specSize;  //说明如果是AT_MOST,EXACTLY那么最终的结果是从measureSpec中拿到的。            break;        }        return result;    }


可以看到如果是UNSPECIFIED,那么View最终的大小将是背景图片宽高和android:minWidth/android:minHeight之间的最大值

protected int getSuggestedMinimumWidth() {        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());    }

                case R.styleable.View_minWidth:                    mMinWidth = a.getDimensionPixelSize(attr, 0);                    break;                case R.styleable.View_minHeight:                    mMinHeight = a.getDimensionPixelSize(attr, 0);



0 0