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.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 WindowMagnificationAnimationController(@iContext Context context)76 WindowMagnificationAnimationController(@UiContext Context context) { 77 this(context, newValueAnimator(context.getResources())); 78 } 79 80 @VisibleForTesting WindowMagnificationAnimationController(Context context, ValueAnimator valueAnimator)81 WindowMagnificationAnimationController(Context context, ValueAnimator valueAnimator) { 82 mContext = context; 83 mValueAnimator = valueAnimator; 84 mValueAnimator.addUpdateListener(this); 85 mValueAnimator.addListener(this); 86 } 87 setWindowMagnificationController(@onNull WindowMagnificationController controller)88 void setWindowMagnificationController(@NonNull WindowMagnificationController controller) { 89 mController = controller; 90 } 91 92 /** 93 * Wraps {@link WindowMagnificationController#enableWindowMagnification(float, float, float, 94 * float, float, IRemoteMagnificationAnimationCallback)} 95 * with transition animation. If the window magnification is not enabled, the scale will start 96 * from 1.0 and the center won't be changed during the animation. If {@link #mState} is 97 * {@code STATE_DISABLING}, the animation runs in reverse. 98 * 99 * @param scale The target scale, or {@link Float#NaN} to leave unchanged. 100 * @param centerX The screen-relative X coordinate around which to center, 101 * or {@link Float#NaN} to leave unchanged. 102 * @param centerY The screen-relative Y coordinate around which to center, 103 * or {@link Float#NaN} to leave unchanged. 104 * @param animationCallback Called when the transition is complete, the given arguments 105 * are as same as current values, or the transition is interrupted 106 * due to the new transition request. 107 * 108 * @see #onAnimationUpdate(ValueAnimator) 109 */ enableWindowMagnification(float scale, float centerX, float centerY, @Nullable IRemoteMagnificationAnimationCallback animationCallback)110 void enableWindowMagnification(float scale, float centerX, float centerY, 111 @Nullable IRemoteMagnificationAnimationCallback animationCallback) { 112 enableWindowMagnification(scale, centerX, centerY, 0f, 0f, animationCallback); 113 } 114 115 /** 116 * Wraps {@link WindowMagnificationController#enableWindowMagnification(float, float, float, 117 * float, float, IRemoteMagnificationAnimationCallback)} 118 * with transition animation. If the window magnification is not enabled, the scale will start 119 * from 1.0 and the center won't be changed during the animation. If {@link #mState} is 120 * {@code STATE_DISABLING}, the animation runs in reverse. 121 * 122 * @param scale The target scale, or {@link Float#NaN} to leave unchanged. 123 * @param centerX The screen-relative X coordinate around which to center for magnification, 124 * or {@link Float#NaN} to leave unchanged. 125 * @param centerY The screen-relative Y coordinate around which to center for magnification, 126 * or {@link Float#NaN} to leave unchanged. 127 * @param magnificationFrameOffsetRatioX Indicate the X coordinate offset between 128 * frame position X and centerX 129 * @param magnificationFrameOffsetRatioY Indicate the Y coordinate offset between 130 * frame position Y and centerY 131 * @param animationCallback Called when the transition is complete, the given arguments 132 * are as same as current values, or the transition is interrupted 133 * due to the new transition request. 134 * 135 * @see #onAnimationUpdate(ValueAnimator) 136 */ enableWindowMagnification(float scale, float centerX, float centerY, float magnificationFrameOffsetRatioX, float magnificationFrameOffsetRatioY, @Nullable IRemoteMagnificationAnimationCallback animationCallback)137 void enableWindowMagnification(float scale, float centerX, float centerY, 138 float magnificationFrameOffsetRatioX, float magnificationFrameOffsetRatioY, 139 @Nullable IRemoteMagnificationAnimationCallback animationCallback) { 140 if (mController == null) { 141 return; 142 } 143 sendAnimationCallback(false); 144 mMagnificationFrameOffsetRatioX = magnificationFrameOffsetRatioX; 145 mMagnificationFrameOffsetRatioY = magnificationFrameOffsetRatioY; 146 147 // Enable window magnification without animation immediately. 148 if (animationCallback == null) { 149 if (mState == STATE_ENABLING || mState == STATE_DISABLING) { 150 mValueAnimator.cancel(); 151 } 152 mController.enableWindowMagnificationInternal(scale, centerX, centerY, 153 mMagnificationFrameOffsetRatioX, mMagnificationFrameOffsetRatioY); 154 updateState(); 155 return; 156 } 157 mAnimationCallback = animationCallback; 158 setupEnableAnimationSpecs(scale, centerX, centerY); 159 160 if (mEndSpec.equals(mStartSpec)) { 161 if (mState == STATE_DISABLED) { 162 mController.enableWindowMagnificationInternal(scale, centerX, centerY, 163 mMagnificationFrameOffsetRatioX, mMagnificationFrameOffsetRatioY); 164 } else if (mState == STATE_ENABLING || mState == STATE_DISABLING) { 165 mValueAnimator.cancel(); 166 } 167 sendAnimationCallback(true); 168 updateState(); 169 } else { 170 if (mState == STATE_DISABLING) { 171 mValueAnimator.reverse(); 172 } else { 173 if (mState == STATE_ENABLING) { 174 mValueAnimator.cancel(); 175 } 176 mValueAnimator.start(); 177 } 178 setState(STATE_ENABLING); 179 } 180 } 181 moveWindowMagnifierToPosition(float centerX, float centerY, IRemoteMagnificationAnimationCallback callback)182 void moveWindowMagnifierToPosition(float centerX, float centerY, 183 IRemoteMagnificationAnimationCallback callback) { 184 if (mState == STATE_ENABLED) { 185 // We set the animation duration to shortAnimTime which would be reset at the end. 186 mValueAnimator.setDuration(mContext.getResources() 187 .getInteger(com.android.internal.R.integer.config_shortAnimTime)); 188 enableWindowMagnification(Float.NaN, centerX, centerY, 189 /* magnificationFrameOffsetRatioX */ Float.NaN, 190 /* magnificationFrameOffsetRatioY */ Float.NaN, callback); 191 } else if (mState == STATE_ENABLING) { 192 sendAnimationCallback(false); 193 mAnimationCallback = callback; 194 mValueAnimator.setDuration(mContext.getResources() 195 .getInteger(com.android.internal.R.integer.config_shortAnimTime)); 196 setupEnableAnimationSpecs(Float.NaN, centerX, centerY); 197 } 198 } 199 setupEnableAnimationSpecs(float scale, float centerX, float centerY)200 private void setupEnableAnimationSpecs(float scale, float centerX, float centerY) { 201 if (mController == null) { 202 return; 203 } 204 final float currentScale = mController.getScale(); 205 final float currentCenterX = mController.getCenterX(); 206 final float currentCenterY = mController.getCenterY(); 207 208 if (mState == STATE_DISABLED) { 209 // We don't need to offset the center during the animation. 210 mStartSpec.set(/* scale*/ 1.0f, centerX, centerY); 211 mEndSpec.set(Float.isNaN(scale) ? mContext.getResources().getInteger( 212 R.integer.magnification_default_scale) : scale, centerX, centerY); 213 } else { 214 mStartSpec.set(currentScale, currentCenterX, currentCenterY); 215 216 final float endScale = (mState == STATE_ENABLING ? mEndSpec.mScale : currentScale); 217 final float endCenterX = 218 (mState == STATE_ENABLING ? mEndSpec.mCenterX : currentCenterX); 219 final float endCenterY = 220 (mState == STATE_ENABLING ? mEndSpec.mCenterY : currentCenterY); 221 222 mEndSpec.set(Float.isNaN(scale) ? endScale : scale, 223 Float.isNaN(centerX) ? endCenterX : centerX, 224 Float.isNaN(centerY) ? endCenterY : centerY); 225 } 226 if (DEBUG) { 227 Log.d(TAG, "SetupEnableAnimationSpecs : mStartSpec = " + mStartSpec + ", endSpec = " 228 + mEndSpec); 229 } 230 } 231 232 /** Returns {@code true} if the animator is running. */ isAnimating()233 boolean isAnimating() { 234 return mValueAnimator.isRunning(); 235 } 236 237 /** 238 * Wraps {@link WindowMagnificationController#deleteWindowMagnification()}} with transition 239 * animation. If the window magnification is enabling, it runs the animation in reverse. 240 * 241 * @param animationCallback Called when the transition is complete, the given arguments 242 * are as same as current values, or the transition is interrupted 243 * due to the new transition request. 244 */ deleteWindowMagnification( @ullable IRemoteMagnificationAnimationCallback animationCallback)245 void deleteWindowMagnification( 246 @Nullable IRemoteMagnificationAnimationCallback animationCallback) { 247 if (mController == null) { 248 return; 249 } 250 sendAnimationCallback(false); 251 // Delete window magnification without animation. 252 if (animationCallback == null) { 253 if (mState == STATE_ENABLING || mState == STATE_DISABLING) { 254 mValueAnimator.cancel(); 255 } 256 mController.deleteWindowMagnification(); 257 updateState(); 258 return; 259 } 260 261 mAnimationCallback = animationCallback; 262 if (mState == STATE_DISABLED || mState == STATE_DISABLING) { 263 if (mState == STATE_DISABLED) { 264 sendAnimationCallback(true); 265 } 266 return; 267 } 268 mStartSpec.set(/* scale*/ 1.0f, Float.NaN, Float.NaN); 269 mEndSpec.set(/* scale*/ mController.getScale(), Float.NaN, Float.NaN); 270 271 mValueAnimator.reverse(); 272 setState(STATE_DISABLING); 273 } 274 updateState()275 private void updateState() { 276 if (Float.isNaN(mController.getScale())) { 277 setState(STATE_DISABLED); 278 } else { 279 setState(STATE_ENABLED); 280 } 281 } 282 setState(@agnificationState int state)283 private void setState(@MagnificationState int state) { 284 if (DEBUG) { 285 Log.d(TAG, "setState from " + mState + " to " + state); 286 } 287 mState = state; 288 } 289 290 @VisibleForTesting getState()291 @MagnificationState int getState() { 292 return mState; 293 } 294 295 @Override onAnimationStart(Animator animation)296 public void onAnimationStart(Animator animation) { 297 mEndAnimationCanceled = false; 298 } 299 300 @Override onAnimationEnd(Animator animation, boolean isReverse)301 public void onAnimationEnd(Animator animation, boolean isReverse) { 302 if (mEndAnimationCanceled || mController == null) { 303 return; 304 } 305 if (mState == STATE_DISABLING) { 306 mController.deleteWindowMagnification(); 307 } 308 updateState(); 309 sendAnimationCallback(true); 310 // We reset the duration to config_longAnimTime 311 mValueAnimator.setDuration(mContext.getResources() 312 .getInteger(com.android.internal.R.integer.config_longAnimTime)); 313 } 314 315 @Override onAnimationEnd(Animator animation)316 public void onAnimationEnd(Animator animation) { 317 } 318 319 @Override onAnimationCancel(Animator animation)320 public void onAnimationCancel(Animator animation) { 321 mEndAnimationCanceled = true; 322 } 323 324 @Override onAnimationRepeat(Animator animation)325 public void onAnimationRepeat(Animator animation) { 326 } 327 sendAnimationCallback(boolean success)328 private void sendAnimationCallback(boolean success) { 329 if (mAnimationCallback != null) { 330 try { 331 mAnimationCallback.onResult(success); 332 if (DEBUG) { 333 Log.d(TAG, "sendAnimationCallback success = " + success); 334 } 335 } catch (RemoteException e) { 336 Log.w(TAG, "sendAnimationCallback failed : " + e); 337 } 338 mAnimationCallback = null; 339 } 340 } 341 342 @Override onAnimationUpdate(ValueAnimator animation)343 public void onAnimationUpdate(ValueAnimator animation) { 344 if (mController == null) { 345 return; 346 } 347 final float fract = animation.getAnimatedFraction(); 348 final float sentScale = mStartSpec.mScale + (mEndSpec.mScale - mStartSpec.mScale) * fract; 349 final float centerX = 350 mStartSpec.mCenterX + (mEndSpec.mCenterX - mStartSpec.mCenterX) * fract; 351 final float centerY = 352 mStartSpec.mCenterY + (mEndSpec.mCenterY - mStartSpec.mCenterY) * fract; 353 mController.enableWindowMagnificationInternal(sentScale, centerX, centerY, 354 mMagnificationFrameOffsetRatioX, mMagnificationFrameOffsetRatioY); 355 } 356 newValueAnimator(Resources resource)357 private static ValueAnimator newValueAnimator(Resources resource) { 358 final ValueAnimator valueAnimator = new ValueAnimator(); 359 valueAnimator.setDuration( 360 resource.getInteger(com.android.internal.R.integer.config_longAnimTime)); 361 valueAnimator.setInterpolator(new AccelerateInterpolator(2.5f)); 362 valueAnimator.setFloatValues(0.0f, 1.0f); 363 return valueAnimator; 364 } 365 366 private static class AnimationSpec { 367 private float mScale = Float.NaN; 368 private float mCenterX = Float.NaN; 369 private float mCenterY = Float.NaN; 370 371 @Override equals(Object other)372 public boolean equals(Object other) { 373 if (this == other) { 374 return true; 375 } 376 377 if (other == null || getClass() != other.getClass()) { 378 return false; 379 } 380 381 final AnimationSpec s = (AnimationSpec) other; 382 return mScale == s.mScale && mCenterX == s.mCenterX && mCenterY == s.mCenterY; 383 } 384 385 @Override hashCode()386 public int hashCode() { 387 int result = (mScale != +0.0f ? Float.floatToIntBits(mScale) : 0); 388 result = 31 * result + (mCenterX != +0.0f ? Float.floatToIntBits(mCenterX) : 0); 389 result = 31 * result + (mCenterY != +0.0f ? Float.floatToIntBits(mCenterY) : 0); 390 return result; 391 } 392 set(float scale, float centerX, float centerY)393 void set(float scale, float centerX, float centerY) { 394 mScale = scale; 395 mCenterX = centerX; 396 mCenterY = centerY; 397 } 398 399 @Override toString()400 public String toString() { 401 return "AnimationSpec{" 402 + "mScale=" + mScale 403 + ", mCenterX=" + mCenterX 404 + ", mCenterY=" + mCenterY 405 + '}'; 406 } 407 } 408 } 409