diff --git a/Assets/Scripts/UI/ScrollPane.cs b/Assets/Scripts/UI/ScrollPane.cs index c116cd97..e41f0950 100644 --- a/Assets/Scripts/UI/ScrollPane.cs +++ b/Assets/Scripts/UI/ScrollPane.cs @@ -1,4 +1,4 @@ -using System; +using System; using UnityEngine; using FairyGUI.Utils; @@ -64,6 +64,7 @@ public class ScrollPane : EventDispatcher Vector2 _tweenChange; Vector2 _tweenTime; Vector2 _tweenDuration; + readonly bool[] _tweenBounce = new bool[2]; Action _refreshDelegate; TimerCallback _tweenUpdateDelegate; @@ -85,9 +86,24 @@ public class ScrollPane : EventDispatcher static int _gestureFlag; - public static float TWEEN_TIME_GO = 0.3f; //调用SetPos(ani)时使用的缓动时间 - public static float TWEEN_TIME_DEFAULT = 0.3f; //惯性滚动的最小缓动时间 - public static float PULL_RATIO = 0.5f; //下拉过顶或者上拉过底时允许超过的距离占显示区域的比例 + public static float TWEEN_TIME_GO = 0.3f; //调用SetPos(ani)时使用的缓动时间 + public static float TWEEN_TIME_DEFAULT = 0.3f; //惯性滚动的最小缓动时间 + public static float PULL_RATIO = 0.5f; //下拉过顶或者上拉过底时允许超过的距离占显示区域的比例 + const float VELOCITY_DAMPING_RATE = 0.833f; + const float RUBBER_BAND_COEFFICIENT = 0.55f; + const float MIN_VELOCITY_SAMPLE_DELTA = 1f / 240f; + const float VELOCITY_SMOOTH_FACTOR = 12f; + const float VELOCITY_SCALE_BLEND = 0.5f; + const float VELOCITY_SCALE_MIN = 0.25f; + const float VELOCITY_SCALE_MAX = 4f; + const float TOUCH_DELTA_EPSILON = 0.001f; + const float INERTIA_TRIGGER_DESKTOP = 480f; + const float INERTIA_TRIGGER_TOUCH = 920f; + const float INERTIA_FULL_RANGE = 1.7f; + const float INERTIA_RELEASE_SCALE = 0.9f; + const float BOUNCE_TIME_MIN = 0.18f; + const float BOUNCE_TIME_MAX = 0.42f; + const float BOUNCE_EPSILON = 0.001f; public ScrollPane(GComponent owner) { @@ -1372,6 +1388,8 @@ void Refresh2() if (pos.x != _container.x || pos.y != _container.y) { + _tweenBounce[0] = false; + _tweenBounce[1] = false; _tweenDuration = new Vector2(TWEEN_TIME_GO, TWEEN_TIME_GO); _tweenStart = _container.xy; _tweenChange = pos - _tweenStart; @@ -1394,6 +1412,68 @@ void Refresh2() UpdatePageController(); } + float GetBounceLimit(int axis, bool positiveEdge) + { + GComponent target = positiveEdge ? _header : _footer; + if (target != null) + { + float limit = axis == 0 ? target.maxWidth : target.maxHeight; + if (limit > 0) + return limit; + } + + return _viewSize[axis] * PULL_RATIO; + } + + float ApplyEdgeResistance(float overshoot, int axis, bool positiveEdge) + { + float limit = GetBounceLimit(axis, positiveEdge); + if (limit <= 0) + return 0; + + float viewSize = Mathf.Max(1, _viewSize[axis]); + float distance = Mathf.Abs(overshoot); + float compressed = (1f - 1f / ((distance * RUBBER_BAND_COEFFICIENT / viewSize) + 1f)) * viewSize; + compressed = Mathf.Min(compressed, limit); + + return positiveEdge ? compressed : -compressed; + } + + float GetReleaseVelocity(int axis) + { + float velocity = Mathf.Abs(_velocity[axis]) * _velocityScale; + if (Stage.touchScreen) + velocity *= 1136f / Mathf.Max(Screen.width, Screen.height); + + return velocity; + } + + float GetBounceDuration(float start, float end, int axis) + { + float distance = Mathf.Abs(end - start); + float limit = Mathf.Max(1, GetBounceLimit(axis, end >= 0)); + float normalizedDistance = Mathf.Clamp01(distance / limit); + float duration = Mathf.Lerp(BOUNCE_TIME_MIN, BOUNCE_TIME_MAX, Mathf.Sqrt(normalizedDistance)); + + float releaseVelocity = GetReleaseVelocity(axis); + if (releaseVelocity > 0) + { + float velocityFactor = Mathf.Clamp01(releaseVelocity / 2400f); + duration = Mathf.Lerp(duration, BOUNCE_TIME_MIN, velocityFactor * 0.35f); + } + + return duration; + } + + void SetBounceTween(int axis, float start, float end) + { + _tweenBounce[axis] = true; + _tweenTime[axis] = 0; + _tweenDuration[axis] = GetBounceDuration(start, end, axis); + _tweenChange[axis] = end - start; + _tweenStart[axis] = start; + } + private void __touchBegin(EventContext context) { if (!_touchEffect) @@ -1505,78 +1585,76 @@ private void __touchMove(EventContext context) sv = sh = true; } - Vector2 newPos = _containerPos + pt - _beginTouchPos; - newPos.x = (int)newPos.x; - newPos.y = (int)newPos.y; + Vector2 newPos = _containerPos + pt - _beginTouchPos; if (sv) { - if (newPos.y > 0) - { - if (!_bouncebackEffect) - _container.y = 0; - else if (_header != null && _header.maxHeight != 0) - _container.y = (int)Mathf.Min(newPos.y * 0.5f, _header.maxHeight); - else - _container.y = (int)Mathf.Min(newPos.y * 0.5f, _viewSize.y * PULL_RATIO); - } - else if (newPos.y < -_overlapSize.y) - { - if (!_bouncebackEffect) - _container.y = -_overlapSize.y; - else if (_footer != null && _footer.maxHeight > 0) - _container.y = (int)Mathf.Max((newPos.y + _overlapSize.y) * 0.5f, -_footer.maxHeight) - _overlapSize.y; - else - _container.y = (int)Mathf.Max((newPos.y + _overlapSize.y) * 0.5f, -_viewSize.y * PULL_RATIO) - _overlapSize.y; - } - else - _container.y = newPos.y; + if (newPos.y > 0) + { + if (!_bouncebackEffect) + _container.y = 0; + else + _container.y = ApplyEdgeResistance(newPos.y, 1, true); + } + else if (newPos.y < -_overlapSize.y) + { + if (!_bouncebackEffect) + _container.y = -_overlapSize.y; + else + _container.y = -_overlapSize.y + ApplyEdgeResistance(newPos.y + _overlapSize.y, 1, false); + } + else + _container.y = newPos.y; } if (sh) { - if (newPos.x > 0) - { - if (!_bouncebackEffect) - _container.x = 0; - else if (_header != null && _header.maxWidth != 0) - _container.x = (int)Mathf.Min(newPos.x * 0.5f, _header.maxWidth); - else - _container.x = (int)Mathf.Min(newPos.x * 0.5f, _viewSize.x * PULL_RATIO); - } - else if (newPos.x < 0 - _overlapSize.x) - { - if (!_bouncebackEffect) - _container.x = -_overlapSize.x; - else if (_footer != null && _footer.maxWidth > 0) - _container.x = (int)Mathf.Max((newPos.x + _overlapSize.x) * 0.5f, -_footer.maxWidth) - _overlapSize.x; - else - _container.x = (int)Mathf.Max((newPos.x + _overlapSize.x) * 0.5f, -_viewSize.x * PULL_RATIO) - _overlapSize.x; - } - else - _container.x = newPos.x; - } - - //更新速度 - float deltaTime = Time.unscaledDeltaTime; - float elapsed = (Time.unscaledTime - _lastMoveTime) * 60 - 1; - if (elapsed > 1) //速度衰减 - _velocity = _velocity * Mathf.Pow(0.833f, elapsed); - Vector2 deltaPosition = pt - _lastTouchPos; - if (!sh) - deltaPosition.x = 0; - if (!sv) - deltaPosition.y = 0; - _velocity = Vector2.Lerp(_velocity, deltaPosition / deltaTime, deltaTime * 10); + if (newPos.x > 0) + { + if (!_bouncebackEffect) + _container.x = 0; + else + _container.x = ApplyEdgeResistance(newPos.x, 0, true); + } + else if (newPos.x < 0 - _overlapSize.x) + { + if (!_bouncebackEffect) + _container.x = -_overlapSize.x; + else + _container.x = -_overlapSize.x + ApplyEdgeResistance(newPos.x + _overlapSize.x, 0, false); + } + else + _container.x = newPos.x; + } + + //更新速度 + float deltaTime = Mathf.Max(Time.unscaledDeltaTime, MIN_VELOCITY_SAMPLE_DELTA); + float elapsed = (Time.unscaledTime - _lastMoveTime) * 60 - 1; + if (elapsed > 1) //速度衰减 + _velocity = _velocity * Mathf.Pow(VELOCITY_DAMPING_RATE, elapsed); + Vector2 deltaPosition = pt - _lastTouchPos; + if (!sh) + deltaPosition.x = 0; + if (!sv) + deltaPosition.y = 0; + float velocityBlend = 1f - Mathf.Exp(-deltaTime * VELOCITY_SMOOTH_FACTOR); + _velocity = Vector2.Lerp(_velocity, deltaPosition / deltaTime, velocityBlend); /*速度计算使用的是本地位移,但在后续的惯性滚动判断中需要用到屏幕位移,所以这里要记录一个位移的比例。 *后续的处理要使用这个比例但不使用坐标转换的方法的原因是,在曲面UI等异形UI中,还无法简单地进行屏幕坐标和本地坐标的转换。 */ - Vector2 deltaGlobalPosition = _lastTouchGlobalPos - evt.position; - if (deltaPosition.x != 0) - _velocityScale = Mathf.Abs(deltaGlobalPosition.x / deltaPosition.x); - else if (deltaPosition.y != 0) - _velocityScale = Mathf.Abs(deltaGlobalPosition.y / deltaPosition.y); + Vector2 deltaGlobalPosition = evt.position - _lastTouchGlobalPos; + float measuredVelocityScale = 0; + if (Mathf.Abs(deltaPosition.x) > TOUCH_DELTA_EPSILON) + measuredVelocityScale = Mathf.Abs(deltaGlobalPosition.x / deltaPosition.x); + else if (Mathf.Abs(deltaPosition.y) > TOUCH_DELTA_EPSILON) + measuredVelocityScale = Mathf.Abs(deltaGlobalPosition.y / deltaPosition.y); + + if (measuredVelocityScale > 0) + { + measuredVelocityScale = Mathf.Clamp(measuredVelocityScale, VELOCITY_SCALE_MIN, VELOCITY_SCALE_MAX); + _velocityScale = Mathf.Lerp(_velocityScale, measuredVelocityScale, VELOCITY_SCALE_BLEND); + } _lastTouchPos = pt; _lastTouchGlobalPos = evt.position; @@ -1622,6 +1700,8 @@ private void __touchEnd(EventContext context) _dragged = false; _tweenStart = _container.xy; + _tweenBounce[0] = false; + _tweenBounce[1] = false; Vector2 endPos = _tweenStart; bool flag = false; @@ -1671,6 +1751,10 @@ private void __touchEnd(EventContext context) } _tweenDuration.Set(TWEEN_TIME_DEFAULT, TWEEN_TIME_DEFAULT); + if (_tweenStart.x > 0 || _tweenStart.x < -_overlapSize.x) + SetBounceTween(0, _tweenStart.x, endPos.x); + if (_tweenStart.y > 0 || _tweenStart.y < -_overlapSize.y) + SetBounceTween(1, _tweenStart.y, endPos.y); } else { @@ -1679,7 +1763,7 @@ private void __touchEnd(EventContext context) { float elapsed = (Time.unscaledTime - _lastMoveTime) * 60 - 1; if (elapsed > 1) - _velocity = _velocity * Mathf.Pow(0.833f, elapsed); + _velocity = _velocity * Mathf.Pow(VELOCITY_DAMPING_RATE, elapsed); //根据速度计算目标位置和需要时间 endPos = UpdateTargetAndDuration(_tweenStart); @@ -2037,18 +2121,27 @@ float AlignByPage(float pos, int axis, bool inertialScrolling) /// /// /// - Vector2 UpdateTargetAndDuration(Vector2 orignPos) - { - Vector2 ret = Vector2.zero; - ret.x = UpdateTargetAndDuration(orignPos.x, 0); - ret.y = UpdateTargetAndDuration(orignPos.y, 1); - return ret; - } - - float UpdateTargetAndDuration(float pos, int axis) - { - float v = _velocity[axis]; - float duration = 0; + Vector2 UpdateTargetAndDuration(Vector2 orignPos) + { + Vector2 ret = Vector2.zero; + ret.x = UpdateTargetAndDuration(orignPos.x, 0); + ret.y = UpdateTargetAndDuration(orignPos.y, 1); + return ret; + } + + static float EvaluateInertiaRatio(float velocity, float threshold) + { + if (velocity <= threshold) + return 0; + + float normalized = Mathf.Clamp01((velocity - threshold) / (threshold * INERTIA_FULL_RANGE)); + return normalized * normalized * (3f - 2f * normalized); // smoothstep + } + + float UpdateTargetAndDuration(float pos, int axis) + { + float v = _velocity[axis]; + float duration = 0; if (pos > 0) pos = 0; @@ -2057,39 +2150,30 @@ float UpdateTargetAndDuration(float pos, int axis) else { //以屏幕像素为基准 - float v2 = Mathf.Abs(v) * _velocityScale; - //在移动设备上,需要对不同分辨率做一个适配,我们的速度判断以1136分辨率为基准 - if (Stage.touchScreen) - v2 *= 1136f / Mathf.Max(Screen.width, Screen.height); - //这里有一些阈值的处理,因为在低速内,不希望产生较大的滚动(甚至不滚动) - float ratio = 0; - if (_pageMode || !Stage.touchScreen) - { - if (v2 > 500) - ratio = Mathf.Pow((v2 - 500) / 500, 2); - } - else - { - if (v2 > 1000) - ratio = Mathf.Pow((v2 - 1000) / 1000, 2); - } - - if (ratio != 0) - { - if (ratio > 1) - ratio = 1; - - v2 *= ratio; - v *= ratio; - _velocity[axis] = v; + float v2 = Mathf.Abs(v) * _velocityScale; + //在移动设备上,需要对不同分辨率做一个适配,我们的速度判断以1136分辨率为基准 + if (Stage.touchScreen) + v2 *= 1136f / Mathf.Max(Screen.width, Screen.height); + //这里有一些阈值的处理,因为在低速内,不希望产生较大的滚动(甚至不滚动) + float ratio = EvaluateInertiaRatio(v2, (_pageMode || !Stage.touchScreen) ? INERTIA_TRIGGER_DESKTOP : INERTIA_TRIGGER_TOUCH); + + if (ratio != 0) + { + ratio *= INERTIA_RELEASE_SCALE; + v2 *= ratio; + v *= ratio; + _velocity[axis] = v; //算法:v*(_decelerationRate的n次幂)= 60,即在n帧后速度降为60(假设每秒60帧)。 duration = Mathf.Log(60 / v2, _decelerationRate) / 60; - //计算距离要使用本地速度 - //理论公式貌似滚动的距离不够,改为经验公式 - //float change = (int)((v/ 60 - 1) / (1 - _decelerationRate)); - float change = (int)(v * duration * 0.4f); + // 使用几何衰减积分距离,手感会比经验系数更稳定。 + float frameCount = duration * 60f; + float change = 0; + if (_decelerationRate < 0.9999f) + change = v / 60f * (1 - Mathf.Pow(_decelerationRate, frameCount)) / (1 - _decelerationRate); + else + change = v * duration; pos += change; } } @@ -2134,6 +2218,8 @@ void KillTween() } _tweening = 0; + _tweenBounce[0] = false; + _tweenBounce[1] = false; Timers.inst.Remove(_tweenUpdateDelegate); UpdateScrollBarVisible(); @@ -2225,6 +2311,8 @@ void TweenUpdate(object param) if (_tweenChange.x == 0 && _tweenChange.y == 0) { _tweening = 0; + _tweenBounce[0] = false; + _tweenBounce[1] = false; Timers.inst.Remove(_tweenUpdateDelegate); LoopCheckingCurrent(); @@ -2252,12 +2340,13 @@ float RunTween(int axis) { newValue = _tweenStart[axis] + _tweenChange[axis]; _tweenChange[axis] = 0; + _tweenBounce[axis] = false; } - else - { - float ratio = EaseFunc(_tweenTime[axis], _tweenDuration[axis]); - newValue = _tweenStart[axis] + (int)(_tweenChange[axis] * ratio); - } + else + { + float ratio = EaseFunc(_tweenTime[axis], _tweenDuration[axis], _tweenBounce[axis]); + newValue = _tweenStart[axis] + _tweenChange[axis] * ratio; + } float threshold1 = 0; float threshold2 = -_overlapSize[axis]; @@ -2278,18 +2367,12 @@ float RunTween(int axis) if (newValue > 20 + threshold1 && _tweenChange[axis] > 0 || newValue > threshold1 && _tweenChange[axis] == 0)//开始回弹 { - _tweenTime[axis] = 0; - _tweenDuration[axis] = TWEEN_TIME_DEFAULT; - _tweenChange[axis] = -newValue + threshold1; - _tweenStart[axis] = newValue; + SetBounceTween(axis, newValue, threshold1); } else if (newValue < threshold2 - 20 && _tweenChange[axis] < 0 || newValue < threshold2 && _tweenChange[axis] == 0)//开始回弹 { - _tweenTime[axis] = 0; - _tweenDuration[axis] = TWEEN_TIME_DEFAULT; - _tweenChange[axis] = threshold2 - newValue; - _tweenStart[axis] = newValue; + SetBounceTween(axis, newValue, threshold2); } } else @@ -2298,11 +2381,13 @@ float RunTween(int axis) { newValue = threshold1; _tweenChange[axis] = 0; + _tweenBounce[axis] = false; } else if (newValue < threshold2) { newValue = threshold2; _tweenChange[axis] = 0; + _tweenBounce[axis] = false; } } } @@ -2312,9 +2397,17 @@ float RunTween(int axis) return newValue; } - static float EaseFunc(float t, float d) + static float EaseFunc(float t, float d, bool bounce) { - return (t = t / d - 1) * t * t + 1;//cubicOut + float normalized = Mathf.Clamp01(t / d); + if (bounce) + { + float response = 8f * normalized; + float settle = 1f - (1f + response) * Mathf.Exp(-response); + return Mathf.Min(1f, settle / (1f - (1f + 8f) * Mathf.Exp(-8f) + BOUNCE_EPSILON)); + } + + return (normalized -= 1f) * normalized * normalized + 1f;//cubicOut } } }