1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.inputmethod.leanback.voice; 18 19 import com.android.inputmethod.leanback.R; 20 21 import android.animation.ObjectAnimator; 22 import android.animation.TimeAnimator; 23 import android.animation.TimeAnimator.TimeListener; 24 import android.content.Context; 25 import android.content.res.TypedArray; 26 import android.graphics.Bitmap; 27 import android.graphics.BitmapFactory; 28 import android.graphics.Canvas; 29 import android.graphics.Color; 30 import android.graphics.Paint; 31 import android.graphics.Rect; 32 import android.util.AttributeSet; 33 import android.util.Log; 34 import android.view.View; 35 import android.view.accessibility.AccessibilityNodeInfo; 36 37 /** 38 * Displays the recording value of the microphone. 39 */ 40 public class BitmapSoundLevelView extends View { 41 private static final boolean DEBUG = false; 42 private static final String TAG = "BitmapSoundLevelsView"; 43 44 private static final int MIC_PRIMARY_LEVEL_IMAGE_OFFSET = 3; 45 private static final int MIC_LEVEL_GUIDELINE_OFFSET = 13; 46 47 private final Paint mEmptyPaint = new Paint(); 48 private Rect mDestRect; 49 50 private final int mEnableBackgroundColor; 51 private final int mDisableBackgroundColor; 52 53 // Generates clock ticks for the animation using the global animation loop. 54 private TimeAnimator mAnimator; 55 56 private int mCurrentVolume; 57 58 // Bitmap for the main level meter, most closely follows the mic. 59 private final Bitmap mPrimaryLevel; 60 61 // Bitmap for trailing level meter, shows a peak level. 62 private final Bitmap mTrailLevel; 63 64 // The minimum size of the levels, that is the size when volume is 0. 65 private final int mMinimumLevelSize; 66 67 // A translation to apply to the center of the levels, allows the levels to be offset from 68 // the center of the mView without having to translate the whole mView. 69 private final int mCenterTranslationX; 70 private final int mCenterTranslationY; 71 72 // Peak level observed, and how many frames left before it starts decaying. 73 private int mPeakLevel; 74 private int mPeakLevelCountDown; 75 76 // Input level is pulled from here. 77 private SpeechLevelSource mLevelSource; 78 79 private Paint mPaint; 80 BitmapSoundLevelView(Context context)81 public BitmapSoundLevelView(Context context) { 82 this(context, null); 83 } 84 BitmapSoundLevelView(Context context, AttributeSet attrs)85 public BitmapSoundLevelView(Context context, AttributeSet attrs) { 86 this(context, attrs, 0); 87 } 88 BitmapSoundLevelView(Context context, AttributeSet attrs, int defStyleAttr)89 public BitmapSoundLevelView(Context context, AttributeSet attrs, int defStyleAttr) { 90 super(context, attrs, defStyleAttr); 91 92 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.BitmapSoundLevelView, 93 defStyleAttr, 0); 94 mEnableBackgroundColor = a.getColor(R.styleable.BitmapSoundLevelView_enabledBackgroundColor, 95 Color.parseColor("#66FFFFFF")); 96 97 mDisableBackgroundColor = a.getColor( 98 R.styleable.BitmapSoundLevelView_disabledBackgroundColor, 99 Color.WHITE); 100 101 boolean primaryLevelEnabled = false; 102 boolean peakLevelEnabled = false; 103 int primaryLevelId = 0; 104 if (a.hasValue(R.styleable.BitmapSoundLevelView_primaryLevels)) { 105 primaryLevelId = a.getResourceId( 106 R.styleable.BitmapSoundLevelView_primaryLevels, R.drawable.vs_reactive_dark); 107 primaryLevelEnabled = true; 108 } 109 110 int trailLevelId = 0; 111 if (a.hasValue(R.styleable.BitmapSoundLevelView_trailLevels)) { 112 trailLevelId = a.getResourceId( 113 R.styleable.BitmapSoundLevelView_trailLevels, R.drawable.vs_reactive_light); 114 peakLevelEnabled = true; 115 } 116 117 mCenterTranslationX = a.getDimensionPixelOffset( 118 R.styleable.BitmapSoundLevelView_levelsCenterX, 0); 119 120 mCenterTranslationY = a.getDimensionPixelOffset( 121 R.styleable.BitmapSoundLevelView_levelsCenterY, 0); 122 123 mMinimumLevelSize = a.getDimensionPixelOffset( 124 R.styleable.BitmapSoundLevelView_minLevelRadius, 0); 125 126 a.recycle(); 127 128 if (primaryLevelEnabled) { 129 mPrimaryLevel = BitmapFactory.decodeResource(getResources(), primaryLevelId); 130 } else { 131 mPrimaryLevel = null; 132 } 133 134 if (peakLevelEnabled) { 135 mTrailLevel = BitmapFactory.decodeResource(getResources(), trailLevelId); 136 } else { 137 mTrailLevel = null; 138 } 139 140 mPaint = new Paint(); 141 142 mDestRect = new Rect(); 143 144 mEmptyPaint.setFilterBitmap(true); 145 146 // Safe source, replaced with system one when attached. 147 mLevelSource = new SpeechLevelSource(); 148 mLevelSource.setSpeechLevel(0); 149 150 // This animator generates ticks that invalidate the 151 // mView so that the animation is synced with the global animation loop. 152 mAnimator = new TimeAnimator(); 153 mAnimator.setRepeatCount(ObjectAnimator.INFINITE); 154 mAnimator.setTimeListener(new TimeListener() { 155 @Override 156 public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) { 157 invalidate(); 158 } 159 }); 160 } 161 162 @Override setEnabled(boolean enabled)163 public void setEnabled(boolean enabled) { 164 super.setEnabled(enabled); 165 updateAnimatorState(); 166 } 167 updateAnimatorState()168 private void updateAnimatorState() { 169 if (isEnabled()) { 170 startAnimator(); 171 } else { 172 stopAnimator(); 173 } 174 } 175 startAnimator()176 private void startAnimator() { 177 if (DEBUG) Log.d(TAG, "startAnimator()"); 178 if (!mAnimator.isStarted()) { 179 mAnimator.start(); 180 } 181 } 182 stopAnimator()183 private void stopAnimator() { 184 if (DEBUG) Log.d(TAG, "stopAnimator()"); 185 mAnimator.cancel(); 186 } 187 188 @Override onAttachedToWindow()189 protected void onAttachedToWindow() { 190 super.onAttachedToWindow(); 191 updateAnimatorState(); 192 } 193 194 @Override onDetachedFromWindow()195 protected void onDetachedFromWindow() { 196 stopAnimator(); 197 super.onDetachedFromWindow(); 198 } 199 200 @Override onWindowFocusChanged(boolean hasWindowFocus)201 public void onWindowFocusChanged(boolean hasWindowFocus) { 202 super.onWindowFocusChanged(hasWindowFocus); 203 if (hasWindowFocus) { 204 updateAnimatorState(); 205 } else { 206 stopAnimator(); 207 } 208 } 209 setLevelSource(SpeechLevelSource source)210 public void setLevelSource(SpeechLevelSource source) { 211 if (DEBUG) { 212 Log.d(TAG, "Speech source set"); 213 } 214 mLevelSource = source; 215 } 216 217 @Override onDraw(Canvas canvas)218 public void onDraw(Canvas canvas) { 219 if (isEnabled()) { 220 canvas.drawColor(mEnableBackgroundColor); 221 222 int level = mLevelSource.getSpeechLevel(); 223 224 // Set the peak level for the trailing circle, goes to a peak, waits there for 225 // some frames, then starts to decay. 226 if (level > mPeakLevel) { 227 mPeakLevel = level; 228 mPeakLevelCountDown = 25; 229 } else { 230 if (mPeakLevelCountDown == 0) { 231 mPeakLevel = Math.max(0, mPeakLevel - 2); 232 } else { 233 mPeakLevelCountDown--; 234 } 235 } 236 237 // Either ease towards the target level, or decay away from it depending on whether 238 // its higher or lower than the current. 239 if (level > mCurrentVolume) { 240 mCurrentVolume = mCurrentVolume + ((level - mCurrentVolume) / 4); 241 } else { 242 mCurrentVolume = (int) (mCurrentVolume * 0.95f); 243 } 244 245 int centerX = mCenterTranslationX + (getWidth() / 2); 246 int centerY = mCenterTranslationY + (getWidth() / 2); 247 if (mTrailLevel != null) { 248 int size = ((centerX - mMinimumLevelSize) * mPeakLevel) / 100 + mMinimumLevelSize; 249 250 mDestRect.set( 251 centerX - size, 252 centerY - size, 253 centerX + size, 254 centerY + size); 255 canvas.drawBitmap(mTrailLevel, null, mDestRect, mEmptyPaint); 256 } 257 258 if (mPrimaryLevel != null) { 259 int size = 260 ((centerX - mMinimumLevelSize) * mCurrentVolume) / 100 + mMinimumLevelSize; 261 262 mDestRect.set( 263 centerX - size, 264 centerY - size, 265 centerX + size, 266 centerY + size); 267 canvas.drawBitmap(mPrimaryLevel, null, mDestRect, mEmptyPaint); 268 mPaint.setColor(getResources().getColor(R.color.search_mic_background)); 269 mPaint.setStyle(Paint.Style.FILL); 270 canvas.drawCircle(centerX, centerY, mMinimumLevelSize - 271 MIC_PRIMARY_LEVEL_IMAGE_OFFSET, mPaint); 272 } 273 if(mTrailLevel != null && mPrimaryLevel != null) { 274 mPaint.setColor(getResources().getColor(R.color.search_mic_levels_guideline)); 275 mPaint.setStyle(Paint.Style.STROKE); 276 canvas.drawCircle(centerX, centerY, centerX - MIC_LEVEL_GUIDELINE_OFFSET, mPaint); 277 } 278 } else { 279 canvas.drawColor(mDisableBackgroundColor); 280 } 281 } 282 } 283