Android UI框架远远不止是一个智能的、组织良好的GUI工具箱。当它摘掉眼镜,甩甩秀发,可是很有魅力哦!这里提到的工具当然不够详尽。但是,它们可能有助于你开发出丰富多彩的应用。
警告:这里所探讨的一些技术接近于Android的边缘。因此,比起我们前面章节所讨论的类而言,它们还不够完善:文档也不够全面,一些特性还处于“转型期”,可能还会遇到一些Bug。如果你遇到问题,Google Group“Android Developers”是非常宝贵的资源。关于工具箱的某些功能的问题有时回答者正是这一功能的开发者。
当你因为某些问题搜索Web时,要仔细查看解决方案的日期。有些特征变化很快。6个月前正常工作的代码可能现在已经无法工作了。当然,任何分布广泛的应用很可能在各个版本上都能够正常运行。通过使用这些技术,你可能会限制应用的生命周期,以及它支持的设备的数量。
本章后面的部分探讨的是一个应用程序,和例8-6所给出的很类似:一些LinearLayout视图,它们包含单个部件的多个实例,每个实例显示不同的图形展示效果。例8-10给出了该部件的关键部分,其代码如前所述,这里略去。该部件只是绘制一些图形对象并定义接口,通过该接口,可以对各种图形进行渲染。
例8-10:Effects widget
public class EffectsWidget extends View { /** The effect to apply to the drawing */ public interface PaintEffect { void setEffect(Paint paint); } // ... // PaintWidget's widget rendering method protected void onDraw(Canvas canvas) { Paint paint = new Paint; paint.setAntiAlias(true); effect.setEffect(paint); paint.setColor(Color.DKGRAY); paint.setStrokeWidth(5); canvas.drawLine(10, 10, 140, 20, paint); paint.setTextSize(26); canvas.drawText("Android", 40, 50, paint); paint = new Paint; paint.setColor(Color.BLACK); canvas.drawText(String.valueOf(id), 2.0F, 12.0F, paint); paint.setStyle(Paint.Style.STROKE); paint.setStrokeWidth(2); canvas.drawRect(canvas.getClipBounds, paint); }}
该应用用到的部件(参见例8-11)看起来也应该很熟悉了。它创建几个EffectsWidget副本,每个副本分别实现自己的效果。底部右栏的部件很特别。它包含一个动画背景。
例8-11:Effects应用
private AnimationDrawable buildEfxView(LinearLayout lv, LinearLayout rv) { lv.addView(new EffectsWidget( this, 1, new EffectsWidget.PaintEffect { @Override public void setEffect(Paint paint) { paint.setShadowLayer(1, 3, 4, Color.BLUE); } })); lv.addView(new EffectsWidget( this, 3, new EffectsWidget.PaintEffect { @Override public void setEffect(Paint paint) { paint.setShader( new LinearGradient( 0.0F, 0.0F, 110.0F, 10.0F, new int { Color.BLACK, Color.RED, Color.YELLOW }, new float { 0.0F, 0.5F, 0.95F }, Shader.TileMode.REPEAT)); } })); lv.addView(new EffectsWidget( this, 5, new EffectsWidget.PaintEffect { @Override public void setEffect(Paint paint) { paint.setMaskFilter( new BlurMaskFilter(2, BlurMaskFilter.Blur.NORMAL)); } })); lv.addView(new EffectsWidget( this, 2, new EffectsWidget.PaintEffect { @Override public void setEffect(Paint paint) { paint.setShadowLayer(3, -8, 7, Color.GREEN); } })); rv.addView(new EffectsWidget( this, 4, new EffectsWidget.PaintEffect { @Override public void setEffect(Paint paint) { paint.setShader( new LinearGradient( 0.0F, 40.0F, 15.0F, 40.0F, Color.BLUE, Color.GREEN, Shader.TileMode.MIRROR)); } })); View w = new EffectsWidget( this, 6, new EffectsWidget.PaintEffect { @Override public void setEffect(Paint paint) { } }); rv.addView(w); w.setBackgroundResource(R.drawable.throbber); return (AnimationDrawable) w.getBackground;}
图8-5给出了以上代码的运行效果。正如之前提到的,widget 6是动画模式展示。我们很快会看到,其背景是红色在闪烁。
图8-5:图形效果
在下一节中我们将对每种效果进行深入讨论。
阴影、渐变、滤镜和硬件加速器
PathEffect、MaskFilter、ColorFilter、Shader和ShadowLayer都是Paint的属性。任何使用Paint绘制的图形都可以使用一种或多种属性来完成变换。图8-5所示的前5个示例给出了其中几种效果。
widget1和widget2显示的是阴影效果。阴影通过setShadowLayer方法控制,参数是半径和x、y位移,控制创建阴影的光源的距离和位置。
第二行的widget显示的是着色。Android工具箱包含一些预构建的着色。widget3和widget4显示了其中一个着色方案——LinearGradient shader(线性渐变着色)。渐变是指在所使用的色彩之间逐渐过渡,例如,给页面背景增添一点生机而不需要使用执行代价很高的位图资源。LinearGradient通过向量指定,它决定颜色过渡的方向和比率,要过渡的一组色彩及模式。最后一个参数mode(模式)决定当全部渐变无法覆盖整个绘制的对象时如何处理。例如,在widget4中,渐变转换的长度只涵盖15个像素,而绘图却超过100个像素。可以使用模式Shader.TileMode.Mirror,使渐变不断重复,沿着绘制方向交替。在这个例子中,渐变转换在15个像素内从蓝色变成绿色,在下一个15像素内从绿色变成蓝色,这样不断交替,直到覆盖满整个画布。
随着Honeycomb的发布,UI框架重构带来的影响之一是限制或完全不支持一些很不常见的绘制效果,比如之前提到的drawTextOnPath和drawTextPos方法。setShadowLayer方法还是可以正常工作,但只适合文本类型。如果widget 1和widget 2都使用新的、硬件加速的图形处理机制,文本会以阴影方式显示,但其上面的线条不会。
可以强制Honeycomb设备以兼容模式启动,这样这些方法还会以Honeycomb之前的方式渲染。强制使用之前的渲染机制(通过软件,而不是硬件渲染)的最佳方式是在应用的manifest文件中添加一个属性,如例8-12所示。
例8-12:取消硬件加速
<application ... android:hardwareAccelerated="false">
实际上,这也是默认方式。为了利用Honeycomb之后带来的硬件加速特性,必须在应用中设置该属性值为true。否则,毫无疑问,当设备升级成Android v3或更新版本后,成百上千的应用会停止正常工作。一定要记住,除非主动在应用中支持硬件加速,否则这个特性就不会用上。
除了硬件加速以外,还可以对应用有更细粒度的控制。除了在manifest文件的application元素中添加android:hardwareAccelerated属性(该属性会影响整个应用),还可以为单个活动应用该属性。如果一个活动不能使用新的绘图机制渲染,可以把该属性设置成false,而把其他活动设置成true。
例8-13给出的两个代码片段说明了如何以更细粒度(比如窗口和视图级别)来控制硬件加速。
例8-13:细粒度级别取消硬件加速
getWindow.setFlags( WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);myView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
然而,要记住的是,你的目标应该是使应用能够在打开硬件加速的模式下正常工作。除了硬件加速外,没有其他的优化措施,尤其对于新的设备,新设备往往会充分利用由硬件优化渲染带来的速度和可用性的提高。
动画
Android UI工具提供多种动画工具。渐变动画(transition animations),Google文档称之为渐变动画(tweened animations),是android.view.animation.Animation的子类:RotateAnimation、TranslateAnimation、ScaleAnimation等。这些动画用作为视图之间的过渡。第二种类型的动画,android.graphics.drawable.AnimationDrawable.AnimationDrawable的子类,有很多不同的效果,可以给任何widget提供背景。最后,在SurfaceView上有完整的动画类,它可以对自己执行的动画完全控制。
因为前两种动画,渐变和背景类型都是通过View实现的,所以它们几乎都可以用于所有widget。
渐变动画
渐变动画是通过调用View的startAnimation方法执行的,包含Animation的实例(或者你自己的子类)。一旦安装了渐变动画,动画会一直运行直到结束。渐变动画没有暂停状态。
动画的核心是它的applyTransformation方法。该方法在调用时可以生成动画的连续帧。例8-14说明了一种转换的实现。正如你看到的,它不会为动画真正生成全部的图形帧。相反,它生成要实现动画效果的单个图形的一系列转换,把这些转换应用于图形中。在P225“矩阵转换”一节中提到,矩阵转换可以实现对象运动效果。渐变动画就是依赖于这个特性的。
例8-14:渐变动画
@Overrideprotected void applyTransformation(float t, Transformation xf) { Matrix xform = xf.getMatrix; float z = ((dir > 0) ? 0.0f : -Z_MAX) - (dir * t * Z_MAX); camera.save; camera.rotateZ(t * 360); camera.translate(0.0F, 0.0F, z); camera.getMatrix(xform); camera.restore; xform.preTranslate(-xCenter, -yCenter); xform.postTranslate(xCenter, yCenter);}
这种特殊的实现使得目标看起来在屏幕上旋转(调用rotate方法),同时还在远处渐渐消逝(调用translate方法)。应用于目标图像的矩阵是通过在调用时传递Transformation对象获得的。
该实现使用了camera,它是工具类Camera的实例。Camera类和手机的摄像头不同,它是一个工具,它可以记录渲染的状态。Camera要把旋转和渐变转换组合成单个矩阵,然后把该矩阵储存为动画转换。
applyTransformation方法的第一个参数t,表示的是帧数。该参数的取值范围在浮点型0.0到1.0之间,也可以理解为动画完成的百分比。该示例使用t表示执行动画效果的图像沿着z轴(和屏幕垂直的一条线)的距离的递加,以及设置该图像通过的全部旋转比例。随着t的增加,动画图像看起来向远处不断逆时针旋转,沿着z轴,变得越来越远。
为了让图像沿着中心变换,preTranslate和postTranslate操作是必要的。默认情况下,矩阵操作是围绕原点(左上角)变换的。如果我们不执行这些围绕变换,则目标图像看起来会沿着左上角旋转。preTranslate操作有效地把原点转移到动画目标的中心,postTranslate操作在转换完成后恢复默认的原点。
如果考虑渐变动画可以实现什么效果,就会发现它实际上可以组成两个动画:前一个屏幕向外执行动画,后一个向内执行动画。例8-14使用变量dir来表示。变量dir的值是1或-1,控制动画图像是向远处渐渐缩小还是向前不断变大。我们只需要找到一种方式来构建不断缩小和不断增长的动画。
这是通过我们所熟悉的Listener模式实现的。Animation类定义了一个监听器,名为Animation.AnimationListener。任何Animation实例都包含一个非空的监听器,启动和停止时都会调用一次监听器,中间每次迭代时也调用一次。监听器会关注缩小的动画何时执行完成,完成后产生一个新的不断变大的动画,其效果和我们期望的一致。例8-15给出了该动画后面部分的实现。
例8-15:渐变动画的构建
public void runAnimation { animateOnce(new AccelerateInterpolator, this);}@Overridepublic void onAnimationEnd(Animation animation) { root.post(new Runnable { public void run { curView.setVisibility(View.GONE); nextView.setVisibility(View.VISIBLE); nextView.requestFocus; new RotationTransitionAnimation(-1, root, nextView, null) .animateOnce(new DecelerateInterpolator, null); } });}void animateOnce( Interpolator interpolator, Animation.AnimationListener listener){ setDuration(700); setInterpolator(interpolator); setAnimationListener(listener); root.startAnimation(this);}
runAnimation方法启动渐变操作。onAnimationEnd方法覆盖了AnimationListener方法,执行后面的操作。当目标图像看起来在很远处时,它隐藏了向外显示动画的图像(curView),以新的图像nextView取代它。然后,它创建新的动画,沿着相反的方向运行,旋转和增长新的图像到前台。
Interpolator类表示对细节的关注。传递给applyTransformation的参数t的值,不需要是随时间推移呈线性分布。在实现中,动画看起来在加速回落,然后随着新的图像出现又缓慢下来。该效果是通过两个interpolator实现的,前半个动画是AccelerateInterpolator,后半个动画是DecelerateInterpolator。如果没有interpolator,那么传递给applyTransformation方法的连续的t值是等差的。这使得动画看起来在匀速运动。AccelerateInterpolator对这些等差t值进行转换,在开始时差值较小,接近相同,此后不断增大。这使得动画看起来是加速的。DecelerateInterpolator的效果刚好相反。Android还提供了CycleInterpolator和LinearInterpolator。
动画组合实际上是在工具箱内构建的,使用AnimationSet类(该名字可能有点让人混淆)。AnimationSet类可以很容易指定列表(list),而不是集合(set),它是有序的,可能顺序多次指向要播放的动画。此外,工具箱提供了一些标准的渐变方式:AlphaAnimation、RotateAnimation、ScaleAnimation和TranslateAnimation。当然,正如前面的例子所示,这些渐变动画不需要对称。当老的图像慢慢淡入一个角落时,新的图像可能就以alpha方式淡出;或者当老的图像淡出时,新的图像可能就从底部出来。有无穷多种的显示方式。
背景动画
Google文档中所描述的逐帧动画(frame-by-frame animation),是非常简洁明了的:一组帧,定期顺序播放。这种动画类型是通过AnimationDrawable子类实现的。
作为Drawable的子类,AnimationDrawable对象可以用于任何能够使用Drawable对象的场景。但是,Drawable本身不包含动画机制。为了实现动画,AnimationDrawable依赖于外部的服务提供者(实现接口Drawable.Callback)来执行动画。
View类实现了Drawable.Callback接口,它可以用于执行AnimationDrawable动画。遗憾的是,它只为以Drawable对象为背景的对象提供动画服务。
但是,好消息,这已经够用了。背景动画能够访问全部widget画布。背景动画所绘制的内容都在View.onDraw方法绘制的内容之后显示,因此很难使用背景动画来实现全部效果。然而,只要善于使用DrawableContainer类(它支持同时执行几个不同的动画),而且因为背景可以随时改变,所以有可能不需要自己实现动画框架就能够实现很多功能。
视图背景中的AnimationDrawable完全能够执行所有操作,比如表示正在执行一些需要长时间处理的操作,可能是一个带翅膀的包从手机飞向高楼,对此只需要把背景变成按钮脉冲。
widget6中跳动的按钮富有说明意义且非常容易实现。例8-16和例8-17给出了所有的代码。动画被定义成resource,代码把它应用到按钮上。可以使用setBackgroundDrawable或setBackgroundResource把背景设置成Drawable对象。
例8-16:逐帧动画(resource)
<animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:oneshot="false"> <item android:drawable="@drawable/throbber_f0" android:duration="160" /> <item android:drawable="@drawable/throbber_f1" android:duration="140" /> <item android:drawable="@drawable/throbber_f2" android:duration="130" /> <item android:drawable="@drawable/throbber_f3" android:duration="100" /> <item android:drawable="@drawable/throbber_f4" android:duration="130" /> <item android:drawable="@drawable/throbber_f5" android:duration="140" /> <item android:drawable="@drawable/throbber_f6" android:duration="160" /></animation-list>
例8-17给出了运行动画的代码。主视图的onClickListener方法会启动动画过渡并切换视图,因此在下一次点击时,主视图会继续在不同视图间切换。此外,还会对背景动画进行切换,这样当主视图退出运行时,就不可见了。
注意:在老版本的Android中,Activity类没有提供onCreate方法来启动后台动画,因此为了能够正常运行,需要某些技巧。这个Bug在API 6(Muffin)中修复了,只需要执行((AnimationBackground)view.getBackground).start,就可以正常工作。
例8-17:逐帧动画(代码)
@Overridepublic void onCreate(Bundle savedInstanceState) { // .... code elided // install the animation click listener final View root = findViewById(R.id.main); findViewById(R.id.main).setOnClickListener( new OnClickListener { @Override public void onClick(View v) { new RotationTransitionAnimation(1, root, cur, next) .runAnimation; // exchange views View t = cur; cur = next; next = t; toggleThrobber; } });}// .... code elidedvoid toggleThrobber { if (null != throbber) { if (efxView.equals(cur)) { throbber.start; } else { throbber.stop; } }}
有必要指出如果你使用过其他的UI框架,尤其是移动UI框架,你可能会习惯于在onDraw方法(或相关的)的前面几行执行背景绘画。如果在这里执行背景绘画,会覆盖了动画。通常来说,使用setBackground方法来控制View的背景是一个良好的习惯,不管是某个纯色、渐变、图像还是动画。
通过resource指定DrawableAnimation是非常灵活的。可以指定一个drawable resource列表——任何想要的构成动画的图像。如果动画需要动态,AnimationDrawable是创建动态的drawable的很好方法,它可以在View的背景中执行动画。
平面视图动画
全屏动画需要SurfaceView。SurfaceView在视图树中提供了一个节点以及显示的空间,任何进程可以在该空间上绘制。当展开SurfaceView节点时,它会和其他的widget一样,接收单击和更新。但是,它不执行绘制,而只是保存屏幕上的空间,避免其他的widget影响框架内的任何像素。
在SurfaceView上绘制需要实现SurfaceHolder.Callback接口。surfaceCreated方法和surfaceDestroyed方法分别通知实现程序绘制平面可用和不可用。调用这两个方法所传递的参数是类SurfaceHolder的实例。在执行这两个方法的调用之间,绘制程序可以调用SurfaceView的lockCanvas方法和unlockCanvasAndPost方法来编辑像素。
以上过程看起来确实比较复杂,虽然有些接近于前面给出的动画。同样,并发问题还是增加了一些令人讨厌且难以查找的Bug。SurfaceView的客户端必须确保访问线程间的任何状态都被正确同步,而且除了在surfaceCreated方法和surfaceDestroyed方法之间的调用,其他地方都没有使用到SurfaceView、Surface或Canvas。很明显,工具箱可以得到更全面的SurfaceView动画的框架支持。
如果考虑使用SurfaceView动画,那你很可能也考虑使用OpenGL图形,因为在SurfaceView上存在OpenGL动画的扩展,虽然这块知识相对比较隐蔽。
OpenGL图形
Android平台对OpenGL图形技术的支持可以说是锦上添花。虽然支持OpenGL图形技术是Android提供的最令人兴奋的技术之一,但它绝对只是Android中比较边缘的技术。随着Honeycomb的发布,OpenGL完全集成到了Android图形技术中。早期的Android版本只支持OpenGL 1.0和1.1,而根据文档,Honeycomb不仅支持OpenGL 2.0,而且还把它作为渲染View对象的基础。OpenGL实质上是一门嵌入在Java中的特定领域的语言。和Java程序员(甚至是Java专家)相比,游戏UI的开发人员很可能会对开发Android OpenGL程序更得心应手。
在讨论OpenGL图形库之前,我们应该先花点时间弄清使用OpenGL绘制的像素在屏幕上是如何显示的。到目前为止,本章已经探讨了Android用来对对象进行组织并显示在屏幕上的复杂的视图框架。OpenGL是一门语言,这门语言的应用程序的运行环境不仅可能在JVM之外的引擎上运行,而且很有可能运行于其他处理器上(图形处理单元Graphics Processing Unit,或称GPU)。协调屏幕上两个进程之间的视图是非常棘手的。
前面提到的SurfaceView差不多够用了。SurfaceView的目的是创建一个平面,除UI图形之外的线程都可以在这个平面上绘图。我们将要讨论的工具是SurfaceView的扩展,它提供了更多的并发支持和OpenGL支持。Android框架中确实存在该工具。Android SDK分发的所有演示应用所执行的OpenGL动画都依赖于工具类GLSurfaceView。因为演示应用是由Android的创建者编写的,它使用了GLSurfaceView这个类,所以在你的应用中也可以考虑使用该类。
GLSurfaceView定义了接口GLSurfaceView.Renderer,它极大地简化了使用OpenGL和GLSurfaceView的复杂性。GLSurfaceView调用渲染方法getConfigSpec来获取OpenGL配置信息。GLSurfaceView还调用两个方法:sizeChanged和surfaceCreated,它们分别用于通知渲染程序其方法发生了改变以及它应该准备绘制。最后,调用接口的核心drawFrame来渲染新的OpenGL帧。
例8-18说明了实现OpenGL渲染器的重要方法
例8-18:使用OpenGL的逐帧动画
// ... some state set up in the constructor@Overridepublic void surfaceCreated(GL10 gl) { // set up the surface gl.glDisable(GL10.GL_DITHER); gl.glHint( GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_FASTEST); gl.glClearColor(0.4f, 0.2f, 0.2f, 0.5f); gl.glShadeModel(GL10.GL_SMOOTH); gl.glEnable(GL10.GL_DEPTH_TEST); // fetch the checkerboard initImage(gl);}@Overridepublic void drawFrame(GL10 gl) { gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT); gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity; GLU.gluLookAt(gl, 0, 0, -5, 0f, 0f, 0f, 0f, 1.0f, 0.0f); gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY); // apply the checker-board to the shape gl.glActiveTexture(GL10.GL_TEXTURE0); gl.glTexEnvx( GL10.GL_TEXTURE_ENV, GL10.GL_TEXTURE_ENV_MODE, GL10.GL_MODULATE); gl.glTexParameterx( GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_REPEAT); gl.glTexParameterx( GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_REPEAT); // animation int t = (int) (SystemClock.uptimeMillis % (10 * 1000L)); gl.glTranslatef(6.0f - (0.0013f * t), 0, 0); // draw gl.glFrontFace(GL10.GL_CCW); gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuf); gl.glEnable(GL10.GL_TEXTURE_2D); gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, textureBuf); gl.glDrawElements( GL10.GL_TRIANGLE_STRIP, 5, GL10.GL_UNSIGNED_SHORT, indexBuf);}private void initImage(GL10 gl) { int textures = new int[1]; gl.glGenTextures(1, textures, 0); gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]); gl.glTexParameterf( GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST); gl.glTexParameterf( GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR); gl.glTexParameterf( GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE); gl.glTexParameterf( GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE); gl.glTexEnvf( GL10.GL_TEXTURE_ENV, GL10.GL_TEXTURE_ENV_MODE, GL10.GL_REPLACE); InputStream in = context.getResources.openRawResource(R.drawable.cb); Bitmap image; try { image = BitmapFactory.decodeStream(in); } finally { try { in.close; } catch(IOException e) { } } GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, image, 0); image.recycle;}
surfaceCreated方法负责前期准备。它设置widget绘制新的平面时需要初始化的一些OpenGL属性。此外,它还调用initImage方法,该方法会读取位图资源,并把它存储为二维结构。最后,调用drawFrame方法,绘制的准备工作至此基本完成。把之前保存的二维结构应用到平面上,它的节点是通过构造函数的vertexBuf设置的;选择动画的阶段;重新绘制场景。图8-6给出了运行OpenGL的示例。
图8-6:OpenGL绘制
值得注意的是,你应该一直记着视图嵌入在活动中,是有生命周期的!如果你使用的是OpenGL,当该活动不可见时,要记得结束长期运行的动画进程。