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