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 <cstdlib>
17 #include <cmath>
18 #include "core/components/checkable/render_switch.h"
19
20 #include "base/log/event_report.h"
21 #include "core/common/font_manager.h"
22 #include "core/components/common/properties/alignment.h"
23 #include "core/components/text/text_component.h"
24
25 namespace OHOS::Ace {
26 namespace {
27
28 #ifdef WEARABLE_PRODUCT
29 constexpr int32_t DEFAULT_SWITCH_ANIMATION_DURATION = 250;
30 #else
31 constexpr int32_t DEFAULT_SWITCH_ANIMATION_DURATION = 450;
32 #endif
33 constexpr Dimension DEFAULT_SWITCH_POINT_PADDING = 2.0_vp;
34
35 } // namespace
36
~RenderSwitch()37 RenderSwitch::~RenderSwitch()
38 {
39 #ifndef WEARABLE_PRODUCT
40 UnSubscribeMultiModal();
41 #endif
42 auto context = context_.Upgrade();
43 if (context) {
44 context->RemoveFontNode(WeakClaim(this));
45 auto fontManager = context->GetFontManager();
46 if (fontManager) {
47 fontManager->RemoveVariationNode(WeakClaim(this));
48 }
49 }
50 }
51
Update(const RefPtr<Component> & component)52 void RenderSwitch::Update(const RefPtr<Component>& component)
53 {
54 RenderCheckable::Update(component);
55 auto switchComponent = AceType::DynamicCast<SwitchComponent>(component);
56 if (!switchComponent) {
57 LOGE("cast to switch component failed");
58 EventReport::SendRenderException(RenderExcepType::RENDER_COMPONENT_ERR);
59 return;
60 }
61 component_ = switchComponent;
62 showText_ = switchComponent->GetShowText();
63 if (showText_) {
64 if (!renderTextOn_ || !renderTextOff_) {
65 InitRenderText();
66 }
67 UpdateRenderText(switchComponent);
68 }
69
70 if (!controller_) {
71 controller_ = AceType::MakeRefPtr<Animator>(GetContext());
72 auto weak = AceType::WeakClaim(this);
73 controller_->AddStopListener(Animator::StatusCallback([weak]() {
74 auto switchComponent = weak.Upgrade();
75 if (switchComponent) {
76 switchComponent->OnAnimationStop();
77 }
78 }));
79 }
80 backgroundSolid_ = switchComponent->IsBackgroundSolid();
81 pointPadding_ = DEFAULT_SWITCH_POINT_PADDING;
82
83 if (switchComponent->GetUpdateType() == UpdateType::ALL) {
84 checked_ = switchComponent->GetValue();
85 }
86
87 ApplyRestoreInfo();
88
89 oldChecked_ = checked_;
90 needReverse_ = (switchComponent->GetTextDirection() == TextDirection::RTL);
91 auto theme = GetTheme<SwitchTheme>();
92 if (theme) {
93 borderWidth_ = theme->GetBorderWidth();
94 }
95 if (!isDraging && !controller_->IsRunning()) {
96 UpdateUIStatus();
97 }
98 HandleDrag();
99 UpdateAccessibilityAttr();
100 #ifndef WEARABLE_PRODUCT
101 if (!component_->GetMultimodalProperties().IsUnavailable()) {
102 PrepareMultiModalEvent();
103 SubscribeMultiModal();
104 }
105 #endif
106 SetAccessibilityClickImpl();
107 }
108
UpdateAccessibilityAttr()109 void RenderSwitch::UpdateAccessibilityAttr()
110 {
111 std::string text;
112 if (component_ && showText_) {
113 if (checked_) {
114 text = textOn_;
115 } else {
116 text = textOff_;
117 }
118 }
119 auto accessibilityNode = GetAccessibilityNode().Upgrade();
120 if (!accessibilityNode) {
121 return;
122 }
123 accessibilityNode->SetText(text);
124 accessibilityNode->SetCheckedState(checked_);
125 if (accessibilityNode->GetClicked()) {
126 accessibilityNode->SetClicked(false);
127 auto context = context_.Upgrade();
128 if (context) {
129 AccessibilityEvent switchEvent;
130 switchEvent.nodeId = accessibilityNode->GetNodeId();
131 switchEvent.eventType = "click";
132 context->SendEventToAccessibility(switchEvent);
133 }
134 }
135 }
136
InitRenderText()137 void RenderSwitch::InitRenderText()
138 {
139 LayoutParam innerLayout;
140 innerLayout.SetMaxSize(Size(Size::INFINITE_SIZE, Size::INFINITE_SIZE));
141
142 textOnComponent_ = AceType::MakeRefPtr<TextComponent>(textOn_);
143 renderTextOn_ = AceType::DynamicCast<RenderText>(textOnComponent_->CreateRenderNode());
144 renderTextOn_->Attach(GetContext());
145 renderTextOn_->Update(textOnComponent_);
146 renderTextOn_->SetLayoutParam(innerLayout);
147
148 textOffComponent_ = AceType::MakeRefPtr<TextComponent>(textOff_);
149 renderTextOff_ = AceType::DynamicCast<RenderText>(textOffComponent_->CreateRenderNode());
150 renderTextOff_->Attach(GetContext());
151 renderTextOff_->Update(textOffComponent_);
152 renderTextOff_->SetLayoutParam(innerLayout);
153
154 auto context = context_.Upgrade();
155 if (context) {
156 auto fontManager = context->GetFontManager();
157 if (fontManager) {
158 fontManager->AddVariationNode(WeakClaim(this));
159 }
160 }
161 }
162
HandleDrag()163 void RenderSwitch::HandleDrag()
164 {
165 if (!disabled_ && !dragRecognizer_) {
166 dragRecognizer_ = AceType::MakeRefPtr<HorizontalDragRecognizer>();
167 dragRecognizer_->SetOnDragStart([weak = AceType::WeakClaim(this)](const DragStartInfo& info) {
168 auto renderSwitch = weak.Upgrade();
169 if (renderSwitch) {
170 renderSwitch->HandleDragStart(info.GetLocalLocation());
171 }
172 });
173 dragRecognizer_->SetOnDragUpdate([weak = AceType::WeakClaim(this)](const DragUpdateInfo& info) {
174 auto renderSwitch = weak.Upgrade();
175 if (renderSwitch) {
176 renderSwitch->HandleDragUpdate(info.GetLocalLocation());
177 }
178 });
179 dragRecognizer_->SetOnDragEnd([weak = AceType::WeakClaim(this)](const DragEndInfo& info) {
180 auto renderSwitch = weak.Upgrade();
181 if (renderSwitch) {
182 renderSwitch->HandleDragEnd(info.GetLocalLocation());
183 }
184 });
185 } else if (disabled_ && dragRecognizer_) {
186 dragRecognizer_ = nullptr;
187 }
188 }
189
PerformLayout()190 void RenderSwitch::PerformLayout()
191 {
192 if (showText_) {
193 width_ = NormalizeToPx(defaultWidth_);
194 height_ = NormalizeToPx(defaultHeight_);
195 textOnSize_ = CalculateTextSize(textOn_, renderTextOn_);
196 textOffSize_ = CalculateTextSize(textOff_, renderTextOff_);
197 } else {
198 RenderCheckable::InitSize();
199 }
200 RenderCheckable::CalculateSize();
201 double maxTextWidth = std::max(textOnSize_.Width(), textOffSize_.Width());
202 double pointHeight = std::max(textOnSize_.Height(), drawSize_.Height());
203 auto pointTextPadding = NormalizeToPx(pointTextPadding_);
204 rawPointSize_ = Size(std::max(maxTextWidth + pointTextPadding * 2, pointHeight), pointHeight);
205 bool pointUseTextHeight = (rawPointSize_.Height() > drawSize_.Height());
206 bool pointUseHeightAsWidth = (rawPointSize_.Width() == drawSize_.Height());
207 auto pointPadding = NormalizeToPx(pointPadding_);
208 if (showText_) {
209 if (pointUseTextHeight) {
210 pointHeight += pointPadding * 2.0;
211 }
212 switchSize_ =
213 Size(pointUseHeightAsWidth ? drawSize_.Width() : (rawPointSize_.Width() + pointPadding) * aspectRatio_,
214 pointHeight);
215 pointRadius_ = pointHeight / 2.0 - pointPadding;
216 } else {
217 switchSize_ = drawSize_;
218 pointRadius_ = drawSize_.Height() / 2.0 - pointPadding;
219 }
220 rawPointSize_ -=
221 Size(pointUseHeightAsWidth ? pointPadding * 2.0 : 0.0, pointUseTextHeight ? 0.0 : pointPadding * 2.0);
222
223 Size layoutSize;
224 if (switchSize_.Width() > width_) {
225 layoutSize = GetLayoutParam().Constrain(Size(switchSize_.Width() + NormalizeToPx(hotZoneHorizontalPadding_) * 2,
226 std::max(switchSize_.Height(), height_) + NormalizeToPx(hotZoneVerticalPadding_) * 2));
227 } else {
228 layoutSize = GetLayoutParam().Constrain(Size(width_, height_));
229 }
230 SetLayoutSize(layoutSize);
231 double widthOverflow = layoutSize.Width() - switchSize_.Width();
232 paintPosition_ = Alignment::GetAlignPosition(layoutSize, switchSize_, Alignment::CENTER) +
233 Offset(widthOverflow > 0.0 ? 0.0 : widthOverflow - NormalizeToPx(hotZoneHorizontalPadding_), 0.0);
234 if (!isDraging && !controller_->IsRunning()) {
235 InitCurrentPointPosition();
236 }
237 UpdateAccessibilityPosition();
238 }
239
HandleDragStart(const Offset & updatePoint)240 void RenderSwitch::HandleDragStart(const Offset& updatePoint)
241 {
242 dragStartPosition_ = updatePoint.GetX();
243 oldChecked_ = checked_;
244 }
245
HandleDragUpdate(const OHOS::Ace::Offset & updatePoint)246 void RenderSwitch::HandleDragUpdate(const OHOS::Ace::Offset& updatePoint)
247 {
248 OnDrag(updatePoint);
249 MarkNeedRender();
250 }
251
HandleDragEnd(const OHOS::Ace::Offset & updatePoint)252 void RenderSwitch::HandleDragEnd(const OHOS::Ace::Offset& updatePoint)
253 {
254 OnDrag(updatePoint);
255 dragStartPosition_ = 0.0;
256 isDraging = false;
257 bool isNeedCallback = (oldChecked_ != checked_);
258 if (isNeedCallback && changeEvent_) {
259 std::string res = checked_ ? "true" : "false";
260 changeEvent_(std::string("\"change\",{\"checked\":").append(res.append("},null")));
261 }
262 oldChecked_ = checked_;
263 InitCurrentPointPosition();
264 UpdateUIStatus();
265 if (isNeedCallback && onChange_) {
266 onChange_(checked_);
267 }
268 auto accessibilityNode = accessibilityNode_.Upgrade();
269 if (accessibilityNode) {
270 accessibilityNode->SetCheckedState(checked_);
271 }
272 MarkNeedRender();
273 }
274
UpdateRenderText(const RefPtr<SwitchComponent> & switchComponent)275 void RenderSwitch::UpdateRenderText(const RefPtr<SwitchComponent>& switchComponent)
276 {
277 auto context = context_.Upgrade();
278 if (context) {
279 if (textStyle_.IsAllowScale() || textStyle_.GetFontSize().Unit() == DimensionUnit::FP) {
280 context->AddFontNode(AceType::WeakClaim(this));
281 }
282 }
283
284 textOn_ = switchComponent->GetTextOn();
285 textOff_ = switchComponent->GetTextOff();
286 textColorOn_ = switchComponent->GetTextColorOn();
287 textColorOff_ = switchComponent->GetTextColorOff();
288 pointTextPadding_ = switchComponent->GetTextPadding();
289
290 textStyle_ = switchComponent->GetTextStyle();
291 textStyle_.SetTextColor(textColorOn_);
292 textOnComponent_->SetData(textOn_);
293 textOnComponent_->SetTextStyle(textStyle_);
294 renderTextOn_->Update(textOnComponent_);
295
296 textStyle_.SetTextColor(textColorOff_);
297 textOffComponent_->SetData(textOff_);
298 textOffComponent_->SetTextStyle(textStyle_);
299 renderTextOff_->Update(textOffComponent_);
300 }
301
OnDrag(const Offset & updatePoint)302 void RenderSwitch::OnDrag(const Offset& updatePoint)
303 {
304 isDraging = true;
305 InitCurrentPointPosition();
306 auto pointPadding = NormalizeToPx(pointPadding_);
307 double pointTrackLength = switchSize_.Width() - rawPointSize_.Width() - pointPadding * 2;
308 double halfTrackLength = pointTrackLength / 2.0;
309 pointPositionDelta_ = updatePoint.GetX() - dragStartPosition_;
310 // let A = [needReverse_], B = [oldChecked_], C = [moveRight], D = [effectiveMove],
311 // and [A'] represents the reverse of [A].
312 // there are four situations in which the checked status needs to be reversed:
313 // 1. A'B'CD, 2. A'BC'D, 3. AB'C'D, 4. ABCD,
314 // so [needChangeStatus] = A'B'CD + ABCD + A'BC'D + AB'C'D = (A'B' + AB)CD + (A'B + AB')C'D
315 bool effectiveMove = std::abs(pointPositionDelta_) > halfTrackLength;
316 bool moveRight = pointPositionDelta_ > 0.0;
317 bool needChangeStatus = ((needReverse_ == oldChecked_) && moveRight && effectiveMove) ||
318 ((needReverse_ != oldChecked_) && !moveRight && effectiveMove);
319 checked_ = needChangeStatus != oldChecked_; // reverse [checked_] status if [needChangeStatus] is true
320
321 // let A = [needReverse_], B = [oldChecked_], C = [moveRight], D = [excessiveMove],
322 // and [A'] represents the reverse of [A].
323 // there are eight situations in which [pointPositionDelta_] needs to be constrained to zero:
324 // 1. A'B'C'D', 2. A'B'C'D, 3. ABC'D', 4. ABC'D, 5. A'BCD', 6. A'BCD, 7. AB'CD', 8. AB'CD,
325 // so [constrainDeltaToZero] = (A'B'C'D' + A'B'C'D) +( ABC'D' + ABC'D) + (A'BCD' + A'BCD) + (AB'CD' + AB'CD)
326 // = A'B'C' + ABC' + A'BC + AB'C = (A'B' + AB)C' + (A'B + AB')C
327 bool excessiveMove = std::abs(pointPositionDelta_) > pointTrackLength;
328 bool constrainDeltaToZero =
329 (moveRight && (needReverse_ != oldChecked_)) || (!moveRight && (needReverse_ == oldChecked_));
330 // there are four situations in which the absolute value of [pointPositionDelta_] needs to be constrained to
331 // trackLength: 1. A'BC'D, 2. AB'C'D, 3. ABCD, 4. A'B'CD,
332 // so [constrainDeltaToTrackLength] = A'BC'D + AB'C'D + ABCD + A'B'CD = (A'BC' + AB'C' + ABC + A'B'C)D
333 // = ((A'B + AB')C' + (AB + A'B')C)D
334 bool constrainDeltaToTrackLength = excessiveMove &&
335 ((!moveRight && (needReverse_ != oldChecked_)) || (moveRight && (needReverse_ == oldChecked_)));
336 double deltaCoefficient = moveRight ? 1.0 : -1.0;
337 if (constrainDeltaToZero) {
338 pointPositionDelta_ = 0.0;
339 } else if (constrainDeltaToTrackLength) {
340 pointPositionDelta_ = deltaCoefficient * pointTrackLength;
341 }
342 currentPointOriginX_ += pointPositionDelta_;
343 }
344
HandleClick()345 void RenderSwitch::HandleClick()
346 {
347 if (controller_->IsRunning()) {
348 controller_->Stop();
349 }
350 UpdateAnimation();
351 controller_->Play();
352
353 if (clickEvent_) {
354 clickEvent_();
355 }
356 }
357
PaintText(const Offset & textOffset,RenderContext & context) const358 void RenderSwitch::PaintText(const Offset& textOffset, RenderContext& context) const
359 {
360 auto paintRenderText = oldChecked_ ? renderTextOn_ : renderTextOff_;
361 auto textSize = paintRenderText->GetLayoutSize();
362 textSize.SetHeight(textSize.Height() > rawPointSize_.Height() ? rawPointSize_.Height() : textSize.Height());
363 auto textPos = Alignment::GetAlignPosition(rawPointSize_, textSize, Alignment::CENTER);
364 paintRenderText->Paint(context, textOffset + textPos);
365 }
366
OnAnimationStop()367 void RenderSwitch::OnAnimationStop()
368 {
369 // after the animation stopped,we need to update the check status
370 RenderCheckable::HandleClick();
371 UpdateAccessibilityAttr();
372 oldChecked_ = checked_;
373 InitCurrentPointPosition();
374 }
375
UpdateAnimation()376 void RenderSwitch::UpdateAnimation()
377 {
378 double from = 0.0;
379 double to = 0.0;
380 auto pointPadding = NormalizeToPx(pointPadding_);
381 if ((oldChecked_ && !needReverse_) || (!oldChecked_ && needReverse_)) {
382 from = switchSize_.Width() - pointPadding - rawPointSize_.Width();
383 to = pointPadding;
384 } else {
385 from = pointPadding;
386 to = switchSize_.Width() - pointPadding - rawPointSize_.Width();
387 }
388
389 if (translate_) {
390 controller_->RemoveInterpolator(translate_);
391 }
392 translate_ = AceType::MakeRefPtr<CurveAnimation<double>>(from, to, Curves::FRICTION);
393 auto weak = AceType::WeakClaim(this);
394 translate_->AddListener(Animation<double>::ValueCallback([weak](double value) {
395 auto switchComp = weak.Upgrade();
396 if (switchComp) {
397 Offset data(value, 0.0);
398 switchComp->UpdatePointPosition(data);
399 }
400 }));
401 controller_->SetDuration(DEFAULT_SWITCH_ANIMATION_DURATION);
402 controller_->AddInterpolator(translate_);
403 }
404
UpdatePointPosition(const Offset & updatePoint)405 void RenderSwitch::UpdatePointPosition(const Offset& updatePoint)
406 {
407 InitCurrentPointPosition();
408 auto pointPadding = NormalizeToPx(pointPadding_);
409 double pointTrackLength = switchSize_.Width() - rawPointSize_.Width() - pointPadding * 2;
410 double movingOffset = updatePoint.GetX();
411 if (movingOffset < 0.0) {
412 movingOffset = 0.0;
413 } else if (movingOffset > pointTrackLength) {
414 movingOffset = pointTrackLength;
415 }
416 if ((oldChecked_ && !needReverse_) || (!oldChecked_ && needReverse_)) {
417 currentPointOriginX_ = movingOffset;
418 uiStatus_ = needReverse_ ? UIStatus::OFF_TO_ON : UIStatus::ON_TO_OFF;
419 } else {
420 uiStatus_ = needReverse_ ? UIStatus::ON_TO_OFF : UIStatus::OFF_TO_ON;
421 currentPointOriginX_ += movingOffset;
422 }
423 MarkNeedRender();
424 }
425
426 #ifndef WEARABLE_PRODUCT
PrepareMultiModalEvent()427 void RenderSwitch::PrepareMultiModalEvent()
428 {
429 if (!multimodalSwitchEvent_) {
430 multimodalSwitchEvent_ = [weak = WeakClaim(this)](const AceMultimodalEvent& event) {
431 auto renderSwitch = weak.Upgrade();
432 static const int32_t switchOn = 1;
433 static const int32_t switchOff = 2;
434 if (renderSwitch) {
435 if (event.GetVoice().action == switchOn) {
436 renderSwitch->OpenSwitch();
437 } else if (event.GetVoice().action == switchOff) {
438 renderSwitch->CloseSwitch();
439 }
440 }
441 };
442 }
443 }
444
SubscribeMultiModal()445 bool RenderSwitch::SubscribeMultiModal()
446 {
447 if (isSubscribeMultimodal_) {
448 return true;
449 }
450 if (multiModalScene_.Invalid()) {
451 const auto pipelineContext = GetContext().Upgrade();
452 if (!pipelineContext) {
453 LOGW("the pipeline context is null");
454 return false;
455 }
456 const auto multimodalManager = pipelineContext->GetMultiModalManager();
457 if (!multimodalManager) {
458 LOGW("the multimodal manager is null");
459 return false;
460 }
461 const auto scene = multimodalManager->GetCurrentMultiModalScene();
462 switchEvent_ = VoiceEvent(component_->GetMultimodalProperties().voiceLabel, SceneLabel::SWITCH);
463 scene->SubscribeVoiceEvent(switchEvent_, multimodalSwitchEvent_);
464
465 multiModalScene_ = scene;
466 isSubscribeMultimodal_ = true;
467 }
468 return true;
469 }
470
UnSubscribeMultiModal()471 bool RenderSwitch::UnSubscribeMultiModal()
472 {
473 if (!isSubscribeMultimodal_) {
474 return true;
475 }
476 auto multiModalScene = multiModalScene_.Upgrade();
477 if (!multiModalScene) {
478 LOGE("fail to destroy multimodal event due to multiModalScene is null");
479 return false;
480 }
481 if (!switchEvent_.GetVoiceContent().empty()) {
482 multiModalScene->UnSubscribeVoiceEvent(switchEvent_);
483 }
484 isSubscribeMultimodal_ = false;
485 return true;
486 }
487 #endif
488
OpenSwitch()489 void RenderSwitch::OpenSwitch()
490 {
491 if (!oldChecked_) {
492 HandleClick();
493 }
494 }
495
CloseSwitch()496 void RenderSwitch::CloseSwitch()
497 {
498 if (oldChecked_) {
499 HandleClick();
500 }
501 }
502
InitCurrentPointPosition()503 void RenderSwitch::InitCurrentPointPosition()
504 {
505 auto pointPadding = NormalizeToPx(pointPadding_);
506 if (needReverse_) {
507 currentPointOriginX_ =
508 oldChecked_ ? pointPadding : (switchSize_.Width() - pointPadding - rawPointSize_.Width());
509 } else {
510 currentPointOriginX_ =
511 oldChecked_ ? (switchSize_.Width() - pointPadding - rawPointSize_.Width()) : pointPadding;
512 }
513 }
514
SetAccessibilityClickImpl()515 void RenderSwitch::SetAccessibilityClickImpl()
516 {
517 auto accessibilityNode = accessibilityNode_.Upgrade();
518 if (accessibilityNode) {
519 auto weakPtr = AceType::WeakClaim(AceType::RawPtr(clickRecognizer_));
520 accessibilityNode->AddSupportAction(AceAction::ACTION_CLICK);
521 accessibilityNode->SetClickableState(true);
522 accessibilityNode->SetCheckableState(true);
523 accessibilityNode->SetActionClickImpl([weakPtr]() {
524 auto click = weakPtr.Upgrade();
525 if (click) {
526 click->OnAccepted();
527 }
528 });
529 }
530 }
531
ProvideRestoreInfo()532 std::string RenderSwitch::ProvideRestoreInfo()
533 {
534 return std::to_string(checked_);
535 }
536
ApplyRestoreInfo()537 void RenderSwitch::ApplyRestoreInfo()
538 {
539 if (GetRestoreInfo().empty()) {
540 return;
541 }
542 checked_ = static_cast<size_t>(StringUtils::StringToInt(GetRestoreInfo()));
543 SetRestoreInfo("");
544 }
545
546 } // namespace OHOS::Ace
547