1 /*
2 * Copyright (c) 2021-2024 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 "bridge/declarative_frontend/jsview/js_scroller.h"
17
18 #include "base/geometry/axis.h"
19 #include "base/log/event_report.h"
20 #include "base/utils/linear_map.h"
21 #include "base/utils/utils.h"
22 #include "bridge/declarative_frontend/engine/js_types.h"
23 #include "bridge/declarative_frontend/jsview/js_view_common_def.h"
24 #include "core/animation/curves.h"
25 #include "core/common/container.h"
26 #include "core/components/common/layout/align_declaration.h"
27
28 namespace OHOS::Ace::Framework {
29 namespace {
30
31 constexpr AlignDeclaration::Edge EDGE_TABLE[] = {
32 AlignDeclaration::Edge::TOP,
33 AlignDeclaration::Edge::CENTER,
34 AlignDeclaration::Edge::BOTTOM,
35 AlignDeclaration::Edge::BASELINE,
36 AlignDeclaration::Edge::START,
37 AlignDeclaration::Edge::MIDDLE,
38 AlignDeclaration::Edge::END,
39 };
40
41 // corresponding to EDGE_TABLE[]
42 constexpr ScrollEdgeType EDGE_TYPE_TABLE[] = { ScrollEdgeType::SCROLL_TOP, ScrollEdgeType::SCROLL_NONE,
43 ScrollEdgeType::SCROLL_BOTTOM, ScrollEdgeType::SCROLL_NONE, ScrollEdgeType::SCROLL_TOP, ScrollEdgeType::SCROLL_NONE,
44 ScrollEdgeType::SCROLL_BOTTOM };
45
46 const LinearMapNode<RefPtr<Curve>> CURVE_MAP[] = {
47 { "ease", Curves::EASE },
48 { "ease-in", Curves::EASE_IN },
49 { "ease-in-out", Curves::EASE_IN_OUT },
50 { "ease-out", Curves::EASE_OUT },
51 { "friction", Curves::FRICTION },
52 { "linear", Curves::LINEAR },
53 };
54
55 static constexpr int ARGS_LENGTH = 2;
56 constexpr double DEFAULT_DURATION = 1000.0;
57 constexpr ScrollAlign ALIGN_TABLE[] = {
58 ScrollAlign::START,
59 ScrollAlign::CENTER,
60 ScrollAlign::END,
61 ScrollAlign::AUTO,
62 };
63
64 const std::regex DIMENSION_REGEX(R"(^[-+]?\d+(?:\.\d+)?(?:px|vp|fp|lpx)?$)", std::regex::icase);
65 } // namespace
66
JSBind(BindingTarget globalObj)67 void JSScroller::JSBind(BindingTarget globalObj)
68 {
69 JSClass<JSScroller>::Declare("Scroller");
70 JSClass<JSScroller>::CustomMethod("scrollTo", &JSScroller::ScrollTo);
71 JSClass<JSScroller>::CustomMethod("scrollEdge", &JSScroller::ScrollEdge);
72 JSClass<JSScroller>::CustomMethod("fling", &JSScroller::Fling);
73 JSClass<JSScroller>::CustomMethod("scrollPage", &JSScroller::ScrollPage);
74 JSClass<JSScroller>::CustomMethod("currentOffset", &JSScroller::CurrentOffset);
75 JSClass<JSScroller>::CustomMethod("scrollToIndex", &JSScroller::ScrollToIndex);
76 JSClass<JSScroller>::CustomMethod("scrollBy", &JSScroller::ScrollBy);
77 JSClass<JSScroller>::CustomMethod("isAtEnd", &JSScroller::IsAtEnd);
78 JSClass<JSScroller>::CustomMethod("getItemRect", &JSScroller::GetItemRect);
79 JSClass<JSScroller>::CustomMethod("getItemIndex", &JSScroller::GetItemIndex);
80 JSClass<JSScroller>::Bind(globalObj, JSScroller::Constructor, JSScroller::Destructor);
81 }
82
Constructor(const JSCallbackInfo & args)83 void JSScroller::Constructor(const JSCallbackInfo& args)
84 {
85 auto scroller = Referenced::MakeRefPtr<JSScroller>();
86 scroller->IncRefCount();
87 args.SetReturnValue(Referenced::RawPtr(scroller));
88 }
89
Destructor(JSScroller * scroller)90 void JSScroller::Destructor(JSScroller* scroller)
91 {
92 if (scroller != nullptr) {
93 scroller->DecRefCount();
94 }
95 }
96
CreateRectangle(const Rect & info)97 JSRef<JSObject> JSScroller::CreateRectangle(const Rect& info)
98 {
99 JSRef<JSObject> rectObj = JSRef<JSObject>::New();
100 rectObj->SetProperty<double>("x", info.Left());
101 rectObj->SetProperty<double>("y", info.Top());
102 rectObj->SetProperty<double>("width", info.Width());
103 rectObj->SetProperty<double>("height", info.Height());
104 return rectObj;
105 }
106
ScrollTo(const JSCallbackInfo & args)107 void JSScroller::ScrollTo(const JSCallbackInfo& args)
108 {
109 if (args.Length() < 1 || !args[0]->IsObject()) {
110 return;
111 }
112
113 JSRef<JSObject> obj = JSRef<JSObject>::Cast(args[0]);
114 Dimension xOffset;
115 Dimension yOffset;
116 auto xOffsetStr = obj->GetProperty("xOffset");
117 auto yOffsetStr = obj->GetProperty("yOffset");
118 if (!std::regex_match(xOffsetStr->ToString(), DIMENSION_REGEX) ||
119 !std::regex_match(yOffsetStr->ToString(), DIMENSION_REGEX) || !ConvertFromJSValue(xOffsetStr, xOffset) ||
120 !ConvertFromJSValue(yOffsetStr, yOffset)) {
121 return;
122 }
123
124 double duration = 0.0;
125 bool smooth = false;
126 bool canOverScroll = false;
127 RefPtr<Curve> curve = Curves::EASE;
128 auto animationValue = obj->GetProperty("animation");
129 if (animationValue->IsObject()) {
130 auto animationObj = JSRef<JSObject>::Cast(animationValue);
131 auto curveArgs = animationObj->GetProperty("curve");
132 bool hasDuration = true;
133 if (!ConvertFromJSValue(animationObj->GetProperty("duration"), duration) || Negative(duration)) {
134 duration = DEFAULT_DURATION;
135 hasDuration = false;
136 }
137 bool hasCurve = ParseCurveParams(curve, curveArgs);
138 bool hasCanOverScroll = ConvertFromJSValue(animationObj->GetProperty("canOverScroll"), canOverScroll);
139 smooth = !hasDuration && !hasCurve && !hasCanOverScroll;
140 } else if (animationValue->IsBoolean()) {
141 smooth = animationValue->ToBoolean();
142 }
143 auto optionCanOverScroll = obj->GetProperty("canOverScroll");
144 bool canStayOverScroll = optionCanOverScroll->IsBoolean() ? optionCanOverScroll->ToBoolean() : false;
145 auto scrollController = controllerWeak_.Upgrade();
146 if (!scrollController) {
147 EventReport::ReportScrollableErrorEvent("Scroller", ScrollableErrorType::CONTROLLER_NOT_BIND,
148 "The controller does not bind a component when calling ScrollTo function");
149 return;
150 }
151 ContainerScope scope(instanceId_);
152 auto direction = scrollController->GetScrollDirection();
153 if (direction == Axis::FREE &&
154 scrollController->FreeScrollTo({ .xOffset = xOffset,
155 .yOffset = yOffset,
156 .duration = static_cast<float>(animationValue->IsBoolean() ? DEFAULT_DURATION : duration),
157 .curve = curve,
158 .smooth = (animationValue->IsBoolean() && smooth) || animationValue->IsObject(),
159 .canOverScroll = canStayOverScroll })) {
160 return;
161 }
162 auto position = direction == Axis::VERTICAL ? yOffset : xOffset;
163 scrollController->SetCanStayOverScroll(canStayOverScroll);
164 scrollController->AnimateTo(position, static_cast<float>(duration), curve, smooth, canOverScroll);
165 }
166
ParseCurveParams(RefPtr<Curve> & curve,const JSRef<JSVal> & jsValue)167 bool JSScroller::ParseCurveParams(RefPtr<Curve>& curve, const JSRef<JSVal>& jsValue)
168 {
169 std::string curveName;
170 if (ConvertFromJSValue(jsValue, curveName)) {
171 auto index = BinarySearchFindIndex(CURVE_MAP, ArraySize(CURVE_MAP), curveName.c_str());
172 if (index >= 0) {
173 curve = CURVE_MAP[index].value;
174 return true;
175 }
176 } else if (jsValue->IsObject()) {
177 JSRef<JSVal> curveString = JSRef<JSObject>::Cast(jsValue)->GetProperty("__curveString");
178 if (curveString->IsString()) {
179 curve = CreateCurve(curveString->ToString());
180 return true;
181 }
182 }
183 return false;
184 }
185
ScrollEdge(const JSCallbackInfo & args)186 void JSScroller::ScrollEdge(const JSCallbackInfo& args)
187 {
188 AlignDeclaration::Edge edge = AlignDeclaration::Edge::AUTO;
189 if (args.Length() < 1 || !ConvertFromJSValue(args[0], EDGE_TABLE, edge)) {
190 return;
191 }
192 auto scrollController = controllerWeak_.Upgrade();
193 if (!scrollController) {
194 EventReport::ReportScrollableErrorEvent("Scroller", ScrollableErrorType::CONTROLLER_NOT_BIND,
195 "The controller does not bind a component when calling ScrollEdge function");
196 return;
197 }
198 ScrollEdgeType edgeType = EDGE_TYPE_TABLE[static_cast<int32_t>(edge)];
199 if (scrollController->GetScrollDirection() == Axis::FREE) { // allow scrolling to left and right edges
200 if (edge == AlignDeclaration::Edge::START) {
201 edgeType = ScrollEdgeType::SCROLL_LEFT;
202 } else if (edge == AlignDeclaration::Edge::END) {
203 edgeType = ScrollEdgeType::SCROLL_RIGHT;
204 }
205 }
206 ContainerScope scope(instanceId_);
207
208 if (args.Length() > 1 && args[1]->IsObject()) {
209 auto obj = JSRef<JSObject>::Cast(args[1]);
210 float velocity = 0.0f;
211 if (ConvertFromJSValue(obj->GetProperty("velocity"), velocity)) {
212 if (velocity > 0) {
213 velocity = Dimension(velocity, DimensionUnit::VP).ConvertToPx();
214 scrollController->ScrollToEdge(edgeType, velocity);
215 return;
216 }
217 }
218 }
219 scrollController->ScrollToEdge(edgeType, true);
220 }
221
Fling(const JSCallbackInfo & args)222 void JSScroller::Fling(const JSCallbackInfo& args)
223 {
224 auto scrollController = controllerWeak_.Upgrade();
225 if (!scrollController) {
226 JSException::Throw(ERROR_CODE_NAMED_ROUTE_ERROR, "%s", "Controller not bound to component.");
227 return;
228 }
229 double flingVelocity = 0.0;
230 if (!args[0]->IsNumber()) {
231 JSException::Throw(ERROR_CODE_PARAM_INVALID, "%s", "The parameter check failed.");
232 return;
233 }
234 flingVelocity = args[0]->ToNumber<double>();
235 if (NearZero(flingVelocity)) {
236 return;
237 }
238 ContainerScope scope(instanceId_);
239 flingVelocity = Dimension(flingVelocity, DimensionUnit::VP).ConvertToPx();
240 scrollController->Fling(flingVelocity);
241 }
242
ScrollToIndex(const JSCallbackInfo & args)243 void JSScroller::ScrollToIndex(const JSCallbackInfo& args)
244 {
245 int32_t index = 0;
246 bool smooth = false;
247 ScrollAlign align = ScrollAlign::NONE;
248 if (args.Length() < 1 || !ConvertFromJSValue(args[0], index) || index < 0) {
249 return;
250 }
251 auto scrollController = controllerWeak_.Upgrade();
252 if (!scrollController) {
253 EventReport::ReportScrollableErrorEvent("Scroller", ScrollableErrorType::CONTROLLER_NOT_BIND,
254 "The controller does not bind a component when calling ScrollToIndex function");
255 return;
256 }
257 // 2: parameters count, 1: parameter index
258 auto smoothArg = args[1];
259 if (args.Length() >= 2 && smoothArg->IsBoolean()) {
260 smooth = smoothArg->ToBoolean();
261 }
262 // 3: parameters count, 2: parameter index
263 if (args.Length() >= 3) {
264 ConvertFromJSValue(args[2], ALIGN_TABLE, align);
265 }
266
267 // 4: parameters count, 3: parameter index
268 std::optional<float> extraOffset = std::nullopt;
269 auto optionArg = args[3];
270 if (args.Length() == 4 && optionArg->IsObject()) {
271 auto obj = JSRef<JSObject>::Cast(optionArg);
272 CalcDimension offset;
273 if (JSViewAbstract::ParseLengthMetricsToDimension(obj->GetProperty("extraOffset"), offset)) {
274 auto offsetPx = offset.ConvertToPx();
275 if (!std::isnan(offsetPx)) {
276 extraOffset = offsetPx;
277 }
278 }
279 }
280 ContainerScope scope(instanceId_);
281 scrollController->ScrollToIndex(index, smooth, align, extraOffset);
282 }
283
ScrollPage(const JSCallbackInfo & args)284 void JSScroller::ScrollPage(const JSCallbackInfo& args)
285 {
286 if (args.Length() < 1 || !args[0]->IsObject()) {
287 return;
288 }
289
290 auto obj = JSRef<JSObject>::Cast(args[0]);
291 bool next = true;
292 if (!ConvertFromJSValue(obj->GetProperty("next"), next)) {
293 return;
294 }
295 bool smooth = false;
296 auto smoothValue = obj->GetProperty("animation");
297 if (smoothValue->IsBoolean()) {
298 smooth = smoothValue->ToBoolean();
299 }
300 auto scrollController = controllerWeak_.Upgrade();
301 if (!scrollController) {
302 EventReport::ReportScrollableErrorEvent("Scroller", ScrollableErrorType::CONTROLLER_NOT_BIND,
303 "The controller does not bind a component when calling ScrollPage function");
304 return;
305 }
306 ContainerScope scope(instanceId_);
307 scrollController->ScrollPage(!next, smooth);
308 }
309
CurrentOffset(const JSCallbackInfo & args)310 void JSScroller::CurrentOffset(const JSCallbackInfo& args)
311 {
312 auto scrollController = controllerWeak_.Upgrade();
313 if (!scrollController) {
314 EventReport::ReportScrollableErrorEvent("Scroller", ScrollableErrorType::CONTROLLER_NOT_BIND,
315 "The controller does not bind a component when calling CurrentOffset function");
316 return;
317 }
318 auto retObj = JSRef<JSObject>::New();
319 ContainerScope scope(instanceId_);
320 auto offset = scrollController->GetCurrentOffset();
321 retObj->SetProperty("xOffset", offset.GetX());
322 retObj->SetProperty("yOffset", offset.GetY());
323 args.SetReturnValue(retObj);
324 }
325
ScrollBy(const JSCallbackInfo & args)326 void JSScroller::ScrollBy(const JSCallbackInfo& args)
327 {
328 if (args.Length() < 2) {
329 return;
330 }
331
332 Dimension xOffset;
333 Dimension yOffset;
334 if (!ConvertFromJSValue(args[0], xOffset) ||
335 !ConvertFromJSValue(args[1], yOffset)) {
336 return;
337 }
338 auto scrollController = controllerWeak_.Upgrade();
339 if (!scrollController) {
340 EventReport::ReportScrollableErrorEvent("Scroller", ScrollableErrorType::CONTROLLER_NOT_BIND,
341 "The controller does not bind a component when calling ScrollBy function");
342 return;
343 }
344
345 ContainerScope scope(instanceId_);
346 auto deltaX = xOffset.Value();
347 auto deltaY = yOffset.Value();
348 auto container = Container::Current();
349 if (container) {
350 auto context = container->GetPipelineContext();
351 if (context) {
352 if (xOffset.Unit() == DimensionUnit::PERCENT) {
353 deltaX = 0.0;
354 } else {
355 deltaX = context->NormalizeToPx(xOffset);
356 }
357 if (yOffset.Unit() == DimensionUnit::PERCENT) {
358 deltaY = 0.0;
359 } else {
360 deltaY = context->NormalizeToPx(yOffset);
361 }
362 }
363 }
364 scrollController->ScrollBy(deltaX, deltaY, false);
365 }
366
IsAtEnd(const JSCallbackInfo & args)367 void JSScroller::IsAtEnd(const JSCallbackInfo& args)
368 {
369 auto scrollController = controllerWeak_.Upgrade();
370 if (!scrollController) {
371 EventReport::ReportScrollableErrorEvent("Scroller", ScrollableErrorType::CONTROLLER_NOT_BIND,
372 "The controller does not bind a component when calling IsAtEnd function");
373 return;
374 }
375 ContainerScope scope(instanceId_);
376 bool isAtEnd = scrollController->IsAtEnd();
377 auto retVal = JSRef<JSVal>::Make(ToJSValue(isAtEnd));
378 args.SetReturnValue(retVal);
379 }
380
GetItemRect(const JSCallbackInfo & args)381 void JSScroller::GetItemRect(const JSCallbackInfo& args)
382 {
383 int32_t index = -1;
384 if (args.Length() != 1 || !ConvertFromJSValue(args[0], index)) {
385 JSException::Throw(ERROR_CODE_PARAM_INVALID, "%s", "Input parameter check failed.");
386 return;
387 }
388 auto scrollController = controllerWeak_.Upgrade();
389 if (scrollController) {
390 ContainerScope scope(instanceId_);
391 auto rectObj = CreateRectangle(scrollController->GetItemRect(index));
392 JSRef<JSVal> rect = JSRef<JSObject>::Cast(rectObj);
393 args.SetReturnValue(rect);
394 } else {
395 JSException::Throw(ERROR_CODE_NAMED_ROUTE_ERROR, "%s", "Controller not bound to component.");
396 }
397 }
398
GetItemIndex(const JSCallbackInfo & args)399 void JSScroller::GetItemIndex(const JSCallbackInfo& args)
400 {
401 if (args.Length() != ARGS_LENGTH) {
402 JSException::Throw(ERROR_CODE_PARAM_INVALID, "%s", "Input parameter length failed.");
403 return;
404 }
405
406 Dimension xOffset;
407 Dimension yOffset;
408 if (!ConvertFromJSValue(args[0], xOffset) ||
409 !ConvertFromJSValue(args[1], yOffset)) {
410 JSException::Throw(ERROR_CODE_PARAM_INVALID, "%s", "Input parameter check failed.");
411 return;
412 }
413 auto scrollController = controllerWeak_.Upgrade();
414 if (!scrollController) {
415 JSException::Throw(ERROR_CODE_NAMED_ROUTE_ERROR, "%s", "Controller not bound to component.");
416 return;
417 }
418
419 ContainerScope scope(instanceId_);
420 auto deltaX = xOffset.Value();
421 auto deltaY = yOffset.Value();
422 auto container = Container::Current();
423 if (container) {
424 auto context = container->GetPipelineContext();
425 if (context) {
426 deltaX = context->NormalizeToPx(xOffset);
427 deltaY = context->NormalizeToPx(yOffset);
428 }
429 }
430 int32_t itemIndex = scrollController->GetItemIndex(deltaX, deltaY);
431 auto retVal = JSRef<JSVal>::Make(ToJSValue(itemIndex));
432 args.SetReturnValue(retVal);
433
434 return;
435 }
436 } // namespace OHOS::Ace::Framework
437