Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 29617453 authored by Miranda Kephart's avatar Miranda Kephart
Browse files

Make screenshot/clipboard dismiss faster

Makes the screenshot view an instance of DraggableConstraintOverlay
and increases the default velocity for both from 1 to 3.

Bug: 222129856
Fix: 222129856
Test: manual (visual change)
Change-Id: If7e6d331c6bedeb5eb9706e484eaaae32e24b138
parent 33e3c435
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -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>
 No newline at end of file
+7 −7
Original line number Diff line number Diff line
@@ -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>
+23 −2
Original line number Diff line number Diff line
@@ -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());

+0 −163
Original line number Diff line number Diff line
/*
 * 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);
    }
}
+354 −0
Original line number Diff line number Diff line
/*
 * 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;
        }
    }
}
Loading