如果本文幫助到你,本人不勝榮幸,如果浪費了你的時間,本人深感抱歉。
希望用最簡單的大白話來幫助那些像我一樣的人。如果有什么錯誤,請一定指出,以免誤導大家、也誤導我。
本文來自:http://www.lxweimin.com/u/320f9e8f7fc9
感謝您的關注。
Android 桌面小部件是我們經常看到的,比如時鐘、天氣、音樂播放器等等。
它可以讓 App 的某些功能直接展示在桌面上,極大的增加了用戶的關注度。
首先糾正一個誤區:
當 App 的小部件被放到了桌面之后,并不代表你的 App 就可以一直在手機后臺運行了。該被殺,它還是會被殺掉的。
所以如果你做小部件的目的是為了讓程序常駐后臺,那么你可以死心了。
但是!!!
雖然它還是能被殺掉,但是用戶能看的見它了啊,用戶可以點擊就打開我們的 APP,所以還是很不錯的。
Android 桌面小部件可以做什么?
小部件可以做什么呢?也就是我們需要實現什么功能。
- 展示。每隔 N 秒/分鐘,刷新一次數據;
- 交互。點擊操作 App 的數據;
- 打開App。打開主頁或指定頁面。
這三個功能,大概就能滿足我們絕大部分需求了吧。
實現桌面小部件需要什么?
如果你從來沒有做過桌面部件,那肯定總是感覺有點慌,無從下手,毫無邏輯。
所以,實現它到底需要什么呢?
- 先聲明 Widget 的一些屬性。在 res 新建 xml 文件夾,創建 appwidget-provider 標簽的 xml 文件。
- 創建桌面要顯示的布局。 在 layout 創建 app_widget.xml。
- 然后來管理 Widget 狀態。實現一個繼承 AppWidgetProvider 的類。
- 最后在 AndroidManifest.xml 里,將 AppWidgetProvider類 和 xml屬性 注冊到一塊。
- 通常我們會加一個 Service 來控制 Widget 的更新時間,后面再講為什么。
做完這些,如果不出錯,就完成了桌面部件。
其實挺簡單的,下面就讓我們來看看具體的實現吧。
實現一個桌面計數器
先上效果圖:
1. 聲明 Widget 的屬性
在 res 新建 xml 文件夾,創建一個 app_widget.xml 的文件。
如果 res 下沒有 xml 文件,則先創建。
app_widget.xml 內容如下:
<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的時間間隔(ms),"86400000"為1個小時,值小于30分鐘時,會被設置為30分鐘。可以用 service、AlarmManager、Timer 控制。
android:previewImage : 預覽圖片,拖動小部件到桌面時有個預覽圖
android:initialLayout : 加載到桌面時對應的布局文件
android:resizeMode : 拉伸的方向。horizontal表示可以水平拉伸,vertical表示可以豎直拉伸
android:widgetCategory : 被顯示的位置。home_screen:將widget添加到桌面,keyguard:widget可以被添加到鎖屏界面。
android:initialKeyguardLayout : 加載到鎖屏界面時對應的布局文件
-->
</appwidget-provider>
屬性的注釋在上面寫的很清楚了,這里需要說兩點。
- 關于寬度和高度的數值定義是很有講究的,在桌面其實是按照“格子”排列的。
看 Google 給的圖。上面我們代碼定義 110dp 也就是說,它占了2*2的空間。
- 第二點很重要。有個 updatePeriodMillis 屬性,更新widget的時間間隔(ms)。
官方給提供了小部件的自動更新時間,但是卻給了限制,你更新的時間必須大于30分鐘,如果小于30分鐘,那默認就是30分鐘。
可以我們就是要5分鐘更新啊,怎么辦呢?
所以就不能使用這個默認更新,我們要自己來通過發送廣播控制更新時間,也就是一開始總步驟里面第4步,加一個 Service 來控制 Widget 的更新時間,這個在最后一步添加。
2. 創建布局文件
在 layout 創建 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="恢復"/>
<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="打開頁面"/>
</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 狀態
這里代碼看起來可能有點多,先聽我講幾個邏輯,再來看代碼。
- Android 的各種東西都有自己的生命周期,Widget 也不例外,它有幾個方法來管理自己的生命周期。
同一個小部件是可以添加多次的,所以更新控件的時候,要把所有的都更新。
onReceive() 用來接收廣播,它并不在生命周期里。但是,其實 onReceive() 是掌控生命周期的。
如下是 onReceive() 父類的源碼,右邊是每個廣播對應的方法。
上面我畫的生命周期的圖,也比較清楚。
然后我們再來看代碼。
新建一個 WidgetProvider 類,繼承 AppWidgetProvider。
主要邏輯在 onReceive() 里,其他的都是生命周期切換時,所處理的事情。
我們在下面分析 onReceive()。
public class WidgetProvider extends AppWidgetProvider {
// 更新 widget 的廣播對應的action
private final String ACTION_UPDATE_ALL = "com.lyl.widget.UPDATE_ALL";
// 保存 widget 的id的HashSet,每新建一個 widget 都會為該 widget 分配一個 id。
private static Set idsSet = new HashSet();
public static int mIndex;
/**
* 接收窗口小部件點擊時發送的廣播
*/
@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)) {
// “按鈕點擊”廣播
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();
// 要顯示的那個數字,每更新一次 + 1
mIndex++; // TODO:可以在這里做更多的邏輯操作,比如:數據處理、網絡請求等。然后去顯示數據
while (it.hasNext()) {
appID = ((Integer) it.next()).intValue();
// 獲取 example_appwidget.xml 對應的RemoteViews
RemoteViews remoteView = new RemoteViews(context.getPackageName(), R.layout.app_widget);
// 設置顯示數字
remoteView.setTextViewText(R.id.widget_txt, String.valueOf(mIndex));
// 設置點擊按鈕對應的PendingIntent:即點擊按鈕時,發送廣播。
remoteView.setOnClickPendingIntent(R.id.widget_btn_reset, getResetPendingIntent(context));
remoteView.setOnClickPendingIntent(R.id.widget_btn_open, getOpenPendingIntent(context));
// 更新 widget
appWidgetManager.updateAppWidget(appID, remoteView);
}
}
/**
* 獲取 重置數字的廣播
*/
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;
}
/**
* 獲取 打開 MainActivity 的 PendingIntent
*/
private PendingIntent getOpenPendingIntent(Context context) {
Intent intent = new Intent();
intent.setClass(context, MainActivity.class);
intent.putExtra("main", "這句話是我從桌面點開傳過去的。");
PendingIntent pi = PendingIntent.getActivity(context, 0, intent, 0);
return pi;
}
/**
* 當該窗口小部件第一次添加到桌面時調用該方法,可添加多次但只第一次調用
*/
@Override
public void onEnabled(Context context) {
// 在第一個 widget 被創建時,開啟服務
Intent intent = new Intent(context, WidgetService.class);
context.startService(intent);
Toast.makeText(context, "開始計數", Toast.LENGTH_SHORT).show();
super.onEnabled(context);
}
// 當 widget 被初次添加 或者 當 widget 的大小被改變時,被調用
@Override
public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle
newOptions) {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions);
}
/**
* 當小部件從備份恢復時調用該方法
*/
@Override
public void onRestored(Context context, int[] oldWidgetIds, int[] newWidgetIds) {
super.onRestored(context, oldWidgetIds, newWidgetIds);
}
/**
* 每次窗口小部件被點擊更新都調用一次該方法
*/
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);
// 每次 widget 被創建時,對應的將widget的id添加到set中
for (int appWidgetId : appWidgetIds) {
idsSet.add(Integer.valueOf(appWidgetId));
}
}
/**
* 每刪除一次窗口小部件就調用一次
*/
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
// 當 widget 被刪除時,對應的刪除set中保存的widget的id
for (int appWidgetId : appWidgetIds) {
idsSet.remove(Integer.valueOf(appWidgetId));
}
super.onDeleted(context, appWidgetIds);
}
/**
* 當最后一個該窗口小部件刪除時調用該方法,注意是最后一個
*/
@Override
public void onDisabled(Context context) {
// 在最后一個 widget 被刪除時,終止服務
Intent intent = new Intent(context, WidgetService.class);
context.stopService(intent);
super.onDisabled(context);
}
}
onReceive(Context context, Intent intent)
它傳了兩個值回來,Context 是跳轉、發廣播用的。
我們用來判斷的是 Intent ,這里用到了 Intent 的兩種方式。
Intent 作為信息傳遞者。
它要把信息傳給誰,可以有三個匹配依據:一個是action,一個是category,一個是data。
String ACTION_UPDATE_ALL = "com.lyl.widget.UPDATE_ALL";
這個最后會在 AndroidManifest.xml 里面注冊時寫進去。
當每隔 N 秒/分鐘,就發送一次這個廣播,更新所有UI。
intent.hasCategory(Intent.CATEGORY_ALTERNATIVE)
是廣播事件里攜帶的 Intent 里設置的,用來匹配。
點擊“恢復”按鈕,計數器清零。
然后是 updateAllAppWidgets() 這個方法,更新 UI。
更新 UI 用到了一個新東西——RemoteViews。
怎么來理解 RemoteViews 呢?
因為,桌面部件并不像平常布局直接展示,它需要通過某種服務去更新UI。但是我們的App怎么能去控制桌面上的布局呢?
所以就需要有一個中間人,類似傳遞者。
我告訴傳遞者,你讓他把我的 R.id.widget_txt ,更新成 “hello world”。
你讓他把我的 R.id.widget_btn_open 按鈕點擊之后去響應 PendingIntent 這件事。
RemoteViews 就是承擔著一個這樣的角色。
然后再去理解代碼,是不是稍微好一點了?
4. 最后就是 Service 控制 Widget 的更新時間
說好的 當每隔 N 秒/分鐘,就發送一次這個廣播。
那到底在哪發呢?也就是我們剛開始說的,用 Service 來控制時間。
新建一個 WidgetService 類,繼承 Service。代碼如下:
/**
* 控制 桌面小部件 更新
* Created by lyl on 2017/8/23.
*/
public class WidgetService extends Service {
// 更新 widget 的廣播對應的 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();
// 每經過指定時間,發送一次廣播
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();
}
/*
* 服務開始時,即調用startService()時,onStartCommand()被執行。
*
* 這個整形可以有四個返回值:start_sticky、start_no_sticky、START_REDELIVER_INTENT、START_STICKY_COMPATIBILITY。
* 它們的含義分別是:
* 1):START_STICKY:如果service進程被kill掉,保留service的狀態為開始狀態,但不保留遞送的intent對象。隨后系統會嘗試重新創建service,
* 由于服務狀態為開始狀態,所以創建服務后一定會調用onStartCommand(Intent,int,int)方法。如果在此期間沒有任何啟動命令被傳遞到service,那么參數Intent將為null;
* 2):START_NOT_STICKY:“非粘性的”。使用這個返回值時,如果在執行完onStartCommand后,服務被異常kill掉,系統不會自動重啟該服務;
* 3):START_REDELIVER_INTENT:重傳Intent。使用這個返回值時,如果在執行完onStartCommand后,服務被異常kill掉,系統會自動重啟該服務,并將Intent的值傳入;
* 4):START_STICKY_COMPATIBILITY:START_STICKY的兼容版本,但不保證服務被kill后一定能重啟。
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
super.onStartCommand(intent, flags, startId);
return START_STICKY;
}
}
在 onCreate 開啟一個計時線程,每1秒發送一個廣播,廣播就是我們自己定義的類型。
5. 在 AndroidManifest.xml 注冊 桌面部件 和 服務
然后就只剩最后一步了,注冊相關信息
<!-- 聲明widget對應的AppWidgetProvider -->
<receiver android:name=".WidgetProvider">
<intent-filter>
<!--這個是必須要有的系統規定-->
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
<!--這個是我們自定義的 action ,用來更新UI,還可以自由添加更多 -->
<action android:name="com.lyl.widget.UPDATE_ALL"/>
</intent-filter>
<!--要顯示的布局-->
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/app_widget"/>
</receiver>
<!-- 用來計時,發送 通知桌面部件更新 -->
<service android:name=".WidgetService" >
<intent-filter>
<!--用來啟動服務-->
<action android:name="android.appwidget.action.APP_WIDGET_SERVICE" />
</intent-filter>
</service>
相應的注釋都在上面,如果我們的App進程被殺掉,服務也被關掉,那就沒辦法更新UI了。
也可以再創建一個 BroadcastReceiver 監聽系統的各種動態,來喚醒我們的通知服務,這就屬于進程保活了。
至此,以上代碼寫完,如果不出問題,運行之后直接去桌面看小工具,我們的App就在里面了,可以添加到桌面。
對于需要定時更新的桌面部件,保證自己的服務在后臺運行也是一件比較重要的事情。
這個我們還是可以好好做一下,畢竟用戶都已經愿意把我們的程序放到桌面上,所以只要友好的引導用戶給你一定的權限,存活概率還是很大。
再不濟,讓用戶主動點開App,也不失為一種辦法。
好的創意才能造就好的App,代碼只是實現。
最后放上項目地址:
https://github.com/Wing-Li/Widget