Unity Scroll View滑动翻页效果实战:从零搭建一个可交互的日历系统

发布时间:2026/7/11 8:43:04

Unity Scroll View滑动翻页效果实战:从零搭建一个可交互的日历系统 Unity Scroll View滑动翻页效果实战从零搭建一个可交互的日历系统在移动应用和游戏开发中日历系统是一个常见但实现起来颇具挑战性的功能组件。传统的日历往往局限于静态展示而现代应用更倾向于实现流畅的滑动翻页交互体验。本文将带你从零开始利用Unity的Scroll View组件构建一个完整的可交互日历系统不仅实现基础的日期展示还能通过滑动或按钮点击实现月份切换并支持日期选择和事件标记等高级功能。1. 项目准备与基础UI搭建首先创建一个新的Unity项目推荐使用2021 LTS或更高版本确保已导入TextMeshPro基础包。我们将采用UGUI系统构建整个日历界面。1.1 创建基础Scroll View结构在Hierarchy面板右键选择UI Scroll View创建滚动视图。建议进行以下调整删除默认的Scrollbar日历翻页通常不需要滚动条设置Scroll Rect组件的Movement Type为Elastic实现弹性边界效果将Inertia设为false禁用惯性滑动更符合日历翻页的交互预期// Scroll View初始配置参考 var scrollRect GetComponentScrollRect(); scrollRect.inertia false; scrollRect.decelerationRate 0.135f; scrollRect.elasticity 0.1f;1.2 日历网格布局在Scroll View的Content对象上添加Grid Layout Group组件配置7列一周七天和适当行数通常5-6行足够显示完整月份。关键参数设置参数建议值说明Cell Size100x100单个日期格子尺寸Spacing5x5格子间距Start CornerUpper Left从左上角开始排列Child AlignmentUpper Center子对象对齐方式// 动态调整Content尺寸的代码 void ResizeContent(int weekCount) { var grid content.GetComponentGridLayoutGroup(); float height weekCount * (grid.cellSize.y grid.spacing.y); content.sizeDelta new Vector2(content.sizeDelta.x, height); }2. 日历数据逻辑实现2.1 日期数据模型创建一个CalendarData类处理日期计算和状态管理[System.Serializable] public class CalendarData { public DateTime currentMonth; public ListCalendarDay days new ListCalendarDay(); public void GenerateMonthData(DateTime date) { currentMonth new DateTime(date.Year, date.Month, 1); days.Clear(); // 计算当月天数 int daysInMonth DateTime.DaysInMonth(date.Year, date.Month); // 计算当月第一天是星期几 DayOfWeek firstDayOfWeek currentMonth.DayOfWeek; int offset (int)firstDayOfWeek - (int)DayOfWeek.Sunday; if (offset 0) offset 7; // 填充空白日期上月末尾 for (int i 0; i offset; i) { days.Add(new CalendarDay { isCurrentMonth false }); } // 填充当月日期 for (int i 1; i daysInMonth; i) { days.Add(new CalendarDay { dayNumber i, date new DateTime(date.Year, date.Month, i), isCurrentMonth true }); } // 计算完整日历需要的行数 int totalCells offset daysInMonth; int weeks Mathf.CeilToInt(totalCells / 7f); return weeks; } } [System.Serializable] public class CalendarDay { public int dayNumber; public DateTime date; public bool isCurrentMonth; public bool hasEvent; }2.2 日期UI生成创建日期格子的Prefab包含TextMeshPro文本和背景Image组件。在代码中动态生成public GameObject dayPrefab; public Transform content; void GenerateCalendarUI(CalendarData data) { // 清除现有日期格子 foreach (Transform child in content) { Destroy(child.gameObject); } // 生成新格子 foreach (var day in data.days) { var dayObj Instantiate(dayPrefab, content); var dayUI dayObj.GetComponentCalendarDayUI(); dayUI.Setup(day); } }3. 滑动翻页效果实现3.1 分页逻辑核心扩展ScrollRect组件实现分页效果[RequireComponent(typeof(ScrollRect))] public class CalendarPager : MonoBehaviour, IBeginDragHandler, IEndDragHandler { [SerializeField] private float pageThreshold 0.3f; [SerializeField] private float smoothTime 0.2f; private ScrollRect scrollRect; private RectTransform content; private bool isDragging; private float targetPosition; private float dragStartPosition; private void Awake() { scrollRect GetComponentScrollRect(); content scrollRect.content; } public void OnBeginDrag(PointerEventData eventData) { isDragging true; dragStartPosition scrollRect.verticalNormalizedPosition; } public void OnEndDrag(PointerEventData eventData) { isDragging false; float currentPos scrollRect.verticalNormalizedPosition; float delta currentPos - dragStartPosition; if (Mathf.Abs(delta) pageThreshold) { // 滑动超过阈值翻页 targetPosition delta 0 ? 1 : 0; } else { // 回弹到当前页 targetPosition currentPos 0.5f ? 0 : 1; } } private void Update() { if (!isDragging !Mathf.Approximately(scrollRect.verticalNormalizedPosition, targetPosition)) { scrollRect.verticalNormalizedPosition Mathf.Lerp( scrollRect.verticalNormalizedPosition, targetPosition, Time.deltaTime / smoothTime ); } } public void GoToNextPage() { targetPosition 0; scrollRect.verticalNormalizedPosition 0; } public void GoToPrevPage() { targetPosition 1; scrollRect.verticalNormalizedPosition 1; } }3.2 月份切换逻辑将分页与日历数据关联public CalendarData calendarData; public CalendarPager pager; void Start() { pager.OnPageChanged.AddListener(HandlePageChange); RefreshCalendar(DateTime.Now); } void HandlePageChange(float position) { if (position 0.5f) { // 滑动到下一页下个月 DateTime nextMonth calendarData.currentMonth.AddMonths(1); RefreshCalendar(nextMonth); } else { // 滑动到上一页上个月 DateTime prevMonth calendarData.currentMonth.AddMonths(-1); RefreshCalendar(prevMonth); } } void RefreshCalendar(DateTime date) { int weeks calendarData.GenerateMonthData(date); GenerateCalendarUI(calendarData); ResizeContent(weeks); pager.ResetPosition(); }4. 交互功能扩展4.1 日期选择功能为每个日期格子添加点击事件public class CalendarDayUI : MonoBehaviour, IPointerClickHandler { [SerializeField] private TextMeshProUGUI dayText; [SerializeField] private Image background; private CalendarDay data; private System.ActionCalendarDay onClick; public void Setup(CalendarDay dayData, System.ActionCalendarDay clickCallback) { data dayData; onClick clickCallback; dayText.text data.isCurrentMonth ? data.dayNumber.ToString() : ; background.color data.isCurrentMonth ? Color.white : new Color(1,1,1,0.3f); } public void OnPointerClick(PointerEventData eventData) { if (data.isCurrentMonth) { onClick?.Invoke(data); } } }4.2 事件标记系统扩展CalendarDay类支持事件标记[System.Serializable] public class CalendarEvent { public string title; public string description; public Color color Color.red; } [System.Serializable] public class CalendarDay { // ...原有字段... public ListCalendarEvent events new ListCalendarEvent(); public void AddEvent(CalendarEvent evt) { events.Add(evt); hasEvent true; } }在UI中显示事件标记public class CalendarDayUI : MonoBehaviour { [SerializeField] private GameObject eventIndicator; public void Setup(CalendarDay dayData) { // ...原有设置代码... eventIndicator.SetActive(dayData.hasEvent); if (dayData.hasEvent) { eventIndicator.GetComponentImage().color dayData.events[0].color; } } }4.3 月份快速切换按钮添加左右箭头按钮实现快速切换public Button prevMonthButton; public Button nextMonthButton; public TextMeshProUGUI monthTitle; void Start() { prevMonthButton.onClick.AddListener(GoToPreviousMonth); nextMonthButton.onClick.AddListener(GoToNextMonth); } void GoToPreviousMonth() { DateTime prevMonth calendarData.currentMonth.AddMonths(-1); RefreshCalendar(prevMonth); pager.GoToPrevPage(); } void GoToNextMonth() { DateTime nextMonth calendarData.currentMonth.AddMonths(1); RefreshCalendar(nextMonth); pager.GoToNextPage(); } void RefreshCalendar(DateTime date) { // ...原有刷新逻辑... monthTitle.text date.ToString(yyyy年MM月); }5. 性能优化与进阶技巧5.1 对象池优化对于频繁创建销毁的日期格子使用对象池技术public class CalendarPool { private QueueGameObject pool new QueueGameObject(); private GameObject prefab; private Transform parent; public CalendarPool(GameObject dayPrefab, Transform contentParent, int initialSize) { prefab dayPrefab; parent contentParent; for (int i 0; i initialSize; i) { var obj Instantiate(prefab, parent); obj.SetActive(false); pool.Enqueue(obj); } } public GameObject Get() { if (pool.Count 0) { var obj pool.Dequeue(); obj.SetActive(true); return obj; } return Instantiate(prefab, parent); } public void Return(GameObject obj) { obj.SetActive(false); pool.Enqueue(obj); } }5.2 滑动灵敏度调节根据不同设备调节滑动灵敏度public class CalendarPager : MonoBehaviour { [SerializeField] private float mobileThreshold 0.2f; [SerializeField] private float desktopThreshold 0.3f; private float currentThreshold; private void Awake() { currentThreshold Application.isMobilePlatform ? mobileThreshold : desktopThreshold; } }5.3 周视图/月视图切换扩展支持不同显示模式public enum CalendarViewMode { Month, Week } public CalendarViewMode currentViewMode; public void ToggleViewMode() { currentViewMode currentViewMode CalendarViewMode.Month ? CalendarViewMode.Week : CalendarViewMode.Month; UpdateViewMode(); } void UpdateViewMode() { var grid content.GetComponentGridLayoutGroup(); if (currentViewMode CalendarViewMode.Week) { grid.constraint GridLayoutGroup.Constraint.FixedRowCount; grid.constraintCount 1; // 只显示一行 scrollRect.movementType ScrollRect.MovementType.Unrestricted; } else { grid.constraint GridLayoutGroup.Constraint.FixedColumnCount; grid.constraintCount 7; scrollRect.movementType ScrollRect.MovementType.Elastic; } RefreshCalendar(calendarData.currentMonth); }在实际项目中这个日历系统已经成功应用于多个商业项目包括健康追踪应用、任务管理工具和社交活动平台。一个特别有用的技巧是在处理国际化的周起始日时可以通过修改Grid Layout Group的startCorner和childAlignment属性来适应不同地区周日或周一作为一周开始的习惯要求。

相关新闻