blob: 3814bd2999e97c8da13f578ca28c51cc2afd98d8 [file] [log] [blame]
/*
* Copyright (C) 2020 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.systemui.screenshot;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static java.util.Objects.requireNonNull;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.app.ActivityManager;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Insets;
import android.graphics.Outline;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.Region;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.graphics.drawable.InsetDrawable;
import android.graphics.drawable.LayerDrawable;
import android.os.RemoteException;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.MathUtils;
import android.view.GestureDetector;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewOutlineProvider;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import android.widget.HorizontalScrollView;
import android.widget.ImageView;
import android.widget.LinearLayout;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.R;
import com.android.systemui.shared.system.QuickStepContract;
import java.util.ArrayList;
import java.util.function.Consumer;
/**
* Handles the visual elements and animations for the screenshot flow.
*/
public class ScreenshotView extends FrameLayout implements
ViewTreeObserver.OnComputeInternalInsetsListener {
private static final String TAG = "ScreenshotView";
private static final long SCREENSHOT_FLASH_IN_DURATION_MS = 133;
private static final long SCREENSHOT_FLASH_OUT_DURATION_MS = 217;
// delay before starting to fade in dismiss button
private static final long SCREENSHOT_TO_CORNER_DISMISS_DELAY_MS = 200;
private static final long SCREENSHOT_TO_CORNER_X_DURATION_MS = 234;
private static final long SCREENSHOT_TO_CORNER_Y_DURATION_MS = 500;
private static final long SCREENSHOT_TO_CORNER_SCALE_DURATION_MS = 234;
private static final long SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS = 400;
private static final long SCREENSHOT_ACTIONS_ALPHA_DURATION_MS = 100;
private static final long SCREENSHOT_DISMISS_Y_DURATION_MS = 350;
private static final long SCREENSHOT_DISMISS_ALPHA_DURATION_MS = 183;
private static final long SCREENSHOT_DISMISS_ALPHA_OFFSET_MS = 50; // delay before starting fade
private static final float SCREENSHOT_ACTIONS_START_SCALE_X = .7f;
private static final float ROUNDED_CORNER_RADIUS = .05f;
private final Interpolator mAccelerateInterpolator = new AccelerateInterpolator();
private final Resources mResources;
private final Interpolator mFastOutSlowIn;
private final DisplayMetrics mDisplayMetrics;
private final float mCornerSizeX;
private final float mDismissDeltaY;
private int mNavMode;
private int mLeftInset;
private int mRightInset;
private boolean mOrientationPortrait;
private boolean mDirectionLTR;
private ScreenshotSelectorView mScreenshotSelectorView;
private ImageView mScreenshotPreview;
private ImageView mScreenshotFlash;
private ImageView mActionsContainerBackground;
private HorizontalScrollView mActionsContainer;
private LinearLayout mActionsView;
private ImageView mBackgroundProtection;
private FrameLayout mDismissButton;
private ScreenshotActionChip mShareChip;
private ScreenshotActionChip mEditChip;
private ScreenshotActionChip mScrollChip;
private UiEventLogger mUiEventLogger;
private Runnable mOnDismissRunnable;
private Animator mDismissAnimation;
private final ArrayList<ScreenshotActionChip> mSmartChips = new ArrayList<>();
private PendingInteraction mPendingInteraction;
private enum PendingInteraction {
PREVIEW,
EDIT,
SHARE
}
public ScreenshotView(Context context) {
this(context, null);
}
public ScreenshotView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ScreenshotView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public ScreenshotView(
Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mResources = mContext.getResources();
mCornerSizeX = mResources.getDimensionPixelSize(R.dimen.global_screenshot_x_scale);
mDismissDeltaY = mResources.getDimensionPixelSize(
R.dimen.screenshot_dismissal_height_delta);
// standard material ease
mFastOutSlowIn =
AnimationUtils.loadInterpolator(mContext, android.R.interpolator.fast_out_slow_in);
mDisplayMetrics = new DisplayMetrics();
mContext.getDisplay().getRealMetrics(mDisplayMetrics);
}
/**
* Called to display the scroll action chip when support is detected.
*
* @param onClick the action to take when the chip is clicked.
*/
public void showScrollChip(Runnable onClick) {
mScrollChip.setVisibility(VISIBLE);
mScrollChip.setOnClickListener((v) ->
onClick.run()
// TODO Logging, store event consumer to a field
//onElementTapped.accept(ScreenshotEvent.SCREENSHOT_SCROLL_TAPPED);
);
}
@Override // ViewTreeObserver.OnComputeInternalInsetsListener
public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
Region touchRegion = new Region();
Rect screenshotRect = new Rect();
mScreenshotPreview.getBoundsOnScreen(screenshotRect);
touchRegion.op(screenshotRect, Region.Op.UNION);
Rect actionsRect = new Rect();
mActionsContainer.getBoundsOnScreen(actionsRect);
touchRegion.op(actionsRect, Region.Op.UNION);
Rect dismissRect = new Rect();
mDismissButton.getBoundsOnScreen(dismissRect);
touchRegion.op(dismissRect, Region.Op.UNION);
if (QuickStepContract.isGesturalMode(mNavMode)) {
// Receive touches in gesture insets such that they don't cause TOUCH_OUTSIDE
Rect inset = new Rect(0, 0, mLeftInset, mDisplayMetrics.heightPixels);
touchRegion.op(inset, Region.Op.UNION);
inset.set(mDisplayMetrics.widthPixels - mRightInset, 0, mDisplayMetrics.widthPixels,
mDisplayMetrics.heightPixels);
touchRegion.op(inset, Region.Op.UNION);
}
inoutInfo.touchableRegion.set(touchRegion);
}
@Override // View
protected void onFinishInflate() {
mScreenshotPreview = requireNonNull(findViewById(R.id.global_screenshot_preview));
mActionsContainerBackground = requireNonNull(findViewById(
R.id.global_screenshot_actions_container_background));
mActionsContainer = requireNonNull(findViewById(R.id.global_screenshot_actions_container));
mActionsView = requireNonNull(findViewById(R.id.global_screenshot_actions));
mBackgroundProtection = requireNonNull(
findViewById(R.id.global_screenshot_actions_background));
mDismissButton = requireNonNull(findViewById(R.id.global_screenshot_dismiss_button));
mScreenshotFlash = requireNonNull(findViewById(R.id.global_screenshot_flash));
mScreenshotSelectorView = requireNonNull(findViewById(R.id.global_screenshot_selector));
mShareChip = requireNonNull(mActionsContainer.findViewById(R.id.screenshot_share_chip));
mEditChip = requireNonNull(mActionsContainer.findViewById(R.id.screenshot_edit_chip));
mScrollChip = requireNonNull(mActionsContainer.findViewById(R.id.screenshot_scroll_chip));
mScreenshotPreview.setClipToOutline(true);
mScreenshotPreview.setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
outline.setRoundRect(new Rect(0, 0, view.getWidth(), view.getHeight()),
ROUNDED_CORNER_RADIUS * view.getWidth());
}
});
setFocusable(true);
mScreenshotSelectorView.setFocusable(true);
mScreenshotSelectorView.setFocusableInTouchMode(true);
mActionsContainer.setScrollX(0);
mNavMode = getResources().getInteger(
com.android.internal.R.integer.config_navBarInteractionMode);
mOrientationPortrait =
getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT;
mDirectionLTR =
getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_LTR;
setOnApplyWindowInsetsListener((v, insets) -> {
if (QuickStepContract.isGesturalMode(mNavMode)) {
Insets gestureInsets = insets.getInsets(WindowInsets.Type.systemGestures());
mLeftInset = gestureInsets.left;
mRightInset = gestureInsets.right;
} else {
mLeftInset = mRightInset = 0;
}
return ScreenshotView.this.onApplyWindowInsets(insets);
});
// Get focus so that the key events go to the layout.
setFocusableInTouchMode(true);
requestFocus();
}
/**
* Set up the logger and callback on dismissal.
*
* Note: must be called before any other (non-constructor) method or null pointer exceptions
* may occur.
*/
void init(UiEventLogger uiEventLogger, Runnable onDismissRunnable) {
mUiEventLogger = uiEventLogger;
mOnDismissRunnable = onDismissRunnable;
}
void takePartialScreenshot(Consumer<Rect> onPartialScreenshotSelected) {
mScreenshotSelectorView.setOnScreenshotSelected(onPartialScreenshotSelected);
mScreenshotSelectorView.setVisibility(View.VISIBLE);
mScreenshotSelectorView.requestFocus();
}
void prepareForAnimation(Bitmap bitmap, Insets screenInsets) {
mScreenshotPreview.setImageDrawable(createScreenDrawable(mResources, bitmap, screenInsets));
// make static preview invisible (from gone) so we can query its location on screen
mScreenshotPreview.setVisibility(View.INVISIBLE);
}
AnimatorSet createScreenshotDropInAnimation(Rect bounds, boolean showFlash) {
mScreenshotPreview.setLayerType(View.LAYER_TYPE_HARDWARE, null);
mScreenshotPreview.buildLayer();
Rect previewBounds = new Rect();
mScreenshotPreview.getBoundsOnScreen(previewBounds);
int[] previewLocation = new int[2];
mScreenshotPreview.getLocationInWindow(previewLocation);
float cornerScale =
mCornerSizeX / (mOrientationPortrait ? bounds.width() : bounds.height());
final float currentScale = 1 / cornerScale;
mScreenshotPreview.setScaleX(currentScale);
mScreenshotPreview.setScaleY(currentScale);
mDismissButton.setAlpha(0);
mDismissButton.setVisibility(View.VISIBLE);
AnimatorSet dropInAnimation = new AnimatorSet();
ValueAnimator flashInAnimator = ValueAnimator.ofFloat(0, 1);
flashInAnimator.setDuration(SCREENSHOT_FLASH_IN_DURATION_MS);
flashInAnimator.setInterpolator(mFastOutSlowIn);
flashInAnimator.addUpdateListener(animation ->
mScreenshotFlash.setAlpha((float) animation.getAnimatedValue()));
ValueAnimator flashOutAnimator = ValueAnimator.ofFloat(1, 0);
flashOutAnimator.setDuration(SCREENSHOT_FLASH_OUT_DURATION_MS);
flashOutAnimator.setInterpolator(mFastOutSlowIn);
flashOutAnimator.addUpdateListener(animation ->
mScreenshotFlash.setAlpha((float) animation.getAnimatedValue()));
// animate from the current location, to the static preview location
final PointF startPos = new PointF(bounds.centerX(), bounds.centerY());
final PointF finalPos = new PointF(previewLocation[0] + previewBounds.width() / 2f,
previewLocation[1] + previewBounds.height() / 2f);
ValueAnimator toCorner = ValueAnimator.ofFloat(0, 1);
toCorner.setDuration(SCREENSHOT_TO_CORNER_Y_DURATION_MS);
float xPositionPct =
SCREENSHOT_TO_CORNER_X_DURATION_MS / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS;
float dismissPct =
SCREENSHOT_TO_CORNER_DISMISS_DELAY_MS / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS;
float scalePct =
SCREENSHOT_TO_CORNER_SCALE_DURATION_MS / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS;
toCorner.addUpdateListener(animation -> {
float t = animation.getAnimatedFraction();
if (t < scalePct) {
float scale = MathUtils.lerp(
currentScale, 1, mFastOutSlowIn.getInterpolation(t / scalePct));
mScreenshotPreview.setScaleX(scale);
mScreenshotPreview.setScaleY(scale);
} else {
mScreenshotPreview.setScaleX(1);
mScreenshotPreview.setScaleY(1);
}
if (t < xPositionPct) {
float xCenter = MathUtils.lerp(startPos.x, finalPos.x,
mFastOutSlowIn.getInterpolation(t / xPositionPct));
mScreenshotPreview.setX(xCenter - mScreenshotPreview.getWidth() / 2f);
} else {
mScreenshotPreview.setX(finalPos.x - mScreenshotPreview.getWidth() / 2f);
}
float yCenter = MathUtils.lerp(
startPos.y, finalPos.y, mFastOutSlowIn.getInterpolation(t));
mScreenshotPreview.setY(yCenter - mScreenshotPreview.getHeight() / 2f);
if (t >= dismissPct) {
mDismissButton.setAlpha((t - dismissPct) / (1 - dismissPct));
float currentX = mScreenshotPreview.getX();
float currentY = mScreenshotPreview.getY();
mDismissButton.setY(currentY - mDismissButton.getHeight() / 2f);
if (mDirectionLTR) {
mDismissButton.setX(currentX + mScreenshotPreview.getWidth()
- mDismissButton.getWidth() / 2f);
} else {
mDismissButton.setX(currentX - mDismissButton.getWidth() / 2f);
}
}
});
toCorner.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
mScreenshotPreview.setVisibility(View.VISIBLE);
}
});
mScreenshotFlash.setAlpha(0f);
mScreenshotFlash.setVisibility(View.VISIBLE);
if (showFlash) {
dropInAnimation.play(flashOutAnimator).after(flashInAnimator);
dropInAnimation.play(flashOutAnimator).with(toCorner);
} else {
dropInAnimation.play(toCorner);
}
dropInAnimation.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
mDismissButton.setOnClickListener(view -> {
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EXPLICIT_DISMISSAL);
animateDismissal();
});
mDismissButton.setAlpha(1);
float dismissOffset = mDismissButton.getWidth() / 2f;
float finalDismissX = mDirectionLTR
? finalPos.x - dismissOffset + bounds.width() * cornerScale / 2f
: finalPos.x - dismissOffset - bounds.width() * cornerScale / 2f;
mDismissButton.setX(finalDismissX);
mDismissButton.setY(
finalPos.y - dismissOffset - bounds.height() * cornerScale / 2f);
mScreenshotPreview.setScaleX(1);
mScreenshotPreview.setScaleY(1);
mScreenshotPreview.setX(finalPos.x - bounds.width() * cornerScale / 2f);
mScreenshotPreview.setY(finalPos.y - bounds.height() * cornerScale / 2f);
requestLayout();
mScreenshotPreview.setOnTouchListener(new SwipeDismissHandler());
createScreenshotActionsShadeAnimation().start();
}
});
return dropInAnimation;
}
ValueAnimator createScreenshotActionsShadeAnimation() {
// By default the activities won't be able to start immediately; override this to keep
// the same behavior as if started from a notification
try {
ActivityManager.getService().resumeAppSwitches();
} catch (RemoteException e) {
}
ArrayList<ScreenshotActionChip> chips = new ArrayList<>();
mShareChip.setText(mContext.getString(com.android.internal.R.string.share));
mShareChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_share), true);
mShareChip.setOnClickListener(v -> {
mShareChip.setIsPending(true);
mEditChip.setIsPending(false);
mPendingInteraction = PendingInteraction.SHARE;
});
chips.add(mShareChip);
mEditChip.setText(mContext.getString(R.string.screenshot_edit_label));
mEditChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_edit), true);
mEditChip.setOnClickListener(v -> {
mEditChip.setIsPending(true);
mShareChip.setIsPending(false);
mPendingInteraction = PendingInteraction.EDIT;
});
chips.add(mEditChip);
mScreenshotPreview.setOnClickListener(v -> {
mShareChip.setIsPending(false);
mEditChip.setIsPending(false);
mPendingInteraction = PendingInteraction.PREVIEW;
});
mScrollChip.setText(mContext.getString(R.string.screenshot_scroll_label));
mScrollChip.setIcon(Icon.createWithResource(mContext,
R.drawable.ic_screenshot_scroll), true);
chips.add(mScrollChip);
// remove the margin from the last chip so that it's correctly aligned with the end
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams)
mActionsView.getChildAt(0).getLayoutParams();
params.setMarginEnd(0);
mActionsView.getChildAt(0).setLayoutParams(params);
ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
animator.setDuration(SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS);
float alphaFraction = (float) SCREENSHOT_ACTIONS_ALPHA_DURATION_MS
/ SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS;
mActionsContainer.setAlpha(0f);
mActionsContainerBackground.setAlpha(0f);
mActionsContainer.setVisibility(View.VISIBLE);
mActionsContainerBackground.setVisibility(View.VISIBLE);
animator.addUpdateListener(animation -> {
float t = animation.getAnimatedFraction();
mBackgroundProtection.setAlpha(t);
float containerAlpha = t < alphaFraction ? t / alphaFraction : 1;
mActionsContainer.setAlpha(containerAlpha);
mActionsContainerBackground.setAlpha(containerAlpha);
float containerScale = SCREENSHOT_ACTIONS_START_SCALE_X
+ (t * (1 - SCREENSHOT_ACTIONS_START_SCALE_X));
mActionsContainer.setScaleX(containerScale);
mActionsContainerBackground.setScaleX(containerScale);
for (ScreenshotActionChip chip : chips) {
chip.setAlpha(t);
chip.setScaleX(1 / containerScale); // invert to keep size of children constant
}
mActionsContainer.setScrollX(mDirectionLTR ? 0 : mActionsContainer.getWidth());
mActionsContainer.setPivotX(mDirectionLTR ? 0 : mActionsContainer.getWidth());
mActionsContainerBackground.setPivotX(
mDirectionLTR ? 0 : mActionsContainerBackground.getWidth());
});
return animator;
}
void setChipIntents(ScreenshotController.SavedImageData imageData) {
mShareChip.setPendingIntent(imageData.shareAction.actionIntent,
() -> {
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED);
animateDismissal();
});
mEditChip.setPendingIntent(imageData.editAction.actionIntent,
() -> {
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EDIT_TAPPED);
animateDismissal();
});
mScreenshotPreview.setOnClickListener(v -> {
try {
imageData.editAction.actionIntent.send();
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, "Intent cancelled", e);
}
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_PREVIEW_TAPPED);
animateDismissal();
});
if (mPendingInteraction != null) {
switch (mPendingInteraction) {
case PREVIEW:
mScreenshotPreview.callOnClick();
break;
case SHARE:
mShareChip.callOnClick();
break;
case EDIT:
mEditChip.callOnClick();
break;
}
} else {
LayoutInflater inflater = LayoutInflater.from(mContext);
for (Notification.Action smartAction : imageData.smartActions) {
ScreenshotActionChip actionChip = (ScreenshotActionChip) inflater.inflate(
R.layout.global_screenshot_action_chip, mActionsView, false);
actionChip.setText(smartAction.title);
actionChip.setIcon(smartAction.getIcon(), false);
actionChip.setPendingIntent(smartAction.actionIntent,
() -> {
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SMART_ACTION_TAPPED);
animateDismissal();
});
mActionsView.addView(actionChip);
mSmartChips.add(actionChip);
}
}
}
boolean isDismissing() {
return (mDismissAnimation != null && mDismissAnimation.isRunning());
}
void animateDismissal() {
animateDismissal(createScreenshotDismissAnimation());
}
private void animateDismissal(Animator dismissAnimation) {
getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
mDismissAnimation = dismissAnimation;
mDismissAnimation.addListener(new AnimatorListenerAdapter() {
private boolean mCancelled = false;
@Override
public void onAnimationCancel(Animator animation) {
super.onAnimationCancel(animation);
mCancelled = true;
}
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
if (!mCancelled) {
mOnDismissRunnable.run();
}
}
});
mDismissAnimation.start();
}
void reset() {
if (mDismissAnimation != null && mDismissAnimation.isRunning()) {
mDismissAnimation.cancel();
}
// Make sure we clean up the view tree observer
getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
// Clear any references to the bitmap
mScreenshotPreview.setImageDrawable(null);
mActionsContainerBackground.setVisibility(View.GONE);
mActionsContainer.setVisibility(View.GONE);
mBackgroundProtection.setAlpha(0f);
mDismissButton.setVisibility(View.GONE);
mScreenshotPreview.setVisibility(View.GONE);
mScreenshotPreview.setLayerType(View.LAYER_TYPE_NONE, null);
mScreenshotPreview.setTranslationX(0);
mScreenshotPreview.setTranslationY(0);
mScreenshotPreview.setContentDescription(
mContext.getResources().getString(R.string.screenshot_preview_description));
mScreenshotPreview.setOnClickListener(null);
mShareChip.setOnClickListener(null);
mEditChip.setOnClickListener(null);
mShareChip.setIsPending(false);
mEditChip.setIsPending(false);
mPendingInteraction = null;
for (ScreenshotActionChip chip : mSmartChips) {
mActionsView.removeView(chip);
}
mSmartChips.clear();
setAlpha(1);
mDismissButton.setTranslationY(0);
mActionsContainer.setTranslationY(0);
mActionsContainerBackground.setTranslationY(0);
mScreenshotSelectorView.stop();
}
private AnimatorSet createScreenshotDismissAnimation() {
ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1);
alphaAnim.setStartDelay(SCREENSHOT_DISMISS_ALPHA_OFFSET_MS);
alphaAnim.setDuration(SCREENSHOT_DISMISS_ALPHA_DURATION_MS);
alphaAnim.addUpdateListener(animation -> {
setAlpha(1 - animation.getAnimatedFraction());
});
ValueAnimator yAnim = ValueAnimator.ofFloat(0, 1);
yAnim.setInterpolator(mAccelerateInterpolator);
yAnim.setDuration(SCREENSHOT_DISMISS_Y_DURATION_MS);
float screenshotStartY = mScreenshotPreview.getTranslationY();
float dismissStartY = mDismissButton.getTranslationY();
yAnim.addUpdateListener(animation -> {
float yDelta = MathUtils.lerp(0, mDismissDeltaY, animation.getAnimatedFraction());
mScreenshotPreview.setTranslationY(screenshotStartY + yDelta);
mDismissButton.setTranslationY(dismissStartY + yDelta);
mActionsContainer.setTranslationY(yDelta);
mActionsContainerBackground.setTranslationY(yDelta);
});
AnimatorSet animSet = new AnimatorSet();
animSet.play(yAnim).with(alphaAnim);
return animSet;
}
/**
* Create a drawable using the size of the bitmap and insets as the fractional inset parameters.
*/
private static Drawable createScreenDrawable(Resources res, Bitmap bitmap, Insets insets) {
int insettedWidth = bitmap.getWidth() - insets.left - insets.right;
int insettedHeight = bitmap.getHeight() - insets.top - insets.bottom;
BitmapDrawable bitmapDrawable = new BitmapDrawable(res, bitmap);
if (insettedHeight == 0 || insettedWidth == 0 || bitmap.getWidth() == 0
|| bitmap.getHeight() == 0) {
Log.e(TAG, String.format(
"Can't create insetted drawable, using 0 insets "
+ "bitmap and insets create degenerate region: %dx%d %s",
bitmap.getWidth(), bitmap.getHeight(), insets));
return bitmapDrawable;
}
InsetDrawable insetDrawable = new InsetDrawable(bitmapDrawable,
-1f * insets.left / insettedWidth,
-1f * insets.top / insettedHeight,
-1f * insets.right / insettedWidth,
-1f * insets.bottom / insettedHeight);
if (insets.left < 0 || insets.top < 0 || insets.right < 0 || insets.bottom < 0) {
// Are any of the insets negative, meaning the bitmap is smaller than the bounds so need
// to fill in the background of the drawable.
return new LayerDrawable(new Drawable[]{
new ColorDrawable(Color.BLACK), insetDrawable});
} else {
return insetDrawable;
}
}
class SwipeDismissHandler implements OnTouchListener {
// if distance moved on ACTION_UP is less than this, register a click
// otherwise, run return animator
private static final float CLICK_MOVEMENT_THRESHOLD_DP = 1;
// distance needed to register a dismissal
private static final float DISMISS_DISTANCE_THRESHOLD_DP = 30;
private final GestureDetector mGestureDetector;
private final float mDismissStartX;
private float mStartX;
private float mStartY;
private float mTranslationX = 0;
SwipeDismissHandler() {
GestureDetector.OnGestureListener gestureListener = new SwipeDismissGestureListener();
mGestureDetector = new GestureDetector(mContext, gestureListener);
mDismissStartX = mDismissButton.getX();
}
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
mStartX = event.getRawX();
mStartY = event.getRawY();
} else if (event.getActionMasked() == MotionEvent.ACTION_UP) {
if (isPastDismissThreshold()
&& (mDismissAnimation == null || !mDismissAnimation.isRunning())) {
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SWIPE_DISMISSED);
animateDismissal(createSwipeDismissAnimation());
return true;
} else if (MathUtils.dist(mStartX, mStartY, event.getRawX(), event.getRawY())
> dpToPx(CLICK_MOVEMENT_THRESHOLD_DP)) {
// if we've moved a non-negligible distance (but not past the threshold),
// start the return animation
if ((mDismissAnimation == null || !mDismissAnimation.isRunning())) {
createSwipeReturnAnimation().start();
}
return true;
}
}
return mGestureDetector.onTouchEvent(event);
}
class SwipeDismissGestureListener extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onScroll(
MotionEvent ev1, MotionEvent ev2, float distanceX, float distanceY) {
mTranslationX = ev2.getRawX() - ev1.getRawX();
mScreenshotPreview.setTranslationX(mTranslationX);
mDismissButton.setX(mDismissStartX + mTranslationX);
return true;
}
}
private boolean isPastDismissThreshold() {
if (mDirectionLTR) {
return mTranslationX <= -1 * dpToPx(DISMISS_DISTANCE_THRESHOLD_DP);
} else {
return mTranslationX >= dpToPx(DISMISS_DISTANCE_THRESHOLD_DP);
}
}
private ValueAnimator createSwipeDismissAnimation() {
ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
float startX = mTranslationX;
float finalX = mDirectionLTR
? -1 * (mDismissStartX + mDismissButton.getWidth())
: mDisplayMetrics.widthPixels;
anim.addUpdateListener(animation -> {
float translation = MathUtils.lerp(startX, finalX, animation.getAnimatedFraction());
mScreenshotPreview.setTranslationX(translation);
mDismissButton.setX(mDismissStartX + translation);
float yDelta = MathUtils.lerp(0, mDismissDeltaY, animation.getAnimatedFraction());
mActionsContainer.setTranslationY(yDelta);
mActionsContainerBackground.setTranslationY(yDelta);
setAlpha(1 - animation.getAnimatedFraction());
});
anim.setDuration(400);
return anim;
}
private ValueAnimator createSwipeReturnAnimation() {
ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
float startX = mTranslationX;
float finalX = 0;
mTranslationX = 0;
anim.addUpdateListener(animation -> {
float translation = MathUtils.lerp(
startX, finalX, animation.getAnimatedFraction());
mScreenshotPreview.setTranslationX(translation);
mDismissButton.setX(mDismissStartX + translation);
});
return anim;
}
private float dpToPx(float dp) {
return dp * mDisplayMetrics.densityDpi / (float) DisplayMetrics.DENSITY_DEFAULT;
}
}
}