通过无障碍控制 Compose 界面滚动的实战和原理剖析

news2024/11/29 4:35:31

Compose-base-accessibility-action-compose.png

前言

针对 Compose UI 工具包,开发者不仅需要掌握如何使用新的 UI 组件达到 design 需求,更需要了解和实现与 UI 的交互逻辑。

比如 touch 事件、Accessibility 事件等等。

  • Compose 中对 touch 事件的处理和原理,笔者已经在《通过调用栈快速探究 Compose 中 touch 事件的处理原理》里进行了阐述
  • Compose 中对 Accessibility 事件的支持和基本原理,笔者已经在 《一文读懂 Compose 支持 Accessibility 无障碍的原理》 里进行了介绍

那么将两个话题相结合,不禁要好奇:利用 Accessibility 针对 Compose 界面模拟 touch 交互,是否真的有效,个中原理又如何?

本文将通过无障碍 DEMO 对 Google Compose 项目 Accompanist 中的 Horizontal Pager sample 模拟注入 Scroll 滚动事件,看下实际效果,并对原理链路进行剖析。

向 Compose 模拟滚动事件

无障碍 DEMO,本来想直接复用曾经红极一时的 AccessibilityTool 开源项目。奈何代码太老编译不过,遂直接写了个 DEMO 来捕捉 AccessibilityEvent 然后分析 AccessibilityNodeInfo

当发现是节点属于 Accompanist 的包名(com.google.accompanist.sample),且可滚动 scrollable 的话,通过无障碍模拟注入 ACTION_SCROLL_FORWARD 的 action。

 public class MyAccessibilityService extends AccessibilityService {
     ...
     @Override
     public void onAccessibilityEvent(AccessibilityEvent event) {
         Log.i(TAG, "onAccessibilityEvent() event: " + event);
 ​
         AccessibilityNodeInfo root;
         ArrayList<AccessibilityNodeInfo> roots = new ArrayList<>();
         ArrayList<AccessibilityNodeInfo> nodeList = new ArrayList<>();try {
             switch (event.getEventType()) {
                 case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
                     Log.i(TAG, "TYPE_WINDOW_STATE_CHANGED()");
                     
                     roots.add(service.getRootInActiveWindow());
                     findAllNode(roots, nodeList);printComposeNode(nodeList);
 ​
                     roots.clear();
                     nodeList.clear();
                     break;
                 ...
             }
         } catch (Throwable e) {
             e.printStackTrace();
         }
     }
     
     private void printComposeNode(ArrayList<AccessibilityNodeInfo> root) {
         for (AccessibilityNodeInfo node : root) {
             if (node.getPackageName().equals("com.google.accompanist.sample")
                     && node.getClassName().equals("android.view.View")) {
                 node.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
             }
         }
     }
     ...
 }

《一文读懂 Compose 支持 Accessibility 无障碍的原理》 里我们介绍过,Compose 通过无障碍代理 AccessibilityDelegate 依据 UI 组件的类型、情况,进行 AccessibilityNodeInfo 实例的构造。

为了兼容传统 View 的内容,会针对实例里的 className 属性进行一定程度的了改写,但范围有限。

LazyColumn 这种的组件,并没有和传统的可滚动的 ListViewScrollViewRecylerView 的名称进行转换,用的仍然是默认的 View 名称。

所以咱们的无障碍 DEMO 不能像以前那样在判断 isScrollable 之外再额外判断 ListView 等传统可滚动 View 的名称了。

话不多说,我们将无障碍 DEMO 在系统的无障碍设置中启用,选择 “allow” 即可。

ddd

然后运行下 Accompanist 的 Horizontal Pager 界面,打印下收集到的 AccessibilityNodeInfo 信息。

android.view.accessibility.AccessibilityNodeInfo@1cfed; ... 
packageName: com.google.accompanist.sample; className: android.view.View; ... 
enabled: true; ... scrollable: true; ...  
actions: [AccessibilityAction: ... AccessibilityAction: ACTION_SCROLL_FORWARD - null]...

可以看到:

  • className 果然是 android.view.View
  • scrollable 是 true
  • 支持的 AccessibilityAction 有 ACTION_SCROLL_FORWARD 等

模拟滚动的效果如下,可以看到一打开 Horizontal Pager 的界面,就自动往右进行了翻页。

ddd

Compose 支持模拟滚动的原理

滚动界面 Horizontal Pager

想了解 Compose 支持通过无障碍模拟滚动的原理,首先需要了解一下 Horizontal Pager 界面的布局和物理手势上触发滚动的一些背景知识。

ddd

该布局主要采用 TopAppBar 展示 Title 栏,内容区域由 Column 组件堆叠。其中:

  • ScrollableTabRow 负责可以横向滚动的 Tab 栏的内容展示
  • HorizontalPager 负责各 Tab 对应内容的展示,会依据 page index 展示对应的 Text 文本,还需要监听 scroll 手势进行横向滚动

ScrollableTabRow 还需要监听 Tab 的点击事件进行 PagerState 的滚动,采用 animateScrollToPage() 进行。

     class HorizontalPagerTabsSample : ComponentActivity() {
         override fun onCreate(savedInstanceState: Bundle?) {
             ...
             setContent {
                 AccompanistSampleTheme {
                     Surface {
                         Sample()
                     }
                 }
             }
         }
     }@Composable
     private fun Sample() {
         Scaffold(
             topBar = {
                 TopAppBar(
                     title = { Text(stringResource(R.string.horiz_pager_title_tabs)) },
                     backgroundColor = MaterialTheme.colors.surface,
                 )
             },
             modifier = Modifier.fillMaxSize()
         ) { padding ->
             val pages = remember {
                 listOf("Home", "Shows", "Movies", "Books", "Really long movies", "Short audiobooks")
             }Column(Modifier.fillMaxSize().padding(padding)) {
                 ...
                 ScrollableTabRow(
                     selectedTabIndex = pagerState.currentPage,
                     ...
                 ) {
                     pages.forEachIndexed { index, title ->
                         Tab(
                             ...
                             onClick = {
                                 coroutineScope.launch {
                                     pagerState.animateScrollToPage(index)
                                 }
                             }
                         )
                     }
                 }HorizontalPager(
                     ...
                 ) { page ->
                     Card {
                         Box(Modifier.fillMaxSize()) {
                             Text(
                                 text = "Page: ${pages[page]}",
                                 ...
                             )
                         }
                     }
                 }
             }
         }
     }

animateScrollToPage() 的实现如下,主要是依据 page 计算滚动的 index 和 scrollOffset。然后调用通用的 LazyListState 的 animateScrollToItem() 执行 smooth 的滚动操作。

         public suspend fun animateScrollToPage(
             @IntRange(from = 0) page: Int,
             @FloatRange(from = -1.0, to = 1.0) pageOffset: Float = 0f,
         ) {
             requireCurrentPage(page, "page")
             requireCurrentPageOffset(pageOffset, "pageOffset")
             try {
                 ...
                 if (pageOffset.absoluteValue <= 0.005f) {
                     lazyListState.animateScrollToItem(index = page)
                 } else {
                     lazyListState.scroll { }
                     ...if (target != null) {
                         lazyListState.animateScrollToItem(
                             index = page,
                             scrollOffset = ((target.size + itemSpacing) * pageOffset).roundToInt()
                         )
                     } else if (layoutInfo.visibleItemsInfo.isNotEmpty()) {
                         ...
                     }
                 }
             } finally {
                 onScrollFinished()
             }
         }

animateScrollToItem() 由 LazyLayoutAnimateScrollScope 完成。

首先需要通过 LazyListState 的 scroll() 挂起函数请求准备执行 scroll 处理,获得调度之后通过 lambda 回调最重要的步骤:ScrollScopescrollBy()

     internal suspend fun LazyLayoutAnimateScrollScope.animateScrollToItem(
         ...
     ) {
         scroll {
             try {
                 ...
                 while (loop && itemCount > 0) {
                     ...
                     anim.animateTo(
                         target,
                         sequentialAnimation = (anim.velocity != 0f)
                     ) {
                         if (!isItemVisible(index)) {
                             // Springs can overshoot their target, clamp to the desired range
                             val coercedValue = if (target > 0) {
                                 value.coerceAtMost(target)
                             } else {
                                 value.coerceAtLeast(target)
                             }
                             val delta = coercedValue - prevValue
                             val consumed = scrollBy(delta)
                             ...
                         }if (isOvershot()) {
                             snapToItem(index = index, scrollOffset = scrollOffset)
                             loop = false
                             cancelAnimation()
                             return@animateTo
                         } ...
                     }
     ​
                     loops++
                 }
             } catch (itemFound: ItemFoundInScroll) {
                 ...
             }
         }
     }

在内容区域手动滚动触发 scroll 的入口和点击 Tab 不同,来自 scroll gesture,但后续都是调用 ScrollScopescrollBy() 完成。

详细链路不再赘述,感兴趣的同学可以 debug 跟一下。

     private class ScrollDraggableState(
         var scrollLogic: ScrollingLogic
     ) : DraggableState, DragScope {
         var latestScrollScope: ScrollScope = NoOpScrollScope
         ...override suspend fun drag(dragPriority: MutatePriority, block: suspend DragScope.() -> Unit) {
             scrollLogic.scrollableState.scroll(dragPriority) {
                 latestScrollScope = this
                 block()
             }
         }
         ...
     }

收集滚动的无障碍语义

Compose 界面所需的 Accessibility 信息,都是通过 Semantics 语义机制来收集的,包括:AccessibilityEvent、AccessibilityNodeInfo 和 AccessibilityAction 信息。

Horizontal Pager 界面里负责主体内容展示的 HorizontalPager 组件,本质上是扩展 LazyRow 而来的,而 LazyRow 和 LazyColumn 一样最终经由 LazyList 抵达 LazyLayout 组件。

     internal fun LazyList(
         ...
     ) {
         ...
         LazyLayout(
             modifier = modifier
                 .then(state.remeasurementModifier)
                 .then(state.awaitLayoutModifier)
                 // 收集语义
                 .lazyLayoutSemantics(
                     itemProviderLambda = itemProviderLambda,
                     state = semanticState,
                     orientation = orientation,
                     userScrollEnabled = userScrollEnabled,
                     reverseScrolling = reverseLayout
                 )
                 .clipScrollableContainer(orientation)
                 .lazyListBeyondBoundsModifier(
                     state,
                     beyondBoundsItemCount,
                     reverseLayout,
                     orientation
                 )
                 .overscroll(overscrollEffect)
                 ...
             ...
         )
     }

LazyLayout 初始化的时候会调用 lazyLayoutSemantics() 收集语义。

     internal fun Modifier.lazyLayoutSemantics(
         ...
     ): Modifier {
         val coroutineScope = rememberCoroutineScope()
         return this.then(
             remember(
                 itemProviderLambda,
                 state,
                 orientation,
                 userScrollEnabled
             ) {
                 val isVertical = orientation == Orientation.Vertical
                 ...val scrollByAction: ((x: Float, y: Float) -> Boolean)? = if (userScrollEnabled) {
                     { x, y ->
                         ...
                         coroutineScope.launch {
                             state.animateScrollBy(delta)
                         }
                         true
                     }
                 } else {
                     null
                 }...
     ​
                 Modifier.semantics {
                     ...
                     if (scrollByAction != null) {
                         scrollBy(action = scrollByAction)
                     }
                     ...
                 }
             }
         )
     }fun SemanticsPropertyReceiver.scrollBy(
         label: String? = null,
         action: ((x: Float, y: Float) -> Boolean)?
     ) {
         this[SemanticsActions.ScrollBy] = AccessibilityAction(label, action)
     }

lazyLayoutSemantics() 会定义一个 scrollByAction 名称的 AccessibilityAction 实例,然后以 ScrollBy 为 key 存放到语义 map 中等待 Accessibility 机制查找和回调。

无障碍回调滚动 action

当其他 App 通过 AccessibilityNodeInfo 执行了 Action 之后,通过 AIDL 最终会进入目标 App 的 performActionHelper()

我们以 ACTION_SCROLL_FORWARD 为例,关注下处理逻辑。

     internal class AndroidComposeViewAccessibilityDelegateCompat ... {
         ...    
         private fun performActionHelper(
             ...
         ): Boolean {
             val node = currentSemanticsNodes[virtualViewId]?.semanticsNode ?: return false
             ...when (action) {
                 ...
                 AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD,
                 AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD,
                 android.R.id.accessibilityActionScrollDown,
                 android.R.id.accessibilityActionScrollUp,
                 android.R.id.accessibilityActionScrollRight,
                 android.R.id.accessibilityActionScrollLeft -> {
                     // Introduce a few shorthands:
                     val scrollForward = action == AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD
                     val scrollBackward = action == AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD
                     ...
                     val scrollHorizontal = scrollLeft || scrollRight || scrollForward || scrollBackward
                     val scrollVertical = scrollUp || scrollDown || scrollForward || scrollBackward
                     ...
                     val scrollAction =
                         node.unmergedConfig.getOrNull(SemanticsActions.ScrollBy) ?: return falseval xScrollState =
                         node.unmergedConfig.getOrNull(SemanticsProperties.HorizontalScrollAxisRange)
                     if (xScrollState != null && scrollHorizontal) {
                         var amountToScroll = viewport.width
                         if (scrollLeft || scrollBackward) {
                             amountToScroll = -amountToScroll
                         }
                         if (xScrollState.reverseScrolling) {
                             amountToScroll = -amountToScroll
                         }
                         if (node.isRtl && (scrollLeft || scrollRight)) {
                             amountToScroll = -amountToScroll
                         }
                         if (xScrollState.canScroll(amountToScroll)) {
                             return scrollAction.action?.invoke(amountToScroll, 0f) ?: false
                         }
                     }val yScrollState =
                         node.unmergedConfig.getOrNull(SemanticsProperties.VerticalScrollAxisRange)
                     if (yScrollState != null && scrollVertical) {
                         ...
                         if (yScrollState.canScroll(amountToScroll)) {
                             return scrollAction.action?.invoke(0f, amountToScroll) ?: false
                         }
                     }return false
                 }
                 ...
             }
         }
  1. 当 Action 类型为 ACTION_SCROLL_FORWARD 的时候,赋值 scrollForward 变量
  2. 从 node 里获取是否支持 x 轴滚动:xScrollState
  3. 两者皆 OK 的话,从语义 map 里以 ScrollBy 为 key 查到的 AccessibilityAction 实例并回调

该 Action 即回到了语义收集时注入的 lambda:

     coroutineScope.launch {
         state.animateScrollBy(delta)
     }

State 的实现为 LazyLayoutSemanticState

     internal fun LazyLayoutSemanticState(
         state: LazyListState,
         isVertical: Boolean
     ): LazyLayoutSemanticState = object : LazyLayoutSemanticState {
         ...
         override suspend fun animateScrollBy(delta: Float) {
             state.animateScrollBy(delta)
         }
         ...
     }

其 animateScrollBy() 实际通过 LazyListState 的 animateScrollBy() 进行,其最终调用 ScrollScopescrollBy()

虽然入口稍稍不同,但最后的逻辑便和物理上手动点击 Tab 或者横向 scroll 一样,完成滚动操作,殊途同归。

     suspend fun ScrollableState.animateScrollBy(
         value: Float,
         animationSpec: AnimationSpec<Float> = spring()
     ): Float {
         var previousValue = 0f
         scroll {
             animate(0f, value, animationSpec = animationSpec) { currentValue, _ ->
                 previousValue += scrollBy(currentValue - previousValue)
             }
         }
         return previousValue
     }

结语

compose_accessibility_scroll.drawio.png

《一文读懂 Compose 支持 Accessibility 无障碍的原理》 里已经介绍过 Compose 和 Accessibility 交互的大体原理,这里只将重点的 scroll 差异体现出来。

  1. Compose 启动的时候根据可滚动组件收集对应语义,以 ScrollBy key 存到整体的 SemanticsConfiguration
  2. 接着在 Accessibility 激活需要准备 Accessibility 信息的时候,将数据提取到 AccessibilityNode 里发送出去
  3. AccessibilityService 发送了 scroll Action 的时候,经由 AccessibilityDelegate 从 SemanticsConfiguration 里查找到对应的 AccessibilityAction 并执行
  4. scrool 的执行由 ScrollScopescrollBy() 完成,这和物理上执行滚动操作是一样的逻辑。

看了上述的 Compose 原理剖析之后,读者或许能感受到:除了开发者需要留意 UI 以外的交互细节,Compose 实现者更需要考虑如何将 UI 的各方各面和原生的 Android View 进行兼容。

不仅仅包括本文提到的 touch、accessibility,还包括大家不常关注到的相关开发细节。比如:

  • 如何 AndroidView 兼容?
  • 如何嵌套的 AndroidView?
  • 如何支持的 UIAutomator 自动化?
  • 如何支持的 Layout Inspector dump?
  • 如何支持的 Android 视图的性能检查?
  • 如何支持的 AndroidTest 机制?
  • 等等

待 Compose 愈加成熟,对于这些相关的开发能力的支持也会更加完善,后期笔者仍会针对其他部分进行持续的分析和介绍。

推荐阅读

  • 《通过调用栈快速探究 Compose 中 touch 事件的处理原理》
  • 《一文读懂 Compose 支持 Accessibility 无障碍的原理》

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

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

相关文章

Point-LIO:鲁棒高带宽激光惯性里程计

1. 动机 现有系统都是基于帧的&#xff0c;类似于VSLAM系统&#xff0c;频率固定&#xff08;例如10Hz)&#xff0c;但是实际上LiDAR是在不同时刻进行顺序采样&#xff0c;然后积累到一帧上&#xff0c;这不可避免地会引入运动畸变&#xff0c;从而影响建图和里程计精度。此外…

NASA数据集——SARAL 近实时增值业务地球物理数据记录海面高度异常

SARAL Near-Real-Time Value-added Operational Geophysical Data Record Sea Surface Height Anomaly SARAL 近实时增值业务地球物理数据记录海面高度异常 简介 2020 年 3 月 18 日至今 ALTIKA_SARAL_L2_OST_XOGDR 这些数据是近实时&#xff08;NRT&#xff09;&#xff…

【稳定检索/投稿优惠】2024年材料科学与能源工程国际会议(MSEE 2024)

2024 International Conference on Materials Science and Energy Engineering 2024年材料科学与能源工程国际会议 【会议信息】 会议简称&#xff1a;MSEE 2024大会地点&#xff1a;中国苏州会议官网&#xff1a;www.iacmsee.com会议邮箱&#xff1a;mseesub-paper.com审稿结…

WPF音乐播放器 零基础4个小时左右

前言&#xff1a;winfrom转wpf用久的熟手说得最多的是,转回去做winfrom难。。当时不明白。。做一个就知道了。 WPF音乐播放器 入口主程序 FontFamily"Microsoft YaHei" FontSize"12" FontWeight"ExtraLight" 居中显示WindowStartupLocation&quo…

【越界写null字节】ACTF2023 easy-netlink

前言 最近在矩阵杯遇到了一道 generic netlink 相关的内核题&#xff0c;然后就简单学习了一下 generic netlink 相关概念&#xff0c;然后又找了一到与 generic netlink 相关的题目。简单来说 generic netlink 相关的题目仅仅是将用户态与内核态的交互方式从传统的 ioctl 变成…

以sqlilabs靶场为例,讲解SQL注入攻击原理【42-53关】

【Less-42】 使用 or 11 -- aaa 密码&#xff0c;登陆成功。 找到注入点&#xff1a;密码输入框。 解题步骤&#xff1a; # 获取数据库名 and updatexml(1,concat(0x7e,(select database()),0x7e),1) -- aaa# 获取数据表名 and updatexml(1,concat(0x7e,(select group_conca…

CSS函数: translate、translate3d的使用

translate()和translate3d()函数可以实现元素在指定轴的平移的功能。函数使用在CSS转换属性transform的属性值。实现转换的函数类型有&#xff1a; translate()&#xff1a;2D平面实现X轴、Y轴的平移translate3d()&#xff1a;3D空间实现位置的平移translateX()&#xff1a;实…

Spring Boot整合Jasypt 库实现配置文件和数据库字段敏感数据的加解密

&#x1f604; 19年之后由于某些原因断更了三年&#xff0c;23年重新扬帆起航&#xff0c;推出更多优质博文&#xff0c;希望大家多多支持&#xff5e; &#x1f337; 古之立大事者&#xff0c;不惟有超世之才&#xff0c;亦必有坚忍不拔之志 &#x1f390; 个人CSND主页——Mi…

idea如何根据路径快速在项目中快速打卡该页面

在idea项目中使用快捷键shift根据路径快速找到该文件并打卡 双击shift(连续按两下shift) -粘贴文件路径-鼠标左键点击选中跳转的路径 自动进入该路径页面 例如&#xff1a;我的实例路径为src/views/user/govType.vue 输入src/views/user/govType或加vue后缀src/views/user/go…

ChatGLM2-6b的本地部署

** 大模型玩了一段时间了&#xff0c;一直没有记录&#xff0c;借假期记录下来 ** ChatGlm2介绍&#xff1a; chatglm2是清华大学发布的中英文双语对话模型&#xff0c;具备强大的问答和对话功能&#xff0c;拥有长达32K的上下文&#xff0c;可以输出比较长的文本。6b的训练参…

Python:处理矩阵之NumPy库(上)

目录 1.前言 2.Python中打开文件操作 3.初步认识NumPy库 4.使用NumPy库 5.NumPy库中的维度 6.array函数 7.arange函数 8.linspace函数 9.logspace函数 10.zeros函数 11.eye函数 前言 NumPy库是一个开源的Python科学计算库&#xff0c;它提供了高性能的多维数组对象、派生对…

linux centos redis-6.2.6一键安装及配置密码

linux centos redis-6.2.6一键安装及配置密码 redis基本原理一、操作阶段&#xff0c;开始安装 redis基本原理 redis作为非关系型nosql数据库&#xff0c;一般公司会作为缓存层&#xff0c;存储唯一会话id&#xff0c;以及请求削峰作用 一、数据结构 Redis支持多种数据结构&a…

操作系统期末复习整理知识点

操作系统的概念&#xff1a;①控制和管理整个计算机系统的硬件和软件资源&#xff0c;并合理地组织调度计算机的工作和资源的分配&#xff1b;②提供给用户和其他软件方便的接口和环境&#xff1b;③是计算机中最基本的系统软件 功能和目标&#xff1a; ①操作系统作为系统资源…

【JAVASE】详讲JAVA语法

这篇你将收获到以下知识&#xff1a; &#xff08;1&#xff09;方法重载 &#xff08;2&#xff09;方法签名 一&#xff1a;方法重载 什么是方法重载&#xff1f; 在一个类中&#xff0c;出现了多个方法的名称相同&#xff0c;但是它们的形参列表是不同的&#xff0c;那…

【Linux系统编程】进程地址空间

目录 前言 进程虚拟地址空间的引入 进程地址空间的概念 进一步理解进程地址空间 为什么需要进程地址空间&#xff1f; 系统层面理解malloc/new内存申请 前言 首先&#xff0c;在我们学习C语言的时候一定会见过如下这张图。&#xff08;没见过也没关系&#xff0c;接下来…

移除重复节点---链表

面试题 02.01. 移除重复节点 - 力扣&#xff08;LeetCode&#xff09; 链表指针p和curr 与head指向同一块空间&#xff1b; p和head来比较相同的值&#xff0c;遇到一样的值、就改变这个空间里面struct的成员变量next指针指向的地址&#xff0c;跳向next的next再比较&#xf…

PDF编辑与转换的终极工具智能PDF处理Acrobat Pro DC

Acrobat Pro DC 2023是一款功能全面的PDF编辑管理软件&#xff0c;支持创建、编辑、转换、签署和共享PDF文件。它具备OCR技术&#xff0c;可将扫描文档转换为可编辑文本&#xff0c;同时提供智能PDF处理技术&#xff0c;确保文件完整性和可读性。此外&#xff0c;软件还支持电子…

目标检测数据集 - 智能零售柜商品检测数据集下载「包含VOC、COCO、YOLO三种格式」

数据集介绍&#xff1a;智能零售柜商品检测数据集&#xff0c;真实智能零售柜监控场景采集高质量商品图片数据&#xff0c;数据集含常见智能零售柜商品图片&#xff0c;包括罐装饮料类、袋装零食类等等。数据标注标签包含 113 个商品类别&#xff1b;适用实际项目应用&#xff…

Python 基于阿里云的OSS对象存储服务实现本地文件上云框架

Python 基于阿里云的OSS对象存储服务实现将文件上云框架 文章目录 Python 基于阿里云的OSS对象存储服务实现将文件上云框架一、前言二、阿里云配置1、获取用户AKEY和AKeySecret2、创建Bucket 三、Python 阿里云oss上云框架1、安装oss2依赖库2、阿里云oss python 一、前言 未来…

C++11 列表初始化(initializer_list),pair

1. {} 初始化 C98 中&#xff0c;允许使用 {} 对数组进行初始化。 int arr[3] { 0, 1, 2 };C11 扩大了 {} 初始化 的使用范围&#xff0c;使其可用于所有内置类型和自定义类型。 struct Date {int _year;int _month;int _day;Date(int year, int month, int day):_year(year…