/* * Copyright (c) 2021-2024 Huawei Device Co., Ltd. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "bridge/declarative_frontend/jsview/js_scroll.h" #if !defined(PREVIEW) && defined(OHOS_PLATFORM) #include "interfaces/inner_api/ui_session/ui_session_manager.h" #endif #include "base/utils/utils.h" #include "bridge/declarative_frontend/jsview/js_scrollable.h" #include "bridge/declarative_frontend/jsview/js_scroller.h" #include "bridge/declarative_frontend/jsview/js_view_common_def.h" #include "bridge/declarative_frontend/jsview/models/scroll_model_impl.h" #include "core/common/container.h" #include "core/components/common/layout/constants.h" #include "core/components/scroll/scrollable.h" #include "core/components_ng/pattern/scroll/inner/scroll_bar.h" #include "core/components_ng/pattern/scroll/scroll_model_ng.h" #include "bridge/declarative_frontend/ark_theme/theme_apply/js_scroll_theme.h" namespace OHOS::Ace { std::unique_ptr<ScrollModel> ScrollModel::instance_ = nullptr; std::mutex ScrollModel::mutex_; ScrollModel* ScrollModel::GetInstance() { if (!instance_) { std::lock_guard<std::mutex> lock(mutex_); if (!instance_) { #ifdef NG_BUILD instance_.reset(new NG::ScrollModelNG()); #else if (Container::IsCurrentUseNewPipeline()) { instance_.reset(new NG::ScrollModelNG()); } else { instance_.reset(new Framework::ScrollModelImpl()); } #endif } } return instance_.get(); } } // namespace OHOS::Ace namespace OHOS::Ace::Framework { namespace { const std::vector<Axis> AXIS = { Axis::VERTICAL, Axis::HORIZONTAL, Axis::FREE, Axis::NONE }; bool ParseJsDimensionArray(const JSRef<JSVal>& jsValue, std::vector<Dimension>& result) { if (!jsValue->IsArray()) { return false; } JSRef<JSArray> array = JSRef<JSArray>::Cast(jsValue); for (size_t i = 0; i < array->Length(); i++) { JSRef<JSVal> value = array->GetValueAt(i); CalcDimension dimension; if (JSViewAbstract::ParseJsDimensionVp(value, dimension)) { result.emplace_back(static_cast<Dimension>(dimension)); } else { return false; } } return true; } bool CheckSnapPaginations(std::vector<Dimension> snapPaginations) { CHECK_NULL_RETURN(!snapPaginations.empty(), false); float preValue = (*snapPaginations.begin()).Value(); CHECK_NULL_RETURN(!Negative(preValue), false); auto unit = (*snapPaginations.begin()).Unit(); for (auto iter = snapPaginations.begin() + 1; iter < snapPaginations.end(); ++iter) { if (Negative((*iter).Value()) || (*iter).Unit() != unit || LessOrEqual((*iter).Value(), preValue)) { return false; } preValue = (*iter).Value(); } return true; } } // namespace void JSScroll::Create(const JSCallbackInfo& info) { ScrollModel::GetInstance()->Create(); if (info.Length() > 0 && info[0]->IsObject()) { JSScroller* jsScroller = JSRef<JSObject>::Cast(info[0])->Unwrap<JSScroller>(); if (jsScroller) { jsScroller->SetInstanceId(Container::CurrentId()); auto positionController = ScrollModel::GetInstance()->GetOrCreateController(); jsScroller->SetController(positionController); // Init scroll bar proxy. auto proxy = jsScroller->GetScrollBarProxy(); if (!proxy) { proxy = ScrollModel::GetInstance()->CreateScrollBarProxy(); jsScroller->SetScrollBarProxy(proxy); } ScrollModel::GetInstance()->SetScrollBarProxy(proxy); } } // init scroll bar std::pair<bool, Color> barColor; barColor.first = false; std::pair<bool, Dimension> barWidth; barWidth.first = false; ScrollModel::GetInstance()->InitScrollBar(GetTheme<ScrollBarTheme>(), barColor, barWidth, EdgeEffect::NONE); JSScrollTheme::ApplyTheme(); } void JSScroll::SetScrollable(int32_t value) { if (value < 0 || value >= static_cast<int32_t>(AXIS.size())) { return; } ScrollModel::GetInstance()->SetAxis(AXIS[value]); } void JSScroll::SetScrollEnabled(const JSCallbackInfo& args) { ScrollModel::GetInstance()->SetScrollEnabled(args[0]->IsBoolean() ? args[0]->ToBoolean() : true); } void JSScroll::OnScrollBeginCallback(const JSCallbackInfo& args) { if (args[0]->IsFunction()) { auto onScrollBegin = [execCtx = args.GetExecutionContext(), func = JSRef<JSFunc>::Cast(args[0])]( const Dimension& dx, const Dimension& dy) -> ScrollInfo { ScrollInfo scrollInfo { .dx = dx, .dy = dy }; JAVASCRIPT_EXECUTION_SCOPE_WITH_CHECK(execCtx, scrollInfo); auto params = ConvertToJSValues(dx, dy); auto result = func->Call(JSRef<JSObject>(), params.size(), params.data()); if (result.IsEmpty()) { return scrollInfo; } if (!result->IsObject()) { return scrollInfo; } auto resObj = JSRef<JSObject>::Cast(result); auto dxRemainValue = resObj->GetProperty("dxRemain"); if (dxRemainValue->IsNumber()) { scrollInfo.dx = Dimension(dxRemainValue->ToNumber<float>(), DimensionUnit::VP); } auto dyRemainValue = resObj->GetProperty("dyRemain"); if (dyRemainValue->IsNumber()) { scrollInfo.dy = Dimension(dyRemainValue->ToNumber<float>(), DimensionUnit::VP); } return scrollInfo; }; ScrollModel::GetInstance()->SetOnScrollBegin(std::move(onScrollBegin)); } args.SetReturnValue(args.This()); } void JSScroll::OnScrollFrameBeginCallback(const JSCallbackInfo& args) { if (args[0]->IsFunction()) { auto onScrollFrameBegin = [execCtx = args.GetExecutionContext(), func = JSRef<JSFunc>::Cast(args[0])]( const Dimension& offset, ScrollState state) -> ScrollFrameResult { OHOS::Ace::ScrollFrameResult scrollRes { .offset = offset }; JAVASCRIPT_EXECUTION_SCOPE_WITH_CHECK(execCtx, scrollRes); auto params = ConvertToJSValues(offset, state); auto result = func->Call(JSRef<JSObject>(), params.size(), params.data()); if (result.IsEmpty()) { return scrollRes; } if (!result->IsObject()) { return scrollRes; } auto resObj = JSRef<JSObject>::Cast(result); auto dxRemainValue = resObj->GetProperty("offsetRemain"); if (dxRemainValue->IsNumber()) { scrollRes.offset = Dimension(dxRemainValue->ToNumber<float>(), DimensionUnit::VP); } return scrollRes; }; ScrollModel::GetInstance()->SetOnScrollFrameBegin(std::move(onScrollFrameBegin)); } args.SetReturnValue(args.This()); } void JSScroll::OnScrollCallback(const JSCallbackInfo& args) { auto callbackInfo = args[0]; if (callbackInfo->IsFunction()) { auto onScroll = [execCtx = args.GetExecutionContext(), func = JSRef<JSFunc>::Cast(callbackInfo)]( const Dimension& xOffset, const Dimension& yOffset) { JAVASCRIPT_EXECUTION_SCOPE_WITH_CHECK(execCtx); auto params = ConvertToJSValues(xOffset, yOffset); func->Call(JSRef<JSObject>(), params.size(), params.data()); }; ScrollModel::GetInstance()->SetOnScroll(std::move(onScroll)); } } void JSScroll::OnWillScrollCallback(const JSCallbackInfo& args) { if (args.Length() <= 0) { return; } if (args[0]->IsFunction()) { auto onScroll = [execCtx = args.GetExecutionContext(), func = JSRef<JSFunc>::Cast(args[0])]( const Dimension& xOffset, const Dimension& yOffset, const ScrollState& scrollState, ScrollSource scrollSource) { auto params = ConvertToJSValues(xOffset, yOffset, scrollState, scrollSource); NG::TwoDimensionScrollResult scrollRes { .xOffset = xOffset, .yOffset = yOffset }; JAVASCRIPT_EXECUTION_SCOPE_WITH_CHECK(execCtx, scrollRes); auto result = func->Call(JSRef<JSObject>(), params.size(), params.data()); if (result.IsEmpty()) { return scrollRes; } if (!result->IsObject()) { return scrollRes; } auto resObj = JSRef<JSObject>::Cast(result); auto dxRemainValue = resObj->GetProperty("xOffset"); if (dxRemainValue->IsNumber()) { scrollRes.xOffset = Dimension(dxRemainValue->ToNumber<float>(), DimensionUnit::VP); } auto dyRemainValue = resObj->GetProperty("yOffset"); if (dyRemainValue->IsNumber()) { scrollRes.yOffset = Dimension(dyRemainValue->ToNumber<float>(), DimensionUnit::VP); } return scrollRes; }; ScrollModel::GetInstance()->SetOnWillScroll(std::move(onScroll)); } else { ScrollModel::GetInstance()->SetOnWillScroll(nullptr); } } void JSScroll::OnDidScrollCallback(const JSCallbackInfo& args) { if (args.Length() > 0 && args[0]->IsFunction()) { auto onScroll = [execCtx = args.GetExecutionContext(), func = JSRef<JSFunc>::Cast(args[0])]( const Dimension& xOffset, const Dimension& yOffset, const ScrollState& scrollState) { JAVASCRIPT_EXECUTION_SCOPE_WITH_CHECK(execCtx); auto params = ConvertToJSValues(xOffset, yOffset, scrollState); func->Call(JSRef<JSObject>(), params.size(), params.data()); }; ScrollModel::GetInstance()->SetOnDidScroll(std::move(onScroll)); } } void JSScroll::OnScrollEdgeCallback(const JSCallbackInfo& args) { if (args[0]->IsFunction()) { auto scrollEdge = [execCtx = args.GetExecutionContext(), func = JSRef<JSFunc>::Cast(args[0])]( const NG::ScrollEdge& side) { JAVASCRIPT_EXECUTION_SCOPE_WITH_CHECK(execCtx); auto params = ConvertToJSValues(side); func->Call(JSRef<JSObject>(), 1, params.data()); #if !defined(PREVIEW) && defined(OHOS_PLATFORM) UiSessionManager::GetInstance().ReportComponentChangeEvent("event", "onScrollEdge"); #endif }; ScrollModel::GetInstance()->SetOnScrollEdge(std::move(scrollEdge)); } args.SetReturnValue(args.This()); } void JSScroll::OnScrollEndCallback(const JSCallbackInfo& args) { if (args[0]->IsFunction()) { auto scrollEnd = [execCtx = args.GetExecutionContext(), func = JSRef<JSFunc>::Cast(args[0])]() { JAVASCRIPT_EXECUTION_SCOPE_WITH_CHECK(execCtx); func->Call(JSRef<JSObject>(), 0, nullptr); #if !defined(PREVIEW) && defined(OHOS_PLATFORM) UiSessionManager::GetInstance().ReportComponentChangeEvent("event", "onScrollEnd"); #endif }; ScrollModel::GetInstance()->SetOnScrollEnd(std::move(scrollEnd)); } args.SetReturnValue(args.This()); } void JSScroll::OnScrollStartCallback(const JSCallbackInfo& args) { if (args[0]->IsFunction()) { auto scrollStart = [execCtx = args.GetExecutionContext(), func = JSRef<JSFunc>::Cast(args[0])]() { JAVASCRIPT_EXECUTION_SCOPE_WITH_CHECK(execCtx); func->Call(JSRef<JSObject>(), 0, nullptr); }; ScrollModel::GetInstance()->SetOnScrollStart(std::move(scrollStart)); } args.SetReturnValue(args.This()); } void JSScroll::OnScrollStopCallback(const JSCallbackInfo& args) { if (args[0]->IsFunction()) { auto scrollStop = [execCtx = args.GetExecutionContext(), func = JSRef<JSFunc>::Cast(args[0])]() { JAVASCRIPT_EXECUTION_SCOPE_WITH_CHECK(execCtx); func->Call(JSRef<JSObject>(), 0, nullptr); #if !defined(PREVIEW) && defined(OHOS_PLATFORM) UiSessionManager::GetInstance().ReportComponentChangeEvent("event", "onScrollStop"); #endif }; ScrollModel::GetInstance()->SetOnScrollStop(std::move(scrollStop)); } args.SetReturnValue(args.This()); } void JSScroll::ReachStartCallback(const JSCallbackInfo& args) { if (args[0]->IsFunction()) { auto onReachStart = [execCtx = args.GetExecutionContext(), func = JSRef<JSFunc>::Cast(args[0])]() { func->Call(JSRef<JSObject>()); return; }; ScrollModel::GetInstance()->SetOnReachStart(std::move(onReachStart)); } args.ReturnSelf(); } void JSScroll::ReachEndCallback(const JSCallbackInfo& args) { if (args[0]->IsFunction()) { auto onReachEnd = [execCtx = args.GetExecutionContext(), func = JSRef<JSFunc>::Cast(args[0])]() { func->Call(JSRef<JSObject>()); return; }; ScrollModel::GetInstance()->SetOnReachEnd(std::move(onReachEnd)); } args.ReturnSelf(); } void JSScroll::JSBind(BindingTarget globalObj) { JSClass<JSScroll>::Declare("Scroll"); MethodOptions opt = MethodOptions::NONE; JSClass<JSScroll>::StaticMethod("create", &JSScroll::Create, opt); JSClass<JSScroll>::StaticMethod("scrollable", &JSScroll::SetScrollable, opt); JSClass<JSScroll>::StaticMethod("onScrollBegin", &JSScroll::OnScrollBeginCallback, opt); JSClass<JSScroll>::StaticMethod("onScrollFrameBegin", &JSScroll::OnScrollFrameBeginCallback, opt); JSClass<JSScroll>::StaticMethod("onScroll", &JSScroll::OnScrollCallback, opt); JSClass<JSScroll>::StaticMethod("onWillScroll", &JSScroll::OnWillScrollCallback, opt); JSClass<JSScroll>::StaticMethod("onDidScroll", &JSScroll::OnDidScrollCallback, opt); JSClass<JSScroll>::StaticMethod("onScrollEdge", &JSScroll::OnScrollEdgeCallback, opt); JSClass<JSScroll>::StaticMethod("onScrollEnd", &JSScroll::OnScrollEndCallback, opt); JSClass<JSScroll>::StaticMethod("onScrollStart", &JSScroll::OnScrollStartCallback, opt); JSClass<JSScroll>::StaticMethod("onScrollStop", &JSScroll::OnScrollStopCallback, opt); JSClass<JSScroll>::StaticMethod("onReachStart", &JSScroll::ReachStartCallback); JSClass<JSScroll>::StaticMethod("onReachEnd", &JSScroll::ReachEndCallback); JSClass<JSScroll>::StaticMethod("onClick", &JSInteractableView::JsOnClick); JSClass<JSScroll>::StaticMethod("onTouch", &JSInteractableView::JsOnTouch); JSClass<JSScroll>::StaticMethod("onHover", &JSInteractableView::JsOnHover); JSClass<JSScroll>::StaticMethod("onKeyEvent", &JSInteractableView::JsOnKey); JSClass<JSScroll>::StaticMethod("onDeleteEvent", &JSInteractableView::JsOnDelete); JSClass<JSScroll>::StaticMethod("onAttach", &JSInteractableView::JsOnAttach); JSClass<JSScroll>::StaticMethod("onAppear", &JSInteractableView::JsOnAppear); JSClass<JSScroll>::StaticMethod("onDetach", &JSInteractableView::JsOnDetach); JSClass<JSScroll>::StaticMethod("onDisAppear", &JSInteractableView::JsOnDisAppear); JSClass<JSScroll>::StaticMethod("edgeEffect", &JSScroll::SetEdgeEffect, opt); JSClass<JSScroll>::StaticMethod("scrollBar", &JSScroll::SetScrollBar, opt); JSClass<JSScroll>::StaticMethod("scrollBarColor", &JSScroll::SetScrollBarColor, opt); JSClass<JSScroll>::StaticMethod("scrollBarWidth", &JSScroll::SetScrollBarWidth, opt); JSClass<JSScroll>::StaticMethod("remoteMessage", &JSInteractableView::JsCommonRemoteMessage); JSClass<JSScroll>::StaticMethod("width", &JSScroll::JsWidth); JSClass<JSScroll>::StaticMethod("height", &JSScroll::JsHeight); JSClass<JSScroll>::StaticMethod("nestedScroll", &JSScroll::SetNestedScroll); JSClass<JSScroll>::StaticMethod("enableScrollInteraction", &JSScroll::SetScrollEnabled); JSClass<JSScroll>::StaticMethod("friction", &JSScroll::SetFriction); JSClass<JSScroll>::StaticMethod("scrollSnap", &JSScroll::SetScrollSnap); JSClass<JSScroll>::StaticMethod("enablePaging", &JSScroll::SetEnablePaging); JSClass<JSScroll>::StaticMethod("clip", &JSScrollable::JsClip); JSClass<JSScroll>::StaticMethod("initialOffset", &JSScroll::SetInitialOffset); JSClass<JSScroll>::InheritAndBind<JSScrollableBase>(globalObj); } void JSScroll::SetScrollBar(const JSCallbackInfo& args) { if (args.Length() < 1) { return; } int32_t displayMode; if (args[0]->IsNull() || args[0]->IsUndefined() || !ParseJsInt32(args[0], displayMode)) { displayMode = static_cast<int32_t>(DisplayMode::AUTO); } ScrollModel::GetInstance()->SetDisplayMode(displayMode); } void JSScroll::SetScrollBarWidth(const JSCallbackInfo& args) { auto pipelineContext = PipelineContext::GetCurrentContext(); CHECK_NULL_VOID(pipelineContext); auto theme = pipelineContext->GetTheme<ScrollBarTheme>(); CHECK_NULL_VOID(theme); CalcDimension scrollBarWidth; if (args.Length() < 1) { return; } if (!ParseJsDimensionVp(args[0], scrollBarWidth) || args[0]->IsNull() || args[0]->IsUndefined() || (args[0]->IsString() && args[0]->ToString().empty()) || LessNotEqual(scrollBarWidth.Value(), 0.0) || scrollBarWidth.Unit() == DimensionUnit::PERCENT) { scrollBarWidth = theme->GetNormalWidth(); } ScrollModel::GetInstance()->SetScrollBarWidth(scrollBarWidth); } void JSScroll::SetScrollBarColor(const JSCallbackInfo& args) { auto pipelineContext = PipelineContext::GetCurrentContext(); CHECK_NULL_VOID(pipelineContext); auto theme = pipelineContext->GetTheme<ScrollBarTheme>(); CHECK_NULL_VOID(theme); Color color(theme->GetForegroundColor()); JSViewAbstract::ParseJsColor(args[0], color); ScrollModel::GetInstance()->SetScrollBarColor(color); } void JSScroll::SetEdgeEffect(const JSCallbackInfo& args) { auto edgeEffect = EdgeEffect::NONE; if (args.Length() > 0) { edgeEffect = JSScrollable::ParseEdgeEffect(args[0], EdgeEffect::NONE); } auto alwaysEnabled = true; if (args.Length() > 1) { alwaysEnabled = JSScrollable::ParseAlwaysEnable(args[1], true); } ScrollModel::GetInstance()->SetEdgeEffect(edgeEffect, alwaysEnabled); } void JSScroll::JsWidth(const JSCallbackInfo& info) { JSViewAbstract::JsWidth(info); ScrollModel::GetInstance()->SetHasWidth(true); } void JSScroll::JsHeight(const JSCallbackInfo& info) { JSViewAbstract::JsHeight(info); ScrollModel::GetInstance()->SetHasHeight(true); } void JSScroll::SetNestedScroll(const JSCallbackInfo& args) { NestedScrollOptions nestedOpt = { .forward = NestedScrollMode::SELF_ONLY, .backward = NestedScrollMode::SELF_ONLY, }; if (args.Length() < 1 || !args[0]->IsObject()) { ScrollModel::GetInstance()->SetNestedScroll(nestedOpt); return; } JSRef<JSObject> obj = JSRef<JSObject>::Cast(args[0]); int32_t froward = 0; JSViewAbstract::ParseJsInt32(obj->GetProperty("scrollForward"), froward); if (froward < static_cast<int32_t>(NestedScrollMode::SELF_ONLY) || froward > static_cast<int32_t>(NestedScrollMode::PARALLEL)) { froward = 0; } int32_t backward = 0; JSViewAbstract::ParseJsInt32(obj->GetProperty("scrollBackward"), backward); if (backward < static_cast<int32_t>(NestedScrollMode::SELF_ONLY) || backward > static_cast<int32_t>(NestedScrollMode::PARALLEL)) { backward = 0; } nestedOpt.forward = static_cast<NestedScrollMode>(froward); nestedOpt.backward = static_cast<NestedScrollMode>(backward); ScrollModel::GetInstance()->SetNestedScroll(nestedOpt); args.ReturnSelf(); } void JSScroll::SetFriction(const JSCallbackInfo& info) { double friction = -1.0; if (!JSViewAbstract::ParseJsDouble(info[0], friction)) { friction = -1.0; } ScrollModel::GetInstance()->SetFriction(friction); } void JSScroll::SetScrollSnap(const JSCallbackInfo& args) { if (args.Length() < 1 || !args[0]->IsObject()) { return; } JSRef<JSObject> obj = JSRef<JSObject>::Cast(args[0]); auto snapAlignValue = obj->GetProperty("snapAlign"); int32_t snapAlign = static_cast<int32_t>(ScrollSnapAlign::NONE); if (snapAlignValue->IsNull() || snapAlignValue->IsUndefined() || !ParseJsInt32(snapAlignValue, snapAlign) || snapAlign < static_cast<int32_t>(ScrollSnapAlign::NONE) || snapAlign > static_cast<int32_t>(ScrollSnapAlign::END)) { snapAlign = static_cast<int32_t>(ScrollSnapAlign::NONE); } auto paginationValue = obj->GetProperty("snapPagination"); CalcDimension intervalSize; std::vector<Dimension> snapPaginations; if (!ParseJsDimensionVp(paginationValue, intervalSize) || intervalSize.IsNegative()) { intervalSize = CalcDimension(0.0); } if (!ParseJsDimensionArray(paginationValue, snapPaginations) || !CheckSnapPaginations(snapPaginations)) { std::vector<Dimension>().swap(snapPaginations); } bool enableSnapToStart = true; bool enableSnapToEnd = true; ParseJsBool(obj->GetProperty("enableSnapToStart"), enableSnapToStart); ParseJsBool(obj->GetProperty("enableSnapToEnd"), enableSnapToEnd); std::pair<bool, bool> enableSnapToSide = { enableSnapToStart, enableSnapToEnd }; ScrollModel::GetInstance()->SetScrollSnap( static_cast<ScrollSnapAlign>(snapAlign), intervalSize, snapPaginations, enableSnapToSide); } void JSScroll::SetEnablePaging(const JSCallbackInfo& args) { if (args.Length() < 1 || !args[0]->IsBoolean()) { return; } ScrollModel::GetInstance()->SetEnablePaging(args[0]->ToBoolean()); } void JSScroll::SetInitialOffset(const JSCallbackInfo& args) { if (args.Length() < 1 || !args[0]->IsObject()) { return; } JSRef<JSObject> obj = JSRef<JSObject>::Cast(args[0]); CalcDimension xOffset; ParseJsDimensionVp(obj->GetProperty("xOffset"), xOffset); CalcDimension yOffset; ParseJsDimensionVp(obj->GetProperty("yOffset"), yOffset); ScrollModel::GetInstance()->SetInitialOffset(NG::OffsetT(xOffset, yOffset)); } } // namespace OHOS::Ace::Framework