這裏要實現的是使用UI模仿3D形式的輪轉圖
- 上半段的代碼的輪轉圖是在手指離開屏幕或者鼠標拖拽左鍵起來的時候才進行輪轉動作的。
- 在觸控或者鼠標按下拖拽的的時候即可進行動畫響應的模式叫做即刻響應模式 在博客後半段
是在離開響應模式的邏輯基礎上改的
代碼裏面比較繞的部分都有註解
讀者在哪裏覺得不懂的可以評論
這裏需要用到Dotween插件,沒有的要先下載
離開響應模式
主要控制邏輯:
邏輯圖如下
代碼分爲兩個部分 主控制部分RotationDiagram和每個子項控制部分RotationDiagramItem
使用方法就是在UGUI下面一個空物體上掛好RotationDiagram腳本 然後賦值好對應的值即可
在實現中幾個關鍵實現的地方是:
- 根據圖片在圓周的ratio來計算出圖片的水平座標值X
- 根據圖片在圓周的ratio來計算出圖片的尺寸大小
- 圖片的深度根據尺寸大小來決定
圖片深度部分是比較繞的地方,這裏通過三個數據結構來大概解釋他的運行原理
-
一個是ItemPosSclData 在一開始的時候就根據在inspector面板的設置進行生成,個數和裏面的對應的位置值和縮放值
-
一個是ItemHierarchyData,在初始化的時候記錄自身對應的ItemPosSclData,在ItemHierarchyData的List根據自身對應的ItemPosSclData的scale排序好後,把排序後自身的index存儲到對應的ItemPosSclData,在ItemPosSclData賦值給RotationDiagramItem後
這個值是每個RotationDiagramItem應該對應的在父物體中的sibling index
UGUI根據物體的sibling順序來控制顯示的層級 -
一個就是RotationDiagramItem了
每次拖拽之後每個RotationDiagramItem自身對應的ItemPosSclData
都會進行更換,更換後根據ItemPosSclData的值來設置自身
主控制部分代碼如下
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UI;
public class RotationDiagram : MonoBehaviour
{
public Vector2 ItemSize;
public Sprite[] ItemSprites;
/// <summary>
/// 卡片間隙的大小
/// </summary>
public float gapOffset;
/// <summary>
/// 每個圖片 隨着它所在的位置比例 縮小的最小倍數
/// </summary>
public float ScaleShrinkMin;
/// <summary>
/// 每個圖片 隨着它所在的位置比例 放大的最大倍數
/// </summary>
public float ScaleAmplifyMax;
private List<RotationDiagramItem> rdItems;
private List<ItemPosSclData> itemPosSclDatas;
// Start is called before the first frame update
void Start()
{
rdItems = new List<RotationDiagramItem>();
itemPosSclDatas = new List<ItemPosSclData>();
CreateItem();
CalulateData();
SetItemData();
}
/// <summary>
/// 獲取單個圖片物體信息模板
/// </summary>
/// <returns></returns>
private GameObject CreateTemplate()
{
GameObject item = new GameObject("Template");
//這裏的sizeDelta可以直接賦值的時候
//是因爲anchor是一個點,這是的sizeDelta表示的意思就是UI的區域範圍
item.AddComponent<RectTransform>().sizeDelta = ItemSize;
item.AddComponent<Image>();
item.AddComponent<RotationDiagramItem>();
return item;
}
private void CreateItem()
{
GameObject template = CreateTemplate();
RotationDiagramItem itemTemp = null;
foreach (Sprite sprite in ItemSprites)
{
//Instantiate傳入對象是場景裏面的物體也是可以的
itemTemp = Instantiate(template).GetComponent<RotationDiagramItem>();
itemTemp.SetTrnParent(transform);
itemTemp.SetSprite(sprite);
itemTemp.AddMoveListener(Change);
rdItems.Add(itemTemp);
}
Destroy(template);
}
private void Change(float endDragOffsetX)
{
//根據拖拽的方向來將 所有的圖片左移或者右移一個單位
int moveDirUnit = endDragOffsetX > 0 ? 1 : -1;
Change(moveDirUnit);
}
/// <summary>
/// 根據單個RotationDiagramItem的拖拽
/// 來進行整體RotationDiagramItem的更新
/// </summary>
/// <param name="moveDirUnit"></param>
private void Change(int moveDirUnit)
{
foreach (RotationDiagramItem rdItem in rdItems)
{
rdItem.ChangeId(moveDirUnit, rdItems.Count);
}
for (int i = 0; i < itemPosSclDatas.Count; i++)
{
//rdItems的數量與itemPosSclDatas一致
rdItems[i].PlayTurnAnim(itemPosSclDatas[rdItems[i].itemPosSclDatasMatchId]);
}
}
private void CalulateData()
{
List<ItemHierarchyData> itemHierarchyDatas = new List<ItemHierarchyData>();
//每個段可以看成是圖片的寬度加上空隙的寬度
//所有的段合起來就是環形的周長 即來回兩遍的x軸大小
//不是單向的長度 因爲這裏是所有的rdItems而不是一半的
float girth = (ItemSize.x + gapOffset) * rdItems.Count;
Debug.LogError(" Screen.width " + Screen.width);
Debug.LogError(" girth / 2 " + girth / 2);
//每移動一段代表的比例
float ratioOffset = 1 / (float)rdItems.Count;
float itemMatchRatio = 0;
//下面的計算中
//以環形中部畫面最上的圖片所在的位置的比率看成是0或1
//以環形中部畫面最下的圖片所在的位置的比率看成是0.5
for (int i = 0; i < rdItems.Count; i++)
{
ItemHierarchyData itemHierarchyData = new ItemHierarchyData
{
relItemPosSclDataId = i
};
itemHierarchyDatas.Add(itemHierarchyData);
rdItems[i].itemPosSclDatasMatchId = i;
ItemPosSclData data = new ItemPosSclData
{
X = GetRelativeRatioXPos(itemMatchRatio, girth),
ScaleMul = GetScaleMul(itemMatchRatio, ScaleAmplifyMax, ScaleShrinkMin)
};
itemMatchRatio += ratioOffset;
itemPosSclDatas.Add(data);
}
//每個lambda表達是式的右邊返回的是 一個數字
//OrderBy是升序排序 按照返回的數字大小對傳入的元素進行排序
//這裏面的u代表的是每個itemHierarchyData
//這裏從上文可知 排序前 u.PosId 與 u所在index 值 一樣
//即itemHierarchyDatas[1].PosId = 1
//並且itemHierarchyDatas的總數量和itemPosSclDatas一樣
//這裏是把itemHierarchyDatas的順序按照他對應的itemPosSclDatas的ScaleMul的升序進行排序
//排序後最開頭的itemHierarchyData是尺寸最小的(邊緣的) 最後面的是尺寸最大的(邊緣的)
itemHierarchyDatas = itemHierarchyDatas.OrderBy(u => itemPosSclDatas[u.relItemPosSclDataId].ScaleMul).ToList();
for (int i = 0; i < itemHierarchyDatas.Count; i++)
{
//這裏雖然itemHierarchyDatas經過排序
//但是每個itemHierarchyDataPosId對應原先的itemPosSclDatas
//找到對應的itemPosSclData之後
//再將本身屬於這個itemPosSclData的Scale對應的排序賦值回去
//即Order最小的一般是尺寸最小的也是最底的 Order最大的一般是尺寸最大的也是最上的
itemPosSclDatas[itemHierarchyDatas[i].relItemPosSclDataId].hierarchyOrder = i;
}
foreach (var item in itemPosSclDatas)
{
Debug.LogError("item " + item);
}
}
private void SetItemData()
{
for (int i = 0; i < itemPosSclDatas.Count; i++)
{
rdItems[i].PlayTurnAnim(itemPosSclDatas[i]);
}
}
/// <summary>
/// 根據圓形軌道的進度返回它在水平x的位置
/// 這裏把環形中部的X位置看成是0
/// </summary>
/// <param name="ratio"></param>
/// <param name="girth"></param>
/// <returns></returns>
private float GetRelativeRatioXPos(float ratio, float girth)
{
if (ratio > 1 || ratio < 0)
{
Debug.LogError("當前比例必須是0-1的值");
return 0;
}
if (ratio >= 0 && ratio < 0.25f)
{
return girth * ratio;
}
//把整個圓形周長濃縮在x方向上看成是一條線L
//L長度就是周長的0.5
//L部分的中點是X是0的位置
//所以最右端是周長的0.25部分 對應的X座標是girth * 0.25
//L部分的中點遮擋的後面那部分是周長0.5的部分
//最左端是周長的0.75的部分 對應的X座標是girth * -0.25
//在ratio從0.25到0.5的這個階段
//是X從周長0.25部分縮短到X爲0的部分
//在ratio從0.5到0.75的這個階段
//是從X爲0的部分變化到X爲-0.25的部分
//在ratio從0.75到1的這個階段
//是從X爲-0.25的部分變化到X爲0的部分
else if (ratio >= 0.25f && ratio < 0.75f)
{
return girth * (0.5f - ratio);
}
else
{
return girth * (ratio - 1);
}
}
public float GetScaleMul(float ratio, float max, float min)
{
if (ratio > 1 || ratio < 0)
{
Debug.LogError("當前比例必須是0-1的值");
return 0;
}
float scaleOffset = max - min;
//乘以2的原因是 最大和最小的相差只是一半的周長
if (ratio < 0.5f)
{
//在環形中間屏幕最外的是max
//在環形中間屏幕最內的是min
//ratio從0(1)變到0.5的時候,是從最大開始變化到最小的過程
return max - scaleOffset * ratio * 2;
}
else
{
//ratio大於0.5變到1的時候,是從最小開始變化到最大的過程
//這裏 1 - ratio 看成是ratio到1(0)還差多少 因爲1(0)的時候是最大的
return max - scaleOffset * (1 - ratio) * 2;
}
//上述規則計算出的尺寸兩邊對稱位置的尺寸一樣
}
}
/// <summary>
/// 存儲與位置比例有關的放大縮小和對應x位置等信息
/// </summary>
public class ItemPosSclData
{
public float X;
public float ScaleMul;
public int hierarchyOrder;
public override string ToString()
{
string str = "ItemPosSclData X " + X + " ScaleMul " + ScaleMul + " Order " + hierarchyOrder;
return str;
}
}
public struct ItemHierarchyData
{
/// <summary>
/// 存儲的是他對應的ItemPosSclData的下標
/// </summary>
public int relItemPosSclDataId;
}
子項部分的邏輯如下:
using System;
using System.Collections;
using System.Collections.Generic;
using DG.Tweening;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
/// <summary>
/// 每個圖片的信息封裝
/// </summary>
public class RotationDiagramItem : MonoBehaviour,IDragHandler,IEndDragHandler
{
/// <summary>
/// 這個變量是自己本身對應的某個itemPosSclData元素
/// 在itemPosSclDatas裏面的下標值
/// 在滑動之後 改變每個RotationDiagramItem對應的下標值
/// 然後再通過下標值找到對應的ItemPosSclData來刷新自己的位置和大小
/// </summary>
public int itemPosSclDatasMatchId;
private Action<float> _moveAction;
private Image _image;
private float _offsetX;
private float _aniTime = 1;
private Image Image
{
get
{
if (_image == null)
_image = GetComponent<Image>();
return _image;
}
}
private RectTransform _rect;
private RectTransform Rect
{
get
{
if (_rect == null)
_rect = GetComponent<RectTransform>();
return _rect;
}
}
public void SetTrnParent(Transform parent)
{
transform.SetParent(parent);
}
public void SetSprite(Sprite sprite)
{
Image.sprite = sprite;
}
public void PlayTurnAnim(ItemPosSclData data)
{
Rect.DOAnchorPos(Vector2.right*data.X, _aniTime);
Rect.DOScale(Vector3.one*data.ScaleMul, _aniTime);
StartCoroutine(WaitAndSetHier(data));
}
//在動畫演示到一半的時候才通過子物體的順序進行顯示層級的更換
//如果一開始就進行 則會在重疊的時候直接先將本來在背面的圖片顯示在前面
private IEnumerator WaitAndSetHier(ItemPosSclData data)
{
yield return new WaitForSeconds(_aniTime * 0.5f);
transform.SetSiblingIndex(data.hierarchyOrder);
}
public void AddMoveListener(Action<float> onMove)
{
_moveAction = onMove;
}
public void ChangeId(int moveDirUnit,int totalRDItemNum)
{
int id = itemPosSclDatasMatchId;
id += moveDirUnit;
//對moveDirUnit爲-1的時候 運算後越界的結果糾正
if (id < 0)
{
id += totalRDItemNum;
}
//對moveDirUnit爲1的時候 運算後越界的結果糾正
itemPosSclDatasMatchId = id % totalRDItemNum;
}
#region 拖拽部分
//在拖拽的每幀都會調用
public void OnDrag(PointerEventData eventData)
{
//將拖拽過程中每幀的x偏移量(有正有負,向右拖拽x爲正)進行計算得到總的偏移量
_offsetX += eventData.delta.x;
}
public void OnEndDrag(PointerEventData eventData)
{
_moveAction(_offsetX);
_offsetX = 0;
}
#endregion
}
即刻響應模式因爲難度較大代碼還在編輯調試中 敬請期待
即刻響應模式
離開響應模式有幾個缺點:
- 只能一次滑動一個單位
- 離開才觸發移動 用戶體驗不好
- 拖拽結束後的動畫過程中 如果再次進行反方向的拖拽 會出bug
這裏針對上述缺點進行更改
改造後有新的特性就是在鼠標按下狀態或者觸摸狀態的時候 根據用戶輸入的X值的改變來實時更新每個圖片的對應位置和大小
相比於離開響應模式,即刻響應模式額外的一些的地方有:
- 記錄每個圖片之間的X值間隔A
- 記錄每個圖片之間的比例值間隔 即每移動一段的X值代表的弧長佔圓周比例a
- 在拖拽的過程中記錄對拖拽的X值所代表的佔總長的比例值c 根據每個圖片本身所屬的比例值與c相加得出這些圖片現在所屬的比例值,
- 在拖拽結束時候,要進行收尾工作,即當前滑動方向再向下一個整數的方向靠齊,相當於現在滑動了
-2.5個單位拖拽結束了,但是轉輪要滑動到-3個的單位,-2.5代表左滑了2.5個單位,右滑是類似的情況
最後感嘆一下 一些看起來很簡單的東西 裏面的細節還真是多 有些東西做到盡善盡美真是不容易啊 很多細節要處理 需要耐心細心才能勝任