blob: e74500e876d1e8d80ad899a4e9f5bdea21630244 [file] [log] [blame]
/*
* 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);
}
}
}