翻译自:file:///Users/baidu/android-sdk/docs/training/custom-views/index.html
产生自定义View
Android框架已提供了大量的View类用于和用户交互和展示各种类型的数据。但是有时你的app可能有内置的View无法满足的特定需求。这篇文章将告诉你如何产生具有鲁棒性和可复用的自定义View。
大纲:
Createing a View Class
产生一个和内置类一样功能的类,有自定义属性并且支持ADT布局编辑器。
Custom Drawing
使用Android图形系统使得你的自定义view看上去具有特色。
Making the View Interactive
用户希望view能和用户平滑交互,自然的响应用户输入。这课讨论如何使用监测手势、物理系、和动画让你的用户体验到专业的用户交互界面。
Optimizing the View
不管你的UI多么漂亮,如果它不能平稳的以很到的帧速率渲染,用户也会不喜欢。学习如何避免常见的性能问题,和如何使用硬件加速将使得你的自定义View运行的更快。
Creating a View Class
设计良好的自定义View类和其他普通的设计良好的类一样,并无区别,也需要遵从相应的设计原则。它封装了一系列特定的功能用于方便与用户的交互。它应该高效的使用CPU和内存等。除了遵循普通类的良好设计原则,自定义View还应该:
- 遵循android标准
- 提供自定义Styleable属性,用于android XML布局
- 发送交互事件
- 兼容各种Android平台
继承View
所有android框架提供的view类都继承自View类。你的自定义view也能继承View或者节省时间继承自已有的view子、例如Button。
为了Android Developer Tools能和你的View交互,你的自定义View至少应该提供一个接受Context和AttributeSet参数的构造方法。该构造方法允许布局编辑器产生和编辑View的实例。
class PieChart extends View { public PieChart(Context context, AttributeSet attrs) { super(context, attrs); } }
定义自定义属性
为了将一个自定义view添加到你的用户界面,你能在xml元素里指定它,然后使用元素属性控制它的展示和行为。写的好的自定义view应该能指定样式,添加相关属性。为了使你的view具有这样的行为,你必须:
- 通过<declare-styleable>元素定义自定义属性
- 在xml里为你自定义的属性指定值
- 运行时检索属性值
- 在你的view里使用这些自定义属性
这部分讨论如何定义自定义属性并指定它们的值。下部分讨论如何在运行时检索和使用这些值
为了定义自定义属性,添加<declare-styleable>资源节点到你的工程。按照惯例,将这些资源放到res/values/attrs.xml文件里。下面是一个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>
该xml文件里声明了两个自定义属性:showText和labelPosition。这两个属性属于PieChart样式实体。通常地,styleable实体名与自定义View的类名相同。虽然这不是强制要求,但一些流行的代码编辑器依赖于该命名规范提供代码自动补全。
一旦你定义了自定义属性,你能像使用系统内置的属性一样在布局文件里使用自定义熟悉。唯一的不同是自定义属性属于不同的命名空间。不是属于http://schemas.android.com/apk/res/android命名空间,它们属于http://schemas.android.com/apk/res/[your custom view package name]。例如下面显示了如何在PieChart里使用自定义属性:
<?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>
为了避免重复写很长的命名空间URI,例子使用xmlns指令。该指令指定了命名空间http://schemas.android.com/apk/res-auto的别名为custom。你能为你的命名空间选择任何你想要的别名。
注意:如果你不是使用的Gradle构建你的工程,你的xmlns URI不能包含res-auto。URI而是必须包含你的工程的全引用名。本例里,非Gradle URI将是:http://schemas.android.com/apk/res/com.example.customviews。
Apply Custom Attributes
当view从布局文件里产生时,XML标签里的所有属性都是从资源Bundle里读取出来的,然后作为AttributeSet参数传递给View的构造方法。虽然直接从AttributeSet里读取属性值是可能的,但是这么做有如下缺点:
- 在属性值里的资源引用是不可resolved的;
- Styles are not applied
我们应该将AttributeSet传给obtainStyledAttributes()。然后该返回已经dereferenced和styled的属性值的TypedArray数组。
为了让你调用obtainSytledAttributes()更容易,Android资源编译器做了许多工作。对应资源目录里的每一个<declare-styledable>资源:自动产生的R.java文件不仅定义了一个资源id的数组,也定义了一些列常量,这些常量定义每一个属性在数组里的索引。你能使用预定义的数组下标常量从TypedArray数字里读取这些值。下面显示了PieChart类如何读取它的自定义属性。
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对象是一个共享资源,在使用后必须被回收。
Add Properties and Events
属性是控制View行为和展示的强有力的方式,但是属性仅仅能在当view初始化读取。为了提供view的动态形态,暴露每一个自定义属性的getter和setter方法供外界读取。下面代码片段表明了PieChart如何暴露属性:
public boolean isShowText() { return mShowText; } public void setShowText(boolean showText) { mShowText = showText; invalidate(); requestLayout(); }
注意:setShowText方法调用了invalidate()和requestLayout()。这些调用使得View行为正常和可靠起到关键和决定作用。你必须在可能引起view的展示发生改变的属性改变后invalidate你的view,这样系统知道自定义view需要被重绘。否则,如果属性改变可能引起View的大小和形状发生改变,那么即需要request一个新的布局。忘记调用这些方法可能引起一些bug,而这样bug可能非常难于发现和找到原因。
自定义View也应该支持时间监听为了用户能处理重要的事件。例如,PieChart暴露了一个叫做onCurrentItemChanged的自定义事件调用来通知监听者用户已旋转了pie chart。
我们可能经常会忘记暴露自定义属性和事件,特别地当只要你自己使用你的自定义View时。花一些时间仔细的定义的自定义View的接口吧,这样以后你就能极大的减少维护成本。关于这,你应该遵循一个原则就是:所有可能会影响到你自定义View展示和行为的属性你都应该暴露出来成为自定义属性。
Design For Accessibility
你的自定义View应该支持最广泛的用户。这包括那些不能看见(例如盲人)或者不能触屏的有残疾的用户。为了对有残疾的用户的支持,你应该:
- 使用android:contentDescription给你的输入域打标签
- 合适时调用
sendAccessibilityEvent()
发送Accessibility事件 - 支持各种控制器,例如D-pad和traceball
关于更多的这方面的讨论请参考Android开放文档里的Making Applications Accessible 。
Custom Drawing
自定义View最重要的部分就是外观。依你的app的需求而定,自定义绘制可能是简单的也可能是复杂的。
Override onDraw()
绘制一个自定义View最重要的一步是重写onDraw()
方法。该方法的接收参数是一个canvas对象。View能使用该Canvas对象画自身。Canvas类里定义了画文本、线、位图和其他原始图像的方法。在onDraw()
方法里你能使用Canvas提供的这些方法绘制自定义View的UI。
当然地,你调用任何绘制方法之前,你必须先产生一个Paint
对象,下个章节将详细讨论Paint
对象
Create Drawing Objects
android.graphics
框架将绘制分成两部分:
- 画什么,由Canvas处理
- 怎么画,由
Paint
处理
例如Canvas提供了一个划线的方法,然而同时,Paint提供了定义线的颜色的方法。Canvas有画矩形的方法,然而Paint提供了方法来决定矩形是否是填充的,以及填充的颜色。简单地说,Canvas定义你能在屏幕上画的形状,然而Paint定义颜色、样式、字体和你画的形状的各个方面。
因此,你绘制任何View之前,你需要定义一个或者多个Paint
对象。PieChart的例子进行这些操作实在一个叫做init的方法里,该方法在构造方法里被调用。
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)); ...提前产生Paint对象是一个显著的优化。View可能会频繁的被重绘。一些绘制对象需要昂贵的初始化过程。在
onDraw()
方法里进行初始化相关的绘制对象将显著的降低性能,并使你的UI展示显得卡顿。
Handle Layout Events
为了合理的绘制你的自定义View,你需要知道自定义View的大小。负责的自定义View可能需要多次的布局计算,这取决于它们在屏幕上所在区域的大小和形状。即使只要一个APP使用你的自定义View,该app也需要处理不同的屏幕大小和不同的屏幕密度以及横竖屏切换的各种比例问题。
虽然View中有许多处理View测量的方法,但大部分方法并不需要重写。如果你的View不需要对大小进行专门的控制,你仅仅需要重写一个方法:onSizeChanged()
.
onSizeChanged()在第一次给View分配大小时会被调用,以及以后由于任何原因View的大小发生改变时也会被调用。在onSizeChanged()
里进行计算view的位置、大小和与尺寸相关的操作,而不是在你每次绘制的时候都重新计算一次这些值。在PieChart例子里,在onSizeChanged()方法里进行的操作有:计算PieChart的矩形边界、文本标签的相对位置和其他的可见元素。
当你的View被分配大小时,布局管理器会认为该大小包含你的View的各种Padding大小。因此,当你计算你的View大小的时候,你必须处理你的Padding值。如下是PieChart例子里PieChart.onSizeChanged()
方法的代码片段,它显示了如何处理这:
// 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);
如何你需要能很好的控制你的自定义View的布局参数,你可以实现onMeasure()方法,该方法的参数是 View.MeasureSpec
, View.MeasureSpec
告诉你你的View该View的父布局建议的View大小,以及是否该大小是一个硬性的最大量还是仅仅是一个建议值。为了优化,这些值被打包成一个整型值,你可以使用View.MeasureSpec的静态方法解压出存储在该整型里的信息。
如下是一个实现onMeasure()方法的例子。如下的实现里,PieChart企图使得它的区域足够大已满足饼图和便签能一样大。
@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的最终的宽和高的值。该帮助方法通过比较View期望的大小和onMeasure()方法传递进来的Spec进行比较来返回合适的View.MeasureSpec
值。 - onMeasure()没有返回值。而是通过调用setMeasuredDimension()来利用测量结果。必须调用setMeasuredDimension()方法,该方法是强制性的。如果你忘记调用该方法,View类将抛出一个运行时异常。
Draw!
一旦你已重写了构造方法完成对象产生,也定义了测量代码,那么你能实现 onDraw()
方法,每一个View实现 onDraw()
方法都不同。但是有一些大多数View都有的通用操作:
- 绘制文本使用drawText(),通过调用
setTypeface()
指定字体。调用setColor()
指定文本颜色。 - 使用
drawRect()
,drawOval()
, 和drawArc()
绘制基本的形状。改变形状是充满的还是只是大概轮廓调用setStyle()方法 - 使用Path类绘制更复杂的形状。通过添加直线和曲线到Path对象里来定义图形,然后使用drawPath()来画该图形。像绘制基本图形一样,也可以通过调用setStyle()方法将path设置成是outlined还是filled或者两种都是。
- 通过产生LinearGradient对象来定梯度(gradient)。通过在填充的形状上调用
LinearGradient
的setShader()方法来实现图形梯度。 - 画位图使用drawBitmap()。
例如,如下是绘制PieChart的代码,它是文本、线和图形的混合使用。
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); }
Making the View Interactive
绘制UI仅仅是产生一个自定义View的一部分。当然,你还需要你的View能响应用户输入。你的图形展示和响应应该尽可能的匹配真实世界。例如,图片不应该突然的立即的弹出展示的区域,然后突然的在其他的某个地方再次展示,因为真实世界里并不是这样。这样会让人感觉很唐突。而是应该让图片自然地从一个地方移动到另一个地方。
用户能感知和接受UI里细微的行为改变或感觉。这样才能让用户体验更好,如同模拟真实世界的响应一样。例如,当用户手指划过一个UI对象的时候,刚开始他们应该感觉到摩擦力,这样滑动应该是延迟的,然后在滑动结束时View加速移出。
Handle Input Gestures
像一些其他的UI框架,Android也支持输入事件模型。用户行为被转换成相应的事件。这些事件触发回调,你能重写这些回调方法来定制如何让的app相应你的用户操作。Android系统里最常见的输入事件是touch,它触发了onTouchEvent(android.view.MotionEvent)
。重写该方法来处理该事件:
@Override public boolean onTouchEvent(MotionEvent event) { return super.onTouchEvent(event); }触摸事件本身并不是十分有用的。现代的触摸UI通过Gestures定义了交互,例如轻击、拉,推、滑动和缩放等。为了将原始的触摸事件转换成手势(gestures),Android提供了
GestureDetector
。
传递一个实现了GestureDetector.OnGestureListener的实例来构造 GestureDetector
对象。如果你仅仅想要处理几个手势,你能继承GestureDetector.SimpleOnGestureListener而不用继承GestureDetector.OnGestureListener。例如,下面的代码产生了一个继承GestureDetector.SimpleOnGestureListener
的类,并重写了onDown(MotionEvent)。
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。这步是必须的,因为所有的手势都以 onDown()
消息开始。如果
GestureDetector.SimpleOnGestureListener
的 onDown()
返回的是false,系统会认为你想要忽略其他的剩余的手势,这样 GestureDetector.OnGestureListener
里的其他的方法将不会被调用。唯一的能返回false的情景是你真的想要忽略全部手势。一旦你实现了 GestureDetector.OnGestureListener
并且产生了 GestureDetector
实例,你能使用
GestureDetector
拦截在 onTouchEvent()
收到的触摸事件。
@Override public boolean onTouchEvent(MotionEvent event) { boolean result = mDetector.onTouchEvent(event); if (!result) { if (event.getAction() == MotionEvent.ACTION_UP) { stopScrolling(); result = true; } } return result; }当你传递
onTouchEvent()
一个触摸事件的时候,它并不识别作为手势的一部分,
onTouchEvent()
返回false。然后你能跑你的自定义手势监测代码。
Create Physically Plausible Motion
手势是控制触屏设备的最强有力的方式,但是这可能是有备常理,也是难以记住,除非它们产生实际可行的结果。这的一个好的例子是滑动手势:用户在屏幕间快速的滑动手指,然后离开屏幕。如果UI在滑动的方向快速移动,然后减慢,这样的UI响应用户也感觉是合理的。似乎好像用户在一个飞轮上推一样并使得它转动。
然而,模拟飞轮的感觉不是重要的。使得飞轮模型正确的工作需要许多物理和数学知识。幸运地,Andorid提供了帮助类来模拟这些行为。Scroller类是处理类似的飞轮滑动手势的基本类。
为了开始滑动,调用fling()方法,该方法需要传入滑动的开始速度和最大最小的x/y值。对于速度值,你能使用GestureDetector计算给你的值。
@Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { mScroller.fling(currentX, currentY, velocityX / SCALE, velocityY / SCALE, minX, minY, maxX, maxY); postInvalidate(); }
注意:虽然通过GestureDetector计算的速度是物理上精确的,但是一些开发者会感觉使用这个值做滑动动画会太快。一般的做法是用一个4~8的除数来除x和y方法的速度。
调用fling()
建立滑动手势的物理模型。然后,你需要在固定的间隔/频率调用Scroller.computeScrollOffset()更新Scroller。computeScrollOffset()通过读取当前时间和使用物理模型读取当前时间的x和y方向的位置来更新Scroller
。调用getCurrX()
和getCurrY()
获取这些值。
大多数View直接传递 Scroller
对象的x和y坐标给scrollTo()
方法。然后PieChart例子有一点不同:它使用当前滚动的y坐标来设置图表的滚动角度。
if (!mScroller.isFinished()) { mScroller.computeScrollOffset(); setPieRotation(mScroller.getCurrY()); }
Scroller
类为你计算滚动位置,但不会自动的将这些值应用到你的View上。确保足够及时的获取和使用这些坐标来使得滚动动画看起来流畅那是你自己的职责。有如下两种方式实现:
- 在调用fling()后调用postInvalidate(),该方法会迫使系统重绘。该技术要求你在onDraw()方法里计算滚动偏移,在每次偏移量改变后调用postInvalidate()。
- 设置ValueAnimator在滑动时来实现动画。调用
addUpdateListener()
添加一个监听器来处理动画更新。
PieChart的例子使用第二种方式。这种方式略复杂点。但这种方式更多的是使用动画系统实现,并不需要必要的View的 invalidation。缺点是ValueAnimator
在API11之前是无效的。因此这种方式不能用于3.0以下的设备上。
mScroller = new Scroller(getContext(), null, true); mScrollAnimator = ValueAnimator.ofFloat(0,1); mScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { if (!mScroller.isFinished()) { mScroller.computeScrollOffset(); setPieRotation(mScroller.getCurrY()); } else { mScrollAnimator.cancel(); onScrollFinished(); } } });
注意:虽然ValueAnimator在API11之后才出现,你仍然可以在低版本的app里使用。你仅仅需要在运行时判断下API版本,如果API版本低于11你需要避免调用动画系统。
Make Your Transitions Smooth
用户期望UI在各种状态间转换流畅。UI元素渐入渐出而不是突然的出现和消失。操作开始和结束流畅而不是粗鲁的开始和停止。从3.0开始的Android property animation framework使得流畅转换容易。
为了使用动画系统,无论何时影响到到你view的展示的属性改变了,都不要直接改变该属性。而是使用来ValueAnimator改变属性。在下面的例子里,修改PieChart里的当前选择的饼状块引起了整个图表旋转,这样使得选择指针在选择块的中心。我们通过ValueAnimator持续几百毫秒的旋转动画来实现旋转,而不是立即的设置新的旋转值。
mAutoCenterAnimator = ObjectAnimator.ofInt(PieChart.this, "PieRotation", 0); mAutoCenterAnimator.setIntValues(targetAngle); mAutoCenterAnimator.setDuration(AUTOCENTER_ANIM_DURATION); mAutoCenterAnimator.start();如果你想要改变的值是你的View的基本属性之一,实现动画改变属性值更容易。因为View有内检的
ViewPropertyAnimator
,
ViewPropertyAnimator
经过优化用于多属性的同步动画。例如:
animate().rotation(targetAngle).setDuration(ANIM_DURATION).start();
Optimizing the View
既然你已实现了设计良好的View用于响应手势和在状态间切换,那么你也需要你的View运行得快。为了避免UI让人感觉卡顿和拖慢系统运行,你必须确保你的动画恒定的运行在每秒60帧。
Do Less, Less Frequently
为了加速你的View,消除可能被频繁调用的不必要的代码。首先消除onDraw()里不必要的代码,最显著最明显的就是在onDraw()方法里。特别地,你应该避免onDraw()里的内存分配,因为分配可能导致垃圾回收,这将引起卡顿。分配对象应该在初始化时或者在动画间。从不要在动画运行时分配对象。
为了让onDraw()方法更瘦身。你也应该确保onDraw()方法不被频繁的调用。大多数情况下,onDraw()方法被调用是由于调用了invalidate()的结果。因此,避免对invalidate()方法的不必要的调用。无论什么时候,尽可能优先调用四个参数的invalidate()方法,而不是无参的invalidate()方法。无参的invalidate()
方法重绘整个View,而四个参数的invalidate()
方法仅仅重绘该View的指定的部分。这种方法将使得绘制更高效,能避免不必要的重绘。
另一个极其昂贵的操作是扫描和遍历布局。任何时候View调用requestLayout(),Android UI 系统需要遍历整个View层次来找出每个View需要展示多大。如果它发现测量冲突,可能需要多次遍历View层级。有时UI设计人员为了使得合适的UI行为可能产生内嵌ViewGroup
的很深的层级。这些很深的View层级引起性能问题。因此,应该使得你的View层级尽可能的浅。
如果你有一个复杂的UI,你应该考虑写一个自定义的ViewGroup
来实现该布局。不像Android系统已提供的View,你的自定义View能对其子View的大小和形状产生针对特定app的实现,这样可以避免为了计算测量而对子View进行遍历。PieChart有子View,但从不测量它们。而是根据自定义布局的算法直接设置它们的尺寸。
Use Hardware Acceleration
从Android3.0起,Android 2D图像系统能使用GPU(Graphics Processing Unit)进行加速。大部分新的Android设备都有CPU硬件。GPU硬件加速更给一些app带来极大的性能提高。但是并不使用于每个app。Android框架给了你很好的控制你的app哪些部分使用硬件加速,哪些部分不用硬件加速的能力。
参见Android开发向导的Hardware Acceleration 学习如何在app、activity或者Window层面开启硬件加速。注意,你必须在AndroidManifest.xml
文件里设置<uses-sdk android:targetSdkVersion="11"/>
来指定你的app目标API高于11.
一旦已开启硬件加速,你可能看到性能可能提高也可能没有提高。对于某些操作,例如缩放、旋转和位图转换,移动GPUs可能是非常有效果的。但例如画线和曲线,它就并不是很擅长了。为了最大化利用GPU加速,你应该最大化GPU擅长的操作,避免GPU不擅长的操作。
例如,在PieChart例子里绘制饼图相对成本高点。每次重绘旋转的饼图将引起UI感觉卡顿。解决方案是将饼状图变成一个子View,然后设置该子View的层级类型为LAYER_TYPE_HARDWARE
。这样GPU能缓存它作为一个静态的图片。如下例子将该子View定义为的 PieChart
内部类,这样是实现该方案的代码改动最小的一种方式。
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; }
代码做了如此改动后,PieChart.PieView.onDraw()
仅仅在该View第一次显示时会被调用。在该app的其他生命周期里,饼状图形作为image被缓存,通过GPU在不同的旋转角度重绘。GPU特别擅长如此的操作,性能提高是极其显著的,立马就能显现出来。
虽然有内存的代价,作为硬件层缓存图片消费视频内存,视频内存是一个有限的资源。因为这个原因,PieChart.PieView的最终版本是设置它的层级类型为LAYER_TYPE_HARDWARE
而用户能自由地滚动。而其他时候,设置层级类型为LAYER_TYPE_NONE
,,从而让GPU不缓存图片。
最后,不要忘记将注解你的代码,在一个view提高性能的技术可能在其他的View上不能提高性能。