应用程序小部件可让您的用户轻松访问应用程序最常用的功能,同时使应用程序出现在用户的主屏幕上。 通过将小部件添加到您的项目中,您可以提供更好的用户体验,同时鼓励用户继续使用您的应用程序,因为每当他们瞥一眼主屏幕时,就会看到您的小部件,显示您的应用程序中最有用和最有用的部分。有趣的内容。
在这个由三部分组成的系列文章中,我们将构建一个应用程序小部件,它具有几乎在每个 Android应用程序小部件中都可以找到的所有功能。
在本系列文章的最后,我们将扩展小部件以根据时间表并响应用户交互自动检索和显示新数据。
我们从上次停下来的地方接机,因此,如果您没有我们在第一篇文章中创建的小部件的副本,则可以从GitHub下载 。
更新布局
第一步是更新布局,以显示一些随时间变化的数据。 为了帮助我们准确地看到小部件何时收到每个更新,我将指示小部件每执行一次更新就检索并显示当前时间。
我将以下内容添加到小部件布局中:
- 显示最后更新标签的
TextView
。 - 一个
TextView
,它将显示上一次更新的时间。
当我们处理new_app_widget.xml文件时,我还将添加TextView
,最终将允许用户手动触发更新:
<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/widget_margin"
android:background="@drawable/widget_background"
android:orientation="vertical" >
<LinearLayout
android:background="@drawable/tvbackground"
style="@style/widget_views"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/id_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/widget_id"
style="@style/widget_text" />
<TextView
android:id="@+id/id_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="."
style="@style/widget_text" />
</LinearLayout>
<TextView
android:id="@+id/launch_url"
style="@style/widget_views"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/URL"
android:background="@drawable/tvbackground"/>
//Add a ‘Tap to update’ TextView//
<TextView
android:id="@+id/update"
style="@style/widget_views"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/update"
android:background="@drawable/tvbackground"/>
<LinearLayout
android:background="@drawable/tvbackground"
style="@style/widget_views"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
//Add a TextView that’ll display our ‘Last Update’ label//
<TextView
android:id="@+id/update_label"
style="@style/widget_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/last_update" />
//Add the TextView that’ll display the time of the last update//
<TextView
android:id="@+id/update_value"
style="@style/widget_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/time" />
</LinearLayout>
</LinearLayout>
接下来,定义我们在布局中引用的新字符串资源:
<string name="update">Tap to update</string>
<string name="last_update">Last Update</string>
<string name="time">%1$s</string>
@string/time
看起来与典型的字符串不同,因为它只是一个占位符,将在运行时被替换。
这给了我们完整的布局:
根据计划更新小部件
由于这是最容易实现的方法,因此我将在设置的时间段过后自动更新我们的小部件。
创建此类自动更新计划时,可以使用的最小间隔为1800000毫秒(30分钟)。 即使将项目的updatePeriodMillis
设置为少于30分钟,您的小部件仍将仅每半小时更新一次。
确定窗口小部件应该多久更新一次绝非易事。 频繁的更新会消耗更多设备电池的电量,但将这些更新的距离设置得太远,您的小部件可能会向用户显示明显过期的信息。
为了找到适合您特定项目的平衡,您需要在一系列更新频率上测试窗口小部件,并测量每个频率对电池寿命的影响,同时监视窗口小部件的内容是否过时。
由于“太频繁”是没有“正确”答案的令人沮丧的主观问题之一,因此它可以通过安排一些用户测试来帮助获得第二意见。 您甚至可以设置一些A / B测试,以检查某些更新频率是否比其他更新频率更积极。
打开项目的AppWidgetProviderInfo文件( res / xml / new_app_widget_info.xml ),您会看到它已经定义了默认更新间隔86400000毫秒(24小时)。
android:updatePeriodMillis="86400000"
有很多小部件每24小时仅更新一次,例如,显示天气预报的小部件可能每天仅需要检索一次新信息。 但是,即使您的窗口小部件每天仅需要更新一次,这也不是测试应用程序时的理想选择。 没有人愿意等待24小时,以查看其onUpdate()
方法是否正确触发,因此通常在执行初始测试时使用最短的时间间隔,然后在需要时更改此值。
为了使测试我们的应用程序更容易,我将使用最小的时间间隔:
android:updatePeriodMillis="1800000"
检索并显示当前时间
onUpdate()
方法负责使用新信息更新窗口小部件的Views
。 如果打开项目的窗口小部件提供程序( java / values / NewAppWidget.java ),则会看到它已经包含onUpdate()
方法的概述:
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
// There may be multiple widgets active, so update all of them//
for (int appWidgetId : appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId);
}
}
无论您的窗口小部件是自动更新还是响应用户交互,更新过程都完全相同:窗口小部件管理器发送带有ACTION_APPWIDGET_UPDATE
动作的广播意图,并且窗口小部件提供程序类通过调用onUpdate()
方法进行响应。依次调用updateAppWidget()
帮助器方法。
一旦我们更新了NewAppWidget.java类以检索并显示当前时间,我们的项目将使用同一段代码,而不管如何触发onUpdate()
方法:
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.widget.RemoteViews;
import android.app.PendingIntent;
import android.content.Intent;
import android.net.Uri;
import java.text.DateFormat;
import java.util.Date;
public class NewAppWidget extends AppWidgetProvider {
static void updateAppWidget(Context context,
AppWidgetManager appWidgetManager,
int appWidgetId) {
//Retrieve the time//
String timeString =
DateFormat.getTimeInstance(DateFormat.SHORT).format(new Date());
//Construct the RemoteViews object//
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.new_app_widget);
views.setTextViewText(R.id.id_value, String.valueOf(appWidgetId));
//Retrieve and display the time//
views.setTextViewText(R.id.update_value,
context.getResources().getString(
R.string.time, timeString));
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://code.tutsplus.com/"));
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
views.setOnClickPendingIntent(R.id.launch_url, pendingIntent);
appWidgetManager.updateAppWidget(appWidgetId, views);
}
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
//If multiple widgets are active, then update them all//
for (int appWidgetId : appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId);
}
}
}
上面的代码存在一个问题:如果用户在一分钟的时间内多次TextView
,则不会有视觉上的指示该小部件已更新,这仅仅是因为没有新的显示时间。 这可能不会给您的最终用户带来麻烦,因为他们不太可能坐在那里,每分钟敲击TextView
数次,想知道为什么时间没有改变。 但是,这可能会在测试您的应用程序时出现问题,因为这意味着您必须在触发onUpdate()
方法之间至少等待60秒。
为了确保始终存在某种视觉确认onUpdate()
方法已成功运行,每当onUpdate()
时,我都会显示一个吐司:
import android.widget.Toast;
...
...
...
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
for (int appWidgetId : appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId);
Toast.makeText(context, "Widget has been updated! ", Toast.LENGTH_SHORT).show();
}
}
}
更新小部件以响应用户操作
现在,我们的窗口小部件将每半小时自动更新一次,但是如何响应用户交互而更新呢?
通过在布局中添加Tap to Update TextView
并扩展NewAppWidget.java类以检索和显示当前时间(无论何时onUpdate()
我们已经奠定了很多基础。
剩下要做的唯一一件事情就是使用AppWidgetManager.ACTION_APPWIDGET_UPDATE
创建一个Intent
,这是一个通知小部件其更新时间的动作,然后响应于用户与Tap to Update TextView
交互来调用此Intent。
这是完整的NewAppWidget.java类:
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.widget.RemoteViews;
import android.app.PendingIntent;
import android.content.Intent;
import android.net.Uri;
import java.text.DateFormat;
import java.util.Date;
import android.widget.Toast;
public class NewAppWidget extends AppWidgetProvider {
static void updateAppWidget(Context context,
AppWidgetManager appWidgetManager,
int appWidgetId) {
//Retrieve the current time//
String timeString =
DateFormat.getTimeInstance(DateFormat.SHORT).format(new Date());
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.new_app_widget);
views.setTextViewText(R.id.id_value, String.valueOf(appWidgetId));
views.setTextViewText(R.id.update_value,
context.getResources().getString(
R.string.time, timeString));
//Create an Intent with the AppWidgetManager.ACTION_APPWIDGET_UPDATE action//
Intent intentUpdate = new Intent(context, NewAppWidget.class);
intentUpdate.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
//Update the current widget instance only, by creating an array that contains the widget’s unique ID//
int[] idArray = new int[]{appWidgetId};
intentUpdate.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, idArray);
//Wrap the intent as a PendingIntent, using PendingIntent.getBroadcast()//
PendingIntent pendingUpdate = PendingIntent.getBroadcast(
context, appWidgetId, intentUpdate,
PendingIntent.FLAG_UPDATE_CURRENT);
//Send the pending intent in response to the user tapping the ‘Update’ TextView//
views.setOnClickPendingIntent(R.id.update, pendingUpdate);
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://code.tutsplus.com/"));
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
views.setOnClickPendingIntent(R.id.launch_url, pendingIntent);
//Request that the AppWidgetManager updates the application widget//
appWidgetManager.updateAppWidget(appWidgetId, views);
}
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
for (int appWidgetId : appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId);
Toast.makeText(context, "Widget has been updated! ", Toast.LENGTH_SHORT).show();
}
}
}
测试项目时,能够按需更新窗口小部件非常宝贵,因为这意味着您不必等待更新间隔过去。 即使您未在完成的应用程序中包括此功能,也可能想要添加一个临时的Tap to Update按钮,以简化测试项目时的工作。
创建预览图像
现在,我们的小部件看上去与Android Studio为我们自动生成的小部件完全不同,但目前仍在使用自动生成的预览图像。
理想情况下,预览图像应鼓励用户从“小部件选择器”中选择您的小部件,但至少应准确地表示小部件的实际外观! 当前,我们的预览图像不会打勾这些框,因此我们需要创建一个新框。
最简单的方法是使用Android模拟器中包含的Widget Preview应用程序。
创建小部件预览
首先,在Android虚拟设备(AVD)上安装项目。 然后打开AVD的应用程序抽屉并启动Widget Preview应用程序。 AVD将显示设备上已安装的每个应用程序的列表-从列表中选择您的应用程序。
您的小部件将显示在空白背景上。 花一些时间调整和调整窗口小部件的大小,以使其看起来最佳,然后对结果感到满意后,选择“ 拍摄快照” 。 屏幕截图将以PNG格式保存在AVD的“ 下载”文件夹中。 要检索此文件,请切换回Android Studio并通过从工具栏中选择“ 视图”>“工具窗口”>“设备文件资源管理器”来打开“ 设备文件资源 管理器 ”。 在设备文件资源管理器视图中,导航到sdcard / Download文件夹,您将在其中找到另存为的预览图像: [app_name] _ori_ [orientation] .png
将此图像拖出Android Studio并将其放置在易于访问的位置,例如您的桌面。 为图像指定一个更具描述性的名称,然后将其拖放到项目的drawable文件夹中。 现在,您可以打开AppWidgetProviderInfo文件(在本例中为new_app_widget_info.xml ),并更改以下行以引用新的预览图像:
android:previewImage="@drawable/example_appwidget_preview"
最后,您可以从项目中删除不必要的example_appwidget_preview可绘制对象。
测试您的小部件
现在该测试完成的小部件了!
首先,将更新后的项目安装在物理Android设备或AVD上。 从主屏幕移除这个小工具的所有现有实例,所以你知道你正在使用最新版本的工作。
要添加窗口小部件,请在主屏幕上按任何空白区域,点击窗口小部件 ,然后选择您的窗口小部件。 您将有机会根据需要调整窗口小部件的大小和位置。
上次更新值将显示您创建此小部件的时间。 按Tap to Update ,小部件应显示吐司和新时间。 如果您在主屏幕上放置窗口小部件的第二个实例,则它应显示与第一个实例不同的时间。
您可以通过为其点击以更新 TextView
的Tap来更新这些小部件实例中的任何一个。 如果触发第一个窗口小部件的手动更新,则第二个窗口小部件将不会更新,反之亦然。
除了手动更新,在App Widget管理将在您创建第一个实例更新你的widget,每30分钟一次,基于时间的所有实例。 尽管手动更新会导致实例显示不同的时间,但每半小时一次,所有实例都会同时更新,此时它们将显示相同的时间。
您可能希望在主屏幕上保留此小部件的一些实例,以便可以监视其内容随时间的变化,以响应自动和手动更新的组合。
结论
在本系列中,我们一直在创建一个Android应用程序小部件,以演示如何实现应用程序小部件中所有最常见的功能。 如果您从一开始就一直在关注,那么到现在为止,您将已经构建了一个小部件,该小部件可以自动更新并响应用户输入,并且能够对onClick事件做出反应(您也可以从GitHub下载完整的项目 )。