目前ugui的功能已经十分强大了,大多数的ui需求都可以通过预定义控件的组合,配合脚本来实现,相信大部分开发者在处理这类需求时,都会按以上方式来制作prefab,这当然是没问题的,不过还有没有更高效简洁的方式呢?
目前ugui预定义的ui控件种类较少,你应该会想过编写自定义的控件来进行拓展,这篇文章会实现一个简单的coverflow效果控件,将其添加到UI的可选控件列表中,并且可通过asset文件配置其参数。项目源码已上传github,链接在文章最后附上。
在UI列表加入自定义控件选项
要自定义控件添加到unity编辑器,控件生成类需要using UnityEditor
,来使用编辑器状态下的类型。
在编写的自定义控件生成类前添加AddComponentMenu("GameObject/UI/CoverFlow")
特性,使该控件可以在顶部工具栏中显示
在控件生成方法前添加[MenuItem("GameObject/UI/CoverFlow")]
特性,使其出现在Hierachy下的右键列表里
实现控件生成方法,点击菜单选项创建控件
控件生成方法必须是静态方法public static void CreateCoverFlow(MenuCommand menuCommand)
,MenuCommand
对象可以捕获菜单点击时的上下文信息,用来确定自定义控件的父对象等,通过AddComponent
方法为对象添加组件,最终组合为你希望呈现的效果。
如下coverflow效果,首先以当前右键点击对象为父对象一个coverflow根对象,为其添加CoverFlowBehavior
行为类(该类也需要自行编写,具体实现在下方给出)作为控制器,并设置相关参数。
在for循环中添加coverflow的子对象,数量尺寸间距等参数由CoverFlowBehavior
指定,为其添加RawImage
、CoverFlowImageBehavior
等组件。
这样一个简单的coverflow效果就完成了,并可以通过右键点击Canvas=>UI=>CoverFlow进行添加了。
//CoverFlow根对象
GameObject coverflow = new GameObject("CoverFlow");
RectTransform coverflowRTrs = coverflow.AddComponent<RectTransform>();
Transform parent = (menuCommand.context as GameObject).transform;
coverflow.transform.SetParent(parent, false)
CoverFlowBehavior coverFlowBehavior = coverflow.AddComponent<CoverFlowBehavior>();
coverFlowBehavior.imageHeight = coverFlowParameters.imageSizeDelta.y;
//CoverFlow图片子对象
for (int i = 0; i < coverFlowParameters.imageCount; i++)
{
GameObject image = new GameObject("Image" + i);
RectTransform imageRTrs = image.AddComponent<RectTransform>();
image.transform.SetParent(coverflowRTrs, false);
imageRTrs.sizeDelta = coverFlowParameters.imageSizeDelta;
image.AddComponent<RawImage>();
AspectRatioFitter aspectRatioFitter = image.AddComponent<AspectRatioFitter>();
aspectRatioFitter.aspectMode = AspectRatioFitter.AspectMode.HeightControlsWidth;
CoverFlowImageBehavior coverFlowImageBehavior = image.AddComponent<CoverFlowImageBehavior>();
coverFlowImageBehavior.coverFlowIndex = i - coverFlowParameters.imageCount / 2;
}
CoverFlowBehavior和CoverFlowImageBehavior的实现
具体实现就不在本篇文章展开讲了,下面把源码附上,逻辑并不复杂
public class CoverFlowBehavior : MonoBehaviour, IDragHandler, IBeginDragHandler, IEndDragHandler
{
[Tooltip("CoverFlow的图片列表")]
public List<Texture2D> t2ds;
[Tooltip("图片间距的基础值")]
public float imageGap = 100;
[Tooltip("图片基础尺寸")]
public float imageHeight = 300;
[Tooltip("起始图片的索引")]
public int initIndex = 0;
public event SetImageDisPlay setImageDisPlay; //发布刷新图片显示效果事件
public event SetImageIndex setImageIndex; //发布刷新图片索引事件
[HideInInspector]
public int childCount;
float dragBeginPosX;
private void Awake()
{
childCount = transform.childCount;
}
void Start()
{
InitIndex(initIndex);
}
public void OnBeginDrag(PointerEventData data)
{
dragBeginPosX = data.position.x;
}
public void OnDrag(PointerEventData data)
{
}
public void OnEndDrag(PointerEventData data)
{
if (data.position.x - dragBeginPosX > 10)
RefreshImageIndex(1);
else if (data.position.x - dragBeginPosX < -10)
RefreshImageIndex(-1);
setImageDisPlay();
}
void RefreshImageIndex(int direction)
{
setImageIndex(direction);
}
/// <summary>
/// 初始化coverflow的显示位置
/// </summary>
/// <param name="index">起始索引</param>
public void InitIndex(int index)
{
if (t2ds.Count == 0)
throw new System.Exception("Coverflow need at least one image, please assign image first!");
for (int i = 0; i < childCount; i++)
{
transform.GetChild(i).GetComponent<CoverFlowImageBehavior>().t2dIndex = index;
if (++index > t2ds.Count - 1)
index = 0;
}
}
public delegate void SetImageIndex(int direction);
public delegate void SetImageDisPlay();
}
public class CoverFlowImageBehavior : MonoBehaviour, IPointerClickHandler
{
public int coverFlowIndex;
public int t2dIndex;
RectTransform rTrs;
RawImage rawImage;
AspectRatioFitter aspectRatioFitter;
CoverFlowBehavior coverFlowBehavior;
public UnityEvent unityEvent;
private void Awake()
{
rTrs = GetComponent<RectTransform>();
rawImage = GetComponent<RawImage>();
aspectRatioFitter = GetComponent<AspectRatioFitter>();
coverFlowBehavior = transform.parent.GetComponent<CoverFlowBehavior>();
coverFlowBehavior.setImageIndex += SetIndex; //订阅刷新图片索引事件
coverFlowBehavior.setImageDisPlay += SetImageDisplay; //订阅刷新图片显示效果事件
}
void Start()
{
SetImageDisplay();
}
public void OnPointerClick(PointerEventData data)
{
if (coverFlowIndex == 0)
{
unityEvent.Invoke();
}
}
/// <summary>
/// 设置CoverFlow中的位置索引和图片的索引
/// </summary>
/// <param name="direction"></param>
public void SetIndex(int direction)
{
coverFlowIndex += direction;
if (coverFlowIndex > coverFlowBehavior.childCount / 2)
{
coverFlowIndex = -coverFlowBehavior.childCount / 2;
t2dIndex -= coverFlowBehavior.childCount;
while(t2dIndex < 0)
t2dIndex += coverFlowBehavior.t2ds.Count;
}
if (coverFlowIndex < -coverFlowBehavior.childCount / 2)
{
coverFlowIndex = coverFlowBehavior.childCount / 2;
t2dIndex += coverFlowBehavior.childCount;
while (t2dIndex > coverFlowBehavior.t2ds.Count - 1)
t2dIndex -= coverFlowBehavior.t2ds.Count;
}
}
/// <summary>
/// 根据索引确定图片显示效果
/// </summary>
public void SetImageDisplay()
{
float targetPosX = coverFlowBehavior.imageGap * coverFlowIndex; //目标位置
float sizeRatio = (coverFlowBehavior.childCount - Mathf.Abs(coverFlowIndex)) / (float)coverFlowBehavior.childCount; //目标尺寸
int targetSiblingIndex = coverFlowBehavior.childCount - 1 - (coverFlowIndex >= 0 ? Mathf.Abs(coverFlowIndex) * 2 : Mathf.Abs(coverFlowIndex) * 2 - 1); //目标层级
float alpha = 1 - (1 - sizeRatio) * 0.8f;
rTrs.SetSiblingIndex(targetSiblingIndex);
rTrs.DOSizeDelta(coverFlowBehavior.imageHeight * Vector2.one * sizeRatio, 0.3f);
rTrs.DOAnchorPosX(targetPosX, 0.3f).onComplete = () =>
{
rawImage.texture = coverFlowBehavior.t2ds[t2dIndex];
aspectRatioFitter.aspectRatio = rawImage.texture.width / (float)rawImage.texture.height;
rawImage.color = new Color(alpha, alpha, alpha, 1);
};
}
}
为生成类添加.asset文件的创建方法
到这里一个自定义插件的编写就已经完成了,但是每次修改生成参数,都需要修改Builder类,我们可以将需要修改的参数放入一个Parameter类中,这里我将其定义为了可以设置值的单例类
public class CoverFlowParameters : ScriptableObject
{
[SerializeField]
public int imageCount = 3;
[SerializeField]
public Vector2 imageSizeDelta = new Vector2();
static CoverFlowParameters instance;
public static CoverFlowParameters Instance
{
get
{
if (instance == null)
instance = CreateInstance<CoverFlowParameters>();
return instance;
}
set
{
instance = value;
}
}
private CoverFlowParameters() { }
}
参数类中有两个序列化的字段,imageCount
、imageSizeDelta
是可以通过asset来修改的,也正是这两个字段作为了CoverFlowBuilder
类的参数
现在为Parameter类成一个.asset文件用来方便参数的设置,下面是一个生成.asset对象的泛型方法,类型T需要继承自ScriptableObject
public static void CreateAsset<T>() where T : ScriptableObject
{
string path = Application.dataPath + "/Settings";
if (!Directory.Exists(path))
Directory.CreateDirectory(path);
AssetDatabase.CreateAsset(CreateInstance(typeof(T)), string.Format("Assets/Settings/{0}.asset", typeof(T).ToString()));
}
用此方法创建CoverFlowBuilder的.asset文件,并将选项扩展到工具栏
[MenuItem("ObjectAsset/CreateCoverFlowAsset")]
static void CreateCoverFlowAsset()
{
AssetBuilder.CreateAsset<CoverFlowBuilder>();
}
若使用.asset文件设置和保存参数,在控件创建方法CreateCoverFlow
中,需要先读取该asset文件中的参数
//从asset中获取coverflow参数
CoverFlowParameters.Instance = AssetDatabase.LoadAssetAtPath<CoverFlowParameters>("Assets/Settings/CoverFlowParameters.asset");
CoverFlowParameters coverFlowParameters = CoverFlowParameters.Instance;
这样就可以在工具栏由ObjectAsset=>CreateCoverFlowAsset来创建Builder类的asset,并通过修改asset文件来指定Builder类的参数了。
项目源码github链接:https://github.com/Luke-Wang/UGUIExtend