Unity UI点击事件系统

一、概述

  • 在Unity 的UGUI开发过程中,我们经常需要对UI图片进行操作响应各种处理比如:点击,长点击,长按,拖拽等多种功能的实现,这时原本的Button组件就不够用了
  • 我们需要自己实现一个简单的点击事件系统来响应用户的各种操作,下面可以分析一下思路。
  • 首先你需要提前了解点击事件的各个接口功能,了解C#中的委托使用,然后再学习这个系统

二、如何实现

  • 当我们想要响应长按等根时间挂钩的功能,就必须要使用Update函数的多帧调用来计算时间(FixUpdate应该也行)
  • 所以,我建议将自己实现的功能尽量写在Updata函数中,并通过一个流程来实现按键的状态切换。

1 基础框架

下图中POINTSTATE 枚举用于改变当前按键状态,通过外部修改按键状态,或者自身状态的变更,来实现按键的多种状态监测。(这里不添加拖拽状态,拖拽状态将会另外添加一个拖拽脚本专门用于响应拖拽事件)

public enum POINTSTATE{
    NONE,
    DOWN,
    STAY,
    UP,
    UPSPACE,
    EXIT,
    EXITSPACE,
}
public class UIEvent: MonoBehaviour
{
	void Update()
    {
        if (!gameObject.activeSelf) return;

        switch(state)
        {
            case POINTSTATE.NONE:   //无状态
                return;
            case POINTSTATE.DOWN:   //按键按下
                return;
            case POINTSTATE.STAY:   //点击停留时
                return;
            case POINTSTATE.UP:     //抬起
                return;
            case POINTSTATE.UPSPACE:                    
                return;
            case POINTSTATE.EXIT:   //退出
                return;
            case POINTSTATE.EXITSPACE:
                return;
        }
    }
}
  • 其次我们需要继承点击事件接口来进行按键响应
  • 每次监测到按下,抬起,结束时,都需要将按键状态进行变更,以便于在Update中进行响应(点击状态这里在Update中更改,所以在重写的OnClick方法中没有进行状态改变)
  • 同时可以将注册的点击事件(后面会写)进行调用,点击监测别直接写这里,后面需要的话可以进行动态添加,所以这里要进行判空操作
  • 注意点击事件只能被有图片的节点响应,所以添加一个特性使其不被无图片的节点继承
[RequireComponent(typeof(Image))]   //有图片组件才能被继承
public class UIEvent: MonoBehaviour , IPointerDownHandler, IPointerClickHandler, IPointerExitHandler, IPointerUpHandler
{
	//点击回调
    Action OnClick = null;
    //按下回调
    Action OnDown = null;
    //抬起回调
    Action OnUp = null;
    //结束回调
    Action OnExit = null;
    //当前点击状态
    POINTSTATE state = POINTSTATE.NONE;
	public void OnPointerClick(PointerEventData eventData)
    {
        if (OnClick != null) OnClick();
    }
    public void OnPointerDown(PointerEventData eventData)
    {
        state = POINTSTATE.DOWN;
        if (OnDown != null) OnDown();
    }
    public void OnPointerUp(PointerEventData eventData)
    {
        state = POINTSTATE.UP;
        if (OnDown != null) OnUp();
    }
    public void OnPointerExit(PointerEventData eventData)
    {
        state = POINTSTATE.EXIT;
        if (OnDown != null) OnExit();
    }
}

2 功能结构

  • 之后便可以进行外部接口的设计,这里添加以下几个
	//点击回调
    Action OnClick = null;
    //按下回调
    Action OnDown = null;
    //抬起回调
    Action OnUp = null;
    //结束回调
    Action OnExit = null;
	//长按回调 单次相应
    Action OnLongClick = null;
    //按下回调 多次相应,且相应速度加快
    Action OnPress = null;
	// 设置点击间隔(防止短时间大量连点操作)
    public static void SetClickSpace(int space)
    // 注册一般点击
    public static void AddClick(GameObject _go, Action _func)
    // 注册按键按下
    public static void AddDown(GameObject _go, Action _func)
    // 注册按键抬起
    public static void AddUp(GameObject _go, Action _func)
    // 注册按键退出
    public static void AddExit(GameObject _go, Action _func)
    // 注册长点击
    public static void AddLongClick(GameObject _go, Action _func, float _space = 1f)
    // 注册长按
    public static void AddPress(GameObject _go, Action _func, float _space = 1f, float _minSpace = 0.1f)
  • 到这里整个结构就比较清晰了,首先通过调用UIEvent的静态接口,给需要的节点添加事件,并在按键按下时,在Update函数中调用已经添加的事件。

2 具体实现

  • 首先实现一个可能被多次调用的接口, 对传入的对象更改RayCastTarget,并返回UIEvent脚本,没有挂载就添加一个。

  • 这里为了方便起见直接写在UIEvent中,如果想结构更加清晰一点可以分开写

    //检查图像并挂载脚本
    static UIEvent Get(GameObject _go)
    {
        Graphic graphic = _go.GetComponent<Graphic>();
        if (graphic) graphic.raycastTarget = true;

        UIEvent uiEvent = _go.GetComponent<UIEvent>();
        if (uiEvent == null) uiEvent = _go.AddComponent<UIEvent>();
        return uiEvent;
    }
  • 然后说外部接口,具体实现如下:
  • 先挂载脚本,然后将改脚本下得相应点击事件值进行更改即可。
  • 其中LongClick函数和Press函数可以额外传入长按时间、最短响应间隔时间。
#region 外部注册相关接口
    // 设置点击间隔
    public static void SetClickSpace(int space)
    {
        timeClickSpace = space;
    }
    /// <summary>
    /// 注册点击
    /// </summary>
    /// <param name="_go">被注册对象</param>
    /// <param name="_func">回调函数</param>
    public static void AddClick(GameObject _go, Action _func)
    {
        UIEvent uIEvent = Get(_go);
        if (uIEvent) uIEvent.OnClick = _func;
    }
    /// <summary>
    /// 注册按下
    /// </summary>
    /// <param name="_go">被注册对象</param>
    /// <param name="_func">回调函数</param>
    public static void AddDown(GameObject _go, Action _func)
    {
        UIEvent uIEvent = Get(_go);
        if (uIEvent) uIEvent.OnDown = _func;
    }
    /// <summary>
    /// 注册抬起
    /// </summary>
    /// <param name="_go">被注册对象</param>
    /// <param name="_func">回调函数</param>
    public static void AddUp(GameObject _go, Action _func)
    {
        UIEvent uIEvent = Get(_go);
        if (uIEvent) uIEvent.OnUp = _func;
    }
    /// <summary>
    /// 注册结束点击
    /// </summary>
    /// <param name="_go">被注册对象</param>
    /// <param name="_func">回调函数</param>
    public static void AddExit(GameObject _go, Action _func)
    {
        UIEvent uIEvent = Get(_go);
        if (uIEvent) uIEvent.OnClick = _func;
    }
    /// <summary>
    /// 注册长点击
    /// </summary>
    /// <param name="_go">被注册对象</param>
    /// <param name="_func">回调函数</param>
    /// <param name="_space">长按时间</param>
    public static void AddLongClick(GameObject _go, Action _func, float _space = 1f)
    {
        UIEvent uIEvent = Get(_go);
        if (uIEvent)
        {
            uIEvent.timeLongClickSpace = _space;
            uIEvent.OnLongClick = _func;
        }
    }
    /// <summary>
    /// 注册长按
    /// </summary>
    /// <param name="_go">被注册对象</param>
    /// <param name="_func">回调函数</param>
    /// <param name="_space">长按时间</param>
    /// <param name="_minSpace">最短响应间隔</param>
    public static void AddPress(GameObject _go, Action _func, float _space = 1f, float _minSpace = 0.1f)
    {
        UIEvent uIEvent = Get(_go);
        if (uIEvent)
        {
            uIEvent.timePressSpace = _space;
            uIEvent.OnPress = _func;
            uIEvent.timePressMinSpace = _minSpace;
        }
    }
  • 最后看一下UpdateLongClickPress事件的执行条件
  1. 首先在上面注册事件
  2. 然后当重写的接口触发 按下 或 抬起 的事件,并将状态进行切换
  3. Update中监测到state值变化,就开始一个新的流程如下
  • 进入POINTSTATE.DOWN,初始化需要用到的值
  • 第二次进入Update函数,此时state已被改为POINTSTATE.STAY
  • 开始判断是否存在LongClickPress回调,有则执行,并且在这个时候,state值不会Update函数内被改变
  • 直到在接口中将值改为POINTSTATE.UP为止
#region 注册相关的属性
    //点击回调
    Action OnClick = null;
    //按下回调
    Action OnDown = null;
    //抬起回调
    Action OnUp = null;
    //结束回调
    Action OnExit = null;
    //长按回调 单次相应
    Action OnLongClick = null;
    //按下回调 多次相应,且相应速度加快
    Action OnPress = null;

    //单词击计数器
    float timeClick = 0;
    //一秒内点击限制
    static int timeClickSpace = 10;

    //长点击计数器
    float timeLongClick = 0;
    //长点击生效时长
    float timeLongClickSpace = 1.0f;

    //长按计数器
    float timePress = 0;
    //长按递减间隔
    float timePressSpace = 1.0f;
    //长按递减间隔缓存
    float timePressSpaceCache = 0;
    //长按最低间隔
    float timePressMinSpace = 0.1f;
    //当前点击状态
    POINTSTATE state = POINTSTATE.NONE;

    #endregion
    //事件响应流程写在Update中
    void Update()
    {
        if (!gameObject.activeSelf) return;

        switch(state)
        {
            case POINTSTATE.NONE:   //无状态
                return;
            case POINTSTATE.DOWN:   //
                timeLongClick = Time.time;              //记录长点击开始时间

                // 如果注册了OnPress事件则直接执行
                if (OnPress != null && Time.time - timePress > (1 / timeClickSpace)) OnPress();
                timePress = Time.time;                  //记录长按开始时间
                timePressSpaceCache = timePressSpace;   // 长按事件响应间隔
                state = POINTSTATE.STAY;
                return;
            case POINTSTATE.STAY:                       //点击停留时
                if(OnLongClick != null) //长点击
                {
                    if(Time.time - timeLongClick > timeLongClickSpace)  //到时间了开始执行
                    {
                        OnLongClick();
                        state = POINTSTATE.NONE;    //进入结束状态
                    }
                }
                if(OnPress != null)     //长按
                {
                    float spaceTime = Time.time - timePress;    //距离上一次间隔时间
                    if(spaceTime > timePressSpaceCache && spaceTime > (1 / timeClickSpace))
                    {
                        //记录相应时间点
                        timePress = Time.time;
                        //缩短相应间隔
                        timePressSpaceCache *= (2 / 3f);
                        timePressSpaceCache = Mathf.Max(timePressSpaceCache, timePressMinSpace);
                        //执行回调
                        OnPress();
                    }
                }
                return;
            case POINTSTATE.UP:                         //抬起
                state = POINTSTATE.UPSPACE;
                return;
            case POINTSTATE.UPSPACE:                    
                return;
            case POINTSTATE.EXIT:                       //退出
                state = POINTSTATE.EXITSPACE;
                return;
            case POINTSTATE.EXITSPACE:
                return;
        }
    }

关于窗口拖拽事件,这里没有选择实现在UIEvent上,只是使用了UIEvent 进行事件注册,过程与之前的方式类似。两个函数分别是

    static UIEventDrag GetDrag(GameObject _go)
    {
        UIEventDrag uIEventDrag = _go.GetComponent<UIEventDrag>();
        if (!uIEventDrag) uIEventDrag = _go.AddComponent<UIEventDrag>();
        return uIEventDrag;
    }
    /// <summary>
    /// 注册拖拽
    /// </summary>
    /// <param name="_go">被移动对象</param>
    /// <param name="_window">需要点击的对象</param>
    public static void AddDrag(GameObject _go, Transform _window)
    {
        UIEventDrag uIEvent = GetDrag(_go);
        uIEvent.IsWindowDrag = true;
        uIEvent.Window = _window;
    }
  • UIEventDrag是另一个挂了Nomo的脚本,代码贴在最后面了,它的实现可以相对独立;这里的实现针对性较强,大家用的时候可以根据需要进行自定义

新增双击

	//双击回调
    Action OnDoubleClick = null;
    //双击最大间隔时间
    private float timeDoubleClick = 0.5f;
    //双击计时
    private float timeDoubleClickCount = 0f;

	void Update()
	{
		switch (pointState)
        {
        	case POINTERSTATE.UP:
                //Log.Print("抬起");                
                //双击
                if (OnDoubleClick != null && Time.time - timeDoubleClickCount <= timeDoubleClick) OnDoubleClick();
                timeDoubleClickCount = Time.time;
                break;
        }
	}
	//注册双击
    public static void AddDoubleClick(GameObject _go, Action _fun, float _space = 0.7f)
    {
        var uievent = Get(_go);
        if (uievent)
        {
            uievent.timeDoubleClick = _space;
            uievent.OnDoubleClick = _fun;
        }
    }

三、完整代码

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public enum POINTSTATE{
    NONE,
    DOWN,
    STAY,
    UP,
    UPSPACE,
    EXIT,
    EXITSPACE,
}

[RequireComponent(typeof(Image))]   //有图片组件才能被继承
public class UIEvent: MonoBehaviour , IPointerDownHandler, IPointerClickHandler, IPointerExitHandler, IPointerUpHandler
{
    #region 注册相关的属性
    //点击回调
    Action OnClick = null;
    //按下回调
    Action OnDown = null;
    //抬起回调
    Action OnUp = null;
    //结束回调
    Action OnExit = null;
    //长按回调 单次相应
    Action OnLongClick = null;
    //按下回调 多次相应,且相应速度加快
    Action OnPress = null;
	//双击回调
    Action OnDoubleClick = null;
    
    //单词击计数器
    float timeClick = 0;
    //一秒内点击限制
    static int timeClickSpace = 10;

	//双击最大间隔时间
    private float timeDoubleClick = 0.5f;
    //双击计时
    private float timeDoubleClickCount = 0f;

    //长点击计数器
    float timeLongClick = 0;
    //长点击生效时长
    float timeLongClickSpace = 1.0f;

    //长按计数器
    float timePress = 0;
    //长按递减间隔
    float timePressSpace = 1.0f;
    //长按递减间隔缓存
    float timePressSpaceCache = 0;
    //长按最低间隔
    float timePressMinSpace = 0.1f;
    //当前点击状态
    POINTSTATE state = POINTSTATE.NONE;

    #endregion
    //事件响应流程写在Update中
    void Update()
    {
        if (!gameObject.activeSelf) return;

        switch(state)
        {
            case POINTSTATE.NONE:   //无状态
                return;
            case POINTSTATE.DOWN:   //
                timeLongClick = Time.time;              //记录长点击开始时间

                // 如果注册了OnPress事件则直接执行
                if (OnPress != null && Time.time - timePress > (1 / timeClickSpace)) OnPress();
                timePress = Time.time;                  //记录长按开始时间
                timePressSpaceCache = timePressSpace;   // 长按事件响应间隔
                state = POINTSTATE.STAY;
                return;
            case POINTSTATE.STAY:                       //点击停留时
                if(OnLongClick != null) //长点击
                {
                    if(Time.time - timeLongClick > timeLongClickSpace)  //到时间了开始执行
                    {
                        OnLongClick();
                        state = POINTSTATE.NONE;    //进入结束状态
                    }
                }
                if(OnPress != null)     //长按
                {
                    float spaceTime = Time.time - timePress;    //距离上一次间隔时间
                    if(spaceTime > timePressSpaceCache && spaceTime > (1 / timeClickSpace))
                    {
                        //记录相应时间点
                        timePress = Time.time;
                        //缩短相应间隔
                        timePressSpaceCache *= (2 / 3f);
                        timePressSpaceCache = Mathf.Max(timePressSpaceCache, timePressMinSpace);
                        //执行回调
                        OnPress();
                    }
                }
                return;
            case POINTSTATE.UP:                         //抬起
            	//双击
            	if (OnDoubleClick != null && Time.time - timeDoubleClickCount <= timeDoubleClick) OnDoubleClick();
                timeDoubleClickCount = Time.time;
                
                state = POINTSTATE.UPSPACE;
                return;
            case POINTSTATE.UPSPACE:                    
                return;
            case POINTSTATE.EXIT:                       //退出
                state = POINTSTATE.EXITSPACE;
                return;
            case POINTSTATE.EXITSPACE:
                return;
        }
    }

    //检查图像并挂载脚本
    static UIEvent Get(GameObject _go)
    {
        Graphic graphic = _go.GetComponent<Graphic>();
        if (graphic) graphic.raycastTarget = true;

        UIEvent uiEvent = _go.GetComponent<UIEvent>();
        if (uiEvent == null) uiEvent = _go.AddComponent<UIEvent>();
        return uiEvent;
    }

    static UIEventDrag GetDrag(GameObject _go)
    {
        UIEventDrag uIEventDrag = _go.GetComponent<UIEventDrag>();
        if (!uIEventDrag) uIEventDrag = _go.AddComponent<UIEventDrag>();
        return uIEventDrag;
    }

    #region 外部注册相关接口
    // 设置点击间隔
    public static void SetClickSpace(int space)
    {
        timeClickSpace = space;
    }
    /// <summary>
    /// 注册点击
    /// </summary>
    /// <param name="_go">被注册对象</param>
    /// <param name="_func">回调函数</param>
    public static void AddClick(GameObject _go, Action _func)
    {
        UIEvent uIEvent = Get(_go);
        if (uIEvent) uIEvent.OnClick = _func;
    }
    /// <summary>
    /// 注册按下
    /// </summary>
    /// <param name="_go">被注册对象</param>
    /// <param name="_func">回调函数</param>
    public static void AddDown(GameObject _go, Action _func)
    {
        UIEvent uIEvent = Get(_go);
        if (uIEvent) uIEvent.OnDown = _func;
    }
    /// <summary>
    /// 注册抬起
    /// </summary>
    /// <param name="_go">被注册对象</param>
    /// <param name="_func">回调函数</param>
    public static void AddUp(GameObject _go, Action _func)
    {
        UIEvent uIEvent = Get(_go);
        if (uIEvent) uIEvent.OnUp = _func;
    }
    /// <summary>
    /// 注册结束点击
    /// </summary>
    /// <param name="_go">被注册对象</param>
    /// <param name="_func">回调函数</param>
    public static void AddExit(GameObject _go, Action _func)
    {
        UIEvent uIEvent = Get(_go);
        if (uIEvent) uIEvent.OnClick = _func;
    }
    /// <summary>
    /// 注册长点击
    /// </summary>
    /// <param name="_go">被注册对象</param>
    /// <param name="_func">回调函数</param>
    /// <param name="_space">长按时间</param>
    public static void AddLongClick(GameObject _go, Action _func, float _space = 1f)
    {
        UIEvent uIEvent = Get(_go);
        if (uIEvent)
        {
            uIEvent.timeLongClickSpace = _space;
            uIEvent.OnLongClick = _func;
        }
    }
    /// <summary>
    /// 注册长按
    /// </summary>
    /// <param name="_go">被注册对象</param>
    /// <param name="_func">回调函数</param>
    /// <param name="_space">长按时间</param>
    /// <param name="_minSpace">最短响应间隔</param>
    public static void AddPress(GameObject _go, Action _func, float _space = 1f, float _minSpace = 0.1f)
    {
        UIEvent uIEvent = Get(_go);
        if (uIEvent)
        {
            uIEvent.timePressSpace = _space;
            uIEvent.OnPress = _func;
            uIEvent.timePressMinSpace = _minSpace;
        }
    }
    /// <summary>
    /// 注册拖拽
    /// </summary>
    /// <param name="_go">被移动对象</param>
    /// <param name="_window">需要点击的对象</param>
    public static void AddDrag(GameObject _go, Transform _window)
    {
        UIEventDrag uIEvent = GetDrag(_go);
        uIEvent.IsWindowDrag = true;
        uIEvent.Window = _window;
    }
    //注册双击
    public static void AddDoubleClick(GameObject _go, Action _fun, float _space = 0.7f)
    {
        var uievent = Get(_go);
        if (uievent)
        {
            uievent.timeDoubleClick = _space;
            uievent.OnDoubleClick = _fun;
        }
    }
    #endregion  

    #region 点击重写
    public void OnPointerClick(PointerEventData eventData)
    {
        //检查间隔
        if (Time.time - timeClick < 1.0f / timeClickSpace) { return; }

        if (OnClick != null)
        {
            OnClick();
            timeClick = Time.time;
        }
    }

    public void OnPointerDown(PointerEventData eventData)
    {
        state = POINTSTATE.DOWN;
        if (OnDown != null) OnDown();
    }

    public void OnPointerUp(PointerEventData eventData)
    {
        state = POINTSTATE.UP;
        if (OnDown != null) OnUp();
    }
    public void OnPointerExit(PointerEventData eventData)
    {
        state = POINTSTATE.EXIT;
        if (OnDown != null) OnExit();
    }
    #endregion
}

using System.Collections;
using System.Collections.Generic;

using UnityEngine;
using UnityEngine.EventSystems;

public class UIEventDrag : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
    public bool IsWindowDrag = false;
    public Transform Window;
    private Vector2 winDiffPoint;
    private Vector2 winDeltaPoint;

    private float lx;
    private float ly;
    public void OnDrag(PointerEventData _eventData)
    {
        if (IsWindowDrag)
        {
            winDeltaPoint = _eventData.position - winDiffPoint;
            Window.localPosition += new Vector3(winDeltaPoint.x, winDeltaPoint.y, 0);
            if (Window.localPosition.x > lx) Window.localPosition = new Vector3(lx, Window.localPosition.y, 0);
            if (Window.localPosition.x < -lx2) Window.localPosition = new Vector3(-lx2, Window.localPosition.y, 0);
            if (Window.localPosition.y > ly) Window.localPosition = new Vector3(Window.localPosition.x, ly, 0);
            if (Window.localPosition.y < -ly2) Window.localPosition = new Vector3(Window.localPosition.x, -ly2, 0);
            winDiffPoint = _eventData.position;
        }
    }

    public void OnBeginDrag(PointerEventData _eventData)
    {

        if (IsWindowDrag)
        {
            winDiffPoint = _eventData.position;
            RectTransform rt = GetComponent<RectTransform>();
            lx = (Screen.width - rt.rect.width) / 2 - transform.localPosition.x;
            lx2 = (Screen.width - rt.rect.width) / 2 + transform.localPosition.x;
            ly = (Screen.height - rt.rect.height) / 2 - transform.localPosition.y;
            ly2 = (Screen.height - rt.rect.height) / 2 + transform.localPosition.y;
        }
    }

    public void OnEndDrag(PointerEventData eventData)
    {

    }
}

  • 19
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

KamikazePilot

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值