这种方法主要用于实现一些不规则的效果,一般需要重写onDraw()
。采用这种方法需要自己支持wrap_content
,并且padding
也需要自己处理。
下面记录Android 开发艺术探索一书中的“自定义圆”示例的完成步骤。在自己的中心点,以宽/高的最小值为直径绘制一个红色的实心圆。
1. 重写 onDraw()
(1)重写原因
View 的 draw 过程中,在onDraw()
中完成 View 自身内容的绘制,由于每个 View 的内容各不相同,所以在View
类中该方法是一个空实现。自定义 View 时,需子类复写onDraw()
,从而完成自身内容的绘制。
(2)具体实现
创建类CircleView
继承View
类,重写onDraw()
,下面是CircleView#onDraw()
的实现。
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
/**
* 在该方法中绘制View自身内容
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
// 半径
int radius = Math.min(width, height) / 2;
// 画圆
canvas.drawCircle(width / 2, height / 2, radius, mPaint);
}
2. 考虑 margin 属性
margin 属性由父容器控制,因此不需要在CircleView
中做特殊处理
3. 考虑 padding 属性
(1)padding 默认无法生效原因
直接继承自 View 和 ViewGroup 的控件,padding 默认是无法生效的,需要自己处理。
(2)处理 padding
针对padding
的问题,也很简单,只要在绘制时考虑一下padding
即可,在绘制时考虑 View 四周的 padding,从而做相应的调整。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
// getWidth():
int width = getWidth() - paddingLeft - paddingRight;
// getHeight():
int height = getHeight() - paddingTop - paddingBottom;
int radius = Math.min(height, width) / 2;
canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2, radius, mPaint);
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.myapplication.diyview.MyCircle
android:layout_width="match_parent"
android:layout_margin="20dp"
android:padding="20dp"
android:background="#000000"
android:id="@+id/diyview_activity_layout_circle"
android:layout_height="100dp" />
</LinearLayout>


4. 考虑 wrap_content 属性
(1)问题描述
自定义 View 时,View 宽/高的wrap_content
属性起不到自身应有的作用,而是起到和match_parent
相同的作用。
-
wrap_content
预期作用
视图的宽/高被设定成刚好适应视图内容的最小尺寸 -
match_parent
视图的宽/高被设定成充满父布局
(2)问题分析
从 View 的源码来看,在自定义 View 不进行任何处理时,
wrap_content
属性起到的作用就是和match_parent
相同。我们对系统控件使用wrap_content
属性有效果,其实还是控件内部处理过了。
对单一 View 的测量过程通过调用View
类的measure()
进行,在measure()
之前要得到测量规格MeasureSpec
,其中普通 View (顶层 View 的暂且先不分析)的MeasureSpec
的得到和父容器的MeasureSpec
和 View 本身的布局参数LayoutParams
相关,具体见下表。

从上表可知
-
当 View 的布局参数为
wrap_content
时
得到的 View 的测量模式都为AT_MOST
,测量规格为parentSize
,即父布局剩余空间 -
当 View 的布局参数为
match_parent
时
得到的 View 的测量模式为AT_MOST
或EXACTLY
,测量规格为parentSize
,即父布局剩余空间
所以说,在自定义 View 不做任何处理,默认的情况下,设置 View 宽/高的
wrap_content
是起不到应有的作用,而是起到和match_parent
相同的效果,即都是充满父容器的剩余尺寸(parentSize
)。
(3)解决方案
为了让wrap_content
属性起到应有效果,自定义 View 时需重写onMeasure()
,在该方法中对该属性做特殊处理。只需要指定一个wrap_content
模式的默认宽/高即可,比如选择 400px 作为默认的宽高。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 获取宽-测量规格的模式和大小
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
// 获取高-测量规格的模式和大小
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 设置wrap_content的默认宽 / 高值
// 默认宽/高的设定并无固定依据,根据需要灵活设置
// 类似TextView,ImageView等针对wrap_content均在onMeasure()对设置默认宽 / 高值有特殊处理,具体读者可以自行查看
int mWidth = 400;
int mHeight = 400;
if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT && getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
setMeasuredDimension(mWidth, mHeight);
} else if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {
setMeasuredDimension(mWidth, heightSize);
} else if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
setMeasuredDimension(widthSize, mHeight);
}
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.myapplication.diyview.MyCircle
android:layout_width="wrap_content"
android:layout_margin="20dp"
android:padding="20dp"
android:background="#000000"
android:id="@+id/diyview_activity_layout_circle"
android:layout_height="100dp" />
</LinearLayout>

具体问题具体分析,这里的固定宽高也可以通过函数调用来实现。这里固定宽高是为了便于分析,如果是自定义 TextView ,那么只需要根据文字的宽高计算控件的宽高,替代这里的固定宽高。
5. 提供自定义属性
(1)在 values 目录下创建自定义属性的 xml 文件
- xml 文件名随意取,没什么限制。
- 用
<declare-styleable>
声明自定义属性集CircleView
在这个集合中定义自定义属性(自定义属性需指定name
和format
两个属性),自定义属性的format
有多种,可查阅文档。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--自定义属性集-->
<declare-styleable name="CircleView">
<!--自定义属性-->
<attr name="circle_color" format="color"/>
</declare-styleable>
</resources>
(2)在 View 的构造函数中解析自定义属性的值并做相应处理
- 加载自定义属性集合
- 解析属性集合中的自定义属性
- 将自定义属性应用到 View
private int mColor = Color.RED;
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 加载自定义属性集合
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
// 解析自定义属性:自定义属性的ID为CircleView_circle_color,即属性集合名_自定义属性名
mColor = typedArray.getColor(R.styleable.CircleView_circle_color, Color.RED);
// 解析完自定义属性后,释放资源
typedArray.recycle();
init();
}
private void init() {
mPaint.setColor(mColor);
}
(3)在布局文件中使用自定义属性
- 为了使用自定义属性,需要在布局文件中添加
schemas
声明
xmlns:app="http://schemas.android.com/apk/res-auto"
app:是自定义属性的前缀,当然也可以换成其他名字,然后就可以在布局文件中使用自定义属性了。
- 在布局文件中使用自定义属性
app:circle_color="@color/colorPrimary"
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.whut.mytest.diy_view.CircleView
android:layout_width="match_parent"
android:layout_height="100dp"
app:circle_color="@color/colorPrimary"/>
</LinearLayout>

参考文献
手把手教你写一个完整的自定义View
Android开发艺术探索
完整代码
1.CircleView
public class CircleView extends View {
private static final String TAG = "CircleView";
// 在自己的中心点,以宽/高的最小值为直径绘制一个红色的实心圆
private int mColor = Color.RED;
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
public CircleView(Context context) {
super(context);
init();
}
public CircleView(Context context, AttributeSet attrs) {
// 若使用自定义属性的话,此函数调用记得修改,原来是:super(context,attrs);
this(context, attrs, 0);
init();
}
public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 加载自定义属性集合
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
// 解析自定义属性:自定义属性的ID为CircleView_circle_color,即属性集合名_自定义属性名
mColor = typedArray.getColor(R.styleable.CircleView_circle_color, Color.BLUE);
Log.d(TAG, "CircleView()::mColor=" + mColor);
// 解析完自定义属性后,释放资源
typedArray.recycle();
init();
}
private void init() {
mPaint.setColor(mColor);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
// getWidth():
int width = getWidth() - paddingLeft - paddingRight;
// getHeight():
int height = getHeight() - paddingTop - paddingBottom;
int radius = Math.min(height, width) / 2;
canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2, radius, mPaint);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 获取宽-测量规格的模式和大小
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
// 获取高-测量规格的模式和大小
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 设置wrap_content的默认宽 / 高值
// 默认宽/高的设定并无固定依据,根据需要灵活设置
// 类似TextView,ImageView等针对wrap_content均在onMeasure()对设置默认宽 / 高值有特殊处理,具体读者可以自行查看
int mWidth = 400;
int mHeight = 400;
if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT &&
getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
setMeasuredDimension(mWidth, mHeight);
} else if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {
setMeasuredDimension(mWidth, heightSize);
} else if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
setMeasuredDimension(widthSize, mHeight);
}
}
}
2.自定义属性
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleView">
<!--声明自定义属性集合CirCleView-->
<attr name="circle_color" format="color"/>
</declare-styleable>
</resources>
3. Activity布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.example.myapplication.diyview.CircleView
android:id="@+id/diyview_activity_layout_circle"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_margin="20dp"
android:background="#000000"
android:padding="20dp"
app:circle_color="@color/colorAccent" />
</LinearLayout>
网友评论