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