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