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