/* * 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.res.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; private Runnable mOnAnimationEndRunnable; 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.updateWindowMagnificationInternal(scale, centerX, centerY, mMagnificationFrameOffsetRatioX, mMagnificationFrameOffsetRatioY); updateState(); return; } mAnimationCallback = animationCallback; setupEnableAnimationSpecs(scale, centerX, centerY); if (mEndSpec.equals(mStartSpec)) { if (mState == STATE_DISABLED) { mController.updateWindowMagnificationInternal(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; } mOnAnimationEndRunnable.run(); 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) { } void setOnAnimationEndRunnable(Runnable runnable) { mOnAnimationEndRunnable = runnable; } 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.updateWindowMagnificationInternal(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 + '}'; } } }