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 =
139 ConvertFromJSValue(animationObj->GetProperty("canOverScroll"), canOverScroll) ? true : false;
140 smooth = !hasDuration && !hasCurve && !hasCanOverScroll ? true : false;
141 } else if (animationValue->IsBoolean()) {
142 smooth = animationValue->ToBoolean();
143 }
144 auto scrollController = controllerWeak_.Upgrade();
145 if (!scrollController) {
146 EventReport::ReportScrollableErrorEvent("Scroller", ScrollableErrorType::CONTROLLER_NOT_BIND,
147 "The controller does not bind a component when calling ScrollTo function");
148 return;
149 }
150 ContainerScope scope(instanceId_);
151 auto direction = scrollController->GetScrollDirection();
152 auto position = direction == Axis::VERTICAL ? yOffset : xOffset;
153 scrollController->AnimateTo(position, static_cast<float>(duration), curve, smooth, canOverScroll);
154 }
155
ParseCurveParams(RefPtr<Curve> & curve,const JSRef<JSVal> & jsValue)156 bool JSScroller::ParseCurveParams(RefPtr<Curve>& curve, const JSRef<JSVal>& jsValue)
157 {
158 std::string curveName;
159 if (ConvertFromJSValue(jsValue, curveName)) {
160 auto index = BinarySearchFindIndex(CURVE_MAP, ArraySize(CURVE_MAP), curveName.c_str());
161 if (index >= 0) {
162 curve = CURVE_MAP[index].value;
163 return true;
164 }
165 } else if (jsValue->IsObject()) {
166 JSRef<JSVal> curveString = JSRef<JSObject>::Cast(jsValue)->GetProperty("__curveString");
167 if (curveString->IsString()) {
168 curve = CreateCurve(curveString->ToString());
169 return true;
170 }
171 }
172 return false;
173 }
174
ScrollEdge(const JSCallbackInfo & args)175 void JSScroller::ScrollEdge(const JSCallbackInfo& args)
176 {
177 AlignDeclaration::Edge edge = AlignDeclaration::Edge::AUTO;
178 if (args.Length() < 1 || !ConvertFromJSValue(args[0], EDGE_TABLE, edge)) {
179 return;
180 }
181 auto scrollController = controllerWeak_.Upgrade();
182 if (!scrollController) {
183 EventReport::ReportScrollableErrorEvent("Scroller", ScrollableErrorType::CONTROLLER_NOT_BIND,
184 "The controller does not bind a component when calling ScrollEdge function");
185 return;
186 }
187 ScrollEdgeType edgeType = EDGE_TYPE_TABLE[static_cast<int32_t>(edge)];
188 ContainerScope scope(instanceId_);
189
190 if (args.Length() > 1 && args[1]->IsObject()) {
191 auto obj = JSRef<JSObject>::Cast(args[1]);
192 float velocity = 0.0f;
193 if (ConvertFromJSValue(obj->GetProperty("velocity"), velocity)) {
194 if (velocity > 0) {
195 velocity = Dimension(velocity, DimensionUnit::VP).ConvertToPx();
196 scrollController->ScrollToEdge(edgeType, velocity);
197 return;
198 }
199 }
200 }
201 scrollController->ScrollToEdge(edgeType, true);
202 }
203
Fling(const JSCallbackInfo & args)204 void JSScroller::Fling(const JSCallbackInfo& args)
205 {
206 auto scrollController = controllerWeak_.Upgrade();
207 if (!scrollController) {
208 JSException::Throw(ERROR_CODE_NAMED_ROUTE_ERROR, "%s", "Controller not bound to component.");
209 return;
210 }
211 double flingVelocity = 0.0;
212 if (!args[0]->IsNumber()) {
213 JSException::Throw(ERROR_CODE_PARAM_INVALID, "%s", "The parameter check failed.");
214 return;
215 }
216 flingVelocity = args[0]->ToNumber<double>();
217 if (NearZero(flingVelocity)) {
218 return;
219 }
220 ContainerScope scope(instanceId_);
221 flingVelocity = Dimension(flingVelocity, DimensionUnit::VP).ConvertToPx();
222 scrollController->Fling(flingVelocity);
223 }
224
ScrollToIndex(const JSCallbackInfo & args)225 void JSScroller::ScrollToIndex(const JSCallbackInfo& args)
226 {
227 int32_t index = 0;
228 bool smooth = false;
229 ScrollAlign align = ScrollAlign::NONE;
230 if (args.Length() < 1 || !ConvertFromJSValue(args[0], index) || index < 0) {
231 return;
232 }
233 auto scrollController = controllerWeak_.Upgrade();
234 if (!scrollController) {
235 EventReport::ReportScrollableErrorEvent("Scroller", ScrollableErrorType::CONTROLLER_NOT_BIND,
236 "The controller does not bind a component when calling ScrollToIndex function");
237 return;
238 }
239 // 2: parameters count, 1: parameter index
240 auto smoothArg = args[1];
241 if (args.Length() >= 2 && smoothArg->IsBoolean()) {
242 smooth = smoothArg->ToBoolean();
243 }
244 // 3: parameters count, 2: parameter index
245 if (args.Length() >= 3) {
246 ConvertFromJSValue(args[2], ALIGN_TABLE, align);
247 }
248
249 // 4: parameters count, 3: parameter index
250 std::optional<float> extraOffset = std::nullopt;
251 auto optionArg = args[3];
252 if (args.Length() == 4 && optionArg->IsObject()) {
253 auto obj = JSRef<JSObject>::Cast(optionArg);
254 CalcDimension offset;
255 if (JSViewAbstract::ParseLengthMetricsToDimension(obj->GetProperty("extraOffset"), offset)) {
256 auto offsetPx = offset.ConvertToPx();
257 if (!std::isnan(offsetPx)) {
258 extraOffset = offsetPx;
259 }
260 }
261 }
262 ContainerScope scope(instanceId_);
263 scrollController->ScrollToIndex(index, smooth, align, extraOffset);
264 }
265
ScrollPage(const JSCallbackInfo & args)266 void JSScroller::ScrollPage(const JSCallbackInfo& args)
267 {
268 if (args.Length() < 1 || !args[0]->IsObject()) {
269 return;
270 }
271
272 auto obj = JSRef<JSObject>::Cast(args[0]);
273 bool next = true;
274 if (!ConvertFromJSValue(obj->GetProperty("next"), next)) {
275 return;
276 }
277 bool smooth = false;
278 auto smoothValue = obj->GetProperty("animation");
279 if (smoothValue->IsBoolean()) {
280 smooth = smoothValue->ToBoolean();
281 }
282 auto scrollController = controllerWeak_.Upgrade();
283 if (!scrollController) {
284 EventReport::ReportScrollableErrorEvent("Scroller", ScrollableErrorType::CONTROLLER_NOT_BIND,
285 "The controller does not bind a component when calling ScrollPage function");
286 return;
287 }
288 ContainerScope scope(instanceId_);
289 scrollController->ScrollPage(!next, smooth);
290 }
291
CurrentOffset(const JSCallbackInfo & args)292 void JSScroller::CurrentOffset(const JSCallbackInfo& args)
293 {
294 auto scrollController = controllerWeak_.Upgrade();
295 if (!scrollController) {
296 EventReport::ReportScrollableErrorEvent("Scroller", ScrollableErrorType::CONTROLLER_NOT_BIND,
297 "The controller does not bind a component when calling CurrentOffset function");
298 return;
299 }
300 auto retObj = JSRef<JSObject>::New();
301 ContainerScope scope(instanceId_);
302 auto offset = scrollController->GetCurrentOffset();
303 retObj->SetProperty("xOffset", offset.GetX());
304 retObj->SetProperty("yOffset", offset.GetY());
305 args.SetReturnValue(retObj);
306 }
307
ScrollBy(const JSCallbackInfo & args)308 void JSScroller::ScrollBy(const JSCallbackInfo& args)
309 {
310 if (args.Length() < 2) {
311 return;
312 }
313
314 Dimension xOffset;
315 Dimension yOffset;
316 if (!ConvertFromJSValue(args[0], xOffset) ||
317 !ConvertFromJSValue(args[1], yOffset)) {
318 return;
319 }
320 auto scrollController = controllerWeak_.Upgrade();
321 if (!scrollController) {
322 EventReport::ReportScrollableErrorEvent("Scroller", ScrollableErrorType::CONTROLLER_NOT_BIND,
323 "The controller does not bind a component when calling ScrollBy function");
324 return;
325 }
326
327 ContainerScope scope(instanceId_);
328 auto deltaX = xOffset.Value();
329 auto deltaY = yOffset.Value();
330 auto container = Container::Current();
331 if (container) {
332 auto context = container->GetPipelineContext();
333 if (context) {
334 if (xOffset.Unit() == DimensionUnit::PERCENT) {
335 deltaX = 0.0;
336 } else {
337 deltaX = context->NormalizeToPx(xOffset);
338 }
339 if (yOffset.Unit() == DimensionUnit::PERCENT) {
340 deltaY = 0.0;
341 } else {
342 deltaY = context->NormalizeToPx(yOffset);
343 }
344 }
345 }
346 scrollController->ScrollBy(deltaX, deltaY, false);
347 }
348
IsAtEnd(const JSCallbackInfo & args)349 void JSScroller::IsAtEnd(const JSCallbackInfo& args)
350 {
351 auto scrollController = controllerWeak_.Upgrade();
352 if (!scrollController) {
353 EventReport::ReportScrollableErrorEvent("Scroller", ScrollableErrorType::CONTROLLER_NOT_BIND,
354 "The controller does not bind a component when calling IsAtEnd function");
355 return;
356 }
357 ContainerScope scope(instanceId_);
358 bool isAtEnd = scrollController->IsAtEnd();
359 auto retVal = JSRef<JSVal>::Make(ToJSValue(isAtEnd));
360 args.SetReturnValue(retVal);
361 }
362
GetItemRect(const JSCallbackInfo & args)363 void JSScroller::GetItemRect(const JSCallbackInfo& args)
364 {
365 int32_t index = -1;
366 if (args.Length() != 1 || !ConvertFromJSValue(args[0], index)) {
367 JSException::Throw(ERROR_CODE_PARAM_INVALID, "%s", "Input parameter check failed.");
368 return;
369 }
370 auto scrollController = controllerWeak_.Upgrade();
371 if (scrollController) {
372 ContainerScope scope(instanceId_);
373 auto rectObj = CreateRectangle(scrollController->GetItemRect(index));
374 JSRef<JSVal> rect = JSRef<JSObject>::Cast(rectObj);
375 args.SetReturnValue(rect);
376 } else {
377 JSException::Throw(ERROR_CODE_NAMED_ROUTE_ERROR, "%s", "Controller not bound to component.");
378 }
379 }
380
GetItemIndex(const JSCallbackInfo & args)381 void JSScroller::GetItemIndex(const JSCallbackInfo& args)
382 {
383 if (args.Length() != ARGS_LENGTH) {
384 JSException::Throw(ERROR_CODE_PARAM_INVALID, "%s", "Input parameter length failed.");
385 return;
386 }
387
388 Dimension xOffset;
389 Dimension yOffset;
390 if (!ConvertFromJSValue(args[0], xOffset) ||
391 !ConvertFromJSValue(args[1], yOffset)) {
392 JSException::Throw(ERROR_CODE_PARAM_INVALID, "%s", "Input parameter check failed.");
393 return;
394 }
395 auto scrollController = controllerWeak_.Upgrade();
396 if (!scrollController) {
397 JSException::Throw(ERROR_CODE_NAMED_ROUTE_ERROR, "%s", "Controller not bound to component.");
398 return;
399 }
400
401 ContainerScope scope(instanceId_);
402 auto deltaX = xOffset.Value();
403 auto deltaY = yOffset.Value();
404 auto container = Container::Current();
405 if (container) {
406 auto context = container->GetPipelineContext();
407 if (context) {
408 deltaX = context->NormalizeToPx(xOffset);
409 deltaY = context->NormalizeToPx(yOffset);
410 }
411 }
412 int32_t itemIndex = scrollController->GetItemIndex(deltaX, deltaY);
413 auto retVal = JSRef<JSVal>::Make(ToJSValue(itemIndex));
414 args.SetReturnValue(retVal);
415
416 return;
417 }
418 } // namespace OHOS::Ace::Framework
419