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

Android 动画/图形入门指南

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (40投票s)

2014年10月5日

CPOL

22分钟阅读

viewsIcon

190530

downloadIcon

13168

学习在 Android 中绘制和制作简单的 2D 和 3D 图形动画。

引言

要使您的应用在竞争对手中脱颖而出,它必须拥有视觉吸引力且令人惊叹的动画。在这方面,Android 框架提供了一套丰富且强大的 API,用于将动画应用于 UI 元素和图形,以及绘制自定义的 2D 和 3D 图形。在本文中,您将通过一系列实践练习,学习如何利用这些 API 来实现简单的动画和创建简单的 2D 和 3D 图形。

动画概述

Android 提供了三种动画系统来满足 Android 应用的动画需求。从最复杂的“属性动画”到更简单的“视图动画”和“可绘制动画”。

属性动画

属性动画系统于 Android 3.0 (API 级别 11) 推出,是一个强大的框架,可让您为任何对象(视图对象或非视图对象)的任何属性以及任何自定义类型制作动画。它是 Android 中首选的动画方法。“android.animation”提供了处理属性动画的类。

视图动画

作为较旧的动画系统,“视图动画”也称为“补间动画”。它只能用于为视图的内容制作动画,并且仅限于简单的变换,如移动、缩放和旋转,但不能动画其背景颜色。“android.view.animation”提供了处理“视图动画”的类。

可绘制动画

可绘制动画通过逐帧显示一系列“Drawable”资源(即图像)在视图对象内播放来工作。它使用“AnimationDrawable”类来实现。

2D 和 3D 图形概述

根据图形类型和应用的性能需求,您可以从以下选项中选择在 Android 上绘制图形:

画布

Android 框架提供了一套 2D 绘图 API,用于使用“Canvas”类提供的各种绘图方法在画布上渲染自定义图形。

OpenGL ES

如果您涉足 3D 图形或交互式游戏应用,则应考虑 Android 框架支持的“OpenGL ES”API。它提供了一套极其强大的工具,用于处理和显示高端动画 3D 图形,这些图形可以利用许多 Android 设备上提供的图形处理单元 (GPU) 的硬件加速。

准备工作

我已经准备了一个 Android 项目供下载 - “AndroidAnimation.zip”。“AndroidAnimation”是一个不完整的应用程序,包含一个主 Activity、六个骨架 Activity 以及后续练习所需的其他资源。启动页是标题为“AndroidAnimation”的“MainActivity”,其中包含六个按钮,用于导航到您将在本教程的系列练习中完成的其他六个 Activity。有关应用完全构建后的视觉组成,请参阅图 1。

图 1:Android 动画应用的视觉组成

您将通过以下步骤进行准备:

  1. 下载并解压“AndroidAnimation.zip”,您的计算机上应该会显示“AndroidAnimation”文件夹。

  2. 启动 Android Studio。

  3. 如果它打开了一个现有项目,请点击文件 > 关闭项目切换到欢迎屏幕。

  4. 在欢迎屏幕的“快速入门”页面上,点击“导入项目…”。

  5. 浏览到“AndroidAnimation”项目,然后点击“确定”在 Android Studio 中打开它。

旅程开始…

属性动画

ValueAnimator

属性动画通过在指定的时间长度内,根据某个随时间变化的函数,将对象的一个属性(例如屏幕上的 x 位置)的值从起始值更改为结束值来制作动画。

在属性动画中,Android 提供了“ValueAnimator”类作为启动和管理整个动画过程的计时引擎。它包含每个动画的详细信息,例如正在动画的属性的起始值和结束值、动画的持续时间、一个定义动画变化速率的“TimeInterpolator”子类,以及一个定义如何计算动画值的“TypeEvaluator”子类。

在动画期间,“ValueAnimator”将已用时间量与动画总持续时间进行比较,得出 0 到 1 之间的“已用进度,其中 0 表示动画完成 0%,1 表示动画完成 100%。例如,在总持续时间为 100ms 的动画中,25ms 时的已用进度将为 0.25。

TimeInterpolator

对于计算出的每个已用进度,“ValueAnimator”将根据为动画设置的“TimeInterpolator”来计算“插值进度”。时间插值器是一个定义动画过程中文档值变化速率的函数。例如,您可以使用“LinearInterpolator”(图 2)来指定动画随时间均匀发生,或者使用“AccelerateDecelerateInterpolator”(图 3)来指定动画开始和结束时缓慢,但在中间加速。从下面的图表中可以看出,两个插值器在 t=0.25 的已用进度时,插值进度(即 f(t)'s)分别约为 0.25 和 0.15。

图 2:LinearInterpolator
图 3:AccelerateDecelerateInterpolator

有关 Android 提供的“TimeInterpolator”各种子类的摘要,请参阅表 1。

表 1:TimeInterpolator 的子类
回调 描述
LinearInterpolator

一种基于时间的函数,其变化速率恒定(图 2)。

AccelerateDecelerateInterpolator

一种基于时间的函数,其变化速率在开始和结束时缓慢,但在中间加速(图 3)。

AccelerateInterpolator

一种基于时间的函数,其变化速率开始缓慢然后加速。

DecelerateInterpolator 一种基于时间的函数,其变化速率开始快速然后减速。这与“AccelerateInterpolator”相反。
AnticipateInterpolator 一种基于时间的函数,其变化开始向后然后向前移动。
OvershootInterpolator 一种基于时间的函数,其变化会超过结束值,然后最终回到结束值。
AnticipateOvershootInterpolator 变化的前半部分采用“AnticipateInterpolator”的特征,后半部分采用“OvershootInterpolator”的特征。动画开始向后,然后向前移动并超过结束值,最后回到结束值。
BounceInterpolator 一系列连续的基于时间的函数,在动画结束时产生弹跳效果。
CycleInterpolator 将动画重复指定的周期数。

TypeEvaluator

对于计算出的每个插值进度,“ValueAnimator”会调用适当的“TypeEvaluator”来计算正在动画的属性值,该计算基于插值进度、起始值和动画的结束值。Android 提供了三个“TypeEvaluator”子类和一个用于创建自定义评估器的接口。有关“TypeEvaluator”各种实现的摘要,请参阅表 2。

表 2:TypeEvaluators
类/接口 说明
IntEvaluator

执行 int 值之间的类型插值。

FloatEvaluator

执行 float 值之间的类型插值。

ArgbEvaluator

执行代表 ARGB 颜色的整数值之间的类型插值。

TypeEvaluator

一个接口,可与“setEvaluator(TypeEvaluator)”方法一起使用,该方法允许实现任意属性类型动画的自定义评估器。

到目前为止,“ValueAnimator”在计算动画的动画值方面非常有用;但它并没有将这些值应用到正在动画的对象的属性上。您需要编写代码来监听“ValueAnimator”计算出的值的更新,并相应地修改动画对象。我真的需要这样做吗?不,您不需要。我们将调用“ValueAnimator”的子类,即“ObjectAnimator”。

ObjectAnimator

ObjectAnimator”是“ValueAnimator”的子类,允许您设置要动画的对象及其属性。它可以计算动画值并相应地用新值更新属性。在创建对象动画时,您将大部分时间使用“ObjectAnimator”。

AnimatorSet

很多时候,您想同时、顺序或在指定延迟后播放多个动画。Android 提供了“AnimatorSet”类来处理此类需求。

代码还是 XML?

除了以编程方式创建对象动画之外,您还可以将属性动画声明为 XML 资源,以便在多个 Activity 中重用。在 XML 中编辑动画详细信息也比在代码中更容易。下面显示了代表相应属性动画类的 XML 标签:

好了,理论够多了,让我们付诸实践。

让事情发生

您将在“AndroidAnimation”项目中创建一个 Activity,该 Activity 将使用“属性动画”系统展示动画的组合。请参阅图 4 了解预期的结果。

图 4:对象动画

图 4 中的屏幕包含一个切换按钮,用于切换动画的开始/停止。当动画开始时,太阳从地面(绿色区域)升向天空(紫红色区域),周围环境逐渐变亮。当太阳到达屏幕顶部时,它开始向地面下降,周围环境逐渐变暗。在早晨和下午之间,两朵云出现并从左向右在天空中流动。

让我们先从太阳开始。在 Android Studio IDE 的“项目”窗格中:

  • 打开“activity_property_animation.xml”,并添加以下 XML 代码以创建布局和 UI 控件,如图 5 所示。

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/sky"
        android:background="#00004C"
        tools:context="com.peterleow.androidanimation.PropertyAnimationActivity">
    
        <ImageView
            android:id="@+id/sun"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:contentDescription="@string/sun"
            android:src="@drawable/sun"
            android:layout_alignParentBottom="true"
            android:layout_centerHorizontal="true" />
    
        <ImageView
            android:id="@+id/cloud1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:contentDescription="@string/cloud"
            android:src="@drawable/cloud"
            android:layout_alignParentRight="true"
            android:layout_marginRight="-200dp"
            android:layout_marginTop="40dp" />
    
        <ImageView
            android:id="@+id/cloud2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:contentDescription="@string/cloud"
            android:src="@drawable/cloud"
            android:layout_alignParentRight="true"
            android:layout_marginRight="-300dp"
            android:layout_marginTop="100dp"/>
    
        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="300dp"
            android:id="@+id/ground"
            android:background="#004700"
            android:layout_alignParentRight="true"
            android:layout_alignParentEnd="true"
            android:layout_alignTop="@+id/sun" >
        </RelativeLayout>
    
        <ImageView
            android:id="@+id/window"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:contentDescription="@string/window"
            android:src="@drawable/window" />
    
        <ToggleButton
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/toggleAnimate"
            android:textColor="#ffffff"
            android:textOn="@string/animation_on"
            android:textOff="@string/animation_off"
            android:checked="false"
            android:onClick="onToggleClicked"
            android:layout_alignParentBottom="true"
            android:layout_alignParentLeft="true"
            android:layout_marginLeft="30dp"
            android:layout_marginBottom="30dp" />
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAppearance="?android:attr/textAppearanceLarge"
            android:textColor="#ffffff"
            android:id="@+id/textView"
            android:layout_centerVertical="true"
            android:layout_alignParentRight="true"
            android:layout_alignParentBottom="true"
            android:layout_marginRight="30dp"
            android:layout_marginBottom="40dp" />
    
    </RelativeLayout>
    
    图 5:activity_property_animation.xml

    XML 文件有一个名为“sky”的整体 <RelativeLayout>,它充当动画的天空。它包含四个 <ImageView> 元素、一个嵌套的 <RelativeLayout>、一个 <TextView> 和一个 <ToggleButton>。

    • 这些 <ImageView> 元素的源来自“app/src/main/res/drawable/”——“sun.xml”(android:src="@drawable/sun")定义了太阳的“ShapeDrawable”对象,“window.xml”(android:src="@drawable/window")定义了窗框的“ShapeDrawable”对象,以及“cloud.gif”(android:src="@drawable/cloud")。

    • 嵌套的 <RelativeLayout> 称为“ground”,在动画中充当地面。

    • 这个 <TextView> 用于在动画期间显示当天的适当问候语。

    • 这个 <ToggleButton> 有一个 onClick 事件侦听器(android:onClick="onToggleClicked"),用于切换动画的开始/停止状态。

    每个控件都已分配唯一的 ID,以便在代码中进行引用和操作。

  • 要绘制太阳,请创建一个“Shape Drawable”资源,该资源代表太阳的“ShapeDrawable”对象,通过在 XML 文件中定义其几何形状,包括颜色和渐变。在“app/src/main/res/drawable/”目录中,创建一个名为“sun.xml”的 XML 文件并添加以下代码:
    <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:dither="true"
        android:shape="oval" >
    
        <gradient
            android:endColor="#ffff7700"
            android:gradientRadius="250"
            android:startColor="#ffffff00"
            android:type="radial"
            android:useLevel="false" />
    
        <size
            android:height="150dp"
            android:width="150dp" />
    
    </shape>

    这个 <shape> 声明了一个“椭圆”形状(android:shape="oval")的“ShapeDrawable”对象。然后通过 <gradient> 和 <size> 元素进一步定义此形状。

  • 同样,要绘制窗框,请创建一个“Shape Drawable”资源,该资源代表窗框的“ShapeDrawable”对象。在“app/src/main/res/drawable/”目录中,创建一个名为“window.xml”的 XML 文件并添加以下代码:

    <shape xmlns:android="http://schemas.android.com/apk/res/android"
    	android:shape="rectangle" >
        <solid android:color="#00000000" />
    
        <stroke
            android:width="20dp"
            android:color="#c16c11" />
    
    </shape>

    这个 <shape> 声明了一个“矩形”形状(android:shape="rectangle")的“ShapeDrawable”对象。然后通过 <stroke> 元素进一步定义此形状。

  • 接下来,定义动画细节。在“app/src/main/res/”目录中,创建一个名为“animator”的子目录,这是存储对象动画资源的默认目录。在此目录中,添加两个 XML 文件,如下所示:

    • 创建一个名为“sun_movement.xml”的动画资源文件,用于动画太阳,并添加以下代码:

      <?xml version="1.0" encoding="utf-8"?>
      
      <set xmlns:android="http://schemas.android.com/apk/res/android"
          android:interpolator="@android:anim/accelerate_decelerate_interpolator"
          android:ordering="sequentially" >
      
          <objectAnimator
              android:duration="10000"
              android:propertyName="y"
              android:repeatCount="infinite"
              android:repeatMode="reverse"
              android:valueTo="20dp"
              android:valueType="floatType" />
      
      </set>

      这个 <set> 声明了一个“AnimatorSet”对象,它将“AccelerateDecelerateInterpolator”作为时间插值器(android:interpolator="@android:anim/accelerate_decelerate_interpolator"),并按顺序(android:ordering="sequentially")动画其动画器(在此示例中只有一个,尽管您可以添加更多动画器,例如用于动画 x 轴的动画器)。在这种情况下,<set> 仅包含一个“ObjectAnimator”类,其中包含各种属性,用于设置对象动画的详细信息。在此示例中,要动画的属性是 y 轴(android:propertyName="y"),要动画的值是 float 类型(android:valueType="floatType"),动画的结束值为 20dp(android:valueTo="20dp"),动画的持续时间为 10000ms,即 10 秒(android:duration="10000")。这些属性还指定动画在每次完成后反向播放(android:repeatMode="reverse")并且无限期重复(android:repeatCount="infinite")。

    • 类似地,创建一个名为“cloud_movement.xml”的动画资源文件,用于动画云,并添加以下代码:
      <?xml version="1.0" encoding="utf-8"?>
      
      <set xmlns:android="http://schemas.android.com/apk/res/android"
          android:interpolator="@android:anim/linear_interpolator"
          android:ordering="together" >
      
          <objectAnimator
              android:duration="10000"
              android:propertyName="x"
              android:repeatCount="infinite"
              android:repeatMode="restart"
              android:valueTo="-300dp"
              android:valueType="floatType" />
      
          <objectAnimator
              android:duration="2000"
              android:propertyName="y"
              android:repeatCount="infinite"
              android:repeatMode="reverse"
              android:valueTo="70dp"
              android:valueType="floatType" />
      
      </set>

      这个 <set> 声明了一个“AnimatorSet”对象,它将“LinearInterpolator”作为时间插值器(android:interpolator="@android:anim/linear_interpolator"),并同时(android:ordering="together")动画其两个“ObjectAnimator”元素——每个元素用于“x”和“y”坐标。

  • 让我们盘点一下到目前为止您所做的工作:创建了一个布局文件和 UI 控件,在 XML 中定义了太阳和窗框的相应“ShapeDrawable”对象,并创建了太阳和云的相应动画资源文件。要触发动画,您需要向 Activity 添加代码。在 Android Studio 中打开“PropertyAnimationActivity.java”,然后按照以下步骤操作:

    • 添加必要的包和成员变量,如下所示:

      package com.peterleow.androidanimation;
      
      import android.animation.Animator;
      import android.animation.AnimatorInflater;
      import android.animation.AnimatorListenerAdapter;
      import android.animation.AnimatorSet;
      import android.animation.ArgbEvaluator;
      import android.animation.ObjectAnimator;
      import android.animation.ValueAnimator;
      import android.app.Activity;
      import android.graphics.Color;
      import android.os.Bundle;
      import android.view.Menu;
      import android.view.MenuItem;
      import android.view.View;
      import android.widget.ImageView;
      import android.widget.TextView;
      import android.widget.Toast;
      import android.widget.ToggleButton;
      
      public class PropertyAnimationActivity extends Activity {
      
          AnimatorSet sunAnimatorSet;
          AnimatorSet cloud1AnimatorSet;
          AnimatorSet cloud2AnimatorSet;
          ValueAnimator skyAnimator;
          ValueAnimator groundAnimator;
      
          @Override
          protected void onCreate(Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
              setContentView(R.layout.activity_property_animation);
      
          // ...

      请注意,“android:animation”包提供了“属性动画”系统中使用的所有类。

    • 添加以下代码以准备太阳的动画:

      @Override
      protected void onCreate(Bundle savedInstanceState) {
      
          // ...
      
          sunAnimatorSet = (AnimatorSet)AnimatorInflater.loadAnimator(this, R.animator.sun_movement);
          ImageView sun = (ImageView) findViewById(R.id.sun);
          sunAnimatorSet.setTarget(sun);
      
          // ...
      
      }

      代码首先加载名为“sun_movement.xml”(R.animator.sun_movement)的动画资源文件(其中包含一个“ObjectAnimator”对象的声明),并将其分配给一个名为“sunAnimatorSet”的“AnimatorSet”对象,然后调用此“AnimatorSet”对象的“setTarget”方法,将动画的目标设置为名为“sun”(R.id.sun)的 ImageView 对象(其图像源为“sun.xml”(android:src="@drawable/sun"))。要开始动画太阳,请调用“start()”方法。但是,稍后您将在切换按钮的 onClick 事件处理程序中添加“start()”方法。

    • 通过扩展“AnimatorListenerAdapter”类,向“sunAnimatorSet”对象添加动画侦听器,以侦听“onAnimationStart()”和“onAnimationEnd()”等事件。例如:

      @Override
      protected void onCreate(Bundle savedInstanceState) {
      
          // ...
      
          sunAnimatorSet.addListener(
      
                  new AnimatorListenerAdapter() {
      
                      public void onAnimationStart(Animator animation) {
                          Toast.makeText(getApplicationContext(), "Animation started!",
                                  Toast.LENGTH_SHORT).show();
                      }
      
                      public void onAnimationEnd(Animator animation) {
                          Toast.makeText(getApplicationContext(), "Animation ended!",
                                  Toast.LENGTH_SHORT).show();
                      }
      
                  });
      
          // ...
      
      }

      该侦听器将在“Toast”弹出消息中捕获并宣布动画的开始和结束事件。

    • 添加以下代码以准备云的动画:

      @Override
      protected void onCreate(Bundle savedInstanceState) {
      
          // ...
      
          // cloud1
          ImageView cloud1 = (ImageView) findViewById(R.id.cloud1);
          cloud1AnimatorSet = (AnimatorSet)AnimatorInflater.loadAnimator(this, R.animator.cloud_movement);
          cloud1AnimatorSet.setTarget(cloud1);
      
          // cloud2
          ImageView cloud2 = (ImageView) findViewById(R.id.cloud2);
          cloud2AnimatorSet = (AnimatorSet)AnimatorInflater.loadAnimator(this, R.animator.cloud_movement);
          cloud2AnimatorSet.setTarget(cloud2);
      
          // ...
      
      }

      请注意,两朵云共享同一个动画资源文件——“cloud_movement.xml”——其中包含两个“ObjectAnimator”对象的声明,它们将并发动画。

    • 添加以下代码以准备天空的动画:

      @Override
      protected void onCreate(Bundle savedInstanceState) {
      
          // ...
      
          skyAnimator = ObjectAnimator.ofInt
                  (findViewById(R.id.sky), "backgroundColor",
                          Color.rgb(0x00, 0x00, 0x4c), Color.rgb(0xae, 0xc2, 0xff));
          //set same duration and animation properties as others
          skyAnimator.setDuration(10000);
          skyAnimator.setEvaluator(new ArgbEvaluator());
          skyAnimator.setRepeatCount(ValueAnimator.INFINITE);
          skyAnimator.setRepeatMode(ValueAnimator.REVERSE);
      
          // ...
      
      }

      代码通过静态“ObjectAnimator.ofInt()”方法创建一个名为“skyAnimator”的“ObjectAnimator”对象(不是从 XML 创建)。此方法接受几个参数——以“R.id.sky”引用的天空对象作为动画目标,对象的属性——在本例中是“backgroundColor”——要动画,以及一组随时间动画的值,在本例中包含起始颜色值和结束颜色值。调用“setXxx()”方法的适当“ObjectAnimator”对象来设置动画的详细信息,例如“skyAnimator.setDuration(10000);”。

    • 通过实现“onAnimationUpdate()”方法,向“skyAnimator”对象添加一个更新侦听器,以在每个动画帧上接收更新回调。例如:

      @Override
      protected void onCreate(Bundle savedInstanceState) {
      
          // ...
      
          skyAnimator.addUpdateListener(
      
                  new ValueAnimator.AnimatorUpdateListener() {
      
                      TextView textView = (TextView) findViewById(R.id.textView);
                      float animatedFractionPrev = 0.0f;
                      float animatedFractionCurr = 0.0f;
      
                      @Override
                      public void onAnimationUpdate(ValueAnimator valueAnimator) {
                          animatedFractionCurr = valueAnimator.getAnimatedFraction();
                          if (animatedFractionCurr > animatedFractionPrev) {
                              if (animatedFractionCurr > 0.0 && animatedFractionCurr <= 0.70) {
                                  textView.setText("Good morning!");
                              } else {
                                  textView.setText("Good day!");
                              }
                          } else {
                              if (animatedFractionCurr >= 0.8) {
                                  textView.setText("Good day!");
                              } else if (animatedFractionCurr < 0.8 && animatedFractionCurr >= 0.1) {
                                  textView.setText("Good afternoon!");
                              } else {
                                  textView.setText("Good Evening!");
                              }
                          }
                          animatedFractionPrev = animatedFractionCurr;
                      }
                  }
          );
      
          // ...
      
      }

      在每次回调时,通过调用“getAnimatedFraction()”来获取当前动画进度。此当前动画进度将用于确定要在“Toast”弹出消息中宣布的适当问候语。我将把这部分逻辑留给您来解决。

    • 添加以下代码以准备地面的动画:

      @Override
      protected void onCreate(Bundle savedInstanceState) {
      
          // ...
      
          groundAnimator = ObjectAnimator.ofInt
                  (findViewById(R.id.ground), "backgroundColor",
                          Color.rgb(0x00, 0x47, 0x00), Color.rgb(0x85, 0xae, 0x85));
          //set same duration and animation properties as others
          groundAnimator.setDuration(10000);
          groundAnimator.setEvaluator(new ArgbEvaluator());
          groundAnimator.setRepeatCount(ValueAnimator.INFINITE);
          groundAnimator.setRepeatMode(ValueAnimator.REVERSE);
      
          // ...
      
      }

      代码与准备天空动画的代码类似。

    • 添加名为“onToggleClicked”的 onClick 事件处理程序,以切换动画的开始/停止。例如:

      public void onToggleClicked(View view) {
      
          boolean on = ((ToggleButton) view).isChecked();
      
          if (on) {
              sunAnimatorSet.start();
              skyAnimator.start();
              groundAnimator.start();
              cloud1AnimatorSet.start();
              cloud2AnimatorSet.start();
          } else {
              sunAnimatorSet.cancel();
              skyAnimator.cancel();
              groundAnimator.cancel();
              cloud1AnimatorSet.cancel();
              cloud2AnimatorSet.cancel();
          }
      
      }}
      
    • 在 Activity 的“onResume()”生命周期事件中添加以下代码,以在 Activity 恢复时将切换按钮状态重置为“未选中”:

      @Override
      public void onResume() {
          super.onResume();
          ((ToggleButton)findViewById(R.id.toggleAnimate)).setChecked(false);
      }

您已完成一项练习,即使用“属性动画”系统实现动画,请在真实设备或 AVD 上进行测试。

视图动画

继续进行一项练习,使用“视图动画”系统创建简单的补间动画。请参阅图 6 了解预期的结果。

图 6:视图动画

图 5 中的屏幕包含一个按钮和五个在屏幕上散布的 CodeProject 吉祥物。当点击按钮时,五个吉祥物将同时旋转并移动到屏幕中心。

在 Android Studio IDE 中:

  • 打开“activity_view_animation.xml”,并添加以下 XML 代码以创建布局和 UI 控件,如图 7 所示。

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        android:paddingBottom="@dimen/activity_vertical_margin"
        tools:context="com.peterleow.androidanimation.ViewAnimationActivity"
        android:background="@color/background">
    
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/btnGetTogether"
            android:onClick="getTogether"
            android:text="@string/get_together"
            android:layout_centerHorizontal="true"/>
    
        <ImageView
            android:id="@+id/imageView1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/codeproject_mascot"
            android:layout_above="@+id/imageView2"
            android:layout_toLeftOf="@+id/imageView2"
            android:layout_toStartOf="@+id/imageView2" />
    
        <ImageView
            android:id="@+id/imageView2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/codeproject_mascot"
            android:layout_centerVertical="true"
            android:layout_alignParentRight="true"/>
    
        <ImageView
            android:id="@+id/imageView3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/codeproject_mascot"
            android:layout_toStartOf="@+id/btnGetTogether"
            android:layout_above="@+id/imageView5"
            android:layout_alignRight="@+id/imageView4"
            android:layout_alignEnd="@+id/imageView4" />
    
        <ImageView
                android:id="@+id/imageView4"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:src="@drawable/codeproject_mascot"
                android:layout_toStartOf="@+id/btnGetTogether"
                android:layout_below="@+id/btnGetTogether"
                android:layout_alignParentLeft="true"
                android:layout_alignParentStart="true" />
    
        <ImageView
            android:id="@+id/imageView5"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/codeproject_mascot"
            android:layout_alignParentBottom="true"
            android:layout_centerHorizontal="true"
            android:layout_marginBottom="43dp" />
    
    </RelativeLayout>
    
    图 7:activity_view_animation.xml

    XML 文件包含以下元素:

    • 这五个 <ImageView> 元素的源来自“app/src/main/res/drawable/”中的“codeproject_mascot.gif”(android:src="@drawable/codeproject_mascot")。

    • 这个 <Button> 元素有一个 onClick 事件侦听器(android:onClick="getTogether"),用于启动动画。

  • 要触发动画,您需要向 Activity 添加代码。在 Android Studio 中打开“ViewAnimationActivity.java”,然后按照以下步骤操作:

    • 添加必要的包,如下所示:

      package com.peterleow.androidanimation;
      
      import android.app.Activity;
      import android.os.Bundle;
      import android.util.DisplayMetrics;
      import android.view.Menu;
      import android.view.MenuItem;
      import android.view.View;
      import android.view.animation.AccelerateDecelerateInterpolator;
      import android.view.animation.Animation;
      import android.view.animation.AnimationSet;
      import android.view.animation.LinearInterpolator;
      import android.view.animation.RotateAnimation;
      import android.view.animation.TranslateAnimation;
      import android.widget.ImageView;
      
      public class ViewAnimationActivity extends Activity {
          @Override
          protected void onCreate(Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
              setContentView(R.layout.activity_view_animation);
          }
      
      // ...

      请注意,“android.view.animation”提供了“视图动画”系统中使用的所有类。

    • 添加事件处理程序“getTogether()”用于按钮的 onClick 事件侦听器(android:onClick="getTogether"),如下所示:

      public void getTogether(View view) {
          
          ImageView imageView = (ImageView) findViewById(R.id.imageView1 );
          move(imageView);
          
          imageView = (ImageView) findViewById(R.id.imageView2 );
          move(imageView);
          
          imageView = (ImageView) findViewById(R.id.imageView3 );
          move(imageView);
          
          imageView = (ImageView) findViewById(R.id.imageView4);
          move(imageView);
          
          imageView = (ImageView) findViewById(R.id.imageView5);
          move(imageView);
      }

      代码只是从 XML 文件获取五个 ImageView 控件,并将它们提交给 move() 方法进行动画处理。

    • 动画代码包含在 move() 方法中,如下所示:

      private void move( View view )
      {
          // Get the x, y coordinates of the screen center
          DisplayMetrics displayMetrics = new DisplayMetrics();
          this.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
          int x_centerOfScreen = (displayMetrics.widthPixels / 2) - (view.getMeasuredWidth() / 2);
          int y_centerOfScreen = (displayMetrics.heightPixels / 2) - (view.getMeasuredHeight() / 2);
          
          // Get the starting coordinates of the view
          int startPosition[] = new int[2];
          view.getLocationOnScreen(startPosition);
          
          AnimationSet animationSet = new AnimationSet(false);
      
          RotateAnimation rotate = new RotateAnimation(0,360, 
                  Animation.RELATIVE_TO_SELF, 0.5f , Animation.RELATIVE_TO_SELF,0.5f );
          rotate.setInterpolator(new LinearInterpolator());
          rotate.setRepeatCount(Animation.INFINITE);
          rotate.setDuration(2000);
      
          animationSet.addAnimation(rotate);
      
          TranslateAnimation translate =  new TranslateAnimation(Animation.ABSOLUTE, 0.0f,
                  Animation.ABSOLUTE, x_centerOfScreen - startPosition[0],
                  Animation.ABSOLUTE, 0.0f, Animation.ABSOLUTE, y_centerOfScreen - startPosition[1]);
          translate.setInterpolator(new AccelerateDecelerateInterpolator());
          translate.setDuration(2000);
          animationSet.addAnimation(translate);
      
          animationSet.setFillAfter(true);
         
          view.startAnimation(animationSet);
      }

      除了获取屏幕中心和 ImageView 对象的起始位置的 x 和 y 坐标外,代码还从“android.view.animation”包创建一个“AnimationSet”对象,名为“animationSet”,用于组合一组将一起播放的动画。

      创建两种类型的动画——“RotateAnimation”和“TranslateAnimation”。前者用于旋转视图,后者用于更改其在屏幕上的位置。这两种动画中的每一种都将具有由各种“setXxx()”方法(例如 setInterpolator()、setDuration() 等)定义的自己的动画参数(例如插值器、持续时间等)。通过调用“addAnimation()”方法将这些动画添加到“animationSet”中。要动画视图,请调用 View 的“startAnimation()”方法,并将“animationSet”对象传递给它。“setFillAfter(true)”指定变换将在完成后保留,换句话说,所有五个吉祥物将在动画结束后停留在屏幕中心。但是,如果您再次单击按钮,五个吉祥物将出现在原始位置,然后再次动画到屏幕中心。这告诉你什么?嗯,视图动画系统下的动画不会修改原始视图,它只是在它们的新位置重新绘制它们。

您已完成一项练习,即使用“视图动画”系统实现动画,请在真实设备或 AVD 上进行测试。

可绘制动画

继续进行一项练习,创建“可绘制动画”,它像幻灯片一样逐个播放一系列可绘制资源。请参阅图 8 了解预期的结果。

图 8:可绘制动画

图 8 中的屏幕显示一张照片。当您单击照片时,它将按顺序循环显示八张照片,每张照片显示 1500ms。

在 Android Studio IDE 中:

  • 打开“activity_drawable_animation.xml”,并添加一个 <ImageView> 元素,如下所示:

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        android:paddingBottom="@dimen/activity_vertical_margin"
        tools:context="com.peterleow.androidanimation.DrawableAnimationActivity"
        android:background="@color/background">
    
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/imageViewSlideShow"
            android:layout_alignParentTop="true"
            android:layout_centerHorizontal="true"
            android:layout_marginTop="43dp" />
    
    </RelativeLayout>
    

    此 <ImageView> 将是照片在幻灯片中更改的占位符。

  • 在“/app/src/main/res/drawable/”目录中,创建一个名为“slide_show.xml”的 XML 文件并添加以下代码:

    <?xml version="1.0" encoding="utf-8"?>
    
    <animation-list xmlns:android="http://schemas.android.com/apk/res/android"
        android:oneshot="false">
    
        <item android:drawable="@drawable/photo_1" android:duration="1500" />
        <item android:drawable="@drawable/photo_2" android:duration="1500" />
        <item android:drawable="@drawable/photo_3" android:duration="1500" />
        <item android:drawable="@drawable/photo_4" android:duration="1500" />
        <item android:drawable="@drawable/photo_5" android:duration="1500" />
        <item android:drawable="@drawable/photo_6" android:duration="1500" />
        <item android:drawable="@drawable/photo_7" android:duration="1500" />
        <item android:drawable="@drawable/photo_8" android:duration="1500" />
    
    </animation-list>
    

    XML 文件包含一个 <animation-list> 节点,该节点包含八个 <item> 节点。每个 <item> 节点代表动画中的一帧,并分配给一张照片(android:drawable="@drawable/photo_1"),并持续动画 1500ms。动画将无限重复,如 <animation-list> 中的“android:oneshot="false"”所指定。

  • 要触发动画,您需要向 Activity 添加代码。打开 Android Studio 中的“DrawableAnimationActivity.java”,然后按照以下步骤操作:

    • 添加必要的包,如下所示:

      package com.peterleow.androidanimation;
      
      import android.app.Activity;
      import android.graphics.drawable.AnimationDrawable;
      import android.os.Bundle;
      import android.view.Menu;
      import android.view.MenuItem;
      import android.view.MotionEvent;
      import android.widget.ImageView;
      
      public class DrawableAnimationActivity extends Activity {
      
          AnimationDrawable slideShowAnimation;
      
          @Override
          protected void onCreate(Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
              setContentView(R.layout.activity_drawable_animation);
      
          // ...

      请注意,“AnimationDrawable”类在“android.graphics.drawable”包中,负责创建逐帧动画。

    • 添加以下代码以准备动画,如下所示:

      @Override
      protected void onCreate(Bundle savedInstanceState) {
      
          // ...
      
          ImageView slideShow = (ImageView) findViewById(R.id.imageViewSlideShow);
          slideShow.setBackgroundResource(R.drawable.slide_show);
          slideShowAnimation = (AnimationDrawable) slideShow.getBackground();
      }

      代码只是获取可绘制资源,即“slide_show.xml”,并将其添加为 ImageView 的背景图像。

    • 添加“onTouchEvent()”,如下所示:

      public boolean onTouchEvent(MotionEvent event) {
      
          if (event.getAction() == MotionEvent.ACTION_DOWN) {
              slideShowAnimation.start();
              return true;
          }
      
          return super.onTouchEvent(event);
      }

      当您触摸屏幕上的照片时,此方法将被触发,并调用“start()”方法来启动幻灯片动画。

您已完成一项练习,即使用“可绘制动画”系统实现动画,请在真实设备或 AVD 上进行测试。

画布

在前面的练习中,您使用了现成的图像和 XML 定义的图形作为可绘制资源。这些可绘制资源是静态的,无法更改。在某些情况下,您需要动态地在屏幕上绘制形状。为此,您必须使用“Canvas”类。

例如,要在屏幕上动态绘制矩形,有两种方法:

通过“ShapeDrawable”类

扩展“View”类,创建一个“RectShape”对象(它是“Shape”类的子类),并根据用户输入设置必要的尺寸,然后将其传递给“ShapeDrawable”对象以管理其在屏幕上的存在。当需要绘制矩形时,调用 View 的“invalidate()”方法,该方法会调用“onDraw()”方法来执行实际绘制。

ShapeDrawable shapeDrawable;

//...

Shape shape = new RectShape();
shapeDrawable = new ShapeDrawable(shape);
shapeDrawable.setBounds(left, top, right, bottom);
shapeDrawable.getPaint().setColor(Color.BLUE);

// ...

invalidate();

重写 View 的“onDraw()”方法,通过调用“draw()”方法来执行绘制,该方法以“Canvas”对象作为参数。

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    shapeDrawable.draw(canvas);
}

使用此方法,您将创建一个允许用户在屏幕上自由绘制矩形的 Activity。请参阅图 9 了解预期的结果。

图 9:在 Canvas 上绘图

当您连续触摸、移动并抬起手指时,将绘制一个蓝色矩形。

首先,创建一个名为“ShapeDrawableView.java”的自定义“View”类,如下所示:

package com.peterleow.androidanimation;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.RectShape;
import android.graphics.drawable.shapes.Shape;
import android.view.MotionEvent;
import android.view.View;
import java.util.ArrayList;
import java.util.List;

/**
 * Created by Peter Leow on 28/9/2014.
 */
public class ShapeDrawableView extends View {

    int startX, startY, endX, endY;

    private List<ShapeDrawable> shapeDrawables =
            new ArrayList<ShapeDrawable>();

    public ShapeDrawableView(Context context) {
        super(context);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        for(ShapeDrawable shapeDrawable: shapeDrawables) {
            shapeDrawable.draw(canvas);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            startX = (int)event.getX();
            startY = (int)event.getY();
            return true;
        } else if (event.getAction() == MotionEvent.ACTION_UP){
            endX = (int)event.getX();
            endY = (int)event.getY();
            if (startX > endX) {
                int tmp = startX;
                startX = endX;
                endX = tmp;
            }
            if (startY > endY) {
                int tmp = startY;
                startY = endY;
                endY = tmp;
            }

            Shape shape = new RectShape();
            ShapeDrawable shapeDrawable = new ShapeDrawable(shape);
            shapeDrawable.setBounds(startX, startY, endX, endY);
            shapeDrawable.getPaint().setColor(Color.BLUE);
            shapeDrawables.add(shapeDrawable);
            invalidate();

            return true;
        }
        return false;
    }
}

代码的一部分将所有绘制的矩形存储在“ArrayList”中,以便在每次“onDraw()”回调时重新绘制它们。代码还包括确定要绘制的矩形的起点和终点的逻辑,通过检测相应手势事件中的手指位置——“MotionEvent.ACTION_DOWN”和“MotionEvent.ACTION_UP”。

接下来,打开“ShapeDrawableActivity”并添加以下代码以显示“ShapeDrawableView”视图。

package com.peterleow.androidanimation;

import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;

public class ShapeDrawableActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        ShapeDrawableView shapeDrawableView = new ShapeDrawableView(this);
        setContentView(shapeDrawableView);
    }

    // ...
}

现在可以进行测试了!

通过“Canvas”的 drawXxx() 方法

扩展“View”类,创建一个“Paint”对象,该对象定义了要绘制的形状的样式和颜色信息。当需要绘制形状时,调用 View 的“invalidate()”方法,该方法会导致回调到“onDraw()”方法执行实际绘制。

Paint paint = new Paint();

//...

paint.setColor(Color.WHITE);
paint.setAntiAlias(true);
paint.setTextSize(45f);
paint.setStrokeWidth(2.0f);
paint.setStyle(Paint.Style.STROKE);
paint.setShadowLayer(5.0f, 10.0f, 10.0f, Color.BLACK);

// ...

invalidate();

重写 View 的“onDraw()”方法,通过调用“drawRect()”方法来绘制矩形,该方法属于“Canvas”对象,并由“onDraw()”方法作为参数。矩形将根据传递到“drawRect()”方法中的“Paint”对象的样式和颜色信息进行渲染。

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    canvas.drawRect(left, top, right, bottom, paint);
}

使用此方法,您将创建一个 Activity,该 Activity 将在屏幕上以不同角度重复“绘制”文本消息。请参阅图 10 了解预期的结果。

图 10:在 Canvas 上绘图

首先,创建一个名为“RotateTextView.java”的自定义“View”类,如下所示:

package com.peterleow.androidanimation;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.View;

/**
 * Created by Peter Leow on 28/9/2014.
 */
public class RotateTextView extends View {

    private String message = "     Code Project";

    public RotateTextView(Context context) {
        super(context);
        this.setBackgroundColor(Color.GRAY);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int viewWidth = getWidth();
        int viewHeight = getHeight();
        canvas.translate(viewWidth/2, viewHeight/2);

        Paint paint = new Paint();
        paint.setColor(Color.WHITE);
        paint.setAntiAlias(true);
        paint.setTextSize(45f);
        paint.setStrokeWidth(2.0f);
        paint.setStyle(Paint.Style.STROKE);
        paint.setShadowLayer(5.0f, 10.0f, 10.0f, Color.BLACK);

        for(int i = 0; i < 10; i++) {

            canvas.drawText(message, 0, 0, paint);

            canvas.rotate(36);
        }
    }
}

代码将以不同角度绘制文本消息十次。

接下来,打开“RotateTextViewActivity”并添加以下代码以显示“RotateTextView”视图。

package com.peterleow.androidanimation;

import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;

public class RotateTextViewActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        RotateTextView rotateTextView = new RotateTextView(this);
        setContentView(rotateTextView);
    }
    
    // ...
}

现在可以进行测试了!

OpenGL ES

2D 图形和动画到此为止,让我们继续对如何使用 OpenGL ES API 在 Android 中创建 3D 图形和动画进行基本了解。

了解基础知识

一般来说,OpenGL ES API 在 Android 框架中提供了两个主要类来实现此目的——“GLSurfaceView”和“GLSurfaceView.Renderer”。

GLSurfaceView”提供了一个视图对象,可以在其中绘制和操作 3D 图形。实际绘制由实现“GLSurfaceView.Renderer”接口并附加到“GLSurfaceView”对象的渲染器对象执行。例如:

package com.peterleow.androidanimation;

import android.app.Activity;
import android.opengl.GLSurfaceView;

public class OpenGLActivity extends Activity {

    private GLSurfaceView glSurfaceView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        glSurfaceView = new GLSurfaceView(this);
        glSurfaceView.setRenderer(new OpenGLRenderer());
        setContentView(glSurfaceView);
    }

// ...

渲染器对象必须实现“GLSurfaceView.Renderer”接口的以下三个方法:

下面显示了渲染器类的骨架:

public class OpenGLRenderer implements GLSurfaceView.Renderer {

    public OpenGLRenderer()
    {
        // ...
    }
    public void onSurfaceCreated(GL10 gl, EGLConfig config)
    {
        // ...
    }
    public void onSurfaceChanged(GL10 gl, int width, int height)
    {
        // ...
    }
    @Override
    public void onDrawFrame(GL10 gl)
    {
        // ...
    }
}

让事情发生

继续进行一项练习,通过使用 OpenGL ES 1.0 创建一个围绕其 3 个轴旋转的立方体。(有不同版本的 API 可用,我选择在这里使用 OpenGL ES 1.0,它足以满足此入门级别。)请参阅图 11 了解预期的结果。

图 11:旋转立方体

首先,创建一个名为“Cube”的 Java 类来表示立方体对象。请按照以下步骤操作:

  • 添加必要的包,特别是这个——“javax.microedition.khronos.opengles.GL10”,并声明一个“ByteBuffer”、“ByteOrder”和“FloatBuffer”。

    package com.peterleow.androidanimation;
    
    import java.nio.ByteBuffer;
    import java.nio.ByteOrder;
    import java.nio.FloatBuffer;
    import javax.microedition.khronos.opengles.GL10;
    
    /**
     * Created by Peter Leow on 29/9/2014.
     */
    public class Cube {
    
        private FloatBuffer vertexBuffer;
        private ByteBuffer  drawListBuffer;
        private FloatBuffer colorBuffer;
    
    // ...
  • 在名为“cubeCoords”的数组中定义立方体的八个顶点的坐标,如下所示:

    // ...
    
    public class Cube {
    
        // ...
    
        // Coordinates for vertices
        static float cubeCoords[] = {
               -1.0f, -1.0f,  1.0f,  // vertex 0 (x0, y0, z0)
                1.0f, -1.0f,  1.0f,  // vertex 1 (x1, y1, z1)
                1.0f,  1.0f,  1.0f,  // vertex 2 (x2, y2, z2)
               -1.0f,  1.0f,  1.0f,  // vertex 3 (x3, y3, z3)
               -1.0f, -1.0f, -1.0f,  // and so on...
                1.0f, -1.0f, -1.0f,
                1.0f,  1.0f, -1.0f,
               -1.0f,  1.0f, -1.0f
        };
    
        // ...
    
    }
  • 在名为“colors”的数组中定义每个顶点的颜色,如下所示:

    // ...
    
    public class Cube {
    
        // ...
    
        // Color definition
        private float colors[] = {
                1.0f, 0.0f, 0.0f, 1.0f, // red for vertex 0
                0.0f, 1.0f, 0.0f, 1.0f, // gree for vertex 1
                0.0f, 0.0f, 1.0f, 1.0f, // blue for vertex 2
                1.0f, 1.0f, 0.0f, 1.0f, // yellow for vertex 3
                1.0f, 1.0f, 0.0f, 1.0f, // and so on...
                0.0f, 0.0f, 1.0f, 1.0f,
                0.0f, 1.0f, 0.0f, 1.0f,
                1.0f, 0.0f, 0.0f, 1.0f
        };
    }
    
    // ...
  • 在名为“drawOrder”的数组中指定绘制立方体的顺序,如下所示:

    // ...
    
    public class Cube {
    
        // ...
    
        // Drawing order of cubeCoords[]
        private byte drawOrder[] = {
                0, 1, 3, 1, 3, 2,
                1, 2, 6, 1, 6, 5,
                0, 3, 7, 0, 7, 4,
                4, 7, 6, 4, 6, 5,
                3, 7, 2, 7, 2, 6,
                0, 4, 1, 4, 1, 5
        };
    
        // ...
    
    }

    数组中每个元素的值对应于“cubeCoords”中顶点的坐标。例如,值 2 指的是顶点 2 (x2, y2, z2),依此类推。

  • 在构造函数中,将顶点、颜色和绘制顺序的信息放入相应的缓冲区。

    public Cube() {
    
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(cubeCoords.length * 4);
        byteBuffer.order(ByteOrder.nativeOrder());
    
        vertexBuffer = byteBuffer.asFloatBuffer();
        vertexBuffer.put(cubeCoords);
        vertexBuffer.position(0);
        
        byteBuffer = ByteBuffer.allocateDirect(colors.length * 4);
        byteBuffer.order(ByteOrder.nativeOrder());
        colorBuffer = byteBuffer.asFloatBuffer();
        colorBuffer.put(colors);
        colorBuffer.position(0);
        
        drawListBuffer = ByteBuffer.allocateDirect(drawOrder.length);
        drawListBuffer.put(drawOrder);
        drawListBuffer.position(0);
    
    }
  • 创建一个“draw()”方法,该方法调用 OpenGL ES 1.0 API 的各种方法来组合对象并渲染立方体。有关这些方法的详细信息,请参阅“javax.microedition.khronos.opengles.GL10”。

    public void draw(GL10 gl) {
    
        // Enable vertex array buffer to be used during rendering
        gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
        // Tell openGL where the vertex array buffer is
        gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
    
    
        // Enable color array buffer to be used during rendering
        gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
        // Tell openGL where the color array buffer is
        gl.glColorPointer(4, GL10.GL_FLOAT, 0, colorBuffer);
        
        // Draw each plane as a pair of triangles, based on the drawListBuffer information
        gl.glDrawElements(GL10.GL_TRIANGLES, drawOrder.length, GL10.GL_UNSIGNED_BYTE, drawListBuffer);
    
    }

您已创建了一个代表立方体对象的 Java 类“Cube”。接下来,创建一个名为“OpenGLRenderer”的渲染器类,该类实现“GLSurfaceView.Renderer”接口来渲染立方体。请按照以下步骤操作:

  • 添加必要的包,并在构造函数中实例化一个“Cube”对象,如下所示:

    package com.peterleow.androidanimation;
    
    import android.opengl.GLSurfaceView;
    import android.opengl.GLU;
    import javax.microedition.khronos.egl.EGLConfig;
    import javax.microedition.khronos.opengles.GL10;
    
    /**
     * Created by Peter Leow on 29/9/2014.
     */
    public class OpenGLRenderer implements GLSurfaceView.Renderer {
    
        private Cube cube;
    
        private float rotationAngle;
    
        public OpenGLRenderer()
        {
            cube = new Cube();
        }
    
    // ...
  • 添加代码以实现“GLSurfaceView.Renderer”接口的三个方法。

    public void onSurfaceCreated(GL10 gl, EGLConfig config)
    {
        gl.glEnable(GL10.GL_DEPTH_TEST);
    }
        
    public void onSurfaceChanged(GL10 gl, int width, int height)
    {
        gl.glViewport(0, 0, width, height);
        gl.glMatrixMode(GL10.GL_PROJECTION);
        gl.glLoadIdentity();
        
        float fovy = 50.0f; // Field of view angle, in degrees, in the Y direction.
        float aspect = (float)width / (float)height;
        float zNear = 0.1f;
        float zFar = 100.0f;
        // Set up a perspective projection matrix
        GLU.gluPerspective(gl, fovy, aspect, zNear, zFar);
        
        gl.glMatrixMode(GL10.GL_MODELVIEW);
        gl.glLoadIdentity();
    }
        
    @Override
    public void onDrawFrame(GL10 gl)
    {
        gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
        // Replace the current matrix with the identity matrix.
        gl.glLoadIdentity();
        gl.glTranslatef(0.0f, 0.0f, -6.0f);
        gl.glScalef(0.8f, 0.8f, 0.8f);
        gl.glRotatef(rotationAngle, 1.0f, 1.0f, 1.0f);
        cube.draw(gl);
        rotationAngle -= 0.4f;
        }
    }

打开“OpenGLActivity”以添加显示 3D 立方体的代码。请按照以下步骤操作:

  • 添加“android.opengl.GLSurfaceView”包,实例化一个“GLSurfaceView”视图,该视图接受前面创建的“OpenGLRenderer”类的实例作为参数,并将此“GLSurfaceView”视图设置为 Activity 的内容视图。

    package com.peterleow.androidanimation;
    
    // ...
    import android.opengl.GLSurfaceView;
    
    public class OpenGLActivity extends Activity {
    
        private GLSurfaceView glSurfaceView;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
    
            glSurfaceView = new GLSurfaceView(this);
            glSurfaceView.setRenderer(new OpenGLRenderer());
            setContentView(glSurfaceView);
        }
    
    // ...
  • 在 Activity 生命周期各自的“onPause()”和“onResume()”方法中添加代码以暂停和恢复渲染。

    @Override
    protected void onPause()
    {
        super.onPause();
        glSurfaceView.onPause();
    }
    
    @Override
    protected void onResume()
    {
        super.onResume();
        glSurfaceView.onResume();
    }
    

摘要

在这段旅程中,您通过一系列实践练习学习并探索了各种 API 来实现简单的动画和创建简单的 2D 和 3D 图形。为了方便参考,我将它们放在一个链接中,以便快速跳转到本文的相应主题。

参考

© . All rights reserved.