/*
|
* Copyright (C) 2014 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.systemui.statusbar;
|
|
import android.animation.Animator;
|
import android.animation.AnimatorListenerAdapter;
|
import android.animation.ArgbEvaluator;
|
import android.animation.PropertyValuesHolder;
|
import android.animation.ValueAnimator;
|
import android.annotation.Nullable;
|
import android.content.Context;
|
import android.content.res.TypedArray;
|
import android.graphics.Canvas;
|
import android.graphics.CanvasProperty;
|
import android.graphics.Color;
|
import android.graphics.Paint;
|
import android.graphics.PorterDuff;
|
import android.graphics.RecordingCanvas;
|
import android.graphics.drawable.Drawable;
|
import android.util.AttributeSet;
|
import android.view.RenderNodeAnimator;
|
import android.view.View;
|
import android.view.ViewAnimationUtils;
|
import android.view.animation.Interpolator;
|
import android.widget.ImageView;
|
|
import com.android.systemui.Interpolators;
|
import com.android.systemui.R;
|
|
/**
|
* An ImageView which does not have overlapping renderings commands and therefore does not need a
|
* layer when alpha is changed.
|
*/
|
public class KeyguardAffordanceView 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;
|
|
protected final int mDarkIconColor;
|
protected final int mNormalColor;
|
private final int mMinBackgroundRadius;
|
private final Paint mCirclePaint;
|
private final ArgbEvaluator mColorInterpolator;
|
private final FlingAnimationUtils mFlingAnimationUtils;
|
private float mCircleRadius;
|
private int mCenterX;
|
private int mCenterY;
|
private ValueAnimator mCircleAnimator;
|
private ValueAnimator mAlphaAnimator;
|
private ValueAnimator mScaleAnimator;
|
private float mCircleStartValue;
|
private boolean mCircleWillBeHidden;
|
private int[] mTempPoint = new int[2];
|
private float mImageScale = 1f;
|
private int mCircleColor;
|
private boolean mIsLeft;
|
private View mPreviewView;
|
private float mCircleStartRadius;
|
private float mMaxCircleSize;
|
private Animator mPreviewClipper;
|
private float mRestingAlpha = 1f;
|
private boolean mSupportHardware;
|
private boolean mFinishing;
|
private boolean mLaunchingAffordance;
|
private boolean mShouldTint = true;
|
|
private CanvasProperty<Float> mHwCircleRadius;
|
private CanvasProperty<Float> mHwCenterX;
|
private CanvasProperty<Float> mHwCenterY;
|
private CanvasProperty<Paint> mHwCirclePaint;
|
|
private AnimatorListenerAdapter mClipEndListener = new AnimatorListenerAdapter() {
|
@Override
|
public void onAnimationEnd(Animator animation) {
|
mPreviewClipper = null;
|
}
|
};
|
private AnimatorListenerAdapter mCircleEndListener = new AnimatorListenerAdapter() {
|
@Override
|
public void onAnimationEnd(Animator animation) {
|
mCircleAnimator = null;
|
}
|
};
|
private AnimatorListenerAdapter mScaleEndListener = new AnimatorListenerAdapter() {
|
@Override
|
public void onAnimationEnd(Animator animation) {
|
mScaleAnimator = null;
|
}
|
};
|
private AnimatorListenerAdapter mAlphaEndListener = new AnimatorListenerAdapter() {
|
@Override
|
public void onAnimationEnd(Animator animation) {
|
mAlphaAnimator = null;
|
}
|
};
|
|
public KeyguardAffordanceView(Context context) {
|
this(context, null);
|
}
|
|
public KeyguardAffordanceView(Context context, AttributeSet attrs) {
|
this(context, attrs, 0);
|
}
|
|
public KeyguardAffordanceView(Context context, AttributeSet attrs, int defStyleAttr) {
|
this(context, attrs, defStyleAttr, 0);
|
}
|
|
public KeyguardAffordanceView(Context context, AttributeSet attrs, int defStyleAttr,
|
int defStyleRes) {
|
super(context, attrs, defStyleAttr, defStyleRes);
|
TypedArray a = context.obtainStyledAttributes(attrs, android.R.styleable.ImageView);
|
|
mCirclePaint = new Paint();
|
mCirclePaint.setAntiAlias(true);
|
mCircleColor = 0xffffffff;
|
mCirclePaint.setColor(mCircleColor);
|
|
mNormalColor = a.getColor(android.R.styleable.ImageView_tint, 0xffffffff);
|
mDarkIconColor = 0xff000000;
|
mMinBackgroundRadius = mContext.getResources().getDimensionPixelSize(
|
R.dimen.keyguard_affordance_min_background_radius);
|
mColorInterpolator = new ArgbEvaluator();
|
mFlingAnimationUtils = new FlingAnimationUtils(mContext, 0.3f);
|
|
a.recycle();
|
}
|
|
public void setImageDrawable(@Nullable Drawable drawable, boolean tint) {
|
super.setImageDrawable(drawable);
|
mShouldTint = tint;
|
updateIconColor();
|
}
|
|
/**
|
* If current drawable should be tinted.
|
*/
|
public boolean shouldTint() {
|
return mShouldTint;
|
}
|
|
@Override
|
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
super.onLayout(changed, left, top, right, bottom);
|
mCenterX = getWidth() / 2;
|
mCenterY = getHeight() / 2;
|
mMaxCircleSize = getMaxCircleSize();
|
}
|
|
@Override
|
protected void onDraw(Canvas canvas) {
|
mSupportHardware = canvas.isHardwareAccelerated();
|
drawBackgroundCircle(canvas);
|
canvas.save();
|
canvas.scale(mImageScale, mImageScale, getWidth() / 2, getHeight() / 2);
|
super.onDraw(canvas);
|
canvas.restore();
|
}
|
|
public void setPreviewView(View v) {
|
if (mPreviewView == v) {
|
return;
|
}
|
View oldPreviewView = mPreviewView;
|
mPreviewView = v;
|
if (mPreviewView != null) {
|
mPreviewView.setVisibility(mLaunchingAffordance
|
? oldPreviewView.getVisibility() : INVISIBLE);
|
}
|
}
|
|
private void updateIconColor() {
|
if (!mShouldTint) return;
|
Drawable drawable = getDrawable().mutate();
|
float alpha = mCircleRadius / mMinBackgroundRadius;
|
alpha = Math.min(1.0f, alpha);
|
int color = (int) mColorInterpolator.evaluate(alpha, mNormalColor, mDarkIconColor);
|
drawable.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
|
}
|
|
private void drawBackgroundCircle(Canvas canvas) {
|
if (mCircleRadius > 0 || mFinishing) {
|
if (mFinishing && mSupportHardware && mHwCenterX != null) {
|
// Our hardware drawing proparties can be null if the finishing started but we have
|
// never drawn before. In that case we are not doing a render thread animation
|
// anyway, so we need to use the normal drawing.
|
RecordingCanvas recordingCanvas = (RecordingCanvas) canvas;
|
recordingCanvas.drawCircle(mHwCenterX, mHwCenterY, mHwCircleRadius,
|
mHwCirclePaint);
|
} else {
|
updateCircleColor();
|
canvas.drawCircle(mCenterX, mCenterY, mCircleRadius, mCirclePaint);
|
}
|
}
|
}
|
|
private void updateCircleColor() {
|
float fraction = 0.5f + 0.5f * Math.max(0.0f, Math.min(1.0f,
|
(mCircleRadius - mMinBackgroundRadius) / (0.5f * mMinBackgroundRadius)));
|
if (mPreviewView != null && mPreviewView.getVisibility() == VISIBLE) {
|
float finishingFraction = 1 - Math.max(0, mCircleRadius - mCircleStartRadius)
|
/ (mMaxCircleSize - mCircleStartRadius);
|
fraction *= finishingFraction;
|
}
|
int color = Color.argb((int) (Color.alpha(mCircleColor) * fraction),
|
Color.red(mCircleColor),
|
Color.green(mCircleColor), Color.blue(mCircleColor));
|
mCirclePaint.setColor(color);
|
}
|
|
public void finishAnimation(float velocity, final Runnable mAnimationEndRunnable) {
|
cancelAnimator(mCircleAnimator);
|
cancelAnimator(mPreviewClipper);
|
mFinishing = true;
|
mCircleStartRadius = mCircleRadius;
|
final float maxCircleSize = getMaxCircleSize();
|
Animator animatorToRadius;
|
if (mSupportHardware) {
|
initHwProperties();
|
animatorToRadius = getRtAnimatorToRadius(maxCircleSize);
|
startRtAlphaFadeIn();
|
} else {
|
animatorToRadius = getAnimatorToRadius(maxCircleSize);
|
}
|
mFlingAnimationUtils.applyDismissing(animatorToRadius, mCircleRadius, maxCircleSize,
|
velocity, maxCircleSize);
|
animatorToRadius.addListener(new AnimatorListenerAdapter() {
|
@Override
|
public void onAnimationEnd(Animator animation) {
|
mAnimationEndRunnable.run();
|
mFinishing = false;
|
mCircleRadius = maxCircleSize;
|
invalidate();
|
}
|
});
|
animatorToRadius.start();
|
setImageAlpha(0, true);
|
if (mPreviewView != null) {
|
mPreviewView.setVisibility(View.VISIBLE);
|
mPreviewClipper = ViewAnimationUtils.createCircularReveal(
|
mPreviewView, getLeft() + mCenterX, getTop() + mCenterY, mCircleRadius,
|
maxCircleSize);
|
mFlingAnimationUtils.applyDismissing(mPreviewClipper, mCircleRadius, maxCircleSize,
|
velocity, maxCircleSize);
|
mPreviewClipper.addListener(mClipEndListener);
|
mPreviewClipper.start();
|
if (mSupportHardware) {
|
startRtCircleFadeOut(animatorToRadius.getDuration());
|
}
|
}
|
}
|
|
/**
|
* Fades in the Circle on the RenderThread. It's used when finishing the circle when it had
|
* alpha 0 in the beginning.
|
*/
|
private void startRtAlphaFadeIn() {
|
if (mCircleRadius == 0 && mPreviewView == null) {
|
Paint modifiedPaint = new Paint(mCirclePaint);
|
modifiedPaint.setColor(mCircleColor);
|
modifiedPaint.setAlpha(0);
|
mHwCirclePaint = CanvasProperty.createPaint(modifiedPaint);
|
RenderNodeAnimator animator = new RenderNodeAnimator(mHwCirclePaint,
|
RenderNodeAnimator.PAINT_ALPHA, 255);
|
animator.setTarget(this);
|
animator.setInterpolator(Interpolators.ALPHA_IN);
|
animator.setDuration(250);
|
animator.start();
|
}
|
}
|
|
public void instantFinishAnimation() {
|
cancelAnimator(mPreviewClipper);
|
if (mPreviewView != null) {
|
mPreviewView.setClipBounds(null);
|
mPreviewView.setVisibility(View.VISIBLE);
|
}
|
mCircleRadius = getMaxCircleSize();
|
setImageAlpha(0, false);
|
invalidate();
|
}
|
|
private void startRtCircleFadeOut(long duration) {
|
RenderNodeAnimator animator = new RenderNodeAnimator(mHwCirclePaint,
|
RenderNodeAnimator.PAINT_ALPHA, 0);
|
animator.setDuration(duration);
|
animator.setInterpolator(Interpolators.ALPHA_OUT);
|
animator.setTarget(this);
|
animator.start();
|
}
|
|
private Animator getRtAnimatorToRadius(float circleRadius) {
|
RenderNodeAnimator animator = new RenderNodeAnimator(mHwCircleRadius, circleRadius);
|
animator.setTarget(this);
|
return animator;
|
}
|
|
private void initHwProperties() {
|
mHwCenterX = CanvasProperty.createFloat(mCenterX);
|
mHwCenterY = CanvasProperty.createFloat(mCenterY);
|
mHwCirclePaint = CanvasProperty.createPaint(mCirclePaint);
|
mHwCircleRadius = CanvasProperty.createFloat(mCircleRadius);
|
}
|
|
private float getMaxCircleSize() {
|
getLocationInWindow(mTempPoint);
|
float rootWidth = getRootView().getWidth();
|
float width = mTempPoint[0] + mCenterX;
|
width = Math.max(rootWidth - width, width);
|
float height = mTempPoint[1] + mCenterY;
|
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(mCircleAnimator);
|
setCircleRadius(circleRadius, false ,true);
|
}
|
|
private void setCircleRadius(float circleRadius, boolean slowAnimation, boolean noAnimation) {
|
|
// Check if we need a new animation
|
boolean radiusHidden = (mCircleAnimator != null && mCircleWillBeHidden)
|
|| (mCircleAnimator == null && mCircleRadius == 0.0f);
|
boolean nowHidden = circleRadius == 0.0f;
|
boolean radiusNeedsAnimation = (radiusHidden != nowHidden) && !noAnimation;
|
if (!radiusNeedsAnimation) {
|
if (mCircleAnimator == null) {
|
mCircleRadius = circleRadius;
|
updateIconColor();
|
invalidate();
|
if (nowHidden) {
|
if (mPreviewView != null) {
|
mPreviewView.setVisibility(View.INVISIBLE);
|
}
|
}
|
} else if (!mCircleWillBeHidden) {
|
|
// We just update the end value
|
float diff = circleRadius - mMinBackgroundRadius;
|
PropertyValuesHolder[] values = mCircleAnimator.getValues();
|
values[0].setFloatValues(mCircleStartValue + diff, circleRadius);
|
mCircleAnimator.setCurrentPlayTime(mCircleAnimator.getCurrentPlayTime());
|
}
|
} else {
|
cancelAnimator(mCircleAnimator);
|
cancelAnimator(mPreviewClipper);
|
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(mCircleRadius - circleRadius)
|
/ (float) mMinBackgroundRadius;
|
duration = (long) (CIRCLE_APPEAR_DURATION * durationFactor);
|
duration = Math.min(duration, CIRCLE_DISAPPEAR_MAX_DURATION);
|
}
|
animator.setDuration(duration);
|
animator.start();
|
if (mPreviewView != null && mPreviewView.getVisibility() == View.VISIBLE) {
|
mPreviewView.setVisibility(View.VISIBLE);
|
mPreviewClipper = ViewAnimationUtils.createCircularReveal(
|
mPreviewView, getLeft() + mCenterX, getTop() + mCenterY, mCircleRadius,
|
circleRadius);
|
mPreviewClipper.setInterpolator(interpolator);
|
mPreviewClipper.setDuration(duration);
|
mPreviewClipper.addListener(mClipEndListener);
|
mPreviewClipper.addListener(new AnimatorListenerAdapter() {
|
@Override
|
public void onAnimationEnd(Animator animation) {
|
mPreviewView.setVisibility(View.INVISIBLE);
|
}
|
});
|
mPreviewClipper.start();
|
}
|
}
|
}
|
|
private ValueAnimator getAnimatorToRadius(float circleRadius) {
|
ValueAnimator animator = ValueAnimator.ofFloat(mCircleRadius, circleRadius);
|
mCircleAnimator = animator;
|
mCircleStartValue = mCircleRadius;
|
mCircleWillBeHidden = circleRadius == 0.0f;
|
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
|
@Override
|
public void onAnimationUpdate(ValueAnimator animation) {
|
mCircleRadius = (float) animation.getAnimatedValue();
|
updateIconColor();
|
invalidate();
|
}
|
});
|
animator.addListener(mCircleEndListener);
|
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,
|
Interpolator interpolator) {
|
cancelAnimator(mScaleAnimator);
|
if (!animate) {
|
mImageScale = imageScale;
|
invalidate();
|
} else {
|
ValueAnimator animator = ValueAnimator.ofFloat(mImageScale, imageScale);
|
mScaleAnimator = animator;
|
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
|
@Override
|
public void onAnimationUpdate(ValueAnimator animation) {
|
mImageScale = (float) animation.getAnimatedValue();
|
invalidate();
|
}
|
});
|
animator.addListener(mScaleEndListener);
|
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(mImageScale - 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 float getRestingAlpha() {
|
return mRestingAlpha;
|
}
|
|
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,
|
Interpolator interpolator, Runnable runnable) {
|
cancelAnimator(mAlphaAnimator);
|
alpha = mLaunchingAffordance ? 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);
|
mAlphaAnimator = animator;
|
animator.addUpdateListener(animation -> {
|
int alpha1 = (int) animation.getAnimatedValue();
|
if (background != null) background.mutate().setAlpha(alpha1);
|
setImageAlpha(alpha1);
|
});
|
animator.addListener(mAlphaEndListener);
|
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();
|
}
|
}
|
|
public boolean isAnimatingAlpha() {
|
return mAlphaAnimator != null;
|
}
|
|
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 mCircleRadius;
|
}
|
|
@Override
|
public boolean performClick() {
|
if (isClickable()) {
|
return super.performClick();
|
} else {
|
return false;
|
}
|
}
|
|
public void setLaunchingAffordance(boolean launchingAffordance) {
|
mLaunchingAffordance = launchingAffordance;
|
}
|
}
|