blob: d061c8ef6e120375663413b9f022dee108bf1748 [file] [log] [blame]
/*
* Copyright (C) 2020 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.accessibility;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UiContext;
import android.content.Context;
import android.content.res.Resources;
import android.os.RemoteException;
import android.util.Log;
import android.view.accessibility.IRemoteMagnificationAnimationCallback;
import android.view.animation.AccelerateInterpolator;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.R;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Provides same functionality of {@link WindowMagnificationController}. Some methods run with
* the animation.
*/
class WindowMagnificationAnimationController implements ValueAnimator.AnimatorUpdateListener,
Animator.AnimatorListener {
private static final String TAG = "WindowMagnificationAnimationController";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
@Retention(RetentionPolicy.SOURCE)
@IntDef({STATE_DISABLED, STATE_ENABLED, STATE_DISABLING, STATE_ENABLING})
@interface MagnificationState {}
// The window magnification is disabled.
@VisibleForTesting static final int STATE_DISABLED = 0;
// The window magnification is enabled.
@VisibleForTesting static final int STATE_ENABLED = 1;
// The window magnification is going to be disabled when the animation is end.
private static final int STATE_DISABLING = 2;
// The animation is running for enabling the window magnification.
private static final int STATE_ENABLING = 3;
private WindowMagnificationController mController;
private final ValueAnimator mValueAnimator;
private final AnimationSpec mStartSpec = new AnimationSpec();
private final AnimationSpec mEndSpec = new AnimationSpec();
private float mMagnificationFrameOffsetRatioX = 0f;
private float mMagnificationFrameOffsetRatioY = 0f;
private final Context mContext;
// Called when the animation is ended successfully without cancelling or mStartSpec and
// mEndSpec are equal.
private IRemoteMagnificationAnimationCallback mAnimationCallback;
// The flag to ignore the animation end callback.
private boolean mEndAnimationCanceled = false;
@MagnificationState
private int mState = STATE_DISABLED;
WindowMagnificationAnimationController(@UiContext Context context) {
this(context, newValueAnimator(context.getResources()));
}
@VisibleForTesting
WindowMagnificationAnimationController(Context context, ValueAnimator valueAnimator) {
mContext = context;
mValueAnimator = valueAnimator;
mValueAnimator.addUpdateListener(this);
mValueAnimator.addListener(this);
}
void setWindowMagnificationController(@NonNull WindowMagnificationController controller) {
mController = controller;
}
/**
* Wraps {@link WindowMagnificationController#enableWindowMagnification(float, float, float,
* float, float, IRemoteMagnificationAnimationCallback)}
* with transition animation. If the window magnification is not enabled, the scale will start
* from 1.0 and the center won't be changed during the animation. If {@link #mState} is
* {@code STATE_DISABLING}, the animation runs in reverse.
*
* @param scale The target scale, or {@link Float#NaN} to leave unchanged.
* @param centerX The screen-relative X coordinate around which to center,
* or {@link Float#NaN} to leave unchanged.
* @param centerY The screen-relative Y coordinate around which to center,
* or {@link Float#NaN} to leave unchanged.
* @param animationCallback Called when the transition is complete, the given arguments
* are as same as current values, or the transition is interrupted
* due to the new transition request.
*
* @see #onAnimationUpdate(ValueAnimator)
*/
void enableWindowMagnification(float scale, float centerX, float centerY,
@Nullable IRemoteMagnificationAnimationCallback animationCallback) {
enableWindowMagnification(scale, centerX, centerY, 0f, 0f, animationCallback);
}
/**
* Wraps {@link WindowMagnificationController#enableWindowMagnification(float, float, float,
* float, float, IRemoteMagnificationAnimationCallback)}
* with transition animation. If the window magnification is not enabled, the scale will start
* from 1.0 and the center won't be changed during the animation. If {@link #mState} is
* {@code STATE_DISABLING}, the animation runs in reverse.
*
* @param scale The target scale, or {@link Float#NaN} to leave unchanged.
* @param centerX The screen-relative X coordinate around which to center for magnification,
* or {@link Float#NaN} to leave unchanged.
* @param centerY The screen-relative Y coordinate around which to center for magnification,
* or {@link Float#NaN} to leave unchanged.
* @param magnificationFrameOffsetRatioX Indicate the X coordinate offset between
* frame position X and centerX
* @param magnificationFrameOffsetRatioY Indicate the Y coordinate offset between
* frame position Y and centerY
* @param animationCallback Called when the transition is complete, the given arguments
* are as same as current values, or the transition is interrupted
* due to the new transition request.
*
* @see #onAnimationUpdate(ValueAnimator)
*/
void enableWindowMagnification(float scale, float centerX, float centerY,
float magnificationFrameOffsetRatioX, float magnificationFrameOffsetRatioY,
@Nullable IRemoteMagnificationAnimationCallback animationCallback) {
if (mController == null) {
return;
}
sendAnimationCallback(false);
mMagnificationFrameOffsetRatioX = magnificationFrameOffsetRatioX;
mMagnificationFrameOffsetRatioY = magnificationFrameOffsetRatioY;
// Enable window magnification without animation immediately.
if (animationCallback == null) {
if (mState == STATE_ENABLING || mState == STATE_DISABLING) {
mValueAnimator.cancel();
}
mController.enableWindowMagnificationInternal(scale, centerX, centerY,
mMagnificationFrameOffsetRatioX, mMagnificationFrameOffsetRatioY);
updateState();
return;
}
mAnimationCallback = animationCallback;
setupEnableAnimationSpecs(scale, centerX, centerY);
if (mEndSpec.equals(mStartSpec)) {
if (mState == STATE_DISABLED) {
mController.enableWindowMagnificationInternal(scale, centerX, centerY,
mMagnificationFrameOffsetRatioX, mMagnificationFrameOffsetRatioY);
} else if (mState == STATE_ENABLING || mState == STATE_DISABLING) {
mValueAnimator.cancel();
}
sendAnimationCallback(true);
updateState();
} else {
if (mState == STATE_DISABLING) {
mValueAnimator.reverse();
} else {
if (mState == STATE_ENABLING) {
mValueAnimator.cancel();
}
mValueAnimator.start();
}
setState(STATE_ENABLING);
}
}
void moveWindowMagnifierToPosition(float centerX, float centerY,
IRemoteMagnificationAnimationCallback callback) {
if (mState == STATE_ENABLED) {
// We set the animation duration to shortAnimTime which would be reset at the end.
mValueAnimator.setDuration(mContext.getResources()
.getInteger(com.android.internal.R.integer.config_shortAnimTime));
enableWindowMagnification(Float.NaN, centerX, centerY,
/* magnificationFrameOffsetRatioX */ Float.NaN,
/* magnificationFrameOffsetRatioY */ Float.NaN, callback);
} else if (mState == STATE_ENABLING) {
sendAnimationCallback(false);
mAnimationCallback = callback;
mValueAnimator.setDuration(mContext.getResources()
.getInteger(com.android.internal.R.integer.config_shortAnimTime));
setupEnableAnimationSpecs(Float.NaN, centerX, centerY);
}
}
private void setupEnableAnimationSpecs(float scale, float centerX, float centerY) {
if (mController == null) {
return;
}
final float currentScale = mController.getScale();
final float currentCenterX = mController.getCenterX();
final float currentCenterY = mController.getCenterY();
if (mState == STATE_DISABLED) {
// We don't need to offset the center during the animation.
mStartSpec.set(/* scale*/ 1.0f, centerX, centerY);
mEndSpec.set(Float.isNaN(scale) ? mContext.getResources().getInteger(
R.integer.magnification_default_scale) : scale, centerX, centerY);
} else {
mStartSpec.set(currentScale, currentCenterX, currentCenterY);
final float endScale = (mState == STATE_ENABLING ? mEndSpec.mScale : currentScale);
final float endCenterX =
(mState == STATE_ENABLING ? mEndSpec.mCenterX : currentCenterX);
final float endCenterY =
(mState == STATE_ENABLING ? mEndSpec.mCenterY : currentCenterY);
mEndSpec.set(Float.isNaN(scale) ? endScale : scale,
Float.isNaN(centerX) ? endCenterX : centerX,
Float.isNaN(centerY) ? endCenterY : centerY);
}
if (DEBUG) {
Log.d(TAG, "SetupEnableAnimationSpecs : mStartSpec = " + mStartSpec + ", endSpec = "
+ mEndSpec);
}
}
/** Returns {@code true} if the animator is running. */
boolean isAnimating() {
return mValueAnimator.isRunning();
}
/**
* Wraps {@link WindowMagnificationController#deleteWindowMagnification()}} with transition
* animation. If the window magnification is enabling, it runs the animation in reverse.
*
* @param animationCallback Called when the transition is complete, the given arguments
* are as same as current values, or the transition is interrupted
* due to the new transition request.
*/
void deleteWindowMagnification(
@Nullable IRemoteMagnificationAnimationCallback animationCallback) {
if (mController == null) {
return;
}
sendAnimationCallback(false);
// Delete window magnification without animation.
if (animationCallback == null) {
if (mState == STATE_ENABLING || mState == STATE_DISABLING) {
mValueAnimator.cancel();
}
mController.deleteWindowMagnification();
updateState();
return;
}
mAnimationCallback = animationCallback;
if (mState == STATE_DISABLED || mState == STATE_DISABLING) {
if (mState == STATE_DISABLED) {
sendAnimationCallback(true);
}
return;
}
mStartSpec.set(/* scale*/ 1.0f, Float.NaN, Float.NaN);
mEndSpec.set(/* scale*/ mController.getScale(), Float.NaN, Float.NaN);
mValueAnimator.reverse();
setState(STATE_DISABLING);
}
private void updateState() {
if (Float.isNaN(mController.getScale())) {
setState(STATE_DISABLED);
} else {
setState(STATE_ENABLED);
}
}
private void setState(@MagnificationState int state) {
if (DEBUG) {
Log.d(TAG, "setState from " + mState + " to " + state);
}
mState = state;
}
@VisibleForTesting
@MagnificationState int getState() {
return mState;
}
@Override
public void onAnimationStart(Animator animation) {
mEndAnimationCanceled = false;
}
@Override
public void onAnimationEnd(Animator animation, boolean isReverse) {
if (mEndAnimationCanceled || mController == null) {
return;
}
if (mState == STATE_DISABLING) {
mController.deleteWindowMagnification();
}
updateState();
sendAnimationCallback(true);
// We reset the duration to config_longAnimTime
mValueAnimator.setDuration(mContext.getResources()
.getInteger(com.android.internal.R.integer.config_longAnimTime));
}
@Override
public void onAnimationEnd(Animator animation) {
}
@Override
public void onAnimationCancel(Animator animation) {
mEndAnimationCanceled = true;
}
@Override
public void onAnimationRepeat(Animator animation) {
}
private void sendAnimationCallback(boolean success) {
if (mAnimationCallback != null) {
try {
mAnimationCallback.onResult(success);
if (DEBUG) {
Log.d(TAG, "sendAnimationCallback success = " + success);
}
} catch (RemoteException e) {
Log.w(TAG, "sendAnimationCallback failed : " + e);
}
mAnimationCallback = null;
}
}
@Override
public void onAnimationUpdate(ValueAnimator animation) {
if (mController == null) {
return;
}
final float fract = animation.getAnimatedFraction();
final float sentScale = mStartSpec.mScale + (mEndSpec.mScale - mStartSpec.mScale) * fract;
final float centerX =
mStartSpec.mCenterX + (mEndSpec.mCenterX - mStartSpec.mCenterX) * fract;
final float centerY =
mStartSpec.mCenterY + (mEndSpec.mCenterY - mStartSpec.mCenterY) * fract;
mController.enableWindowMagnificationInternal(sentScale, centerX, centerY,
mMagnificationFrameOffsetRatioX, mMagnificationFrameOffsetRatioY);
}
private static ValueAnimator newValueAnimator(Resources resource) {
final ValueAnimator valueAnimator = new ValueAnimator();
valueAnimator.setDuration(
resource.getInteger(com.android.internal.R.integer.config_longAnimTime));
valueAnimator.setInterpolator(new AccelerateInterpolator(2.5f));
valueAnimator.setFloatValues(0.0f, 1.0f);
return valueAnimator;
}
private static class AnimationSpec {
private float mScale = Float.NaN;
private float mCenterX = Float.NaN;
private float mCenterY = Float.NaN;
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (other == null || getClass() != other.getClass()) {
return false;
}
final AnimationSpec s = (AnimationSpec) other;
return mScale == s.mScale && mCenterX == s.mCenterX && mCenterY == s.mCenterY;
}
@Override
public int hashCode() {
int result = (mScale != +0.0f ? Float.floatToIntBits(mScale) : 0);
result = 31 * result + (mCenterX != +0.0f ? Float.floatToIntBits(mCenterX) : 0);
result = 31 * result + (mCenterY != +0.0f ? Float.floatToIntBits(mCenterY) : 0);
return result;
}
void set(float scale, float centerX, float centerY) {
mScale = scale;
mCenterX = centerX;
mCenterY = centerY;
}
@Override
public String toString() {
return "AnimationSpec{"
+ "mScale=" + mScale
+ ", mCenterX=" + mCenterX
+ ", mCenterY=" + mCenterY
+ '}';
}
}
}