原文: Android 桌面小組件使用-Stars-One的雜貨小窩 藉助公司上的幾個項目,算是學習了Android桌面小組件的用法,記下踩坑記錄 基本步驟 1.創建小組件佈局 這裡需要註意的事,小組件佈局里不能使用自定義View,只能使用原生的組件,比如說LinearLayout,TextView ...
藉助公司上的幾個項目,算是學習了Android桌面小組件的用法,記下踩坑記錄
基本步驟
1.創建小組件佈局
這裡需要註意的事,小組件佈局里不能使用自定義View,只能使用原生的組件,比如說LinearLayout,TextView,連約束佈局都不能使用
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/tvDate"
style="@style/textStyle14"
android:textColor="#313131"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="2023-12-10" />
<ImageView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<TextView
android:id="@+id/tvTime"
android:textColor="#313131"
style="@style/textStyle14"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="12:10" />
</LinearLayout>
<LinearLayout
android:layout_marginTop="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/result_clean"/>
<LinearLayout
android:orientation="vertical"
android:layout_width="0dp"
android:layout_marginStart="9dp"
android:gravity="center_vertical"
android:layout_height="match_parent"
android:layout_weight="1" >
<TextView
style="@style/textStyle14"
android:textColor="#313131"
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="125.4MB"/>
<TextView
style="@style/textStyle14"
android:textColor="#313131"
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Junk"/>
</LinearLayout>
<TextView
android:layout_gravity="center_vertical"
android:id="@+id/tvClean"
android:textColor="#313131"
style="@style/textStyle14"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Clean" />
</LinearLayout>
</LinearLayout>
2.創建provider
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.widget.RemoteViews
import android.widget.RemoteViews.RemoteView
import ten.jou.recover.R
class CleaningWidget : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
appWidgetIds.forEach {
//如果小組件佈局中使用不支持的組件,這裡創建RemoteViews時候,IDE會報紅提示!
val remoteView = RemoteViews(context.packageName, R.layout.widget_layout)
//綁定數據
remoteView.setTextViewText(R.id.tv1,"hello world")
appWidgetManager.updateAppWidget(it, remoteView)
}
}
}
AppWidgetProvider本質就是一個廣播接收器,所以在清單文件需要聲明(見步驟4)
這裡先補充下,RemoteViews對於TextView,ImageView等View,有設置文本,字體顏色,圖片等相關方法,但並不是所有方法都支持,綁定數據的時候需要註意下小組件是否支持!
3.創建xml屬性聲明
在xml文件夾里新建widget_info.xml文件:
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:targetCellWidth="4"
android:targetCellHeight="2"
android:minWidth="250dp"
android:minHeight="110dp"
android:updatePeriodMillis="0"
android:initialLayout="@layout/widget_layout"
tools:targetApi="s">
</appwidget-provider>
Android12版本以上新增的2個屬性,聲明組件是4*2大小
- targetCellWidth
- targetCellHeight
4.清單文件聲明
<receiver
android:name=".view.CleaningWidget"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_info" />
</receiver>
5.代碼添加小組件
官方說Android12不允許直接通過代碼添加小組件,只能讓用戶手動去桌面拖動添加,但是我手頭的三星系統卻是支持的(也是Android12),具體還沒有細究...
而官方文檔上的寫的例子如下:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val context = this@DesktopWidgetActivity
val appWidgetManager: AppWidgetManager =
context.getSystemService(AppWidgetManager::class.java)
val myProvider = ComponentName(context, CleaningWidget::class.java)
//判斷啟動器是否支持小組件pin
val successCallback = if (appWidgetManager.isRequestPinAppWidgetSupported) {
// Create the PendingIntent object only if your app needs to be notified
// that the user allowed the widget to be pinned. Note that, if the pinning
// operation fails, your app isn't notified.
Intent(context, CleaningWidget::class.java).let { intent ->
// Configure the intent so that your app's broadcast receiver gets
// the callback successfully. This callback receives the ID of the
// newly-pinned widget (EXTRA_APPWIDGET_ID).
//適配android12的
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
PendingIntent.getBroadcast(
context,
0,
intent,
flags
)
}
} else {
null
}
appWidgetManager.requestPinAppWidget(myProvider, null, successCallback)
}
這裡提下,上面的設置flags方法
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
有個新項目的targetSdk為34(即Android14),如果使用上面的代碼會出現下麵崩潰錯誤提示
Targeting U+ (version 34 and above) disallows creating or retrieving a PendingIntent with FLAG_MUTABLE, an implicit Intent within and without FLAG_NO_CREATE and FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT for security reasons. To retrieve an already existing PendingIntent, use FLAG_NO_CREATE, however, to create a new PendingIntent with an implicit Intent use FLAG_IMMUTABLE.
實際上提示已經告訴我們怎麼去改代碼了,我這裡把PendingIntent.FLAG_MUTABLE
改為FLAG_IMMUTABLE
就不會出現了上述的崩潰問題
應該是Android14添加的限制:
- 如果Intent不傳數據,必須使用
PendingIntent.FLAG_IMMUTABLE
- 如果是需要傳遞數據,則還是需要使用
PendingIntent.FLAG_MUTABLE
定時刷新小組件UI
首先,我們得知道,如何主動去更新數據:
val context = it.context
val appWidgetManager: AppWidgetManager = context.getSystemService(AppWidgetManager::class.java)
val myProvider = ComponentName(context, CleaningWidget::class.java)
val remoview = CleaningWidget.getRemoteViewTest(context)
//更新某類組件
appWidgetManager.updateAppWidget(myProvider,remoview)
//更新具體某個組件id
appWidgetManager.updateAppWidget(widgetId,remoview)
getRemoteViewTest方法就是創建一個remoteview,然後調用remoteview相關方法設置文本之類的進行數據填充,代碼就略過不寫了,詳見上述基本步驟2
上面的方法我們註意到updateAppWidget
可以傳不同的參數,一般我們用的第二個方法,指定更新某個組件
但這裡又是需要我們傳一個組件id,所以就是在步驟2的時候,我們根據需要需要存儲下widgetId比較好,一般存入資料庫,或者使用SharePreference也可
然後,就是對於定時的情況和對應方案:
- 如果是間隔多長更新一次,可以使用開一個服務,在服務中開啟協程進行
- 如果是單純的時間文本更新,可以使用TextClock組件,比如說 12:21這種
- 小組件的xml中預設可以設置定時更新時長,不過最短只能需要15分鐘
- 可以使用鬧鐘服務AlarmManager來實現定時,不過此用法需要結合pendingintent和廣播接收器使用,最終要在廣播接收器里調用更新數據方法
- JobScheduler來實現定時更新,似乎受系統省電策略影響,適用於不太精確的定時事件(官方文檔上推薦這個)
- WorkManager來實現定時更新(實際上算是JobScheduler升級版),似乎受系統省電策略影響,適用於不太精確的定時事件
應該是除了第一種方法,其他都是可以在應用被殺死的情況進行更新小組件UI
小組件播放動畫
progressbar實現
幀動畫不手動調用anim.start()
方法是不會播放的,然後在網上看到一篇文章,使用了progressbar來實現,步驟如下:
在drawable文件夾準備幀動畫文件
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:oneshot="false" android:visible="true">
<item android:drawable="@drawable/cat_1" android:duration="100" />
<item android:drawable="@drawable/cat_2" android:duration="100" />
<item android:drawable="@drawable/cat_3" android:duration="100" />
<item android:drawable="@drawable/cat_4" android:duration="100" />
</animation-list>
<ProgressBar
android:indeterminateDrawable="@drawable/cat_animdrawable"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
indeterminateDrawable設置為上面的幀動畫文件即可
layoutanim實現
主要是利用viewgroup的初次顯示的時候,會展示當前view的添加動畫效果,從而實現比較簡單的動畫效果,如平移,縮放等
可以看實現的敲木魚一文Android-桌面小組件RemoteViews播放木魚動畫 - 掘金
使用ViewFlipper
ViewFlipper主要是輪播使用的
裡面可放幾個元素,之後通過設置autoStart為true,則保證自動輪播
flipInterval屬性則是每個元素的間隔時間(幀動畫的時間),單位為ms
不過在remoteview中使用的話,缺點就是裡面的元素數目只能固定死
否則只能通過定義不同layout文件(如3個元素則是某個layout,4個元素則是某個layout,然後根據選擇來創建remoteview)
<ViewFlipper
android:id="@+id/viewFlipper"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_margin="4dp"
android:autoStart="true"
android:flipInterval="800">
<ImageView
android:id="@+id/vf_img_1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitXY"
android:src="@drawable/peace_talisman_1" />
<ImageView
android:id="@+id/vf_img_2"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitXY"
android:src="@drawable/peace_talisman_2" />
</ViewFlipper>
補充
獲取當前桌面的組件id列表
//獲得當前桌面已添加的組件的id列表(可能用戶添加了多個)
val context = it.context
val appWidgetManager: AppWidgetManager = context.getSystemService(AppWidgetManager::class.java)
val myProvider = ComponentName(context, CleaningWidget::class.java)
val info =appWidgetManager.getAppWidgetIds(myProvider)
toast(info.size.toString())
參考
- 構建應用微件 | Android 開發者 | Android Developers
- Android 12桌面小組件 - 掘金
- Android 12上煥然一新的小組件:美觀、便捷和實用 - 掘金
- baiyuas.github.io | 拜雨個人博客
- Android小部件APP Widget開發 - 掘金
- 【精選】Android 桌面小組件 AppWidgetProvider-CSDN博客
- 【APP Widget】使用代碼申請添加小部件,展示添加彈窗。 - 掘金
- Android-桌面小組件RemoteViews播放木魚動畫 - 掘金
- 【Android小知識點】Widget中實現動畫的一種極簡方式_桌面小控制項幀動畫-CSDN博客
- 【APP Widget】使用WorkManager定時更新小部件 - 掘金
提問之前,請先看提問須知 點擊右側圖標發起提問 或者加入QQ群一起學習 TornadoFx學習交流群:1071184701