mirror of
https://github.com/material-components/material-components-android.git
synced 2026-02-20 08:39:55 +08:00
* commit 'e274607cdee3deb8f2f15a6bbdaf6051dd6fa6bc': Ensure that SwipeDismissBehavior affects the correct view GitOrigin-RevId=9d6eb9beafef317f0f1bc5596ab47114c161f41a PiperOrigin-RevId: 140551102
393 lines
14 KiB
Java
393 lines
14 KiB
Java
/*
|
|
* Copyright (C) 2015 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 android.support.design.widget;
|
|
|
|
import android.support.annotation.IntDef;
|
|
import android.support.annotation.NonNull;
|
|
import android.support.v4.view.MotionEventCompat;
|
|
import android.support.v4.view.ViewCompat;
|
|
import android.support.v4.widget.ViewDragHelper;
|
|
import android.view.MotionEvent;
|
|
import android.view.View;
|
|
import android.view.ViewGroup;
|
|
|
|
import java.lang.annotation.Retention;
|
|
import java.lang.annotation.RetentionPolicy;
|
|
|
|
/**
|
|
* An interaction behavior plugin for child views of {@link CoordinatorLayout} to provide support
|
|
* for the 'swipe-to-dismiss' gesture.
|
|
*/
|
|
public class SwipeDismissBehavior<V extends View> extends CoordinatorLayout.Behavior<V> {
|
|
|
|
/**
|
|
* A view is not currently being dragged or animating as a result of a fling/snap.
|
|
*/
|
|
public static final int STATE_IDLE = ViewDragHelper.STATE_IDLE;
|
|
|
|
/**
|
|
* A view is currently being dragged. The position is currently changing as a result
|
|
* of user input or simulated user input.
|
|
*/
|
|
public static final int STATE_DRAGGING = ViewDragHelper.STATE_DRAGGING;
|
|
|
|
/**
|
|
* A view is currently settling into place as a result of a fling or
|
|
* predefined non-interactive motion.
|
|
*/
|
|
public static final int STATE_SETTLING = ViewDragHelper.STATE_SETTLING;
|
|
|
|
/** @hide */
|
|
@IntDef({SWIPE_DIRECTION_START_TO_END, SWIPE_DIRECTION_END_TO_START, SWIPE_DIRECTION_ANY})
|
|
@Retention(RetentionPolicy.SOURCE)
|
|
private @interface SwipeDirection {}
|
|
|
|
/**
|
|
* Swipe direction that only allows swiping in the direction of start-to-end. That is
|
|
* left-to-right in LTR, or right-to-left in RTL.
|
|
*/
|
|
public static final int SWIPE_DIRECTION_START_TO_END = 0;
|
|
|
|
/**
|
|
* Swipe direction that only allows swiping in the direction of end-to-start. That is
|
|
* right-to-left in LTR or left-to-right in RTL.
|
|
*/
|
|
public static final int SWIPE_DIRECTION_END_TO_START = 1;
|
|
|
|
/**
|
|
* Swipe direction which allows swiping in either direction.
|
|
*/
|
|
public static final int SWIPE_DIRECTION_ANY = 2;
|
|
|
|
private static final float DEFAULT_DRAG_DISMISS_THRESHOLD = 0.5f;
|
|
private static final float DEFAULT_ALPHA_START_DISTANCE = 0f;
|
|
private static final float DEFAULT_ALPHA_END_DISTANCE = DEFAULT_DRAG_DISMISS_THRESHOLD;
|
|
|
|
private ViewDragHelper mViewDragHelper;
|
|
private OnDismissListener mListener;
|
|
private boolean mIgnoreEvents;
|
|
|
|
private float mSensitivity = 0f;
|
|
private boolean mSensitivitySet;
|
|
|
|
private int mSwipeDirection = SWIPE_DIRECTION_ANY;
|
|
private float mDragDismissThreshold = DEFAULT_DRAG_DISMISS_THRESHOLD;
|
|
private float mAlphaStartSwipeDistance = DEFAULT_ALPHA_START_DISTANCE;
|
|
private float mAlphaEndSwipeDistance = DEFAULT_ALPHA_END_DISTANCE;
|
|
|
|
/**
|
|
* Callback interface used to notify the application that the view has been dismissed.
|
|
*/
|
|
public interface OnDismissListener {
|
|
/**
|
|
* Called when {@code view} has been dismissed via swiping.
|
|
*/
|
|
public void onDismiss(View view);
|
|
|
|
/**
|
|
* Called when the drag state has changed.
|
|
*
|
|
* @param state the new state. One of
|
|
* {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}.
|
|
*/
|
|
public void onDragStateChanged(int state);
|
|
}
|
|
|
|
/**
|
|
* Set the listener to be used when a dismiss event occurs.
|
|
*
|
|
* @param listener the listener to use.
|
|
*/
|
|
public void setListener(OnDismissListener listener) {
|
|
mListener = listener;
|
|
}
|
|
|
|
/**
|
|
* Sets the swipe direction for this behavior.
|
|
*
|
|
* @param direction one of the {@link #SWIPE_DIRECTION_START_TO_END},
|
|
* {@link #SWIPE_DIRECTION_END_TO_START} or {@link #SWIPE_DIRECTION_ANY}
|
|
*/
|
|
public void setSwipeDirection(@SwipeDirection int direction) {
|
|
mSwipeDirection = direction;
|
|
}
|
|
|
|
/**
|
|
* Set the threshold for telling if a view has been dragged enough to be dismissed.
|
|
*
|
|
* @param distance a ratio of a view's width, values are clamped to 0 >= x <= 1f;
|
|
*/
|
|
public void setDragDismissDistance(float distance) {
|
|
mDragDismissThreshold = clamp(0f, distance, 1f);
|
|
}
|
|
|
|
/**
|
|
* The minimum swipe distance before the view's alpha is modified.
|
|
*
|
|
* @param fraction the distance as a fraction of the view's width.
|
|
*/
|
|
public void setStartAlphaSwipeDistance(float fraction) {
|
|
mAlphaStartSwipeDistance = clamp(0f, fraction, 1f);
|
|
}
|
|
|
|
/**
|
|
* The maximum swipe distance for the view's alpha is modified.
|
|
*
|
|
* @param fraction the distance as a fraction of the view's width.
|
|
*/
|
|
public void setEndAlphaSwipeDistance(float fraction) {
|
|
mAlphaEndSwipeDistance = clamp(0f, fraction, 1f);
|
|
}
|
|
|
|
/**
|
|
* Set the sensitivity used for detecting the start of a swipe. This only takes effect if
|
|
* no touch handling has occured yet.
|
|
*
|
|
* @param sensitivity Multiplier for how sensitive we should be about detecting
|
|
* the start of a drag. Larger values are more sensitive. 1.0f is normal.
|
|
*/
|
|
public void setSensitivity(float sensitivity) {
|
|
mSensitivity = sensitivity;
|
|
mSensitivitySet = true;
|
|
}
|
|
|
|
@Override
|
|
public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) {
|
|
switch (MotionEventCompat.getActionMasked(event)) {
|
|
case MotionEvent.ACTION_UP:
|
|
case MotionEvent.ACTION_CANCEL:
|
|
// Reset the ignore flag
|
|
if (mIgnoreEvents) {
|
|
mIgnoreEvents = false;
|
|
return false;
|
|
}
|
|
break;
|
|
default:
|
|
mIgnoreEvents = !parent.isPointInChildBounds(child,
|
|
(int) event.getX(), (int) event.getY());
|
|
break;
|
|
}
|
|
|
|
if (mIgnoreEvents) {
|
|
return false;
|
|
}
|
|
|
|
ensureViewDragHelper(parent);
|
|
return mViewDragHelper.shouldInterceptTouchEvent(event);
|
|
}
|
|
|
|
@Override
|
|
public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) {
|
|
if (mViewDragHelper != null) {
|
|
mViewDragHelper.processTouchEvent(event);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Called when the user's input indicates that they want to swipe the given view.
|
|
*
|
|
* @param view View the user is attempting to swipe
|
|
* @return true if the view can be dismissed via swiping, false otherwise
|
|
*/
|
|
public boolean canSwipeDismissView(@NonNull View view) {
|
|
return true;
|
|
}
|
|
|
|
private final ViewDragHelper.Callback mDragCallback = new ViewDragHelper.Callback() {
|
|
private int mOriginalCapturedViewLeft;
|
|
|
|
@Override
|
|
public boolean tryCaptureView(View child, int pointerId) {
|
|
return canSwipeDismissView(child);
|
|
}
|
|
|
|
@Override
|
|
public void onViewCaptured(View capturedChild, int activePointerId) {
|
|
mOriginalCapturedViewLeft = capturedChild.getLeft();
|
|
}
|
|
|
|
@Override
|
|
public void onViewDragStateChanged(int state) {
|
|
if (mListener != null) {
|
|
mListener.onDragStateChanged(state);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onViewReleased(View child, float xvel, float yvel) {
|
|
final int childWidth = child.getWidth();
|
|
int targetLeft;
|
|
boolean dismiss = false;
|
|
|
|
if (shouldDismiss(child, xvel)) {
|
|
targetLeft = child.getLeft() < mOriginalCapturedViewLeft
|
|
? mOriginalCapturedViewLeft - childWidth
|
|
: mOriginalCapturedViewLeft + childWidth;
|
|
dismiss = true;
|
|
} else {
|
|
// Else, reset back to the original left
|
|
targetLeft = mOriginalCapturedViewLeft;
|
|
}
|
|
|
|
if (mViewDragHelper.settleCapturedViewAt(targetLeft, child.getTop())) {
|
|
ViewCompat.postOnAnimation(child, new SettleRunnable(child, dismiss));
|
|
} else if (dismiss && mListener != null) {
|
|
mListener.onDismiss(child);
|
|
}
|
|
}
|
|
|
|
private boolean shouldDismiss(View child, float xvel) {
|
|
if (xvel != 0f) {
|
|
final boolean isRtl = ViewCompat.getLayoutDirection(child)
|
|
== ViewCompat.LAYOUT_DIRECTION_RTL;
|
|
|
|
if (mSwipeDirection == SWIPE_DIRECTION_ANY) {
|
|
// We don't care about the direction so return true
|
|
return true;
|
|
} else if (mSwipeDirection == SWIPE_DIRECTION_START_TO_END) {
|
|
// We only allow start-to-end swiping, so the fling needs to be in the
|
|
// correct direction
|
|
return isRtl ? xvel < 0f : xvel > 0f;
|
|
} else if (mSwipeDirection == SWIPE_DIRECTION_END_TO_START) {
|
|
// We only allow end-to-start swiping, so the fling needs to be in the
|
|
// correct direction
|
|
return isRtl ? xvel > 0f : xvel < 0f;
|
|
}
|
|
} else {
|
|
final int distance = child.getLeft() - mOriginalCapturedViewLeft;
|
|
final int thresholdDistance = Math.round(child.getWidth() * mDragDismissThreshold);
|
|
return Math.abs(distance) >= thresholdDistance;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public int getViewHorizontalDragRange(View child) {
|
|
return child.getWidth();
|
|
}
|
|
|
|
@Override
|
|
public int clampViewPositionHorizontal(View child, int left, int dx) {
|
|
final boolean isRtl = ViewCompat.getLayoutDirection(child)
|
|
== ViewCompat.LAYOUT_DIRECTION_RTL;
|
|
int min, max;
|
|
|
|
if (mSwipeDirection == SWIPE_DIRECTION_START_TO_END) {
|
|
if (isRtl) {
|
|
min = mOriginalCapturedViewLeft - child.getWidth();
|
|
max = mOriginalCapturedViewLeft;
|
|
} else {
|
|
min = mOriginalCapturedViewLeft;
|
|
max = mOriginalCapturedViewLeft + child.getWidth();
|
|
}
|
|
} else if (mSwipeDirection == SWIPE_DIRECTION_END_TO_START) {
|
|
if (isRtl) {
|
|
min = mOriginalCapturedViewLeft;
|
|
max = mOriginalCapturedViewLeft + child.getWidth();
|
|
} else {
|
|
min = mOriginalCapturedViewLeft - child.getWidth();
|
|
max = mOriginalCapturedViewLeft;
|
|
}
|
|
} else {
|
|
min = mOriginalCapturedViewLeft - child.getWidth();
|
|
max = mOriginalCapturedViewLeft + child.getWidth();
|
|
}
|
|
|
|
return clamp(min, left, max);
|
|
}
|
|
|
|
@Override
|
|
public int clampViewPositionVertical(View child, int top, int dy) {
|
|
return child.getTop();
|
|
}
|
|
|
|
@Override
|
|
public void onViewPositionChanged(View child, int left, int top, int dx, int dy) {
|
|
final float startAlphaDistance = mOriginalCapturedViewLeft
|
|
+ child.getWidth() * mAlphaStartSwipeDistance;
|
|
final float endAlphaDistance = mOriginalCapturedViewLeft
|
|
+ child.getWidth() * mAlphaEndSwipeDistance;
|
|
|
|
if (left <= startAlphaDistance) {
|
|
ViewCompat.setAlpha(child, 1f);
|
|
} else if (left >= endAlphaDistance) {
|
|
ViewCompat.setAlpha(child, 0f);
|
|
} else {
|
|
// We're between the start and end distances
|
|
final float distance = fraction(startAlphaDistance, endAlphaDistance, left);
|
|
ViewCompat.setAlpha(child, clamp(0f, 1f - distance, 1f));
|
|
}
|
|
}
|
|
};
|
|
|
|
private void ensureViewDragHelper(ViewGroup parent) {
|
|
if (mViewDragHelper == null) {
|
|
mViewDragHelper = mSensitivitySet
|
|
? ViewDragHelper.create(parent, mSensitivity, mDragCallback)
|
|
: ViewDragHelper.create(parent, mDragCallback);
|
|
}
|
|
}
|
|
|
|
private class SettleRunnable implements Runnable {
|
|
private final View mView;
|
|
private final boolean mDismiss;
|
|
|
|
SettleRunnable(View view, boolean dismiss) {
|
|
mView = view;
|
|
mDismiss = dismiss;
|
|
}
|
|
|
|
@Override
|
|
public void run() {
|
|
if (mViewDragHelper != null && mViewDragHelper.continueSettling(true)) {
|
|
ViewCompat.postOnAnimation(mView, this);
|
|
} else {
|
|
if (mDismiss && mListener != null) {
|
|
mListener.onDismiss(mView);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static float clamp(float min, float value, float max) {
|
|
return Math.min(Math.max(min, value), max);
|
|
}
|
|
|
|
private static int clamp(int min, int value, int max) {
|
|
return Math.min(Math.max(min, value), max);
|
|
}
|
|
|
|
/**
|
|
* Retrieve the current drag state of this behavior. This will return one of
|
|
* {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}.
|
|
*
|
|
* @return The current drag state
|
|
*/
|
|
public int getDragState() {
|
|
return mViewDragHelper != null ? mViewDragHelper.getViewDragState() : STATE_IDLE;
|
|
}
|
|
|
|
/**
|
|
* The fraction that {@code value} is between {@code startValue} and {@code endValue}.
|
|
*/
|
|
static float fraction(float startValue, float endValue, float value) {
|
|
return (value - startValue) / (endValue - startValue);
|
|
}
|
|
} |