1 /*
2  * Copyright (c) 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_tabs_feature.h"
17 
18 #include "bridge/declarative_frontend/jsview/js_scroller.h"
19 #include "bridge/declarative_frontend/jsview/js_tabs_controller.h"
20 
21 namespace OHOS::Ace::Framework {
22 namespace {
23 struct ScrollInfo {
24     bool isTouching = false;
25     bool isScrolling = false;
26     bool isAtTop = false;
27     bool isAtBottom = false;
28     std::optional<WeakPtr<JSScroller>> parentScroller;
29 };
30 
31 using ScrollInfoMap = std::map<WeakPtr<JSScroller>, ScrollInfo>;
32 using BindInfoMap = std::map<WeakPtr<NG::TabsControllerNG>, ScrollInfoMap>;
33 
34 const auto INDEX_ZERO = 0;
35 const auto INDEX_ONE = 1;
36 const auto INDEX_TWO = 2;
37 const auto SHOW_TAB_BAR_DELAY = 2000;
38 const auto SCROLL_RANGE = 36;
39 
40 BindInfoMap bindInfoMap_;
41 
HandleOnTouchEvent(WeakPtr<JSScroller> jsScrollerWeak,const TouchEventInfo & info)42 void HandleOnTouchEvent(WeakPtr<JSScroller> jsScrollerWeak, const TouchEventInfo& info)
43 {
44     auto touchType = info.GetTouches().front().GetTouchType();
45     if (touchType != TouchType::DOWN && touchType != TouchType::UP && touchType != TouchType::CANCEL) {
46         return;
47     }
48 
49     for (auto& bindInfo : bindInfoMap_) {
50         auto& scrollInfoMap = bindInfo.second;
51         auto scrollInfoIter = scrollInfoMap.find(jsScrollerWeak);
52         if (scrollInfoIter == scrollInfoMap.end()) {
53             continue;
54         }
55         auto& scrollInfo = scrollInfoIter->second;
56         auto tabsController = bindInfo.first.Upgrade();
57         if (touchType == TouchType::DOWN) {
58             scrollInfo.isTouching = true;
59             if (!scrollInfo.isAtTop && !scrollInfo.isAtBottom && tabsController) {
60                 tabsController->StopShowTabBar();
61             }
62         } else if (touchType == TouchType::UP || touchType == TouchType::CANCEL) {
63             scrollInfo.isTouching = false;
64             if (!scrollInfo.isScrolling && tabsController) {
65                 tabsController->StartShowTabBar(SHOW_TAB_BAR_DELAY);
66             }
67         }
68     }
69 }
70 
HandleOnPanActionEndEvent(WeakPtr<JSScroller> jsScrollerWeak,const GestureEvent & info)71 void HandleOnPanActionEndEvent(WeakPtr<JSScroller> jsScrollerWeak, const GestureEvent& info)
72 {
73     auto velocity = info.GetMainVelocity();
74     if (!NearZero(velocity)) {
75         return;
76     }
77 
78     for (auto& bindInfo : bindInfoMap_) {
79         auto& scrollInfoMap = bindInfo.second;
80         if (scrollInfoMap.find(jsScrollerWeak) == scrollInfoMap.end()) {
81             continue;
82         }
83         auto tabsController = bindInfo.first.Upgrade();
84         if (tabsController) {
85             tabsController->StartShowTabBar(SHOW_TAB_BAR_DELAY);
86         }
87     }
88 }
89 
HandleOnReachEvent(WeakPtr<JSScroller> jsScrollerWeak,bool isTopEvent)90 void HandleOnReachEvent(WeakPtr<JSScroller> jsScrollerWeak, bool isTopEvent)
91 {
92     for (auto& bindInfo : bindInfoMap_) {
93         auto& scrollInfoMap = bindInfo.second;
94         auto scrollInfoIter = scrollInfoMap.find(jsScrollerWeak);
95         if (scrollInfoIter == scrollInfoMap.end()) {
96             continue;
97         }
98         auto& scrollInfo = scrollInfoIter->second;
99         if (isTopEvent) {
100             scrollInfo.isAtTop = true;
101         } else {
102             scrollInfo.isAtBottom = true;
103         }
104     }
105 }
106 
HandleOnScrollStartEvent(WeakPtr<JSScroller> jsScrollerWeak)107 void HandleOnScrollStartEvent(WeakPtr<JSScroller> jsScrollerWeak)
108 {
109     for (auto& bindInfo : bindInfoMap_) {
110         auto& scrollInfoMap = bindInfo.second;
111         auto scrollInfoIter = scrollInfoMap.find(jsScrollerWeak);
112         if (scrollInfoIter == scrollInfoMap.end()) {
113             continue;
114         }
115         auto& scrollInfo = scrollInfoIter->second;
116         scrollInfo.isScrolling = true;
117         auto tabsController = bindInfo.first.Upgrade();
118         if (!scrollInfo.isAtTop && !scrollInfo.isAtBottom && !scrollInfo.isTouching && tabsController) {
119             tabsController->StopShowTabBar();
120         }
121     }
122 }
123 
HandleOnScrollStopEvent(WeakPtr<JSScroller> jsScrollerWeak)124 void HandleOnScrollStopEvent(WeakPtr<JSScroller> jsScrollerWeak)
125 {
126     for (auto& bindInfo : bindInfoMap_) {
127         auto& scrollInfoMap = bindInfo.second;
128         auto scrollInfoIter = scrollInfoMap.find(jsScrollerWeak);
129         if (scrollInfoIter == scrollInfoMap.end()) {
130             continue;
131         }
132         auto& scrollInfo = scrollInfoIter->second;
133         scrollInfo.isScrolling = false;
134         auto tabsController = bindInfo.first.Upgrade();
135         if (!scrollInfo.parentScroller.has_value() && !scrollInfo.isTouching && tabsController) {
136             // start show tab bar when parent scrollable component stop scroll.
137             tabsController->StartShowTabBar(SHOW_TAB_BAR_DELAY);
138         }
139     }
140 }
141 
HandleOnDidScrollEvent(WeakPtr<JSScroller> jsScrollerWeak,Dimension dimension,ScrollState state,bool isAtTop,bool isAtBottom)142 void HandleOnDidScrollEvent(
143     WeakPtr<JSScroller> jsScrollerWeak, Dimension dimension, ScrollState state, bool isAtTop, bool isAtBottom)
144 {
145     for (auto& bindInfo : bindInfoMap_) {
146         auto& scrollInfoMap = bindInfo.second;
147         auto scrollInfoIter = scrollInfoMap.find(jsScrollerWeak);
148         if (scrollInfoIter == scrollInfoMap.end()) {
149             continue;
150         }
151         auto& scrollInfo = scrollInfoIter->second;
152         if ((scrollInfo.isAtTop && isAtTop) || (scrollInfo.isAtBottom && isAtBottom)) {
153             continue;
154         }
155         auto tabsController = bindInfo.first.Upgrade();
156         if (tabsController) {
157             if (scrollInfo.isScrolling) {
158                 auto ratio = dimension.ConvertToPx() / Dimension(SCROLL_RANGE, DimensionUnit::VP).ConvertToPx();
159                 tabsController->UpdateTabBarHiddenRatio(ratio);
160             }
161 
162             auto isChildReachTop = !scrollInfo.isAtTop && isAtTop;
163             auto isChildReachBottom = !scrollInfo.isAtBottom && isAtBottom;
164             auto isParentAtTop = true;
165             auto isParentAtBottom = true;
166             if (scrollInfo.parentScroller.has_value()) {
167                 auto iter = scrollInfoMap.find(scrollInfo.parentScroller.value());
168                 isParentAtTop = iter == scrollInfoMap.end() || iter->second.isAtTop;
169                 isParentAtBottom = iter == scrollInfoMap.end() || iter->second.isAtBottom;
170             }
171             if ((isChildReachTop && isParentAtTop) || (isChildReachBottom && isParentAtBottom)) {
172                 tabsController->StartShowTabBar();
173             }
174         }
175         scrollInfo.isAtTop = isAtTop;
176         scrollInfo.isAtBottom = isAtBottom;
177     }
178 }
179 
CreateObserver(WeakPtr<JSScroller> jsScrollerWeak)180 ScrollerObserver CreateObserver(WeakPtr<JSScroller> jsScrollerWeak)
181 {
182     ScrollerObserver observer;
183 
184     auto touchEvent = [jsScrollerWeak](const TouchEventInfo& info) {
185         HandleOnTouchEvent(jsScrollerWeak, info);
186     };
187     observer.onTouchEvent = AceType::MakeRefPtr<NG::TouchEventImpl>(std::move(touchEvent));
188 
189     auto panActionEndEvent = [jsScrollerWeak](const GestureEvent& info) {
190         HandleOnPanActionEndEvent(jsScrollerWeak, info);
191     };
192     observer.onPanActionEndEvent = panActionEndEvent;
193 
194     auto reachStartEvent = [jsScrollerWeak]() {
195         HandleOnReachEvent(jsScrollerWeak, true);
196     };
197     observer.onReachStartEvent = std::move(reachStartEvent);
198 
199     auto reachEndEvent = [jsScrollerWeak]() {
200         HandleOnReachEvent(jsScrollerWeak, false);
201     };
202     observer.onReachEndEvent = std::move(reachEndEvent);
203 
204     auto scrollStartEvent = [jsScrollerWeak]() {
205         HandleOnScrollStartEvent(jsScrollerWeak);
206     };
207     observer.onScrollStartEvent = std::move(scrollStartEvent);
208 
209     auto scrollStopEvent = [jsScrollerWeak]() {
210         HandleOnScrollStopEvent(jsScrollerWeak);
211     };
212     observer.onScrollStopEvent = std::move(scrollStopEvent);
213 
214     auto didScrollEvent = [jsScrollerWeak](Dimension dimension, ScrollState state,
215         ScrollSource source, bool isAtTop, bool isAtBottom) {
216         HandleOnDidScrollEvent(jsScrollerWeak, dimension, state, isAtTop, isAtBottom);
217     };
218     observer.onDidScrollEvent = std::move(didScrollEvent);
219 
220     return observer;
221 }
222 
HandleBindTabsToScrollable(const JSRef<JSObject> & jsTabsControllerVal,const JSRef<JSObject> & jsScrollerVal,const std::optional<JSRef<JSObject>> & parentJsScrollerVal)223 void HandleBindTabsToScrollable(const JSRef<JSObject>& jsTabsControllerVal, const JSRef<JSObject>& jsScrollerVal,
224     const std::optional<JSRef<JSObject>>& parentJsScrollerVal)
225 {
226     auto* jsTabsController = jsTabsControllerVal->Unwrap<JSTabsController>();
227     CHECK_NULL_VOID(jsTabsController);
228     auto tabsController = jsTabsController->GetTabsController();
229     CHECK_NULL_VOID(tabsController);
230     auto tabsControllerWeak = AceType::WeakClaim(AceType::RawPtr(tabsController));
231     auto* jsScroller = jsScrollerVal->Unwrap<JSScroller>();
232     CHECK_NULL_VOID(jsScroller);
233     auto jsScrollerWeak = AceType::WeakClaim(jsScroller);
234 
235     ScrollInfoMap scrollInfoMap;
236     auto bindInfoIter = bindInfoMap_.find(tabsControllerWeak);
237     if (bindInfoIter != bindInfoMap_.end()) {
238         scrollInfoMap = bindInfoIter->second;
239         if (scrollInfoMap.find(jsScrollerWeak) != scrollInfoMap.end()) {
240             return;
241         }
242     }
243     auto observer = CreateObserver(jsScrollerWeak);
244     jsScroller->SetObserver(observer);
245     ScrollInfo scrollInfo;
246     if (parentJsScrollerVal.has_value()) {
247         auto* parentJsScroller = parentJsScrollerVal.value()->Unwrap<JSScroller>();
248         if (parentJsScroller) {
249             scrollInfo.parentScroller = AceType::WeakClaim(parentJsScroller);
250         }
251     }
252     scrollInfoMap[jsScrollerWeak] = scrollInfo;
253     bindInfoMap_[tabsControllerWeak] = scrollInfoMap;
254 }
255 
HandleUnbindTabsFromScrollable(const JSRef<JSObject> & jsTabsControllerVal,const JSRef<JSObject> & jsScrollerVal,const std::optional<JSRef<JSObject>> & parentJsScrollerVal)256 void HandleUnbindTabsFromScrollable(const JSRef<JSObject>& jsTabsControllerVal, const JSRef<JSObject>& jsScrollerVal,
257     const std::optional<JSRef<JSObject>>& parentJsScrollerVal)
258 {
259     auto* jsTabsController = jsTabsControllerVal->Unwrap<JSTabsController>();
260     CHECK_NULL_VOID(jsTabsController);
261     auto tabsController = jsTabsController->GetTabsController();
262     CHECK_NULL_VOID(tabsController);
263     auto tabsControllerWeak = AceType::WeakClaim(AceType::RawPtr(tabsController));
264     auto* jsScroller = jsScrollerVal->Unwrap<JSScroller>();
265     CHECK_NULL_VOID(jsScroller);
266     auto jsScrollerWeak = AceType::WeakClaim(jsScroller);
267 
268     auto bindInfoIter = bindInfoMap_.find(tabsControllerWeak);
269     if (bindInfoIter == bindInfoMap_.end()) {
270         return;
271     }
272     auto& scrollInfoMap = bindInfoIter->second;
273     if (scrollInfoMap.find(jsScrollerWeak) != scrollInfoMap.end()) {
274         scrollInfoMap.erase(jsScrollerWeak);
275         if (scrollInfoMap.empty()) {
276             bindInfoMap_.erase(tabsControllerWeak);
277         }
278         tabsController->StartShowTabBar();
279     }
280 
281     if (parentJsScrollerVal.has_value()) {
282         // unbind nested scrollable component.
283         auto* parentJsScroller = parentJsScrollerVal.value()->Unwrap<JSScroller>();
284         CHECK_NULL_VOID(parentJsScroller);
285         auto parentJsScrollerWeak = AceType::WeakClaim(parentJsScroller);
286 
287         auto needRemoveParent = true;
288         for (const auto& scrollInfo : scrollInfoMap) {
289             if (scrollInfo.second.parentScroller.has_value() &&
290                 scrollInfo.second.parentScroller.value() == parentJsScrollerWeak) {
291                 needRemoveParent = false;
292             }
293         }
294         if (needRemoveParent) {
295             scrollInfoMap.erase(parentJsScrollerWeak);
296             if (scrollInfoMap.empty()) {
297                 bindInfoMap_.erase(tabsControllerWeak);
298             }
299             tabsController->StartShowTabBar();
300         }
301     }
302 }
303 
304 } // namespace
305 
BindTabsToScrollable(const JSCallbackInfo & info)306 void JSTabsFeature::BindTabsToScrollable(const JSCallbackInfo& info)
307 {
308     if (info.Length() <= INDEX_ONE) {
309         return;
310     }
311     if (!info[INDEX_ZERO]->IsObject() || !info[INDEX_ONE]->IsObject()) {
312         return;
313     }
314 
315     HandleBindTabsToScrollable(info[INDEX_ZERO], info[INDEX_ONE], std::nullopt);
316 }
317 
UnbindTabsFromScrollable(const JSCallbackInfo & info)318 void JSTabsFeature::UnbindTabsFromScrollable(const JSCallbackInfo& info)
319 {
320     if (info.Length() <= INDEX_ONE) {
321         return;
322     }
323     if (!info[INDEX_ZERO]->IsObject() || !info[INDEX_ONE]->IsObject()) {
324         return;
325     }
326 
327     HandleUnbindTabsFromScrollable(info[INDEX_ZERO], info[INDEX_ONE], std::nullopt);
328 }
329 
BindTabsToNestedScrollable(const JSCallbackInfo & info)330 void JSTabsFeature::BindTabsToNestedScrollable(const JSCallbackInfo& info)
331 {
332     if (info.Length() <= INDEX_TWO) {
333         return;
334     }
335     if (!info[INDEX_ZERO]->IsObject() || !info[INDEX_ONE]->IsObject() || !info[INDEX_TWO]->IsObject()) {
336         return;
337     }
338 
339     HandleBindTabsToScrollable(info[INDEX_ZERO], info[INDEX_ONE], std::nullopt);
340     HandleBindTabsToScrollable(info[INDEX_ZERO], info[INDEX_TWO], info[INDEX_ONE]);
341 }
342 
UnbindTabsFromNestedScrollable(const JSCallbackInfo & info)343 void JSTabsFeature::UnbindTabsFromNestedScrollable(const JSCallbackInfo& info)
344 {
345     if (info.Length() <= INDEX_TWO) {
346         return;
347     }
348     if (!info[INDEX_ZERO]->IsObject() || !info[INDEX_ONE]->IsObject() || !info[INDEX_TWO]->IsObject()) {
349         return;
350     }
351 
352     HandleUnbindTabsFromScrollable(info[INDEX_ZERO], info[INDEX_TWO], info[INDEX_ONE]);
353 }
354 
355 } // namespace OHOS::Ace::Framework
356