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

Compose 实践与探索十七 —— 多指手势与自定义触摸反馈

上一节我们讲了滑动的手势识别以及嵌套滑动,二者都属于触摸反馈这个大的范畴内的知识。本节我们将深入触摸反馈这个话题,讲一讲多指手势的识别与完全自定义的触摸反馈的实现。

1、多指手势

多指手势可以分为两类:

  1. 利用 API 处理预设好的手势
  2. 自定义的多指手势识别:自己分析触摸到屏幕上的每一根手指的滑动轨迹,然后识别对应的手势

本节讲解第 1 种,下节介绍第 2 种。

Compose 提供了三种多指手势的识别:移动、放缩与旋转,它们都存在于 detectTransformGestures 函数中,该函数也需要在 pointerInput() 内使用。我们先来看该函数的参数:

/**
* 一个用于旋转、平移和缩放的手势检测器。一旦达到触摸阈值,用户可以使用旋转、平移和缩放手势。
* 当发生旋转、缩放或平移中的任何一种手势时,将调用 onGesture,传递旋转角度(以度为单位)、
* 缩放比例因子和像素偏移量。每个改变都是前一次调用和当前手势之间的差异。在触摸阈值之后,这将
* 消耗所有位置变化。onGesture 还将提供所有已按下指针的中心点。
*
* 如果 panZoomLock 设置为 true,则只有在检测到旋转的触摸阈值之前才允许旋转,然后才能进行平移
* 或缩放动作。否则,将检测到平移和缩放手势,但不会检测到旋转手势。如果 panZoomLock 设置为 false,
* 则一旦触摸阈值被触发,将检测到所有三种手势。
*/
suspend fun PointerInputScope.detectTransformGestures(panZoomLock: Boolean = false,onGesture: (centroid: Offset, pan: Offset, zoom: Float, rotation: Float) -> Unit
)

第一个参数 panZoomLock 是一个开关,分为两种情况:

  • 当它为 false 时,三种手势可以同时识别
  • 当它为 true 时,如果先识别到旋转操作,那么就不会再监测滑动和缩放;如果先监测到滑动或缩放,那么就不会再监测旋转。相当于是把滑动和缩放放在一组,旋转单独放在另外一组,只监测先触发的那组操作

第二个参数 onGesture 参数是一个回调函数,它的参数含义如下:

  • centroid:所有按下手指的中心点。这是一个辅助参数,需要配合后面三个参数使用
  • pan:位移参数,表示中心点 centroid 在这一时刻与上一时刻的位置偏移量
  • zoom:这一时刻与上一时刻相比的放缩倍数
  • rotation:这一时刻与上一时刻相比的旋转角度

2、完全自定义触摸算法

前面我们讲了很多半自动化 API 进行手势识别,那些 API 可以覆盖绝大多数的使用场景。本节我们来介绍,如何通过底层 API 实现完全自定义的触摸算法。

2.1 写法、思维逻辑与代码框架

完全自定义触摸算法意味着要从获取触摸事件开始,自己对每一个触摸事件进行处理。

与前面介绍过的半自动化 API 类似的是,需要在 Modifier.pointerInput() 中调用底层 API 来接收触摸事件,这个底层 API 是 AwaitPointerEventScope 接口内的 awaitPointerEvent():

@RestrictsSuspension
@JvmDefaultWithCompatibility
interface AwaitPointerEventScope : Density {/*** 挂起协程,直到指定的输入通道(input pass)报告 PointerEvent 事件,pass 参数默认值是* PointerEventPass.Main。* [awaitPointerEvent] 会在受限制的挂起作用域中以同步的方式恢复执行。这意味着调用者在* [awaitPointerEvent] 返回后可以立即对输入做出反应,并同时影响当前帧和输入处理管道的下一个* 处理程序或阶段。调用者应在等待下一个事件前,对返回的 [PointerEvent] 进行修改,以便在输入处* 理流程的下一阶段运行前消费该事件的某些处理结果。*/suspend fun awaitPointerEvent(pass: PointerEventPass = PointerEventPass.Main): PointerEvent
}

它的大致使用模式如下:

setContent {// 尾随 lambda 是挂起函数环境的Modifier.pointerInput(Unit) {// 事件对象var event: PointerEvent// 提供调用 awaitPointerEvent() 的环境 AwaitPointerEventScopeawaitPointerEventScope {while (true) {// 挂起以获取触摸事件event = awaitPointerEvent()}}}
}

awaitPointerEvent() 的返回值类型是 PointerEvent,该类型内部封装了原生的触摸事件类型 MotionEvent。 通过 PointerEvent 的 type 属性可以获取事件的具体类型:

    actual var type: PointerEventType = calculatePointerEventType()internal setprivate fun calculatePointerEventType(): PointerEventType {val motionEvent = motionEventif (motionEvent != null) {return when (motionEvent.actionMasked) {MotionEvent.ACTION_DOWN,MotionEvent.ACTION_POINTER_DOWN -> PointerEventType.PressMotionEvent.ACTION_UP,MotionEvent.ACTION_POINTER_UP -> PointerEventType.ReleaseMotionEvent.ACTION_HOVER_MOVE,MotionEvent.ACTION_MOVE -> PointerEventType.MoveMotionEvent.ACTION_HOVER_ENTER -> PointerEventType.EnterMotionEvent.ACTION_HOVER_EXIT -> PointerEventType.ExitACTION_SCROLL -> PointerEventType.Scrollelse -> PointerEventType.Unknown}}...return PointerEventType.Move}

在 calculatePointerEventType() 中能看出原生的 MotionEvent 的类型在 Compose 中对应 PointerEventType 的哪种类型。需要注意的是,原生中把第一个手指按下的类型定义为 ACTION_DOWN,把非第一个手指按下的类型定义为 ACTION_POINTER_DOWN。但是在 Compose 中,将按下类型都合并为 Press。同样的,原生的最后一个手指抬起是 ACTION_UP,非最后一个手指抬起是 ACTION_POINTER_UP,在 Compose 中也被合并为 Release。

此外,我们还应注意到,Compose 获取事件的代码形式与原生不同。原生是在发生触摸事件后通过回调 onTouchEvent() 传入具体的事件类型实现的事件监听:

class CustomView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {override fun onTouchEvent(event: MotionEvent?): Boolean {when (event?.action) {MotionEvent.ACTION_DOWN -> {...}MotionEvent.ACTION_POINTER_DOWN -> {...}else -> {...}}return super.onTouchEvent(event)}
}

而 Compose 则需要自己循环调用 awaitPointerEvent() 去获取发生的事件 PointerEvent,然后通过 PointerEvent 的 type 属性去判断具体的触摸事件类型。看似是 Compose 要比 Android 原生更麻烦一些,但实际上这并不是 Compose 与原生的区别,而是传统的 Java 回调与 Kotlin 协程在写法上的不同造成的。协程以线性代码完成了异步操作,从整体上来讲,这样的写法要比回调更加直观和简单。

接下来举个例子,自己实现一个点击监听器:

fun Modifier.clickListener(onClick: () -> Unit) = pointerInput(Unit) {awaitPointerEventScope {// 循环接收所有的触摸事件while (true) {// 先接收一串触摸事件的第一个事件,按下事件val down = awaitPointerEvent()// 循环接收后续事件while (true) {val event = awaitPointerEvent()// 滑动事件不能超出组件范围,否则抬起时不响应 onClickif (event.type == PointerEventType.Move) {val position = event.changes[0].positionif (position.x < 0 || position.x > size.width || position.y < 0 || position.y > size.height) {break}} else if (event.type == PointerEventType.Release) {onClick()break}}}}
}

以上是实现的比较粗糙的点击监听器,在组件范围内按下并抬起后会回调 onClick 参数,但如果是在组件范围外抬起,就跳出本次的事件监听,从下一次的按下事件重新开始。监听过程中,使用了 PointerEvent 的 changes 数组:

actual val changes: List<PointerInputChange>

该数组的元素类型 PointerInputChange 是触摸事件的核心数据结构,用于描述单个指针(如手指、触控笔或鼠标)的输入状态变化,它是手势处理系统中最细粒度的操作单元。changes 数组就是所有指针触摸状态的变化,示例代码中只取了第一个元素,实际上就是只针对第一个按下的指针进行了状态的判断,没有支持多指逻辑。

下面我们可以试着对示例代码进行优化。

首先,使用在 1.4.0-alpha01 版本中新增的 awaitEachGesture() 可以简化示例代码,它可以帮我们实现连续手势的检测,因此就不用在最外层加一个 while(true) 了:

fun Modifier.clickListener(onClick: () -> Unit) = pointerInput(Unit) {awaitEachGesture {// 先接收一串触摸事件的第一个事件,按下事件val down = awaitPointerEvent()// 循环接收后续事件while (true) {val event = awaitPointerEvent()// 滑动事件不能超出组件范围,否则抬起时不响应 onClickif (event.type == PointerEventType.Move) {val position = event.changes[0].positionif (position.x < 0 || position.x > size.width || position.y < 0 || position.y > size.height) {break}} else if (event.type == PointerEventType.Release) {onClick()break}}}
}

此外,要对抬起和按下事件做细致的区分。因为前面我们贴出相关代码了,Compose 将任何手指的抬起和按下都定义为 Press 和 Release 类型,不同的手指按下与抬起的效果应该是不同的。对于示例代码而言,应该是最后一个手指抬起(对应原生的 MotionEvent.ACTION_UP)才认为发生了点击事件,所以要添加一个“抬起的是最后一个手指”的条件,

fun Modifier.clickListener(onClick: () -> Unit) = pointerInput(Unit) {awaitEachGesture {val down = awaitPointerEvent()while (true) {val event = awaitPointerEvent()if (event.type == PointerEventType.Move) {val position = event.changes[0].positionif (position.x < 0 || position.x > size.width || position.y < 0 || position.y > size.height) {break}} else if (event.type == PointerEventType.Release && event.changes.size == 1) { // 添加抬起时只有一个手指的条件onClick()break}}}
}

关于判断手指抬起的更详细逻辑,可以参考 AwaitPointerEventScope.waitForUpOrCancellation() 的代码:

suspend fun AwaitPointerEventScope.waitForUpOrCancellation(pass: PointerEventPass = PointerEventPass.Main
): PointerInputChange? {while (true) {val event = awaitPointerEvent(pass)// 判断所有手指都抬起了if (event.changes.fastAll { it.changedToUp() }) {// All pointers are upreturn event.changes[0]}if (event.changes.fastAny {it.isConsumed || it.isOutOfBounds(size, extendedTouchPadding)}) {return null // Canceled}// Check for cancel by position consumption. We can look on the Final pass of the// existing pointer event because it comes after the pass we checked above.val consumeCheck = awaitPointerEvent(PointerEventPass.Final)if (consumeCheck.changes.fastAny { it.isConsumed }) {return null}}
}

获取按下事件的函数也可以优化为 awaitFirstDown():

fun Modifier.clickListener(onClick: () -> Unit) = pointerInput(Unit) {awaitEachGesture {// 先接收一串触摸事件的第一个事件,按下事件val down = awaitFirstDown()// 循环接收后续事件while (true) {val event = awaitPointerEvent()// 滑动事件不能超出组件范围,否则抬起时不响应 onClickif (event.type == PointerEventType.Move) {val position = event.changes[0].positionif (position.x < 0 || position.x > size.width || position.y < 0 || position.y > size.height) {break}} else if (event.type == PointerEventType.Release && event.changes.size == 1) {onClick()break}}}
}

2.2 触摸事件的消费、拦截、取消

Android 原生为什么会有事件的消费、拦截和取消这些概念呢?因为手势可能会冲突。比如用户在屏幕上点击了一下,那么它点击到的是子组件还是父组件呢?这要看哪个组件消费了这个点击事件。结合实际场景,可能会有两种不同的表现:

  1. 在一个列表中,每个列表项上都有一个点赞按钮,当用户点击点赞按钮时,它触发的点赞按钮而不是整个列表项的点击。因为用户想点击的一定是表面的那个组件,所以 Android 通常会让子组件优先消费,只有在子组件不消费时才会让父组件消费
  2. 另外一种情况,还是在滑动列表中,点在一个列表项上然后向上滑动,用户是要滑动父组件,而不是点击列表项。此时就是父组件拦截了该事件,这个拦截发生在子组件消费之前

总的来说,原生的触摸事件的处理分为拦截与消费两大步骤,在子组件消费之前,会向上按照父组件到根组件的方向去询问是否要进行拦截,如果有任意一个父组件拦截了事件,那么就由该组件消费事件,并向子组件发送一个取消事件,在手指抬起之前,后续的所有事件都不会再发送给子组件。

而假如没有任何父级组件拦截事件,该事件就由子组件开始消费,只有在子组件不消费该事件时,才会一级一级地向父组件的方向传递,查看是否消费。

因此,在原生中,拦截是一个较为强势的过程,只有父组件不拦截才轮到子组件消费。

而在 Compose 中,不再有拦截的概念,只有消费的概念,或者说,把拦截的过程合并到消费中了。首先,事件会先传给父组件,无论父组件是否消费该事件,它都会继续向下传导到子组件中,只不过,如果父组件消费了事件,那么子组件就无法消费该事件了,但是可以利用事件已经被消费的事实去做一些别的事情,然后再次把该事件传回给父组件,形成一个父 -> 子 -> 父的事件传递链。最后再传回给父组件,是为了给嵌套滑动提供基础支持。这是原生的拦截 -> 消费过程所不具备的。

Compose 的事件消费原则:

  • 使用事件前,先检查该事件是否已经被消费了,一般是没有被消费才可被使用,但也有例外,比如滑动过程是不介意按下事件是否被消费了的
  • 使用完某个事件,一般应该消费掉该事件。但肯定也会有特殊场景,比如不想因为对事件的处理影响整体流程,那么处理完事件后也可以不消费掉它

最后来说一下相关的 API。

此前我们说过了 awaitPointerEvent(),它的参数 pass 的默认值为 PointerEventPass.Main,PointerEventPass 实际上就是定义了事件传递流程的枚举类:

enum class PointerEventPass {Initial, Main, Final
}

Initial 表示事件第一次由父组件传递给子组件的过程,Main 表示事件由子组件传给父组件的过程,而 Final 表示事件第二次父组件到子组件的过程。当我们一起使用这三个参数时,得到的其实是同一个事件对象:

// event1、event2、event3 是同一个 PointerEvent 对象
val event1 = awaitPointerEvent(PointerEventPass.Initial)
val event2 = awaitPointerEvent(PointerEventPass.Main)
val event3 = awaitPointerEvent(PointerEventPass.Final)

消费事件与检查事件是否已经被消费:

event2.changes[0].consume()
event2.changes[0].isConsumed

完整的代码可以参考 Compose 官方的滑动监听、长按监听等源码。

2.3 多指手势和多重手势

多重手势是指多个手指可以同时进行旋转、滑动、缩放手势,即一个手势可以起多重作用。

如何计算多指手势?对多个触摸点做综合计算,比如说多指滑动,先拿到滑动事件,计算所有手指的中心点,然后用这一时刻中心点与上一时刻中心点的差值作为滑动位移。中心点就用所有点的坐标加和除以触摸点的个数即可。 当然这个有现成函数可以用,比如 calculatePan() 就是计算多指滑动的中心点的 API:

val event = awaitPointerEvent()
event.calculatePan()

其余的还有 calculateRotation()、calculateZoom()、calculateCentroid() 等。

相关文章:

Compose 实践与探索十七 —— 多指手势与自定义触摸反馈

上一节我们讲了滑动的手势识别以及嵌套滑动&#xff0c;二者都属于触摸反馈这个大的范畴内的知识。本节我们将深入触摸反馈这个话题&#xff0c;讲一讲多指手势的识别与完全自定义的触摸反馈的实现。 1、多指手势 多指手势可以分为两类&#xff1a; 利用 API 处理预设好的手…...

哈希表 - 两个数组的交集(集合、数组) - JS

一、Set基础 在 JavaScript 中&#xff0c;Set 是一种集合&#xff08;Collection&#xff09;​数据结构&#xff0c;用于存储唯一值​&#xff08;不允许重复&#xff09;&#xff0c;并且可以高效地进行添加、删除、查询等操作。它类似于数组&#xff08;Array&#xff09;…...

26_ajax

目录 了解 接口 前后端交互 一、安装服务器环境 nodejs ajax发起请求 渲染响应结果 get方式传递参数 post方式传递参数 封装ajax_上 封装ajax下 了解 清楚前后端交互就可以写一些后端代码了。小项目 现在写项目开发的时候都是前后端分离 之前都没有前端这个东西&a…...

Java面试黄金宝典24

1. 什么是跳表 定义 跳表&#xff08;Skip List&#xff09;是一种随机化的数据结构&#xff0c;它基于有序链表发展而来&#xff0c;通过在每个节点中维护多个指向其他节点的指针&#xff0c;以多层链表的形式组织数据。其核心思想是在链表基础上增加额外层次&#xff0c;每…...

每日c/c++题 备战蓝桥杯(全排列问题)

题目描述 按照字典序输出自然数 1 到 n 所有不重复的排列&#xff0c;即 n 的全排列&#xff0c;要求所产生的任一数字序列中不允许出现重复的数字。 输入格式 一个整数 n。 输出格式 由 1∼n 组成的所有不重复的数字序列&#xff0c;每行一个序列。 每个数字保留 5 个场…...

Layui实现table动态添加行,可删除、表格可编辑,小数校验

实现如图需求&#xff0c;layui实现的可编辑table&#xff0c;包含B、C、D、E列&#xff0c;A列不用实现出现&#xff0c;A列放在附件就是让你明白&#xff0c;不同的物料名称&#xff0c;行是不一样的。除了头部表头和E列不能编辑&#xff0c;每个表格都可编辑&#xff0c;其中…...

Spring Boot 非web应用程序

​​​​​在 Spring Boot 框架中&#xff0c;要创建一个非Web应用程序&#xff08;纯Java程序&#xff09; main方法运行&#xff0c;不启动tomcat&#xff0c;main方法执行结束&#xff0c;程序就退出了&#xff1b; 方式一 1、SpringBoot开发纯Java程序&#xff0c;应该采…...

数据分析中的基线校正算法全解析:原理、实现与应用

数据分析中的基线校正算法全解析:原理、实现与应用 在数据分析中,基线漂移是一个常见问题,会严重影响数据的解释和分析精度。本文将详细介绍12种主流基线校正方法,包括数学原理、Python实现代码和适用场景分析。 基线漂移问题概述 基线漂移主要由以下因素引起: 仪器强度…...

国外计算机证书推荐(考证)(6 Sigma、AWS、APICS、IIA、Microsoft、Oracle、PMI、Red Hat)

文章目录 证书推荐1. 六西格玛 (6 Sigma)2. 亚马逊网络服务 (AWS)3. 美国生产与库存控制学会 (APICS)4. 内部审计师协会 (IIA)5. 微软 (Microsoft)6. 甲骨文 (Oracle)7. 项目管理协会 (PMI)8. 红帽 (Red Hat) 证书推荐 1. 六西格玛 (6 Sigma) 介绍&#xff1a;六西格玛是一种…...

linux》》docker 、containerd 保存镜像、打包tar、加载tar镜像

Linux》》docker: 默认情况下&#xff0c;Docker镜像保存在/var/lib/docker/目录下。 当您使用docker pull命令从Docker Hub或私有镜像仓库中拉取镜像时&#xff0c;Docker会自动将镜像文件保存在/var/lib/docker/image/目录下。 每个镜像都由一个或多个层组成&#xff0c;这些…...

大数据(2)Hadoop架构深度拆解:HDFS与MapReduce企业级实战与高阶调优

目录 一、分布式系统的设计哲学演进1.1 从Google三驾马车到现代数据湖 二、企业级HDFS架构全景图2.1 联邦架构的深度实践2.2 生产环境容灾设计2.3 性能压测方法论 三、MapReduce引擎内核解密3.1 Shuffle机制全链路优化3.2 资源调度革命&#xff1a;从MRv1到YARN3.3 企业级编码规…...

【数学建模】动态规划算法(Dynamic Programming,简称DP)详解与应用

动态规划算法详解与应用 文章目录 动态规划算法详解与应用引言动态规划的基本概念动态规划的设计步骤经典动态规划问题1. 斐波那契数列2. 背包问题3. 最长公共子序列(LCS) 动态规划的优化技巧动态规划的应用领域总结 引言 动态规划(Dynamic Programming&#xff0c;简称DP)是一…...

UE学习记录part11

第14节 breakable actors 147 destructible meshes a geometry collection is basically a set of static meshes that we get after we fracture a mesh. 几何体集合基本上是我们在断开网格后获得的一组静态网格。 选中要破碎的网格物品&#xff0c;创建集合 可以选择不同的…...

LeetCode知识点整理

1、Scanner 输入&#xff1a; import java.util.Scanner;public class Main {public static void main(String[] args) {Scanner scanner new Scanner(System.in);// 读取整数int num scanner.nextInt();// 读取一行字符串String line scanner.nextLine();scanner.close();…...

浅析车规芯片软错误防护加固的重要性

随着汽车电子技术的飞速发展&#xff0c;汽车已经从传统的机械交通工具转变为高度依赖电子系统的智能移动终端。车规芯片作为汽车电子系统的核心部件&#xff0c;其可靠性和安全性直接关系到车辆的正常运行和驾乘人员的安全。然而&#xff0c;车规芯片在复杂的运行环境中面临着…...

Android Jetpack学习总结(源码级理解)

ViewModel 和 LiveData 是 Android Jetpack 组件库中的两个核心组件&#xff0c;它们能帮助开发者更有效地管理 UI 相关的数据&#xff0c;并且能够在配置变更&#xff08;如屏幕旋转&#xff09;时保存和恢复 UI 数据。 ViewModel作用 瞬态数据丢失的恢复&#xff0c;比如横竖…...

Matlab_Simulink中导入CSV数据与仿真实现方法

前言 在Simulink仿真中&#xff0c;常需将外部数据&#xff08;如CSV文件或MATLAB工作空间变量&#xff09;作为输入信号驱动模型。本文介绍如何高效导入CSV数据至MATLAB工作空间&#xff0c;并通过From Workspace模块实现数据到Simulink的精确传输&#xff0c;适用于运动控制…...

Go 语言规范学习(6)

文章目录 StatementsTerminating statementsEmpty statementsLabeled statementsExpression statementsSend statementsIncDec statementsAssignment statementsIf statementsSwitch statementsExpression switchesType switches For statementsFor statements with single con…...

设计模式——设计模式理念

文章目录 参考&#xff1a;[设计模式——设计模式理念](https://mp.weixin.qq.com/s/IEduZFF6SaeAthWFFV6zKQ)参考&#xff1a;[设计模式——工厂方法模式](https://mp.weixin.qq.com/s/7tKIPtjvDxDJm4uFnqGsgQ)参考&#xff1a;[设计模式——抽象工厂模式](https://mp.weixin.…...

解析 ID 数组传参的解决方案:基于 Axios 的实现

解析 ID 数组传参的解决方案&#xff1a;基于 Axios 的实现 在实际开发中&#xff0c;经常需要将一个 ID 数组作为参数传递给后端接口。然而&#xff0c;不同的后端框架和前端库对数组参数的处理方式可能有所不同。通过一个具体的例子&#xff0c;在前端使用 Axios 框架发送 I…...

C语言快速入门-C语言基础知识

这个c语言入门&#xff0c;目标人群是有代码基础的&#xff0c;例如你之前学过javaSE&#xff0c;看此文章可能是更有帮助&#xff0c;会让你快速掌握他们之间的差异&#xff0c;文章内容大部分都是泛谈&#xff0c;详细的部分我会在之后时间发布&#xff0c;我也在慢慢学习&am…...

Ubuntu 22.04 上安装 VS Code

在 Ubuntu 22.04 上安装 VS Code 的方法如下&#xff1a; 方法 1&#xff1a;通过 APT 包管理器安装 更新系统包索引&#xff1a; 打开终端并执行以下命令&#xff1a; sudo apt update安装依赖项&#xff1a; 执行以下命令以安装所需的依赖项&#xff1a; sudo apt install s…...

AI人工智能-PyCharm的介绍安装应用

下载与安装 创建python项目 项目路径&#xff1a;C:\Users\miloq\Desktop\python_project 配置环境 提前找到conda配置的python-base路径 配置conda环境 运行项目 运行结果...

Todesk介绍

文章目录 ToDesk 软件介绍1. 软件概述2. ToDesk 的功能特点2.1 简单易用2.2 高质量的图像与流畅的操作2.3 跨平台支持2.4 多屏显示与协作2.5 文件传输功能2.6 实时聊天与语音通话2.7 远程唤醒与自动启动2.8 多种权限设置与安全性2.9 无需公网 IP 3. ToDesk 的应用场景3.1 个人使…...

【JavaEE】springMVC返回Http响应

目录 一、返回页面二、Controller和ResponseBody与RestController区别三、返回HTML代码⽚段四、返回JSON五、HttpServletResponse设置状态码六、设置Header6.1 HttpServletResponse设置6.2 RequestMapping设置 一、返回页面 步骤如下&#xff1a; 我们先要在static目录下创建…...

青少年编程与数学 02-011 MySQL数据库应用 02课题、MySQL数据库安装

青少年编程与数学 02-011 MySQL数据库应用 02课题、MySQL数据库安装 一、安装Windows系统Linux系统&#xff08;以Ubuntu 20.04为例&#xff09;macOS系统 二、配置&#xff08;一&#xff09;Windows系统1. 创建配置文件2. 初始化数据库3. 启动MySQL服务4. 登录MySQL5. 修改ro…...

springboot441-基于SpringBoot的校园自助交易系统(源码+数据库+纯前后端分离+部署讲解等)

&#x1f495;&#x1f495;作者&#xff1a; 爱笑学姐 &#x1f495;&#x1f495;个人简介&#xff1a;十年Java&#xff0c;Python美女程序员一枚&#xff0c;精通计算机专业前后端各类框架。 &#x1f495;&#x1f495;各类成品Java毕设 。javaweb&#xff0c;ssm&#xf…...

【安全运营】关于攻击面管理相关概念的梳理(一)

目录 一、ASM 介绍ASM 是“Attack Surface Management”&#xff08;攻击面管理&#xff09;的缩写【框架视角&#xff0c;广义概念】1. 介绍2. 兴起的原因3. 工作流程3.1 资产发现3.2 分类和优先级排序3.3 修复3.4 监控 二、EASM 介绍EASM 是 "External Attack Surface M…...

IPv6 网络访问异常 | 时好时坏 / 部分访问正常

注&#xff1a;本文为 “ IPv6 间接性连接异常” 相关文章合辑。 略作重排&#xff0c;未去重。 如有内容异常&#xff0c;请看原文。 IPv6 间接性连接异常&#xff1f;尝试调整路由器的 MTU 设置 Nero978 2024-1-29 17:54 背景 2024 年 1 月 29 日&#xff0c;因寒假返家…...

Unity编辑器功能及拓展(1) —特殊的Editor文件夹

Unity中的Editor文件夹是一个具有特殊用途的目录&#xff0c;主要用于存放与编辑器扩展功能相关的脚本和资源。 一.纠缠不清的UnityEditor 我们Unity中进行游戏构建时&#xff0c;我们经常遇到关于UnityEditor相关命名空间丢失的报错&#xff0c;这时候&#xff0c;只得将报错…...

LLMs之PE:《Tracing the thoughts of a large language model》翻译与解读

LLMs之PE&#xff1a;《Tracing the thoughts of a large language model》翻译与解读 导读&#xff1a;这篇论文的核心贡献在于提出了一种新颖的、基于提示工程的LLMs推理过程追踪技术——“Tracing Thoughts”。该技术通过精心设计的提示&#xff0c;引导LLMs生成其推理过程的…...

[Python] 贪心算法简单版

贪心算法-简单版 贪心算法的一般使用场景是给定一个列表ls, 让你在使用最少的数据的情况下达到或超过n. 我们就来使用上面讲到的这个朴素的例题来讲讲贪心算法的基本模板: 2-1.排序 既然要用最少的数据, 我们就要优先用大的数据拼, 为了实现这个效果, 我们得先给列表从大到小…...

游戏引擎学习第191天

回顾并制定今天的计划 最近几天&#xff0c;我们有一些偏离了原计划的方向&#xff0c;主要是开始了一些调试代码的工作。最初我们计划进行一些调试功能的添加&#xff0c;但是随着工作的深入&#xff0c;我们开始清理和整理调试界面的呈现方式&#xff0c;以便能够做一些更复…...

Git撤回操作全场景指南:未推送与已推送,保留和不保留修改的差异处理

一、未推送到远程仓库的提交&#xff08;仅本地存在&#xff09; 特点&#xff1a;可直接修改本地提交历史&#xff0c;不会影响他人 1. 保留修改重新提交 git reset --soft HEAD~1 # 操作效果&#xff1a; # - 撤销最后一次提交 # - 保留工作区所有修改 # - 暂存区内容保持…...

Java 贪吃蛇游戏

这段 Java 代码实现了一个经典的贪吃蛇游戏。玩家可以使用键盘的上下左右箭头键控制蛇的移动方向&#xff0c;蛇会在游戏面板中移动并尝试吃掉随机生成的食物。每吃掉一个食物&#xff0c;蛇的身体会变长&#xff0c;玩家的得分也会增加。如果蛇撞到自己的身体或者撞到游戏面板…...

QT图片轮播器(QT实操学习2)

1.项目架构 1.UI界面 2.widget.h​ #ifndef WIDGET_H #define WIDGET_H#include <QWidget>#define TIMEOUT 1 * 1000 QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPACEclass Widget : public QWidget {Q_OBJECTpublic:Widget(QWidget *parent n…...

mac m1/m2/m3 pyaudio的安装

google了很多方法&#xff0c;也尝试了 issue68的方法&#xff0c; 但是均失败了&#xff0c;但是问deepseek竟然成功了&#xff0c;下面是deepseek r1给出的方法。在M3 pro芯片上可以成功运行. 安装homebrew /bin/bash -c "$(curl -fsSL https://raw.githubusercontent…...

Appium中元素定位的注意点

应用场景 了解这些注意点可以以后在出错误的时候&#xff0c;更快速的定位问题原因。 示例 使用 find_element_by_xx 或 find_elements_by_xx 的方法&#xff0c;分别传入一个没有的“特征“会是什么结果呢? 核心代码 driver.find_element_by_id("xxx") drive…...

《深入探索 Python 数据分析:用 Pandas 高效处理与可视化大型数据集》

《深入探索 Python 数据分析:用 Pandas 高效处理与可视化大型数据集》 引言:从零到分析高手 数据是当代社会最宝贵的资源,而数据分析技能是现代职业人不可或缺的一部分。在数据科学的领域中,Python 已成为当之无愧的“首选语言”,其强大的生态系统和简洁的语法让人如虎添…...

[GWCTF 2019]我有一个数据库1 [CVE phpMyAdmin漏洞]

扫出来一些东西 访问/phpmyadmin 发现界面 这里用到了CVE-2018-12613&#xff0c;光速学习 出现漏洞的代码是&#xff1a; $target_blacklist array (import.php, export.php );// If we have a valid target, lets load that script instead if (! empty($_REQUEST[targe…...

spring 常用注解区别及使用场景

1. 组件注册注解 Bean 作用&#xff1a;用于方法上&#xff0c;表示该方法返回的对象由Spring容器管理。通常用于配置类&#xff08;Configuration&#xff09;中&#xff0c;注册第三方库或自定义的Bean。 使用场合&#xff1a; 当你需要将非Spring管理的类&#xff08;如第…...

【后端】【Django】信号使用详解

Django post_save 信号使用详解&#xff08;循序渐进&#xff09; 一、信号的基本概念 Django 的 信号&#xff08;Signal&#xff09; 允许不同部分的代码在发生某些事件时进行通信&#xff0c;而不需要直接调用。这种机制可以解耦代码&#xff0c;让不同的模块独立工作。 …...

ML算法数学概念

交叉熵损失&#xff08;Cross-Entropy Loss&#xff09; 是机器学习和深度学习中常用的一种损失函数&#xff0c;主要用于衡量两个概率分布之间的差异。它在分类问题中&#xff08;尤其是多分类问题&#xff09;被广泛使用&#xff0c;因为它能够有效地评估模型预测的概率分布与…...

wps 怎么显示隐藏文字

wps 怎么显示隐藏文字 》文件》选项》视图》勾选“隐藏文字” wps怎么设置隐藏文字 wps怎么设置隐藏文字...

Vue3 其它API Teleport 传送门

Vue3 其它API Teleport 传送门 在定义一个模态框时&#xff0c;父组件的filter属性会影响子组件的position属性&#xff0c;导致模态框定位错误使用Teleport解决这个问题把模态框代码传送到body标签下...

亚马逊玩具品类技术驱动型选品策略:从趋势洞察到合规基建

一、全球玩具电商技术演进趋势 &#xff08;技术化重构原市场背景&#xff09; 数据可视化分析&#xff1a;通过亚马逊SP-API抓取2023年玩具品类GMV分布热力图 监管技术升级&#xff1a; 美国CPSC启用AI质检系统&#xff08;缺陷识别准确率92.7%&#xff09; 欧盟EPR合规接口…...

SpringBoot3+EasyExcel通过WriteHandler动态实现表头重命名

方案简介 为了通过 EasyExcel 实现动态表头重命名&#xff0c;可以封装一个方法&#xff0c;传入动态的新表头名称列表&#xff08;List<String>&#xff09;&#xff0c;并结合 WriteHandler 接口来重命名表头。同时&#xff0c;通过 EasyExcel 将数据直接写入到输出流…...

PHY——LAN8720A 寄存器读写 (二)

文章目录 PHY——LAN8720A 寄存器读写 (二)工程配置引脚初始化代码以太网初始化代码PHY 接口实现LAN8720 接口实现PHY 接口测试 PHY——LAN8720A 寄存器读写 (二) 工程配置 这里以野火电子的 F429 开发板为例&#xff0c;配置以太网外设 这里有一点需要注意原理图 RMII_TXD0…...

HTML5和CSS3的一些特性

HTML5 和 CSS3 是现代网页设计的基础技术&#xff0c;它们引入了许多新特性和功能&#xff0c;极大地丰富了网页的表现力和交互能力。 HTML5 的一些重要特性包括&#xff1a; 新的语义化标签: HTML5 引入了一些重要的语义化标签如 <header>, <footer>, <articl…...

sass报错,忽略 Sass 弃用警告,降级版本

最有效的方法是创建一个 .sassrc.json 文件来配置 Sass 编译器。告诉 Sass 编译器忽略来自依赖项的警告消息。 解决方案&#xff1a; 1. 在项目根目录创建 .sassrc.json 文件&#xff1a; {"quietDeps": true }这个配置会让 Sass 编译器忽略所有来自依赖项&#x…...