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

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.mrgr.cn/news/36987.html

相关文章:

  • 哈希表(一)
  • 【Python语言初识(五)】
  • 828华为云征文|使用Flexus X实例安装宝塔面板教学
  • (二)Optional
  • 数据结构编程实践20讲(Python版)—02链表
  • Shell脚本基础——实训项目任务
  • 超详细的 pytest教程 之前后置方法和 fixture 机制
  • 【C++】入门基础知识-1
  • 如何从huggingface下载
  • 循环神经网络笔记
  • linux常用命令(cheng)
  • C++学习笔记(45)
  • C++(string字符串、函数)
  • 【Linux】Linux工具——CMake入门
  • 【理解 Java 中的 for 循环】
  • 实时数字人DH_live使用案例
  • 破局汽车智能化浪潮:Tire 1供应商的网络优化与升级策略
  • linux信号 | 学习信号三步走 | 全解析信号的产生方式
  • 2024.9.26
  • Qt5和Qt6获取屏幕的宽高,有区别