| OLD | NEW |
| (Empty) |
| 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 "base/bind.h" | |
| 6 #include "base/lazy_instance.h" | |
| 7 #include "base/message_loop.h" | |
| 8 #include "chrome/browser/speech/speech_input_bubble.h" | |
| 9 #include "content/public/browser/web_contents.h" | |
| 10 #include "grit/generated_resources.h" | |
| 11 #include "grit/theme_resources.h" | |
| 12 #include "ui/base/resource/resource_bundle.h" | |
| 13 #include "ui/gfx/canvas_skia.h" | |
| 14 #include "ui/gfx/rect.h" | |
| 15 #include "ui/gfx/skbitmap_operations.h" | |
| 16 | |
| 17 using content::WebContents; | |
| 18 | |
| 19 namespace { | |
| 20 | |
| 21 const color_utils::HSL kGrayscaleShift = { -1, 0, 0.6 }; | |
| 22 const int kWarmingUpAnimationStartMs = 500; | |
| 23 const int kWarmingUpAnimationStepMs = 100; | |
| 24 const int kRecognizingAnimationStepMs = 100; | |
| 25 | |
| 26 // A lazily initialized singleton to hold all the image used by the speech | |
| 27 // input bubbles and safely destroy them on exit. | |
| 28 class SpeechInputBubbleImages { | |
| 29 public: | |
| 30 const std::vector<SkBitmap>& spinner() { return spinner_; } | |
| 31 const std::vector<SkBitmap>& warm_up() { return warm_up_; } | |
| 32 SkBitmap* mic_full() { return mic_full_; } | |
| 33 SkBitmap* mic_empty() { return mic_empty_; } | |
| 34 SkBitmap* mic_noise() { return mic_noise_; } | |
| 35 SkBitmap* mic_mask() { return mic_mask_; } | |
| 36 | |
| 37 private: | |
| 38 // Private constructor to enforce singleton. | |
| 39 friend struct base::DefaultLazyInstanceTraits<SpeechInputBubbleImages>; | |
| 40 SpeechInputBubbleImages(); | |
| 41 | |
| 42 std::vector<SkBitmap> spinner_; // Frames for the progress spinner. | |
| 43 std::vector<SkBitmap> warm_up_; // Frames for the warm up animation. | |
| 44 | |
| 45 // These bitmaps are owned by ResourceBundle and need not be destroyed. | |
| 46 SkBitmap* mic_full_; // Mic image with full volume. | |
| 47 SkBitmap* mic_noise_; // Mic image with full noise volume. | |
| 48 SkBitmap* mic_empty_; // Mic image with zero volume. | |
| 49 SkBitmap* mic_mask_; // Gradient mask used by the volume indicator. | |
| 50 }; | |
| 51 | |
| 52 SpeechInputBubbleImages::SpeechInputBubbleImages() { | |
| 53 mic_empty_ = ResourceBundle::GetSharedInstance().GetBitmapNamed( | |
| 54 IDR_SPEECH_INPUT_MIC_EMPTY); | |
| 55 mic_noise_ = ResourceBundle::GetSharedInstance().GetBitmapNamed( | |
| 56 IDR_SPEECH_INPUT_MIC_NOISE); | |
| 57 mic_full_ = ResourceBundle::GetSharedInstance().GetBitmapNamed( | |
| 58 IDR_SPEECH_INPUT_MIC_FULL); | |
| 59 mic_mask_ = ResourceBundle::GetSharedInstance().GetBitmapNamed( | |
| 60 IDR_SPEECH_INPUT_MIC_MASK); | |
| 61 | |
| 62 // The sprite image consists of all the animation frames put together in one | |
| 63 // horizontal/wide image. Each animation frame is square in shape within the | |
| 64 // sprite. | |
| 65 SkBitmap* spinner_image = ResourceBundle::GetSharedInstance().GetBitmapNamed( | |
| 66 IDR_SPEECH_INPUT_SPINNER); | |
| 67 int frame_size = spinner_image->height(); | |
| 68 | |
| 69 // When recording starts up, it may take a short while (few ms or even a | |
| 70 // couple of seconds) before the audio device starts really capturing data. | |
| 71 // This is more apparent on first use. To cover such cases we show a warming | |
| 72 // up state in the bubble starting with a blank spinner image. If audio data | |
| 73 // starts coming in within a couple hundred ms, we switch to the recording | |
| 74 // UI and if it takes longer, we show the real warm up animation frames. | |
| 75 // This reduces visual jank for the most part. | |
| 76 SkBitmap empty_spinner; | |
| 77 empty_spinner.setConfig(SkBitmap::kARGB_8888_Config, frame_size, frame_size); | |
| 78 empty_spinner.allocPixels(); | |
| 79 empty_spinner.eraseRGB(255, 255, 255); | |
| 80 warm_up_.push_back(empty_spinner); | |
| 81 | |
| 82 for (SkIRect src_rect(SkIRect::MakeWH(frame_size, frame_size)); | |
| 83 src_rect.fLeft < spinner_image->width(); | |
| 84 src_rect.offset(frame_size, 0)) { | |
| 85 SkBitmap frame; | |
| 86 spinner_image->extractSubset(&frame, src_rect); | |
| 87 | |
| 88 // The bitmap created by extractSubset just points to the same pixels as | |
| 89 // the original and adjusts rowBytes accordingly. However that doesn't | |
| 90 // render properly and gets vertically squished in Linux due to a bug in | |
| 91 // Skia. Until that gets fixed we work around by taking a real copy of it | |
| 92 // below as the copied bitmap has the correct rowBytes and renders fine. | |
| 93 SkBitmap frame_copy; | |
| 94 frame.copyTo(&frame_copy, SkBitmap::kARGB_8888_Config); | |
| 95 spinner_.push_back(frame_copy); | |
| 96 | |
| 97 // The warm up spinner animation is a gray scale version of the real one. | |
| 98 warm_up_.push_back(SkBitmapOperations::CreateHSLShiftedBitmap( | |
| 99 frame_copy, kGrayscaleShift)); | |
| 100 } | |
| 101 } | |
| 102 | |
| 103 base::LazyInstance<SpeechInputBubbleImages> g_images = | |
| 104 LAZY_INSTANCE_INITIALIZER; | |
| 105 | |
| 106 } // namespace | |
| 107 | |
| 108 SpeechInputBubble::FactoryMethod SpeechInputBubble::factory_ = NULL; | |
| 109 const int SpeechInputBubble::kBubbleTargetOffsetX = 10; | |
| 110 | |
| 111 SpeechInputBubble* SpeechInputBubble::Create(WebContents* web_contents, | |
| 112 Delegate* delegate, | |
| 113 const gfx::Rect& element_rect) { | |
| 114 if (factory_) | |
| 115 return (*factory_)(web_contents, delegate, element_rect); | |
| 116 | |
| 117 // Has the tab already closed before bubble create request was processed? | |
| 118 if (!web_contents) | |
| 119 return NULL; | |
| 120 | |
| 121 return CreateNativeBubble(web_contents, delegate, element_rect); | |
| 122 } | |
| 123 | |
| 124 SpeechInputBubbleBase::SpeechInputBubbleBase(WebContents* web_contents) | |
| 125 : ALLOW_THIS_IN_INITIALIZER_LIST(weak_factory_(this)), | |
| 126 display_mode_(DISPLAY_MODE_RECORDING), | |
| 127 web_contents_(web_contents) { | |
| 128 mic_image_.reset(new SkBitmap()); | |
| 129 mic_image_->setConfig(SkBitmap::kARGB_8888_Config, | |
| 130 g_images.Get().mic_empty()->width(), | |
| 131 g_images.Get().mic_empty()->height()); | |
| 132 mic_image_->allocPixels(); | |
| 133 | |
| 134 buffer_image_.reset(new SkBitmap()); | |
| 135 buffer_image_->setConfig(SkBitmap::kARGB_8888_Config, | |
| 136 g_images.Get().mic_empty()->width(), | |
| 137 g_images.Get().mic_empty()->height()); | |
| 138 buffer_image_->allocPixels(); | |
| 139 } | |
| 140 | |
| 141 SpeechInputBubbleBase::~SpeechInputBubbleBase() { | |
| 142 // This destructor is added to make sure members such as the scoped_ptr | |
| 143 // get destroyed here and the derived classes don't have to care about such | |
| 144 // member variables which they don't use. | |
| 145 } | |
| 146 | |
| 147 void SpeechInputBubbleBase::SetWarmUpMode() { | |
| 148 weak_factory_.InvalidateWeakPtrs(); | |
| 149 display_mode_ = DISPLAY_MODE_WARM_UP; | |
| 150 animation_step_ = 0; | |
| 151 DoWarmingUpAnimationStep(); | |
| 152 UpdateLayout(); | |
| 153 } | |
| 154 | |
| 155 void SpeechInputBubbleBase::DoWarmingUpAnimationStep() { | |
| 156 SetImage(g_images.Get().warm_up()[animation_step_]); | |
| 157 MessageLoop::current()->PostDelayedTask( | |
| 158 FROM_HERE, | |
| 159 base::Bind(&SpeechInputBubbleBase::DoWarmingUpAnimationStep, | |
| 160 weak_factory_.GetWeakPtr()), | |
| 161 base::TimeDelta::FromMilliseconds( | |
| 162 animation_step_ == 0 ? kWarmingUpAnimationStartMs | |
| 163 : kWarmingUpAnimationStepMs)); | |
| 164 if (++animation_step_ >= static_cast<int>(g_images.Get().warm_up().size())) | |
| 165 animation_step_ = 1; // Frame 0 is skipped during the animation. | |
| 166 } | |
| 167 | |
| 168 void SpeechInputBubbleBase::SetRecordingMode() { | |
| 169 weak_factory_.InvalidateWeakPtrs(); | |
| 170 display_mode_ = DISPLAY_MODE_RECORDING; | |
| 171 SetInputVolume(0, 0); | |
| 172 UpdateLayout(); | |
| 173 } | |
| 174 | |
| 175 void SpeechInputBubbleBase::SetRecognizingMode() { | |
| 176 display_mode_ = DISPLAY_MODE_RECOGNIZING; | |
| 177 animation_step_ = 0; | |
| 178 DoRecognizingAnimationStep(); | |
| 179 UpdateLayout(); | |
| 180 } | |
| 181 | |
| 182 void SpeechInputBubbleBase::DoRecognizingAnimationStep() { | |
| 183 SetImage(g_images.Get().spinner()[animation_step_]); | |
| 184 if (++animation_step_ >= static_cast<int>(g_images.Get().spinner().size())) | |
| 185 animation_step_ = 0; | |
| 186 MessageLoop::current()->PostDelayedTask( | |
| 187 FROM_HERE, | |
| 188 base::Bind(&SpeechInputBubbleBase::DoRecognizingAnimationStep, | |
| 189 weak_factory_.GetWeakPtr()), | |
| 190 base::TimeDelta::FromMilliseconds(kRecognizingAnimationStepMs)); | |
| 191 } | |
| 192 | |
| 193 void SpeechInputBubbleBase::SetMessage(const string16& text) { | |
| 194 weak_factory_.InvalidateWeakPtrs(); | |
| 195 message_text_ = text; | |
| 196 display_mode_ = DISPLAY_MODE_MESSAGE; | |
| 197 UpdateLayout(); | |
| 198 } | |
| 199 | |
| 200 void SpeechInputBubbleBase::DrawVolumeOverlay(SkCanvas* canvas, | |
| 201 const SkBitmap& bitmap, | |
| 202 float volume) { | |
| 203 buffer_image_->eraseARGB(0, 0, 0, 0); | |
| 204 | |
| 205 int width = mic_image_->width(); | |
| 206 int height = mic_image_->height(); | |
| 207 SkCanvas buffer_canvas(*buffer_image_); | |
| 208 | |
| 209 buffer_canvas.save(); | |
| 210 const int kVolumeSteps = 12; | |
| 211 SkScalar clip_right = | |
| 212 (((1.0f - volume) * (width * (kVolumeSteps + 1))) - width) / kVolumeSteps; | |
| 213 buffer_canvas.clipRect(SkRect::MakeLTRB(0, 0, | |
| 214 SkIntToScalar(width) - clip_right, SkIntToScalar(height))); | |
| 215 buffer_canvas.drawBitmap(bitmap, 0, 0); | |
| 216 buffer_canvas.restore(); | |
| 217 SkPaint multiply_paint; | |
| 218 multiply_paint.setXfermode(SkXfermode::Create(SkXfermode::kMultiply_Mode)); | |
| 219 buffer_canvas.drawBitmap(*g_images.Get().mic_mask(), -clip_right, 0, | |
| 220 &multiply_paint); | |
| 221 | |
| 222 canvas->drawBitmap(*buffer_image_.get(), 0, 0); | |
| 223 } | |
| 224 | |
| 225 void SpeechInputBubbleBase::SetInputVolume(float volume, float noise_volume) { | |
| 226 mic_image_->eraseARGB(0, 0, 0, 0); | |
| 227 SkCanvas canvas(*mic_image_); | |
| 228 | |
| 229 // Draw the empty volume image first and the current volume image on top, | |
| 230 // and then the noise volume image on top of both. | |
| 231 canvas.drawBitmap(*g_images.Get().mic_empty(), 0, 0); | |
| 232 DrawVolumeOverlay(&canvas, *g_images.Get().mic_full(), volume); | |
| 233 DrawVolumeOverlay(&canvas, *g_images.Get().mic_noise(), noise_volume); | |
| 234 | |
| 235 SetImage(*mic_image_.get()); | |
| 236 } | |
| 237 | |
| 238 WebContents* SpeechInputBubbleBase::web_contents() { | |
| 239 return web_contents_; | |
| 240 } | |
| 241 | |
| 242 void SpeechInputBubbleBase::SetImage(const SkBitmap& image) { | |
| 243 icon_image_.reset(new SkBitmap(image)); | |
| 244 UpdateImage(); | |
| 245 } | |
| 246 | |
| 247 SkBitmap SpeechInputBubbleBase::icon_image() { | |
| 248 return (icon_image_ != NULL) ? *icon_image_ : SkBitmap(); | |
| 249 } | |
| OLD | NEW |