blob: 03fa93d8a3bc42402efc704ea95713090b2628b3 [file] [log] [blame]
/*
* Copyright (C) 2023 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.server.accessibility.magnification;
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.annotation.AnyThread;
import android.annotation.MainThread;
import android.content.Context;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.Handler;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.android.internal.R;
/**
* This class is used to show of magnification thumbnail
* from FullScreenMagnification. It is responsible for
* show of magnification and fade in/out animation, and
* it just only uses in FullScreenMagnification
*/
public class MagnificationThumbnail {
private static final boolean DEBUG = false;
private static final String LOG_TAG = "MagnificationThumbnail";
private static final int FADE_IN_ANIMATION_DURATION_MS = 200;
private static final int FADE_OUT_ANIMATION_DURATION_MS = 1000;
private static final int LINGER_DURATION_MS = 500;
private Rect mWindowBounds;
private final Context mContext;
private final WindowManager mWindowManager;
private final Handler mHandler;
@VisibleForTesting
public final FrameLayout mThumbnailLayout;
private final View mThumbnailView;
private int mThumbnailWidth;
private int mThumbnailHeight;
private final WindowManager.LayoutParams mBackgroundParams;
private boolean mVisible = false;
private static final float ASPECT_RATIO = 14f;
private static final float BG_ASPECT_RATIO = ASPECT_RATIO / 2f;
private ObjectAnimator mThumbnailAnimator;
private boolean mIsFadingIn;
/**
* FullScreenMagnificationThumbnail Constructor
*/
public MagnificationThumbnail(Context context, WindowManager windowManager, Handler handler) {
mContext = context;
mWindowManager = windowManager;
mHandler = handler;
mWindowBounds = mWindowManager.getCurrentWindowMetrics().getBounds();
mThumbnailLayout = (FrameLayout) LayoutInflater.from(mContext)
.inflate(R.layout.thumbnail_background_view, /* root: */ null);
mThumbnailView =
mThumbnailLayout.findViewById(R.id.accessibility_magnification_thumbnail_view);
mBackgroundParams = createLayoutParams();
mThumbnailWidth = 0;
mThumbnailHeight = 0;
}
/**
* Sets the magnificationBounds for Thumbnail and resets the position on the screen.
*
* @param currentBounds the current magnification bounds
*/
@AnyThread
public void setThumbnailBounds(Rect currentBounds, float scale, float centerX, float centerY) {
if (DEBUG) {
Log.d(LOG_TAG, "setThumbnailBounds " + currentBounds);
}
mHandler.post(() -> {
mWindowBounds = currentBounds;
setBackgroundBounds();
if (mVisible) {
updateThumbnailMainThread(scale, centerX, centerY);
}
});
}
private void setBackgroundBounds() {
Point magnificationBoundary = getMagnificationThumbnailPadding(mContext);
mThumbnailWidth = (int) (mWindowBounds.width() / BG_ASPECT_RATIO);
mThumbnailHeight = (int) (mWindowBounds.height() / BG_ASPECT_RATIO);
int initX = magnificationBoundary.x;
int initY = magnificationBoundary.y;
mBackgroundParams.width = mThumbnailWidth;
mBackgroundParams.height = mThumbnailHeight;
mBackgroundParams.x = initX;
mBackgroundParams.y = initY;
}
@MainThread
private void showThumbnail() {
if (DEBUG) {
Log.d(LOG_TAG, "showThumbnail " + mVisible);
}
animateThumbnail(true);
}
/**
* Hides thumbnail and removes the view from the window when finished animating.
*/
@AnyThread
public void hideThumbnail() {
mHandler.post(this::hideThumbnailMainThread);
}
@MainThread
private void hideThumbnailMainThread() {
if (DEBUG) {
Log.d(LOG_TAG, "hideThumbnail " + mVisible);
}
if (mVisible) {
animateThumbnail(false);
}
}
/**
* Animates the thumbnail in or out and resets the timeout to auto-hiding.
*
* @param fadeIn true: fade in, false fade out
*/
@MainThread
private void animateThumbnail(boolean fadeIn) {
if (DEBUG) {
Log.d(
LOG_TAG,
"setThumbnailAnimation "
+ " fadeIn: " + fadeIn
+ " mVisible: " + mVisible
+ " isFadingIn: " + mIsFadingIn
+ " isRunning: " + mThumbnailAnimator
);
}
// Reset countdown to hide automatically
mHandler.removeCallbacks(this::hideThumbnailMainThread);
if (fadeIn) {
mHandler.postDelayed(this::hideThumbnailMainThread, LINGER_DURATION_MS);
}
if (fadeIn == mIsFadingIn) {
return;
}
mIsFadingIn = fadeIn;
if (fadeIn && !mVisible) {
mWindowManager.addView(mThumbnailLayout, mBackgroundParams);
mVisible = true;
}
if (mThumbnailAnimator != null) {
mThumbnailAnimator.cancel();
}
mThumbnailAnimator = ObjectAnimator.ofFloat(
mThumbnailLayout,
"alpha",
fadeIn ? 1f : 0f
);
mThumbnailAnimator.setDuration(
fadeIn ? FADE_IN_ANIMATION_DURATION_MS : FADE_OUT_ANIMATION_DURATION_MS
);
mThumbnailAnimator.addListener(new Animator.AnimatorListener() {
private boolean mIsCancelled;
@Override
public void onAnimationStart(@NonNull Animator animation) {
}
@Override
public void onAnimationEnd(@NonNull Animator animation) {
if (DEBUG) {
Log.d(
LOG_TAG,
"onAnimationEnd "
+ " fadeIn: " + fadeIn
+ " mVisible: " + mVisible
+ " mIsCancelled: " + mIsCancelled
+ " animation: " + animation);
}
if (mIsCancelled) {
return;
}
if (!fadeIn && mVisible) {
mWindowManager.removeView(mThumbnailLayout);
mVisible = false;
}
}
@Override
public void onAnimationCancel(@NonNull Animator animation) {
if (DEBUG) {
Log.d(LOG_TAG, "onAnimationCancel "
+ " fadeIn: " + fadeIn
+ " mVisible: " + mVisible
+ " animation: " + animation);
}
mIsCancelled = true;
}
@Override
public void onAnimationRepeat(@NonNull Animator animation) {
}
});
mThumbnailAnimator.start();
}
/**
* Scale up/down the current magnification thumbnail spec.
*
* <p>Will show/hide the thumbnail with animations when appropriate.
*
* @param scale the magnification scale
* @param centerX the unscaled, screen-relative X coordinate of the center
* of the viewport, or {@link Float#NaN} to leave unchanged
* @param centerY the unscaled, screen-relative Y coordinate of the center
* of the viewport, or {@link Float#NaN} to leave unchanged
*/
@AnyThread
public void updateThumbnail(float scale, float centerX, float centerY) {
mHandler.post(() -> updateThumbnailMainThread(scale, centerX, centerY));
}
@MainThread
private void updateThumbnailMainThread(float scale, float centerX, float centerY) {
// Restart the fadeout countdown (or show if it's hidden)
showThumbnail();
var scaleDown = Float.isNaN(scale) ? mThumbnailView.getScaleX() : 1f / scale;
if (!Float.isNaN(scale)) {
mThumbnailView.setScaleX(scaleDown);
mThumbnailView.setScaleY(scaleDown);
}
float thumbnailWidth;
float thumbnailHeight;
if (mThumbnailView.getWidth() == 0 || mThumbnailView.getHeight() == 0) {
// if the thumbnail view size is not updated correctly, we just use the cached values.
thumbnailWidth = mThumbnailWidth;
thumbnailHeight = mThumbnailHeight;
} else {
thumbnailWidth = mThumbnailView.getWidth();
thumbnailHeight = mThumbnailView.getHeight();
}
if (!Float.isNaN(centerX)) {
var padding = mThumbnailView.getPaddingTop();
var ratio = 1f / BG_ASPECT_RATIO;
var centerXScaled = centerX * ratio - (thumbnailWidth / 2f + padding);
var centerYScaled = centerY * ratio - (thumbnailHeight / 2f + padding);
if (DEBUG) {
Log.d(
LOG_TAG,
"updateThumbnail centerXScaled : " + centerXScaled
+ " centerYScaled : " + centerYScaled
+ " getTranslationX : " + mThumbnailView.getTranslationX()
+ " ratio : " + ratio
);
}
mThumbnailView.setTranslationX(centerXScaled);
mThumbnailView.setTranslationY(centerYScaled);
}
}
private WindowManager.LayoutParams createLayoutParams() {
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_MAGNIFICATION_OVERLAY,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
PixelFormat.TRANSPARENT);
params.inputFeatures = WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL;
params.gravity = Gravity.BOTTOM | Gravity.LEFT;
params.setFitInsetsTypes(WindowInsets.Type.ime() | WindowInsets.Type.navigationBars());
return params;
}
private Point getMagnificationThumbnailPadding(Context context) {
Point thumbnailPaddings = new Point(0, 0);
final int defaultPadding = mContext.getResources()
.getDimensionPixelSize(R.dimen.accessibility_magnification_thumbnail_padding);
thumbnailPaddings.x = defaultPadding;
thumbnailPaddings.y = defaultPadding;
return thumbnailPaddings;
}
}