如果本文幫助到你,本人不勝榮幸,如果浪費(fèi)了你的時(shí)間,本人深感抱歉。
希望用最簡(jiǎn)單的大白話來(lái)幫助那些像我一樣的人。如果有什么錯(cuò)誤,請(qǐng)一定指出,以免誤導(dǎo)大家、也誤導(dǎo)我。
本文來(lái)自:http://www.itdecent.cn/u/320f9e8f7fc9
感謝您的關(guān)注。
Android 桌面小部件是我們經(jīng)??吹降?,比如時(shí)鐘、天氣、音樂(lè)播放器等等。
它可以讓 App 的某些功能直接展示在桌面上,極大的增加了用戶(hù)的關(guān)注度。
首先糾正一個(gè)誤區(qū):
當(dāng) App 的小部件被放到了桌面之后,并不代表你的 App 就可以一直在手機(jī)后臺(tái)運(yùn)行了。該被殺,它還是會(huì)被殺掉的。
所以如果你做小部件的目的是為了讓程序常駐后臺(tái),那么你可以死心了。
但是?。?!
雖然它還是能被殺掉,但是用戶(hù)能看的見(jiàn)它了啊,用戶(hù)可以點(diǎn)擊就打開(kāi)我們的 APP,所以還是很不錯(cuò)的。
Android 桌面小部件可以做什么?
小部件可以做什么呢?也就是我們需要實(shí)現(xiàn)什么功能。
- 展示。每隔 N 秒/分鐘,刷新一次數(shù)據(jù);
- 交互。點(diǎn)擊操作 App 的數(shù)據(jù);
- 打開(kāi)App。打開(kāi)主頁(yè)或指定頁(yè)面。
這三個(gè)功能,大概就能滿(mǎn)足我們絕大部分需求了吧。
實(shí)現(xiàn)桌面小部件需要什么?
如果你從來(lái)沒(méi)有做過(guò)桌面部件,那肯定總是感覺(jué)有點(diǎn)慌,無(wú)從下手,毫無(wú)邏輯。
所以,實(shí)現(xiàn)它到底需要什么呢?
- 先聲明 Widget 的一些屬性。在 res 新建 xml 文件夾,創(chuàng)建 appwidget-provider 標(biāo)簽的 xml 文件。
- 創(chuàng)建桌面要顯示的布局。 在 layout 創(chuàng)建 app_widget.xml。
- 然后來(lái)管理 Widget 狀態(tài)。實(shí)現(xiàn)一個(gè)繼承 AppWidgetProvider 的類(lèi)。
- 最后在 AndroidManifest.xml 里,將 AppWidgetProvider類(lèi) 和 xml屬性 注冊(cè)到一塊。
- 通常我們會(huì)加一個(gè) Service 來(lái)控制 Widget 的更新時(shí)間,后面再講為什么。
做完這些,如果不出錯(cuò),就完成了桌面部件。
其實(shí)挺簡(jiǎn)單的,下面就讓我們來(lái)看看具體的實(shí)現(xiàn)吧。
實(shí)現(xiàn)一個(gè)桌面計(jì)數(shù)器
先上效果圖:

1. 聲明 Widget 的屬性
在 res 新建 xml 文件夾,創(chuàng)建一個(gè) app_widget.xml 的文件。
如果 res 下沒(méi)有 xml 文件,則先創(chuàng)建。
app_widget.xml 內(nèi)容如下:
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/app_widget"
android:minHeight="110dp"
android:minWidth="110dp"
android:previewImage="@mipmap/ic_launcher"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen|keyguard">
<!--
android:minWidth : 最小寬度
android:minHeight : 最小高度
android:updatePeriodMillis : 更新widget的時(shí)間間隔(ms),"86400000"為1個(gè)小時(shí),值小于30分鐘時(shí),會(huì)被設(shè)置為30分鐘??梢杂?service、AlarmManager、Timer 控制。
android:previewImage : 預(yù)覽圖片,拖動(dòng)小部件到桌面時(shí)有個(gè)預(yù)覽圖
android:initialLayout : 加載到桌面時(shí)對(duì)應(yīng)的布局文件
android:resizeMode : 拉伸的方向。horizontal表示可以水平拉伸,vertical表示可以豎直拉伸
android:widgetCategory : 被顯示的位置。home_screen:將widget添加到桌面,keyguard:widget可以被添加到鎖屏界面。
android:initialKeyguardLayout : 加載到鎖屏界面時(shí)對(duì)應(yīng)的布局文件
-->
</appwidget-provider>
屬性的注釋在上面寫(xiě)的很清楚了,這里需要說(shuō)兩點(diǎn)。
- 關(guān)于寬度和高度的數(shù)值定義是很有講究的,在桌面其實(shí)是按照“格子”排列的。
看 Google 給的圖。上面我們代碼定義 110dp 也就是說(shuō),它占了2*2的空間。

- 第二點(diǎn)很重要。有個(gè) updatePeriodMillis 屬性,更新widget的時(shí)間間隔(ms)。
官方給提供了小部件的自動(dòng)更新時(shí)間,但是卻給了限制,你更新的時(shí)間必須大于30分鐘,如果小于30分鐘,那默認(rèn)就是30分鐘。
可以我們就是要5分鐘更新啊,怎么辦呢?
所以就不能使用這個(gè)默認(rèn)更新,我們要自己來(lái)通過(guò)發(fā)送廣播控制更新時(shí)間,也就是一開(kāi)始總步驟里面第4步,加一個(gè) Service 來(lái)控制 Widget 的更新時(shí)間,這個(gè)在最后一步添加。
2. 創(chuàng)建布局文件
在 layout 創(chuàng)建 app_widget.xml 文件。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<TextView
android:id="@+id/widget_txt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textSize="36sp"
android:textStyle="bold"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/widget_btn_reset"
style="@style/Widget.AppCompat.Toolbar.Button.Navigation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="恢復(fù)"/>
<Button
android:id="@+id/widget_btn_open"
style="@style/Widget.AppCompat.Toolbar.Button.Navigation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:text="打開(kāi)頁(yè)面"/>
</LinearLayout>
</LinearLayout>
這里要注意的就是 桌面部件并不支持 Android 所有的控件。
支持的控件如下:
App Widget支持的布局:
FrameLayout
LinearLayout
RelativeLayout
GridLayout
App Widget支持的控件:
AnalogClock
Button
Chronometer
ImageButton
ImageView
ProgressBar
TextView
ViewFlipper
ListView
GridView
StackView
AdapterViewFlipper
3. 管理 Widget 狀態(tài)
這里代碼看起來(lái)可能有點(diǎn)多,先聽(tīng)我講幾個(gè)邏輯,再來(lái)看代碼。
- Android 的各種東西都有自己的生命周期,Widget 也不例外,它有幾個(gè)方法來(lái)管理自己的生命周期。

同一個(gè)小部件是可以添加多次的,所以更新控件的時(shí)候,要把所有的都更新。
onReceive() 用來(lái)接收廣播,它并不在生命周期里。但是,其實(shí) onReceive() 是掌控生命周期的。
如下是 onReceive() 父類(lèi)的源碼,右邊是每個(gè)廣播對(duì)應(yīng)的方法。
上面我畫(huà)的生命周期的圖,也比較清楚。

然后我們?cè)賮?lái)看代碼。
新建一個(gè) WidgetProvider 類(lèi),繼承 AppWidgetProvider。
主要邏輯在 onReceive() 里,其他的都是生命周期切換時(shí),所處理的事情。
我們?cè)谙旅娣治?onReceive()。
public class WidgetProvider extends AppWidgetProvider {
// 更新 widget 的廣播對(duì)應(yīng)的action
private final String ACTION_UPDATE_ALL = "com.lyl.widget.UPDATE_ALL";
// 保存 widget 的id的HashSet,每新建一個(gè) widget 都會(huì)為該 widget 分配一個(gè) id。
private static Set idsSet = new HashSet();
public static int mIndex;
/**
* 接收窗口小部件點(diǎn)擊時(shí)發(fā)送的廣播
*/
@Override
public void onReceive(final Context context, Intent intent) {
super.onReceive(context, intent);
final String action = intent.getAction();
if (ACTION_UPDATE_ALL.equals(action)) {
// “更新”廣播
updateAllAppWidgets(context, AppWidgetManager.getInstance(context), idsSet);
} else if (intent.hasCategory(Intent.CATEGORY_ALTERNATIVE)) {
// “按鈕點(diǎn)擊”廣播
mIndex = 0;
updateAllAppWidgets(context, AppWidgetManager.getInstance(context), idsSet);
}
}
// 更新所有的 widget
private void updateAllAppWidgets(Context context, AppWidgetManager appWidgetManager, Set set) {
// widget 的id
int appID;
// 迭代器,用于遍歷所有保存的widget的id
Iterator it = set.iterator();
// 要顯示的那個(gè)數(shù)字,每更新一次 + 1
mIndex++; // TODO:可以在這里做更多的邏輯操作,比如:數(shù)據(jù)處理、網(wǎng)絡(luò)請(qǐng)求等。然后去顯示數(shù)據(jù)
while (it.hasNext()) {
appID = ((Integer) it.next()).intValue();
// 獲取 example_appwidget.xml 對(duì)應(yīng)的RemoteViews
RemoteViews remoteView = new RemoteViews(context.getPackageName(), R.layout.app_widget);
// 設(shè)置顯示數(shù)字
remoteView.setTextViewText(R.id.widget_txt, String.valueOf(mIndex));
// 設(shè)置點(diǎn)擊按鈕對(duì)應(yīng)的PendingIntent:即點(diǎn)擊按鈕時(shí),發(fā)送廣播。
remoteView.setOnClickPendingIntent(R.id.widget_btn_reset, getResetPendingIntent(context));
remoteView.setOnClickPendingIntent(R.id.widget_btn_open, getOpenPendingIntent(context));
// 更新 widget
appWidgetManager.updateAppWidget(appID, remoteView);
}
}
/**
* 獲取 重置數(shù)字的廣播
*/
private PendingIntent getResetPendingIntent(Context context) {
Intent intent = new Intent();
intent.setClass(context, WidgetProvider.class);
intent.addCategory(Intent.CATEGORY_ALTERNATIVE);
PendingIntent pi = PendingIntent.getBroadcast(context, 0, intent, 0);
return pi;
}
/**
* 獲取 打開(kāi) MainActivity 的 PendingIntent
*/
private PendingIntent getOpenPendingIntent(Context context) {
Intent intent = new Intent();
intent.setClass(context, MainActivity.class);
intent.putExtra("main", "這句話是我從桌面點(diǎn)開(kāi)傳過(guò)去的。");
PendingIntent pi = PendingIntent.getActivity(context, 0, intent, 0);
return pi;
}
/**
* 當(dāng)該窗口小部件第一次添加到桌面時(shí)調(diào)用該方法,可添加多次但只第一次調(diào)用
*/
@Override
public void onEnabled(Context context) {
// 在第一個(gè) widget 被創(chuàng)建時(shí),開(kāi)啟服務(wù)
Intent intent = new Intent(context, WidgetService.class);
context.startService(intent);
Toast.makeText(context, "開(kāi)始計(jì)數(shù)", Toast.LENGTH_SHORT).show();
super.onEnabled(context);
}
// 當(dāng) widget 被初次添加 或者 當(dāng) widget 的大小被改變時(shí),被調(diào)用
@Override
public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle
newOptions) {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions);
}
/**
* 當(dāng)小部件從備份恢復(fù)時(shí)調(diào)用該方法
*/
@Override
public void onRestored(Context context, int[] oldWidgetIds, int[] newWidgetIds) {
super.onRestored(context, oldWidgetIds, newWidgetIds);
}
/**
* 每次窗口小部件被點(diǎn)擊更新都調(diào)用一次該方法
*/
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);
// 每次 widget 被創(chuàng)建時(shí),對(duì)應(yīng)的將widget的id添加到set中
for (int appWidgetId : appWidgetIds) {
idsSet.add(Integer.valueOf(appWidgetId));
}
}
/**
* 每刪除一次窗口小部件就調(diào)用一次
*/
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
// 當(dāng) widget 被刪除時(shí),對(duì)應(yīng)的刪除set中保存的widget的id
for (int appWidgetId : appWidgetIds) {
idsSet.remove(Integer.valueOf(appWidgetId));
}
super.onDeleted(context, appWidgetIds);
}
/**
* 當(dāng)最后一個(gè)該窗口小部件刪除時(shí)調(diào)用該方法,注意是最后一個(gè)
*/
@Override
public void onDisabled(Context context) {
// 在最后一個(gè) widget 被刪除時(shí),終止服務(wù)
Intent intent = new Intent(context, WidgetService.class);
context.stopService(intent);
super.onDisabled(context);
}
}
onReceive(Context context, Intent intent)
它傳了兩個(gè)值回來(lái),Context 是跳轉(zhuǎn)、發(fā)廣播用的。
我們用來(lái)判斷的是 Intent ,這里用到了 Intent 的兩種方式。
Intent 作為信息傳遞者。
它要把信息傳給誰(shuí),可以有三個(gè)匹配依據(jù):一個(gè)是action,一個(gè)是category,一個(gè)是data。
String ACTION_UPDATE_ALL = "com.lyl.widget.UPDATE_ALL";
這個(gè)最后會(huì)在 AndroidManifest.xml 里面注冊(cè)時(shí)寫(xiě)進(jìn)去。
當(dāng)每隔 N 秒/分鐘,就發(fā)送一次這個(gè)廣播,更新所有UI。
intent.hasCategory(Intent.CATEGORY_ALTERNATIVE)
是廣播事件里攜帶的 Intent 里設(shè)置的,用來(lái)匹配。
點(diǎn)擊“恢復(fù)”按鈕,計(jì)數(shù)器清零。
然后是 updateAllAppWidgets() 這個(gè)方法,更新 UI。
更新 UI 用到了一個(gè)新東西——RemoteViews。
怎么來(lái)理解 RemoteViews 呢?
因?yàn)?,桌面部件并不像平常布局直接展示,它需要通過(guò)某種服務(wù)去更新UI。但是我們的App怎么能去控制桌面上的布局呢?
所以就需要有一個(gè)中間人,類(lèi)似傳遞者。
我告訴傳遞者,你讓他把我的 R.id.widget_txt ,更新成 “hello world”。
你讓他把我的 R.id.widget_btn_open 按鈕點(diǎn)擊之后去響應(yīng) PendingIntent 這件事。
RemoteViews 就是承擔(dān)著一個(gè)這樣的角色。
然后再去理解代碼,是不是稍微好一點(diǎn)了?
4. 最后就是 Service 控制 Widget 的更新時(shí)間
說(shuō)好的 當(dāng)每隔 N 秒/分鐘,就發(fā)送一次這個(gè)廣播。
那到底在哪發(fā)呢?也就是我們剛開(kāi)始說(shuō)的,用 Service 來(lái)控制時(shí)間。
新建一個(gè) WidgetService 類(lèi),繼承 Service。代碼如下:
/**
* 控制 桌面小部件 更新
* Created by lyl on 2017/8/23.
*/
public class WidgetService extends Service {
// 更新 widget 的廣播對(duì)應(yīng)的 action
private final String ACTION_UPDATE_ALL = "com.lyl.widget.UPDATE_ALL";
// 周期性更新 widget 的周期
private static final int UPDATE_TIME = 1000;
private Timer mTimer;
private TimerTask mTimerTask;
@Override
public void onCreate() {
super.onCreate();
// 每經(jīng)過(guò)指定時(shí)間,發(fā)送一次廣播
mTimer = new Timer();
mTimerTask = new TimerTask() {
@Override
public void run() {
Intent updateIntent = new Intent(ACTION_UPDATE_ALL);
sendBroadcast(updateIntent);
}
};
mTimer.schedule(mTimerTask, 1000, UPDATE_TIME);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onDestroy() {
super.onDestroy();
mTimerTask.cancel();
mTimer.cancel();
}
/*
* 服務(wù)開(kāi)始時(shí),即調(diào)用startService()時(shí),onStartCommand()被執(zhí)行。
*
* 這個(gè)整形可以有四個(gè)返回值:start_sticky、start_no_sticky、START_REDELIVER_INTENT、START_STICKY_COMPATIBILITY。
* 它們的含義分別是:
* 1):START_STICKY:如果service進(jìn)程被kill掉,保留service的狀態(tài)為開(kāi)始狀態(tài),但不保留遞送的intent對(duì)象。隨后系統(tǒng)會(huì)嘗試重新創(chuàng)建service,
* 由于服務(wù)狀態(tài)為開(kāi)始狀態(tài),所以創(chuàng)建服務(wù)后一定會(huì)調(diào)用onStartCommand(Intent,int,int)方法。如果在此期間沒(méi)有任何啟動(dòng)命令被傳遞到service,那么參數(shù)Intent將為null;
* 2):START_NOT_STICKY:“非粘性的”。使用這個(gè)返回值時(shí),如果在執(zhí)行完onStartCommand后,服務(wù)被異常kill掉,系統(tǒng)不會(huì)自動(dòng)重啟該服務(wù);
* 3):START_REDELIVER_INTENT:重傳Intent。使用這個(gè)返回值時(shí),如果在執(zhí)行完onStartCommand后,服務(wù)被異常kill掉,系統(tǒng)會(huì)自動(dòng)重啟該服務(wù),并將Intent的值傳入;
* 4):START_STICKY_COMPATIBILITY:START_STICKY的兼容版本,但不保證服務(wù)被kill后一定能重啟。
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
super.onStartCommand(intent, flags, startId);
return START_STICKY;
}
}
在 onCreate 開(kāi)啟一個(gè)計(jì)時(shí)線程,每1秒發(fā)送一個(gè)廣播,廣播就是我們自己定義的類(lèi)型。
5. 在 AndroidManifest.xml 注冊(cè) 桌面部件 和 服務(wù)
然后就只剩最后一步了,注冊(cè)相關(guān)信息
<!-- 聲明widget對(duì)應(yīng)的AppWidgetProvider -->
<receiver android:name=".WidgetProvider">
<intent-filter>
<!--這個(gè)是必須要有的系統(tǒng)規(guī)定-->
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
<!--這個(gè)是我們自定義的 action ,用來(lái)更新UI,還可以自由添加更多 -->
<action android:name="com.lyl.widget.UPDATE_ALL"/>
</intent-filter>
<!--要顯示的布局-->
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/app_widget"/>
</receiver>
<!-- 用來(lái)計(jì)時(shí),發(fā)送 通知桌面部件更新 -->
<service android:name=".WidgetService" >
<intent-filter>
<!--用來(lái)啟動(dòng)服務(wù)-->
<action android:name="android.appwidget.action.APP_WIDGET_SERVICE" />
</intent-filter>
</service>
相應(yīng)的注釋都在上面,如果我們的App進(jìn)程被殺掉,服務(wù)也被關(guān)掉,那就沒(méi)辦法更新UI了。
也可以再創(chuàng)建一個(gè) BroadcastReceiver 監(jiān)聽(tīng)系統(tǒng)的各種動(dòng)態(tài),來(lái)喚醒我們的通知服務(wù),這就屬于進(jìn)程?;盍恕?/p>
至此,以上代碼寫(xiě)完,如果不出問(wèn)題,運(yùn)行之后直接去桌面看小工具,我們的App就在里面了,可以添加到桌面。
對(duì)于需要定時(shí)更新的桌面部件,保證自己的服務(wù)在后臺(tái)運(yùn)行也是一件比較重要的事情。
這個(gè)我們還是可以好好做一下,畢竟用戶(hù)都已經(jīng)愿意把我們的程序放到桌面上,所以只要友好的引導(dǎo)用戶(hù)給你一定的權(quán)限,存活概率還是很大。
再不濟(jì),讓用戶(hù)主動(dòng)點(diǎn)開(kāi)App,也不失為一種辦法。
好的創(chuàng)意才能造就好的App,代碼只是實(shí)現(xiàn)。
最后放上項(xiàng)目地址:
https://github.com/Wing-Li/Widget