blob: c2b595de8fc1a0708d978330d65e42be19256c76 [file] [log] [blame]
/*
* Copyright (C) 2017 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.wear.widget;
import android.content.Context;
import android.support.annotation.UiThread;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import java.util.ArrayList;
/**
* A layout enabling left-to-right swipe-to-dismiss, intended for use within an activity.
*
* <p>At least one listener must be {@link #addCallback(Callback) added} to act on a dismissal
* action. A listener will typically remove a containing view or fragment from the current
* activity.
*
* <p>To suppress a swipe-dismiss gesture, at least one contained view must be scrollable,
* indicating that it would like to consume any horizontal touch gestures in that direction. In
* this case this view will only allow swipe-to-dismiss on the very edge of the left-hand-side of
* the screen. If you wish to entirely disable the swipe-to-dismiss gesture,
* {@link #setSwipeable(boolean)} can be used for more direct control over the feature.
*/
@UiThread
public class SwipeDismissFrameLayout extends SwipeDismissLayout {
private static final String TAG = "SwipeDismissFrameLayout";
private static final float TRANSLATION_MIN_ALPHA = 0.5f;
private static final float DEFAULT_INTERPOLATION_FACTOR = 1.5f;
/** Implement this callback to act on particular stages of the dismissal. */
@UiThread
public abstract static class Callback {
/**
* Notifies listeners that the view is now considering to start a dismiss gesture from a
* particular point on the screen. The default implementation returns true for all
* coordinates so that is is possible to start a swipe-to-dismiss gesture from any location.
* If any one instance of this Callback returns false for a given set of coordinates,
* swipe-to-dismiss will not be allowed to start in that point.
*
* @param layout The layout associated with this callback.
* @param xDown The x coordinate of the initial {@link android.view.MotionEvent#ACTION_DOWN}
* event for this motion.
* @param yDown The y coordinate of the initial {@link android.view.MotionEvent#ACTION_DOWN}
* event for this motion.
* @return true if this gesture should be recognized as a swipe to dismiss gesture, false
* otherwise.
*/
boolean onPreSwipeStart(SwipeDismissFrameLayout layout, float xDown, float yDown) {
return true;
}
/**
* Notifies listeners that the view is now being dragged as part of a dismiss gesture.
*
* @param layout The layout associated with this callback.
*/
public void onSwipeStarted(SwipeDismissFrameLayout layout) {
}
/**
* Notifies listeners that the swipe gesture has ended without a dismissal.
*
* @param layout The layout associated with this callback.
*/
public void onSwipeCanceled(SwipeDismissFrameLayout layout) {
}
/**
* Notifies listeners the dismissal is complete and the view now off screen.
*
* @param layout The layout associated with this callback.
*/
public void onDismissed(SwipeDismissFrameLayout layout) {
}
}
private final OnPreSwipeListener mOnPreSwipeListener = new MyOnPreSwipeListener();
private final OnDismissedListener mOnDismissedListener = new MyOnDismissedListener();
private final OnSwipeProgressChangedListener mOnSwipeProgressListener =
new MyOnSwipeProgressChangedListener();
private final ArrayList<Callback> mCallbacks = new ArrayList<>();
private final int mAnimationTime;
private final DecelerateInterpolator mCancelInterpolator;
private final AccelerateInterpolator mDismissInterpolator;
private final DecelerateInterpolator mCompleteDismissGestureInterpolator;
private boolean mStarted;
/**
* Simple constructor to use when creating a view from code.
*
* @param context The {@link Context} the view is running in, through which it can access the
* current theme, resources, etc.
*/
public SwipeDismissFrameLayout(Context context) {
this(context, null, 0);
}
/**
* Constructor that is called when inflating a view from XML. This is called when a view is
* being constructed from an XML file, supplying attributes that were specified in the XML file.
* This version uses a default style of 0, so the only attribute values applied are those in the
* Context's Theme and the given AttributeSet.
*
* <p>
*
* <p>The method onFinishInflate() will be called after all children have been added.
*
* @param context The {@link Context} the view is running in, through which it can access the
* current theme, resources, etc.
* @param attrs The attributes of the XML tag that is inflating the view.
*/
public SwipeDismissFrameLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
/**
* Perform inflation from XML and apply a class-specific base style from a theme attribute.
* This constructor allows subclasses to use their own base style when they are inflating.
*
* @param context The {@link Context} the view is running in, through which it can access the
* current theme, resources, etc.
* @param attrs The attributes of the XML tag that is inflating the view.
* @param defStyle An attribute in the current theme that contains a reference to a style
* resource that supplies default values for the view. Can be 0 to not look for
* defaults.
*/
public SwipeDismissFrameLayout(Context context, AttributeSet attrs, int defStyle) {
this(context, attrs, defStyle, 0);
}
/**
* Perform inflation from XML and apply a class-specific base style from a theme attribute.
* This constructor allows subclasses to use their own base style when they are inflating.
*
* @param context The {@link Context} the view is running in, through which it can access the
* current theme, resources, etc.
* @param attrs The attributes of the XML tag that is inflating the view.
* @param defStyle An attribute in the current theme that contains a reference to a style
* resource that supplies default values for the view. Can be 0 to not look for
* defaults.
* @param defStyleRes This corresponds to the fourth argument
* of {@link View#View(Context, AttributeSet, int, int)}. It allows a style
* resource to be specified when creating the view.
*/
public SwipeDismissFrameLayout(Context context, AttributeSet attrs, int defStyle,
int defStyleRes) {
super(context, attrs, defStyle, defStyleRes);
setOnPreSwipeListener(mOnPreSwipeListener);
setOnDismissedListener(mOnDismissedListener);
setOnSwipeProgressChangedListener(mOnSwipeProgressListener);
mAnimationTime = getContext().getResources().getInteger(
android.R.integer.config_shortAnimTime);
mCancelInterpolator = new DecelerateInterpolator(DEFAULT_INTERPOLATION_FACTOR);
mDismissInterpolator = new AccelerateInterpolator(DEFAULT_INTERPOLATION_FACTOR);
mCompleteDismissGestureInterpolator = new DecelerateInterpolator(
DEFAULT_INTERPOLATION_FACTOR);
}
/** Adds a callback for dismissal. */
public void addCallback(Callback callback) {
if (callback == null) {
throw new NullPointerException("addCallback called with null callback");
}
mCallbacks.add(callback);
}
/** Removes a callback that was added with {@link #addCallback(Callback)}. */
public void removeCallback(Callback callback) {
if (callback == null) {
throw new NullPointerException("removeCallback called with null callback");
}
if (!mCallbacks.remove(callback)) {
throw new IllegalStateException("removeCallback called with nonexistent callback");
}
}
/**
* Resets this view to the original state. This method cancels any pending animations on this
* view and resets the alpha as well as x translation values.
*/
private void resetTranslationAndAlpha() {
animate().cancel();
setTranslationX(0);
setAlpha(1);
mStarted = false;
}
private final class MyOnPreSwipeListener implements OnPreSwipeListener {
@Override
public boolean onPreSwipe(SwipeDismissLayout layout, float xDown, float yDown) {
for (Callback callback : mCallbacks) {
if (!callback.onPreSwipeStart(SwipeDismissFrameLayout.this, xDown, yDown)) {
return false;
}
}
return true;
}
}
private final class MyOnDismissedListener implements OnDismissedListener {
@Override
public void onDismissed(SwipeDismissLayout layout) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onDismissed()");
}
animate()
.translationX(getWidth())
.alpha(0)
.setDuration(mAnimationTime)
.setInterpolator(
mStarted ? mCompleteDismissGestureInterpolator : mDismissInterpolator)
.withEndAction(
new Runnable() {
@Override
public void run() {
for (int i = mCallbacks.size() - 1; i >= 0; i--) {
Callback callbacks = mCallbacks.get(i);
callbacks.onDismissed(SwipeDismissFrameLayout.this);
}
resetTranslationAndAlpha();
}
});
}
}
private final class MyOnSwipeProgressChangedListener implements OnSwipeProgressChangedListener {
@Override
public void onSwipeProgressChanged(SwipeDismissLayout layout, float progress,
float translate) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onSwipeProgressChanged() - " + translate);
}
setTranslationX(translate);
setAlpha(1 - (progress * TRANSLATION_MIN_ALPHA));
if (!mStarted) {
for (int i = mCallbacks.size() - 1; i >= 0; i--) {
Callback callbacks = mCallbacks.get(i);
callbacks.onSwipeStarted(SwipeDismissFrameLayout.this);
}
mStarted = true;
}
}
@Override
public void onSwipeCanceled(SwipeDismissLayout layout) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onSwipeCanceled() run swipe cancel animation");
}
mStarted = false;
animate()
.translationX(0)
.alpha(1)
.setDuration(mAnimationTime)
.setInterpolator(mCancelInterpolator)
.withEndAction(
new Runnable() {
@Override
public void run() {
for (int i = mCallbacks.size() - 1; i >= 0; i--) {
Callback callbacks = mCallbacks.get(i);
callbacks.onSwipeCanceled(SwipeDismissFrameLayout.this);
}
resetTranslationAndAlpha();
}
});
}
}
}