/* * 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); } } }