穩(wěn)定的 Glance 來了,安卓小部件有救了!

穩(wěn)定的 Glance 來了,安卓小部件有救了!

穩(wěn)定版本的 Glance 終于發(fā)布了,來一起看看吧,看看這一路的旅程,看看好用么,再看看如何使用!

前世今生

故事發(fā)生在兩年的一天吧,其實(shí)夸張了,不到兩年,而是 633 天前。。。

image.png

Jetpack 的更新網(wǎng)站上發(fā)現(xiàn)多了一個(gè)名叫 Glance 的庫,版本為 1.1.0-alpha01,發(fā)現(xiàn)這個(gè)庫后就趕快點(diǎn)擊進(jìn)去看看是干啥用的:

image.png

看到這個(gè)庫的簡介的時(shí)候給我高興壞了,大致意思是:可以使用 Compose 風(fēng)格的 API 來為小部件構(gòu)建布局。然后就嘗試了下并寫了一篇文章:Jetpack Glance?小部件的春天來了

小部件這個(gè)東西雖然是安卓中首先發(fā)布的,但是這么多年來一直平平無奇,直到蘋果 IOS 中也“推出”了小部件之后,才喚起了小部件的第二春,然后安卓官方、也就是谷歌才想起來自己原來也有這么個(gè)東西,就在 Android 12 中才對(duì)小部件做了一些改進(jìn),不容易啊,這么多年來第一次給安卓小部件增加了一些內(nèi)容。。。

之后接著官方也看不下去了,看不下去什么呢?多年前的安卓開發(fā)使用起小部件沒有問題,但是現(xiàn)在的安卓開發(fā)變?yōu)榱?Compose ,而小部件還是只能使用 XML ,于是乎,Glance 應(yīng)運(yùn)而生!

短短幾行字,基本聊了下 Glance 的前世今生,一個(gè)庫,要 635 天才能從 alpha 版本變?yōu)?stable,如果再加上第一個(gè) alpha 版本的開發(fā)時(shí)間的話,肯定超過了兩年。。。這個(gè)速度如果放到國內(nèi)的話。。。。算了,大家理解就好。其實(shí)也不能怪他們,Jetpack 中的庫實(shí)在是太多了,都需要時(shí)間和人力維護(hù)嘛!

下面再來看一下 Glance 的發(fā)布時(shí)間線吧:

image.png

沒有辜負(fù)我這么久的等待,哈哈哈!

之前那篇文章使用的是我寫的一個(gè)天氣,這回改下,改為使用 “玩安卓” 吧!

本文中的代碼地址:玩安卓 Github:https://github.com/zhujiang521/PlayAndroid

添加依賴

dependencies {
    implementation "androidx.glance:glance:1.0.0"
}

android {
   buildFeatures {
       compose true
   }

   composeOptions {
       kotlinCompilerExtensionVersion = "1.5.3"
   }
}

依賴添加很簡單,如果你的項(xiàng)目中有 Compose 的話,只需要添加下 dependencies 中的內(nèi)容即可。

創(chuàng)建小部件

首先來創(chuàng)建一個(gè)小部件,大家都知道,小部件其實(shí)就是一個(gè) BroadcastReceiver,所以需要在 AndroidManifest 中聲明下:

<receiver
    android:name=".widget.ArticleListWidget"
    android:exported="false">
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    </intent-filter>

    <meta-data
        android:name="android.appwidget.provider"
        android:resource="@xml/article_list_widget_info" />
</receiver>

上面的代碼大部分大家都很熟悉了,唯一和普通廣播不同的就是多了一個(gè)配置項(xiàng),如果寫過小部件的應(yīng)該也很熟悉了:

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/app_widget_description"
    android:initialKeyguardLayout="@layout/glance_default_loading_layout"
    android:initialLayout="@layout/glance_default_loading_layout"
    android:minWidth="110dp"
    android:minHeight="69dp"
    android:minResizeWidth="110dp"
    android:minResizeHeight="69dp"
    android:resizeMode="horizontal|vertical"
    android:targetCellWidth="2"
    android:targetCellHeight="2"
    android:updatePeriodMillis="86400000"
    android:widgetCategory="home_screen" />

這里的配置項(xiàng)其實(shí)不少,上面所列舉的只是常用的一些,那到底都可以配置那些項(xiàng)呢?點(diǎn)進(jìn)去看看不得了!

<declare-styleable name="AppWidgetProviderInfo">
    <!-- AppWidget的最小寬度 -->
    <attr name="minWidth"/>
    <!-- AppWidget的最小高度 -->
    <attr name="minHeight"/>
    <!-- AppWidget可以調(diào)整大小的最小寬度. -->
    <attr name="minResizeWidth" format="dimension"/>
    <!-- AppWidget可以調(diào)整大小的最小高度. -->
    <attr name="minResizeHeight" format="dimension"/>
    <!-- AppWidget可以調(diào)整大小的最大寬度. -->
    <attr name="maxResizeWidth" format="dimension"/>
    <!-- AppWidget可以調(diào)整大小的最大高度. -->
    <attr name="maxResizeHeight" format="dimension"/>
    <!-- AppWidget的默認(rèn)寬度,以桌面網(wǎng)格單元為單位 -->
    <attr name="targetCellWidth" format="integer"/>
    <!-- AppWidget的默認(rèn)高度,以桌面網(wǎng)格單元為單位 -->
    <attr name="targetCellHeight" format="integer"/>
    <!-- 更新周期(以毫秒為單位),如果AppWidget將更新自己,則為0 -->
    <attr name="updatePeriodMillis" format="integer" />
    <!-- 初始布局的資源id -->
    <attr name="initialLayout" format="reference" />
    <!-- 初始Keyguard布局的資源id -->
    <attr name="initialKeyguardLayout" format="reference" />
    <!-- 要啟動(dòng)配置的AppWidget包中的類名。如果沒有提供,則不會(huì)啟動(dòng)任何活動(dòng) -->
    <attr name="configure" format="string" />
    <!-- 在可繪制的資源id中預(yù)覽AppWidget配置后的樣子。如果沒有提供,則將使用AppWidget的圖標(biāo) -->
    <attr name="previewImage" format="reference" />
    <!-- 預(yù)覽AppWidget配置后的樣子的布局資源id。與previewImage不同,previewLayout可以更好地在不同的區(qū)域、系統(tǒng)主題、顯示大小和密度等方面展示AppWidget。如果提供了,它將優(yōu)先于支持的小部件主機(jī)上的previewImage。否則,將使用previewImage -->
    <attr name="previewLayout" format="reference" />
    <!-- AppWidget子視圖的視圖id,應(yīng)該是自動(dòng)高級(jí)的。通過小部件的主機(jī) -->
    <attr name="autoAdvanceViewId" format="reference" />
    <!-- 可選參數(shù),指示是否以及如何調(diào)整此小部件的大小。支持使用|運(yùn)算符組合值,也就是說可以橫向和縱向可以同時(shí)使用 -->
    <attr name="resizeMode" format="integer">
        <flag name="none" value="0x0" />
        <flag name="horizontal" value="0x1" />
        <flag name="vertical" value="0x2" />
    </attr>
    <!-- 可選參數(shù),指示可以顯示此小部件的位置,即。主屏幕,鍵盤保護(hù),搜索欄或其任何組合. -->
    <attr name="widgetCategory" format="integer">
        <flag name="home_screen" value="0x1" />
        <flag name="keyguard" value="0x2" />
        <flag name="searchbox" value="0x4" />
    </attr>
    <!-- 指示小部件支持的各種特性的標(biāo)志。這些是對(duì)小部件主機(jī)的提示,實(shí)際上并不改變小部件的行為 -->
    <attr name="widgetFeatures" format="integer">
        <!-- 小部件可以在綁定后隨時(shí)重新配置 -->
        <flag name="reconfigurable" value="0x1" />
        <!-- 小部件由應(yīng)用程序直接添加,不需要出現(xiàn)在可用小部件的全局列表中 -->
        <flag name="hide_from_picker" value="0x2" />
        <!-- 小部件提供了一個(gè)默認(rèn)配置。主機(jī)可能決定不啟動(dòng)所提供的配置活動(dòng) -->
        <flag name="configuration_optional" value="0x4" />
    </attr>
    <!-- 包含小部件簡短描述的字符串的資源標(biāo)識(shí)符 -->
    <attr name="description" />
</declare-styleable>

由于配置項(xiàng)確實(shí)不少,所以直接寫了下注釋,大家根據(jù)需求進(jìn)行使用即可,目前這是所有的小部件配置項(xiàng),有一些是在 Android 12 中新增的。

工欲善其事,必先利其器

配置項(xiàng)寫好了,接下來該編寫小部件的代碼了!

GlanceAppWidgetReceiver

之前編寫小部件的時(shí)候都會(huì)用到 AppWidgetProvider ,它繼承自 BroadcastReceiver ,但現(xiàn)在使用 Glance 需要繼承 GlanceAppWidgetReceiver ,那么 GlanceAppWidgetReceiver 是個(gè)啥?來,3、2、1,上代碼!

abstract class GlanceAppWidgetReceiver : AppWidgetProvider() {

    ......

    /**
     * 用于生成AppWidget并將其發(fā)送給AppWidgetManager的GlanceAppWidget的實(shí)例
     * 注意:這不會(huì)為GlanceAppWidget設(shè)置CoroutineContext,它將始終在主線程上運(yùn)行。
     */
    abstract val glanceAppWidget: GlanceAppWidget
    
    ......
}

通過上面代碼可以看出 GlanceAppWidgetReceiver 繼承自 AppWidgetProvider ,是一個(gè)抽象類,并且需要實(shí)現(xiàn)一個(gè)抽象函數(shù) glanceAppWidget ,這個(gè)函數(shù)需要返回的對(duì)象為 GlanceAppWidget 。

GlanceAppWidget

那就再來看下 GlanceAppWidget 吧,來,3、2、1,上代碼!

abstract class GlanceAppWidget(
    @LayoutRes
    internal val errorUiLayout: Int = R.layout.glance_error_layout,
) {

    ......
  
    /**
     * 重寫此函數(shù)以提供 Glance Composable
     */
    abstract suspend fun provideGlance(
        context: Context,
        id: GlanceId,
    )

    /**
     * 定義對(duì)大小的處理。
     */
    open val sizeMode: SizeMode = SizeMode.Single

    /**
     * 特定于視圖的小部件數(shù)據(jù)的數(shù)據(jù)存儲(chǔ)。
     */
    open val stateDefinition: GlanceStateDefinition<*>? = PreferencesGlanceStateDefinition

    /**
     * 當(dāng)應(yīng)用程序小部件從其主機(jī)上刪除時(shí)由框架調(diào)用。當(dāng)該方法返回時(shí),與glanceId關(guān)聯(lián)的狀態(tài)將被刪除。
     */
    open suspend fun onDelete(context: Context, glanceId: GlanceId) {}
  
    ......
}

可以看到 GlanceAppWidget 也是一個(gè)抽象類,構(gòu)建這個(gè)類時(shí)有一個(gè)可選參數(shù),意思是遇到錯(cuò)誤時(shí)需要展示的布局。然后有幾個(gè)子類可以重寫的函數(shù),還有一個(gè)必須實(shí)現(xiàn)的抽象函數(shù),下面來分別看下吧:

  • provideGlance 此函數(shù)為抽象函數(shù),子類必須重寫;重寫此函數(shù)以提供 Glance Composable,也就是說這個(gè)函數(shù)是用來編寫布局的。一旦數(shù)據(jù)準(zhǔn)備好,使用 provideContent 提供可組合對(duì)象。provideGlance 作為 CoroutineWorker 在后臺(tái)運(yùn)行,以響應(yīng) updateupdateAll 的調(diào)用,以及來自Launcher 的請(qǐng)求。在 provideContent 被調(diào)用之前,provideGlance 受限于 WorkManager 時(shí)間限制(目前為十分鐘),在調(diào)用 provideContent 之后,組合繼續(xù)運(yùn)行并重新組合大約45秒。當(dāng)接收到UI交互或更新請(qǐng)求時(shí),會(huì)添加額外的時(shí)間來處理這些請(qǐng)求。需要注意的是:如果 provideGlance 已經(jīng)在運(yùn)行,updateupdateAll 不會(huì)重新啟動(dòng)。因此應(yīng)該在調(diào)用 provideContent 之前加載初始數(shù)據(jù),然后在組合中觀察數(shù)據(jù)源(例如 collectasstate)。這可以確保小部件在組合處于活動(dòng)狀態(tài)時(shí)繼續(xù)更新,當(dāng)從應(yīng)用程序的其他地方更新數(shù)據(jù)源時(shí),確保調(diào)用update,以防這個(gè)小部件的Worker當(dāng)前沒有運(yùn)行。
  • sizeMode 定義對(duì)小部件大小的處理,這個(gè)會(huì)在下面展開來說
  • stateDefinition 特定于視圖的小部件數(shù)據(jù)的數(shù)據(jù)存儲(chǔ),當(dāng)存儲(chǔ)數(shù)據(jù)發(fā)生變化時(shí),小部件會(huì)進(jìn)行刷新
  • onDelete 應(yīng)用程序小部件從其主機(jī)上刪除時(shí)由框架調(diào)用。當(dāng)該方法返回時(shí),與glanceId關(guān)聯(lián)的狀態(tài)將被刪除。

SizeMode

OK,上面簡單看了下 GlanceAppWidget 中的公開函數(shù),接下來看下 SizeMode ,老規(guī)矩,3、2、1,上代碼!

sealed interface SizeMode {
    /**
     * GlanceAppWidget提供了一個(gè)UI。LocalSize將是AppWidget的最小尺寸,在AppWidget提供程序信息中定義,單個(gè)
     */
    object Single : SizeMode {
        override fun toString(): String = "SizeMode.Single"
    }

    /**
     * 為每個(gè)AppWidget可能顯示的大小提供了一個(gè)UI。大小列表由選項(xiàng)包提供(參見getAppWidgetOptions)。每個(gè)大小都將調(diào)用可組合對(duì)象。在調(diào)用期間,LocalSize將是生成UI的對(duì)象。
     */
    object Exact : SizeMode {
        override fun toString(): String = "SizeMode.Exact"
    }

    /**
     * 在Android 12及以后的版本中,每個(gè)提供的大小將調(diào)用一次composable,并且從大小到視圖的映射將被發(fā)送到系統(tǒng)。然后框架將根據(jù)App Widget的當(dāng)前大小來決定顯示哪個(gè)視圖。在Android 12之前,composable將被調(diào)用用于顯示App Widget的每個(gè)大小(如Exact)。對(duì)于每種尺寸,將選擇最佳視圖,即適合可用空間的最大視圖,或者如果不適合則選擇最小視圖。Params: sizes -要使用的大小列表,不能為空。
     */
    class Responsive(val sizes: Set<DpSize>) : SizeMode {

        init {
            require(sizes.isNotEmpty()) { "The set of sizes cannot be empty" }
        }

        ......
    }
}

可以看到 SizeMode 是一個(gè)接口,一共有三個(gè)類實(shí)現(xiàn)了 SizeMode 接口,SingleExact 好理解一些,Responsive 不太好理解,但是還記得 Android 12 中小部件的更新么?RemoteView 增加了一個(gè)構(gòu)造函數(shù),來看下吧:

public RemoteViews(@NonNull Map<SizeF, RemoteViews> remoteViews)

即每個(gè)提供的大小將調(diào)用一次 composable ,并且從大小到視圖的映射將被發(fā)送到系統(tǒng),也就是說會(huì)將定義好的大小做緩存,可以優(yōu)化小部件的展示。

愛碼士

上面說了半天還沒進(jìn)入正題,一行正經(jīng)代碼都還沒寫。。。

先來搞一個(gè) GlanceAppWidget 吧:

class ArticleListWidgetGlance : GlanceAppWidget() {

    override suspend fun provideGlance(context: Context, id: GlanceId) {
        provideContent {
            // 編寫 Glance 代碼
        }
    }

}

預(yù)料之中,繼承自 GlanceAppWidget ,實(shí)現(xiàn)抽象函數(shù) provideGlance ,但還是無法在 provideGlance 中直接使用 Glance 來編寫 Compose 風(fēng)格的布局,還需要調(diào)用 provideContent ,上面其實(shí)也提到過了,那就來看下 provideContent 吧,3、2、1,上代碼!

suspend fun GlanceAppWidget.provideContent(
    content: @Composable @GlanceComposable () -> Unit
): Nothing {
    coroutineContext[ContentReceiver]?.provideContent(content)
        ?: error("provideContent requires a ContentReceiver and should only be called from " + "GlanceAppWidget.provideGlance")
}

可以看到這是一個(gè)擴(kuò)展函數(shù),只有一個(gè)參數(shù),看到這個(gè)參數(shù)是不是就理解了,終于看到了咱們熟悉的 @Composable ,需要注意的是:如果此函數(shù)與自身并發(fā)調(diào)用,則前一個(gè)調(diào)用將拋出 CancellationException,新內(nèi)容將替換它。還有就是這個(gè)函數(shù)只能從 GlanceAppWidget.provideGlance 調(diào)用。

OK,GlanceAppWidget 編好了之后就該寫下 GlanceAppWidgetReceiver 了,上代碼!

class ArticleListWidget : GlanceAppWidgetReceiver() {
    override val glanceAppWidget: GlanceAppWidget = ArticleListWidgetGlance()
}

更簡單了,只有三行代碼,同樣地,也實(shí)現(xiàn)了 GlanceAppWidgetReceiver 的抽象函數(shù),并返回了剛創(chuàng)建好的 ArticleListWidget 。

其實(shí)到這里為止 Glance 的整套流程就簡單跑通了。接下來就來編寫下布局吧:

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

??!熟悉的配方!熟悉的味道!

爽,爽,爽

看著上面熟悉的味道是不是很舒服,哈哈哈,寫小部件終于也可以優(yōu)雅一些了!

耗時(shí)操作優(yōu)化

不知道大家注意到?jīng)]有,provideGlance 竟然是一個(gè)掛起函數(shù),這是什么意思,難道是???

沒錯(cuò)!可以放心地在這里執(zhí)行耗時(shí)操作了!比如你就可以這樣:

override suspend fun provideGlance(context: Context, id: GlanceId) {
    val name  = getName()
    provideContent {
        Text(text = name)
    }
}

private suspend fun getName():String {
    delay(5000L)
    return "我愛你啊"
}

下面來運(yùn)行看下效果!

image.png

是不是挺好,解決了小部件的一大坑!

小部件更新

小部件的更新一直也是個(gè)問題,比如橫豎屏轉(zhuǎn)換后小部件的刷新、系統(tǒng)配置修改了之后的刷新,這些都是沒有的,系統(tǒng)應(yīng)用可以和系統(tǒng)進(jìn)行一些騷操作,但是普通應(yīng)用不可以啊,所以 Glance 中就引入了 WorkManager 來改善這個(gè)問題,最低可以設(shè)置十分鐘的間隔刷新。

下面就來簡單看下使用吧:

class WorkWorker(
    private val context: Context,
    workerParameters: WorkerParameters
) : CoroutineWorker(context, workerParameters) {

    companion object {

        private val uniqueWorkName = WorkWorker::class.java.simpleName

        // 排隊(duì)進(jìn)行工作
        fun enqueue(context: Context, size: DpSize, glanceId: GlanceId, force: Boolean = false) {
            val manager = WorkManager.getInstance(context)
            val requestBuilder = OneTimeWorkRequestBuilder<WorkWorker>().apply {
                addTag(glanceId.toString())
                setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
                setInputData(
                    Data.Builder()
                        .putFloat("width", size.width.value.toPx)
                        .putFloat("height", size.height.value.toPx)
                        .putBoolean("force", force)
                        .build()
                )
            }
            val workPolicy = if (force) {
                ExistingWorkPolicy.REPLACE
            } else {
                ExistingWorkPolicy.KEEP
            }

            manager.enqueueUniqueWork(
                uniqueWorkName + size.width + size.height,
                workPolicy,
                requestBuilder.build()
            )
        }

        /**
         * 取消任何正在進(jìn)行的工作
         */
        fun cancel(context: Context, glanceId: GlanceId) {
            WorkManager.getInstance(context).cancelAllWorkByTag(glanceId.toString())
        }
    }

    override suspend fun doWork(): Result {
        // 需要執(zhí)行的操作
        return Result.success()
    }
}

OK,先創(chuàng)建了一個(gè) Work,然后看下在 Glance 中如何使用吧!

override suspend fun onDelete(context: Context, glanceId: GlanceId) {
    super.onDelete(context, glanceId)
    WorkWorker.cancel(context, glanceId)
}

override suspend fun provideGlance(context: Context, id: GlanceId) {
    provideContent {
        val size = LocalSize.current
        GlanceTheme {
            CircularProgressIndicator()
            // 在合成完成后,使用glanceId作為標(biāo)記為worker排隊(duì),以便在小部件實(shí)例被刪除的情況下取消所有作業(yè)
            val glanceId = LocalGlanceId.current
            SideEffect {
                WorkWorker.enqueue(context, size, glanceId)
            }
        }
    }
}

很簡單,在 provideGlance 中排隊(duì)執(zhí)行操作,然后在 onDelete 中將 Work 取消了即可。

便捷的 ListView

寫過小部件的都知道 ListView 特別坑,原生小部件想要實(shí)現(xiàn) ListView 需要實(shí)現(xiàn) FactoryService 等,而在 Glance 這里直接兩三行代碼搞定。

LazyColumn(
    modifier = GlanceModifier.fillMaxSize().padding(horizontal = 10.dp)
) {
    items(articleList) { data ->
        GlanceArticleItem(context, data)
    }
}

沒錯(cuò),和 Compose 中一樣,名字也一樣,都是 LazyColumn ,寫起來非常便捷。

更方便的 LocalXXX

大家都知道 Compose 中的 LocalXXX 非常方便好用,Glance 中也提供了一些:

/**
 * 生成的概覽視圖的大小。概覽視圖至少有那么多空間可以顯示。確切的含義可能會(huì)根據(jù)表面及其配置方式而變化。
 */
val LocalSize = staticCompositionLocalOf<DpSize> { error("No default size") }

/**
 * 生成概覽視圖時(shí)應(yīng)用程序的上下文。
 */
val LocalContext = staticCompositionLocalOf<Context> { error("No default context") }

/**
 * 本地視圖狀態(tài),在surface實(shí)現(xiàn)中定義。用于特定于視圖的狀態(tài)數(shù)據(jù)的可定制存儲(chǔ)。
 */
val LocalState = compositionLocalOf<Any?> { null }

/**
 * 當(dāng)前合成生成的概覽視圖的唯一Id。
 */
val LocalGlanceId = staticCompositionLocalOf<GlanceId> { error("No default glance id") }

不過這塊需要注意包的導(dǎo)入問題。

Action

小部件中之前如果想要實(shí)現(xiàn)點(diǎn)擊效果的話只能使用 PendingIntent ,這樣很麻煩,現(xiàn)在 Glance 為我們提供了 Action ,使用方法如下:

Button(text = "Glance按鈕", onClick = actionStartActivity(ComponentName("包名","包名+類名")))
Button(text = "Glance按鈕", onClick = actionStartActivity<MainActivity>())
Button(text = "Glance按鈕", onClick = actionStartActivity(MainActivity::class.java))

不僅如此,還可以像下面這樣操作:

Text(text = "點(diǎn)擊", modifier = GlanceModifier.clickable {
    Log.e("TAG", "provideGlance: click")
})

這個(gè)實(shí)在是太方便了!推薦大家使用。但這個(gè)需要注意,如果想使用這個(gè)實(shí)現(xiàn)動(dòng)畫效果的話是不行的,因?yàn)樗鼪]有辦法在特別短的時(shí)間內(nèi)刷新,我之前嘗試過 Compose 中的屬性動(dòng)畫 animate*AsState ,結(jié)果就是只執(zhí)行了最后的結(jié)果,中間過程全部忽略了。。。。

坑,坑,坑

“人家官方廢了這么大勁開發(fā)出來的庫,怎么能說人家坑呢?”

“因?yàn)樗_實(shí)坑?。 ?/p>

坑一

剛才看到的熟悉的代碼,其實(shí)一點(diǎn)也不熟悉,為什么這么說,來看下導(dǎo)入的包就知道了:

import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import androidx.glance.GlanceModifier
import androidx.glance.GlanceTheme
import androidx.glance.action.Action
import androidx.glance.action.clickable
import androidx.glance.appwidget.action.actionStartActivity
import androidx.glance.appwidget.cornerRadius
import androidx.glance.background
import androidx.glance.layout.Alignment
import androidx.glance.layout.Column
import androidx.glance.layout.Row
import androidx.glance.layout.Spacer
import androidx.glance.layout.fillMaxWidth
import androidx.glance.layout.height
import androidx.glance.layout.padding
import androidx.glance.layout.size
import androidx.glance.layout.wrapContentWidth
import androidx.glance.text.Text

雖然 Composable 還是使用的 Compose 的,但是里面的可組合項(xiàng)全部是 Glance 中重寫的。。。。

咱就是說??!有沒有一種可能,就是你在寫的時(shí)候自然地就導(dǎo)入了 Compose 的包?運(yùn)行直接報(bào)錯(cuò)!也沒有任何提醒。。

image.png

是不是?沒有一點(diǎn)提醒,這種情況官方有沒有一種可能,就是像是 Glance 中的 Modifier 一樣,也在前面加一個(gè)前綴,讓開發(fā)者能夠容易區(qū)分一點(diǎn)?即使加前綴不好看,你們不想加,有沒有可能修改下編譯器,讓編譯器告訴開發(fā)者不能這么寫行不行?

坑二

圖片的加載,圖片是安卓開發(fā)中太常見的東西了,以前咱們使用 ImageView 來進(jìn)行圖片的展示,現(xiàn)在有了 Compose 了我們使用 Image 來進(jìn)行展示,Glance 中同樣是使用 Image 來展示,來玩?zhèn)€游戲吧,找不同!先來看下 Compose 中的 Image

@Composable
fun Image(
    painter: Painter,
    contentDescription: String?,
    modifier: Modifier = Modifier,
    alignment: Alignment = Alignment.Center,
    contentScale: ContentScale = ContentScale.Fit,
    alpha: Float = DefaultAlpha,
    colorFilter: ColorFilter? = null
)

再來看下 Glance 中的 Image

@Composable
fun Image(
    provider: ImageProvider,
    contentDescription: String?,
    modifier: GlanceModifier = GlanceModifier,
    contentScale: ContentScale = ContentScale.Fit,
    colorFilter: ColorFilter? = null
)

是不是很像,但是 Glance 因?yàn)?RemoteView 的限制少了一些功能,在 Compose 中咱們可以通過 painterResource 來構(gòu)建出 Painter,但在 Glance 中又換了個(gè)名字 ImageProvider ,咱就是說啊,有沒有一種可能,就是要不你就都學(xué) Compose ,要不你就都不學(xué)。。。。

還有就是文字,來看下 Glance 中的 Text 吧:

@Composable
fun Text(
    text: String,
    modifier: GlanceModifier = GlanceModifier,
    style: TextStyle = defaultTextStyle,
    maxLines: Int = Int.MAX_VALUE,
)

雖然 Compose 中的 Text 接收的也是一個(gè) String,但是人家有 stringResource 函數(shù)啊,你呢。。。忘寫了么?

算了,自己寫一個(gè)吧:

@Composable
fun stringResource(@StringRes id: Int): String {
    return LocalContext.current.getString(id)
}

這個(gè)函數(shù)我個(gè)人覺得可以放到 Glance 中。。。。

總結(jié)

今天所講的 Glance 其實(shí)也是基于 Compose 的,由此可見,Google 現(xiàn)在對(duì) Compose 發(fā)力非常足,如果大家想系統(tǒng)地學(xué)習(xí) Compose 的話,可以購買我的新書《Jetpack Compose:Android全新UI編程》進(jìn)行閱讀,里面有完整的 Compose 框架供大家學(xué)習(xí)。

京東購買地址

當(dāng)當(dāng)購買地址

本文中的代碼地址:玩安卓 Github:https://github.com/zhujiang521/PlayAndroid

如果對(duì)你有幫助的話,別忘記點(diǎn)個(gè) Star,感激不盡,大家如果有疑問的話可以在評(píng)論區(qū)提出來。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容