| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ash/login/ui/auth_icon_view.h" |
| |
| #include "ash/login/ui/horizontal_image_sequence_animation_decoder.h" |
| #include "ash/style/ash_color_provider.h" |
| #include "ash/style/color_util.h" |
| #include "ui/base/metadata/metadata_impl_macros.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "ui/compositor/layer.h" |
| #include "ui/compositor/layer_animation_sequence.h" |
| #include "ui/compositor/layer_animator.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/geometry/size.h" |
| #include "ui/gfx/paint_throbber.h" |
| #include "ui/gfx/paint_vector_icon.h" |
| #include "ui/gfx/vector_icon_types.h" |
| #include "ui/views/border.h" |
| #include "ui/views/layout/box_layout.h" |
| #include "ui/views/view_class_properties.h" |
| |
| namespace ash { |
| |
| namespace { |
| |
| struct ShakeAnimationStep { |
| int x_offset; |
| int duration_ms; |
| }; |
| |
| constexpr int kAuthIconSizeDp = 32; |
| constexpr int kIconMarginDp = 10; |
| constexpr int kAuthIconViewDp = kAuthIconSizeDp + 2 * kIconMarginDp; |
| constexpr int kProgressAnimationStrokeWidth = 3; |
| |
| // This determines how frequently we paint the progress spinner. |
| // 1 frame / 30 msec = about 30 frames per second |
| // 30 fps seems to be smooth enough to look good without excessive painting. |
| constexpr base::TimeDelta kProgressFrameDuration = base::Milliseconds(30); |
| // How long the nudge animation takes to scale up and fade out. |
| constexpr base::TimeDelta kNudgeAnimationScalingDuration = |
| base::Milliseconds(2000); |
| // How long the delay between the repeating nudge animation takes in between |
| // cycles. |
| constexpr base::TimeDelta kNudgeAnimationDelayDuration = |
| base::Milliseconds(1000); |
| // How opaque the nudge animation will reset the view to. |
| constexpr float kOpacityReset = 0.5; |
| // Size that nudge animation scales view up and down by. |
| constexpr SkScalar kTransformScaleUpSize = 3; |
| // The interpolation of transform fails when scaling all the way down to 0. |
| constexpr SkScalar kTransformScaleDownSize = 0.01; |
| |
| // See spec: |
| // https://carbon.googleplex.com/cr-os-motion-work/pages/sign-in/undefined/e05c4091-eea2-4c5a-a6f8-38fd37953e7b#a929eb9f-2840-4b37-be52-97d96ca2aafa |
| constexpr ShakeAnimationStep kShakeAnimationSteps[] = { |
| {-5, 83}, {8, 83}, {-7, 66}, {7, 66}, {-7, 66}, {7, 66}, {-3, 83}}; |
| |
| SkColor GetColor(AuthIconView::Color color) { |
| switch (color) { |
| case AuthIconView::Color::kPrimary: |
| return AshColorProvider::Get()->GetContentLayerColor( |
| AshColorProvider::ContentLayerType::kIconColorPrimary); |
| case AuthIconView::Color::kDisabled: |
| return ColorUtil::GetDisabledColor( |
| GetColor(AuthIconView::Color::kPrimary)); |
| case AuthIconView::Color::kError: |
| // TODO(crbug.com/1233614): Either find a system color to match the color |
| // in the Fingerprint animation png sequence, or upload new png files with |
| // the right color. |
| return AshColorProvider::Get()->GetContentLayerColor( |
| AshColorProvider::ContentLayerType::kIconColorAlert); |
| case AuthIconView::Color::kPositive: |
| return AshColorProvider::Get()->GetContentLayerColor( |
| AshColorProvider::ContentLayerType::kIconColorPositive); |
| } |
| } |
| |
| } // namespace |
| |
| AuthIconView::AuthIconView() { |
| SetLayoutManager(std::make_unique<views::BoxLayout>()); |
| |
| icon_ = AddChildView(std::make_unique<AnimatedRoundedImageView>( |
| gfx::Size(kAuthIconSizeDp, kAuthIconSizeDp), |
| /*corner_radius=*/0)); |
| icon_->SetProperty(views::kMarginsKey, gfx::Insets(kIconMarginDp)); |
| |
| // Set up layer to allow for animation. |
| icon_->SetPaintToLayer(); |
| icon_->layer()->SetFillsBoundsOpaquely(false); |
| icon_->layer()->GetAnimator()->set_preemption_strategy( |
| ui::LayerAnimator::PreemptionStrategy::IMMEDIATELY_ANIMATE_TO_NEW_TARGET); |
| } |
| |
| AuthIconView::~AuthIconView() = default; |
| |
| void AuthIconView::SetIcon(const gfx::VectorIcon& icon, Color color) { |
| icon_->SetImage( |
| gfx::CreateVectorIcon(icon, kAuthIconSizeDp, GetColor(color))); |
| } |
| |
| void AuthIconView::SetCircleImage(int size, SkColor color) { |
| gfx::ImageSkia circle_icon = |
| gfx::CanvasImageSource::MakeImageSkia<CircleImageSource>(size, color); |
| icon_->SetImage(circle_icon); |
| } |
| |
| void AuthIconView::SetAnimation(int animation_resource_id, |
| base::TimeDelta duration, |
| int num_frames) { |
| icon_->SetAnimationDecoder( |
| std::make_unique<HorizontalImageSequenceAnimationDecoder>( |
| *ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed( |
| animation_resource_id), |
| duration, num_frames), |
| AnimatedRoundedImageView::Playback::kSingle); |
| } |
| |
| void AuthIconView::RunErrorShakeAnimation() { |
| StopAnimating(); |
| |
| auto transform_sequence = std::make_unique<ui::LayerAnimationSequence>(); |
| gfx::Transform transform; |
| for (const ShakeAnimationStep& step : kShakeAnimationSteps) { |
| transform.Translate(step.x_offset, /*y=*/0); |
| auto element = ui::LayerAnimationElement::CreateTransformElement( |
| transform, base::Milliseconds(step.duration_ms)); |
| element->set_tween_type(gfx::Tween::Type::EASE_IN_OUT_2); |
| transform_sequence->AddElement(std::move(element)); |
| } |
| |
| // Animator takes ownership of transform_sequence. |
| icon_->layer()->GetAnimator()->StartAnimation(transform_sequence.release()); |
| } |
| |
| void AuthIconView::RunNudgeAnimation() { |
| StopAnimating(); |
| |
| // Create two separate animation sequences and run in parallel. |
| auto opacity_sequence = std::make_unique<ui::LayerAnimationSequence>(); |
| auto transform_sequence = std::make_unique<ui::LayerAnimationSequence>(); |
| |
| // Fade out view by gradually setting opacity to 0. |
| auto element = ui::LayerAnimationElement::CreateOpacityElement( |
| 0, kNudgeAnimationScalingDuration); |
| element->set_tween_type(gfx::Tween::Type::ACCEL_0_80_DECEL_80); |
| opacity_sequence->AddElement(std::move(element)); |
| |
| // Reset opacity so that |opacity_sequence| can be repeated. |
| element = ui::LayerAnimationElement::CreateOpacityElement(kOpacityReset, |
| base::TimeDelta()); |
| opacity_sequence->AddElement(std::move(element)); |
| |
| element = ui::LayerAnimationElement::CreatePauseElement( |
| 0, kNudgeAnimationDelayDuration); |
| opacity_sequence->AddElement(std::move(element)); |
| |
| opacity_sequence->set_is_repeating(true); |
| |
| // Every time it scales, translate by |center_offset| so that the view scales |
| // outward from center point. |
| int half_icon_size = kAuthIconSizeDp / 2; |
| auto center_offset = gfx::Vector2d(half_icon_size, half_icon_size); |
| |
| gfx::Transform transform; |
| transform.Translate(center_offset); |
| // Make view larger. |
| transform.Scale(/*x=*/kTransformScaleUpSize, /*y=*/kTransformScaleUpSize); |
| transform.Translate(-center_offset); |
| element = ui::LayerAnimationElement::CreateTransformElement( |
| transform, kNudgeAnimationScalingDuration); |
| element->set_tween_type(gfx::Tween::Type::ACCEL_0_40_DECEL_100); |
| transform_sequence->AddElement(std::move(element)); |
| |
| transform = gfx::Transform(); |
| transform.Translate(center_offset); |
| // Make view smaller. |
| transform.Scale(/*x=*/kTransformScaleDownSize, /*y=*/kTransformScaleDownSize); |
| transform.Translate(-center_offset); |
| element = ui::LayerAnimationElement::CreateTransformElement( |
| transform, base::TimeDelta()); |
| transform_sequence->AddElement(std::move(element)); |
| |
| element = ui::LayerAnimationElement::CreatePauseElement( |
| 0, kNudgeAnimationDelayDuration); |
| transform_sequence->AddElement(std::move(element)); |
| |
| transform_sequence->set_is_repeating(true); |
| |
| // Animator takes ownership of opacity_sequence and transform_sequence. |
| icon_->layer()->GetAnimator()->StartAnimation(opacity_sequence.release()); |
| icon_->layer()->GetAnimator()->StartAnimation(transform_sequence.release()); |
| } |
| |
| void AuthIconView::StartProgressAnimation() { |
| // Progress animation already running. |
| if (progress_animation_timer_.IsRunning()) |
| return; |
| |
| progress_animation_start_time_ = base::TimeTicks::Now(); |
| progress_animation_timer_.Start( |
| FROM_HERE, kProgressFrameDuration, |
| base::BindRepeating(&AuthIconView::SchedulePaint, |
| base::Unretained(this))); |
| SchedulePaint(); |
| } |
| |
| void AuthIconView::StopProgressAnimation() { |
| // Progress already stopped. |
| if (!progress_animation_timer_.IsRunning()) |
| return; |
| |
| progress_animation_timer_.Stop(); |
| SchedulePaint(); |
| } |
| |
| void AuthIconView::StopAnimating() { |
| icon_->layer()->GetAnimator()->StopAnimating(); |
| } |
| |
| void AuthIconView::OnPaint(gfx::Canvas* canvas) { |
| // Draw the icon first. |
| views::View::OnPaint(canvas); |
| |
| // Draw the progress spinner on top if it's currently running. |
| if (progress_animation_timer_.IsRunning()) { |
| SkColor color = AshColorProvider::Get()->GetContentLayerColor( |
| AshColorProvider::ContentLayerType::kProgressBarColorForeground); |
| base::TimeDelta elapsed_time = |
| base::TimeTicks::Now() - progress_animation_start_time_; |
| gfx::PaintThrobberSpinning(canvas, GetContentsBounds(), color, elapsed_time, |
| kProgressAnimationStrokeWidth); |
| } |
| } |
| |
| gfx::Size AuthIconView::CalculatePreferredSize() const { |
| return gfx::Size(kAuthIconViewDp, kAuthIconViewDp); |
| } |
| |
| void AuthIconView::OnGestureEvent(ui::GestureEvent* event) { |
| if (event->type() != ui::ET_GESTURE_TAP && |
| event->type() != ui::ET_GESTURE_TAP_DOWN) |
| return; |
| |
| if (on_tap_or_click_callback_) { |
| on_tap_or_click_callback_.Run(); |
| } |
| } |
| |
| bool AuthIconView::OnMousePressed(const ui::MouseEvent& event) { |
| if (on_tap_or_click_callback_) { |
| on_tap_or_click_callback_.Run(); |
| return true; |
| } |
| return false; |
| } |
| |
| AuthIconView::CircleImageSource::CircleImageSource(int size, SkColor color) |
| : gfx::CanvasImageSource(gfx::Size(size, size)), color_(color) {} |
| |
| void AuthIconView::CircleImageSource::Draw(gfx::Canvas* canvas) { |
| float radius = size().width() / 2.0f; |
| cc::PaintFlags flags; |
| flags.setStyle(cc::PaintFlags::kFill_Style); |
| flags.setAntiAlias(true); |
| flags.setColor(color_); |
| canvas->DrawCircle(gfx::PointF(radius, radius), radius, flags); |
| } |
| |
| BEGIN_METADATA(AuthIconView, View) |
| END_METADATA |
| |
| } // namespace ash |