| /* |
| * Copyright (C) 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License |
| */ |
| |
| package com.android.inputmethod.leanback.voice; |
| |
| import com.android.inputmethod.leanback.R; |
| |
| import android.animation.ObjectAnimator; |
| import android.animation.TimeAnimator; |
| import android.animation.TimeAnimator.TimeListener; |
| import android.content.Context; |
| import android.content.res.TypedArray; |
| import android.graphics.Bitmap; |
| import android.graphics.BitmapFactory; |
| import android.graphics.Canvas; |
| import android.graphics.Color; |
| import android.graphics.Paint; |
| import android.graphics.Rect; |
| import android.util.AttributeSet; |
| import android.util.Log; |
| import android.view.View; |
| import android.view.accessibility.AccessibilityNodeInfo; |
| |
| /** |
| * Displays the recording value of the microphone. |
| */ |
| public class BitmapSoundLevelView extends View { |
| private static final boolean DEBUG = false; |
| private static final String TAG = "BitmapSoundLevelsView"; |
| |
| private static final int MIC_PRIMARY_LEVEL_IMAGE_OFFSET = 3; |
| private static final int MIC_LEVEL_GUIDELINE_OFFSET = 13; |
| |
| private final Paint mEmptyPaint = new Paint(); |
| private Rect mDestRect; |
| |
| private final int mEnableBackgroundColor; |
| private final int mDisableBackgroundColor; |
| |
| // Generates clock ticks for the animation using the global animation loop. |
| private TimeAnimator mAnimator; |
| |
| private int mCurrentVolume; |
| |
| // Bitmap for the main level meter, most closely follows the mic. |
| private final Bitmap mPrimaryLevel; |
| |
| // Bitmap for trailing level meter, shows a peak level. |
| private final Bitmap mTrailLevel; |
| |
| // The minimum size of the levels, that is the size when volume is 0. |
| private final int mMinimumLevelSize; |
| |
| // A translation to apply to the center of the levels, allows the levels to be offset from |
| // the center of the mView without having to translate the whole mView. |
| private final int mCenterTranslationX; |
| private final int mCenterTranslationY; |
| |
| // Peak level observed, and how many frames left before it starts decaying. |
| private int mPeakLevel; |
| private int mPeakLevelCountDown; |
| |
| // Input level is pulled from here. |
| private SpeechLevelSource mLevelSource; |
| |
| private Paint mPaint; |
| |
| public BitmapSoundLevelView(Context context) { |
| this(context, null); |
| } |
| |
| public BitmapSoundLevelView(Context context, AttributeSet attrs) { |
| this(context, attrs, 0); |
| } |
| |
| public BitmapSoundLevelView(Context context, AttributeSet attrs, int defStyleAttr) { |
| super(context, attrs, defStyleAttr); |
| |
| TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.BitmapSoundLevelView, |
| defStyleAttr, 0); |
| mEnableBackgroundColor = a.getColor(R.styleable.BitmapSoundLevelView_enabledBackgroundColor, |
| Color.parseColor("#66FFFFFF")); |
| |
| mDisableBackgroundColor = a.getColor( |
| R.styleable.BitmapSoundLevelView_disabledBackgroundColor, |
| Color.WHITE); |
| |
| boolean primaryLevelEnabled = false; |
| boolean peakLevelEnabled = false; |
| int primaryLevelId = 0; |
| if (a.hasValue(R.styleable.BitmapSoundLevelView_primaryLevels)) { |
| primaryLevelId = a.getResourceId( |
| R.styleable.BitmapSoundLevelView_primaryLevels, R.drawable.vs_reactive_dark); |
| primaryLevelEnabled = true; |
| } |
| |
| int trailLevelId = 0; |
| if (a.hasValue(R.styleable.BitmapSoundLevelView_trailLevels)) { |
| trailLevelId = a.getResourceId( |
| R.styleable.BitmapSoundLevelView_trailLevels, R.drawable.vs_reactive_light); |
| peakLevelEnabled = true; |
| } |
| |
| mCenterTranslationX = a.getDimensionPixelOffset( |
| R.styleable.BitmapSoundLevelView_levelsCenterX, 0); |
| |
| mCenterTranslationY = a.getDimensionPixelOffset( |
| R.styleable.BitmapSoundLevelView_levelsCenterY, 0); |
| |
| mMinimumLevelSize = a.getDimensionPixelOffset( |
| R.styleable.BitmapSoundLevelView_minLevelRadius, 0); |
| |
| a.recycle(); |
| |
| if (primaryLevelEnabled) { |
| mPrimaryLevel = BitmapFactory.decodeResource(getResources(), primaryLevelId); |
| } else { |
| mPrimaryLevel = null; |
| } |
| |
| if (peakLevelEnabled) { |
| mTrailLevel = BitmapFactory.decodeResource(getResources(), trailLevelId); |
| } else { |
| mTrailLevel = null; |
| } |
| |
| mPaint = new Paint(); |
| |
| mDestRect = new Rect(); |
| |
| mEmptyPaint.setFilterBitmap(true); |
| |
| // Safe source, replaced with system one when attached. |
| mLevelSource = new SpeechLevelSource(); |
| mLevelSource.setSpeechLevel(0); |
| |
| // This animator generates ticks that invalidate the |
| // mView so that the animation is synced with the global animation loop. |
| mAnimator = new TimeAnimator(); |
| mAnimator.setRepeatCount(ObjectAnimator.INFINITE); |
| mAnimator.setTimeListener(new TimeListener() { |
| @Override |
| public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) { |
| invalidate(); |
| } |
| }); |
| } |
| |
| @Override |
| public void setEnabled(boolean enabled) { |
| super.setEnabled(enabled); |
| updateAnimatorState(); |
| } |
| |
| private void updateAnimatorState() { |
| if (isEnabled()) { |
| startAnimator(); |
| } else { |
| stopAnimator(); |
| } |
| } |
| |
| private void startAnimator() { |
| if (DEBUG) Log.d(TAG, "startAnimator()"); |
| if (!mAnimator.isStarted()) { |
| mAnimator.start(); |
| } |
| } |
| |
| private void stopAnimator() { |
| if (DEBUG) Log.d(TAG, "stopAnimator()"); |
| mAnimator.cancel(); |
| } |
| |
| @Override |
| protected void onAttachedToWindow() { |
| super.onAttachedToWindow(); |
| updateAnimatorState(); |
| } |
| |
| @Override |
| protected void onDetachedFromWindow() { |
| stopAnimator(); |
| super.onDetachedFromWindow(); |
| } |
| |
| @Override |
| public void onWindowFocusChanged(boolean hasWindowFocus) { |
| super.onWindowFocusChanged(hasWindowFocus); |
| if (hasWindowFocus) { |
| updateAnimatorState(); |
| } else { |
| stopAnimator(); |
| } |
| } |
| |
| public void setLevelSource(SpeechLevelSource source) { |
| if (DEBUG) { |
| Log.d(TAG, "Speech source set"); |
| } |
| mLevelSource = source; |
| } |
| |
| @Override |
| public void onDraw(Canvas canvas) { |
| if (isEnabled()) { |
| canvas.drawColor(mEnableBackgroundColor); |
| |
| int level = mLevelSource.getSpeechLevel(); |
| |
| // Set the peak level for the trailing circle, goes to a peak, waits there for |
| // some frames, then starts to decay. |
| if (level > mPeakLevel) { |
| mPeakLevel = level; |
| mPeakLevelCountDown = 25; |
| } else { |
| if (mPeakLevelCountDown == 0) { |
| mPeakLevel = Math.max(0, mPeakLevel - 2); |
| } else { |
| mPeakLevelCountDown--; |
| } |
| } |
| |
| // Either ease towards the target level, or decay away from it depending on whether |
| // its higher or lower than the current. |
| if (level > mCurrentVolume) { |
| mCurrentVolume = mCurrentVolume + ((level - mCurrentVolume) / 4); |
| } else { |
| mCurrentVolume = (int) (mCurrentVolume * 0.95f); |
| } |
| |
| int centerX = mCenterTranslationX + (getWidth() / 2); |
| int centerY = mCenterTranslationY + (getWidth() / 2); |
| if (mTrailLevel != null) { |
| int size = ((centerX - mMinimumLevelSize) * mPeakLevel) / 100 + mMinimumLevelSize; |
| |
| mDestRect.set( |
| centerX - size, |
| centerY - size, |
| centerX + size, |
| centerY + size); |
| canvas.drawBitmap(mTrailLevel, null, mDestRect, mEmptyPaint); |
| } |
| |
| if (mPrimaryLevel != null) { |
| int size = |
| ((centerX - mMinimumLevelSize) * mCurrentVolume) / 100 + mMinimumLevelSize; |
| |
| mDestRect.set( |
| centerX - size, |
| centerY - size, |
| centerX + size, |
| centerY + size); |
| canvas.drawBitmap(mPrimaryLevel, null, mDestRect, mEmptyPaint); |
| mPaint.setColor(getResources().getColor(R.color.search_mic_background)); |
| mPaint.setStyle(Paint.Style.FILL); |
| canvas.drawCircle(centerX, centerY, mMinimumLevelSize - |
| MIC_PRIMARY_LEVEL_IMAGE_OFFSET, mPaint); |
| } |
| if(mTrailLevel != null && mPrimaryLevel != null) { |
| mPaint.setColor(getResources().getColor(R.color.search_mic_levels_guideline)); |
| mPaint.setStyle(Paint.Style.STROKE); |
| canvas.drawCircle(centerX, centerY, centerX - MIC_LEVEL_GUIDELINE_OFFSET, mPaint); |
| } |
| } else { |
| canvas.drawColor(mDisableBackgroundColor); |
| } |
| } |
| } |