C#自定义日期时间选择器
目录
日期时间选择器
1.界面搭建
1.1.日期选择器
1.2.时间选择器
2.分析
2.1.日期选择器
2.2.时间选择器
3.具体实现
3.1.时间选择器DatePicker
3.2.某天DayItem
3.3.时间选择器TimePicker
3.4.日期时间选择器DateTimePicker
4.效果展示编辑
本篇文章来分享一下如何制作一个日期时间选择器,主要包括日期选择器和时分秒选择器两大部分。展示效果如下。
日期时间选择器
1.界面搭建
1.1.日期选择器
日期选择器主要包括年月选择和日历部分,年月选择部分主要有4个年月切换的按钮,日历部分主要通过Unity布局组件来进行搭建日历表格,使用Grid Layout Group(网格布局组)可以快速实现,如下图。想要了解Unity布局,可查看【一文读懂】Unity布局组件:Horizontal Layout Group(水平布局组),Vertical Layout Group(垂直布局组),Grid Layout Group(网格布局组)
其中Day需添加Toggle组件,所有的Day共用一个Toggle Group,确保只能选中一天。
1.2.时间选择器
时间选择器包括时分秒的选择, 如下是 时 选择部分的搭建,注意添加Mask组件,限制显示的内容,分、秒 选择部分的搭建同理。
2.分析
2.1.日期选择器
日期选择器核心功能是展示日历,观察日历可以发现,主要有三个部分:当前月的所有日期,上一个月的后几天,下一个月的前几天。
那如何得到具体的日期范围呢?可以通过计算当前月 1 号的星期位置,确定当前月日期在日历表格中的显示范围,然后在范围前后补充上个月末和下月初的日期,最终形成一个完整的日历表格(包含当前月所有日期及前后月的补充日期),为了更好的展示效果,可以通过透明度区分当前月与其他月份的日期。具体逻辑如下
/// <summary> 展示日历 </summary>
public void ShowCalendar()
{curWeek = (int)((new DateTime(curSelectedDateTime.Year, curSelectedDateTime.Month, 1))).DayOfWeek;curDays = DateTime.DaysInMonth(curSelectedDateTime.Year, curSelectedDateTime.Month);timeText.text = curSelectedDateTime.ToString("yyyy/MM");//当前月的 日 索引int monthDaysIndex = 0;//得到上个月的天数int lastMonthDays = DateTime.DaysInMonth(curSelectedDateTime.Year, curSelectedDateTime.Month - 1 <= 0 ? 12 : curSelectedDateTime.Month - 1);if (curSelectedDateTime.Month - 1 <= 0){lastMonthDays = DateTime.DaysInMonth(curSelectedDateTime.Year - 1, 12);}else{lastMonthDays = DateTime.DaysInMonth(curSelectedDateTime.Year, curSelectedDateTime.Month - 1);}for (int i = 0; i < calenderTable.childCount; i++){Transform dayTrans = calenderTable.GetChild(i);//确定当前月的1号进行设置if (i >= curWeek - 1 && i < curDays + curWeek - 1){int showCurMonthDay = monthDaysIndex + 1;dayTrans.GetComponentInChildren<TMP_Text>().text = showCurMonthDay.ToString();Color color = dayTrans.GetComponentInChildren<TMP_Text>().color;color.a = 1;dayTrans.GetComponentInChildren<TMP_Text>().color = color;DateTime dateTime = new DateTime(curSelectedDateTime.Year, curSelectedDateTime.Month, showCurMonthDay);dayTrans.GetComponent<DayItem>().SetDataTime(dateTime);++monthDaysIndex;}else{if (i < curWeek - 1)//展示上个月的最后几天{int showLastMonthDay = lastMonthDays - curWeek + i + 2;dayTrans.GetComponentInChildren<TMP_Text>().text = showLastMonthDay.ToString();Color color = dayTrans.GetComponentInChildren<TMP_Text>().color;color.a = 100.0f / 255.0f;dayTrans.GetComponentInChildren<TMP_Text>().color = color;DateTime dateTime;if (curSelectedDateTime.Month - 1 <= 0){dateTime = new DateTime(curSelectedDateTime.Year - 1, 12, showLastMonthDay);}else{dateTime = new DateTime(curSelectedDateTime.Year, curSelectedDateTime.Month - 1, showLastMonthDay);}dayTrans.GetComponent<DayItem>().SetDataTime(dateTime);}if (i >= curDays + curWeek - 1)//展示下个月的开始几天{int showNextMonthDay = i - curDays - curWeek + 2;dayTrans.GetComponentInChildren<TMP_Text>().text = showNextMonthDay.ToString();Color color = dayTrans.GetComponentInChildren<TMP_Text>().color;color.a = 100.0f / 255.0f;dayTrans.GetComponentInChildren<TMP_Text>().color = color;DateTime dateTime;if (curSelectedDateTime.Month + 1 > 12){dateTime = new DateTime(curSelectedDateTime.Year + 1, 1, showNextMonthDay);}else{dateTime = new DateTime(curSelectedDateTime.Year, curSelectedDateTime.Month + 1, showNextMonthDay);}dayTrans.GetComponent<DayItem>().SetDataTime(dateTime);}}}for (int i = 0; i < calenderTable.childCount; i++){Transform trans = calenderTable.GetChild(i);DateTime time = new DateTime(curSelectedDateTime.Year, curSelectedDateTime.Month, curSelectedDateTime.Day);if (DateTime.Equals(trans.GetComponent<DayItem>().dateTime, time)){trans.GetComponent<Toggle>().isOn = false;trans.GetComponent<Toggle>().isOn = true;}}
}
在改变日期后,要展示当前选中的日期,这样的行为联动,我们可以想到事件的特点,“事件发生后,其他对象做出响应”,此时我们可以使用事件来实现,事件的相关知识可以参考【超详细】C#事件。
2.2.时间选择器
通过拖拽进行时间的具体选择,需继承IBeginDragHandler, IDragHandler, IEndDragHandler接口。
那如何更新选择的时间?我们可以将垂直滑动操作转换为时间的调整,通过灵敏度和最大增量控制滑动范围,同时确保时间的合法。在改变时间后,要更新当前选择的时间,同理,也需使用到事件,如下是更新选择的时间的具体实现。
/// <summary> 更新选择的时间 </summary>
/// <param name="eventData"></param>
private void UpdateSelectTime(PointerEventData eventData)
{float deltaY = eventData.position.y - oldPos.y;itemContent.GetComponent<RectTransform>().anchoredPosition = new Vector3(0, itemContent.GetComponent<RectTransform>().anchoredPosition.y + deltaY, 0);//设置灵敏度和最大增量float sensitivity = 0.1f; //灵敏度系数float maxDelta = 0.5f; //最大增量//计算增量float changeDelta = deltaY * sensitivity;changeDelta = Mathf.Clamp(changeDelta, -maxDelta, maxDelta); //限制增量if (Mathf.Abs(changeDelta) > maxDelta){changeDelta = Mathf.Sign(changeDelta) * maxDelta; //限制增量}//根据时间类型更新时间DateTime dateTime = curSelectedDateTime;switch (timeType){case TimeType.Hour:dateTime = curSelectedDateTime.AddHours(changeDelta);break;case TimeType.Minute:dateTime = curSelectedDateTime.AddMinutes(changeDelta);break;case TimeType.Second:dateTime = curSelectedDateTime.AddSeconds(changeDelta);break;}//检查时间是否合法if (IsDateTimeLegal(dateTime, minDateTime, maxDateTime)){curSelectedDateTime = dateTime;RefreshTimeList();onTimeChanged?.Invoke(timeType, curSelectedDateTime);}//更新旧位置oldPos = eventData.position;
}
3.具体实现
3.1.时间选择器DatePicker
using System;
using UnityEngine;
using UnityEngine.UI;
using TMPro;namespace DateTimePicker
{/// <summary> 日期选择器 </summary>public class DatePicker : MonoBehaviour{private Button preYearBtn;private Button nextYearBtn;private Button preMonthBtn;private Button nextMonthBtn;[HideInInspector]public TMP_Text timeText;private Transform calenderTable;/// <summary> 当前选择的日期时间 </summary>public DateTime curSelectedDateTime;/// <summary> 当前日期月份1号的星期 0-星期天、1-星期一、2-星期二... </summary>private int curWeek;/// <summary> 当前日期所在月份的天数 </summary>private int curDays;private void Awake(){preYearBtn = transform.FindChildByName("PreYearBtn").GetComponent<Button>();nextYearBtn = transform.FindChildByName("NextYearBtn").GetComponent<Button>();preMonthBtn = transform.FindChildByName("PreMonthBtn").GetComponent<Button>();nextMonthBtn = transform.FindChildByName("NextMonthBtn").GetComponent<Button>();timeText = transform.FindChildByName("TimeText").GetComponent<TMP_Text>();calenderTable = transform.FindChildByName("CalenderTable");preYearBtn.onClick.AddListener(OnPreYearBtnClicked);nextYearBtn.onClick.AddListener(OnNextYearBtnClicked);preMonthBtn.onClick.AddListener(OnPreMonthBtnClicked);nextMonthBtn.onClick.AddListener(OnNextMonthBtnClicked);}/// <summary> 展示日历 </summary>public void ShowCalendar(){curWeek = (int)((new DateTime(curSelectedDateTime.Year, curSelectedDateTime.Month, 1))).DayOfWeek;curDays = DateTime.DaysInMonth(curSelectedDateTime.Year, curSelectedDateTime.Month);timeText.text = curSelectedDateTime.ToString("yyyy/MM");//当前月的 日 索引int monthDaysIndex = 0;//得到上个月的天数int lastMonthDays = DateTime.DaysInMonth(curSelectedDateTime.Year, curSelectedDateTime.Month - 1 <= 0 ? 12 : curSelectedDateTime.Month - 1);if (curSelectedDateTime.Month - 1 <= 0){lastMonthDays = DateTime.DaysInMonth(curSelectedDateTime.Year - 1, 12);}else{lastMonthDays = DateTime.DaysInMonth(curSelectedDateTime.Year, curSelectedDateTime.Month - 1);}for (int i = 0; i < calenderTable.childCount; i++){Transform dayTrans = calenderTable.GetChild(i);//确定当前月的1号进行设置if (i >= curWeek - 1 && i < curDays + curWeek - 1){int showCurMonthDay = monthDaysIndex + 1;dayTrans.GetComponentInChildren<TMP_Text>().text = showCurMonthDay.ToString();Color color = dayTrans.GetComponentInChildren<TMP_Text>().color;color.a = 1;dayTrans.GetComponentInChildren<TMP_Text>().color = color;DateTime dateTime = new DateTime(curSelectedDateTime.Year, curSelectedDateTime.Month, showCurMonthDay);dayTrans.GetComponent<DayItem>().SetDataTime(dateTime);++monthDaysIndex;}else{if (i < curWeek - 1)//展示上个月的最后几天{int showLastMonthDay = lastMonthDays - curWeek + i + 2;dayTrans.GetComponentInChildren<TMP_Text>().text = showLastMonthDay.ToString();Color color = dayTrans.GetComponentInChildren<TMP_Text>().color;color.a = 100.0f / 255.0f;dayTrans.GetComponentInChildren<TMP_Text>().color = color;DateTime dateTime;if (curSelectedDateTime.Month - 1 <= 0){dateTime = new DateTime(curSelectedDateTime.Year - 1, 12, showLastMonthDay);}else{dateTime = new DateTime(curSelectedDateTime.Year, curSelectedDateTime.Month - 1, showLastMonthDay);}dayTrans.GetComponent<DayItem>().SetDataTime(dateTime);}if (i >= curDays + curWeek - 1)//展示下个月的开始几天{int showNextMonthDay = i - curDays - curWeek + 2;dayTrans.GetComponentInChildren<TMP_Text>().text = showNextMonthDay.ToString();Color color = dayTrans.GetComponentInChildren<TMP_Text>().color;color.a = 100.0f / 255.0f;dayTrans.GetComponentInChildren<TMP_Text>().color = color;DateTime dateTime;if (curSelectedDateTime.Month + 1 > 12){dateTime = new DateTime(curSelectedDateTime.Year + 1, 1, showNextMonthDay);}else{dateTime = new DateTime(curSelectedDateTime.Year, curSelectedDateTime.Month + 1, showNextMonthDay);}dayTrans.GetComponent<DayItem>().SetDataTime(dateTime);}}}for (int i = 0; i < calenderTable.childCount; i++){Transform trans = calenderTable.GetChild(i);DateTime time = new DateTime(curSelectedDateTime.Year, curSelectedDateTime.Month, curSelectedDateTime.Day);if (DateTime.Equals(trans.GetComponent<DayItem>().dateTime, time)){trans.GetComponent<Toggle>().isOn = false;trans.GetComponent<Toggle>().isOn = true;}}}/// <summary> 上一年 </summary>private void OnPreYearBtnClicked(){//检查是否可以减去一年if (curSelectedDateTime.Year > DateTime.MinValue.Year){curSelectedDateTime = curSelectedDateTime.AddYears(-1);ShowCalendar();}}/// <summary> 下一年 </summary>private void OnNextYearBtnClicked(){//检查是否可以加上一年if (curSelectedDateTime.Year < DateTime.MaxValue.Year){curSelectedDateTime = curSelectedDateTime.AddYears(1);ShowCalendar();}}/// <summary> 上一月 </summary>private void OnPreMonthBtnClicked(){//检查是否可以减去一个月if (curSelectedDateTime >= DateTime.MinValue.AddMonths(1)){curSelectedDateTime = curSelectedDateTime.AddMonths(-1);ShowCalendar();}}/// <summary> 下一月 </summary>private void OnNextMonthBtnClicked(){//检查是否可以加上一个月if (curSelectedDateTime <= DateTime.MaxValue.AddMonths(-1)){curSelectedDateTime = curSelectedDateTime.AddMonths(1);ShowCalendar();}}}
}
3.2.某天DayItem
using System;
using UnityEngine;
using UnityEngine.UI;namespace DateTimePicker
{/// <summary> 改变日期 的 委托 </summary>public delegate void DayClick(Transform transform, DateTime dateTime);/// <summary> 某天 </summary>public class DayItem : MonoBehaviour{public DateTime dateTime;/// <summary> 改变日期 </summary>public event DayClick OnDayClicked;private void Start(){transform.GetComponent<Toggle>().onValueChanged.AddListener(OnToggleChanged);}private void OnToggleChanged(bool isOn){if (isOn){transform.parent.parent.parent.GetComponent<DatePicker>().curSelectedDateTime = dateTime;OnDayClicked?.Invoke(transform, dateTime);}}public void SetDataTime(DateTime time){dateTime = time;}}
}
3.3.时间选择器TimePicker
using System;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;namespace DateTimePicker
{/// <summary> 时间选择器:时、分、秒 </summary>public class TimePicker : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler{public TimeType timeType;public int itemCount = 7;/// <summary> 子物体 </summary>private GameObject item;/// <summary> 容器对象,item的父物体 </summary>private Transform itemContent;[HideInInspector]/// <summary> 当前选择的日期时间 </summary>public DateTime curSelectedDateTime;private DateTime minDateTime;private DateTime maxDateTime;private Vector3 oldPos;/// <summary> 改变时间 </summary>public event OnTimeChanged onTimeChanged;private void Awake(){item = transform.FindChildByName("Item").gameObject;itemContent = transform.FindChildByName("Content");}/// <summary> 初始化时间选择器 </summary>/// <param name="dt"></param>public void InitPicker(DateTime dt){SetMinAndMaxDateTimeInDay(dt);for (int i = 0; i < itemCount; i++){int posIndex = i - itemCount / 2;CreateItem(posIndex);}RefreshTimeList();}/// <summary> 设置当前选中日期的时间范围 </summary>/// <param name="dt"></param>public void SetMinAndMaxDateTimeInDay(DateTime dt){curSelectedDateTime = dt;minDateTime = new DateTime(curSelectedDateTime.Year, curSelectedDateTime.Month, curSelectedDateTime.Day, 0, 0, 0);maxDateTime = new DateTime(curSelectedDateTime.Year, curSelectedDateTime.Month, curSelectedDateTime.Day, 23, 59, 59);}/// <summary> 创建 日历中 的 天Item </summary>/// <param name="posIndex"></param>private void CreateItem(int posIndex){GameObject go = UnityEngine.Object.Instantiate(item, itemContent);go.transform.localScale = Vector3.one;go.SetActive(true);if (posIndex != 0){go.transform.GetComponent<TMP_Text>().color = new Color(1, 1, 1, 1 - 0.2f - (Mathf.Abs(posIndex) / (itemCount / 2.0f + 1)));}}/// <summary> 刷新时间列表 </summary>private void RefreshTimeList(){DateTime dateTime = new DateTime();for (int i = 0; i < itemCount; i++){int posIndex = i - itemCount / 2;switch (timeType){case TimeType.Hour:dateTime = curSelectedDateTime.AddHours(posIndex);break;case TimeType.Minute:dateTime = curSelectedDateTime.AddMinutes(posIndex);break;case TimeType.Second:dateTime = curSelectedDateTime.AddSeconds(posIndex);break;}string str = "";if (IsDateTimeLegal(dateTime, minDateTime, maxDateTime)){switch (timeType){case TimeType.Hour:str = dateTime.Hour.ToString("D2");break;case TimeType.Minute:str = dateTime.Minute.ToString("D2");break;case TimeType.Second:str = dateTime.Second.ToString("D2");break;}}itemContent.GetChild(i).GetComponent<TMP_Text>().text = str;}}/// <summary> 判断某个日期是否在某段日期范围内,返回布尔值 </summary> /// <param name="dt">要判断的日期</param> /// <param name="minDt">开始日期</param> /// <param name="maxDt">结束日期</param> /// <returns></returns> private bool IsDateTimeLegal(DateTime dt, DateTime minDt, DateTime maxDt){return dt.CompareTo(minDt) >= 0 && dt.CompareTo(maxDt) <= 0;}public void OnBeginDrag(PointerEventData eventData){oldPos = eventData.position;}public void OnDrag(PointerEventData eventData){UpdateSelectTime(eventData);}public void OnEndDrag(PointerEventData eventData){itemContent.GetComponent<RectTransform>().anchoredPosition = Vector3.zero;}/// <summary> 更新选择的时间 </summary>/// <param name="eventData"></param>private void UpdateSelectTime(PointerEventData eventData){float deltaY = eventData.position.y - oldPos.y;itemContent.GetComponent<RectTransform>().anchoredPosition = new Vector3(0, itemContent.GetComponent<RectTransform>().anchoredPosition.y + deltaY, 0);//设置灵敏度和最大增量float sensitivity = 0.1f; //灵敏度系数float maxDelta = 0.5f; //最大增量//计算增量float changeDelta = deltaY * sensitivity;changeDelta = Mathf.Clamp(changeDelta, -maxDelta, maxDelta); //限制增量if (Mathf.Abs(changeDelta) > maxDelta){changeDelta = Mathf.Sign(changeDelta) * maxDelta; //限制增量}//根据时间类型更新时间DateTime dateTime = curSelectedDateTime;switch (timeType){case TimeType.Hour:dateTime = curSelectedDateTime.AddHours(changeDelta);break;case TimeType.Minute:dateTime = curSelectedDateTime.AddMinutes(changeDelta);break;case TimeType.Second:dateTime = curSelectedDateTime.AddSeconds(changeDelta);break;}//检查时间是否合法if (IsDateTimeLegal(dateTime, minDateTime, maxDateTime)){curSelectedDateTime = dateTime;RefreshTimeList();onTimeChanged?.Invoke(timeType, curSelectedDateTime);}//更新旧位置oldPos = eventData.position;}}/// <summary> 时间类型:时、分、秒 </summary>public enum TimeType{Hour,Minute,Second}/// <summary> 改变时间 的 委托 </summary>public delegate void OnTimeChanged(TimeType timeType, DateTime dateTime);
}
3.4.日期时间选择器DateTimePicker
using System;
using TMPro;
using UnityEngine;namespace DateTimePicker
{/// <summary> 日期时间选择器 </summary>public class DateTimePicker : MonoBehaviour{private TMP_Text selectedTime;private DatePicker datePicker;private TMP_Text timePickerText;private TimePicker hourPicker;private TimePicker minutePicker;private TimePicker secondPicker;private DateTime selectedDateTime;private void Awake(){selectedTime = transform.FindChildByName("SelectedTime").GetComponent<TMP_Text>();datePicker = transform.FindChildByName("DatePicker").GetComponent<DatePicker>();timePickerText = transform.FindChildByName("TimePickerText").GetComponent<TMP_Text>();hourPicker = transform.FindChildByName("HourPicker").GetComponent<TimePicker>();minutePicker = transform.FindChildByName("MinutePicker").GetComponent<TimePicker>();secondPicker = transform.FindChildByName("SecondPicker").GetComponent<TimePicker>();selectedDateTime = DateTime.Now;}private void Start(){datePicker.curSelectedDateTime = selectedDateTime;int datePickerCalenderTableChildCount = transform.FindChildByName("CalenderTable").childCount;for (int i = 0; i < datePickerCalenderTableChildCount; i++){transform.FindChildByName("CalenderTable").GetChild(i).GetComponent<DayItem>().OnDayClicked += OnDayClicked;}datePicker.ShowCalendar();hourPicker.curSelectedDateTime = selectedDateTime;hourPicker.InitPicker(selectedDateTime);hourPicker.onTimeChanged += OnTimeChanged;minutePicker.curSelectedDateTime = selectedDateTime;minutePicker.InitPicker(selectedDateTime);minutePicker.onTimeChanged += OnTimeChanged;secondPicker.curSelectedDateTime = selectedDateTime;secondPicker.InitPicker(selectedDateTime);secondPicker.onTimeChanged += OnTimeChanged;timePickerText.text = selectedDateTime.ToString("T");selectedTime.text = selectedDateTime.ToString("G");}private void OnDayClicked(Transform dayItem, DateTime dateTime){selectedDateTime = new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, selectedDateTime.Hour, selectedDateTime.Minute, selectedDateTime.Second);selectedTime.text = selectedDateTime.ToString("G");hourPicker.curSelectedDateTime = selectedDateTime;hourPicker.SetMinAndMaxDateTimeInDay(selectedDateTime);minutePicker.curSelectedDateTime = selectedDateTime;minutePicker.SetMinAndMaxDateTimeInDay(selectedDateTime);secondPicker.curSelectedDateTime = selectedDateTime;secondPicker.SetMinAndMaxDateTimeInDay(selectedDateTime);}private void OnTimeChanged(TimeType timeType, DateTime dateTime){//根据时间类型更新对应部分if (timeType == TimeType.Hour){selectedDateTime = new DateTime(selectedDateTime.Year, selectedDateTime.Month, selectedDateTime.Day, dateTime.Hour, selectedDateTime.Minute, selectedDateTime.Second);}else if (timeType == TimeType.Minute){selectedDateTime = new DateTime(selectedDateTime.Year, selectedDateTime.Month, selectedDateTime.Day, selectedDateTime.Hour, dateTime.Minute, selectedDateTime.Second);}else if (timeType == TimeType.Second){selectedDateTime = new DateTime(selectedDateTime.Year, selectedDateTime.Month, selectedDateTime.Day, selectedDateTime.Hour, selectedDateTime.Minute, dateTime.Second);}timePickerText.text = selectedDateTime.ToString("T");selectedTime.text = selectedDateTime.ToString("G");}}
}
注意:将脚本挂载到对应的对象上,时间选择器注意设置对应的时间类型和数量。
4.效果展示
好了,本次的分享到这里就结束啦,希望对你有所帮助~