Android5 高级教程(五)
十六、广播接收器和长期服务
除了活动、内容提供者和服务,广播接收器是 Android 流程中的另一个组件。广播接收器是可以响应客户端发送的广播消息的组件。该消息被建模为意图。此外,广播消息(intent)可以由一个以上的接收器来响应。
活动或服务等客户端组件使用上下文类中可用的 sendBroadcast(intent) 方法来发送广播。广播意图的接收组件将需要从 Android SDK 中可用的 BroadcastReceiver 类继承。这些广播接收器需要通过一个 receiver 组件标签在清单文件中注册,以表明接收器对响应某种类型的广播意图感兴趣。
发送广播
清单 16-1 显示了发送广播事件的示例代码。这段代码创建了一个具有唯一意图动作字符串的意图,在其上放置了一个名为消息的额外字段,并调用 sendBroadcast() 方法。将额外的放在目标上是可选的。
清单 16-1 。传播意图
//This code is in class: TestBCRActivity.java
//Project: TestBroadcastReceiver, Download: ProAndroid5_Ch16_TestReceivers.zip
private void testSendBroadcast(Activity activity) {
//Create an intent with a unique action string
String uniqueActionString = "com.androidbook.intents.testbc";
Intent broadcastIntent = new Intent(uniqueActionString);
//Allow stand alone cross-processes that have broadcast receivers
//in them to be started even though they are in stopped state.
broadcastIntent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);
broadcastIntent.putExtra("message", "Hello world");
activity.sendBroadcast(broadcastIntent);
}
在清单 16-1 中,动作是一个满足您需求的任意标识符。为了使这个动作字符串唯一,您可能希望使用一个类似于 Java 包的名称空间。此外,我们将在本章后面的“进程外接收者”一节中讨论跨进程 FLAG _ INCLUDE _ STOPPED _ PACKAGES
编码一个简单的接收器
清单 16-2 显示了一个广播接收器,它可以响应来自清单 16-1 的广播意图。
清单 16-2 。示例广播接收器代码
//This class is in TestBroadcastReceiver project in the download
//The download for this chapter is: ProAndroid5_Ch16_TestReceivers.zip
public class TestReceiver extends BroadcastReceiver {
private static final String tag = "TestReceiver";
@Override
public void onReceive(Context context, Intent intent) {
Log.d("TestReceiver", "intent=" + intent);
String message = intent.getStringExtra("message");
Log.d(tag, message);
}
}
创建一个广播接收器非常简单。扩展 BroadcastReceiver 类并覆盖 onReceive() 方法。我们能够看到接收者的意图并从中提取信息。接下来,我们需要在清单文件中将广播接收器注册为接收器。
在清单文件中注册接收方
清单 16-3 展示了如何将一个接收者声明为意图的接收者,其动作是 com . androidbook . intents . testbc。
清单 16-3 。清单文件中的接收器定义
<!--
In filename: AndroidManifest.xml
Project: TestBroadcastReceiver, Download: ProAndroid5_Ch16_TestReceivers.zip
-->
<manifest>
<application>
...
<activity>...</activity>
...
<receiver android:name=".TestReceiver">
<intent-filter>
<action android:name="com.androidbook.intents.testbc"/>
</intent-filter>
</receiver>
...
</application>
</manifest>
与活动等其他组件节点一样, receiver 元素是应用元素的子节点。
有了接收者(清单 16-2 )及其在清单文件(清单 16-3 )中的注册,您可以使用清单 16-1 中的客户端代码来调用接收者。我们已经在本章末尾提供了本章的可下载 ZIP 文件 pro Android 5 _ Ch16 _ test receivers . ZIP 的参考。这个 ZIP 文件有两个项目。到目前为止引用的代码在项目 TestBroadcastReceiver 中。
容纳多个接收器
广播的概念是可以有一个以上的接收器。让我们将 TestReceiver (参见清单 16-2 )复制为 TestReceiver2 ,看看两者是否能够响应相同的广播消息。 TestReceiver2 的代码在清单 16-4 中给出。
清单 16-4 。测试接收 2 的源代码
//Filename: TestReceiver2.java
//Project: TestBroadcastReceiver, Download: ProAndroid5_Ch16_TestReceivers.zip
public class TestReceiver2 extends BroadcastReceiver {
private static final String tag = "TestReceiver2";
@Override
public void onReceive(Context context, Intent intent) {
Log.d(tag, "intent=" + intent);
String message = intent.getStringExtra("message");
Log.d(tag, message);
}
}
将这个接收者添加到您的清单文件中,如清单 16-5 所示。
清单 16-5 。清单文件中的 TestReceiver2 定义
<!--
In filename: AndroidManifest.xml
Project: TestBroadcastReceiver, Download: ProAndroid5_Ch16_TestReceivers.zip
-->
<receiver android:name=".TestReceiver2">
<intent-filter>
<action android:name="com.androidbook.intents.testbc"/>
</intent-filter>
</receiver>
现在,如果你像清单 16-1 中的那样启动事件,两个接收者都将被调用。
我们已经在第十三章中指出,主线程运行所有属于单个进程的广播接收器。您可以通过打印出每个接收器中的线程签名来证明这一点,包括主线调用代码。您将看到相同的线程按顺序运行这段代码。 sendBroadcast() 对广播消息进行排队,并让主线程返回其队列。接收者对这个排队消息的响应是由同一个主线程按顺序执行的。当有多个接收者时,依赖于执行顺序来决定首先调用哪个接收者并不是一个好的设计。
使用进程外接收器
广播的意图更可能是响应它的进程是未知的,并且与客户端进程分离。你可以通过复制一个你目前展示的接收器并创建一个单独的来证明这一点。apk 文件。然后,当您从清单 16-1 中触发事件时,您将会看到两个进程内接收者(那些在同一个项目或中的接收者)。apk 文件)和进程外接收器(那些在单独的中的)。apk 文件)被调用。您还将通过 LogCat 消息看到进程内和进程外接收器在各自的主线程中运行。
然而,在 API 12 (Android 3.1)之后,外部进程中的广播接收器出现了一些问题。这是由于 SDK 出于安全考虑而采用的发布模式。您可以在本章提供的参考链接中了解更多信息。随着这一变化,应用在安装时将处于停止状态。可以启动组件的意图现在可以指定只针对那些处于启动状态的应用。默认情况下,旧的行为仍然存在。但是,对于广播意图,系统会自动添加一个标记来排除处于停止状态的应用。为了克服前一点,可以在广播意图上明确地设置意图标志,以将那些停止的应用作为有效目标包括在内。这就是你在清单 16-1 的代码中看到的。
我们在本章的可下载 ZIP 文件 proandroid 5 _ Ch16 _ test receivers . ZIP 中包含了一个额外的独立项目,名为 standalone broadcast receiver 来测试这个概念。要尝试它,您必须在模拟器上部署调用项目 TestBroadcastReceiver 和独立接收方的项目 StandloneBroadcastReceiver。然后,您可以使用 TestBroadcastReceiver 项目发送广播事件,并监视来自 standalone broadcastreceiver 的接收器响应的 LogCat。
使用来自接收者的通知
广播接收器通常需要向用户传达已发生的事情或状态。这通常是通过系统通知栏中的通知图标提醒用户来实现的。我们现在将向您展示如何从广播接收器创建通知、发送通知以及通过通知管理器查看通知。
通过通知管理器监控通知
Android 在通知区域显示通知图标作为提醒。通知区域位于装置顶部,呈一条带状,看起来像图 16-1 中的。通知区域的外观和位置可能会根据设备是平板电脑还是手机而变化,有时也会根据 Android 版本而变化。
图 16-1 。安卓通知图标状态栏
图 16-1 中所示的通知区域称为状态栏 。它包含电池电量、信号强度等系统指标。当我们发送通知时,通知会以图标的形式出现在图 16-1 所示的区域。通知图标如图 16-2 所示。
图 16-2 。状态栏显示通知图标
通知图标是向用户指示需要观察某些情况的指示器。要查看完整的通知,您必须用手指按住图标,像拉窗帘一样向下拖动图 16-2 中的标题栏。这将扩大通知区域,如图图 16-3 所示。
图 16-3 。扩展通知视图
在图 16-3 的通知的放大视图中,您可以看到通知的详细信息。您可以单击通知详细信息来激发显示通知所属的完整应用的意图。您可以使用此视图来清除通知。此外,根据设备和版本,可能有打开通知的替代方式。现在让我们看看如何生成如图图 16-2 和图 16-3 所示的通知图标。
发送通知
当您创建一个通知对象时,它需要有以下元素:
- 要显示的图标
- 像“你好,世界”这样的文字
- 交付的时间
一旦构造了通知对象,就可以通过询问名为 Context 的系统服务的上下文来获得通知管理器引用。通知 _ 服务。然后,您要求通知管理器发送通知。清单 16-6 有发送通知的广播接收器的源代码,如图图 16-2 和图 16-2 所示。
清单 16-6 。发送通知的接收者
//Filename: NotificationReceiver.java
//Project: StandaloneBroadcastReceiver, Download: ProAndroid5_Ch16_TestReceivers.zip
public class NotificationReceiver extends BroadcastReceiver {
private static final String tag = "Notification Receiver";
@Override
public void onReceive(Context context, Intent intent) {
Log.d(tag, "intent=" + intent);
String message = intent.getStringExtra("message");
Log.d(tag, message);
this.sendNotification(context, message);
}
private void sendNotification(Context ctx, String message) {
//Get the notification manager
String ns = Context.NOTIFICATION_SERVICE;
NotificationManager nm =
(NotificationManager)ctx.getSystemService(ns);
//Prepare Notification Object Details
int icon = R.drawable.robot;
CharSequence tickerText = "Hello";
long when = System.currentTimeMillis();
//Get the intent to fire when the notification is selected
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("[`www.google.com`](http://www.google.com)"));
PendingIntent pi = PendingIntent.getActivity(ctx, 0, intent, 0);
//Create the notification object through the builder
Notification notification =
new Notification.Builder(ctx)
.setContentTitle("title")
.setContentText(tickerText)
.setSmallIcon(icon)
.setWhen(when)
.setContentIntent(pi)
.setContentInfo("Addtional Information:Content Info")
.build();
//Send notification
//The first argument is a unique id for this notification.
//This id allows you to cancel the notification later
//This id also allows you to update your notification
//by creating a new notification and resending it against that id
//This id is unique with in this application
nm.notify(1, notification);
}
}
展开通知时,会显示通知的内容视图。这就是你在图 16-2 中看到的。内容视图需要是一个 RemoteViews 对象。然而,我们不直接传递内容视图。根据传递给 Builder 对象的参数,Builder 对象创建一个适当的 RemoteViews 对象,并在通知上设置它。如果需要的话,构建器界面还有一个方法可以直接将内容视图设置为一个整体。
直接使用远程视图查看通知内容的步骤如下:
- 创建布局文件。
- 使用包名和布局文件 ID 创建一个 RemoteViews 对象。
- 在通知上调用 setContent() 。Builder 对象,然后调用 build()方法创建通知对象,然后将通知对象发送给通知管理器。
请记住,只有有限的一组控件可以参与远程视图,例如框架布局、线性布局、相对布局、模拟时钟、按钮、计时器、图像按钮、图像视图、进度条、文本视图。
清单 16-6 中的代码使用 Builder 对象创建一个通知来设置隐式内容视图(通过标题和文本)和触发意图(在我们的例子中,这个意图是浏览器意图)。可以创建一个新的通知,通过通知管理器重新发送,以便使用通知的唯一 ID 更新其先前的实例。通知的 ID 在清单 16-6 中设置为 1,在这个应用上下文中是惟一的。这种唯一性允许我们不断地更新通知发生了什么,并在需要时取消它。
您可能还想在创建通知时查看各种可用的标志,例如 FLAG_NO_CLEAR 和 FLAG _ understand _ EVENT,以控制这些通知的持久性。您可以使用以下 URL 来检查这些标志:
[`developer.android.com/reference/android/app/Notification.html`](http://developer.android.com/reference/android/app/Notification.html)
在广播接收器中启动活动
虽然我们建议你在需要通知用户时使用通知管理器,但 Android 确实允许你明确地产生一个活动。您可以通过使用常用的 startActivity() 方法来实现这一点,但是要将以下标志添加到用作 startActivity()参数的 intent 中:
- 意图。FLAG_ACTIVITY_NEW_TASK
- 意图。背景标志
- 意图。FLAG_ACTIVITY_SINGLE_TOP
探索长期运行的接收器和服务
到目前为止,我们已经介绍了广播接收器的快乐之路,其中广播接收器的执行不太可能超过 10 秒。如果我们想执行超过 10 秒的任务,问题空间就有点复杂。
要了解原因,让我们回顾一下关于广播接收器的一些事实:
- 与 Android 进程的其他组件一样,广播接收器运行在主线程上。因此,在广播接收器中拦截代码将会拦截主线程,并导致 ANR。广播接收器的时间限制是 10 秒,而活动的时间限制是 5 秒。这是一点缓刑,但不太多。
- 承载广播接收器的进程将随着广播接收器的执行而开始和终止。因此,在广播接收方的 onReceive() 方法返回后,该过程将不再继续。当然,这是假设进程只包含广播接收器。如果流程包含其他已经在运行的组件,比如活动或服务,那么流程的生命周期也会考虑这些组件的生命周期。
- 与服务进程不同,广播接收器进程不会重新启动。
- 如果广播接收器要启动一个单独的线程并返回到主线程,Android 将假设工作已经完成,即使有线程在运行,也会关闭进程,使这些线程突然停止。
- Android 在调用广播服务时自动获取部分唤醒锁,并在主线程中从服务返回时释放它。唤醒锁是 SDK 中可用的一种机制和 API 类,用于防止设备进入睡眠状态,或者在设备已经睡眠时将其唤醒。
给定这些谓词,我们如何执行长时间运行的代码来响应广播事件呢?
了解长期运行的广播接收器协议
答案在于解决以下问题:
- 我们显然需要一个单独的线程,以便主线程可以返回并避免 ANR 消息。
- 为了阻止 Android 杀死进程和工作线程,我们需要告诉 Android 这个进程包含一个组件,比如一个有生命周期的服务。因此,我们需要创建或启动该服务。服务本身不能直接完成超过 5 秒钟的工作,因为这发生在主线程上,所以服务需要启动一个工作线程并让主线程离开。
- 在工作线程执行期间,我们需要保持部分唤醒锁,这样设备就不会进入睡眠状态。部分唤醒锁将允许设备在不打开屏幕等情况下运行代码,从而延长电池寿命。
- 必须在接收器的主线代码中获得部分唤醒锁;否则就太晚了。例如,您不能在服务中这样做,因为在广播接收器发出的 startService() 和开始执行的服务的 onStartCommand() 之间可能太晚了。
- 因为我们正在创建一个服务,所以服务本身可能会因为内存不足而关闭和重新启动。如果发生这种情况,我们需要再次获取唤醒锁。
- 当服务的 onStartCommand() 方法启动的工作线程完成工作时,它需要告诉服务停止,这样它就可以被放在床上,而不是被 Android 起死回生。
- 也有可能发生不止一个广播事件。考虑到这一点,我们需要小心我们需要产生多少工作线程。
鉴于这些事实,延长广播接收器寿命的推荐协议如下:
- 在广播接收器的 onReceive() 方法中获取(静态)部分唤醒锁。部分唤醒锁需要是静态的,以允许广播接收器和服务之间的通信。没有其他方法将唤醒锁的引用传递给服务,因为服务是通过不带参数的默认构造函数调用的。
- 启动一个本地服务,这样进程就不会被终止。
- 在服务中,启动一个工作线程来完成这项工作。不要在服务的 onStart() 方法中工作。如果你这样做了,你基本上是再次阻碍了主线。
- 当工作线程完成时,告诉服务直接或通过处理器停止自己。
- 让服务关闭静态唤醒锁。
了解 IntentService
认识到服务不阻塞主线程的需要,Android 提供了一个名为 IntentService 的工具本地服务实现,将工作卸载到工作线程,以便在将工作调度到子线程后释放主线程。在这种方案下,当你在一个 IntentService 上调用 startService() 时, IntentService 将使用一个循环和一个处理器将该请求排队到一个子线程,以便调用 IntentService 的一个派生方法在单个工作线程上完成实际工作。
下面是针对 IntentService 的 API 文档:
IntentService 是按需处理异步请求(表示为意图)的服务的基类。客户端通过 startService(Intent)调用发送请求;该服务根据需要启动,使用工作线程依次处理每个意图,并在工作耗尽时自行停止。这种“工作队列处理器”模式通常用于从应用的主线程中卸载任务。IntentService 类的存在是为了简化这种模式,并处理其中的机制。若要使用它,请扩展 IntentService 并实现 onhandleinent(Intent)。IntentService 将接收意图,启动一个工作线程,并在适当的时候停止服务。所有请求都在单个工作线程上处理—它们可能需要多长时间(并且不会阻塞应用的主循环),但是一次只会处理一个请求。
这个想法用清单 16-7 中的一个简单例子来演示。您扩展了 IntentService ,并在 onHandleIntent() 方法中提供了您想要做的事情。
清单 16-7 。使用 IntentService
//You can see file Test30SecBCRService.java for example
//Project: StandaloneBroadcastReceiver, Download: ProAndroid5_Ch16_TestReceivers.zip
public class MyService extends IntentService {
public MyService()
{ super("some-java-package-like-name-used-for-debugging"); }
protected void onHandleIntent(Intent intent) {
//log thread signature if you want to see that it is running on a separate thread
//Ex: Utils.logThreadSignature("MyService");
//do the work in this subthread
//and return
}
}
一旦有了这样的服务,就可以在清单文件中注册这个服务,并使用客户端代码调用这个服务作为 context . start service(new Intent(context,MyService.class)) 。这将导致调用清单 16-7 中的 onhandleinentent()。您会注意到,如果您在实际代码中使用清单 16-7 中注释掉的方法 utils . logthreadsignature(),它将打印工作线程而不是主线程的 ID。您可以在项目中看到 Utils 类,并下载在清单 16-7 的注释部分列出的引用。
为广播接收器扩展 IntentService
从广播接收器的角度来看,一个 IntentService 是一件美好的事情。它让我们在不阻塞主线程的情况下执行长时间运行的代码。不仅如此,作为一个服务, IntentService 提供了一个在广播代码返回时保持运行的进程。那么我们可以使用 IntentService 来满足长期运行操作的需求吗?是也不是。
是的,因为 IntentService 做两件事:首先,它保持流程运行,因为它是一个服务。第二,它让主线程离开并避免相关的 ANR 消息。
要理解“不”的答案,你需要多理解一点唤醒锁。当通过警报管理器调用广播接收器时,设备可能没有打开。因此,警报管理器通过调用电源管理器并请求唤醒锁来部分打开设备(仅足以在没有任何 UI 的情况下运行代码)。广播接收器一返回,唤醒锁就被释放。
这使得 IntentService 调用没有唤醒锁,因此设备可能会在实际代码运行之前进入睡眠状态。然而, IntentService ,作为服务的通用扩展,它不获取唤醒锁。所以我们需要在 IntentService 之上的进一步支持。我们需要一个抽象。
马克·墨菲创建了一个名为的 IntentService 的变体,它保留了使用 IntentService 的语义,但也获取了唤醒锁,并在各种条件下正确释放它。你可以在github.com/commonsguy/cwac-wakeful看看他的实现。
探索长期运行的广播服务抽象
wakeflintentservice 是一个很好的抽象。然而,我们想更进一步,使我们的抽象与扩展 IntentService 的方法并行,如清单 16-7 所示,做 IntentService 所做的一切,但也提供了一些好处:
- 将传递给广播接收器的原始意图传递给被覆盖的方法 on handle identity。这允许我们在很大程度上隐藏广播接收器,模拟响应广播消息而启动服务的编程体验。这确实是这个抽象的目标,同时也有一些额外的东西。
- 获取和释放唤醒锁(类似于唤醒服务)。
- 处理正在重新启动的服务。
- 允许在同一进程中以统一的方式处理多个接收器和多个服务的唤醒锁。
我们将这个抽象类称为 alongningnonstickybroadcastservice。顾名思义,我们希望这个服务允许长时间运行的工作。它也将专门为广播接收器而建造。这个服务也是非粘性的(我们将在本章后面解释这个概念,但简单来说,这表明如果队列中没有消息,Android 将不会启动该服务)。为了允许一个 IntentService 的行为,它将扩展 IntentService 并覆盖 on hand lenent 方法。
结合这些想法,抽象的 alongningnonstickybroadcastservice 服务将具有类似于清单 16-8 的签名。
清单 16-8 。长期运行的服务抽象概念
public abstract class ALongRunningNonStickyBroadcastService extends IntentService {
//...other implementation details
//the following method will be called by the onHandleIntent of IntentService
//this is where the actual work happens in this derived abstract class
protected abstract void handleBroadcastIntent(Intent broadcastIntent);
//...other implementation details
}
这个 alongningnonstickybroadcastservice 的实现细节有点复杂,在我们解释了为什么要追求这种类型的服务之后,我们将很快介绍它们。我们想首先展示它的实用性和简单性。
一旦我们有了清单 16-8 中的这个抽象类,清单 16-7 中的 MyService 示例就可以重写为清单 16-9 中的示例。
清单 16-9 。长期运行的服务示例用法
public class MyService extends ALongRunningNonStickyBroadcastService {
//..other implementation details
protected void handleBroadcastIntent(Intent broadcastIntent) {
//You can use the following method to see which thread runs this code
//Utils.logThreadSignature("MyService");
//do the work here
//and return
}
//..other implementation details
}
清单 16-9 的简单之处在于,一旦客户端发出广播意图,这个代码就会被调用。尤其是事实上,你是直接接收,未经修改,同样的意图,调用广播接收器。好像广播接收器已经从解决方案中消失了。
正如你所看到的,你可以扩展这个新的长期运行的服务类(就像 IntentService 和 WakefulIntentService )并覆盖一个单独的方法,在广播接收器中做很少的事情甚至什么都不做。你的工作将在一个工作线程中完成(多亏了 IntentService ),不会阻塞主线程。
清单 16-9 是一个演示这个概念的简单例子。让我们来看一个更完整的实现,它实现了一个长时间运行的服务,可以运行 60 秒来响应一个广播事件(证明我们可以运行超过 10 秒并避免 ANR 消息)。我们将这个服务恰当地称为 Test60SecBCRService (BCR 代表广播接收器),其实现如清单 16-10 所示。
清单 16-10 。test60 secbcrservice 的源代码
//Filename: Test30SecBCRService.java
//Project: StandaloneBroadcastReceiver, Download: ProAndroid5_Ch16_TestReceivers.zip
public class Test60SecBCRService extends ALongRunningNonStickyBroadcastService {
public static String tag = "Test60SecBCRService";
//Required by IntentService to pass the classname for debug needs
public Test60SecBCRService(){
super("com.androidbook.service.Test60SecBCRService");
}
/* Perform long-running operations in this method.
* This is executed in a separate thread.
*/
@Override
protected void handleBroadcastIntent(Intent broadcastIntent) {
//Utils class is in the download project mentioned
Utils.logThreadSignature(tag);
Log.d(tag,"Sleeping for 60 secs");
//Use the thread to sleep for 60 seconds
Utils.sleepForInSecs(60);
String message =
broadcastIntent.getStringExtra("message");
Log.d(tag,"Job completed");
Log.d(tag,message);
}
}
正如您所看到的,这段代码成功地模拟了工作 60 秒,并且仍然避免了 ANR 消息。清单 16-10 中的实用方法是不言自明的,可以在本章的下载项目中找到。项目名和下载文件名在清单 16-10 中代码的注释部分。
设计长期运行的接收器
一旦我们有了清单 16-10 中的长期运行的服务,我们需要能够从广播接收器调用服务。同样,我们追求的是尽可能隐藏广播接收器的抽象。
长时间运行的广播接收器的第一个目标是将工作委托给长时间运行的服务。为此,长时间运行的接收者需要长时间运行的服务的类名来调用它。第二个目标是获得唤醒锁。第三个目标是将调用广播接收器的初衷传递给服务。我们将通过将原始意图作为一个可打包粘贴到意图附加中来实现这一点。我们将使用原意作为这个额外的名称。然后,长时间运行的服务提取 original_intent 并将其传递给长时间运行的服务的被覆盖的方法(稍后您将在长时间运行的服务的实现中看到这一点)。因此,这种设备给人的印象是,长时间运行的服务实际上是广播接收器的延伸。
让我们把这三样东西抽象出来,提供一个基类。这个长期运行的接收器抽象需要的唯一一点信息是通过一个名为 getLRSClass() 的抽象方法得到的长期运行的服务类的名称( LRSClass )。
将这些需求放在一起,实现抽象类的源代码在清单 16-11 中。
清单 16-11 。 单独运行接收方抽象
//Filename: ALongRunningReceiver.java
//Project: StandaloneBroadcastReceiver, Download: ProAndroid5_Ch16_TestReceivers.zip
public abstract class ALongRunningReceiver extends BroadcastReceiver {
private static final String tag = "ALongRunningReceiver";
@Override
public void onReceive(Context context, Intent intent) {
Log.d(tag,"Receiver started");
//LightedGreenRoom abstracts the Android WakeLock
//to keep the device partially on.
//In short this is equivalent to turning on
//or acquiring the wake lock.
LightedGreenRoom.setup(context);
startService(context,intent);
Log.d(tag,"Receiver finished");
}
private void startService(Context context, Intent intent) {
Intent serviceIntent = new Intent(context,getLRSClass());
serviceIntent.putExtra("original_intent", intent);
context.startService(serviceIntent);
}
/*
* Override this method to return the
* "class" object belonging to the
* nonsticky service class.
*/
public abstract Class getLRSClass();
}
在前面的广播接收器代码中,您可以看到对名为 LightedGreenRoom 的类的引用。这是一个静态唤醒锁的包装器。除了作为一个唤醒锁,这个类试图迎合多接收器,多服务等工作。,以便所有的 waki-ness 得到适当的协调。出于理解的目的,你可以把它当作一个静态的唤醒锁。这种抽象被称为 LightedGreenRoom,因为它旨在像各种“绿色”运动一样为设备节能。此外,它之所以被称为“点亮”,是因为它一开始就被“点亮”,因为广播接收器一启动它就打开它。最后一个使用它的服务将关闭它。
一旦接收器抽象可用,您将需要一个与清单 16-11 中的 60 秒长时间运行的服务协同工作的接收器。在清单 16-12 中提供了这样一个接收器。
清单 16-12 。一个长期运行的广播接收器示例, Test60SecBCR
//Filename: Test60SecBCR.java
//Project: StandaloneBroadcastReceiver, Download: ProAndroid5_Ch16_TestReceivers.zip
public class Test60SecBCR extends ALongRunningReceiver {
@Override
public Class getLRSClass() {
Utils.logThreadSignature("Test60SecBCR");
return Test60SecBCRService.class;
}
}
就像清单 16-10 和清单 16-11 中的服务抽象一样,清单 16-12 中的代码使用了广播接收器的抽象。接收者抽象启动由 getLRSClass() 方法返回的服务类所指示的服务。
到目前为止,我们已经展示了为什么我们需要两个重要的抽象来实现由广播接收器调用的长期运行的服务:
- ALongRunningNonStickyBroadcastService(清单 16-8 )
- ALongRunningReceiver(清单 16-11
用明亮的温室提取唤醒锁
如前所述, LightedGreenRoom 抽象的主要目的是简化与唤醒锁的交互,唤醒锁用于在后台处理期间保持设备开启。您真的不需要 LightedGreenRoom 的实现细节,而只需要它的接口和针对它的调用。请记住,它只是 Android SDK 唤醒锁的一个薄薄的包装。在其最简单的实现中,它可以像打开(获取)和关闭(释放)唤醒锁一样简单。清单 16-13 展示了唤醒锁是如何被使用的,如 SDK 中所述。
清单 16-13 。使用唤醒锁 API 的伪代码
//Get access to the power manager service
PowerManager pm =
(PowerManager)inCtx.getSystemService(Context.POWER_SERVICE);
//Get hold of a wake lock
PowerManager.WakeLock wl =
pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, tag);
//Acquire the wake lock
wl.acquire();
//do some work
//while this work is being done the device will be on partially
//release the Wakelock
wl.release();
考虑到这种交互,广播接收器应该获得锁,当长时间运行的服务结束时,它需要释放锁。如前所述,没有好的方法将唤醒锁变量从广播接收器传递给服务。服务知道这个唤醒锁的唯一方式是使用静态或应用级变量。
获取和释放唤醒锁的另一个困难是引用计数。由于广播接收器被多次调用,如果调用重叠,将会有多次调用来获取唤醒锁。类似地,将有多个调用要释放。如果获取和释放调用的数量不匹配,我们最终会得到一个唤醒锁,在最坏的情况下,它会让设备保持比需要的时间长得多的时间。此外,当不再需要该服务并且垃圾收集运行时,如果唤醒锁计数不匹配,LogCat 中将出现运行时异常。这些问题促使我们尽最大努力将唤醒锁抽象为一个亮室以确保正确使用。每个进程都有一个这样的对象来保持一个唤醒锁,并确保它被正确地打开和关闭。包含的项目有这个类的实现。如果您发现代码由于要考虑的条件太多而太复杂,您可以从一个简单的静态变量开始,在服务启动和关闭时打开和关闭它,并对其进行优化以适应您的特定条件。
广播接收器和服务相互通信的合理方法是通过静态变量。我们没有将唤醒锁设为静态,而是将整个灯光温室设为静态实例。然而,灯光温室中的其他变量都是局部的,不稳定的。
为了方便起见, LightedGreenRoom 的每个公共方法也被公开为静态方法。我们使用了以“s_”开始命名这些方法的惯例。相反,您可以选择摆脱静态方法,直接调用 LightedGreenRoom 的单个对象实例。
实现长期运行的服务
为了呈现长期运行的服务抽象,我们必须再绕一圈来解释服务的生命周期以及它如何与 onStartCommand 的实现相关联。这是最终负责启动工作线程和服务语义的方法。
当一个服务通过 startService 启动时,该服务首先被创建,其 onStartCommand 方法被调用。Android 规定将这个过程保存在内存中,这样即使在服务多个传入的客户端请求时,服务也可以完成。但是,在内存要求苛刻的情况下,Android 可能会选择回收进程,并调用服务的 onDestroy() 方法。
注意当服务没有执行其 onCreate() 、 onStart() 或 onDestroy() 方法时,或者换句话说,当服务空闲时,Android 试图为服务调用 onDestroy() 方法来回收其资源。
然而,与被关闭的活动不同,如果队列中有未决的 startService 意图,则服务被调度为在资源可用时再次重启。服务将被唤醒,下一个意图将通过 onStartCommand() 传递给它。当然,服务带回时会调用 onCreate() 。
因为如果服务没有被显式停止,它们会自动重启,所以有理由认为,与活动和其他组件不同,服务组件从根本上来说是一个粘性组件。
了解非粘性服务
如果客户端明确调用 stopService ,服务不会自动重启。根据仍然连接的客户端数量,这个 stopService 方法可以将服务转移到停止状态,此时服务的 onDestroy 方法被调用,服务生命周期结束。一旦服务被它的最后一个客户端像这样停止,该服务将不会恢复。
当所有事情都按照设计发生时,这个协议工作得很好,其中 start 和 stop 方法被依次调用和执行,没有遗漏。在 Android 2.0 之前,即使没有工作要做,设备也会看到许多服务在周围徘徊并占用资源,这意味着即使队列中没有消息,Android 也会将服务带回内存。当停止服务因为异常或者因为在 onStartCommand 和停止服务之间的流程被取消而没有被调用时,就会发生这种情况。
Android 2.0 引入了一个解决方案,这样我们可以向系统表明,如果没有未决的意图,它不应该麻烦重启服务。这是通过返回非粘性标志(服务来完成的。来自的 START _ NOT _ STICKYonstart command。
然而,不粘并不是真的不粘。即使我们将这项服务标记为非粘性的,如果有悬而未决的意图,Android 将使这项服务起死回生。此设置仅适用于没有待定意向的情况。
理解粘性服务
那么一个服务真正的粘性是什么意思呢?粘旗(服务。START_STICKY 意味着 Android 应该重启服务,即使没有未决的意图。当服务重新启动时,调用 o nCreate 和 onStartCommand 的空意图。这将给服务一个机会,如果需要的话,调用 stopSelf 如果合适的话。这意味着粘性服务需要在重启时处理无效意图。
了解重新交付意图选项
尤其是本地服务遵循一种模式,即成对调用 onStart 和 stopSelf 。一个客户端调用 onStart 。当服务完成这项工作时,它调用 stopSelf 。如果一个服务需要 30 分钟来完成一个任务,那么它在 30 分钟内不会调用 stopSelf 。同时,由于低内存条件和更高优先级的作业,服务被回收。如果我们使用非粘性标志,服务将不会被唤醒,我们也不会调用 stopSelf 。
很多时候,这样是可以的。但是,如果你想确定这两个调用是否确实发生,你可以告诉 Android 在调用 stopSelf 之前不要 unqueue 这个 start 事件。这确保了当服务被回收时,除非调用了 stopSelf ,否则总会有一个挂起的事件。这被称为重新交付模式,可以通过返回服务来回复 onStartCommand 方法。START _ rede deliver _ INTENT 标志。
为长期运行的服务编码
现在您已经有了关于 IntentService 的背景、服务启动标志和亮着灯的绿色房间,我们准备看看清单 16-14 中的长期运行的服务。
清单 16-14 。长期运行的服务抽象
//Filename: ALongRunningNonStickyBroadcastService.java
//Project: StandaloneBroadcastReceiver, Download: ProAndroid5_Ch16_TestReceivers.zip
public abstract class ALongRunningNonStickyBroadcastService
extends IntentService {
public static String tag = "ALongRunningBroadcastService";
//This is what you override to do your work
protected abstract void
handleBroadcastIntent(Intent broadcastIntent);
public ALongRunningNonStickyBroadcastService(String name){
super(name);
}
/*
* This method can be invoked under two circumstances
* 1\. When a broadcast receiver issues a "startService"
* 2\. when android restarts this service due to pending "startService" intents.
*
* In case 1, the broadcast receiver has already
* set up the "lightedgreenroom" and thereby gotten the wake lock
*
* In case 2, we need to do the same.
*/
@Override
public void onCreate() {
super.onCreate();
//Set up the green room
//The setup is capable of getting called multiple times.
LightedGreenRoom.setup(this.getApplicationContext());
//It is possible that more than one service of this type is running.
//Knowing the number will allow us to clean up the wake locks in ondestroy.
LightedGreenRoom.s_registerClient();
}
@Override
public int onStartCommand(Intent intent, int flag, int startId) {
//Call the IntentService "onstart"
super.onStart(intent, startId);
//Tell the green room there is a visitor
LightedGreenRoom.s_enter();
//mark this as nonsticky
//Means: Don't restart the service if there are no
//pending intents.
return Service.START_NOT_STICKY;
}
/*
* Note that this method call runs in a secondary thread setup by the IntentService.
*
* Override this method from IntentService.
* Retrieve the original broadcast intent.
* Call the derived class to handle the broadcast intent.
* finally tell the lighted room that you are leaving.
* if this is the last visitor then the lock
* will be released.
*/
@Override
final protected void onHandleIntent(Intent intent) {
try {
Intent broadcastIntent
= intent.getParcelableExtra("original_intent");
handleBroadcastIntent(broadcastIntent);
}
finally {
//release the wake lock if you are the last one
LightedGreenRoom.s_leave();
}
}
/* If Android reclaims this process, this method will release the lock
* irrespective of how many visitors there are.
*/
@Override
public void onDestroy() {
super.onDestroy();
//Do any cleanup, if needed, when a service no longer needs a wake lock
LightedGreenRoom.s_unRegisterClient();
}
}
这个类扩展了 IntentService ,并获得了由 IntentService 设置的工作线程的所有好处。此外,它进一步特殊化了 IntentService ,使其成为一个非粘性服务。从开发人员的角度来看,主要关注的方法是抽象的 handleBroadcastIntent()方法。清单 16-15 展示了如何在清单文件中设置接收者和相应的服务。
清单 16-15 。长期运行的接收者和服务定义
<!--
In filename: AndroidManifest.xml
Project: StandaloneBroadcastReceiver, Download: ProAndroid5_Ch16_TestReceivers.zip
-->
<manifest...>
......
<application....>
<receiver android:name=".Test60SecBCR">
<intent-filter>
<action android:name="com.androidbook.intents.testbc"/>
</intent-filter>
</receiver>
<service android:name=".Test60SecBCRService"/>
</application>
.....
<uses-permission android:name="android.permission.WAKE_LOCK"/>
</manifest>
请注意,您将需要 wake lock 权限来运行这个长期运行的接收器抽象。本章的可下载项目中提供了所有接收器和长期运行服务的完整源代码。清单 16-15 展示了广播接收器调用的长期运行服务的本质。这个抽象声明你写几行代码来创建一个类似于 Test60SecBCR ( 清单 16-12 )的接收器,然后写一个类似于代码 Test60SecBCRService ( 清单 16-10 )中的 java 方法。给定接收者和想要长时间运行的 java 方法,您可以执行该方法来响应广播事件。这种抽象确保了该方法可以运行尽可能长的时间,而不会产生 ARM。该抽象负责 a)保持流程活动,b)调用服务,c)负责唤醒锁,以及 d)将广播意图传递给服务。最后,这种抽象模拟了从广播事件中“调用一个可以无限制执行的方法”。
广播接收器中的附加主题
由于篇幅所限,我们无法在本书中涵盖广播接收器的所有方面。我们还没有涉及的一个话题是限制发送和接收广播的安全机会。您可以在接收方上使用 export 属性来决定是否可以从外部进程调用它。您还可以通过清单文件或以编程方式启用或禁用接收器。我们还没有介绍一个叫做 sendOrderBroadcast 的方法,它可以帮助我们按照包括链接在内的顺序调用广播接收器。你可以从 BroadcastReceiver 类的主要 API 文档中了解这些方面。
此外,在 Android 支持库 SDK 的版本 4 中,有一个名为 LocalBroadcastManager 的类,用于优化对严格本地的广播接收器的调用。由于是本地的,所以不需要考虑所有的安全限制。根据 SDK,当使用这个类时,还有系统级的优化。
同样在 Android 支持库 SDK 的版本 4 中,有一个名为 WakefulBroadcastReceiver 的类,它封装了一些我们为长期运行的服务需求所涵盖的相同概念。
参考
以下是本章所涵盖主题的有用参考:
developer . Android . com/reference/Android/content/broadcast receiver . html??:broadcast receiverAPI。你可以在这个链接中找到更多关于订购广播和广播接收器生命周期的信息。这是一个极好的资源。developer . Android . com/reference/Android/support/v4/content/wakefulbroadcastreceiver . html:Android API 参考。developer . Android . com/reference/Android/support/v4/content/localbroadcastmanager . html:Android API 参考。developer . Android . com/reference/Android/app/Service . html??:服务 API。在处理长时间运行的服务时,这种参考尤其有用。developer . Android . com/reference/Android/app/notification manager . html??:notification managerAPI。developer . Android . com/reference/Android/app/Notification . html??:通知 API。您将在这里看到用于处理通知的各种选项,如内容视图和声音效果。- http://developer . Android . com/reference/Android/widget/remote views . htmlT3:remote viewsAPI。 RemoteViews 用于构建通知的定制详细视图。
- :作者对长期运行服务的研究。
- :作者对广播接收器的研究。本说明还解释了如何从接收方启动活动。
- :本书可下载项目列表。对于这一章,寻找一个名为 proandroid 5 _ Ch16 _ test receivers . ZIP 的 ZIP 文件。这个 ZIP 文件有两个项目: TestBroadcastReceiver 和 standalone broadcastreceiver。后者依赖于前者,所以按这个顺序安装它们。本章中的源代码片段用它们的文件名和它们在什么项目中可用来标注。
摘要
在这一章中,我们已经讨论了广播接收器、通知管理器以及服务抽象在最大限度地利用广播接收器中的作用。我们也给出了一个实用的抽象来模拟广播接收器长期运行的广播服务。
十七、探索警报管理器
在 Android 中,intent 对象用于启动 UI 活动、后台服务或广播接收器。通常这些意图是由用户动作触发的。在 Android 中,你也可以使用闹钟来触发广播意图,请注意,只是广播意图。然后,被调用的广播接收器可以选择开始一项活动或一项服务。
在本章中,您将了解警报管理器 API。警报管理器 API 用于安排广播意图在特定时间启动。我们将这种在特定时间安排广播意图的过程称为设置警报。
我们还将向您展示如何安排定期重复的闹铃。我们将向您展示如何取消已经设置的警报。
当意图对象被存储以供以后使用时,它被称为待定意图。由于警报管理器一直在使用未决意图,你也将在本章中看到未决意图的用法和复杂性。
设置简单的警报
我们将从在特定时间设置闹钟并让它呼叫广播接收器开始这一章。一旦广播接收器被调用,你可以使用来自第十六章的信息在广播接收器中执行简单的和长时间运行的操作。
访问警报管理器很简单,如清单 17-1 所示。
清单 17-1 。访问警报管理器
//In filename: SendAlarmOnceTester.java
AlarmManager am =
(AlarmManager)
anyContextObject.getSystemService(Context.ALARM_SERVICE);
变量 anyContextObject 指的是一个上下文对象。例如,如果您从活动菜单中调用此代码,上下文变量将是活动。为了设置特定日期和时间的闹钟,您需要一个由 Java 日历对象标识的 time 实例。清单 17-2 显示了一个实用函数,它为当前时间之后的某个特定时刻提供了一个日历对象。
清单 17-2 。一些有用的日历工具
//In filename: Utils.java
public class Utils {
public static Calendar getTimeAfterInSecs(int secs) {
Calendar cal = Calendar.getInstance();
cal.add(Calendar.SECOND,secs);
return cal;
}
}
在本章的可下载项目中,您将看到更多基于日历的工具,它们可以通过多种方式到达时间实例。现在,我们需要一个接收器来设置我们计划设置的警报。清单 17-3 中显示了一个简单的接收器。
清单 17-3 。 TestReceiver 用于测试警报广播
//In filename: TestReceiver.java
public class TestReceiver extends BroadcastReceiver {
private static final String tag = "TestReceiver";
@Override
public void onReceive(Context context, Intent intent) {
Log.d (tag, "intent=" + intent);
String message = intent.getStringExtra("message");
Log.d(tag, message);
}
}
您需要使用 < receiver > 标签在清单文件中注册这个接收者,如清单 17-4 所示。接收器在第十六章中有详细介绍。
清单 17-4 。注册广播接收器
<!-- In filename: AndroidManifest.xml -->
<receiver android:name=".TestReceiver"/>
在 Android 中,警报实际上是一种广播意图,被安排在稍后的时间。这个意图应该调用的接收方组件在意图中明确指定(通过它的类名)。清单 17-5 显示了一个意图,可以用来调用我们在清单 17-3 中的广播接收器。
清单 17-5 。创建一个指向 TestReceiver 的意图
//In filename: SendAlarmOnceTester.java
Intent intent = new Intent(mContext, TestReceiver.class);
intent.putExtra("message", "Single Shot Alarm");
我们也有机会在创建意图时加载“额外内容”。因为警报管理器存储了一个意图供以后使用,我们需要从清单 17-5 的意图中创建一个待定意图。清单 17-6 显示了如何从一个标准意向创建一个待定意向。
清单 17-6 。创建待定意向
//In filename: SendAlarmOnceTester.java
PendingIntent pendingIntent =
PendingIntent.getBroadcast(
mContext, //context, or activity, or service
1, //request id, used for disambiguating this intent
intent, //intent to be delivered
0); //pending intent flags
注意,我们已经要求 pending content 类构造一个显式适用于广播的待定 Intent。创建待定意向的其他变化在清单 17-7 中列出:
清单 17-7 。用于创建待定意向的多个 API
//useful to start an activity
PendingIntent activityPendingIntent = PendingIntent.getActivity(..args..);
//useful to start a service
PendingIntent servicePendingIntent = PendingIntent.getService(..args..);
在清单 17-7 中,方法 getActivity() 和 getService() 的参数类似于清单 17-6 中 getBroadcast() 方法的参数。请注意,警报需要广播待定意图,而不是活动待定意图或服务待定意图。
我们将在本章后面更详细地讨论请求 id 参数,我们在清单 17-6 中将它设置为 1。简而言之,它用于分离两个在所有其他方面都等于的意图对象。
待定意向标志对警报管理器影响很小或没有影响。建议不使用任何标志,并使用 0 作为它们的值。这些意向标志通常有助于控制待定意向的生存期。但是,在这种情况下,生命周期由警报管理器维护。例如,要取消一个待定的意向,您要求警报管理器取消它。
一旦我们有了作为日历对象的以毫秒为单位的时间实例和指向接收者的待定意向,我们就可以通过调用警报管理器的 set() 方法来设置一个警报。这显示在清单 17-8 中。
清单 17-8 。使用警报管理器的 set() 方法
//In filename: SendAlarmOnceTester.java
Calendar cal = Utils.getTimeAfterInSecs(30);
//...other code that gets the pendingintent etc
am.set(AlarmManager.RTC_WAKEUP,
cal.getTimeInMillis(),
pendingIntent);
set() 方法的第一个参数表示闹铃的唤醒性质,以及我们将用于闹铃的参考时钟。该参数的可能值为 AlarmManager。RTC_WAKEUP ,警报管理器。RTC ,警报管理器。ELAPSED_REALTIME , AlarmManager。ELAPSED_REALTIME_WAKEUP
这些常量中的 elapsed 字指的是自设备最近启动以来的时间,以毫秒为单位。因此,它指的是设备时钟。 RTC 时间是指当你在设备上查看自己的时钟时,你在设备上看到的人类时钟/时间。这些常量中的 WAKEUP 一词指的是警报的性质,比如警报是应该唤醒设备还是只是在设备最终唤醒的第一时间传递。综上所述, RTC_WAKEUP 表示使用实时时钟,器件应该被唤醒。常量 ELAPSED_REALTIME 表示使用设备时钟,不唤醒设备;相反,要在第一时间发出警报。
当调用清单 17-8 中的方法时,警报管理器将调用清单 17-3 中的测试接收器,在该方法被调用的日历时间之后 30 秒,如果设备处于睡眠状态,警报管理器也会唤醒设备。
反复触发警报
现在让我们考虑如何设置一个重复的闹钟;见清单 17-9 。
清单 17-9 。设置重复闹钟
public void sendRepeatingAlarm() {
Calendar cal = Utils.getTimeAfterInSecs(30);
//Get an intent to invoke the receiver
Intent intent = new Intent(this.mContext, TestReceiver.class);
intent.putExtra("message", "Repeating Alarm");
int requestid = 2;
PendingIntent pi = this.getDistinctPendingIntent(intent, requestid);
// Schedule the alarm!
AlarmManager am =
(AlarmManager)
this.mContext.getSystemService(Context.ALARM_SERVICE);
am.setRepeating(AlarmManager.RTC_WAKEUP,
cal.getTimeInMillis(),
5*1000, //5 secs repeat
pi);
}
protected PendingIntent getDistinctPendingIntent(Intent intent, int requestId) {
PendingIntent pi =
PendingIntent.getBroadcast(
mContext, //context, or activity
requestId, //request id
intent, //intent to be delivered
0);
return pi;
}
清单 17-9 中代码的关键元素被突出显示。通过调用警报管理器对象上的 setRepeating() 方法来设置重复警报。此方法的主要输入是指向接收者的挂起意图。我们使用了在清单 17-5 中创建的相同意图,指向 TestReceiver 的意图。然而,当我们用清单 17-5 中的意图制作一个待定意图时,我们将唯一请求代码的值改为 2。如果我们不这样做,我们会看到一点奇怪的行为,我们现在解释。假设我们打算通过两个不同的警报调用同一个接收器:一个警报只响一次,另一个警报重复响。因为两个警报都指向同一个接收者,所以它们需要使用指向同一个接收者的意图。指向同一个接收者的两个意图之间没有任何其他区别,被认为是相同的意图。因此,当我们告诉警报管理器将 intent 1 上的警报设置为一次性警报,然后将 intent 2 上的警报设置为重复警报时,我们可能会认为它们是两个不同的警报。但是,在内部,两个警报指向相同的 intent 值,因为 intent 1 和 intent 2 的值相同。这就是为什么警报实际上与其设置目的相同(特别是通过值)。因此,如果意图相同,后一个警报会覆盖第一个警报。
同样,如果两个意图具有相同的动作、类型、数据、类别或类,则它们被认为是相同的。在计算意图的唯一性时,额外的因素不包括在内。此外,如果两个待定意图的底层意图相同并且请求 id 匹配,则它们被认为是相同的。因为我们可以使用请求 ID 来区分两个待定的意图,所以清单 17-8 中的代码通过使用请求 id 参数克服了源意图的相似性。这个请求 id 到待定内容 API 的参数将在所有其他内容匹配时将一个待定意图与另一个待定意图分开。
如果您将未决意图(通过值而不是通过其 Java 对象引用)本身视为您设置不同时间的警报,这一切都应该是有意义的。
取消警报
清单 17-10 中的代码用于取消警报。
清单 17-10 。取消重复警报
public void cancelRepeatingAlarm() {
//Get an intent that was originally
//used to invoke TestReceiver class
Intent intent = new Intent(this.mContext, TestReceiver.class);
//To cancel, extra is not necessary to be filled in
//intent.putExtra("message", "Repeating Alarm");
PendingIntent pi = this.getDistinctPendingIntent(intent, 2);
// Cancel the alarm!
AlarmManager am =
(AlarmManager)
this.mContext.getSystemService(Context.ALARM_SERVICE);
am.cancel(pi);
}
要取消一个警报,我们必须首先构造一个挂起的意图,然后将其作为参数传递给警报管理器的 cancel() 方法。但是,您必须注意确保待定内容在设置警报时以完全相同的方式构建,包括请求代码和目标接收器。
在构建取消意图时,可以忽略原始意图中的额外意图(清单 17-10 ),因为额外意图在意图的唯一性中不起作用,因此取消该意图。
了解警报的准确性
在 API 19 之前,Android 会在尽可能接近指定时间的时候触发警报。从 API 19 开始,为了电池寿命,彼此靠近的警报被捆绑。如果你需要更老的行为,有一个版本的 set() 方法叫做 setExact() 。还有一种叫做 setWindow() 的方法,它允许有效率的空间,也允许有保证的窗口。类似地,方法 setRepeating() 现在是不精确的。与 setExact() 方法不同, setRepeating() 没有确切的版本。如果你有这样的需求,你就得用 setExact() 自己重复多次。
了解警报的持续性
关于警报的另一个注意事项是,它们不会在设备重启后保存。这意味着您需要将警报设置和待定意图保存在持久性存储中,并根据设备重启广播动作和可能的时变广播动作(例如,意图)重新注册它们。动作 _ 启动 _ 完成,意图。ACTION_TIME_CHANGED ,意图。动作 _ 时区 _ 改变。
参考
以下参考资料将帮助您了解本章中讨论的主题的更多信息:
developer . Android . com/reference/Android/app/alarm manager . html:警报管理器 API。你会在这里看到像设置、设置重复和取消这样的方法的签名。developer . Android . com/reference/Android/app/pending intent . html:如何构造待定意向。不要太关注挂起的意图标志;它们对警报管理器来说并不重要。- :使用日期和时间类的快速示例和参考。
- :我们对警报管理者的研究。
- :本书可下载项目列表。对于这一章,寻找一个名为 proandroid 5 _ Ch17 _ testalarmmanager . ZIP 的 ZIP 文件。
摘要
本章探讨了用于设置和取消警报的警报管理器 API。本章向您展示了如何将警报连接到广播服务。本章还向您展示了警报是如何与意图紧密相关的。
十八、探索 2D 动画
动画允许屏幕上的对象随时间改变其颜色、位置、大小或方向。Android 中的动画功能实用、有趣且简单。它们在应用中经常使用。
Android 2.3 及之前的版本支持三种类型的动画 :逐帧动画(frame-by-frame animation),以固定的间隔一个接一个地绘制一系列帧时出现;布局动画,您可以在容器(如列表和表格)中动画显示视图的布局;和视图动画,其中任何视图都可以被动画化。在布局动画中,焦点不是任何给定的视图,而是视图集合起来形成复合布局的方式。Android 3.0 通过将其扩展到任何 Java 属性(包括 UI 元素的属性)来增强动画。我们将首先介绍 2.3 之前的特性,然后介绍 3.0 之后的特性。根据您的使用情况,这两种功能都适用。
探索逐帧动画
逐帧动画是一系列图像以快速间隔连续显示,最终效果是物体移动或变化。图 18-1 显示了一组圆,每个圆的不同位置都有一个球。有了这些图像中的一些(它们是帧),你可以使用动画来让球绕着圆圈运动。
图 18-1 。动画的示例图像帧
图 18-1 中的每个圆都是一个独立的图像。给图像一个基本名称 colored_ball ,并将其中八个图像存储在 /res/drawable 子目录中,这样您就可以使用它们的资源 id 来访问它们。每个图像的名称将具有图案 coloured-ballN,其中 N 是代表图像编号的数字。我们正在计划的动画活动将看起来像图 18-2 。
图 18-2 。逐帧动画测试装具
图 18-2 中的主要控制是动画视图,显示球被放置在一个椭圆/圆上。顶部的按钮用于开始和停止动画。顶部有一个调试便笺簿,用于记录事件。清单 18-1 显示了用于创建图 18-2 中的活动的布局。
清单 18-1 。帧动画示例的 XML 布局文件
<?xml version="1.0" encoding="utf-8"?>
<!--
filename: /res/layout/frame_animations_layout.xml
Download: ProAndroid5_ch18_TestFrameAnimation.zip
-->
<LinearLayout xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
android:orientation="vertical"
android:layout_width="fill_parent" android:layout_height="fill_parent">
<TextView android:id="@+id/textViewId1"
android:layout_width="fill_parent"
android:layout_height="wrap_content" android:text="Debug Scratch Pad"/>
<Button
android:id="@+id/startFAButtonId"
android:layout_width="fill_parent" android:layout_height="wrap_content"
android:text="Start Animation"/>
<ImageView
android:id="@+id/animationImage"
android:layout_width="fill_parent" android:layout_height="wrap_content"/>
</LinearLayout>
第一个控件是 debug-scratch 文本控件,它是一个简单的 TextView 。然后添加一个按钮来开始和停止动画。最后一个视图是 ImageView ,用于播放动画。
在 Android 中,逐帧动画是通过类 AnimationDrawable 实现的。这个类是一个可提取的。这些对象通常用作视图的背景。 AnimationDrawable 除了是一个 Drawable 之外,还可以获取其他 Drawable 资源的列表(比如图片)并以指定的间隔渲染。要使用这个 AnimationDrawable 类,首先要有一组 Drawable 资源(例如,一组图像)放在 /res/drawable 子目录中。然后,您将使用这些图像的列表构建一个定义了 AnimationDrawable 的 XML 文件(参见清单 18-2 )。这个 XML 文件也需要放在 /res/drawable 子目录中。
清单 18-2 。定义要制作动画的帧列表的 XML 文件
<!--
filename: /res/drawable/frame_animation.xml
Download: ProAndroid5_ch18_TestFrameAnimation.zip
-->
<animation-list xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
android:oneshot="false">
<item android:drawable="@drawable/colored_ball1" android:duration="50" />
<item android:drawable="@drawable/colored_ball2" android:duration="50" />
<item android:drawable="@drawable/colored_ball3" android:duration="50" />
<item android:drawable="@drawable/colored_ball4" android:duration="50" />
<item android:drawable="@drawable/colored_ball5" android:duration="50" />
<item android:drawable="@drawable/colored_ball6" android:duration="50" />
<item android:drawable="@drawable/colored_ball7" android:duration="50" />
<item android:drawable="@drawable/colored_ball8" android:duration="50" />
</animation-list>
每个框架都指向您通过资源 id 收集的一个彩球图像。动画列表标签被转换成代表图像集合的动画绘制对象。然后,您需要将这个 AnimationDrawable 设置为活动布局中的 ImageView 控件的背景资源。假设这个 XML 文件的文件名是 frame_animation.xml 并且它位于 /res/drawable 子目录中,您可以使用下面的代码将 AnimationDrawable 设置为 ImageView 的背景:
view.setBackgroundResource(R.drawable.frame_animation); //See Listing 18-3
通过这段代码,Android 意识到资源 IDr . drawable . frame _ animation 是一个 XML 资源,并相应地为其构造了一个合适的 AnimationDrawable Java 对象,然后将其设置为背景。设置好之后,你可以通过对视图对象执行获取来访问这个动画绘制对象,如下所示:
Object backgroundObject = view.getBackground();
AnimationDrawable ad = (AnimationDrawable)backgroundObject;
一旦有了 AnimationDrawable 对象,就可以使用它的 start() 和 stop() 方法来开始和停止动画。下面是这个对象的另外两个重要方法:
setOneShot(boolean);
addFrame(drawable, duration);
setOneShot(true) 方法运行动画一次,然后停止。 addFrame() 方法使用 Drawable 对象添加一个新帧,并设置其显示持续时间。 addFrame() 方法的功能类似于 清单 18-2 中的 XML 标签 android:drawable 。将这些放在一起,就可以得到图 18-1 中我们的逐帧动画活动的完整代码。
清单 18-3 。逐帧动画测试工具的完整代码
// filename: FrameAnimationActivity.java
// Download: ProAndroid5_ch18_TestFrameAnimation.zip
public class FrameAnimationActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.frame_animations_layout);
this.setupButton();
}
private void setupButton(){
Button b = (Button)this.findViewById(R.id.startFAButtonId);
b.setOnClickListener(
new Button.OnClickListener(){
public void onClick(View v) {animate();}
});
}
private void animate() {
ImageView imgView = (ImageView)findViewById(R.id.animationImage);
imgView.setVisibility(ImageView.VISIBLE);
imgView.setBackgroundResource(R.drawable.frame_animation);
AnimationDrawable frameAnimation = (AnimationDrawable)imgView.getBackground();
if (frameAnimation.isRunning()) {
frameAnimation.stop();
}
else {
frameAnimation.stop();
frameAnimation.start();
}
}
}//eof-class
清单 18-3 中的方法 animate() 在活动中定位 ImageView ,并将其背景设置为由资源 r . drawable . frame _ animation 标识的 AnimationDrawable 。这个动画资源 ID 指向清单 18-3 中早先的动画定义。该方法中的其余代码检索这个 AnimationDrawable 对象,并在该对象上调用动画方法。在同一个清单 18-3 中,设置了开始/停止按钮,如果动画正在运行,该按钮可以停止动画;如果动画没有运行,该按钮可以启动它。如果将清单 18-2 中动画定义的 oneshot 属性设置为 true ,动画会在一次后停止。
探索布局动画
LayoutAnimation 用于动画显示 Android 布局中的视图。你可以使用这种类型的动画,例如像列表视图和网格视图这样的普通布局控件。与逐帧动画不同,布局动画不是通过重复帧而是通过改变视图的变换矩阵来实现的。Android 中的每个视图都有一个将视图映射到屏幕的转换矩阵。通过更改这个矩阵,您可以实现视图的缩放、旋转和移动(平移)。这种依赖于更改属性和重绘图像的动画称为补间动画。基本上 LayoutAnimation 是布局中视图的变换矩阵的补间动画。在布局上指定的布局动画应用于该布局中的所有视图。
这些是可以应用于布局的补间动画类型:
- 缩放动画:用于沿 x 轴、y 轴或两者放大或缩小视图。您还可以指定希望动画围绕其发生的轴心点。
- 旋转动画:用于将视图围绕枢轴点旋转一定的角度。
- 平移动画:用于沿 x 轴或 y 轴移动视图。
- Alpha 动画:用于改变视图的透明度。
这些动画在 /res/anim 子目录中被定义为 XML 文件。清单 18-4 显示了一个在 XML 文件中声明的比例动画。
清单 18-4 。在位于 /res/anim/scale.xml 的 XML 文件中定义的缩放动画
<set xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
android:interpolator="@android:anim/accelerate_interpolator">
<scale
android:fromXScale="1"
android:toXScale="1"
android:fromYScale="0.1"
android:toYScale="1.0"
android:duration="500"
android:pivotX="50%"
android:pivotY="50%"
android:startOffset="100" />
</set>
动画 XML 中的参数具有“from”和“to”风格,以指示该属性的开始和结束值。动画的其他属性还包括动画持续时间和时间插值器。插值器决定动画参数的变化率,如清单 18-4 中的比例。我们将很快介绍插值器。清单 18-4 中的 XML 文件可以与一个布局相关联,以动画显示该布局的组成视图。
注意像清单 18-4 中的比例动画在 android.view.animation 包中被表示为 Java 类。这些类的 Java 文档不仅描述了 Java 方法,还描述了每种类型的动画所允许的 XML 参数。
我们可以使用图 18-3 中的列表视图来测试一些布局动画。当您运行本章的示例项目时,您会看到这个活动。proandroid 5 _ ch18 _ testlayoutanimation . zip
图 18-3 。要制作动画的列表视图
该活动的布局在清单 18-5 中。
清单 18-5 。ListView XML 布局文件
<?xml version="1.0" encoding="utf-8"?>
<!--
filename: /res/layout/list_layout.xml
project: ProAndroid5_ch18_TestLayoutAnimation.zip
-->
<LinearLayout xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<ListView
android:id="@+id/list_view_id"
android:layout_width="fill_parent"
android:layout_height="fill_parent"/>
</LinearLayout>
清单 18-5 显示了一个简单的线性布局,其中有一个列表视图。将布局从 18-5 显示为图 18-3 的活动代码在清单 18-6 中。
清单 18-6 。布局-动画活动代码
//filename: LayoutAnimationActivity.java
//project: ProAndroid5_ch18_TestLayoutAnimation.zip
public class LayoutAnimationActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.list_layout);
setupListView();
}
private void setupListView() {
String[] listItems = new String[] {
"Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6",
};
ArrayAdapter<String> listItemAdapter =
new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, listItems);
ListView lv = (ListView)this.findViewById(R.id.list_view_id);
lv.setAdapter(listItemAdapter);
}
}
现在让我们看看如何将清单 18-4 中的缩放动画应用到这个列表视图中。列表视图需要另一个 XML 文件,作为它自己和清单 18-4 中的比例动画之间的中介。这是因为清单 18-4 中定义的动画是通用的,适用于任何视图。另一方面,布局是视图的集合。因此清单 18-7 中的中介布局动画 XML 文件重用了通用动画 XML 文件,并指定了适用于视图集合的附加属性。这个中介布局动画 XML 文件如清单 18-9 所示。
清单 18-7 。布局控制器 XML 文件
<?xml version="1.0" encoding="utf-8"?>
<!--
filename: /res/anim/list_layout_controller.xml (ProAndroid5_ch18_TestLayoutAnimation.zip)
-->
<layoutAnimation xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
android:delay="100%"
android:animationOrder="reverse"
android:animation="@anim/scale" />
这个 XML 文件需要在 /res/anim 子目录中。这个 XML 文件指定列表中的动画应该反向进行,每个项目的动画应该以相对于总动画持续时间 100%的延迟开始。100%的持续时间确保一个项目的动画在下一个项目的动画开始之前完成。您可以更改该百分比以适应动画的需要。任何小于 100%的值都将导致项目的重叠动画。这个 mediator XML 文件还通过资源引用 @anim/scale 引用单个动画文件 scale.xml ( 清单 18-4 )。清单 18-8 展示了如何通过清单 18-7 的中介将清单 18-4 的动画附加到清单 18-5 的活动布局上。
清单 18-8 。 list_layout.xml 文件的更新代码
<?xml version="1.0" encoding="utf-8"?>
<!--
filename: /res/layout/list_layout.xml(ProAndroid5_ch18_TestLayoutAnimation.zip)
-->
<LinearLayout xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
android:orientation="vertical"
android:layout_width="fill_parent" android:layout_height="fill_parent">
<ListView android:id="@+id/list_view_id"
android:persistentDrawingCache="animation|scrolling"
android:layout_width="fill_parent" android:layout_height="fill_parent"
android:layoutAnimation="@anim/list_layout_controller" />
</LinearLayout>
在清单 18-8 中,android:layoutAnimation 是指向清单 18-7 的中介 XML 文件的标签,该文件又指向清单 18-5 的 scale.xml 。在清单 18-8 中,Android SDK 文档建议在列表视图上设置 persistentDrawingCache 标签,以优化动画和滚动。如果您要运行应用 pro Android 5 _ ch18 _ testlayoutanimation . zip,您将会看到当加载活动时,缩放动画在单个列表项上生效。我们已经将动画持续时间设置为 500 ms,以便在绘制每个列表项时可以观察到比例变化。
使用这个示例程序,您可以尝试不同的动画类型。你可以用清单 18-9 中的代码尝试 alpha 动画。
清单 18-9 。测试 alpha 动画的 alpha.xml 文件
<?xml version="1.0" encoding="utf-8"?>
<!-- file: /res/anim/alpha.xml(ProAndroid5_ch18_TestLayoutAnimation.zip) -->
<alpha xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
android:interpolator="@android:anim/accelerate_interpolator"
android:fromAlpha="0.0" android:toAlpha="1.0" android:duration="1000" />
Alpha 动画控制(颜色的)褪色。在清单 18-9 中,阿尔法动画的颜色在 1 秒钟内从不可见变为全亮度。如果你打算使用相同的中介文件,不要忘记改变中介 XML 文件(见清单 18-7 )指向新的动画文件。
清单 18-10 显示了一个结合了位置变化和颜色渐变的动画。
清单 18-10 。通过动画集组合平移和 Alpha 动画
<?xml version="1.0" encoding="utf-8"?>
<!-- file:/res/anim/alpha_translate.xml(ProAndroid5_ch18_TestLayoutAnimation.zip)-->
<set xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
android:interpolator="@android:anim/accelerate_interpolator">
<translate android:fromYDelta="-100%" android:toYDelta="0"android:duration="500"/>
<alpha android:fromAlpha="0.0" android:toAlpha="1.0" android:duration="500"/>
</set>
注意清单 18-10 的动画集中有两个动画。翻译动画将在当前分配的显示空间中从上到下移动文本。当文本项下降到它的槽中时,Alpha 动画将把颜色渐变从不可见变为可见。要查看这个动画,请参考文件名@ anim/alpha _ translate . XML 更改 layout animationmediator XML 文件。清单 18-11 显示了旋转动画的定义。
清单 18-11 。旋转动画 XML 文件
<!-- file: /res/anim/rotate.xml(ProAndroid5_ch18_TestLayoutAnimation.zip) -->
<rotate xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
android:interpolator="@android:anim/accelerate_interpolator"
android:fromDegrees="0.0" android:toDegrees="360"
android:pivotX="50%" android:pivotY="50%"
android:duration="500" />
清单 18-11 将列表中的每个文本项围绕文本项的中点旋转一整圈。让我们来谈谈你在动画 XML 文件中看到的插值器。
理解插值器
插值器 告诉属性如何随时间从其起始值变化到结束值。变化是线性的还是指数的?变化会很快开始,到最后会慢下来吗?
清单 18-9 中的阿尔法动画将插补器标识为加速 _ 插补器。有一个相应的 Java 对象定义了这个插值器的行为。由于我们已经在清单 18-9 中将这个内插器指定为一个资源引用,所以必须有一个对应于@ anim/accelerate _ interpolator 的文件来描述这个 Java 对象是什么以及它可能需要什么附加参数。清单 8-12 显示了资源引用@ Android:anim/accelerate _ interpolator 指向的资源 XML 文件定义:
清单 18-12 。作为 XML 资源的插值器定义
<accelerateInterpolator
xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
factor="1" />
你可以在根 Android SDK 包的子目录/RES/anim/accelerate _ interpolator . XML 中看到这个 XML 文件。(注意:根据版本的不同,该文件可能会有所不同。)XML 标签 accelerateInterpolator 对应 Java 类 Android . view . animation . accelerate interpolator。您可以查阅相应的 Java 文档来了解哪些 XML 标签是可用的。该插值器的目标是在给定基于双曲线的时间间隔的情况下提供倍增因子。清单 18-13 中的源代码片段说明了这一点。(注意:根据 Android 版本的不同,此代码可能会有所不同。)
清单 18-13 。核心 Android SDK 中 AccelerateInterpolator 的示例代码
public float getInterpolation(float input) {
if (mFactor == 1.0f) {
return (float)(input * input);
}
else {
return (float)Math.pow(input, 2 * mFactor);
}
}
每个插值器实现 getInterpolation 方法的方式不同。在加速插值器的情况下,如果插值器在资源文件中设置为因子 1.0 ,它将在每个间隔返回输入的平方。否则,它将返回输入的乘方,该乘方将按因子数量进一步缩放。如果因子是 1.5 ,你会看到一个三次函数,而不是平方函数。
支持的插值器包括加速减速插值器、加速插值器、周期插值器、减速插值器、线性插值器、预测插值器、预测过冲插值器、反弹插值器和过冲插值器。
要了解这些插值器有多灵活,快速查看一下清单 18-14 中的反弹插值器,它在动画周期结束时反弹对象(即来回移动它):
清单 18-14 。核心 Android SDK 中的 BounceInterpolator 实现
public class BounceInterpolator implements Interpolator {
private static float bounce(float t) {
return t * t * 8.0f;
}
public float getInterpolation(float t) {
t *= 1.1226f;
if (t < 0.3535f) return bounce(t);
else if (t < 0.7408f) return bounce(t - 0.54719f) + 0.7f;
else if (t < 0.9644f) return bounce(t - 0.8526f) + 0.9f;
else return bounce(t - 1.0435f) + 0.95f;
}
}
您可以在以下 URL 找到这些不同插值器的行为描述:
[`developer.android.com/reference/android/view/animation/package-summary.html`](http://developer.android.com/reference/android/view/animation/package-summary.html)
这些类的 Java 文档还指出了可用于控制它们的 XML 标记。
探索视图动画
通过视图动画,您可以通过操纵视图的变换矩阵来制作视图动画。变换矩阵就像一个透镜,将视图投射到显示器上。变换矩阵会影响投影视图的比例、大小、位置和颜色。
恒等式变换矩阵保留原始视图。从单位矩阵开始,应用一系列数学变换,包括大小、位置和方向。然后,将最终矩阵设置为要转换的视图的转换矩阵。
Android 通过允许向视图注册动画对象来公开视图的转换矩阵。动画对象将被传递给变换矩阵。
考虑将图 18-4 作为视图动画的演示。“开始动画”按钮使列表视图从屏幕中间的小区域开始,逐渐充满整个空间。清单 18-15 显示了用于该活动的 XML 布局文件。
图 18-4 。查看动画活动
清单 18-15 。视图动画活动的 XML 布局文件
<?xml version="1.0" encoding="utf-8"?>
<!-- filen: at /res/layout/list_layout.xml(ProAndroid5_ch18_TestViewAnimation.zip) -->
<LinearLayout xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<Button
android:id="@+id/btn_animate"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Start Animation"/>
<ListView
android:id="@+id/list_view_id"
android:persistentDrawingCache="animation|scrolling"
android:layout_width="fill_parent"
android:layout_height="fill_parent"/>
</LinearLayout>
清单 18-16 显示了加载该布局的活动代码。
清单 18-16 。动画前视图动画活动的代码
//filename: ViewAnimationActivity.java(ProAndroid5_ch18_TestViewAnimation.zip)
public class ViewAnimationActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.list_layout);
setupListView();
this.setupButton();
}
private void setupListView() {
String[] listItems = new String[] {
"Item 1", "Item 2", "Item 3","Item 4", "Item 5", "Item 6",
};
ArrayAdapter<String> listItemAdapter =
new ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,listItems);
ListView lv = (ListView)this.findViewById(R.id.list_view_id);
lv.setAdapter(listItemAdapter);
}
private void setupButton() {
Button b = (Button)this.findViewById(R.id.btn_animate);
b.setOnClickListener(
new Button.OnClickListener(){
public void onClick(View v) {
//animateListView();
}
});
}
}
有了这段代码,你会看到用户界面如图 18-4 所示。为了给图 18-4 所示的列表视图添加动画,我们需要一个从 Android . view . animation . animation 派生的类。清单 18-17 显示了这个类。
清单 18-17 。用于视图动画类的代码
//filename: ViewAnimation.java project: ProAndroid5_ch18_TestViewAnimation.zip
public class ViewAnimation extends Animation {
@Override
public void initialize(int width, int height, int parentWidth, int parentHeight){
super.initialize(width, height, parentWidth, parentHeight);
setDuration(2500); setFillAfter(true);
setInterpolator(new LinearInterpolator());
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
final Matrix matrix = t.getMatrix();
matrix.setScale(interpolatedTime, interpolatedTime);
}
}
在清单 18-7 中 初始化方法是一个带有视图维度的回调方法。动画参数可以在这里初始化。这里动画持续时间被设置为 2.5 秒。通过将 FillAfter 设置为 true ,我们已经将动画效果设置为在动画完成后保持不变。我们设置了一个线性插值器。所有这些属性都来自基本的 Android . view . animation . animation 类。
动画的主要部分发生在应用转换方法中。Android SDK 反复调用这个方法来模拟动画。Android 每次调用该方法,interpoled time 都有不同的值。该值从 0 到 1 变化,取决于动画在初始化期间设置的 2.5 秒持续时间内的位置。当插补时间为 1 时,动画结束。我们在这个方法中的目标是通过名为 t 的转换对象来改变可用的转换矩阵。首先得到矩阵,并改变它的一些东西。当视图被绘制时,新的矩阵将生效。矩阵对象上可用的方法记录在 SDK 中,网址为
[`developer.android.com/reference/android/graphics/Matrix.html`](http://developer.android.com/reference/android/graphics/Matrix.html)
在清单 18-17 中,改变矩阵的代码是
matrix.setScale(interpolatedTime, interpolatedTime);
setScale 方法取两个参数:x 方向的缩放因子和 y 方向的缩放因子。因为插值时间在 0 和 1 之间,您可以直接使用该值作为比例因子。在动画开始时,x 和 y 方向的比例因子都是 0 。动画进行到一半时,该值在 x 和 y 方向上都将为 0.5 。在动画结束时,视图将处于其最大尺寸,因为在 x 和 y 方向的缩放因子都是 1 。这个动画的最终结果是列表视图开始很小,然后变大。清单 18-18 显示了您需要在清单 18-15 中添加活动类并从按钮点击中调用它的函数。
清单 18-18 。视图动画活动的代码,包括动画
private void animateListView() {
ListView lv = (ListView)this.findViewById(R.id.list_view_id);
lv.startAnimation(new ViewAnimation());
}
注意在关于视图动画的这一节中,我们将建议清单 18-18 中的视图动画类的替代实现。在提供的项目中,该类有多种版本,如 ViewAnimation 、 ViewAnimation1 、 ViewAnimation2 和 ViewAnimation3 。后续讨论中的代码片段将在注释中指出这些类中的哪些类持有该代码。示例项目中只有一个用于动画的菜单项。为了测试每一个变化,你必须用相应的版本替换清单 18-18 中的 ViewAnimation() 类,并重新运行程序来查看改变后的动画。
当你用清单 18-17 中的 ViewAnimation 类运行代码时,你会注意到一些奇怪的事情。列表视图不是从屏幕中间均匀变大,而是从左上角变大。这是因为矩阵运算的原点在左上角。为了获得想要的效果,你首先必须移动整个视图,使视图的中心与动画中心(左上角)相匹配。然后,应用矩阵,并将视图移回先前的中心。清单 18-16 中的重写代码如清单 18-19 所示。
清单 18-19 。使用预翻译和后翻译 查看动画
//filename: ViewAnimation1.java project: ProAndroid5_ch18_TestViewAnimation.zip
public class ViewAnimation extends Animation {
float centerX, centerY;
public ViewAnimation(){}
@Override
public void initialize(int width, int height, int parentWidth, int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
centerX = width/2.0f; centerY = height/2.0f;
setDuration(2500); setFillAfter(true);
setInterpolator(new LinearInterpolator());
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
final Matrix matrix = t.getMatrix();
matrix.setScale(interpolatedTime, interpolatedTime);
matrix.preTranslate(-centerX, -centerY); matrix.postTranslate(centerX, centerY);
}
}
预转换和后转换方法在缩放操作之前和之后设置矩阵。这相当于一前一后进行三次矩阵变换。考虑一下清单 18-20 中的代码
清单 18-20 。转换矩阵前后转换的标准模式
matrix.setScale(interpolatedTime, interpolatedTime);
matrix.preTranslate(-centerX, -centerY);
matrix.postTranslate(centerX, centerY);
清单 18-20 中的代码相当于
move to a different center
scale it
move to the original center
你会看到这种前后后的模式经常被应用。你也可以在矩阵类上使用其他方法来实现这个结果,但是这种技术很普通,因为它很简洁。
矩阵类不仅允许你缩放一个视图,还可以通过平移方法移动它,或者通过旋转方法改变它的方向。您可以尝试这些方法,并查看生成的动画。前面“布局动画”一节中介绍的动画都是使用这个 Matrix 类上的方法在内部实现的。
在 2D 使用照相机提供深度感知
Android 中的图形包通过 Camera 类提供了另一个与变换矩阵相关的特性。这个课程提供了对 2D 视角的深度感知。你可以拿我们的 ListView 为例,将它从屏幕上沿 z 轴向后移动 10 个像素,并绕 y 轴旋转 30 度。清单 18-21 是一个使用摄像机操作变换矩阵的例子。
清单 18-21 。使用相机对象
//filename: ViewAnimation2.java project: ProAndroid5_ch18_TestViewAnimation.zip
public class ViewAnimation extends Animation {
float centerX, centerY;
Camera camera = new Camera();
public ViewAnimation(float cx, float cy){
centerX = cx; centerY = cy;
}
@Override
public void initialize(int width, int height, int parentWidth, int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
setDuration(2500); setFillAfter(true);
setInterpolator(new LinearInterpolator());
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
final Matrix matrix = t.getMatrix();
camera.save();
camera.translate(0.0f, 0.0f, (1300 - 1300.0f * interpolatedTime));
camera.rotateY(360 * interpolatedTime);
camera.getMatrix(matrix);
matrix.preTranslate(-centerX, -centerY);
matrix.postTranslate(centerX, centerY);
camera.restore();
}
}
这段代码通过首先将视图 1300 个像素放回 z 轴,然后将它放回 z 坐标为 0 的平面,来为列表视图制作动画。执行此操作时,代码还会围绕 y 轴将视图从 0 旋转到 360 度。相机。清单 18-21 中的 translate(x,y,z) 方法告诉 camera 对象平移视图,这样当 interpolatedTime 为 0 (动画开始时),z 值将为 1300 。随着动画的进行,z 值会越来越小,直到最后,当插补时间变为 1 并且 z 值变为 0 时。
方法 camera . rotatey(360 * interpolated time)利用了相机绕轴的 3D 旋转。在动画开始时,该值将为 0 。动画最后会是 360 。
方法 camera.getMatrix(matrix )获取目前为止在相机上执行的操作,并将这些操作应用于传入的矩阵。一旦代码这样做了,矩阵就有了它所需要的翻译,以获得拥有一个摄像机的最终效果。现在不再需要相机对象,因为矩阵中嵌入了所有操作。然后,在矩阵上做前和后移动中心并将其带回来。最后,您将相机设置为之前保存的原始状态。使用清单 18-21 中的代码,你会看到列表视图以旋转的方式从视图中心到达屏幕前方。由于这个版本的 ViewAnimation 需要额外的构造参数,清单 18-22 显示了如何调用这个版本的 AnimationView :
清单 18-22 。使用预翻译和后翻译 查看动画
//filename: ViewAnimationActivity.java
//project: ProAndroid5_ch18_TestViewAnimation.zip
ListView lv = (ListView)this.findViewById(R.id.list_view_id);
float cx = (float)(lv.getWidth()/2.0);
float cy = (float)(lv.getHeight()/2.0);
lv.startAnimation(new ViewAnimation(cx, cy));
作为我们关于视图动画讨论的一部分,我们向您展示了如何通过扩展一个 Animation 类并将其应用于一个视图来制作任何视图的动画。除了让你操作矩阵(通过相机类直接和间接操作)之外,动画类还让你检测动画中的不同阶段。我们将在接下来讨论这个问题。
探索 AnimationListener 类
Android SDK 有一个监听器接口, AnimationListener ,用于监控动画事件。清单 18-23 通过实现 AnimationListener 接口演示了这些动画事件。
清单 18-23 。动画监听器接口的实现
//filename: ViewAnimationListener.java
//project: ProAndroid5_ch18_TestViewAnimation.zip
public class ViewAnimationListener implements Animation.AnimationListener {
public ViewAnimationListener(){}
public void onAnimationStart(Animation animation) {
Log.d("Animation Example", "onAnimationStart");
}
public void onAnimationEnd(Animation animation) {
Log.d("Animation Example", "onAnimationEnd");
}
public void onAnimationRepeat(Animation animation) {
Log.d("Animation Example", "onAnimationRepeat");
}
}
在清单 18-23 中的 viewoanimationlistener 类只是记录消息。清单 18-24 中的代码展示了如何将一个动画监听器附加到一个动画对象。
清单 18-24 。将 AnimationListener 附加到动画对象
private void animateListView(){
ListView lv = (ListView)this.findViewById(R.id.list_view_id);
//Init width,height and assuming ViewAnimation from Listing 18-21
ViewAnimation animation = new ViewAnimation(width,height);
animation.setAnimationListener(new ViewAnimationListener());
lv.startAnimation(animation);
}
关于变换矩阵的注记
正如你在本章中看到的,矩阵是转换视图和动画的关键。让我们探索一下矩阵类的一些关键方法。
- Matrix.reset() :将矩阵重置为单位矩阵,这不会导致应用时视图发生变化
- Matrix.setScale(...一个参数名..):改变尺寸
- Matrix.setTranslate(...一个参数名..):改变位置模拟移动
- Matrix.setRotate(...一个参数名..):改变方向
- Matrix.setSkew(...一个参数名..):扭曲视图
最后四个方法有输入参数。
您可以将矩阵相乘,以复合单个变换的效果。在清单 18-25 中,考虑三个矩阵, m1 、 m2 和 m3 ,它们是单位矩阵:
清单 18-25 。使用预翻译和后翻译 查看动画
m1.setScale(..scale args..);
m2.setTranslate(..translate args..)
m3.setConcat(m1,m2)
用 m1 变换一个视图,然后用 m2 变换结果视图,相当于用 m3 变换同一个视图。注意 m3.setConcat(m1,m2) 不同于 m3.setConcat(m2,m1)。setConcat(matrix1,matrix2) 按照给定的顺序将两个矩阵相乘。
你已经看到了预翻译和后翻译方法用来影响矩阵转换的模式。事实上, pre 和 post 方法并不是 translate 所独有的,对于每一个 set 变换方法,你都有 pre 和 post 的版本。最终,一个预转换如 m1.preTranslate(m2 )相当于
m1.setConcat(m2,m1)
以类似的方式,方法 m1.postTranslate(m2) 等价于
m1.setConcat(m1,m2)
考虑清单 18-26 中的代码
清单 18-26 。翻译前后模式
matrix.setScale(interpolatedTime, interpolatedTime);
matrix.preTranslate(-centerX, -centerY);
matrix.postTranslate(centerX, centerY);
这个清单 18-26 中的代码等同于清单 18-27 中的代码
清单 18-27 。翻译前后模式的等效性
Matrix matrixPreTranslate = new Matrix();
matrixPreTranslate.setTranslate(-centerX, -centerY);
Matrix matrixPostTranslate = new Matrix();
matrixPostTranslate.setTranslate(centerX, centerY);
matrix.setConcat(matrixPreTranslate,matrix);
matrix.setConcat(matrix,matrixPostTranslate);
探索属性动画:新的动画 API
Android 的 3.0 和 4.0 对动画 API 进行了大修。这种新的动画方法被称为属性动画。属性 animation API 范围很广,差异很大,足以将以前的 animation API(3 . x 之前的版本)称为传统 API,即使以前的方法仍然有效且没有被弃用。旧的动画 API 在 android.view.animation 包里。新的动画 API 在 android.animation 包中。新属性动画 API 中的关键概念是:
- 鼓舞者
- 价值动画师
- 对象动画师
- 动画师集合
- 动画制作者
- 动画听众
- 财产价值持有者
- 类型评估者
- 查看属性动画
- 布局转换
- XML 文件中定义的动画
我们将在这一章的其余部分讨论这些概念。
了解属性动画
属性动画方法随时间改变属性的值。该属性可以是任何东西,例如独立的整数、浮点数或对象(如视图)的特定属性。例如,通过使用一个名为 ValueAnimator 的动画类,你可以在 5 秒内将一个 int 值从 10 改变到 200(参见清单 18-28 )。
清单 18-28 。一个简单的值动画师
//file: TestBasicValueEvaluator.java(ProAndroid5_ch18_TestPropertyAnimation.zip)
//Define an animator to change an int value from 10 to 200
ValueAnimator anim = ValueAnimator.ofInt(10, 200);
//set the duration for the animation
anim.setDuration(5000); //5 seconds, default 300 ms
//Provide a callback to monitor the changing value
anim.addUpdateListener(
new ValueAnimator.AnimatorUpdateListener() {
public void onAnimationUpdate(ValueAnimator animation) {
Integer value = (Integer) animation.getAnimatedValue();
// this code gets called many many times for 5 seconds.
// The value will range from 10 to 200
}
}
);
anim.start();
这个想法很容易理解。一个 ValueAnimator 是一个每 10 毫秒做一件事的机制(这是默认的帧率)。虽然这是默认的帧速率,但是根据系统负载的不同,你可能不会被调用那么多次。对于给定的例子,我们可以预期在 5 秒内被调用 500 次。在模拟器上,我们的测试显示它可能只有 10 倍。然而,最后一次呼叫将接近 5 秒的持续时间。
在为每一帧(每 10 毫秒)调用的相应回调中,您可以选择更新视图或任何其他方面来影响动画。除了 onAnimationUpdate 之外,通用 Animator 上还有其他有用的回调函数。来自 Android SDK 的 AnimatorListener 接口(清单 18-28 ),可以通过其基类 Animator 附加到 ValueAnimator 。所以在一个 ValueAnimator 上,你可以做 addListener(Animator。。参见清单 18-29 。
清单 18-29 。 AnimatorListener 回调接口
public static interface Animator.AnimatorListener {
abstract void onAnimationStart(Animator animation);
abstract void onAnimationRepeat(Animator animation);
abstract void onAnimationCancel(Animator animation);
abstract void onAnimationEnd(Animator animation);
}
您可以使用清单 18-29 中的这些回调函数在动画期间或之后进一步作用于感兴趣的对象。
属性动画依赖于启动动画的线程上的 android.os.Looper 的可用性。这通常是 UI 线程的情况。当动画线程是主线程时,回调也发生在 UI 线程上。
当你使用 ValueAnimators 和它们的监听器时,请记住这些对象的生命周期。即使你让一个价值动画师的引用离开你的本地范围,价值动画师将继续存在,直到它完成动画。如果你要添加一个监听器,那么监听器持有的所有引用在 ValueAnimator 的生命周期内也是有效的。
为属性动画设计一个测试平台
从价值动画器的基本思想开始,Android 提供了许多派生的方法来制作任意对象的动画,尤其是视图。为了演示这些机制,我们将采用线性布局的简单文本视图,并对其 alpha 属性(模拟透明度动画)以及 x 和 y 位置(模拟移动)进行动画处理。我们将使用图 18-5 作为锚来解释属性动画概念。
图 18-5 。展示房产动画的活动
图 18-5 中的每个按钮使用一个独立的机制来激活图底部的文本视图。我们将演示的机制如下:
- 按钮 1:使用对象动画,在一个视图中淡出和淡入交替点击一个按钮。
- 按钮 2:使用动画师设置 ,依次运行淡出动画和淡入动画。
- 按钮 3:使用一个 AnimatiorSetBuilder 对象将多个动画以“之前”、“之后”或“与”的关系捆绑在一起。使用此方法运行与按钮 2 相同的动画。
- 按钮 4:为按钮 2 的序列动画定义一个 XML 文件,并将其附加到相同动画效果的文本视图中。
- 按钮 5:使用一个 PropertyValuesHolder 对象,在同一个动画中动画显示文本视图的多个属性。我们将更改 x 和 y 值,将文本视图从右下方移动到左上方。
- 按钮 6:使用 view property animator 将文本视图从右下角移动到左上角(与按钮 5 动画相同)。
- 按钮 7:在自定义点对象上使用 TypeEvaluator 将文本视图从右下角移动到左上角(与按钮 5 的动画相同)。
- 按钮 8:使用关键帧来影响文本视图上的移动和 alpha 变化(与按钮 5 的动画相同,但是是交错的)。
构建图 18-5 中的活动非常简单。您可以在下载项目文件 pro Android 5 _ ch18 _ testpropertyaanimation . zip 中看到该活动的布局和活动代码。让我们从第一个按钮开始。
带有对象动画器的动画视图
图 18-5 (Fadeout: Animator)中的第一个按钮调用清单 18-30 中的 toggleAnimation(View) 方法。
清单 18-30 。使用对象动画制作工具的基本视图动画
//file:TestPropertyAnimationActivity.java(ProAndroid5_ch18_TestPropertyAnimation.zip)
public void toggleAnimation(View btnView) {
Button tButton = (Button)btnView; //The button we have pressed
//m_tv: is the pointer to the text view
//Animate the alpha from current value to 0 this will make it invisible
if (m_tv.getAlpha() != 0) {
ObjectAnimator fadeOut = ObjectAnimator.ofFloat(m_tv, "alpha", 0f);
fadeOut.setDuration(5000);
fadeOut.start();
tButton.setText("Fade In");
}
//Animate the alpha from current value to 1 this will make it visible
else {
ObjectAnimator fadeIn = ObjectAnimator.ofFloat(m_tv, "alpha", 1f);
fadeIn.setDuration(5000);
fadeIn.start();
tButton.setText("Fade out");
}
}
清单 18-30 中的代码首先检查文本视图的 alpha 值。如果这个值大于 0 ,那么代码假设文本视图是可见的,并运行一个淡出动画。淡出动画结束时,文本视图将不可见。如果文本视图的 alpha 值为 0 ,那么代码假定文本视图不可见,并运行一个淡入动画使文本视图再次可见。
清单 18-30 中的 ObjectAnimator 代码非常简单。使用静态方法 ofFloat() 在文本视图(m_tv)上获得一个 ObjectAnimator 。这个方法的第一个参数是一个对象(m_tv)。第二个参数是您希望 ObjectAnimator 修改或制作动画的对象的属性名。在文本视图 m_tv 的情况下,该属性名为 alpha 。目标对象需要有一个公共方法来匹配这个名称。对于名为 alpha 的属性,对应的视图对象需要有下面的 set 方法:
view.setAlpha(float f);
第三个参数是动画结束时属性的值。如果指定第四个参数,则第三个参数是起始值,第四个参数是目标值。您可以传递更多的参数,只要它们都是 float s。动画将使用这些值作为动画过程中的中间值。
如果您只指定了“到”值,那么“从”值将通过使用
view.getAlpha();
当你播放这个动画的时候,文字视图会先逐渐消失。清单 18-30 中的代码将按钮重命名为“淡入”现在,如果你再次点击按钮,现在称为“淡入”,运行清单 18-30 中的第二个动画,文本视图将在 5 秒内逐渐出现。
用 AnimatorSet 实现顺序动画
图 18-5 中的按钮 2 一个接一个地运行两个动画:一个淡出,接着是一个淡入。我们可以使用动画监听器回调来等待第一个动画结束,然后开始第二个动画。有一种自动化的方式通过类 AnimatorSet 来运行动画,以获得相同的效果。在清单 18-31 中,按钮 2 演示了这一点。
清单 18-31 。通过动画师设置 的连续动画
//file:TestPropertyAnimationActivity.java(ProAndroid5_ch18_TestPropertyAnimation.zip)
public void sequentialAnimation(View bView) {
ObjectAnimator fadeOut = ObjectAnimator.ofFloat(m_tv, "alpha", 0f);
ObjectAnimator fadeIn = ObjectAnimator.ofFloat(m_tv, "alpha", 1f);
AnimatorSet as = new AnimatorSet();
as.playSequentially(fadeOut,fadeIn);
as.setDuration(5000); //5 secs
as.start();
}
在清单 18-31 中,我们创建了两个动画制作人:一个淡出动画制作人和一个淡入动画制作人。然后,我们创建了一个动画师集,并告诉它按顺序播放这两个动画。
您还可以通过调用方法 playTogether() 来选择使用 animator 集合一起播放动画。这两种方法, playSequentially() 和 playTogether() ,都可以接受数量可变的动画师对象。
当你播放这个动画时,文本视图会逐渐消失,然后重新出现,很像你之前看到的动画。
用 AnimatorSet 设置动画关系。建设者
AnimatorSet 还提供了一种更加精细的方式,通过一个叫做 AnimatorSet 的工具类来链接动画。建造者。清单 18-32 展示了这一点。
清单 18-32 。使用 AnimatorSetBuilder
//filename: TestPropertyAnimationActivity.java (ProAndroid5_ch18_TestPropertyAnimation.zip)
public void testAnimationBuilder(View v) {
ObjectAnimator fadeOut = ObjectAnimator.ofFloat(m_tv, "alpha", 0f);
ObjectAnimator fadeIn = ObjectAnimator.ofFloat(m_tv, "alpha", 1f);
AnimatorSet as = new AnimatorSet();
//play() returns the nested class: AnimatorSet.Builder
as.play(fadeOut).before(fadeIn);
as.setDuration(5000); //5 secs
as.start();
}
一个动画师集上的 play 方法返回一个名为动画师集的类。建造者。这纯粹是一个工具类。这个类上的方法有 after(动画师)、before(动画师)、和 with(动画师)。这个类由你通过 play 方法提供的第一个动画师初始化。对这个对象的每一个其他调用都与这个原始动画师有关。考虑清单 18-33 中的:
清单 18-33 。使用动画集。建设者
AnimatorSet.Builder builder = someSet.play(main_animator).before(animator1);
用这个代码动画师 1 将在主 _ 动画师之后播放。当我们说 builder.after(animator2) 时,animator2 的动画会在 main_animator 之前播放。方法与(animator) 一起播放动画。
使用 AnimationBuilder 的关键点在于,通过 before() 、 after() 和 with() 建立的关系不是链式的,而只是绑定到从 play ()方法获得的原始 animator。此外,动画 start() 方法不在构建器对象上,而是在原始 animator 集上。当您通过 Button3 播放这个动画时,文本视图将逐渐消失,然后重新出现,就像上一个动画一样。
使用 XML 加载动画
Android SDK 允许在 XML 资源文件中描述动画制作人,这是唯一可以期待的。Android SDK 有一个新的资源类型叫做 R.animator 来区分 animator 资源文件。这些 XML 文件存储在 /res/animator 子目录中。清单 18-34 是一个在 XML 文件中定义的动画集的例子。
清单 18-34 。一个 Animator XML 资源文件
<?xml version="1.0" encoding="utf-8" ?>
<!-- file: /res/animator/fadein.xml (ProAndroid5_ch18_TestPropertyAnimation.zip) -->
<set xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
android:ordering="sequentially">
<objectAnimator
android:interpolator="@android:interpolator/accelerate_cubic"
android:valueFrom="1" android:valueTo="0"
android:valueType="floatType" android:propertyName="alpha"
android:duration="5000" />
<objectAnimator
android:interpolator="@android:interpolator/accelerate_cubic"
android:valueFrom="0" android:valueTo="1"
android:valueType="floatType" android:propertyName="alpha"
android:duration="5000" />
</set>
您自然会想知道有什么 XML 节点可以用来定义这些动画。从 4.0 开始,允许的 XML 标签如下:
- 动画师:绑定到值动画师
- objectAnimator :绑定到 ObjectAnimator
- 设置:绑定到动画师设置
您可以在下面的 Android SDK URL 中看到关于这些标签的基本讨论:
[`developer.android.com/guide/topics/graphics/prop-animation.html#declaring-xml`](http://developer.android.com/guide/topics/graphics/prop-animation.html#declaring-xml)
动画标签的完整 XML 参考可在以下 URL 找到:
[`developer.android.com/guide/topics/resources/animation-resource.html#Property`](http://developer.android.com/guide/topics/resources/animation-resource.html#Property)
一旦你有了这个 XML 文件,你就可以使用清单 18-35 所示的方法来播放这个动画。
清单 18-35 。加载 Animator XML 资源文件
//file: TestPropertyAnimationActivity.java(ProAndroid5_ch18_TestPropertyAnimation.zip)
public void sequentialAnimationXML(View bView) {
AnimatorSet set = (AnimatorSet)AnimatorInflater.loadAnimator(this, R.animator.fadein);
set.setTarget(m_tv);
set.start();
}
请注意,首先加载动画 XML 文件,然后显式地将对象设置为动画是多么必要。在我们的例子中,要制作动画的对象是由 m_tv 表示的文本视图。清单 18-35 中的方法被按钮 4 调用(FadeOut/FadeIn XML)。当这个动画运行时,文本视图将首先淡出,然后通过淡入重新出现,就像以前的 alpha 动画一样。
使用 PropertyValuesHolder
到目前为止,我们已经看到了如何在单个动画中制作单个值的动画。类 PropertyValuesHolder 让我们在动画周期中制作多个值的动画。清单 18-36 演示了 PropertyValuesHolder 类的使用。
清单 18-36 。使用 PropertyValueHolder 类
//file: TestPropertyAnimationActivity.java(ProAndroid5_ch18_TestPropertyAnimation.zip)
public void testPropertiesHolder(View v) {
//Get the current coordinates of the text view.
//This allows us to know starting and ending positions to animate
float h = m_tv.getHeight(); float w = m_tv.getWidth();
float x = m_tv.getX(); float y = m_tv.getY();
//Set the view to the bottom right as a starting point
m_tv.setX(w); m_tv.setY(h);
//from the right bottom animate "x" to its original position: top left
PropertyValuesHolder pvhX = PropertyValuesHolder.ofFloat("x", x);
//from the right bottom animate "y" to its original position
PropertyValuesHolder pvhY = PropertyValuesHolder.ofFloat("y", y);
//when you do not specify the from position, the animation will take the current position
//as the from position.
//Tell the object animator to consider both
//"x" and "y" properties to animate to their respective target values.
ObjectAnimator oa = ObjectAnimator.ofPropertyValuesHolder(m_tv, pvhX, pvhY);
//set the duration
oa.setDuration(5000); //5 secs
//here is a way to set an interpolator on any animator
oa.setInterpolator(new AccelerateDecelerateInterpolator());
oa.start();
}
一个 PropertyValuesHolder 类保存一个属性名及其目标值。然后你可以定义许多这些 PropertyValuesHolder 用它们自己的属性来制作动画。您可以将这组 PropertyValuesHolder 提供给对象动画师。然后,对象动画制作者会将这些属性设置为它们在目标对象上各自的值。随着动画的每次刷新,来自每个 PropertyValuesHolder 的所有值将一次全部应用。这比并行应用多个动画更有效。
图 18-5 中的按钮 5 运行清单 18-36 中的代码。当这个动画运行时,文本视图将从右下角出现,并在 5 秒钟内迁移到左上角。
了解视图属性动画
Android SDK 有一个优化的方法来激活视图的各种属性。这是通过一个名为 viewpropertyimator 的类来完成的。清单 18-37 使用这个类将文本视图从右下角移动到左上角。
清单 18-37 。使用视图属性动画器
//file: TestPropertyAnimationActivity.java(ProAndroid5_ch18_TestPropertyAnimation.zip)
public void testViewAnimator(View v) {
//Remember current boundaries
float h = m_tv.getHeight(); float w = m_tv.getWidth();
float x = m_tv.getX(); float y = m_tv.getY();
//Position the view at bottom right
m_tv.setX(w); m_tv.setY(h);
//Get a ViewPropertyAnimator from the text view
ViewPropertyAnimator vpa = m_tv.animate();
//Set as many target values you want to set
vpa.x(x); vpa.y(y);
//Set duration and interpolators
vpa.setDuration(5000); //2 secs
vpa.setInterpolator(new AccelerateDecelerateInterpolator());
//The animation automatically starts when the UI thread gets to it.
//No need to explicitly call the start method.
//vpa.start();
}
使用 viewpropertyimator 的步骤如下:
- 通过调用视图上的 animate() 方法来获得 viewpropertyimator。
- 使用 viewpropertyianimator 对象来设置该视图的各种最终属性,如 x 、 y 、 scale 、 alpha 等等。
- 让 UI 线程通过从函数返回来继续。动画将自动开始。
该动画由按钮 6 调用。当此动画运行时,文本视图将从右下方迁移到左上方。
了解类型评估者
正如我们所见,对象动画师在每个动画周期中直接在目标对象上设置特定值。到目前为止,这些值都是单点值,例如 float s, int s,等等。如果你的目标对象有一个本身就是对象的属性,会发生什么?这就是类型赋值器发挥作用的地方。
为了说明这一点,考虑一个视图,我们想要在其上设置两个值,比如' x 和' y '。清单 18-35 展示了我们如何封装一个常规视图,我们知道如何改变 x 和 y 。封装将允许动画通过 Android 图形包中可用的 PointF 抽象为 x 和 y 调用一次。我们将提供一个 setPoint(PointF) 方法,然后在该方法中解析出 x 和 y 并在视图上设置它们。看一下清单 18-38 。
清单 18-38 。通过类型评估器 制作视图动画
//file: AnimatableView.java(ProAndroid5_ch18_TestPropertyAnimation.zip)
public class MyAnimatableView {
PointF curPoint = null; View m_v = null;
public MyAnimatableView(View v) {
curPoint = new PointF(v.getX(),v.getY());
m_v = v;
}
public PointF getPoint() {
return curPoint;
}
public void setPoint(PointF p) {
curPoint = p;
m_v.setX(p.x);
m_v.setY(p.y);
}
}
在代码清单 18-38 中,TypeEvaluator 是一个助手对象,它知道如何在动画周期中设置一个复合值,比如一个二维或三维点。在涉及复合字段(表示为一个对象)的场景中, ObjectAnimator 将获取起始复合值(类似于 x 和 y 的复合的 PointF 对象)、结束复合值,并将它们传递给 TypeEvaluator 辅助对象以获取中间对象值。然后在目标对象上设置这个复合值。清单 18-39 显示了 TypeEvlautor 如何通过它的 evaluate 方法计算这个中间值。
清单 18-39 。编码一个类型评估器
//file: MyPointEvaluator.java(ProAndroid5_ch18_TestPropertyAnimation.zip)
public class MyPointEvaluator implements TypeEvaluator<PointF> {
public PointF evaluate(float fraction, PointF startValue, PointF endValue) {
PointF startPoint = (PointF) startValue;
PointF endPoint = (PointF) endValue;
return new PointF(
startPoint.x + fraction * (endPoint.x - startPoint.x),
startPoint.y + fraction * (endPoint.y - startPoint.y));
}
}
从清单 18-39 中可以看出,您需要从 TypeEvaluator 接口继承并实现 evaluate() 方法。在这种方法中,您将获得动画总进度的一部分。您可以使用该分数来调整中间复合值,并将其作为类型化值返回。
清单 18-40 展示了一个 ObjectAnimator 如何使用 MyAnimatableView 和 MyPointEvaluator 来为一个视图的复合值制作动画。
清单 18-40 。使用型评估器
//file: TestPropertyAnimationActivity.java(ProAndroid5_ch18_TestPropertyAnimation.zip)
public void testTypeEvaluator(View v) {
float h = m_tv.getHeight(); float w = m_tv.getWidth();
float x = m_tv.getX(); float y = m_tv.getY();
PointF startingPoint = new PointF(w,h);
PointF endingPoint = new PointF(x,y);
//m_atv: You will need this code in your activity earlier as a local variable:
MyAnimatableView m_atv = new MyAnimatableView(m_tv);
ObjectAnimator viewCompositeValueAnimator =
ObjectAnimator.ofObject(m_atv
,"point", new MyPointEvaluator()
,startingPoint, endingPoint);
viewCompositeValueAnimator.setDuration(5000);
viewCompositeValueAnimator.start();
}
注意清单 18-40 中的注意到对象动画制作人正在使用方法 ofObject() 而不是 ofFloat() 或 ofInt() 。还要注意,动画的起始值和结束值是由类 PointF 表示的复合值。对象动画器的目标现在是为 PointF 提供一个中间值,然后将其传递给自定义类 MyAnimatableView 上的方法 setPoint(PointF) 。类 MyAnimatableView 可以相应地在包含的文本视图上设置各自的属性。使用类型评估器的清单 18-40 中的动画由按钮 7 调用。当此动画运行时,视图将从右下方迁移到左上方。
理解关键帧
在动画周期中,关键帧是放置关键时间标记(重要的时间实例)的有用位置。关键帧指定在给定时刻某个属性的特定值。关键标记的时间介于 0(动画开始)和 1(动画结束)之间。一旦你收集了这些关键帧值,你就将它们与一个特定的属性相对应,比如 alpha 、 x 或者 y 。关键帧与其各自属性的关联是通过 PropertyValuesHolder 类完成的。然后,您告诉 ObjectAnimator 为生成的 PropertyValuesHolder 制作动画。清单 18-41 演示了关键帧动画。
清单 18-41 。使用关键帧制作视图动画
//file:TestPropertyAnimationActivity.java(ProAndroid5_ch18_TestPropertyAnimation.zip)
public void testKeyFrames(View v) {
float h = m_tv.getHeight(); float w = m_tv.getWidth();
float x = m_tv.getX(); float y = m_tv.getY();
//Start frame : 0.2, alpha: 0.8
Keyframe kf0 = Keyframe.ofFloat(0.2f, 0.8f);
//Middle frame: 0.5, alpha: 0.2
Keyframe kf1 = Keyframe.ofFloat(.5f, 0.2f);
//end frame: 0.8, alpha: 0.8
Keyframe kf2 = Keyframe.ofFloat(0.8f, 0.8f);
PropertyValuesHolder pvhAlpha =
PropertyValuesHolder.ofKeyframe("alpha", kf0, kf1, kf2);
PropertyValuesHolder pvhX =
PropertyValuesHolder.ofFloat("x", w, x);
//end frame
ObjectAnimator anim =
ObjectAnimator.ofPropertyValuesHolder(m_tv, pvhAlpha,pvhX);
anim.setDuration(5000);
anim.start();
}
清单 18-41 中的动画由按钮 8 调用。当此动画运行时,您将看到文本从右向左移动。当 20%的时间过去后,α会变为 80%。 alpha 值将在中途达到 20%,并在动画时间的第 80 百分位变回 80%。
了解布局转换
属性动画 API 还通过 LayoutTransition 类提供基于布局的动画。这个类作为标准 API Java 文档的一部分在下面的 URL 中有很好的记录。
[`developer.android.com/reference/android/animation/LayoutTransition.html`](http://developer.android.com/reference/android/animation/LayoutTransition.html)
这里我们将只总结布局转换的关键点。要在一个视图组上启用布局转换(大多数布局都是视图组),您需要使用清单 18-42 中的代码。
清单 18-42 。设置布局转换
viewgroup.setLayoutTransition(
new LayoutTransition()
);
使用清单 18-42 中的代码,布局容器(视图组)将在添加和删除视图时显示默认转换。LayoutTransition 对象有四种不同的默认动画,涵盖以下每种情况:
- 添加视图(由于添加或放映而出现的视图的动画)
- 变化出现(布局中其余项目的动画,因为它们可能会由于添加新项目而改变其大小或外观)
- 移除视图(因移除或隐藏而消失的视图的动画)
- 变化消失(布局中剩余项目的动画,因为它们可能由于项目被移除而改变大小或外观)
如果您想要为每种情况定制动画,您可以在 LayoutTransition 对象上设置它们。在清单 18-43 中有一个例子。
清单 18-43 。布局过渡方法
//Here is how you get a new layout transition
LayoutTransition lt = new LayoutTransition();
//You can set this layout transition on a layout
someLayout.setLayoutTransition(lt);
//obtain a default animator if you need to remember
Animator defaultAppearAnimator = lt.getAnimator(APPEARING);
//create a new animator
ObjectAnimator someNewObjectAnimator1, someOtherObjectAnimator2;
//set it as your custom animator for the allowed set of animators
lt.setAnimator(APPEARING, someNewObjectAnimator1);
lt.setAnimator(CHANGE_APPEARING, someNewObjectAnimator1);
lt.setAnimator(DISAPPEARING, someNewObjectAnimator1);
lt.setAnimator(CHANGE_DISAPPEARING, someOtherObjectAnimator2);
因为您提供给布局过渡的动画应用于每个视图,所以动画在应用于每个视图之前会在内部克隆。
资源
当你使用 Android 动画 API 时,这里有一些有用的链接:
- :安卓属性动画作者研究笔记。
Android-developers . blogspot . com/2011/02/animation-in-honeycomb . html:房产动画重点博客。Android-developers . blogspot . com/2011/05/introducing-view property animator . html:关于查看属性动画的博客。developer . Android . com/guide/topics/graphics/prop-animation . html:Android SDK 中关于属性动画的主要文档。developer . Android . com/guide/topics/graphics/animation . html:Android 文档链接所有动画类型,包括属性动画和旧式动画。developer . Android . com/reference/Android/view/animation/package-summary . html:旧版动画包的 Java doc APIAndroid . view . animation。developer . Android . com/guide/topics/resources/animation-resource . html:各种动画类型的 XML 标签。- :本章可下载的测试项目。压缩文件的名称分别为 pro Android 5 _ ch18 _ testframeanimation . zip、pro Android 5 _ ch18 _ testlayoutanimation . zip、pro Android 5 _ ch18 _ testviewinanimation . zip 和 pro Android 5 _ ch18 _ testpropertyamination . zip。
摘要
在这一章中,我们已经介绍了逐帧动画、布局动画、视图动画、插值器、变换矩阵、摄像机以及使用新属性动画 API 的各种方法。所有的概念都有可用的代码片段,并有可用的可下载项目支持。