Merge "Make screenshot/clipboard dismiss faster" into tm-dev

This commit is contained in:
Miranda Kephart
2022-03-08 22:05:49 +00:00
committed by Android (Google) Code Review
7 changed files with 416 additions and 459 deletions

View File

@@ -26,7 +26,7 @@
android:layout_width="match_parent"
android:layout_gravity="bottom"
android:src="@drawable/overlay_actions_background_protection"/>
<com.android.systemui.clipboardoverlay.DraggableConstraintLayout
<com.android.systemui.screenshot.DraggableConstraintLayout
android:id="@+id/clipboard_ui"
android:theme="@style/FloatingOverlay"
android:layout_width="match_parent"
@@ -146,5 +146,5 @@
android:layout_margin="@dimen/overlay_dismiss_button_margin"
android:src="@drawable/overlay_cancel"/>
</FrameLayout>
</com.android.systemui.clipboardoverlay.DraggableConstraintLayout>
</com.android.systemui.screenshot.DraggableConstraintLayout>
</FrameLayout>

View File

@@ -14,25 +14,25 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<androidx.constraintlayout.widget.ConstraintLayout
<com.android.systemui.screenshot.DraggableConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/screenshot_actions_container_background"
android:id="@+id/actions_container_background"
android:visibility="gone"
android:layout_height="0dp"
android:layout_width="0dp"
android:elevation="1dp"
android:background="@drawable/action_chip_container_background"
android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal"
app:layout_constraintBottom_toBottomOf="@+id/screenshot_actions_container"
app:layout_constraintBottom_toBottomOf="@+id/actions_container"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/screenshot_actions_container"
app:layout_constraintEnd_toEndOf="@+id/screenshot_actions_container"/>
app:layout_constraintTop_toTopOf="@+id/actions_container"
app:layout_constraintEnd_toEndOf="@+id/actions_container"/>
<HorizontalScrollView
android:id="@+id/screenshot_actions_container"
android:id="@+id/actions_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/overlay_action_container_margin_horizontal"
@@ -130,4 +130,4 @@
app:layout_constraintStart_toStartOf="@id/screenshot_preview"
app:layout_constraintTop_toTopOf="@id/screenshot_preview"
android:elevation="@dimen/overlay_preview_elevation"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.android.systemui.screenshot.DraggableConstraintLayout>

View File

@@ -73,6 +73,7 @@ import android.widget.TextView;
import com.android.internal.policy.PhoneWindow;
import com.android.systemui.R;
import com.android.systemui.screenshot.DraggableConstraintLayout;
import com.android.systemui.screenshot.FloatingWindowUtil;
import com.android.systemui.screenshot.OverlayActionChip;
import com.android.systemui.screenshot.TimeoutHandler;
@@ -166,8 +167,28 @@ public class ClipboardOverlayController {
mRemoteCopyChip = requireNonNull(mView.findViewById(R.id.remote_copy_chip));
mDismissButton = requireNonNull(mView.findViewById(R.id.dismiss_button));
mView.setOnDismissEndCallback(this::hideImmediate);
mView.setOnInteractionCallback(mTimeoutHandler::resetTimeout);
mView.setCallbacks(new DraggableConstraintLayout.SwipeDismissCallbacks() {
@Override
public void onInteraction() {
mTimeoutHandler.resetTimeout();
}
@Override
public void onSwipeDismissInitiated(Animator animator) {
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
mContainer.animate().alpha(0).start();
}
});
}
@Override
public void onDismissComplete() {
hideImmediate();
}
});
mDismissButton.setOnClickListener(view -> animateOut());

View File

@@ -1,163 +0,0 @@
/*
* Copyright (C) 2021 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.clipboardoverlay;
import android.animation.Animator;
import android.content.Context;
import android.graphics.Rect;
import android.graphics.Region;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.android.systemui.R;
import com.android.systemui.screenshot.SwipeDismissHandler;
import java.util.function.Consumer;
/**
* ConstraintLayout that is draggable when touched in a specific region
*/
public class DraggableConstraintLayout extends ConstraintLayout
implements ViewTreeObserver.OnComputeInternalInsetsListener {
private final SwipeDismissHandler mSwipeDismissHandler;
private final GestureDetector mSwipeDetector;
private Consumer<Animator> mOnDismissInitiated;
private Runnable mOnDismissComplete;
private Runnable mOnInteraction;
public DraggableConstraintLayout(Context context) {
this(context, null);
}
public DraggableConstraintLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DraggableConstraintLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mSwipeDismissHandler = new SwipeDismissHandler(mContext, this,
new SwipeDismissHandler.SwipeDismissCallbacks() {
@Override
public void onInteraction() {
if (mOnInteraction != null) {
mOnInteraction.run();
}
}
@Override
public void onSwipeDismissInitiated(Animator animator) {
if (mOnDismissInitiated != null) {
mOnDismissInitiated.accept(animator);
}
}
@Override
public void onDismissComplete() {
if (mOnDismissComplete != null) {
mOnDismissComplete.run();
}
}
});
setOnTouchListener(mSwipeDismissHandler);
mSwipeDetector = new GestureDetector(mContext,
new GestureDetector.SimpleOnGestureListener() {
final Rect mActionsRect = new Rect();
@Override
public boolean onScroll(
MotionEvent ev1, MotionEvent ev2, float distanceX, float distanceY) {
View actionsContainer = findViewById(R.id.actions_container);
actionsContainer.getBoundsOnScreen(mActionsRect);
// return true if we aren't in the actions bar, or if we are but it isn't
// scrollable in the direction of movement
return !mActionsRect.contains((int) ev2.getRawX(), (int) ev2.getRawY())
|| !actionsContainer.canScrollHorizontally((int) distanceX);
}
});
mSwipeDetector.setIsLongpressEnabled(false);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
mSwipeDismissHandler.onTouch(this, ev);
}
return mSwipeDetector.onTouchEvent(ev);
}
/**
* Dismiss the view, with animation controlled by SwipeDismissHandler
*/
public void dismiss() {
mSwipeDismissHandler.dismiss();
}
/**
* Set the callback to be run after view is dismissed (before animation; receives animator as
* input)
*/
public void setOnDismissStartCallback(Consumer<Animator> callback) {
mOnDismissInitiated = callback;
}
/**
* Set the callback to be run after view is dismissed
*/
public void setOnDismissEndCallback(Runnable callback) {
mOnDismissComplete = callback;
}
/**
* Set the callback to be run when the view is interacted with (e.g. tapped)
*/
public void setOnInteractionCallback(Runnable callback) {
mOnInteraction = callback;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
getViewTreeObserver().addOnComputeInternalInsetsListener(this);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
}
@Override
public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
// Only child views are touchable.
Region r = new Region();
Rect rect = new Rect();
for (int i = 0; i < getChildCount(); i++) {
getChildAt(i).getGlobalVisibleRect(rect);
r.op(rect, Region.Op.UNION);
}
inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
inoutInfo.touchableRegion.set(r);
}
}

View File

@@ -0,0 +1,354 @@
/*
* Copyright (C) 2021 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 com.android.systemui.screenshot.LogConfig.DEBUG_ANIM;
import static com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Rect;
import android.graphics.Region;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.MathUtils;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.android.systemui.R;
/**
* ConstraintLayout that is draggable when touched in a specific region
*/
public class DraggableConstraintLayout extends ConstraintLayout
implements ViewTreeObserver.OnComputeInternalInsetsListener {
private final SwipeDismissHandler mSwipeDismissHandler;
private final GestureDetector mSwipeDetector;
private View mActionsContainer;
private SwipeDismissCallbacks mCallbacks;
/**
* Stores the callbacks when the view is interacted with or dismissed.
*/
public interface SwipeDismissCallbacks {
/**
* Run when the view is interacted with (touched)
*/
default void onInteraction() {
}
/**
* Run when the view is dismissed (the distance threshold is met), pre-dismissal animation
*/
default void onSwipeDismissInitiated(Animator animator) {
}
/**
* Run when the view is dismissed (the distance threshold is met), post-dismissal animation
*/
default void onDismissComplete() {
}
}
public DraggableConstraintLayout(Context context) {
this(context, null);
}
public DraggableConstraintLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DraggableConstraintLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mSwipeDismissHandler = new SwipeDismissHandler(mContext, this);
setOnTouchListener(mSwipeDismissHandler);
mSwipeDetector = new GestureDetector(mContext,
new GestureDetector.SimpleOnGestureListener() {
final Rect mActionsRect = new Rect();
@Override
public boolean onScroll(
MotionEvent ev1, MotionEvent ev2, float distanceX, float distanceY) {
mActionsContainer.getBoundsOnScreen(mActionsRect);
// return true if we aren't in the actions bar, or if we are but it isn't
// scrollable in the direction of movement
return !mActionsRect.contains((int) ev2.getRawX(), (int) ev2.getRawY())
|| !mActionsContainer.canScrollHorizontally((int) distanceX);
}
});
mSwipeDetector.setIsLongpressEnabled(false);
}
public void setCallbacks(SwipeDismissCallbacks callbacks) {
mCallbacks = callbacks;
}
@Override // View
protected void onFinishInflate() {
mActionsContainer = findViewById(R.id.actions_container_background);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
mSwipeDismissHandler.onTouch(this, ev);
}
return mSwipeDetector.onTouchEvent(ev);
}
public int getVisibleRight() {
return mActionsContainer.getRight();
}
/**
* Cancel current dismissal animation, if any
*/
public void cancelDismissal() {
mSwipeDismissHandler.cancel();
}
/**
* Return whether the view is currently dismissing
*/
public boolean isDismissing() {
return mSwipeDismissHandler.isDismissing();
}
/**
* Dismiss the view, with animation controlled by SwipeDismissHandler
*/
public void dismiss() {
mSwipeDismissHandler.dismiss();
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
getViewTreeObserver().addOnComputeInternalInsetsListener(this);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
}
@Override
public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
// Only child views are touchable.
Region r = new Region();
Rect rect = new Rect();
for (int i = 0; i < getChildCount(); i++) {
getChildAt(i).getGlobalVisibleRect(rect);
r.op(rect, Region.Op.UNION);
}
inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
inoutInfo.touchableRegion.set(r);
}
/**
* Allows a view to be swipe-dismissed, or returned to its location if distance threshold is not
* met
*/
private class SwipeDismissHandler implements OnTouchListener {
private static final String TAG = "SwipeDismissHandler";
// distance needed to register a dismissal
private static final float DISMISS_DISTANCE_THRESHOLD_DP = 20;
private final DraggableConstraintLayout mView;
private final GestureDetector mGestureDetector;
private final DisplayMetrics mDisplayMetrics;
private ValueAnimator mDismissAnimation;
private float mStartX;
// Keeps track of the most recent direction (between the last two move events).
// -1 for left; +1 for right.
private int mDirectionX;
private float mPreviousX;
SwipeDismissHandler(Context context, DraggableConstraintLayout view) {
mView = view;
GestureDetector.OnGestureListener gestureListener = new SwipeDismissGestureListener();
mGestureDetector = new GestureDetector(context, gestureListener);
mDisplayMetrics = new DisplayMetrics();
context.getDisplay().getRealMetrics(mDisplayMetrics);
mCallbacks = new SwipeDismissCallbacks() {
}; // default to unimplemented callbacks
}
@Override
public boolean onTouch(View view, MotionEvent event) {
boolean gestureResult = mGestureDetector.onTouchEvent(event);
mCallbacks.onInteraction();
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
mStartX = event.getRawX();
mPreviousX = mStartX;
return true;
} else if (event.getActionMasked() == MotionEvent.ACTION_UP) {
if (mDismissAnimation != null && mDismissAnimation.isRunning()) {
return true;
}
if (isPastDismissThreshold()) {
dismiss();
} else {
// if we've moved, but not past the threshold, start the return animation
if (DEBUG_DISMISS) {
Log.d(TAG, "swipe gesture abandoned");
}
createSwipeReturnAnimation().start();
}
return true;
}
return gestureResult;
}
class SwipeDismissGestureListener extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onScroll(
MotionEvent ev1, MotionEvent ev2, float distanceX, float distanceY) {
mView.setTranslationX(ev2.getRawX() - mStartX);
mDirectionX = (ev2.getRawX() < mPreviousX) ? -1 : 1;
mPreviousX = ev2.getRawX();
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
if (mView.getTranslationX() * velocityX > 0
&& (mDismissAnimation == null || !mDismissAnimation.isRunning())) {
ValueAnimator dismissAnimator =
createSwipeDismissAnimation(velocityX / (float) 1000);
mCallbacks.onSwipeDismissInitiated(dismissAnimator);
dismiss(dismissAnimator);
return true;
}
return false;
}
}
private boolean isPastDismissThreshold() {
float translationX = mView.getTranslationX();
// Determines whether the absolute translation from the start is in the same direction
// as the current movement. For example, if the user moves most of the way to the right,
// but then starts dragging back left, we do not dismiss even though the absolute
// distance is greater than the threshold.
if (translationX * mDirectionX > 0) {
return Math.abs(translationX) >= FloatingWindowUtil.dpToPx(mDisplayMetrics,
DISMISS_DISTANCE_THRESHOLD_DP);
}
return false;
}
boolean isDismissing() {
return (mDismissAnimation != null && mDismissAnimation.isRunning());
}
void cancel() {
if (isDismissing()) {
if (DEBUG_ANIM) {
Log.d(TAG, "cancelling dismiss animation");
}
mDismissAnimation.cancel();
}
}
void dismiss() {
ValueAnimator anim = createSwipeDismissAnimation(3);
mCallbacks.onSwipeDismissInitiated(anim);
dismiss(anim);
}
private void dismiss(ValueAnimator animator) {
mDismissAnimation = animator;
mDismissAnimation.addListener(new AnimatorListenerAdapter() {
private boolean mCancelled;
@Override
public void onAnimationCancel(Animator animation) {
super.onAnimationCancel(animation);
mCancelled = true;
}
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
if (!mCancelled) {
mCallbacks.onDismissComplete();
}
}
});
mDismissAnimation.start();
}
private ValueAnimator createSwipeDismissAnimation(float velocity) {
// velocity is measured in pixels per millisecond
velocity = Math.min(3, Math.max(1, velocity));
ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
float startX = mView.getTranslationX();
// make sure the UI gets all the way off the screen in the direction of movement
// (the actions container background is guaranteed to be both the leftmost and
// rightmost UI element in LTR and RTL)
float finalX;
int layoutDir =
mView.getContext().getResources().getConfiguration().getLayoutDirection();
if (startX > 0 || (startX == 0 && layoutDir == LAYOUT_DIRECTION_RTL)) {
finalX = mDisplayMetrics.widthPixels;
} else {
finalX = -1 * mActionsContainer.getRight();
}
float distance = Math.abs(finalX - startX);
anim.addUpdateListener(animation -> {
float translation = MathUtils.lerp(startX, finalX, animation.getAnimatedFraction());
mView.setTranslationX(translation);
mView.setAlpha(1 - animation.getAnimatedFraction());
});
anim.setDuration((long) (distance / Math.abs(velocity)));
return anim;
}
private ValueAnimator createSwipeReturnAnimation() {
ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
float startX = mView.getTranslationX();
float finalX = 0;
anim.addUpdateListener(animation -> {
float translation = MathUtils.lerp(
startX, finalX, animation.getAnimatedFraction());
mView.setTranslationX(translation);
});
return anim;
}
}
}

View File

@@ -141,7 +141,7 @@ public class ScreenshotView extends FrameLayout implements
private ScreenshotSelectorView mScreenshotSelectorView;
private ImageView mScrollingScrim;
private View mScreenshotStatic;
private DraggableConstraintLayout mScreenshotStatic;
private ImageView mScreenshotPreview;
private View mScreenshotPreviewBorder;
private ImageView mScrollablePreview;
@@ -159,7 +159,6 @@ public class ScreenshotView extends FrameLayout implements
private UiEventLogger mUiEventLogger;
private ScreenshotViewCallback mCallbacks;
private boolean mPendingSharedTransition;
private SwipeDismissHandler mSwipeDismissHandler;
private InputMonitorCompat mInputMonitor;
private InputChannelCompat.InputEventReceiver mInputEventReceiver;
private boolean mShowScrollablePreview;
@@ -332,19 +331,6 @@ public class ScreenshotView extends FrameLayout implements
}
}
@Override // ViewGroup
public boolean onInterceptTouchEvent(MotionEvent ev) {
// scrolling scrim should not be swipeable; return early if we're on the scrim
if (!getSwipeRegion().contains((int) ev.getRawX(), (int) ev.getRawY())) {
return false;
}
// always pass through the down event so the swipe handler knows the initial state
if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
mSwipeDismissHandler.onTouch(this, ev);
}
return mSwipeDetector.onTouchEvent(ev);
}
@Override // View
protected void onFinishInflate() {
mScrollingScrim = requireNonNull(findViewById(R.id.screenshot_scrolling_scrim));
@@ -356,8 +342,8 @@ public class ScreenshotView extends FrameLayout implements
mScreenshotPreview.setClipToOutline(true);
mActionsContainerBackground = requireNonNull(findViewById(
R.id.screenshot_actions_container_background));
mActionsContainer = requireNonNull(findViewById(R.id.screenshot_actions_container));
R.id.actions_container_background));
mActionsContainer = requireNonNull(findViewById(R.id.actions_container));
mActionsView = requireNonNull(findViewById(R.id.screenshot_actions));
mBackgroundProtection = requireNonNull(
findViewById(R.id.screenshot_actions_background));
@@ -395,35 +381,34 @@ public class ScreenshotView extends FrameLayout implements
setFocusableInTouchMode(true);
requestFocus();
mSwipeDismissHandler = new SwipeDismissHandler(mContext, mScreenshotStatic,
new SwipeDismissHandler.SwipeDismissCallbacks() {
@Override
public void onInteraction() {
mCallbacks.onUserInteraction();
}
mScreenshotStatic.setCallbacks(new DraggableConstraintLayout.SwipeDismissCallbacks() {
@Override
public void onInteraction() {
mCallbacks.onUserInteraction();
}
@Override
public void onSwipeDismissInitiated(Animator animator) {
if (DEBUG_DISMISS) {
Log.d(ScreenshotView.TAG, "dismiss triggered via swipe gesture");
}
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SWIPE_DISMISSED, 0,
mPackageName);
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onSwipeDismissInitiated(Animator anim) {
if (DEBUG_DISMISS) {
Log.d(ScreenshotView.TAG, "dismiss triggered via swipe gesture");
}
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SWIPE_DISMISSED, 0,
mPackageName);
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
mBackgroundProtection.animate()
.alpha(0).setDuration(anim.getDuration()).start();
}
});
}
@Override
public void onDismissComplete() {
mCallbacks.onDismiss();
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
mBackgroundProtection.animate()
.alpha(0).setDuration(animation.getDuration()).start();
}
});
}
@Override
public void onDismissComplete() {
mCallbacks.onDismiss();
}
});
}
View getScreenshotPreview() {
@@ -648,8 +633,6 @@ public class ScreenshotView extends FrameLayout implements
requestLayout();
createScreenshotActionsShadeAnimation().start();
setOnTouchListener(mSwipeDismissHandler);
}
});
@@ -958,7 +941,7 @@ public class ScreenshotView extends FrameLayout implements
}
boolean isDismissing() {
return mSwipeDismissHandler.isDismissing();
return mScreenshotStatic.isDismissing();
}
boolean isPendingSharedTransition() {
@@ -966,15 +949,14 @@ public class ScreenshotView extends FrameLayout implements
}
void animateDismissal() {
mSwipeDismissHandler.dismiss();
mScreenshotStatic.dismiss();
}
void reset() {
if (DEBUG_UI) {
Log.d(TAG, "reset screenshot view");
}
mSwipeDismissHandler.cancel();
mScreenshotStatic.cancelDismissal();
if (DEBUG_WINDOW) {
Log.d(TAG, "removing OnComputeInternalInsetsListener");
}

View File

@@ -1,237 +0,0 @@
/*
* Copyright (C) 2021 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 com.android.systemui.screenshot.LogConfig.DEBUG_ANIM;
import static com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.MathUtils;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
/**
* Allows a view to be swipe-dismissed, or returned to its location if distance threshold is not met
*/
public class SwipeDismissHandler implements View.OnTouchListener {
private static final String TAG = "SwipeDismissHandler";
// distance needed to register a dismissal
private static final float DISMISS_DISTANCE_THRESHOLD_DP = 20;
/**
* Stores the callbacks when the view is interacted with or dismissed.
*/
public interface SwipeDismissCallbacks {
/**
* Run when the view is interacted with (touched)
*/
void onInteraction();
/**
* Run when the view is dismissed (the distance threshold is met), pre-dismissal animation
*/
void onSwipeDismissInitiated(Animator animator);
/**
* Run when the view is dismissed (the distance threshold is met), post-dismissal animation
*/
void onDismissComplete();
}
private final View mView;
private final SwipeDismissCallbacks mCallbacks;
private final GestureDetector mGestureDetector;
private DisplayMetrics mDisplayMetrics;
private ValueAnimator mDismissAnimation;
private float mStartX;
// Keeps track of the most recent direction (between the last two move events).
// -1 for left; +1 for right.
private int mDirectionX;
private float mPreviousX;
public SwipeDismissHandler(Context context, View view, SwipeDismissCallbacks callbacks) {
mView = view;
mCallbacks = callbacks;
GestureDetector.OnGestureListener gestureListener = new SwipeDismissGestureListener();
mGestureDetector = new GestureDetector(context, gestureListener);
mDisplayMetrics = new DisplayMetrics();
context.getDisplay().getRealMetrics(mDisplayMetrics);
}
@Override
public boolean onTouch(View view, MotionEvent event) {
boolean gestureResult = mGestureDetector.onTouchEvent(event);
mCallbacks.onInteraction();
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
mStartX = event.getRawX();
mPreviousX = mStartX;
return true;
} else if (event.getActionMasked() == MotionEvent.ACTION_UP) {
if (mDismissAnimation != null && mDismissAnimation.isRunning()) {
return true;
}
if (isPastDismissThreshold()) {
ValueAnimator dismissAnimator = createSwipeDismissAnimation(1);
mCallbacks.onSwipeDismissInitiated(dismissAnimator);
dismiss(dismissAnimator);
} else {
// if we've moved, but not past the threshold, start the return animation
if (DEBUG_DISMISS) {
Log.d(TAG, "swipe gesture abandoned");
}
createSwipeReturnAnimation().start();
}
return true;
}
return gestureResult;
}
class SwipeDismissGestureListener extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onScroll(
MotionEvent ev1, MotionEvent ev2, float distanceX, float distanceY) {
mView.setTranslationX(ev2.getRawX() - mStartX);
mDirectionX = (ev2.getRawX() < mPreviousX) ? -1 : 1;
mPreviousX = ev2.getRawX();
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
if (mView.getTranslationX() * velocityX > 0
&& (mDismissAnimation == null || !mDismissAnimation.isRunning())) {
ValueAnimator dismissAnimator =
createSwipeDismissAnimation(velocityX / (float) 1000);
mCallbacks.onSwipeDismissInitiated(dismissAnimator);
dismiss(dismissAnimator);
return true;
}
return false;
}
}
private boolean isPastDismissThreshold() {
float translationX = mView.getTranslationX();
// Determines whether the absolute translation from the start is in the same direction
// as the current movement. For example, if the user moves most of the way to the right,
// but then starts dragging back left, we do not dismiss even though the absolute
// distance is greater than the threshold.
if (translationX * mDirectionX > 0) {
return Math.abs(translationX) >= FloatingWindowUtil.dpToPx(mDisplayMetrics,
DISMISS_DISTANCE_THRESHOLD_DP);
}
return false;
}
/**
* Return whether the view is currently being dismissed
*/
public boolean isDismissing() {
return (mDismissAnimation != null && mDismissAnimation.isRunning());
}
/**
* Cancel the currently-running dismissal animation, if any.
*/
public void cancel() {
if (isDismissing()) {
if (DEBUG_ANIM) {
Log.d(TAG, "cancelling dismiss animation");
}
mDismissAnimation.cancel();
}
}
/**
* Start dismissal animation (will run onDismiss callback when animation complete)
*/
public void dismiss() {
dismiss(createSwipeDismissAnimation(1));
}
private void dismiss(ValueAnimator animator) {
mDismissAnimation = animator;
mDismissAnimation.addListener(new AnimatorListenerAdapter() {
private boolean mCancelled;
@Override
public void onAnimationCancel(Animator animation) {
super.onAnimationCancel(animation);
mCancelled = true;
}
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
if (!mCancelled) {
mCallbacks.onDismissComplete();
}
}
});
mDismissAnimation.start();
}
private ValueAnimator createSwipeDismissAnimation(float velocity) {
// velocity is measured in pixels per millisecond
velocity = Math.min(3, Math.max(1, velocity));
ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
float startX = mView.getTranslationX();
// make sure the UI gets all the way off the screen in the direction of movement
// (the actions container background is guaranteed to be both the leftmost and
// rightmost UI element in LTR and RTL)
float finalX;
int layoutDir = mView.getContext().getResources().getConfiguration().getLayoutDirection();
if (startX > 0 || (startX == 0 && layoutDir == View.LAYOUT_DIRECTION_RTL)) {
finalX = mDisplayMetrics.widthPixels;
} else {
finalX = -1 * mView.getRight();
}
float distance = Math.abs(finalX - startX);
anim.addUpdateListener(animation -> {
float translation = MathUtils.lerp(startX, finalX, animation.getAnimatedFraction());
mView.setTranslationX(translation);
mView.setAlpha(1 - animation.getAnimatedFraction());
});
anim.setDuration((long) (distance / Math.abs(velocity)));
return anim;
}
private ValueAnimator createSwipeReturnAnimation() {
ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
float startX = mView.getTranslationX();
float finalX = 0;
anim.addUpdateListener(animation -> {
float translation = MathUtils.lerp(
startX, finalX, animation.getAnimatedFraction());
mView.setTranslationX(translation);
});
return anim;
}
}