From f078829186737e5f33358c9169aa06378ec98bba Mon Sep 17 00:00:00 2001 From: kurobon-jp <5210329+kurobon-jp@users.noreply.github.com> Date: Sun, 12 Jul 2026 16:43:13 +0900 Subject: [PATCH 1/3] Refactor scroll state management and improve null safety for Scroller * **Enhance Scroller lifecycle:** * Ensure `_scroller` is always initialized in `BaseScroll` by providing a default instance if `null`, removing numerous null checks. * Make `ScrollStatus` enum public and introduce `OnScrollStateChanged` event in `Scroller` for external observation of scroll state. * Update all internal status changes in `Scroller` to use the new `Status` property, which dispatches the `OnScrollStateChanged` event. * **Improve BaseScroll configurability and events:** * Expose `OnScrollStateChanged` event from `Scroller` via `BaseScroll`. * Add `IsIdling` property to `BaseScroll`. * Convert `IsDraggable` and `IsScrollable` into serialized fields in `BaseScroll`, enabling direct configuration in the Inspector. * Update drag and scroll handlers in `BaseScroll` to respect the new `IsDraggable` and `IsScrollable` fields. * Remove redundant null checks on `_scroller` throughout `BaseScroll`. * **Cleanup:** * Remove `#if UNITY_EDITOR` block for `_tracker.Clear()` in `BaseScroll.OnDisable`. --- Assets/Runtime/BaseScroll.cs | 98 ++++++++++++++++++++------------ Assets/Runtime/Utils/Scroller.cs | 49 ++++++++++------ 2 files changed, 93 insertions(+), 54 deletions(-) diff --git a/Assets/Runtime/BaseScroll.cs b/Assets/Runtime/BaseScroll.cs index d71f444..d606572 100644 --- a/Assets/Runtime/BaseScroll.cs +++ b/Assets/Runtime/BaseScroll.cs @@ -16,29 +16,56 @@ public abstract class BaseScroll : UIBehaviour, IBeginDragHandler, [SerializeField] private RectTransform _content; [SerializeField] private Scroller _scroller; [SerializeField] private Scrollbar _scrollbar; + [SerializeField] private bool _isDraggable = true; + [SerializeField] private bool _isScrollable = true; private bool _isDirty; private int _pointerId = int.MinValue; private int _dataCount; private bool _isResized; - internal Scroller Scroller => _scroller; + internal Scroller Scroller + { + get + { + _scroller ??= new Scroller(); + return _scroller; + } + } + protected RectTransform Content => _content; internal CellViewPool CellViewPool { get; } = new(); protected TDataSource DataSource { get; private set; } protected float ViewportSize { get; private set; } protected float ViewportHalf { get; private set; } public Range VisibleRange { get; private set; } = Range.Empty; - public ScrollEvent OnValueChanged => Scroller?.OnValueChanged; - public bool IsDraggable { get; set; } = true; - public bool IsScrollable { get; set; } = true; - public bool IsDragging => Scroller?.IsDragging ?? false; - public bool IsScrolling => Scroller?.IsScrolling ?? false; - public float ScrollPosition => Scroller?.ScrollPosition ?? 0f; - public float NormalizedPosition => Scroller?.NormalizedPosition ?? 0f; + public ScrollEvent OnValueChanged => _scroller.OnValueChanged; + public bool IsDragging => Scroller.IsDragging; + public bool IsScrolling => Scroller.IsScrolling; + public bool IsIdling => Scroller.IsIdling; + public float ScrollPosition => Scroller.ScrollPosition; + public float NormalizedPosition => Scroller.NormalizedPosition; + + public bool IsDraggable + { + get => _isDraggable; + set => _isDraggable = value; + } + + public bool IsScrollable + { + get => _isScrollable; + set => _isScrollable = value; + } public event Action OnVisibleRangeChanged; + public event Action OnScrollStateChanged + { + add => Scroller.OnScrollStateChanged += value; + remove => Scroller.OnScrollStateChanged -= value; + } + protected override void OnEnable() { _pointerId = int.MinValue; @@ -55,9 +82,6 @@ protected override void OnEnable() protected override void OnDisable() { -#if UNITY_EDITOR - _tracker.Clear(); -#endif if (_scrollbar == null) return; _scrollbar.onValueChanged.RemoveListener(OnScrollbarValueChanged); } @@ -69,25 +93,24 @@ protected override void OnRectTransformDimensionsChange() private void OnScrollbarValueChanged(float normalizedPosition) { - if (_scroller == null) return; - _scroller.OnScroll(); + Scroller.OnScroll(); SetNormalizedPosition(normalizedPosition); } private void UpdateScrollbar() { - if (_scrollbar == null || _scroller == null) return; - _scrollbar.SetValueWithoutNotify(_scroller.NormalizedPosition); + if (_scrollbar == null) return; + _scrollbar.SetValueWithoutNotify(Scroller.NormalizedPosition); } private void Resize() { - if (DataSource == null || _scroller == null) return; - ViewportSize = _viewport.rect.size[_scroller.Axis]; + if (DataSource == null) return; + ViewportSize = _viewport.rect.size[Scroller.Axis]; ViewportHalf = ViewportSize * 0.5f; var scrollSize = GetScrollSize(); - _scroller.Initialize(scrollSize); + Scroller.Initialize(scrollSize); _isResized = true; if (_scrollbar == null) return; @@ -136,8 +159,7 @@ protected void UpdatePosition(float targetPosition) _isDirty = false; } - if (_scroller == null) return; - var scrollDelta = _scroller.Update(targetPosition); + var scrollDelta = Scroller.Update(targetPosition); UpdateScrollbar(); var visibleRange = Reposition(scrollDelta, _isResized); _isResized = false; @@ -147,43 +169,49 @@ protected void UpdatePosition(float targetPosition) OnVisibleRangeChanged?.Invoke(visibleRange); } - if (_scroller.IsScrolling) + if (Scroller.IsScrolling) { - _scroller.Stop(); + Scroller.Stop(); } } private void SetNormalizedPosition(float normalizedPosition) { - _scroller.NormalizedPosition = normalizedPosition; - OnNormalizePositionChanged(_scroller.NormalizedPosition); + Scroller.NormalizedPosition = normalizedPosition; + OnNormalizePositionChanged(Scroller.NormalizedPosition); } void IBeginDragHandler.OnBeginDrag(PointerEventData e) { - if (DataSource == null || _pointerId != int.MinValue) return; + if (DataSource == null || _pointerId != int.MinValue || !IsDraggable) return; _pointerId = e.pointerId; - _scroller?.OnBeginDrag(e); + Scroller.OnBeginDrag(e); } void IDragHandler.OnDrag(PointerEventData e) { if (DataSource == null || _pointerId != e.pointerId || !IsDraggable) return; - _scroller?.OnDrag(e); + Scroller.OnDrag(e); } void IEndDragHandler.OnEndDrag(PointerEventData e) { if (DataSource == null || _pointerId != e.pointerId) return; _pointerId = int.MinValue; - var targetPosition = _scroller?.OnEndDrag(e) ?? 0f; + if (!IsDraggable) + { + Scroller.Stop(); + return; + } + + var targetPosition = Scroller.OnEndDrag(e); OnDrag(targetPosition); } void IScrollHandler.OnScroll(PointerEventData e) { if (DataSource == null || !IsScrollable) return; - _scroller?.OnScroll(); + Scroller.OnScroll(); OnScroll(e.scrollDelta.GetAxialValue()); } @@ -199,9 +227,8 @@ protected virtual void OnNormalizePositionChanged(float normalizedPosition) public void StopScroll() { - if (_scroller == null) return; - var velocity = _scroller.Velocity; - _scroller.Stop(); + var velocity = Scroller.Velocity; + Scroller.Stop(); OnStopScroll(velocity); } @@ -217,8 +244,7 @@ protected void SetDirty() private void SetAxisPivotAndDeltaSize() { - if (_scroller == null) return; - var axis = _scroller.Axis; + var axis = Scroller.Axis; if (_viewport != null) { _viewport.SetPivot(0.5f, axis); @@ -235,9 +261,9 @@ private void SetAxisPivotAndDeltaSize() protected override void OnValidate() { - if (IsDestroyed() || _scroller == null) return; + if (IsDestroyed()) return; _tracker.Clear(); - var axis = _scroller.Axis; + var axis = Scroller.Axis; var pivot = axis == 0 ? DrivenTransformProperties.PivotX : DrivenTransformProperties.PivotY; diff --git a/Assets/Runtime/Utils/Scroller.cs b/Assets/Runtime/Utils/Scroller.cs index c162a7f..83b3038 100644 --- a/Assets/Runtime/Utils/Scroller.cs +++ b/Assets/Runtime/Utils/Scroller.cs @@ -4,17 +4,17 @@ namespace SimpleScroll { + public enum ScrollStatus + { + Idle, + Dragging, + Scrolling, + Coasting + } + [Serializable] internal class Scroller { - private enum ScrollStatus - { - Idle, - Dragging, - Scrolling, - Coasting - } - internal const float VelocityThreshold = 10f; [SerializeField] private RectTransform.Axis _axis; @@ -38,15 +38,28 @@ private enum ScrollStatus private RectTransform _dragTarget; private int _dragFrame; + internal ScrollStatus Status + { + get => _status; + set + { + if (_status == value) return; + _status = value; + OnScrollStateChanged?.Invoke(value); + } + } + internal int Axis => (int)_axis; internal int Direction => _axis == RectTransform.Axis.Horizontal ? -1 : 1; internal bool IsInertia => _inertia; - internal bool IsIdling => _status == ScrollStatus.Idle; - internal bool IsDragging => _status == ScrollStatus.Dragging; - internal bool IsScrolling => _status == ScrollStatus.Scrolling; + internal bool IsIdling => Status == ScrollStatus.Idle; + internal bool IsDragging => Status == ScrollStatus.Dragging; + internal bool IsScrolling => Status == ScrollStatus.Scrolling; internal float Velocity => _velocity * -Direction; internal ScrollEvent OnValueChanged => _onValueChanged; + internal event Action OnScrollStateChanged; + internal float ScrollSize { get => _scrollSize; @@ -97,7 +110,7 @@ internal void OnBeginDrag(PointerEventData e) { _dragTarget = e.pointerDrag?.transform as RectTransform; if (_dragTarget == null) return; - _status = ScrollStatus.Dragging; + Status = ScrollStatus.Dragging; _velocity = 0f; RectTransformUtility.ScreenPointToLocalPointInRectangle(_dragTarget, e.position, e.pressEventCamera, out _pointerPoint); @@ -121,26 +134,26 @@ internal float OnEndDrag(PointerEventData e) { if (!_inertia) { - _status = ScrollStatus.Idle; + Status = ScrollStatus.Idle; // _velocity = 0f; return ScrollPosition; } - _status = ScrollStatus.Coasting; + Status = ScrollStatus.Coasting; _velocity = Mathf.Clamp(_velocity, -_maxVelocity, _maxVelocity); return ScrollPosition + _velocity * _velocity / (_deceleration * 2f) * Mathf.Sign(_velocity); } internal void OnScroll() { - _status = ScrollStatus.Scrolling; + Status = ScrollStatus.Scrolling; _velocity = 0f; } internal float Update(float targetPosition) { var deltaTime = Time.unscaledDeltaTime; - if (_status == ScrollStatus.Dragging) + if (Status == ScrollStatus.Dragging) { if (_dragFrame != Time.frameCount) { @@ -159,7 +172,7 @@ internal float Update(float targetPosition) var speed = _velocity; var scrollPos = ScrollPosition; - if (_status == ScrollStatus.Coasting) + if (Status == ScrollStatus.Coasting) { var absSpeed = Mathf.Abs(speed); var smoothTime = Mathf.Max(0.05f, absSpeed / _deceleration); @@ -199,7 +212,7 @@ internal float Update(float targetPosition) internal void Stop() { - _status = ScrollStatus.Idle; + Status = ScrollStatus.Idle; _velocity = 0f; } From 198d4bcd4b4b73b1fdcbbcbc9915a68184584a9e Mon Sep 17 00:00:00 2001 From: kurobon-jp <5210329+kurobon-jp@users.noreply.github.com> Date: Sun, 12 Jul 2026 16:44:09 +0900 Subject: [PATCH 2/3] Refactor: Introduce BaseScrollEditor for common inspector GUI * Created a new `BaseScrollEditor` class to consolidate common UI Toolkit inspector properties for various scroll components. * Centralized the display of properties such as `_scroller._axis`, `_viewport`, `_content`, `_contentPadding`, `_space`, `_isScrollable`, `_isDraggable`, `_scroller`, and `_scroller._onValueChanged` in the base editor. * Implemented a value change callback for `_isDraggable` to dynamically enable or disable the `_scroller` property field. * Introduced a `protected virtual void CreateInherentProperties(VisualElement container)` method for derived editors to add their specific properties. * Refactored `CarouselScrollEditor`, `FixedGridScrollEditor`, `FixedListScrollEditor`, `LazyLayoutListScrollEditor`, and `SizedListScrollEditor` to inherit from `BaseScrollEditor`. * Moved the unique property fields in these derived editors into their respective `CreateInherentProperties` overrides, eliminating redundant code. --- Assets/Editor/BaseScrollEditor.cs | 31 +++++++++++++++++++++ Assets/Editor/BaseScrollEditor.cs.meta | 3 ++ Assets/Editor/CarouselScrollEditor.cs | 12 ++------ Assets/Editor/FixedGridScroll.cs | 13 ++------- Assets/Editor/FixedListScrollEditor.cs | 13 ++------- Assets/Editor/LazyLayoutListScrollEditor.cs | 13 ++------- Assets/Editor/SizedListScrollEditor.cs | 13 ++------- 7 files changed, 44 insertions(+), 54 deletions(-) create mode 100644 Assets/Editor/BaseScrollEditor.cs create mode 100644 Assets/Editor/BaseScrollEditor.cs.meta diff --git a/Assets/Editor/BaseScrollEditor.cs b/Assets/Editor/BaseScrollEditor.cs new file mode 100644 index 0000000..00b2c87 --- /dev/null +++ b/Assets/Editor/BaseScrollEditor.cs @@ -0,0 +1,31 @@ +using UnityEditor.UIElements; +using UnityEngine.UIElements; + +namespace SimpleScroll.Editor +{ + public abstract class BaseScrollEditor : UnityEditor.Editor + { + public override VisualElement CreateInspectorGUI() + { + var container = new VisualElement(); + container.Add(new PropertyField(serializedObject.FindProperty("_scroller._axis"))); + container.Add(new PropertyField(serializedObject.FindProperty("_viewport"))); + container.Add(new PropertyField(serializedObject.FindProperty("_content"))); + container.Add(new PropertyField(serializedObject.FindProperty("_contentPadding"))); + container.Add(new PropertyField(serializedObject.FindProperty("_space"))); + CreateInherentProperties(container); + container.Add(new PropertyField(serializedObject.FindProperty("_isScrollable"), "Scrollable")); + var isDraggable = new PropertyField(serializedObject.FindProperty("_isDraggable"), "Draggable"); + var scroller = new ScrollerDrawer().CreatePropertyGUI(serializedObject.FindProperty("_scroller")); + isDraggable.RegisterValueChangeCallback(evt => { scroller.SetEnabled(evt.changedProperty.boolValue); }); + container.Add(isDraggable); + container.Add(scroller); + container.Add(new PropertyField(serializedObject.FindProperty("_scroller._onValueChanged"))); + return container; + } + + protected virtual void CreateInherentProperties(VisualElement container) + { + } + } +} \ No newline at end of file diff --git a/Assets/Editor/BaseScrollEditor.cs.meta b/Assets/Editor/BaseScrollEditor.cs.meta new file mode 100644 index 0000000..ef5a312 --- /dev/null +++ b/Assets/Editor/BaseScrollEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6042707d0dbe469495cdf69587e5a792 +timeCreated: 1783839452 \ No newline at end of file diff --git a/Assets/Editor/CarouselScrollEditor.cs b/Assets/Editor/CarouselScrollEditor.cs index 2a4544a..bdede81 100644 --- a/Assets/Editor/CarouselScrollEditor.cs +++ b/Assets/Editor/CarouselScrollEditor.cs @@ -6,21 +6,13 @@ namespace SimpleScroll.Editor { [CustomEditor(typeof(CarouselScroll))] - public class CarouselScrollEditor : UnityEditor.Editor + public class CarouselScrollEditor : BaseScrollEditor { - public override VisualElement CreateInspectorGUI() + protected override void CreateInherentProperties(VisualElement container) { - var container = new VisualElement(); - container.Add(new PropertyField(serializedObject.FindProperty("_scroller._axis"))); - container.Add(new PropertyField(serializedObject.FindProperty("_viewport"))); - container.Add(new PropertyField(serializedObject.FindProperty("_content"))); - container.Add(new PropertyField(serializedObject.FindProperty("_space"))); container.Add(new PropertyField(serializedObject.FindProperty("_cellSize"))); container.Add(new PropertyField(serializedObject.FindProperty("_loop"))); container.Add(new PropertyField(serializedObject.FindProperty("_indicator"))); - container.Add(new ScrollerDrawer().CreatePropertyGUI(serializedObject.FindProperty("_scroller"))); - container.Add(new PropertyField(serializedObject.FindProperty("_scroller._onValueChanged"))); - return container; } } } diff --git a/Assets/Editor/FixedGridScroll.cs b/Assets/Editor/FixedGridScroll.cs index c3d3250..4994bb6 100644 --- a/Assets/Editor/FixedGridScroll.cs +++ b/Assets/Editor/FixedGridScroll.cs @@ -6,22 +6,13 @@ namespace SimpleScroll.Editor { [CustomEditor(typeof(FixedGridScroll))] - public class FixedGridScrollEditor : UnityEditor.Editor + public class FixedGridScrollEditor : BaseScrollEditor { - public override VisualElement CreateInspectorGUI() + protected override void CreateInherentProperties(VisualElement container) { - var container = new VisualElement(); - container.Add(new PropertyField(serializedObject.FindProperty("_scroller._axis"))); - container.Add(new PropertyField(serializedObject.FindProperty("_viewport"))); - container.Add(new PropertyField(serializedObject.FindProperty("_content"))); - container.Add(new PropertyField(serializedObject.FindProperty("_contentPadding"))); - container.Add(new PropertyField(serializedObject.FindProperty("_space"))); container.Add(new PropertyField(serializedObject.FindProperty("_cellSize"))); container.Add(new PropertyField(serializedObject.FindProperty("_column"))); container.Add(new PropertyField(serializedObject.FindProperty("_scrollbar"))); - container.Add(new ScrollerDrawer().CreatePropertyGUI(serializedObject.FindProperty("_scroller"))); - container.Add(new PropertyField(serializedObject.FindProperty("_scroller._onValueChanged"))); - return container; } } } diff --git a/Assets/Editor/FixedListScrollEditor.cs b/Assets/Editor/FixedListScrollEditor.cs index 71e4ae5..e425b64 100644 --- a/Assets/Editor/FixedListScrollEditor.cs +++ b/Assets/Editor/FixedListScrollEditor.cs @@ -6,21 +6,12 @@ namespace SimpleScroll.Editor { [CustomEditor(typeof(FixedListScroll))] - public class FixedListScrollEditor : UnityEditor.Editor + public class FixedListScrollEditor : BaseScrollEditor { - public override VisualElement CreateInspectorGUI() + protected override void CreateInherentProperties(VisualElement container) { - var container = new VisualElement(); - container.Add(new PropertyField(serializedObject.FindProperty("_scroller._axis"))); - container.Add(new PropertyField(serializedObject.FindProperty("_viewport"))); - container.Add(new PropertyField(serializedObject.FindProperty("_content"))); - container.Add(new PropertyField(serializedObject.FindProperty("_contentPadding"))); - container.Add(new PropertyField(serializedObject.FindProperty("_space"))); container.Add(new PropertyField(serializedObject.FindProperty("_cellSize"))); container.Add(new PropertyField(serializedObject.FindProperty("_scrollbar"))); - container.Add(new ScrollerDrawer().CreatePropertyGUI(serializedObject.FindProperty("_scroller"))); - container.Add(new PropertyField(serializedObject.FindProperty("_scroller._onValueChanged"))); - return container; } } } diff --git a/Assets/Editor/LazyLayoutListScrollEditor.cs b/Assets/Editor/LazyLayoutListScrollEditor.cs index 8eeb9c2..9354669 100644 --- a/Assets/Editor/LazyLayoutListScrollEditor.cs +++ b/Assets/Editor/LazyLayoutListScrollEditor.cs @@ -6,21 +6,12 @@ namespace SimpleScroll.Editor { [CustomEditor(typeof(LazyLayoutListScroll))] - public class LazyLayoutListScrollEditor : UnityEditor.Editor + public class LazyLayoutListScrollEditor : BaseScrollEditor { - public override VisualElement CreateInspectorGUI() + protected override void CreateInherentProperties(VisualElement container) { - var container = new VisualElement(); - container.Add(new PropertyField(serializedObject.FindProperty("_scroller._axis"))); - container.Add(new PropertyField(serializedObject.FindProperty("_viewport"))); - container.Add(new PropertyField(serializedObject.FindProperty("_content"))); - container.Add(new PropertyField(serializedObject.FindProperty("_contentPadding"))); - container.Add(new PropertyField(serializedObject.FindProperty("_space"))); container.Add(new PropertyField(serializedObject.FindProperty("_defaultCellSize"))); container.Add(new PropertyField(serializedObject.FindProperty("_scrollbar"))); - container.Add(new ScrollerDrawer().CreatePropertyGUI(serializedObject.FindProperty("_scroller"))); - container.Add(new PropertyField(serializedObject.FindProperty("_scroller._onValueChanged"))); - return container; } } } diff --git a/Assets/Editor/SizedListScrollEditor.cs b/Assets/Editor/SizedListScrollEditor.cs index 8ddb9bd..1af4031 100644 --- a/Assets/Editor/SizedListScrollEditor.cs +++ b/Assets/Editor/SizedListScrollEditor.cs @@ -6,20 +6,11 @@ namespace SimpleScroll.Editor { [CustomEditor(typeof(SizedListScroll))] - public class SizedListScrollEditor : UnityEditor.Editor + public class SizedListScrollEditor : BaseScrollEditor { - public override VisualElement CreateInspectorGUI() + protected override void CreateInherentProperties(VisualElement container) { - var container = new VisualElement(); - container.Add(new PropertyField(serializedObject.FindProperty("_scroller._axis"))); - container.Add(new PropertyField(serializedObject.FindProperty("_viewport"))); - container.Add(new PropertyField(serializedObject.FindProperty("_content"))); - container.Add(new PropertyField(serializedObject.FindProperty("_contentPadding"))); - container.Add(new PropertyField(serializedObject.FindProperty("_space"))); container.Add(new PropertyField(serializedObject.FindProperty("_scrollbar"))); - container.Add(new ScrollerDrawer().CreatePropertyGUI(serializedObject.FindProperty("_scroller"))); - container.Add(new PropertyField(serializedObject.FindProperty("_scroller._onValueChanged"))); - return container; } } } From 93a676dd4e9a4460b76ab7ac97ddfd7068741e58 Mon Sep 17 00:00:00 2001 From: kurobon-jp <5210329+kurobon-jp@users.noreply.github.com> Date: Mon, 13 Jul 2026 11:42:58 +0900 Subject: [PATCH 3/3] Refactor: Simplify OnEndDrag logic for non-draggable scrolls * Removed redundant `Scroller.Stop()` call within `OnEndDrag` when `IsDraggable` is false. * Ensured `Scroller.OnEndDrag` is not called if the scroll component is not draggable. * Streamlined the early exit condition in `OnEndDrag` for improved clarity and efficiency. * Prevents unnecessary operations when the scroll functionality is disabled. --- Assets/Runtime/BaseScroll.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Assets/Runtime/BaseScroll.cs b/Assets/Runtime/BaseScroll.cs index d606572..6540b8b 100644 --- a/Assets/Runtime/BaseScroll.cs +++ b/Assets/Runtime/BaseScroll.cs @@ -198,12 +198,7 @@ void IEndDragHandler.OnEndDrag(PointerEventData e) { if (DataSource == null || _pointerId != e.pointerId) return; _pointerId = int.MinValue; - if (!IsDraggable) - { - Scroller.Stop(); - return; - } - + if (!IsDraggable) return; var targetPosition = Scroller.OnEndDrag(e); OnDrag(targetPosition); }