作者:郭孝星
微博:郭孝星的新浪微博
博客:http://blog.csdn.net/allenwells
Github:https://github.com/AllenWells
设计良好的类总是相似的,它使用一个易用的接口来封装一个特定的功能,它能有效的使用CPU和内存,我们在设计View类时,通常会考虑以下因素:
- 遵循Android标准规则
- 提供自定义的风格属性值并能够被Android XML Layout所识别。
- 发出可访问的事件
- 能够兼容Android的不同平台
下面我们就来介绍如何一步步的去实现一个设计良好的类。
一 继承一个View类
Android Framework里的View类都继承于View,我们自定义的View可以直接继承View或者其他View的子类。为了能够让ADT识别我们的View,我们必须至少提供一个构造器,如下所示:
class PieChart extends View {
public PieChart(Context context, AttributeSet attrs) {
super(context, attrs);
}
}
二 定义自设属性
为了添加一个内置的View到UI上,我们需要通过XML属性来指定它的样式和行为,良好的自定义View可以通过XML添加和改变样式,为了达到这种效果,我们通常会考虑:
- 为自定义的View在资源标签下定义自设的属性
- 在XML Layout中指定属性值
- 在运行时获得属性值
- 把获取到的属性值应用到自定义的View上
定义自设属性,添加到res/values/attrs.xml文件中,如下所示:
<resources>
<declare-styleable name="PieChart">
<attr name="showText" format="boolean" />
<attr name="labelPosition" format="enum">
<enum name="left" value="0"/>
<enum name="right" value="1"/>
</attr>
</declare-styleable>
</resources>
以上定义了两个自设属性:showText和labelPosition,它们都归属于PieChat的项目下的styleable实例,styleable实例的名字通常和自定义View的名字一致。
当我们定义了自设的属性,我们就可以在Layout XML文件中使用它们,就像内置属性一样,唯一不同时自设属性归属于不容的命名空间,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:custom="http://schemas.android.com/apk/res/com.example.customviews">
<com.example.customviews.charting.PieChart
custom:showText="true"
custom:labelPosition="left" />
</LinearLayout>
注意:
- 为了避免输入长串的namespace名字,示例上面使用了 xmlns 指令,这个指令可以指派custom作为 http://schemas.android.com/apk/res/com.example.customviews namespace的别名。我们也可以使用其他别名作为namespace。
- 如果你的view是一个Inner Class,我们需要指定这个View的Outer Class。同样的,如果PieChart有一个Inner Class叫做PieView。为了使用这个类中自设的属性,我们需要使用com.example.customviews.charting.PieChart$PieView。
三 应用自设属性
当View从XML Layout被创建的时候,在XML标签下的属性值都是从res下读取出来并传递到View的构造器作为一个AttributeSet的参数,尽管可以从AttributeSet中直接读取数值,但这样做有以下弊端:
- 拥有的属性资源并没有经过解析
- styles并没有应用上
我们通过attrs的方法是可以直接获取到属性值的,但是不能确定值的类型,如下所示:
//通过此方法可以获取title的值,但是不知道它的类型,处理起来很容易出问题。
String title = attrs.getAttributeValue(null, "title");
int resId = attrs.getAttributeResourceValue(null, "title", 0);
title = context.getText(resId));
取而代之的方法是通过obtainStyledAttributes()方法来获取属性值,该方法会传递一个TypedArray对象,Android资源编译器对res目录里的每一个,自动生成R.java文件定义了存放属性ID的数组和常量,这些常量用来引用数组中的每个属性。我们可以通过TypedArray对象来读取这些属性。
public PieChart(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs,
R.styleable.PieChart,
0, 0);
try {
mShowText = a.getBoolean(R.styleable.PieChart_showText, false);
mTextPos = a.getInteger(R.styleable.PieChart_labelPosition, 0);
} finally { a.recycle();
}
}
注意:TypedArray对象是一个共享对象,使用完毕后应该进行回收。
四 添加属性和事件
Attributes是一个强大的控制View行为和外观的方法,但是它仅仅能够在View被初始化的时候被读取到,为了提供一个动态的行为,我们需要设置一些set和get方法,如下所示:
public boolean isShowText() {
return mShowText;
}
public void setShowText(boolean showText) {
mShowText = showText;
//invalidate()和requestLayout()两个方法的调用是确保稳定运行的关键。当
//View的某些内容发生变化的时候,需要调用invalidate来通知系统对这个View
//进行redraw,当某些元素变化会引起组件大小变化时,需要调用requestLayout
//方法。调用时若忘了这两个方法,将会导致hard-to-find bugs。
invalidate();
requestLayout();
}
除了暴露属性之外,我们还需要暴露事件,自定义的View也需要能够支持响应事件的监听器。
五 绘制View的外观
5.1 重写onDraw()方法
5.1.1 创建绘制对象
绘制一个自定义View的外观最重要的步骤是重写onDraw(),onDraw()的参数是一个Canvas对象,Canvas对象定义了绘制文本、线条、图像和许多其他图形的方法。
onDraw()方法会做以下常见操作:
- 绘制文字使用drawText()。指定字体通过调用setTypeface(), 通过setColor()来设置文字颜色.
- 绘制基本图形使用drawRect(), drawOval(), drawArc(). 通过setStyle()来指定形状是否需要filled, outlined.
- 绘制一些复杂的图形,使用Path类. 通过给Path对象添加直线与曲线, 然后使用drawPath()来绘制图形. 和基本图形一样,。是outlined, filled, both.
- 通过创建LinearGradient对象来定义渐变。调用setShader()来使用LinearGradient。
- 通过使用drawBitmap来绘制图片.
举例
protected void onDraw(Canvas canvas) {
super.onDraw(canvas); // Draw the shadow
canvas.drawOval(
mShadowBounds,
mShadowPaint
);
// Draw the label text
canvas.drawText(mData.get(mCurrentItem).mLabel, mTextX, mTextY, mTextPaint);
// Draw the pie slices
for (int i = 0; i < mData.size(); ++i) {
Item it = mData.get(i);
mPiePaint.setShader(it.mShader);
canvas.drawArc(mBounds,
360 - it.mEndAngle,
it.mEndAngle - it.mStartAngle,
true, mPiePaint);
}
// Draw the pointer
canvas.drawLine(mTextX, mPointerY, mPointerX, mPointerY, mTextPaint);
canvas.drawCircle(mPointerX, mPointerY, mPointerSize, mTextPaint);
}
Android Graphics Framework把绘制定义为下面两类:
- Canvas:绘制什么
- Paint:如何绘制
举例
创建Paint对象,定义颜色、样式和字体等。
private void init() {
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setColor(mTextColor);
if (mTextHeight == 0) {
mTextHeight = mTextPaint.getTextSize();
} else {
mTextPaint.setTextSize(mTextHeight);
}
mPiePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPiePaint.setStyle(Paint.Style.FILL);
mPiePaint.setTextSize(mTextHeight);
mShadowPaint = new Paint(0);
mShadowPaint.setColor(0xff101010);
mShadowPaint.setMaskFilter(new BlurMaskFilter(8, BlurMaskFilter.Blur.NORMAL));
5.1.2 处理布局事件
为了正确的绘制自定义的View,我们需要知道View的大小。复杂的自定义View通常需要根据在屏幕上的大小与形状执行多次layout计算。而不是假设这个view在屏幕上的显示大小。即使只有一个程序会使用自定义View,仍然是需要处理屏幕大小不同,密度不同,方向不同所带来的影响。
View中有很多方法可以用来计算大小。
- onSizeChanged()
onSizeChanged():当View第一次被赋予一个大小时,或者View的大小被更改时触发该方法,我们可以在该方法里计算位置、间距和其他View的大小值。
当我们的View被设置大小时,布局管理器会假定这个大小包括所有View的内边距(Padding),当我们计算View的大小时,我们需要处理内边距的值,如下所示:
// Account for padding
float xpad = (float)(getPaddingLeft() + getPaddingRight());
float ypad = (float)(getPaddingTop() + getPaddingBottom());
// Account for the label
if (mShowText) xpad += mTextWidth;
float ww = (float)w - xpad;
float hh = (float)h - ypad;
// Figure out how big we can make the pie.
float diameter = Math.min(ww, hh);
- onMeasure()
onMeasure()方法用来精确控制View的大小,该方法的参数是View.MeaureSpec,该参数会告知我们的View的父控件的大小。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Try for a width based on our minimum
int minw = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth();
int w = resolveSizeAndState(minw, widthMeasureSpec, 1);
// Whatever the width ends up being, ask for a height that would let the pie
// get as big as it can
int minh = MeasureSpec.getSize(w) - (int)mTextWidth + getPaddingBottom() + getPaddingTop();
int h = resolveSizeAndState(MeasureSpec.getSize(w) - (int)mTextWidth, heightMeasureSpec, 0);
setMeasuredDimension(w, h);
}
注意:
- 计算的过程有把view的padding考虑进去。这个在后面会提到,这部分是view所控制的。
- 帮助方法resolveSizeAndState()是用来创建最终的宽高值的。这个方法会通过比较view的需求大小与spec值,返回一个合适的View.MeasureSpec值,并传递到onMeasure方法中。
- onMeasure()没有返回值。它通过调用setMeasuredDimension()来获取结果。调用这个方法是强制执行的,如果我们遗漏了这个方法,会出现运行时异常。
六 处理输入手势
Android提供一个输入事件的模型,用户的动作会转换成触发一些回调函数的事件,我们可以通过重写这些回调方法来处理用户的饿输入事件。
常见的用户输入事件时Touch事件,多种Touch事件之间的相互作用称为Gesture,常见的Gesture有以下几种:
- tapping
- pulling
- flinging
- zooming
GestureDetector用来管理Gesture,它通过传入的GestureDetector.OnGestureListener来构建,如果我们只想处理简单的几种手势操作,我们也可以传入GestureDetector.SimpleOnGestureListener,如下所示:
class mListener extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onDown(MotionEvent e) {
return true;
}
}
mDetector = new GestureDetector(PieChart.this.getContext(), new mListener());
不管我们是否使用GestureDetector.SimpleOnGestureListener, 我们总是必须实现onDown()方法,并返回true。因为所有的gestures都是从onDown()开始的。如果你在onDown()里面返回false,系统会认为我们想要忽略后续的gesture,那么GestureDetector.OnGestureListener的其他回调方法就不会被执行到了。
一旦我们实现了GestureDetector.OnGestureListener并且创建了GestureDetector的实例, 我们可以使用我们的GestureDetector来中止你在onTouchEvent里面收到的touch事件,如下所示:
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean result = mDetector.onTouchEvent(event);
if (!result) {
if (event.getAction() == MotionEvent.ACTION_UP) {
stopScrolling();
result = true;
}
}
return result;
}
七 优化View性能
7.1 提升方法效率
为了设计良好的View,我们的View应该能执行的更快,不出现卡顿,动画也应该保持在60fps。为了加速我们的View,对于频繁调用的方法,应该尽量减少不必要的方法,在初始化或者动画间隙做内存非配的工作。
下面我们来讨论如何提升一些常见方法的效率。
- onDraw()方法
onDraw()方法,我们应该尽量减少onDraw()方法的调用,也即invalidate()方法的调用,如果真的有需求调用invalidate()方法,也应该调用带参数的invalidate()方法进行精确绘制,而不是无参数的invalidate()方法,因为无参数的invalidate()方法会绘制整个View。
- requestLayout()方法
requestLayout()方法,会使得Android UI系统去遍历整个View的层级来计算出每一个view的大小。如果找到有冲突的值,它会需要重新计算好几次。另外需要尽量保持View的层级是扁平化的,这样对提高效率很有帮助。如果去设计一个复杂的UI,我们应该考虑写一个自定义的ViewGroup来执行它的layout操作。与内置的View不同,自定义的View可以使得程序仅仅测量这一部分,这避免了遍历整个View的层级结构来计算大小。
7.2 使用硬件加速
从Android 3.0开始,Android的2D图像系统可以通过GPU (Graphics Processing Unit)来加速。GPU硬件加速可以提高许多程序的性能。但是这并不是说它适合所有的程序。Android Framework让我们能够随意控制你的程序的各个部分是否启用硬件加速。
一旦你开启了硬件加速,性能的提示并不一定可以明显察觉到。移动设备的GPU在某些例如scaling,rotating与translating的操作中表现良好。但是对其他一些任务,比如画直线或曲线,则表现不佳。为了充分发挥GPU加速,我们应该最大化GPU擅长的操作的数量,最小化GPU不擅长操作的数量。
举例
绘制pie是相对来说比较费时的。解决方案是把pie放到一个子View中,并设置View使用LAYER_TYPE_HARDWARE来进行加速。
private class PieView extends View {
public PieView(Context context) {
super(context);
if (!isInEditMode()) {
setLayerType(View.LAYER_TYPE_HARDWARE, null);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (Item it : mData) { mPiePaint.setShader(it.mShader);
canvas.drawArc(mBounds,
360 - it.mEndAngle,
it.mEndAngle - it.mStartAngle,
true, mPiePaint);
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
mBounds = new RectF(0, 0, w, h);
}
RectF mBounds;
}
版权声明:当我们认真的去做一件事的时候,就能发现其中的无穷乐趣,丰富多彩的技术宛如路上的风景,边走边欣赏。