聊聊自定义View那些事

聊聊自定义View那些事

    • 为什么要自定义View
    • 如何自定义View
      • 为什么要重写onMeasure方法
      • 如何重写onMeasure方法
    • 总结

为什么要自定义View

  • 为了实现某些炫酷的动画和效果。
  • 为了优化应用性能。
  • 系统控件满足不了需要

如何自定义View

  1. 继承系统View,实现相应的构造方法
public class CircleView extends View {

    private Paint mPaint;

    private float cx;
    private float cy;
	//这个是外部通过new 的方式调用
    public CircleView(Context context) {
        super(context);
    }
	//这个是外部通过xml 的方式调用
    public CircleView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }
    ...
}
  1. 重写onDraw方法,因为这个方法是你把所有东西绘制上去,实现自定义View的基础。
	private void init() {
        mPaint = new Paint();
        //抗锯齿
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(5);
    }

    @Override
    protected void onDraw(Canvas canvas) {
    	//super内部空实现
        super.onDraw(canvas);
        cx = getMeasuredWidth()/2;
        cy = getMeasuredHeight()/2;
        float radius = Math.min(cx,cy);
        radius = radius-5;
        //画了个圆
        canvas.drawCircle(cx,cy,radius,mPaint);
    }

为什么要重写onMeasure方法

  1. 配置到xml中,run一下,看下效果,你会发现这个圆出来了。但是超级大,那么系统是如何控制View的大小的呢?
 <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <com.okay.myapplication.CircleView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    </FrameLayout>
  1. 我们重写onMeasure方法,看看父类究竟干了些什么工作?
 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        Log.d("CircleView", MeasureSpec.getSize(widthMeasureSpec)+":"+ MeasureSpec.getSize(heightMeasureSpec));
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

这里先留下个问题,就是onMeasure方法走了两次,why?
先不管那个,我们看下源码是怎么帮我们测量view的宽高的。

  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
    /**
     * @return The suggested minimum width of the view.
     * 得到最小的宽度,如果设置了Background,取最小宽度和背景的最小宽度之中的最大值
     */
    protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

/**
     * Utility to return a default size. Uses the supplied size if the
     * MeasureSpec imposed no constraints. Will get larger if allowed
     * by the MeasureSpec.
     *
     * @param size Default size for this view
     * @param measureSpec Constraints imposed by the parent
     * @return The size this view should be.
     */
    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        //不能确定子view的大小
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        //最大能给子孩子的大小尺寸,也就是父控件的最大尺寸specSize
        case MeasureSpec.AT_MOST:
        //子孩子在xml里写的尺寸是固定的,android:layout_width="50dp"
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }
  1. 研究下widthMeasureSpec和heightMeasureSpec这两个参数是怎么来的?
    经过追代码,发现ViewGroup里面measureChild方法里面调用了view的measure,传进来参数。
 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);
    }

getChildMeasureSpec关键方法,负责childWidthMeasureSpec值的拼装,lp.width就是childDimension,也就是我们在xml里android:layout_width写的值。

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

  • widthMeasureSpec和heightMeasureSpec是父控件传给子控件的测量规则和大小,测量规则和大小是根据父控件测量规则和子孩子在xml写的属性值来共同决定的。
  • 当View采用固定宽/高时( 即设置固定的dp/px) ,不管父容器的MeasureSpec是什么,View的MeasureSpec都是EXACTLY模式,并且大小遵循我们设置的值。
  • 当View的宽/高是 match_parent 时,View的MeasureSpec都是EXACTLY模式并且其大小等于父容器的剩余空间。
  • 当View的宽/高是 wrap_content 时,View的MeasureSpec都是AT_MOST模式并且其大小不能超过父容器的剩余空间。
  • 父容器的UNSPECIFIED模式,一般用于系统内部多次Measure时,表示一种测量的状态,一般来说我们不需要关注此模式。
  1. 因为我们自定义view的宽高可能需要一个最小值来显示,来绘制我们的图形,但是当我们调用者没在xml写具体数值,就会导致我们宽度和高度是父布局的大小,以至于显示异常。所以我们要重写onMeasure方法,来根据自己的需求,设置子view的宽高。

如何重写onMeasure方法

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        Log.d("CircleView", MeasureSpec.getSize(widthMeasureSpec)+":"+ MeasureSpec.getSize(heightMeasureSpec));
        Log.d("CircleView", MeasureSpec.getMode(widthMeasureSpec)+":"+ MeasureSpec.getMode(heightMeasureSpec));
//        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(getSize(50,widthMeasureSpec),getSize(50,heightMeasureSpec));
        Log.d("CircleView", getMeasuredWidth()+":"+ getMeasuredHeight());
    }

    public static int getSize(int minSize, int measureSpec) {
        int result = minSize;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
            // Parent asked to see how big we want to be
            case MeasureSpec.UNSPECIFIED:
            // Parent has imposed a maximum size on us
            case MeasureSpec.AT_MOST:
                result = Math.min(minSize,specSize);
                break;
            //Parent has imposed an exact size on us
            case MeasureSpec.EXACTLY:
            default:
                result = specSize;
                break;
        }
        return result;
    }

总结

  • 实践去吧,多写几个自定义view就会了。

你可能感兴趣的:(android,android,移动开发)