簡介
App Widgets 是一個可以嵌入到其他app(比如 HomeScreen)的能接收到週期性更新的一個微型的App 界面。這個view對用戶來說相當於一個小插件或者小部件。如果我們把這個小部件放到桌面的話,可以方便用戶不用打開app而能查看到重要的消息。
重要的類
AppWidgetProviderInfo
一個描述appWIdget
的類,此類中的信息表示我們的app widget layout
、最小高寬度或者更新頻率等等信息。
AppWidgetProvider
這個類非常重要,我們在開發的時候也會繼承它。這個類集成自broadcastreceiver
,而且我們可以跳進它源碼中看到,在它的onReceive()
裏面分發了很多事件,比如更新app widget
會分發到onUpdate()
,第一次添加app widget
會分發到onEnable()
,最後一個app widget
刪除後,會調用onDisable()
等等。
RemoteViews
一個用於遠程傳輸的view,其實它不是一個view,它會把我們的view和對該view的操作用action
封裝起來,然後序列化傳輸到另外的進程,然後反序列化,然後再在另外的進程對view進行我們要進行的操作。
AppWidgetManager
連接app進程和我們widget所處的進程的一個橋樑。後面詳細講。
用法
我們知道,一個app widget
是放在homeScreen或者鎖屏界面的,所以它實際上是運行在其他進程的,所以這裏得用到進程間通信。這也是爲什麼開發一個app widget
很麻煩的原因。
AndroidManifest.xml
首先在我們的AndroidManifest.xml
中聲明一個AppWidgetProvider
用於接收app widget的更新事件。AppWidgetProvider
實際上是一個廣播,所以註冊方式和廣播類似。
<receiver android:name="ExampleAppWidgetProvider" >
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/example_appwidget_info" />
</receiver>
上面代碼中有一個meta-data
,後面的name必須是android.appwidget.provider
。因爲系統正是根據這個來找到我們widget的配置文件的,下面resource就是它的配置文件,它對應着上面的AppWidgetProviderInfo
。我們在resource中配置的所有配置項都可以在這個類中找到。
xml/example_appwidget_info.xml
:
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="40dp"
android:minHeight="40dp"
android:updatePeriodMillis="86400000"
android:previewImage="@drawable/preview"
android:initialLayout="@layout/example_appwidget"
android:configure="com.example.android.ExampleAppWidgetConfigure"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen">
</appwidget-provider>
-
android:updatePeriodMillis
:更新時間(默認30min),每過一段時間,系統會給我們的AppWidgetProvider
發送一個update消息,我們可以在AppWidgetProvider
中的onUpdate()
中來處理更新widget界面。 -
android:previewImage
:預覽圖標,用戶在桌面選擇widget的時候可以看到這個圖標。和ic_launcher
有點類似的感覺。 -
android:initialLayout
:layout,不用說都知道 -
android:configure
:配置。要是有需要的話,用戶在添加widget的時候會打開這個Activity。來自定義配置相關的參數。 -
android:resizeMode
:widget可以被拉伸的方向。horizontal表示可以水平拉伸,vertical表示可以豎直拉伸 -
android:widgetCategory
: widget可以顯示的地方,可以是桌面,也可以是鎖屏。
在添加configure
的時候,我們也要在AndroidMainfest.xml中爲要打開的Activity設置intent-filter:
<activity android:name=".ExampleAppWidgetConfigure">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
</intent-filter>
</activity>
layout
由於widget不是跑在我們自己的進程,所以對其界面有限制,因爲系統會用到binder
通信,把我們的傳到另外一個進程去。
widget現在支持的佈局有:
FrameLayout
LinearLayout
RelativeLayout
GridLayout
支持的控件有:
AnalogClock
Button
Chronometer
ImageButton
ImageView
ProgressBar
TextView
ViewFlipper
ListView
GridView
StackView
AdapterViewFlipper
其實RemoteViews
也支持ViewStub
。
margin
在android4.0以前,我們得爲widget的邊緣添加margin,因爲它不能觸碰到屏幕邊界。
在android 4.0以後,系統默認爲我們添加了padding,所以我們在android4.0以後不用管。
AppWidgetProvider
我們的widget對應着AppWidgetProvider
,我們在AppWidgetProvider
中接收系統發給我們的更新、添加、刪除widget消息。
onUpdate()
我們可以在這個函數裏面做一些更新界面的事情。當updatePeriodMillis
所設置的時候到達後,就會運行這個函數。
onEnable()
用戶添加第一個widget的時候會調用
onDisable()
和onEnable相反,刪除最後一個widget的時候調用。
onAppWidgetOptionsChanged()
widget尺寸改變的時候調用(用戶可以調整大小)。
onDeleted()
用戶每次刪除widget的時候調用
onReceive(Context, Intent)
上述函數都在這個函數裏面分發。也就是說,實際上系統是給app發的一個個廣播事件。在這個函數中根據不同的事件,調用上述不同的函數。
public class ExampleAppWidgetProvider extends AppWidgetProvider {
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
final int N = appWidgetIds.length;//該widget個數
for (int i=0; i<N; i++) {
int appWidgetId = appWidgetIds[i];
//pendingIntent是一種還沒有發生的intent
//我們可以用pendingIntent來爲view設置一個onClick後調用的Intent事件。
//這裏是當用戶點擊了R.id.button按鈕後,用intent打開ExampleActivity。
Intent intent = new Intent(context, ExampleActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.appwidget_provider_layout);
views.setOnClickPendingIntent(R.id.button, pendingIntent);
//用appwidgetManager來通知系統更新widget
appWidgetManager.updateAppWidget(appWidgetId, views);
}
}
}
上面代碼只是一點擊widget是某個button打開activity的例子。當然我們也可以在這裏用PendingIntent來設置一個事件,讓用戶點擊某個控件後可以向我們發送一個廣播,然後在接收到廣播後做我們想做的事情。
Intent intent = new Intent();
intent.addAction(xxx);
PendingIntent ppendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0);
當然,我們得在ExampleAppWidgetProvider
加入相應的action。
到這裏,widget基本的操作已經完成。
Widget配合ListView等Collections使用
在我們日常開發中,ListView
等Collections的使用是必不可少的,這一節就講一下關於如何在widget中使用collections,我這裏用listview爲代表,其他的都類似。
前面沒有Collections
的時候,直接用RemoteViews
就可以直接填充視圖,然後加上Listview後,你會發現,沒法來填充ListView裏面的Item。
這裏就要介紹下在用Collections
的時候,其他幾個重要的類了:
-
RemoteViewsService
:一個連接widget和app進程的service,我們可以繼承這個類,來提供一個adapter給ListView/GridView
。 -
RemoteViewsService.RemoteViewsFactory
:一個類似BaseAdapter一樣的東西,我們可以直接把它看成ListView的Adapter。
有了這兩個類就好說了。首先RemoteViewsService
是一個Service,所以我們得在manifest.xml
裏面註冊,而且得申明綁定widget的權限。
<service android:name="MyWidgetService"
...
android:permission="android.permission.BIND_REMOTEVIEWS" />
申明後,我們的ListView得這麼用(官方栗子):
public void onUpdate(Context context, AppWidgetManager appWidgetManager,
int[] appWidgetIds) {
//初始化listView並指定adapter
for (int i = 0; i < appWidgetIds.length; ++i) {
//先new一個intent來啓動service
//這個service會提供給我們listview的item的界面。
Intent intent = new Intent(context, MyWidgetService.class);
//把app widget id 放到intent裏面
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
//widget_layout.xml裏面包含一個listview
RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
//爲listview指定一個 remote adapter,這個adapter會用intent連接到一個特定的service(MyWidgetService)
//我們後面就可以在service裏面來提供數據。
rv.setRemoteAdapter(appWidgetIds[i], R.id.list_view, intent);
//爲我們的listview指定一個empty view,這個empty view得也在上面的widget_layout裏面
rv.setEmptyView(R.id. , R.id.empty_view);
//這裏爲了讓我們的item可以設置點擊事件,我們得爲listview設置一個pending intent template(一個pendingIntent模板)
//然後後面我們會在adapter裏面爲每個item設置一個fillInIntent
Intent toastIntent = new Intent(context, ExampleAppWidgetProvider.class);
//這裏設置了一個action。
//ExampleAppWidgetProvider.TOAST_ACTION是我們自定義的一個action。這裏得把這個action添加到廣播接收者裏面。以便我們能接收到按下item後的廣播事件。
toastIntent.setAction(ExampleAppWidgetProvider.TOAST_ACTION);
toastIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
PendingIntent toastPendingIntent = PendingIntent.getBroadcast(context, 0, toastIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
rv.setPendingIntentTemplate(R.id.list_view, toastPendingIntent);
appWidgetManager.updateAppWidget(appWidgetIds[i], rv);
}
super.onUpdate(context, appWidgetManager, appWidgetIds);
}
下面我們來看看MyWidgetService
:
public class MyWidgetServiceextends RemoteViewsService {
@Override
public RemoteViewsFactory onGetViewFactory(Intent intent) {
//爲widget提供一個adapter
return new MyWidgetService(this.getApplicationContext(), intent);
}
}
class MyWidgetService implements RemoteViewsService.RemoteViewsFactory {
private static final int mCount = 10;
private List<WidgetItem> mWidgetItems = new ArrayList<WidgetItem>();
private Context mContext;
private int mAppWidgetId;
public MyWidgetService(Context context, Intent intent) {
mContext = context;
mAppWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
}
// 初始化data
public void onCreate() {
//這裏不建議下載或者做一些很重的操作,因爲這裏停留20s,你就會收到一個ANR的獎勵。
for (int i = 0; i < mCount; i++) {
mWidgetItems.add(new WidgetItem(i + "!"));
}
...
}
...
//相當於baseAdapter的getView()方法。
public RemoteViews getViewAt(int position) {
//這裏用layout創建一個RemoteVies。
RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.widget_item);
rv.setTextViewText(R.id.widget_item, mWidgetItems.get(position).text);
//這裏用fill-intent來填充前面設置的Intent模板
Bundle extras = new Bundle();
extras.putInt(ExampleAppWidgetProvider.EXTRA_ITEM, position);
Intent fillInIntent = new Intent();
fillInIntent.putExtras(extras);
//爲item設置intent onClick。當點擊後,會發出一個特定intent的廣播。我們在ExampleAppWidgetProvider裏面接收廣播就可以做我們想做的事了。
//但是這裏還是要記得在AndroidManifest.xml裏面ExampleAppWidgetProvider申明接收這個action。
rv.setOnClickFillInIntent(R.id.widget_item, fillInIntent);
...
// 返回view
return rv;
}
...
}
但是如果我們要更新listview中的數據的話,下面官方文檔裏面給出一一幅圖,很好地描述了工作流程,我們只要更新數據後,調用AppWidgetManager.notifyAppWidgetViewDataChanged()
就可以了。
大坑提醒:要是在開發中發現程序跑出來的效果不是我們所想象的, 最好把app下載了,然後重新添加桌面小插件可能就解決問題了,我在實際開發的時候,被這個問題坑了好幾次==。
RemoteViews的工作原理
看了上面的開發流程,我們來大概分析一下它的工作原理。
RemoteViews
主要用於通知欄和桌面小部件,然而通知欄和桌面小部件又是分別由NotificationManager
和AppWidgetManager
管理的。而我們在開發的時候,經常還很把它們和NotificationManagerService
或者AppWidgetService
一起使用。而我們在其他Android開發的時候,經常會遇到各種Manager。還有很多的Service
,比如WindowManagerService
與WindowManager
。用多了我們就會發現,它們的用法有很多相似的地方。
要是稍微觀察一下它們的工作機制的話,就會發現它們都是差不多的。
其實AppWidgetManager
是通過Binder
和SystemServer
進程中的AppWidgetService
通信的。
由此可見,桌面小插件實際上是在AppWidgetService
被加載的。而它們又是運行的SystemServer中,這就滿足了跨進程通信的條件。
我們發現RemoteViews
實際上是實現了Parcelable
接口的,所以它也是便於在Binder中進行跨進程傳輸的。
還記得我們在new RemoteViews的時候,是傳遞了一個包名進去的麼?
其實系統會根據RemoteViews
中指定的包名去得到相應的資源,然後用LayoutInflater來加載佈局文件。這就在SystemServer中加載了一個View了。然後還記得我們對View的操作都是用set來完成的麼?比如:
rv.setTextViewText(R.id.text_view,"hello");
然後來看看源碼:
public void setTextViewText(int viewId, CharSequence text) {
setCharSequence(viewId, "setText", text);
}
public void setCharSequence(int viewId, String methodName, CharSequence value) {
addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
}
我們發現爲viewsetText
的時候,本來平常view是通過view.setText(xxx)
來完成的,剛好這裏把我們的id和方法setText還有text記錄下來,然後new了一個action
.其實這裏的ReflectionAction
就是繼承Action
的。然後再addAction();
其實在RemoteViews裏面有一個ArrayList<Action>
。addAction()會把我們的Action放到list
裏面。當把這個RemoteViews
傳輸到SystemServer的時候 ,系統會調用RemoteViews
的apply()
方法:
public View apply(Context context, ViewGroup parent, OnClickHandler handler) {
RemoteViews rvToApply = getRemoteViewsToApply(context);
View result;
LayoutInflater inflater = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
// Clone inflater so we load resources from correct context and
// we don't add a filter to the static version returned by getSystemService.
inflater = inflater.cloneInContext(inflationContext);
inflater.setFilter(this);
result = inflater.inflate(rvToApply.getLayoutId(), parent, false);
rvToApply.performApply(result, parent, handler);
return result;
}
印證了剛剛說了用inflater加載佈局,然後再調用perforApply()
方法:
private void performApply(View v, ViewGroup parent, OnClickHandler handler) {
if (mActions != null) {
handler = handler == null ? DEFAULT_ON_CLICK_HANDLER : handler;
final int count = mActions.size();
for (int i = 0; i < count; i++) {
Action a = mActions.get(i);
a.apply(v, parent, handler);
}
}
}
遍歷Action ,然後調用其apply()
方法。我們來看看剛剛的ReflectionAction
的apply()
方法:
public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
final View view = root.findViewById(viewId);
if (view == null) return;
Class<?> param = getParameterType();
if (param == null) {
throw new ActionException("bad type: " + this.type);
}
try {
getMethod(view, this.methodName, param).invoke(view, wrapArg(this.value));
} catch (ActionException e) {
throw e;
} catch (Exception ex) {
throw new ActionException(ex);
}
}
豁然開朗,用反射調用我們實際操作的view.setText()
。
首先序列化,再通過Binder傳遞對象,然後再在SystemServer中來apply我們的操作。這就完成了我們想要的操作,方法非常巧妙。
總結
跨進程操作很巧妙,我們可以通過學習google的做法,在我們實際開發當中,也有不少的跨進程操作,我們也可以用這種方式來實現操作其他對象。特別是在開放SDK中,這種方式非常有幫助,異步調用要執行的代碼。