稳定的 Glance 来了,安卓小部件有救了!

news2025/1/15 23:01:23

稳定的 Glance 来了,安卓小部件有救了!

稳定版本的 Glance 终于发布了,来一起看看吧,看看这一路的旅程,看看好用么,再看看如何使用!

前世今生

故事发生在两年的一天吧,其实夸张了,不到两年,而是 633 天前。。。

image.png

Jetpack 的更新网站上发现多了一个名叫 Glance 的库,版本为 1.1.0-alpha01,发现这个库后就赶快点击进去看看是干啥用的:

image.png

看到这个库的简介的时候给我高兴坏了,大致意思是:可以使用 Compose 风格的 API 来为小部件构建布局。然后就尝试了下并写了一篇文章:Jetpack Glance?小部件的春天来了

小部件这个东西虽然是安卓中首先发布的,但是这么多年来一直平平无奇,直到苹果 IOS 中也“推出”了小部件之后,才唤起了小部件的第二春,然后安卓官方、也就是谷歌才想起来自己原来也有这么个东西,就在 Android 12 中才对小部件做了一些改进,不容易啊,这么多年来第一次给安卓小部件增加了一些内容。。。

之后接着官方也看不下去了,看不下去什么呢?多年前的安卓开发使用起小部件没有问题,但是现在的安卓开发变为了 Compose ,而小部件还是只能使用 XML ,于是乎,Glance 应运而生!

短短几行字,基本聊了下 Glance 的前世今生,一个库,要 635 天才能从 alpha 版本变为 stable,如果再加上第一个 alpha 版本的开发时间的话,肯定超过了两年。。。这个速度如果放到国内的话。。。。算了,大家理解就好。其实也不能怪他们,Jetpack 中的库实在是太多了,都需要时间和人力维护嘛!

下面再来看一下 Glance 的发布时间线吧:

image.png

没有辜负我这么久的等待,哈哈哈!

之前那篇文章使用的是我写的一个天气,这回改下,改为使用 “玩安卓” 吧!

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

添加依赖

dependencies {
    implementation "androidx.glance:glance:1.0.0"
}
​
android {
   buildFeatures {
       compose true
   }
​
   composeOptions {
       kotlinCompilerExtensionVersion = "1.5.3"
   }
}

依赖添加很简单,如果你的项目中有 Compose 的话,只需要添加下 dependencies 中的内容即可。

创建小部件

首先来创建一个小部件,大家都知道,小部件其实就是一个 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>

上面的代码大部分大家都很熟悉了,唯一和普通广播不同的就是多了一个配置项,如果写过小部件的应该也很熟悉了:

<?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" />

这里的配置项其实不少,上面所列举的只是常用的一些,那到底都可以配置那些项呢?点进去看看不得了!

<declare-styleable name="AppWidgetProviderInfo">
    <!-- AppWidget的最小宽度 -->
    <attr name="minWidth"/>
    <!-- AppWidget的最小高度 -->
    <attr name="minHeight"/>
    <!-- AppWidget可以调整大小的最小宽度. -->
    <attr name="minResizeWidth" format="dimension"/>
    <!-- AppWidget可以调整大小的最小高度. -->
    <attr name="minResizeHeight" format="dimension"/>
    <!-- AppWidget可以调整大小的最大宽度. -->
    <attr name="maxResizeWidth" format="dimension"/>
    <!-- AppWidget可以调整大小的最大高度. -->
    <attr name="maxResizeHeight" format="dimension"/>
    <!-- AppWidget的默认宽度,以桌面网格单元为单位 -->
    <attr name="targetCellWidth" format="integer"/>
    <!-- AppWidget的默认高度,以桌面网格单元为单位 -->
    <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" />
    <!-- 要启动配置的AppWidget包中的类名。如果没有提供,则不会启动任何活动 -->
    <attr name="configure" format="string" />
    <!-- 在可绘制的资源id中预览AppWidget配置后的样子。如果没有提供,则将使用AppWidget的图标 -->
    <attr name="previewImage" format="reference" />
    <!-- 预览AppWidget配置后的样子的布局资源id。与previewImage不同,previewLayout可以更好地在不同的区域、系统主题、显示大小和密度等方面展示AppWidget。如果提供了,它将优先于支持的小部件主机上的previewImage。否则,将使用previewImage -->
    <attr name="previewLayout" format="reference" />
    <!-- AppWidget子视图的视图id,应该是自动高级的。通过小部件的主机 -->
    <attr name="autoAdvanceViewId" format="reference" />
    <!-- 可选参数,指示是否以及如何调整此小部件的大小。支持使用|运算符组合值,也就是说可以横向和纵向可以同时使用 -->
    <attr name="resizeMode" format="integer">
        <flag name="none" value="0x0" />
        <flag name="horizontal" value="0x1" />
        <flag name="vertical" value="0x2" />
    </attr>
    <!-- 可选参数,指示可以显示此小部件的位置,即。主屏幕,键盘保护,搜索栏或其任何组合. -->
    <attr name="widgetCategory" format="integer">
        <flag name="home_screen" value="0x1" />
        <flag name="keyguard" value="0x2" />
        <flag name="searchbox" value="0x4" />
    </attr>
    <!-- 指示小部件支持的各种特性的标志。这些是对小部件主机的提示,实际上并不改变小部件的行为 -->
    <attr name="widgetFeatures" format="integer">
        <!-- 小部件可以在绑定后随时重新配置 -->
        <flag name="reconfigurable" value="0x1" />
        <!-- 小部件由应用程序直接添加,不需要出现在可用小部件的全局列表中 -->
        <flag name="hide_from_picker" value="0x2" />
        <!-- 小部件提供了一个默认配置。主机可能决定不启动所提供的配置活动 -->
        <flag name="configuration_optional" value="0x4" />
    </attr>
    <!-- 包含小部件简短描述的字符串的资源标识符 -->
    <attr name="description" />
</declare-styleable>

由于配置项确实不少,所以直接写了下注释,大家根据需求进行使用即可,目前这是所有的小部件配置项,有一些是在 Android 12 中新增的。

工欲善其事,必先利其器

配置项写好了,接下来该编写小部件的代码了!

GlanceAppWidgetReceiver

之前编写小部件的时候都会用到 AppWidgetProvider ,它继承自 BroadcastReceiver ,但现在使用 Glance 需要继承 GlanceAppWidgetReceiver ,那么 GlanceAppWidgetReceiver 是个啥?来,3、2、1,上代码!

abstract class GlanceAppWidgetReceiver : AppWidgetProvider() {
​
    ......
​
    /**
     * 用于生成AppWidget并将其发送给AppWidgetManager的GlanceAppWidget的实例
     * 注意:这不会为GlanceAppWidget设置CoroutineContext,它将始终在主线程上运行。
     */
    abstract val glanceAppWidget: GlanceAppWidget
    
    ......
}

通过上面代码可以看出 GlanceAppWidgetReceiver 继承自 AppWidgetProvider ,是一个抽象类,并且需要实现一个抽象函数 glanceAppWidget ,这个函数需要返回的对象为 GlanceAppWidget

GlanceAppWidget

那就再来看下 GlanceAppWidget 吧,来,3、2、1,上代码!

abstract class GlanceAppWidget(
    @LayoutRes
    internal val errorUiLayout: Int = R.layout.glance_error_layout,
) {
​
    ......
  
    /**
     * 重写此函数以提供 Glance Composable
     */
    abstract suspend fun provideGlance(
        context: Context,
        id: GlanceId,
    )
​
    /**
     * 定义对大小的处理。
     */
    open val sizeMode: SizeMode = SizeMode.Single
​
    /**
     * 特定于视图的小部件数据的数据存储。
     */
    open val stateDefinition: GlanceStateDefinition<*>? = PreferencesGlanceStateDefinition
​
    /**
     * 当应用程序小部件从其主机上删除时由框架调用。当该方法返回时,与glanceId关联的状态将被删除。
     */
    open suspend fun onDelete(context: Context, glanceId: GlanceId) {}
  
    ......
}

可以看到 GlanceAppWidget 也是一个抽象类,构建这个类时有一个可选参数,意思是遇到错误时需要展示的布局。然后有几个子类可以重写的函数,还有一个必须实现的抽象函数,下面来分别看下吧:

  • provideGlance 此函数为抽象函数,子类必须重写;重写此函数以提供 Glance Composable,也就是说这个函数是用来编写布局的。一旦数据准备好,使用 provideContent 提供可组合对象。provideGlance 作为 CoroutineWorker 在后台运行,以响应 updateupdateAll 的调用,以及来自Launcher 的请求。在 provideContent 被调用之前,provideGlance 受限于 WorkManager 时间限制(目前为十分钟),在调用 provideContent 之后,组合继续运行并重新组合大约45秒。当接收到UI交互或更新请求时,会添加额外的时间来处理这些请求。需要注意的是:如果 provideGlance 已经在运行,updateupdateAll 不会重新启动。因此应该在调用 provideContent 之前加载初始数据,然后在组合中观察数据源(例如 collectasstate)。这可以确保小部件在组合处于活动状态时继续更新,当从应用程序的其他地方更新数据源时,确保调用update,以防这个小部件的Worker当前没有运行。
  • sizeMode 定义对小部件大小的处理,这个会在下面展开来说
  • stateDefinition 特定于视图的小部件数据的数据存储,当存储数据发生变化时,小部件会进行刷新
  • onDelete 应用程序小部件从其主机上删除时由框架调用。当该方法返回时,与glanceId关联的状态将被删除。

SizeMode

OK,上面简单看了下 GlanceAppWidget 中的公开函数,接下来看下 SizeMode ,老规矩,3、2、1,上代码!

sealed interface SizeMode {
    /**
     * GlanceAppWidget提供了一个UI。LocalSize将是AppWidget的最小尺寸,在AppWidget提供程序信息中定义,单个
     */
    object Single : SizeMode {
        override fun toString(): String = "SizeMode.Single"
    }
​
    /**
     * 为每个AppWidget可能显示的大小提供了一个UI。大小列表由选项包提供(参见getAppWidgetOptions)。每个大小都将调用可组合对象。在调用期间,LocalSize将是生成UI的对象。
     */
    object Exact : SizeMode {
        override fun toString(): String = "SizeMode.Exact"
    }
​
    /**
     * 在Android 12及以后的版本中,每个提供的大小将调用一次composable,并且从大小到视图的映射将被发送到系统。然后框架将根据App Widget的当前大小来决定显示哪个视图。在Android 12之前,composable将被调用用于显示App Widget的每个大小(如Exact)。对于每种尺寸,将选择最佳视图,即适合可用空间的最大视图,或者如果不适合则选择最小视图。Params: sizes -要使用的大小列表,不能为空。
     */
    class Responsive(val sizes: Set<DpSize>) : SizeMode {
​
        init {
            require(sizes.isNotEmpty()) { "The set of sizes cannot be empty" }
        }
​
        ......
    }
}

可以看到 SizeMode 是一个接口,一共有三个类实现了 SizeMode 接口,SingleExact 好理解一些,Responsive 不太好理解,但是还记得 Android 12 中小部件的更新么?RemoteView 增加了一个构造函数,来看下吧:

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

即每个提供的大小将调用一次 composable ,并且从大小到视图的映射将被发送到系统,也就是说会将定义好的大小做缓存,可以优化小部件的展示。

爱码士

上面说了半天还没进入正题,一行正经代码都还没写。。。

先来搞一个 GlanceAppWidget 吧:

class ArticleListWidgetGlance : GlanceAppWidget() {
​
    override suspend fun provideGlance(context: Context, id: GlanceId) {
        provideContent {
            // 编写 Glance 代码
        }
    }
​
}

预料之中,继承自 GlanceAppWidget ,实现抽象函数 provideGlance ,但还是无法在 provideGlance 中直接使用 Glance 来编写 Compose 风格的布局,还需要调用 provideContent ,上面其实也提到过了,那就来看下 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")
}

可以看到这是一个扩展函数,只有一个参数,看到这个参数是不是就理解了,终于看到了咱们熟悉的 @Composable ,需要注意的是:如果此函数与自身并发调用,则前一个调用将抛出 CancellationException,新内容将替换它。还有就是这个函数只能从 GlanceAppWidget.provideGlance 调用。

OK,GlanceAppWidget 编好了之后就该写下 GlanceAppWidgetReceiver 了,上代码!

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

更简单了,只有三行代码,同样地,也实现了 GlanceAppWidgetReceiver 的抽象函数,并返回了刚创建好的 ArticleListWidget

其实到这里为止 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)
                    }
                }
            }
        }
    }
}

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

爽,爽,爽

看着上面熟悉的味道是不是很舒服,哈哈哈,写小部件终于也可以优雅一些了!

耗时操作优化

不知道大家注意到没有,provideGlance 竟然是一个挂起函数,这是什么意思,难道是???

没错!可以放心地在这里执行耗时操作了!比如你就可以这样:

override suspend fun provideGlance(context: Context, id: GlanceId) {
    val name  = getName()
    provideContent {
        Text(text = name)
    }
}
​
private suspend fun getName():String {
    delay(5000L)
    return "我爱你啊"
}

下面来运行看下效果!

添加小部件.gif

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

小部件更新

小部件的更新一直也是个问题,比如横竖屏转换后小部件的刷新、系统配置修改了之后的刷新,这些都是没有的,系统应用可以和系统进行一些骚操作,但是普通应用不可以啊,所以 Glance 中就引入了 WorkManager 来改善这个问题,最低可以设置十分钟的间隔刷新。

下面就来简单看下使用吧:

class WorkWorker(
    private val context: Context,
    workerParameters: WorkerParameters
) : CoroutineWorker(context, workerParameters) {
​
    companion object {
​
        private val uniqueWorkName = WorkWorker::class.java.simpleName
​
        // 排队进行工作
        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()
            )
        }
​
        /**
         * 取消任何正在进行的工作
         */
        fun cancel(context: Context, glanceId: GlanceId) {
            WorkManager.getInstance(context).cancelAllWorkByTag(glanceId.toString())
        }
    }
​
    override suspend fun doWork(): Result {
        // 需要执行的操作
        return Result.success()
    }
}

OK,先创建了一个 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作为标记为worker排队,以便在小部件实例被删除的情况下取消所有作业
            val glanceId = LocalGlanceId.current
            SideEffect {
                WorkWorker.enqueue(context, size, glanceId)
            }
        }
    }
}

很简单,在 provideGlance 中排队执行操作,然后在 onDelete 中将 Work 取消了即可。

便捷的 ListView

写过小部件的都知道 ListView 特别坑,原生小部件想要实现 ListView 需要实现 FactoryService 等,而在 Glance 这里直接两三行代码搞定。

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

没错,和 Compose 中一样,名字也一样,都是 LazyColumn ,写起来非常便捷。

更方便的 LocalXXX

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

/**
 * 生成的概览视图的大小。概览视图至少有那么多空间可以显示。确切的含义可能会根据表面及其配置方式而变化。
 */
val LocalSize = staticCompositionLocalOf<DpSize> { error("No default size") }
​
/**
 * 生成概览视图时应用程序的上下文。
 */
val LocalContext = staticCompositionLocalOf<Context> { error("No default context") }
​
/**
 * 本地视图状态,在surface实现中定义。用于特定于视图的状态数据的可定制存储。
 */
val LocalState = compositionLocalOf<Any?> { null }
​
/**
 * 当前合成生成的概览视图的唯一Id。
 */
val LocalGlanceId = staticCompositionLocalOf<GlanceId> { error("No default glance id") }

不过这块需要注意包的导入问题。

Action

小部件中之前如果想要实现点击效果的话只能使用 PendingIntent ,这样很麻烦,现在 Glance 为我们提供了 Action ,使用方法如下:

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

不仅如此,还可以像下面这样操作:

Text(text = "点击", modifier = GlanceModifier.clickable {
    Log.e("TAG", "provideGlance: click")
})

这个实在是太方便了!推荐大家使用。但这个需要注意,如果想使用这个实现动画效果的话是不行的,因为它没有办法在特别短的时间内刷新,我之前尝试过 Compose 中的属性动画 animate*AsState ,结果就是只执行了最后的结果,中间过程全部忽略了。。。。

坑,坑,坑

“人家官方废了这么大劲开发出来的库,怎么能说人家坑呢?”

“因为它确实坑啊!”

坑一

刚才看到的熟悉的代码,其实一点也不熟悉,为什么这么说,来看下导入的包就知道了:

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 的,但是里面的可组合项全部是 Glance 中重写的。。。。

咱就是说啊!有没有一种可能,就是你在写的时候自然地就导入了 Compose 的包?运行直接报错!也没有任何提醒。。

image.png

是不是?没有一点提醒,这种情况官方有没有一种可能,就是像是 Glance 中的 Modifier 一样,也在前面加一个前缀,让开发者能够容易区分一点?即使加前缀不好看,你们不想加,有没有可能修改下编译器,让编译器告诉开发者不能这么写行不行?

坑二

图片的加载,图片是安卓开发中太常见的东西了,以前咱们使用 ImageView 来进行图片的展示,现在有了 Compose 了我们使用 Image 来进行展示,Glance 中同样是使用 Image 来展示,来玩个游戏吧,找不同!先来看下 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 因为 RemoteView 的限制少了一些功能,在 Compose 中咱们可以通过 painterResource 来构建出 Painter,但在 Glance 中又换了个名字 ImageProvider ,咱就是说啊,有没有一种可能,就是要不你就都学 Compose ,要不你就都不学。。。。

还有就是文字,来看下 Glance 中的 Text 吧:

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

虽然 Compose 中的 Text 接收的也是一个 String,但是人家有 stringResource 函数啊,你呢。。。忘写了么?

算了,自己写一个吧:

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

这个函数我个人觉得可以放到 Glance 中。。。。

总结

今天所讲的 Glance 其实也是基于 Compose 的,由此可见,Google 现在对 Compose 发力非常足,如果大家想系统地学习 Compose 的话,可以购买我的新书《Jetpack Compose:Android全新UI编程》进行阅读,里面有完整的 Compose 框架供大家学习。

京东购买地址

当当购买地址

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

如果对你有帮助的话,别忘记点个 Star,感激不尽,大家如果有疑问的话可以在评论区提出来。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1000540.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据结构算法刷题(29)动态规划

思路一&#xff1a;回溯&#xff1a;按照选和不选的判断方式&#xff0c;使用回溯来解决这个问题。 class Solution: def rob(self, nums: List[int]) -> int: n len(nums) #数组的长度 def dfs(i): if i<0: #到达边界条件后 return 0 #返回最大金额是0 res max(dfs(i…

医疗环境升级改造,从此温湿度监控不用慌!

在现代医疗环境中&#xff0c;为了确保患者的舒适和康复&#xff0c;温度和湿度的监控和管理变得至关重要。特别是在疗养院这样的医疗机构中&#xff0c;患者的健康与室内环境条件密切相关。温度和湿度的不适当波动可能导致不仅是不适&#xff0c;还可能加剧病情&#xff0c;影…

9月8日上课内容 第一章 rsync远程同步

主从复制总结 redis主从复制 是为了数据冗余和读写分离 在这两种模式中&#xff0c;有两种角色主节点 (master) 和从节点 (slave) &#xff0c;主节点负责处理写的操作&#xff0c;并将数据更改复制到一个或多个从节点。这样我们的主节点负载减轻&#xff0c;从节点可以提供数据…

UI设计新手指南之什么是UI设计?

最直观的&#xff0c;只要用手机&#xff0c;打开界面就会接触到 UI 设计。 UI 设计是什么&#xff1f;它是指对软件人机交互、操作逻辑、界面美观的整体设计。UI设计的目的是创造出简单、直观、易于使用的界面&#xff0c;为用户提供良好的体验。随着移动互联网的兴起&#x…

分享一下奶茶店怎么在小程序上做商城功能

随着移动互联网的普及&#xff0c;越来越多的消费者倾向于在手机上完成购物需求。对于奶茶店来说&#xff0c;在小程序上开设商城功能不仅可以扩大销售渠道&#xff0c;还能提高品牌知名度和用户体验。本文将探讨如何在小程序上为奶茶店实现商城功能。 对于奶茶店的商城功能&am…

Spring-MVC使用JSR303及拦截器,增强网络隐私安全

目录 一、JSR303 ( 1 ) 是什么 ( 2 ) 作用 ( 3 ) 常用注解 ( 4 ) 入门使用 二、拦截器 2.1 是什么 2.2 拦截器与过滤器的区别 2.3 应用场景 2.4 基础使用 2.5 用户登录权限控制 给我们带来的收获 一、JSR303 ( 1 ) 是什么 JSR 303是Java规范请求&#xff…

旅游复苏弹高OTA业绩,未来走势却有“U型曲线”与“抛物线”之变

文 | 螳螂观察 作者 | 易不二 经历了3年蛰伏&#xff0c;旅游业确实熬出头了&#xff0c;OTA也迎来了强势反弹。 自年初起就逐步恢复的旅游行业&#xff0c;经历了暑期小高潮后&#xff0c;正在逐步以强劲的复苏能力&#xff0c;为OTA们的期中财报增彩。 今年二季度&#x…

83 # 静态服务中间件 koa-static 的使用以及实现

静态服务中间件&#xff1a;koa-static 中间件可以决定是否向下执行&#xff0c;如果自己可以处理&#xff0c;那么直接处理完毕结束&#xff0c;如果自己处理不了&#xff0c;next 方法会继续向下执行 新建 public 文件夹&#xff0c;里面添加 index.html、style.css 文件 …

机器学习_个人笔记_周志华(更新中......)

第1章 绪论 1.1 引言 形成优秀的心理表征&#xff0c;自然能成为领域内的专家。 系统1 & 系统2。 机器学习&#xff1a;致力于研究如何通过计算的手段&#xff0c;利用经验来改善系统自身的性能。主要研究计算机从数据中产生model的算法&#xff0c;即“learning algori…

myCobot Pro600六轴机械臂与3D深度视觉:物体精确识别抓取堆叠

Introduction 随着时代的进步&#xff0c;各种精密的机械臂&#xff0c;人形机器人不断的问世。我们即将迎来到处都是机器人的高科技时代。为了跟上时代的脚步&#xff0c;我最近入手了一台myCobot pro 600机械臂&#xff0c;我主要是想要用它来学习机械臂相关得控制以及机器视…

【跟小嘉学 Apache Flink】二、Flink 快速上手

系列文章目录 【跟小嘉学 Apache Flink】一、Apache Flink 介绍 【跟小嘉学 Apache Flink】二、Flink 快速上手 文章目录 系列文章目录[TOC](文章目录) 一、创建工程1.1、创建 Maven 工程1.2、log4j 配置 二、批处理单词统计&#xff08;DataSet API&#xff09;2.1、创建 Bat…

今日话题:解决Linux中可以识别但无法连接Airpods问题(亲测且实用)

今日话题&#xff1a;解决Linux中可以识别但无法连接Airpods问题 起因经过结果方式一方式二 起因经过 在根据“Linux启动黑屏卡住Logo登录界面无法进入系统的终极解决方式”博客解决掉gdm3以及lightdm图形界面之间冲突的问题后&#xff0c;准备设置打开蓝牙连接Airpods&#x…

Java | 多线程

不爱生姜不吃醋⭐️ 如果本文有什么错误的话欢迎在评论区中指正 与其明天开始&#xff0c;不如现在行动&#xff01; 文章目录 &#x1f334;前言&#x1f334;一、什么是多线程&#xff1f;1.进程2.线程3.多线程作用 &#x1f334;二、多线程中的两个概念1. 并发2. 并行3.举例…

python-面向运行时性能优化-threading

python-面向运行时性能优化-threading 一:线程基础1> 线程状态2> 线程同步1. 锁的状态3> 线程通信-条件变量4> 线程阻塞-之间转换1. 阻塞分类二:threading类1> threading介绍2> Thread类1. Thread的生命周期2. 实例化Thread类3. 继承Thread类4. Thread构造…

9.11作业

实现一个对数组求和的函数&#xff0c;数组通过实参传递给函数 sum0 arr(11 22 33 44 55) Sum() {for i in ${arr[*]}do$((sumi))donereturn $sum } Sum ${arr[*]} var$? echo $var写一个函数&#xff0c;输出当前用户的uid和gid&#xff0c;并使用变量接收结果 Sum() {aid -…

C高级作业 【使用shell脚本】 实现一个对数组求和的函数,数组通过实参传递给函数+写一个函数输出当前用户的uid和gid,并使用变量接收结果

作业 1、实现一个对数组求和的函数&#xff0c;数组通过实参传递给函数 #!/bin/bash # 定义求和函数 function sum() {local arr("$") # 将传入的参数保存到一个数组中local sum0 # 初始化求和为0# 遍历数组元素进行求和for num in "${arr[]}";dosum$…

零代码编程:用ChatGPT批量合并ts文件

文件夹中有很多个ts后缀的视频文件&#xff0c;要合并成一个视频文件&#xff0c;在ChatGPT中可以这样输入提示词&#xff1a; 你是一个Python编程专家&#xff0c;要完成一个批量合并ts文件的任务&#xff0c;具体步骤如下&#xff1a; 打开文件夹&#xff1a;C:\Users\dell\…

亚马逊测评下单怎么操作?有没有自动下单软件?

想要进行亚马逊的测评下单&#xff0c;可以按照以下步骤进行操作&#xff1a; 首先&#xff0c;在亚马逊官方网站上搜索你感兴趣的产品&#xff0c;选择你喜欢的产品并查看其详细信息、价格以及其他用户的评价&#xff0c;确认你的购买意向后&#xff0c;点击“加入购物车”将…

C高级 shell指令分支和循环

实现一个对数组求和的函数&#xff0c;数组通过实参传递给函数 #!/bin/bash s0 function sum() {local brr($*)for i in ${brr[*]}do((si))doneecho $s } arr(1 2 3 4 5 6 7 8 9 ) sum ${arr[*]}写一个函数&#xff0c;输出当前用户的uid和gid&#xff0c;并使用变量接收结果 #…

Grafana配置邮件告警

1、创建一个监控图 2、grafana邮件配置 vim /etc/grafana/grafana.ini [smtp] enabled true host smtp.163.com:465 user qinziteng05163.com password xxxxx # 授权码 from_address qinziteng05163.com from_name Grafanasystemctl restart grafana-serv…