Android App Widget实现举例
Author: nex3z
2016-02-20
App Widget是可以嵌入在其他应用(通常是主屏幕)的小型应用,接受定时的更新。创建一个App Widget,至少需要:
- AppWidgetProviderInfo:使用xml描述Widget,如Widget的尺寸、布局、更新频率等。
- AppWidgetProvider:定义了与Widget进行交互的基本方法。
- View Layout:Widget对应的布局。
下面通过一个例子说明如何创建一个简单的App Widget。这个例子在RecyclerView使用方法举例(2)的基础上,为App添加一个Widget,用于在主屏幕显示电影海报,效果如图1所示。
图1
1. 创建App Widget布局
1.1. App Widget的布局的限制
App Widget的布局基于RemoteViews,并不支持全部的Layout和View Widget。仅能使用以下四种Layout:
- FrameLayout
- LinearLayout
- RelativeLayout
- GridLayout
以及以下几种Widget:
- AnalogClock
- Button
- Chronometer
- ImageButton
- ImageView
- ProgressBar
- TextView
- ViewFlipper
- ListView
- GridView
- StackView
- AdapterViewFlipper
App Widget的布局不支持以上的派生以及自定义View。
1.2. Margin的控制
从Android 4.0开始,App Widget的边缘会被自动加入padding,用于更好地和图标对齐。为了在不同版本系统上获得相同的显示效果,在res/values/dimens.xml中定义:
<dimen name="widget_margin">8dp</dimen>
<dimen name="widget_margin">8dp</dimen>
<dimen name="widget_margin">8dp</dimen>
在res/values-v14/dimens.xml中定义:
<dimen name="widget_margin">0dp</dimen>
<dimen name="widget_margin">0dp</dimen>
<dimen name="widget_margin">0dp</dimen>
然后把widget_margin 添加到App Widget的布局里面,例如:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/widget_margin" >
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/widget_margin" >
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" >
...
</LinearLayout>
</FrameLayout>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/widget_margin" >
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" >
...
</LinearLayout>
</FrameLayout>
1.3. 创建布局
创建App Widget布局widget_poster.xml如下,ImageView用于显示海报。
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/widget_margin" >
android:id="@+id/widget_movie_poster"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:adjustViewBounds="true"
android:src="@drawable/placeholder_poster"
tools:src="@drawable/placeholder_poster" />
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/widget_margin" >
<ImageView
android:id="@+id/widget_movie_poster"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:adjustViewBounds="true"
android:src="@drawable/placeholder_poster"
tools:src="@drawable/placeholder_poster" />
</FrameLayout>
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/widget_margin" >
<ImageView
android:id="@+id/widget_movie_poster"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:adjustViewBounds="true"
android:src="@drawable/placeholder_poster"
tools:src="@drawable/placeholder_poster" />
</FrameLayout>
【完整代码】
2. 创建AppWidgetProviderInfo
首先需要创建用于描述Widget的AppWidgetProviderInfo。在res目录下创建xml文件夹,在其中新建simple_widget_info.xml如下:
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minHeight="180dp"
android:previewImage="@drawable/poster_widget_preview"
android:updatePeriodMillis="1800000"
android:initialLayout="@layout/poster_widget"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen">
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="110dp"
android:minHeight="180dp"
android:previewImage="@drawable/poster_widget_preview"
android:updatePeriodMillis="1800000"
android:initialLayout="@layout/poster_widget"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen">
</appwidget-provider>
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="110dp"
android:minHeight="180dp"
android:previewImage="@drawable/poster_widget_preview"
android:updatePeriodMillis="1800000"
android:initialLayout="@layout/poster_widget"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen">
</appwidget-provider>
这里android:previewImage 是在选择Widget时显示的预览图片,如果不提供,默认使用应用图标。Android模拟器自带了一个获取Widget预览图片的应用。android:updatePeriodMillis 是更新间隔,最短为30分钟。android:initialLayout 指定了Widget的布局。android:widgetCategory 用于声明Widget是用于主屏幕(home_screen )还是锁屏(keyguard ),只有5.0之前版本支持锁屏Widget,5.0及之后版本仅home_screen 有效。
3. 创建AppWidgetProvider
创建SimpleWidgetProvider继承AppWidgetProvider,为其添加onUpdate() 方法。该方法会在更新App Widget时被调用。注意这里仍处于Main Thread,不适合进行耗时的操作。可以将耗时操作放在IntertService中处理。
public class SimpleWidgetProvider extends AppWidgetProvider {
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
context.startService(new Intent(context, SimpleWidgetIntentService.class));
public class SimpleWidgetProvider extends AppWidgetProvider {
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
context.startService(new Intent(context, SimpleWidgetIntentService.class));
}
}
public class SimpleWidgetProvider extends AppWidgetProvider {
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
context.startService(new Intent(context, SimpleWidgetIntentService.class));
}
}
【完整代码】
接下来创建用于进行耗时操作的IntentService如下:
public class SimpleWidgetIntentService extends IntentService {
public SimpleWidgetIntentService() {
super("SimpleWidgetIntentService");
protected void onHandleIntent(Intent intent) {
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this);
int[] appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(this,
SimpleWidgetProvider.class));
MovieService movieService = App.getRestClient().getMovieService();
Call<MovieResponse> call = movieService.getMovies(MovieService.SORT_BY_POPULARITY_DESC, 1);
MovieResponse response = call.execute().body();
List<Movie> movies = response.getMovies();
Random random = new Random();
int pick = random.nextInt(movies.size());
String posterPath = movies.get(pick).getPosterPath();
Bitmap poster = ImageUtility.downloadBitmap(ImageUtility.getImageUrl(posterPath));
for (int appWidgetId : appWidgetIds) {
Intent launchIntent = new Intent(this, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, launchIntent, 0);
RemoteViews views = new RemoteViews(getPackageName(), R.layout.widget_poster);
views.setOnClickPendingIntent(R.id.widget_movie_poster, pendingIntent);
views.setImageViewBitmap(R.id.widget_movie_poster, poster);
appWidgetManager.updateAppWidget(appWidgetId, views);
} catch (IOException e) {
Log.e(LOG_TAG, e.getMessage());
public class SimpleWidgetIntentService extends IntentService {
public SimpleWidgetIntentService() {
super("SimpleWidgetIntentService");
}
@Override
protected void onHandleIntent(Intent intent) {
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this);
int[] appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(this,
SimpleWidgetProvider.class));
MovieService movieService = App.getRestClient().getMovieService();
Call<MovieResponse> call = movieService.getMovies(MovieService.SORT_BY_POPULARITY_DESC, 1);
try {
MovieResponse response = call.execute().body();
List<Movie> movies = response.getMovies();
Random random = new Random();
int pick = random.nextInt(movies.size());
String posterPath = movies.get(pick).getPosterPath();
Bitmap poster = ImageUtility.downloadBitmap(ImageUtility.getImageUrl(posterPath));
for (int appWidgetId : appWidgetIds) {
Intent launchIntent = new Intent(this, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, launchIntent, 0);
RemoteViews views = new RemoteViews(getPackageName(), R.layout.widget_poster);
views.setOnClickPendingIntent(R.id.widget_movie_poster, pendingIntent);
views.setImageViewBitmap(R.id.widget_movie_poster, poster);
appWidgetManager.updateAppWidget(appWidgetId, views);
}
} catch (IOException e) {
Log.e(LOG_TAG, e.getMessage());
}
}
}
public class SimpleWidgetIntentService extends IntentService {
public SimpleWidgetIntentService() {
super("SimpleWidgetIntentService");
}
@Override
protected void onHandleIntent(Intent intent) {
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this);
int[] appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(this,
SimpleWidgetProvider.class));
MovieService movieService = App.getRestClient().getMovieService();
Call<MovieResponse> call = movieService.getMovies(MovieService.SORT_BY_POPULARITY_DESC, 1);
try {
MovieResponse response = call.execute().body();
List<Movie> movies = response.getMovies();
Random random = new Random();
int pick = random.nextInt(movies.size());
String posterPath = movies.get(pick).getPosterPath();
Bitmap poster = ImageUtility.downloadBitmap(ImageUtility.getImageUrl(posterPath));
for (int appWidgetId : appWidgetIds) {
Intent launchIntent = new Intent(this, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, launchIntent, 0);
RemoteViews views = new RemoteViews(getPackageName(), R.layout.widget_poster);
views.setOnClickPendingIntent(R.id.widget_movie_poster, pendingIntent);
views.setImageViewBitmap(R.id.widget_movie_poster, poster);
appWidgetManager.updateAppWidget(appWidgetId, views);
}
} catch (IOException e) {
Log.e(LOG_TAG, e.getMessage());
}
}
}
【完整代码】
用户可以向主屏幕添加多个相同的App Widget,这些App Widget通过各自的ID来区别。首先由AppWidgetManager获取了appWidgetIds ,appWidgetIds 保存了目前所有的App Widget的ID。然后在循环for (int appWidgetId : appWidgetIds) 里面,对各个App Widget进行更新:首先获取RemoteViews,为其添加OnClickPendingIntent(点击App Widget会打开MainActivity),并通过setImageViewBitmap() 为其添加之前下载的海报图片。
4. 在Manifest中声明App Widget
将之前实现的AppWidgetProvider,也就是SimpleWidgetProvider,加入到AndroidManifest.xml中:
<receiver android:name=".widget.SimpleWidgetProvider"
android:label="@string/simple_widget_name" >
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/simple_widget_info" />
<receiver android:name=".widget.SimpleWidgetProvider"
android:label="@string/simple_widget_name" >
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/simple_widget_info" />
</receiver>
<receiver android:name=".widget.SimpleWidgetProvider"
android:label="@string/simple_widget_name" >
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/simple_widget_info" />
</receiver>
这里的android:label 会显示在选择App Widget的界面。
至此便完成了一个App Widget的创建。
5. 完整代码。
完整代码可以参考这里。
6. 关于AppWidgetProvider
AppWidgetProvider继承自BroadcastReceiver,在源码中可以看到,AppWidgetProvider在onReceive() 中对接收到的Intent进行解析,并调用对应的方法进行处理。
* Implements {@link BroadcastReceiver#onReceive} to dispatch calls to the various
* other methods on AppWidgetProvider.
* @param context The Context in which the receiver is running.
* @param intent The Intent being received.
// BEGIN_INCLUDE(onReceive)
public void onReceive(Context context, Intent intent) {
// Protect against rogue update broadcasts (not really a security issue,
// just filter bad broacasts out so subclasses are less likely to crash).
String action = intent.getAction();
if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) {
Bundle extras = intent.getExtras();
int[] appWidgetIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS);
if (appWidgetIds != null && appWidgetIds.length > 0) {
this.onUpdate(context, AppWidgetManager.getInstance(context), appWidgetIds);
} else if (AppWidgetManager.ACTION_APPWIDGET_DELETED.equals(action)) {
Bundle extras = intent.getExtras();
if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID)) {
final int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID);
this.onDeleted(context, new int[] { appWidgetId });
} else if (AppWidgetManager.ACTION_APPWIDGET_OPTIONS_CHANGED.equals(action)) {
Bundle extras = intent.getExtras();
if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID)
&& extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS)) {
int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID);
Bundle widgetExtras = extras.getBundle(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS);
this.onAppWidgetOptionsChanged(context, AppWidgetManager.getInstance(context),
appWidgetId, widgetExtras);
} else if (AppWidgetManager.ACTION_APPWIDGET_ENABLED.equals(action)) {
} else if (AppWidgetManager.ACTION_APPWIDGET_DISABLED.equals(action)) {
this.onDisabled(context);
} else if (AppWidgetManager.ACTION_APPWIDGET_RESTORED.equals(action)) {
Bundle extras = intent.getExtras();
int[] oldIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_OLD_IDS);
int[] newIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS);
if (oldIds != null && oldIds.length > 0) {
this.onRestored(context, oldIds, newIds);
this.onUpdate(context, AppWidgetManager.getInstance(context), newIds);
/**
* Implements {@link BroadcastReceiver#onReceive} to dispatch calls to the various
* other methods on AppWidgetProvider.
*
* @param context The Context in which the receiver is running.
* @param intent The Intent being received.
*/
// BEGIN_INCLUDE(onReceive)
public void onReceive(Context context, Intent intent) {
// Protect against rogue update broadcasts (not really a security issue,
// just filter bad broacasts out so subclasses are less likely to crash).
String action = intent.getAction();
if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) {
Bundle extras = intent.getExtras();
if (extras != null) {
int[] appWidgetIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS);
if (appWidgetIds != null && appWidgetIds.length > 0) {
this.onUpdate(context, AppWidgetManager.getInstance(context), appWidgetIds);
}
}
} else if (AppWidgetManager.ACTION_APPWIDGET_DELETED.equals(action)) {
Bundle extras = intent.getExtras();
if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID)) {
final int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID);
this.onDeleted(context, new int[] { appWidgetId });
}
} else if (AppWidgetManager.ACTION_APPWIDGET_OPTIONS_CHANGED.equals(action)) {
Bundle extras = intent.getExtras();
if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID)
&& extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS)) {
int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID);
Bundle widgetExtras = extras.getBundle(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS);
this.onAppWidgetOptionsChanged(context, AppWidgetManager.getInstance(context),
appWidgetId, widgetExtras);
}
} else if (AppWidgetManager.ACTION_APPWIDGET_ENABLED.equals(action)) {
this.onEnabled(context);
} else if (AppWidgetManager.ACTION_APPWIDGET_DISABLED.equals(action)) {
this.onDisabled(context);
} else if (AppWidgetManager.ACTION_APPWIDGET_RESTORED.equals(action)) {
Bundle extras = intent.getExtras();
if (extras != null) {
int[] oldIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_OLD_IDS);
int[] newIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS);
if (oldIds != null && oldIds.length > 0) {
this.onRestored(context, oldIds, newIds);
this.onUpdate(context, AppWidgetManager.getInstance(context), newIds);
}
}
}
}
/**
* Implements {@link BroadcastReceiver#onReceive} to dispatch calls to the various
* other methods on AppWidgetProvider.
*
* @param context The Context in which the receiver is running.
* @param intent The Intent being received.
*/
// BEGIN_INCLUDE(onReceive)
public void onReceive(Context context, Intent intent) {
// Protect against rogue update broadcasts (not really a security issue,
// just filter bad broacasts out so subclasses are less likely to crash).
String action = intent.getAction();
if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) {
Bundle extras = intent.getExtras();
if (extras != null) {
int[] appWidgetIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS);
if (appWidgetIds != null && appWidgetIds.length > 0) {
this.onUpdate(context, AppWidgetManager.getInstance(context), appWidgetIds);
}
}
} else if (AppWidgetManager.ACTION_APPWIDGET_DELETED.equals(action)) {
Bundle extras = intent.getExtras();
if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID)) {
final int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID);
this.onDeleted(context, new int[] { appWidgetId });
}
} else if (AppWidgetManager.ACTION_APPWIDGET_OPTIONS_CHANGED.equals(action)) {
Bundle extras = intent.getExtras();
if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID)
&& extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS)) {
int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID);
Bundle widgetExtras = extras.getBundle(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS);
this.onAppWidgetOptionsChanged(context, AppWidgetManager.getInstance(context),
appWidgetId, widgetExtras);
}
} else if (AppWidgetManager.ACTION_APPWIDGET_ENABLED.equals(action)) {
this.onEnabled(context);
} else if (AppWidgetManager.ACTION_APPWIDGET_DISABLED.equals(action)) {
this.onDisabled(context);
} else if (AppWidgetManager.ACTION_APPWIDGET_RESTORED.equals(action)) {
Bundle extras = intent.getExtras();
if (extras != null) {
int[] oldIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_OLD_IDS);
int[] newIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS);
if (oldIds != null && oldIds.length > 0) {
this.onRestored(context, oldIds, newIds);
this.onUpdate(context, AppWidgetManager.getInstance(context), newIds);
}
}
}
}
例如对于ACTION_APPWIDGET_UPDATE ,首先尝试App Widget ID列表appWidgetIds ,然后调用onUpdate() ,我们在自己实现的SimpleWidgetProvider中重写了onUpdate() ,用于进行具体的更新操作。
在SimpleWidgetProvider中,通过重写onReceive() ,可以让SimpleWidgetProvider接收自定义广播,如:
public void onReceive(@NonNull Context context, @NonNull Intent intent) {
super.onReceive(context, intent);
if (SunshineSyncAdapter.ACTION_DATA_UPDATED.equals(intent.getAction())) {
context.startService(new Intent(context, TodayWidgetIntentService.class));
@Override
public void onReceive(@NonNull Context context, @NonNull Intent intent) {
super.onReceive(context, intent);
if (SunshineSyncAdapter.ACTION_DATA_UPDATED.equals(intent.getAction())) {
context.startService(new Intent(context, TodayWidgetIntentService.class));
}
}
@Override
public void onReceive(@NonNull Context context, @NonNull Intent intent) {
super.onReceive(context, intent);
if (SunshineSyncAdapter.ACTION_DATA_UPDATED.equals(intent.getAction())) {
context.startService(new Intent(context, TodayWidgetIntentService.class));
}
}