聊聊自定义View那些事
16lz
2021-01-26
聊聊自定义View那些事
- 为什么要自定义View
- 如何自定义View
- 为什么要重写onMeasure方法
- 如何重写onMeasure方法
- 总结
为什么要自定义View
- 为了实现某些炫酷的动画和效果。
- 为了优化应用性能。
- 系统控件满足不了需要
如何自定义View
- 继承系统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); } ...}
- 重写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方法
- 配置到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>
- 我们重写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; }
- 研究下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时,表示一种测量的状态,一般来说我们不需要关注此模式。
- 因为我们自定义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(安卓)Studio--快捷键
- 反编译apk文件,得到其源代码的方法
- Android(安卓)学习之Camera拍照流程
- Android(安卓)RecyclerView与ListView局部刷新
- Android本地存储数据方法(超简单)
- Android之FastJSon基本使用,属性首字母大小写的问题
- 在android中通过JNI调用本地方法
- 使用Canvas的drawTextOnPath方法实现沿着Path绘制文本
- Android点击事件分发机制源码分析1——Activity