| /* |
| * 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.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.io.PrintWriter; |
| 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. |
| private static final int STATE_DISABLED = 0; |
| // The window magnification is enabled. |
| private 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 final WindowMagnificationController mController; |
| private final ValueAnimator mValueAnimator; |
| private final AnimationSpec mStartSpec = new AnimationSpec(); |
| private final AnimationSpec mEndSpec = new AnimationSpec(); |
| 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, |
| WindowMagnificationController controller) { |
| this(context, controller, newValueAnimator(context.getResources())); |
| } |
| |
| @VisibleForTesting |
| WindowMagnificationAnimationController(Context context, |
| WindowMagnificationController controller, ValueAnimator valueAnimator) { |
| mContext = context; |
| mController = controller; |
| mValueAnimator = valueAnimator; |
| mValueAnimator.addUpdateListener(this); |
| mValueAnimator.addListener(this); |
| } |
| |
| /** |
| * Wraps {@link WindowMagnificationController#enableWindowMagnification(float, float, float)} |
| * 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) { |
| sendAnimationCallback(false); |
| mAnimationCallback = animationCallback; |
| setupEnableAnimationSpecs(scale, centerX, centerY); |
| if (mEndSpec.equals(mStartSpec)) { |
| if (mState == STATE_DISABLED) { |
| mController.enableWindowMagnification(scale, centerX, centerY); |
| } else if (mState == STATE_ENABLING || mState == STATE_DISABLING) { |
| mValueAnimator.cancel(); |
| } |
| sendAnimationCallback(true); |
| setState(STATE_ENABLED); |
| } else { |
| if (mState == STATE_DISABLING) { |
| mValueAnimator.reverse(); |
| } else { |
| if (mState == STATE_ENABLING) { |
| mValueAnimator.cancel(); |
| } |
| mValueAnimator.start(); |
| } |
| setState(STATE_ENABLING); |
| } |
| } |
| |
| private void setupEnableAnimationSpecs(float scale, float centerX, float centerY) { |
| 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); |
| mEndSpec.set(Float.isNaN(scale) ? currentScale : scale, |
| Float.isNaN(centerX) ? currentCenterX : centerX, |
| Float.isNaN(centerY) ? currentCenterY : centerY); |
| } |
| if (DEBUG) { |
| Log.d(TAG, "SetupEnableAnimationSpecs : mStartSpec = " + mStartSpec + ", endSpec = " |
| + mEndSpec); |
| } |
| } |
| |
| /** |
| * Wraps {@link WindowMagnificationController#setScale(float)}. If the animation is |
| * running, it has no effect. |
| */ |
| void setScale(float scale) { |
| if (mValueAnimator.isRunning()) { |
| return; |
| } |
| mController.setScale(scale); |
| } |
| |
| /** |
| * 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) { |
| sendAnimationCallback(false); |
| 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); |
| } |
| |
| /** |
| * Wraps {@link WindowMagnificationController#moveWindowMagnifier(float, float)}. If the |
| * animation is running, it has no effect. |
| * @param offsetX The amount in pixels to offset the window magnifier in the X direction, in |
| * current screen pixels. |
| * @param offsetY The amount in pixels to offset the window magnifier in the Y direction, in |
| * current screen pixels. |
| */ |
| void moveWindowMagnifier(float offsetX, float offsetY) { |
| if (mValueAnimator.isRunning()) { |
| return; |
| } |
| mController.moveWindowMagnifier(offsetX, offsetY); |
| } |
| |
| void onConfigurationChanged(int configDiff) { |
| mController.onConfigurationChanged(configDiff); |
| } |
| |
| private void setState(@MagnificationState int state) { |
| if (DEBUG) { |
| Log.d(TAG, "setState from " + mState + " to " + state); |
| } |
| mState = state; |
| } |
| |
| @Override |
| public void onAnimationStart(Animator animation) { |
| mEndAnimationCanceled = false; |
| } |
| |
| @Override |
| public void onAnimationEnd(Animator animation, boolean isReverse) { |
| if (mEndAnimationCanceled) { |
| return; |
| } |
| if (isReverse) { |
| mController.deleteWindowMagnification(); |
| setState(STATE_DISABLED); |
| } else { |
| setState(STATE_ENABLED); |
| } |
| sendAnimationCallback(true); |
| } |
| |
| @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) { |
| 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.enableWindowMagnification(sentScale, centerX, centerY); |
| } |
| |
| public void updateSysUiStateFlag() { |
| mController.updateSysUIStateFlag(); |
| } |
| |
| void dump(PrintWriter pw) { |
| mController.dump(pw); |
| } |
| |
| private static ValueAnimator newValueAnimator(Resources resources) { |
| final ValueAnimator valueAnimator = new ValueAnimator(); |
| valueAnimator.setDuration( |
| resources.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 |
| + '}'; |
| } |
| } |
| } |