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