之前写过两篇关于 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<Action>();  
   }
  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<out ListenableWorker>
) : SessionManager {
  private val sessions = mutableMapOf<String, Session>()
​
  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<Any>(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<Any?>(null, neverEqualPolicy())
  private val options = mutableStateOf(Bundle(), neverEqualPolicy())
  private var lambdas = mapOf<String, List<LambdaAction>>()
​
  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<Emittable> = mutableListOf<Emittable>()
​
  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 <T : Emittable> GlanceNode(
  noinline factory: () -> T,
  update: @DisallowComposableCalls Updater<T>.() -> Unit
) {
  ComposeNode<T, Applier>(factory, update)
}
​
@Composable
inline fun <T : Emittable> GlanceNode(
  noinline factory: () -> T,
  update: @DisallowComposableCalls Updater<T>.() -> Unit,
  content: @Composable @GlanceComposable () -> Unit,
) {
  ComposeNode<T, Applier>(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<Emittable>,
  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<Int, Map<SizeSelector, Int>> = 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<WidthModifier>()?.let {
        applySimpleWidthModifier(context, this, it, R.id.rootView)
       }
      modifier.findModifier<HeightModifier>()?.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<PaddingModifier>() == 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<String>? =
      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 ,我们感兴趣的话能够都去看看,这块代码写的仍是挺好的!

本篇文章就到这儿吧,其实有的当地还想说的更细一些,但即便略过了许多,仍是现已篇幅不短了。。。假如文章对你有协助的话,还请点赞重视收藏!