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 "frameworks/bridge/declarative_frontend/jsview/js_navdestination_scrollable_processor.h"
17 
18 #include "base/log/ace_scoring_log.h"
19 #include "bridge/declarative_frontend/engine/js_ref_ptr.h"
20 #include "bridge/declarative_frontend/engine/js_types.h"
21 
22 namespace OHOS::Ace::Framework {
23 namespace {
24 constexpr float SCROLL_RATIO = 2.0f;
25 
CreateObserver(WeakPtr<JSNavDestinationScrollableProcessor> weakProcessor,WeakPtr<JSScroller> weakScroller)26 ScrollerObserver CreateObserver(
27     WeakPtr<JSNavDestinationScrollableProcessor> weakProcessor, WeakPtr<JSScroller> weakScroller)
28 {
29     ScrollerObserver observer;
30     auto touchEvent = [weakProcessor, weakScroller](const TouchEventInfo& info) {
31         auto processor = weakProcessor.Upgrade();
32         CHECK_NULL_VOID(processor);
33         processor->HandleOnTouchEvent(weakScroller, info);
34     };
35     observer.onTouchEvent = AceType::MakeRefPtr<NG::TouchEventImpl>(std::move(touchEvent));
36 
37     observer.onReachStartEvent = [weakProcessor, weakScroller]() {
38         auto processor = weakProcessor.Upgrade();
39         CHECK_NULL_VOID(processor);
40         processor->HandleOnReachEvent(weakScroller, true);
41     };
42 
43     observer.onReachEndEvent = [weakProcessor, weakScroller]() {
44         auto processor = weakProcessor.Upgrade();
45         CHECK_NULL_VOID(processor);
46         processor->HandleOnReachEvent(weakScroller, false);
47     };
48 
49     observer.onScrollStartEvent = [weakProcessor, weakScroller]() {
50         auto processor = weakProcessor.Upgrade();
51         CHECK_NULL_VOID(processor);
52         processor->HandleOnScrollStartEvent(weakScroller);
53     };
54 
55     observer.onScrollStopEvent = [weakProcessor, weakScroller]() {
56         auto processor = weakProcessor.Upgrade();
57         CHECK_NULL_VOID(processor);
58         processor->HandleOnScrollStopEvent(weakScroller);
59     };
60 
61     observer.onDidScrollEvent =
62         [weakProcessor, weakScroller](
63             Dimension dimension, ScrollState state, ScrollSource source, bool isAtTop, bool isAtBottom) {
64             auto processor = weakProcessor.Upgrade();
65             CHECK_NULL_VOID(processor);
66             processor->HandleOnDidScrollEvent(weakScroller, dimension, source, isAtTop, isAtBottom);
67         };
68 
69     return observer;
70 }
71 
ParseScrollerArray(const JSCallbackInfo & info)72 std::vector<WeakPtr<JSScroller>> ParseScrollerArray(const JSCallbackInfo& info)
73 {
74     std::vector<WeakPtr<JSScroller>> scrollers;
75     if (info.Length() < 1 || !info[0]->IsArray()) {
76         return scrollers;
77     }
78 
79     auto scrollerArray = JSRef<JSArray>::Cast(info[0]);
80     auto arraySize = scrollerArray->Length();
81     for (size_t idx = 0; idx < arraySize; idx++) {
82         auto item = scrollerArray->GetValueAt(idx);
83         if (!item->IsObject()) {
84             continue;
85         }
86         auto* scroller = JSRef<JSObject>::Cast(item)->Unwrap<JSScroller>();
87         if (!scroller) {
88             continue;
89         }
90         scrollers.emplace_back(AceType::WeakClaim(scroller));
91     }
92     return scrollers;
93 }
94 
ParseNestedScrollerArray(const JSCallbackInfo & info)95 std::vector<std::pair<WeakPtr<JSScroller>, WeakPtr<JSScroller>>> ParseNestedScrollerArray(const JSCallbackInfo& info)
96 {
97     std::vector<std::pair<WeakPtr<JSScroller>, WeakPtr<JSScroller>>> nestedScrollers;
98     if (info.Length() < 1 || !info[0]->IsArray()) {
99         return nestedScrollers;
100     }
101 
102     auto nestedScrollerArray = JSRef<JSArray>::Cast(info[0]);
103     auto arraySize = nestedScrollerArray->Length();
104     for (size_t idx = 0; idx < arraySize; idx++) {
105         auto item = nestedScrollerArray->GetValueAt(idx);
106         if (!item->IsObject()) {
107             continue;
108         }
109         auto jsNestedScrollInfo = JSRef<JSObject>::Cast(item);
110         auto jsChildScroller = jsNestedScrollInfo->GetProperty("child");
111         auto jsParentScroller = jsNestedScrollInfo->GetProperty("parent");
112         if (!jsChildScroller->IsObject() || !jsParentScroller->IsObject()) {
113             continue;
114         }
115         auto* childScroller = JSRef<JSObject>::Cast(jsChildScroller)->Unwrap<JSScroller>();
116         auto* parentScroller = JSRef<JSObject>::Cast(jsParentScroller)->Unwrap<JSScroller>();
117         if (!childScroller || !parentScroller) {
118             continue;
119         }
120         nestedScrollers.emplace_back(AceType::WeakClaim(childScroller), AceType::WeakClaim(parentScroller));
121     }
122     return nestedScrollers;
123 }
124 } // namespace
125 
HandleOnTouchEvent(WeakPtr<JSScroller> weakScroller,const TouchEventInfo & info)126 void JSNavDestinationScrollableProcessor::HandleOnTouchEvent(
127     WeakPtr<JSScroller> weakScroller, const TouchEventInfo& info)
128 {
129     const auto& touches = info.GetTouches();
130     if (touches.empty()) {
131         return;
132     }
133     auto touchType = touches.front().GetTouchType();
134     if (touchType != TouchType::DOWN && touchType != TouchType::UP && touchType != TouchType::CANCEL) {
135         return;
136     }
137     auto navDestPattern = weakPattern_.Upgrade();
138     CHECK_NULL_VOID(navDestPattern);
139     auto it = scrollInfoMap_.find(weakScroller);
140     if (it == scrollInfoMap_.end()) {
141         return;
142     }
143     auto& scrollInfo = it->second;
144     if (touchType == TouchType::DOWN) {
145         scrollInfo.isTouching = true;
146         if (!scrollInfo.isAtTop && !scrollInfo.isAtBottom) {
147             // If we have started the task of showing titleBar/toolBar delayed task, we need to cancel it.
148             navDestPattern->CancelShowTitleAndToolBarTask();
149         }
150         return;
151     }
152     scrollInfo.isTouching = false;
153     if (scrollInfo.isScrolling) {
154         return;
155     }
156     /**
157      * When touching and scrolling stops, it is necessary to check
158      * whether the titleBar&toolBar should be restored to its original position.
159      */
160     auto pipeline = navDestPattern->GetContext();
161     CHECK_NULL_VOID(pipeline);
162     pipeline->AddAfterLayoutTask([weakPattern = weakPattern_]() {
163         auto pattern = weakPattern.Upgrade();
164         CHECK_NULL_VOID(pattern);
165         pattern->ResetTitleAndToolBarState();
166     });
167     pipeline->RequestFrame();
168 }
169 
HandleOnReachEvent(WeakPtr<JSScroller> weakScroller,bool isTopEvent)170 void JSNavDestinationScrollableProcessor::HandleOnReachEvent(WeakPtr<JSScroller> weakScroller, bool isTopEvent)
171 {
172     auto it = scrollInfoMap_.find(weakScroller);
173     if (it == scrollInfoMap_.end()) {
174         return;
175     }
176     auto& scrollInfo = it->second;
177     if (isTopEvent) {
178         scrollInfo.isAtTop = true;
179     } else {
180         scrollInfo.isAtBottom = true;
181     }
182 }
183 
HandleOnScrollStartEvent(WeakPtr<JSScroller> weakScroller)184 void JSNavDestinationScrollableProcessor::HandleOnScrollStartEvent(WeakPtr<JSScroller> weakScroller)
185 {
186     auto it = scrollInfoMap_.find(weakScroller);
187     if (it == scrollInfoMap_.end()) {
188         return;
189     }
190     auto navDestPattern = weakPattern_.Upgrade();
191     CHECK_NULL_VOID(navDestPattern);
192     auto& scrollInfo = it->second;
193     scrollInfo.isScrolling = true;
194     if (!scrollInfo.isAtTop && !scrollInfo.isAtBottom && !scrollInfo.isTouching) {
195         // If we have started the task of showing titleBar/toolBar delayed task, we need to cancel it.
196         navDestPattern->CancelShowTitleAndToolBarTask();
197     }
198 }
199 
HandleOnScrollStopEvent(WeakPtr<JSScroller> weakScroller)200 void JSNavDestinationScrollableProcessor::HandleOnScrollStopEvent(WeakPtr<JSScroller> weakScroller)
201 {
202     auto it = scrollInfoMap_.find(weakScroller);
203     if (it == scrollInfoMap_.end()) {
204         return;
205     }
206     auto& scrollInfo = it->second;
207     scrollInfo.isScrolling = false;
208     if (scrollInfo.isTouching) {
209         return;
210     }
211     /**
212      * When touching and scrolling stops, it is necessary to check
213      * whether the titleBar&toolBar should be restored to its original position.
214      */
215     auto navDestPattern = weakPattern_.Upgrade();
216     CHECK_NULL_VOID(navDestPattern);
217     auto pipeline = navDestPattern->GetContext();
218     CHECK_NULL_VOID(pipeline);
219     pipeline->AddAfterLayoutTask([weakPattern = weakPattern_]() {
220         auto pattern = weakPattern.Upgrade();
221         CHECK_NULL_VOID(pattern);
222         pattern->ResetTitleAndToolBarState();
223     });
224     pipeline->RequestFrame();
225 }
226 
HandleOnDidScrollEvent(WeakPtr<JSScroller> weakScroller,Dimension dimension,ScrollSource source,bool isAtTop,bool isAtBottom)227 void JSNavDestinationScrollableProcessor::HandleOnDidScrollEvent(
228     WeakPtr<JSScroller> weakScroller, Dimension dimension, ScrollSource source, bool isAtTop, bool isAtBottom)
229 {
230     auto it = scrollInfoMap_.find(weakScroller);
231     if (it == scrollInfoMap_.end()) {
232         return;
233     }
234     auto& scrollInfo = it->second;
235     if ((scrollInfo.isAtTop && isAtTop) || (scrollInfo.isAtBottom && isAtBottom)) {
236         // If we have already scrolled to the top or bottom, just return.
237         return;
238     }
239 
240     auto navDestPattern = weakPattern_.Upgrade();
241     CHECK_NULL_VOID(navDestPattern);
242     auto pipeline = navDestPattern->GetContext();
243     CHECK_NULL_VOID(pipeline);
244     if (scrollInfo.isScrolling) {
245         auto offset = dimension.ConvertToPx() / SCROLL_RATIO;
246         if (!(source == ScrollSource::SCROLLER || source == ScrollSource::SCROLLER_ANIMATION) || NonPositive(offset)) {
247             /**
248              * We will respond to user actions by scrolling up or down. But for the scrolling triggered by developers
249              * through the frontend interface, we will only respond to scrolling down.
250              */
251             pipeline->AddAfterLayoutTask([weakPattern = weakPattern_, offset]() {
252                 auto pattern = weakPattern.Upgrade();
253                 CHECK_NULL_VOID(pattern);
254                 pattern->UpdateTitleAndToolBarHiddenOffset(offset);
255             });
256             pipeline->RequestFrame();
257         }
258     }
259 
260     auto isChildReachTop = !scrollInfo.isAtTop && isAtTop;
261     auto isChildReachBottom = !scrollInfo.isAtBottom && isAtBottom;
262     auto isParentAtTop = true;
263     auto isParentAtBottom = true;
264     if (scrollInfo.parentScroller.has_value()) {
265         auto iter = scrollInfoMap_.find(scrollInfo.parentScroller.value());
266         isParentAtTop = iter == scrollInfoMap_.end() || iter->second.isAtTop;
267         isParentAtBottom = iter == scrollInfoMap_.end() || iter->second.isAtBottom;
268     }
269     /**
270      * For non-nested scrolling component, we need show titleBar&toolBar immediately when scrolled
271      * to the top or bottom. But for the nested scrolling components, the titleBar&toolBar can only be show
272      * immediately when the parent component also reaches the top or bottom.
273      */
274     if ((isChildReachTop && isParentAtTop) || (isChildReachBottom && isParentAtBottom)) {
275         pipeline->AddAfterLayoutTask([weakPattern = weakPattern_]() {
276             auto pattern = weakPattern.Upgrade();
277             CHECK_NULL_VOID(pattern);
278             pattern->ShowTitleAndToolBar();
279         });
280         pipeline->RequestFrame();
281     }
282 
283     scrollInfo.isAtTop = isAtTop;
284     scrollInfo.isAtBottom = isAtBottom;
285 }
286 
BindToScrollable(const JSCallbackInfo & info)287 void JSNavDestinationScrollableProcessor::BindToScrollable(const JSCallbackInfo& info)
288 {
289     needUpdateBindingRelation_ = true;
290     incommingScrollers_.clear();
291     std::vector<WeakPtr<JSScroller>> scrollers = ParseScrollerArray(info);
292     for (const auto& scroller : scrollers) {
293         incommingScrollers_.emplace(scroller);
294     }
295 }
296 
BindToNestedScrollable(const JSCallbackInfo & info)297 void JSNavDestinationScrollableProcessor::BindToNestedScrollable(const JSCallbackInfo& info)
298 {
299     needUpdateBindingRelation_ = true;
300     incommingNestedScrollers_.clear();
301     auto nestedScrollers = ParseNestedScrollerArray(info);
302     for (const auto& scrollerPair : nestedScrollers) {
303         incommingNestedScrollers_.emplace(scrollerPair.second, std::nullopt);
304         incommingNestedScrollers_.emplace(scrollerPair.first, scrollerPair.second);
305     }
306 }
307 
UpdateBindingRelation()308 void JSNavDestinationScrollableProcessor::UpdateBindingRelation()
309 {
310     if (!needUpdateBindingRelation_) {
311         return;
312     }
313     needUpdateBindingRelation_ = false;
314 
315     // mark all scroller need unbind.
316     for (auto& pair : scrollInfoMap_) {
317         pair.second.needUnbind = true;
318     }
319 
320     CombineIncomingScrollers();
321     // If the bindingRelation has changed or there is no bindingRelation, then we need show titleBar&toolBar again.
322     bool needShowBar = false;
323     if (BuildNewBindingRelation()) {
324         needShowBar = true;
325     }
326     if (RemoveUnneededBindingRelation()) {
327         needShowBar = true;
328     }
329     if (scrollInfoMap_.empty()) {
330         needShowBar = true;
331     }
332     if (!needShowBar) {
333         return;
334     }
335     auto pattern = weakPattern_.Upgrade();
336     CHECK_NULL_VOID(pattern);
337     pattern->ShowTitleAndToolBar();
338 }
339 
CombineIncomingScrollers()340 void JSNavDestinationScrollableProcessor::CombineIncomingScrollers()
341 {
342     for (auto& scroller : incommingScrollers_) {
343         NestedScrollers nestedScroller(scroller, std::nullopt);
344         auto it = incommingNestedScrollers_.find(nestedScroller);
345         if (it != incommingNestedScrollers_.end()) {
346             continue;
347         }
348         incommingNestedScrollers_.emplace(nestedScroller);
349     }
350     incommingScrollers_.clear();
351 }
352 
BuildNewBindingRelation()353 bool JSNavDestinationScrollableProcessor::BuildNewBindingRelation()
354 {
355     bool buildNewRelation = false;
356     for (auto& scrollers : incommingNestedScrollers_) {
357         auto it = scrollInfoMap_.find(scrollers.child);
358         if (it != scrollInfoMap_.end()) {
359             it->second.needUnbind = false;
360             it->second.parentScroller = scrollers.parent;
361             continue;
362         }
363 
364         auto jsScroller = scrollers.child.Upgrade();
365         if (!jsScroller) {
366             continue;
367         }
368         auto observer = CreateObserver(WeakClaim(this), scrollers.child);
369         jsScroller->AddObserver(observer, nodeId_);
370         ScrollInfo info;
371         info.parentScroller = scrollers.parent;
372         info.needUnbind = false;
373         scrollInfoMap_.emplace(scrollers.child, info);
374         buildNewRelation = true;
375     }
376     incommingNestedScrollers_.clear();
377     return buildNewRelation;
378 }
379 
RemoveUnneededBindingRelation()380 bool JSNavDestinationScrollableProcessor::RemoveUnneededBindingRelation()
381 {
382     bool unbindRelation = false;
383     auto infoIter = scrollInfoMap_.begin();
384     for (; infoIter != scrollInfoMap_.end();) {
385         if (!infoIter->second.needUnbind) {
386             ++infoIter;
387             continue;
388         }
389 
390         auto jsScroller = infoIter->first.Upgrade();
391         if (jsScroller) {
392             jsScroller->RemoveObserver(nodeId_);
393         }
394         infoIter = scrollInfoMap_.erase(infoIter);
395         unbindRelation = true;
396     }
397     return unbindRelation;
398 }
399 
UnbindAllScrollers()400 void JSNavDestinationScrollableProcessor::UnbindAllScrollers()
401 {
402     needUpdateBindingRelation_ = true;
403     incommingScrollers_.clear();
404     incommingNestedScrollers_.clear();
405     UpdateBindingRelation();
406 }
407 } // namespace OHOS::Ace::Framework
408