Merge "Make screenshot/clipboard dismiss faster" into tm-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
63a29eadbd
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user