Android系统提供了丰富的视图组件,如TextView、ImageView、Button等,以及一些容器组件,如ConstraintLayout、LinearLayout、FrameLayout等,使用这些组件可以实现各种常见的视图效果。然而,在某些情况下,我们可能需要实现更加个性化和独特的展示效果,而系统提供的组件很难满足这种需求。这时候,就需要使用自定义View来实现了。
自定义View的实现流程大致分为以下几方面:
- 创建自定义View类继承自View/现有控件类
- View的测量过程onMeasure
- View的绘制过程onDraw
下面将依序实现各流程
创建自定义View类
继承View创建一个自定义View类
自定义View类可以直接继承自View实现,也可以根据业务场景直接继承自特定的控件。
重写构造方法
1 2 3 4 5 6 7 8 9 10 11
| public class MyView extends View { public MyView(Context context) { super(context, null); init(context); }
public MyView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(context); } }
|
定义View的自定义属性
自定义属性属于styleable,这样我们就可以在xml布局文件中直接声明自定义View的属性来增加View的灵活性
1 2 3 4 5 6 7 8
| <?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="MyView"> <attr name="MyText" format="string|reference" /> <attr name="MySize" format="dimension|reference" /> </declare-styleable> </resources>
|
使用自定义属性
1 2 3 4 5 6 7 8 9 10 11 12
| private void init(Context context, AttributeSet attrs, int defStyleAttr) { TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyView, defStyleAttr, 0);
text = typedArray.getString(R.styleable.MyView_customText); dimension = typedArray.getDimension(R.styleable.MyView_customSize, 30);
typedArray.recycle(); }
|
自定义View的测量:onMeasure
MeasureSpec
MeasureSpec是View的内部类,它封装了一个View的尺寸,在onMeasure()当中会根据这个MeasureSpec的值来确定View的宽高。
MeasureSpec的值保存在一个int值当中。一个int值有32位,前两位表示模式mode后30位表示大小size。即MeasureSpec =mode + size。
在MeasureSpec当中一共存在三种mode:UNSPECIFIED、EXACTLY 和 AT_MOST。
MeasureSpec.EXACTLY
在布局中指定了宽高 是一个确切的值(比如100dp 可以直接使用)对应match_parent。
MeasureSpec.ATMOST
AT_MOST是最大的意思,这时需要测量自己控件需要的的宽高,如果大于父控件的,那么就用父控件的大小,如果小于父控件,就使用父控件大小 总结下来就是尽可能地给最大 但是不能超过父控件。对应wrap_content。
MeasureSpec.UNSPECIFIED
表示尽可能大的显示在listview和scrollview中就是这样测量宽高的,因为是滚动控件,一般自己定义时很少用到 看scrollview的源码会看到它的模式是获取最大的显示方式。
通常,可以使用MeasureSpec类的getSize()和getMode()方法来获取宽度和高度的建议值以及测量模式。调用setMeasuredDimension()方法将计算出的宽高传入。
1 2 3 4 5 6
| int specMode = MeasureSpec.getMode(measureSpec)
int specSize = MeasureSpec.getSize(measureSpec)
int measureSpec=MeasureSpec.makeMeasureSpec(size, mode);
|
在View当中,MeasureSpace的测量代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| 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) { case MeasureSpec.EXACTLY: if (childDimension >= 0) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { resultSize = size; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; case MeasureSpec.AT_MOST: if (childDimension >= 0) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { resultSize = size; resultMode = MeasureSpec.AT_MOST; } else if (childDimension == LayoutParams.WRAP_CONTENT) { resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; case MeasureSpec.UNSPECIFIED: if (childDimension >= 0) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } break; } return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
|
自定义View的绘制:onDraw
draw流程也就是把View绘制到屏幕上的过程,整个流程的入口在View的draw()方法之中,而源码注释也写的很明白,整个过程可以分为6个步骤。
- 如果需要,绘制背景。
- 有过有必要,保存当前canvas。
- 绘制View的内容。
- 绘制子View。
- 如果有必要,绘制边缘、阴影等效果。
- 绘制装饰,如滚动条等等。
自定义控件被创建并且测量代码写好后,接下来就调用onDraw()来绘制View。以下是一个绘制圆形的示例:
1 2 3 4 5 6 7 8 9
| @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); int r = getMeasuredWidth()/2; int centerX = getLeft() + r; int centerY = getTop() + r; canvas.drawCircle(centerX,centerY,r,mPaint);
|