1 /*
2  * Copyright (c) 2021-2022 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 "core/components_v2/indexer/render_popup_list.h"
17 
18 #include "base/log/log.h"
19 #include "core/animation/bilateral_spring_node.h"
20 #include "core/components/common/properties/shadow_config.h"
21 #include "core/components/scroll/render_scroll.h"
22 #include "core/components/scroll/render_single_child_scroll.h"
23 #include "core/components/scroll/scroll_spring_effect.h"
24 #include "core/components_v2/indexer/popup_list_component.h"
25 #include "core/components_v2/indexer/popup_list_item_component.h"
26 
27 namespace OHOS::Ace::V2 {
28 constexpr double VIEW_PORT_SCALE = 3.0;
29 
Update(const RefPtr<Component> & component)30 void RenderPopupList::Update(const RefPtr<Component>& component)
31 {
32     component_ = AceType::DynamicCast<PopupListComponent>(component);
33     ACE_DCHECK(component_);
34 
35     boxComponent_ = AceType::DynamicCast<BoxComponent>(component_->GetChildren().front());
36     ACE_DCHECK(boxComponent_);
37 
38     popupSelectedEventFun_ = AceAsyncEvent<void(const std::shared_ptr<IndexerEventInfo>&)>::
39         Create(component_->GetPopupSelectedEvent(), context_);
40 
41     auto axis = component_->GetDirection();
42 
43     if (scrollable_) {
44         scrollable_->SetAxis(axis);
45     } else {
46         auto callback = [weak = AceType::WeakClaim(this)](double offset, int32_t source) {
47             auto renderPopupList = weak.Upgrade();
48             if (!renderPopupList) {
49                 return false;
50             }
51 
52             Offset delta;
53             delta.SetX(0.0);
54             delta.SetY(offset);
55 
56             renderPopupList->AdjustOffset(delta, source);
57             return renderPopupList->UpdateScrollPosition(delta.GetY(), source);
58         };
59         scrollable_ = AceType::MakeRefPtr<Scrollable>(callback, axis);
60         scrollable_->Initialize(context_);
61     }
62 
63     // only support spring
64     if (component_->GetEdgeEffect() == EdgeEffect::SPRING) {
65         if (!scrollEffect_ || scrollEffect_->GetEdgeEffect() != EdgeEffect::SPRING) {
66             scrollEffect_ = AceType::MakeRefPtr<ScrollSpringEffect>();
67             ResetEdgeEffect();
68         }
69     } else {
70         scrollEffect_ = nullptr;
71     }
72 
73     MarkNeedLayout();
74 }
75 
PerformLayout()76 void RenderPopupList::PerformLayout()
77 {
78     // Check validation of layout size
79     if (NearZero(viewPortWidth_) || NearZero(viewPortHeight_)) {
80         LOGW("Cannot layout using invalid view port");
81         return;
82     }
83 
84     const auto maxSize = Size(viewPortWidth_, viewPortHeight_);
85     const auto minSize = GetLayoutParam().GetMinSize();
86     const auto innerLayout = LayoutParam(maxSize, minSize);
87     double curMainPos = LayoutOrRecycleCurrentItems(innerLayout);
88 
89     for (size_t newIndex = startIndex_ + items_.size(); curMainPos < endMainPos_; ++newIndex) {
90         auto child = RequestAndLayoutNewItem(newIndex, innerLayout);
91         if (!child) {
92             startIndex_ = std::min(startIndex_, datas_.size());
93             break;
94         }
95         curMainPos += child->GetLayoutSize().Height();
96     }
97 
98     bool noEdgeEffect = (scrollable_ && scrollable_->IsAnimationNotRunning()) || !scrollEffect_;
99     // Check if reach the end of popup list
100     reachEnd_ = LessOrEqual(curMainPos, viewPortHeight_);
101     if (noEdgeEffect && reachEnd_) {
102         // Adjust end of popup list to match the end of layout
103         currentOffset_ += viewPortHeight_ - curMainPos;
104         curMainPos = viewPortHeight_;
105     }
106 
107     // Try to request new items at start if needed
108     for (; currentOffset_ > startMainPos_ && startIndex_ > 0; --startIndex_) {
109         auto child = RequestAndLayoutNewItem(startIndex_ - 1, innerLayout);
110         if (!child) {
111             break;
112         }
113         currentOffset_ -= child->GetLayoutSize().Height();
114     }
115 
116     // Check if reach the start of list
117     reachStart_ = GreatOrEqual(currentOffset_, 0.0);
118     if (noEdgeEffect && reachStart_) {
119         curMainPos -= currentOffset_;
120         currentOffset_ = 0;
121     }
122 
123     // Check if disable or enable scrollable
124     CalculateMainScrollExtent(curMainPos);
125 
126     // Set position for each child
127     SetItemsPosition();
128 
129     // Set layout size of popup list component itself
130     SetLayoutSize(GetLayoutParam().Constrain(Size(viewPortWidth_, viewPortHeight_)));
131 }
132 
TouchTest(const Point & globalPoint,const Point & parentLocalPoint,const TouchRestrict & touchRestrict,TouchTestResult & result)133 bool RenderPopupList::TouchTest(const Point& globalPoint, const Point& parentLocalPoint,
134     const TouchRestrict& touchRestrict, TouchTestResult& result)
135 {
136     if (GetDisableTouchEvent() || disabled_) {
137         return false;
138     }
139 
140     // calculate touch point in which item.
141     CalTouchPoint(globalPoint, popupSelected_);
142 
143     // call js function
144     OnPopupSelected(popupSelected_);
145 
146     // Calculates the local point location and coordinate offset in this node.
147     const auto localPoint = parentLocalPoint - GetPaintRect().GetOffset();
148     const auto coordinateOffset = globalPoint - localPoint;
149     globalPoint_ = globalPoint;
150     OnTouchTestHit(coordinateOffset, touchRestrict, result);
151 
152     return true;
153 }
154 
OnTouchTestHit(const Offset & coordinateOffset,const TouchRestrict & touchRestrict,TouchTestResult & result)155 void RenderPopupList::OnTouchTestHit(
156     const Offset& coordinateOffset, const TouchRestrict& touchRestrict, TouchTestResult& result)
157 {
158     if (!GetVisible()) {
159         return;
160     }
161 
162     // Disable scroll while expand all items
163     if (!scrollable_ || !scrollable_->Available()) {
164         LOGE("[popup list] OnTouchTestHit scrollable_ not available");
165         return;
166     }
167 
168     scrollable_->SetCoordinateOffset(coordinateOffset);
169     result.emplace_back(scrollable_);
170 }
171 
OnRequestPopupDataSelected(std::vector<std::string> & data)172 void RenderPopupList::OnRequestPopupDataSelected(std::vector<std::string>& data)
173 {
174     if (!GetChildren().empty()) {
175         ClearChildren();
176     }
177     items_.clear();
178     datas_.clear();
179 
180     // load string data
181     datas_.assign(data.begin(), data.end());
182 
183     // calculate view port size
184     viewPortWidth_ = PipelineBase::Vp2PxWithCurrentDensity(POPUP_BOX_SIZE);
185     viewPortHeight_ = ApplyLayoutParam();
186 
187     LOGI("size of datas %{public}zu view port height %{public}.2f ", datas_.size(), viewPortHeight_);
188 
189     // create render node
190     if (data.size() == 0) {
191         boxComponent_->SetWidth(POPUP_ZERO_SIZE);
192         boxComponent_->SetHeight(POPUP_ZERO_SIZE);
193     } else {
194         boxComponent_->SetWidth(viewPortWidth_);
195         boxComponent_->SetHeight(viewPortHeight_);
196     }
197     if (!renderBox_) {
198         renderBox_ = AceType::DynamicCast<RenderBox>(RenderBox::Create());
199     }
200     AddChild(renderBox_);
201     renderBox_->Attach(GetContext());
202     renderBox_->Update(boxComponent_);
203     renderBox_->PerformLayout();
204     renderBox_->MarkNeedRender();
205     MarkNeedLayout();
206 }
207 
OnPopupSelected(int32_t selected) const208 void RenderPopupList::OnPopupSelected(int32_t selected) const
209 {
210     if (popupSelectedEventFun_) {
211         auto event = std::make_shared<IndexerEventInfo>(selected);
212         if (event) {
213             popupSelectedEventFun_(event);
214         }
215     }
216 }
217 
CalTouchPoint(const Point & globalPoint,int32_t & selected)218 void RenderPopupList::CalTouchPoint(const Point& globalPoint, int32_t& selected)
219 {
220     selected = INVALID_POPUP_SELECTED;
221     std::list<RefPtr<RenderPopupListItem>>::iterator iter;
222     int32_t index = 0;
223     for (iter = items_.begin(); iter != items_.end(); ++iter) {
224         auto renderItem = *iter;
225         std::unique_ptr<Rect> rect = std::make_unique<Rect>(renderItem->GetGlobalOffset(),
226                                         renderItem->GetLayoutSize());
227         if (rect->IsInRegion(globalPoint)) {
228             selected = index + static_cast<int32_t>(startIndex_);
229             renderItem->UpdateBoxSelected();
230         } else {
231             renderItem->UpdateBoxNormal();
232         }
233         index++;
234     }
235 }
236 
ApplyLayoutParam()237 double RenderPopupList::ApplyLayoutParam()
238 {
239     double viewPortSize = 0.0;
240     if (datas_.size() > POPUP_ITEM_VIEW_MAX_COUNT) {
241         viewPortSize = PipelineBase::Vp2PxWithCurrentDensity(POPUP_BOX_SIZE) *
242                        POPUP_ITEM_VIEW_MAX_COUNT + POPUP_BORDER_RADIUS_SIZE;
243     } else {
244         viewPortSize = PipelineBase::Vp2PxWithCurrentDensity(POPUP_BOX_SIZE) * datas_.size() + POPUP_BORDER_RADIUS_SIZE;
245     }
246 
247     // start position and end position of scrollable
248     startMainPos_ = -viewPortSize;
249     endMainPos_ = startMainPos_ + (viewPortSize * VIEW_PORT_SCALE);
250 
251     return viewPortSize;
252 }
253 
LayoutOrRecycleCurrentItems(const LayoutParam & layoutParam)254 double RenderPopupList::LayoutOrRecycleCurrentItems(const LayoutParam& layoutParam)
255 {
256     double curMainPos = currentOffset_;
257     size_t curIndex = startIndex_;
258     for (auto it = items_.begin(); it != items_.end(); ++curIndex) {
259         const auto& child = *(it);
260         if (LessOrEqual(curMainPos, endMainPos_)) {
261             child->Layout(layoutParam);
262             curMainPos += child->GetLayoutSize().Height();
263             if (GreatOrEqual(curMainPos, startMainPos_)) {
264                 ++it;
265                 continue;
266             }
267             currentOffset_ = curMainPos;
268             startIndex_ = curIndex + 1;
269         }
270 
271         it = items_.erase(it);
272     }
273     return curMainPos;
274 }
275 
RequestAndLayoutNewItem(size_t index,const LayoutParam & layoutParam)276 RefPtr<RenderPopupListItem> RenderPopupList::RequestAndLayoutNewItem(size_t index, const LayoutParam& layoutParam)
277 {
278     if (index >= datas_.size()) {
279         return nullptr;
280     }
281 
282     RefPtr<PopupListItemComponent> itemComponent = AceType::MakeRefPtr<PopupListItemComponent>(datas_[index]);
283     RefPtr<RenderPopupListItem> renderItem = AceType::DynamicCast<RenderPopupListItem>(RenderPopupListItem::Create());
284     AddChild(renderItem);
285     if (renderItem) {
286         renderItem->Attach(GetContext());
287         renderItem->Update(itemComponent);
288         renderItem->Layout(layoutParam);
289         if (index < startIndex_) {
290             items_.emplace_front(renderItem);
291         } else {
292             items_.emplace_back(renderItem);
293         }
294     }
295 
296     return renderItem;
297 }
298 
CalculateMainScrollExtent(double curMainPos)299 void RenderPopupList::CalculateMainScrollExtent(double curMainPos)
300 {
301     // check current is out of boundary
302     isOutOfBoundary_ = LessNotEqual(curMainPos, viewPortHeight_) || GreatNotEqual(currentOffset_, 0.0);
303     // content length
304     mainScrollExtent_ = curMainPos - currentOffset_;
305     // disable scroll when content length less than mainSize
306     if (scrollable_) {
307         scrollable_->MarkAvailable(GreatOrEqual(mainScrollExtent_, viewPortHeight_));
308     }
309 }
310 
SetItemsPosition()311 void RenderPopupList::SetItemsPosition()
312 {
313     double curMainPos = currentOffset_;
314 
315     for (const auto& child : items_) {
316         double childHeight = child->GetLayoutSize().Height();
317         child->SetPosition(Offset(0.0, curMainPos));
318         curMainPos += childHeight;
319     }
320 }
321 
GetCurrentPosition() const322 double RenderPopupList::GetCurrentPosition() const
323 {
324     return currentOffset_;
325 }
326 
IsOutOfBoundary() const327 bool RenderPopupList::IsOutOfBoundary() const
328 {
329     return isOutOfBoundary_;
330 }
331 
AdjustOffset(Offset & delta,int32_t source)332 void RenderPopupList::AdjustOffset(Offset& delta, int32_t source)
333 {
334     // when scrollEffect equal to none, no need to adjust offset
335     if (!scrollEffect_) {
336         return;
337     }
338 
339     if (delta.IsZero() || source == SCROLL_FROM_ANIMATION || source == SCROLL_FROM_ANIMATION_SPRING) {
340         return;
341     }
342 
343     double offset = delta.GetY();
344     if (NearZero(viewPortHeight_) || NearZero(offset)) {
345         return;
346     }
347 
348     double maxScrollExtent = mainScrollExtent_ - viewPortHeight_;
349     double overscrollPastStart = 0.0;
350     double overscrollPastEnd = 0.0;
351     double overscrollPast = 0.0;
352     bool easing = false;
353 
354     overscrollPastStart = std::max(GetCurrentPosition(), 0.0);
355     overscrollPastEnd = std::max(-GetCurrentPosition() - maxScrollExtent, 0.0);
356     // do not adjust offset if direction opposite from the overScroll direction when out of boundary
357     if ((overscrollPastStart > 0.0 && offset < 0.0) || (overscrollPastEnd > 0.0 && offset > 0.0)) {
358         return;
359     }
360     easing = (overscrollPastStart > 0.0 && offset > 0.0) || (overscrollPastEnd > 0.0 && offset < 0.0);
361 
362     overscrollPast = std::max(overscrollPastStart, overscrollPastEnd);
363     double friction = easing ? RenderScroll::CalculateFriction((overscrollPast - std::abs(offset)) / viewPortHeight_)
364                              : RenderScroll::CalculateFriction(overscrollPast / viewPortHeight_);
365     double direction = offset / std::abs(offset);
366     offset = direction * RenderScroll::CalculateOffsetByFriction(overscrollPast, std::abs(offset), friction);
367     // set delta after adjust
368     delta.SetY(offset);
369 }
370 
UpdateScrollPosition(double offset,int32_t source)371 bool RenderPopupList::UpdateScrollPosition(double offset, int32_t source)
372 {
373     if (source == SCROLL_FROM_START) {
374         return true;
375     }
376 
377     if (NearZero(offset)) {
378         return true;
379     }
380 
381     if (reachStart_ && reachEnd_) {
382         return false;
383     }
384 
385     if (offset > 0.0) {
386         if (reachStart_ && !scrollEffect_) {
387             return false;
388         }
389         reachEnd_ = false;
390     } else {
391         if (reachEnd_ && !scrollEffect_) {
392             return false;
393         }
394         reachStart_ = false;
395     }
396 
397     currentOffset_ += offset;
398     MarkNeedLayout(true);
399     return true;
400 }
401 
ResetEdgeEffect()402 void RenderPopupList::ResetEdgeEffect()
403 {
404     if (!scrollEffect_) {
405         LOGE("ResetEdgeEffect failed, scrollEffect_ is nullptr");
406         return;
407     }
408 
409     scrollEffect_->SetCurrentPositionCallback([weak = AceType::WeakClaim(this)]() {
410         auto popupList = weak.Upgrade();
411         if (popupList) {
412             return popupList->GetCurrentPosition();
413         }
414         return 0.0;
415     });
416     scrollEffect_->SetLeadingCallback([weak = AceType::WeakClaim(this)]() {
417         auto popupList = weak.Upgrade();
418         if (popupList) {
419             return popupList->GetLayoutSize().Height() - popupList->mainScrollExtent_;
420         }
421         return 0.0;
422     });
423 
424     scrollEffect_->SetTrailingCallback([weak = AceType::WeakClaim(this)]() { return 0.0; });
425     scrollEffect_->SetInitLeadingCallback([weak = AceType::WeakClaim(this)]() {
426         auto popupList = weak.Upgrade();
427         if (popupList) {
428             return popupList->GetLayoutSize().Height() - popupList->mainScrollExtent_;
429         }
430         return 0.0;
431     });
432     scrollEffect_->SetInitTrailingCallback([weak = AceType::WeakClaim(this)]() { return 0.0; });
433     scrollEffect_->SetScrollNode(AceType::WeakClaim(this));
434     SetEdgeEffectAttribute();
435     scrollEffect_->InitialEdgeEffect();
436 }
437 
SetEdgeEffectAttribute()438 void RenderPopupList::SetEdgeEffectAttribute()
439 {
440     if (scrollEffect_ && scrollable_) {
441         scrollEffect_->SetScrollable(scrollable_);
442         scrollEffect_->RegisterSpringCallback();
443         if (scrollEffect_->IsSpringEffect()) {
444             scrollable_->SetOutBoundaryCallback([weakScroll = AceType::WeakClaim(this)]() {
445                 auto scroll = weakScroll.Upgrade();
446                 if (scroll) {
447                     return scroll->IsOutOfBoundary();
448                 }
449                 return false;
450             });
451         }
452     }
453 }
454 } // namespace OHOS::Ace::V2
455