探索 Jetpack Glance 的魔法

之前写过两篇关于 Jetpack Glance 的文章,分别是第一个 alpha 版本:Jetpack Glance?小部件的春天来了,以及第一个 release 版本发布时写的:稳定的 Glance 来了,安卓小部件有救了!

前世

大家都知道,小部件是运行在桌面中的,并不是运行在自己的应用中,那么数据的传输就涉及到了跨进程,Google 专门为这些需要跨进程绘制布局的需求写了一个名叫 RemoteViews 的类,比如 NotificationWidget 等,大家千万不要被它名字影响力,虽然它叫 View,但它并不是一个 View。。。

public class RemoteViews implements Parcelable, Filter {}

看到了吧,大骗子。。。

RemoteViews 是比较坑的,它只能支持特定的一些布局:

  • AdapterViewFlipper:可以实现图片、文字等的轮播
  • FrameLayout
  • GridLayout
  • GridView
  • LinearLayout
  • ListView
  • RelativeLayout
  • StackView:卡片状的,可以进行滑动
  • ViewFlipper:也是用来实现轮博的

这些布局大家应该都使用过,这块就不再进行赘述。接下来看下 RemoteViews 支持的特定控件:

  • AnalogClock:用来实现表盘样式的时钟
  • Button
  • Chronometer:计时器
  • ImageButton
  • ImageView
  • ProgressBar
  • TextClock
  • TextView

这些控件大家肯定也很熟悉,但安卓中的控件那么多,RemoteViews 只能支持这么几个。。。后来官方也看不下去了,又在 Android 12 中新增了以下几个控件:

  • CheckBox
  • RadioButton
  • RadioGroup
  • Switch

像大家熟知的 RecyclerViewEditTextSeekBarSpinner 等等都是不支持的哈。有人可能会想,那我自定义 View 不得了,不好意思,也不可以。。。还有人会想,那我继承它支持的控件,然后自定义不得了,Sorry,还是不可以。。。RemoteViews 只是描述了可在另一个进程中显示的视图层次结构的类,层次结构是从布局资源文件中加载出来的,该类只提供了一些基本操作来修改布局中的内容。简单来看一个例子大家就知道了:

public void setTextViewText(int viewId, CharSequence text) {
    setCharSequence(viewId, "setText", text);
}
public void setCharSequence(int viewId, String methodName, CharSequence value) {
    addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
}
private void addAction(Action a) {
    ...
    if (mActions == null) {
        mActions = new ArrayList();   
    }
    mActions.add(a);
}

上面代码逻辑很简单:先调用 setCharSequence 并传入函数名和参数值,然后创建 ReflectionAction 实例,之后保存操作 View 的 id、函数名、参数值,最后将 ReflectionAction 实例保存在 mActions

AppWidgetManager 提交更新之后 RemoteViews 便会由 Binder 跨进程传输到 SystemServer 进程中 ,之后在这个进程 RemoteViews 会执行它的 apply 函数或者 reapply 函数。apply 加载布局到ViewGroup中,与它作用类似的还有 reApply,二者区别在于 apply 加载布局并更新布局、而 reApply 只更新界面。

private View apply(Context context, ViewGroup directParent, ViewGroup rootParent,
        @Nullable SizeF size, ActionApplyParams params) {
    RemoteViews rvToApply = getRemoteViewsToApply(context, size);
    View result = inflateView(context, rvToApply, directParent,
            params.applyThemeResId, params.colorResources);
    rvToApply.performApply(result, rootParent, params);
    return result;
}

上面代码也不难理解,首先获取创建的 RemoteViews 实例,通过调用 inflateView 函数加载布局到布局容器中,然后调用 RemoteViewsperformApply 函数执行保存的 Action

private void performApply(View v, ViewGroup parent, ActionApplyParams params) {
    params = params.clone();
    if (params.handler == null) {
        params.handler = DEFAULT_INTERACTION_HANDLER;
    }
    if (mActions != null) {
        final int count = mActions.size();
        for (int i = 0; i < count; i++) {
            mActions.get(i).apply(v, parent, params);
        }
    }
}

这个函数中就干了一件事,取出之前保存 Action 的集合,循环执行其中的每个 Action 执行其 apply 函数,从上面我们直到此处保存的是,接下来就看下 Actionapply 函数;

@Override
public final void apply(View root, ViewGroup rootParent, ActionApplyParams params) {
    final View view = root.findViewById(viewId);
    if (view == null) return;

    Class param = getParameterType(this.type);
    if (param == null) {
        throw new ActionException("bad type: " + this.type);
    }
    Object value = getParameterValue(view);
    try {
        getMethod(view, this.methodName, param, false /* async */).invoke(view, value);
    } catch (Throwable ex) {
        throw new ActionException(ex);
    }
}

代码一目了然,找到对应 id 的 View ,然后根据参数类型以及函数名称通过反射来执行对应操作。再来简单梳理下 RemoteView 的工作流程吧:首先在调用 set 函数后并不会直接更新布局,此时会创建反射 Action 并保存起来,RemoteView 在跨进程设置后,通过调用 applyreapply 加载和更新布局,完成后从遍历所有的 Action ,然后执行其 apply 函数,最后在 apply 函数中,根据保存的函数名和参数,反射执行函数修改界面。到这里就可以解释为啥不能使用自定义 View 或者别的控件了。

今生

了解了 RemoteViews 的大概工作流程之后,来看下直接使用 RemoteViews 创建 Widget 布局的代码吧:

internal fun updateAppWidget(
    context: Context,
    appWidgetManager: AppWidgetManager,
    appWidgetId: Int
) {
    val widgetText = context.getString(R.string.appwidget_text)
    // 创建 RemoteViews
    val views = RemoteViews(context.packageName, R.layout.new_app_widget)
    views.setTextViewText(R.id.appwidget_text, widgetText)

    // 指示小部件管理器更新小部件
    appWidgetManager.updateAppWidget(appWidgetId, views)
}

咱们再来看 Glance ,就会感觉到很神奇,竟然可以使用 Compose 的方式编写 Widget ,但目前也只是 WidgetNotification 是不支持的哈。

override suspend fun provideGlance(context: Context, id: GlanceId) {
    val articleList = getArticleList()
    provideContent {
        GlanceTheme {
            Column {
                Text(stringResource(id = R.string.widget_name))
                LazyColumn {
                    items(articleList) { data ->
                        GlanceArticleItem(context, data)
                    }
                }
            }
        }
    }
}

不知道大家有没有这种感觉,我在第一次使用 Glance 的时候就感觉太神奇了,这是魔法啊!以为官方对 RemoteView 的整套流程给改了,所以才能支持这种方式的代码,开心了许久。但,看了 Glance 的源码之后发现不是那么回事,它并没有改变 RemoteViews ,只是做了一层优雅的封装,让我们能专注于 UI 以及数据的实现,尽力改变安卓中小部件难开发的现状!故才会有这篇文章,同大家一起欣赏下 Glance 的优雅以及老版本中遗留的无奈。

探索

上一篇文章中介绍过,GlanceAppWidgetReceiver 中有一个抽象函数,需要返回一个 GlanceAppWidget ,而咱们的类 Compose 代码就是在 GlanceAppWidget 中的抽象函数 provideGlance 中进行的,且需要在 provideGlance 中调用它的一个扩展函数 provideContent ,在 provideContent 中我们就能写类 Compose 的布局代码了。

GlanceAppWidget

GlanceAppWidget 在上一篇文章中也提到过,简单说了下子类需要实现的以及可以重写的函数,但里面的具体实现都没有提到,上一篇文章主要还是使用为主,单纯使用的话光看上一篇其实够用。接下来详细来看看吧!

abstract class GlanceAppWidget {
  
    private val sessionManager: SessionManager = GlanceSessionManager

    abstract suspend fun provideGlance(
        context: Context,
        id: GlanceId,
    )

    // 在provideGlance中运行这个组合,并将结果发送给AppWidgetManager。
    suspend fun update(
        context: Context,
        id: GlanceId
    ) {
        update(context, id.appWidgetId)
    }

    // 调用onDelete,然后清除与appWidgetId关联的本地数据,当Widget实例从主机中删除时调用。
    internal suspend fun deleted(context: Context, appWidgetId: Int) {
        val glanceId = AppWidgetId(appWidgetId)
        sessionManager.closeSession(glanceId.toSessionKey())
        onDelete(context, glanceId)
    }

    // 内部版本更新,由广播接收器直接使用。
    internal suspend fun update(
        context: Context,
        appWidgetId: Int,
        options: Bundle? = null,
    ) {
        val glanceId = AppWidgetId(appWidgetId)
        val session = sessionManager.getSession(glanceId.toSessionKey()) as AppWidgetSession
        session.updateGlance()
    }

    // 触发要在此小部件的AppWidgetSession中运行的操作
    internal suspend fun triggerAction(
        context: Context,
        appWidgetId: Int,
        actionKey: String,
        options: Bundle? = null,
    ) {
        val glanceId = AppWidgetId(appWidgetId)
        val session = sessionManager.getSession(glanceId.toSessionKey()) as AppWidgetSession
        session.runLambda(actionKey)
    }

    /**
     * 检测到调整大小事件时调用的内部函数。
     */
    internal suspend fun resize(
        context: Context,
        appWidgetId: Int,
        options: Bundle
    ) {
        val glanceId = AppWidgetId(appWidgetId)
        val session = sessionManager.getSession(glanceId.toSessionKey()) as AppWidgetSession
        session.updateAppWidgetOptions(options)
    }
}

上面代码就是上一篇文章中忽略的,当然,也是经过修改的,删除了一些影响阅读的代码,即一些判断是否运行或者判断是否有效、可以执行的代码。

SessionManager

看完这几十行代码后,通篇都感受到了 SessionManager 这个东西很重要!因为不管是updatedelete ,还是 resize 等,全部都和它相关!那就不得不关注下了!

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
interface SessionManager {
    // 为 Glance 启动一个 Session
    suspend fun startSession(context: Context, session: Session)

    // 关闭key对应 Session 的通道
    suspend fun closeSession(key: String)

    // 如果 Session 使用给定的键处于活动状态,则返回true
    suspend fun isSessionRunning(context: Context, key: String): Boolean

    // 获取与密钥对应的 Session(如果存在)
    fun getSession(key: String): Session?

    val keyParam: String
        get() = "KEY"
}

可以看出SessionManager 是一个接口,里面定义了几个函数,它是 Glance 表面的入口点,用于启动一个 Session 来处理它们的组合。可以看到在 GlanceAppWidgetSessionManager 是调用了 GlanceSessionManager ,那它应该就是 SessionManager 的实现类了,咱们来看看:

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
val GlanceSessionManager: SessionManager = SessionManagerImpl(SessionWorker::class.java)

嘿,果然,和咱们想的一致,有一个实现类名字叫 SessionManagerImpl ,并且传入了一个参数,看名称应该是一个 Work,咱们接着往下追,先看看 SessionManagerImpl 的实现:

internal class SessionManagerImpl(
    private val workerClass: Class
) : SessionManager {
    private val sessions = mutableMapOf()

    override suspend fun startSession(context: Context, session: Session) {
        synchronized(sessions) {
            sessions.put(session.key, session)
        }?.close()
        val workRequest = OneTimeWorkRequest.Builder(workerClass).build()
        WorkManager.getInstance(context)
            .enqueueUniqueWork(session.key, ExistingWorkPolicy.REPLACE, workRequest)
            .result.await()
        enqueueDelayedWorker(context)
    }

    override fun getSession(key: String): Session? = synchronized(sessions) {
        sessions[key]
    }

    override suspend fun isSessionRunning(context: Context, key: String) =
        (WorkManager.getInstance(context).getWorkInfosForUniqueWork(key).await()
            .any { it.state == WorkInfo.State.RUNNING } && synchronized(sessions) {
            sessions.containsKey(key)
        })

    override suspend fun closeSession(key: String) {
        synchronized(sessions) {
            sessions.remove(key)
        }?.close()
    }

    /**
     * Workaround worker to fix b/119920965
     */
    private fun enqueueDelayedWorker(context: Context) {
        WorkManager.getInstance(context).enqueueUniqueWork(
            "sessionWorkerKeepEnabled",
            ExistingWorkPolicy.KEEP,
            OneTimeWorkRequest.Builder(workerClass)
                .setInitialDelay(10 * 365, TimeUnit.DAYS)
                .setConstraints(
                    Constraints.Builder()
                        .setRequiresCharging(true)
                        .build()
                )
                .build()
        )
    }
}

可以看到它的构造函数中传入的参数类型为 ListenableWorker ,它是 CoroutineWorker 的父类。代码中实现了 SessionManager 接口中的几个函数,然后剩下的代码就比较清晰了,有一个全局的 map 来存储 Session ,在 startSession 中存入、closeSession 中取出。剩下的就是 WorkManager 的操作了,开始的时候定义一个一次的工作进行执行,这个工作就是构造函数中传入进来的。

这块需要注意 enqueueDelayedWorker 这个函数,可以看上面的注释,它是 Workaround 的,这个函数很丑陋。。。直接定义了一个十年后的工作,也就是十年内不会执行。。。其实有可能一个手机用超过十年,建议再多定义些年。。。。

这个问题之前在 Widget 中尝试使用 WorkManager 的时候就遇到了,会导致重复刷新,具体大家取 Google 下,大概意思就是执行操作的时候会导致发送一条广播,这个广播会出发小部件的刷新,然后这应该属于 WorkManager 的问题,但是 WorkManager 有理由也不会改,所以这块只能定义了一个丑陋的函数。。。

Session

好了,不吐槽了,咱们继续,既然刚看了下 SessionManager ,那么 Session 又是啥啊?

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
abstract class Session(val key: String) {
    private val eventChannel = Channel(Channel.UNLIMITED)

    // 创建EmittableWithChildren,它将被用作appler的根目录。
    abstract fun createRootEmittable(): EmittableWithChildren

    // 提供要在composition中运行的Glance可组合文件。
    abstract fun provideGlance(context: Context): @Composable @GlanceComposable () -> Unit

    // 处理运行provideGlance产生的Emittable树。这也将要求对未来的重组结果。返回:如果树已被处理并且会话已准备好处理事件,则返回true。
    abstract suspend fun processEmittableTree(
        context: Context,
        root: EmittableWithChildren
    ): Boolean

    // 处理发送到此会话的事件。
    abstract suspend fun processEvent(context: Context, event: Any)

    // 为要由 Session 处理的事件排队。这些请求可以通过调用receiveEvents来处理。Session 实现应该用公共函数包装sendEvent,以发送其 Session 支持的事件类型。
    protected suspend fun sendEvent(event: Any) {
        eventChannel.send(event)
    }

    // 处理传入事件,另外为接收到的每个事件运行块。这个函数挂起,直到close被调用。
    suspend fun receiveEvents(context: Context, block: (Any) -> Unit) {
        try {
            for (event in eventChannel) {
                block(event)
                processEvent(context, event)
            }
        } catch (_: ClosedReceiveChannelException) {
        }
    }

    fun close() {
        eventChannel.close()
    }
}

OK,可以看到 Session 也是一个接口,里面定义了一些函数,这些函数的作用都在代码中写了注释,大家可以直接看注释。值得激动的是这块的一个名叫 provideGlance 的函数,哈哈哈,终于看到这个名字了。不过才刚开始。。。接口肯定有实现类,这块咱们一会再来看!

SessionWorker

下面咱们来看下刚才执行的工作,就是刚才传入的 SessionWorker ,那就接着来看下 SessionWorker 吧!

internal class SessionWorker(
    appContext: Context,
    private val params: WorkerParameters, // 参数配置
    private val sessionManager: SessionManager = GlanceSessionManager,
    private val timeouts: TimeoutOptions = TimeoutOptions(), // 超时选项
    override val coroutineContext: CoroutineDispatcher = Dispatchers.Main // 默认主线程
) : CoroutineWorker(appContext, params) {

    private val key = inputData.getString(sessionManager.keyParam)

    private suspend fun doWork(): Result {
        // 根据 Key 获取 Session
        val session = sessionManager.getSession(key)
        // 获取全局快照监视器
        val snapshotMonitor = launch { globalSnapshotMonitor() }
        // 创建 EmittableWithChildren,它将被用作 appler 的根目录。
        val root = session.createRootEmittable()
        // 用于执行重组和对一个或多个组合应用更新的调度器。
        val recomposer = Recomposer(coroutineContext)
        // Composition 来启动一个组合,Applier 是应用程序负责应用在组合过程中发出的基于树的操作
        val composition = Composition(Applier(root), recomposer).apply {
            setContent(session.provideGlance(applicationContext))
        }

        launch {
            var lastRecomposeCount = recomposer.changeCount
            recomposer.currentState.collect { state ->
                if (DEBUG) Log.d(TAG, "Recomposer(${session.key}): currentState=$state")
                when (state) {
                    Recomposer.State.Idle -> {
                        // 处理运行provideGlance产生的Emittable树
                        session.processEmittableTree(
                            applicationContext,
                            root.copy() as EmittableWithChildren
                        )
                    }
                    Recomposer.State.ShutDown -> cancel()
                    else -> {}
                }
            }
        }
      
        ......
      
        // 关闭相关资源
        composition.dispose()
        snapshotMonitor.cancel()
        recomposer.close()
        recomposer.join()
        return Result.success()
    }
}

可以看到在 SessionWorker 中就要干一些正事了,大家应该都使用过 WorkManager ,咱们直接看 doWork 函数吧,里面对代码进行了一些删除,方便大家走通逻辑,里面代码都添加了注释。

Recomposer 是一个调度器,绘制 UI 目前还用不到它,可以看到这块调用了 SessionprovideGlance ,但 Session 是一个接口,所以需要看看它的实现类。

AppWidgetSession

AppWidgetSession 就是 Session 的实现类,来一起看下吧:

internal class AppWidgetSession(
    private val widget: GlanceAppWidget,
    private val id: AppWidgetId,
    private val initialOptions: Bundle? = null,
    private val configManager: ConfigManager = GlanceState,
) : Session(id.toSessionKey()) {

    private val glanceState = mutableStateOf(null, neverEqualPolicy())
    private val options = mutableStateOf(Bundle(), neverEqualPolicy())
    private var lambdas = mapOf>()

    override fun createRootEmittable() = RemoteViewsRoot(MaxComposeTreeDepth)

    // 提供要在composition中运行的Glance可组合文件。
    override fun provideGlance(context: Context): @Composable @GlanceComposable () -> Unit = {
        CompositionLocalProvider(
            LocalContext provides context,
            LocalGlanceId provides id,
            LocalAppWidgetOptions provides options.value,
            LocalState provides glanceState.value,
        ) {
            val manager = remember { context.appWidgetManager }
            val minSize = remember { appWidgetMinSize() }
            remember { widget.runGlance(context, id) }
            SideEffect { glanceState.value }
        }
    }

    // // 处理运行provideGlance产生的Emittable树
    override suspend fun processEmittableTree(
        context: Context,
        root: EmittableWithChildren
    ): Boolean {
        root as RemoteViewsRoot
        // 创建一个LayoutConfiguration,从文件中检索已知的布局(如果存在)。
        val layoutConfig = LayoutConfiguration.load(context, id.appWidgetId)
        val appWidgetManager = context.appWidgetManager
        val receiver = appWidgetManager.getAppWidgetInfo(id.appWidgetId).provider
        // 合成一个树  
        normalizeCompositionTree(root)
        // 遍历Emittable树并更新所有LambdaActions的键
        lambdas = root.updateLambdaActionKeys()
        // 将 Composition 转为 RemoteViews
        val rv = translateComposition(
            context,
            id.appWidgetId,
            root,
            layoutConfig,
            layoutConfig.addLayout(root),
            DpSize.Unspecified,
            receiver
        )
        // 刷新 RemoteViews
        appWidgetManager.updateAppWidget(id.appWidgetId, rv)
        lastRemoteViews = rv
        return true
    }
    ......
}

这个类中的东西是非常多的,所以需要慢慢来看,这块先放了三个函数,这两个函数处理的内容其实在 Session 中已经说了,这块咱们就来看看具体实现。

createRootEmittable 中创建了一个 RemoteViewsRoot ,并且设置最大深度为50,这是 Glance 中预制合成的最大深度,虽然没有硬限制,但还是应该避免深度递归,因为深度递归会导致 RemoteViews 太大而无法发送。

provideGlance 中将 appWidgetManagerminSize 等内容保存起来,然后向 GlanceSession 提供 Compose 方式的布局,挂起直到 Session 关闭。

processEmittableTree 这个函数内容是比较多的,咱们慢慢来看,首先参数 root 使用的就是 createRootEmittable 函数创建的 RemoteViewsRoot ,刚才咱们也看到了在 SessionWorker 中调用到了,然后创建一个 LayoutConfiguration ,从文件中检索已知的布局,再获取下小部件的信息,然后调用 normalizeCompositionTree 函数将 Composition 都合成一个树,之后遍历 Emittable 树并更新所有 LambdaActions 的键,再根据现有信息创建出咱们熟知的 RemoteViews ,最后调用了咱们同样熟知的 appWidgetManager.updateAppWidget 来对小部件进行刷新。

到这里其实已经简单走了一遍 Glance 刷新的流程,但。。。感觉缺少点什么,是找到了 RemoteViews ,那里面写的那些 Compose 方式的布局哪去了,比如 TextButton 等等。。。

花明

还记得上面一直提到的 Emittable 么,类似的还有 EmittableWithChildren ,还有上面转换为 RemoteViewstranslateComposition 函数还没看上面留下的疑惑在这些地方就能解决!

Emittable

首先来看下 Emittable 吧:

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
interface Emittable {
    var modifier: GlanceModifier
    fun copy(): Emittable
}

可以看到这就是一个接口,但这个接口中有一个 Glance 中熟悉的 GlanceModifier ,看来是找对地方了!

再来看下 EmittableWithChildren

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
abstract class EmittableWithChildren(
    internal var maxDepth: Int = Int.MAX_VALUE,
    internal val resetsDepthForChildren: Boolean = false
) : Emittable {
    val children: MutableList = mutableListOf()

    protected fun childrenToString(): String =
        children.joinToString(",\n").prependIndent("  ")
}

可以看到 EmittableWithChildren 实现了 Emittable ,同时还是一个抽象类,构造函数中设定了两个参数,一个是最大深度,另一个是是否为子 EmittableWithChildren 来重置深度。

其实 Emittable 对应的实现就是 RemoteViews 中所对应的控件,与之对应,EmittableWithChildren 的子类就是 RemoteViews 中所对应的布局。上面所提到的 RemoteViewsRoot 其实就是一个 EmittableWithChildren ,只不过特殊一点,它表示根布局。

Button

虽然 RemoteViews 支持的控件不多,但也不少,同样地,这里咱们也随便挑一个来看吧:

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class EmittableButton : Emittable {
    override var modifier: GlanceModifier = GlanceModifier
    var text: String = ""
    var style: TextStyle? = null
    var colors: ButtonColors? = null
    var enabled: Boolean = true
    var maxLines: Int = Int.MAX_VALUE

    override fun copy(): Emittable = EmittableButton().also {
        it.modifier = modifier
        it.text = text
        it.style = style
        it.colors = colors
        it.enabled = enabled
        it.maxLines = maxLines
    }

    override fun toString(): String = "EmittableButton('$text', enabled=$enabled, style=$style, " +
        "colors=$colors modifier=$modifier, maxLines=$maxLines)"
}

代码不多,除了 GlanceModifier 外还定义了一些 Button 需要使用到的参数,比如 Textcolors 等。咱们接着往上看:

@Composable
internal fun ButtonElement(
    text: String,
    onClick: Action,
    modifier: GlanceModifier = GlanceModifier,
    enabled: Boolean = true,
    style: TextStyle? = null,
    colors: ButtonColors = ButtonDefaults.buttonColors(),
    maxLines: Int = Int.MAX_VALUE,
) {
    var finalModifier = if (enabled) modifier.clickable(onClick) else modifier
    finalModifier = finalModifier.background(colors.backgroundColor)
    val finalStyle =
        style?.copy(color = colors.contentColor) ?: TextStyle(color = colors.contentColor)

    GlanceNode(
        factory = ::EmittableButton,
        update = {
            this.set(text) { this.text = it }
            this.set(finalModifier) { this.modifier = it }
            this.set(finalStyle) { this.style = it }
            this.set(colors) { this.colors = it }
            this.set(enabled) { this.enabled = it }
            this.set(maxLines) { this.maxLines = it }
        }
    )
}

这块代码逻辑很简单,但是有一个新的东西:GlanceNode ,咱们先来简单看下:

@Composable
inline fun  GlanceNode(
    noinline factory: () -> T,
    update: @DisallowComposableCalls Updater.() -> Unit
) {
    ComposeNode(factory, update)
}

@Composable
inline fun  GlanceNode(
    noinline factory: () -> T,
    update: @DisallowComposableCalls Updater.() -> Unit,
    content: @Composable @GlanceComposable () -> Unit,
) {
    ComposeNode(factory, update, content)
}

OK,这是两个用来构建 Glance 节点的函数,看参数也能理解,一个有子布局,另一个没有,对应地就是一个表示控件,另一个表示布局。

接下来咱们再往上看:

@Composable
fun Button(
    text: String,
    onClick: Action,
    modifier: GlanceModifier = GlanceModifier,
    enabled: Boolean = true,
    style: TextStyle? = null,
    colors: ButtonColors = ButtonDefaults.buttonColors(),
    maxLines: Int = Int.MAX_VALUE,
) = ButtonElement(text, onClick, modifier, enabled, style, colors, maxLines)

OK,已经到了咱们调用的 Button 了。但感觉还是不对啊,它究竟在哪块实现的 Button 呢?

translateComposition

不着急,刚才还有一个 translateComposition 函数没看呢!谜底应该就在这里了!

internal fun translateComposition(
    context: Context,
    appWidgetId: Int,
    element: RemoteViewsRoot,
    layoutConfiguration: LayoutConfiguration?,
    rootViewIndex: Int,
    layoutSize: DpSize,
    actionBroadcastReceiver: ComponentName? = null,
) =
    translateComposition(
        TranslationContext(
            context,
            appWidgetId,
            context.isRtl,
            layoutConfiguration,
            itemPosition = -1,
            layoutSize = layoutSize,
            actionBroadcastReceiver = actionBroadcastReceiver,
        ),
        element.children,
        rootViewIndex,
    )

先来看下这个函数的参数,RemoteViewsRoot 上面提到过了,但还没有看到它是什么,来看下吧:

internal class RemoteViewsRoot(private val maxDepth: Int) : EmittableWithChildren(maxDepth) {
    override var modifier: GlanceModifier = GlanceModifier
    override fun copy(): Emittable = RemoteViewsRoot(maxDepth).also {
        it.modifier = modifier
        it.children.addAll(children.map { it.copy() })
    }

    override fun toString(): String = "RemoteViewsRoot(" +
        "modifier=$modifier, " +
        "children=[\n${childrenToString()}\n]" +
        ")"
}

和之前猜的一样,RemoteViewsRoot 果然就是 EmittableWithChildren 的子类,只不过稍微特殊一点,是 Glance 中的根布局。

那接着来看 translateComposition 函数,这里并没有做什么,而是直接调用了一个同名函数,同名函数中调用了 TranslationContext ,这个其实就是一个数据类,这块先不看了,咱们接着往下走!然后将 RemoteViewsRoot 中的子布局以及 rootView 的索引一块传入这个同名函数。

internal fun translateComposition(
    translationContext: TranslationContext,
    children: List,
    rootViewIndex: Int
): RemoteViews {
    ......
    return children.single().let { child ->
            val remoteViewsInfo = createRootView(translationContext, child.modifier, rootViewIndex)
            remoteViewsInfo.remoteViews.apply {
                translateChild(translationContext.forRoot(root = remoteViewsInfo), child)
            }
        }
}

这块的代码也经过了删减,咱们直接来看剩下的代码,这块调用了一个叫 createRootView 的函数,创建了一个 RemoteViewsInfo ,这其实就是 GlanceRemoteViews 的一层封装,然后接着使用 translateChild 来将 Compose 方式写的布局及控件给加载出来!

面纱

到这里终于看到了希望,createRootViewtranslateChild 两个函数中就有我们想要看到的东西!不过在看这两个函数前还要先来看下 GlanceRemoteViews 的封装 RemoteViewsInfo

internal data class RemoteViewsInfo(
    val remoteViews: RemoteViews,
    val view: InsertedViewInfo,
)

internal data class InsertedViewInfo(
    val mainViewId: Int = View.NO_ID,
    val complexViewId: Int = View.NO_ID,
    val children: Map> = emptyMap(),
)

代码很清晰,就是封装了 RemoteViews 的信息,可以看到还有一个类 InsertedViewInfo ,这里面就包括了布局 id、其中的元素 id、以及关于布局内容的其他细节。

接下来再来看 createRootView

internal fun createRootView(
    translationContext: TranslationContext,
    modifier: GlanceModifier,
    aliasIndex: Int
): RemoteViewsInfo {
    val context = translationContext.context
    val sizeSelector = SizeSelector(LayoutSize.Wrap, LayoutSize.Wrap)
    val layoutId = FirstRootAlias + aliasIndex
    ......  
    return RemoteViewsInfo(
        remoteViews = remoteViews(translationContext, layoutId).apply {
            modifier.findModifier()?.let {
                applySimpleWidthModifier(context, this, it, R.id.rootView)
            }
            modifier.findModifier()?.let {
                applySimpleHeightModifier(context, this, it, R.id.rootView)
            }
            ......
        },
        view = InsertedViewInfo(
            mainViewId = R.id.rootView,
            children = emptyMap()
        )
    )
}

根据传入的参数先将 RemoteViews 给构建出来,然后根据 GlanceModifier 来给根布局设置宽高,之后再将 InsertedViewInfo 给构建出来,下面来看下 applySimpleWidthModifier 吧!

internal fun applySimpleWidthModifier(
    context: Context,
    rv: RemoteViews,
    modifier: WidthModifier,
    viewId: Int,
) {
    val width = modifier.width
    setViewWidth(rv, viewId, width)
}

OK,接着来看 setViewWidth

fun setViewWidth(rv: RemoteViews, viewId: Int, width: Dimension) {
    when (width) {
        is Dimension.Wrap -> {
            rv.setViewLayoutWidth(viewId, WRAP_CONTENT.toFloat(), COMPLEX_UNIT_PX)
        }
        is Dimension.Expand -> rv.setViewLayoutWidth(viewId, 0f, COMPLEX_UNIT_PX)
        is Dimension.Dp -> rv.setViewLayoutWidth(viewId, width.dp.value, COMPLEX_UNIT_DIP)
        is Dimension.Resource -> rv.setViewLayoutWidthDimen(viewId, width.res)
        Dimension.Fill -> {
            rv.setViewLayoutWidth(viewId, MATCH_PARENT.toFloat(), COMPLEX_UNIT_PX)
        }
    }.let {}
}

这块代码实在是太亲切了,就是咱们熟知的 setViewLayoutWidth ,高度设置类似,这块也就不看了。

揭开

下面就该看 translateChild 函数了!

internal fun RemoteViews.translateChild(
    translationContext: TranslationContext,
    element: Emittable
) {
    when (element) {
        is EmittableBox -> translateEmittableBox(translationContext, element)
        is EmittableButton -> translateEmittableButton(translationContext, element)
        is EmittableRow -> translateEmittableRow(translationContext, element)
        is EmittableColumn -> translateEmittableColumn(translationContext, element)
        is EmittableText -> translateEmittableText(translationContext, element)
        is EmittableLazyListItem -> translateEmittableLazyListItem(translationContext, element)
        is EmittableLazyColumn -> translateEmittableLazyColumn(translationContext, element)
        is EmittableAndroidRemoteViews -> {
            translateEmittableAndroidRemoteViews(translationContext, element)
        }
        is EmittableCheckBox -> translateEmittableCheckBox(translationContext, element)
        is EmittableSpacer -> translateEmittableSpacer(translationContext, element)
        is EmittableSwitch -> translateEmittableSwitch(translationContext, element)
        is EmittableImage -> translateEmittableImage(translationContext, element)
        is EmittableLinearProgressIndicator -> {
            translateEmittableLinearProgressIndicator(translationContext, element)
        }
        is EmittableCircularProgressIndicator -> {
            translateEmittableCircularProgressIndicator(translationContext, element)
        }
        is EmittableLazyVerticalGrid -> {
            translateEmittableLazyVerticalGrid(translationContext, element)
        }
        is EmittableLazyVerticalGridListItem -> {
          translateEmittableLazyVerticalGridListItem(translationContext, element)
        }
        is EmittableRadioButton -> translateEmittableRadioButton(translationContext, element)
        is EmittableSizeBox -> translateEmittableSizeBox(translationContext, element)
        else -> {
            throw IllegalArgumentException(
                "Unknown element type ${element.javaClass.canonicalName}"
            )
        }
    }
}

咦~,这是什么啊?没错,就是 RemoteViews 中支持的这些布局及控件,根据 Emittable 来判断需要创建的布局及控件!

同样地,这里咱们还来看下 Button

private fun RemoteViews.translateEmittableButton(
    translationContext: TranslationContext,
    element: EmittableButton
) {
    val viewDef = insertView(translationContext, LayoutType.Button, element.modifier)
    setText(
        translationContext,
        viewDef.mainViewId,
        element.text,
        element.style,
        maxLines = element.maxLines,
        verticalTextGravity = Gravity.CENTER_VERTICAL,
    )

    // 调整appWidget特定的修饰符
    element.modifier = element.modifier
        .enabled(element.enabled)
        .cornerRadius(16.dp)
    if (element.modifier.findModifier() == null) {
        element.modifier = element.modifier.padding(horizontal = 16.dp, vertical = 8.dp)
    }
    applyModifiers(translationContext, this, element.modifier, viewDef)
}

首先根据当前的类型通过 insertView 函数来构建出对应的 InsertedViewInfo ,这块描述的类型就是 LayoutType.ButtonLayoutType 就是一个枚举类,里面同样定义了 RemoteViews 中可用的控件及布局,来看下吧:

internal enum class LayoutType {
    Row,
    Column,
    Box,
    Text,
    List,
    CheckBox,
    CheckBoxBackport,
    Button,
    Frame,
    LinearProgressIndicator,
    CircularProgressIndicator,
    VerticalGridOneColumn,
    VerticalGridTwoColumns,
    VerticalGridThreeColumns,
    VerticalGridFourColumns,
    VerticalGridFiveColumns,
    VerticalGridAutoFit,

    Swtch,
    SwtchBackport,
    ImageCrop,
    ImageFit,
    ImageFillBounds,
    ImageCropDecorative,
    ImageFitDecorative,
    ImageFillBoundsDecorative,
    RadioButton,
    RadioButtonBackport,
    RadioRow,
    RadioColumn,
}

就是一个抽象类,没有什么可说的,但是需要注意的是:Java 关键字,比如 switch,不能用于布局id。接着往下看,下面来看下 insertView 吧:

internal fun RemoteViews.insertView(
    translationContext: TranslationContext,
    type: LayoutType,
    modifier: GlanceModifier
): InsertedViewInfo {
    val childLayout = selectLayout33(type, modifier)
    return insertViewInternal(translationContext, childLayout, modifier)
}

通过向 selectLayout33 函数传入 modifier 和对应的 type 获取到对应的布局,接着通过 insertViewInternal 函数来构建出 InsertedViewInfo

接着再来看 setText 函数,通过调用 setText 函数来对文字进行设置,应该就是文字大小啊、行数啊等等文字相关的配置项,最后再根据 Glance 中特定的修饰符来修改对应的配置!

具体来看看吧!

internal fun RemoteViews.setText(
    translationContext: TranslationContext,
    resId: Int,
    text: String,
    style: TextStyle?,
    maxLines: Int,
    verticalTextGravity: Int = Gravity.TOP,
) {
    if (maxLines != Int.MAX_VALUE) {
        setTextViewMaxLines(resId, maxLines)
    }
    ......
    style.fontStyle?.let {
        spans.add(StyleSpan(if (it == FontStyle.Italic) Typeface.ITALIC else Typeface.NORMAL))
    }
    style.fontFamily?.let { family ->
        spans.add(TypefaceSpan(family.family))
    }
    setTextViewText(resId, content)

    when (val colorProvider = style.color) {
        is FixedColorProvider -> setTextColor(resId, colorProvider.color.toArgb())
        is ResourceColorProvider -> {
            setTextViewTextColorResource(resId, colorProvider.resId)
        }

        is DayNightColorProvider -> {
            setTextViewTextColor(
                    resId,
                    notNight = colorProvider.day.toArgb(),
                    night = colorProvider.night.toArgb()
                )
        }
    }
}

可以看到这是一个 RemoteViews 的扩展函数,里面根据传入的参数来对文字相关的内容进行了配置,和咱们上面猜想的一致!

GlanceModifier

如果说 Compose 中什么东西最神奇、最厉害、最牛逼!很多人肯定脱口而出:Modifier

同样地,在 Glance 中也有与之对应的 GlanceModifier ,它的功能虽然没有 Modifier 多,但也是将 RemoteViews 中能实现功能都给实现了!

而真正使用 GlanceModifier 的地方就是上面提到的 applyModifiers 函数!

internal fun applyModifiers(
    translationContext: TranslationContext,
    rv: RemoteViews,
    modifiers: GlanceModifier,
    viewDef: InsertedViewInfo,
) {
    val context = translationContext.context
    var widthModifier: WidthModifier? = null
    var heightModifier: HeightModifier? = null
    var paddingModifiers: PaddingModifier? = null
    var cornerRadius: Dimension? = null
    var visibility = Visibility.Visible
    var actionModifier: ActionModifier? = null
    var enabled: EnabledModifier? = null
    var clipToOutline: ClipToOutlineModifier? = null
    var semanticsModifier: SemanticsModifier? = null
    modifiers.foldIn(Unit) { _, modifier ->
        when (modifier) {
            is ActionModifier -> {
                actionModifier = modifier
            }
            is WidthModifier -> widthModifier = modifier
            is HeightModifier -> heightModifier = modifier
            is BackgroundModifier -> applyBackgroundModifier(context, rv, modifier, viewDef)
            is PaddingModifier -> {
                paddingModifiers = paddingModifiers?.let { it + modifier } ?: modifier
            }
            is VisibilityModifier -> visibility = modifier.visibility
            is CornerRadiusModifier -> cornerRadius = modifier.radius
            is ClipToOutlineModifier -> clipToOutline = modifier
            is EnabledModifier -> enabled = modifier
            is SemanticsModifier -> semanticsModifier = modifier
            else -> {
                Log.w(GlanceAppWidgetTag, "Unknown modifier '$modifier', nothing done.")
            }
        }
    }
    applySizeModifiers(translationContext, rv, widthModifier, heightModifier, viewDef)
    actionModifier?.let { applyAction(translationContext, rv, it.action, viewDef.mainViewId) }
    cornerRadius?.let { applyRoundedCorners(rv, viewDef.mainViewId, it) }
    paddingModifiers?.let { padding ->
        val absolutePadding = padding.toDp(context.resources).toAbsolute(translationContext.isRtl)
        val displayMetrics = context.resources.displayMetrics
        rv.setViewPadding(
            viewDef.mainViewId,
            ......
        )
    }
    clipToOutline?.let {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            rv.setBoolean(viewDef.mainViewId, "setClipToOutline", true)
        }
    }
    enabled?.let {
        rv.setBoolean(viewDef.mainViewId, "setEnabled", it.enabled)
    }
    semanticsModifier?.let { semantics ->
        val contentDescription: List? =
            semantics.configuration.getOrNull(SemanticsProperties.ContentDescription)
        if (contentDescription != null) {
            rv.setContentDescription(viewDef.mainViewId, contentDescription.joinToString())
        }
    }
    rv.setViewVisibility(viewDef.mainViewId, visibility.toViewVisibility())
}

代码看着不少,但其实逻辑都不难,就是将 GlanceModifier 中的内容取出来,然后设置到对应的布局和控件中!同样地来看一个例子吧:

private fun applySizeModifiers(
    translationContext: TranslationContext,
    rv: RemoteViews,
    widthModifier: WidthModifier?,
    heightModifier: HeightModifier?,
    viewDef: InsertedViewInfo
) {
    val context = translationContext.context
    if (viewDef.isSimple) {
        widthModifier?.let { applySimpleWidthModifier(context, rv, it, viewDef.mainViewId) }
        heightModifier?.let { applySimpleHeightModifier(context, rv, it, viewDef.mainViewId) }
        return
    }

    val width = widthModifier?.width
    val height = heightModifier?.height

    val useMatchSizeWidth = width is Dimension.Fill || width is Dimension.Expand
    val useMatchSizeHeight = height is Dimension.Fill || height is Dimension.Expand
    val sizeViewLayout = when {
        useMatchSizeWidth && useMatchSizeHeight -> R.layout.size_match_match
        useMatchSizeWidth -> R.layout.size_match_wrap
        useMatchSizeHeight -> R.layout.size_wrap_match
        else -> R.layout.size_wrap_wrap
    }

    val sizeTargetViewId = rv.inflateViewStub(translationContext, R.id.sizeViewStub, sizeViewLayout)

    fun Dimension.Dp.toPixels() = dp.toPixels(context)
    fun Dimension.Resource.toPixels() = context.resources.getDimensionPixelSize(res)
    when (width) {
        is Dimension.Dp -> rv.setTextViewWidth(sizeTargetViewId, width.toPixels())
        is Dimension.Resource -> rv.setTextViewWidth(sizeTargetViewId, width.toPixels())
        Dimension.Expand, Dimension.Fill, Dimension.Wrap, null -> {
        }
    }.let {}
    when (height) {
        is Dimension.Dp -> rv.setTextViewHeight(sizeTargetViewId, height.toPixels())
        is Dimension.Resource -> rv.setTextViewHeight(sizeTargetViewId, height.toPixels())
        Dimension.Expand, Dimension.Fill, Dimension.Wrap, null -> {
        }
    }.let {}
}

大家别看着代码多,其实都是唬人的,逻辑还是很清晰的,就是直接根据宽高等信息来给布局或者控件来设置大小!由于 applySizeModifiers 是同时给控件和布局使用的,所以需要判断各种情况,所以代码看着比较多。别的其实原理一样,比如 applyRoundedCorners 就是给布局或控件设置圆角的,setEnabled 就是控制控件或布局是否启动等等。

总结

本文先介绍了下 RemoteViews ,然后从 GlanceAppWidget 开始,一步一步跟踪代码,最终找到了 Glance 真正实现布局及 RemoteViews 的地方。

通篇看下来,不禁感叹:代码写的整体逻辑还是很清晰的,Kotlin 的语法糖使用地也很甜,但始终还是受限于 RemoteViews ,导致有很多限制!也看到了其实 Glance 没有魔法,所谓的魔法也都是进行了封装而已。其实最好的方案就是 RemoteViews 不再限制特定的布局及控件,也无需使用 Glance ,而是直接使用 Compose !但目前来看只是设想,实现起来还是比较困难的。

本文中看的其实只是 Glance 的一整套流程,里面还有很多地方没有写出来,比如 Compose 中大名鼎鼎的快照系统,比如小部件中难以实现、但 Glance 中却很简单就能实现的 ListView 等等。RemoteViews 支持的控件这里咱们只看了最简单的 Button ,大家感兴趣的话可以都去看看,这块代码写的还是挺好的!

本篇文章就到这里吧,其实有的地方还想说的更细一些,但即使略过了很多,还是已经篇幅不短了。。。如果文章对你有帮助的话,还请点赞关注收藏!

你可能感兴趣的:(探索 Jetpack Glance 的魔法)