## 前言 此次會先以首次碰Widget的觀點寫, 目標是做出一個即時看板,每隔固定時間可以刷新API,更新最新的資料並顯示。 ## 如何創建Widget? ### 方法1. 直接用原生自動生成 1. 直接在要創的資料夾右鍵 New -> 找widget ,就會出現下面的畫面 ![image](https://hackmd.io/_uploads/HJ5uhJ726.png) 2. 再來說說這些在講啥 - Placement:選擇的是這個widget是在主螢幕還是鎖定螢幕上出現(鎖定螢幕有限制版本5以下才能用XD),所以基本上就是Homescreen 對應xml的 widgetCategory - Resizable:就是放到桌面上後,他可以怎麼拉,垂直or水平or都可or不給拉 對應xml的 resizeMode (如果要Both 就是寫`horizontal|vertical`) - 剩下就是設定最小寬高,每個cells 就是一格,直接用螢幕比對就行 - Configuration Screen:這個是讓他在放小工具時,會自動連到你設定的Activity,然後可以直接吃設定 EX:讓他顯示特定文字阿,或是自己的設定檔。 3. 右鍵創建的缺點就是... 他會爆改專案設定檔,然後各種建立資源,如果是已經有再用的專案,就要用git去看被改了那些,有點麻煩XD~ 4. 好處就是按Finish後直接build就能看到Sample XD 一秒完成 ### 方法2. 自己創建 1. 要自己創就要做這些步驟XD ![image](https://hackmd.io/_uploads/BJij0173a.png) 2. 步驟摘要: 1. 創建布局:手動在res/layout下創建Widget布局文件。 2. 實現AppWidgetProvider:創建一個繼承自AppWidgetProvider的類。 3. 配置文件:在res/xml下創建Widget的配置XML文件。 4. 更新Manifest:在AndroidManifest.xml中添加`<receiver>`標籤以聲明您的Widget提供者。 5. 如果有要做Configure還要再另外加相關設定。 6. 測試Widget:確保在不同條件下測試Widget的行為。 3. 優點就專案不會被調整、也比較知道自己在幹嘛,缺點當然就是要自己一項一項查拉~ 4. 配遺漏或沒想法,就請開個空白分支,然後先用右鍵自動創建,看一下需要參考的地方,然後就可以砍掉分支,或是直接Rollback ## 不自動生成的創建方式 QuickStart 當作練習,也想說讓自己熟一點Widget,所以當然要自己來呀~ 不過在創建上會直接先用最陽春版本,也就是不自訂Configure~ `以下步驟只是最一開始生成而已,詳細邏輯請看後面` ### Step.1 創建布局:手動在res/layout下創建Widget布局文件。 1. 基本上就跟正常創Layout一樣,只是他有限定只有一些View可以用(詳細看最後一點)。 2. 常見的ConstraintLayout、View、RecycleView都不能用~ 4. 如果你有用到不能使用的View,IDE通常會紅字提示你(在放R.layout.xxx檔案那邊),在看是什麼View不能用換掉就好 5. 如果你用自動創建,會發現他有跑出一些Style、Theme,但實際上其實都不用用到也沒差XD,直接background寫好圓角就行 6. 截至目前(最後更新於2023年4月),RemoteViews支持以下視圖(GPT提供): ``` FrameLayout LinearLayout RelativeLayout GridLayout TextView ImageView Button ImageButton ProgressBar SeekBar AnalogClock Chronometer StackView AdapterViewFlipper ListView GridView ViewFlipper ``` ### Step.2 實現AppWidgetProvider:創建一個繼承自AppWidgetProvider的類。 ```kotlin= /** * Implementation of App Widget functionality. * App Widget Configuration implemented in [RealtimeCaseDashBoardAppWidgetConfigureActivity] */ class RealtimeCaseDashBoardAppWidget : AppWidgetProvider() { /** * 如果是使次建立,並不一定會呼叫到onUpdate唷~ * 另外因為小工具使用者可以創建多個,因此會需要每個更新都要處理 * - 這邊主要就是自己呼叫 or 系統給定的時間更新(預設是24hr) */ override fun onUpdate( context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray ) { // There may be multiple widgets active, so update all of them for (appWidgetId in appWidgetIds) { //更新Widget~ updateAppWidget(context, appWidgetManager, appWidgetId) } } // 每個Widget被移除時觸發 override fun onDeleted(context: Context, appWidgetIds: IntArray) { // When the user deletes the widget, delete the preference associated with it. for (appWidgetId in appWidgetIds) { deleteTitlePref(context, appWidgetId) } } // 當第一個Widget被放出來時觸發 override fun onEnabled(context: Context) { // Enter relevant functionality for when the first widget is created } // 當最後一個Widget被移除時觸發 override fun onDisabled(context: Context) { // Enter relevant functionality for when the last widget is disabled } } internal fun updateAppWidget( context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int ) { val widgetText = loadTitlePref(context, appWidgetId) // Construct the RemoteViews object val views = RemoteViews(context.packageName, R.layout.realtime_case_dash_board_app_widget) views.setTextViewText(R.id.appwidget_text, widgetText) // Instruct the widget manager to update the widget appWidgetManager.updateAppWidget(appWidgetId, views) } ``` ### Step.3 配置文件:在res/xml下創建Widget的配置XML文件。 1. 設定檔範例如下,詳細細節可以到[官網](https://developer.android.com/develop/ui/views/appwidgets?hl=zh-tw)查,部分欄位說明 - configure 可選:有要讓每次小工具放到桌面時,跑出設定頁的話,要把Activity的位置放上去,沒有就不用設定 - description:小工具的描述,但有些版本或手機不會跑出來 - resizeMode:是否可以水平/垂直拉,會被最小值擋住。 - updatePeriodMillis:每隔多久會觸發widget的onUpdate,最低限制是3600000毫秒(1小時),越短頻率更新越耗效能。 2. 有版本差異的欄位說明 - 最小/預設大小設定 - minWidth、minHeight:單位是dp,不知道怎麼算可以看官網,或是先用右鍵生成 - targetCellWidth、targetCellHeight:直接用格子數代表,Android12以上要設定 - 可以看[官網的格數dp對照表](https://developer.android.com/develop/ui/views/appwidgets/layouts?hl=zh-tw#anatomy_determining_size) - 小工具預覽圖 - **previewImage** => 小工具的預覽圖案 - previewLayout => 這個會直接跑xml檔的預覽,Android 12以上才能用;沒設定就是顯示previewImage - 小工具是否可在螢幕鎖屏使用 - widgetCategory:小工具可以在哪邊用 home_screen (主畫面) | keyguard (鎖定屏幕 - Android 5以後無法用) - initialKeyguardLayout 可選:有放到螢幕鎖定的小工具才要,但Android 5 以後此權限已被拔除,所以基本上不會用到 3. 完整widget_info 範例 ``` <?xml version="1.0" encoding="utf-8"?> <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:configure="設定的Activity位置,要包含包名 ex: com.example.SettingActivity" android:description="@string/app_widget_description" android:initialKeyguardLayout="@layout/realtime_case_dash_board_app_widget" android:initialLayout="@layout/realtime_case_dash_board_app_widget" android:minWidth="250dp" android:minHeight="110dp" android:previewImage="@drawable/example_appwidget_preview" android:previewLayout="@layout/realtime_case_dash_board_app_widget" android:resizeMode="horizontal|vertical" android:targetCellWidth="4" android:targetCellHeight="2" android:updatePeriodMillis="86400000" android:widgetCategory="home_screen|keyguard" /> ``` ### Step.4 更新Manifest:在AndroidManifest.xml中添加`<receiver>`標籤以聲明您的Widget提供者。 ```xml= <receiver android:name="Step2.創建的Wiget檔案位置" android:exported="false"> <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> </intent-filter> <meta-data android:name="android.appwidget.provider" android:resource="@xml/Step3.創建的xml" /> </receiver> <!-- 如果有用到 Widget ListView 要在定義這個Service --> <service android:name="ListView實現的RemoteViewsService" android:permission="android.permission.BIND_REMOTEVIEWS" android:exported="false"> <intent-filter> <action android:name="android.widget.RemoteViewsService" /> </intent-filter> </service> <!-- 如果有小工具的設定頁面的話 --> <activity android:name="小工具設定頁面的Activity" android:exported="false"> <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" /> </intent-filter> </activity> ``` ## 一些Widget需要知道的Tips 1. 官方自己的自動更新時間,限制最少要間隔30分鐘,常見作法會直接用內建的AlarmManager 做計時,而AlarmManager的最小間隔限制是1分鐘。 - 自動更新:30min - AlarmManager: 1min - 其他方式:看各自方式的限制 2. 要注意如果刷新的越頻繁,就越會吃資源&電量,代價就是要嘛使用者去調整APP權限,加入白名單中,不然就是很有機會被系統強制中斷,然後小工具就會當住,要移除重放才會正常。 3. 會很常用到`PendingIntent`,記得flags要注意,有些情況是要設置成PendingIntent.FLAG_MUTABLE,例如用到ListViews的時候。 4. 目前無支援ViewBinding 5. 使用前記得詳讀[官方文件](https://developer.android.com/develop/ui/views/appwidgets?hl=zh-tw) 6. 可以直接看官方Sample: - [ListView_Sample - 單純畫面](https://android.googlesource.com/platform/development/+/refs/heads/main/samples/StackWidget/src/com/example/android/stackwidget?autodive=0%2F) - [WeatherListWidget_Sample - 假資料模擬API更新](https://android.googlesource.com/platform/development/+/refs/heads/main/samples/WeatherListWidget/) ## 即時看板工具實現 先說結論,目前先Pending了~ 因為自動更新後很常遇到整個Widgets掛掉,因此就沒做即時更新了,目前嘗試以一小時更新一次+手動觸發,觀察是否還會掛掉。 ### 即時看板用到的技術 1. 基本的Widget更新畫面&接收資料方式 2. 自動更新 - AlarmManager (雖然後面沒使用,但一樣可以說XD) 3. Widget打Api 4. ListView 的使用 - 畫面顯示&點擊事件 5. 先上Code,這裡只有Update & 直接說範例,要看完整請往上直接看官方Sample即可: ```kotlin= override fun onUpdate( context: Context?, appWidgetManager: AppWidgetManager?, appWidgetIds: IntArray? ) { //檢查必要的東西是否都在,不在就不做事 if(context == null || appWidgetManager == null){ debugLog("onUpdate context is null $context / $appWidgetManager") return } context ?: return appWidgetManager ?: return //更新每個Widget appWidgetIds?.forEach { widgetId -> //過濾掉無效的APPWIDGET_ID if (widgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { return } //開始更新處理 coroutineScope.launch { try { //part3.打Api val errorMsg = updateCaseListData() withContext(Dispatchers.Main){ //part1.更新畫面資訊 updateAppWidget(context, appWidgetManager, widgetId, errorMsg) //part4.更新ListView appWidgetManager.notifyListViewUpdateData(widgetId) } } catch (e: Exception) { debugLog(e) } } } //part2.開始啟動自動更新的鬧鐘 context.startAlarmManager(appWidgetIds) } } ``` ### Part1.基本的Widget更新畫面&接收資料方式 1. 更新畫面,都是先創出RemoteViews 2. 再透過AppWidgetManger更新,更新有分成不同種類,EX: - 全部刷新(最耗資源,全部都重建) `updateAppWidget(appWidgetId, remoteViews)` - 部分刷新(只更新有變化的地方) `partiallyUpdateAppWidget(appWidgetId, remoteViews)` - 更新ListView(跟呼叫RecycleView notifyDataChanged一樣) `notifyAppWidgetViewDataChanged` ```kotlin= private fun updateAppWidget( context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, errorMsg: String = "" ) { //創出RemoteViews val remoteViews = RemoteViews(context.packageName, R.layout.widget_realtime_case_dash_board) //操作RemoteViews更新要更新的地方 remoteViews.setHeaderInfo() remoteViews.setUpdateClicker(context, appWidgetId) remoteViews.setListViewData(context, appWidgetId) remoteViews.setListViewClicker(context, appWidgetId) // 更新到Widget上 appWidgetManager.updateAppWidget(appWidgetId, remoteViews) } ``` #### 基本畫面顯示 1. 要直接用有開出來的方法,做設定 EX: `setTextViewText(viewId, text)` 其實就是 `findViewById<TextView>(viewId).text = text` ```kotlin= private fun RemoteViews.setHeaderInfo() { val searchHint = when { areaHistory.isNotEmpty() || categoryHistory.isNotEmpty() -> "正在顯示:篩選後案件" else -> "正在顯示:全部案件" } setTextViewText(R.id.tvWidgetRealtimeCaseDashboardSubTitle, searchHint) //取得現在時間 EX: 12:00 val currentTime = Calendar.getInstance(Locale.TAIWAN).time //date to YYYY/MM/DD HH:mm like 2024/02/27 12:00 val dateString = android.text.format.DateFormat.format("yyyy/MM/dd", currentTime).toString() //date to HH:mm val timeString = android.text.format.DateFormat.format("HH:mm", currentTime).toString() setTextViewText( R.id.tvWidgetRealtimeCaseDashboardReloadTime, "$dateString $timeString 🔄️" ) } ``` #### 基本畫面設定點擊事件 1. 要先創建PendingIntent,然後設定當該View被點擊時,就會觸發該Intent,也就是他是依靠Intent驅動的 2. 把PendingIntent設定到View,就像setOnClicker一樣,只是他是先new好,也只有intent能用,剩下判斷要寫到onReceive收到該action或妳要的extras內容後再自己寫看要怎麼辦 3. 簡單來說:定義Intent -> 放入View點擊觸發 -> 點擊後行為 onReceive定義 ```kotlin= private fun RemoteViews.setUpdateClicker(context: Context, appWidgetId: Int) { //設定更新列表資料的點擊效果,點時間或空資料 val clickViewIntent = Intent(context, RealtimeCaseDashBoardWidgetProvider::class.java).apply { //action & Extras 內容都可以自己定義 action = CLICK_ACTION putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, intArrayOf(appWidgetId)) putExtra(UPDATE_LIST, true) } val pendingIntent = PendingIntent.getBroadcast( context, 0, clickViewIntent, //Android12以上一定要加PendingIntent.FLAG_IMMUTABLE or .FLAG_MUTABLE,不然會報錯,差別就是該Intent還能不能調整 // PendingIntent.FLAG_UPDATE_CURRENT 代表這個Intent有收到要立刻處理,所以兩個都會加 PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT ) //把PendingIntent設定到View setOnClickPendingIntent( R.id.tvWidgetRealtimeCaseDashboardReloadTime, pendingIntent ) setOnClickPendingIntent( R.id.tvWidgetRealtimeCaseDashboardEmpty, pendingIntent ) } ``` #### 點擊事件後續 ```kotlin= override fun onReceive(context: Context?, intent: Intent?) { //... // 假設有收到UPDATE_LIST就代表 更新整個Widget val updateList = intent?.getBooleanExtra(UPDATE_LIST, false) ?: false val isClickAction = intent?.action.orEmpty() == CLICK_ACTION if (updateList) { //做事 _ Like原本要寫在onClickerListener裡的動作 updateCaseData(context, appWidgetId, appWidgetIds, isClickAction) } } } ``` ### Part2. 自動更新 - AlarmManager 1. 做一個更新的PendingIntent,時間到觸發即可。 2. 這邊觸發是用`PendingIntent.getBroadcast`,因此還是會在onReceive收到 3. AppWidgetManager.ACTION_APPWIDGET_UPDATE,是Widget內建的Action會觸發onUpdate ```kotlin= private fun Context.startAlarmManager(appWidgetIds: IntArray? = intArrayOf()) { testInfoLog("startAlarmManager ${appWidgetIds.toString()}") val alarmManager = getSystemService(Context.ALARM_SERVICE) as? AlarmManager ?: return val intent = Intent(this, RealtimeCaseDashBoardWidgetProvider::class.java).apply { //這邊可自行定義,AppWidgetManager.ACTION_APPWIDGET_UPDATE,是Widget內建的會觸發onUpdate action = AppWidgetManager.ACTION_APPWIDGET_UPDATE putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds) putExtra(UPDATE_LIST, true) } val pendingIntent = PendingIntent.getBroadcast( this, 0, intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT ) //設定每60分鐘更新一次 => 最低限制的間隔時間為1分鐘,目前實測1分鐘滿容易導致他就不更新了 val updateTime = 1000 * 60 * 60 * 1L alarmManager.setRepeating( AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime(), updateTime, pendingIntent ) } ``` ### Part3. Widget打Api 其實就跟正常打API一樣,只是資料如果要傳送給ListView,要透過本地傳遞,EX: SharedPerence、ContentProvider、Room等等 ```kotlin= /** * 打完資料後會儲存到SP裡面,在呼叫onDataSetChanged時WidgetViewsFactory會拿這個資料再轉成換面顯示的Data */ private suspend fun updateCaseListData(): String { //創建API參數... //打API caseRepository.getCaseList(reqProj).fold( onSuccess = { response -> val dataList = response.dataList val newData = dataList.mapNotNull { //轉成ListView的資料格式 WidgetViewsFactory.WidgetItem( caseId = it.caseId, caseTitle = it.caseTitle, casePostTime = it.updateTime ) } var errorMsg = response.message if(newData.isNotEmpty()){ val jsonStr = gson.toJson(newData) //存到本地資料 caseListStr = jsonStr }else{ //存到本地資料 caseListStr = "" errorMsg = response.message.ifEmpty { "沒有符合條件的案件,請至APP更改篩選條件" } } return errorMsg }, onFailure = { debugLog(it) return "讀取案件列表失敗,請嘗試重新安裝小工具" } ) } ``` ### Part4. ListView 的使用 - 畫面顯示&點擊事件 1. 要使用ListView,需要繼承`RemoteViewsService`&實現`RemoteViewsFactory`才能使用 2. 他的概念是透過`RemoteViewsService`,去監聽一些事件,再透過這些事件的觸發做對應的事情,有點像provider 3. 但他的主要職責只是在於渲染ListView畫面 4. AppWidgetProvider: - 把RemoteViewsService跟ListView的Adapter綁定 - 綁定資料空時的畫面Layout - 設定每個item被點擊時的PendingIntent 5. RemoteViewsFactory: - 設定每個item要顯示的畫面 - 製作FillInIntent,給予該item的Data - 更新DataList 6. 幾個跟一般用RecycleView不同的點: - DataList更新不是外面直接傳入,而是要透過不同管道拿取。EX:直接打API或本地拿取,建議本地,因為如果還有錯誤處理外面沒辦法判讀,職責也容易模糊。 - 點擊事件的更新: - Provider的setPendingIntentTemplate意思是當使用者點擊時,都會傳遞這個PendingIntent出來,像是一個範本的概念。 - RemoteViewsFactory的setOnClickFillInIntent,則是只要填充到Intent的內容 - 結合起來也就是當item被點擊時,會創出Template給的PendingIntent,然後把FillInIntent的Extras放入,再回傳出去 - 因此該PendingIntent會被改變,切記flags一定要添加`PendingIntent.FLAG_MUTABLE`,否則會無法正確把FillInIntent的資料放入 #### RemoteViewsService 1. 很Easy,其實就是把RemoteViewsFactory的實體給他而已 ```kotlin= class WidgetService: RemoteViewsService() { override fun onGetViewFactory(intent: Intent): RemoteViewsFactory { return WidgetViewsFactory(this.applicationContext, intent) } } ``` 2. 因為是Service,要到Manifest定義 ```xml= <!-- Widget ListView --> <service android:name="RemoteViewsService Class" android:exported="false" android:permission="android.permission.BIND_REMOTEVIEWS"> <intent-filter> <action android:name="android.widget.RemoteViewsService" /> </intent-filter> </service> ``` #### RemoteViewsFactory 實現 1. 在設定點擊是用FillInIntent,他的概念是會把原本放到ListView的PendingIntent,把FillInItntent填充進去,因此每個item的Data資料要放,就是從這邊放入 ```kotlin= class WidgetViewsFactory( private val context: Context, intent: Intent ) : RemoteViewsFactory, KoinComponent { data class WidgetItem( @SerializedName("case_id") val caseId: String, @SerializedName("case_title") val caseTitle: String, @SerializedName("case_post_time") val casePostTime: String ) private val appWidgetId: Int = intent.getIntExtra( AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID ) private val coroutineScope = createCoroutineScope(Dispatchers.IO) /** * 更新會在[RealtimeCaseDashBoardWidgetProvider] 打完API更新,這是本地資料SP(依自己的實現即可) */ private var caseListStr by Preference() private val gson by inject<Gson>() private val dataList = mutableListOf<WidgetItem>() override fun onCreate() { //可以做些初始化動作 } override fun onDataSetChanged() { //不切coroutinr也沒差,就是跟新dataList資料而已 //更新後會自動觸發onGetViewAt的Fuction coroutineScope.launch { val newData = getCaseList() withContext(Dispatchers.Main) { dataList.clear() dataList.addAll(newData.toList()) } } } //從本地拿取資料 private fun getCaseList(): List<WidgetItem> { return try { val type = object : TypeToken<List<WidgetItem>>() {}.type gson.fromJson<List<WidgetItem>>(caseListStr, type) } catch (e: Exception) { debugLog("Widget getCaseList error: $e") emptyList() } } override fun onDestroy() { testInfoLog("Widget onDestroy") coroutineScope.cancel() dataList.clear() } override fun getCount(): Int { return dataList.size } //重點,設定View要怎麼顯示 override fun getViewAt(position: Int): RemoteViews { //設定要怎麼顯示View val remoteViews = RemoteViews(context.packageName, R.layout.item_widget_realtime_case) val data = dataList.getOrNull(position) ?: return remoteViews remoteViews.setTextViewText(R.id.tvItemWidgetRealtimeCaseTitle, data.caseTitle) remoteViews.setTextViewText(R.id.tvItemWidgetRealtimeCaseTime, data.casePostTime) //設定每個View的點擊事件,用FillInIntent val fillInIntent = Intent().apply { Bundle().also { extras -> extras.putString(RealtimeCaseDashBoardWidgetProvider.CASE_ID, data.caseId) putExtras(extras) } } remoteViews.setOnClickFillInIntent(R.id.llItemWidgetRealtimeCase, fillInIntent) return remoteViews } /** * 回傳null 會用預設的載入中顯示 */ override fun getLoadingView(): RemoteViews? { return null } override fun getViewTypeCount(): Int { return 1 } //返回該位置的Item的ID,盡量唯一值,會跟下面的ID選項相同 override fun getItemId(position: Int): Long { return position.toLong() } //返回的ID是否為唯一值且穩定,true的話會提高效率 override fun hasStableIds(): Boolean { return true } } ``` #### Provider設定 ```kotlin= /** 設定ListView的畫面顯示 */ private fun RemoteViews.setListViewData(context: Context, appWidgetId: Int) { val listViewIntent = Intent(context, WidgetService::class.java).apply { putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME)) } //將 RemoteViewsService 跟 Adapter綁定 setRemoteAdapter(R.id.listViewWidgetRealtimeCaseDashboard, listViewIntent) //設定空畫面,需在同個FrameLayout中,當資料空時會自動切換 setEmptyView( R.id.listViewWidgetRealtimeCaseDashboard, R.id.tvWidgetRealtimeCaseDashboardEmpty ) } /** 設定listView點擊事件 */ private fun RemoteViews.setListViewClicker(context: Context, appWidgetId: Int) { val listItemClickViewIntent: PendingIntent = Intent(context, RealtimeCaseDashBoardWidgetProvider::class.java).run { action = REALTIME_CASE_ITEM_CLICK_ACTION putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME)) //請注意一定要設定 PendingIntent.FLAG_MUTABLE,因為FillInIntent會改變Intent的內容! PendingIntent.getBroadcast( context, 0, this, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE ) } //設定每個項目的PendingIntent setPendingIntentTemplate( R.id.listViewWidgetRealtimeCaseDashboard, listItemClickViewIntent ) } ``` ##### 空畫面xml檔 1. 直接把空畫面跟ListView重疊即可,不需要設定visiable,當空值時他會自動切換 ```xml= <FrameLayout android:id="@+id/frameLayoutWidgetRealtimeCaseDashboard" android:layout_width="match_parent" android:layout_height="match_parent"> <ListView android:id="@+id/listViewWidgetRealtimeCaseDashboard" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginTop="10dp" android:clipToPadding="false" android:paddingBottom="15dp" /> <TextView android:id="@+id/tvWidgetRealtimeCaseDashboardEmpty" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/neutral_400" android:gravity="center" android:text="@string/realtimeCaseDashBoardWidgetEmpty" android:textSize="20sp"/> </FrameLayout> ``` ### 小結 1. 目前擱置中,尚存Bug,例如建多個工具後有問題等等... 尚有滿多地方需再繼續研究。 2. 主要剩餘突破點為,當被系統殺死後,有什麼方法有辦法喚回,恢復正常運作 3. 如果沒辦法就是看更新頻率低+手動觸發,如何讓使用者體驗是良好的,而不會覺得很麻煩。 ## 之後可繼續研究 1. 可以設定APPWIDGET_CONFIGURE,讓使用者在使用時直接設定一些資訊,例如可以自訂顏色、文字,或是條件等等。 - 會是另外開一個Activity讓他做事,感覺彈性跟可以做的事情滿多的。 2. 可能會朝向較固定的資料開發,EX:顯示特定QRCode,先嘗試跟產品結合,上線,在看有沒有遇到什麼坑。