草庐IT

Unity 如何实现卡片循环滚动效果

CoderZ1010 2023-09-01 原文

文章目录


简介

功能需求如图所示,点击下一个按钮,所有卡片向右滚动,其中最后一张需要变更为最前面的一张,点击上一个按钮,所有卡片向左滚动,最前面的一张需要变更为最后一张,实现循环滚动效果。

最中间的一张表示当前选中项,变更为选中项的滚动过程中,需要逐渐放大到指定值,相反则需要恢复到默认大小。

实现思路:

  • 定义卡片的摆放规则;
  • 调整卡片的层级关系;
  • 调整卡片的尺寸大小;
  • 卡片向指定方向移动,动态调整位置、大小、层级关系。

定义卡片的摆放规则

第一张卡片放在正中间,其余卡片分成两部分分别放在左右两侧,因此如果卡片数量为奇数,则左右两侧卡片数量一致,如果卡片数量为偶数,多出的一张需要放到左侧或者右侧,这里我们定义为放到右侧。

卡片摆放的顺序如下图所示,在遍历生成时会判断当前索引是否小等于卡片数量/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);
        }
    }
}

有关Unity 如何实现卡片循环滚动效果的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  3. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  4. ruby-on-rails - 如何验证 update_all 是否实际在 Rails 中更新 - 2

    给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru

  5. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  6. ruby - 树顶语法无限循环 - 2

    我脑子里浮现出一些关于一种新编程语言的想法,所以我想我会尝试实现它。一位friend建议我尝试使用Treetop(Rubygem)来创建一个解析器。Treetop的文档很少,我以前从未做过这种事情。我的解析器表现得好像有一个无限循环,但没有堆栈跟踪;事实证明很难追踪到。有人可以指出入门级解析/AST指南的方向吗?我真的需要一些列出规则、常见用法等的东西来使用像Treetop这样的工具。我的语法分析器在GitHub上,以防有人希望帮助我改进它。class{initialize=lambda(name){receiver.name=name}greet=lambda{IO.puts("He

  7. ruby-on-rails - 在 Ruby 中循环遍历多个数组 - 2

    我有多个ActiveRecord子类Item的实例数组,我需要根据最早的事件循环打印。在这种情况下,我需要打印付款和维护日期,如下所示:ItemAmaintenancerequiredin5daysItemBpaymentrequiredin6daysItemApaymentrequiredin7daysItemBmaintenancerequiredin8days我目前有两个查询,用于查找maintenance和payment项目(非排他性查询),并输出如下内容:paymentrequiredin...maintenancerequiredin...有什么方法可以改善上述(丑陋的)代

  8. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

    我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

  9. ruby - 如何指定 Rack 处理程序 - 2

    Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack

  10. ruby - 如何每月在 Heroku 运行一次 Scheduler 插件? - 2

    在选择我想要运行操作的频率时,唯一的选项是“每天”、“每小时”和“每10分钟”。谢谢!我想为我的Rails3.1应用程序运行调度程序。 最佳答案 这不是一个优雅的解决方案,但您可以安排它每天运行,并在实际开始工作之前检查日期是否为当月的第一天。 关于ruby-如何每月在Heroku运行一次Scheduler插件?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/8692687/

随机推荐