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