滾動(dòng)文本框?qū)崿F(xiàn):RectTransform動(dòng)畫(huà)與UGUI動(dòng)態(tài)布局)
1. 項(xiàng)目概述為什么我們需要一個(gè)“會(huì)自己動(dòng)”的文本框在Unity UI開(kāi)發(fā)里處理大段文本是家常便飯。無(wú)論是游戲里的任務(wù)日志、劇情旁白還是應(yīng)用中的公告、聊天記錄我們經(jīng)常遇到一個(gè)經(jīng)典難題文本內(nèi)容超出了預(yù)設(shè)的顯示區(qū)域。新手的第一反應(yīng)可能是調(diào)大文本框的尺寸但這會(huì)破壞UI布局或者手動(dòng)添加滾動(dòng)條讓用戶去拖拽這在需要“自動(dòng)播放”劇情或展示跑馬燈信息時(shí)體驗(yàn)就非常割裂。這時(shí)候一個(gè)能夠自動(dòng)滾動(dòng)的文本框就成了剛需。它能讓文本內(nèi)容像電影字幕一樣從底部緩緩上升至頂部或者從右向左平滑移動(dòng)用戶無(wú)需交互即可完整閱讀。聽(tīng)起來(lái)似乎需要復(fù)雜的腳本控制其實(shí)不然Unity內(nèi)置的UI系統(tǒng)特別是Rect Transform組件本身就蘊(yùn)藏著實(shí)現(xiàn)這一效果的強(qiáng)大潛力只是很多開(kāi)發(fā)者習(xí)慣了其基礎(chǔ)的錨點(diǎn)功能而忽略了它在動(dòng)態(tài)布局和動(dòng)畫(huà)控制上的“隱藏玩法”。本項(xiàng)目標(biāo)題“從零構(gòu)建Unity自動(dòng)滾動(dòng)文本框揭秘Rect Transform的隱藏玩法”核心就是跳出對(duì)Rect Transform作為“靜態(tài)定位框”的刻板印象將其視為一個(gè)可以驅(qū)動(dòng)UI元素進(jìn)行復(fù)雜、平滑位移的動(dòng)畫(huà)控制器。我們將不依賴任何第三方插件完全利用Unity原生的UI組件和C#腳本深入Rect Transform的anchoredPosition、sizeDelta等關(guān)鍵屬性結(jié)合協(xié)程Coroutine或Dotween等動(dòng)畫(huà)系統(tǒng)打造一個(gè)性能高效、可控性強(qiáng)的自動(dòng)滾動(dòng)解決方案。這不僅是實(shí)現(xiàn)一個(gè)功能更是一次對(duì)Unity UI底層原理的深度探索理解了它你就能舉一反三創(chuàng)造出更多動(dòng)態(tài)的UI效果。2. 核心思路拆解讓Rect Transform“動(dòng)”起來(lái)的關(guān)鍵實(shí)現(xiàn)自動(dòng)滾動(dòng)本質(zhì)上是控制文本框的顯示視口與文本內(nèi)容之間的相對(duì)位置關(guān)系持續(xù)變化。在Unity的UGUI體系中我們通常使用Scroll Rect組件來(lái)處理滾動(dòng)但對(duì)于自動(dòng)滾動(dòng)這種單向、勻速的位移直接操控被嵌套在Scroll Rect下的內(nèi)容物體通常是帶有Rect Transform和Text或TextMeshPro組件的GameObject的Rect Transform屬性是更直接、更高效的選擇。2.1 核心組件與屬性分析我們的方案將圍繞以下幾個(gè)核心構(gòu)建UI結(jié)構(gòu)一個(gè)作為“視窗”的父級(jí)UI元素如Image或空物體其Rect Transform決定了顯示區(qū)域。其Mask組件或RectMask2D組件用于裁剪超出部分。內(nèi)部嵌套一個(gè)作為“內(nèi)容”的子級(jí)UI元素承載實(shí)際的文本。驅(qū)動(dòng)核心 - RectTransform內(nèi)容的Rect Transform組件是我們操作的核心對(duì)象。我們將通過(guò)腳本動(dòng)態(tài)修改其anchoredPosition屬性來(lái)實(shí)現(xiàn)滾動(dòng)。anchoredPosition這是相對(duì)于其父級(jí)錨點(diǎn)的位置。對(duì)于垂直滾動(dòng)我們主要修改其Y值對(duì)于水平滾動(dòng)則修改X值。讓這個(gè)值隨時(shí)間線性變化就產(chǎn)生了滾動(dòng)效果。sizeDelta這個(gè)屬性反映了RectTransform的尺寸與其錨點(diǎn)定義的“初始”尺寸之間的差值。當(dāng)內(nèi)容文本的高度或?qū)挾茸兓瘯r(shí)我們需要?jiǎng)討B(tài)計(jì)算并設(shè)置sizeDelta以確保內(nèi)容區(qū)域足夠容納所有文本這是計(jì)算滾動(dòng)總距離的基礎(chǔ)。文本組件使用Text或更強(qiáng)大的TextMeshPro - Text組件來(lái)顯示文本。我們需要獲取文本的preferredHeight首選高度或preferredWidth以確定內(nèi)容的總尺寸??刂颇_本一個(gè)C#腳本掛載在內(nèi)容物體或一個(gè)管理器上負(fù)責(zé)計(jì)算滾動(dòng)參數(shù)、啟動(dòng)/停止?jié)L動(dòng)、控制滾動(dòng)速度等邏輯。2.2 方案選型為什么不用Scroll Rect的自動(dòng)滾動(dòng)你可能會(huì)問(wèn)Scroll Rect本身不是有滾動(dòng)功能嗎為什么還要“重新發(fā)明輪子”這里有幾個(gè)關(guān)鍵考量控制粒度Scroll Rect的滾動(dòng)與用戶的拖拽輸入或通過(guò)腳本模擬的velocity緊密耦合。實(shí)現(xiàn)一個(gè)勻速、精準(zhǔn)的自動(dòng)滾動(dòng)需要額外處理慣性、減速等效果不夠純粹。直接控制anchoredPosition我們可以精確到每一幀的位移量。性能開(kāi)銷(xiāo)Scroll Rect組件包含了對(duì)布局、事件的處理對(duì)于簡(jiǎn)單的自動(dòng)滾動(dòng)場(chǎng)景來(lái)說(shuō)略顯臃腫。直接操作Transform屬性性能更優(yōu)。理解原理本項(xiàng)目的一個(gè)重要目的就是“揭秘隱藏玩法”。通過(guò)自己實(shí)現(xiàn)你能更深刻地理解UGUI的坐標(biāo)系統(tǒng)、錨點(diǎn)與軸心的關(guān)系以及anchoredPosition與localPosition的區(qū)別這些知識(shí)在解決其他UI動(dòng)態(tài)效果時(shí)至關(guān)重要。因此我們的方案是輕量、直接且具有教育意義的。它剝離了不必要的復(fù)雜性直擊問(wèn)題核心。3. 從零搭建UI場(chǎng)景與基礎(chǔ)設(shè)置讓我們開(kāi)始動(dòng)手。首先在Unity中創(chuàng)建一個(gè)新的UI場(chǎng)景。3.1 創(chuàng)建UI層級(jí)結(jié)構(gòu)在Canvas下創(chuàng)建一個(gè)空GameObject命名為ScrollViewport。這個(gè)將作為我們的視窗。為ScrollViewport添加一個(gè)Image組件用于背景可選并務(wù)必添加一個(gè)Mask或RectMask2D組件。RectMask2D性能更好但只適用于矩形裁剪。這里我們選擇RectMask2D。調(diào)整ScrollViewport的Rect Transform設(shè)定好你希望的顯示區(qū)域大小和位置。例如設(shè)置Width400 Height200。在ScrollViewport下創(chuàng)建一個(gè)子物體命名為Content。它將承載我們的文本。為Content添加Text或TextMeshPro - Text組件。如果你使用TextMeshPro記得先導(dǎo)入TMP Essentials資源包。關(guān)鍵的一步設(shè)置Content的Rect Transform錨點(diǎn)Anchors。為了實(shí)現(xiàn)從下至上的垂直滾動(dòng)我們需要將錨點(diǎn)預(yù)設(shè)設(shè)置為底部拉伸Bottom-Stretch。具體操作是在Rect Transform組件中點(diǎn)擊錨點(diǎn)預(yù)設(shè)圖標(biāo)選擇底部中間的那個(gè)然后按住Alt鍵點(diǎn)擊使其同時(shí)應(yīng)用錨點(diǎn)和位置。此時(shí)Content的錨點(diǎn)Min和Max應(yīng)為(0,0)和(1,0)即錨定在父物體的底部水平拉伸。它的Pivot軸心可以保持為(0.5, 0.5)或設(shè)為(0.5, 0)后者在計(jì)算時(shí)更方便。注意錨點(diǎn)的設(shè)置是后續(xù)滾動(dòng)計(jì)算正確與否的基石。如果錨點(diǎn)設(shè)置錯(cuò)誤anchoredPosition的變化將不會(huì)產(chǎn)生你預(yù)期的移動(dòng)效果。垂直滾動(dòng)通常錨定在底部或頂部水平滾動(dòng)則錨定在左側(cè)或右側(cè)。3.2 編寫(xiě)核心控制腳本創(chuàng)建一個(gè)C#腳本命名為AutoScrollText將其掛載到Content物體上。using UnityEngine; using UnityEngine.UI; // 如果使用標(biāo)準(zhǔn)Text using TMPro; // 如果使用TextMeshPro using System.Collections; public class AutoScrollText : MonoBehaviour { [Header(滾動(dòng)控制)] public float scrollSpeed 50.0f; // 像素/秒 public float startDelay 1.0f; // 開(kāi)始滾動(dòng)前的延遲 public bool loop false; // 是否循環(huán)滾動(dòng) [Header(組件引用)] [SerializeField] private RectTransform contentRect; [SerializeField] private Text textComponent; // 標(biāo)準(zhǔn)Text [SerializeField] private TMP_Text tmpTextComponent; // TextMeshPro Text private float contentHeight; private float viewportHeight; private Vector2 initialAnchoredPos; private Coroutine scrollCoroutine; void Start() { InitializeComponents(); InitializeDimensions(); StartAutoScroll(); } void InitializeComponents() { if (contentRect null) contentRect GetComponentRectTransform(); if (textComponent null) textComponent GetComponentText(); if (tmpTextComponent null) tmpTextComponent GetComponentTMP_Text(); // 記錄初始位置 initialAnchoredPos contentRect.anchoredPosition; } void InitializeDimensions() { // 獲取視窗高度父物體高度 RectTransform viewportRect contentRect.parent.GetComponentRectTransform(); viewportHeight viewportRect.rect.height; // 獲取內(nèi)容文本的實(shí)際高度 if (tmpTextComponent ! null) { // TextMeshPro 需要強(qiáng)制重建布局來(lái)獲取準(zhǔn)確的preferredHeight tmpTextComponent.ForceMeshUpdate(); contentHeight tmpTextComponent.preferredHeight; } else if (textComponent ! null) { contentHeight textComponent.preferredHeight; } else { Debug.LogError(未找到Text或TMP_Text組件); contentHeight viewportHeight; // fallback } // 關(guān)鍵調(diào)整Content的Height使其等于文本高度這樣才能有可滾動(dòng)的空間 contentRect.sizeDelta new Vector2(contentRect.sizeDelta.x, contentHeight); } }這段代碼完成了基礎(chǔ)的初始化和尺寸計(jì)算。InitializeDimensions方法是第一個(gè)關(guān)鍵點(diǎn)它計(jì)算了文本內(nèi)容的總高度contentHeight和視窗高度viewportHeight并通過(guò)設(shè)置contentRect.sizeDelta來(lái)確保內(nèi)容物體的高度足以容納全部文本。如果內(nèi)容高度小于視窗高度則無(wú)需滾動(dòng)這是一個(gè)重要的邊界條件判斷我們稍后會(huì)加上。4. 實(shí)現(xiàn)自動(dòng)滾動(dòng)動(dòng)畫(huà)邏輯接下來(lái)我們實(shí)現(xiàn)滾動(dòng)的核心協(xié)程。在AutoScrollText腳本中添加以下方法public void StartAutoScroll() { if (scrollCoroutine ! null) { StopCoroutine(scrollCoroutine); } scrollCoroutine StartCoroutine(ScrollRoutine()); } public void StopAutoScroll() { if (scrollCoroutine ! null) { StopCoroutine(scrollCoroutine); scrollCoroutine null; } } IEnumerator ScrollRoutine() { // 延遲開(kāi)始 yield return new WaitForSeconds(startDelay); // 邊界檢查如果內(nèi)容高度小于等于視窗高度則無(wú)需滾動(dòng) if (contentHeight viewportHeight Mathf.Epsilon) { Debug.Log(內(nèi)容高度不足無(wú)需滾動(dòng)。); yield break; } // 計(jì)算需要滾動(dòng)的總距離像素 float totalScrollDistance contentHeight - viewportHeight; // 計(jì)算滾動(dòng)所需總時(shí)間 float totalScrollTime totalScrollDistance / scrollSpeed; float elapsedTime 0f; while (elapsedTime totalScrollTime) { elapsedTime Time.deltaTime; float normalizedTime Mathf.Clamp01(elapsedTime / totalScrollTime); // 核心根據(jù)時(shí)間比例計(jì)算新的Y軸位置 // 初始位置是錨點(diǎn)位置底部對(duì)齊時(shí)為0滾動(dòng)到頂部時(shí)位置應(yīng)為 totalScrollDistance // 因?yàn)槲覀兪菑南峦蠞L所以 anchoredPosition.y 要增加 float newY Mathf.Lerp(0, totalScrollDistance, normalizedTime); contentRect.anchoredPosition new Vector2(contentRect.anchoredPosition.x, newY); yield return null; // 等待下一幀 } // 滾動(dòng)結(jié)束后根據(jù)循環(huán)設(shè)置決定行為 if (loop) { // 重置位置重新開(kāi)始 contentRect.anchoredPosition initialAnchoredPos; StartAutoScroll(); } else { // 停留在最終位置 contentRect.anchoredPosition new Vector2(contentRect.anchoredPosition.x, totalScrollDistance); } }這個(gè)ScrollRoutine協(xié)程是滾動(dòng)的發(fā)動(dòng)機(jī)。它首先進(jìn)行延遲然后計(jì)算從底部anchoredPosition.y 0滾動(dòng)到頂部剛好使文本最后一行進(jìn)入視窗anchoredPosition.y totalScrollDistance所需的總時(shí)間和距離。在每一幀中它使用Mathf.Lerp進(jìn)行線性插值平滑地更新contentRect.anchoredPosition.y的值。實(shí)操心得這里使用Mathf.Lerp進(jìn)行基于時(shí)間的插值比直接在每幀增加固定像素值如position.y speed * Time.deltaTime更好。因?yàn)楹笳呤軒什▌?dòng)影響滾動(dòng)總時(shí)間不精確。而基于時(shí)間的插值能保證無(wú)論幀率高低都在totalScrollTime秒內(nèi)完成滾動(dòng)體驗(yàn)更穩(wěn)定。4.1 處理動(dòng)態(tài)文本與尺寸更新在實(shí)際應(yīng)用中文本內(nèi)容可能是動(dòng)態(tài)變化的。我們需要在文本更新后重新計(jì)算尺寸并可能重啟滾動(dòng)。添加以下方法public void UpdateText(string newText) { if (tmpTextComponent ! null) tmpTextComponent.text newText; else if (textComponent ! null) textComponent.text newText; else return; // 停止當(dāng)前滾動(dòng) StopAutoScroll(); // 強(qiáng)制立即重建布局針對(duì)TMP尤其重要 if (tmpTextComponent ! null) tmpTextComponent.ForceMeshUpdate(); // 重新初始化尺寸 InitializeDimensions(); // 重置位置 contentRect.anchoredPosition initialAnchoredPos; // 重新開(kāi)始滾動(dòng) StartAutoScroll(); }4.2 使用Dotween優(yōu)化動(dòng)畫(huà)可選雖然協(xié)程方式已經(jīng)足夠但使用流行的動(dòng)畫(huà)插件Dotween可以讓代碼更簡(jiǎn)潔并輕松實(shí)現(xiàn)更復(fù)雜的緩動(dòng)效果。你需要先安裝Dotween插件。using DG.Tweening; // 引入Dotween命名空間 // 修改ScrollRoutine方法或新建一個(gè)使用Dotween的方法 public void StartAutoScrollWithTween() { if (contentHeight viewportHeight) return; float totalScrollDistance contentHeight - viewportHeight; float totalScrollTime totalScrollDistance / scrollSpeed; // 停止任何現(xiàn)有動(dòng)畫(huà) contentRect.DOKill(); // 重置位置 contentRect.anchoredPosition new Vector2(contentRect.anchoredPosition.x, 0); // 使用Dotween創(chuàng)建動(dòng)畫(huà) contentRect.DOAnchorPosY(totalScrollDistance, totalScrollTime) .SetDelay(startDelay) .SetEase(Ease.Linear) // 線性滾動(dòng) .OnComplete(() { if (loop) { StartAutoScrollWithTween(); // 循環(huán) } }); }Dotween版本代碼更短且自動(dòng)處理了時(shí)間縮放、暫停、銷(xiāo)毀等問(wèn)題SetEase方法可以輕松更換為其他緩動(dòng)函數(shù)如Ease.OutQuad來(lái)實(shí)現(xiàn)加速或減速效果非常靈活。5. 高級(jí)功能與細(xì)節(jié)打磨一個(gè)基礎(chǔ)的自動(dòng)滾動(dòng)文本框已經(jīng)完成但要投入實(shí)際項(xiàng)目還需要考慮更多細(xì)節(jié)。5.1 滾動(dòng)速度與時(shí)長(zhǎng)模式我們目前是基于速度像素/秒來(lái)計(jì)算時(shí)長(zhǎng)。有時(shí)產(chǎn)品可能要求“無(wú)論文本多長(zhǎng)都在固定時(shí)間內(nèi)滾完”。我們可以增加一個(gè)模式切換。在腳本頭部添加枚舉和變量public enum ScrollMode { FixedSpeed, FixedDuration } public ScrollMode scrollMode ScrollMode.FixedSpeed; public float fixedScrollDuration 10.0f; // 固定時(shí)長(zhǎng)模式下的總時(shí)間修改InitializeDimensions方法后的計(jì)算邏輯float totalScrollTime; if (scrollMode ScrollMode.FixedSpeed) { totalScrollTime totalScrollDistance / scrollSpeed; } else // FixedDuration { totalScrollTime fixedScrollDuration; // 可以根據(jù)總時(shí)長(zhǎng)反推出一個(gè)平均速度用于顯示或其他邏輯 // scrollSpeed totalScrollDistance / fixedScrollDuration; }5.2 暫停、繼續(xù)與跳轉(zhuǎn)功能為提升交互性我們可以添加暫停和繼續(xù)的方法以及跳轉(zhuǎn)到特定進(jìn)度的功能。private bool isPaused false; private float currentScrollTime 0f; // 需要在協(xié)程中更新這個(gè)值 // 在ScrollRoutine的while循環(huán)中加入暫停判斷 while (elapsedTime totalScrollTime) { if (!isPaused) { elapsedTime Time.deltaTime; currentScrollTime elapsedTime; // 記錄當(dāng)前時(shí)間 // ... 計(jì)算位置 ... } yield return null; } public void PauseScroll() { isPaused true; } public void ResumeScroll() { isPaused false; } public void JumpToNormalizedPosition(float normalizedPosition) { // normalizedPosition 范圍 0~1 normalizedPosition Mathf.Clamp01(normalizedPosition); StopAutoScroll(); // 停止當(dāng)前協(xié)程 float targetY Mathf.Lerp(0, contentHeight - viewportHeight, normalizedPosition); contentRect.anchoredPosition new Vector2(contentRect.anchoredPosition.x, targetY); // 如果需要從跳轉(zhuǎn)點(diǎn)繼續(xù)滾動(dòng)需要重新計(jì)算elapsedTime并啟動(dòng)新協(xié)程 // currentScrollTime totalScrollTime * normalizedPosition; // StartAutoScroll(); // 注意修改協(xié)程以支持從指定時(shí)間開(kāi)始 }5.3 性能優(yōu)化與注意事項(xiàng)文本重建與ForceMeshUpdate對(duì)于TextMeshPro在獲取preferredHeight前調(diào)用ForceMeshUpdate()是必須的否則獲取到的是上一幀的舊尺寸。這是一個(gè)常見(jiàn)的坑。對(duì)于大量動(dòng)態(tài)更新的文本頻繁調(diào)用ForceMeshUpdate可能有性能開(kāi)銷(xiāo)需要權(quán)衡。RectMask2D vs Mask優(yōu)先使用RectMask2D它不需要額外的圖形繪制性能更好。但Mask支持任意形狀的遮罩通過(guò)Alpha通道功能更強(qiáng)。對(duì)象池與超長(zhǎng)文本如果文本極長(zhǎng)比如成千上萬(wàn)行一次性生成所有文本的Mesh可能會(huì)造成卡頓。這時(shí)需要考慮分頁(yè)加載或使用TextMeshPro的TMP_Dynamic功能但這已超出本基礎(chǔ)自動(dòng)滾動(dòng)的范疇。錨點(diǎn)與軸心的陷阱再次強(qiáng)調(diào)如果滾動(dòng)方向或起始位置不對(duì)99%的問(wèn)題出在Content的Rect Transform錨點(diǎn)Anchors和軸心Pivot設(shè)置上。垂直滾動(dòng)通常需要錨定在底部Y Min0, Y Max0軸心也在底部Pivot Y0這樣anchoredPosition.y從0開(kāi)始增加時(shí)內(nèi)容才會(huì)向上移動(dòng)。務(wù)必理解anchoredPosition是相對(duì)于錨點(diǎn)的位置。6. 常見(jiàn)問(wèn)題排查與實(shí)戰(zhàn)技巧在實(shí)際開(kāi)發(fā)中你可能會(huì)遇到以下問(wèn)題問(wèn)題1文本根本不滾動(dòng)或者朝錯(cuò)誤方向滾動(dòng)。排查檢查Content物體的錨點(diǎn)。對(duì)于從下至上滾動(dòng)錨點(diǎn)應(yīng)設(shè)置在父物體底部。檢查腳本中計(jì)算newY的公式確保是從小到大Lerp(0, positiveDistance, t)。技巧在Scene視圖中將Content的Rect Transform的錨點(diǎn)預(yù)設(shè)切換到不同的模式觀察其藍(lán)色矩形框的變化理解錨點(diǎn)如何影響anchoredPosition的含義。問(wèn)題2滾動(dòng)到底部時(shí)最后一行文字顯示不全或留有空白。排查檢查contentHeight的計(jì)算是否準(zhǔn)確。對(duì)于TextMeshPro確認(rèn)在計(jì)算前調(diào)用了ForceMeshUpdate()。檢查視窗高度viewportHeight是否包含了內(nèi)邊距Padding。有時(shí)RectMask2D的邊界或父物體的Padding會(huì)略微影響實(shí)際可視區(qū)域。技巧可以在計(jì)算totalScrollDistance時(shí)增加一個(gè)微調(diào)偏移量float offset 5f進(jìn)行手動(dòng)校準(zhǔn)float totalScrollDistance contentHeight - viewportHeight offset;。問(wèn)題3在滾動(dòng)過(guò)程中更改文本位置會(huì)錯(cuò)亂。排查你是否在UpdateText方法中正確調(diào)用了InitializeDimensions()來(lái)更新contentHeight和contentRect.sizeDelta是否重置了anchoredPosition技巧將重置位置和重啟滾動(dòng)的邏輯封裝好確保任何導(dǎo)致內(nèi)容尺寸變化的操作后都能重新啟動(dòng)一個(gè)干凈的滾動(dòng)流程。問(wèn)題4在低幀率設(shè)備上滾動(dòng)不流暢或有卡頓。排查避免在每幀的Update中執(zhí)行耗時(shí)操作如復(fù)雜的字符串處理。確保文本組件沒(méi)有啟用Rich Text解析過(guò)于復(fù)雜的標(biāo)簽如果不需要。技巧使用Canvas.WillRenderCanvases事件或ContentSizeFitter組件需謹(jǐn)慎使用可能引起布局循環(huán)來(lái)更高效地響應(yīng)尺寸變化。對(duì)于純展示型自動(dòng)滾動(dòng)使用Dotween等經(jīng)過(guò)優(yōu)化的動(dòng)畫(huà)庫(kù)通常比手寫(xiě)協(xié)程插值更穩(wěn)定。問(wèn)題5如何實(shí)現(xiàn)“滾動(dòng)到末尾后暫停幾秒再重置循環(huán)”技巧在協(xié)程的滾動(dòng)結(jié)束部分while循環(huán)之后加入延遲即可。// 滾動(dòng)到頂部后 contentRect.anchoredPosition new Vector2(contentRect.anchoredPosition.x, totalScrollDistance); yield return new WaitForSeconds(2.0f); // 暫停2秒 if (loop) { contentRect.anchoredPosition initialAnchoredPos; StartAutoScroll(); }通過(guò)這個(gè)項(xiàng)目我們不僅實(shí)現(xiàn)了一個(gè)實(shí)用的自動(dòng)滾動(dòng)文本框更重要的是深入理解了Rect Transform的屬性如何驅(qū)動(dòng)UI動(dòng)畫(huà)。這種直接操作底層屬性的方式為你打開(kāi)了UI動(dòng)態(tài)效果的一扇新大門(mén)。你可以嘗試用類(lèi)似的原理實(shí)現(xiàn)橫向跑馬燈、視差滾動(dòng)列表、甚至更復(fù)雜的非線性動(dòng)畫(huà)讓UI界面真正“活”起來(lái)。記住核心永遠(yuǎn)是anchoredPosition、sizeDelta、錨點(diǎn)與軸心這幾個(gè)概念吃透了它們很多UI難題都會(huì)迎刃而解。