Jetpack 之Glance+Compose实现一个小组件

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
)

其余控件差异大同小异,这里不做赘述。

相关推荐
小蜜蜂嗡嗡42 分钟前
Android Studio flutter项目运行、打包时间太长
android·flutter·android studio
aqi001 小时前
FFmpeg开发笔记(七十一)使用国产的QPlayer2实现双播放器观看视频
android·ffmpeg·音视频·流媒体
zhangphil2 小时前
Android理解onTrimMemory中ComponentCallbacks2的内存警戒水位线值
android
你过来啊你3 小时前
Android View的绘制原理详解
android
移动开发者1号6 小时前
使用 Android App Bundle 极致压缩应用体积
android·kotlin
移动开发者1号6 小时前
构建高可用线上性能监控体系:从原理到实战
android·kotlin
ii_best10 小时前
按键精灵支持安卓14、15系统,兼容64位环境开发辅助工具
android
美狐美颜sdk11 小时前
跨平台直播美颜SDK集成实录:Android/iOS如何适配贴纸功能
android·人工智能·ios·架构·音视频·美颜sdk·第三方美颜sdk
恋猫de小郭15 小时前
Meta 宣布加入 Kotlin 基金会,将为 Kotlin 和 Android 生态提供全新支持
android·开发语言·ios·kotlin
aqi0016 小时前
FFmpeg开发笔记(七十七)Android的开源音视频剪辑框架RxFFmpeg
android·ffmpeg·音视频·流媒体