Android实际开发过程中,不可避免的需要实现一些不规则的效果,这种效果不方便使用控件组合方式实现,往往需要静态或者动态地显示一些不规则的图形。这种效果需要通过绘制的方式来实现,即重写onDraw方法。
我们先使用用一种最简单的方式来创建我们的第一个自定义VIew。打开Android Studio,先新建一个测试工程,然后使用模板自动生成一个自定义View,关键步骤如图
。
这里面有两个重要的知识点:
1.如何添加自定义属性?
参考自动生成的自定义View,可以看到添加自定义属性的步骤如下:
1.在values目录下创建自定义属性的xml,比如:attrs_custom_view.xml,这个名字无限制。本例自动生成的代码如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CustomView">
<attr name="exampleString" format="string"/>
<attr name="exampleDimension" format="dimension"/>
<attr name="exampleColor" format="color"/>
<attr name="exampleDrawable" format="color|reference"/>
</declare-styleable>
</resources>
这里的格式(format)除了包含基本数据类型string,integer,boolean等,还有格式color是指颜色,reference是指资源id,dimension是指尺寸等。
2.在View的构造方法中解析自定义属性的值并做相应处理。
// Load attributes
final TypedArray a = getContext().obtainStyledAttributes(
attrs, R.styleable.CustomView, defStyle, 0);
mExampleString = a.getString(
R.styleable.CustomView_exampleString);
mExampleColor = a.getColor(
R.styleable.CustomView_exampleColor,
mExampleColor);
// Use getDimensionPixelSize or getDimensionPixelOffset when dealing with
// values that should fall on pixel boundaries.
mExampleDimension = a.getDimension(
R.styleable.CustomView_exampleDimension,
mExampleDimension);
if (a.hasValue(R.styleable.CustomView_exampleDrawable)) {
mExampleDrawable = a.getDrawable(
R.styleable.CustomView_exampleDrawable);
mExampleDrawable.setCallback(this);
}
a.recycle();
解析完后,通过recycle方法来释放资源。
3.在布局文件中使用自定义属性,如下所示。
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.my.customview.view.CustomView
android:layout_width="300dp"
android:layout_height="300dp"
android:background="#ccc"
android:paddingBottom="40dp"
android:paddingLeft="20dp"
app:exampleColor="#33b5e5"
app:exampleDimension="24sp"
app:exampleDrawable="@android:drawable/ic_menu_add"
app:exampleString="Hello, CustomView"/>
</FrameLayout>
为了使用自定义属性,需要在布局文件中添加schemas声明: xmlns:app="http://schemas.android.com/apk/res-auto
。
在这个声明中,app是自定义属性的前缀,当然可以使用其他名字,但是CustomView中自定义属性的前缀必须和这里一致,然后就可以在CustomView中使用自定义属性了,如: app:exampleDimension="24sp"。自动生成的代码采用的是:xmlns:app="http://schemas.android.com/apk/res/com.example.my.customview.view"
这种方式命名,也就是会在apk/res/+包名。这两种方式没有本质的区别,但是我倾向于使用res-auto这种声明方式。
2. 自定义View为什么需要重写onDraw方法?
我们需要先学习下非ViewGroup的View的绘制流程。View的draw()源码,在该阶段真正地开始对视图进行绘制。
/**
* Manually render this view (and all of its children) to the given Canvas.
* The view must have already done a full layout before this function is
* called. When implementing a view, implement
* {@link #onDraw(android.graphics.Canvas)} instead of overriding this method.
* If you do need to override this method, call the superclass version.
*
* @param canvas The Canvas to which the View is rendered.
*/
@CallSuper
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
// 重载的onDraw在这里被调用
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// we're done...
return;
}
看完了draw()的源码,我们就要把注意力集中在onDraw()了。
/**
* Implement this to do your drawing.
*
* @param canvas the canvas on which the background will be drawn
*/
protected void onDraw(Canvas canvas) {
}
官方文档中介绍
The Canvas class holds the "draw" calls. To draw something, you need 4 basic components: A Bitmap to hold the pixels, a Canvas to host the draw calls (writing into the bitmap), a drawing primitive (e.g. Rect, Path, text, Bitmap), and a paint (to describe the colors and styles for the drawing).
在绘图时需要明确的4个基本要素:
1.一个Bitmap保存了所绘图像的各个像素(pixel)
2.一个源,即是你想画的东西(比如矩形,路径,文本,抑或另一个位图)
3.一个canvas执行绘图操作。 比如,canvas.drawCircle(),canvas.drawLine()。
4.一支画笔(Paint)来绘图。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// TODO: consider storing these as member variables to reduce
// allocations per draw cycle.
int paddingLeft = getPaddingLeft();
int paddingTop = getPaddingTop();
int paddingRight = getPaddingRight();
int paddingBottom = getPaddingBottom();
int contentWidth = getWidth() - paddingLeft - paddingRight;
int contentHeight = getHeight() - paddingTop - paddingBottom;
// Draw the text.
canvas.drawText(mExampleString,
paddingLeft + (contentWidth - mTextWidth) / 2,
paddingTop + (contentHeight + mTextHeight) / 2,
mTextPaint);
// Draw the example drawable on top of the text.
if (mExampleDrawable != null) {
mExampleDrawable.setBounds(paddingLeft, paddingTop,
paddingLeft + contentWidth, paddingTop + contentHeight);
mExampleDrawable.draw(canvas);
}
}
总结
本文介绍一种非常简单的自定义View方式,讲述了可能遇到的疑惑,后续会有更多文章来记录我重新学习自定义View的过程。