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 androidx.wear.widget;
18 
19 import android.content.Context;
20 import android.util.AttributeSet;
21 
22 import androidx.annotation.UiThread;
23 
24 import java.util.ArrayList;
25 
26 /**
27  * A layout enabling left-to-right swipe-to-dismiss, intended for use within an activity.
28  *
29  * <p>At least one listener must be {@link #addCallback(Callback) added} to act on a dismissal
30  * action. A listener will typically remove a containing view or fragment from the current
31  * activity.
32  *
33  * <p>To suppress a swipe-dismiss gesture, at least one contained view must be scrollable,
34  * indicating that it would like to consume any horizontal touch gestures in that direction. In
35  * this  case this view will only allow swipe-to-dismiss on the very edge of the left-hand-side of
36  * the screen. If you wish to entirely disable the swipe-to-dismiss gesture,
37  * {@link #setSwipeable(boolean)} can be used for more direct control over the feature.
38  */
39 @UiThread
40 public class SwipeDismissFrameLayout extends DismissibleFrameLayout {
41 
42     public static final float DEFAULT_DISMISS_DRAG_WIDTH_RATIO = .33f;
43 
44 
45     /** Implement this callback to act on particular stages of the dismissal. */
46     @UiThread
47     public abstract static class Callback {
48 
49         /**
50          * Notifies listeners that the view is now being dragged as part of a dismiss gesture.
51          *
52          * @param layout The layout associated with this callback.
53          */
onSwipeStarted(SwipeDismissFrameLayout layout)54         public void onSwipeStarted(SwipeDismissFrameLayout layout) {
55         }
56 
57         /**
58          * Notifies listeners that the swipe gesture has ended without a dismissal.
59          *
60          * @param layout The layout associated with this callback.
61          */
onSwipeCanceled(SwipeDismissFrameLayout layout)62         public void onSwipeCanceled(SwipeDismissFrameLayout layout) {
63         }
64 
65         /**
66          * Notifies listeners that the dismissal is complete and the view is now off screen.
67          * @param layout The layout associated with this callback.
68          */
onDismissed(SwipeDismissFrameLayout layout)69         public void onDismissed(SwipeDismissFrameLayout layout) {
70         }
71     }
72 
73     final ArrayList<Callback> mCallbacksCompat = new ArrayList<>();
74 
75     /**
76      * Simple constructor to use when creating a view from code.
77      *
78      * @param context The {@link Context} the view is running in, through which it can access the
79      *                current theme, resources, etc.
80      */
SwipeDismissFrameLayout(Context context)81     public SwipeDismissFrameLayout(Context context) {
82         this(context, null, 0);
83     }
84 
85     /**
86      * Constructor that is called when inflating a view from XML. This is called when a view is
87      * being constructed from an XML file, supplying attributes that were specified in the XML file.
88      * This version uses a default style of 0, so the only attribute values applied are those in the
89      * Context's Theme and the given AttributeSet.
90      *
91      * <p>
92      *
93      * <p>The method onFinishInflate() will be called after all children have been added.
94      *
95      * @param context The {@link Context} the view is running in, through which it can access the
96      *                current theme, resources, etc.
97      * @param attrs   The attributes of the XML tag that is inflating the view.
98      */
SwipeDismissFrameLayout(Context context, AttributeSet attrs)99     public SwipeDismissFrameLayout(Context context, AttributeSet attrs) {
100         this(context, attrs, 0);
101     }
102 
103     /**
104      * Perform inflation from XML and apply a class-specific base style from a theme attribute.
105      * This constructor allows subclasses to use their own base style when they are inflating.
106      *
107      * @param context  The {@link Context} the view is running in, through which it can access the
108      *                 current theme, resources, etc.
109      * @param attrs    The attributes of the XML tag that is inflating the view.
110      * @param defStyle An attribute in the current theme that contains a reference to a style
111      *                 resource that supplies default values for the view. Can be 0 to not look for
112      *                 defaults.
113      */
SwipeDismissFrameLayout(Context context, AttributeSet attrs, int defStyle)114     public SwipeDismissFrameLayout(Context context, AttributeSet attrs, int defStyle) {
115         this(context, attrs, defStyle, 0);
116     }
117 
118     /**
119      * Perform inflation from XML and apply a class-specific base style from a theme attribute.
120      * This constructor allows subclasses to use their own base style when they are inflating.
121      *
122      * @param context  The {@link Context} the view is running in, through which it can access the
123      *                 current theme, resources, etc.
124      * @param attrs    The attributes of the XML tag that is inflating the view.
125      * @param defStyle An attribute in the current theme that contains a reference to a style
126      *                 resource that supplies default values for the view. Can be 0 to not look for
127      *                 defaults.
128      * @param defStyleRes It allows a style resource to be specified when creating the view.
129      */
SwipeDismissFrameLayout(Context context, AttributeSet attrs, int defStyle, int defStyleRes)130     public SwipeDismissFrameLayout(Context context, AttributeSet attrs, int defStyle,
131             int defStyleRes) {
132         super(context, attrs, defStyle, defStyleRes);
133     }
134 
135     /** Adds a callback for dismissal. */
addCallback(Callback callback)136     public void addCallback(Callback callback) {
137         if (callback == null) {
138             throw new NullPointerException("addCallback called with null callback");
139         }
140 
141         mCallbacksCompat.add(callback);
142     }
143 
144     /** Removes a callback that was added with {@link #addCallback(Callback)}. */
removeCallback(Callback callback)145     public void removeCallback(Callback callback) {
146         if (callback == null) {
147             throw new NullPointerException("removeCallback called with null callback");
148         }
149         if (!mCallbacksCompat.remove(callback)) {
150             throw new IllegalStateException("removeCallback called with nonexistent callback");
151         }
152     }
153 
154     /**
155      * Set the layout to be dismissible by swipe or not.
156      * @param swipeable Whether the layout should react to the swipe gesture.
157      */
setSwipeable(boolean swipeable)158     public void setSwipeable(boolean swipeable) {
159         super.setSwipeDismissible(swipeable);
160     }
161 
162     /** Returns true if the frame layout can be dismissed by swipe gestures. */
isSwipeable()163     public boolean isSwipeable() {
164         return super.isDismissableBySwipe();
165     }
166 
167     /**
168      * Sets the minimum ratio of the screen after which the swipe gesture is treated as
169      * swipe-to-dismiss.
170      *
171      * @param ratio the ratio of the screen at which the swipe gesture is treated as
172      *              swipe-to-dismiss. should be provided as a fraction of the screen
173      */
setDismissMinDragWidthRatio(float ratio)174     public void setDismissMinDragWidthRatio(float ratio) {
175         if (isSwipeable()) {
176             getSwipeDismissController().setDismissMinDragWidthRatio(ratio);
177         }
178     }
179 
180     /**
181      * Gets the minimum ratio of the screen after which the swipe gesture is treated as
182      * swipe-to-dismiss.
183      */
getDismissMinDragWidthRatio()184     public float getDismissMinDragWidthRatio() {
185         if (isSwipeable()) {
186             return getSwipeDismissController().getDismissMinDragWidthRatio();
187         }
188         return DEFAULT_DISMISS_DRAG_WIDTH_RATIO;
189     }
190 
191     @Override
performDismissFinishedCallbacks()192     protected void performDismissFinishedCallbacks() {
193         super.performDismissFinishedCallbacks();
194         for (int i = mCallbacksCompat.size() - 1; i >= 0; i--) {
195             mCallbacksCompat.get(i).onDismissed(this);
196         }
197     }
198 
199     @Override
performDismissStartedCallbacks()200     protected void performDismissStartedCallbacks() {
201         super.performDismissStartedCallbacks();
202         for (int i = mCallbacksCompat.size() - 1; i >= 0; i--) {
203             mCallbacksCompat.get(i).onSwipeStarted(this);
204         }
205     }
206 
207     @Override
performDismissCanceledCallbacks()208     protected void performDismissCanceledCallbacks() {
209         super.performDismissCanceledCallbacks();
210         for (int i = mCallbacksCompat.size() - 1; i >= 0; i--) {
211             mCallbacksCompat.get(i).onSwipeCanceled(this);
212         }
213     }
214 }