Android 自定义View

自定义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
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值