1 /* 2 * Copyright (C) 2023 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 package com.android.server.accessibility.magnification; 17 18 import android.animation.Animator; 19 import android.animation.ObjectAnimator; 20 import android.annotation.AnyThread; 21 import android.annotation.MainThread; 22 import android.content.Context; 23 import android.graphics.PixelFormat; 24 import android.graphics.Point; 25 import android.graphics.Rect; 26 import android.os.Handler; 27 import android.util.Log; 28 import android.view.Gravity; 29 import android.view.LayoutInflater; 30 import android.view.View; 31 import android.view.WindowInsets; 32 import android.view.WindowManager; 33 import android.widget.FrameLayout; 34 35 import androidx.annotation.NonNull; 36 import androidx.annotation.VisibleForTesting; 37 38 import com.android.internal.R; 39 /** 40 * This class is used to show of magnification thumbnail 41 * from FullScreenMagnification. It is responsible for 42 * show of magnification and fade in/out animation, and 43 * it just only uses in FullScreenMagnification 44 */ 45 public class MagnificationThumbnail { 46 private static final boolean DEBUG = false; 47 private static final String LOG_TAG = "MagnificationThumbnail"; 48 49 private static final int FADE_IN_ANIMATION_DURATION_MS = 200; 50 private static final int FADE_OUT_ANIMATION_DURATION_MS = 1000; 51 private static final int LINGER_DURATION_MS = 500; 52 53 private Rect mWindowBounds; 54 private final Context mContext; 55 private final WindowManager mWindowManager; 56 private final Handler mHandler; 57 58 @VisibleForTesting 59 public final FrameLayout mThumbnailLayout; 60 61 private final View mThumbnailView; 62 private int mThumbnailWidth; 63 private int mThumbnailHeight; 64 65 private final WindowManager.LayoutParams mBackgroundParams; 66 private boolean mVisible = false; 67 68 private static final float ASPECT_RATIO = 14f; 69 private static final float BG_ASPECT_RATIO = ASPECT_RATIO / 2f; 70 71 private ObjectAnimator mThumbnailAnimator; 72 private boolean mIsFadingIn; 73 74 /** 75 * FullScreenMagnificationThumbnail Constructor 76 */ MagnificationThumbnail(Context context, WindowManager windowManager, Handler handler)77 public MagnificationThumbnail(Context context, WindowManager windowManager, Handler handler) { 78 mContext = context; 79 mWindowManager = windowManager; 80 mHandler = handler; 81 mWindowBounds = mWindowManager.getCurrentWindowMetrics().getBounds(); 82 mThumbnailLayout = (FrameLayout) LayoutInflater.from(mContext) 83 .inflate(R.layout.thumbnail_background_view, /* root: */ null); 84 mThumbnailView = 85 mThumbnailLayout.findViewById(R.id.accessibility_magnification_thumbnail_view); 86 mBackgroundParams = createLayoutParams(); 87 mThumbnailWidth = 0; 88 mThumbnailHeight = 0; 89 } 90 91 /** 92 * Sets the magnificationBounds for Thumbnail and resets the position on the screen. 93 * 94 * @param currentBounds the current magnification bounds 95 */ 96 @AnyThread setThumbnailBounds(Rect currentBounds, float scale, float centerX, float centerY)97 public void setThumbnailBounds(Rect currentBounds, float scale, float centerX, float centerY) { 98 if (DEBUG) { 99 Log.d(LOG_TAG, "setThumbnailBounds " + currentBounds); 100 } 101 mHandler.post(() -> { 102 refreshBackgroundBounds(currentBounds); 103 if (mVisible) { 104 updateThumbnailMainThread(scale, centerX, centerY); 105 } 106 }); 107 } 108 109 @MainThread refreshBackgroundBounds(Rect currentBounds)110 private void refreshBackgroundBounds(Rect currentBounds) { 111 mWindowBounds = currentBounds; 112 113 Point magnificationBoundary = getMagnificationThumbnailPadding(mContext); 114 mThumbnailWidth = (int) (mWindowBounds.width() / BG_ASPECT_RATIO); 115 mThumbnailHeight = (int) (mWindowBounds.height() / BG_ASPECT_RATIO); 116 int initX = magnificationBoundary.x; 117 int initY = magnificationBoundary.y; 118 mBackgroundParams.width = mThumbnailWidth; 119 mBackgroundParams.height = mThumbnailHeight; 120 mBackgroundParams.x = initX; 121 mBackgroundParams.y = initY; 122 123 if (mVisible) { 124 mWindowManager.updateViewLayout(mThumbnailLayout, mBackgroundParams); 125 } 126 } 127 128 @MainThread showThumbnail()129 private void showThumbnail() { 130 if (DEBUG) { 131 Log.d(LOG_TAG, "showThumbnail " + mVisible); 132 } 133 animateThumbnail(true); 134 } 135 136 /** 137 * Hides thumbnail and removes the view from the window when finished animating. 138 */ 139 @AnyThread hideThumbnail()140 public void hideThumbnail() { 141 mHandler.post(this::hideThumbnailMainThread); 142 } 143 144 @MainThread hideThumbnailMainThread()145 private void hideThumbnailMainThread() { 146 if (DEBUG) { 147 Log.d(LOG_TAG, "hideThumbnail " + mVisible); 148 } 149 if (mVisible) { 150 animateThumbnail(false); 151 } 152 } 153 154 /** 155 * Animates the thumbnail in or out and resets the timeout to auto-hiding. 156 * 157 * @param fadeIn true: fade in, false fade out 158 */ 159 @MainThread animateThumbnail(boolean fadeIn)160 private void animateThumbnail(boolean fadeIn) { 161 if (DEBUG) { 162 Log.d( 163 LOG_TAG, 164 "setThumbnailAnimation " 165 + " fadeIn: " + fadeIn 166 + " mVisible: " + mVisible 167 + " isFadingIn: " + mIsFadingIn 168 + " isRunning: " + mThumbnailAnimator 169 ); 170 } 171 172 // Reset countdown to hide automatically 173 mHandler.removeCallbacks(this::hideThumbnailMainThread); 174 if (fadeIn) { 175 mHandler.postDelayed(this::hideThumbnailMainThread, LINGER_DURATION_MS); 176 } 177 178 if (fadeIn == mIsFadingIn) { 179 return; 180 } 181 mIsFadingIn = fadeIn; 182 183 if (fadeIn && !mVisible) { 184 mWindowManager.addView(mThumbnailLayout, mBackgroundParams); 185 mVisible = true; 186 } 187 188 if (mThumbnailAnimator != null) { 189 mThumbnailAnimator.cancel(); 190 } 191 mThumbnailAnimator = ObjectAnimator.ofFloat( 192 mThumbnailLayout, 193 "alpha", 194 fadeIn ? 1f : 0f 195 ); 196 mThumbnailAnimator.setDuration( 197 fadeIn ? FADE_IN_ANIMATION_DURATION_MS : FADE_OUT_ANIMATION_DURATION_MS 198 ); 199 mThumbnailAnimator.addListener(new Animator.AnimatorListener() { 200 private boolean mIsCancelled; 201 202 @Override 203 public void onAnimationStart(@NonNull Animator animation) { 204 205 } 206 207 @Override 208 public void onAnimationEnd(@NonNull Animator animation) { 209 if (DEBUG) { 210 Log.d( 211 LOG_TAG, 212 "onAnimationEnd " 213 + " fadeIn: " + fadeIn 214 + " mVisible: " + mVisible 215 + " mIsCancelled: " + mIsCancelled 216 + " animation: " + animation); 217 } 218 if (mIsCancelled) { 219 return; 220 } 221 if (!fadeIn && mVisible) { 222 mWindowManager.removeView(mThumbnailLayout); 223 mVisible = false; 224 } 225 } 226 227 @Override 228 public void onAnimationCancel(@NonNull Animator animation) { 229 if (DEBUG) { 230 Log.d(LOG_TAG, "onAnimationCancel " 231 + " fadeIn: " + fadeIn 232 + " mVisible: " + mVisible 233 + " animation: " + animation); 234 } 235 mIsCancelled = true; 236 } 237 238 @Override 239 public void onAnimationRepeat(@NonNull Animator animation) { 240 241 } 242 }); 243 244 mThumbnailAnimator.start(); 245 } 246 247 /** 248 * Scale up/down the current magnification thumbnail spec. 249 * 250 * <p>Will show/hide the thumbnail with animations when appropriate. 251 * 252 * @param scale the magnification scale 253 * @param centerX the unscaled, screen-relative X coordinate of the center 254 * of the viewport, or {@link Float#NaN} to leave unchanged 255 * @param centerY the unscaled, screen-relative Y coordinate of the center 256 * of the viewport, or {@link Float#NaN} to leave unchanged 257 */ 258 @AnyThread updateThumbnail(float scale, float centerX, float centerY)259 public void updateThumbnail(float scale, float centerX, float centerY) { 260 mHandler.post(() -> updateThumbnailMainThread(scale, centerX, centerY)); 261 } 262 263 @MainThread updateThumbnailMainThread(float scale, float centerX, float centerY)264 private void updateThumbnailMainThread(float scale, float centerX, float centerY) { 265 // Restart the fadeout countdown (or show if it's hidden) 266 showThumbnail(); 267 268 var scaleDown = Float.isNaN(scale) ? mThumbnailView.getScaleX() : 1f / scale; 269 if (!Float.isNaN(scale)) { 270 mThumbnailView.setScaleX(scaleDown); 271 mThumbnailView.setScaleY(scaleDown); 272 } 273 274 if (!Float.isNaN(centerX) 275 && !Float.isNaN(centerY) 276 && mThumbnailWidth > 0 277 && mThumbnailHeight > 0 278 ) { 279 var padding = mThumbnailView.getPaddingTop(); 280 var ratio = 1f / BG_ASPECT_RATIO; 281 var centerXScaled = centerX * ratio - (mThumbnailWidth / 2f + padding); 282 var centerYScaled = centerY * ratio - (mThumbnailHeight / 2f + padding); 283 284 if (DEBUG) { 285 Log.d( 286 LOG_TAG, 287 "updateThumbnail centerXScaled : " + centerXScaled 288 + " centerYScaled : " + centerYScaled 289 + " getTranslationX : " + mThumbnailView.getTranslationX() 290 + " ratio : " + ratio 291 ); 292 } 293 294 mThumbnailView.setTranslationX(centerXScaled); 295 mThumbnailView.setTranslationY(centerYScaled); 296 } 297 } 298 createLayoutParams()299 private WindowManager.LayoutParams createLayoutParams() { 300 WindowManager.LayoutParams params = new WindowManager.LayoutParams( 301 WindowManager.LayoutParams.WRAP_CONTENT, 302 WindowManager.LayoutParams.WRAP_CONTENT, 303 WindowManager.LayoutParams.TYPE_MAGNIFICATION_OVERLAY, 304 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 305 | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, 306 PixelFormat.TRANSPARENT); 307 params.inputFeatures = WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL; 308 params.gravity = Gravity.BOTTOM | Gravity.LEFT; 309 params.setFitInsetsTypes(WindowInsets.Type.ime() | WindowInsets.Type.navigationBars()); 310 return params; 311 } 312 getMagnificationThumbnailPadding(Context context)313 private Point getMagnificationThumbnailPadding(Context context) { 314 Point thumbnailPaddings = new Point(0, 0); 315 final int defaultPadding = mContext.getResources() 316 .getDimensionPixelSize(R.dimen.accessibility_magnification_thumbnail_padding); 317 thumbnailPaddings.x = defaultPadding; 318 thumbnailPaddings.y = defaultPadding; 319 return thumbnailPaddings; 320 } 321 } 322