一、概述
- 在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
中,如果想结构更加清晰一点可以分开写文章来源:https://www.toymoban.com/news/detail-796614.html
//检查图像并挂载脚本
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;
}
}
- 最后看一下
Update
中LongClick
和Press
事件的执行条件
- 首先在上面注册事件
- 然后当重写的接口触发 按下 或 抬起 的事件,并将状态进行切换
-
Update
中监测到state
值变化,就开始一个新的流程如下
- 进入
POINTSTATE.DOWN
,初始化需要用到的值 - 第二次进入
Update
函数,此时state
已被改为POINTSTATE.STAY
- 开始判断是否存在
LongClick
和Press
回调,有则执行,并且在这个时候,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 进行事件注册,过程与之前的方式类似。两个函数分别是文章来源地址https://www.toymoban.com/news/detail-796614.html
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)
{
}
}
到了这里,关于Unity UI点击事件系统的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!