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