•What
AppWidget 即桌面小部件,也叫桌面控件,就是能直接顯示在Android系統桌面上的小程序;
這么說可能有點抽象,看圖:
像這種,桌面上的天氣、時鍾、搜索框等等,都屬於 APP Widget;
一些用戶使用比較頻繁的程序,可以做成AppWidget,這樣能方便地使用。
AppWidget 是Android 系統應用開發層面的一部分,有着特殊用途,使用得當的化,的確會為app 增色不少;
它的工作原理是把一個進程的控件嵌入到另外一個進程的窗口里的一種方法。
長按桌面空白處,會出現一個 AppWidget 的文件夾;
在里面找到相應的 AppWidget ,長按拖出,即可將 AppWidget 添加到桌面;
•How
首先,新建一個項目,我命名為 TestAppWidget:
將項目結構模式改為 Project 模式:
然后,右擊 app, New->Widget->App Widget :
來到如下界面:
- Class Name : 小控件的名字,這里我選擇默認的
- resizeMode : 小控件可以被拉伸的方向
- horizontal : 水平拉伸
- vertical : 數值拉伸
- Both : 兩者都
- none : 無
- Minimum Width : 小控件占用的寬度單元格
- Minimun Height : 小控件占用的高度單元格
Width,Height 暫且都選為默認值,后期覺得不合適可以更改;
一切准備就緒,點擊 FINISH 就創建了一個 Android Studio 默認的 App Widget;
讓我們來看看這個默認的 Widget 長啥樣,首先將這個 Widget 放置到桌面:
![]()
上圖紅框框中的便是默認的 Widget 的樣式;
•通過代碼深入了解Widget
AppWidget 是通過 BroadcastReceiver 的形式進行控制的;
開發 AppWidget 的主要類為 AppWidgetProvider,該類繼承自 BroadcastReceiver。
為了實現桌面小部件,開發者只要開發一個繼承自 AppWidgetProvider 的子類,並重寫它的 onUpdate() 方法即可。
重寫該方法,一般來說可按如下幾個步驟進行:
1、創建一個 RemoteViews 對象,這個對象加載時指定了桌面小部件的界面布局文件。
2、設置 RemoteViews 創建時加載的布局文件中各個元素的屬性。
3、創建一個 ComponentName 對象
4、調用 AppWidgetManager 更新桌面小部件。
在 App/src/main/java 目錄下,有一個自動生成的 NewAppWidget.java 文件:
NewAppWidget.java
/** * Implementation of App Widget functionality. */ public class NewAppWidget extends AppWidgetProvider { static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) { CharSequence widgetText = context.getString(R.string.appwidget_text); // Construct the RemoteViews object RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.new_app_widget); views.setTextViewText(R.id.appwidget_text, widgetText); // Instruct the widget manager to update the widget appWidgetManager.updateAppWidget(appWidgetId, views); } @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); } } @Override public void onEnabled(Context context) { // Enter relevant functionality for when the first widget is created } @Override public void onDisabled(Context context) { // Enter relevant functionality for when the last widget is disabled } }該類繼承自 AppWidgetProvider ,Android Studio 默認幫我們重寫 onUpdate() 方法遍歷 appWidgetId,並調用了 updateAppWidget() 方法。
再看 updateAppWidget() 方法,很簡單,只有四行:
- 第一行:CharSequence widgetText = context.getString(R.string.appwidget_text);
- 聲明了一個字符串
- 第二行: RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.new_app_widget);
- 創建了一個 RemoteViews 對象
- 第一個參數傳應用程序包名
- 第二個參數指定了 RemoteViews 加載的布局文件
- 這一行對應上面步驟中說的第一點
可以看到在 app/src/main/res/layout/ 目錄下面 Android Studio 自動生成了一個 new_app_widget.xml 文件:
new_app_widget.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="?attr/appWidgetBackgroundColor" android:padding="@dimen/widget_margin" android:theme="@style/ThemeOverlay.TestAppWidget.AppWidgetContainer"> <TextView android:id="@+id/appwidget_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_centerVertical="true" android:layout_margin="8dp" android:background="?attr/appWidgetBackgroundColor" android:contentDescription="@string/appwidget_text" android:text="@string/appwidget_text" android:textColor="?attr/appWidgetTextColor" android:textSize="24sp" android:textStyle="bold|italic" /> </RelativeLayout>這個文件就是我們最后看到的桌面小部件的樣子,布局文件中只有一個TextView。
這是你可能會問,想要加圖片可以嗎?
可以,就像正常的 Activity 布局一樣添加 ImageView 就行了;
不過需要注意的是 小部件布局文件可以添加的組件是有限制的,詳細內容在下文介紹RemoteViews 時再說。
- 第三行: views.setTextViewText(R.id.appwidget_text, widgetText);
- 將第一行聲明的字符串賦值給上面布局文件中的 TextView
- 注意這里賦值時,指定 TextView 的 id,要對應起來
- 這一行對於了上面步驟中的第二點。
- 第四行: appWidgetManager.updateAppWidget(appWidgetId, views);
- 這里調用了 appWidgetManager.updateAppWidget() 方法,更新小部件
- 這一行對應了上面步驟中的第四點
這時,你可能有疑問了,上面明明說了四個步驟,其中第三步,創建一個 ComponentName 對象,明明就不需要。
的確,這個例子中也沒有用到。
如果我們手敲第四步代碼,Android Studio 的智能提示會告訴你 appWidgetManager.updateAppWidget() 有三個重載的方法。
源碼中三個方法沒有寫在一起,為了方便,這里我復制貼出官方 API 中的介紹:
- void updateAppWidget(ComponentName provider, RemoteViews views)
- Set the RemoteViews to use for all AppWidget instances for the supplied AppWidget provider
- void updateAppWidget(int[] appWidgetIds, RemoteViews views))
- Set the RemoteViews to use for the specified appWidgetIds
- void updateAppWidget(int appWidgetId, RemoteViews views)
- Set the RemoteViews to use for the specified appWidgetId
這個三個方法都接收兩個參數,第二個參數都是 RemoteViews 對象。
其中第一個方法的第一個參數就是 ComponentName 對象,更新所有的 AppWidgetProvider 提供的所有的 AppWidget 實例;
第二個方法,更新明確指定 Id 的 AppWidget 的對象集;
第三個方法,更新明確指定 Id 的某個 AppWidget 對象。
所以一般我們使用第一個方法,針對所有的 AppWidget 對象,我們也可以根據需要選擇性地去更新。
到這里,所有步驟都結束了,就完了?還沒。
前面說了,自定義的 NewAppWidget 繼承自 AppWidgetProvider,而 AppWidgetProvider 又是繼承自 BroadCastReceiver;
所以說 NewAppWidget 本質上是一個廣播接收者,屬於四大組件之一,需要我們的清單文件中注冊。
打開 AndroidManifest.xml 文件可以看到,的確是注冊了小部件的,內容如下:
AndroidManifest.xml
<receiver android:name=".NewAppWidget"> <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> </intent-filter> <meta-data android:name="android.appwidget.provider" android:resource="@xml/new_app_widget_info" /> </receiver>上面代碼中有一個 Action,這個 Action 必須要加,且不能更改,屬於系統規范,是作為小部件的標識而存在的。
如果不加,這個 Receiver 就不會出現在小部件列表里面。
然后看到小部件指定了 android:resource="@xml/new_app_widget_info" 作為 meta-data;
細心的你發現了,在 res 目錄下面建立了一個 xml 文件夾,xml 文件夾下有一個 new_app_widget_info.xml 文件:
new_app_widget_info.xml
<?xml version="1.0" encoding="utf-8"?> <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:initialKeyguardLayout="@layout/new_app_widget" android:initialLayout="@layout/new_app_widget" android:minWidth="40dp" android:minHeight="40dp" android:previewImage="@drawable/example_appwidget_preview" android:resizeMode="horizontal|vertical" android:updatePeriodMillis="86400000" android:widgetCategory="home_screen"> </appwidget-provider>這里配置了一些小部件的基本信息:
- minWidth 和 minHeight 屬性值指定默認情況下 App Widget 占用的最小空間量 。
- 在默認主屏幕中,定義了具有特定高度和寬度的單元格網格,用來放置 App Widget
- 如果定義的最小寬度或高度的值不匹配的單元格網格的尺寸,則該App Widget尺寸會向上取最接近的大小
尺寸規則計算如下:
有關調整應用程序小部件大小的更多信息,請參閱 應用程序小部件設計指南。
PS : 為使應用程序小部件可跨設備移植,應用程序小部件的最小尺寸不得大於4 x 4單元。
resizeMode 屬性指定 App Widget 調整大小的規則
- 該 resizeMode 屬性的值包括 horizontal(水平) , vertical(垂直) 和 none(無)
- 支持多種模式填寫多個值,中間用 | 隔開
- minResizeWidth 和 minResizeHeight 屬性值指定應用 Widget 的絕對最小尺寸。
- 這些值指定 App Widget 尺寸小於該值將難以辨認或無法使用
- 使用這些屬性可以使用戶將 App Widget 的大小調整為小於 minWidth 和 minHeight 屬性定義的默認大小
- 這些屬性在Android 3.1中引入,計算規則如上所述。
minResizeHeight 屬性指定App Widget 可以調整到的最小高度(以dps為單位)
如果該字段值大於 minHeight 或未啟用垂直調整大小,則此字段無效
minResizeWidth 屬性指定App Widget可以調整到的最小寬度(以dps為單位)
如果該字段大於 minWidth 或未啟用水平調整大小,則此字段無效
有關調整應用程序小部件大小的更多信息,請參閱 應用程序小部件設計指南。
updatePeriodMillis 屬性定義 App Widget 更新頻率,單位為毫秒。
這個時間設定了 AppWidgetProvider 調用 onUpdate() 回調方法請求更新的頻率
該值不能保證實際更新操作會准時發生,建議在設定是不要太頻繁地進行更新(為了節省電池,每小時不要超過一次)
配置中的更新頻率可以允許用戶自行調整
需要注意的是:
- 如果App Widget在更新(如定義updatePeriodMillis)時設備處於睡眠狀態,則設備將喚醒以執行更新
- 如果更新頻率非常低,則不會對電池壽命造成重大問題
- 如果您需要頻繁地更新,可以配置在設備處於睡眠狀態時不需要更新,這個可以根據喚醒設備的警報來執行更新
- 為此,在AppWidgetProvider收到AlarmManager的Intent警報 ,將警報類型設置為ELAPSED_REALTIME或RTC
- 這樣在設備醒着時才會發出警報。然后設置updatePeriodMillis為零(“0”)
initialLayout 屬性定義 App Widget 布局的布局資源
configure 屬性定義了當用戶添加 App Widget 時啟動的
這個 Activity 是用來配置 App Widget 屬性的一個頁面(可選,詳情參閱創建應用程序小部件配置活動)
previewImage 屬性指定 App Widget 的預覽圖,在小部件選擇列表中展示
- 如果未提供,則用應用程序的啟動器圖標
- 該字段對應於文件中元素的 android:previewImage 屬性
- 有關使用的更多討論,請參見設置預覽圖像
widgetCategory 屬性聲明您的 App Widget 支持顯示的位置
可以是主屏幕(home_screen),鎖定屏幕(keyguard),或者同時顯示在兩者上
在低於 Android 5.0 的版本支持鎖定屏幕小部件,對於Android 5.0及更高版本,僅 home_screen 有效
關 <appwidget-provider> 元素接受的屬性的更多信息,請參考:AppWidgetProviderInfo類介紹
為了開發出更強大一點小部件,我們還需要進一步了解 RemoteViews 和 AppWidgetProvider。
•AppWidget的妝容——RemoteViews
下面簡單說說 RemoteViews 相關的幾個類。
RemoteViews
RemoteViews,從字面意思理解為它是一個遠程視圖。
是一種遠程的 View,它在其它進程中顯示,卻可以在另一個進程中更新。
RemoteViews 在 Android 中的使用場景主要有:自定義通知欄和桌面小部件。
在 RemoteViews 的構造函數中,第二個參數接收一個 layout 文件來確定 RemoteViews 的視圖;
然后,我們調用 RemoteViews 中的 set 方法對 layout 中的各個組件進行設置;
例如,可以調用 setTextViewText() 來設置 TextView 組件的文本。
前面提到,小部件布局文件可以添加的組件是有限制的;
它可以支持的 View 類型包括四種布局:
- FrameLayout
- LinearLayout
- RelativeLayout
- GridLayout
以及 13 種 View:
- AnalogClock
- Button
- Chronometer
- ImageButton
- ImageView
- ProgressBar
- TextView
- ViewFlipper
- ListView
- GridView
- StackView
- AdapterViewFlipper
- ViewSub
RemoteViews 提供了一系列 setXXX() 方法來為小部件的子視圖設置屬性。具體可以參考 API 文檔。
RemoteViewsService
RemoteViewsService,是管理RemoteViews的服務。
一般,當AppWidget 中包含 GridView、ListView、StackView 等集合視圖時;
才需要使用RemoteViewsService來進行更新、管理。
RemoteViewsService 更新集合視圖的一般步驟是:
- 通過 setRemoteAdapter() 方法來設置 RemoteViews 對應 RemoteViewsService 。
- 之后在 RemoteViewsService 中,實現 RemoteViewsFactory 接口。
- 然后,在 RemoteViewsFactory 接口中對集合視圖的各個子項進行設置,例如 ListView 中的每一Item。
RemoteViewsFactory
通過 RemoteViewsService 中的介紹,我們知道 RemoteViewsService 是通過 RemoteViewsFactory 來具體管理 layout 中集合視圖的;
RemoteViewsFactory 是 RemoteViewsService 中的一個內部接口。
RemoteViewsFactory 提供了一系列的方法管理集合視圖中的每一項,例如:
- RemoteViews getViewAt(int position)
- 通過getViewAt()來獲取“集合視圖”中的第position項的視圖,視圖是以RemoteViews的對象返回的。
- int getCount()
- 通過getCount()來獲取“集合視圖”中所有子項的總數。
•AppWidget的美貌——AppWidgetProvider
我們說一位女孩漂亮,除了因為她穿的衣服、化的妝漂亮以外,我想最主要的原因還是她本人長的漂亮吧。
同樣,小部件之所以有附着在桌面,跨進程更新 View 的能力,主要是因為 AppWidgetProvider 是一個廣播接收者。
我們發現,上面的例子中,Android Studio 幫我們自動生成的代碼中;
除了 onUpdate() 方法被我們重寫了,還有重寫 onEnable() 和 onDisable() 兩個方法;
但都是空實現,這兩個方法什么時候會被調用?
還有,我們說自定義的 NewAppWidget 繼承自 AppWidgetProvider,而 NewAppWidget 又是BroadCastReceiver 的子類;
而我們卻沒有向寫常規廣播接收者一樣重寫 onReceiver() 方法?
下面跟進去 AppWidgetProvider 源碼,一探究竟。
這個類代碼並不多,其實,AppWidgetProvider 除去構造方法外,總共只有下面這些方法:
- onEnable() : 當小部件第一次被添加到桌面時回調該方法,可添加多次,但只在第一次調用
- 對應廣播的 Action 為:ACTION_APPWIDGET_ENABLE
- onUpdate() : 當小部件被添加時或者每次小部件更新時都會調用一次該方法
- 配置文件中配置小部件的更新周期 updatePeriodMillis,每次更新都會調用
- 對應廣播 Action 為:ACTION_APPWIDGET_UPDATE 和 ACTION_APPWIDGET_RESTORED
- onDisabled() : 當最后一個該類型的小部件從桌面移除時調用
- 對應廣播的 Action 為:ACTION_APPWIDGET_DISABLED
- onDeleted() : 每刪除一個小部件就調用一次
- 對應廣播的 Action 為: ACTION_APPWIDGET_DELETED
- onRestored() : 當小部件從備份中還原,或者恢復設置的時候,會調用,實際用的比較少
- 對應廣播的 Action 為 ACTION_APPWIDGET_RESTORED。
- onAppWidgetOptionsChanged() : 當小部件布局發生更改的時候調用
- 對應廣播的 Action 為 ACTION_APPWIDGET_OPTIONS_CHANGED。
最后就是 onReceive() 方法了,AppWidgetProvider 重寫了該方法,用於分發具體的時間給上述的方法。
•聲明
參考資料: