Unity实现卡片循环滚动效果的示例详解
作者:CoderZ1010 发布时间:2022-06-06 16:04:47
简介
功能需求如图所示,点击下一个按钮,所有卡片向右滚动,其中最后一张需要变更为最前面的一张,点击上一个按钮,所有卡片向左滚动,最前面的一张需要变更为最后一张,实现循环滚动效果。
最中间的一张表示当前选中项,变更为选中项的滚动过程中,需要逐渐放大到指定值,相反则需要恢复到默认大小。
实现思路:
定义卡片的摆放规则;
调整卡片的层级关系;
调整卡片的尺寸大小;
卡片向指定方向移动,动态调整位置、大小、层级关系。
定义卡片的摆放规则
第一张卡片放在正中间,其余卡片分成两部分分别放在左右两侧,因此如果卡片数量为奇数,则左右两侧卡片数量一致,如果卡片数量为偶数,多出的一张需要放到左侧或者右侧,这里我们定义为放到右侧。
卡片摆放的顺序如下图所示,在遍历生成时会判断当前索引是否小等于卡片数量/2,是则将卡片生成在索引值*指定卡片间距的位置上,否则将其生成在(索引值-卡片数量)*指定卡片间距的位置上。
代码实现:
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
public class LoopScrollView : MonoBehaviour
{
[SerializeField] private Texture[] roomTextures; //所有卡片
[SerializeField] private GameObject itemPrefab; //列表项预制体
[SerializeField] private Transform itemParent; //列表项的父级,将卡片生成到该物体下
[SerializeField] private float interval = 450f; //卡片之间的间距
private void Start()
{
for (int i = 0; i < roomTextures.Length; i++)
{
var tex = roomTextures[i];
var instance = Instantiate(itemPrefab);
instance.SetActive(true);
instance.transform.SetParent(itemParent, false);
instance.GetComponent<RawImage>().texture = tex;
instance.name = i.ToString();
//坐标位置
(instance.transform as RectTransform).anchoredPosition3D = Vector3.right * interval
* (i <= roomTextures.Length / 2 ? i : (i - roomTextures.Length));
}
}
}
调整卡片的层级关系
卡片的层级关系如图所示,第一张也就是中间的照片(编号0)需要在最上层,向左、向右逐渐遮挡下层,在Hierarchy层级窗口的表现则是编号0的卡片在最下方,编号1卡片在编号2卡片下方以遮挡编号2卡片,编号4卡片在编号3卡片下方以遮挡编号3卡片。
在遍历生成卡片时判断当前索引值是否小等于卡片数量/2,是则在层级中将其插入到最上方,也就是SiblingIndex=0,否则将其插入在第一张卡片之上,第一张卡片始终在最下方,也就是说插入为倒数第二个,即SiblingIndex=父节点的子物体数量-2。
代码如下:
//层级关系
instance.transform.SetSiblingIndex(i <= roomTextures.Length / 2 ? 0 : itemParent.childCount - 2);
调整卡片的尺寸大小
大小的调整比较简单,只需要将第一张卡片放大一定倍数即可。
//大小
instance.transform.localScale = (i == 0 ? 1.2f : 1f) * Vector3.one;
至此已经完成了卡片的生成,但是如何在点击上一个、下一个按钮时动态调整所有卡片的坐标、层级和大小才是关键。
动态调整位置、层级和大小
移动动画
首先为每张卡片添加脚本,用于实现卡片的移动逻辑,使用插值的形式来实现动画过程,假设动画所需时长为0.5秒
,使用变量float类型变量timer
来计时,自增Time.deltaTime * 2
以使其在0.5秒内的取值从0增加为1,并使用Mathf.Clamp01
来钳制其取值范围不要超过1。
代码如下:
using UnityEngine;
public class LoopScrollViewItem : MonoBehaviour
{
private RectTransform rectTransform;
private int index; //用于记录当前所在位置
private Vector3 cacheScale; //开始移动时的大小
private Vector3 cacheAnchorPosition3d; //开始移动时的坐标
private Vector3 targetAnchorPostion3D; //目标坐标
private int targetSiblingIndex; //目标层级
private bool isMoving; //是否正在移动标识
private float timer; //计时
private bool last; //是否为最右侧的那张卡片
private void Awake()
{
rectTransform = GetComponent<RectTransform>();
}
public int Index
{
get
{
return index;
}
set
{
if (index != value)
{
index = value;
}
}
}
private void Update()
{
if (isMoving)
{
timer += Time.deltaTime * 2f;
timer = Mathf.Clamp01(timer);
if (timer >= .2f)
{
transform.SetSiblingIndex(targetSiblingIndex);
}
rectTransform.anchoredPosition3D = Vector3.Lerp(cacheAnchorPosition3d, targetAnchorPostion3D, last ? 1f : timer);
transform.localScale = Vector3.Lerp(cacheScale, (index == 0 ? 1.3f : 1f) * Vector3.one, last ? 1f : timer);
if (timer == 1f)
{
isMoving = false;
}
}
}
public void Move(LoopScrollViewData data, bool last)
{
timer = 0f;
targetAnchorPostion3D = data.AnchorPosition3D;
targetSiblingIndex = data.SiblingIndex;
cacheAnchorPosition3d = rectTransform.anchoredPosition3D;
cacheScale = transform.localScale;
isMoving = true;
this.last = last;
}
}
其中last
变量用于标识是否为最右侧的那张卡片,如果是,使其立即变为最左侧的卡片,不表现动画过程,目的是为了防止如下图所示,卡片从最右侧移动到最左侧的穿帮现象:
在生成卡片时,为卡片物体添加该脚本,并添加到列表中进行缓存,同时,定义一个用于存储各编号对应的层级和坐标的数据结构,代码如下:
using UnityEngine;
public class LoopScrollViewData
{
public int SiblingIndex { get; private set; }
public Vector3 AnchorPosition3D { get; private set; }
public LoopScrollViewData(int siblingIndex, Vector3 anchorPosition3D)
{
SiblingIndex = siblingIndex;
AnchorPosition3D = anchorPosition3D;
}
}
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
public class LoopScrollView : MonoBehaviour
{
[SerializeField] private Texture[] roomTextures; //所有卡片
[SerializeField] private GameObject itemPrefab; //列表项预制体
[SerializeField] private Transform itemParent; //列表项的父级,将卡片生成到该物体下
[SerializeField] private float interval = 400f; //卡片之间的间距
//生成的卡片列表
private readonly List<LoopScrollViewItem> itemList = new List<LoopScrollViewItem>();
//字典用于存储各位置对应的卡片层级和坐标
private readonly Dictionary<int, LoopScrollViewData> map = new Dictionary<int, LoopScrollViewData>();
private void Start()
{
for (int i = 0; i < roomTextures.Length; i++)
{
var tex = roomTextures[i];
var instance = Instantiate(itemPrefab);
instance.SetActive(true);
instance.transform.SetParent(itemParent, false);
instance.GetComponent<RawImage>().texture = tex;
instance.name = i.ToString();
//坐标位置
(instance.transform as RectTransform).anchoredPosition3D = Vector3.right * interval
* (i <= roomTextures.Length / 2 ? i : (i - roomTextures.Length));
//层级关系
instance.transform.SetSiblingIndex(i <= roomTextures.Length / 2 ? 0 : itemParent.childCount - 2);
//大小
instance.transform.localScale = (i == 0 ? 1.2f : 1f) * Vector3.one;
var item = instance.AddComponent<LoopScrollViewItem>();
item.Index = i;
itemList.Add(item);
}
for (int i = 0; i < itemList.Count; i++)
{
var item = itemList[i];
map.Add(i, new LoopScrollViewData(item.transform.GetSiblingIndex(), (item.transform as RectTransform).anchoredPosition3D));
}
}
}
按钮事件
在生成卡片时,记录了卡片当前的编号,以及各编号对应的层级和位置,在点击下一个、上一个按钮时,只需要根据卡片当前的编号+1
或-1
来获取目标层级和位置即可。
编号自增后,如果等于卡片的数量,表示当前卡片已经是列表中最后一个,需要将其编号设为0
,相反,当编号自减后,如果小于0,表示当前卡片已经是列表中第一个,需要将其编号设为列表长度-1,以实现循环。
完整代码:
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
public class LoopScrollView : MonoBehaviour
{
[SerializeField] private Texture[] roomTextures; //所有卡片
[SerializeField] private GameObject itemPrefab; //列表项预制体
[SerializeField] private Transform itemParent; //列表项的父级,将卡片生成到该物体下
[SerializeField] private Button prevButton; //上一个按钮
[SerializeField] private Button nextButton; //下一个按钮
[SerializeField] private float interval = 400f; //卡片之间的间距
//生成的卡片列表
private readonly List<LoopScrollViewItem> itemList = new List<LoopScrollViewItem>();
//字典用于存储各位置对应的卡片层级和坐标
private readonly Dictionary<int, LoopScrollViewData> map = new Dictionary<int, LoopScrollViewData>();
private void Start()
{
for (int i = 0; i < roomTextures.Length; i++)
{
var tex = roomTextures[i];
var instance = Instantiate(itemPrefab);
instance.SetActive(true);
instance.transform.SetParent(itemParent, false);
instance.GetComponent<RawImage>().texture = tex;
instance.name = i.ToString();
//坐标位置
(instance.transform as RectTransform).anchoredPosition3D = Vector3.right * interval
* (i <= roomTextures.Length / 2 ? i : (i - roomTextures.Length));
//层级关系
instance.transform.SetSiblingIndex(i <= roomTextures.Length / 2 ? 0 : itemParent.childCount - 2);
//大小
instance.transform.localScale = (i == 0 ? 1.2f : 1f) * Vector3.one;
var item = instance.AddComponent<LoopScrollViewItem>();
item.Index = i;
itemList.Add(item);
}
for (int i = 0; i < itemList.Count; i++)
{
var item = itemList[i];
map.Add(i, new LoopScrollViewData(item.transform.GetSiblingIndex(), (item.transform as RectTransform).anchoredPosition3D));
}
//添加按钮点击事件
nextButton.onClick.AddListener(OnNextButtonClick);
prevButton.onClick.AddListener(OnPrevButtonClick);
}
//下一个按钮点击事件
private void OnNextButtonClick()
{
for (int i = 0; i < itemList.Count; i++)
{
var item = itemList[i];
bool last = item.Index == itemList.Count / 2;
int index = item.Index + 1;
index = index >= itemList.Count ? 0 : index;
item.Index = index;
item.Move(map[index], last);
}
}
//上一个按钮点击事件
private void OnPrevButtonClick()
{
for (int i = 0; i < itemList.Count; i++)
{
var item = itemList[i];
int index = item.Index - 1;
index = index < 0 ? itemList.Count - 1 : index;
item.Index = index;
item.Move(map[index], false);
}
}
}
来源:https://blog.csdn.net/qq_42139931/article/details/128240698


猜你喜欢
- 崩溃来源使用过AIDL进行跨进程通信的同学,肯定遇到过DeadObjectException这个崩溃,那么这个崩溃是怎么来的,我们又该如何解
- 下面有一个字符串阵列:string[] elements = {"adsf","etwert" ,&
- 一,内部类访问成员1,内部类可以直接访问外部类的成员,包括私有。2,外部类要访问内部类,必须建立内部类对象。class Outer{int
- 【说明】 TextView是用来显示文本的组件。以下介绍的是XML代码中的属性,在java代码中同样可通过 ”组件名.setXXX()方法设
- 方法一: IDictionaryEnumerator enumerator = thProduct.GetEn
- 前言:自定义View可以分为两种方式:第一种通过继承ViewGroup,内部通过addView的方式将其他的View组合到一起。第二种则是通
- <script>//验证身份证号方法var test=function(idcard){var Errors=new Array
- package cn.liangjintang.httpproxy;import java.io.BufferedReader;import
- 默认日志 Logback :默认情况下,Spring Boot会用Logback来记录日志,并用INFO级别输出到控制台。在运行应用程序和其
- 本文实例为大家介绍了几个可用的类,供大家参考,具体内容如下1.SQLHelper类using System;using System.Col
- 本节我们主要介绍 Ribbon 的一些常用配置和配置 Ribbon 的两种方式。常用配置1. 禁用 Eureka当我们在 RestTempl
- 本文实例讲述了java在网页上面抓取邮件地址的方法。分享给大家供大家参考。具体实现方法如下:import java.io.BufferedR
- 前言Java 中的 synchronized关键字可以在多线程环境下用来作为线程安全的同步锁。本文不讨论 synchronized 的具体使
- C#用户定义类型转换•用于自定义类和结构能够进行隐式转换和显示转换.例如:将一个自定义类类型转换成整型,浮点型等,反之亦然.C#提供隐式转换
- springmvc @RequestBody String类型参数通过如下配置: <bean id="mapp
- SpringMVC常用组件DispatcherServlet:前端控制器,不需要工程师开发,由框架提供作用:统一处理请求和响应,整个流程控制
- 使用idea创建javaweb项目idea还是写框架项目比较爽,原生的javaweb项目不是特别方便,这篇文章就是记录一下创建的过程图较多注
- using System.Collections.Generic;using System.Linq;using System.Refle
- 借用@Caching实现入参是基本类型的:@Caching(evict={@CacheEvict(value = Cache.CONSTAN
- 前言C++类中有几个特殊的非静态成员函数,当用户未定义这些函数时,编译器将给出默认实现。C++11前有四个特殊函数,C++11引入移动语义特