1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.accessibility; 18 19 import android.animation.Animator; 20 import android.animation.ValueAnimator; 21 import android.annotation.IntDef; 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.annotation.UiContext; 25 import android.content.Context; 26 import android.content.res.Resources; 27 import android.os.RemoteException; 28 import android.util.Log; 29 import android.view.accessibility.IRemoteMagnificationAnimationCallback; 30 import android.view.animation.AccelerateInterpolator; 31 32 import com.android.internal.annotations.VisibleForTesting; 33 import com.android.systemui.res.R; 34 35 import java.lang.annotation.Retention; 36 import java.lang.annotation.RetentionPolicy; 37 38 /** 39 * Provides same functionality of {@link WindowMagnificationController}. Some methods run with 40 * the animation. 41 */ 42 class WindowMagnificationAnimationController implements ValueAnimator.AnimatorUpdateListener, 43 Animator.AnimatorListener { 44 45 private static final String TAG = "WindowMagnificationAnimationController"; 46 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 47 48 @Retention(RetentionPolicy.SOURCE) 49 @IntDef({STATE_DISABLED, STATE_ENABLED, STATE_DISABLING, STATE_ENABLING}) 50 @interface MagnificationState {} 51 52 // The window magnification is disabled. 53 @VisibleForTesting static final int STATE_DISABLED = 0; 54 // The window magnification is enabled. 55 @VisibleForTesting static final int STATE_ENABLED = 1; 56 // The window magnification is going to be disabled when the animation is end. 57 private static final int STATE_DISABLING = 2; 58 // The animation is running for enabling the window magnification. 59 private static final int STATE_ENABLING = 3; 60 61 private WindowMagnificationController mController; 62 private final ValueAnimator mValueAnimator; 63 private final AnimationSpec mStartSpec = new AnimationSpec(); 64 private final AnimationSpec mEndSpec = new AnimationSpec(); 65 private float mMagnificationFrameOffsetRatioX = 0f; 66 private float mMagnificationFrameOffsetRatioY = 0f; 67 private final Context mContext; 68 // Called when the animation is ended successfully without cancelling or mStartSpec and 69 // mEndSpec are equal. 70 private IRemoteMagnificationAnimationCallback mAnimationCallback; 71 // The flag to ignore the animation end callback. 72 private boolean mEndAnimationCanceled = false; 73 @MagnificationState 74 private int mState = STATE_DISABLED; 75 private Runnable mOnAnimationEndRunnable; 76 WindowMagnificationAnimationController(@iContext Context context)77 WindowMagnificationAnimationController(@UiContext Context context) { 78 this(context, newValueAnimator(context.getResources())); 79 } 80 81 @VisibleForTesting WindowMagnificationAnimationController(Context context, ValueAnimator valueAnimator)82 WindowMagnificationAnimationController(Context context, ValueAnimator valueAnimator) { 83 mContext = context; 84 mValueAnimator = valueAnimator; 85 mValueAnimator.addUpdateListener(this); 86 mValueAnimator.addListener(this); 87 } 88 setWindowMagnificationController(@onNull WindowMagnificationController controller)89 void setWindowMagnificationController(@NonNull WindowMagnificationController controller) { 90 mController = controller; 91 } 92 93 /** 94 * Wraps {@link WindowMagnificationController#enableWindowMagnification(float, float, float, 95 * float, float, IRemoteMagnificationAnimationCallback)} 96 * with transition animation. If the window magnification is not enabled, the scale will start 97 * from 1.0 and the center won't be changed during the animation. If {@link #mState} is 98 * {@code STATE_DISABLING}, the animation runs in reverse. 99 * 100 * @param scale The target scale, or {@link Float#NaN} to leave unchanged. 101 * @param centerX The screen-relative X coordinate around which to center, 102 * or {@link Float#NaN} to leave unchanged. 103 * @param centerY The screen-relative Y coordinate around which to center, 104 * or {@link Float#NaN} to leave unchanged. 105 * @param animationCallback Called when the transition is complete, the given arguments 106 * are as same as current values, or the transition is interrupted 107 * due to the new transition request. 108 * 109 * @see #onAnimationUpdate(ValueAnimator) 110 */ enableWindowMagnification(float scale, float centerX, float centerY, @Nullable IRemoteMagnificationAnimationCallback animationCallback)111 void enableWindowMagnification(float scale, float centerX, float centerY, 112 @Nullable IRemoteMagnificationAnimationCallback animationCallback) { 113 enableWindowMagnification(scale, centerX, centerY, 0f, 0f, animationCallback); 114 } 115 116 /** 117 * Wraps {@link WindowMagnificationController#enableWindowMagnification(float, float, float, 118 * float, float, IRemoteMagnificationAnimationCallback)} 119 * with transition animation. If the window magnification is not enabled, the scale will start 120 * from 1.0 and the center won't be changed during the animation. If {@link #mState} is 121 * {@code STATE_DISABLING}, the animation runs in reverse. 122 * 123 * @param scale The target scale, or {@link Float#NaN} to leave unchanged. 124 * @param centerX The screen-relative X coordinate around which to center for magnification, 125 * or {@link Float#NaN} to leave unchanged. 126 * @param centerY The screen-relative Y coordinate around which to center for magnification, 127 * or {@link Float#NaN} to leave unchanged. 128 * @param magnificationFrameOffsetRatioX Indicate the X coordinate offset between 129 * frame position X and centerX 130 * @param magnificationFrameOffsetRatioY Indicate the Y coordinate offset between 131 * frame position Y and centerY 132 * @param animationCallback Called when the transition is complete, the given arguments 133 * are as same as current values, or the transition is interrupted 134 * due to the new transition request. 135 * 136 * @see #onAnimationUpdate(ValueAnimator) 137 */ enableWindowMagnification(float scale, float centerX, float centerY, float magnificationFrameOffsetRatioX, float magnificationFrameOffsetRatioY, @Nullable IRemoteMagnificationAnimationCallback animationCallback)138 void enableWindowMagnification(float scale, float centerX, float centerY, 139 float magnificationFrameOffsetRatioX, float magnificationFrameOffsetRatioY, 140 @Nullable IRemoteMagnificationAnimationCallback animationCallback) { 141 if (mController == null) { 142 return; 143 } 144 sendAnimationCallback(false); 145 mMagnificationFrameOffsetRatioX = magnificationFrameOffsetRatioX; 146 mMagnificationFrameOffsetRatioY = magnificationFrameOffsetRatioY; 147 148 // Enable window magnification without animation immediately. 149 if (animationCallback == null) { 150 if (mState == STATE_ENABLING || mState == STATE_DISABLING) { 151 mValueAnimator.cancel(); 152 } 153 mController.updateWindowMagnificationInternal(scale, centerX, centerY, 154 mMagnificationFrameOffsetRatioX, mMagnificationFrameOffsetRatioY); 155 updateState(); 156 return; 157 } 158 mAnimationCallback = animationCallback; 159 setupEnableAnimationSpecs(scale, centerX, centerY); 160 161 if (mEndSpec.equals(mStartSpec)) { 162 if (mState == STATE_DISABLED) { 163 mController.updateWindowMagnificationInternal(scale, centerX, centerY, 164 mMagnificationFrameOffsetRatioX, mMagnificationFrameOffsetRatioY); 165 } else if (mState == STATE_ENABLING || mState == STATE_DISABLING) { 166 mValueAnimator.cancel(); 167 } 168 sendAnimationCallback(true); 169 updateState(); 170 } else { 171 if (mState == STATE_DISABLING) { 172 mValueAnimator.reverse(); 173 } else { 174 if (mState == STATE_ENABLING) { 175 mValueAnimator.cancel(); 176 } 177 mValueAnimator.start(); 178 } 179 setState(STATE_ENABLING); 180 } 181 } 182 moveWindowMagnifierToPosition(float centerX, float centerY, IRemoteMagnificationAnimationCallback callback)183 void moveWindowMagnifierToPosition(float centerX, float centerY, 184 IRemoteMagnificationAnimationCallback callback) { 185 if (mState == STATE_ENABLED) { 186 // We set the animation duration to shortAnimTime which would be reset at the end. 187 mValueAnimator.setDuration(mContext.getResources() 188 .getInteger(com.android.internal.R.integer.config_shortAnimTime)); 189 enableWindowMagnification(Float.NaN, centerX, centerY, 190 /* magnificationFrameOffsetRatioX */ Float.NaN, 191 /* magnificationFrameOffsetRatioY */ Float.NaN, callback); 192 } else if (mState == STATE_ENABLING) { 193 sendAnimationCallback(false); 194 mAnimationCallback = callback; 195 mValueAnimator.setDuration(mContext.getResources() 196 .getInteger(com.android.internal.R.integer.config_shortAnimTime)); 197 setupEnableAnimationSpecs(Float.NaN, centerX, centerY); 198 } 199 } 200 setupEnableAnimationSpecs(float scale, float centerX, float centerY)201 private void setupEnableAnimationSpecs(float scale, float centerX, float centerY) { 202 if (mController == null) { 203 return; 204 } 205 final float currentScale = mController.getScale(); 206 final float currentCenterX = mController.getCenterX(); 207 final float currentCenterY = mController.getCenterY(); 208 209 if (mState == STATE_DISABLED) { 210 // We don't need to offset the center during the animation. 211 mStartSpec.set(/* scale*/ 1.0f, centerX, centerY); 212 mEndSpec.set(Float.isNaN(scale) ? mContext.getResources().getInteger( 213 R.integer.magnification_default_scale) : scale, centerX, centerY); 214 } else { 215 mStartSpec.set(currentScale, currentCenterX, currentCenterY); 216 217 final float endScale = (mState == STATE_ENABLING ? mEndSpec.mScale : currentScale); 218 final float endCenterX = 219 (mState == STATE_ENABLING ? mEndSpec.mCenterX : currentCenterX); 220 final float endCenterY = 221 (mState == STATE_ENABLING ? mEndSpec.mCenterY : currentCenterY); 222 223 mEndSpec.set(Float.isNaN(scale) ? endScale : scale, 224 Float.isNaN(centerX) ? endCenterX : centerX, 225 Float.isNaN(centerY) ? endCenterY : centerY); 226 } 227 if (DEBUG) { 228 Log.d(TAG, "SetupEnableAnimationSpecs : mStartSpec = " + mStartSpec + ", endSpec = " 229 + mEndSpec); 230 } 231 } 232 233 /** Returns {@code true} if the animator is running. */ isAnimating()234 boolean isAnimating() { 235 return mValueAnimator.isRunning(); 236 } 237 238 /** 239 * Wraps {@link WindowMagnificationController#deleteWindowMagnification()}} with transition 240 * animation. If the window magnification is enabling, it runs the animation in reverse. 241 * 242 * @param animationCallback Called when the transition is complete, the given arguments 243 * are as same as current values, or the transition is interrupted 244 * due to the new transition request. 245 */ deleteWindowMagnification( @ullable IRemoteMagnificationAnimationCallback animationCallback)246 void deleteWindowMagnification( 247 @Nullable IRemoteMagnificationAnimationCallback animationCallback) { 248 if (mController == null) { 249 return; 250 } 251 sendAnimationCallback(false); 252 // Delete window magnification without animation. 253 if (animationCallback == null) { 254 if (mState == STATE_ENABLING || mState == STATE_DISABLING) { 255 mValueAnimator.cancel(); 256 } 257 mController.deleteWindowMagnification(); 258 updateState(); 259 return; 260 } 261 262 mAnimationCallback = animationCallback; 263 if (mState == STATE_DISABLED || mState == STATE_DISABLING) { 264 if (mState == STATE_DISABLED) { 265 sendAnimationCallback(true); 266 } 267 return; 268 } 269 mStartSpec.set(/* scale*/ 1.0f, Float.NaN, Float.NaN); 270 mEndSpec.set(/* scale*/ mController.getScale(), Float.NaN, Float.NaN); 271 272 mValueAnimator.reverse(); 273 setState(STATE_DISABLING); 274 } 275 updateState()276 private void updateState() { 277 if (Float.isNaN(mController.getScale())) { 278 setState(STATE_DISABLED); 279 } else { 280 setState(STATE_ENABLED); 281 } 282 } 283 setState(@agnificationState int state)284 private void setState(@MagnificationState int state) { 285 if (DEBUG) { 286 Log.d(TAG, "setState from " + mState + " to " + state); 287 } 288 mState = state; 289 } 290 291 @VisibleForTesting getState()292 @MagnificationState int getState() { 293 return mState; 294 } 295 296 @Override onAnimationStart(Animator animation)297 public void onAnimationStart(Animator animation) { 298 mEndAnimationCanceled = false; 299 } 300 301 @Override onAnimationEnd(Animator animation, boolean isReverse)302 public void onAnimationEnd(Animator animation, boolean isReverse) { 303 if (mEndAnimationCanceled || mController == null) { 304 return; 305 } 306 307 mOnAnimationEndRunnable.run(); 308 309 if (mState == STATE_DISABLING) { 310 mController.deleteWindowMagnification(); 311 } 312 updateState(); 313 sendAnimationCallback(true); 314 // We reset the duration to config_longAnimTime 315 mValueAnimator.setDuration(mContext.getResources() 316 .getInteger(com.android.internal.R.integer.config_longAnimTime)); 317 } 318 319 @Override onAnimationEnd(Animator animation)320 public void onAnimationEnd(Animator animation) { 321 } 322 323 @Override onAnimationCancel(Animator animation)324 public void onAnimationCancel(Animator animation) { 325 mEndAnimationCanceled = true; 326 } 327 328 @Override onAnimationRepeat(Animator animation)329 public void onAnimationRepeat(Animator animation) { 330 } 331 setOnAnimationEndRunnable(Runnable runnable)332 void setOnAnimationEndRunnable(Runnable runnable) { 333 mOnAnimationEndRunnable = runnable; 334 } 335 sendAnimationCallback(boolean success)336 private void sendAnimationCallback(boolean success) { 337 if (mAnimationCallback != null) { 338 try { 339 mAnimationCallback.onResult(success); 340 if (DEBUG) { 341 Log.d(TAG, "sendAnimationCallback success = " + success); 342 } 343 } catch (RemoteException e) { 344 Log.w(TAG, "sendAnimationCallback failed : " + e); 345 } 346 mAnimationCallback = null; 347 } 348 } 349 350 @Override onAnimationUpdate(ValueAnimator animation)351 public void onAnimationUpdate(ValueAnimator animation) { 352 if (mController == null) { 353 return; 354 } 355 final float fract = animation.getAnimatedFraction(); 356 final float sentScale = mStartSpec.mScale + (mEndSpec.mScale - mStartSpec.mScale) * fract; 357 final float centerX = 358 mStartSpec.mCenterX + (mEndSpec.mCenterX - mStartSpec.mCenterX) * fract; 359 final float centerY = 360 mStartSpec.mCenterY + (mEndSpec.mCenterY - mStartSpec.mCenterY) * fract; 361 mController.updateWindowMagnificationInternal(sentScale, centerX, centerY, 362 mMagnificationFrameOffsetRatioX, mMagnificationFrameOffsetRatioY); 363 } 364 newValueAnimator(Resources resource)365 private static ValueAnimator newValueAnimator(Resources resource) { 366 final ValueAnimator valueAnimator = new ValueAnimator(); 367 valueAnimator.setDuration( 368 resource.getInteger(com.android.internal.R.integer.config_longAnimTime)); 369 valueAnimator.setInterpolator(new AccelerateInterpolator(2.5f)); 370 valueAnimator.setFloatValues(0.0f, 1.0f); 371 return valueAnimator; 372 } 373 374 private static class AnimationSpec { 375 private float mScale = Float.NaN; 376 private float mCenterX = Float.NaN; 377 private float mCenterY = Float.NaN; 378 379 @Override equals(Object other)380 public boolean equals(Object other) { 381 if (this == other) { 382 return true; 383 } 384 385 if (other == null || getClass() != other.getClass()) { 386 return false; 387 } 388 389 final AnimationSpec s = (AnimationSpec) other; 390 return mScale == s.mScale && mCenterX == s.mCenterX && mCenterY == s.mCenterY; 391 } 392 393 @Override hashCode()394 public int hashCode() { 395 int result = (mScale != +0.0f ? Float.floatToIntBits(mScale) : 0); 396 result = 31 * result + (mCenterX != +0.0f ? Float.floatToIntBits(mCenterX) : 0); 397 result = 31 * result + (mCenterY != +0.0f ? Float.floatToIntBits(mCenterY) : 0); 398 return result; 399 } 400 set(float scale, float centerX, float centerY)401 void set(float scale, float centerX, float centerY) { 402 mScale = scale; 403 mCenterX = centerX; 404 mCenterY = centerY; 405 } 406 407 @Override toString()408 public String toString() { 409 return "AnimationSpec{" 410 + "mScale=" + mScale 411 + ", mCenterX=" + mCenterX 412 + ", mCenterY=" + mCenterY 413 + '}'; 414 } 415 } 416 } 417