# 淺談 Android Widget - 2 _ 實作篇QRCode顯示篇 ## 前言 繼上次[淺談 Android Widget - 1](/6w8LtFEDSMGKRrU6Ew3qKg) 後,這次來實作一個更新頻率較低的畫面,顯示一個QRCode畫面。 ### 需求規格總覽 1. 使用者未登入時,有登入按鈕可直接進入App登入頁。 2. 使用者有登入時,顯示QRCode、使用者姓名。 3. 皆有重新整理按鈕,已刷新狀態。 4. 不同登入狀態時,畫面文案不同。 5. 每日會自動刷新、App登入狀態改變時也會刷新。 ### 本次會使用到的方式 1. 從App內呼叫widget更新 2. widget手動更新 3. widget按鈕 -> 開啟到App 指定頁 4. 在Widget使用Glide 5. 在Widget 顯示/隱藏 View ### 畫面預覽 未登入時: ![image](https://hackmd.io/_uploads/BywZlpOt0.png) 登入後: ![image](https://hackmd.io/_uploads/SkhMeauKA.png) ## 各使用方式說明 初始的細節就不贅述了,可以看上一篇文章,僅就這次有新用到的部分分享 ### 1. 從App內呼叫widget更新 在需要的地方,透過送AppWidgetManager 本身有的broadcase即可,他也可以呼叫enabled、deleted等等的方法 更改對應的intent.action即可 - clazz: 放的是有實作的AppWidgetProvider,每個widget都會有一個 - 以此為例,代表送一個widget update的資料到broadcase,會觸發widget的`onReceive` => `onUpdate` - 但他有延遲,最慢一分鐘才會更新&有機率丟失,所以Widget建議還是要有可以手動觸發刷新的機制 ```kotlin= fun Context.notifyWidgetUpdate(clazz: Class<out AppWidgetProvider>) { val intent = Intent(this, clazz) intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE sendBroadcast(intent) } ``` ### 2. widget手動更新 此處所做的是當使用者點擊重新整理後,重新檢查登入狀態及刷新畫面,思路是: 1. 定義一個自己辨識的Tag(String) 2. 設定view點擊事件,有觸發的話送包含這個tag的 broadcase事件 3. 在onReceive時,偵測這個事件,有的話就做希望的行為 => 因此可以選擇要全部更新,還是只動某部分的view ```kotlin= companion object { /** * 1. 定義一個自己辨識的Tag(String) **/ const val UPDATE_QRCODE = "UPDATE_QRCODE" } fun update( context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int){ val remoteViews = RemoteViews(context.packageName, R.layout.widget_invite_evaluate_qrcode) remoteViews.setUpdateClicker(context, appWidgetId) } /** * 2. 設定view點擊事件,有觸發的話送包含這個tag的 broadcase事件 **/ private fun RemoteViews.setUpdateClicker(context: Context, appWidgetId: Int) { //設定點擊的觸發intent val clickViewIntent = Intent(context, {你的WidgetProvider}::class.java).apply { action = Constants.MessageNotificationKeys.CLICK_ACTION putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, intArrayOf(appWidgetId)) //放入自定義的辨別Tag putExtra(UPDATE_QRCODE, true) } //包裝成pendingIntent 因為widgetClick只吃他,注意因為是送Broadcast,所以用 `getBroadcast` val pendingIntent = PendingIntent.getBroadcast( context, 0, clickViewIntent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT ) //設置點擊事件 點擊的viewId + PendingIntent) setOnClickPendingIntent( R.id.tvWidgetInviteEvaluateQrcodeReload, pendingIntent ) } /** * 在onReceive時,偵測這個事件,有的話就做希望的行為 **/ override fun onReceive(context: Context?, intent: Intent?) { //取得此次更新的WidgetId,可能是Array也可能是單一個,以單一個為主 val (appWidgetIds, appWidgetId) = getAppWidgetIds(intent) // 看這次的intent有沒有包含自定義的標籤 val updateQrcode = intent?.getBooleanExtra(UPDATE_QRCODE, false) ?: false if(updateQrcode){ //有的話,更新你想更新的地方,當然也可以全部刷新 updateQrcode(context, appWidgetIds) } super.onReceive(context, intent) } ``` ### 3. widget按鈕 -> 開啟到App 指定頁 這次是利用專案的Deeplink轉導開啟,但實際上要直接開到指定Activity也是可以的,只是如果你是有一個主要Activity一定要有開啟的話,會被跳過~ ```kotlin= //使用範例 fun update( context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int){ val remoteViews = RemoteViews(context.packageName, R.layout.widget_invite_evaluate_qrcode) remoteViews.setLoginClicker(context, appWidgetId) } private fun RemoteViews.setLoginClicker(context: Context) { //這邊是利用Deeplink轉導,當然也是可以直接開到指定頁面 val intent = MainActivity.createIntent(context).apply { data = Uri.parse("{你的Deeplink連結}") } //注意這裡因為是要開activity,所以是用getActivity val pendingIntent = PendingIntent.getActivity( context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE ) //設置點擊事件 點擊的viewId + PendingIntent) setOnClickPendingIntent( R.id.tvWidgetInviteEvaluateLogin, pendingIntent ) } ``` ### 4. 在Widget使用Glide 因為無法直接用Glide.into(ImageView),因此只能先將圖片下載成bitmap後,在透過setImageViewBitmap的方法放入,步驟= 1. 先下載/取得Bitmap 2. 處理取得成功&失敗情況,並設置ImageView 3. 重新渲染widget畫面 (因為Glide去載bitmap會切執行緒,因此好了以後要手動呼叫渲染) ```kotlin= //使用範例 fun update( context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int){ val remoteViews = RemoteViews(context.packageName, R.layout.widget_invite_evaluate_qrcode) remoteViews.setQRCodeImageView(context, appWidgetManager, appWidgetId) } private fun RemoteViews.setQRCodeImageView( context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int ) { //看你如何取得你的圖片下載網址 val webUrl = getWebUrl() Glide.with(context) .asBitmap() .load(webUrl) .into(object : CustomTarget<Bitmap>() { //成功時用setImageViewBitmap override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) { setImageViewBitmap(R.id.ivWidgetInviteEvaluateQrcode, resource) //設定後記得要重新刷新 appWidgetManager.updateAppWidget(appWidgetId, this@setQRCodeImageView) } //失敗時直接用setImageViewResource放預設圖片 override fun onLoadCleared(placeholder: Drawable?) { setImageViewResource( R.id.ivWidgetInviteEvaluateQrcode, R.drawable.img_broken_image ) //設定後記得要重新刷新 appWidgetManager.updateAppWidget(appWidgetId, this@setQRCodeImageView) } }) } ``` ### 5. 在Widget 顯示/隱藏 View 用remoteViews的`setViewVisibility(viewId, View.VISIBLE/INVISIBLE/GONE)` 方法即可。 ### 6. 防連點刷新 因為widget只要很頻繁刷新,滿容易壞的,如果有連點有些操作也會異常,因此防連點還是需要的,目前的作法是在`onReceive`時阻擋他的事件。 **防雷點是,lastClickTime(紀錄點擊時間的參數)記得一定要放在object中**,因為provider他隨時可能會處在被消滅狀態,或是拿出不同實體,因此有類似的參數你會每次都拿到新的 ```kotlin= companion object { private const val CLICK_INTERVAL = 1000L // 防止連點的間隔時間 //注意紀錄上次點擊時間的變數要放在companion object中 private var lastClickTime = 0L } override fun onReceive(context: Context?, intent: Intent?) { val currentTime = System.currentTimeMillis() //這裡是明確找有符合的action 才做防止連點,避免點擊跟系統或自己呼叫的更新重疊撞到,因此只針對明確要防的處理 val isDoubleClickAction = listOf( Constants.MessageNotificationKeys.CLICK_ACTION ).contains(intent?.action.orEmpty()) //在緩衝時間,或是不需要管他的action時,才會往後走 if (currentTime - lastClickTime >= CLICK_INTERVAL || !isDoubleClickAction) { //更新點擊時間 lastClickTime = currentTime //處理要處理的項目,ex: 更新QRcode只讓他一秒內更新一次 when { updateQrcode -> updateQrcode(context, appWidgetIds) } } } ``` ## 小結 1. 這次的實做比起上次即時看板來的簡易多,所需要實現的類別也較少,故障率也較低,算是不錯的實驗。 2. 更新頻率仍是widget的一大致命傷,除了手動更新時會快一些,不然在App開啟中手動呼叫的變化,會看你前一分鐘有沒有刷新到,以及你關掉App的速度,有可能會沒送成功。 => 不過解法也還好,就是在規劃上記得準備好手動刷新的接口就行。 3. 總體來說對比上次失敗的情況,目前的是可正常運用在專案中了。 ## 之後可持續研究部分 1. 可以在看有沒有什麼適合做widget的,像是儀錶板之類的,讓使用者可以在外部手動刷新看有沒有訊息 2. 或是可以看看能不能結合推播刷新widget等相關機制 3. 可以再嘗試看看有打api 或 ListView等其他元件或情境的操作