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