Android App Widget实现举例
App Widget是可以嵌入在其他应用(通常是主屏幕)的小型应用,接受定时的更新。创建一个App Widget,至少需要:
- AppWidgetProviderInfo:使用xml描述Widget,如Widget的尺寸、布局、更新频率等。
- AppWidgetProvider:定义了与Widget进行交互的基本方法。
- View Layout:Widget对应的布局。
下面通过一个例子说明如何创建一个简单的App Widget。这个例子在RecyclerView使用方法举例(2)的基础上,为App添加一个Widget,用于在主屏幕显示电影海报,效果如图1所示。
Contents
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>
在res/values-v14/dimens.xml中定义:
<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" > <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" > <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: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 { @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"); } @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" > <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(); 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接收自定义广播,如:
@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)); } }