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