Android视图框架提供了一个丰富的视觉工具包,用这个可以创建应用UI。不过,你可能会发现你需要的功能在stock视图中没有。对于这些情况,Android允许你创建自定义的视图,那将会更好地满足你的需求。Android分两步绘制视图,测量阶段和布局阶段;在创建一个自定义视图时你需要重写onMeasure和onDraw方法;使用自定义视图属性需要你为那些属性定义一个新的XML命名空间;复合视图把多个视图结合成为一个自定义组件。
理解Android如何绘制视图
在学习如何创建自定义视图之前,你需要理解Android如何绘制和显示。Android UI按照层级摆放。这个层级包括系统元素,例如通知栏和导航栏等,还有现在的活动视图层级。当一个活动被带到前台,系统要求知道该活动的根结点,然后绘制显示屏上的视图层级。当你调用setContentView时需要设置活动的根结点。活动布局需要包括顶部通知和动作栏以及底部导航栏之间的所有东西(如果有的话)。
显示屏被绘制的区域被标记为无效(invalid)。任何与无效区域交互的东西都需要被重新绘制。当系统绘制一个活动时会调用invalidate(),但是你也可以通过在视图上调用invalidate()强制其发生。当视图被绘制好之后,它就会被标记为有效(valid)
绘制分为两个阶段。在第一阶段,层级的根结点被要求衡量自身。然后根结点会测量每个子视图(child view)。每个子视图随后又会测量它的子视图。这样,视图层级的每个视图尺寸都被测量出来了。在每一级别上,父视图(parent view)都会给予子视图一个特定的尺寸或者会要求它们自己设置自己的尺寸。
当测量阶段结束之后,系统会执行视图层级的布局。它会先序遍历布局树,把每个视图绘制到一个位图上。父视图首先绘制,然后在其上绘制子视图。布局完成之后,绘制系统会向屏幕绘制位图并把它们展示给用户。
创建自定义视图
创建自定义UI组件首先要继承一个视图类。你可以继承基础视图类来获得最大的可配置性,或者你也可以从现有的视图类开始并添加你需要的功能。你的选择取决于你应用的需要。两种方法的基础实现是相同的:继承视图并重写适当的方法,并增加自定义的代码。
这个只适用于静态视图或者低性能的2D图像。如果你想要创建3D图形或者复杂动画,你应当继承SurfaceView或者使用RenderScript或OpenGL。
开始学习如何创建自定义视图,首先让我们创建一个简单的自定义视图,这个视图会展示十字形状的两条线。开始时,创建一个继承自View类的名为CrossView的新类:
public class CrossView extends View { public CrossView(Context context, AttributeSet attrs) { super(context, attrs); } }
请注意,该构造函数需要一个Context和一个AttributeSet对象。Context提供了应用资源和系统服务的接口,系统服务用来正确地扩展视图以及把它加到你的活动中。AttributeSet要求传递XML参数到活动中。当学习如何创建自定义XML属性时会学到更多关于这个的知识。通常你需要调用superclass中重写的方法来执行活动的初始设置。完成这个之后,你可能想要重写两个基础方法:onMeasure和onDraw。
OnMeasure
系统调用onMeasure方法来决定视图及其子视图的尺寸,它传入实际是MeasureSpecs的两个参数。一个MeasureSpec就是一个模式表示和一个整型尺寸值的结合。它被当做一个整数来实现,以减少不必要的对象分配。模式告知视图它该如何计算尺寸。下面是可能的模式。
UNSPECIFIED: 父视图没有在这个视图上做任何限制,它可以是任意尺寸。
AT_MOST: 该视图可以是小于等于MeasureSpec尺寸的任意尺寸。
EXACTLY: 不管要求如何,该视图都会与MeasureSpec尺寸相同。
当你创建一个自定义视图并重写onMeasure方法时,你需要正确处理每个case。此外,测量要求指定你用已决定的整数尺寸值调用setMeasureDimensions方法。如果你不能做到这一点,会抛出一个IllegalStateException。
1.重写onMeasure方法:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int measureWidth = calculateMeasure(widthMeasureSpec); int measureHeight = calculateMeasure(heightMeasureSpec); setMeasuredDimension(measureWidth, measureHeight); }
private static final int DEFAULT_SIZE = 100; private int calculateMeasure(int measureSpec) { float density = getResources().getDisplayMetrics().density;//像素密度 int result = (int) (DEFAULT_SIZE * density); return result; }
3.在MeasureSpec中检索模式和尺寸可取:
private int calculateMeasure(int measureSpec) { float density = getResources().getDisplayMetrics().density; int result = (int) (DEFAULT_SIZE * density); int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); }
4.基于模式选择尺寸:
private int calculateMeasure(int measureSpec) { float density = getResources().getDisplayMetrics().density; int result = (int) (DEFAULT_SIZE * density); int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); if (specMode == MeasureSpec.EXACTLY){ result = specSize; } else if (specMode == MeasureSpec.AT_MOST) { result = Math.min(result, specSize); } return result; }
如果模式被设置为EXACTLY,那么输入的尺寸将会被使用。如果模式被设置为AT_MOST,那么将会使用DEFAULT_SIZE和输入尺寸的较小值,否则,则会使用DEFAULT_SIZE。
资源
当你编译Android应用程序时,SDK不是仅仅盲目地把应用资源复制到APK中。相反,它把它们编译成一个更高效的二进制格式来减少它们的尺寸并改善查询性能。要在运行时访问资源,你可以使用一个Resources对象。你可以通过从应用的上下文调用getResource()方法,以检索Resources对象。Resources对象提供了一些方法,这些方法采用资源已编译的整数ID,并返回正确类型的资源。
OnDraw
当视图应当绘制其内容时会调用onDraw方法。它传入一个Canvas对象,该对象包含视图的底层位图。Canvas提供方法来进行用来构建视图的基础绘制操作,它在其内部位图上执行那些绘制操作。
你可以调用Canvas的一个绘制方法或者使用一个绘制原语(primitive)来执行实际的绘图。Android提供了集中构建UI的绘图原语:矩形、椭圆、路径、文本、和位图。你也需要一个Paint对象来保存将被应用到绘制中的样式。它处理诸如颜色和文本大小之类的事情。
1.创建一个Paint对象来保存十字的样式:
private Paint mPaint; public CrossView(Context context, AttributeSet attrs) { super(context, attrs); mPaint = new Paint(); mPaint.setAntiAlias(true); //设置抗锯齿 mPaint.setColor(0xFFFFFFFF);//设置画笔颜色 }
提示:
视图同样有被其父视图调用的draw方法。这个方法处理基础的绘制步骤,像设置画布以及绘制背景等。你应当避免重写这个方法,而要重写onDraw方法。
2.重写onDraw方法。所有的在画布上绘图的调用都应当受对应的save()和restore()的约束:
@Override public void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.save(); //code canvas.restore(); }
3.要使得绘制代码简单,基于视图的尺寸缩放画布。这样做让你可以使用简单的0到1之间的浮点数绘制,而不需要带有尺寸值:
@Override public void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.save(); //自定义代码 int scale = getWidth(); canvas.scale(scale, scale); //end canvas.restore(); }
4.要绘制十字的两条线,你将用到Canvas的drawLines方法:
private float[] mPoints = {0.5f, 0f, 0.5f, 1f, 0f, 0.5f, 1f, 0.5f}; @Override public void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.save(); //自定义代码 int scale = getWidth(); canvas.scale(scale, scale); canvas.drawLines(mPoints, mPaint); //end canvas.restore(); }
DrawLines方法采用一个包含要绘制的线的数组(每行端点的两个x坐标和两个y坐标),以及一个用来在画布画线的Paint对象。通过缩放画布,你可以使用小数浮点值指定所有的尺寸。
5.创建一个活动来展示视图:
public class CustomViewActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); } }
6.打开main.xml文件并添加CrossView:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center_horizontal" android:orientation="vertical" > <com.example.CrossView android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout>
内部类(Inner Class)
当在布局中使用自定义视图时,你通常使用类名作为元素标签名。然而,如果自定义视图是另一个Java类的内部类时这样就不行了,因为要求的$符号在Android XML布局中不合法。例如,你要让CrossView成为CustomViewsActivity的一个内部类,那么该布局将不会编译。在这种情况下,你将需要使用类属性来设置合乎要求的视图名称:
<view
class="com.example.CustomViewsActivity$CrossView"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
注意到这里使用了小写(view),而不是大写(View)元素。这意味着它是一个通用视图并且类定义可以在类属性中找到。
当在XML中使用自定义视图时,你必须使用完整包名来通知系统你要扩展哪个视图类。