之前写过两篇关于 Jetpack Glance
的文章,分别是第一个 alpha
版本:Jetpack Glance?小部件的春天来了,以及第一个 release
版本发布时写的:稳定的 Glance 来了,安卓小部件有救了!
前世
大家都知道,小部件是运行在桌面中的,并不是运行在自己的应用中,那么数据的传输就涉及到了跨进程,Google
专门为这些需要跨进程绘制布局的需求写了一个名叫 RemoteViews
的类,比如 Notification
、Widget
等,大家千万不要被它名字影响力,虽然它叫 View
,但它并不是一个 View
。。。
kotlin
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
像大家熟知的 RecyclerView
、EditText
、SeekBar
、Spinner
等等都是不支持的哈。有人可能会想,那我自定义 View
不得了,不好意思,也不可以。。。还有人会想,那我继承它支持的控件,然后自定义不得了,Sorry,还是不可以。。。RemoteViews
只是描述了可在另一个进程中显示的视图层次结构的类,层次结构是从布局资源文件中加载出来的,该类只提供了一些基本操作来修改布局中的内容。简单来看一个例子大家就知道了:
arduino
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
只更新界面。
less
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
函数加载布局到布局容器中,然后调用 RemoteViews
的 performApply
函数执行保存的 Action
,
ini
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
函数,从上面我们直到此处保存的是,接下来就看下 Action
中 apply
函数;
java
@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
在跨进程设置后,通过调用 apply
和 reapply
加载和更新布局,完成后从遍历所有的 Action
,然后执行其 apply
函数,最后在 apply
函数中,根据保存的函数名和参数,反射执行函数修改界面。到这里就可以解释为啥不能使用自定义 View
或者别的控件了。
今生
了解了 RemoteViews
的大概工作流程之后,来看下直接使用 RemoteViews
创建 Widget
布局的代码吧:
kotlin
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
,但目前也只是 Widget
,Notification
是不支持的哈。
kotlin
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
在上一篇文章中也提到过,简单说了下子类需要实现的以及可以重写的函数,但里面的具体实现都没有提到,上一篇文章主要还是使用为主,单纯使用的话光看上一篇其实够用。接下来详细来看看吧!
kotlin
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
这个东西很重要!因为不管是update
、delete
,还是 resize
等,全部都和它相关!那就不得不关注下了!
kotlin
@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
来处理它们的组合。可以看到在 GlanceAppWidget
中 SessionManager
是调用了 GlanceSessionManager
,那它应该就是 SessionManager
的实现类了,咱们来看看:
kotlin
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
val GlanceSessionManager: SessionManager = SessionManagerImpl(SessionWorker::class.java)
嘿,果然,和咱们想的一致,有一个实现类名字叫 SessionManagerImpl
,并且传入了一个参数,看名称应该是一个 Work
,咱们接着往下追,先看看 SessionManagerImpl
的实现:
kotlin
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
又是啥啊?
kotlin
@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
吧!
kotlin
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 目前还用不到它,可以看到这块调用了 Session
中 provideGlance
,但 Session
是一个接口,所以需要看看它的实现类。
AppWidgetSession
AppWidgetSession
就是 Session
的实现类,来一起看下吧:
kotlin
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
中将 appWidgetManager
、minSize
等内容保存起来,然后向 GlanceSession
提供 Compose
方式的布局,挂起直到 Session
关闭。
processEmittableTree
这个函数内容是比较多的,咱们慢慢来看,首先参数 root
使用的就是 createRootEmittable
函数创建的 RemoteViewsRoot
,刚才咱们也看到了在 SessionWorker
中调用到了,然后创建一个 LayoutConfiguration
,从文件中检索已知的布局,再获取下小部件的信息,然后调用 normalizeCompositionTree
函数将 Composition
都合成一个树,之后遍历 Emittable
树并更新所有 LambdaActions
的键,再根据现有信息创建出咱们熟知的 RemoteViews
,最后调用了咱们同样熟知的 appWidgetManager.updateAppWidget
来对小部件进行刷新。
到这里其实已经简单走了一遍 Glance
刷新的流程,但。。。感觉缺少点什么,是找到了 RemoteViews
,那里面写的那些 Compose
方式的布局哪去了,比如 Text
、Button
等等。。。
花明
还记得上面一直提到的 Emittable
么,类似的还有 EmittableWithChildren
,还有上面转换为 RemoteViews
的 translateComposition
函数还没看上面留下的疑惑在这些地方就能解决!
Emittable
首先来看下 Emittable
吧:
kotlin
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
interface Emittable {
var modifier: GlanceModifier
fun copy(): Emittable
}
可以看到这就是一个接口,但这个接口中有一个 Glance
中熟悉的 GlanceModifier
,看来是找对地方了!
再来看下 EmittableWithChildren
:
kotlin
@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
支持的控件不多,但也不少,同样地,这里咱们也随便挑一个来看吧:
kotlin
@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
需要使用到的参数,比如 Text
、colors
等。咱们接着往上看:
kotlin
@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
,咱们先来简单看下:
less
@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
节点的函数,看参数也能理解,一个有子布局,另一个没有,对应地就是一个表示控件,另一个表示布局。
接下来咱们再往上看:
kotlin
@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
函数没看呢!谜底应该就在这里了!
kotlin
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
上面提到过了,但还没有看到它是什么,来看下吧:
kotlin
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
的索引一块传入这个同名函数。
kotlin
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
,这其实就是 Glance
对 RemoteViews
的一层封装,然后接着使用 translateChild
来将 Compose
方式写的布局及控件给加载出来!
面纱
到这里终于看到了希望,createRootView
和 translateChild
两个函数中就有我们想要看到的东西!不过在看这两个函数前还要先来看下 Glance
对 RemoteViews
的封装 RemoteViewsInfo
!
kotlin
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
:
kotlin
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
吧!
kotlin
internal fun applySimpleWidthModifier(
context: Context,
rv: RemoteViews,
modifier: WidthModifier,
viewId: Int,
) {
val width = modifier.width
setViewWidth(rv, viewId, width)
}
OK,接着来看 setViewWidth
:
kotlin
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
函数了!
scss
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
!
kotlin
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.Button
,LayoutType
就是一个枚举类,里面同样定义了 RemoteViews
中可用的控件及布局,来看下吧:
kotlin
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
吧:
kotlin
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
中特定的修饰符来修改对应的配置!
具体来看看吧!
kotlin
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
函数!
kotlin
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
中的内容取出来,然后设置到对应的布局和控件中!同样地来看一个例子吧:
kotlin
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
,大家感兴趣的话可以都去看看,这块代码写的还是挺好的!
本篇文章就到这里吧,其实有的地方还想说的更细一些,但即使略过了很多,还是已经篇幅不短了。。。如果文章对你有帮助的话,还请点赞关注收藏!