| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ash/wm/gestures/long_press_affordance_handler.h" |
| |
| #include "ash/display/display_controller.h" |
| #include "ash/root_window_controller.h" |
| #include "ash/shell.h" |
| #include "ash/shell_window_ids.h" |
| #include "third_party/skia/include/core/SkColor.h" |
| #include "third_party/skia/include/core/SkPaint.h" |
| #include "third_party/skia/include/core/SkPath.h" |
| #include "third_party/skia/include/core/SkRect.h" |
| #include "third_party/skia/include/effects/SkGradientShader.h" |
| #include "ui/aura/client/screen_position_client.h" |
| #include "ui/aura/root_window.h" |
| #include "ui/aura/window.h" |
| #include "ui/compositor/layer.h" |
| #include "ui/events/gestures/gesture_configuration.h" |
| #include "ui/events/gestures/gesture_util.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/screen.h" |
| #include "ui/gfx/transform.h" |
| #include "ui/views/view.h" |
| #include "ui/views/widget/widget.h" |
| |
| namespace ash { |
| namespace internal { |
| namespace { |
| |
| const int kAffordanceOuterRadius = 60; |
| const int kAffordanceInnerRadius = 50; |
| |
| // Angles from x-axis at which the outer and inner circles start. |
| const int kAffordanceOuterStartAngle = -109; |
| const int kAffordanceInnerStartAngle = -65; |
| |
| const int kAffordanceGlowWidth = 20; |
| // The following is half width to avoid division by 2. |
| const int kAffordanceArcWidth = 3; |
| |
| // Start and end values for various animations. |
| const double kAffordanceScaleStartValue = 0.8; |
| const double kAffordanceScaleEndValue = 1.0; |
| const double kAffordanceShrinkScaleEndValue = 0.5; |
| const double kAffordanceOpacityStartValue = 0.1; |
| const double kAffordanceOpacityEndValue = 0.5; |
| const int kAffordanceAngleStartValue = 0; |
| // The end angle is a bit greater than 360 to make sure the circle completes at |
| // the end of the animation. |
| const int kAffordanceAngleEndValue = 380; |
| const int kAffordanceDelayBeforeShrinkMs = 200; |
| const int kAffordanceShrinkAnimationDurationMs = 100; |
| |
| // Visual constants. |
| const SkColor kAffordanceGlowStartColor = SkColorSetARGB(24, 255, 255, 255); |
| const SkColor kAffordanceGlowEndColor = SkColorSetARGB(0, 255, 255, 255); |
| const SkColor kAffordanceArcColor = SkColorSetARGB(80, 0, 0, 0); |
| const int kAffordanceFrameRateHz = 60; |
| |
| views::Widget* CreateAffordanceWidget(aura::Window* root_window) { |
| views::Widget* widget = new views::Widget; |
| views::Widget::InitParams params; |
| params.type = views::Widget::InitParams::TYPE_WINDOW_FRAMELESS; |
| params.keep_on_top = true; |
| params.accept_events = false; |
| params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; |
| params.context = root_window; |
| params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW; |
| widget->Init(params); |
| widget->SetOpacity(0xFF); |
| GetRootWindowController(root_window)->GetContainer( |
| kShellWindowId_OverlayContainer)->AddChild(widget->GetNativeWindow()); |
| return widget; |
| } |
| |
| void PaintAffordanceArc(gfx::Canvas* canvas, |
| gfx::Point& center, |
| int radius, |
| int start_angle, |
| int end_angle) { |
| SkPaint paint; |
| paint.setStyle(SkPaint::kStroke_Style); |
| paint.setStrokeWidth(2 * kAffordanceArcWidth); |
| paint.setColor(kAffordanceArcColor); |
| paint.setAntiAlias(true); |
| |
| SkPath arc_path; |
| arc_path.addArc(SkRect::MakeXYWH(center.x() - radius, |
| center.y() - radius, |
| 2 * radius, |
| 2 * radius), |
| start_angle, end_angle); |
| canvas->DrawPath(arc_path, paint); |
| } |
| |
| void PaintAffordanceGlow(gfx::Canvas* canvas, |
| gfx::Point& center, |
| int start_radius, |
| int end_radius, |
| SkColor* colors, |
| SkScalar* pos, |
| int num_colors) { |
| SkPoint sk_center; |
| int radius = (end_radius + start_radius) / 2; |
| int glow_width = end_radius - start_radius; |
| sk_center.iset(center.x(), center.y()); |
| skia::RefPtr<SkShader> shader = skia::AdoptRef( |
| SkGradientShader::CreateTwoPointRadial( |
| sk_center, |
| SkIntToScalar(start_radius), |
| sk_center, |
| SkIntToScalar(end_radius), |
| colors, |
| pos, |
| num_colors, |
| SkShader::kClamp_TileMode)); |
| DCHECK(shader); |
| SkPaint paint; |
| paint.setStyle(SkPaint::kStroke_Style); |
| paint.setStrokeWidth(glow_width); |
| paint.setShader(shader.get()); |
| paint.setAntiAlias(true); |
| SkPath arc_path; |
| arc_path.addArc(SkRect::MakeXYWH(center.x() - radius, |
| center.y() - radius, |
| 2 * radius, |
| 2 * radius), |
| 0, 360); |
| canvas->DrawPath(arc_path, paint); |
| } |
| |
| } // namespace |
| |
| // View of the LongPressAffordanceHandler. Draws the actual contents and |
| // updates as the animation proceeds. It also maintains the views::Widget that |
| // the animation is shown in. |
| class LongPressAffordanceHandler::LongPressAffordanceView |
| : public views::View { |
| public: |
| LongPressAffordanceView(const gfx::Point& event_location, |
| aura::Window* root_window) |
| : views::View(), |
| widget_(CreateAffordanceWidget(root_window)), |
| current_angle_(kAffordanceAngleStartValue), |
| current_scale_(kAffordanceScaleStartValue) { |
| widget_->SetContentsView(this); |
| widget_->SetAlwaysOnTop(true); |
| |
| // We are owned by the LongPressAffordance. |
| set_owned_by_client(); |
| gfx::Point point = event_location; |
| aura::client::GetScreenPositionClient(root_window)->ConvertPointToScreen( |
| root_window, &point); |
| widget_->SetBounds(gfx::Rect( |
| point.x() - (kAffordanceOuterRadius + kAffordanceGlowWidth), |
| point.y() - (kAffordanceOuterRadius + kAffordanceGlowWidth), |
| GetPreferredSize().width(), |
| GetPreferredSize().height())); |
| widget_->Show(); |
| widget_->GetNativeView()->layer()->SetOpacity(kAffordanceOpacityStartValue); |
| } |
| |
| virtual ~LongPressAffordanceView() { |
| } |
| |
| void UpdateWithGrowAnimation(gfx::Animation* animation) { |
| // Update the portion of the circle filled so far and re-draw. |
| current_angle_ = animation->CurrentValueBetween(kAffordanceAngleStartValue, |
| kAffordanceAngleEndValue); |
| current_scale_ = animation->CurrentValueBetween(kAffordanceScaleStartValue, |
| kAffordanceScaleEndValue); |
| widget_->GetNativeView()->layer()->SetOpacity( |
| animation->CurrentValueBetween(kAffordanceOpacityStartValue, |
| kAffordanceOpacityEndValue)); |
| SchedulePaint(); |
| } |
| |
| void UpdateWithShrinkAnimation(gfx::Animation* animation) { |
| current_scale_ = animation->CurrentValueBetween(kAffordanceScaleEndValue, |
| kAffordanceShrinkScaleEndValue); |
| widget_->GetNativeView()->layer()->SetOpacity( |
| animation->CurrentValueBetween(kAffordanceOpacityEndValue, |
| kAffordanceOpacityStartValue)); |
| SchedulePaint(); |
| } |
| |
| private: |
| // Overridden from views::View. |
| virtual gfx::Size GetPreferredSize() OVERRIDE { |
| return gfx::Size(2 * (kAffordanceOuterRadius + kAffordanceGlowWidth), |
| 2 * (kAffordanceOuterRadius + kAffordanceGlowWidth)); |
| } |
| |
| virtual void OnPaint(gfx::Canvas* canvas) OVERRIDE { |
| gfx::Point center(GetPreferredSize().width() / 2, |
| GetPreferredSize().height() / 2); |
| canvas->Save(); |
| |
| gfx::Transform scale; |
| scale.Scale(current_scale_, current_scale_); |
| // We want to scale from the center. |
| canvas->Translate(center.OffsetFromOrigin()); |
| canvas->Transform(scale); |
| canvas->Translate(-center.OffsetFromOrigin()); |
| |
| // Paint affordance glow |
| int start_radius = kAffordanceInnerRadius - kAffordanceGlowWidth; |
| int end_radius = kAffordanceOuterRadius + kAffordanceGlowWidth; |
| const int num_colors = 3; |
| SkScalar pos[num_colors] = {0, 0.5, 1}; |
| SkColor colors[num_colors] = {kAffordanceGlowEndColor, |
| kAffordanceGlowStartColor, kAffordanceGlowEndColor}; |
| PaintAffordanceGlow(canvas, center, start_radius, end_radius, colors, pos, |
| num_colors); |
| |
| // Paint inner circle. |
| PaintAffordanceArc(canvas, center, kAffordanceInnerRadius, |
| kAffordanceInnerStartAngle, -current_angle_); |
| // Paint outer circle. |
| PaintAffordanceArc(canvas, center, kAffordanceOuterRadius, |
| kAffordanceOuterStartAngle, current_angle_); |
| |
| canvas->Restore(); |
| } |
| |
| scoped_ptr<views::Widget> widget_; |
| int current_angle_; |
| double current_scale_; |
| |
| DISALLOW_COPY_AND_ASSIGN(LongPressAffordanceView); |
| }; |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // LongPressAffordanceHandler, public |
| |
| LongPressAffordanceHandler::LongPressAffordanceHandler() |
| : gfx::LinearAnimation(kAffordanceFrameRateHz, this), |
| tap_down_touch_id_(-1), |
| tap_down_display_id_(0), |
| current_animation_type_(NONE) {} |
| |
| LongPressAffordanceHandler::~LongPressAffordanceHandler() {} |
| |
| void LongPressAffordanceHandler::ProcessEvent(aura::Window* target, |
| ui::LocatedEvent* event, |
| int touch_id) { |
| // Once we have a touch id, we are only interested in event of that touch id. |
| if (tap_down_touch_id_ != -1 && tap_down_touch_id_ != touch_id) |
| return; |
| int64 timer_start_time_ms = |
| ui::GestureConfiguration::semi_long_press_time_in_seconds() * 1000; |
| switch (event->type()) { |
| case ui::ET_GESTURE_TAP_DOWN: |
| // Start animation. |
| tap_down_location_ = event->root_location(); |
| tap_down_touch_id_ = touch_id; |
| current_animation_type_ = GROW_ANIMATION; |
| tap_down_display_id_ = |
| Shell::GetScreen()->GetDisplayNearestWindow(target).id(); |
| timer_.Start(FROM_HERE, |
| base::TimeDelta::FromMilliseconds(timer_start_time_ms), |
| this, |
| &LongPressAffordanceHandler::StartAnimation); |
| break; |
| case ui::ET_TOUCH_MOVED: |
| // If animation is running, We want it to be robust to small finger |
| // movements. So we stop the animation only when the finger moves a |
| // certain distance. |
| if (!ui::gestures::IsInsideManhattanSquare( |
| event->root_location(), tap_down_location_)) |
| StopAnimation(); |
| break; |
| case ui::ET_GESTURE_SHOW_PRESS: |
| case ui::ET_TOUCH_CANCELLED: |
| case ui::ET_GESTURE_END: |
| // We will stop the animation on TOUCH_RELEASED. |
| break; |
| case ui::ET_GESTURE_LONG_PRESS: |
| if (is_animating()) |
| End(); |
| break; |
| default: |
| // On all other touch and gesture events, we hide the animation. |
| StopAnimation(); |
| break; |
| } |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // LongPressAffordanceHandler, private |
| |
| void LongPressAffordanceHandler::StartAnimation() { |
| aura::Window* root_window = NULL; |
| switch (current_animation_type_) { |
| case GROW_ANIMATION: |
| root_window = Shell::GetInstance()->display_controller()-> |
| GetRootWindowForDisplayId(tap_down_display_id_); |
| if (!root_window) { |
| StopAnimation(); |
| return; |
| } |
| view_.reset(new LongPressAffordanceView(tap_down_location_, root_window)); |
| SetDuration( |
| ui::GestureConfiguration::long_press_time_in_seconds() * 1000 - |
| ui::GestureConfiguration::semi_long_press_time_in_seconds() * 1000 - |
| kAffordanceDelayBeforeShrinkMs); |
| Start(); |
| break; |
| case SHRINK_ANIMATION: |
| SetDuration(kAffordanceShrinkAnimationDurationMs); |
| Start(); |
| break; |
| default: |
| NOTREACHED(); |
| break; |
| } |
| } |
| |
| void LongPressAffordanceHandler::StopAnimation() { |
| if (timer_.IsRunning()) |
| timer_.Stop(); |
| // Since, Animation::Stop() calls AnimationEnded(), we need to reset the |
| // |current_animation_type_| before Stop(), otherwise AnimationEnded() may |
| // start the timer again. |
| current_animation_type_ = NONE; |
| if (is_animating()) |
| Stop(); |
| view_.reset(); |
| tap_down_touch_id_ = -1; |
| tap_down_display_id_ = 0; |
| } |
| |
| void LongPressAffordanceHandler::AnimateToState(double state) { |
| DCHECK(view_.get()); |
| switch (current_animation_type_) { |
| case GROW_ANIMATION: |
| view_->UpdateWithGrowAnimation(this); |
| break; |
| case SHRINK_ANIMATION: |
| view_->UpdateWithShrinkAnimation(this); |
| break; |
| default: |
| NOTREACHED(); |
| break; |
| } |
| } |
| |
| bool LongPressAffordanceHandler::ShouldSendCanceledFromStop() { |
| return false; |
| } |
| |
| void LongPressAffordanceHandler::AnimationEnded( |
| const gfx::Animation* animation) { |
| switch (current_animation_type_) { |
| case GROW_ANIMATION: |
| current_animation_type_ = SHRINK_ANIMATION; |
| timer_.Start(FROM_HERE, |
| base::TimeDelta::FromMilliseconds(kAffordanceDelayBeforeShrinkMs), |
| this, &LongPressAffordanceHandler::StartAnimation); |
| break; |
| case SHRINK_ANIMATION: |
| current_animation_type_ = NONE; |
| // fall through to reset the view. |
| default: |
| view_.reset(); |
| tap_down_touch_id_ = -1; |
| tap_down_display_id_ = 0; |
| break; |
| } |
| } |
| |
| } // namespace internal |
| } // namespace ash |