View的工作流主要是指measure、layout、draw,即测量、布局和绘制。
- measure确定View的测量宽/高
- layout确定View最终的宽高和四个顶点的位置。
- draw则将View绘制到屏幕上
案例:做一个圆形的控件
public class CircleView extends View {
private int mColor = Color.RED;
//设置画笔
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
public CircleView(Context context) {
super(context);
initView();
}
public CircleView(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
private void initView() {
mPaint.setColor(mColor);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int paddingRight = getPaddingRight();
int paddingLeft = getPaddingLeft();
int paddingBottom = getPaddingBottom();
int paddingTop = getPaddingTop();
//获取控件的宽和高
int width = getWidth() - paddingRight - paddingLeft;
int height = getHeight() - paddingBottom - paddingTop;
//获取控件宽和高中最小值,然后除以2,得到控件的半径
int radiu = Math.min(width, height) / 2;
//绘制View
canvas.drawCircle(width / 2 + paddingLeft, height / 2 + paddingTop, radiu, mPaint);
}
}
上述代码是自定义的红色的圆形控件。但是有两个问题:
问题一:
<zx.demo.zx_circle_view.view.CircleView
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
<zx.demo.zx_circle_view.view.CircleView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
两种布局的效果一样,都是第一种的match_parent效果,那么怎样设定一个wrap_content效果?
解决方法:
想要解决这个问题,那么就需要引入MeasureSpec。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST
&& heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(500, 500);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(500, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, 500);
}
}
问题二:
怎样添加自定义属性,比如添加控件选择控件的颜色。
解决方法:
第一步、在values目录下创建自定义属性的XML。
第二步、在View的构造方法中解析自定义属性的值并做相应处理。
public CircleView(Context context, AttributeSet attrs) {
super(context, attrs);
//加载自定义属性集合CircleView
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
//解析CircleView中的circle_color属性,如果默认情况下为红色
mColor = typedArray.getColor(R.styleable.CircleView_circle_color, Color.RED);
//释放资源
typedArray.recycle();
initView();
}
第三步、在布局文件中使用自定义属性
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
<zx.demo.zx_circle_view.view.CircleView
android:layout_width="300dp"
android:layout_height="300dp"
app:circle_color="@color/colorPrimary"
/>
</RelativeLayout>
注意:xmlns:app=”http://schemas.android.com/apk/res-auto”
app:自定义属性的前缀,可以换其他名字,但是CircleView中的自定义属性的前缀必须和它一致。
还有一种声明方式是:xmlns:app=”http://schemas.android.com/apk/res/zx.demo.zx_circle_view”,在apk/res/后面加上包名。
MeasureSpec理解:
第二个问题的解决很好理解,但是第一个问题解决方案中的MeasureSpec是什么东西呢?
MeasureSpec代表一个32位int值,其中最高的2位代表SpecMode,后面的30为代表SpecSize。
一个MeasureSpec封装了父布局传递给子布局的布局要求,每个MeasureSpec代表了一组宽度和高度的要求,一个MeasureSpec由大小和模式组成,它有三种模式:
- UNSPECIFIED(未指定):父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量状态。
- EXACTLY(完全):父容器已经检测到View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这两种模式。
- AT_MOST(至多):父容器指定了一个可用大小即SpecSize,View的的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应于LayoutParams中的wrap_content。