• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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