526 lines
18 KiB
Java
526 lines
18 KiB
Java
![]() |
/*
|
||
|
* Copyright (C) 2022 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 android.inputmethodservice.navigationbar;
|
||
|
|
||
|
import android.animation.Animator;
|
||
|
import android.animation.AnimatorListenerAdapter;
|
||
|
import android.animation.ObjectAnimator;
|
||
|
import android.annotation.DimenRes;
|
||
|
import android.content.Context;
|
||
|
import android.graphics.Canvas;
|
||
|
import android.graphics.CanvasProperty;
|
||
|
import android.graphics.ColorFilter;
|
||
|
import android.graphics.Paint;
|
||
|
import android.graphics.PixelFormat;
|
||
|
import android.graphics.RecordingCanvas;
|
||
|
import android.graphics.drawable.Drawable;
|
||
|
import android.os.Handler;
|
||
|
import android.os.Trace;
|
||
|
import android.view.RenderNodeAnimator;
|
||
|
import android.view.View;
|
||
|
import android.view.ViewConfiguration;
|
||
|
import android.view.animation.Interpolator;
|
||
|
import android.view.animation.PathInterpolator;
|
||
|
|
||
|
import java.util.ArrayList;
|
||
|
import java.util.HashSet;
|
||
|
|
||
|
final class KeyButtonRipple extends Drawable {
|
||
|
|
||
|
private static final float GLOW_MAX_SCALE_FACTOR = 1.35f;
|
||
|
private static final float GLOW_MAX_ALPHA = 0.2f;
|
||
|
private static final float GLOW_MAX_ALPHA_DARK = 0.1f;
|
||
|
private static final int ANIMATION_DURATION_SCALE = 350;
|
||
|
private static final int ANIMATION_DURATION_FADE = 450;
|
||
|
private static final Interpolator ALPHA_OUT_INTERPOLATOR =
|
||
|
new PathInterpolator(0f, 0f, 0.8f, 1f);
|
||
|
|
||
|
@DimenRes
|
||
|
private final int mMaxWidthResource;
|
||
|
|
||
|
private Paint mRipplePaint;
|
||
|
private CanvasProperty<Float> mLeftProp;
|
||
|
private CanvasProperty<Float> mTopProp;
|
||
|
private CanvasProperty<Float> mRightProp;
|
||
|
private CanvasProperty<Float> mBottomProp;
|
||
|
private CanvasProperty<Float> mRxProp;
|
||
|
private CanvasProperty<Float> mRyProp;
|
||
|
private CanvasProperty<Paint> mPaintProp;
|
||
|
private float mGlowAlpha = 0f;
|
||
|
private float mGlowScale = 1f;
|
||
|
private boolean mPressed;
|
||
|
private boolean mVisible;
|
||
|
private boolean mDrawingHardwareGlow;
|
||
|
private int mMaxWidth;
|
||
|
private boolean mLastDark;
|
||
|
private boolean mDark;
|
||
|
private boolean mDelayTouchFeedback;
|
||
|
|
||
|
private final Interpolator mInterpolator = new LogInterpolator();
|
||
|
private boolean mSupportHardware;
|
||
|
private final View mTargetView;
|
||
|
private final Handler mHandler = new Handler();
|
||
|
|
||
|
private final HashSet<Animator> mRunningAnimations = new HashSet<>();
|
||
|
private final ArrayList<Animator> mTmpArray = new ArrayList<>();
|
||
|
|
||
|
private final TraceAnimatorListener mExitHwTraceAnimator =
|
||
|
new TraceAnimatorListener("exitHardware");
|
||
|
private final TraceAnimatorListener mEnterHwTraceAnimator =
|
||
|
new TraceAnimatorListener("enterHardware");
|
||
|
|
||
|
public enum Type {
|
||
|
OVAL,
|
||
|
ROUNDED_RECT
|
||
|
}
|
||
|
|
||
|
private Type mType = Type.ROUNDED_RECT;
|
||
|
|
||
|
KeyButtonRipple(Context ctx, View targetView, @DimenRes int maxWidthResource) {
|
||
|
mMaxWidthResource = maxWidthResource;
|
||
|
mMaxWidth = ctx.getResources().getDimensionPixelSize(maxWidthResource);
|
||
|
mTargetView = targetView;
|
||
|
}
|
||
|
|
||
|
public void updateResources() {
|
||
|
mMaxWidth = mTargetView.getContext().getResources()
|
||
|
.getDimensionPixelSize(mMaxWidthResource);
|
||
|
invalidateSelf();
|
||
|
}
|
||
|
|
||
|
public void setDarkIntensity(float darkIntensity) {
|
||
|
mDark = darkIntensity >= 0.5f;
|
||
|
}
|
||
|
|
||
|
public void setDelayTouchFeedback(boolean delay) {
|
||
|
mDelayTouchFeedback = delay;
|
||
|
}
|
||
|
|
||
|
public void setType(Type type) {
|
||
|
mType = type;
|
||
|
}
|
||
|
|
||
|
private Paint getRipplePaint() {
|
||
|
if (mRipplePaint == null) {
|
||
|
mRipplePaint = new Paint();
|
||
|
mRipplePaint.setAntiAlias(true);
|
||
|
mRipplePaint.setColor(mLastDark ? 0xff000000 : 0xffffffff);
|
||
|
}
|
||
|
return mRipplePaint;
|
||
|
}
|
||
|
|
||
|
private void drawSoftware(Canvas canvas) {
|
||
|
if (mGlowAlpha > 0f) {
|
||
|
final Paint p = getRipplePaint();
|
||
|
p.setAlpha((int) (mGlowAlpha * 255f));
|
||
|
|
||
|
final float w = getBounds().width();
|
||
|
final float h = getBounds().height();
|
||
|
final boolean horizontal = w > h;
|
||
|
final float diameter = getRippleSize() * mGlowScale;
|
||
|
final float radius = diameter * .5f;
|
||
|
final float cx = w * .5f;
|
||
|
final float cy = h * .5f;
|
||
|
final float rx = horizontal ? radius : cx;
|
||
|
final float ry = horizontal ? cy : radius;
|
||
|
final float corner = horizontal ? cy : cx;
|
||
|
|
||
|
if (mType == Type.ROUNDED_RECT) {
|
||
|
canvas.drawRoundRect(cx - rx, cy - ry, cx + rx, cy + ry, corner, corner, p);
|
||
|
} else {
|
||
|
canvas.save();
|
||
|
canvas.translate(cx, cy);
|
||
|
float r = Math.min(rx, ry);
|
||
|
canvas.drawOval(-r, -r, r, r, p);
|
||
|
canvas.restore();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void draw(Canvas canvas) {
|
||
|
mSupportHardware = canvas.isHardwareAccelerated();
|
||
|
if (mSupportHardware) {
|
||
|
drawHardware((RecordingCanvas) canvas);
|
||
|
} else {
|
||
|
drawSoftware(canvas);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void setAlpha(int alpha) {
|
||
|
// Not supported.
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void setColorFilter(ColorFilter colorFilter) {
|
||
|
// Not supported.
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int getOpacity() {
|
||
|
return PixelFormat.TRANSLUCENT;
|
||
|
}
|
||
|
|
||
|
private boolean isHorizontal() {
|
||
|
return getBounds().width() > getBounds().height();
|
||
|
}
|
||
|
|
||
|
private void drawHardware(RecordingCanvas c) {
|
||
|
if (mDrawingHardwareGlow) {
|
||
|
if (mType == Type.ROUNDED_RECT) {
|
||
|
c.drawRoundRect(mLeftProp, mTopProp, mRightProp, mBottomProp, mRxProp, mRyProp,
|
||
|
mPaintProp);
|
||
|
} else {
|
||
|
CanvasProperty<Float> cx = CanvasProperty.createFloat(getBounds().width() / 2);
|
||
|
CanvasProperty<Float> cy = CanvasProperty.createFloat(getBounds().height() / 2);
|
||
|
int d = Math.min(getBounds().width(), getBounds().height());
|
||
|
CanvasProperty<Float> r = CanvasProperty.createFloat(1.0f * d / 2);
|
||
|
c.drawCircle(cx, cy, r, mPaintProp);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** Gets the glow alpha, used by {@link android.animation.ObjectAnimator} via reflection. */
|
||
|
public float getGlowAlpha() {
|
||
|
return mGlowAlpha;
|
||
|
}
|
||
|
|
||
|
/** Sets the glow alpha, used by {@link android.animation.ObjectAnimator} via reflection. */
|
||
|
public void setGlowAlpha(float x) {
|
||
|
mGlowAlpha = x;
|
||
|
invalidateSelf();
|
||
|
}
|
||
|
|
||
|
/** Gets the glow scale, used by {@link android.animation.ObjectAnimator} via reflection. */
|
||
|
public float getGlowScale() {
|
||
|
return mGlowScale;
|
||
|
}
|
||
|
|
||
|
/** Sets the glow scale, used by {@link android.animation.ObjectAnimator} via reflection. */
|
||
|
public void setGlowScale(float x) {
|
||
|
mGlowScale = x;
|
||
|
invalidateSelf();
|
||
|
}
|
||
|
|
||
|
private float getMaxGlowAlpha() {
|
||
|
return mLastDark ? GLOW_MAX_ALPHA_DARK : GLOW_MAX_ALPHA;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
protected boolean onStateChange(int[] state) {
|
||
|
boolean pressed = false;
|
||
|
for (int i = 0; i < state.length; i++) {
|
||
|
if (state[i] == android.R.attr.state_pressed) {
|
||
|
pressed = true;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
if (pressed != mPressed) {
|
||
|
setPressed(pressed);
|
||
|
mPressed = pressed;
|
||
|
return true;
|
||
|
} else {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public boolean setVisible(boolean visible, boolean restart) {
|
||
|
boolean changed = super.setVisible(visible, restart);
|
||
|
if (changed) {
|
||
|
// End any existing animations when the visibility changes
|
||
|
jumpToCurrentState();
|
||
|
}
|
||
|
return changed;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void jumpToCurrentState() {
|
||
|
endAnimations("jumpToCurrentState", false /* cancel */);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public boolean isStateful() {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public boolean hasFocusStateSpecified() {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
public void setPressed(boolean pressed) {
|
||
|
if (mDark != mLastDark && pressed) {
|
||
|
mRipplePaint = null;
|
||
|
mLastDark = mDark;
|
||
|
}
|
||
|
if (mSupportHardware) {
|
||
|
setPressedHardware(pressed);
|
||
|
} else {
|
||
|
setPressedSoftware(pressed);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Abort the ripple while it is delayed and before shown used only when setShouldDelayStartTouch
|
||
|
* is enabled.
|
||
|
*/
|
||
|
public void abortDelayedRipple() {
|
||
|
mHandler.removeCallbacksAndMessages(null);
|
||
|
}
|
||
|
|
||
|
private void endAnimations(String reason, boolean cancel) {
|
||
|
Trace.beginSection("KeyButtonRipple.endAnim: reason=" + reason + " cancel=" + cancel);
|
||
|
Trace.endSection();
|
||
|
mVisible = false;
|
||
|
mTmpArray.addAll(mRunningAnimations);
|
||
|
int size = mTmpArray.size();
|
||
|
for (int i = 0; i < size; i++) {
|
||
|
Animator a = mTmpArray.get(i);
|
||
|
if (cancel) {
|
||
|
a.cancel();
|
||
|
} else {
|
||
|
a.end();
|
||
|
}
|
||
|
}
|
||
|
mTmpArray.clear();
|
||
|
mRunningAnimations.clear();
|
||
|
mHandler.removeCallbacksAndMessages(null);
|
||
|
}
|
||
|
|
||
|
private void setPressedSoftware(boolean pressed) {
|
||
|
if (pressed) {
|
||
|
if (mDelayTouchFeedback) {
|
||
|
if (mRunningAnimations.isEmpty()) {
|
||
|
mHandler.removeCallbacksAndMessages(null);
|
||
|
mHandler.postDelayed(this::enterSoftware, ViewConfiguration.getTapTimeout());
|
||
|
} else if (mVisible) {
|
||
|
enterSoftware();
|
||
|
}
|
||
|
} else {
|
||
|
enterSoftware();
|
||
|
}
|
||
|
} else {
|
||
|
exitSoftware();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void enterSoftware() {
|
||
|
endAnimations("enterSoftware", true /* cancel */);
|
||
|
mVisible = true;
|
||
|
mGlowAlpha = getMaxGlowAlpha();
|
||
|
ObjectAnimator scaleAnimator = ObjectAnimator.ofFloat(this, "glowScale",
|
||
|
0f, GLOW_MAX_SCALE_FACTOR);
|
||
|
scaleAnimator.setInterpolator(mInterpolator);
|
||
|
scaleAnimator.setDuration(ANIMATION_DURATION_SCALE);
|
||
|
scaleAnimator.addListener(mAnimatorListener);
|
||
|
scaleAnimator.start();
|
||
|
mRunningAnimations.add(scaleAnimator);
|
||
|
|
||
|
// With the delay, it could eventually animate the enter animation with no pressed state,
|
||
|
// then immediately show the exit animation. If this is skipped there will be no ripple.
|
||
|
if (mDelayTouchFeedback && !mPressed) {
|
||
|
exitSoftware();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void exitSoftware() {
|
||
|
ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(this, "glowAlpha", mGlowAlpha, 0f);
|
||
|
alphaAnimator.setInterpolator(ALPHA_OUT_INTERPOLATOR);
|
||
|
alphaAnimator.setDuration(ANIMATION_DURATION_FADE);
|
||
|
alphaAnimator.addListener(mAnimatorListener);
|
||
|
alphaAnimator.start();
|
||
|
mRunningAnimations.add(alphaAnimator);
|
||
|
}
|
||
|
|
||
|
private void setPressedHardware(boolean pressed) {
|
||
|
if (pressed) {
|
||
|
if (mDelayTouchFeedback) {
|
||
|
if (mRunningAnimations.isEmpty()) {
|
||
|
mHandler.removeCallbacksAndMessages(null);
|
||
|
mHandler.postDelayed(this::enterHardware, ViewConfiguration.getTapTimeout());
|
||
|
} else if (mVisible) {
|
||
|
enterHardware();
|
||
|
}
|
||
|
} else {
|
||
|
enterHardware();
|
||
|
}
|
||
|
} else {
|
||
|
exitHardware();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets the left/top property for the round rect to {@code prop} depending on whether we are
|
||
|
* horizontal or vertical mode.
|
||
|
*/
|
||
|
private void setExtendStart(CanvasProperty<Float> prop) {
|
||
|
if (isHorizontal()) {
|
||
|
mLeftProp = prop;
|
||
|
} else {
|
||
|
mTopProp = prop;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private CanvasProperty<Float> getExtendStart() {
|
||
|
return isHorizontal() ? mLeftProp : mTopProp;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets the right/bottom property for the round rect to {@code prop} depending on whether we are
|
||
|
* horizontal or vertical mode.
|
||
|
*/
|
||
|
private void setExtendEnd(CanvasProperty<Float> prop) {
|
||
|
if (isHorizontal()) {
|
||
|
mRightProp = prop;
|
||
|
} else {
|
||
|
mBottomProp = prop;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private CanvasProperty<Float> getExtendEnd() {
|
||
|
return isHorizontal() ? mRightProp : mBottomProp;
|
||
|
}
|
||
|
|
||
|
private int getExtendSize() {
|
||
|
return isHorizontal() ? getBounds().width() : getBounds().height();
|
||
|
}
|
||
|
|
||
|
private int getRippleSize() {
|
||
|
int size = isHorizontal() ? getBounds().width() : getBounds().height();
|
||
|
return Math.min(size, mMaxWidth);
|
||
|
}
|
||
|
|
||
|
private void enterHardware() {
|
||
|
endAnimations("enterHardware", true /* cancel */);
|
||
|
mVisible = true;
|
||
|
mDrawingHardwareGlow = true;
|
||
|
setExtendStart(CanvasProperty.createFloat(getExtendSize() / 2));
|
||
|
final RenderNodeAnimator startAnim = new RenderNodeAnimator(getExtendStart(),
|
||
|
getExtendSize() / 2 - GLOW_MAX_SCALE_FACTOR * getRippleSize() / 2);
|
||
|
startAnim.setDuration(ANIMATION_DURATION_SCALE);
|
||
|
startAnim.setInterpolator(mInterpolator);
|
||
|
startAnim.addListener(mAnimatorListener);
|
||
|
startAnim.setTarget(mTargetView);
|
||
|
|
||
|
setExtendEnd(CanvasProperty.createFloat(getExtendSize() / 2));
|
||
|
final RenderNodeAnimator endAnim = new RenderNodeAnimator(getExtendEnd(),
|
||
|
getExtendSize() / 2 + GLOW_MAX_SCALE_FACTOR * getRippleSize() / 2);
|
||
|
endAnim.setDuration(ANIMATION_DURATION_SCALE);
|
||
|
endAnim.setInterpolator(mInterpolator);
|
||
|
endAnim.addListener(mAnimatorListener);
|
||
|
endAnim.addListener(mEnterHwTraceAnimator);
|
||
|
endAnim.setTarget(mTargetView);
|
||
|
|
||
|
if (isHorizontal()) {
|
||
|
mTopProp = CanvasProperty.createFloat(0f);
|
||
|
mBottomProp = CanvasProperty.createFloat(getBounds().height());
|
||
|
mRxProp = CanvasProperty.createFloat(getBounds().height() / 2);
|
||
|
mRyProp = CanvasProperty.createFloat(getBounds().height() / 2);
|
||
|
} else {
|
||
|
mLeftProp = CanvasProperty.createFloat(0f);
|
||
|
mRightProp = CanvasProperty.createFloat(getBounds().width());
|
||
|
mRxProp = CanvasProperty.createFloat(getBounds().width() / 2);
|
||
|
mRyProp = CanvasProperty.createFloat(getBounds().width() / 2);
|
||
|
}
|
||
|
|
||
|
mGlowScale = GLOW_MAX_SCALE_FACTOR;
|
||
|
mGlowAlpha = getMaxGlowAlpha();
|
||
|
mRipplePaint = getRipplePaint();
|
||
|
mRipplePaint.setAlpha((int) (mGlowAlpha * 255));
|
||
|
mPaintProp = CanvasProperty.createPaint(mRipplePaint);
|
||
|
|
||
|
startAnim.start();
|
||
|
endAnim.start();
|
||
|
mRunningAnimations.add(startAnim);
|
||
|
mRunningAnimations.add(endAnim);
|
||
|
|
||
|
invalidateSelf();
|
||
|
|
||
|
// With the delay, it could eventually animate the enter animation with no pressed state,
|
||
|
// then immediately show the exit animation. If this is skipped there will be no ripple.
|
||
|
if (mDelayTouchFeedback && !mPressed) {
|
||
|
exitHardware();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void exitHardware() {
|
||
|
mPaintProp = CanvasProperty.createPaint(getRipplePaint());
|
||
|
final RenderNodeAnimator opacityAnim = new RenderNodeAnimator(mPaintProp,
|
||
|
RenderNodeAnimator.PAINT_ALPHA, 0);
|
||
|
opacityAnim.setDuration(ANIMATION_DURATION_FADE);
|
||
|
opacityAnim.setInterpolator(ALPHA_OUT_INTERPOLATOR);
|
||
|
opacityAnim.addListener(mAnimatorListener);
|
||
|
opacityAnim.addListener(mExitHwTraceAnimator);
|
||
|
opacityAnim.setTarget(mTargetView);
|
||
|
|
||
|
opacityAnim.start();
|
||
|
mRunningAnimations.add(opacityAnim);
|
||
|
|
||
|
invalidateSelf();
|
||
|
}
|
||
|
|
||
|
private final AnimatorListenerAdapter mAnimatorListener =
|
||
|
new AnimatorListenerAdapter() {
|
||
|
@Override
|
||
|
public void onAnimationEnd(Animator animation) {
|
||
|
mRunningAnimations.remove(animation);
|
||
|
if (mRunningAnimations.isEmpty() && !mPressed) {
|
||
|
mVisible = false;
|
||
|
mDrawingHardwareGlow = false;
|
||
|
invalidateSelf();
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
private static final class TraceAnimatorListener extends AnimatorListenerAdapter {
|
||
|
private final String mName;
|
||
|
TraceAnimatorListener(String name) {
|
||
|
mName = name;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onAnimationStart(Animator animation) {
|
||
|
Trace.beginSection("KeyButtonRipple.start." + mName);
|
||
|
Trace.endSection();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onAnimationCancel(Animator animation) {
|
||
|
Trace.beginSection("KeyButtonRipple.cancel." + mName);
|
||
|
Trace.endSection();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onAnimationEnd(Animator animation) {
|
||
|
Trace.beginSection("KeyButtonRipple.end." + mName);
|
||
|
Trace.endSection();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Interpolator with a smooth log deceleration
|
||
|
*/
|
||
|
private static final class LogInterpolator implements Interpolator {
|
||
|
@Override
|
||
|
public float getInterpolation(float input) {
|
||
|
return 1 - (float) Math.pow(400, -input * 1.4);
|
||
|
}
|
||
|
}
|
||
|
}
|