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
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider
xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/app_name"
android:minWidth="150dp"
android:minHeight="66dp"
android:resizeMode="horizontal|vertical"
android:targetCellWidth="3"
android:targetCellHeight="2"
android:widgetCategory="home_screen"
/>
我在上一篇介绍widget的文章中说过,widget其实就是个广播,广播属于四大组件,而四大组件都要在AndroidManifest清单文件中注册:
<receiver
android:name=".CounterWidgetReceiver"
android:enabled="@bool/glance_appwidget_available"
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/widget_info" />
</receiver>
对应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(this@GlanceAppWidgetReceiver, 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 <T : Activity> actionStartActivity(
activity: Class<T>,
parameters: ActionParameters = actionParametersOf()
): Action = StartActivityClassAction(activity, parameters)
//调用actionStartActivity启动Activity,内联函数
public inline fun <reified T : Activity> actionStartActivity(
parameters: ActionParameters = actionParametersOf()
): Action = actionStartActivity(T::class.java, parameters)\
其对应的使用方式也简单:
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 <T : ActionCallback> actionRunCallback(
callbackClass: Class<T>,
parameters: ActionParameters = actionParametersOf()
): Action
inline fun <reified T : ActionCallback> actionRunCallback(parameters: ActionParameters = actionParametersOf()): Action
使用方式:
先创建一个继承actionRunCallback的回调类:
class ActionDemoCallBack : ActionCallback {
override suspend fun onRun(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
TODO("Not yet implemented")
}
}
然后在控件中调用:
Button(text = "CallBack", onClick = actionRunCallback<ActionDemoCallBack>())
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 <T : Service> actionStartService(
service: Class<T>,
isForegroundService: Boolean = false
): Action
inline fun <reified T : Service> actionStartService(isForegroundService: Boolean = false): Action
这里的isForegroundService参数含义是此服务是前台服务。在调用之前也需要先创建对应Service:
class ActionDemoService : Service() {
override fun onBind(intent: Intent?): IBinder? {
TODO("Not yet implemented")
}
}
其在控件中使用方式如下:
Button(text = "start", onClick = actionStartService<ActionDemoService>())
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 <T : BroadcastReceiver> actionSendBroadcast(receiver: Class<T>): Action
inline fun <reified T : BroadcastReceiver> actionSendBroadcast(): Action
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<Int>("widget-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
)
其余控件差异大同小异,这里不做赘述。