1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.support.wear.widget; 18 19 import android.content.Context; 20 import android.support.annotation.UiThread; 21 import android.util.AttributeSet; 22 import android.util.Log; 23 import android.view.View; 24 import android.view.animation.AccelerateInterpolator; 25 import android.view.animation.DecelerateInterpolator; 26 27 import java.util.ArrayList; 28 29 /** 30 * A layout enabling left-to-right swipe-to-dismiss, intended for use within an activity. 31 * 32 * <p>At least one listener must be {@link #addCallback(Callback) added} to act on a dismissal 33 * action. A listener will typically remove a containing view or fragment from the current 34 * activity. 35 * 36 * <p>To suppress a swipe-dismiss gesture, at least one contained view must be scrollable, 37 * indicating that it would like to consume any horizontal touch gestures in that direction. In 38 * this case this view will only allow swipe-to-dismiss on the very edge of the left-hand-side of 39 * the screen. If you wish to entirely disable the swipe-to-dismiss gesture, 40 * {@link #setSwipeable(boolean)} can be used for more direct control over the feature. 41 */ 42 @UiThread 43 public class SwipeDismissFrameLayout extends SwipeDismissLayout { 44 45 private static final String TAG = "SwipeDismissFrameLayout"; 46 47 private static final float TRANSLATION_MIN_ALPHA = 0.5f; 48 private static final float DEFAULT_INTERPOLATION_FACTOR = 1.5f; 49 50 /** Implement this callback to act on particular stages of the dismissal. */ 51 @UiThread 52 public abstract static class Callback { 53 /** 54 * Notifies listeners that the view is now considering to start a dismiss gesture from a 55 * particular point on the screen. The default implementation returns true for all 56 * coordinates so that is is possible to start a swipe-to-dismiss gesture from any location. 57 * If any one instance of this Callback returns false for a given set of coordinates, 58 * swipe-to-dismiss will not be allowed to start in that point. 59 * 60 * @param layout The layout associated with this callback. 61 * @param xDown The x coordinate of the initial {@link android.view.MotionEvent#ACTION_DOWN} 62 * event for this motion. 63 * @param yDown The y coordinate of the initial {@link android.view.MotionEvent#ACTION_DOWN} 64 * event for this motion. 65 * @return true if this gesture should be recognized as a swipe to dismiss gesture, false 66 * otherwise. 67 */ onPreSwipeStart(SwipeDismissFrameLayout layout, float xDown, float yDown)68 boolean onPreSwipeStart(SwipeDismissFrameLayout layout, float xDown, float yDown) { 69 return true; 70 } 71 72 /** 73 * Notifies listeners that the view is now being dragged as part of a dismiss gesture. 74 * 75 * @param layout The layout associated with this callback. 76 */ onSwipeStarted(SwipeDismissFrameLayout layout)77 public void onSwipeStarted(SwipeDismissFrameLayout layout) { 78 } 79 80 /** 81 * Notifies listeners that the swipe gesture has ended without a dismissal. 82 * 83 * @param layout The layout associated with this callback. 84 */ onSwipeCanceled(SwipeDismissFrameLayout layout)85 public void onSwipeCanceled(SwipeDismissFrameLayout layout) { 86 } 87 88 /** 89 * Notifies listeners the dismissal is complete and the view now off screen. 90 * 91 * @param layout The layout associated with this callback. 92 */ onDismissed(SwipeDismissFrameLayout layout)93 public void onDismissed(SwipeDismissFrameLayout layout) { 94 } 95 } 96 97 private final OnPreSwipeListener mOnPreSwipeListener = new MyOnPreSwipeListener(); 98 private final OnDismissedListener mOnDismissedListener = new MyOnDismissedListener(); 99 100 private final OnSwipeProgressChangedListener mOnSwipeProgressListener = 101 new MyOnSwipeProgressChangedListener(); 102 103 private final ArrayList<Callback> mCallbacks = new ArrayList<>(); 104 private final int mAnimationTime; 105 private final DecelerateInterpolator mCancelInterpolator; 106 private final AccelerateInterpolator mDismissInterpolator; 107 private final DecelerateInterpolator mCompleteDismissGestureInterpolator; 108 109 private boolean mStarted; 110 111 /** 112 * Simple constructor to use when creating a view from code. 113 * 114 * @param context The {@link Context} the view is running in, through which it can access the 115 * current theme, resources, etc. 116 */ SwipeDismissFrameLayout(Context context)117 public SwipeDismissFrameLayout(Context context) { 118 this(context, null, 0); 119 } 120 121 /** 122 * Constructor that is called when inflating a view from XML. This is called when a view is 123 * being constructed from an XML file, supplying attributes that were specified in the XML file. 124 * This version uses a default style of 0, so the only attribute values applied are those in the 125 * Context's Theme and the given AttributeSet. 126 * 127 * <p> 128 * 129 * <p>The method onFinishInflate() will be called after all children have been added. 130 * 131 * @param context The {@link Context} the view is running in, through which it can access the 132 * current theme, resources, etc. 133 * @param attrs The attributes of the XML tag that is inflating the view. 134 */ SwipeDismissFrameLayout(Context context, AttributeSet attrs)135 public SwipeDismissFrameLayout(Context context, AttributeSet attrs) { 136 this(context, attrs, 0); 137 } 138 139 /** 140 * Perform inflation from XML and apply a class-specific base style from a theme attribute. 141 * This constructor allows subclasses to use their own base style when they are inflating. 142 * 143 * @param context The {@link Context} the view is running in, through which it can access the 144 * current theme, resources, etc. 145 * @param attrs The attributes of the XML tag that is inflating the view. 146 * @param defStyle An attribute in the current theme that contains a reference to a style 147 * resource that supplies default values for the view. Can be 0 to not look for 148 * defaults. 149 */ SwipeDismissFrameLayout(Context context, AttributeSet attrs, int defStyle)150 public SwipeDismissFrameLayout(Context context, AttributeSet attrs, int defStyle) { 151 this(context, attrs, defStyle, 0); 152 } 153 154 /** 155 * Perform inflation from XML and apply a class-specific base style from a theme attribute. 156 * This constructor allows subclasses to use their own base style when they are inflating. 157 * 158 * @param context The {@link Context} the view is running in, through which it can access the 159 * current theme, resources, etc. 160 * @param attrs The attributes of the XML tag that is inflating the view. 161 * @param defStyle An attribute in the current theme that contains a reference to a style 162 * resource that supplies default values for the view. Can be 0 to not look for 163 * defaults. 164 * @param defStyleRes This corresponds to the fourth argument 165 * of {@link View#View(Context, AttributeSet, int, int)}. It allows a style 166 * resource to be specified when creating the view. 167 */ SwipeDismissFrameLayout(Context context, AttributeSet attrs, int defStyle, int defStyleRes)168 public SwipeDismissFrameLayout(Context context, AttributeSet attrs, int defStyle, 169 int defStyleRes) { 170 super(context, attrs, defStyle, defStyleRes); 171 setOnPreSwipeListener(mOnPreSwipeListener); 172 setOnDismissedListener(mOnDismissedListener); 173 setOnSwipeProgressChangedListener(mOnSwipeProgressListener); 174 mAnimationTime = getContext().getResources().getInteger( 175 android.R.integer.config_shortAnimTime); 176 mCancelInterpolator = new DecelerateInterpolator(DEFAULT_INTERPOLATION_FACTOR); 177 mDismissInterpolator = new AccelerateInterpolator(DEFAULT_INTERPOLATION_FACTOR); 178 mCompleteDismissGestureInterpolator = new DecelerateInterpolator( 179 DEFAULT_INTERPOLATION_FACTOR); 180 } 181 182 /** Adds a callback for dismissal. */ addCallback(Callback callback)183 public void addCallback(Callback callback) { 184 if (callback == null) { 185 throw new NullPointerException("addCallback called with null callback"); 186 } 187 mCallbacks.add(callback); 188 } 189 190 /** Removes a callback that was added with {@link #addCallback(Callback)}. */ removeCallback(Callback callback)191 public void removeCallback(Callback callback) { 192 if (callback == null) { 193 throw new NullPointerException("removeCallback called with null callback"); 194 } 195 if (!mCallbacks.remove(callback)) { 196 throw new IllegalStateException("removeCallback called with nonexistent callback"); 197 } 198 } 199 200 /** 201 * Resets this view to the original state. This method cancels any pending animations on this 202 * view and resets the alpha as well as x translation values. 203 */ resetTranslationAndAlpha()204 private void resetTranslationAndAlpha() { 205 animate().cancel(); 206 setTranslationX(0); 207 setAlpha(1); 208 mStarted = false; 209 } 210 211 private final class MyOnPreSwipeListener implements OnPreSwipeListener { 212 213 @Override onPreSwipe(SwipeDismissLayout layout, float xDown, float yDown)214 public boolean onPreSwipe(SwipeDismissLayout layout, float xDown, float yDown) { 215 for (Callback callback : mCallbacks) { 216 if (!callback.onPreSwipeStart(SwipeDismissFrameLayout.this, xDown, yDown)) { 217 return false; 218 } 219 } 220 return true; 221 } 222 } 223 224 private final class MyOnDismissedListener implements OnDismissedListener { 225 226 @Override onDismissed(SwipeDismissLayout layout)227 public void onDismissed(SwipeDismissLayout layout) { 228 if (Log.isLoggable(TAG, Log.DEBUG)) { 229 Log.d(TAG, "onDismissed()"); 230 } 231 animate() 232 .translationX(getWidth()) 233 .alpha(0) 234 .setDuration(mAnimationTime) 235 .setInterpolator( 236 mStarted ? mCompleteDismissGestureInterpolator : mDismissInterpolator) 237 .withEndAction( 238 new Runnable() { 239 @Override 240 public void run() { 241 for (int i = mCallbacks.size() - 1; i >= 0; i--) { 242 Callback callbacks = mCallbacks.get(i); 243 callbacks.onDismissed(SwipeDismissFrameLayout.this); 244 } 245 resetTranslationAndAlpha(); 246 } 247 }); 248 } 249 } 250 251 private final class MyOnSwipeProgressChangedListener implements OnSwipeProgressChangedListener { 252 253 @Override onSwipeProgressChanged(SwipeDismissLayout layout, float progress, float translate)254 public void onSwipeProgressChanged(SwipeDismissLayout layout, float progress, 255 float translate) { 256 if (Log.isLoggable(TAG, Log.DEBUG)) { 257 Log.d(TAG, "onSwipeProgressChanged() - " + translate); 258 } 259 setTranslationX(translate); 260 setAlpha(1 - (progress * TRANSLATION_MIN_ALPHA)); 261 if (!mStarted) { 262 for (int i = mCallbacks.size() - 1; i >= 0; i--) { 263 Callback callbacks = mCallbacks.get(i); 264 callbacks.onSwipeStarted(SwipeDismissFrameLayout.this); 265 } 266 mStarted = true; 267 } 268 } 269 270 @Override onSwipeCanceled(SwipeDismissLayout layout)271 public void onSwipeCanceled(SwipeDismissLayout layout) { 272 if (Log.isLoggable(TAG, Log.DEBUG)) { 273 Log.d(TAG, "onSwipeCanceled() run swipe cancel animation"); 274 } 275 mStarted = false; 276 animate() 277 .translationX(0) 278 .alpha(1) 279 .setDuration(mAnimationTime) 280 .setInterpolator(mCancelInterpolator) 281 .withEndAction( 282 new Runnable() { 283 @Override 284 public void run() { 285 for (int i = mCallbacks.size() - 1; i >= 0; i--) { 286 Callback callbacks = mCallbacks.get(i); 287 callbacks.onSwipeCanceled(SwipeDismissFrameLayout.this); 288 } 289 resetTranslationAndAlpha(); 290 } 291 }); 292 } 293 } 294 } 295