1 /*
2 * Copyright (c) 2023 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_ng/animation/geometry_transition.h"
17
18 #include "core/common/container.h"
19 #include "core/common/container_scope.h"
20 #include "core/components_ng/base/frame_node.h"
21 #include "core/components_ng/property/border_property.h"
22 #include "core/components_ng/property/property.h"
23 #include "core/components_ng/layout/layout_property.h"
24 #include "core/pipeline_ng/pipeline_context.h"
25
26 namespace OHOS::Ace::NG {
27 // Geometry transition is used for hero animation dealing with matched pair of inNode and outNode holding the
28 // same key. During geometry transition inNode starts with the size and position of outNode(inNode active),
29 // animates to the place where it should to be(inNode identity), meanwhile outNode starts with its own size
30 // and position(outNode identity), animates to the final size and position of inNode(outNode active). Although
31 // we have two transitions but these two transitions fit together perfectly, so the appearance looks like a
32 // single view move from its old position to its new position, thus visual focus guidance is completed.
GeometryTransition(const std::string & id,const WeakPtr<FrameNode> & frameNode,bool followWithoutTransition)33 GeometryTransition::GeometryTransition(const std::string& id, const WeakPtr<FrameNode>& frameNode,
34 bool followWithoutTransition) : id_(id), inNode_(frameNode), followWithoutTransition_(followWithoutTransition) {}
35
IsInAndOutEmpty() const36 bool GeometryTransition::IsInAndOutEmpty() const
37 {
38 return !inNode_.Upgrade() && !outNode_.Upgrade();
39 }
40
IsInAndOutValid() const41 bool GeometryTransition::IsInAndOutValid() const
42 {
43 return inNode_.Upgrade() && outNode_.Upgrade();
44 }
45
IsRunning() const46 bool GeometryTransition::IsRunning() const
47 {
48 return IsInAndOutValid() && (hasInAnim_ || hasOutAnim_);
49 }
50
IsNodeInAndActive(const WeakPtr<FrameNode> & frameNode) const51 bool GeometryTransition::IsNodeInAndActive(const WeakPtr<FrameNode>& frameNode) const
52 {
53 return IsInAndOutValid() && hasInAnim_ && frameNode.Upgrade() == inNode_ && state_ == State::ACTIVE;
54 }
55
IsNodeInAndIdentity(const WeakPtr<FrameNode> & frameNode) const56 bool GeometryTransition::IsNodeInAndIdentity(const WeakPtr<FrameNode>& frameNode) const
57 {
58 return IsInAndOutValid() && hasInAnim_ && frameNode.Upgrade() == inNode_ && state_ == State::IDENTITY;
59 }
60
IsNodeOutAndActive(const WeakPtr<FrameNode> & frameNode) const61 bool GeometryTransition::IsNodeOutAndActive(const WeakPtr<FrameNode>& frameNode) const
62 {
63 return IsInAndOutValid() && hasOutAnim_ && frameNode.Upgrade() == outNode_;
64 }
65
SwapInAndOut(bool condition)66 void GeometryTransition::SwapInAndOut(bool condition)
67 {
68 if (condition) {
69 std::swap(inNode_, outNode_);
70 }
71 }
72
GetMatchedPair(bool isNodeIn) const73 std::pair<RefPtr<FrameNode>, RefPtr<FrameNode>> GeometryTransition::GetMatchedPair(bool isNodeIn) const
74 {
75 auto self = isNodeIn ? inNode_ : outNode_;
76 auto target = isNodeIn ? outNode_ : inNode_;
77 return { self.Upgrade(), target.Upgrade() };
78 }
79
RecordOutNodeFrame()80 void GeometryTransition::RecordOutNodeFrame()
81 {
82 auto outNode = outNode_.Upgrade();
83 CHECK_NULL_VOID_NOLOG(outNode);
84 auto renderContext = outNode->GetRenderContext();
85 CHECK_NULL_VOID_NOLOG(renderContext);
86 outNodeParentPos_ = outNode->GetPaintRectGlobalOffsetWithTranslate(true);
87 auto nodeTransform = renderContext->GetPaintRectWithTransform();
88 outNodePos_ = outNodeParentPos_ + nodeTransform.GetOffset();
89 outNodeSize_ = nodeTransform.GetSize();
90 }
91
MarkLayoutDirty(const RefPtr<FrameNode> & node,int32_t layoutPriority)92 void GeometryTransition::MarkLayoutDirty(const RefPtr<FrameNode>& node, int32_t layoutPriority)
93 {
94 CHECK_NULL_VOID(node && node->GetLayoutProperty());
95 if (layoutPriority) {
96 node->SetLayoutPriority(layoutPriority);
97 }
98 node->GetLayoutProperty()->CleanDirty();
99 node->MarkDirtyNode(PROPERTY_UPDATE_MEASURE_SELF_AND_CHILD);
100 }
101
102 // Build should be called during node tree build phase dealing with node add/remove or appearing/disappearing
Build(const WeakPtr<FrameNode> & frameNode,bool isNodeIn)103 void GeometryTransition::Build(const WeakPtr<FrameNode>& frameNode, bool isNodeIn)
104 {
105 state_ = State::IDLE;
106 outNodeTargetAbsRect_ = std::nullopt;
107 if (IsInAndOutEmpty()) {
108 hasInAnim_ = false;
109 hasOutAnim_ = false;
110 }
111 auto node = frameNode.Upgrade();
112 CHECK_NULL_VOID(node && node->GetRenderContext() && !id_.empty());
113 LOGD("GeometryTransition: build node: %{public}d %{public}s", node->GetId(), isNodeIn ? "in" : "out");
114 if (!isNodeIn && (frameNode == inNode_ || frameNode == outNode_)) {
115 SwapInAndOut(frameNode == inNode_);
116 RecordOutNodeFrame();
117 hasOutAnim_ = true;
118 }
119 if (isNodeIn && (frameNode != inNode_)) {
120 auto inNode = inNode_.Upgrade();
121 if (inNode != nullptr && !inNode->IsRemoving() && !inNode->IsOnMainTree()) {
122 inNode_ = frameNode;
123 return;
124 }
125 SwapInAndOut(inNode != nullptr);
126 inNode_ = frameNode;
127 hasInAnim_ = true;
128 }
129
130 auto inNode = inNode_.Upgrade();
131 auto outNode = outNode_.Upgrade();
132 CHECK_NULL_VOID_NOLOG(IsInAndOutValid() && (inNode != outNode));
133
134 bool follow = false;
135 if (hasOutAnim_) {
136 MarkLayoutDirty(outNode, -1);
137 if (!hasInAnim_) {
138 follow = OnFollowWithoutTransition(false);
139 }
140 }
141 if (hasInAnim_ && !follow) {
142 state_ = State::ACTIVE;
143 MarkLayoutDirty(inNode, 1);
144 if (!hasOutAnim_) {
145 follow = OnFollowWithoutTransition(true);
146 }
147 }
148
149 LOGD("GeometryTransition: inAnim: %{public}d, outAnim: %{public}d, follow: %{public}d, "
150 "inNode: %{public}d, %{public}s, outNode: %{public}d, %{public}s", hasInAnim_, hasOutAnim_, follow,
151 inNode->GetId(), inNode->GetTag().c_str(), outNode->GetId(), outNode->GetTag().c_str());
152 }
153
154 // Update should be called during node update phase when node exists
Update(const WeakPtr<FrameNode> & which,const WeakPtr<FrameNode> & value)155 bool GeometryTransition::Update(const WeakPtr<FrameNode>& which, const WeakPtr<FrameNode>& value)
156 {
157 bool ret = false;
158 std::string str;
159 if (which.Upgrade() == inNode_.Upgrade()) {
160 str += "inNode updated: ";
161 inNode_ = value;
162 ret = true;
163 } else if (which.Upgrade() == outNode_.Upgrade()) {
164 str += "outNode updated: ";
165 outNode_ = value;
166 ret = true;
167 } else {
168 str += "noneNode updated: ";
169 }
170 str += "old value: ";
171 str += which.Upgrade() ? std::to_string(which.Upgrade()->GetId()) : "null";
172 str += ", new value: ";
173 str += value.Upgrade() ? std::to_string(value.Upgrade()->GetId()) : "null";
174 LOGD("GeometryTransition: %{public}s", str.c_str());
175 return ret;
176 }
177
178 // Called before layout, perform layout constraints match modifications in active state to
179 // impact self and children's measure and layout.
WillLayout(const RefPtr<LayoutWrapper> & layoutWrapper)180 void GeometryTransition::WillLayout(const RefPtr<LayoutWrapper>& layoutWrapper)
181 {
182 CHECK_NULL_VOID(layoutWrapper);
183 if (!layoutWrapper->IsRootMeasureNode()) {
184 return;
185 }
186 auto hostNode = layoutWrapper->GetHostNode();
187 if (IsNodeInAndActive(hostNode)) {
188 layoutPropertyIn_ = hostNode->GetLayoutProperty()->Clone();
189 ModifyLayoutConstraint(layoutWrapper, true);
190 } else if (IsNodeOutAndActive(hostNode)) {
191 layoutPropertyOut_ = hostNode->GetLayoutProperty()->Clone();
192 ModifyLayoutConstraint(layoutWrapper, false);
193 }
194 }
195
196 // Called after layout, perform final adjustments of geometry position
DidLayout(const RefPtr<LayoutWrapper> & layoutWrapper)197 void GeometryTransition::DidLayout(const RefPtr<LayoutWrapper>& layoutWrapper)
198 {
199 CHECK_NULL_VOID(layoutWrapper);
200 auto node = layoutWrapper->GetHostNode();
201 CHECK_NULL_VOID(node);
202 bool isRoot = layoutWrapper->IsRootMeasureNode();
203 std::optional<bool> direction = std::nullopt;
204
205 if (isRoot && IsNodeInAndActive(node)) {
206 LOGD("GeometryTransition: node: %{public}d in and active", node->GetId());
207 state_ = State::IDENTITY;
208 auto geometryNode = node->GetGeometryNode();
209 CHECK_NULL_VOID(geometryNode);
210 inNodeActiveFrameSize_ = geometryNode->GetFrameSize();
211 layoutPropertyIn_->UpdatePropertyChangeFlag(PROPERTY_UPDATE_MEASURE);
212 node->SetLayoutProperty(layoutPropertyIn_);
213 layoutPropertyIn_.Reset();
214 } else if (IsNodeInAndIdentity(node)) {
215 LOGD("GeometryTransition: node: %{public}d in and identity", node->GetId());
216 state_ = State::IDLE;
217 node->SetLayoutPriority(0);
218 direction = true;
219 hasInAnim_ = false;
220 } else if (isRoot && IsNodeOutAndActive(node)) {
221 LOGD("GeometryTransition: node: %{public}d out and active", node->GetId());
222 hasOutAnim_ = false;
223 CHECK_NULL_VOID(!hasInAnim_);
224 direction = false;
225 }
226
227 if (direction.has_value()) {
228 auto pipeline = AceType::DynamicCast<NG::PipelineContext>(PipelineBase::GetCurrentContext());
229 CHECK_NULL_VOID(pipeline);
230 pipeline->AddAfterLayoutTask([weak = WeakClaim(this), isNodeIn = direction.value()]() {
231 auto geometryTransition = weak.Upgrade();
232 CHECK_NULL_VOID(geometryTransition);
233 geometryTransition->SyncGeometry(isNodeIn);
234 if (!isNodeIn) {
235 auto outNode = geometryTransition->outNode_.Upgrade();
236 if (outNode && geometryTransition->layoutPropertyOut_) {
237 outNode->SetLayoutProperty(geometryTransition->layoutPropertyOut_);
238 geometryTransition->layoutPropertyOut_->UpdatePropertyChangeFlag(PROPERTY_UPDATE_MEASURE);
239 geometryTransition->layoutPropertyOut_.Reset();
240 }
241 }
242 });
243 }
244 }
245
ModifyLayoutConstraint(const RefPtr<LayoutWrapper> & layoutWrapper,bool isNodeIn)246 void GeometryTransition::ModifyLayoutConstraint(const RefPtr<LayoutWrapper>& layoutWrapper, bool isNodeIn)
247 {
248 // outNode's frame is the target frame for active inNode to match,
249 // inNode's frame is the target frame for active outNode to match.
250 auto [self, target] = GetMatchedPair(isNodeIn);
251 CHECK_NULL_VOID(self);
252 CHECK_NULL_VOID(target);
253 // target's geometry is ensured ready to use because layout nodes are sorted to respect dependency,
254 // the order is active inNode, normal layout, active outNode.
255 auto targetGeometryNode = target->GetGeometryNode();
256 CHECK_NULL_VOID(targetGeometryNode);
257 auto targetRenderContext = target->GetRenderContext();
258 CHECK_NULL_VOID(targetRenderContext);
259 auto size = isNodeIn ?
260 (target->IsRemoving() ? outNodeSize_ : targetRenderContext->GetPaintRectWithTransform().GetSize()) :
261 targetGeometryNode->GetFrameSize();
262 auto targetSize = CalcSize(NG::CalcLength(size.Width()), NG::CalcLength(size.Height()));
263 auto layoutProperty = layoutWrapper->GetLayoutProperty();
264 CHECK_NULL_VOID(layoutProperty);
265 layoutProperty->UpdateUserDefinedIdealSize(targetSize);
266 LOGD("GeometryTransition: node: %{public}d modify size to: %{public}s",
267 self->GetId(), targetSize.ToString().c_str());
268 // if node has aspect ratio we'll ignore it in active state, instead target's aspect ratio is respected
269 const auto& magicItemProperty = layoutProperty->GetMagicItemProperty();
270 auto hasAspectRatio = magicItemProperty ? magicItemProperty->HasAspectRatio() : false;
271 if (hasAspectRatio && size.IsPositive()) {
272 auto targetAspectRatio = size.Width() / size.Height();
273 magicItemProperty->UpdateAspectRatio(targetAspectRatio);
274 LOGD("GeometryTransition: node: %{public}d modify aspect ratio to: %{public}f",
275 self->GetId(), targetAspectRatio);
276 }
277 }
278
SyncGeometry(bool isNodeIn)279 void GeometryTransition::SyncGeometry(bool isNodeIn)
280 {
281 // outNode's position is the target position for active inNode to match,
282 // inNode's position is the target position for active outNode to match.
283 auto [self, target] = GetMatchedPair(isNodeIn);
284 CHECK_NULL_VOID(self);
285 CHECK_NULL_VOID(target);
286 auto renderContext = self->GetRenderContext();
287 auto targetRenderContext = target->GetRenderContext();
288 CHECK_NULL_VOID(renderContext);
289 CHECK_NULL_VOID(targetRenderContext);
290 auto geometryNode = self->GetGeometryNode();
291 CHECK_NULL_VOID(geometryNode);
292 // get own parent's global position, parent's transform is not taken into account other than translate
293 auto parentPos = self->IsRemoving() ? outNodeParentPos_ : self->GetPaintRectGlobalOffsetWithTranslate(true);
294 // get target's global position, target own transform is taken into account
295 auto targetPos = target->IsRemoving() ? outNodePos_ : target->GetPaintRectGlobalOffsetWithTranslate(true) +
296 targetRenderContext->GetPaintRectWithTransform().GetOffset();
297 // adjust self's position to match with target's position, here we only need to adjust node self,
298 // its children's positions are still determined by layout process.
299 auto activeFrameRect = isNodeIn ? RectF(targetPos - parentPos, inNodeActiveFrameSize_) :
300 RectF(targetPos - parentPos, geometryNode->GetFrameSize());
301 auto activeCornerRadius = targetRenderContext->GetBorderRadius().value_or(BorderRadiusProperty());
302 auto cornerRadius = renderContext->GetBorderRadius().value_or(BorderRadiusProperty());
303
304 if (!isNodeIn) {
305 // save outNode's target global frame rect for future compare to sync outNode with inNode's frame change
306 outNodeTargetAbsRect_ = RectF(targetPos, activeFrameRect.GetSize());
307 } else if (target->IsRemoving()) {
308 // notify backend for hierarchy processing to avoid outNode being shaded
309 renderContext->RegisterSharedTransition(targetRenderContext);
310 }
311
312 // draw self and children in sandbox which will not be affected by parent's transition
313 renderContext->SetSandBox(parentPos);
314
315 AnimationUtils::AnimateWithCurrentOptions(
316 [&]() {
317 // sync geometry in active state
318 renderContext->UpdateBorderRadius(activeCornerRadius);
319 renderContext->SyncGeometryProperties(activeFrameRect);
320 // sync geometry in identity state for inNode
321 if (isNodeIn) {
322 renderContext->UpdateBorderRadius(cornerRadius);
323 renderContext->SyncGeometryProperties(RawPtr(geometryNode));
324 }
325 },
326 [id = Container::CurrentId(), nodeWeak = WeakClaim(RawPtr(self))]() {
327 ContainerScope scope(id);
328 auto node = nodeWeak.Upgrade();
329 CHECK_NULL_VOID_NOLOG(node);
330 auto renderContext = node->GetRenderContext();
331 CHECK_NULL_VOID_NOLOG(renderContext);
332 renderContext->SetSandBox(std::nullopt);
333 LOGD("GeometryTransition: node%{public}d animation completed", node->GetId());
334 },
335 false);
336
337 LOGD("GeometryTransition: node: %{public}d, parent: %{public}s, target: %{public}s, "
338 "active frame: %{public}s, identity frame: %{public}s",
339 self->GetId(), parentPos.ToString().c_str(), targetPos.ToString().c_str(),
340 activeFrameRect.ToString().c_str(), isNodeIn ? geometryNode->GetFrameRect().ToString().c_str() : "no log");
341 }
342
CreateHolderNode(const RefPtr<FrameNode> & node)343 RefPtr<FrameNode> CreateHolderNode(const RefPtr<FrameNode>& node)
344 {
345 CHECK_NULL_RETURN_NOLOG(node, nullptr);
346 auto newNode = FrameNode::CreateFrameNode(
347 node->GetTag(), ElementRegister::GetInstance()->MakeUniqueId(), AceType::MakeRefPtr<Pattern>());
348 newNode->SetGeometryNode(node->GetGeometryNode()->Clone());
349 auto frameSize = node->GetGeometryNode()->GetFrameSize();
350 newNode->GetLayoutProperty()->UpdateUserDefinedIdealSize(
351 CalcSize(CalcLength(frameSize.Width()), CalcLength(frameSize.Height())));
352 return newNode;
353 }
354
355 // For nodes without transition (still on the tree), but still need to follow the matched node which has
356 // transition (parameter is its transition direction).
OnFollowWithoutTransition(std::optional<bool> direction)357 bool GeometryTransition::OnFollowWithoutTransition(std::optional<bool> direction)
358 {
359 CHECK_NULL_RETURN_NOLOG(followWithoutTransition_, false);
360 if (!direction.has_value()) {
361 auto holder = holder_.Upgrade();
362 auto inNode = inNode_.Upgrade();
363 auto outNode = outNode_.Upgrade();
364 CHECK_NULL_RETURN(holder && inNode && outNode, false);
365 auto parent = holder->GetParent();
366 auto inRenderContext = inNode->GetRenderContext();
367 auto outRenderContext = outNode->GetRenderContext();
368 CHECK_NULL_RETURN(parent && inRenderContext && outRenderContext, false);
369 parent->ReplaceChild(holder, outNode);
370 parent->RemoveDisappearingChild(outNode);
371 parent->MarkDirtyNode(PROPERTY_UPDATE_MEASURE_SELF_AND_CHILD);
372 inRenderContext->UnregisterSharedTransition(outRenderContext);
373 holder_ = nullptr;
374 hasOutAnim_ = false;
375 LOGD("GeometryTransition: follow is cancelled");
376 return false;
377 }
378
379 bool isNodeIn = direction.value();
380 if (isNodeIn) {
381 auto outNode = outNode_.Upgrade();
382 CHECK_NULL_RETURN(outNode, false);
383 auto parent = outNode->GetParent();
384 CHECK_NULL_RETURN(parent, false);
385 auto holder = CreateHolderNode(outNode);
386 CHECK_NULL_RETURN(holder, false);
387 RecordOutNodeFrame();
388 parent->ReplaceChild(outNode, holder);
389 parent->AddDisappearingChild(outNode);
390 holder_ = holder;
391 MarkLayoutDirty(outNode, -1);
392 hasOutAnim_ = true;
393 LOGD("GeometryTransition: follow is started");
394 } else {
395 auto inNode = inNode_.Upgrade();
396 auto holder = holder_.Upgrade();
397 CHECK_NULL_RETURN(inNode && holder, false);
398 auto parent = holder->GetParent();
399 CHECK_NULL_RETURN(parent, false);
400 parent->ReplaceChild(holder, inNode);
401 parent->RemoveDisappearingChild(inNode);
402 holder_ = nullptr;
403 state_ = State::ACTIVE;
404 MarkLayoutDirty(inNode, 1);
405 hasInAnim_ = true;
406 LOGD("GeometryTransition: follow is ended");
407 }
408 return true;
409 }
410
411 // during outNode animation is running target inNode's frame is changed, outNode needs to change as well to
412 // match tightly.
OnReSync()413 void GeometryTransition::OnReSync()
414 {
415 auto inNode = inNode_.Upgrade();
416 auto outNode = outNode_.Upgrade();
417 CHECK_NULL_VOID_NOLOG(inNode && outNode && outNode->IsRemoving() && outNodeTargetAbsRect_);
418 auto renderContext = inNode->GetRenderContext();
419 CHECK_NULL_VOID_NOLOG(renderContext && renderContext->IsSynced());
420 auto paintRect = renderContext->GetPaintRectWithTransform();
421 auto inNodeAbsRect = RectF(inNode->GetPaintRectGlobalOffsetWithTranslate(true) + paintRect.GetOffset(),
422 paintRect.GetSize());
423 auto inNodeAbsRectOld = outNodeTargetAbsRect_.value();
424 if (inNodeAbsRect != inNodeAbsRectOld) {
425 hasOutAnim_ = true;
426 outNodeTargetAbsRect_.reset();
427 MarkLayoutDirty(outNode);
428 LOGD("GeometryTransition: outNode: %{public}d %{public}s is marked dirty to respond to frame change of "
429 "inNode: %{public}d %{public}s", outNode->GetId(), inNodeAbsRectOld.ToString().c_str(),
430 inNode->GetId(), inNodeAbsRect.ToString().c_str());
431 }
432 }
433
434 // if nodes with geometry transitions are added during layout, we need to execute additional layout in current frame
OnAdditionalLayout(const WeakPtr<FrameNode> & frameNode)435 bool GeometryTransition::OnAdditionalLayout(const WeakPtr<FrameNode>& frameNode)
436 {
437 bool ret = false;
438 auto node = frameNode.Upgrade();
439 CHECK_NULL_RETURN(node, false);
440 if (IsNodeInAndActive(frameNode)) {
441 auto parent = node->GetParent();
442 while (parent) {
443 auto parentNode = AceType::DynamicCast<FrameNode>(parent);
444 if (parentNode) {
445 MarkLayoutDirty(node);
446 MarkLayoutDirty(parentNode);
447 ret = true;
448 LOGD("GeometryTransition: node: %{public}d, parent node: %{public}d is marked dirty",
449 node->GetId(), parentNode->GetId());
450 break;
451 }
452 parent = parent->GetParent();
453 }
454 } else if (IsNodeOutAndActive(frameNode)) {
455 MarkLayoutDirty(node);
456 ret = true;
457 }
458 return ret;
459 }
460
ToString() const461 std::string GeometryTransition::ToString() const
462 {
463 return std::string("in: ") + (inNode_.Upgrade() ? std::to_string(inNode_.Upgrade()->GetId()) : "null") +
464 std::string(", out: ") + (outNode_.Upgrade() ? std::to_string(outNode_.Upgrade()->GetId()) : "null");
465 }
466 } // namespace OHOS::Ace::NG