/*
* Copyright 2010-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file 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.amazonaws.mobileconnectors.lex.interactionkit.ui;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import android.view.animation.LinearInterpolator;
import com.amazonaws.mobileconnectors.lex.interactionkit.Response;
import com.amazonaws.mobileconnectors.lex.interactionkit.exceptions.InvalidParameterException;
import com.amazonaws.services.lex.R;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class InteractiveVoiceView extends View {
private static final String TAG = "Lex";
private InteractiveVoiceViewAdapter viewAdapter;
private InteractiveVoiceListener listener;
// Widget states.
private static final int NORMAL = 0;
private static final int LISTENING = 1;
private static final int AWAITING_RESPONSE = 2;
// Default boundary sizes of the.
private static final int BIB_INT_DP = 32;
// Wait animation arc angle in degrees.
private static final float ARC_LEN_ANGLE = 52.51f;
// Period for the wait spinner arc to complete one revolution.
private static final int ARC_PERIOD_SEC = 2;
private static final float GOLDEN_RATIO = 1.809f;
// Default colors.
private static final String DEFAULT_COLOR_BUTTON_BACKGROUND = "#FFFFFF";
private static final String DEFAULT_COLOR_BUTTON_BOUNDARY = "#8D9496";
private static final String DEFAULT_COLOR_TINT_NORMAL = "#329AD6";
private static final String DEFAULT_COLOR_TINT_LISTENING = "#2A5C91";
private static final String DEFAULT_COLOR_TINT_WAITING = "#8D9496";
private static final String DEFAULT_COLOR_ANIMATED_CIRCLE = "#4EA9DC";
private static final String DEFAULT_COLOR_ANIMATED_RING = "#2A5C91";
// Animation colors.
private int mIconColorNormal;
private int mIconColorListening;
private int mIconColorWaiting;
private int mButtonBoundaryColor;
// Application context.
private final Context context;
// Bitmaps to store button icons.
private Bitmap mIconBmp;
BitmapDrawable bitmapDrawableIcon;
// Boundary for animated wait arc.
private RectF oval;
Drawable mDrawable;
private int canvasWidth;
private int canvasHeight;
// Color and styles for animation.
private Paint mPaintAnimationListening;
private Paint mPaintAnimationWaiting;
private Paint mPaintButtonBackground;
private Paint mPaintButtonBoundary;
// Animation sequence.
private AnimatorSet mAnimatorSet;
// Animators for wait.
private ValueAnimator mValueAnimator;
// Current widget state.
private int state = NORMAL;
// View boundary's.
private float mAnimationRadiusPix;
private float mButtonRadiusPix;
private float mCurrentRadiusPix;
private float mCurrentStrokePix;
private float mCurrentArcPosition;
private int mAnimationGapPix;
public InteractiveVoiceView(Context context) {
super(context);
this.context = context;
init();
}
public InteractiveVoiceView(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
init();
}
/**
* Initializes this view. Sets all components.
*/
private void init() {
mAnimatorSet = new AnimatorSet();
mIconColorNormal = Color.parseColor(DEFAULT_COLOR_TINT_NORMAL);
mIconColorWaiting = Color.parseColor(DEFAULT_COLOR_TINT_WAITING);
mIconColorListening = Color.parseColor(DEFAULT_COLOR_TINT_LISTENING);
mButtonBoundaryColor = Color.parseColor(DEFAULT_COLOR_BUTTON_BOUNDARY);
// Set image bitmaps for normal and listening states.
mIconBmp = BitmapFactory.decodeResource(getResources(), R.drawable.mic);
bitmapDrawableIcon = new BitmapDrawable(getResources(), mIconBmp);
// Color and style for animated components.
mPaintAnimationListening = new Paint();
mPaintAnimationListening.setAntiAlias(true);
mPaintAnimationListening.setColor(Color.parseColor(DEFAULT_COLOR_ANIMATED_CIRCLE));
mPaintAnimationWaiting = new Paint();
mPaintAnimationWaiting.setAntiAlias(true);
mPaintAnimationWaiting.setColor(Color.parseColor(DEFAULT_COLOR_ANIMATED_RING));
mCurrentStrokePix = 20.0f;
mPaintAnimationWaiting.setStrokeWidth(mCurrentStrokePix);
mPaintButtonBackground = new Paint();
mPaintButtonBackground.setAntiAlias(true);
mPaintButtonBackground.setStrokeWidth(5);
mPaintButtonBackground.setColor(Color.parseColor(DEFAULT_COLOR_BUTTON_BACKGROUND));
mPaintButtonBoundary = new Paint();
mPaintButtonBoundary.setAntiAlias(true);
mPaintButtonBoundary.setStrokeWidth(3);
mPaintButtonBoundary.setStyle(Paint.Style.STROKE);
mPaintButtonBoundary.setColor(mButtonBoundaryColor);
// Minimum radius in pixels.
mButtonRadiusPix = dip2pix(BIB_INT_DP) / 2;
mCurrentRadiusPix = mButtonRadiusPix;
// Bounds for the wait display ring.
oval = new RectF();
mCurrentArcPosition = 0f;
// Set client adapter.
viewAdapter = InteractiveVoiceViewAdapter.getInstance(context, this);
// Set current state as normal and draw this view.
state = NORMAL;
invalidate();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mAnimationRadiusPix = Math.min(w, h) / 2;
mAnimationGapPix = Math.round(mAnimationRadiusPix / GOLDEN_RATIO);
mButtonRadiusPix = mAnimationRadiusPix - mAnimationGapPix;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvasWidth = canvas.getWidth();
canvasHeight = canvas.getHeight();
final Rect bounds = canvas.getClipBounds();
int buttonColor = mIconColorNormal;
switch (state) {
case LISTENING:
if (mCurrentRadiusPix > mButtonRadiusPix) {
// Draw circle to animate voice.
canvas.drawCircle(canvasWidth / 2, canvasHeight / 2, mCurrentRadiusPix, mPaintAnimationListening);
}
// Draw the button boundary and background.
oval.set(calculateButtonBounds(bounds));
canvas.drawArc(oval, 0, 360, false, mPaintButtonBackground);
canvas.drawArc(oval, 0, 360, false, mPaintButtonBoundary);
buttonColor = mIconColorListening;
break;
case NORMAL:
// Draw the button boundary and background.
oval.set(calculateButtonBounds(bounds));
canvas.drawArc(oval, 0, 360, false, mPaintButtonBackground);
canvas.drawArc(oval, 0, 360, false, mPaintButtonBoundary);
break;
case AWAITING_RESPONSE:
// Animate circular wait indicator.
canvas.drawColor(0, PorterDuff.Mode.CLEAR);
oval.set(calculateButtonBounds(bounds));
canvas.drawArc(oval, 0, 360, false, mPaintButtonBackground);
canvas.drawArc(oval, mCurrentArcPosition, ARC_LEN_ANGLE, false, mPaintAnimationWaiting);
// Draw the button boundary.
canvas.drawArc(oval, 0, 360, false, mPaintButtonBoundary);
buttonColor = mIconColorWaiting;
break;
}
// Draw the button icon.
bitmapDrawableIcon.setBounds(calculateButtonBounds(bounds));
mDrawable = bitmapDrawableIcon.mutate();
mDrawable.setColorFilter(buttonColor, PorterDuff.Mode.SRC_ATOP);
mDrawable.draw(canvas);
}
/**
* Animates sound by modulating radius of a displayed circle. This animation just performs one
* raise and fall of the circle (radius).
* @param soundLevel
*/
public void animateSoundLevel(float soundLevel) {
reset();
state = LISTENING;
final float radius = soundlevel2radius(soundLevel);
mAnimatorSet.playSequentially(
ObjectAnimator.ofFloat(this, "CurrentRadius", getCurrentRadius(), radius).setDuration(20),
ObjectAnimator.ofFloat(this, "CurrentRadius", radius, mButtonRadiusPix).setDuration(400)
);
mAnimatorSet.start();
}
/**
* Start wait spinner animation. {@link InteractiveVoiceView#ARC_PERIOD_SEC} represents the
* period for the spinner arc to complete one revolution. This animation is continued indefinitely,
* until the animation stopped - {@link InteractiveVoiceView#animateNone()}.
*/
public void animateWaitSpinner() {
reset();
state = AWAITING_RESPONSE;
mValueAnimator = ValueAnimator.ofFloat(0f, 1f);
mValueAnimator.setDuration(TimeUnit.SECONDS.toMillis(ARC_PERIOD_SEC));
mValueAnimator.setRepeatCount(ValueAnimator.INFINITE);
mValueAnimator.setRepeatMode(ValueAnimator.RESTART);
mValueAnimator.setInterpolator(new LinearInterpolator());
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
setCurrentAngle((Float) animation.getAnimatedValue());
}
});
mValueAnimator.start();
invalidate();
}
/**
* Stop current, if any, animation and change the button state to normal state.
*/
public void animateNone() {
reset();
state = NORMAL;
invalidate();
}
/**
* Returns current radius of the voice animation circle.
* @return current radius for the animated circle.
*/
public final float getCurrentRadius() {
return mCurrentRadiusPix;
}
/**
* Sets new radius for voice animation and requests for a new drawing.
* @param radius the new radius for voice animation.
*/
public final void setCurrentRadius(float radius) {
mCurrentRadiusPix = radius;
invalidate();
}
/**
* Sets new position of the arc, for wait animation.
* @param currentAngle a fraction between 0 and 1.
*/
private void setCurrentAngle(float currentAngle) {
// Move arc to a new position and request a new draw.
mCurrentArcPosition = 360 * currentAngle;
invalidate();
}
/**
* Stops current animation and prepares the widget for a new drawing.
*/
private void reset() {
if (mAnimatorSet != null && mAnimatorSet.isRunning()) {
mAnimatorSet.cancel();
}
if (mValueAnimator != null && mValueAnimator.isRunning()) {
mValueAnimator.cancel();
}
mCurrentArcPosition = 0f;
}
/**
* Converts density-independent pixel value to pixels.
* @param sizeInDp size as density-independent pixel.
* @return size in pixels.
*/
private int dip2pix(int sizeInDp){
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, sizeInDp, context.getResources().getDisplayMetrics());
}
/**
* Calculates bounds for the button boundary. {@link InteractiveVoiceView#mAnimationRadiusPix}
* represents the radius of the largest circle to drawn in the current view. This area should
* encapsulates the button and the sound animation. The region to draw the button boundary is a
* concentric inner circle. This method calculates the bounds for this inner circle.
* @param bounds the complete drawable area.
* @return bounds for the button boundary.
*/
private Rect calculateButtonBounds(Rect bounds){
return new Rect(
bounds.left + mAnimationGapPix,
bounds.top + mAnimationGapPix,
bounds.right - mAnimationGapPix,
bounds.bottom - mAnimationGapPix);
}
/**
* Converts the current rms value of sound to radius. This implementation assumes RMS value in
* the {@code soundLevel} and performs linear translation.
* <b>Override this method for custom implementation.</b>
* @param soundLevel sound level.
* @return radius.
*/
protected float soundlevel2radius(float soundLevel) {
// rms values are between -2 to 2 so we baseline it to 0 return a value
// between 0 and 1.
final float calculatedRadius = (soundLevel + 2) * mAnimationRadiusPix / 4;
if (calculatedRadius <= mButtonRadiusPix) {
return mButtonRadiusPix;
} else if (calculatedRadius >= mAnimationRadiusPix) {
return mAnimationRadiusPix;
}
return calculatedRadius;
}
/**
* Set interaction listener for this view.
* @param listener
*/
public void setInteractiveVoiceListener(InteractiveVoiceListener listener) {
this.listener = listener;
viewAdapter.setVoiceListener(listener);
}
/**
* Returns view adapter.
* @return The adapter set for this object.
*/
public InteractiveVoiceViewAdapter getViewAdapter() {
return this.viewAdapter;
}
/**
* Sets the color of the animation for audio input from device's microphone.
* @param color The int value of the color.
*/
public void setColorVoiceAnimation(String color) {
mPaintAnimationListening.setColor(getColor(color));
}
/**
* Sets the color of the animation for audio input from device's microphone.
* @param color The hex-decimal color code as a string, e.g. "#FFFFFF".
*/
public void setColorWaitSpinner(String color) {
mPaintAnimationWaiting.setColor(getColor(color));
}
/**
* Sets the color of the circle representing the button boundary.
* @param color The hex-decimal color code as a string, e.g. "#FFFFFF".
*/
public void setDefaultColorButtonBoundary(String color) {
mPaintButtonBoundary.setColor(getColor(color));
}
/**
* Sets the color of the button icon for normal state.
* @param color The hex-decimal color code as a string, e.g. "#FFFFFF".
*/
public void setColorIconNormal(String color) {
mIconColorNormal = getColor(color);
}
/**
* Sets the color of the button icon when listening for speech.
* @param color The hex-decimal color code as a string, e.g. "#FFFFFF".
*/
public void setColorIconListening(String color) {
mIconColorListening = getColor(color);
}
/**
* Sets the color of the button icon when the client is waiting for response
* from Amazon Lex bot.
*
* @param color The hex-decimal color code as a string, e.g. "#FFFFFF".
*/
public void setColorIconAwaitingResponse(String color) {
mIconColorWaiting = getColor(color);
}
/**
* Returns the int value of the hex-decimal color.
* @param color The hex-decimal color code as a string, e.g. "#FFFFFF".
* @return The int value of the supplied color.
*/
private int getColor(String color) {
try {
return Color.parseColor(color);
} catch (final Exception e) {
throw new InvalidParameterException("Error invalid color: " + color, e);
}
}
/**
* Implement this listener to interact with the Amazon Lex bots.
*/
public interface InteractiveVoiceListener {
/**
* Called when the dialog is complete and is ready for client side
* fulfillment.
*
* @param slots This contains the slots gathered by the Amazon Lex bot
* in the dialog.
* @param intent This is the intent identified by the Amazon Lex bot,
* from the dialog.
*/
void dialogReadyForFulfillment(Map<String, String> slots, String intent);
/**
* Called on receiving response from the Amazon Lex bot.
*
* @param response The response from the Amazon Lex bot.
*/
void onResponse(Response response);
/**
* Called on encountering errors during a dialog.
*
* @param responseText The response from the Amazon Lex bot as text.
* @param e The exception for the error.
*/
void onError(String responseText, Exception e);
}
}