聊聊自定义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就会了。

更多相关文章

  1. Android(安卓)Studio--快捷键
  2. 反编译apk文件,得到其源代码的方法
  3. Android(安卓)学习之Camera拍照流程
  4. Android(安卓)RecyclerView与ListView局部刷新
  5. Android本地存储数据方法(超简单)
  6. Android之FastJSon基本使用,属性首字母大小写的问题
  7. 在android中通过JNI调用本地方法
  8. 使用Canvas的drawTextOnPath方法实现沿着Path绘制文本
  9. Android点击事件分发机制源码分析1——Activity

随机推荐

  1. Android(安卓)5.0后图片报错:libpng warni
  2. Android(安卓)中ImageView的ScaleType使
  3. 居中总结
  4. android adb shell:unknown host service
  5. flutter 集成极光推送(有图)
  6. Android(13)——RecyclerView列表流行控
  7. 关于android 图片加载优化
  8. 利用Android自带的http包进行网络请求
  9. android 遇到Error:Execution failed for
  10. 如何解决向eclipse导入android project时