如果本文幫助到你,本人不勝榮幸,如果浪費了你的時間,本人深感抱歉。 希望用最簡單的大白話來幫助那些像我一樣的人。如果有什麼錯誤,請一定指出,以免誤導大家、也誤導我。 本文來自:www.jianshu.com/u/320f9e8f7… 感謝您的關注。
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,程式碼只是實現。
最後放上專案地址: github.com/Wing-Li/Wid…