若是本文幫助到你,本人不勝榮幸,若是浪費了你的時間,本人深感抱歉。 但願用最簡單的大白話來幫助那些像我同樣的人。若是有什麼錯誤,請必定指出,以避免誤導你們、也誤導我。 本文來自:www.jianshu.com/u/320f9e8f7… 感謝您的關注。android
Android 桌面小部件是咱們常常看到的,好比時鐘、天氣、音樂播放器等等。 它可讓 App 的某些功能直接展現在桌面上,極大的增長了用戶的關注度。git
首先糾正一個誤區: 當 App 的小部件被放到了桌面以後,並不表明你的 App 就能夠一直在手機後臺運行了。該被殺,它仍是會被殺掉的。 因此若是你作小部件的目的是爲了讓程序常駐後臺,那麼你能夠死心了。github
可是!!! 雖然它仍是能被殺掉,可是用戶能看的見它了啊,用戶能夠點擊就打開咱們的 APP,因此仍是很不錯的。網絡
小部件能夠作什麼呢?也就是咱們須要實現什麼功能。app
這三個功能,大概就能知足咱們絕大部分需求了吧。ide
若是你歷來沒有作過桌面部件,那確定老是感受有點慌,無從下手,毫無邏輯。 因此,實現它到底須要什麼呢?工具
- 先聲明 Widget 的一些屬性。 在 res 新建 xml 文件夾,建立 appwidget-provider 標籤的 xml 文件。
- 建立桌面要顯示的佈局。 在 layout 建立 app_widget.xml。
- 而後來管理 Widget 狀態。 實現一個繼承 AppWidgetProvider 的類。
- 最後在 AndroidManifest.xml 裏,將 AppWidgetProvider類 和 xml屬性 註冊到一塊。
- 一般咱們會加一個 Service 來控制 Widget 的更新時間,後面再講爲何。
作完這些,若是不出錯,就完成了桌面部件。 其實挺簡單的,下面就讓咱們來看看具體的實現吧。佈局
先上效果圖:spa
在 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>
複製代碼
屬性的註釋在上面寫的很清楚了,這裏須要說兩點。
在 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
複製代碼
這裏代碼看起來可能有點多,先聽我講幾個邏輯,再來看代碼。
同一個小部件是能夠添加屢次的,因此更新控件的時候,要把全部的都更新。
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);
}
}
複製代碼
它傳了兩個值回來,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 就是承擔着一個這樣的角色。
而後再去理解代碼,是否是稍微好一點了?
說好的 當每隔 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秒發送一個廣播,廣播就是咱們本身定義的類型。
而後就只剩最後一步了,註冊相關信息
<!-- 聲明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…