65.9K
CodeProject 正在变化。 阅读更多。
Home

第11部分 - Android 动画/图形新手指南

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2014 年 10 月 6 日

CPOL

39分钟阅读

viewsIcon

51483

downloadIcon

1596

您想要的、 最简单的图形和动画教程。

目录

1. 背景

2. OpenGL ES
    2.1 使用 OpenGL ES 2.0 在 Android 中开始 2D 图形
    2.2 OpenGL ES 2.0 绘图基础
    2.3 在 OpenGL ES 2.0 中处理触摸事件
    2.4 OpenGL ES 2.0 2D 动画
    2.5 使用 Android OpenGL ES 2.0 进行 3D 图形绘制
           2.5.1 3D 图形基础
           2.5.2 使用 OpenGL 进行 3D 图形绘制

3. 视图动画
     3.1 准备应用以同时使用 OpenGL 和视图动画
     3.2 基于 XML 的视图动画基础

4. 属性动画
     4.1 Value Animator
     4.2 Object Animator
     4.3 AnimatorSet

5. Drawable 动画

6. Canvas API

7. 结论

亮点

  • 一个利用三角形概念绘制所有 OpenGL 原语的教程
  • 在 OpenGL 中从 2D 原语开发 3D 原语
  • OpenGL 触摸事件处理器
  • 在同一应用程序中同时使用 OpenGL 和 Canvas
  • Canvas API 的抽象
  • Drawable 动画部署问题解决方案
  • 所有动画的用例讨论

1. 背景

Android 作为移动平台,是一些高端手机上流行的移动操作系统之一。用户在智能手机上花费大量金钱,因为它们的实用性。其中一个因素是游戏和优秀的应用程序。许多娱乐应用程序只需要良好的动画功能和动画对象无闪烁渲染。此外,动画必须与用户输入(如触摸和传感器)绑定,以使环境更直观。OpenGL 是一个开源跨平台图形平台,它利用设备功能,并使用硬件功能创建和操作图形。

本教程将尽我所能,尽可能流畅地教授 Android 图形和动画的基础知识。像往常一样,我们还将看到一些用例和操作方法。但由于主题广泛,我们将针对每个概念使用简单的应用程序,而不是像我们大多数教程那样只使用一个应用程序。

2. OpenGL ES

到目前为止,我们学习的 Android 知识中,都使用 Android View 来绘制对象。图像绘制在 Image Views 上,Button、EditText、TextView 都是放置在主表单的 ContentView 上的视图。

然而,视图是高级类。当我们谈论 OpenGL 时,我们谈论的是硬件级别的绘制和操作。因此,Android 视图不适合 OpenGL 操作。因此,我们需要创建一个 OpenGL SurfaceView,它是 OpenGL 动画的主要视图,并将当前的 ContentView 设置为该 SurfaceView。现在,我们绘制或操作的任何内容都将在 OpenGL SurfaceView 上。

我们可以使用 OpenGL 渲染器渲染任意数量的对象。什么是渲染器?例如,如果您在屏幕上绘制一个三角形并想旋转它,或者您有一条线想在整个场景中移动,那么使用标准图形编程,您必须在每次更新时计算并更新末端坐标(称为顶点坐标)。渲染器是一个负责此操作的引擎。因此,您只需告诉对象需要移动到哪里以及以什么角度移动,底层的几何形状将由渲染器处理。

渲染器可以在 Surface View 上渲染的 OpenGL 对象可以是形状对象,如多边形、矩形、三角形等,也可以是图像,或者是组合了多个基本对象的复杂对象。这些对象中的每一个都可以具有几何形状和坐标系,用于在屏幕上绘制它们,并且还可以具有着色器。着色器是一种通过颜色或纹理为对象着色的概念。

图 2.1 清楚地解释了这些概念。

图 2.1 Android 中的 OpenGL ES 工作流程

此时,您应该知道 Android 对 OpenGL 的支持可以明显分为两类主要的 API:OpenGL ES 1.x 和更现代的 OpenGL ES 2.x。

这两个 API 类别在主要方面彼此不同,在大多数情况下不能一起使用。所有现代和新设备都支持 ES 2.x API。因此,本教程中我们将只使用 OpenGL ES 2.0,毕竟谁想在 Windows 8x 时代学习 Windows 98 呢。

2.1 使用 OpenGL ES 2.0 在 Android 中开始 2D 图形

首先,我们需要通过 Manifest 文件告知当前应用程序必须使用 OpenGL,这样当您发布应用程序时,它就不会显示在不支持该应用程序的设备上。

<uses-feature android:glEsVersion="0x00020000" android:required="true" />

正如我们所讨论的,我们将在活动类中使用 GLSurfaceView 作为主视图。因此,您的 xml 布局中不需要太多内容。只需创建一个如下所示的简单布局即可。

<LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:orientation="vertical" >

</LinearLayout>

就是这么简单。现在您需要一个 GLSurfaceView 对象作为主视图。但是,您将使用自己的逻辑渲染自己的对象。因此,扩展该类并覆盖您想要的方法是有意义的。

因此,首先通过扩展 GLSurfaceView 创建一个名为 MyGlSurfaceView 的简单类

 

public class MyGlSurfaceView extends GLSurfaceView 
{

    MyRenderer mr;
    TextView tv;
    MainActivity maContext;
    public MyGlSurfaceView(Context context) 
    {
          super(context);
            
          setEGLContextClientVersion(2);
      
        setRenderer(new Renderer() {
            
              public void onSurfaceCreated(GL10 unused, EGLConfig config) {
                    // Set the background frame color
                    GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);// r g b alpha
                }

                public void onDrawFrame(GL10 unused) {
                    // Redraw background color
                    GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
                }

                public void onSurfaceChanged(GL10 unused, int width, int height) {
                    GLES20.glViewport(0, 0, width, height);
                }
        });
          setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
    }
    
     @Override
     public boolean onTouchEvent(MotionEvent e) 
     {
         final float x=e.getX();
         final float y=e.getY();

        return false;
         
     }
    

}

当您创建一个扩展 GLSurfaceView 的类时,它会提示您实现构造函数。您应该通过调用 super 实例化您的 SurfaceView 类的对象来实现在构造函数。此外,使用 setEGLContextClientVersion(2) 告诉 Android 您将使用 ES 版本 2。

在构造函数中,您必须设置渲染器。假设我们目前没有任何要渲染的内容,我们可以简单地编写 setRenderer(new Renderer() ); 一旦您这样做,Eclipse 将自动创建用于创建 Renderer 类对象的方法。一旦方法创建完成,我们将使用简单的单行代码来完成初始化工作。

有三种方法需要覆盖:

 

  • onSurfaceCreated:当 GLSurface 实例初始化渲染对象时调用。它使用 glClearColor 将背景清除为黑色。最后一个参数是 ALPHA 或透明度参数。您可以尝试使用前三个参数 r、g、b 值来使用其他颜色。

 

  • onDrawFrame:每当需要重新绘制(或渲染更改)时调用。每次屏幕渲染时,我们都会清除绘图缓冲区并预设。由于我们打算进行彩色绘图,我们使用 GL_COLOR_BUFFER_BIT。您还可以根据您的应用程序和渲染偏好使用 GL_DEPTH_BUFFER_BITGL_STENCIL_BUFFER_BIT
  • onSurfaceChanged:当您更改设备视图时调用,例如从横向到纵向。因此,我们将使用 GLES20.glViewport 选项根据当前设备方向重新初始化视口。您可能已经知道视口是多边形查看区域,这是对象渲染的区域。

 

最后,您可以看到 setRenderMode 选项,它可以设置为 RENDERMODE_WHEN_DIRTYRENDERMODE_CONTINUOUSLY。Android 的典型帧速率约为 10fps。保持渲染模式 CONTINUESLY 将强制在每个刷新实例中渲染或重新绘制所有图形对象。WHEN_DIRTY 仅当渲染器对象的任何矩阵发生更改时,才强制调用渲染器的 OnFrameDraw。我们很快就会了解矩阵。

 

尽管您正在使用 GL20,但在参数中使用 GL10 可能会让您感到惊讶。不要被它迷惑,这是为了保持与以前 GL 版本的连续性,并且大部分时间都不会使用。

回到 Renderer 类选项

尽管扩展 GLSurfaceView 不会提示您覆盖 OnTouchEvent,但我仍建议您在应用程序的基本结构中使用此方法。

好的,现在让我们将注意力转向 MainActivity。和您一样,我也非常渴望在手机上看到我的第一个 OpenGL 应用程序。

让我们在 MainActivity 中声明一个 MyGlSurfaceView 类的对象作为类成员

MyGlSurfaceView mgsv;

让我们在 onCreate 中通过传递 MainActivity 对象来初始化对象。请记住,在其他 Android 应用程序中,我们使用 setContentView(R.layout.activity_main)。但在这里,我们并不真正关心 activity_main。因此,我们只是将 R.layout.activity_main 替换为 MyGlSurfaceView 对象。

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //setContentView(R.layout.activity_main);
        
         mgsv=new MyGlSurfaceView(this);
        setContentView(mgsv);
    }

完成我们的辛勤工作后,让我们构建并运行我们的应用程序。以下是使用 GLES20.glClearColor(7.0f, 2.0f, 0.0f, .20f); 运行应用程序的屏幕截图

图 2.2 第一个 OpenGL 屏幕

好的!我们现在准备更详细地使用 OpenGL ES API。但是在此之前,我想为我们的讨论增添一些我没能在其他文章中找到的趣味。

通常需要从 OpenGL 向 Activity 表单发送一些数据。例如,您可以考虑将简单游戏的得分返回到活动,在那里将其更新到数据库,或者您可以考虑在活动中触发 Toast 以进行一些 OpenGL 工作。因此,我们正在讨论将一些数据或消息传递回调用表单。

为了能够与活动实例进行通信,您所要做的就是在 SurfaceView 类中创建一个 Activity 对象,并使用从活动传递的上下文初始化该对象。因此,让我们从触摸事件处理程序跟踪 x 和 y 坐标,并将该数据传递到 MainActivity 表单的标题。

在 MySurfaceView 类中,声明

MainActivity maContext;

在 MySurfaceView 的构造函数中,初始化

maContext=(MainActivity) context;

最后,从 onTouchEvent 将 x 和 y 数据放入标题!

 @Override
     public boolean onTouchEvent(MotionEvent e) 
     {
         final float x=e.getX();
         final float y=e.getY();
         Log.i("GL Surface View","X="+x+" Y="+y);
         maContext.runOnUiThread(new Runnable() {

             @Override
             public void run() {
                 maContext.setTitle("X="+x+" Y="+y);    
             }
         });
        return false;
         
     }

现在,当您调试并运行应用程序并在屏幕上移动手指时,您将在标题栏中看到 x 和 y 值。

图 2.3 从 OpenGL 到 MainActivity 的消息传递

2.2 OpenGL ES 2.0 绘图基础

本文假设您没有 OpenGL 方面的先验知识。如果您曾经使用过 OpenGL,您必须知道在 OpenGL ES 2.0 中绘图并非如您所想的那样直接。其次,如果您使用过其他图形平台,例如 Android 的原生图形支持或 .Net GDI+,您可能已经习惯了使用诸如 drawLine、drawCircle 等调用进行绘图。但 OpenGL 利用您的硬件进行图形渲染。因此,即使是绘制直线和正方形等简单形状也需要大量的代码。

所以,我们在这里做一些非常特别的事情

我们首先学习绘图的要点,这有助于您理解绘图的机制,然后我们将开发一个带有 API 的自定义绘图类,使您可以使用更传统的绘图调用轻松绘制简单形状。但首先是首先。

为了理解绘制一个对象需要什么,尝试分析图 2.4

图 2.4 OpenGL ES 2.0 绘图逻辑

1) 首先,您想要绘制的任何内容都需要指定为一组顶点坐标。顶点必须形成一个闭合图形。三角形将有三个顶点,正方形将有四个。为了定义一条线,需要将其视为一个宽度非常薄的矩形;一个圆形可能有 360 个顶点,根据中心和半径计算。一个点可以定义为一个半径非常小的圆形。

2) OpenGL 使用着色器绘制对象。因此,顶点和颜色代码需要传递到着色器。着色器首先使用顶点着色器绘制顶点,然后使用片段着色器渲染其表面。整个着色器系统必须预编译并作为 OpenGL 程序保存,该程序将用于绘制形状。

3) 投影:它将 OpenGL 坐标系映射到设备坐标系。

4) 摄像机对象是一个虚拟对象,使视图更逼真。您可以实际更改摄像机位置,以从不同角度呈现视图。这对于 3D 渲染通常非常重要。

毋庸置疑,draw 是从渲染器的 onFrameDraw 中调用的。

每当你在网上看到关于 OpenGL 的教程时,它们都会为不同的形状呈现不同的类,因为 OpenGL 的方法对于每种形状都略有不同。但正如你在图 2.4 中看到的那样,基本的渲染系统并没有改变,我们在这里会非常聪明地玩,通过创建一个简单的绘图逻辑来缩短学习曲线。

所以准备 MySimpleOpenGLES2DrawingClass

首先我们来定义着色器。

 private final String vertexShaderCode =
                // This matrix member variable provides a hook to manipulate
                // the coordinates of the objects that use this vertex shader
                "uniform mat4 uMVPMatrix;" +
                "attribute vec4 vPosition;" +
                "void main() {" +
                // The matrix must be included as a modifier of gl_Position.
                // Note that the uMVPMatrix factor *must be first* in order
                // for the matrix multiplication product to be correct.
                "  gl_Position = uMVPMatrix * vPosition;" +
                "}";

        private final String fragmentShaderCode =
                "precision mediump float;" +
                "uniform vec4 vColor;" +
                "void main() {" +
                "  gl_FragColor = vColor;" +
                "}";


它们是简单的 C 语言程序,放在一个字符串中。vertexShader 期望一个投影矩阵和顶点坐标,并通过将两者相乘来生成最终坐标。

fragmentShader 最直接,将颜色变量赋值给 gl_fragColor。

我们首先需要用我们的着色器代码加载着色器,然后进行编译。

GLES20.glShaderSource(source,type) 可以使用 fragmentShaderCode 或 vertexShaderCode 调用,类型分别为 GLES20.GL_FRAGMENT_SHADERGLES20.GL_VERTEX_SHADER。此方法返回一个整数着色器代码。这可以使用 GLES20.glCompileShader(shader) 进行编译。为了避免代码混乱,让我们创建一个简单的实用方法 loadShader,如下所示

  public static int loadShader(int type, String shaderCode){

            // create a vertex shader type (GLES20.GL_VERTEX_SHADER)
            // or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
            int shader = GLES20.glCreateShader(type);

            // add the source code to the shader and compile it
            GLES20.glShaderSource(shader, shaderCode);
            GLES20.glCompileShader(shader);

            return shader;
        }

从图 2.4 可以清楚地看出,我们必须传递形状的坐标和颜色。系统必须通过将它们传递到着色器代码中,然后创建一个预编译的程序来创建 OpenGL 对象。这个程序将与摄像机和投影对象一起用于绘制形状。

正如我们从本小节顶部的讨论第 1 点已经知道的那样,顶点必须形成一个闭合路径。三角形可以被认为是一种基本形状。其余的形状可以通过遵循特定路径从这种形状定义。

图 2.5 解释了如何使用三角形绘制来绘制不同的形状。我使用三角剖分是因为这也是 3D 网格的基本构建块。因此,使用相对相似的 2D 概念有助于简化对 3D 的理解。

图 2.5:在 OpenGL ES 2.0 中定义形状(三角形方法)

从上图可以很清楚地看出,无论我们想绘制什么形状,只要我们成功绘制了三角形,其余的都只是小菜一碟。

现在让我们为 MyOpenGLES2DrawingClass 提供一个参数化构造函数

 public MyGeneralOpenGLES2DrawingClass(int coordsPerVertex,float []coordinates,float[]color,short[]drawOrder)
        {
            this.drawOrder=drawOrder;
            COORDS_PER_VERTEX=coordsPerVertex;
            coords=coordinates;
            
            vertexCount = coords.length / COORDS_PER_VERTEX;
            vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex as float has 4 bytes
            this.color=color;
            
            
            ////////////////////////////////////////
         // initialize vertex byte buffer for shape coordinates
            ByteBuffer bb = ByteBuffer.allocateDirect(
                    // (number of coordinate values * 4 bytes per float)
                    coords.length * 4);
            // use the device hardware's native byte order
            bb.order(ByteOrder.nativeOrder());

            // create a floating point buffer from the ByteBuffer
            vertexBuffer = bb.asFloatBuffer();
            // add the coordinates to the FloatBuffer
            vertexBuffer.put(coords);
            // set the buffer to read the first coordinate
            vertexBuffer.position(0);
            ///////////////////////////////////
            ByteBuffer dlb = ByteBuffer.allocateDirect(
                    // (# of coordinate values * 2 bytes per short)
                    drawOrder.length * 2);
            dlb.order(ByteOrder.nativeOrder());
            drawListBuffer = dlb.asShortBuffer();
            drawListBuffer.put(drawOrder);
            drawListBuffer.position(0);
            //////////////////////////////////////////
         //   prepare shaders and OpenGL program
            int vertexShader = loadShader(
                    GLES20.GL_VERTEX_SHADER, vertexShaderCode);
            int fragmentShader = loadShader(
                    GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode);

            mProgram = GLES20.glCreateProgram();             // create empty OpenGL Program
            GLES20.glAttachShader(mProgram, vertexShader);   // add the vertex shader to program
            GLES20.glAttachShader(mProgram, fragmentShader); // add the fragment shader to program
            GLES20.glLinkProgram(mProgram);                  // create OpenGL program executables

            /////////////////////////////////////////////////
        }


一旦我们理解了图 2.4 和图 2.5,上面的代码就会变得非常容易。我们用所有的坐标值初始化 vertexBuffer。mProgram 是一个整数变量,它类似于预编译的 OpenGL 代码的指针。程序首先附加 vertexShader,然后是 fragmentShader。最后,它被预编译。

我们已经为 draw 方法准备好所有必需的要素。让我们也完成 draw 方法,这不应该太困难。

        public void draw(float[] mvpMatrix) {
            // Add program to OpenGL environment
            GLES20.glUseProgram(mProgram);

            // get handle to vertex shader's vPosition member
            mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");

            // Enable a handle to the triangle vertices
            GLES20.glEnableVertexAttribArray(mPositionHandle);

            // Prepare the triangle coordinate data
            GLES20.glVertexAttribPointer(
                    mPositionHandle, COORDS_PER_VERTEX,
                    GLES20.GL_FLOAT, false,
                    vertexStride, vertexBuffer);

            // get handle to fragment shader's vColor member
            mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");

            // Set color for drawing the triangle
            GLES20.glUniform4fv(mColorHandle, 1, color, 0);

            // get handle to shape's transformation matrix
            mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
         

            // Apply the projection and view transformation
            GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
         

            // Draw the triangle
            int drawMode=GLES20.GL_TRIANGLES;
            GLES20.glDrawElements( drawMode, drawOrder.length,   GLES20.GL_UNSIGNED_SHORT, drawListBuffer);
            // Disable vertex array
            GLES20.glDisableVertexAttribArray(mPositionHandle);
        }

所以我们首先使用我们创建的 mProgram 对象。使用 glGetAttributeLocation,我们获取 vertextShader 中 vPosition 变量的内存位置。在 mPositionHandle 中获取它之后,我们通过调用 glEnableVertexAttribArray 来启用它以容纳顶点数组。最后,我们将 vertexBuffer 加载到其中。

对于片段着色器,我们获取 vColor 内存位置并加载我们的颜色矩阵。

uMVPMatrixModel-View-Projection-Matrix,是 OpenGL 程序使用的模型矩阵。它由 Renderer 对象根据渲染要求更新。通过调用 glUniformMatrix4fv 方法,可以获得它的指针到 mMVPMatrixHandle 中。最后,我们通过调用 glDrawElements 绘制形状。

这是我们的最终类

package com.integratedideas.animationandghraphics.openglutilities;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.ShortBuffer;


import android.opengl.GLES20;

public class MyGeneralOpenGLES2DrawingClass 
{
     private final String vertexShaderCode =
                // This matrix member variable provides a hook to manipulate
                // the coordinates of the objects that use this vertex shader
                "uniform mat4 uMVPMatrix;" +
                "attribute vec4 vPosition;" +
                "void main() {" +
                // The matrix must be included as a modifier of gl_Position.
                // Note that the uMVPMatrix factor *must be first* in order
                // for the matrix multiplication product to be correct.
                "  gl_Position = uMVPMatrix * vPosition;" +
                "}";

        private final String fragmentShaderCode =
                "precision mediump float;" +
                "uniform vec4 vColor;" +
                "void main() {" +
                "  gl_FragColor = vColor;" +
                "}";

        private final FloatBuffer vertexBuffer;
        private final int mProgram;
        private int mPositionHandle;
        private int mColorHandle;
        private int mMVPMatrixHandle;
        private  ShortBuffer drawListBuffer;
        private  short drawOrder[] = { 0, 1, 2};

        static int COORDS_PER_VERTEX = 0;// Argument1
        static float coords[] = {};// Argument 2
        private final int vertexCount;
        private final int vertexStride; // 4 bytes per vertex
        public float []color=new float[4];
        int drawMode=GLES20.GL_TRIANGLES;
        public MyGeneralOpenGLES2DrawingClass(int coordsPerVertex,float []coordinates,float[]color,short[]drawOrder)
        {
            this.drawOrder=drawOrder;
            COORDS_PER_VERTEX=coordsPerVertex;
            coords=coordinates;
            
            vertexCount = coords.length / COORDS_PER_VERTEX;
            vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex
            this.color=color;
            
            
            ////////////////////////////////////////
         // initialize vertex byte buffer for shape coordinates
            ByteBuffer bb = ByteBuffer.allocateDirect(
                    // (number of coordinate values * 4 bytes per float)
                    coords.length * 4);
            // use the device hardware's native byte order
            bb.order(ByteOrder.nativeOrder());

            // create a floating point buffer from the ByteBuffer
            vertexBuffer = bb.asFloatBuffer();
            // add the coordinates to the FloatBuffer
            vertexBuffer.put(coords);
            // set the buffer to read the first coordinate
            vertexBuffer.position(0);
            ///////////////////////////////////
            ByteBuffer dlb = ByteBuffer.allocateDirect(
                    // (# of coordinate values * 2 bytes per short)
                    drawOrder.length * 2);
            dlb.order(ByteOrder.nativeOrder());
            drawListBuffer = dlb.asShortBuffer();
            drawListBuffer.put(drawOrder);
            drawListBuffer.position(0);
            //////////////////////////////////////////
         //   prepare shaders and OpenGL program
            int vertexShader = loadShader(
                    GLES20.GL_VERTEX_SHADER, vertexShaderCode);
            int fragmentShader = loadShader(
                    GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode);

            mProgram = GLES20.glCreateProgram();             // create empty OpenGL Program
            GLES20.glAttachShader(mProgram, vertexShader);   // add the vertex shader to program
            GLES20.glAttachShader(mProgram, fragmentShader); // add the fragment shader to program
            GLES20.glLinkProgram(mProgram);                  // create OpenGL program executables

            /////////////////////////////////////////////////
        }
       
        public static int loadShader(int type, String shaderCode){

            // create a vertex shader type (GLES20.GL_VERTEX_SHADER)
            // or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
            int shader = GLES20.glCreateShader(type);

            // add the source code to the shader and compile it
            GLES20.glShaderSource(shader, shaderCode);
            GLES20.glCompileShader(shader);

            return shader;
        }
                public void draw(float[] mvpMatrix) {
            // Add program to OpenGL environment
            GLES20.glUseProgram(mProgram);

            // get handle to vertex shader's vPosition member
            mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");

            // Enable a handle to the triangle vertices
            GLES20.glEnableVertexAttribArray(mPositionHandle);

            // Prepare the triangle coordinate data
            GLES20.glVertexAttribPointer(
                    mPositionHandle, COORDS_PER_VERTEX,
                    GLES20.GL_FLOAT, false,
                    vertexStride, vertexBuffer);

            // get handle to fragment shader's vColor member
            mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");

            // Set color for drawing the triangle
            GLES20.glUniform4fv(mColorHandle, 1, color, 0);

            // get handle to shape's transformation matrix
            mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
         

            // Apply the projection and view transformation
            GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
         

            // Draw the triangle
            //GLES20.glDrawArrays(drawMode, 0, vertexCount);
            GLES20.glDrawElements( drawMode, drawOrder.length,   GLES20.GL_UNSIGNED_SHORT, drawListBuffer);
            // Disable vertex array
            GLES20.glDisableVertexAttribArray(mPositionHandle);
        }
}

但在我们测试我们精彩的工作之前,还有一些工作要做。我们需要与渲染器合作。

请记住 图 2.4 中的内容,最终的绘制逻辑应该结合相机视图和投影视图来渲染对象。

我们可以像下面这样使用 Matrix.setLookAtM 初始化一个 ViewMatrix mViewMatrix。

Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);

建议读者在 Eclipse 中将鼠标悬停在此命令上,以获取有关参数的更多详细信息。

通过下图理解投影矩阵会更容易

图 2.6 OpenGL 标准化坐标系和投影概念

从上图可以清楚地看出,最简单的投影只是一个缩放。由于 OpenGL 提供了一个设备无关的标准化坐标系,当屏幕发生变化时(例如从纵向切换到横向),会调用 onSurfaceChanged 方法。因此,我们需要修改此方法以使用从宽度和高度比率获得的缩放比例更新我们的投影矩阵。

 @Override
    public void onSurfaceChanged(GL10 unused, int width, int height) {
        // Adjust the viewport based on geometry changes,
        // such as screen rotation
        GLES20.glViewport(0, 0, width, height);

        float ratio = (float) width / height;

        // this projection matrix is applied to object coordinates
        // in the onDrawFrame() method
        Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);

    }

最后,我们的 onDrawFrame 被修改为

    @Override
    public void onDrawFrame(GL10 unused) {
        float[] scratch = new float[16];

        // Draw background color
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);

        // Set the camera position (View matrix)
        Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);

        // Calculate the projection and view transformation
        Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);


////.................... Drawing Demo...................................................//

    // Line Demo..............................
   
     new MyGeneralOpenGLES2DrawingClass(3, new float[]{ -0.5f,  0.5f, 0.0f,     -0.5f, -0.5f, 0.0f,   -0.49f, -0.5f, 0.0f,       -0.49f,  0.5f, 0.0f }, new float[]{1.0f,0.0f,0.0f,1.0f},new short[]{0,1,2,0,2,3}).draw(mMVPMatrix);
       // Rectangle Demo..............................
        new MyGeneralOpenGLES2DrawingClass(3, new float[]{ -1.0f,  0.5f, 0.0f,     -1.0f, 1.0f, 0.0f,   0, 1.0f, 0.0f,       0,  .5f, 0.0f }, new float[]{0.0f,1.5f,0.0f,1.0f},new short[]{0,1,2,0,2,3}).draw(mMVPMatrix);   
        //Triangle..............................
        new MyGeneralOpenGLES2DrawingClass(3, new float[]{ 0.9f,  0.7f, 0.0f,     .9f,.2f, 0.0f,   .4f, .2f, 0.0f,  }, new float[]{0.0f,0.0f,1.0f,1.0f},new short[]{0,1,2}).draw(mMVPMatrix);
    }


能够通过一行代码并利用我们通用的类来显示一条线、一个三角形和一个矩形,真是太棒了!

图 2.7 使用 OpenGL ES 绘制基本形状的输出

请注意,我们没有绘制圆形。因为使用图 2.4 中给出的模式,创建圆形坐标系很繁琐。{v1,v2,v3,v4,v5....v365} 会是更好的格式。这种格式可以使用 drawArray 方法和 GL_TRIANGLE_FAN 标志绘制。所以我们只需稍作修改我们的通用绘图类

 if(coords.length>100)
            GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, 364);
            else
            GLES20.glDrawElements( drawMode, drawOrder.length,   GLES20.GL_UNSIGNED_SHORT, drawListBuffer);
           
            GLES20.glDisableVertexAttribArray(mPositionHandle);

你只需按照以下代码行即可绘制一个圆形:

  //////////////////////// Circle/////////////////////////////////////
        float vertices[] = new float[364 * 3];
        vertices[0] = 0;
        vertices[1] = 0;
        vertices[2] = 0;
        
          float radious=.5;

        for(int i =1; i <364; i++){
            vertices[(i * 3)+ 0] = (float) (radious * Math.cos((3.14/180) * (float)i ) + vertices[0]);
            vertices[(i * 3)+ 1] = (float) (radious * Math.sin((3.14/180) * (float)i ) + vertices[1]);
            vertices[(i * 3)+ 2] = 0;
        }
        new MyGeneralOpenGLES2DrawingClass(3, vertices, new float[]{0.0f,0.0f,1.0f,1.0f},new short[]{0,1,2}).draw(mMVPMatrix);

其中顶点 0,1,2 是圆心的 x,y,z 坐标。

图 2.8 在纵向模式下包含圆形演示的结果

2.3 在 OpenGL ES 2.0 中处理触摸事件

现在这有点棘手。当您在互联网上搜索“如何找出哪个 OpenGL 对象被触摸”的解决方案时,您会得到大量的建议,但几乎看不到任何可行的解决方案。这是因为 OpenGL 没有提供任何原生支持来告诉您是否或哪个对象被触摸。

为了理解问题的严重性,请首先参考 图 2.3。您可以看到触摸坐标是设备的绝对坐标。观察渲染器 onDraw 方法演示中的三角形、矩形绘制坐标以及 图 2.6。您很可能被误导地认为投影基本上就是缩放。因此,从技术上讲,将绝对坐标系转换为“投影”坐标系应该不难。但 OpenGL 中的投影是两阶段的。首先将设备坐标转换为归一化坐标系,然后使用 Camera 对象在渲染器的 onDraw 方法中开发投影坐标系。

因此,如果您更改以下行中粗体下划线的 z 坐标,您将看到不同的显示效果。

 Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);

下图显示了一个完全颠倒的视图,其比例根据摄像机角度而不同。

图 2.9 基于摄像机角度的显示变化

因此,如果我们真的想找出哪个对象被触摸,我们需要开发以下算法

1) 在 SurfaceView 类中,使用主活动的上下文,查找设备的当前屏幕宽度和高度。

 

2) 在 surface view 类中的 onTouch 覆盖方法中,通过将坐标除以设备宽度和设备高度,然后将原点调整到中心,获得一个标准化坐标。将新坐标称为标准化坐标。

第 1) 和 2) 点的实现如下

public float[] SimpleTouch2GLCoord( Point touch)
       {  
         Display display = maContext.getWindowManager().getDefaultDisplay();
           Point size = new Point();
           display.getSize(size);
           
           float screenW = size.x;
           float screenH = size.y;

           float normalizedX = 2f * touch.x/screenW - 1f;
            float normalizedY = 1f - 2f*touch.y/screenH;
            float normalizedZ = 0.0f;
            return ( new float[]{normalizedX,normalizedY,normalizedZ});

       }
    

3) 将归一化坐标传递给渲染类,在那里使用此处给出的算法,将归一化坐标系转换为投影坐标系。

  public float[]glCoordinate(float normalizedX,float normalizedY)
  {
      float[] invertedMatrix, transformMatrix,
      normalizedInPoint, outPoint;
  invertedMatrix = new float[16];
  transformMatrix = new float[16];
  normalizedInPoint = new float[4];
  normalizedInPoint[0] =
            normalizedX;
           normalizedInPoint[1] =
            normalizedY;
           normalizedInPoint[2] = - 1.0f;
           normalizedInPoint[3] = 1.0f;

  outPoint = new float[4];
  Matrix.multiplyMM(
          transformMatrix, 0,
          mProjectionMatrix, 0,
          mMVPMatrix, 0);
  Matrix.invertM(invertedMatrix, 0,
          transformMatrix, 0);  
  Matrix.multiplyMV(
          outPoint, 0,
          invertedMatrix, 0,
          normalizedInPoint, 0);

      if (outPoint[3] == 0.0)
      {
          // Avoid /0 error.
          Log.e("World coords", "ERROR!");
          return new float[]{9999,9999,9999};
      }
float []c=new float[]{ outPoint[0] / outPoint[3],outPoint[1] / outPoint[3]};
return c;

      
  }

4) 现在在 MyGeneralOpenGLES2DrawingClass 中,传递投影坐标,以及绘图对象本身的投影坐标。比较基于搜索技术

a) 对于圆形,通过计算中心(第一个顶点)和第二个顶点(圆周上的一个点)之间的欧几里得距离来计算半径。如果触摸点的距离小于中心到半径的距离,则触摸到圆形。

b) 对于矩形,检查触摸点是否在第一个顶点和第三个顶点之间(它们是第一个对角线的两个顶点,定义了正方形/矩形的区域)

c) 对于三角形,计算中心点为 centerX=(v1X+v2X+v3X)/3, centerY=(v1Y+v2Y+v3Y)/3,其中 v1,v2 和 v3 是三个顶点。现在计算中心点和 v2 之间的差值,该差值充当阈值。如果触摸点和中心点之间的距离小于阈值,则选择三角形。

       public boolean isTouched(float[] touchPoint)//x y z
        {
            float x2=touchPoint[0];
            float y2=touchPoint[1];
            if(coords.length==9)
            {
                
            // Triangle
                float midPointX=(coords[0]+coords[3]+coords[6])/3;
                float midPointY=(coords[1]+coords[4]+coords[7])/3;
            // Distance from 2nd vertex will work as threshold
                float thrDist=eucledian(midPointX, midPointY, coords[3], coords[4]);
                float dstFromTouch=eucledian(midPointX, midPointY, x2, y2);
                if(dstFromTouch<=thrDist)
                {
                    Log.i("Matched","Triangle");
                    return true;
                }
                          
                    
            }
            if(coords.length==12)
            {
                //Line square or Rectangle
                // Just checking if touch point is between 1st and last vertex is enough
                if(x2>=coords[0] && x2<=coords[6]&& y2>=coords[1] && y2<=coords[7] )
                {
                    Log.i("Matched","Rect/Line");
                    return true;
                }
                
            }
            if(coords.length>100)
            {
                //Circle
                // So calculate the distance between first and second vertex. That's the radious
                // Check for proximity of touch distance with radious.
                float radi=eucledian(coords[0], coords[1], coords[3], coords[4]);
                float dstFromTouch=eucledian(coords[0], coords[1], x2, y2);
                if(dstFromTouch<=radi)
                {
                    Log.i("Matched","Circle");
                    return true;
                }
            }
            
            return false;
        }

其中 eucledian 方法的实现如下:

float eucledian(float x1,float y1,float x2,float y2)
        {
            float d=(float) Math.sqrt((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2));
            return d;
            
        }

为了测试这个,只需修改 onTouch 方法如下:

@Override
public boolean onTouchEvent(MotionEvent e)
     {
final float x=e.getX();
final float y=e.getY();
         
final float[] normCoord=SimpleTouch2GLCoord(new Point((int)x,(int) y));
final float []glCoord=rend.glCoordinate(normCoord[0], normCoord[1]);
Log.i("GlX="+glCoord[0]+" glY="+glCoord[1] ,"X="+x+" Y="+y);
maContext.runOnUiThread(new Runnable() {

            @Override
             public void run() 
             {
                 String s="GlX="+glCoord[0]+" glY="+glCoord[1] +" X="+x+" Y="+y;
                 if(rend.triangle.isTouched(glCoord))
                 {
                     s=s+" TRI TOUCHED";
                 }
                 if(rend.line.isTouched(glCoord))
                 {
                     s=s+" LINE TOUCHED";
                 }
                 if(rend.circle.isTouched(glCoord))
                 {
                     s=s+" CIR TOUCHED";
                 }
                 if(rend.rect.isTouched(glCoord))
                 {
                     s=s+" RECT TOUCHED";
                 }
                 maContext.setTitle(s);    
             }
         });
        return false;
         
     }

如果一切顺利,您会看到如下结果:

图 2.10:触摸对象检测结果

2.4 OpenGL ES 2.0 2D 动画

OpenGL 中特别有三种开箱即用的动画支持

a) 缩放、平移和旋转变换。这与 wpf 中采用的概念相同。由于 OpenGL 绘图的基础是顶点矩阵,因此应用动画所需要做的就是在渲染时对矩阵应用变换。因此原始对象坐标不会改变,只会进行变换!

这组特定的变换也称为“渲染变换”,因为只对对象的渲染进行变换。

那么,如果要使用动画,代码应该写在哪里呢?

你猜对了!在渲染的 onFrameDraw 方法中。

所以旋转变换

1) 创建一个旋转矩阵

 Matrix.setRotateM(mRotationMatrix, 0, mAngle, 0, 0, -1.0f);

其中 mAngle 是旋转角度。

2) 将旋转矩阵与投影矩阵相乘。请务必不要改变顺序,因为这里所有的乘法都是向量运算。

Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0);

3) 现在您所要做的就是使用草图矩阵而不是投影来绘制对象

rect.draw(scratch);

4) 如果您需要对对象进行特定操作,那么检查通过触摸选择对象,如果选中特定对象,则执行旋转。因此,从 MySurfaceView 中,设置渲染器类的一个变量,以通知要操作的对象。

              if(rend.triangle.isTouched(glCoord))
                {
                    rend.mover="TRI";
                }
                if(rend.line.isTouched(glCoord))
                {
                   rend.mover="LINE";
                }
                if(rend.circle.isTouched(glCoord))
                {
                   rend.mover="CIR";
                }
                if(rend.rect.isTouched(glCoord))
                {
                   rend.mover="RECT";
                }

最后在渲染方法的 onFrameDraw 中,根据变量 mover 的值应用变换

if(!mover.equals("TRI"))
        {
        triangle.draw(mMVPMatrix);
        }
        else
        {
            triangle.draw(scratch);
        }
        if(!mover.equals("CIR"))
        {
             circle.draw(mMVPMatrix);
        }
        else
        {
             circle.draw(scratch);
        }

mAngle 可以通过检查之前的和当前的触摸位置来设置。

图 2.11:基于渲染变换的动画

对于 缩放平移 变换,分别使用 Matrix.translateMMatrix.scaleM 方法。

2.5 使用 Android OpenGL ES 2.0 进行 3D 图形绘制

2.5.1 3D 图形基础

OpenGL 的美妙之处在于,一旦你学会了 2D 的基本知识,3D 就不是什么大问题了。但什么是三维图形呢?

3D 图形是一种图形编程,它使渲染器能够渲染 3D 对象的多个面,以呈现其 x-y 和 z 的所有维度,并能够以自然的方式旋转轴,以便不同的面可以在轴上旋转以实现 360 度视角。

拿任何 3D 对象,比如一个盒子。现在从顶部看它。你不会看到它的 3D 视图,但你会看到盒子顶部的正方形。当你稍微侧身看物体时,你会看到顶部、左侧和右侧的表面。这就是 3D 视图。所以简单来说,3D 就是渲染多面对象的多个面。这种渲染可以通过巧妙地放置相机对象来实现。

请看下图。

图 2.12:带摄像头的 3D 坐标系

图中绿点是摄像头对象。您可以将一个对象连同坐标一起放置,然后适当调整您的摄像头,以 3D 视角查看该对象。

即使在我们处理 2D 图形时,我们采用的坐标系也是 x-y-z,并且在我们所有的坐标系中都使用了 z=0。此外,在早期的示例中,我们只绘制了对象的一个面。我们处理的是没有高度的对象。

我们使用 {顶点} 和 {绘制顺序} 来指定 2D 形状。在指定 3D 形状时,我们使用以下符号:

节点:由 x、y 和 z 三个坐标表示的点。
边:连接两个点的线(也可以称为顶点)。
面:由至少三个点定义的表面。
线框:仅由节点和边组成的形状。

图 2.13:3D 术语解释

从我们目前的讨论中,我们可以得出以下基本结论:

1) 一个 3D 对象可能有很多面,但为了呈现 3D 视角,用户必须至少呈现三个面。

2) 3D 对象只不过是 2D 形状的闭合图形。因此,渲染必须至少有三个这样的 2D 面,并以系统化的方式渲染,以呈现 3D 视角。

3) 有几种数学方法可以解决上述问题。但最基本和最可靠的方法是正投影

以下关于正投影的维基百科图片是理解 3D 视角的绝佳图表。

图 2.14:正投影(维基百科)

我们已经将 2D 形状视为由基本三角形组成。在前面的部分中,我们展示了几乎任何 2D 形状都可以建模为三角形的组合。由于 3D 是至少 3 个此类 2D 面组成的闭合图形,因此 3D 视图可以轻松地称为三角形组,或者通常称为:三角形网格。

下图 2.15 (来自 doc.cgal.org) 阐述了我们的理论:

图 2.15:作为三角形网格的 3D 线框

2.5.2 使用 OpenGL 进行 3D 图形绘制

我们现在将理解我们为 2D 形状开发的通用绘图类的强大功能。在阅读完 2.5.1 节后,您会不会惊讶地发现即使是 3D 形状也可以使用我们的 MyGeneralOpenGLES2DrawingClass 轻松绘制?您不应该。因为它所需要的只是用节点的坐标(2D 中的顶点)按顺序初始化类的对象,并指定三角形的绘制顺序!

虽然理想情况下您应该从 3D 模型文件(例如 blender 文件)读取节点信息,但我将展示一个原始坐标,并向您展示 3D 立方体绘制的基础知识。一旦您学习了这些基础知识,您就可以根据自己的情况修改概念。

这是立方体坐标

 private float verticesCube[] = {
            -1.0f, -1.0f, -1.0f,
            1.0f, -1.0f, -1.0f,
            1.0f,  1.0f, -1.0f,
            -1.0f, 1.0f, -1.0f,
            -1.0f, -1.0f,  1.0f,
            1.0f, -1.0f,  1.0f,
            1.0f,  1.0f,  1.0f,
            -1.0f,  1.0f,  1.0f
            };

这将在坐标系长度和宽度范围内形成绝对居中的立方体。但为了正确显示,我将它缩小

 for(int i=0;i<verticesCube.length;i++)
        {
            verticesCube[i]=verticesCube[i]/3;
        }

现在要绘制这个立方体,我们需要指定顶点的绘制顺序

这是顶点的顺序:

private short indicesCube[] = {
          0, 4, 5, 0, 5, 1,
          1, 5, 6, 1, 6, 2,
          2, 6, 7, 2, 7, 3,
          3, 7, 4, 3, 4, 0,
          4, 7, 6, 4, 6, 5,
          3, 0, 1, 3, 1, 2
          };

请注意,您可以从任何三角形开始,只要您覆盖所有对应于所有面的三角形!

最后,让我们定义一个 MyGeneralOpenGLES2Drawing 类对象并将其初始化为

private float colorsCube[] = {
            0.3f,  0.2f,  1.0f,  1.0f,
         };

cube=new MyGeneralOpenGLES2DrawingClass(3, verticesCube,colorsCube,indicesCube);

现在你所要做的就是在 MyGeneralOpenGLES2DrawingClass 中添加以下部分

if(drawOrder.length==36)//cube
                {
                    GLES20.glDrawElements(drawMode, 36, GLES20.GL_UNSIGNED_SHORT,  drawListBuffer);    
                }

然后呢?Bingo!我们的 3D 形状已经准备好了。

图 2.16:OpenGL 中的 3D 渲染

最有趣的是,OpenGL 视口不需要任何单独的配置来渲染 3D。它可以轻松地在同一视口中渲染 3D 和 2D 对象!

继续 下载 AnimationAndGhraphics.zip 并使用这些类吧!

3. 视图动画

顾名思义,本节我们将处理视图及其动画。但它们究竟是如何使用的呢?

例如,如果您在一个文本框中输入了一个电子邮件地址,但输入错误了,如果能让它的颜色从正常变为红色,再变回红色,以吸引用户的注意力,是不是很好呢?

一个简单的跑马灯式控件如何播放一些广告或新闻项目?当表单中的某个字段需要填写时,只需将文本框压缩和扩展一次以吸引用户的注意力,这将是多么直观!这些都是 UI 级别的动画,旨在改善应用程序体验并为应用程序带来“哇”的因素。

在我们讨论的所有上述示例中,我们都谈到了更改 UI 控件或视图的背景、宽度-高度和位置。这些是视图的属性。您实际上可以编写一个简单的计时器并以编程方式实现它。但这带来了为所有应用程序以不同方式硬编码此类动画的问题。

Android 提供了一种独特且 可重用 的方法,使用 XML 来实现。这被称为 视图动画

3.1 准备应用以同时使用 OpenGL 和视图动画

正如我们所见,使用 OpenGL 需要不同的渲染表面和内容视图。然而,这并不意味着如果您正在使用 OpenGL,就不能使用常规的 UI 布局。您可以使用 Fragment 概念将 OpenGL SurfaceView 和布局一起使用。但是,对于我们当前的应用程序,我们只会在 OpenGL 视图和布局视图之间切换。

我们还将创建两个菜单选项:一个允许切换到布局视图,当应用程序显示基于布局的视图时,另一个菜单会加载,其中包含视图动画的选项。我们将通过将不同的动画应用于一个简单的 ImageView 来理解视图动画的概念。

首先将您的主布局更改如下:

<RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:orientation="vertical" >

    <TextView
        android:id="@+id/tvTop"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="@string/graphics_and_animation_demo" />

    <ImageView
        android:id="@+id/imgMain"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/tvTop"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="146dp"
        android:src="@drawable/gn_logo" />

</RelativeLayout>

因此在设计模式下,您的布局看起来像图 3.1。

图 3.1:视图动画演示的 activity_main.xml 修改

如果您的 Eclipse 弹出“找不到资源 gn_logo”错误,请不要担心。只需使用属性编辑器从 drawable 中选择一张图片即可。您可以随时将图片上传到 drawable。(要了解更多关于图片管理的信息,您可以阅读此处的教程)

我们将首先呈现 OpenGL 视图,并允许用户通过主菜单选择动画表单视图。当动画表单呈现时,我们将加载另一个菜单,其中提供了一些动画选项以及返回 OpenGL 表单的机会。

res/menu.main.xml

<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context="com.integratedideas.animationandghraphics.MainActivity" >

        
        <item
        android:id="@+id/menuViewAnimation"
        android:orderInCategory="100"
        android:showAsAction="always"
        android:title="View Animation"/>      
    
        
    </menu>

res/menu/animation_choice.xml

   <menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context="com.integratedideas.animationandghraphics.MainActivity" >
     <item
        android:id="@+id/menuAnimationOptions"
        android:orderInCategory="100"
        android:showAsAction="always"
        android:title="Animation Option">      
        <item
            android:id="@+id/menuFadeIn"
        android:orderInCategory="100"
        android:showAsAction="never"
        android:title="Fade In"/>
        <item
            android:id="@+id/menuFadeOut"
        android:orderInCategory="100"
        android:showAsAction="never"
        android:title="Fade Out"/>
        <item
            android:id="@+id/menuZoomIn"
        android:orderInCategory="100"
        android:showAsAction="never"
        android:title="Zoom In"/>
        <item
            android:id="@+id/menZoomOut"
        android:orderInCategory="100"
        android:showAsAction="never"
        android:title="Zoom Out"/>
        <item
            android:id="@+id/menuSlideUp"
        android:orderInCategory="100"
        android:showAsAction="never"
        android:title="Slide Up"/>
        
        <item
            android:id="@+id/menuSlideDown"
        android:orderInCategory="100"
        android:showAsAction="never"
        android:title="Slide Down"/>
        <item
            android:id="@+id/menuRotate"
        android:orderInCategory="100"
        android:showAsAction="never"
        android:title="Rotate"/>
         <item
        android:id="@+id/menuOpenGL"
        android:orderInCategory="100"
        android:showAsAction="always"
        android:title="OpenGL View"/></item>
           </menu>

现在在 ActivityMain.java 中,根据设置为 1 的整数选项来加载菜单,该选项会触发主菜单显示。当选择“视图动画”选项时,整数选项会更改,并加载另一个菜单。这两个选项的选择也会更改视图。因此,我们将使用 setContentView 来加载适当的视图。

 int menuNo=1;
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {

        // Inflate the menu; this adds items to the action bar if it is present.
        if(menuNo==1)
        getMenuInflater().inflate(R.menu.main, menu);
        else
            getMenuInflater().inflate(R.menu.animation_choice, menu);    
        
        return true;
    }

    
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();
        switch(id)
        {
        case R.id.menuViewAnimation:
            setContentView(R.layout.activity_main);
            menuNo=2;
            invalidateOptionsMenu();// to call back oncreate menu again
            break;
        case R.id.menuOpenGL:
            menuNo=1;
            setContentView(new MyGlSurfaceView(this));
            invalidateOptionsMenu();
            break;
        
        }
        return super.onOptionsItemSelected(item);
    }


现在构建并运行您的应用程序。

图 3.2 在 OpenGL 和布局视图之间切换

涵盖这一部分的目的在于,很多时候,在一个应用程序中结合布局和 OpenGL 视口,并带有一些切换机制变得非常重要。在这里,我们学习了一种非常简单但有效的技术,可以将所有资源放在同一个篮子里!

3.2 基于 XML 的视图动画基础

基于 XML 的视图动画工作流程可以通过查看图 3.3 来理解。

图 3.3 视图动画工作流程

与您在 OpenGL 部分所做的工作相比,这个工作流程的逻辑非常简单。首先,您需要在 res 文件夹下的 anim 文件夹中创建一个 xml 文件。

最简单的形式是,Android 动画 xml 文件必须有一个 PROPERTY 标签。标签名称没有限制。<alpha>, <scale>, <translate>, <rotate> 是视图动画中常用的一些 PROPERTY

例如,要创建淡入/淡出动画,我们可以使用属性名称 alpha 或 intensity。属性标签必须至少有一个 android:toSomeProperty 和一个 android:fromSomeProperty。其中 SomeProperty 是您要进行动画的 Android 属性。

alpha、XScale、YScale、XDelta、YDelta、Degrees 是您可以用于 SomeProperty 的一些值

使用 repeatCount 可以指定您希望动画持续的次数。infinity 表示动画将永远持续。

duration 指定动画将持续的毫秒数。

如果您希望按顺序执行多个不同的序列,那么您需要从上到下依次指定不同的属性标签。

该 xml 文件在 onCreate 方法中加载到一个 Animation 对象中。Activity 类还可以实现 AnimationListener 接口,在这种情况下,它将具有三个事件方法,即 startAnimation、repeatAnimation 和 endAnimation,可以在其中放置任何逻辑代码。

通过调用要动画的视图对象的 startAnimation 方法即可开始动画。

时间 插值 是一种指定函数的方式,通过该函数指定动画中间时间戳的中间结果。

让我们看一个淡入动画的基本示例。淡入是一个过程,其中控件的 alpha 或透明度从 1 变为 0。这是它的简单 xml 文件。

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true" >

    <alpha
        android:duration="1000"
        android:fromAlpha="0.0"
        android:interpolator="@android:anim/accelerate_interpolator"
        android:toAlpha="1.0" />

</set>

在您的 res/anim 文件夹中创建一个名为 fade_in.xml 的 xml 文件并复制代码。如果 anim 文件夹不存在,您可以随时创建该文件夹。请确保文件夹名称或文件名称中不使用大写字母。

在我们使用 MainActivity 之前,我们需要对视图的工作方式有更多了解。

看,findViewById() 调用必须始终在 setContentView 调用之后。基本上 findViewById 可以找到 子视图 的 id。因此,在此特定工作中,如果您尝试在 onCreate 方法中初始化 ImageView 实例,它将为 null。因此,您必须在 onOptionItemSelected 方法中初始化 ImageView 实例,在该方法中您将 main_activity 布局分配为内容视图。

ImageView iv;
int menuNo=1;
@Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();
        switch(id)
        {
        case R.id.menuViewAnimation:
            setContentView(R.layout.activity_main);
            iv=(ImageView)findViewById(R.id.imgMain);
            menuNo=2;
            invalidateOptionsMenu();// to call back oncreate menu again
            break;
        case R.id.menuOpenGL:
            menuNo=1;
            setContentView(new MyGlSurfaceView(this));
            invalidateOptionsMenu();
            break;
        case R.id.menuFadeIn:
            anim = AnimationUtils.loadAnimation(getApplicationContext(),  R.anim.fade_in);
            iv.startAnimation(anim);
            break;
        
        }

最后,这是示例淡入动画的输出

图 3.4:淡入动画的结果

正如我们所讨论的,您可以按顺序组合不同的动画,例如以下动画

<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true"
    android:interpolator="@android:anim/linear_interpolator" >

    <!-- Use startOffset to give delay between animations -->

    <!-- Move -->
    <scale
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:duration="4000"
        android:fromXScale="1"
        android:fromYScale="1"
        android:pivotX="50%"
        android:pivotY="50%"
        android:toXScale="4"
        android:toYScale="4" >
    </scale>

    <!-- Rotate 180 degrees -->
    <rotate
        android:duration="500"
        android:fromDegrees="0"
        android:pivotX="50%"
        android:pivotY="50%"
        android:repeatCount="infinite"
        android:repeatMode="restart"
        android:toDegrees="360" />

</set>

下载视图动画演示 并使用演示中提供的不同动画。最重要的是,您可以将这些动画集应用于任何视图或视图组。

4. 属性动画

属性动画是另一种动画支持类别。视图动画的问题在于,动画只适用于视图对象。其次,视图动画只使用新的动画属性渲染相关的视图,而不影响容器。例如,假设您正在对一个按钮应用移动动画。即使按钮在容器中移动,它的点击位置仍然相同,这需要由用户代码处理。

有几种非视图对象需要动画。以一个简单的 CountDownTimer 为例。它的值应该持续减少直到 0。您不能使用视图动画,因为没有视图附加到整数。因此,如果没有动画支持,您必须声明一个计时器并更新这些值。但属性动画有助于高效地执行此类更新。

请注意,您不应被“动画”一词误导,它通常指视图端口上的一些缓慢渲染变化。属性动画必须被理解为一种简单有效的方式,以平滑的方式更改某些值,而无需计时器和监听器的麻烦,这些可能不一定包括渲染。

它中间结果的计算基于插值。

可用的插值模式有:

  • LinearInterpolator(线性插值器)
  • AccelerateDecelerateInterpolator(加速减速插值器)
  • AccelerateInterpolator(加速插值器)
  • AnticipateInterpolator(反向插值器)
  • AnticipateOvershootInterpolator(反向超调插值器)
  • BounceInterpolator(弹跳插值器)
  • CycleInterpolator(循环插值器)
  • DecelerateInterpolator(减速插值器)
  • LinearInterpolator(线性插值器)
  • OvershootInterpolator(超调插值器)

属性动画不仅可以改变变量的值,还可以轻松地应用于对象的特定属性(包括视图对象)。因此,如果您对 ImageView 的高度值从 10 变为 100 执行属性动画,那么随着动画的进行,视图的高度实际上会随着时间从 10 变为 100。

 

4.1 Value Animator

属性动画的核心是一个名为 ValueAnimator 的类。顾名思义,它可以动画或更新某个范围内的值。在它的更新事件处理程序中,可以将更新后的值应用于多个不同的对象或对象的属性。

ValueAnimator 可以更新不同的值,如 ofFloat、ofInt 或 ofProperty。首先让我们看一个动画浮点值的漂亮示例。

声明一个 ValueAnimator 对象,通过指定要更改的值类型来初始化它。最后,向其添加 UpdateListener。在更新方法中,您可以将更新后的值应用于任何对象。有趣的是,这还允许 UI 渲染。因此,您无需实现任何其他线程或后台。

ValueAnimator animation = ValueAnimator.ofFloat(0f, 1f);
            animation.setDuration(8000);
            animation.addUpdateListener(new AnimatorUpdateListener() 
            {
                
                @Override
                public void onAnimationUpdate(ValueAnimator animation) 
                {
                    float val=Float.parseFloat(animation.getAnimatedValue().toString());
                    iv.setAlpha(val);
                    iv.setScaleX(val);
                    iv.setScaleY(val);
                    // TODO Auto-generated method stub
                    tv.setText(animation.getAnimatedValue().toString());
                    
                }
            });
            
            animation.start();
            break;
            

如上例所示,我们使用相同的更新值在 TextView 中打印,以更新 ImageView 的 alpha 和 ScaleX 属性。但是使用 ViewAnimation,我们只能对序列的一个属性应用动画。多个属性是按顺序动画的,因此它们的值不同步。请参阅多个控件和属性使用相同值的结果

图 4.1:值动画的结果

4.2 Object Animator

Value animator 的问题在于,如果需要修改视图对象,则需要使用更新事件中的代码进行处理。如果我们能够通过动画器直接更新对象的属性而无需编码,那该怎么办?

是的,这是可能的,并且由第二组属性动画器(称为 ObjectAnimator)支持。

ObjectAnimator oa=ObjectAnimator.ofFloat(iv, "translationX", 0, 400);
            oa.setDuration(6000);
            oa.start();
            break;

上面的代码片段将把您的图像从当前位置移动到结束位置,其中 endX=currentX+400; 因此,第一个参数是对象,第二个是属性,第三个和第四个参数分别是起始值和结束值。

您可以将 ObjectAnimation 应用于其他属性,例如 scaleX、alpha、scaleY 等。

ObjectAnimator oa=ObjectAnimator.ofFloat(iv, "scaleX", 0, 4);

ObjectAnimator oa=ObjectAnimator.ofFloat(iv, "rotation", 0, 45);

请记住,旋转是以度而不是弧度表示的。

 

对于 ValueAnimatorObjectAnimator,如果您希望动画从向前到向后,再从向后到向前继续,则使用 RepeatMode 和 RepeatCount。

oa.setRepeatMode(ValueAnimator.REVERSE);
oa.setRepeatCount(ValueAnimator.INFINITE);

4.3 AnimatorSet

如果我们想同时对一个对象应用不同类型的动画怎么办?不用担心。AnimatorSet 调用对象可以用于同时执行多个动画。

其中一个最常见的用途是在应用缩放属性时。比如说,当你为 scaleX 创建一个 ObjectAnimator 时,对象在 x 方向上不断放大,但你大多数时候想要做的是同时应用 scale x 和 scale y,这用 ObjectAnimator 是不可能的。因此我们选择 AnimatorSet。

AnimatorSet 可以同时运行多个 ValueAnimator 或 ObjectAnimator 类型的动画。这不会为独立的动画器引入任何动画属性,如持续时间/插值等。独立的动画器受其自己的规则集支配。

          /////// First Define Independent Animators with their Property///////////////   

           ObjectAnimator oaRotation=ObjectAnimator.ofFloat(iv, "rotation", 0, 45);
            oaRotation.setDuration(5000);
            oaRotation.setRepeatCount(ValueAnimator.INFINITE);
            oaRotation.setRepeatMode(ValueAnimator.REVERSE);
            ObjectAnimator oaScaleX=ObjectAnimator.ofFloat(iv, "scaleX", 0, 4);
            oaScaleX.setDuration(5000);
            ObjectAnimator oaScaleY=ObjectAnimator.ofFloat(iv, "scaleY", 0, 4);
            oaScaleY.setDuration(5000);
            ObjectAnimator oaAlpha=ObjectAnimator.ofFloat(iv, "alpha", 0, 1);
            oaAlpha.setDuration(5000);
            ////////////////////////////////////////////////////////////////////
            AnimatorSet combine = new AnimatorSet();
        ///////////////////// Define the order of playing
            combine.playTogether(oaScaleX,oaScaleY);
            combine.play(oaAlpha).before(oaRotation);

      /////////////// Start the Animation/////////////////
            combine.start();

使用 playTogether,您可以安排任意数量的动画同时运行。

你可以在另一个动画之前或之后播放动画。每个动画都可以有自己独立的属性,例如不同的持续时间。

5. Drawable 动画

动画最纯粹和最古老的形式基本上是从代表完整动作的一组图像中按顺序渲染图像。这是动画电影、定格动画等所依赖的原理。

Android 提供了一种很好的动画此类图像序列的方式。这类动画值得任何开发人员的尊重。但是,当您实际处理设计时,您肯定会质疑 Android 开发人员在处理这类动画时所做的糟糕设计决策。

回到 drawable 动画,顾名思义,您必须在任何 drawable 文件夹(例如 xhdpi 或 xxhdpi 等)中拥有一组图像帧。

在这里,我们将动画一只狐狸木偶。请参阅图 5.1 以了解您的图像配置应该是什么样子。

图 5.1:准备 Drawable 动画

现在你必须在 drawable-xhdpi(或你正在使用的任何 drawable 文件夹)中创建一个 xml 文件,名称与你的动画相同。我创建了一个名为 fox_puppet.xml 的 xml 文件。

此 xml 是指向所有帧及其行为的指针。

<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="true">
    <item android:drawable="@drawable/photo1" android:duration="200" />
    <item android:drawable="@drawable/photo2" android:duration="200" />
    <item android:drawable="@drawable/photo3" android:duration="200" />
    <item android:drawable="@drawable/photo4" android:duration="200" />
    <item android:drawable="@drawable/photo5" android:duration="200" />
    <item android:drawable="@drawable/photo6" android:duration="200" />
    <item android:drawable="@drawable/photo7" android:duration="200" />

.

.

.

</animation-list>

请注意 xml 文件的 onshot 属性。如果它设置为 true,动画将只触发一次。如果设置为 false,动画将持续重复。

DrawableAnimation 图像在图像的背景中渲染。因此,我们在主布局中考虑另一个名为 ivPuppet 的图像。

对于 Drawable 动画,首先使用你的 xml 文件名初始化图像的 BackgroundResource 属性。然后将 AnimationDrawable 类对象赋值给 BackgroundResource,它当然是一个 drawable。你唯一需要做的就是调用 start 方法来启动动画。

顺便说一句,别忘了将 ImageView 的图像设置为 null,否则动画将在背景中播放,但会被前景图像遮挡。

AnimationDrawable puppet;
     void PerformDrawableAnimation()
     {
         iv.setImageBitmap(null);
         iv.setBackgroundResource(R.drawable.fox_puppet);
         puppet = (AnimationDrawable) iv.getBackground();
         puppet.start();
         
     }

图 5.2:Drawable 动画的结果

您还可以像其他动画一样添加 updateListeners 和其他事件处理程序。然而,这种设计最糟糕之处在于您可能需要组织动画。假设您的应用程序中有十种不同的动画。您肯定希望将它们分组到特定的目录中。因此,最好的方法是使用子目录并将特定动画的动画保留在相应的目录中。不是吗?

不完全是!这是因为 drawable 不识别任何子目录。因此您需要将所有图像都堆放在根 drawable 文件夹中。

然而,我们是 CodeProject 的一员,所以我们更聪明一点。对吗?所以我们必须找到一个解决基本设计缺陷的变通办法。

允许子目录的资源是 assets。因此,您所要做的就是创建一个名为 fox 的文件夹,并将所有图像序列放入其中。现在请注意,在 xml 文件中您使用的是 drawable。Asset 目录不会提供其资源作为 drawable。因此,您需要在这里做一些编程技巧。您必须使用 addFrame() 方法从位图加载帧,而不是通过 xml 文件指定帧。

首先,您必须从资产资源打开输入流。然后,您必须将其转换为位图,该位图将用于创建可绘制对象。此对象连同动画时间作为参数传递给 addFrame 方法。

最后,将动画对象分配给图像的背景资源。

AnimationDrawable puppet;
     void PerformDrawableAnimation()
     {
         iv.setImageBitmap(null);
        // iv.setBackgroundResource(R.drawable.fox_puppet);
    //     puppet = (AnimationDrawable) iv.getBackground();
    //////////// My Method/////////////////////
         puppet=new AnimationDrawable();
         InputStream is = null;
         for(int i=1;i<=31;i++)
         {
         try 
         {
             is = this.getResources().getAssets().open("fox/photo"+i+".png");
             Bitmap b = BitmapFactory.decodeStream(is);
             Drawable d=new BitmapDrawable(b);
             puppet.addFrame(d, 200);
         } catch (Exception e) 
         {
             ;
         }
         }
         iv.setBackgroundDrawable(puppet);
         puppet.setOneShot(true);
         //////////////////////////////////
         puppet.start();
         
     }

AnimationDrawable 类的 set 方法可以添加许多其他动画行为。

这比我们看到的大多数基于 xml 的第一种方法要好得多。它避免了您创建和维护不同的 xml 文件。

6. Canvas API

Android 提供了一套令人惊叹的 2D 绘图 API,允许您在画布上绘制自己的图形(形状和位图)。基本上,Android 中的每个视图都使用由 Canvas 类处理的 onDraw 调用进行渲染。为了显示或绘制(渲染)任何视觉项目,您需要一个位图,其像素将表示视觉效果,以及一个 Canvas 来绘制到像素中。此绘制包括绘制基本形状(如圆形、椭圆、矩形)或其他位图。您还需要一个 Paint 对象,它定义使用 Canvas 调用和绘制基本形状(如圆形/矩形)在位图上进行绘制,这我们在 OpenGL 部分已经学过。然而,与 OpenGL 不同的是,Canvas API 中的绘制要直接得多,它们使用绝对坐标系而不是我们在 OpenGL 部分中使用的带投影的归一化坐标系。以下是 Android Canvas 类支持的主要绘制调用(或 API)。

图 6.1 Canvas 绘图 API

为了测试 Canvas API,您可以覆盖您的活动类的 onDraw 方法或创建一个扩展 View 的类。您可以在该类的 onDraw 方法中加入您的绘图逻辑。

所以我们创建了我们的 View 类,名为 DrawingView

public class DrawingView extends View {
    
    //drawing path
    private Path drawPath;
    //drawing and canvas paint
    private Paint drawPaint, canvasPaint;
    //initial color
    private int paintColor = 0xFF660000;
    //canvas
    private Canvas drawCanvas;
    //canvas bitmap
    private Bitmap canvasBitmap;

    public DrawingView(Context context, AttributeSet attrs){
        super(context, attrs);
        setupDrawing();
    }

    //setup drawing
    private void setupDrawing(){

        //prepare for drawing and setup paint stroke properties
        drawPath = new Path();
        drawPaint = new Paint();
        drawPaint.setColor(paintColor);
        drawPaint.setAntiAlias(true);
        drawPaint.setStrokeWidth(20);
        drawPaint.setStyle(Paint.Style.STROKE);
        drawPaint.setStrokeJoin(Paint.Join.ROUND);
        drawPaint.setStrokeCap(Paint.Cap.ROUND);
        canvasPaint = new Paint(Paint.DITHER_FLAG);
    }
    
    //size assigned to view
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        canvasBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
        drawCanvas = new Canvas(canvasBitmap);
    }
    
    //draw the view - will be called after touch event
    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawBitmap(canvasBitmap, 0, 0, canvasPaint);
        canvas.drawPath(drawPath, drawPaint);
        
    }
    
    //register user touches as drawing action
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float touchX = event.getX();
        float touchY = event.getY();
        //respond to down, move and up events
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            drawPath.moveTo(touchX, touchY);
            break;
        case MotionEvent.ACTION_MOVE:
            drawPath.lineTo(touchX, touchY);
            break;
        case MotionEvent.ACTION_UP:
            drawPath.lineTo(touchX, touchY);
            drawCanvas.drawPath(drawPath, drawPaint);
            drawPath.reset();
            break;
        default:
            return false;
        }
        //redraw
        invalidate();
        return true;
        
    }
    
    //update color
    public void setColor(String newColor){
        invalidate();
        paintColor = Color.parseColor(newColor);
        drawPaint.setColor(paintColor);
    }
    public void setColor(int color){
        invalidate();
        paintColor = color;
        drawPaint.setColor(paintColor);
    }
    public int getColor()
    {
    
        return drawPaint.getColor();
    }
}


如前所述,我们有一个画笔对象和一个位图,将在其上使用画笔对象进行绘图调用。drawpath 是一个路径对象,它保存一组点,这些点根据绘图画布上的触摸进行更新。画布使用 drawpaint 绘制路径,drawpaint 是一个用特定颜色初始化的画笔对象。当触摸释放时,会从最后一个点到触摸释放点绘制一条线,并且 drawPath 会重置。如果没有位图对象,每次释放触摸时,画布都会重置。您可以通过注释掉 onDraw 方法中的 canvas.drawBitmap(canvasBitmap, 0, 0, canvasPaint); 行来验证这一点。

我们修改了 activity_main.xml 以容纳新的视图

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >
<com.integratedideas.animationandghraphics.DrawingView
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:id="@+id/dvMain"
    />
    
    <TextView
        android:id="@+id/tvTop"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="@string/graphics_and_animation_demo" />

    <ImageView
        android:id="@+id/imgMain"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/tvTop"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="41dp"
        android:src="@drawable/gn_logo"
        tools:ignore="ContentDescription" />

    <ImageView
        android:id="@+id/ivAnimation"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:src="@drawable/photo11" />

    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentLeft="true" >

        <Button
            android:id="@+id/btnColor"
            style="?android:attr/buttonStyleSmall"
            android:layout_width="wrap_content"
            android:layout_height="29dp"
            android:text="Color" />

    </LinearLayout>

</RelativeLayout>

我们在这里还使用了一个颜色按钮,这样我们就可以触发绘图颜色的改变。

现在,每当 activity_main 被设置为内容视图时,您都需要初始化颜色按钮的对象和 drawView 的对象。我们在 onOptionsItemSelected 事件处理程序中完成此操作。

drawView = (DrawingView)findViewById(R.id.dvMain);
            btnColor=(Button)findViewById(R.id.btnColor);
            btnColor.setOnClickListener(this);

为了实现颜色选择器,您需要从这里下载 yuku.ambilwarna 颜色选择器。将项目导入 Android。现在右键单击您的项目,选择属性,然后从左侧面板选择 Android。单击右下角的添加按钮并选择 AmbilWarna。

为了能够在点击按钮时显示颜色对话框,请更新 onClick 方法。

    @Override
    public void onClick(View v) 
    {
        // TODO Auto-generated method stub
    Button b=(Button)v;
    if(b.getText().toString().trim().equals("Color"))
    {
        int c=drawView.getColor();
        awd=new AmbilWarnaDialog(MainActivity.this,Color.BLACK,new OnAmbilWarnaListener() {
            
            @Override
            public void onOk(AmbilWarnaDialog arg0, int arg1)
            {
                // TODO Auto-generated method stub
                drawView.setColor(arg1);    
            }
            
            @Override
            public void onCancel(AmbilWarnaDialog arg0) {
                // TODO Auto-generated method stub
                
            }
        });
        awd.show();
        
    }
    }

结果可见图 6.2

图 6.2 Canvas API 上的操作

为了了解其他绘图调用如何完成,让我们稍微修改一下触摸事件处理程序,以添加一个起始圆和结束圆。

@Override
    public boolean onTouchEvent(MotionEvent event) {
        float touchX = event.getX();
        float touchY = event.getY();
        //respond to down, move and up events
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            drawPath.moveTo(touchX, touchY);
            drawCanvas.drawCircle(touchX, touchY, 15, drawPaint);
            break;
        case MotionEvent.ACTION_MOVE:
            drawPath.lineTo(touchX, touchY);
            
            break;
        case MotionEvent.ACTION_UP:
            drawPath.lineTo(touchX, touchY);
            drawCanvas.drawCircle(touchX, touchY, 15, drawPaint);
            drawCanvas.drawPath(drawPath, drawPaint);
            drawPath.reset();
            break;
        default:
            return false;
        }
        //redraw
        invalidate();
        return true;
        
    }

结果看起来像图 6.3。

图 6.3:drawCircle Canvas API 调用的结果

使用 Canvas API 绘制位图也不是什么大问题。您需要一个位图对象(最好是小尺寸的),您可以使用 drawBitmap 调用绘制此对象。为了测试该方法的速度,我在触摸事件中使用了 drawBitmap,以查看该方法是否足够响应。

case MotionEvent.ACTION_MOVE:
            drawPath.lineTo(touchX, touchY);
            try{
            drawCanvas.drawBitmap(icon, touchX-30, touchY-30,null);
            }catch(Exception ex)
            {
                
            }
            break;

其中 icon 是在构造函数中初始化的位图对象

icon = BitmapFactory.decodeResource(getResources(),   R.drawable.ic_launcher);

看起来像这样:

图 6.4 drawBitmap Canvas API 调用结果

您可以实现自己的逻辑,并创作出富有创意的图画!

7. 结论

图形和动画是设计和开发创新应用的重要方面。Android 中有多种选择来动画化对象和视觉效果。我们试图在本教程中以简单的方式呈现其中大部分。然而,仍然存在一个更大的问题。在哪些用例中使用哪种方法。如果您正在构建下一代响应式游戏,那么 OpenGL 必须是您唯一的选择。这是因为硬件用于绘图。与 Canvas API 相比,OpenGL 速度非常快。但是,如果您正在为儿童构建简单的绘图应用程序,则 OpenGL 绘制基本形状变得困难,Canvas API 更适合此类应用程序。如果您想创建响应式 UI,其中 UI 以某些动画响应事件,那么属性动画和视图动画非常适合此类情况。Drawable 动画是播放简单动画电影/卡通剪辑应用程序的绝佳工具。总而言之,为您的应用程序需求尝试不同的 API,并测试它们的速度、内存和效率,然后选择最适合的。

 

 

 

© . All rights reserved.