• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (c) 2025 Huawei Device Co., Ltd.
3  * Licensed under the Apache License, Version 2.0 (the "License");
4  * you may not use this file except in compliance with the License.
5  * You may obtain a copy of the License at
6  *
7  *     http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14  */
15 
16 #include "core/components_ng/pattern/scroll/free_scroll_controller.h"
17 
18 #include "core/components_ng/pattern/scroll/scroll_pattern.h"
19 #include "core/components_ng/pattern/scrollable/axis/axis_animator.h"
20 #include "core/components_ng/pattern/scrollable/scrollable_animation_consts.h"
21 #include "core/components_ng/pattern/scrollable/scrollable_properties.h"
22 #include "core/components_ng/render/animation_utils.h"
23 
24 namespace OHOS::Ace::NG {
FreeScrollController(ScrollPattern & pattern)25 FreeScrollController::FreeScrollController(ScrollPattern& pattern) : pattern_(pattern)
26 {
27     offset_ = MakeRefPtr<NodeAnimatablePropertyOffsetF>(OffsetF {}, [weak = WeakClaim(this)](const OffsetF& newOffset) {
28         auto controller = weak.Upgrade();
29         if (controller) {
30             controller->HandleOffsetUpdate(newOffset);
31         }
32     });
33     auto* renderCtx = pattern_.GetRenderContext();
34     CHECK_NULL_VOID(renderCtx);
35     renderCtx->AttachNodeAnimatableProperty(offset_);
36 
37     InitializePanRecognizer();
38     InitializeTouchEvent();
39 }
40 
~FreeScrollController()41 FreeScrollController::~FreeScrollController()
42 {
43     if (offset_) {
44         auto* renderCtx = pattern_.GetRenderContext();
45         CHECK_NULL_VOID(renderCtx);
46         renderCtx->DetachNodeAnimatableProperty(offset_);
47     }
48     if (freeTouch_) {
49         auto hub = pattern_.GetGestureHub();
50         CHECK_NULL_VOID(hub);
51         hub->RemoveTouchEvent(freeTouch_);
52     }
53 }
54 
InitializePanRecognizer()55 void FreeScrollController::InitializePanRecognizer()
56 {
57     PanDirection panDirection { .type = PanDirection::ALL };
58     const double distance = SystemProperties::GetScrollableDistance();
59     PanDistanceMap distanceMap;
60 
61     if (Positive(distance)) {
62         distanceMap[SourceTool::UNKNOWN] = distance;
63     } else {
64         distanceMap[SourceTool::UNKNOWN] = DEFAULT_PAN_DISTANCE.ConvertToPx();
65         distanceMap[SourceTool::PEN] = DEFAULT_PEN_PAN_DISTANCE.ConvertToPx();
66     }
67 
68     freePanGesture_ = MakeRefPtr<PanRecognizer>(DEFAULT_PAN_FINGER, panDirection, distanceMap);
69     freePanGesture_->SetOnActionStart([weak = WeakClaim(this)](const GestureEvent& event) {
70         auto controller = weak.Upgrade();
71         if (controller) {
72             controller->HandlePanStart(event);
73         }
74     });
75     freePanGesture_->SetOnActionUpdate([weak = WeakClaim(this)](const GestureEvent& event) {
76         auto controller = weak.Upgrade();
77         if (controller) {
78             controller->HandlePanUpdate(event);
79         }
80     });
81     const auto endCallback = [weak = WeakClaim(this)](const GestureEvent& event) {
82         auto controller = weak.Upgrade();
83         if (controller) {
84             controller->HandlePanEndOrCancel(event);
85         }
86     };
87     freePanGesture_->SetOnActionEnd(endCallback);
88     freePanGesture_->SetOnActionCancel(endCallback);
89     freePanGesture_->SetRecognizerType(GestureTypeName::PAN_GESTURE);
90     freePanGesture_->SetIsSystemGesture(true);
91     freePanGesture_->SetIsAllowMouse(false);
92     freePanGesture_->SetSysGestureJudge(
93         [](const RefPtr<GestureInfo>& gestureInfo, const std::shared_ptr<BaseGestureEvent>& info) {
94             if (gestureInfo->GetInputEventType() == InputEventType::AXIS &&
95                 (info->IsKeyPressed(KeyCode::KEY_CTRL_LEFT) || info->IsKeyPressed(KeyCode::KEY_CTRL_RIGHT))) {
96                 return GestureJudgeResult::REJECT;
97             }
98             return GestureJudgeResult::CONTINUE;
99         });
100 }
101 
102 namespace {
103 using State = FreeScrollController::State;
ToScrollSource(State state)104 ScrollSource ToScrollSource(State state)
105 {
106     switch (state) {
107         case State::IDLE:
108             return ScrollSource::SCROLLER;
109         case State::DRAG:
110             return ScrollSource::DRAG;
111         case State::FLING:
112             return ScrollSource::FLING;
113         case State::EXTERNAL_FLING:
114             return ScrollSource::SCROLLER_ANIMATION;
115         case State::BOUNCE:
116             return ScrollSource::EDGE_EFFECT;
117         default:
118             return ScrollSource::OTHER_USER_INPUT; // Default to IDLE if unknown
119     }
120 }
121 
ToScrollState(State state)122 ScrollState ToScrollState(State state)
123 {
124     switch (state) {
125         case State::IDLE:
126             return ScrollState::IDLE;
127         case State::DRAG:
128             return ScrollState::SCROLL;
129         case State::FLING:
130         case State::EXTERNAL_FLING:
131         case State::BOUNCE:
132             return ScrollState::FLING;
133         default:
134             return ScrollState::IDLE; // Default to IDLE if unknown
135     }
136 }
137 
InAnimation(State state)138 bool InAnimation(State state)
139 {
140     return state == State::FLING || state == State::EXTERNAL_FLING || state == State::BOUNCE;
141 }
142 
143 /**
144  * @return ratio (non-negative) between overScroll and viewport length.
145  */
GetGamma(float offset,float scrollableDistance,float viewLength)146 float GetGamma(float offset, float scrollableDistance, float viewLength)
147 {
148     if (NearZero(viewLength)) {
149         return 1.0f;
150     }
151     if (Positive(offset)) {
152         return offset / viewLength;
153     }
154     if (LessNotEqual(offset, -scrollableDistance)) {
155         return -(scrollableDistance + offset) / viewLength;
156     }
157     return 0.0f;
158 }
159 
GetFriction(const ScrollPattern & pattern)160 float GetFriction(const ScrollPattern& pattern)
161 {
162     auto friction = static_cast<float>(pattern.GetFriction());
163     if (NonPositive(friction)) {
164         auto* ctx = pattern.GetContext();
165         CHECK_NULL_RETURN(ctx, 0.0f);
166         auto theme = ctx->GetTheme<ScrollableTheme>();
167         CHECK_NULL_RETURN(theme, 0.0f);
168         friction = theme->GetFriction();
169     }
170     return friction * -FRICTION_SCALE;
171 }
172 
CreateSpringOption(float friction)173 AnimationOption CreateSpringOption(float friction)
174 {
175     if (NearZero(friction)) {
176         TAG_LOGW(AceLogTag::ACE_SCROLL, "CreateSpringOption called with zero friction, returning default option.");
177         return {};
178     }
179     const auto curve = AceType::MakeRefPtr<ResponsiveSpringMotion>(fabs(2 * ACE_PI / friction), 1.0f, 0.0f);
180     AnimationOption option(curve, CUSTOM_SPRING_ANIMATION_DURATION);
181     option.SetFinishCallbackType(FinishCallbackType::LOGICALLY);
182     return option;
183 }
184 
185 constexpr float EDGE_FRICTION = 10;
186 } // namespace
187 
HandlePanStart(const GestureEvent & event)188 void FreeScrollController::HandlePanStart(const GestureEvent& event)
189 {
190     state_ = State::DRAG;
191     FireOnScrollStart();
192     if (axisAnimator_ && !Scrollable::IsMouseWheelScroll(event)) {
193         axisAnimator_->StopAxisAnimation();
194     }
195 }
196 
HandlePanUpdate(const GestureEvent & event)197 void FreeScrollController::HandlePanUpdate(const GestureEvent& event)
198 {
199     size_t fingers = event.GetFingerList().size();
200     auto dx = static_cast<float>(event.GetDelta().GetX());
201     auto dy = static_cast<float>(event.GetDelta().GetY());
202     if (fingers > 1) {
203         dx /= fingers;
204         dy /= fingers;
205     }
206     const float newX = offset_->Get().GetX() + dx;
207     const float newY = offset_->Get().GetY() + dy;
208     const auto scrollableArea = pattern_.GetViewPortExtent() - pattern_.GetViewSize();
209 
210     const float gammaX = GetGamma(newX, scrollableArea.Width(), pattern_.GetViewSize().Width());
211     const float gammaY = GetGamma(newY, scrollableArea.Height(), pattern_.GetViewSize().Height());
212     // apply friction if overScrolling
213     OffsetF deltaF { NearZero(gammaX) ? dx : dx * pattern_.CalculateFriction(gammaX),
214         NearZero(gammaY) ? dy : dy * pattern_.CalculateFriction(gammaY) };
215     deltaF = FireOnWillScroll(deltaF, ScrollState::SCROLL, ScrollSource::DRAG);
216     const auto newOffset = offset_->Get() + deltaF;
217     CheckCrashEdge(newOffset, scrollableArea);
218     if (Scrollable::IsMouseWheelScroll(event)) {
219         AnimateOnMouseScroll(deltaF); // use animation to make mouse wheel scroll smoother
220         return;
221     }
222     offset_->Set(newOffset);
223     pattern_.MarkDirty();
224 }
225 
HandlePanEndOrCancel(const GestureEvent & event)226 void FreeScrollController::HandlePanEndOrCancel(const GestureEvent& event)
227 {
228     state_ = State::IDLE;
229     if (Scrollable::IsMouseWheelScroll(event)) {
230         FireOnScrollEnd(); // no fling animation in mouse wheel scroll
231         return;
232     }
233     const auto& src = event.GetVelocity();
234     OffsetF velocity { static_cast<float>(src.GetVelocityX()), static_cast<float>(src.GetVelocityY()) };
235     Fling(velocity);
236     if (state_ == State::IDLE) {
237         // If the state is IDLE, it means no fling animation is running.
238         // We can fire the onScrollEnd event here.
239         FireOnScrollEnd();
240     }
241 }
242 
Fling(const OffsetF & velocity)243 void FreeScrollController::Fling(const OffsetF& velocity)
244 {
245     const bool outOfBounds = ClampPosition(offset_->Get()) != offset_->Get();
246     const float friction = outOfBounds ? EDGE_FRICTION : GetFriction(pattern_);
247     if (NearZero(friction)) {
248         TAG_LOGW(AceLogTag::ACE_SCROLL, "Fling called with zero friction, skipping fling animation.");
249         return;
250     }
251 
252     OffsetF finalPos = offset_->Get() + velocity * FLING_SCALE_K / friction;
253     if (outOfBounds) {
254         finalPos = ClampPosition(finalPos);
255     } // when not out of bounds, finalPos doesn't need clamping because we would clamp it later during the
256       // animation when we reach edge and increase friction.
257 
258     if (finalPos == offset_->Get()) {
259         // No movement, no need to animate.
260         return;
261     }
262     state_ = State::FLING;
263     offset_->AnimateWithVelocity(CreateSpringOption(friction), finalPos, velocity, [weak = WeakClaim(this)]() {
264         auto self = weak.Upgrade();
265         CHECK_NULL_VOID(self);
266         if (self->state_ == State::BOUNCE) {
267             return; // don't trigger if we transitioned to BOUNCE state
268         }
269         self->HandleAnimationEnd();
270     });
271 }
272 
HandleOffsetUpdate(const OffsetF & currentValue)273 void FreeScrollController::HandleOffsetUpdate(const OffsetF& currentValue)
274 {
275     pattern_.MarkDirty();
276     if (state_ == State::DRAG) {
277         return; // callbacks and checks already handled in HandlePanUpdate
278     }
279 
280     FireOnWillScroll(currentValue - actualOffset_, ToScrollState(state_), ToScrollSource(state_));
281     const bool reachedEdge = CheckCrashEdge(currentValue, pattern_.GetViewPortExtent() - pattern_.GetViewSize());
282     if (state_ == State::FLING && reachedEdge) {
283         // change friction during animation and transition to BOUNCE animation
284         const auto finalPos = ClampPosition(offset_->GetStagingValue());
285         AnimationUtils::Animate(
286             CreateSpringOption(EDGE_FRICTION),
287             [weak = WeakPtr(offset_), finalPos]() {
288                 auto prop = weak.Upgrade();
289                 CHECK_NULL_VOID(prop);
290                 prop->Set(finalPos);
291             },
292             [weak = WeakClaim(this)]() {
293                 auto self = weak.Upgrade();
294                 CHECK_NULL_VOID(self);
295                 self->HandleAnimationEnd();
296             });
297         state_ = State::BOUNCE;
298     }
299 }
300 
HandleAnimationEnd()301 void FreeScrollController::HandleAnimationEnd()
302 {
303     state_ = State::IDLE;
304     FireOnScrollEnd();
305 }
306 
ClampPosition(const OffsetF & finalPos) const307 OffsetF FreeScrollController::ClampPosition(const OffsetF& finalPos) const
308 {
309     OffsetF clampedPos = finalPos;
310     clampedPos.SetX(std::clamp(clampedPos.GetX(), std::min(-pattern_.GetScrollableDistance(), 0.0f), 0.0f));
311 
312     float verticalLimit = -(pattern_.GetViewPortExtent().Height() - pattern_.GetViewSize().Height());
313     clampedPos.SetY(std::clamp(clampedPos.GetY(), std::min(verticalLimit, 0.0f), 0.0f));
314     return clampedPos;
315 }
316 
InitializeTouchEvent()317 void FreeScrollController::InitializeTouchEvent()
318 {
319     auto touchTask = [weak = WeakClaim(this)](const TouchEventInfo& info) {
320         auto controller = weak.Upgrade();
321         CHECK_NULL_VOID(controller);
322 
323         switch (info.GetTouches().front().GetTouchType()) {
324             case TouchType::DOWN:
325                 controller->HandleTouchDown();
326                 break;
327             case TouchType::UP:
328             case TouchType::CANCEL:
329                 controller->HandleTouchUpOrCancel();
330                 break;
331             default:
332                 break;
333         }
334     };
335 
336     freeTouch_ = MakeRefPtr<TouchEventImpl>(std::move(touchTask));
337     auto hub = pattern_.GetGestureHub();
338     CHECK_NULL_VOID(hub);
339     hub->AddTouchEvent(freeTouch_);
340 }
341 
HandleTouchDown()342 void FreeScrollController::HandleTouchDown()
343 {
344     if (state_ == State::DRAG) {
345         return; // ignore touch down of second finger
346     }
347     StopScrollAnimation();
348 }
349 
StopScrollAnimation()350 void FreeScrollController::StopScrollAnimation()
351 {
352     AnimationOption option;
353     option.SetCurve(Curves::EASE);
354     option.SetDuration(0);
355     AnimationUtils::StartAnimation(
356         option, [this]() { offset_->Set(offset_->Get()); }, nullptr);
357     state_ = State::IDLE;
358 }
359 
HandleTouchUpOrCancel()360 void FreeScrollController::HandleTouchUpOrCancel()
361 {
362     if (state_ == State::IDLE) {
363         // animate if currently out of bounds
364         Fling({});
365     }
366 }
367 
HandleAxisAnimationFrame(float newOffset)368 void FreeScrollController::HandleAxisAnimationFrame(float newOffset)
369 {
370     if (InAnimation(state_)) { // can't update offset if in animation
371         return;
372     }
373     auto offset = offset_->Get();
374     mouseWheelScrollIsVertical_ ? offset.SetY(newOffset) : offset.SetX(newOffset);
375     offset_->Set(ClampPosition(offset));
376 }
377 
AnimateOnMouseScroll(const OffsetF & delta)378 void FreeScrollController::AnimateOnMouseScroll(const OffsetF& delta)
379 {
380     mouseWheelScrollIsVertical_ = NearZero(delta.GetX());
381     if (!axisAnimator_) {
382         axisAnimator_ = MakeRefPtr<AxisAnimator>(
383             [wk = WeakClaim(this)](float newOffset) { // animation frame callback
384                 auto self = wk.Upgrade();
385                 CHECK_NULL_VOID(self);
386                 self->HandleAxisAnimationFrame(newOffset);
387             },
388             nullptr, nullptr);
389         axisAnimator_->Initialize(WeakClaim(pattern_.GetContext()));
390     }
391     Axis axis = mouseWheelScrollIsVertical_ ? Axis::VERTICAL : Axis::HORIZONTAL;
392     axisAnimator_->OnAxis(delta.GetMainOffset(axis), offset_->Get().GetMainOffset(axis));
393 }
394 
GetOffset() const395 OffsetF FreeScrollController::GetOffset() const
396 {
397     if (offset_) {
398         return offset_->Get();
399     }
400     return {};
401 }
402 
OnLayoutFinished(const OffsetF & adjustedOffset,const SizeF & scrollableArea)403 void FreeScrollController::OnLayoutFinished(const OffsetF& adjustedOffset, const SizeF& scrollableArea)
404 {
405     if (offset_ && offset_->Get() != adjustedOffset &&
406         !InAnimation(state_)) { // modifying animatableProperty during animation not allowed
407         offset_->Set(adjustedOffset);
408     }
409     if (adjustedOffset != actualOffset_) {
410         // Fire onDidScroll only if the offset has changed.
411         FireOnDidScroll(adjustedOffset - actualOffset_, ToScrollState(state_));
412         actualOffset_ = adjustedOffset;
413     }
414     auto props = pattern_.GetLayoutProperty<ScrollLayoutProperty>();
415     CHECK_NULL_VOID(props);
416     if (scrollableArea.IsNonNegative()) {
417         enableScroll_ = props->GetScrollEnabled().value_or(true);
418     } else {
419         enableScroll_ = props->GetScrollEnabled().value_or(true) && pattern_.GetAlwaysEnabled();
420     }
421     if (freePanGesture_) {
422         freePanGesture_->SetEnabled(enableScroll_);
423     }
424 }
425 
SetOffset(OffsetF newPos,bool allowOverScroll)426 void FreeScrollController::SetOffset(OffsetF newPos, bool allowOverScroll)
427 {
428     if (InAnimation(state_)) {
429         StopScrollAnimation();
430     }
431     if (!allowOverScroll) {
432         newPos = ClampPosition(newPos);
433     }
434     if (newPos != offset_->Get()) {
435         offset_->Set(newPos);
436     }
437 }
438 
439 namespace {
ToVp(float value)440 Dimension ToVp(float value)
441 {
442     return Dimension { Dimension(value).ConvertToVp(), DimensionUnit::VP };
443 }
444 } // namespace
445 
FireOnWillScroll(const OffsetF & delta,ScrollState state,ScrollSource source) const446 OffsetF FreeScrollController::FireOnWillScroll(const OffsetF& delta, ScrollState state, ScrollSource source) const
447 {
448     auto eventHub = pattern_.GetOrCreateEventHub<ScrollEventHub>();
449     CHECK_NULL_RETURN(eventHub, delta);
450     const auto& onScroll = eventHub->GetOnWillScrollEvent();
451     const auto& frameCb = eventHub->GetJSFrameNodeOnScrollWillScroll();
452 
453     // delta sign is reversed in user space
454     std::optional<TwoDimensionScrollResult> res;
455     if (onScroll) {
456         res = onScroll(ToVp(-delta.GetX()), ToVp(-delta.GetY()), state, source);
457     }
458     if (frameCb) {
459         if (res) {
460             // use the result from JS callback if available
461             res = frameCb(res->xOffset, res->yOffset, state, source);
462         } else {
463             res = frameCb(ToVp(-delta.GetX()), ToVp(-delta.GetY()), state, source);
464         }
465     }
466     if (!res) {
467         return delta;
468     }
469     auto* context = pattern_.GetContext();
470     CHECK_NULL_RETURN(context, delta);
471     return { -context->NormalizeToPx(res->xOffset), -context->NormalizeToPx(res->yOffset) };
472 }
473 
FireOnDidScroll(const OffsetF & delta,ScrollState state) const474 void FreeScrollController::FireOnDidScroll(const OffsetF& delta, ScrollState state) const
475 {
476     auto eventHub = pattern_.GetOrCreateEventHub<ScrollEventHub>();
477     CHECK_NULL_VOID(eventHub);
478     const auto& onScroll = eventHub->GetOnDidScrollEvent();
479     const auto& frameCb = eventHub->GetJSFrameNodeOnScrollDidScroll();
480     if (onScroll) {
481         onScroll(ToVp(-delta.GetX()), ToVp(-delta.GetY()), state);
482     }
483     if (frameCb) {
484         frameCb(ToVp(-delta.GetX()), ToVp(-delta.GetY()), state);
485     }
486 }
487 
FireOnScrollStart() const488 void FreeScrollController::FireOnScrollStart() const
489 {
490     auto eventHub = pattern_.GetOrCreateEventHub<ScrollEventHub>();
491     CHECK_NULL_VOID(eventHub);
492     const auto& onScrollStart = eventHub->GetOnScrollStart();
493     const auto& frameCb = eventHub->GetJSFrameNodeOnScrollStart();
494     if (onScrollStart) {
495         onScrollStart();
496     }
497     if (frameCb) {
498         frameCb();
499     }
500     pattern_.AddEventsFiredInfo(ScrollableEventType::ON_SCROLL_START);
501     if (auto scrollBar = pattern_.Get2DScrollBar()) {
502         scrollBar->OnScrollStart();
503     }
504 }
505 
FireOnScrollEnd() const506 void FreeScrollController::FireOnScrollEnd() const
507 {
508     auto eventHub = pattern_.GetOrCreateEventHub<ScrollEventHub>();
509     CHECK_NULL_VOID(eventHub);
510     const auto& onScrollStop = eventHub->GetOnScrollStop();
511     const auto& frameCb = eventHub->GetJSFrameNodeOnScrollStop();
512     if (onScrollStop) {
513         onScrollStop();
514     }
515     if (frameCb) {
516         frameCb();
517     }
518     pattern_.AddEventsFiredInfo(ScrollableEventType::ON_SCROLL_STOP);
519     if (auto scrollBar = pattern_.Get2DScrollBar()) {
520         scrollBar->OnScrollEnd();
521     }
522 }
523 
FireOnScrollEdge(const std::vector<ScrollEdge> & edges) const524 void FreeScrollController::FireOnScrollEdge(const std::vector<ScrollEdge>& edges) const
525 {
526     auto eventHub = pattern_.GetOrCreateEventHub<ScrollEventHub>();
527     CHECK_NULL_VOID(eventHub);
528     const auto& onScrollEdge = eventHub->GetScrollEdgeEvent();
529     CHECK_NULL_VOID(onScrollEdge);
530     for (auto&& edge : edges) {
531         onScrollEdge(edge);
532     }
533     pattern_.AddEventsFiredInfo(ScrollableEventType::ON_SCROLL_EDGE);
534 }
535 
CheckCrashEdge(const OffsetF & newOffset,const SizeF & scrollableArea) const536 bool FreeScrollController::CheckCrashEdge(const OffsetF& newOffset, const SizeF& scrollableArea) const
537 {
538     CHECK_NULL_RETURN(offset_, false);
539     std::vector<ScrollEdge> edges;
540     const auto checkEdge = [&](float prev, float curr, float minVal, ScrollEdge edgeMin, ScrollEdge edgeMax) {
541         if (Negative(prev) && NonNegative(curr)) {
542             edges.emplace_back(edgeMin);
543         } else if (GreatNotEqual(prev, minVal) && LessOrEqual(curr, minVal)) {
544             edges.emplace_back(edgeMax);
545         }
546     };
547 
548     checkEdge(actualOffset_.GetX(), newOffset.GetX(), -scrollableArea.Width(), ScrollEdge::LEFT, ScrollEdge::RIGHT);
549     checkEdge(actualOffset_.GetY(), newOffset.GetY(), -scrollableArea.Height(), ScrollEdge::TOP, ScrollEdge::BOTTOM);
550 
551     if (!edges.empty()) {
552         FireOnScrollEdge(edges);
553         return true;
554     }
555     return false;
556 }
557 
558 using std::optional;
ScrollTo(OffsetF finalPos,const optional<float> & velocity,optional<int32_t> duration,RefPtr<Curve> curve,bool allowOverScroll)559 void FreeScrollController::ScrollTo(OffsetF finalPos, const optional<float>& velocity, optional<int32_t> duration,
560     RefPtr<Curve> curve, bool allowOverScroll)
561 {
562     StopScrollAnimation();
563     if (!allowOverScroll) {
564         finalPos = ClampPosition(finalPos);
565     }
566     if (finalPos == offset_->Get()) {
567         // No movement, no need to animate.
568         return;
569     }
570 
571     if (!curve) {
572         curve = MakeRefPtr<InterpolatingSpring>(velocity.value_or(DEFAULT_SCROLL_TO_VELOCITY), DEFAULT_SCROLL_TO_MASS,
573             DEFAULT_SCROLL_TO_STIFFNESS, DEFAULT_SCROLL_TO_DAMPING);
574     }
575     AnimationUtils::StartAnimation(
576         { curve, duration.value_or(CUSTOM_SPRING_ANIMATION_DURATION) },
577         [weak = WeakPtr(offset_), finalPos]() {
578             auto prop = weak.Upgrade();
579             CHECK_NULL_VOID(prop);
580             prop->Set(finalPos);
581         },
582         [weak = WeakClaim(this)]() {
583             auto self = weak.Upgrade();
584             CHECK_NULL_VOID(self);
585             self->HandleAnimationEnd();
586         });
587     state_ = State::EXTERNAL_FLING;
588     FireOnScrollStart();
589 }
590 } // namespace OHOS::Ace::NG
591