Jetpack 之Glance+Compose實現一個小元件
持續創作,加速成長!這是我參與「掘金日新計劃 · 6 月更文挑戰」的第18天,點選檢視活動詳情
Glance,官方對其解釋是使用 Jetpack Compose 樣式的 API 構建遠端 Surface 的佈局,通俗的講就是使用Compose風格的API來搭建小外掛佈局,其最新版本是2022年2月23日更新的1.0.0-alpha03。眾所周知,Compose樣式的API與原生差別不小,至於widget這塊改動如何,接下來讓我們來一探究竟。
宣告依賴項
第一步肯定要新增對應依賴,相應的都是在build.gradle中新增,如果你的工程還沒支援Compose,要先新增:
android {
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.1.0-beta03"
}
kotlinOptions {
jvmTarget = "1.8"
}
}
如果已經支援,上述依賴可以省略,但下述依賴不能省略,繼續新增:
dependencies {
implementation("androidx.glance:glance-appwidget:1.0.0-alpha03")
implementation("androidx.glance:glance-wear-tiles:1.0.0-alpha03")
}
以上是官方的標準依賴方式,同樣以下面這種方式依賴也可以:
implementation 'androidx.glance:glance-appwidget:+'
implementation 'androidx.glance:glance:+'
implementation "androidx.glance:glance-appwidget:1.0.0-alpha03"
建立對應 widget
首先編寫對應佈局,放在對應/layout/xml目錄下:
widget_info.xml ```
我在上一篇介紹widget的文章中說過,widget其實就是個廣播,廣播屬於四大元件,而四大元件都要在AndroidManifest清單檔案中註冊:
對應CounterWidgetReceiver程式碼為:
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
import com.ktfly.comapp.ui.theme.CounterWidget
class CounterWidgetReceiver : GlanceAppWidgetReceiver(){
override val glanceAppWidget: GlanceAppWidget = CounterWidget()
}
可能看到這裡你就迷惑了,widget對應廣播類不是要繼承AppWidgetProvider然後實現相應方法的嗎,其實Glance提供的GlanceAppWidgetReceiver類就已經繼承了AppWidgetProvider,我們使用Glance需要GlanceAppWidgetReceiver:
abstract class GlanceAppWidgetReceiver : AppWidgetProvider() {
private companion object {
private const val TAG = "GlanceAppWidgetReceiver"
}
/**
* Instance of the [GlanceAppWidget] to use to generate the App Widget and send it to the
* [AppWidgetManager]
*/
abstract val glanceAppWidget: GlanceAppWidget
@CallSuper
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
Log.w(
TAG,
"Using Glance in devices with API<23 is untested and might behave unexpectedly."
)
}
goAsync {
updateManager(context)
appWidgetIds.map { async { glanceAppWidget.update(context, appWidgetManager, it) } }
.awaitAll()
}
}
@CallSuper
override fun onAppWidgetOptionsChanged(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int,
newOptions: Bundle
) {
goAsync {
updateManager(context)
glanceAppWidget.resize(context, appWidgetManager, appWidgetId, newOptions)
}
}
@CallSuper
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
goAsync {
updateManager(context)
appWidgetIds.forEach { glanceAppWidget.deleted(context, it) }
}
}
private fun CoroutineScope.updateManager(context: Context) {
launch {
runAndLogExceptions {
GlanceAppWidgetManager(context)
.updateReceiver([email protected], glanceAppWidget)
}
}
}
override fun onReceive(context: Context, intent: Intent) {
runAndLogExceptions {
if (intent.action == Intent.ACTION_LOCALE_CHANGED) {
val appWidgetManager = AppWidgetManager.getInstance(context)
val componentName =
ComponentName(context.packageName, checkNotNull(javaClass.canonicalName))
onUpdate(
context,
appWidgetManager,
appWidgetManager.getAppWidgetIds(componentName)
)
return
}
super.onReceive(context, intent)
}
}
}
private inline fun runAndLogExceptions(block: () -> Unit) { try { block() } catch (ex: CancellationException) { // Nothing to do } catch (throwable: Throwable) { logException(throwable) } } ``` 基本流程方法跟原生widget的差別不大,其含義也無差別,如果對原生Widget不太瞭解的同學可以翻閱我上一篇文章,這裡還有官方註釋:"Using Glance in devices with API<23 is untested and might behave unexpectedly."。在6.0版本以下的Android系統上使用Glance的情況未經測試可能有出乎意料的情況發生。在開始編寫widget程式碼之前,我們先來了解下其使用元件與Compose中的對應元件的些許差別。
差別
根據官方提示,可使用的Compose組合項如下:Box、Row、Column、Text、Button、LazyColumn、Image、Spacer。原生widget是不支援自定義View的,但Compose能通過自定義元件的方式來“自定義”出我們想要的檢視,這一點來看相對更加靈活。
Compose中使用的修飾符是Modifier,這裡修飾可組合項的修飾符是GlanceModifier,使用方式並無二致,其餘元件也有些許差異,這個我們放到後面來說,
Action
以前使用widget跳轉頁面啥的,都離不開PendingIntent,但是Glance中則採取另一套方式:
actionStartActivity
看函式命名就得知,通過Action啟動Activity。共有三種使用方式: ``` // 通過包名啟動Activity public fun actionStartActivity( componentName: ComponentName, parameters: ActionParameters = actionParametersOf() ): Action = StartActivityComponentAction(componentName, parameters)
// 直接啟動Activity
public fun
//呼叫actionStartActivity啟動Activity,行內函數
public inline fun
其對應的使用方式也簡單:
Button(text = "Jump", onClick = actionStartActivity(ComponentName("com.ktfly.comapp","com.ktfly.comapp.page.ShowActivity")))
Button(text = "Jump", onClick = actionStartActivity<ShowActivity>())
Button(text = "Jump", onClick = actionStartActivity(ShowActivity::class.java))
actionRunCallback
顧名思義,此函式是通過Action執行Callback,以下是官方提供的使用說明:\
```
fun
inline fun
使用方式:
先建立一個繼承actionRunCallback的回撥類:
class ActionDemoCallBack : ActionCallback {
override suspend fun onRun(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
TODO("Not yet implemented")
}
}
然後在控制元件中呼叫:
```
Button(text = "CallBack", onClick = actionRunCallback
Button(text = "CallBack", onClick = actionRunCallback(ActionDemoCallBack::class.java))\ ```
actionStartService
此函式是通過Action啟動Service,有以下四個使用方式: ``` fun actionStartService( intent: Intent, isForegroundService: Boolean = false ): Action
fun actionStartService( componentName: ComponentName, isForegroundService: Boolean = false ): Action
fun
inline fun 這裡的isForegroundService引數含義是此服務是前臺服務。在呼叫之前也需要先建立對應Service:
class ActionDemoService : Service() {
override fun onBind(intent: Intent?): IBinder? {
TODO("Not yet implemented")
}
}
```
其在控制元件中使用方式如下:
```
Button(text = "start", onClick = actionStartService
Button(text = "start", onClick = actionStartService(ActionDemoService::class.java)) ```
actionStartBroadcastReceiver
此函式是通過Action啟動BroadcastReceiver,有以下使用方式:
``` fun actionSendBroadcast( action: String, componentName: ComponentName? = null ): Action
fun actionSendBroadcast(intent: Intent): Action
fun actionSendBroadcast(componentName: ComponentName): Action
fun
inline fun
fun actionStartActivity( intent: Intent, parameters: ActionParameters = actionParametersOf() ): Action ``` 其各函式用法跟actionStartActivity函式差不多,這裡不做贅述。你會發現以上函式中經常出現ActionParameters。其實ActionParameters就是給Action提供引數,這裡不做贅述。
建立widget
建立對應的widget類,通過GlanceStateDefinition來保留GlanceAppWidget的狀態,通過點選事件回撥自定義的ActionCallBack達到更改widget中數字的目的: ``` import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.intPreferencesKey import androidx.glance. import androidx.glance.action.ActionParameters import androidx.glance.action.actionParametersOf import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.action.ActionCallback import androidx.glance.appwidget.action.actionRunCallback import androidx.glance.appwidget.state.updateAppWidgetState import androidx.glance.layout. import androidx.glance.state.GlanceStateDefinition import androidx.glance.state.PreferencesGlanceStateDefinition import androidx.glance.text.Text import androidx.glance.text.TextAlign import androidx.glance.text.TextStyle import androidx.glance.unit.ColorProvider
private val countPreferenceKey = intPreferencesKey("widget-key")
private val countParamKey = ActionParameters.Key
class CounterWidget : GlanceAppWidget(){
override val stateDefinition: GlanceStateDefinition<*> =
PreferencesGlanceStateDefinition
@Composable
override fun Content(){
val prefs = currentState<Preferences>()
val count = prefs[countPreferenceKey] ?: 1
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalAlignment = Alignment.CenterVertically,
modifier = GlanceModifier
.background(Color.Yellow)
.fillMaxSize()
) {
Text(
text = count.toString(),
modifier = GlanceModifier.fillMaxWidth(),
style = TextStyle(
textAlign = TextAlign.Center,
color = ColorProvider(Color.Blue),
fontSize = 50.sp
)
)
Spacer(modifier = GlanceModifier.padding(8.dp))
Button(
text = "變兩倍",
modifier = GlanceModifier
.background(Color(0xFFB6C0C9))
.size(100.dp,50.dp),
onClick = actionRunCallback<UpdateActionCallback>(
parameters = actionParametersOf(
countParamKey to (count + count)
)
)
)
}
}
}
class UpdateActionCallback : ActionCallback{ override suspend fun onRun(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
val count = requireNotNull(parameters[countParamKey])
updateAppWidgetState(
context = context,
definition = PreferencesGlanceStateDefinition,
glanceId = glanceId
){ preferences ->
preferences.toMutablePreferences()
.apply {
this[countPreferenceKey] = count
}
}
CounterWidget().update(context,glanceId)
}
} ``` 執行後效果如下:
也許你會發現上述導包與平常Compose導包不一樣:
控制元件導的包都是glance包下的,當然不僅是Column,還有Button、Image等引數都有變化,但變化不大,例如Image的差異: ``` 原Compose中: Image( modifier = modifier, painter = BitmapPainter(bitmap), contentDescription = "", contentScale = contentScale )
Image( modifier = modifier, painter = painterResource(資源id), contentDescription = "", contentScale = contentScale )
Glance中: public fun Image( provider: ImageProvider, contentDescription: String?, modifier: GlanceModifier = GlanceModifier, contentScale: ContentScale = ContentScale.Fit ) ```
其餘控制元件差異大同小異,這裡不做贅述。
- 一文講完Jetpack常用修飾符
- JetpackCompose中的Dialog、AlertDialog
- Activity互動問題,你確定都知道?
- Kotlin中的內建函式-apply、let
- Compose自定義動畫API指南
- Android EditText關於imeOptions的設定和響應
- Jetpack 之Glance Compose實現一個小元件
- Compose高級別API動畫指南
- Android錄音功能的實現及踩坑記錄
- LayoutInflater原始碼解析及常見相關報錯分析
- UI自動重新整理大法:DataBinding資料繫結
- DataBinding簡易入門
- 還在用findViewById,不來了解下其它方式?