简易UGUI插件的开发教程

目前ugui的功能已经十分强大了,大多数的ui需求都可以通过预定义控件的组合,配合脚本来实现,相信大部分开发者在处理这类需求时,都会按以上方式来制作prefab,这当然是没问题的,不过还有没有更高效简洁的方式呢?
目前ugui预定义的ui控件种类较少,你应该会想过编写自定义的控件来进行拓展,这篇文章会实现一个简单的coverflow效果控件,将其添加到UI的可选控件列表中,并且可通过asset文件配置其参数。项目源码已上传github,链接在文章最后附上。

扩展完成效果.png
coverflow效果.gif

在UI列表加入自定义控件选项

要自定义控件添加到unity编辑器,控件生成类需要using UnityEditor,来使用编辑器状态下的类型。
在编写的自定义控件生成类前添加AddComponentMenu("GameObject/UI/CoverFlow")特性,使该控件可以在顶部工具栏中显示

工具栏扩展效果.png

在控件生成方法前添加[MenuItem("GameObject/UI/CoverFlow")]特性,使其出现在Hierachy下的右键列表里

扩展完成效果.png

实现控件生成方法,点击菜单选项创建控件

控件生成方法必须是静态方法public static void CreateCoverFlow(MenuCommand menuCommand)MenuCommand对象可以捕获菜单点击时的上下文信息,用来确定自定义控件的父对象等,通过AddComponent方法为对象添加组件,最终组合为你希望呈现的效果。
如下coverflow效果,首先以当前右键点击对象为父对象一个coverflow根对象,为其添加CoverFlowBehavior行为类(该类也需要自行编写,具体实现在下方给出)作为控制器,并设置相关参数。
在for循环中添加coverflow的子对象,数量尺寸间距等参数由CoverFlowBehavior指定,为其添加RawImageCoverFlowImageBehavior等组件。
这样一个简单的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() { }
}

参数类中有两个序列化的字段,imageCountimageSizeDelta是可以通过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创建选项.png

若使用.asset文件设置和保存参数,在控件创建方法CreateCoverFlow中,需要先读取该asset文件中的参数

        //从asset中获取coverflow参数
        CoverFlowParameters.Instance = AssetDatabase.LoadAssetAtPath<CoverFlowParameters>("Assets/Settings/CoverFlowParameters.asset");
        CoverFlowParameters coverFlowParameters = CoverFlowParameters.Instance;

这样就可以在工具栏由ObjectAsset=>CreateCoverFlowAsset来创建Builder类的asset,并通过修改asset文件来指定Builder类的参数了。


生成的asset文件.png
通过asset文件指定参数.png

项目源码github链接:https://github.com/Luke-Wang/UGUIExtend

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342