1 /* 2 * Copyright (C) 2015 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 package com.android.messaging.ui.mediapicker; 17 18 import android.animation.ObjectAnimator; 19 import android.animation.TimeAnimator; 20 import android.animation.TimeAnimator.TimeListener; 21 import android.content.Context; 22 import android.content.res.TypedArray; 23 import android.graphics.Canvas; 24 import android.graphics.Color; 25 import android.graphics.Paint; 26 import android.graphics.Paint.Style; 27 import android.util.AttributeSet; 28 import android.util.Log; 29 import android.view.View; 30 import android.view.accessibility.AccessibilityNodeInfo; 31 32 import com.android.messaging.R; 33 import com.android.messaging.util.LogUtil; 34 35 /** 36 * This view draws circular sound levels. By default the sound levels are black, unless 37 * otherwise defined via {@link #mPrimaryLevelPaint}. 38 */ 39 public class SoundLevels extends View { 40 private static final String TAG = LogUtil.BUGLE_TAG; 41 private static final boolean DEBUG = false; 42 43 private boolean mCenterDefined; 44 private int mCenterX; 45 private int mCenterY; 46 47 // Paint for the main level meter, most closely follows the mic. 48 private final Paint mPrimaryLevelPaint; 49 50 // The minimum size of the levels as a percentage of the max, that is the size when volume is 0. 51 private final float mMinimumLevel; 52 53 // The minimum size of the levels, that is the size when volume is 0. 54 private final float mMinimumLevelSize; 55 56 // The maximum size of the levels, that is the size when volume is 100. 57 private final float mMaximumLevelSize; 58 59 // Generates clock ticks for the animation using the global animation loop. 60 private final TimeAnimator mSpeechLevelsAnimator; 61 62 private float mCurrentVolume; 63 64 // Indicates whether we should be animating the sound level. 65 private boolean mIsEnabled; 66 67 // Input level is pulled from here. 68 private AudioLevelSource mLevelSource; 69 SoundLevels(final Context context)70 public SoundLevels(final Context context) { 71 this(context, null); 72 } 73 SoundLevels(final Context context, final AttributeSet attrs)74 public SoundLevels(final Context context, final AttributeSet attrs) { 75 this(context, attrs, 0); 76 } 77 SoundLevels(final Context context, final AttributeSet attrs, final int defStyle)78 public SoundLevels(final Context context, final AttributeSet attrs, final int defStyle) { 79 super(context, attrs, defStyle); 80 81 // Safe source, replaced with system one when attached. 82 mLevelSource = new AudioLevelSource(); 83 mLevelSource.setSpeechLevel(0); 84 85 final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SoundLevels, 86 defStyle, 0); 87 88 mMaximumLevelSize = a.getDimensionPixelOffset( 89 R.styleable.SoundLevels_maxLevelRadius, 0); 90 mMinimumLevelSize = a.getDimensionPixelOffset( 91 R.styleable.SoundLevels_minLevelRadius, 0); 92 mMinimumLevel = mMinimumLevelSize / mMaximumLevelSize; 93 94 mPrimaryLevelPaint = new Paint(); 95 mPrimaryLevelPaint.setColor( 96 a.getColor(R.styleable.SoundLevels_primaryColor, Color.BLACK)); 97 mPrimaryLevelPaint.setFlags(Paint.ANTI_ALIAS_FLAG); 98 99 a.recycle(); 100 101 // This animator generates ticks that invalidate the 102 // view so that the animation is synced with the global animation loop. 103 // TODO: We could probably remove this in favor of using postInvalidateOnAnimation 104 // which might improve things further. 105 mSpeechLevelsAnimator = new TimeAnimator(); 106 mSpeechLevelsAnimator.setRepeatCount(ObjectAnimator.INFINITE); 107 mSpeechLevelsAnimator.setTimeListener(new TimeListener() { 108 @Override 109 public void onTimeUpdate(final TimeAnimator animation, final long totalTime, 110 final long deltaTime) { 111 invalidate(); 112 } 113 }); 114 } 115 116 @Override onDraw(final Canvas canvas)117 protected void onDraw(final Canvas canvas) { 118 if (!mIsEnabled) { 119 return; 120 } 121 122 if (!mCenterDefined) { 123 // One time computation here, because we can't rely on getWidth() to be computed at 124 // constructor time or in onFinishInflate :(. 125 mCenterX = getWidth() / 2; 126 mCenterY = getWidth() / 2; 127 mCenterDefined = true; 128 } 129 130 final int level = mLevelSource.getSpeechLevel(); 131 // Either ease towards the target level, or decay away from it depending on whether 132 // its higher or lower than the current. 133 if (level > mCurrentVolume) { 134 mCurrentVolume = mCurrentVolume + ((level - mCurrentVolume) / 4); 135 } else { 136 mCurrentVolume = mCurrentVolume * 0.95f; 137 } 138 139 final float radius = mMinimumLevel + (1f - mMinimumLevel) * mCurrentVolume / 100; 140 mPrimaryLevelPaint.setStyle(Style.FILL); 141 canvas.drawCircle(mCenterX, mCenterY, radius * mMaximumLevelSize, mPrimaryLevelPaint); 142 } 143 setLevelSource(final AudioLevelSource source)144 public void setLevelSource(final AudioLevelSource source) { 145 if (DEBUG) { 146 Log.d(TAG, "Speech source set."); 147 } 148 mLevelSource = source; 149 } 150 startSpeechLevelsAnimator()151 private void startSpeechLevelsAnimator() { 152 if (DEBUG) { 153 Log.d(TAG, "startAnimator()"); 154 } 155 if (!mSpeechLevelsAnimator.isStarted()) { 156 mSpeechLevelsAnimator.start(); 157 } 158 } 159 stopSpeechLevelsAnimator()160 private void stopSpeechLevelsAnimator() { 161 if (DEBUG) { 162 Log.d(TAG, "stopAnimator()"); 163 } 164 if (mSpeechLevelsAnimator.isStarted()) { 165 mSpeechLevelsAnimator.end(); 166 } 167 } 168 169 @Override onDetachedFromWindow()170 protected void onDetachedFromWindow() { 171 super.onDetachedFromWindow(); 172 stopSpeechLevelsAnimator(); 173 } 174 175 @Override setEnabled(final boolean enabled)176 public void setEnabled(final boolean enabled) { 177 if (enabled == mIsEnabled) { 178 return; 179 } 180 if (DEBUG) { 181 Log.d("TAG", "setEnabled: " + enabled); 182 } 183 super.setEnabled(enabled); 184 mIsEnabled = enabled; 185 setKeepScreenOn(enabled); 186 updateSpeechLevelsAnimatorState(); 187 } 188 updateSpeechLevelsAnimatorState()189 private void updateSpeechLevelsAnimatorState() { 190 if (mIsEnabled) { 191 startSpeechLevelsAnimator(); 192 } else { 193 stopSpeechLevelsAnimator(); 194 } 195 } 196 197 /** 198 * This is required to make the View findable by uiautomator 199 */ 200 @Override onInitializeAccessibilityNodeInfo(final AccessibilityNodeInfo info)201 public void onInitializeAccessibilityNodeInfo(final AccessibilityNodeInfo info) { 202 super.onInitializeAccessibilityNodeInfo(info); 203 info.setClassName(SoundLevels.class.getCanonicalName()); 204 } 205 206 /** 207 * Set the alpha level of the sound circles. 208 */ setPrimaryColorAlpha(final int alpha)209 public void setPrimaryColorAlpha(final int alpha) { 210 mPrimaryLevelPaint.setAlpha(alpha); 211 } 212 } 213