这里要实现的是使用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个单位,右滑是类似的情况
最后感叹一下 一些看起来很简单的东西 里面的细节还真是多 有些东西做到尽善尽美真是不容易啊 很多细节要处理 需要耐心细心才能胜任