UGUI动态元素大小的滑动无限列表

效果与使用说明

效果

  1. 可以滑动
  2. 无限列表(严格来说也和常规的不太一样)
  3. 可以通过曲线调整元素大小

在这里插入图片描述

使用说明

  1. 列表元素位于脚本挂载处的直接子级
  2. 最大的元素位于脚本挂载元素的pivot处
  3. 水平列表的对齐依据是所有元素pivot都在一条线上
  4. 默认在最左侧和最右侧元素外有1个元素(本身看不见,但是在移动的时候可能会移动到视野内)
  5. 基于4,如果希望view外还有更多的元素(虽然不知道出于什么目的),可以调大“左/右侧元素数目”并加mask遮罩住。
  6. 通过dragFactor调整拖动的敏感度
  7. 设置InterestedElem的意义是动态传递出某个感兴趣的数据的下标(因而可以实现某些视觉效果,例如某个特效的跟随)
  8. 不同的项目资源管理和加载的方法不太一样,我这里Init是简单地读取直接子级的元素(作演示用),在实际使用时需要更改为自己项目的方式
  9. 如果期望在列表滑到最左侧或者最右侧有一些效果,则需要为Action<bool,bool> OnReachSide添加实现,其中第一个bool代表是否到达左侧,第二个bool代表是否到达右侧。
  10. 如果期望某个元素被选中(即该元素出现在最大的那个位置)有效果,则需要为OnSelectElem添加实现,例如选中元素后展示该元素的详细信息。值得注意的是,在滑动过程中任何一个元素经过最大元素的位置都会触发这个,即使滑动还没有停下。如果期望在滑动结束才传递出对应元素的信息,则需要调用OnSelectElemStable

【关于曲线】

曲线的横坐标,0处是view最左侧元素的大小,1是view最右侧元素的大小,0.5是中间最大元素。

纵坐标代表相对于中间最大元素,也就是说一般情况下0.5处的纵坐标应当为1

原理

选定最大元素右边的元素作为StepLength(别抬杠说为什么左边的不行,反正不一定两侧都有元素,到时候自己改一下

根据输入的长度L,使用L/StepLength得到一个标准化的拖动距离,代表元素被拖动越过几个元素的位置。

然后根据拖动的距离插值即可,可以认为类似于关键帧动画。

考虑到某一帧玩家可能滑动速度极快,快到多个元素都会划过中间最大元素的位置(好吧我不知道他们为什么要这么干但我有理由相信有人会这么干),此时不必要把一个元素插值经过好几个记录点。我们本可以把这个简化为向相邻元素的拖动移动过程,所以先整体刷新到合适的位置,比如说想做移动4.5个标准化的拖动距离,那我就先刷新数据为移动完4个的情况,再向左插值0.5个元素的移动

这种刷新实质上造成了拖动元素后该元素显示内容被重新(甚至反复)刷新,间接存在性能问题(我在后文的缺陷也提到了)

其实一开始不是很想使用Update的,但是纯在OnDrag里实现容易出Bug,尤其是一帧存在多个拖动的情况。

代码

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
/*
* 总体逻辑就根据中间元素的位置确认其余元素的位置和缩放
* 比最中间元素稍小的元素称为次级元素,次级元素pivot到中间元素pivot的距离为单位距离StepLength
* 根据拖动距离和StepLength的比值决定元素会朝某个方向移动多少
* 最中间的元素默认位于父元素的pivot处
* 1. 列表元素位于脚本挂载处的直接子级
* 2. 最大的元素位于脚本挂载元素的pivot处
* 3. 曲线的横坐标,0处是view最左侧元素的大小,1是view最右侧元素的大小,0.5是中间最大元素
* 4. 曲线的纵坐标,表示较之于最大元素的缩放比例,一般情况下0.5处值为1
* 5. 如无特殊情况,一般建议maxElemWidth和最大元素的保持一致
*/
public class DynamicElemSizeScroll : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{[Tooltip("元素较之于最大元素的衰减")]public AnimationCurve curve;[Tooltip("左侧展示元素的个数")]public int leftElemCount = 3;[Tooltip("右侧展示元素的个数")]public int rightElemCount = 3;[Tooltip("最大元素的宽度")]public float maxElemWidth;[Tooltip("元素间距")]public int elemBias = 20;[Tooltip("拖拽的敏感程度")]public float dragFactor = 1f;[Tooltip("是否启用感兴趣的元素标记,启用后会记录展示感兴趣的元素的位置用来实现某些效果")]public bool useInterestedElem = false;// 存有要展示的所有元素的信息public List<int> elemData;// 依据elemData的值来刷新指定元素public Action<Transform, int, int> refreshElem;public Action<bool, bool> OnReachSide;  // 是否到达了某一侧,第一个bool是左,第二个bool是右public Action<int> OnSelectElem;        // 当某个元素被选中(在拖拽过程中,有元素经过最大的位置就会被选中)public Action<int> OnSelectElemStable;  // 当某个元素被选中(且无拖拽)才会调用这个public Action<Transform> OnInterestedIdxDisplay;    // 当某个感兴趣的数据处于被展示的状态时,调用这个函数用来实现某些效果(例如于其上显示某些东西)public int interestedIdx = -1;  // 当存在某个感兴趣的数据被展示时,计划传出其坐标,用以展示某些效果private bool needUpdate = false;private int curSelectDataIdx = -1;private int adjustOffset = 0;private float lastDragNormalizedDistance = 0;// drag relatingbool isDraging = false;private Vector3 dragStartPosition;private Vector3 dragEndPosition;private float dragDistance = 0;// scale cacheList<Vector3> scaleCache;List<Vector3> positionCache;// elem data cacheLinkedList<Transform> elemCache;private int CacheSize { get => leftElemCount + rightElemCount + 1 + 2; }private int CacheLeftCount { get => leftElemCount + 1; }private float StepLength { get => positionCache[leftElemCount + 1].x - positionCache[leftElemCount].x; }private int CurSelectDataIdx{get => curSelectDataIdx;set{if (value != curSelectDataIdx && IsValidIdx(value)){curSelectDataIdx = value;RefreshElemImmediately();if (value == 0) { OnReachSide?.Invoke(true, false); }else if (value == CacheSize - 1) { OnReachSide?.Invoke(false, true); }else { OnReachSide?.Invoke(false, false); }OnSelectElem?.Invoke(elemData[value]);}}}private float DragDistance{get => dragDistance;set{if (value == 0) { needUpdate = false; }if (value != dragDistance){dragDistance = value;needUpdate = true;}}}void Awake(){InitDefaultCurve();InitScaleCache();   //这里和位置初始化有时序耦合,必须先放在前}void OnEnable(){InitPosCache();}void Update(){if (!needUpdate) return;var normalizedDistance = GetNormalizedLength(DragDistance);normalizedDistance *= dragFactor;normalizedDistance += adjustOffset;if (normalizedDistance == 0) return;    // 原地就不移动TryProcessInputAbsGreaterThanOne(ref normalizedDistance);if (!IsCanScroll(normalizedDistance)){normalizedDistance = 0;}lastDragNormalizedDistance = normalizedDistance;var f = normalizedDistance > 0 ? elemCache.First : elemCache.Last;int idx = normalizedDistance > 0 ? 0 : CacheSize - 1;while (IsValidIdx(idx) && f != null){LerpElemWithInput(f, idx, normalizedDistance);f = normalizedDistance > 0 ? f.Next : f.Previous;idx += normalizedDistance > 0 ? 1 : -1;}if (useInterestedElem){Transform tf = IsInterestedElemDisp() ? GetInterestedElemTrans() : null;OnInterestedIdxDisplay?.Invoke(tf);}DragDistance = 0;}#region Interface Implementationpublic void OnBeginDrag(PointerEventData eventData){isDraging = true;dragStartPosition = eventData.position;}public void OnEndDrag(PointerEventData eventData){isDraging = false;dragEndPosition = eventData.position;adjustOffset = 0;DragDistance = 0;TryStartSanp();}public void OnDrag(PointerEventData eventData){float deltaDistance = eventData.position.x - dragStartPosition.x;if (deltaDistance == 0 || !IsCanScroll(deltaDistance)) return;DragDistance = deltaDistance;}#endregion#region Data Init/// <summary>/// 在曲线没初始化的情况下初始化默认曲线/// </summary>void InitDefaultCurve(){if (curve.keys.Length > 0) return;curve.AddKey(0, 0.6866682f);curve.AddKey(1f / 6f, 0.7557174f);curve.AddKey(1f / 3f, 0.86251f);curve.AddKey(0.5f, 1f);curve.AddKey(2f / 3f, 0.86251f);curve.AddKey(5f / 6f, 0.7557174f);curve.AddKey(1, 0.6866682f);}void InitScaleCache(){if (scaleCache == null) scaleCache = new List<Vector3>(CacheSize);scaleCache.Clear();for (int i = 0; i < CacheLeftCount; i++)    // i{scaleCache.Add(GetScaleInCurve(CacheLeftCount - i, true) * Vector3.one);}scaleCache.Add(Vector3.one);for (int i = 0; i < rightElemCount + 1; i++)    // CacheLeftCount + 1 + i{scaleCache.Add(GetScaleInCurve(i + 1, false) * Vector3.one);}}void InitPosCache(){if (positionCache == null) positionCache = new List<Vector3>(CacheSize);positionCache.Clear();for (int i = 0; i < CacheSize; i++){positionCache.Add(Vector3.zero);}positionCache[CacheLeftCount] = Vector3.zero;Vector3 preScale;Vector3 thisScale;Vector3 prePosition;Vector3 temp = Vector3.zero;//var pivot = elemCache.First.Value.GetComponent<RectTransform>().pivot;Vector2 pivot = new Vector2(0.5f, 0.5f);for (int i = 0; i < CacheLeftCount; i++){temp = Vector3.zero;preScale = scaleCache[CacheLeftCount - i];thisScale = scaleCache[CacheLeftCount - 1 - i];prePosition = positionCache[CacheLeftCount - i];temp.x = preScale.x * maxElemWidth * pivot.x + elemBias + thisScale.x * maxElemWidth * (1f - pivot.x);temp.x *= -1;positionCache[CacheLeftCount - 1 - i] = temp + prePosition;}for (int i = 0; i < rightElemCount + 1; i++){temp = Vector3.zero;preScale = scaleCache[CacheLeftCount + i];thisScale = scaleCache[CacheLeftCount + 1 + i];prePosition = positionCache[CacheLeftCount + i];temp.x = preScale.x * maxElemWidth * (1f - pivot.x) + elemBias + thisScale.x * maxElemWidth * pivot.x;positionCache[CacheLeftCount + 1 + i] = temp + prePosition;}}public void InitScrollData(List<int> elemData, int selectDataIdx, Action<Transform, int, int> refreshAction){this.elemData = elemData;this.refreshElem = refreshAction;if (elemCache == null) elemCache = new LinkedList<Transform>();int childCount = transform.childCount;for (int i = 0; i < transform.childCount; i++){var temp = transform.GetChild(i);elemCache.AddLast(temp);var rt = temp.GetComponent<RectTransform>();rt.anchoredPosition = positionCache[i];rt.localScale = scaleCache[i];}CurSelectDataIdx = selectDataIdx;}public void Init4Test(int selectDataIndex){if (elemCache == null) elemCache = new LinkedList<Transform>();int childCount = transform.childCount;for (int i = 0; i < transform.childCount; i++){var temp = transform.GetChild(i);elemCache.AddLast(temp);var rt = temp.GetComponent<RectTransform>();rt.anchoredPosition = positionCache[i];rt.localScale = scaleCache[i];}elemData = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8 };refreshElem = (Transform t, int idx, int i) =>{var numText = t.Find("#txt").GetComponent<Text>();numText.text = idx.ToString();// click index == i};RefreshElemImmediately();}public void RefreshScroll(int selectDataIndex){CurSelectDataIdx = selectDataIndex;}#endregion#region LERP Logicfloat GetNormalizedLength(float input){return input / StepLength;}bool IsCanScroll(float input){bool res = true;if (input > 0 && curSelectDataIdx == 0) res = false;if (input < 0 && curSelectDataIdx == elemData.Count - 1) res = false;return res;}/// <summary>/// 对输入值的绝对值大于1的进行处理,确保每一次移动操作都是和相邻元素进行/// </summary>/// <param name="distance"></param>void TryProcessInputAbsGreaterThanOne(ref float distance){var moveCount = CountIntegersToZero(distance);if (moveCount > 0){if (distance < 0){dragStartPosition.x -= StepLength;CurSelectDataIdx += moveCount;distance += moveCount;}else{dragStartPosition.x += StepLength;CurSelectDataIdx -= moveCount;distance -= moveCount;}}// 至此distance是一个介于-1到1之间的值,这样可以把任何一次移动都转化为向相邻位置元素的移动}void LerpElemWithInput(LinkedListNode<Transform> node, int index, float distance){var rt = node.Value.GetComponent<RectTransform>();rt.localPosition = GetLerpPos(distance, index);rt.localScale = GetLerpScale(distance, index);}int CountIntegersToZero(float number){return Mathf.FloorToInt(Mathf.Abs(number));}Vector3 GetLerpPos(float inputValue, int index){if (inputValue < 0 && index - 1 >= 0){// 插值到 index-1return Vector3.Lerp(positionCache[index], positionCache[index - 1], Mathf.Abs(inputValue));}else if (inputValue > 0 && index + 1 < CacheSize){// 插值到 index+1return Vector3.Lerp(positionCache[index], positionCache[index + 1], inputValue);}else{// 输入值为 0,直接返回当前值return positionCache[index];}}Vector3 GetLerpScale(float inputValue, int index){if (inputValue < 0 && index - 1 >= 0){// 插值到 index-1return Vector3.Lerp(scaleCache[index], scaleCache[index - 1], Mathf.Abs(inputValue));}else if (inputValue > 0 && index + 1 < CacheSize){// 插值到 index+1return Vector3.Lerp(scaleCache[index], scaleCache[index + 1], inputValue);}else{// 输入值为 0,直接返回当前值return scaleCache[index];}}#endregion#region Move Logic/// <summary>/// 根据CurSelectDataIdx修改elemCache的元素到其本应该出现的位置(无动画,这个位置就是动画结束的位置)/// </summary>void RefreshElemImmediately(){var ptr = elemCache.First;var idx = CurSelectDataIdx - leftElemCount - 1;var loop = 0;RectTransform rt;bool isValid;while (ptr != null){isValid = IsValidIdx(idx);rt = ptr.Value.GetComponent<RectTransform>();ptr.Value.gameObject.SetActive(isValid);if (isValid) { refreshElem(rt, elemData[idx], loop); }rt.localPosition = positionCache[loop];rt.localScale = scaleCache[loop];idx++;loop++;ptr = ptr.Next;}if (useInterestedElem){Transform tf = IsInterestedElemDisp() ? GetInterestedElemTrans() : null;OnInterestedIdxDisplay?.Invoke(tf);}}bool IsValidIdx(int idx){return idx >= 0 && idx < elemData.Count;}#endregion#region Snap Logicvoid TryStartSanp(){if (lastDragNormalizedDistance <= -0.5f) CurSelectDataIdx++;else if (lastDragNormalizedDistance >= 0.5f) CurSelectDataIdx--;else{RefreshElemImmediately();}OnSelectElemStable?.Invoke(CurSelectDataIdx);lastDragNormalizedDistance = 0;}#endregion#region Interested Elembool IsInterestedElemDisp(){bool res = false;if (interestedIdx >= curSelectDataIdx){res = interestedIdx - curSelectDataIdx <= rightElemCount;}else{res = curSelectDataIdx - interestedIdx <= leftElemCount;}return res;}Transform GetInterestedElemTrans(){int idx = curSelectDataIdx - CacheLeftCount;var ptr = elemCache.First;while (ptr != null){if (idx == interestedIdx){return ptr.Value;}idx++;ptr = ptr.Next;}return null;}#endregion#region Cache Utils/// <summary>/// 获取曲线单侧距离中间元素第idx个节点的值(一般仅初始化cache用)/// </summary>/// <param name="idx">第idx个元素(例如中间元素左侧的idx==1)</param>/// <param name="isLeft">是否是左侧(false==右侧)</param>/// <returns></returns>float GetScaleInCurve(int idx, bool isLeft){float offset = isLeft ? 0 : 0.5f;if (isLeft && idx == leftElemCount + 1) idx--;if (!isLeft && idx == rightElemCount + 1) idx--;idx = isLeft ? leftElemCount - idx : idx;return curve.Evaluate(offset + 0.5f * (float)idx / (isLeft ? leftElemCount : rightElemCount));}#endregion#region Drag Simpublic void SetDrag2Elem(int idx){var i = CurSelectDataIdx - 4 + idx;if (i < 0) i = 0;if (i >= elemData.Count) i = elemData.Count - 1;CurSelectDataIdx = i;OnSelectElemStable?.Invoke(i);}#endregion
}

垂直滚动版本

讲道理做成通用组件该写垂直版本的,但是,哎,时值中秋,我想玩游戏,遂不写。

缺陷与声明

缺陷

  1. 没有垂直版本
  2. 每次有一个新的元素经过最大的(被select的位置)位置会导致整体被刷新一遍(实现原理就是如此),这是一个性能开销,我知道本应如何处理,我只是写的时候思路歪了写成这样了。这样确实没有传统的无限列表的性能高。不过考虑到本文还是展示思路为主,就这样吧。(所以其实大可不必使用LinkedList)
  3. 没有实现元素的点击移动。目前我只是refreshElem的第三个参数传信息,直接把界面刷新了,相当于点击元素直接出现在最大的位置,没有移动过去的过程。可以考虑添加一个函数模拟拖动,按照恒定的速度把一定的偏移量添加至dragDIstance
  4. 吸附较为生硬(正如我所说,我只是放出了一个初步版本)

声明

本文涉及的代码遵从CC4.0协议

趁着放假写了一下,比较草率(然后拖到现在才整理并发出来),可改进的地方也有很多,不代表本人用到项目中的最终品质。

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

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

相关文章

kafka下载配置

下载安装 参开kafka社区 zookeeperkafka消息队列群集部署https://apache.csdn.net/66c958fb10164416336632c3.html 下载 kafka_2.12-3.2.0安装包快速下载地址分享 官网下载链接地址&#xff1a; 官网下载地址&#xff1a;https://kafka.apache.org/downloads 官网呢下载慢…

基于Node.js+Express+MySQL+VUE实现的计算机毕业设计共享单车管理网站

单车信息选择骑行 骑行状态留言公告/springboot/javaWEB/J2EE/MYSQL数据库/vue前后分离小程序 功能如下&#xff1a; 一、开发目标 在共享经济日益盛行的今天&#xff0c;共享单车作为一种绿色、便捷的出行方式&#xff0c;已经深入人们的日常生活。然而&#xff0c;随着共享…

解读滁州少儿自闭症寄宿制学校:为孩子重新定义未来

为自闭症儿童点亮希望之光&#xff1a;星贝育园自闭症儿童寄宿制学校的温暖之旅 在繁华与喧嚣交织的都市一隅&#xff0c;广州的星贝育园自闭症儿童寄宿制学校如同一座温馨的灯塔&#xff0c;为那些在社交与沟通海洋中迷失方向的小小航船指引着方向&#xff0c;重新定义了他们…

win 录屏软件有哪些?5个软件帮助你快速进行电脑录屏。

win 录屏软件有哪些&#xff1f;5个软件帮助你快速进行电脑录屏。 在 Windows 系统上录屏操作十分常见&#xff0c;无论是制作教程、记录游戏片段&#xff0c;还是录制会议和演示文稿&#xff0c;都需要一个高效、稳定的录屏软件。以下是五款适合 Windows 系统的录屏软件&…

docker - maven 插件自动构建镜像(构建镜像:ebuy-docker:v2.0)

文章目录 1、docker服务端开启远程访问2、在pom.xml文件plugins下添加Maven的docker插件3、编写dockerfile文件4、执行maven的打包命令5、查看 镜像 ebuy-docker:v2.06、创建 容器 ebuy-dockerv2.0 上面手动构建镜像的过程比较繁琐&#xff0c;使用Maven的docker插件可以实现镜…

混合专家模型在大模型微调领域进展

前言&#xff1a;随着大规模语言模型&#xff08;LLM&#xff09;的快速发展&#xff0c;人工智能在自然语言处理领域取得了巨大的进步。在将大模型转化为实际生产力时&#xff0c;不免需要针对实际的任务对大模型进行微调。然而&#xff0c;随着模型规模的增长&#xff0c;微调…

【最新华为OD机试E卷-支持在线评测】分苹果(100分)多语言题解-(Python/C/JavaScript/Java/Cpp)

🍭 大家好这里是春秋招笔试突围 ,一枚热爱算法的程序员 💻 ACM金牌🏅️团队 | 大厂实习经历 | 多年算法竞赛经历 ✨ 本系列打算持续跟新华为OD-E/D卷的多语言AC题解 🧩 大部分包含 Python / C / Javascript / Java / Cpp 多语言代码 👏 感谢大家的订阅➕ 和 喜欢�…

[Linux][进程][进程的七种状态]

进程状态是操作系统用来管理进程的一种手段&#xff0c;操作系统通过动态的调整进程状态来合理的分配资源&#xff0c;维护整个系统的生态。 // Linux内核对进程各个状态的定义&#xff0c;Linux系统的进程的状态不考虑/* * The task state array is a strange "bitmap&qu…

尚庭公寓-接口定义

5. 接口定义 5.1 后台管理系统接口定义 5.1.1 公寓信息管理 5.1.1.1 属性管理 属性管理页面包含公寓和房间各种可选的属性信息&#xff0c;其中包括房间的可选支付方式、房间的可选租期、房间的配套、公寓的配套等等。其所需接口如下 房间支付方式管理 页面如下 所需接口如…

【笔记】如何将本地的.md变成不影响阅读的类pdf模式

在1处搜索markdown viewer 在2处勾选url复选框 将需要阅读的md文件的本地路径去除双引号&#xff08;如果没有双引号不必做任何处理&#xff09; 直接放进浏览器url地址栏 正常显示图片与文字 解决

如何将泰语入门提高到精通呢?

要精通泰语&#xff0c;需要从基础的字母和发音开始学习&#xff0c;并通过积累词汇、频繁练习口语、沉浸在语言环境中来不断提高。参加在线课程或找专业教师进行系统性学习也很有帮助。此外&#xff0c;利用各种教材和在线资源&#xff0c;以及保持持续和一致的学习态度&#…

【线程】线程池

线程池通过一个线程安全的阻塞任务队列加上一个或一个以上的线程实现&#xff0c;线程池中的线程可以从阻塞队列中获取任务进行任务处理&#xff0c;当线程都处于繁忙状态时可以将任务加入阻塞队列中&#xff0c;等到其它的线程空闲后进行处理。 线程池作用&#xff1a; 1.降…

Teams集成-订阅事件处理

在Teams会议侧边栏应用开发-会议转写-CSDN博客的基础上&#xff0c;使用/delta接口尝试获取实时转写&#xff0c;发现只能更新了一次&#xff0c;然后就不再更新了&#xff0c;想尝试使用订阅事件去获取转写&#xff0c;发现也不是实时的&#xff0c;当会议结束时&#xff0c;订…

排序题目:对角线遍历 II

文章目录 题目标题和出处难度题目描述要求示例数据范围 解法思路和算法代码复杂度分析 题目 标题和出处 标题&#xff1a;对角线遍历 II 出处&#xff1a;1424. 对角线遍历 II 难度 6 级 题目描述 要求 给定一个二维整数数组 nums \texttt{nums} nums&#xff0c;将 …

阅读记录:iCaRL: Incremental Classifier and Representation Learning

1. Contribution 提出了一种新的训练策略&#xff0c;iCaRL&#xff1a;允许以增量方式学习&#xff1a;只需要同时存在一小部分类别的训练数据&#xff0c;新类别可以逐步添加。同时学习分类器和数据表示&#xff1a;iCaRL能够同时学习强大的分类器和数据表示&#xff0c;这与…

vscode【实用插件】Markdown Preview Enhanced 预览 .md 文件

安装 在 vscode 插件市场的搜索 Markdown Preview Enhanced点安装 使用 用 vscode 打开任意 .md 文件右键快捷菜单 最终效果 可打开导航目录

有哪些小众但高逼格的蓝牙耳机推荐?百元开放式耳机推荐大赏

如今的耳机市场中&#xff0c;主流品牌的影响力不容小觑。然而&#xff0c;还有一些小众的耳机品牌&#xff0c;犹如未被发掘的珍宝&#xff0c;静候着人们去探索。这些小众品牌或许没有进行大规模的广告推广&#xff0c;但它们凭借独特的设计、出色的音质以及对品质的不懈坚持…

需求: 通过后台生成的树形结构,返回给前台用于动态生成表格标题,并将对应标题下面的信息对应起来

1. 如图所以&#xff0c;完成以下内容对应 2. 代码示例如下&#xff0c; 动态生成树形结构列名称&#xff0c;并将表格中存在的值与其对应起来 /*** 查询资源计划列表** param resourcePlan 资源计划* return 资源计划*/Overridepublic Map<String, Object> selectResour…

【通俗易懂】FFT求解全过程,各参数详细解释

在进行FFT全过程讲解之前&#xff0c;小编先给大家解释一下&#xff0c;在FFT中出现的一些参数名词解释。 &#xff08;1&#xff09;采样频率 Fs Fs 1 / 采样间隔 根据奈奎斯特定理&#xff1a;Fs ≥ 最高频率分量的两倍&#xff0c;这样才能避免混叠 &#xff08;2&…

CAT1 RTU软硬件设计开源资料分析(TCP协议+Modbus协议+GNSS定位版本 )

01 CAT1 RTU方案简介&#xff1a; 远程终端单元( Remote Terminal Unit&#xff0c;RTU)&#xff0c;一种针对通信距离较长和工业现场环境恶劣而设计的具有模块化结构的、特殊的计算机测控单元&#xff0c;它将末端检测仪表和执行机构与远程控制中心相连接。 奇迹TCP RTUGNS…