blob: 46879ea3ffa917f42f73f0c248e3b63a3e38344e [file] [log] [blame]
/*
* Copyright (C) 2016 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.incallui.answer.impl.affordance;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ArgbEvaluator;
import android.animation.PropertyValuesHolder;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewAnimationUtils;
import android.view.animation.Interpolator;
import android.widget.ImageView;
import com.android.incallui.answer.impl.utils.FlingAnimationUtils;
import com.android.incallui.answer.impl.utils.Interpolators;
/** Button that allows swiping to trigger */
public class SwipeButtonView extends ImageView {
private static final long CIRCLE_APPEAR_DURATION = 80;
private static final long CIRCLE_DISAPPEAR_MAX_DURATION = 200;
private static final long NORMAL_ANIMATION_DURATION = 200;
public static final float MAX_ICON_SCALE_AMOUNT = 1.5f;
public static final float MIN_ICON_SCALE_AMOUNT = 0.8f;
private final int minBackgroundRadius;
private final Paint circlePaint;
private final int inverseColor;
private final int normalColor;
private final ArgbEvaluator colorInterpolator;
private final FlingAnimationUtils flingAnimationUtils;
private float circleRadius;
private int centerX;
private int centerY;
private ValueAnimator circleAnimator;
private ValueAnimator alphaAnimator;
private ValueAnimator scaleAnimator;
private float circleStartValue;
private boolean circleWillBeHidden;
private int[] tempPoint = new int[2];
private float tmageScale = 1f;
private int circleColor;
private View previewView;
private float circleStartRadius;
private float maxCircleSize;
private Animator previewClipper;
private float restingAlpha = SwipeButtonHelper.SWIPE_RESTING_ALPHA_AMOUNT;
private boolean finishing;
private boolean launchingAffordance;
private AnimatorListenerAdapter clipEndListener =
new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
previewClipper = null;
}
};
private AnimatorListenerAdapter circleEndListener =
new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
circleAnimator = null;
}
};
private AnimatorListenerAdapter scaleEndListener =
new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
scaleAnimator = null;
}
};
private AnimatorListenerAdapter alphaEndListener =
new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
alphaAnimator = null;
}
};
public SwipeButtonView(Context context) {
this(context, null);
}
public SwipeButtonView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SwipeButtonView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public SwipeButtonView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
circlePaint = new Paint();
circlePaint.setAntiAlias(true);
circleColor = 0xffffffff;
circlePaint.setColor(circleColor);
normalColor = 0xffffffff;
inverseColor = 0xff000000;
minBackgroundRadius =
context
.getResources()
.getDimensionPixelSize(R.dimen.answer_affordance_min_background_radius);
colorInterpolator = new ArgbEvaluator();
flingAnimationUtils = new FlingAnimationUtils(context, 0.3f);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
centerX = getWidth() / 2;
centerY = getHeight() / 2;
maxCircleSize = getMaxCircleSize();
}
@Override
protected void onDraw(Canvas canvas) {
drawBackgroundCircle(canvas);
canvas.save();
canvas.scale(tmageScale, tmageScale, getWidth() / 2, getHeight() / 2);
super.onDraw(canvas);
canvas.restore();
}
public void setPreviewView(@Nullable View v) {
View oldPreviewView = previewView;
previewView = v;
if (previewView != null) {
previewView.setVisibility(launchingAffordance ? oldPreviewView.getVisibility() : INVISIBLE);
}
}
private void updateIconColor() {
Drawable drawable = getDrawable().mutate();
float alpha = circleRadius / minBackgroundRadius;
alpha = Math.min(1.0f, alpha);
int color = (int) colorInterpolator.evaluate(alpha, normalColor, inverseColor);
drawable.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
}
private void drawBackgroundCircle(Canvas canvas) {
if (circleRadius > 0 || finishing) {
updateCircleColor();
canvas.drawCircle(centerX, centerY, circleRadius, circlePaint);
}
}
private void updateCircleColor() {
float fraction =
0.5f
+ 0.5f
* Math.max(
0.0f,
Math.min(
1.0f, (circleRadius - minBackgroundRadius) / (0.5f * minBackgroundRadius)));
if (previewView != null && previewView.getVisibility() == VISIBLE) {
float finishingFraction =
1 - Math.max(0, circleRadius - circleStartRadius) / (maxCircleSize - circleStartRadius);
fraction *= finishingFraction;
}
int color =
Color.argb(
(int) (Color.alpha(circleColor) * fraction),
Color.red(circleColor),
Color.green(circleColor),
Color.blue(circleColor));
circlePaint.setColor(color);
}
public void finishAnimation(float velocity, @Nullable final Runnable mAnimationEndRunnable) {
cancelAnimator(circleAnimator);
cancelAnimator(previewClipper);
finishing = true;
circleStartRadius = circleRadius;
final float maxCircleSize = getMaxCircleSize();
Animator animatorToRadius;
animatorToRadius = getAnimatorToRadius(maxCircleSize);
flingAnimationUtils.applyDismissing(
animatorToRadius, circleRadius, maxCircleSize, velocity, maxCircleSize);
animatorToRadius.addListener(
new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (mAnimationEndRunnable != null) {
mAnimationEndRunnable.run();
}
finishing = false;
circleRadius = maxCircleSize;
invalidate();
}
});
animatorToRadius.start();
setImageAlpha(0, true);
if (previewView != null) {
previewView.setVisibility(View.VISIBLE);
previewClipper =
ViewAnimationUtils.createCircularReveal(
previewView, getLeft() + centerX, getTop() + centerY, circleRadius, maxCircleSize);
flingAnimationUtils.applyDismissing(
previewClipper, circleRadius, maxCircleSize, velocity, maxCircleSize);
previewClipper.addListener(clipEndListener);
previewClipper.start();
}
}
public void instantFinishAnimation() {
cancelAnimator(previewClipper);
if (previewView != null) {
previewView.setClipBounds(null);
previewView.setVisibility(View.VISIBLE);
}
circleRadius = getMaxCircleSize();
setImageAlpha(0, false);
invalidate();
}
private float getMaxCircleSize() {
getLocationInWindow(tempPoint);
float rootWidth = getRootView().getWidth();
float width = tempPoint[0] + centerX;
width = Math.max(rootWidth - width, width);
float height = tempPoint[1] + centerY;
return (float) Math.hypot(width, height);
}
public void setCircleRadius(float circleRadius) {
setCircleRadius(circleRadius, false, false);
}
public void setCircleRadius(float circleRadius, boolean slowAnimation) {
setCircleRadius(circleRadius, slowAnimation, false);
}
public void setCircleRadiusWithoutAnimation(float circleRadius) {
cancelAnimator(circleAnimator);
setCircleRadius(circleRadius, false, true);
}
private void setCircleRadius(float circleRadius, boolean slowAnimation, boolean noAnimation) {
// Check if we need a new animation
boolean radiusHidden =
(circleAnimator != null && circleWillBeHidden)
|| (circleAnimator == null && this.circleRadius == 0.0f);
boolean nowHidden = circleRadius == 0.0f;
boolean radiusNeedsAnimation = (radiusHidden != nowHidden) && !noAnimation;
if (!radiusNeedsAnimation) {
if (circleAnimator == null) {
this.circleRadius = circleRadius;
updateIconColor();
invalidate();
if (nowHidden) {
if (previewView != null) {
previewView.setVisibility(View.INVISIBLE);
}
}
} else if (!circleWillBeHidden) {
// We just update the end value
float diff = circleRadius - minBackgroundRadius;
PropertyValuesHolder[] values = circleAnimator.getValues();
values[0].setFloatValues(circleStartValue + diff, circleRadius);
circleAnimator.setCurrentPlayTime(circleAnimator.getCurrentPlayTime());
}
} else {
cancelAnimator(circleAnimator);
cancelAnimator(previewClipper);
ValueAnimator animator = getAnimatorToRadius(circleRadius);
Interpolator interpolator =
circleRadius == 0.0f
? Interpolators.FAST_OUT_LINEAR_IN
: Interpolators.LINEAR_OUT_SLOW_IN;
animator.setInterpolator(interpolator);
long duration = 250;
if (!slowAnimation) {
float durationFactor =
Math.abs(this.circleRadius - circleRadius) / (float) minBackgroundRadius;
duration = (long) (CIRCLE_APPEAR_DURATION * durationFactor);
duration = Math.min(duration, CIRCLE_DISAPPEAR_MAX_DURATION);
}
animator.setDuration(duration);
animator.start();
if (previewView != null && previewView.getVisibility() == View.VISIBLE) {
previewView.setVisibility(View.VISIBLE);
previewClipper =
ViewAnimationUtils.createCircularReveal(
previewView,
getLeft() + centerX,
getTop() + centerY,
this.circleRadius,
circleRadius);
previewClipper.setInterpolator(interpolator);
previewClipper.setDuration(duration);
previewClipper.addListener(clipEndListener);
previewClipper.addListener(
new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
previewView.setVisibility(View.INVISIBLE);
}
});
previewClipper.start();
}
}
}
private ValueAnimator getAnimatorToRadius(float circleRadius) {
ValueAnimator animator = ValueAnimator.ofFloat(this.circleRadius, circleRadius);
circleAnimator = animator;
circleStartValue = this.circleRadius;
circleWillBeHidden = circleRadius == 0.0f;
animator.addUpdateListener(
new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
SwipeButtonView.this.circleRadius = (float) animation.getAnimatedValue();
updateIconColor();
invalidate();
}
});
animator.addListener(circleEndListener);
return animator;
}
private void cancelAnimator(Animator animator) {
if (animator != null) {
animator.cancel();
}
}
public void setImageScale(float imageScale, boolean animate) {
setImageScale(imageScale, animate, -1, null);
}
/**
* Sets the scale of the containing image
*
* @param imageScale The new Scale.
* @param animate Should an animation be performed
* @param duration If animate, whats the duration? When -1 we take the default duration
* @param interpolator If animate, whats the interpolator? When null we take the default
* interpolator.
*/
public void setImageScale(
float imageScale, boolean animate, long duration, @Nullable Interpolator interpolator) {
cancelAnimator(scaleAnimator);
if (!animate) {
tmageScale = imageScale;
invalidate();
} else {
ValueAnimator animator = ValueAnimator.ofFloat(tmageScale, imageScale);
scaleAnimator = animator;
animator.addUpdateListener(
new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
tmageScale = (float) animation.getAnimatedValue();
invalidate();
}
});
animator.addListener(scaleEndListener);
if (interpolator == null) {
interpolator =
imageScale == 0.0f
? Interpolators.FAST_OUT_LINEAR_IN
: Interpolators.LINEAR_OUT_SLOW_IN;
}
animator.setInterpolator(interpolator);
if (duration == -1) {
float durationFactor = Math.abs(tmageScale - imageScale) / (1.0f - MIN_ICON_SCALE_AMOUNT);
durationFactor = Math.min(1.0f, durationFactor);
duration = (long) (NORMAL_ANIMATION_DURATION * durationFactor);
}
animator.setDuration(duration);
animator.start();
}
}
public void setRestingAlpha(float alpha) {
restingAlpha = alpha;
// TODO: Handle the case an animation is playing.
setImageAlpha(alpha, false);
}
public float getRestingAlpha() {
return restingAlpha;
}
public void setImageAlpha(float alpha, boolean animate) {
setImageAlpha(alpha, animate, -1, null, null);
}
/**
* Sets the alpha of the containing image
*
* @param alpha The new alpha.
* @param animate Should an animation be performed
* @param duration If animate, whats the duration? When -1 we take the default duration
* @param interpolator If animate, whats the interpolator? When null we take the default
* interpolator.
*/
public void setImageAlpha(
float alpha,
boolean animate,
long duration,
@Nullable Interpolator interpolator,
@Nullable Runnable runnable) {
cancelAnimator(alphaAnimator);
alpha = launchingAffordance ? 0 : alpha;
int endAlpha = (int) (alpha * 255);
final Drawable background = getBackground();
if (!animate) {
if (background != null) {
background.mutate().setAlpha(endAlpha);
}
setImageAlpha(endAlpha);
} else {
int currentAlpha = getImageAlpha();
ValueAnimator animator = ValueAnimator.ofInt(currentAlpha, endAlpha);
alphaAnimator = animator;
animator.addUpdateListener(
new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int alpha = (int) animation.getAnimatedValue();
if (background != null) {
background.mutate().setAlpha(alpha);
}
setImageAlpha(alpha);
}
});
animator.addListener(alphaEndListener);
if (interpolator == null) {
interpolator =
alpha == 0.0f ? Interpolators.FAST_OUT_LINEAR_IN : Interpolators.LINEAR_OUT_SLOW_IN;
}
animator.setInterpolator(interpolator);
if (duration == -1) {
float durationFactor = Math.abs(currentAlpha - endAlpha) / 255f;
durationFactor = Math.min(1.0f, durationFactor);
duration = (long) (NORMAL_ANIMATION_DURATION * durationFactor);
}
animator.setDuration(duration);
if (runnable != null) {
animator.addListener(getEndListener(runnable));
}
animator.start();
}
}
private Animator.AnimatorListener getEndListener(final Runnable runnable) {
return new AnimatorListenerAdapter() {
boolean mCancelled;
@Override
public void onAnimationCancel(Animator animation) {
mCancelled = true;
}
@Override
public void onAnimationEnd(Animator animation) {
if (!mCancelled) {
runnable.run();
}
}
};
}
public float getCircleRadius() {
return circleRadius;
}
@Override
public boolean performClick() {
return isClickable() && super.performClick();
}
public void setLaunchingAffordance(boolean launchingAffordance) {
this.launchingAffordance = launchingAffordance;
}
}