1 /*
2 * Copyright (c) 2021 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/bubble/render_bubble.h"
17
18 #include "base/geometry/offset.h"
19 #include "base/log/event_report.h"
20 #include "base/utils/string_utils.h"
21 #include "base/utils/system_properties.h"
22 #include "core/accessibility/accessibility_utils.h"
23 #include "core/components/box/box_component.h"
24 #include "core/components/box/render_box.h"
25 #include "core/components/bubble/bubble_element.h"
26 #include "core/components/stack/stack_element.h"
27 #include "core/event/ace_event_helper.h"
28 #include "core/pipeline/base/composed_element.h"
29
30 namespace OHOS::Ace {
31 namespace {
32
33 constexpr Dimension ARROW_WIDTH = 32.0_vp;
34 constexpr Dimension GRID_MARGIN_PORTRAIT = 48.0_vp;
35 constexpr Dimension GRID_SPACING = 24.0_vp;
36 constexpr Dimension GRID_SPACING_TOTAL = 232.0_vp;
37 constexpr Dimension HORIZON_SPACING_WITH_SCREEN = 6.0_vp;
38 constexpr int32_t GRID_NUMBER_LANDSCAPE = 8;
39 constexpr int32_t BUBBLR_GRID_MAX_LANDSCAPE = 6;
40
41 } // namespace
42
43 const Dimension RenderBubble::BUBBLE_SPACING(8.0, DimensionUnit::VP);
44
RenderBubble()45 RenderBubble::RenderBubble()
46 {
47 rawDetector_ = AceType::MakeRefPtr<RawRecognizer>();
48 rawDetector_->SetOnTouchDown([weak = WeakClaim(this)](const TouchEventInfo& info) {
49 auto bubble = weak.Upgrade();
50 if (bubble) {
51 bubble->HandleTouch();
52 }
53 });
54 }
55
Update(const RefPtr<Component> & component)56 void RenderBubble::Update(const RefPtr<Component>& component)
57 {
58 const auto bubble = AceType::DynamicCast<BubbleComponent>(component);
59 if (!bubble) {
60 LOGE("RenderBubble update with nullptr");
61 EventReport::SendRenderException(RenderExcepType::RENDER_COMPONENT_ERR);
62 return;
63 }
64 if (!bubble->GetPopupParam()) {
65 return;
66 }
67 bubbleComponent_ = bubble;
68 maskColor_ = bubble->GetPopupParam()->GetMaskColor();
69 backgroundColor_ = bubble->GetPopupParam()->GetBackgroundColor();
70 placement_ = bubble->GetPopupParam()->GetPlacement();
71 onVisibilityChange_ =
72 AceAsyncEvent<void(const std::string&)>::Create(bubble->GetPopupParam()->GetOnVisibilityChange(), context_);
73 isShow_ = bubble->GetPopupParam()->IsShow();
74 enableArrow_ = bubble->GetPopupParam()->EnableArrow();
75 padding_ = bubble->GetPopupParam()->GetPadding();
76 margin_ = bubble->GetPopupParam()->GetMargin();
77 border_ = bubble->GetPopupParam()->GetBorder();
78 arrowOffset_ = bubble->GetPopupParam()->GetArrowOffset();
79 targetId_ = bubble->GetPopupParam()->GetTargetId();
80 weakStack_ = bubble->GetWeakStack();
81 useCustom_ = bubble->GetPopupParam()->IsUseCustom();
82 SetDisableTouchEvent(bubble->IsDisabledStatus());
83 SetInterceptTouchEvent(bubbleComponent_->GetPopupParam()->HasAction() || bubble->IsDisabledStatus());
84
85 // When app is hide and there is no button in popup, pop popup.
86 auto context = context_.Upgrade();
87 if (context) {
88 context->SetPopupEventHandler([weak = WeakClaim(this)] {
89 auto bubble = weak.Upgrade();
90 if (bubble) {
91 auto bubbleComponent = bubble->bubbleComponent_;
92 if (bubbleComponent && !bubbleComponent->GetPopupParam()->HasAction()) {
93 bubble->PopBubble();
94 }
95 }
96 });
97 }
98
99 MarkNeedLayout();
100 }
101
UpdateAccessibilityInfo(Size size,Offset offset)102 void RenderBubble::UpdateAccessibilityInfo(Size size, Offset offset)
103 {
104 if (!bubbleComponent_) {
105 return;
106 }
107 auto context = context_.Upgrade();
108 if (!context) {
109 LOGE("RenderBubble context is null");
110 return;
111 }
112 auto viewScale = context->GetViewScale();
113 if (NearZero(viewScale)) {
114 LOGE("RenderBubble viewScale is zero.");
115 return;
116 }
117 auto accessibilityManager = context->GetAccessibilityManager();
118 if (!accessibilityManager) {
119 LOGE("RenderBubble accessibilityManager is null");
120 return;
121 }
122 auto nodeId = StringUtils::StringToInt(bubbleComponent_->GetId());
123 auto accessibilityNode = accessibilityManager->GetAccessibilityNodeById(nodeId);
124 if (!accessibilityNode) {
125 LOGE("RenderBubble accessibilityNode is null.");
126 return;
127 }
128 accessibilityNode->SetWidth((size.Width()) * viewScale);
129 accessibilityNode->SetHeight((size.Height()) * viewScale);
130 accessibilityNode->SetLeft((offset.GetX()) * viewScale);
131 accessibilityNode->SetTop((offset.GetY()) * viewScale);
132 accessibilityNode->SetLongClickableState(true);
133 accessibilityNode->SetClickableState(false);
134
135 accessibilityNode->AddSupportAction(AceAction::ACTION_LONG_CLICK);
136 accessibilityNode->SetActionLongClickImpl([weakPtr = WeakClaim(this)]() {
137 const auto& bubble = weakPtr.Upgrade();
138 if (bubble) {
139 bubble->PopBubble();
140 }
141 });
142 }
143
PerformLayout()144 void RenderBubble::PerformLayout()
145 {
146 InitTargetSizeAndPosition();
147 SetLayoutSize(GetLayoutParam().GetMaxSize());
148 LayoutParam innerLayout = GetLayoutParam();
149 if (!useCustom_) {
150 if (SystemProperties::GetDevcieOrientation() == DeviceOrientation::PORTRAIT) {
151 innerLayout.SetMaxSize(Size(innerLayout.GetMaxSize().Width() - NormalizeToPx(GRID_MARGIN_PORTRAIT),
152 innerLayout.GetMaxSize().Height()));
153 } else {
154 static const int32_t gridGaps = 5;
155 double colWidth =
156 (innerLayout.GetMaxSize().Width() - NormalizeToPx(GRID_SPACING_TOTAL)) / GRID_NUMBER_LANDSCAPE;
157 innerLayout.SetMaxSize(Size(colWidth * BUBBLR_GRID_MAX_LANDSCAPE + NormalizeToPx(GRID_SPACING) * gridGaps,
158 innerLayout.GetMaxSize().Height()));
159 }
160 }
161 if (!GetChildren().empty()) {
162 const auto& child = GetChildren().front();
163 child->Layout(innerLayout);
164 childSize_ = child->GetLayoutSize();
165 UpdateBorderRadius();
166 childOffset_ = GetChildPosition(childSize_);
167 child->SetPosition(childOffset_);
168 UpdateAccessibilityInfo(childSize_, childOffset_);
169 }
170 }
171
InitTargetSizeAndPosition()172 void RenderBubble::InitTargetSizeAndPosition()
173 {
174 auto context = context_.Upgrade();
175 if (!context) {
176 return;
177 }
178 auto targetElement = context->GetComposedElementById(targetId_);
179 if (!targetElement) {
180 LOGE("Get target element by target id return null");
181 isShow_ = false;
182 return;
183 }
184 auto targetRender = targetElement->GetRenderNode();
185 if (!targetRender) {
186 return;
187 }
188 targetSize_ = targetRender->GetLayoutSize();
189 targetOffset_ = targetRender->GetOffsetToPage();
190 if (bubbleComponent_ && bubbleComponent_->GetPopupParam()) {
191 auto targetMargin = bubbleComponent_->GetPopupParam()->GetTargetMargin();
192 targetSize_ -= targetMargin.GetLayoutSizeInPx(context->GetDipScale());
193 targetOffset_ += targetMargin.GetOffsetInPx(context->GetDipScale());
194 }
195 }
196
InitArrowState()197 void RenderBubble::InitArrowState()
198 {
199 if (!enableArrow_) {
200 showTopArrow_ = false;
201 showBottomArrow_ = false;
202 return;
203 }
204
205 double arrowWidth = NormalizeToPx(ARROW_WIDTH);
206 showTopArrow_ = GreatOrEqual(
207 childSize_.Width() -
208 std::max(NormalizePercentToPx(padding_.Left(), false), NormalizeToPx(border_.TopLeftRadius().GetX())) -
209 std::max(NormalizePercentToPx(padding_.Right(), false), NormalizeToPx(border_.TopRightRadius().GetX())),
210 arrowWidth);
211 showBottomArrow_ = GreatOrEqual(
212 childSize_.Width() -
213 std::max(NormalizePercentToPx(padding_.Left(), false), NormalizeToPx(border_.BottomLeftRadius().GetX())) -
214 std::max(NormalizePercentToPx(padding_.Right(), false), NormalizeToPx(border_.BottomRightRadius().GetX())),
215 arrowWidth);
216 }
217
GetChildPosition(const Size & childSize)218 Offset RenderBubble::GetChildPosition(const Size& childSize)
219 {
220 InitArrowState();
221 double scaledBubbleSpacing = NormalizeToPx(BUBBLE_SPACING);
222 Offset bottomPosition = Offset(targetOffset_.GetX() + (targetSize_.Width() - childSize.Width()) / 2.0,
223 targetOffset_.GetY() + targetSize_.Height() + scaledBubbleSpacing + NormalizePercentToPx(margin_.Top(), true));
224 if (showBottomArrow_) {
225 bottomPosition += Offset(0.0, scaledBubbleSpacing);
226 }
227 Offset topPosition = Offset(targetOffset_.GetX() + (targetSize_.Width() - childSize.Width()) / 2.0,
228 targetOffset_.GetY() - childSize.Height() - scaledBubbleSpacing - NormalizePercentToPx(margin_.Bottom(), true));
229 if (showTopArrow_) {
230 topPosition += Offset(0.0, -scaledBubbleSpacing);
231 }
232 Offset topArrowPosition = Offset(targetOffset_.GetX() + targetSize_.Width() / 2.0,
233 targetOffset_.GetY() - scaledBubbleSpacing - NormalizePercentToPx(margin_.Bottom(), true));
234 Offset bottomArrowPosition = Offset(targetOffset_.GetX() + targetSize_.Width() / 2.0,
235 targetOffset_.GetY() + targetSize_.Height() + scaledBubbleSpacing + NormalizePercentToPx(margin_.Top(), true));
236 Offset originOffset =
237 GetPositionWithPlacement(childSize, topPosition, bottomPosition, topArrowPosition, bottomArrowPosition);
238 Offset childPosition = originOffset;
239 arrowPlacement_ = placement_;
240
241 // Fit popup to screen range.
242 ErrorPositionType errorType = GetErrorPositionType(childPosition, childSize);
243 if (errorType == ErrorPositionType::NORMAL) {
244 return childPosition;
245 }
246 // If childPosition is error, adjust bubble to bottom.
247 if (placement_ != Placement::TOP || errorType == ErrorPositionType::TOP_LEFT_ERROR) {
248 childPosition = FitToScreen(bottomPosition, childSize);
249 arrowPosition_ = bottomArrowPosition + (childPosition - bottomPosition);
250 arrowPlacement_ = Placement::BOTTOM;
251 if (GetErrorPositionType(childPosition, childSize) == ErrorPositionType::NORMAL) {
252 return childPosition;
253 }
254 }
255 // If childPosition is error, adjust bubble to top.
256 childPosition = FitToScreen(topPosition, childSize);
257 arrowPosition_ = topArrowPosition + (childPosition - topPosition);
258 arrowPlacement_ = Placement::TOP;
259 if (GetErrorPositionType(childPosition, childSize) == ErrorPositionType::NORMAL) {
260 return childPosition;
261 }
262 // If childPosition is error, adjust bubble to origin position.
263 arrowPlacement_ = placement_;
264 arrowPosition_ = arrowPlacement_ == Placement::TOP ? topArrowPosition : bottomArrowPosition;
265 return originOffset;
266 }
267
GetPositionWithPlacement(const Size & childSize,const Offset & topPosition,const Offset & bottomPosition,const Offset & topArrowPosition,const Offset & bottomArrowPosition)268 Offset RenderBubble::GetPositionWithPlacement(const Size& childSize, const Offset& topPosition,
269 const Offset& bottomPosition, const Offset& topArrowPosition, const Offset& bottomArrowPosition)
270 {
271 Offset childPosition;
272 double bubbleSpacing = NormalizeToPx(BUBBLE_SPACING);
273 switch (placement_) {
274 case Placement::LEFT:
275 childPosition = Offset(
276 targetOffset_.GetX() - childSize.Width() - bubbleSpacing - NormalizePercentToPx(margin_.Right(), false),
277 targetOffset_.GetY() + targetSize_.Height() / 2.0 - childSize.Height() / 2.0);
278 break;
279 case Placement::RIGHT:
280 childPosition = Offset(targetOffset_.GetX() + targetSize_.Width() + bubbleSpacing +
281 NormalizePercentToPx(margin_.Left(), false),
282 targetOffset_.GetY() + targetSize_.Height() / 2.0 - childSize.Height() / 2.0);
283 break;
284 case Placement::TOP:
285 childPosition = topPosition;
286 arrowPosition_ = topArrowPosition;
287 break;
288 case Placement::BOTTOM:
289 childPosition = bottomPosition;
290 arrowPosition_ = bottomArrowPosition;
291 break;
292 case Placement::TOP_LEFT:
293 childPosition =
294 Offset(targetOffset_.GetX() - childSize.Width() - NormalizePercentToPx(margin_.Right(), false),
295 targetOffset_.GetY() - childSize.Height() - NormalizePercentToPx(margin_.Bottom(), true));
296 break;
297 case Placement::TOP_RIGHT:
298 childPosition =
299 Offset(targetOffset_.GetX() + targetSize_.Width() + NormalizePercentToPx(margin_.Left(), false),
300 targetOffset_.GetY() - childSize.Height() - NormalizePercentToPx(margin_.Bottom(), true));
301 break;
302 case Placement::BOTTOM_LEFT:
303 childPosition =
304 Offset(targetOffset_.GetX() - childSize.Width() - NormalizePercentToPx(margin_.Right(), false),
305 targetOffset_.GetY() + targetSize_.Height() + NormalizePercentToPx(margin_.Top(), true));
306 break;
307 case Placement::BOTTOM_RIGHT:
308 childPosition =
309 Offset(targetOffset_.GetX() + targetSize_.Width() + NormalizePercentToPx(margin_.Left(), false),
310 targetOffset_.GetY() + targetSize_.Height() + NormalizePercentToPx(margin_.Top(), true));
311 break;
312 default:
313 break;
314 }
315 return childPosition;
316 }
317
FitToScreen(const Offset & fitPosition,const Size & childSize)318 Offset RenderBubble::FitToScreen(const Offset& fitPosition, const Size& childSize)
319 {
320 auto validation = GetErrorPositionType(fitPosition, childSize);
321 if (validation == ErrorPositionType::NORMAL) {
322 return fitPosition;
323 }
324 Offset childPosition = fitPosition;
325 double horizonSpacing = NormalizeToPx(HORIZON_SPACING_WITH_SCREEN);
326 if (validation == ErrorPositionType::TOP_LEFT_ERROR) {
327 childPosition.SetX(horizonSpacing);
328 } else {
329 childPosition.SetX(GetLayoutSize().Width() - childSize.Width() - horizonSpacing);
330 }
331 return childPosition;
332 }
333
GetErrorPositionType(const Offset & childOffset,const Size & childSize)334 RenderBubble::ErrorPositionType RenderBubble::GetErrorPositionType(const Offset& childOffset, const Size& childSize)
335 {
336 double horizonSpacing = NormalizeToPx(HORIZON_SPACING_WITH_SCREEN);
337 TouchRegion validRegion = TouchRegion(
338 Offset(horizonSpacing, 0.0), Offset(GetLayoutSize().Width() - horizonSpacing, GetLayoutSize().Height()));
339 if (!validRegion.ContainsInRegion(childOffset.GetX(), childOffset.GetY())) {
340 return ErrorPositionType::TOP_LEFT_ERROR;
341 }
342 if (!validRegion.ContainsInRegion(
343 childOffset.GetX() + childSize.Width(), childOffset.GetY() + childSize.Height())) {
344 return ErrorPositionType::BOTTOM_RIGHT_ERROR;
345 }
346 return ErrorPositionType::NORMAL;
347 }
348
OnHiddenChanged(bool hidden)349 void RenderBubble::OnHiddenChanged(bool hidden)
350 {
351 if (!bubbleComponent_ || !bubbleComponent_->GetPopupParam()) {
352 return;
353 }
354 // When page is hidden and there is no button in popup, pop bubble.
355 if (hidden && !bubbleComponent_->GetPopupParam()->HasAction()) {
356 PopBubble();
357 }
358 }
359
HandleTouch()360 void RenderBubble::HandleTouch()
361 {
362 if (!bubbleComponent_ || !bubbleComponent_->GetPopupParam()) {
363 return;
364 }
365 if (!bubbleComponent_->GetPopupParam()->HasAction()) {
366 PopBubble();
367 UpdateAccessibilityInfo(Size(), Offset());
368 }
369 }
370
OnTouchTestHit(const Offset & coordinateOffset,const TouchRestrict & touchRestrict,TouchTestResult & result)371 void RenderBubble::OnTouchTestHit(
372 const Offset& coordinateOffset, const TouchRestrict& touchRestrict, TouchTestResult& result)
373 {
374 rawDetector_->SetCoordinateOffset(coordinateOffset);
375 result.emplace_back(rawDetector_);
376 }
377
PopBubble()378 bool RenderBubble::PopBubble()
379 {
380 auto stackElement = weakStack_.Upgrade();
381 if (!stackElement) {
382 return false;
383 }
384 stackElement->PopPopup(bubbleComponent_->GetId());
385 auto stateChangeEvent = bubbleComponent_->GetStateChangeEvent();
386 if (stateChangeEvent) {
387 stateChangeEvent(false);
388 }
389
390 auto context = context_.Upgrade();
391 if (!context) {
392 return false;
393 }
394 #if !defined(WINDOWS_PLATFORM) and !defined(MAC_PLATFORM)
395 const auto& accessibilityManager = context->GetAccessibilityManager();
396 if (accessibilityManager) {
397 accessibilityManager->RemoveAccessibilityNodeById(StringUtils::StringToInt(bubbleComponent_->GetId()));
398 }
399 #else
400 const auto& accessibilityManager = context->GetAccessibilityManager();
401 if (accessibilityManager) {
402 auto bubbleNodeId = StringUtils::StringToInt(bubbleComponent_->GetId());
403 auto node = accessibilityManager->GetAccessibilityNodeById(bubbleNodeId);
404 if (node) {
405 auto children = node->GetChildList();
406 for (auto& child : children) {
407 child->SetVisible(false);
408 child->ClearRect();
409 }
410 }
411 }
412 #endif
413 return true;
414 }
415
FirePopEvent()416 void RenderBubble::FirePopEvent()
417 {
418 if (onVisibilityChange_) {
419 std::string param = std::string("\"visibilitychange\",{\"visibility\":").append("false}");
420 onVisibilityChange_(param);
421 }
422 }
423
HandleMouseEvent(const MouseEvent & event)424 bool RenderBubble::HandleMouseEvent(const MouseEvent& event)
425 {
426 if (event.button != MouseButton::NONE_BUTTON && event.button != MouseButton::LEFT_BUTTON &&
427 event.action == MouseAction::PRESS) {
428 HandleTouch();
429 }
430 return true;
431 }
432
433 } // namespace OHOS::Ace