1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "ash/wm/gestures/long_press_affordance_handler.h"
6
7#include "ash/display/display_controller.h"
8#include "ash/root_window_controller.h"
9#include "ash/shell.h"
10#include "ash/shell_window_ids.h"
11#include "third_party/skia/include/core/SkColor.h"
12#include "third_party/skia/include/core/SkPaint.h"
13#include "third_party/skia/include/core/SkPath.h"
14#include "third_party/skia/include/core/SkRect.h"
15#include "third_party/skia/include/effects/SkGradientShader.h"
16#include "ui/aura/client/screen_position_client.h"
17#include "ui/aura/root_window.h"
18#include "ui/aura/window.h"
19#include "ui/compositor/layer.h"
20#include "ui/events/gestures/gesture_configuration.h"
21#include "ui/events/gestures/gesture_util.h"
22#include "ui/gfx/canvas.h"
23#include "ui/gfx/screen.h"
24#include "ui/gfx/transform.h"
25#include "ui/views/view.h"
26#include "ui/views/widget/widget.h"
27
28namespace ash {
29namespace internal {
30namespace {
31
32const int kAffordanceOuterRadius = 60;
33const int kAffordanceInnerRadius = 50;
34
35// Angles from x-axis at which the outer and inner circles start.
36const int kAffordanceOuterStartAngle = -109;
37const int kAffordanceInnerStartAngle = -65;
38
39const int kAffordanceGlowWidth = 20;
40// The following is half width to avoid division by 2.
41const int kAffordanceArcWidth = 3;
42
43// Start and end values for various animations.
44const double kAffordanceScaleStartValue = 0.8;
45const double kAffordanceScaleEndValue = 1.0;
46const double kAffordanceShrinkScaleEndValue = 0.5;
47const double kAffordanceOpacityStartValue = 0.1;
48const double kAffordanceOpacityEndValue = 0.5;
49const int kAffordanceAngleStartValue = 0;
50// The end angle is a bit greater than 360 to make sure the circle completes at
51// the end of the animation.
52const int kAffordanceAngleEndValue = 380;
53const int kAffordanceDelayBeforeShrinkMs = 200;
54const int kAffordanceShrinkAnimationDurationMs = 100;
55
56// Visual constants.
57const SkColor kAffordanceGlowStartColor = SkColorSetARGB(24, 255, 255, 255);
58const SkColor kAffordanceGlowEndColor = SkColorSetARGB(0, 255, 255, 255);
59const SkColor kAffordanceArcColor = SkColorSetARGB(80, 0, 0, 0);
60const int kAffordanceFrameRateHz = 60;
61
62views::Widget* CreateAffordanceWidget(aura::Window* root_window) {
63  views::Widget* widget = new views::Widget;
64  views::Widget::InitParams params;
65  params.type = views::Widget::InitParams::TYPE_WINDOW_FRAMELESS;
66  params.keep_on_top = true;
67  params.accept_events = false;
68  params.can_activate = false;
69  params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
70  params.context = root_window;
71  params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW;
72  widget->Init(params);
73  widget->SetOpacity(0xFF);
74  GetRootWindowController(root_window)->GetContainer(
75      kShellWindowId_OverlayContainer)->AddChild(widget->GetNativeWindow());
76  return widget;
77}
78
79void PaintAffordanceArc(gfx::Canvas* canvas,
80                        gfx::Point& center,
81                        int radius,
82                        int start_angle,
83                        int end_angle) {
84  SkPaint paint;
85  paint.setStyle(SkPaint::kStroke_Style);
86  paint.setStrokeWidth(2 * kAffordanceArcWidth);
87  paint.setColor(kAffordanceArcColor);
88  paint.setAntiAlias(true);
89
90  SkPath arc_path;
91  arc_path.addArc(SkRect::MakeXYWH(center.x() - radius,
92                                   center.y() - radius,
93                                   2 * radius,
94                                   2 * radius),
95                  start_angle, end_angle);
96  canvas->DrawPath(arc_path, paint);
97}
98
99void PaintAffordanceGlow(gfx::Canvas* canvas,
100                         gfx::Point& center,
101                         int start_radius,
102                         int end_radius,
103                         SkColor* colors,
104                         SkScalar* pos,
105                         int num_colors) {
106  SkPoint sk_center;
107  int radius = (end_radius + start_radius) / 2;
108  int glow_width = end_radius - start_radius;
109  sk_center.iset(center.x(), center.y());
110  skia::RefPtr<SkShader> shader = skia::AdoptRef(
111      SkGradientShader::CreateTwoPointRadial(
112          sk_center,
113          SkIntToScalar(start_radius),
114          sk_center,
115          SkIntToScalar(end_radius),
116          colors,
117          pos,
118          num_colors,
119          SkShader::kClamp_TileMode));
120  DCHECK(shader);
121  SkPaint paint;
122  paint.setStyle(SkPaint::kStroke_Style);
123  paint.setStrokeWidth(glow_width);
124  paint.setShader(shader.get());
125  paint.setAntiAlias(true);
126  SkPath arc_path;
127  arc_path.addArc(SkRect::MakeXYWH(center.x() - radius,
128                                   center.y() - radius,
129                                   2 * radius,
130                                   2 * radius),
131                  0, 360);
132  canvas->DrawPath(arc_path, paint);
133}
134
135}  // namespace
136
137// View of the LongPressAffordanceHandler. Draws the actual contents and
138// updates as the animation proceeds. It also maintains the views::Widget that
139// the animation is shown in.
140class LongPressAffordanceHandler::LongPressAffordanceView
141    : public views::View {
142 public:
143  LongPressAffordanceView(const gfx::Point& event_location,
144                          aura::Window* root_window)
145      : views::View(),
146        widget_(CreateAffordanceWidget(root_window)),
147        current_angle_(kAffordanceAngleStartValue),
148        current_scale_(kAffordanceScaleStartValue) {
149    widget_->SetContentsView(this);
150    widget_->SetAlwaysOnTop(true);
151
152    // We are owned by the LongPressAffordance.
153    set_owned_by_client();
154    gfx::Point point = event_location;
155    aura::client::GetScreenPositionClient(root_window)->ConvertPointToScreen(
156        root_window, &point);
157    widget_->SetBounds(gfx::Rect(
158        point.x() - (kAffordanceOuterRadius + kAffordanceGlowWidth),
159        point.y() - (kAffordanceOuterRadius + kAffordanceGlowWidth),
160        GetPreferredSize().width(),
161        GetPreferredSize().height()));
162    widget_->Show();
163    widget_->GetNativeView()->layer()->SetOpacity(kAffordanceOpacityStartValue);
164  }
165
166  virtual ~LongPressAffordanceView() {
167  }
168
169  void UpdateWithGrowAnimation(gfx::Animation* animation) {
170    // Update the portion of the circle filled so far and re-draw.
171    current_angle_ = animation->CurrentValueBetween(kAffordanceAngleStartValue,
172        kAffordanceAngleEndValue);
173    current_scale_ = animation->CurrentValueBetween(kAffordanceScaleStartValue,
174        kAffordanceScaleEndValue);
175    widget_->GetNativeView()->layer()->SetOpacity(
176        animation->CurrentValueBetween(kAffordanceOpacityStartValue,
177            kAffordanceOpacityEndValue));
178    SchedulePaint();
179  }
180
181  void UpdateWithShrinkAnimation(gfx::Animation* animation) {
182    current_scale_ = animation->CurrentValueBetween(kAffordanceScaleEndValue,
183        kAffordanceShrinkScaleEndValue);
184    widget_->GetNativeView()->layer()->SetOpacity(
185        animation->CurrentValueBetween(kAffordanceOpacityEndValue,
186            kAffordanceOpacityStartValue));
187    SchedulePaint();
188  }
189
190 private:
191  // Overridden from views::View.
192  virtual gfx::Size GetPreferredSize() OVERRIDE {
193    return gfx::Size(2 * (kAffordanceOuterRadius + kAffordanceGlowWidth),
194        2 * (kAffordanceOuterRadius + kAffordanceGlowWidth));
195  }
196
197  virtual void OnPaint(gfx::Canvas* canvas) OVERRIDE {
198    gfx::Point center(GetPreferredSize().width() / 2,
199                      GetPreferredSize().height() / 2);
200    canvas->Save();
201
202    gfx::Transform scale;
203    scale.Scale(current_scale_, current_scale_);
204    // We want to scale from the center.
205    canvas->Translate(center.OffsetFromOrigin());
206    canvas->Transform(scale);
207    canvas->Translate(-center.OffsetFromOrigin());
208
209    // Paint affordance glow
210    int start_radius = kAffordanceInnerRadius - kAffordanceGlowWidth;
211    int end_radius = kAffordanceOuterRadius + kAffordanceGlowWidth;
212    const int num_colors = 3;
213    SkScalar pos[num_colors] = {0, 0.5, 1};
214    SkColor colors[num_colors] = {kAffordanceGlowEndColor,
215        kAffordanceGlowStartColor, kAffordanceGlowEndColor};
216    PaintAffordanceGlow(canvas, center, start_radius, end_radius, colors, pos,
217        num_colors);
218
219    // Paint inner circle.
220    PaintAffordanceArc(canvas, center, kAffordanceInnerRadius,
221        kAffordanceInnerStartAngle, -current_angle_);
222    // Paint outer circle.
223    PaintAffordanceArc(canvas, center, kAffordanceOuterRadius,
224        kAffordanceOuterStartAngle, current_angle_);
225
226    canvas->Restore();
227  }
228
229  scoped_ptr<views::Widget> widget_;
230  int current_angle_;
231  double current_scale_;
232
233  DISALLOW_COPY_AND_ASSIGN(LongPressAffordanceView);
234};
235
236////////////////////////////////////////////////////////////////////////////////
237// LongPressAffordanceHandler, public
238
239LongPressAffordanceHandler::LongPressAffordanceHandler()
240    : gfx::LinearAnimation(kAffordanceFrameRateHz, NULL),
241      tap_down_target_(NULL),
242      current_animation_type_(NONE) {}
243
244LongPressAffordanceHandler::~LongPressAffordanceHandler() {
245  StopAffordance();
246}
247
248void LongPressAffordanceHandler::ProcessEvent(aura::Window* target,
249                                              ui::GestureEvent* event) {
250  // Once we have a target, we are only interested in events with that target.
251  if (tap_down_target_ && tap_down_target_ != target)
252    return;
253  switch (event->type()) {
254    case ui::ET_GESTURE_TAP_DOWN: {
255      // Start timer that will start animation on "semi-long-press".
256      tap_down_location_ = event->root_location();
257      SetTapDownTarget(target);
258      current_animation_type_ = GROW_ANIMATION;
259      int64 timer_start_time_ms =
260          ui::GestureConfiguration::semi_long_press_time_in_seconds() * 1000;
261      timer_.Start(FROM_HERE,
262                   base::TimeDelta::FromMilliseconds(timer_start_time_ms),
263                   this,
264                   &LongPressAffordanceHandler::StartAnimation);
265      break;
266    }
267    case ui::ET_GESTURE_TAP:
268    case ui::ET_GESTURE_TAP_CANCEL:
269      StopAffordance();
270      break;
271    case ui::ET_GESTURE_LONG_PRESS:
272      End();
273      break;
274    default:
275      break;
276  }
277}
278
279////////////////////////////////////////////////////////////////////////////////
280// LongPressAffordanceHandler, private
281
282void LongPressAffordanceHandler::StartAnimation() {
283  switch (current_animation_type_) {
284    case GROW_ANIMATION: {
285      aura::Window* root_window = tap_down_target_->GetRootWindow();
286      if (!root_window) {
287        StopAffordance();
288        return;
289      }
290      view_.reset(new LongPressAffordanceView(tap_down_location_, root_window));
291      SetDuration(
292          ui::GestureConfiguration::long_press_time_in_seconds() * 1000 -
293          ui::GestureConfiguration::semi_long_press_time_in_seconds() * 1000 -
294          kAffordanceDelayBeforeShrinkMs);
295      Start();
296      break;
297    }
298    case SHRINK_ANIMATION:
299      SetDuration(kAffordanceShrinkAnimationDurationMs);
300      Start();
301      break;
302    default:
303      NOTREACHED();
304      break;
305  }
306}
307
308void LongPressAffordanceHandler::StopAffordance() {
309  if (timer_.IsRunning())
310    timer_.Stop();
311  // Since, Animation::Stop() calls AnimationStopped(), we need to reset the
312  // |current_animation_type_| before Stop(), otherwise AnimationStopped() may
313  // start the timer again.
314  current_animation_type_ = NONE;
315  Stop();
316  view_.reset();
317  SetTapDownTarget(NULL);
318}
319
320void LongPressAffordanceHandler::SetTapDownTarget(aura::Window* target) {
321  if (tap_down_target_ == target)
322    return;
323
324  if (tap_down_target_)
325    tap_down_target_->RemoveObserver(this);
326  tap_down_target_ = target;
327  if (tap_down_target_)
328    tap_down_target_->AddObserver(this);
329}
330
331void LongPressAffordanceHandler::AnimateToState(double state) {
332  DCHECK(view_.get());
333  switch (current_animation_type_) {
334    case GROW_ANIMATION:
335      view_->UpdateWithGrowAnimation(this);
336      break;
337    case SHRINK_ANIMATION:
338      view_->UpdateWithShrinkAnimation(this);
339      break;
340    default:
341      NOTREACHED();
342      break;
343  }
344}
345
346void LongPressAffordanceHandler::AnimationStopped() {
347  switch (current_animation_type_) {
348    case GROW_ANIMATION:
349      current_animation_type_ = SHRINK_ANIMATION;
350      timer_.Start(FROM_HERE,
351          base::TimeDelta::FromMilliseconds(kAffordanceDelayBeforeShrinkMs),
352          this, &LongPressAffordanceHandler::StartAnimation);
353      break;
354    case SHRINK_ANIMATION:
355      current_animation_type_ = NONE;
356      // fall through to reset the view.
357    default:
358      view_.reset();
359      SetTapDownTarget(NULL);
360      break;
361  }
362}
363
364void LongPressAffordanceHandler::OnWindowDestroying(aura::Window* window) {
365  DCHECK_EQ(tap_down_target_, window);
366  StopAffordance();
367}
368
369}  // namespace internal
370}  // namespace ash
371