dataSet) {
30 | mContext = context;
31 | mDataSet = dataSet;
32 | mInflater = LayoutInflater.from(context);
33 |
34 | // uncomment if you want to open only one row at a time
35 | // binderHelper.setOpenOnlyOne(true);
36 | }
37 |
38 | @Override
39 | public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
40 | View view = mInflater.inflate(R.layout.row_list, parent, false);
41 | return new ViewHolder(view);
42 | }
43 |
44 | @Override
45 | public void onBindViewHolder(RecyclerView.ViewHolder h, int position) {
46 | final ViewHolder holder = (ViewHolder) h;
47 |
48 | if (mDataSet != null && 0 <= position && position < mDataSet.size()) {
49 | final String data = mDataSet.get(position);
50 |
51 | // Use ViewBindHelper to restore and save the open/close state of the SwipeRevealView
52 | // put an unique string id as value, can be any string which uniquely define the data
53 | binderHelper.bind(holder.swipeLayout, data);
54 |
55 | // Bind your data here
56 | holder.bind(data);
57 | }
58 | }
59 |
60 | @Override
61 | public int getItemCount() {
62 | if (mDataSet == null)
63 | return 0;
64 | return mDataSet.size();
65 | }
66 |
67 | /**
68 | * Only if you need to restore open/close state when the orientation is changed.
69 | * Call this method in {@link android.app.Activity#onSaveInstanceState(Bundle)}
70 | */
71 | public void saveStates(Bundle outState) {
72 | binderHelper.saveStates(outState);
73 | }
74 |
75 | /**
76 | * Only if you need to restore open/close state when the orientation is changed.
77 | * Call this method in {@link android.app.Activity#onRestoreInstanceState(Bundle)}
78 | */
79 | public void restoreStates(Bundle inState) {
80 | binderHelper.restoreStates(inState);
81 | }
82 |
83 | private class ViewHolder extends RecyclerView.ViewHolder {
84 | private SwipeRevealLayout swipeLayout;
85 | private View frontLayout;
86 | private View deleteLayout;
87 | private TextView textView;
88 |
89 | public ViewHolder(View itemView) {
90 | super(itemView);
91 | swipeLayout = (SwipeRevealLayout) itemView.findViewById(R.id.swipe_layout);
92 | frontLayout = itemView.findViewById(R.id.front_layout);
93 | deleteLayout = itemView.findViewById(R.id.delete_layout);
94 | textView = (TextView) itemView.findViewById(R.id.text);
95 | }
96 |
97 | public void bind(final String data) {
98 | deleteLayout.setOnClickListener(new View.OnClickListener() {
99 | @Override
100 | public void onClick(View v) {
101 | mDataSet.remove(getAdapterPosition());
102 | notifyItemRemoved(getAdapterPosition());
103 | }
104 | });
105 |
106 | textView.setText(data);
107 |
108 | frontLayout.setOnClickListener(new View.OnClickListener() {
109 | @Override
110 | public void onClick(View view) {
111 | String displayText = "" + data + " clicked";
112 | Toast.makeText(mContext, displayText, Toast.LENGTH_SHORT).show();
113 | Log.d("RecyclerAdapter", displayText);
114 | }
115 | });
116 | }
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## SwipeRevealLayout
2 | A layout that you can swipe/slide to show another layout.
3 |
4 | ### Demo
5 | ##### Overview
6 | 
7 |
8 | ##### Drag mode
9 |
10 | Drag mode normal:
11 | 
12 |
13 | Drag mode same_level:
14 | 
15 |
16 | ### Features
17 | * Flexible, easy to use with RecyclerView, ListView or any view that requires view binding.
18 | * Four drag edges (left, right, top, bottom).
19 | * Two drag modes:
20 | * Normal (the secondary view is underneath the main view).
21 | * Same level (the secondary view sticks to the edge of the main view).
22 | * Able to open one row at a time.
23 | * Minimum api level 9.
24 |
25 | ### Usage
26 | #### Dependencies
27 | ```groovy
28 | dependencies {
29 | compile 'com.chauthai.swipereveallayout:swipe-reveal-layout:1.4.1'
30 | }
31 | ```
32 |
33 | #### Layout file
34 | ```xml
35 |
40 |
41 |
42 |
45 |
46 |
47 |
50 |
51 |
52 | ```
53 | ```app:mode``` can be ```normal``` or ```same_level```
54 |
55 | ```app:dragEdge``` can be ```left```, ```right```, ```top``` or ```bottom```
56 |
57 | #### Use with RecyclerView, ListView, GridView...
58 | ##### In your Adapter class:
59 | ```java
60 | public class Adapter extends RecyclerView.Adapter {
61 | // This object helps you save/restore the open/close state of each view
62 | private final ViewBinderHelper viewBinderHelper = new ViewBinderHelper();
63 |
64 | public Adapter() {
65 | // uncomment the line below if you want to open only one row at a time
66 | // viewBinderHelper.setOpenOnlyOne(true);
67 | }
68 |
69 | @Override
70 | public void onBindViewHolder(ViewHolder holder, int position) {
71 | // get your data object first.
72 | YourDataObject dataObject = mDataSet.get(position);
73 |
74 | // Save/restore the open/close state.
75 | // You need to provide a String id which uniquely defines the data object.
76 | viewBinderHelper.bind(holder.swipeRevealLayout, dataObject.getId());
77 |
78 | // do your regular binding stuff here
79 | }
80 | }
81 | ```
82 |
83 | ##### Optional, to restore/save the open/close state when the device's orientation is changed:
84 | ##### Adapter class:
85 | ```java
86 | public class YourAdapter extends RecyclerView.Adapter {
87 | ...
88 |
89 | public void saveStates(Bundle outState) {
90 | viewBinderHelper.saveStates(outState);
91 | }
92 |
93 | public void restoreStates(Bundle inState) {
94 | viewBinderHelper.restoreStates(inState);
95 | }
96 | }
97 | ```
98 | ##### Activity class:
99 | ```java
100 | public class YourActivity extends Activity {
101 | ...
102 |
103 | @Override
104 | protected void onSaveInstanceState(Bundle outState) {
105 | super.onSaveInstanceState(outState);
106 | if (adapter != null) {
107 | adapter.saveStates(outState);
108 | }
109 | }
110 |
111 | @Override
112 | protected void onRestoreInstanceState(Bundle savedInstanceState) {
113 | super.onRestoreInstanceState(savedInstanceState);
114 | if (adapter != null) {
115 | adapter.restoreStates(savedInstanceState);
116 | }
117 | }
118 | }
119 | ```
120 |
121 | #### Useful Methods/Attributes
122 | ```app:minDistRequestDisallowParent```: The minimum distance (in px or dp) to the closest drag edge that the SwipeRevealLayout will disallow the parent to intercept touch event. It basically means the minimum distance to swipe until a RecyclerView (or something similar) cannot be scrolled.
123 |
124 | ```setSwipeListener(SwipeListener swipeListener)```: set the listener for the layout. You can use the full interface ```SwipeListener``` or a simplified listener class ```SimpleSwipeListener```
125 |
126 | ```open(boolean animation)```, ```close(boolean animation)```: open/close the layout. If ```animation``` is set to false, the listener will not be called.
127 |
128 | ```isOpened()```, ```isClosed()```: check if the layout is fully opened or closed.
129 |
130 | ```setMinFlingVelocity(int velocity)```: set the minimum fling velocity (dp/sec) to cause the layout to open/close.
131 |
132 | ```setDragEdge(int edge)```: Change the edge where the layout can be dragged from.
133 |
134 | ```setLockDrag(boolean lock)```: If set to true, the user cannot drag/swipe the layout.
135 |
136 | ```viewBinderHelper.lockSwipe(String... id), viewBinderHelper.unlockSwipe(String... id)```: Lock/unlock layouts which are binded to the binderHelper.
137 |
138 | ```viewBinderHelper.setOpenOnlyOne(boolean openOnlyOne)```: If ```openOnlyOne``` is set to true, you can only open one row at a time.
139 |
140 | ```viewBinderHelper.openLayout(String id)```: Open a layout. ```id``` is the id of the data object which is bind to the layout.
141 |
142 | ```viewBinderHelper.closeLayout(String id)```: Close a layout. ```id``` is the id of the data object which is bind to the layout.
143 |
144 | ### License
145 | ```
146 | The MIT License (MIT)
147 |
148 | Copyright (c) 2016 Chau Thai
149 |
150 | Permission is hereby granted, free of charge, to any person obtaining a copy
151 | of this software and associated documentation files (the "Software"), to deal
152 | in the Software without restriction, including without limitation the rights
153 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
154 | copies of the Software, and to permit persons to whom the Software is
155 | furnished to do so, subject to the following conditions:
156 |
157 | The above copyright notice and this permission notice shall be included in all
158 | copies or substantial portions of the Software.
159 |
160 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
161 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
162 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
163 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
164 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
165 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
166 | SOFTWARE.
167 | ```
168 |
--------------------------------------------------------------------------------
/demo/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
19 |
20 |
28 |
29 |
33 |
34 |
42 |
43 |
51 |
52 |
53 |
58 |
59 |
65 |
66 |
67 |
68 |
69 |
78 |
79 |
83 |
84 |
92 |
93 |
101 |
102 |
103 |
108 |
109 |
115 |
116 |
117 |
118 |
127 |
128 |
133 |
134 |
139 |
140 |
141 |
146 |
147 |
153 |
154 |
155 |
156 |
165 |
166 |
171 |
172 |
177 |
178 |
179 |
184 |
185 |
191 |
192 |
193 |
194 |
--------------------------------------------------------------------------------
/swipe-reveal-layout/src/main/java/com/chauthai/swipereveallayout/ViewBinderHelper.java:
--------------------------------------------------------------------------------
1 | /**
2 | The MIT License (MIT)
3 |
4 | Copyright (c) 2016 Chau Thai
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | */
24 |
25 | package com.chauthai.swipereveallayout;
26 |
27 | import android.os.Bundle;
28 |
29 | import java.util.Arrays;
30 | import java.util.Collections;
31 | import java.util.HashMap;
32 | import java.util.HashSet;
33 | import java.util.Map;
34 | import java.util.Set;
35 |
36 | /**
37 | * ViewBinderHelper provides a quick and easy solution to restore the open/close state
38 | * of the items in RecyclerView, ListView, GridView or any view that requires its child view
39 | * to bind the view to a data object.
40 | *
41 | * When you bind you data object to a view, use {@link #bind(SwipeRevealLayout, String)} to
42 | * save and restore the open/close state of the view.
43 | *
44 | * Optionally, if you also want to save and restore the open/close state when the device's
45 | * orientation is changed, call {@link #saveStates(Bundle)} in {@link android.app.Activity#onSaveInstanceState(Bundle)}
46 | * and {@link #restoreStates(Bundle)} in {@link android.app.Activity#onRestoreInstanceState(Bundle)}
47 | */
48 | public class ViewBinderHelper {
49 | private static final String BUNDLE_MAP_KEY = "ViewBinderHelper_Bundle_Map_Key";
50 |
51 | private Map mapStates = Collections.synchronizedMap(new HashMap());
52 | private Map mapLayouts = Collections.synchronizedMap(new HashMap());
53 | private Set lockedSwipeSet = Collections.synchronizedSet(new HashSet());
54 |
55 | private volatile boolean openOnlyOne = false;
56 | private final Object stateChangeLock = new Object();
57 |
58 | /**
59 | * Help to save and restore open/close state of the swipeLayout. Call this method
60 | * when you bind your view holder with the data object.
61 | *
62 | * @param swipeLayout swipeLayout of the current view.
63 | * @param id a string that uniquely defines the data object of the current view.
64 | */
65 | public void bind(final SwipeRevealLayout swipeLayout, final String id) {
66 | if (swipeLayout.shouldRequestLayout()) {
67 | swipeLayout.requestLayout();
68 | }
69 |
70 | mapLayouts.values().remove(swipeLayout);
71 | mapLayouts.put(id, swipeLayout);
72 |
73 | swipeLayout.abort();
74 | swipeLayout.setDragStateChangeListener(new SwipeRevealLayout.DragStateChangeListener() {
75 | @Override
76 | public void onDragStateChanged(int state) {
77 | mapStates.put(id, state);
78 |
79 | if (openOnlyOne) {
80 | closeOthers(id, swipeLayout);
81 | }
82 | }
83 | });
84 |
85 | // first time binding.
86 | if (!mapStates.containsKey(id)) {
87 | mapStates.put(id, SwipeRevealLayout.STATE_CLOSE);
88 | swipeLayout.close(false);
89 | }
90 |
91 | // not the first time, then close or open depends on the current state.
92 | else {
93 | int state = mapStates.get(id);
94 |
95 | if (state == SwipeRevealLayout.STATE_CLOSE || state == SwipeRevealLayout.STATE_CLOSING ||
96 | state == SwipeRevealLayout.STATE_DRAGGING) {
97 | swipeLayout.close(false);
98 | } else {
99 | swipeLayout.open(false);
100 | }
101 | }
102 |
103 | // set lock swipe
104 | swipeLayout.setLockDrag(lockedSwipeSet.contains(id));
105 | }
106 |
107 | /**
108 | * Only if you need to restore open/close state when the orientation is changed.
109 | * Call this method in {@link android.app.Activity#onSaveInstanceState(Bundle)}
110 | */
111 | public void saveStates(Bundle outState) {
112 | if (outState == null)
113 | return;
114 |
115 | Bundle statesBundle = new Bundle();
116 | for (Map.Entry entry : mapStates.entrySet()) {
117 | statesBundle.putInt(entry.getKey(), entry.getValue());
118 | }
119 |
120 | outState.putBundle(BUNDLE_MAP_KEY, statesBundle);
121 | }
122 |
123 |
124 | /**
125 | * Only if you need to restore open/close state when the orientation is changed.
126 | * Call this method in {@link android.app.Activity#onRestoreInstanceState(Bundle)}
127 | */
128 | @SuppressWarnings({"unchecked", "ConstantConditions"})
129 | public void restoreStates(Bundle inState) {
130 | if (inState == null)
131 | return;
132 |
133 | if (inState.containsKey(BUNDLE_MAP_KEY)) {
134 | HashMap restoredMap = new HashMap<>();
135 |
136 | Bundle statesBundle = inState.getBundle(BUNDLE_MAP_KEY);
137 | Set keySet = statesBundle.keySet();
138 |
139 | if (keySet != null) {
140 | for (String key : keySet) {
141 | restoredMap.put(key, statesBundle.getInt(key));
142 | }
143 | }
144 |
145 | mapStates = restoredMap;
146 | }
147 | }
148 |
149 | /**
150 | * Lock swipe for some layouts.
151 | * @param id a string that uniquely defines the data object.
152 | */
153 | public void lockSwipe(String... id) {
154 | setLockSwipe(true, id);
155 | }
156 |
157 | /**
158 | * Unlock swipe for some layouts.
159 | * @param id a string that uniquely defines the data object.
160 | */
161 | public void unlockSwipe(String... id) {
162 | setLockSwipe(false, id);
163 | }
164 |
165 | /**
166 | * @param openOnlyOne If set to true, then only one row can be opened at a time.
167 | */
168 | public void setOpenOnlyOne(boolean openOnlyOne) {
169 | this.openOnlyOne = openOnlyOne;
170 | }
171 |
172 | /**
173 | * Open a specific layout.
174 | * @param id unique id which identifies the data object which is bind to the layout.
175 | */
176 | public void openLayout(final String id) {
177 | synchronized (stateChangeLock) {
178 | mapStates.put(id, SwipeRevealLayout.STATE_OPEN);
179 |
180 | if (mapLayouts.containsKey(id)) {
181 | final SwipeRevealLayout layout = mapLayouts.get(id);
182 | layout.open(true);
183 | }
184 | else if (openOnlyOne) {
185 | closeOthers(id, mapLayouts.get(id));
186 | }
187 | }
188 | }
189 |
190 | /**
191 | * Close a specific layout.
192 | * @param id unique id which identifies the data object which is bind to the layout.
193 | */
194 | public void closeLayout(final String id) {
195 | synchronized (stateChangeLock) {
196 | mapStates.put(id, SwipeRevealLayout.STATE_CLOSE);
197 |
198 | if (mapLayouts.containsKey(id)) {
199 | final SwipeRevealLayout layout = mapLayouts.get(id);
200 | layout.close(true);
201 | }
202 | }
203 | }
204 |
205 | /**
206 | * Close others swipe layout.
207 | * @param id layout which bind with this data object id will be excluded.
208 | * @param swipeLayout will be excluded.
209 | */
210 | private void closeOthers(String id, SwipeRevealLayout swipeLayout) {
211 | synchronized (stateChangeLock) {
212 | // close other rows if openOnlyOne is true.
213 | if (getOpenCount() > 1) {
214 | for (Map.Entry entry : mapStates.entrySet()) {
215 | if (!entry.getKey().equals(id)) {
216 | entry.setValue(SwipeRevealLayout.STATE_CLOSE);
217 | }
218 | }
219 |
220 | for (SwipeRevealLayout layout : mapLayouts.values()) {
221 | if (layout != swipeLayout) {
222 | layout.close(true);
223 | }
224 | }
225 | }
226 | }
227 | }
228 |
229 | private void setLockSwipe(boolean lock, String... id) {
230 | if (id == null || id.length == 0)
231 | return;
232 |
233 | if (lock)
234 | lockedSwipeSet.addAll(Arrays.asList(id));
235 | else
236 | lockedSwipeSet.removeAll(Arrays.asList(id));
237 |
238 | for (String s : id) {
239 | SwipeRevealLayout layout = mapLayouts.get(s);
240 | if (layout != null) {
241 | layout.setLockDrag(lock);
242 | }
243 | }
244 | }
245 |
246 | private int getOpenCount() {
247 | int total = 0;
248 |
249 | for (int state : mapStates.values()) {
250 | if (state == SwipeRevealLayout.STATE_OPEN || state == SwipeRevealLayout.STATE_OPENING) {
251 | total++;
252 | }
253 | }
254 |
255 | return total;
256 | }
257 | }
258 |
--------------------------------------------------------------------------------
/swipe-reveal-layout/src/main/java/com/chauthai/swipereveallayout/SwipeRevealLayout.java:
--------------------------------------------------------------------------------
1 | /**
2 | The MIT License (MIT)
3 |
4 | Copyright (c) 2016 Chau Thai
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | */
24 |
25 | package com.chauthai.swipereveallayout;
26 |
27 | import android.annotation.SuppressLint;
28 | import android.content.Context;
29 | import android.content.res.Resources;
30 | import android.content.res.TypedArray;
31 | import android.graphics.Rect;
32 | import android.support.v4.view.GestureDetectorCompat;
33 | import android.support.v4.view.ViewCompat;
34 | import android.support.v4.widget.ViewDragHelper;
35 | import android.util.AttributeSet;
36 | import android.util.DisplayMetrics;
37 | import android.util.Log;
38 | import android.view.GestureDetector;
39 | import android.view.MotionEvent;
40 | import android.view.View;
41 | import android.view.ViewGroup;
42 |
43 | @SuppressLint("RtlHardcoded")
44 | public class SwipeRevealLayout extends ViewGroup {
45 | // These states are used only for ViewBindHelper
46 | protected static final int STATE_CLOSE = 0;
47 | protected static final int STATE_CLOSING = 1;
48 | protected static final int STATE_OPEN = 2;
49 | protected static final int STATE_OPENING = 3;
50 | protected static final int STATE_DRAGGING = 4;
51 |
52 | private static final int DEFAULT_MIN_FLING_VELOCITY = 300; // dp per second
53 | private static final int DEFAULT_MIN_DIST_REQUEST_DISALLOW_PARENT = 1; // dp
54 |
55 | public static final int DRAG_EDGE_LEFT = 0x1;
56 | public static final int DRAG_EDGE_RIGHT = 0x1 << 1;
57 | public static final int DRAG_EDGE_TOP = 0x1 << 2;
58 | public static final int DRAG_EDGE_BOTTOM = 0x1 << 3;
59 |
60 | /**
61 | * The secondary view will be under the main view.
62 | */
63 | public static final int MODE_NORMAL = 0;
64 |
65 | /**
66 | * The secondary view will stick the edge of the main view.
67 | */
68 | public static final int MODE_SAME_LEVEL = 1;
69 |
70 | /**
71 | * Main view is the view which is shown when the layout is closed.
72 | */
73 | private View mMainView;
74 |
75 | /**
76 | * Secondary view is the view which is shown when the layout is opened.
77 | */
78 | private View mSecondaryView;
79 |
80 | /**
81 | * The rectangle position of the main view when the layout is closed.
82 | */
83 | private Rect mRectMainClose = new Rect();
84 |
85 | /**
86 | * The rectangle position of the main view when the layout is opened.
87 | */
88 | private Rect mRectMainOpen = new Rect();
89 |
90 | /**
91 | * The rectangle position of the secondary view when the layout is closed.
92 | */
93 | private Rect mRectSecClose = new Rect();
94 |
95 | /**
96 | * The rectangle position of the secondary view when the layout is opened.
97 | */
98 | private Rect mRectSecOpen = new Rect();
99 |
100 | /**
101 | * The minimum distance (px) to the closest drag edge that the SwipeRevealLayout
102 | * will disallow the parent to intercept touch event.
103 | */
104 | private int mMinDistRequestDisallowParent = 0;
105 |
106 | private boolean mIsOpenBeforeInit = false;
107 | private volatile boolean mAborted = false;
108 | private volatile boolean mIsScrolling = false;
109 | private volatile boolean mLockDrag = false;
110 |
111 | private int mMinFlingVelocity = DEFAULT_MIN_FLING_VELOCITY;
112 | private int mState = STATE_CLOSE;
113 | private int mMode = MODE_NORMAL;
114 |
115 | private int mLastMainLeft = 0;
116 | private int mLastMainTop = 0;
117 |
118 | private int mDragEdge = DRAG_EDGE_LEFT;
119 |
120 | private float mDragDist = 0;
121 | private float mPrevX = -1;
122 | private float mPrevY = -1;
123 |
124 | private ViewDragHelper mDragHelper;
125 | private GestureDetectorCompat mGestureDetector;
126 |
127 | private DragStateChangeListener mDragStateChangeListener; // only used for ViewBindHelper
128 | private SwipeListener mSwipeListener;
129 |
130 | private int mOnLayoutCount = 0;
131 |
132 | interface DragStateChangeListener {
133 | void onDragStateChanged(int state);
134 | }
135 |
136 | /**
137 | * Listener for monitoring events about swipe layout.
138 | */
139 | public interface SwipeListener {
140 | /**
141 | * Called when the main view becomes completely closed.
142 | */
143 | void onClosed(SwipeRevealLayout view);
144 |
145 | /**
146 | * Called when the main view becomes completely opened.
147 | */
148 | void onOpened(SwipeRevealLayout view);
149 |
150 | /**
151 | * Called when the main view's position changes.
152 | * @param slideOffset The new offset of the main view within its range, from 0-1
153 | */
154 | void onSlide(SwipeRevealLayout view, float slideOffset);
155 | }
156 |
157 | /**
158 | * No-op stub for {@link SwipeListener}. If you only want ot implement a subset
159 | * of the listener methods, you can extend this instead of implement the full interface.
160 | */
161 | public static class SimpleSwipeListener implements SwipeListener {
162 | @Override
163 | public void onClosed(SwipeRevealLayout view) {}
164 |
165 | @Override
166 | public void onOpened(SwipeRevealLayout view) {}
167 |
168 | @Override
169 | public void onSlide(SwipeRevealLayout view, float slideOffset) {}
170 | }
171 |
172 | public SwipeRevealLayout(Context context) {
173 | super(context);
174 | init(context, null);
175 | }
176 |
177 | public SwipeRevealLayout(Context context, AttributeSet attrs) {
178 | super(context, attrs);
179 | init(context, attrs);
180 | }
181 |
182 | public SwipeRevealLayout(Context context, AttributeSet attrs, int defStyleAttr) {
183 | super(context, attrs, defStyleAttr);
184 | }
185 |
186 | @Override
187 | public boolean onTouchEvent(MotionEvent event) {
188 | mGestureDetector.onTouchEvent(event);
189 | mDragHelper.processTouchEvent(event);
190 | return true;
191 | }
192 |
193 | @Override
194 | public boolean onInterceptTouchEvent(MotionEvent ev) {
195 | if (isDragLocked()) {
196 | return super.onInterceptTouchEvent(ev);
197 | }
198 |
199 | mDragHelper.processTouchEvent(ev);
200 | mGestureDetector.onTouchEvent(ev);
201 | accumulateDragDist(ev);
202 |
203 | boolean couldBecomeClick = couldBecomeClick(ev);
204 | boolean settling = mDragHelper.getViewDragState() == ViewDragHelper.STATE_SETTLING;
205 | boolean idleAfterScrolled = mDragHelper.getViewDragState() == ViewDragHelper.STATE_IDLE
206 | && mIsScrolling;
207 |
208 | // must be placed as the last statement
209 | mPrevX = ev.getX();
210 | mPrevY = ev.getY();
211 |
212 | // return true => intercept, cannot trigger onClick event
213 | return !couldBecomeClick && (settling || idleAfterScrolled);
214 | }
215 |
216 | @Override
217 | protected void onFinishInflate() {
218 | super.onFinishInflate();
219 |
220 | // get views
221 | if (getChildCount() >= 2) {
222 | mSecondaryView = getChildAt(0);
223 | mMainView = getChildAt(1);
224 | }
225 | else if (getChildCount() == 1) {
226 | mMainView = getChildAt(0);
227 | }
228 | }
229 |
230 | /**
231 | * {@inheritDoc}
232 | */
233 | @SuppressWarnings("ConstantConditions")
234 | @Override
235 | protected void onLayout(boolean changed, int l, int t, int r, int b) {
236 | mAborted = false;
237 |
238 | for (int index = 0; index < getChildCount(); index++) {
239 | final View child = getChildAt(index);
240 |
241 | int left, right, top, bottom;
242 | left = right = top = bottom = 0;
243 |
244 | final int minLeft = getPaddingLeft();
245 | final int maxRight = Math.max(r - getPaddingRight() - l, 0);
246 | final int minTop = getPaddingTop();
247 | final int maxBottom = Math.max(b - getPaddingBottom() - t, 0);
248 |
249 | int measuredChildHeight = child.getMeasuredHeight();
250 | int measuredChildWidth = child.getMeasuredWidth();
251 |
252 | // need to take account if child size is match_parent
253 | final LayoutParams childParams = child.getLayoutParams();
254 | boolean matchParentHeight = false;
255 | boolean matchParentWidth = false;
256 |
257 | if (childParams != null) {
258 | matchParentHeight = (childParams.height == LayoutParams.MATCH_PARENT) ||
259 | (childParams.height == LayoutParams.FILL_PARENT);
260 | matchParentWidth = (childParams.width == LayoutParams.MATCH_PARENT) ||
261 | (childParams.width == LayoutParams.FILL_PARENT);
262 | }
263 |
264 | if (matchParentHeight) {
265 | measuredChildHeight = maxBottom - minTop;
266 | childParams.height = measuredChildHeight;
267 | }
268 |
269 | if (matchParentWidth) {
270 | measuredChildWidth = maxRight - minLeft;
271 | childParams.width = measuredChildWidth;
272 | }
273 |
274 | switch (mDragEdge) {
275 | case DRAG_EDGE_RIGHT:
276 | left = Math.max(r - measuredChildWidth - getPaddingRight() - l, minLeft);
277 | top = Math.min(getPaddingTop(), maxBottom);
278 | right = Math.max(r - getPaddingRight() - l, minLeft);
279 | bottom = Math.min(measuredChildHeight + getPaddingTop(), maxBottom);
280 | break;
281 |
282 | case DRAG_EDGE_LEFT:
283 | left = Math.min(getPaddingLeft(), maxRight);
284 | top = Math.min(getPaddingTop(), maxBottom);
285 | right = Math.min(measuredChildWidth + getPaddingLeft(), maxRight);
286 | bottom = Math.min(measuredChildHeight + getPaddingTop(), maxBottom);
287 | break;
288 |
289 | case DRAG_EDGE_TOP:
290 | left = Math.min(getPaddingLeft(), maxRight);
291 | top = Math.min(getPaddingTop(), maxBottom);
292 | right = Math.min(measuredChildWidth + getPaddingLeft(), maxRight);
293 | bottom = Math.min(measuredChildHeight + getPaddingTop(), maxBottom);
294 | break;
295 |
296 | case DRAG_EDGE_BOTTOM:
297 | left = Math.min(getPaddingLeft(), maxRight);
298 | top = Math.max(b - measuredChildHeight - getPaddingBottom() - t, minTop);
299 | right = Math.min(measuredChildWidth + getPaddingLeft(), maxRight);
300 | bottom = Math.max(b - getPaddingBottom() - t, minTop);
301 | break;
302 | }
303 |
304 | child.layout(left, top, right, bottom);
305 | }
306 |
307 | // taking account offset when mode is SAME_LEVEL
308 | if (mMode == MODE_SAME_LEVEL) {
309 | switch (mDragEdge) {
310 | case DRAG_EDGE_LEFT:
311 | mSecondaryView.offsetLeftAndRight(-mSecondaryView.getWidth());
312 | break;
313 |
314 | case DRAG_EDGE_RIGHT:
315 | mSecondaryView.offsetLeftAndRight(mSecondaryView.getWidth());
316 | break;
317 |
318 | case DRAG_EDGE_TOP:
319 | mSecondaryView.offsetTopAndBottom(-mSecondaryView.getHeight());
320 | break;
321 |
322 | case DRAG_EDGE_BOTTOM:
323 | mSecondaryView.offsetTopAndBottom(mSecondaryView.getHeight());
324 | }
325 | }
326 |
327 | initRects();
328 |
329 | if (mIsOpenBeforeInit) {
330 | open(false);
331 | } else {
332 | close(false);
333 | }
334 |
335 | mLastMainLeft = mMainView.getLeft();
336 | mLastMainTop = mMainView.getTop();
337 |
338 | mOnLayoutCount++;
339 | }
340 |
341 | /**
342 | * {@inheritDoc}
343 | */
344 | @Override
345 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
346 | if (getChildCount() < 2) {
347 | throw new RuntimeException("Layout must have two children");
348 | }
349 |
350 | final LayoutParams params = getLayoutParams();
351 |
352 | final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
353 | final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
354 |
355 | int desiredWidth = 0;
356 | int desiredHeight = 0;
357 |
358 | // first find the largest child
359 | for (int i = 0; i < getChildCount(); i++) {
360 | final View child = getChildAt(i);
361 | measureChild(child, widthMeasureSpec, heightMeasureSpec);
362 | desiredWidth = Math.max(child.getMeasuredWidth(), desiredWidth);
363 | desiredHeight = Math.max(child.getMeasuredHeight(), desiredHeight);
364 | }
365 | // create new measure spec using the largest child width
366 | widthMeasureSpec = MeasureSpec.makeMeasureSpec(desiredWidth, widthMode);
367 | heightMeasureSpec = MeasureSpec.makeMeasureSpec(desiredHeight, heightMode);
368 |
369 | final int measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
370 | final int measuredHeight = MeasureSpec.getSize(heightMeasureSpec);
371 |
372 | for (int i = 0; i < getChildCount(); i++) {
373 | final View child = getChildAt(i);
374 | final LayoutParams childParams = child.getLayoutParams();
375 |
376 | if (childParams != null) {
377 | if (childParams.height == LayoutParams.MATCH_PARENT) {
378 | child.setMinimumHeight(measuredHeight);
379 | }
380 |
381 | if (childParams.width == LayoutParams.MATCH_PARENT) {
382 | child.setMinimumWidth(measuredWidth);
383 | }
384 | }
385 |
386 | measureChild(child, widthMeasureSpec, heightMeasureSpec);
387 | desiredWidth = Math.max(child.getMeasuredWidth(), desiredWidth);
388 | desiredHeight = Math.max(child.getMeasuredHeight(), desiredHeight);
389 | }
390 |
391 | // taking accounts of padding
392 | desiredWidth += getPaddingLeft() + getPaddingRight();
393 | desiredHeight += getPaddingTop() + getPaddingBottom();
394 |
395 | // adjust desired width
396 | if (widthMode == MeasureSpec.EXACTLY) {
397 | desiredWidth = measuredWidth;
398 | } else {
399 | if (params.width == LayoutParams.MATCH_PARENT) {
400 | desiredWidth = measuredWidth;
401 | }
402 |
403 | if (widthMode == MeasureSpec.AT_MOST) {
404 | desiredWidth = (desiredWidth > measuredWidth)? measuredWidth : desiredWidth;
405 | }
406 | }
407 |
408 | // adjust desired height
409 | if (heightMode == MeasureSpec.EXACTLY) {
410 | desiredHeight = measuredHeight;
411 | } else {
412 | if (params.height == LayoutParams.MATCH_PARENT) {
413 | desiredHeight = measuredHeight;
414 | }
415 |
416 | if (heightMode == MeasureSpec.AT_MOST) {
417 | desiredHeight = (desiredHeight > measuredHeight)? measuredHeight : desiredHeight;
418 | }
419 | }
420 |
421 | setMeasuredDimension(desiredWidth, desiredHeight);
422 | }
423 |
424 | @Override
425 | public void computeScroll() {
426 | if (mDragHelper.continueSettling(true)) {
427 | ViewCompat.postInvalidateOnAnimation(this);
428 | }
429 | }
430 |
431 | /**
432 | * Open the panel to show the secondary view
433 | * @param animation true to animate the open motion. {@link SwipeListener} won't be
434 | * called if is animation is false.
435 | */
436 | public void open(boolean animation) {
437 | mIsOpenBeforeInit = true;
438 | mAborted = false;
439 |
440 | if (animation) {
441 | mState = STATE_OPENING;
442 | mDragHelper.smoothSlideViewTo(mMainView, mRectMainOpen.left, mRectMainOpen.top);
443 |
444 | if (mDragStateChangeListener != null) {
445 | mDragStateChangeListener.onDragStateChanged(mState);
446 | }
447 | } else {
448 | mState = STATE_OPEN;
449 | mDragHelper.abort();
450 |
451 | mMainView.layout(
452 | mRectMainOpen.left,
453 | mRectMainOpen.top,
454 | mRectMainOpen.right,
455 | mRectMainOpen.bottom
456 | );
457 |
458 | mSecondaryView.layout(
459 | mRectSecOpen.left,
460 | mRectSecOpen.top,
461 | mRectSecOpen.right,
462 | mRectSecOpen.bottom
463 | );
464 | }
465 |
466 | ViewCompat.postInvalidateOnAnimation(SwipeRevealLayout.this);
467 | }
468 |
469 | /**
470 | * Close the panel to hide the secondary view
471 | * @param animation true to animate the close motion. {@link SwipeListener} won't be
472 | * called if is animation is false.
473 | */
474 | public void close(boolean animation) {
475 | mIsOpenBeforeInit = false;
476 | mAborted = false;
477 |
478 | if (animation) {
479 | mState = STATE_CLOSING;
480 | mDragHelper.smoothSlideViewTo(mMainView, mRectMainClose.left, mRectMainClose.top);
481 |
482 | if (mDragStateChangeListener != null) {
483 | mDragStateChangeListener.onDragStateChanged(mState);
484 | }
485 |
486 | } else {
487 | mState = STATE_CLOSE;
488 | mDragHelper.abort();
489 |
490 | mMainView.layout(
491 | mRectMainClose.left,
492 | mRectMainClose.top,
493 | mRectMainClose.right,
494 | mRectMainClose.bottom
495 | );
496 |
497 | mSecondaryView.layout(
498 | mRectSecClose.left,
499 | mRectSecClose.top,
500 | mRectSecClose.right,
501 | mRectSecClose.bottom
502 | );
503 | }
504 |
505 | ViewCompat.postInvalidateOnAnimation(SwipeRevealLayout.this);
506 | }
507 |
508 | /**
509 | * Set the minimum fling velocity to cause the layout to open/close.
510 | * @param velocity dp per second
511 | */
512 | public void setMinFlingVelocity(int velocity) {
513 | mMinFlingVelocity = velocity;
514 | }
515 |
516 | /**
517 | * Get the minimum fling velocity to cause the layout to open/close.
518 | * @return dp per second
519 | */
520 | public int getMinFlingVelocity() {
521 | return mMinFlingVelocity;
522 | }
523 |
524 | /**
525 | * Set the edge where the layout can be dragged from.
526 | * @param dragEdge Can be one of these
527 | *
528 | * - {@link #DRAG_EDGE_LEFT}
529 | * - {@link #DRAG_EDGE_TOP}
530 | * - {@link #DRAG_EDGE_RIGHT}
531 | * - {@link #DRAG_EDGE_BOTTOM}
532 | *
533 | */
534 | public void setDragEdge(int dragEdge) {
535 | mDragEdge = dragEdge;
536 | }
537 |
538 | /**
539 | * Get the edge where the layout can be dragged from.
540 | * @return Can be one of these
541 | *
542 | * - {@link #DRAG_EDGE_LEFT}
543 | * - {@link #DRAG_EDGE_TOP}
544 | * - {@link #DRAG_EDGE_RIGHT}
545 | * - {@link #DRAG_EDGE_BOTTOM}
546 | *
547 | */
548 | public int getDragEdge() {
549 | return mDragEdge;
550 | }
551 |
552 | public void setSwipeListener(SwipeListener listener) {
553 | mSwipeListener = listener;
554 | }
555 |
556 | /**
557 | * @param lock if set to true, the user cannot drag/swipe the layout.
558 | */
559 | public void setLockDrag(boolean lock) {
560 | mLockDrag = lock;
561 | }
562 |
563 | /**
564 | * @return true if the drag/swipe motion is currently locked.
565 | */
566 | public boolean isDragLocked() {
567 | return mLockDrag;
568 | }
569 |
570 | /**
571 | * @return true if layout is fully opened, false otherwise.
572 | */
573 | public boolean isOpened() {
574 | return (mState == STATE_OPEN);
575 | }
576 |
577 | /**
578 | * @return true if layout is fully closed, false otherwise.
579 | */
580 | public boolean isClosed() {
581 | return (mState == STATE_CLOSE);
582 | }
583 |
584 | /** Only used for {@link ViewBinderHelper} */
585 | void setDragStateChangeListener(DragStateChangeListener listener) {
586 | mDragStateChangeListener = listener;
587 | }
588 |
589 | /** Abort current motion in progress. Only used for {@link ViewBinderHelper} */
590 | protected void abort() {
591 | mAborted = true;
592 | mDragHelper.abort();
593 | }
594 |
595 | /**
596 | * In RecyclerView/ListView, onLayout should be called 2 times to display children views correctly.
597 | * This method check if it've already called onLayout two times.
598 | * @return true if you should call {@link #requestLayout()}.
599 | */
600 | protected boolean shouldRequestLayout() {
601 | return mOnLayoutCount < 2;
602 | }
603 |
604 |
605 | private int getMainOpenLeft() {
606 | switch (mDragEdge) {
607 | case DRAG_EDGE_LEFT:
608 | return mRectMainClose.left + mSecondaryView.getWidth();
609 |
610 | case DRAG_EDGE_RIGHT:
611 | return mRectMainClose.left - mSecondaryView.getWidth();
612 |
613 | case DRAG_EDGE_TOP:
614 | return mRectMainClose.left;
615 |
616 | case DRAG_EDGE_BOTTOM:
617 | return mRectMainClose.left;
618 |
619 | default:
620 | return 0;
621 | }
622 | }
623 |
624 | private int getMainOpenTop() {
625 | switch (mDragEdge) {
626 | case DRAG_EDGE_LEFT:
627 | return mRectMainClose.top;
628 |
629 | case DRAG_EDGE_RIGHT:
630 | return mRectMainClose.top;
631 |
632 | case DRAG_EDGE_TOP:
633 | return mRectMainClose.top + mSecondaryView.getHeight();
634 |
635 | case DRAG_EDGE_BOTTOM:
636 | return mRectMainClose.top - mSecondaryView.getHeight();
637 |
638 | default:
639 | return 0;
640 | }
641 | }
642 |
643 | private int getSecOpenLeft() {
644 | if (mMode == MODE_NORMAL || mDragEdge == DRAG_EDGE_BOTTOM || mDragEdge == DRAG_EDGE_TOP) {
645 | return mRectSecClose.left;
646 | }
647 |
648 | if (mDragEdge == DRAG_EDGE_LEFT) {
649 | return mRectSecClose.left + mSecondaryView.getWidth();
650 | } else {
651 | return mRectSecClose.left - mSecondaryView.getWidth();
652 | }
653 | }
654 |
655 | private int getSecOpenTop() {
656 | if (mMode == MODE_NORMAL || mDragEdge == DRAG_EDGE_LEFT || mDragEdge == DRAG_EDGE_RIGHT) {
657 | return mRectSecClose.top;
658 | }
659 |
660 | if (mDragEdge == DRAG_EDGE_TOP) {
661 | return mRectSecClose.top + mSecondaryView.getHeight();
662 | } else {
663 | return mRectSecClose.top - mSecondaryView.getHeight();
664 | }
665 | }
666 |
667 | private void initRects() {
668 | // close position of main view
669 | mRectMainClose.set(
670 | mMainView.getLeft(),
671 | mMainView.getTop(),
672 | mMainView.getRight(),
673 | mMainView.getBottom()
674 | );
675 |
676 | // close position of secondary view
677 | mRectSecClose.set(
678 | mSecondaryView.getLeft(),
679 | mSecondaryView.getTop(),
680 | mSecondaryView.getRight(),
681 | mSecondaryView.getBottom()
682 | );
683 |
684 | // open position of the main view
685 | mRectMainOpen.set(
686 | getMainOpenLeft(),
687 | getMainOpenTop(),
688 | getMainOpenLeft() + mMainView.getWidth(),
689 | getMainOpenTop() + mMainView.getHeight()
690 | );
691 |
692 | // open position of the secondary view
693 | mRectSecOpen.set(
694 | getSecOpenLeft(),
695 | getSecOpenTop(),
696 | getSecOpenLeft() + mSecondaryView.getWidth(),
697 | getSecOpenTop() + mSecondaryView.getHeight()
698 | );
699 | }
700 |
701 | private boolean couldBecomeClick(MotionEvent ev) {
702 | return isInMainView(ev) && !shouldInitiateADrag();
703 | }
704 |
705 | private boolean isInMainView(MotionEvent ev) {
706 | float x = ev.getX();
707 | float y = ev.getY();
708 |
709 | boolean withinVertical = mMainView.getTop() <= y && y <= mMainView.getBottom();
710 | boolean withinHorizontal = mMainView.getLeft() <= x && x <= mMainView.getRight();
711 |
712 | return withinVertical && withinHorizontal;
713 | }
714 |
715 | private boolean shouldInitiateADrag() {
716 | float minDistToInitiateDrag = mDragHelper.getTouchSlop();
717 | return mDragDist >= minDistToInitiateDrag;
718 | }
719 |
720 | private void accumulateDragDist(MotionEvent ev) {
721 | final int action = ev.getAction();
722 | if (action == MotionEvent.ACTION_DOWN) {
723 | mDragDist = 0;
724 | return;
725 | }
726 |
727 | boolean dragHorizontally = getDragEdge() == DRAG_EDGE_LEFT ||
728 | getDragEdge() == DRAG_EDGE_RIGHT;
729 |
730 | float dragged;
731 | if (dragHorizontally) {
732 | dragged = Math.abs(ev.getX() - mPrevX);
733 | } else {
734 | dragged = Math.abs(ev.getY() - mPrevY);
735 | }
736 |
737 | mDragDist += dragged;
738 | }
739 |
740 | private void init(Context context, AttributeSet attrs) {
741 | if (attrs != null && context != null) {
742 | TypedArray a = context.getTheme().obtainStyledAttributes(
743 | attrs,
744 | R.styleable.SwipeRevealLayout,
745 | 0, 0
746 | );
747 |
748 | mDragEdge = a.getInteger(R.styleable.SwipeRevealLayout_dragEdge, DRAG_EDGE_LEFT);
749 | mMinFlingVelocity = a.getInteger(R.styleable.SwipeRevealLayout_flingVelocity, DEFAULT_MIN_FLING_VELOCITY);
750 | mMode = a.getInteger(R.styleable.SwipeRevealLayout_mode, MODE_NORMAL);
751 |
752 | mMinDistRequestDisallowParent = a.getDimensionPixelSize(
753 | R.styleable.SwipeRevealLayout_minDistRequestDisallowParent,
754 | dpToPx(DEFAULT_MIN_DIST_REQUEST_DISALLOW_PARENT)
755 | );
756 | }
757 |
758 | mDragHelper = ViewDragHelper.create(this, 1.0f, mDragHelperCallback);
759 | mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_ALL);
760 |
761 | mGestureDetector = new GestureDetectorCompat(context, mGestureListener);
762 | }
763 |
764 | private final GestureDetector.OnGestureListener mGestureListener = new GestureDetector.SimpleOnGestureListener() {
765 | boolean hasDisallowed = false;
766 |
767 | @Override
768 | public boolean onDown(MotionEvent e) {
769 | mIsScrolling = false;
770 | hasDisallowed = false;
771 | return true;
772 | }
773 |
774 | @Override
775 | public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
776 | mIsScrolling = true;
777 | return false;
778 | }
779 |
780 | @Override
781 | public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
782 | mIsScrolling = true;
783 |
784 | if (getParent() != null) {
785 | boolean shouldDisallow;
786 |
787 | if (!hasDisallowed) {
788 | shouldDisallow = getDistToClosestEdge() >= mMinDistRequestDisallowParent;
789 | if (shouldDisallow) {
790 | hasDisallowed = true;
791 | }
792 | } else {
793 | shouldDisallow = true;
794 | }
795 |
796 | // disallow parent to intercept touch event so that the layout will work
797 | // properly on RecyclerView or view that handles scroll gesture.
798 | getParent().requestDisallowInterceptTouchEvent(shouldDisallow);
799 | }
800 |
801 | return false;
802 | }
803 | };
804 |
805 | private int getDistToClosestEdge() {
806 | switch (mDragEdge) {
807 | case DRAG_EDGE_LEFT:
808 | final int pivotRight = mRectMainClose.left + mSecondaryView.getWidth();
809 |
810 | return Math.min(
811 | mMainView.getLeft() - mRectMainClose.left,
812 | pivotRight - mMainView.getLeft()
813 | );
814 |
815 | case DRAG_EDGE_RIGHT:
816 | final int pivotLeft = mRectMainClose.right - mSecondaryView.getWidth();
817 |
818 | return Math.min(
819 | mMainView.getRight() - pivotLeft,
820 | mRectMainClose.right - mMainView.getRight()
821 | );
822 |
823 | case DRAG_EDGE_TOP:
824 | final int pivotBottom = mRectMainClose.top + mSecondaryView.getHeight();
825 |
826 | return Math.min(
827 | mMainView.getBottom() - pivotBottom,
828 | pivotBottom - mMainView.getTop()
829 | );
830 |
831 | case DRAG_EDGE_BOTTOM:
832 | final int pivotTop = mRectMainClose.bottom - mSecondaryView.getHeight();
833 |
834 | return Math.min(
835 | mRectMainClose.bottom - mMainView.getBottom(),
836 | mMainView.getBottom() - pivotTop
837 | );
838 | }
839 |
840 | return 0;
841 | }
842 |
843 | private int getHalfwayPivotHorizontal() {
844 | if (mDragEdge == DRAG_EDGE_LEFT) {
845 | return mRectMainClose.left + mSecondaryView.getWidth() / 2;
846 | } else {
847 | return mRectMainClose.right - mSecondaryView.getWidth() / 2;
848 | }
849 | }
850 |
851 | private int getHalfwayPivotVertical() {
852 | if (mDragEdge == DRAG_EDGE_TOP) {
853 | return mRectMainClose.top + mSecondaryView.getHeight() / 2;
854 | } else {
855 | return mRectMainClose.bottom - mSecondaryView.getHeight() / 2;
856 | }
857 | }
858 |
859 | private final ViewDragHelper.Callback mDragHelperCallback = new ViewDragHelper.Callback() {
860 | @Override
861 | public boolean tryCaptureView(View child, int pointerId) {
862 | mAborted = false;
863 |
864 | if (mLockDrag)
865 | return false;
866 |
867 | mDragHelper.captureChildView(mMainView, pointerId);
868 | return false;
869 | }
870 |
871 | @Override
872 | public int clampViewPositionVertical(View child, int top, int dy) {
873 | switch (mDragEdge) {
874 | case DRAG_EDGE_TOP:
875 | return Math.max(
876 | Math.min(top, mRectMainClose.top + mSecondaryView.getHeight()),
877 | mRectMainClose.top
878 | );
879 |
880 | case DRAG_EDGE_BOTTOM:
881 | return Math.max(
882 | Math.min(top, mRectMainClose.top),
883 | mRectMainClose.top - mSecondaryView.getHeight()
884 | );
885 |
886 | default:
887 | return child.getTop();
888 | }
889 | }
890 |
891 | @Override
892 | public int clampViewPositionHorizontal(View child, int left, int dx) {
893 | switch (mDragEdge) {
894 | case DRAG_EDGE_RIGHT:
895 | return Math.max(
896 | Math.min(left, mRectMainClose.left),
897 | mRectMainClose.left - mSecondaryView.getWidth()
898 | );
899 |
900 | case DRAG_EDGE_LEFT:
901 | return Math.max(
902 | Math.min(left, mRectMainClose.left + mSecondaryView.getWidth()),
903 | mRectMainClose.left
904 | );
905 |
906 | default:
907 | return child.getLeft();
908 | }
909 | }
910 |
911 | @Override
912 | public void onViewReleased(View releasedChild, float xvel, float yvel) {
913 | final boolean velRightExceeded = pxToDp((int) xvel) >= mMinFlingVelocity;
914 | final boolean velLeftExceeded = pxToDp((int) xvel) <= -mMinFlingVelocity;
915 | final boolean velUpExceeded = pxToDp((int) yvel) <= -mMinFlingVelocity;
916 | final boolean velDownExceeded = pxToDp((int) yvel) >= mMinFlingVelocity;
917 |
918 | final int pivotHorizontal = getHalfwayPivotHorizontal();
919 | final int pivotVertical = getHalfwayPivotVertical();
920 |
921 | switch (mDragEdge) {
922 | case DRAG_EDGE_RIGHT:
923 | if (velRightExceeded) {
924 | close(true);
925 | } else if (velLeftExceeded) {
926 | open(true);
927 | } else {
928 | if (mMainView.getRight() < pivotHorizontal) {
929 | open(true);
930 | } else {
931 | close(true);
932 | }
933 | }
934 | break;
935 |
936 | case DRAG_EDGE_LEFT:
937 | if (velRightExceeded) {
938 | open(true);
939 | } else if (velLeftExceeded) {
940 | close(true);
941 | } else {
942 | if (mMainView.getLeft() < pivotHorizontal) {
943 | close(true);
944 | } else {
945 | open(true);
946 | }
947 | }
948 | break;
949 |
950 | case DRAG_EDGE_TOP:
951 | if (velUpExceeded) {
952 | close(true);
953 | } else if (velDownExceeded) {
954 | open(true);
955 | } else {
956 | if (mMainView.getTop() < pivotVertical) {
957 | close(true);
958 | } else {
959 | open(true);
960 | }
961 | }
962 | break;
963 |
964 | case DRAG_EDGE_BOTTOM:
965 | if (velUpExceeded) {
966 | open(true);
967 | } else if (velDownExceeded) {
968 | close(true);
969 | } else {
970 | if (mMainView.getBottom() < pivotVertical) {
971 | open(true);
972 | } else {
973 | close(true);
974 | }
975 | }
976 | break;
977 | }
978 | }
979 |
980 | @Override
981 | public void onEdgeDragStarted(int edgeFlags, int pointerId) {
982 | super.onEdgeDragStarted(edgeFlags, pointerId);
983 |
984 | if (mLockDrag) {
985 | return;
986 | }
987 |
988 | boolean edgeStartLeft = (mDragEdge == DRAG_EDGE_RIGHT)
989 | && edgeFlags == ViewDragHelper.EDGE_LEFT;
990 |
991 | boolean edgeStartRight = (mDragEdge == DRAG_EDGE_LEFT)
992 | && edgeFlags == ViewDragHelper.EDGE_RIGHT;
993 |
994 | boolean edgeStartTop = (mDragEdge == DRAG_EDGE_BOTTOM)
995 | && edgeFlags == ViewDragHelper.EDGE_TOP;
996 |
997 | boolean edgeStartBottom = (mDragEdge == DRAG_EDGE_TOP)
998 | && edgeFlags == ViewDragHelper.EDGE_BOTTOM;
999 |
1000 | if (edgeStartLeft || edgeStartRight || edgeStartTop || edgeStartBottom) {
1001 | mDragHelper.captureChildView(mMainView, pointerId);
1002 | }
1003 | }
1004 |
1005 | @Override
1006 | public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
1007 | super.onViewPositionChanged(changedView, left, top, dx, dy);
1008 | if (mMode == MODE_SAME_LEVEL) {
1009 | if (mDragEdge == DRAG_EDGE_LEFT || mDragEdge == DRAG_EDGE_RIGHT) {
1010 | mSecondaryView.offsetLeftAndRight(dx);
1011 | } else {
1012 | mSecondaryView.offsetTopAndBottom(dy);
1013 | }
1014 | }
1015 |
1016 | boolean isMoved = (mMainView.getLeft() != mLastMainLeft) || (mMainView.getTop() != mLastMainTop);
1017 | if (mSwipeListener != null && isMoved) {
1018 | if (mMainView.getLeft() == mRectMainClose.left && mMainView.getTop() == mRectMainClose.top) {
1019 | mSwipeListener.onClosed(SwipeRevealLayout.this);
1020 | }
1021 | else if (mMainView.getLeft() == mRectMainOpen.left && mMainView.getTop() == mRectMainOpen.top) {
1022 | mSwipeListener.onOpened(SwipeRevealLayout.this);
1023 | }
1024 | else {
1025 | mSwipeListener.onSlide(SwipeRevealLayout.this, getSlideOffset());
1026 | }
1027 | }
1028 |
1029 | mLastMainLeft = mMainView.getLeft();
1030 | mLastMainTop = mMainView.getTop();
1031 | ViewCompat.postInvalidateOnAnimation(SwipeRevealLayout.this);
1032 | }
1033 |
1034 | private float getSlideOffset() {
1035 | switch (mDragEdge) {
1036 | case DRAG_EDGE_LEFT:
1037 | return (float) (mMainView.getLeft() - mRectMainClose.left) / mSecondaryView.getWidth();
1038 |
1039 | case DRAG_EDGE_RIGHT:
1040 | return (float) (mRectMainClose.left - mMainView.getLeft()) / mSecondaryView.getWidth();
1041 |
1042 | case DRAG_EDGE_TOP:
1043 | return (float) (mMainView.getTop() - mRectMainClose.top) / mSecondaryView.getHeight();
1044 |
1045 | case DRAG_EDGE_BOTTOM:
1046 | return (float) (mRectMainClose.top - mMainView.getTop()) / mSecondaryView.getHeight();
1047 |
1048 | default:
1049 | return 0;
1050 | }
1051 | }
1052 |
1053 | @Override
1054 | public void onViewDragStateChanged(int state) {
1055 | super.onViewDragStateChanged(state);
1056 | final int prevState = mState;
1057 |
1058 | switch (state) {
1059 | case ViewDragHelper.STATE_DRAGGING:
1060 | mState = STATE_DRAGGING;
1061 | break;
1062 |
1063 | case ViewDragHelper.STATE_IDLE:
1064 |
1065 | // drag edge is left or right
1066 | if (mDragEdge == DRAG_EDGE_LEFT || mDragEdge == DRAG_EDGE_RIGHT) {
1067 | if (mMainView.getLeft() == mRectMainClose.left) {
1068 | mState = STATE_CLOSE;
1069 | } else {
1070 | mState = STATE_OPEN;
1071 | }
1072 | }
1073 |
1074 | // drag edge is top or bottom
1075 | else {
1076 | if (mMainView.getTop() == mRectMainClose.top) {
1077 | mState = STATE_CLOSE;
1078 | } else {
1079 | mState = STATE_OPEN;
1080 | }
1081 | }
1082 | break;
1083 | }
1084 |
1085 | if (mDragStateChangeListener != null && !mAborted && prevState != mState) {
1086 | mDragStateChangeListener.onDragStateChanged(mState);
1087 | }
1088 | }
1089 | };
1090 |
1091 | public static String getStateString(int state) {
1092 | switch (state) {
1093 | case STATE_CLOSE:
1094 | return "state_close";
1095 |
1096 | case STATE_CLOSING:
1097 | return "state_closing";
1098 |
1099 | case STATE_OPEN:
1100 | return "state_open";
1101 |
1102 | case STATE_OPENING:
1103 | return "state_opening";
1104 |
1105 | case STATE_DRAGGING:
1106 | return "state_dragging";
1107 |
1108 | default:
1109 | return "undefined";
1110 | }
1111 | }
1112 |
1113 | private int pxToDp(int px) {
1114 | Resources resources = getContext().getResources();
1115 | DisplayMetrics metrics = resources.getDisplayMetrics();
1116 | return (int) (px / ((float)metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT));
1117 | }
1118 |
1119 | private int dpToPx(int dp) {
1120 | Resources resources = getContext().getResources();
1121 | DisplayMetrics metrics = resources.getDisplayMetrics();
1122 | return (int) (dp * ((float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT));
1123 | }
1124 | }
1125 |
--------------------------------------------------------------------------------