| Index: cc/animation/animation_player.cc
|
| diff --git a/cc/animation/animation_player.cc b/cc/animation/animation_player.cc
|
| index bef51a07390a6c1f822681804232856ad22c3950..de5801e74f423e2638bb7402114a7811deec5cc3 100644
|
| --- a/cc/animation/animation_player.cc
|
| +++ b/cc/animation/animation_player.cc
|
| @@ -9,7 +9,7 @@
|
| #include "cc/animation/animation_delegate.h"
|
| #include "cc/animation/animation_host.h"
|
| #include "cc/animation/animation_timeline.h"
|
| -#include "cc/animation/element_animations.h"
|
| +#include "cc/animation/scroll_offset_animation_curve.h"
|
|
|
| namespace cc {
|
|
|
| @@ -23,7 +23,8 @@ AnimationPlayer::AnimationPlayer(int id)
|
| element_animations_(),
|
| animation_delegate_(),
|
| id_(id),
|
| - needs_push_properties_(false) {
|
| + needs_push_properties_(false),
|
| + needs_to_start_animations_(false) {
|
| DCHECK(id_);
|
| }
|
|
|
| @@ -104,80 +105,141 @@ void AnimationPlayer::BindElementAnimations() {
|
| animation_host_->GetElementAnimationsForElementId(element_id_);
|
| DCHECK(element_animations_);
|
|
|
| - // Pass all accumulated animations to ElementAnimations.
|
| for (auto& animation : animations_)
|
| - element_animations_->AddAnimation(std::move(animation));
|
| + AnimationAddedForProperty(animation->target_property());
|
|
|
| - if (!animations_.empty())
|
| - SetNeedsCommit();
|
| SetNeedsPushProperties();
|
| -
|
| - animations_.clear();
|
| }
|
|
|
| void AnimationPlayer::UnbindElementAnimations() {
|
| SetNeedsPushProperties();
|
| -
|
| element_animations_ = nullptr;
|
| - DCHECK(animations_.empty());
|
| }
|
|
|
| void AnimationPlayer::AddAnimation(std::unique_ptr<Animation> animation) {
|
| DCHECK(animation->target_property() != TargetProperty::SCROLL_OFFSET ||
|
| (animation_host_ && animation_host_->SupportsScrollAnimations()));
|
| + DCHECK(!animation->is_impl_only() ||
|
| + animation->target_property() == TargetProperty::SCROLL_OFFSET);
|
|
|
| + TargetProperty::Type target_property = animation->target_property();
|
| + animations_.push_back(std::move(animation));
|
| if (element_animations_) {
|
| - element_animations_->AddAnimation(std::move(animation));
|
| - SetNeedsCommit();
|
| + AnimationAddedForProperty(target_property);
|
| SetNeedsPushProperties();
|
| - } else {
|
| - animations_.push_back(std::move(animation));
|
| }
|
| }
|
|
|
| -void AnimationPlayer::PauseAnimation(int animation_id, double time_offset) {
|
| +void AnimationPlayer::AnimationAddedForProperty(
|
| + TargetProperty::Type target_property) {
|
| DCHECK(element_animations_);
|
| - element_animations_->PauseAnimation(
|
| - animation_id, base::TimeDelta::FromSecondsD(time_offset));
|
| +
|
| SetNeedsCommit();
|
| - SetNeedsPushProperties();
|
| + needs_to_start_animations_ = true;
|
| +
|
| + element_animations_->UpdateActivationNormal();
|
| + element_animations_->UpdateClientAnimationState(target_property);
|
| +}
|
| +
|
| +void AnimationPlayer::PauseAnimation(int animation_id, double time_offset) {
|
| + const base::TimeDelta time_delta = base::TimeDelta::FromSecondsD(time_offset);
|
| +
|
| + for (size_t i = 0; i < animations_.size(); ++i) {
|
| + if (animations_[i]->id() == animation_id) {
|
| + animations_[i]->SetRunState(Animation::PAUSED,
|
| + time_delta + animations_[i]->start_time() +
|
| + animations_[i]->time_offset());
|
| + }
|
| + }
|
| +
|
| + if (element_animations_) {
|
| + SetNeedsCommit();
|
| + SetNeedsPushProperties();
|
| + }
|
| }
|
|
|
| void AnimationPlayer::RemoveAnimation(int animation_id) {
|
| + bool removed_transform_animation = false;
|
| + bool removed_opacity_animation = false;
|
| + bool removed_filter_animation = false;
|
| + // Since we want to use the animations that we're going to remove, we need to
|
| + // use a stable_parition here instead of remove_if. Remove_if leaves the
|
| + // removed items in an unspecified state.
|
| + auto animations_to_remove = std::stable_partition(
|
| + animations_.begin(), animations_.end(),
|
| + [animation_id](const std::unique_ptr<Animation>& animation) {
|
| + return animation->id() != animation_id;
|
| + });
|
| + for (auto it = animations_to_remove; it != animations_.end(); ++it) {
|
| + if ((*it)->target_property() == TargetProperty::SCROLL_OFFSET) {
|
| + if (element_animations_)
|
| + element_animations_->SetScrollOffsetAnimationWasInterrupted();
|
| + } else if ((*it)->target_property() == TargetProperty::TRANSFORM &&
|
| + !(*it)->is_finished()) {
|
| + removed_transform_animation = true;
|
| + } else if ((*it)->target_property() == TargetProperty::OPACITY &&
|
| + !(*it)->is_finished()) {
|
| + removed_opacity_animation = true;
|
| + } else if ((*it)->target_property() == TargetProperty::FILTER &&
|
| + !(*it)->is_finished()) {
|
| + removed_filter_animation = true;
|
| + }
|
| + }
|
| +
|
| + animations_.erase(animations_to_remove, animations_.end());
|
| +
|
| if (element_animations_) {
|
| - element_animations_->RemoveAnimation(animation_id);
|
| + element_animations_->UpdateActivationNormal();
|
| + element_animations_->UpdateClientAnimationState(removed_transform_animation,
|
| + removed_opacity_animation,
|
| + removed_filter_animation);
|
| SetNeedsCommit();
|
| SetNeedsPushProperties();
|
| - } else {
|
| - auto animations_to_remove = std::remove_if(
|
| - animations_.begin(), animations_.end(),
|
| - [animation_id](const std::unique_ptr<Animation>& animation) {
|
| - return animation->id() == animation_id;
|
| - });
|
| - animations_.erase(animations_to_remove, animations_.end());
|
| }
|
| }
|
|
|
| void AnimationPlayer::AbortAnimation(int animation_id) {
|
| - DCHECK(element_animations_);
|
| - element_animations_->AbortAnimation(animation_id);
|
| - SetNeedsCommit();
|
| - SetNeedsPushProperties();
|
| + if (Animation* animation = GetAnimationById(animation_id)) {
|
| + if (!animation->is_finished()) {
|
| + animation->SetRunState(Animation::ABORTED, last_tick_time_);
|
| + if (element_animations_)
|
| + element_animations_->UpdateClientAnimationState(
|
| + animation->target_property());
|
| + }
|
| + }
|
| +
|
| + if (element_animations_) {
|
| + SetNeedsCommit();
|
| + SetNeedsPushProperties();
|
| + }
|
| }
|
|
|
| void AnimationPlayer::AbortAnimations(TargetProperty::Type target_property,
|
| bool needs_completion) {
|
| + if (needs_completion)
|
| + DCHECK(target_property == TargetProperty::SCROLL_OFFSET);
|
| +
|
| + bool aborted_animation = false;
|
| + for (size_t i = 0; i < animations_.size(); ++i) {
|
| + if (animations_[i]->target_property() == target_property &&
|
| + !animations_[i]->is_finished()) {
|
| + // Currently only impl-only scroll offset animations can be completed on
|
| + // the main thread.
|
| + if (needs_completion && animations_[i]->is_impl_only()) {
|
| + animations_[i]->SetRunState(Animation::ABORTED_BUT_NEEDS_COMPLETION,
|
| + last_tick_time_);
|
| + } else {
|
| + animations_[i]->SetRunState(Animation::ABORTED, last_tick_time_);
|
| + }
|
| + aborted_animation = true;
|
| + }
|
| + }
|
| +
|
| if (element_animations_) {
|
| - element_animations_->AbortAnimations(target_property, needs_completion);
|
| + if (aborted_animation)
|
| + element_animations_->UpdateClientAnimationState(target_property);
|
| SetNeedsCommit();
|
| SetNeedsPushProperties();
|
| - } else {
|
| - auto animations_to_remove = std::remove_if(
|
| - animations_.begin(), animations_.end(),
|
| - [target_property](const std::unique_ptr<Animation>& animation) {
|
| - return animation->target_property() == target_property;
|
| - });
|
| - animations_.erase(animations_to_remove, animations_.end());
|
| }
|
| }
|
|
|
| @@ -186,59 +248,110 @@ void AnimationPlayer::PushPropertiesTo(AnimationPlayer* player_impl) {
|
| return;
|
| needs_push_properties_ = false;
|
|
|
| + // Create or destroy ElementAnimations.
|
| if (element_id_ != player_impl->element_id()) {
|
| if (player_impl->element_id())
|
| player_impl->DetachElement();
|
| if (element_id_)
|
| player_impl->AttachElement(element_id_);
|
| }
|
| +
|
| + if (!has_any_animation() && !player_impl->has_any_animation())
|
| + return;
|
| +
|
| + MarkAbortedAnimationsForDeletion(player_impl);
|
| + PurgeAnimationsMarkedForDeletion();
|
| + PushNewAnimationsToImplThread(player_impl);
|
| +
|
| + // Remove finished impl side animations only after pushing,
|
| + // and only after the animations are deleted on the main thread
|
| + // this insures we will never push an animation twice.
|
| + RemoveAnimationsCompletedOnMainThread(player_impl);
|
| +
|
| + PushPropertiesToImplThread(player_impl);
|
| }
|
|
|
| -void AnimationPlayer::NotifyAnimationStarted(
|
| - base::TimeTicks monotonic_time,
|
| - TargetProperty::Type target_property,
|
| - int group) {
|
| - if (animation_delegate_)
|
| - animation_delegate_->NotifyAnimationStarted(monotonic_time, target_property,
|
| - group);
|
| +bool AnimationPlayer::NotifyAnimationStarted(const AnimationEvent& event) {
|
| + DCHECK(!event.is_impl_only);
|
| +
|
| + for (size_t i = 0; i < animations_.size(); ++i) {
|
| + if (animations_[i]->group() == event.group_id &&
|
| + animations_[i]->target_property() == event.target_property &&
|
| + animations_[i]->needs_synchronized_start_time()) {
|
| + animations_[i]->set_needs_synchronized_start_time(false);
|
| + if (!animations_[i]->has_set_start_time())
|
| + animations_[i]->set_start_time(event.monotonic_time);
|
| +
|
| + if (animation_delegate_) {
|
| + animation_delegate_->NotifyAnimationStarted(
|
| + event.monotonic_time, event.target_property, event.group_id);
|
| + }
|
| + return true;
|
| + }
|
| + }
|
| +
|
| + return false;
|
| }
|
|
|
| -void AnimationPlayer::NotifyAnimationFinished(
|
| - base::TimeTicks monotonic_time,
|
| - TargetProperty::Type target_property,
|
| - int group) {
|
| - if (animation_delegate_)
|
| - animation_delegate_->NotifyAnimationFinished(monotonic_time,
|
| - target_property, group);
|
| +bool AnimationPlayer::NotifyAnimationFinished(const AnimationEvent& event) {
|
| + DCHECK(!event.is_impl_only);
|
| +
|
| + for (size_t i = 0; i < animations_.size(); ++i) {
|
| + if (animations_[i]->group() == event.group_id &&
|
| + animations_[i]->target_property() == event.target_property) {
|
| + animations_[i]->set_received_finished_event(true);
|
| +
|
| + if (animation_delegate_) {
|
| + animation_delegate_->NotifyAnimationFinished(
|
| + event.monotonic_time, event.target_property, event.group_id);
|
| + }
|
| + return true;
|
| + }
|
| + }
|
| +
|
| + return false;
|
| }
|
|
|
| -void AnimationPlayer::NotifyAnimationAborted(
|
| - base::TimeTicks monotonic_time,
|
| +bool AnimationPlayer::NotifyAnimationFinishedForTesting(
|
| TargetProperty::Type target_property,
|
| - int group) {
|
| - if (animation_delegate_)
|
| - animation_delegate_->NotifyAnimationAborted(monotonic_time, target_property,
|
| - group);
|
| + int group_id) {
|
| + AnimationEvent event(AnimationEvent::FINISHED, element_id_, group_id,
|
| + target_property, base::TimeTicks());
|
| + return NotifyAnimationFinished(event);
|
| }
|
|
|
| -void AnimationPlayer::NotifyAnimationWaitingForDeletion() {
|
| - // We need to purge animations marked for deletion.
|
| - SetNeedsPushProperties();
|
| +bool AnimationPlayer::NotifyAnimationAborted(const AnimationEvent& event) {
|
| + DCHECK(!event.is_impl_only);
|
| +
|
| + for (size_t i = 0; i < animations_.size(); ++i) {
|
| + if (animations_[i]->group() == event.group_id &&
|
| + animations_[i]->target_property() == event.target_property) {
|
| + animations_[i]->SetRunState(Animation::ABORTED, event.monotonic_time);
|
| + animations_[i]->set_received_finished_event(true);
|
| + if (animation_delegate_) {
|
| + animation_delegate_->NotifyAnimationAborted(
|
| + event.monotonic_time, event.target_property, event.group_id);
|
| + }
|
| + return true;
|
| + }
|
| + }
|
| +
|
| + return false;
|
| }
|
|
|
| -void AnimationPlayer::NotifyAnimationTakeover(
|
| - base::TimeTicks monotonic_time,
|
| - TargetProperty::Type target_property,
|
| - double animation_start_time,
|
| - std::unique_ptr<AnimationCurve> curve) {
|
| +void AnimationPlayer::NotifyAnimationTakeover(const AnimationEvent& event) {
|
| + DCHECK(!event.is_impl_only);
|
| + DCHECK(event.target_property == TargetProperty::SCROLL_OFFSET);
|
| +
|
| // We need to purge animations marked for deletion on CT.
|
| SetNeedsPushProperties();
|
|
|
| if (animation_delegate_) {
|
| - DCHECK(curve);
|
| + DCHECK(event.curve);
|
| + std::unique_ptr<AnimationCurve> animation_curve = event.curve->Clone();
|
| animation_delegate_->NotifyAnimationTakeover(
|
| - monotonic_time, target_property, animation_start_time,
|
| - std::move(curve));
|
| + event.monotonic_time, event.target_property, event.animation_start_time,
|
| + std::move(animation_curve));
|
| }
|
| }
|
|
|
| @@ -257,4 +370,780 @@ void AnimationPlayer::SetNeedsPushProperties() {
|
| element_animations_->SetNeedsPushProperties();
|
| }
|
|
|
| +bool AnimationPlayer::HasActiveAnimation() const {
|
| + for (size_t i = 0; i < animations_.size(); ++i) {
|
| + if (!animations_[i]->is_finished())
|
| + return true;
|
| + }
|
| + return false;
|
| +}
|
| +
|
| +bool AnimationPlayer::HasNonDeletedAnimation() const {
|
| + for (size_t i = 0; i < animations_.size(); ++i) {
|
| + if (animations_[i]->run_state() != Animation::WAITING_FOR_DELETION)
|
| + return true;
|
| + }
|
| +
|
| + return false;
|
| +}
|
| +
|
| +void AnimationPlayer::StartAnimations(base::TimeTicks monotonic_time) {
|
| + DCHECK(needs_to_start_animations_);
|
| + needs_to_start_animations_ = false;
|
| + // First collect running properties affecting each type of element.
|
| + TargetProperties blocked_properties_for_active_elements;
|
| + TargetProperties blocked_properties_for_pending_elements;
|
| + std::vector<size_t> animations_waiting_for_target;
|
| +
|
| + animations_waiting_for_target.reserve(animations_.size());
|
| + for (size_t i = 0; i < animations_.size(); ++i) {
|
| + if (animations_[i]->run_state() == Animation::STARTING ||
|
| + animations_[i]->run_state() == Animation::RUNNING) {
|
| + if (animations_[i]->affects_active_elements()) {
|
| + blocked_properties_for_active_elements[animations_[i]
|
| + ->target_property()] = true;
|
| + }
|
| + if (animations_[i]->affects_pending_elements()) {
|
| + blocked_properties_for_pending_elements[animations_[i]
|
| + ->target_property()] = true;
|
| + }
|
| + } else if (animations_[i]->run_state() ==
|
| + Animation::WAITING_FOR_TARGET_AVAILABILITY) {
|
| + animations_waiting_for_target.push_back(i);
|
| + }
|
| + }
|
| +
|
| + for (size_t i = 0; i < animations_waiting_for_target.size(); ++i) {
|
| + // Collect all properties for animations with the same group id (they
|
| + // should all also be in the list of animations).
|
| + size_t animation_index = animations_waiting_for_target[i];
|
| + Animation* animation_waiting_for_target =
|
| + animations_[animation_index].get();
|
| + // Check for the run state again even though the animation was waiting
|
| + // for target because it might have changed the run state while handling
|
| + // previous animation in this loop (if they belong to same group).
|
| + if (animation_waiting_for_target->run_state() ==
|
| + Animation::WAITING_FOR_TARGET_AVAILABILITY) {
|
| + TargetProperties enqueued_properties;
|
| + bool affects_active_elements =
|
| + animation_waiting_for_target->affects_active_elements();
|
| + bool affects_pending_elements =
|
| + animation_waiting_for_target->affects_pending_elements();
|
| + enqueued_properties[animation_waiting_for_target->target_property()] =
|
| + true;
|
| + for (size_t j = animation_index + 1; j < animations_.size(); ++j) {
|
| + if (animation_waiting_for_target->group() == animations_[j]->group()) {
|
| + enqueued_properties[animations_[j]->target_property()] = true;
|
| + affects_active_elements |= animations_[j]->affects_active_elements();
|
| + affects_pending_elements |=
|
| + animations_[j]->affects_pending_elements();
|
| + }
|
| + }
|
| +
|
| + // Check to see if intersection of the list of properties affected by
|
| + // the group and the list of currently blocked properties is null, taking
|
| + // into account the type(s) of elements affected by the group. In any
|
| + // case, the group's target properties need to be added to the lists of
|
| + // blocked properties.
|
| + bool null_intersection = true;
|
| + static_assert(TargetProperty::FIRST_TARGET_PROPERTY == 0,
|
| + "TargetProperty must be 0-based enum");
|
| + for (int property = TargetProperty::FIRST_TARGET_PROPERTY;
|
| + property <= TargetProperty::LAST_TARGET_PROPERTY; ++property) {
|
| + if (enqueued_properties[property]) {
|
| + if (affects_active_elements) {
|
| + if (blocked_properties_for_active_elements[property])
|
| + null_intersection = false;
|
| + else
|
| + blocked_properties_for_active_elements[property] = true;
|
| + }
|
| + if (affects_pending_elements) {
|
| + if (blocked_properties_for_pending_elements[property])
|
| + null_intersection = false;
|
| + else
|
| + blocked_properties_for_pending_elements[property] = true;
|
| + }
|
| + }
|
| + }
|
| +
|
| + // If the intersection is null, then we are free to start the animations
|
| + // in the group.
|
| + if (null_intersection) {
|
| + animation_waiting_for_target->SetRunState(Animation::STARTING,
|
| + monotonic_time);
|
| + for (size_t j = animation_index + 1; j < animations_.size(); ++j) {
|
| + if (animation_waiting_for_target->group() ==
|
| + animations_[j]->group()) {
|
| + animations_[j]->SetRunState(Animation::STARTING, monotonic_time);
|
| + }
|
| + }
|
| + } else {
|
| + needs_to_start_animations_ = true;
|
| + }
|
| + }
|
| + }
|
| +}
|
| +
|
| +void AnimationPlayer::PromoteStartedAnimations(base::TimeTicks monotonic_time,
|
| + AnimationEvents* events) {
|
| + for (size_t i = 0; i < animations_.size(); ++i) {
|
| + if (animations_[i]->run_state() == Animation::STARTING &&
|
| + animations_[i]->affects_active_elements()) {
|
| + animations_[i]->SetRunState(Animation::RUNNING, monotonic_time);
|
| + if (!animations_[i]->has_set_start_time() &&
|
| + !animations_[i]->needs_synchronized_start_time())
|
| + animations_[i]->set_start_time(monotonic_time);
|
| + if (events) {
|
| + base::TimeTicks start_time;
|
| + if (animations_[i]->has_set_start_time())
|
| + start_time = animations_[i]->start_time();
|
| + else
|
| + start_time = monotonic_time;
|
| + AnimationEvent started_event(
|
| + AnimationEvent::STARTED, element_id_, animations_[i]->group(),
|
| + animations_[i]->target_property(), start_time);
|
| + started_event.is_impl_only = animations_[i]->is_impl_only();
|
| + if (started_event.is_impl_only) {
|
| + // Notify delegate directly, do not record the event.
|
| + if (animation_delegate_) {
|
| + animation_delegate_->NotifyAnimationStarted(
|
| + started_event.monotonic_time, started_event.target_property,
|
| + started_event.group_id);
|
| + }
|
| + } else {
|
| + events->events_.push_back(started_event);
|
| + }
|
| + }
|
| + }
|
| + }
|
| +}
|
| +
|
| +void AnimationPlayer::MarkAnimationsForDeletion(base::TimeTicks monotonic_time,
|
| + AnimationEvents* events) {
|
| + bool marked_animations_for_deletions = false;
|
| + std::vector<size_t> animations_with_same_group_id;
|
| +
|
| + animations_with_same_group_id.reserve(animations_.size());
|
| + // Non-aborted animations are marked for deletion after a corresponding
|
| + // AnimationEvent::FINISHED event is sent or received. This means that if
|
| + // we don't have an events vector, we must ensure that non-aborted animations
|
| + // have received a finished event before marking them for deletion.
|
| + for (size_t i = 0; i < animations_.size(); i++) {
|
| + int group_id = animations_[i]->group();
|
| + if (animations_[i]->run_state() == Animation::ABORTED) {
|
| + if (events && !animations_[i]->is_impl_only()) {
|
| + AnimationEvent aborted_event(
|
| + AnimationEvent::ABORTED, element_id_, group_id,
|
| + animations_[i]->target_property(), monotonic_time);
|
| + events->events_.push_back(aborted_event);
|
| + }
|
| + // If on the compositor or on the main thread and received finish event,
|
| + // animation can be marked for deletion.
|
| + if (events || animations_[i]->received_finished_event()) {
|
| + animations_[i]->SetRunState(Animation::WAITING_FOR_DELETION,
|
| + monotonic_time);
|
| + marked_animations_for_deletions = true;
|
| + }
|
| + continue;
|
| + }
|
| +
|
| + // If running on the compositor and need to complete an aborted animation
|
| + // on the main thread.
|
| + if (events &&
|
| + animations_[i]->run_state() ==
|
| + Animation::ABORTED_BUT_NEEDS_COMPLETION) {
|
| + AnimationEvent aborted_event(AnimationEvent::TAKEOVER, element_id_,
|
| + group_id, animations_[i]->target_property(),
|
| + monotonic_time);
|
| + aborted_event.animation_start_time =
|
| + (animations_[i]->start_time() - base::TimeTicks()).InSecondsF();
|
| + const ScrollOffsetAnimationCurve* scroll_offset_animation_curve =
|
| + animations_[i]->curve()->ToScrollOffsetAnimationCurve();
|
| + aborted_event.curve = scroll_offset_animation_curve->Clone();
|
| + // Notify the compositor that the animation is finished.
|
| + if (animation_delegate_) {
|
| + animation_delegate_->NotifyAnimationFinished(
|
| + aborted_event.monotonic_time, aborted_event.target_property,
|
| + aborted_event.group_id);
|
| + }
|
| + // Notify main thread.
|
| + events->events_.push_back(aborted_event);
|
| +
|
| + // Remove the animation from the compositor.
|
| + animations_[i]->SetRunState(Animation::WAITING_FOR_DELETION,
|
| + monotonic_time);
|
| + marked_animations_for_deletions = true;
|
| + continue;
|
| + }
|
| +
|
| + bool all_anims_with_same_id_are_finished = false;
|
| +
|
| + // Since deleting an animation on the main thread leads to its deletion
|
| + // on the impl thread, we only mark a FINISHED main thread animation for
|
| + // deletion once it has received a FINISHED event from the impl thread.
|
| + bool animation_i_will_send_or_has_received_finish_event =
|
| + animations_[i]->is_controlling_instance() ||
|
| + animations_[i]->is_impl_only() ||
|
| + animations_[i]->received_finished_event();
|
| + // If an animation is finished, and not already marked for deletion,
|
| + // find out if all other animations in the same group are also finished.
|
| + if (animations_[i]->run_state() == Animation::FINISHED &&
|
| + animation_i_will_send_or_has_received_finish_event) {
|
| + // Clear the animations_with_same_group_id if it was added for
|
| + // the previous animation's iteration.
|
| + if (animations_with_same_group_id.size() > 0)
|
| + animations_with_same_group_id.clear();
|
| + all_anims_with_same_id_are_finished = true;
|
| + for (size_t j = 0; j < animations_.size(); ++j) {
|
| + bool animation_j_will_send_or_has_received_finish_event =
|
| + animations_[j]->is_controlling_instance() ||
|
| + animations_[j]->is_impl_only() ||
|
| + animations_[j]->received_finished_event();
|
| + if (group_id == animations_[j]->group()) {
|
| + if (!animations_[j]->is_finished() ||
|
| + (animations_[j]->run_state() == Animation::FINISHED &&
|
| + !animation_j_will_send_or_has_received_finish_event)) {
|
| + all_anims_with_same_id_are_finished = false;
|
| + break;
|
| + } else if (j >= i &&
|
| + animations_[j]->run_state() != Animation::ABORTED) {
|
| + // Mark down the animations which belong to the same group
|
| + // and is not yet aborted. If this current iteration finds that all
|
| + // animations with same ID are finished, then the marked
|
| + // animations below will be set to WAITING_FOR_DELETION in next
|
| + // iteration.
|
| + animations_with_same_group_id.push_back(j);
|
| + }
|
| + }
|
| + }
|
| + }
|
| + if (all_anims_with_same_id_are_finished) {
|
| + // We now need to remove all animations with the same group id as
|
| + // group_id (and send along animation finished notifications, if
|
| + // necessary).
|
| + for (size_t j = 0; j < animations_with_same_group_id.size(); j++) {
|
| + size_t animation_index = animations_with_same_group_id[j];
|
| + if (events) {
|
| + AnimationEvent finished_event(
|
| + AnimationEvent::FINISHED, element_id_,
|
| + animations_[animation_index]->group(),
|
| + animations_[animation_index]->target_property(), monotonic_time);
|
| + finished_event.is_impl_only =
|
| + animations_[animation_index]->is_impl_only();
|
| + if (finished_event.is_impl_only) {
|
| + // Notify delegate directly, do not record the event.
|
| + if (animation_delegate_) {
|
| + animation_delegate_->NotifyAnimationFinished(
|
| + finished_event.monotonic_time, finished_event.target_property,
|
| + finished_event.group_id);
|
| + }
|
| + } else {
|
| + events->events_.push_back(finished_event);
|
| + }
|
| + }
|
| + animations_[animation_index]->SetRunState(
|
| + Animation::WAITING_FOR_DELETION, monotonic_time);
|
| + }
|
| + marked_animations_for_deletions = true;
|
| + }
|
| + }
|
| +
|
| + // Notify about animations waiting for deletion.
|
| + // We need to purge animations marked for deletion, which happens in
|
| + // PushProperties().
|
| + if (marked_animations_for_deletions)
|
| + SetNeedsPushProperties();
|
| +}
|
| +
|
| +void AnimationPlayer::TickAnimations(base::TimeTicks monotonic_time) {
|
| + DCHECK(element_animations_);
|
| +
|
| + for (size_t i = 0; i < animations_.size(); ++i) {
|
| + if (animations_[i]->run_state() == Animation::STARTING ||
|
| + animations_[i]->run_state() == Animation::RUNNING ||
|
| + animations_[i]->run_state() == Animation::PAUSED) {
|
| + if (!animations_[i]->InEffect(monotonic_time))
|
| + continue;
|
| +
|
| + base::TimeDelta trimmed =
|
| + animations_[i]->TrimTimeToCurrentIteration(monotonic_time);
|
| +
|
| + switch (animations_[i]->target_property()) {
|
| + case TargetProperty::TRANSFORM: {
|
| + const TransformAnimationCurve* transform_animation_curve =
|
| + animations_[i]->curve()->ToTransformAnimationCurve();
|
| + const gfx::Transform transform =
|
| + transform_animation_curve->GetValue(trimmed);
|
| + element_animations_->NotifyClientTransformAnimated(
|
| + transform, animations_[i]->affects_active_elements(),
|
| + animations_[i]->affects_pending_elements());
|
| + break;
|
| + }
|
| +
|
| + case TargetProperty::OPACITY: {
|
| + const FloatAnimationCurve* float_animation_curve =
|
| + animations_[i]->curve()->ToFloatAnimationCurve();
|
| + const float opacity = std::max(
|
| + std::min(float_animation_curve->GetValue(trimmed), 1.0f), 0.f);
|
| + element_animations_->NotifyClientOpacityAnimated(
|
| + opacity, animations_[i]->affects_active_elements(),
|
| + animations_[i]->affects_pending_elements());
|
| + break;
|
| + }
|
| +
|
| + case TargetProperty::FILTER: {
|
| + const FilterAnimationCurve* filter_animation_curve =
|
| + animations_[i]->curve()->ToFilterAnimationCurve();
|
| + const FilterOperations filter =
|
| + filter_animation_curve->GetValue(trimmed);
|
| + element_animations_->NotifyClientFilterAnimated(
|
| + filter, animations_[i]->affects_active_elements(),
|
| + animations_[i]->affects_pending_elements());
|
| + break;
|
| + }
|
| +
|
| + case TargetProperty::BACKGROUND_COLOR: {
|
| + // Not yet implemented.
|
| + break;
|
| + }
|
| +
|
| + case TargetProperty::SCROLL_OFFSET: {
|
| + const ScrollOffsetAnimationCurve* scroll_offset_animation_curve =
|
| + animations_[i]->curve()->ToScrollOffsetAnimationCurve();
|
| + const gfx::ScrollOffset scroll_offset =
|
| + scroll_offset_animation_curve->GetValue(trimmed);
|
| + element_animations_->NotifyClientScrollOffsetAnimated(
|
| + scroll_offset, animations_[i]->affects_active_elements(),
|
| + animations_[i]->affects_pending_elements());
|
| + break;
|
| + }
|
| + }
|
| + }
|
| + }
|
| +
|
| + last_tick_time_ = monotonic_time;
|
| +}
|
| +
|
| +void AnimationPlayer::MarkFinishedAnimations(base::TimeTicks monotonic_time) {
|
| + bool finished_transform_animation = false;
|
| + bool finished_opacity_animation = false;
|
| + bool finished_filter_animation = false;
|
| + for (size_t i = 0; i < animations_.size(); ++i) {
|
| + if (!animations_[i]->is_finished() &&
|
| + animations_[i]->IsFinishedAt(monotonic_time)) {
|
| + animations_[i]->SetRunState(Animation::FINISHED, monotonic_time);
|
| + if (animations_[i]->target_property() == TargetProperty::TRANSFORM)
|
| + finished_transform_animation = true;
|
| + else if (animations_[i]->target_property() == TargetProperty::OPACITY)
|
| + finished_opacity_animation = true;
|
| + else if (animations_[i]->target_property() == TargetProperty::FILTER)
|
| + finished_filter_animation = true;
|
| + }
|
| + }
|
| +
|
| + DCHECK(element_animations_);
|
| + element_animations_->UpdateClientAnimationState(finished_transform_animation,
|
| + finished_opacity_animation,
|
| + finished_filter_animation);
|
| +}
|
| +
|
| +void AnimationPlayer::ActivateAnimations(bool* changed_transform_animation,
|
| + bool* changed_opacity_animation,
|
| + bool* changed_filter_animation) {
|
| + for (size_t i = 0; i < animations_.size(); ++i) {
|
| + if (animations_[i]->affects_active_elements() !=
|
| + animations_[i]->affects_pending_elements()) {
|
| + if (animations_[i]->target_property() == TargetProperty::TRANSFORM)
|
| + *changed_transform_animation = true;
|
| + else if (animations_[i]->target_property() == TargetProperty::OPACITY)
|
| + *changed_opacity_animation = true;
|
| + else if (animations_[i]->target_property() == TargetProperty::FILTER)
|
| + *changed_filter_animation = true;
|
| + }
|
| + animations_[i]->set_affects_active_elements(
|
| + animations_[i]->affects_pending_elements());
|
| + }
|
| + auto affects_no_elements = [](const std::unique_ptr<Animation>& animation) {
|
| + return !animation->affects_active_elements() &&
|
| + !animation->affects_pending_elements();
|
| + };
|
| + animations_.erase(std::remove_if(animations_.begin(), animations_.end(),
|
| + affects_no_elements),
|
| + animations_.end());
|
| +}
|
| +
|
| +bool AnimationPlayer::HasFilterAnimationThatInflatesBounds() const {
|
| + for (size_t i = 0; i < animations_.size(); ++i) {
|
| + if (!animations_[i]->is_finished() &&
|
| + animations_[i]->target_property() == TargetProperty::FILTER &&
|
| + animations_[i]
|
| + ->curve()
|
| + ->ToFilterAnimationCurve()
|
| + ->HasFilterThatMovesPixels())
|
| + return true;
|
| + }
|
| +
|
| + return false;
|
| +}
|
| +
|
| +bool AnimationPlayer::HasTransformAnimationThatInflatesBounds() const {
|
| + return IsCurrentlyAnimatingProperty(TargetProperty::TRANSFORM,
|
| + ElementListType::ACTIVE) ||
|
| + IsCurrentlyAnimatingProperty(TargetProperty::TRANSFORM,
|
| + ElementListType::PENDING);
|
| +}
|
| +
|
| +bool AnimationPlayer::TransformAnimationBoundsForBox(const gfx::BoxF& box,
|
| + gfx::BoxF* bounds) const {
|
| + DCHECK(HasTransformAnimationThatInflatesBounds())
|
| + << "TransformAnimationBoundsForBox will give incorrect results if there "
|
| + << "are no transform animations affecting bounds, non-animated transform "
|
| + << "is not known";
|
| +
|
| + // Compute bounds based on animations for which is_finished() is false.
|
| + // Do nothing if there are no such animations; in this case, it is assumed
|
| + // that callers will take care of computing bounds based on the owning layer's
|
| + // actual transform.
|
| + *bounds = gfx::BoxF();
|
| + for (size_t i = 0; i < animations_.size(); ++i) {
|
| + if (animations_[i]->is_finished() ||
|
| + animations_[i]->target_property() != TargetProperty::TRANSFORM)
|
| + continue;
|
| +
|
| + const TransformAnimationCurve* transform_animation_curve =
|
| + animations_[i]->curve()->ToTransformAnimationCurve();
|
| + gfx::BoxF animation_bounds;
|
| + bool success =
|
| + transform_animation_curve->AnimatedBoundsForBox(box, &animation_bounds);
|
| + if (!success)
|
| + return false;
|
| + bounds->Union(animation_bounds);
|
| + }
|
| +
|
| + return true;
|
| +}
|
| +
|
| +bool AnimationPlayer::HasAnimationThatAffectsScale() const {
|
| + for (size_t i = 0; i < animations_.size(); ++i) {
|
| + if (animations_[i]->is_finished() ||
|
| + animations_[i]->target_property() != TargetProperty::TRANSFORM)
|
| + continue;
|
| +
|
| + const TransformAnimationCurve* transform_animation_curve =
|
| + animations_[i]->curve()->ToTransformAnimationCurve();
|
| + if (transform_animation_curve->AffectsScale())
|
| + return true;
|
| + }
|
| +
|
| + return false;
|
| +}
|
| +
|
| +bool AnimationPlayer::HasOnlyTranslationTransforms(
|
| + ElementListType list_type) const {
|
| + for (size_t i = 0; i < animations_.size(); ++i) {
|
| + if (animations_[i]->is_finished() ||
|
| + animations_[i]->target_property() != TargetProperty::TRANSFORM)
|
| + continue;
|
| +
|
| + if ((list_type == ElementListType::ACTIVE &&
|
| + !animations_[i]->affects_active_elements()) ||
|
| + (list_type == ElementListType::PENDING &&
|
| + !animations_[i]->affects_pending_elements()))
|
| + continue;
|
| +
|
| + const TransformAnimationCurve* transform_animation_curve =
|
| + animations_[i]->curve()->ToTransformAnimationCurve();
|
| + if (!transform_animation_curve->IsTranslation())
|
| + return false;
|
| + }
|
| +
|
| + return true;
|
| +}
|
| +
|
| +bool AnimationPlayer::AnimationsPreserveAxisAlignment() const {
|
| + for (size_t i = 0; i < animations_.size(); ++i) {
|
| + if (animations_[i]->is_finished() ||
|
| + animations_[i]->target_property() != TargetProperty::TRANSFORM)
|
| + continue;
|
| +
|
| + const TransformAnimationCurve* transform_animation_curve =
|
| + animations_[i]->curve()->ToTransformAnimationCurve();
|
| + if (!transform_animation_curve->PreservesAxisAlignment())
|
| + return false;
|
| + }
|
| +
|
| + return true;
|
| +}
|
| +
|
| +bool AnimationPlayer::AnimationStartScale(ElementListType list_type,
|
| + float* start_scale) const {
|
| + *start_scale = 0.f;
|
| + for (size_t i = 0; i < animations_.size(); ++i) {
|
| + if (animations_[i]->is_finished() ||
|
| + animations_[i]->target_property() != TargetProperty::TRANSFORM)
|
| + continue;
|
| +
|
| + if ((list_type == ElementListType::ACTIVE &&
|
| + !animations_[i]->affects_active_elements()) ||
|
| + (list_type == ElementListType::PENDING &&
|
| + !animations_[i]->affects_pending_elements()))
|
| + continue;
|
| +
|
| + bool forward_direction = true;
|
| + switch (animations_[i]->direction()) {
|
| + case Animation::Direction::NORMAL:
|
| + case Animation::Direction::ALTERNATE_NORMAL:
|
| + forward_direction = animations_[i]->playback_rate() >= 0.0;
|
| + break;
|
| + case Animation::Direction::REVERSE:
|
| + case Animation::Direction::ALTERNATE_REVERSE:
|
| + forward_direction = animations_[i]->playback_rate() < 0.0;
|
| + break;
|
| + }
|
| +
|
| + const TransformAnimationCurve* transform_animation_curve =
|
| + animations_[i]->curve()->ToTransformAnimationCurve();
|
| + float animation_start_scale = 0.f;
|
| + if (!transform_animation_curve->AnimationStartScale(forward_direction,
|
| + &animation_start_scale))
|
| + return false;
|
| + *start_scale = std::max(*start_scale, animation_start_scale);
|
| + }
|
| + return true;
|
| +}
|
| +
|
| +bool AnimationPlayer::MaximumTargetScale(ElementListType list_type,
|
| + float* max_scale) const {
|
| + *max_scale = 0.f;
|
| + for (size_t i = 0; i < animations_.size(); ++i) {
|
| + if (animations_[i]->is_finished() ||
|
| + animations_[i]->target_property() != TargetProperty::TRANSFORM)
|
| + continue;
|
| +
|
| + if ((list_type == ElementListType::ACTIVE &&
|
| + !animations_[i]->affects_active_elements()) ||
|
| + (list_type == ElementListType::PENDING &&
|
| + !animations_[i]->affects_pending_elements()))
|
| + continue;
|
| +
|
| + bool forward_direction = true;
|
| + switch (animations_[i]->direction()) {
|
| + case Animation::Direction::NORMAL:
|
| + case Animation::Direction::ALTERNATE_NORMAL:
|
| + forward_direction = animations_[i]->playback_rate() >= 0.0;
|
| + break;
|
| + case Animation::Direction::REVERSE:
|
| + case Animation::Direction::ALTERNATE_REVERSE:
|
| + forward_direction = animations_[i]->playback_rate() < 0.0;
|
| + break;
|
| + }
|
| +
|
| + const TransformAnimationCurve* transform_animation_curve =
|
| + animations_[i]->curve()->ToTransformAnimationCurve();
|
| + float animation_scale = 0.f;
|
| + if (!transform_animation_curve->MaximumTargetScale(forward_direction,
|
| + &animation_scale))
|
| + return false;
|
| + *max_scale = std::max(*max_scale, animation_scale);
|
| + }
|
| +
|
| + return true;
|
| +}
|
| +
|
| +bool AnimationPlayer::IsPotentiallyAnimatingProperty(
|
| + TargetProperty::Type target_property,
|
| + ElementListType list_type) const {
|
| + for (size_t i = 0; i < animations_.size(); ++i) {
|
| + if (!animations_[i]->is_finished() &&
|
| + animations_[i]->target_property() == target_property) {
|
| + if ((list_type == ElementListType::ACTIVE &&
|
| + animations_[i]->affects_active_elements()) ||
|
| + (list_type == ElementListType::PENDING &&
|
| + animations_[i]->affects_pending_elements()))
|
| + return true;
|
| + }
|
| + }
|
| + return false;
|
| +}
|
| +
|
| +bool AnimationPlayer::IsCurrentlyAnimatingProperty(
|
| + TargetProperty::Type target_property,
|
| + ElementListType list_type) const {
|
| + for (size_t i = 0; i < animations_.size(); ++i) {
|
| + if (!animations_[i]->is_finished() &&
|
| + animations_[i]->InEffect(last_tick_time_) &&
|
| + animations_[i]->target_property() == target_property) {
|
| + if ((list_type == ElementListType::ACTIVE &&
|
| + animations_[i]->affects_active_elements()) ||
|
| + (list_type == ElementListType::PENDING &&
|
| + animations_[i]->affects_pending_elements()))
|
| + return true;
|
| + }
|
| + }
|
| + return false;
|
| +}
|
| +
|
| +bool AnimationPlayer::HasElementInActiveList() const {
|
| + DCHECK(element_animations_);
|
| + return element_animations_->has_element_in_active_list();
|
| +}
|
| +
|
| +gfx::ScrollOffset AnimationPlayer::ScrollOffsetForAnimation() const {
|
| + DCHECK(element_animations_);
|
| + return element_animations_->ScrollOffsetForAnimation();
|
| +}
|
| +
|
| +Animation* AnimationPlayer::GetAnimation(
|
| + TargetProperty::Type target_property) const {
|
| + for (size_t i = 0; i < animations_.size(); ++i) {
|
| + size_t index = animations_.size() - i - 1;
|
| + if (animations_[index]->target_property() == target_property)
|
| + return animations_[index].get();
|
| + }
|
| + return nullptr;
|
| +}
|
| +
|
| +Animation* AnimationPlayer::GetAnimationById(int animation_id) const {
|
| + for (size_t i = 0; i < animations_.size(); ++i)
|
| + if (animations_[i]->id() == animation_id)
|
| + return animations_[i].get();
|
| + return nullptr;
|
| +}
|
| +
|
| +void AnimationPlayer::MarkAbortedAnimationsForDeletion(
|
| + AnimationPlayer* animation_player_impl) const {
|
| + bool aborted_transform_animation = false;
|
| + bool aborted_opacity_animation = false;
|
| + bool aborted_filter_animation = false;
|
| + auto& animations_impl = animation_player_impl->animations_;
|
| + for (const auto& animation_impl : animations_impl) {
|
| + // If the animation has been aborted on the main thread, mark it for
|
| + // deletion.
|
| + if (Animation* animation = GetAnimationById(animation_impl->id())) {
|
| + if (animation->run_state() == Animation::ABORTED) {
|
| + animation_impl->SetRunState(Animation::WAITING_FOR_DELETION,
|
| + animation_player_impl->last_tick_time_);
|
| + animation->SetRunState(Animation::WAITING_FOR_DELETION,
|
| + last_tick_time_);
|
| + if (animation_impl->target_property() == TargetProperty::TRANSFORM)
|
| + aborted_transform_animation = true;
|
| + else if (animation_impl->target_property() == TargetProperty::OPACITY)
|
| + aborted_opacity_animation = true;
|
| + else if (animation_impl->target_property() == TargetProperty::FILTER)
|
| + aborted_filter_animation = true;
|
| + }
|
| + }
|
| + }
|
| +
|
| + if (element_animations_)
|
| + element_animations_->SetNeedsUpdateImplClientState(
|
| + aborted_transform_animation, aborted_opacity_animation,
|
| + aborted_filter_animation);
|
| +}
|
| +
|
| +void AnimationPlayer::PurgeAnimationsMarkedForDeletion() {
|
| + animations_.erase(
|
| + std::remove_if(animations_.begin(), animations_.end(),
|
| + [](const std::unique_ptr<Animation>& animation) {
|
| + return animation->run_state() ==
|
| + Animation::WAITING_FOR_DELETION;
|
| + }),
|
| + animations_.end());
|
| +}
|
| +
|
| +void AnimationPlayer::PushNewAnimationsToImplThread(
|
| + AnimationPlayer* animation_player_impl) const {
|
| + // Any new animations owned by the main thread's ElementAnimations are cloned
|
| + // and added to the impl thread's ElementAnimations.
|
| + for (size_t i = 0; i < animations_.size(); ++i) {
|
| + // If the animation is already running on the impl thread, there is no
|
| + // need to copy it over.
|
| + if (animation_player_impl->GetAnimationById(animations_[i]->id()))
|
| + continue;
|
| +
|
| + if (animations_[i]->target_property() == TargetProperty::SCROLL_OFFSET &&
|
| + !animations_[i]
|
| + ->curve()
|
| + ->ToScrollOffsetAnimationCurve()
|
| + ->HasSetInitialValue()) {
|
| + gfx::ScrollOffset current_scroll_offset;
|
| + if (animation_player_impl->HasElementInActiveList()) {
|
| + current_scroll_offset =
|
| + animation_player_impl->ScrollOffsetForAnimation();
|
| + } else {
|
| + // The owning layer isn't yet in the active tree, so the main thread
|
| + // scroll offset will be up to date.
|
| + current_scroll_offset = ScrollOffsetForAnimation();
|
| + }
|
| + animations_[i]->curve()->ToScrollOffsetAnimationCurve()->SetInitialValue(
|
| + current_scroll_offset);
|
| + }
|
| +
|
| + // The new animation should be set to run as soon as possible.
|
| + Animation::RunState initial_run_state =
|
| + Animation::WAITING_FOR_TARGET_AVAILABILITY;
|
| + std::unique_ptr<Animation> to_add(
|
| + animations_[i]->CloneAndInitialize(initial_run_state));
|
| + DCHECK(!to_add->needs_synchronized_start_time());
|
| + to_add->set_affects_active_elements(false);
|
| + animation_player_impl->AddAnimation(std::move(to_add));
|
| + }
|
| +}
|
| +
|
| +static bool IsCompleted(Animation* animation,
|
| + const AnimationPlayer* main_thread_player) {
|
| + if (animation->is_impl_only()) {
|
| + return (animation->run_state() == Animation::WAITING_FOR_DELETION);
|
| + } else {
|
| + return !main_thread_player->GetAnimationById(animation->id());
|
| + }
|
| +}
|
| +
|
| +void AnimationPlayer::RemoveAnimationsCompletedOnMainThread(
|
| + AnimationPlayer* animation_player_impl) const {
|
| + bool removed_transform_animation = false;
|
| + bool removed_opacity_animation = false;
|
| + bool removed_filter_animation = false;
|
| + // Animations removed on the main thread should no longer affect pending
|
| + // elements, and should stop affecting active elements after the next call
|
| + // to ActivateAnimations. If already WAITING_FOR_DELETION, they can be removed
|
| + // immediately.
|
| + auto& animations = animation_player_impl->animations_;
|
| + for (const auto& animation : animations) {
|
| + if (IsCompleted(animation.get(), this)) {
|
| + animation->set_affects_pending_elements(false);
|
| + if (animation->target_property() == TargetProperty::TRANSFORM)
|
| + removed_transform_animation = true;
|
| + else if (animation->target_property() == TargetProperty::OPACITY)
|
| + removed_opacity_animation = true;
|
| + else if (animation->target_property() == TargetProperty::FILTER)
|
| + removed_filter_animation = true;
|
| + }
|
| + }
|
| + auto affects_active_only_and_is_waiting_for_deletion =
|
| + [](const std::unique_ptr<Animation>& animation) {
|
| + return animation->run_state() == Animation::WAITING_FOR_DELETION &&
|
| + !animation->affects_pending_elements();
|
| + };
|
| + animations.erase(
|
| + std::remove_if(animations.begin(), animations.end(),
|
| + affects_active_only_and_is_waiting_for_deletion),
|
| + animations.end());
|
| +
|
| + if (element_animations_)
|
| + element_animations_->SetNeedsUpdateImplClientState(
|
| + removed_transform_animation, removed_opacity_animation,
|
| + removed_filter_animation);
|
| +}
|
| +
|
| +void AnimationPlayer::PushPropertiesToImplThread(
|
| + AnimationPlayer* animation_player_impl) {
|
| + for (size_t i = 0; i < animations_.size(); ++i) {
|
| + Animation* current_impl =
|
| + animation_player_impl->GetAnimationById(animations_[i]->id());
|
| + if (current_impl)
|
| + animations_[i]->PushPropertiesTo(current_impl);
|
| + }
|
| +}
|
| +
|
| } // namespace cc
|
|
|