├── .gitignore ├── LICENSE ├── README.md ├── art ├── ic_web.png ├── screenshot.png └── support.jpeg ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── library ├── build.gradle ├── libs │ └── android-support-v4.jar └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── me │ │ └── imid │ │ └── swipebacklayout │ │ └── lib │ │ ├── SwipeBackLayout.java │ │ ├── Utils.java │ │ ├── ViewDragHelper.java │ │ └── app │ │ ├── SwipeBackActivity.java │ │ ├── SwipeBackActivityBase.java │ │ ├── SwipeBackActivityHelper.java │ │ ├── SwipeBackListenerActivityAdapter.java │ │ └── SwipeBackPreferenceActivity.java │ └── res │ ├── drawable-xhdpi │ ├── shadow_bottom.png │ ├── shadow_left.png │ └── shadow_right.png │ ├── layout │ └── swipeback_layout.xml │ └── values │ ├── attrs.xml │ └── styles.xml ├── samples ├── build.gradle ├── libs │ └── android-support-v4.jar └── src │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-web.png │ ├── java │ └── me │ │ └── imid │ │ └── swipebacklayout │ │ └── demo │ │ ├── DemoActivity.java │ │ └── PreferenceUtils.java │ └── res │ ├── drawable-hdpi │ └── ic_launcher.png │ ├── drawable-mdpi │ └── ic_launcher.png │ ├── drawable-xhdpi │ └── ic_launcher.png │ ├── drawable-xxhdpi │ └── ic_launcher.png │ ├── layout │ └── activity_demo.xml │ ├── menu │ └── main.xml │ ├── values-sw600dp │ └── dimens.xml │ ├── values-sw720dp-land │ └── dimens.xml │ └── values │ ├── color.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | #Android generated 2 | bin 3 | gen 4 | gen* 5 | 6 | #Eclipse 7 | .project 8 | .classpath 9 | .settings 10 | 11 | #IntelliJ IDEA 12 | .idea 13 | *.iml 14 | *.ipr 15 | *.iws 16 | out 17 | build 18 | 19 | #Maven 20 | target 21 | release.properties 22 | pom.xml.* 23 | 24 | #Ant 25 | build.xml 26 | local.properties 27 | proguard.cfg 28 | 29 | #OSX 30 | .DS_Store 31 | 32 | #gradle 33 | .gradle 34 | library/gradle/ 35 | library/gradlew 36 | library/gradlew.bat 37 | 38 | #keystore 39 | keystore 40 | gradle.properties 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [yyyy] [name of copyright owner] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SwipeBackLayout 2 | === 3 | 4 | An Android library that help you to build app with swipe back gesture. 5 | 6 | 7 | ![](https://github.com/Issacw0ng/SwipeBackLayout/blob/master/art/screenshot.png?raw=true) 8 | 9 | 10 | Demo Apk 11 | === 12 | [GooglePlay](https://play.google.com/store/apps/details?id=me.imid.swipebacklayout.demo) 13 | 14 | 15 | Requirement 16 | === 17 | The latest android-support-v4.jar should be referenced by your project. 18 | 19 | Usage 20 | === 21 | 1. Add SwipeBackLayout as a dependency to your existing project. 22 | 2. To enable SwipeBackLayout, you can simply make your `Activity` extend `SwipeBackActivity`: 23 | * In `onCreate` method, `setContentView()` should be called as usual. 24 | * You will have access to the `getSwipeBackLayout()` method so you can customize the `SwipeBackLayout`. 25 | 3. Make window translucent by adding `true` to your theme. 26 | 27 | Simple Example 28 | === 29 | ``` 30 | public class DemoActivity extends SwipeBackActivity implements View.OnClickListener { 31 | private int[] mBgColors; 32 | 33 | private static int mBgIndex = 0; 34 | 35 | private String mKeyTrackingMode; 36 | 37 | private RadioGroup mTrackingModeGroup; 38 | 39 | private SwipeBackLayout mSwipeBackLayout; 40 | 41 | @Override 42 | protected void onCreate(Bundle savedInstanceState) { 43 | super.onCreate(savedInstanceState); 44 | setContentView(R.layout.activity_demo); 45 | changeActionBarColor(); 46 | findViews(); 47 | mKeyTrackingMode = getString(R.string.key_tracking_mode); 48 | mSwipeBackLayout = getSwipeBackLayout(); 49 | 50 | mTrackingModeGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { 51 | @Override 52 | public void onCheckedChanged(RadioGroup group, int checkedId) { 53 | int edgeFlag; 54 | switch (checkedId) { 55 | case R.id.mode_left: 56 | edgeFlag = SwipeBackLayout.EDGE_LEFT; 57 | break; 58 | case R.id.mode_right: 59 | edgeFlag = SwipeBackLayout.EDGE_RIGHT; 60 | break; 61 | case R.id.mode_bottom: 62 | edgeFlag = SwipeBackLayout.EDGE_BOTTOM; 63 | break; 64 | default: 65 | edgeFlag = SwipeBackLayout.EDGE_ALL; 66 | } 67 | mSwipeBackLayout.setEdgeTrackingEnabled(edgeFlag); 68 | saveTrackingMode(edgeFlag); 69 | } 70 | }); 71 | } 72 | ... 73 | ``` 74 | 75 | Download 76 | === 77 | Download via Jcenter: 78 | ``` 79 | compile 'me.imid.swipebacklayout.lib:library:1.1.0' 80 | ``` 81 | 82 | 83 | Support 84 | === 85 | 86 | 87 | 88 | 89 | Pull Requests 90 | === 91 | I will gladly accept pull requests for fixes and feature enhancements but please do them in the develop branch. 92 | 93 | License 94 | === 95 | 96 | Copyright 2013 Isaac Wang 97 | 98 | Licensed under the Apache License, Version 2.0 (the "License"); 99 | you may not use this file except in compliance with the License. 100 | You may obtain a copy of the License at 101 | 102 | http://www.apache.org/licenses/LICENSE-2.0 103 | 104 | Unless required by applicable law or agreed to in writing, software 105 | distributed under the License is distributed on an "AS IS" BASIS, 106 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 107 | See the License for the specific language governing permissions and 108 | limitations under the License. 109 | -------------------------------------------------------------------------------- /art/ic_web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikew0ng/SwipeBackLayout/7c6b0ac424813266cec57db86d65a7c9d9ff5fa6/art/ic_web.png -------------------------------------------------------------------------------- /art/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikew0ng/SwipeBackLayout/7c6b0ac424813266cec57db86d65a7c9d9ff5fa6/art/screenshot.png -------------------------------------------------------------------------------- /art/support.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikew0ng/SwipeBackLayout/7c6b0ac424813266cec57db86d65a7c9d9ff5fa6/art/support.jpeg -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | google() 5 | } 6 | dependencies { 7 | classpath 'com.android.tools.build:gradle:3.0.0' 8 | classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' 9 | classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7' 10 | } 11 | } 12 | 13 | allprojects { 14 | repositories { 15 | google() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikew0ng/SwipeBackLayout/7c6b0ac424813266cec57db86d65a7c9d9ff5fa6/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Jul 11 17:40:25 PDT 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip 7 | -------------------------------------------------------------------------------- /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 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'com.github.dcendents.android-maven' 3 | apply plugin: 'com.jfrog.bintray' 4 | 5 | android { 6 | compileSdkVersion 26 7 | buildToolsVersion '26.0.2' 8 | 9 | defaultConfig { 10 | minSdkVersion 14 11 | targetSdkVersion 26 12 | versionCode 2 13 | versionName "1.3.0" 14 | } 15 | } 16 | 17 | dependencies { 18 | implementation 'com.android.support:appcompat-v7:26.1.0' 19 | } 20 | 21 | version="1.3.0" 22 | def siteUrl = 'https://github.com/ikew0ng/SwipeBackLayout' 23 | def gitUrl = 'https://github.com/ikew0ng/SwipeBackLayout.git' 24 | group = "me.imid.swipebacklayout.lib" 25 | 26 | install { 27 | repositories.mavenInstaller { 28 | pom { 29 | project { 30 | packaging 'aar' 31 | // Add your description here 32 | name 'An Android library that help you to build app with swipe back gesture.' 33 | url siteUrl 34 | licenses { 35 | license { 36 | name 'The Apache Software License, Version 2.0' 37 | url 'http://www.apache.org/licenses/LICENSE-2.0.txt' 38 | } 39 | } 40 | developers { 41 | developer { 42 | id 'ike' 43 | name 'ike' 44 | email 'ike@imid.me' 45 | } 46 | } 47 | scm { 48 | connection gitUrl 49 | developerConnection gitUrl 50 | url siteUrl 51 | } 52 | } 53 | } 54 | } 55 | } 56 | task sourcesJar(type: Jar) { 57 | from android.sourceSets.main.java.srcDirs 58 | classifier = 'sources' 59 | } 60 | artifacts { 61 | archives sourcesJar 62 | } 63 | Properties properties = new Properties() 64 | File gradle_properties = project.file('gradle.properties'); 65 | if (gradle_properties.exists()) { 66 | properties.load(project.file('gradle.properties').newDataInputStream()) 67 | } 68 | bintray { 69 | user = properties.getProperty("bintray.user") 70 | key = properties.getProperty("bintray.apikey") 71 | configurations = ['archives'] 72 | pkg { 73 | repo = "maven" 74 | name = "SwipeBackLayout" 75 | websiteUrl = siteUrl 76 | vcsUrl = gitUrl 77 | licenses = ["Apache-2.0"] 78 | publish = true 79 | } 80 | } -------------------------------------------------------------------------------- /library/libs/android-support-v4.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikew0ng/SwipeBackLayout/7c6b0ac424813266cec57db86d65a7c9d9ff5fa6/library/libs/android-support-v4.jar -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /library/src/main/java/me/imid/swipebacklayout/lib/SwipeBackLayout.java: -------------------------------------------------------------------------------- 1 | package me.imid.swipebacklayout.lib; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.res.TypedArray; 6 | import android.graphics.Canvas; 7 | import android.graphics.Rect; 8 | import android.graphics.drawable.Drawable; 9 | import android.support.v4.view.ViewCompat; 10 | import android.util.AttributeSet; 11 | import android.view.MotionEvent; 12 | import android.view.View; 13 | import android.view.ViewGroup; 14 | import android.widget.FrameLayout; 15 | 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | 19 | import me.imid.swipebacklayout.lib.app.SwipeBackListenerActivityAdapter; 20 | 21 | public class SwipeBackLayout extends FrameLayout { 22 | /** 23 | * Minimum velocity that will be detected as a fling 24 | */ 25 | private static final int MIN_FLING_VELOCITY = 400; // dips per second 26 | 27 | private static final int DEFAULT_SCRIM_COLOR = 0x99000000; 28 | 29 | private static final int FULL_ALPHA = 255; 30 | 31 | /** 32 | * Edge flag indicating that the left edge should be affected. 33 | */ 34 | public static final int EDGE_LEFT = ViewDragHelper.EDGE_LEFT; 35 | 36 | /** 37 | * Edge flag indicating that the right edge should be affected. 38 | */ 39 | public static final int EDGE_RIGHT = ViewDragHelper.EDGE_RIGHT; 40 | 41 | /** 42 | * Edge flag indicating that the bottom edge should be affected. 43 | */ 44 | public static final int EDGE_BOTTOM = ViewDragHelper.EDGE_BOTTOM; 45 | 46 | /** 47 | * Edge flag set indicating all edges should be affected. 48 | */ 49 | public static final int EDGE_ALL = EDGE_LEFT | EDGE_RIGHT | EDGE_BOTTOM; 50 | 51 | /** 52 | * A view is not currently being dragged or animating as a result of a 53 | * fling/snap. 54 | */ 55 | public static final int STATE_IDLE = ViewDragHelper.STATE_IDLE; 56 | 57 | /** 58 | * A view is currently being dragged. The position is currently changing as 59 | * a result of user input or simulated user input. 60 | */ 61 | public static final int STATE_DRAGGING = ViewDragHelper.STATE_DRAGGING; 62 | 63 | /** 64 | * A view is currently settling into place as a result of a fling or 65 | * predefined non-interactive motion. 66 | */ 67 | public static final int STATE_SETTLING = ViewDragHelper.STATE_SETTLING; 68 | 69 | /** 70 | * Default threshold of scroll 71 | */ 72 | private static final float DEFAULT_SCROLL_THRESHOLD = 0.3f; 73 | 74 | private static final int OVERSCROLL_DISTANCE = 10; 75 | 76 | private static final int[] EDGE_FLAGS = { 77 | EDGE_LEFT, EDGE_RIGHT, EDGE_BOTTOM, EDGE_ALL 78 | }; 79 | 80 | private int mEdgeFlag; 81 | 82 | /** 83 | * Threshold of scroll, we will close the activity, when scrollPercent over 84 | * this value; 85 | */ 86 | private float mScrollThreshold = DEFAULT_SCROLL_THRESHOLD; 87 | 88 | private Activity mActivity; 89 | 90 | private boolean mEnable = true; 91 | 92 | private View mContentView; 93 | 94 | private ViewDragHelper mDragHelper; 95 | 96 | private float mScrollPercent; 97 | 98 | private int mContentLeft; 99 | 100 | private int mContentTop; 101 | 102 | /** 103 | * The set of listeners to be sent events through. 104 | */ 105 | private List mListeners; 106 | 107 | private Drawable mShadowLeft; 108 | 109 | private Drawable mShadowRight; 110 | 111 | private Drawable mShadowBottom; 112 | 113 | private float mScrimOpacity; 114 | 115 | private int mScrimColor = DEFAULT_SCRIM_COLOR; 116 | 117 | private boolean mInLayout; 118 | 119 | private Rect mTmpRect = new Rect(); 120 | 121 | /** 122 | * Edge being dragged 123 | */ 124 | private int mTrackingEdge; 125 | 126 | public SwipeBackLayout(Context context) { 127 | this(context, null); 128 | } 129 | 130 | public SwipeBackLayout(Context context, AttributeSet attrs) { 131 | this(context, attrs, R.attr.SwipeBackLayoutStyle); 132 | } 133 | 134 | public SwipeBackLayout(Context context, AttributeSet attrs, int defStyle) { 135 | super(context, attrs); 136 | mDragHelper = ViewDragHelper.create(this, new ViewDragCallback()); 137 | 138 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SwipeBackLayout, defStyle, 139 | R.style.SwipeBackLayout); 140 | 141 | int edgeSize = a.getDimensionPixelSize(R.styleable.SwipeBackLayout_edge_size, -1); 142 | if (edgeSize > 0) 143 | setEdgeSize(edgeSize); 144 | int mode = EDGE_FLAGS[a.getInt(R.styleable.SwipeBackLayout_edge_flag, 0)]; 145 | setEdgeTrackingEnabled(mode); 146 | 147 | int shadowLeft = a.getResourceId(R.styleable.SwipeBackLayout_shadow_left, 148 | R.drawable.shadow_left); 149 | int shadowRight = a.getResourceId(R.styleable.SwipeBackLayout_shadow_right, 150 | R.drawable.shadow_right); 151 | int shadowBottom = a.getResourceId(R.styleable.SwipeBackLayout_shadow_bottom, 152 | R.drawable.shadow_bottom); 153 | setShadow(shadowLeft, EDGE_LEFT); 154 | setShadow(shadowRight, EDGE_RIGHT); 155 | setShadow(shadowBottom, EDGE_BOTTOM); 156 | a.recycle(); 157 | final float density = getResources().getDisplayMetrics().density; 158 | final float minVel = MIN_FLING_VELOCITY * density; 159 | mDragHelper.setMinVelocity(minVel); 160 | mDragHelper.setMaxVelocity(minVel * 2f); 161 | } 162 | 163 | /** 164 | * Sets the sensitivity of the NavigationLayout. 165 | * 166 | * @param context The application context. 167 | * @param sensitivity value between 0 and 1, the final value for touchSlop = 168 | * ViewConfiguration.getScaledTouchSlop * (1 / s); 169 | */ 170 | public void setSensitivity(Context context, float sensitivity) { 171 | mDragHelper.setSensitivity(context, sensitivity); 172 | } 173 | 174 | /** 175 | * Set up contentView which will be moved by user gesture 176 | * 177 | * @param view 178 | */ 179 | public void setContentView(View view) { 180 | mContentView = view; 181 | } 182 | 183 | public void setEnableGesture(boolean enable) { 184 | mEnable = enable; 185 | } 186 | 187 | /** 188 | * Enable edge tracking for the selected edges of the parent view. The 189 | * callback's 190 | * {@link me.imid.swipebacklayout.lib.ViewDragHelper.Callback#onEdgeTouched(int, int)} 191 | * and 192 | * {@link me.imid.swipebacklayout.lib.ViewDragHelper.Callback#onEdgeDragStarted(int, int)} 193 | * methods will only be invoked for edges for which edge tracking has been 194 | * enabled. 195 | * 196 | * @param edgeFlags Combination of edge flags describing the edges to watch 197 | * @see #EDGE_LEFT 198 | * @see #EDGE_RIGHT 199 | * @see #EDGE_BOTTOM 200 | */ 201 | public void setEdgeTrackingEnabled(int edgeFlags) { 202 | mEdgeFlag = edgeFlags; 203 | mDragHelper.setEdgeTrackingEnabled(mEdgeFlag); 204 | } 205 | 206 | /** 207 | * Set a color to use for the scrim that obscures primary content while a 208 | * drawer is open. 209 | * 210 | * @param color Color to use in 0xAARRGGBB format. 211 | */ 212 | public void setScrimColor(int color) { 213 | mScrimColor = color; 214 | invalidate(); 215 | } 216 | 217 | /** 218 | * Set the size of an edge. This is the range in pixels along the edges of 219 | * this view that will actively detect edge touches or drags if edge 220 | * tracking is enabled. 221 | * 222 | * @param size The size of an edge in pixels 223 | */ 224 | public void setEdgeSize(int size) { 225 | mDragHelper.setEdgeSize(size); 226 | } 227 | 228 | /** 229 | * Register a callback to be invoked when a swipe event is sent to this 230 | * view. 231 | * 232 | * @param listener the swipe listener to attach to this view 233 | * @deprecated use {@link #addSwipeListener} instead 234 | */ 235 | @Deprecated 236 | public void setSwipeListener(SwipeListener listener) { 237 | addSwipeListener(listener); 238 | } 239 | 240 | /** 241 | * Add a callback to be invoked when a swipe event is sent to this view. 242 | * 243 | * @param listener the swipe listener to attach to this view 244 | */ 245 | public void addSwipeListener(SwipeListener listener) { 246 | if (mListeners == null) { 247 | mListeners = new ArrayList(); 248 | } 249 | mListeners.add(listener); 250 | } 251 | 252 | /** 253 | * Removes a listener from the set of listeners 254 | * 255 | * @param listener 256 | */ 257 | public void removeSwipeListener(SwipeListener listener) { 258 | if (mListeners == null) { 259 | return; 260 | } 261 | mListeners.remove(listener); 262 | } 263 | 264 | public static interface SwipeListener { 265 | /** 266 | * Invoke when state or scrollPercent changed 267 | * 268 | * @param state flag to describe scroll state 269 | * @param scrollPercent scroll percent of this view 270 | * @see #STATE_IDLE 271 | * @see #STATE_DRAGGING 272 | * @see #STATE_SETTLING 273 | */ 274 | public void onScrollStateChange(int state, float scrollPercent); 275 | 276 | /** 277 | * Invoke when edge touched 278 | * 279 | * @param edgeFlag edge flag describing the edge being touched 280 | * @see #EDGE_LEFT 281 | * @see #EDGE_RIGHT 282 | * @see #EDGE_BOTTOM 283 | */ 284 | public void onEdgeTouch(int edgeFlag); 285 | 286 | /** 287 | * Invoke when scroll percent over the threshold for the first time 288 | */ 289 | public void onScrollOverThreshold(); 290 | } 291 | 292 | public interface SwipeListenerEx extends SwipeListener { 293 | void onContentViewSwipedBack(); 294 | } 295 | 296 | /** 297 | * Set scroll threshold, we will close the activity, when scrollPercent over 298 | * this value 299 | * 300 | * @param threshold 301 | */ 302 | public void setScrollThresHold(float threshold) { 303 | if (threshold >= 1.0f || threshold <= 0) { 304 | throw new IllegalArgumentException("Threshold value should be between 0 and 1.0"); 305 | } 306 | mScrollThreshold = threshold; 307 | } 308 | 309 | /** 310 | * Set a drawable used for edge shadow. 311 | * 312 | * @param shadow Drawable to use 313 | * @param edgeFlags Combination of edge flags describing the edge to set 314 | * @see #EDGE_LEFT 315 | * @see #EDGE_RIGHT 316 | * @see #EDGE_BOTTOM 317 | */ 318 | public void setShadow(Drawable shadow, int edgeFlag) { 319 | if ((edgeFlag & EDGE_LEFT) != 0) { 320 | mShadowLeft = shadow; 321 | } else if ((edgeFlag & EDGE_RIGHT) != 0) { 322 | mShadowRight = shadow; 323 | } else if ((edgeFlag & EDGE_BOTTOM) != 0) { 324 | mShadowBottom = shadow; 325 | } 326 | invalidate(); 327 | } 328 | 329 | /** 330 | * Set a drawable used for edge shadow. 331 | * 332 | * @param resId Resource of drawable to use 333 | * @param edgeFlags Combination of edge flags describing the edge to set 334 | * @see #EDGE_LEFT 335 | * @see #EDGE_RIGHT 336 | * @see #EDGE_BOTTOM 337 | */ 338 | public void setShadow(int resId, int edgeFlag) { 339 | setShadow(getResources().getDrawable(resId), edgeFlag); 340 | } 341 | 342 | /** 343 | * Scroll out contentView and finish the activity 344 | */ 345 | public void scrollToFinishActivity() { 346 | final int childWidth = mContentView.getWidth(); 347 | final int childHeight = mContentView.getHeight(); 348 | 349 | int left = 0, top = 0; 350 | if ((mEdgeFlag & EDGE_LEFT) != 0) { 351 | left = childWidth + mShadowLeft.getIntrinsicWidth() + OVERSCROLL_DISTANCE; 352 | mTrackingEdge = EDGE_LEFT; 353 | } else if ((mEdgeFlag & EDGE_RIGHT) != 0) { 354 | left = -childWidth - mShadowRight.getIntrinsicWidth() - OVERSCROLL_DISTANCE; 355 | mTrackingEdge = EDGE_RIGHT; 356 | } else if ((mEdgeFlag & EDGE_BOTTOM) != 0) { 357 | top = -childHeight - mShadowBottom.getIntrinsicHeight() - OVERSCROLL_DISTANCE; 358 | mTrackingEdge = EDGE_BOTTOM; 359 | } 360 | 361 | mDragHelper.smoothSlideViewTo(mContentView, left, top); 362 | invalidate(); 363 | } 364 | 365 | @Override 366 | public boolean onInterceptTouchEvent(MotionEvent event) { 367 | if (!mEnable) { 368 | return false; 369 | } 370 | try { 371 | return mDragHelper.shouldInterceptTouchEvent(event); 372 | } catch (ArrayIndexOutOfBoundsException e) { 373 | // FIXME: handle exception 374 | // issues #9 375 | return false; 376 | } 377 | } 378 | 379 | @Override 380 | public boolean onTouchEvent(MotionEvent event) { 381 | if (!mEnable) { 382 | return false; 383 | } 384 | mDragHelper.processTouchEvent(event); 385 | return true; 386 | } 387 | 388 | @Override 389 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 390 | mInLayout = true; 391 | if (mContentView != null) 392 | mContentView.layout(mContentLeft, mContentTop, 393 | mContentLeft + mContentView.getMeasuredWidth(), 394 | mContentTop + mContentView.getMeasuredHeight()); 395 | mInLayout = false; 396 | } 397 | 398 | @Override 399 | public void requestLayout() { 400 | if (!mInLayout) { 401 | super.requestLayout(); 402 | } 403 | } 404 | 405 | @Override 406 | protected boolean drawChild(Canvas canvas, View child, long drawingTime) { 407 | final boolean drawContent = child == mContentView; 408 | 409 | boolean ret = super.drawChild(canvas, child, drawingTime); 410 | if (mScrimOpacity > 0 && drawContent 411 | && mDragHelper.getViewDragState() != ViewDragHelper.STATE_IDLE) { 412 | drawShadow(canvas, child); 413 | drawScrim(canvas, child); 414 | } 415 | return ret; 416 | } 417 | 418 | private void drawScrim(Canvas canvas, View child) { 419 | final int baseAlpha = (mScrimColor & 0xff000000) >>> 24; 420 | final int alpha = (int) (baseAlpha * mScrimOpacity); 421 | final int color = alpha << 24 | (mScrimColor & 0xffffff); 422 | 423 | if ((mTrackingEdge & EDGE_LEFT) != 0) { 424 | canvas.clipRect(0, 0, child.getLeft(), getHeight()); 425 | } else if ((mTrackingEdge & EDGE_RIGHT) != 0) { 426 | canvas.clipRect(child.getRight(), 0, getRight(), getHeight()); 427 | } else if ((mTrackingEdge & EDGE_BOTTOM) != 0) { 428 | canvas.clipRect(child.getLeft(), child.getBottom(), getRight(), getHeight()); 429 | } 430 | canvas.drawColor(color); 431 | } 432 | 433 | private void drawShadow(Canvas canvas, View child) { 434 | final Rect childRect = mTmpRect; 435 | child.getHitRect(childRect); 436 | 437 | if ((mEdgeFlag & EDGE_LEFT) != 0) { 438 | mShadowLeft.setBounds(childRect.left - mShadowLeft.getIntrinsicWidth(), childRect.top, 439 | childRect.left, childRect.bottom); 440 | mShadowLeft.setAlpha((int) (mScrimOpacity * FULL_ALPHA)); 441 | mShadowLeft.draw(canvas); 442 | } 443 | 444 | if ((mEdgeFlag & EDGE_RIGHT) != 0) { 445 | mShadowRight.setBounds(childRect.right, childRect.top, 446 | childRect.right + mShadowRight.getIntrinsicWidth(), childRect.bottom); 447 | mShadowRight.setAlpha((int) (mScrimOpacity * FULL_ALPHA)); 448 | mShadowRight.draw(canvas); 449 | } 450 | 451 | if ((mEdgeFlag & EDGE_BOTTOM) != 0) { 452 | mShadowBottom.setBounds(childRect.left, childRect.bottom, childRect.right, 453 | childRect.bottom + mShadowBottom.getIntrinsicHeight()); 454 | mShadowBottom.setAlpha((int) (mScrimOpacity * FULL_ALPHA)); 455 | mShadowBottom.draw(canvas); 456 | } 457 | } 458 | 459 | public void attachToActivity(Activity activity) { 460 | mActivity = activity; 461 | TypedArray a = activity.getTheme().obtainStyledAttributes(new int[]{ 462 | android.R.attr.windowBackground 463 | }); 464 | int background = a.getResourceId(0, 0); 465 | a.recycle(); 466 | 467 | ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView(); 468 | ViewGroup decorChild = (ViewGroup) decor.getChildAt(0); 469 | decorChild.setBackgroundResource(background); 470 | decor.removeView(decorChild); 471 | addView(decorChild); 472 | setContentView(decorChild); 473 | addSwipeListener(new SwipeBackListenerActivityAdapter(activity)); 474 | decor.addView(this); 475 | } 476 | 477 | @Override 478 | public void computeScroll() { 479 | mScrimOpacity = 1 - mScrollPercent; 480 | if (mDragHelper.continueSettling(true)) { 481 | ViewCompat.postInvalidateOnAnimation(this); 482 | } 483 | } 484 | 485 | private class ViewDragCallback extends ViewDragHelper.Callback { 486 | private boolean mIsScrollOverValid; 487 | 488 | @Override 489 | public boolean tryCaptureView(View view, int i) { 490 | boolean ret = mDragHelper.isEdgeTouched(mEdgeFlag, i); 491 | if (ret) { 492 | if (mDragHelper.isEdgeTouched(EDGE_LEFT, i)) { 493 | mTrackingEdge = EDGE_LEFT; 494 | } else if (mDragHelper.isEdgeTouched(EDGE_RIGHT, i)) { 495 | mTrackingEdge = EDGE_RIGHT; 496 | } else if (mDragHelper.isEdgeTouched(EDGE_BOTTOM, i)) { 497 | mTrackingEdge = EDGE_BOTTOM; 498 | } 499 | if (mListeners != null && !mListeners.isEmpty()) { 500 | for (SwipeListener listener : mListeners) { 501 | listener.onEdgeTouch(mTrackingEdge); 502 | } 503 | } 504 | mIsScrollOverValid = true; 505 | } 506 | boolean directionCheck = false; 507 | if (mEdgeFlag == EDGE_LEFT || mEdgeFlag == EDGE_RIGHT) { 508 | directionCheck = !mDragHelper.checkTouchSlop(ViewDragHelper.DIRECTION_VERTICAL, i); 509 | } else if (mEdgeFlag == EDGE_BOTTOM) { 510 | directionCheck = !mDragHelper 511 | .checkTouchSlop(ViewDragHelper.DIRECTION_HORIZONTAL, i); 512 | } else if (mEdgeFlag == EDGE_ALL) { 513 | directionCheck = true; 514 | } 515 | return ret & directionCheck; 516 | } 517 | 518 | @Override 519 | public int getViewHorizontalDragRange(View child) { 520 | return mEdgeFlag & (EDGE_LEFT | EDGE_RIGHT); 521 | } 522 | 523 | @Override 524 | public int getViewVerticalDragRange(View child) { 525 | return mEdgeFlag & EDGE_BOTTOM; 526 | } 527 | 528 | @Override 529 | public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { 530 | super.onViewPositionChanged(changedView, left, top, dx, dy); 531 | if ((mTrackingEdge & EDGE_LEFT) != 0) { 532 | mScrollPercent = Math.abs((float) left 533 | / (mContentView.getWidth() + mShadowLeft.getIntrinsicWidth())); 534 | } else if ((mTrackingEdge & EDGE_RIGHT) != 0) { 535 | mScrollPercent = Math.abs((float) left 536 | / (mContentView.getWidth() + mShadowRight.getIntrinsicWidth())); 537 | } else if ((mTrackingEdge & EDGE_BOTTOM) != 0) { 538 | mScrollPercent = Math.abs((float) top 539 | / (mContentView.getHeight() + mShadowBottom.getIntrinsicHeight())); 540 | } 541 | mContentLeft = left; 542 | mContentTop = top; 543 | invalidate(); 544 | if (mScrollPercent < mScrollThreshold && !mIsScrollOverValid) { 545 | mIsScrollOverValid = true; 546 | } 547 | 548 | if (mListeners != null && !mListeners.isEmpty()) { 549 | for (SwipeListener listener : mListeners) { 550 | listener.onScrollStateChange(mDragHelper.getViewDragState(), mScrollPercent); 551 | } 552 | } 553 | 554 | if (mListeners != null && !mListeners.isEmpty() 555 | && mDragHelper.getViewDragState() == STATE_DRAGGING 556 | && mScrollPercent >= mScrollThreshold && mIsScrollOverValid) { 557 | mIsScrollOverValid = false; 558 | for (SwipeListener listener : mListeners) { 559 | listener.onScrollOverThreshold(); 560 | } 561 | } 562 | 563 | if (mScrollPercent >= 1) { 564 | if (null != mListeners && !mListeners.isEmpty()) { 565 | for (SwipeListener listener : mListeners) { 566 | if (listener instanceof SwipeListenerEx) { 567 | ((SwipeListenerEx) listener).onContentViewSwipedBack(); 568 | } 569 | } 570 | } 571 | } 572 | } 573 | 574 | @Override 575 | public void onViewReleased(View releasedChild, float xvel, float yvel) { 576 | final int childWidth = releasedChild.getWidth(); 577 | final int childHeight = releasedChild.getHeight(); 578 | 579 | int left = 0, top = 0; 580 | if ((mTrackingEdge & EDGE_LEFT) != 0) { 581 | left = xvel > 0 || xvel == 0 && mScrollPercent > mScrollThreshold ? childWidth 582 | + mShadowLeft.getIntrinsicWidth() + OVERSCROLL_DISTANCE : 0; 583 | } else if ((mTrackingEdge & EDGE_RIGHT) != 0) { 584 | left = xvel < 0 || xvel == 0 && mScrollPercent > mScrollThreshold ? -(childWidth 585 | + mShadowLeft.getIntrinsicWidth() + OVERSCROLL_DISTANCE) : 0; 586 | } else if ((mTrackingEdge & EDGE_BOTTOM) != 0) { 587 | top = yvel < 0 || yvel == 0 && mScrollPercent > mScrollThreshold ? -(childHeight 588 | + mShadowBottom.getIntrinsicHeight() + OVERSCROLL_DISTANCE) : 0; 589 | } 590 | 591 | mDragHelper.settleCapturedViewAt(left, top); 592 | invalidate(); 593 | } 594 | 595 | @Override 596 | public int clampViewPositionHorizontal(View child, int left, int dx) { 597 | int ret = 0; 598 | if ((mTrackingEdge & EDGE_LEFT) != 0) { 599 | ret = Math.min(child.getWidth(), Math.max(left, 0)); 600 | } else if ((mTrackingEdge & EDGE_RIGHT) != 0) { 601 | ret = Math.min(0, Math.max(left, -child.getWidth())); 602 | } 603 | return ret; 604 | } 605 | 606 | @Override 607 | public int clampViewPositionVertical(View child, int top, int dy) { 608 | int ret = 0; 609 | if ((mTrackingEdge & EDGE_BOTTOM) != 0) { 610 | ret = Math.min(0, Math.max(top, -child.getHeight())); 611 | } 612 | return ret; 613 | } 614 | 615 | @Override 616 | public void onViewDragStateChanged(int state) { 617 | super.onViewDragStateChanged(state); 618 | if (mListeners != null && !mListeners.isEmpty()) { 619 | for (SwipeListener listener : mListeners) { 620 | listener.onScrollStateChange(state, mScrollPercent); 621 | } 622 | } 623 | } 624 | } 625 | } 626 | -------------------------------------------------------------------------------- /library/src/main/java/me/imid/swipebacklayout/lib/Utils.java: -------------------------------------------------------------------------------- 1 | 2 | package me.imid.swipebacklayout.lib; 3 | 4 | import android.app.Activity; 5 | import android.app.ActivityOptions; 6 | import android.os.Build; 7 | 8 | import java.lang.reflect.Method; 9 | 10 | /** 11 | * Created by Chaojun Wang on 6/9/14. 12 | */ 13 | public class Utils { 14 | private Utils() { 15 | } 16 | 17 | /** 18 | * Convert a translucent themed Activity 19 | * {@link android.R.attr#windowIsTranslucent} to a fullscreen opaque 20 | * Activity. 21 | *

22 | * Call this whenever the background of a translucent Activity has changed 23 | * to become opaque. Doing so will allow the {@link android.view.Surface} of 24 | * the Activity behind to be released. 25 | *

26 | * This call has no effect on non-translucent activities or on activities 27 | * with the {@link android.R.attr#windowIsFloating} attribute. 28 | */ 29 | public static void convertActivityFromTranslucent(Activity activity) { 30 | try { 31 | Method method = Activity.class.getDeclaredMethod("convertFromTranslucent"); 32 | method.setAccessible(true); 33 | method.invoke(activity); 34 | } catch (Throwable t) { 35 | } 36 | } 37 | 38 | /** 39 | * Convert a translucent themed Activity 40 | * {@link android.R.attr#windowIsTranslucent} back from opaque to 41 | * translucent following a call to 42 | * {@link #convertActivityFromTranslucent(android.app.Activity)} . 43 | *

44 | * Calling this allows the Activity behind this one to be seen again. Once 45 | * all such Activities have been redrawn 46 | *

47 | * This call has no effect on non-translucent activities or on activities 48 | * with the {@link android.R.attr#windowIsFloating} attribute. 49 | */ 50 | public static void convertActivityToTranslucent(Activity activity) { 51 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 52 | convertActivityToTranslucentAfterL(activity); 53 | } else { 54 | convertActivityToTranslucentBeforeL(activity); 55 | } 56 | } 57 | 58 | /** 59 | * Calling the convertToTranslucent method on platforms before Android 5.0 60 | */ 61 | public static void convertActivityToTranslucentBeforeL(Activity activity) { 62 | try { 63 | Class[] classes = Activity.class.getDeclaredClasses(); 64 | Class translucentConversionListenerClazz = null; 65 | for (Class clazz : classes) { 66 | if (clazz.getSimpleName().contains("TranslucentConversionListener")) { 67 | translucentConversionListenerClazz = clazz; 68 | } 69 | } 70 | Method method = Activity.class.getDeclaredMethod("convertToTranslucent", 71 | translucentConversionListenerClazz); 72 | method.setAccessible(true); 73 | method.invoke(activity, new Object[] { 74 | null 75 | }); 76 | } catch (Throwable t) { 77 | } 78 | } 79 | 80 | /** 81 | * Calling the convertToTranslucent method on platforms after Android 5.0 82 | */ 83 | private static void convertActivityToTranslucentAfterL(Activity activity) { 84 | try { 85 | Method getActivityOptions = Activity.class.getDeclaredMethod("getActivityOptions"); 86 | getActivityOptions.setAccessible(true); 87 | Object options = getActivityOptions.invoke(activity); 88 | 89 | Class[] classes = Activity.class.getDeclaredClasses(); 90 | Class translucentConversionListenerClazz = null; 91 | for (Class clazz : classes) { 92 | if (clazz.getSimpleName().contains("TranslucentConversionListener")) { 93 | translucentConversionListenerClazz = clazz; 94 | } 95 | } 96 | Method convertToTranslucent = Activity.class.getDeclaredMethod("convertToTranslucent", 97 | translucentConversionListenerClazz, ActivityOptions.class); 98 | convertToTranslucent.setAccessible(true); 99 | convertToTranslucent.invoke(activity, null, options); 100 | } catch (Throwable t) { 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /library/src/main/java/me/imid/swipebacklayout/lib/ViewDragHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 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 me.imid.swipebacklayout.lib; 18 | 19 | import android.content.Context; 20 | import android.support.v4.view.MotionEventCompat; 21 | import android.support.v4.view.VelocityTrackerCompat; 22 | import android.support.v4.view.ViewCompat; 23 | import android.support.v4.widget.ScrollerCompat; 24 | import android.view.MotionEvent; 25 | import android.view.VelocityTracker; 26 | import android.view.View; 27 | import android.view.ViewConfiguration; 28 | import android.view.ViewGroup; 29 | import android.view.animation.Interpolator; 30 | 31 | import java.util.Arrays; 32 | 33 | /** 34 | * ViewDragHelper is a utility class for writing custom ViewGroups. It offers a 35 | * number of useful operations and state tracking for allowing a user to drag 36 | * and reposition views within their parent ViewGroup. 37 | */ 38 | public class ViewDragHelper { 39 | private static final String TAG = "ViewDragHelper"; 40 | 41 | /** 42 | * A null/invalid pointer ID. 43 | */ 44 | public static final int INVALID_POINTER = -1; 45 | 46 | /** 47 | * A view is not currently being dragged or animating as a result of a 48 | * fling/snap. 49 | */ 50 | public static final int STATE_IDLE = 0; 51 | 52 | /** 53 | * A view is currently being dragged. The position is currently changing as 54 | * a result of user input or simulated user input. 55 | */ 56 | public static final int STATE_DRAGGING = 1; 57 | 58 | /** 59 | * A view is currently settling into place as a result of a fling or 60 | * predefined non-interactive motion. 61 | */ 62 | public static final int STATE_SETTLING = 2; 63 | 64 | /** 65 | * Edge flag indicating that the left edge should be affected. 66 | */ 67 | public static final int EDGE_LEFT = 1 << 0; 68 | 69 | /** 70 | * Edge flag indicating that the right edge should be affected. 71 | */ 72 | public static final int EDGE_RIGHT = 1 << 1; 73 | 74 | /** 75 | * Edge flag indicating that the top edge should be affected. 76 | */ 77 | public static final int EDGE_TOP = 1 << 2; 78 | 79 | /** 80 | * Edge flag indicating that the bottom edge should be affected. 81 | */ 82 | public static final int EDGE_BOTTOM = 1 << 3; 83 | 84 | /** 85 | * Edge flag set indicating all edges should be affected. 86 | */ 87 | public static final int EDGE_ALL = EDGE_LEFT | EDGE_TOP | EDGE_RIGHT | EDGE_BOTTOM; 88 | 89 | /** 90 | * Indicates that a check should occur along the horizontal axis 91 | */ 92 | public static final int DIRECTION_HORIZONTAL = 1 << 0; 93 | 94 | /** 95 | * Indicates that a check should occur along the vertical axis 96 | */ 97 | public static final int DIRECTION_VERTICAL = 1 << 1; 98 | 99 | /** 100 | * Indicates that a check should occur along all axes 101 | */ 102 | public static final int DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL; 103 | 104 | public static final int EDGE_SIZE = 20; // dp 105 | 106 | private static final int BASE_SETTLE_DURATION = 256; // ms 107 | 108 | private static final int MAX_SETTLE_DURATION = 600; // ms 109 | 110 | // Current drag state; idle, dragging or settling 111 | private int mDragState; 112 | 113 | // Distance to travel before a drag may begin 114 | private int mTouchSlop; 115 | 116 | // Last known position/pointer tracking 117 | private int mActivePointerId = INVALID_POINTER; 118 | 119 | private float[] mInitialMotionX; 120 | 121 | private float[] mInitialMotionY; 122 | 123 | private float[] mLastMotionX; 124 | 125 | private float[] mLastMotionY; 126 | 127 | private int[] mInitialEdgeTouched; 128 | 129 | private int[] mEdgeDragsInProgress; 130 | 131 | private int[] mEdgeDragsLocked; 132 | 133 | private int mPointersDown; 134 | 135 | private VelocityTracker mVelocityTracker; 136 | 137 | private float mMaxVelocity; 138 | 139 | private float mMinVelocity; 140 | 141 | private int mEdgeSize; 142 | 143 | private int mTrackingEdges; 144 | 145 | private ScrollerCompat mScroller; 146 | 147 | private final Callback mCallback; 148 | 149 | private View mCapturedView; 150 | 151 | private boolean mReleaseInProgress; 152 | 153 | private final ViewGroup mParentView; 154 | 155 | /** 156 | * A Callback is used as a communication channel with the ViewDragHelper 157 | * back to the parent view using it. on*methods are invoked on 158 | * siginficant events and several accessor methods are expected to provide 159 | * the ViewDragHelper with more information about the state of the parent 160 | * view upon request. The callback also makes decisions governing the range 161 | * and draggability of child views. 162 | */ 163 | public static abstract class Callback { 164 | /** 165 | * Called when the drag state changes. See the STATE_* 166 | * constants for more information. 167 | * 168 | * @param state The new drag state 169 | * @see #STATE_IDLE 170 | * @see #STATE_DRAGGING 171 | * @see #STATE_SETTLING 172 | */ 173 | public void onViewDragStateChanged(int state) { 174 | } 175 | 176 | /** 177 | * Called when the captured view's position changes as the result of a 178 | * drag or settle. 179 | * 180 | * @param changedView View whose position changed 181 | * @param left New X coordinate of the left edge of the view 182 | * @param top New Y coordinate of the top edge of the view 183 | * @param dx Change in X position from the last call 184 | * @param dy Change in Y position from the last call 185 | */ 186 | public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { 187 | } 188 | 189 | /** 190 | * Called when a child view is captured for dragging or settling. The ID 191 | * of the pointer currently dragging the captured view is supplied. If 192 | * activePointerId is identified as {@link #INVALID_POINTER} the capture 193 | * is programmatic instead of pointer-initiated. 194 | * 195 | * @param capturedChild Child view that was captured 196 | * @param activePointerId Pointer id tracking the child capture 197 | */ 198 | public void onViewCaptured(View capturedChild, int activePointerId) { 199 | } 200 | 201 | /** 202 | * Called when the child view is no longer being actively dragged. The 203 | * fling velocity is also supplied, if relevant. The velocity values may 204 | * be clamped to system minimums or maximums. 205 | *

206 | * Calling code may decide to fling or otherwise release the view to let 207 | * it settle into place. It should do so using 208 | * {@link #settleCapturedViewAt(int, int)} or 209 | * {@link #flingCapturedView(int, int, int, int)}. If the Callback 210 | * invokes one of these methods, the ViewDragHelper will enter 211 | * {@link #STATE_SETTLING} and the view capture will not fully end until 212 | * it comes to a complete stop. If neither of these methods is invoked 213 | * before onViewReleased returns, the view will stop in 214 | * place and the ViewDragHelper will return to {@link #STATE_IDLE}. 215 | *

216 | * 217 | * @param releasedChild The captured child view now being released 218 | * @param xvel X velocity of the pointer as it left the screen in pixels 219 | * per second. 220 | * @param yvel Y velocity of the pointer as it left the screen in pixels 221 | * per second. 222 | */ 223 | public void onViewReleased(View releasedChild, float xvel, float yvel) { 224 | } 225 | 226 | /** 227 | * Called when one of the subscribed edges in the parent view has been 228 | * touched by the user while no child view is currently captured. 229 | * 230 | * @param edgeFlags A combination of edge flags describing the edge(s) 231 | * currently touched 232 | * @param pointerId ID of the pointer touching the described edge(s) 233 | * @see #EDGE_LEFT 234 | * @see #EDGE_TOP 235 | * @see #EDGE_RIGHT 236 | * @see #EDGE_BOTTOM 237 | */ 238 | public void onEdgeTouched(int edgeFlags, int pointerId) { 239 | } 240 | 241 | /** 242 | * Called when the given edge may become locked. This can happen if an 243 | * edge drag was preliminarily rejected before beginning, but after 244 | * {@link #onEdgeTouched(int, int)} was called. This method should 245 | * return true to lock this edge or false to leave it unlocked. The 246 | * default behavior is to leave edges unlocked. 247 | * 248 | * @param edgeFlags A combination of edge flags describing the edge(s) 249 | * locked 250 | * @return true to lock the edge, false to leave it unlocked 251 | */ 252 | public boolean onEdgeLock(int edgeFlags) { 253 | return false; 254 | } 255 | 256 | /** 257 | * Called when the user has started a deliberate drag away from one of 258 | * the subscribed edges in the parent view while no child view is 259 | * currently captured. 260 | * 261 | * @param edgeFlags A combination of edge flags describing the edge(s) 262 | * dragged 263 | * @param pointerId ID of the pointer touching the described edge(s) 264 | * @see #EDGE_LEFT 265 | * @see #EDGE_TOP 266 | * @see #EDGE_RIGHT 267 | * @see #EDGE_BOTTOM 268 | */ 269 | public void onEdgeDragStarted(int edgeFlags, int pointerId) { 270 | } 271 | 272 | /** 273 | * Called to determine the Z-order of child views. 274 | * 275 | * @param index the ordered position to query for 276 | * @return index of the view that should be ordered at position 277 | * index 278 | */ 279 | public int getOrderedChildIndex(int index) { 280 | return index; 281 | } 282 | 283 | /** 284 | * Return the magnitude of a draggable child view's horizontal range of 285 | * motion in pixels. This method should return 0 for views that cannot 286 | * move horizontally. 287 | * 288 | * @param child Child view to check 289 | * @return range of horizontal motion in pixels 290 | */ 291 | public int getViewHorizontalDragRange(View child) { 292 | return 0; 293 | } 294 | 295 | /** 296 | * Return the magnitude of a draggable child view's vertical range of 297 | * motion in pixels. This method should return 0 for views that cannot 298 | * move vertically. 299 | * 300 | * @param child Child view to check 301 | * @return range of vertical motion in pixels 302 | */ 303 | public int getViewVerticalDragRange(View child) { 304 | return 0; 305 | } 306 | 307 | /** 308 | * Called when the user's input indicates that they want to capture the 309 | * given child view with the pointer indicated by pointerId. The 310 | * callback should return true if the user is permitted to drag the 311 | * given view with the indicated pointer. 312 | *

313 | * ViewDragHelper may call this method multiple times for the same view 314 | * even if the view is already captured; this indicates that a new 315 | * pointer is trying to take control of the view. 316 | *

317 | *

318 | * If this method returns true, a call to 319 | * {@link #onViewCaptured(android.view.View, int)} will follow if the 320 | * capture is successful. 321 | *

322 | * 323 | * @param child Child the user is attempting to capture 324 | * @param pointerId ID of the pointer attempting the capture 325 | * @return true if capture should be allowed, false otherwise 326 | */ 327 | public abstract boolean tryCaptureView(View child, int pointerId); 328 | 329 | /** 330 | * Restrict the motion of the dragged child view along the horizontal 331 | * axis. The default implementation does not allow horizontal motion; 332 | * the extending class must override this method and provide the desired 333 | * clamping. 334 | * 335 | * @param child Child view being dragged 336 | * @param left Attempted motion along the X axis 337 | * @param dx Proposed change in position for left 338 | * @return The new clamped position for left 339 | */ 340 | public int clampViewPositionHorizontal(View child, int left, int dx) { 341 | return 0; 342 | } 343 | 344 | /** 345 | * Restrict the motion of the dragged child view along the vertical 346 | * axis. The default implementation does not allow vertical motion; the 347 | * extending class must override this method and provide the desired 348 | * clamping. 349 | * 350 | * @param child Child view being dragged 351 | * @param top Attempted motion along the Y axis 352 | * @param dy Proposed change in position for top 353 | * @return The new clamped position for top 354 | */ 355 | public int clampViewPositionVertical(View child, int top, int dy) { 356 | return 0; 357 | } 358 | } 359 | 360 | /** 361 | * Interpolator defining the animation curve for mScroller 362 | */ 363 | private static final Interpolator sInterpolator = new Interpolator() { 364 | public float getInterpolation(float t) { 365 | t -= 1.0f; 366 | return t * t * t * t * t + 1.0f; 367 | } 368 | }; 369 | 370 | private final Runnable mSetIdleRunnable = new Runnable() { 371 | public void run() { 372 | setDragState(STATE_IDLE); 373 | } 374 | }; 375 | 376 | /** 377 | * Factory method to create a new ViewDragHelper. 378 | * 379 | * @param forParent Parent view to monitor 380 | * @param cb Callback to provide information and receive events 381 | * @return a new ViewDragHelper instance 382 | */ 383 | public static ViewDragHelper create(ViewGroup forParent, Callback cb) { 384 | return new ViewDragHelper(forParent.getContext(), forParent, cb); 385 | } 386 | 387 | /** 388 | * Factory method to create a new ViewDragHelper. 389 | * 390 | * @param forParent Parent view to monitor 391 | * @param sensitivity Multiplier for how sensitive the helper should be 392 | * about detecting the start of a drag. Larger values are more 393 | * sensitive. 1.0f is normal. 394 | * @param cb Callback to provide information and receive events 395 | * @return a new ViewDragHelper instance 396 | */ 397 | public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) { 398 | final ViewDragHelper helper = create(forParent, cb); 399 | helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity)); 400 | return helper; 401 | } 402 | 403 | /** 404 | * Apps should use ViewDragHelper.create() to get a new instance. This will 405 | * allow VDH to use internal compatibility implementations for different 406 | * platform versions. 407 | * 408 | * @param context Context to initialize config-dependent params from 409 | * @param forParent Parent view to monitor 410 | */ 411 | private ViewDragHelper(Context context, ViewGroup forParent, Callback cb) { 412 | if (forParent == null) { 413 | throw new IllegalArgumentException("Parent view may not be null"); 414 | } 415 | if (cb == null) { 416 | throw new IllegalArgumentException("Callback may not be null"); 417 | } 418 | 419 | mParentView = forParent; 420 | mCallback = cb; 421 | 422 | final ViewConfiguration vc = ViewConfiguration.get(context); 423 | final float density = context.getResources().getDisplayMetrics().density; 424 | mEdgeSize = (int) (EDGE_SIZE * density + 0.5f); 425 | 426 | mTouchSlop = vc.getScaledTouchSlop(); 427 | mMaxVelocity = vc.getScaledMaximumFlingVelocity(); 428 | mMinVelocity = vc.getScaledMinimumFlingVelocity(); 429 | mScroller = ScrollerCompat.create(context, sInterpolator); 430 | } 431 | 432 | /** 433 | * Sets the sensitivity of the dragger. 434 | * 435 | * @param context The application context. 436 | * @param sensitivity value between 0 and 1, the final value for touchSlop = 437 | * ViewConfiguration.getScaledTouchSlop * (1 / s); 438 | */ 439 | public void setSensitivity(Context context, float sensitivity) { 440 | float s = Math.max(0f, Math.min(1.0f, sensitivity)); 441 | ViewConfiguration viewConfiguration = ViewConfiguration.get(context); 442 | mTouchSlop = (int) (viewConfiguration.getScaledTouchSlop() * (1 / s)); 443 | } 444 | 445 | /** 446 | * Set the minimum velocity that will be detected as having a magnitude 447 | * greater than zero in pixels per second. Callback methods accepting a 448 | * velocity will be clamped appropriately. 449 | * 450 | * @param minVel minimum velocity to detect 451 | */ 452 | public void setMinVelocity(float minVel) { 453 | mMinVelocity = minVel; 454 | } 455 | 456 | /** 457 | * Set the max velocity that will be detected as having a magnitude 458 | * greater than zero in pixels per second. Callback methods accepting a 459 | * velocity will be clamped appropriately. 460 | * 461 | * @param maxVel max velocity to detect 462 | */ 463 | public void setMaxVelocity(float maxVel) { 464 | mMaxVelocity = maxVel; 465 | } 466 | 467 | /** 468 | * Return the currently configured minimum velocity. Any flings with a 469 | * magnitude less than this value in pixels per second. Callback methods 470 | * accepting a velocity will receive zero as a velocity value if the real 471 | * detected velocity was below this threshold. 472 | * 473 | * @return the minimum velocity that will be detected 474 | */ 475 | public float getMinVelocity() { 476 | return mMinVelocity; 477 | } 478 | 479 | /** 480 | * Retrieve the current drag state of this helper. This will return one of 481 | * {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}. 482 | * 483 | * @return The current drag state 484 | */ 485 | public int getViewDragState() { 486 | return mDragState; 487 | } 488 | 489 | /** 490 | * Enable edge tracking for the selected edges of the parent view. The 491 | * callback's 492 | * {@link me.imid.swipebacklayout.lib.ViewDragHelper.Callback#onEdgeTouched(int, int)} 493 | * and 494 | * {@link me.imid.swipebacklayout.lib.ViewDragHelper.Callback#onEdgeDragStarted(int, int)} 495 | * methods will only be invoked for edges for which edge tracking has been 496 | * enabled. 497 | * 498 | * @param edgeFlags Combination of edge flags describing the edges to watch 499 | * @see #EDGE_LEFT 500 | * @see #EDGE_TOP 501 | * @see #EDGE_RIGHT 502 | * @see #EDGE_BOTTOM 503 | */ 504 | public void setEdgeTrackingEnabled(int edgeFlags) { 505 | mTrackingEdges = edgeFlags; 506 | } 507 | 508 | /** 509 | * Return the size of an edge. This is the range in pixels along the edges 510 | * of this view that will actively detect edge touches or drags if edge 511 | * tracking is enabled. 512 | * 513 | * @return The size of an edge in pixels 514 | * @see #setEdgeTrackingEnabled(int) 515 | */ 516 | public int getEdgeSize() { 517 | return mEdgeSize; 518 | } 519 | 520 | /** 521 | * Set the size of an edge. This is the range in pixels along the edges of 522 | * this view that will actively detect edge touches or drags if edge 523 | * tracking is enabled. 524 | * 525 | * @param size The size of an edge in pixels 526 | */ 527 | public void setEdgeSize(int size) { 528 | mEdgeSize = size; 529 | } 530 | 531 | /** 532 | * Capture a specific child view for dragging within the parent. The 533 | * callback will be notified but 534 | * {@link me.imid.swipebacklayout.lib.ViewDragHelper.Callback#tryCaptureView(android.view.View, int)} 535 | * will not be asked permission to capture this view. 536 | * 537 | * @param childView Child view to capture 538 | * @param activePointerId ID of the pointer that is dragging the captured 539 | * child view 540 | */ 541 | public void captureChildView(View childView, int activePointerId) { 542 | if (childView.getParent() != mParentView) { 543 | throw new IllegalArgumentException("captureChildView: parameter must be a descendant " 544 | + "of the ViewDragHelper's tracked parent view (" + mParentView + ")"); 545 | } 546 | 547 | mCapturedView = childView; 548 | mActivePointerId = activePointerId; 549 | mCallback.onViewCaptured(childView, activePointerId); 550 | setDragState(STATE_DRAGGING); 551 | } 552 | 553 | /** 554 | * @return The currently captured view, or null if no view has been 555 | * captured. 556 | */ 557 | public View getCapturedView() { 558 | return mCapturedView; 559 | } 560 | 561 | /** 562 | * @return The ID of the pointer currently dragging the captured view, or 563 | * {@link #INVALID_POINTER}. 564 | */ 565 | public int getActivePointerId() { 566 | return mActivePointerId; 567 | } 568 | 569 | /** 570 | * @return The minimum distance in pixels that the user must travel to 571 | * initiate a drag 572 | */ 573 | public int getTouchSlop() { 574 | return mTouchSlop; 575 | } 576 | 577 | /** 578 | * The result of a call to this method is equivalent to 579 | * {@link #processTouchEvent(android.view.MotionEvent)} receiving an 580 | * ACTION_CANCEL event. 581 | */ 582 | public void cancel() { 583 | mActivePointerId = INVALID_POINTER; 584 | clearMotionHistory(); 585 | 586 | if (mVelocityTracker != null) { 587 | mVelocityTracker.recycle(); 588 | mVelocityTracker = null; 589 | } 590 | } 591 | 592 | /** 593 | * {@link #cancel()}, but also abort all motion in progress and snap to the 594 | * end of any animation. 595 | */ 596 | public void abort() { 597 | cancel(); 598 | if (mDragState == STATE_SETTLING) { 599 | final int oldX = mScroller.getCurrX(); 600 | final int oldY = mScroller.getCurrY(); 601 | mScroller.abortAnimation(); 602 | final int newX = mScroller.getCurrX(); 603 | final int newY = mScroller.getCurrY(); 604 | mCallback.onViewPositionChanged(mCapturedView, newX, newY, newX - oldX, newY - oldY); 605 | } 606 | setDragState(STATE_IDLE); 607 | } 608 | 609 | /** 610 | * Animate the view child to the given (left, top) position. If 611 | * this method returns true, the caller should invoke 612 | * {@link #continueSettling(boolean)} on each subsequent frame to continue 613 | * the motion until it returns false. If this method returns false there is 614 | * no further work to do to complete the movement. 615 | *

616 | * This operation does not count as a capture event, though 617 | * {@link #getCapturedView()} will still report the sliding view while the 618 | * slide is in progress. 619 | *

620 | * 621 | * @param child Child view to capture and animate 622 | * @param finalLeft Final left position of child 623 | * @param finalTop Final top position of child 624 | * @return true if animation should continue through 625 | * {@link #continueSettling(boolean)} calls 626 | */ 627 | public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop) { 628 | mCapturedView = child; 629 | mActivePointerId = INVALID_POINTER; 630 | 631 | return forceSettleCapturedViewAt(finalLeft, finalTop, 0, 0); 632 | } 633 | 634 | /** 635 | * Settle the captured view at the given (left, top) position. The 636 | * appropriate velocity from prior motion will be taken into account. If 637 | * this method returns true, the caller should invoke 638 | * {@link #continueSettling(boolean)} on each subsequent frame to continue 639 | * the motion until it returns false. If this method returns false there is 640 | * no further work to do to complete the movement. 641 | * 642 | * @param finalLeft Settled left edge position for the captured view 643 | * @param finalTop Settled top edge position for the captured view 644 | * @return true if animation should continue through 645 | * {@link #continueSettling(boolean)} calls 646 | */ 647 | public boolean settleCapturedViewAt(int finalLeft, int finalTop) { 648 | if (!mReleaseInProgress) { 649 | throw new IllegalStateException("Cannot settleCapturedViewAt outside of a call to " 650 | + "Callback#onViewReleased"); 651 | } 652 | 653 | return forceSettleCapturedViewAt(finalLeft, finalTop, 654 | (int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId), 655 | (int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId)); 656 | } 657 | 658 | /** 659 | * Settle the captured view at the given (left, top) position. 660 | * 661 | * @param finalLeft Target left position for the captured view 662 | * @param finalTop Target top position for the captured view 663 | * @param xvel Horizontal velocity 664 | * @param yvel Vertical velocity 665 | * @return true if animation should continue through 666 | * {@link #continueSettling(boolean)} calls 667 | */ 668 | private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) { 669 | final int startLeft = mCapturedView.getLeft(); 670 | final int startTop = mCapturedView.getTop(); 671 | final int dx = finalLeft - startLeft; 672 | final int dy = finalTop - startTop; 673 | 674 | if (dx == 0 && dy == 0) { 675 | // Nothing to do. Send callbacks, be done. 676 | mScroller.abortAnimation(); 677 | setDragState(STATE_IDLE); 678 | return false; 679 | } 680 | 681 | final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel); 682 | mScroller.startScroll(startLeft, startTop, dx, dy, duration); 683 | 684 | setDragState(STATE_SETTLING); 685 | return true; 686 | } 687 | 688 | private int computeSettleDuration(View child, int dx, int dy, int xvel, int yvel) { 689 | xvel = clampMag(xvel, (int) mMinVelocity, (int) mMaxVelocity); 690 | yvel = clampMag(yvel, (int) mMinVelocity, (int) mMaxVelocity); 691 | final int absDx = Math.abs(dx); 692 | final int absDy = Math.abs(dy); 693 | final int absXVel = Math.abs(xvel); 694 | final int absYVel = Math.abs(yvel); 695 | final int addedVel = absXVel + absYVel; 696 | final int addedDistance = absDx + absDy; 697 | 698 | final float xweight = xvel != 0 ? (float) absXVel / addedVel : (float) absDx 699 | / addedDistance; 700 | final float yweight = yvel != 0 ? (float) absYVel / addedVel : (float) absDy 701 | / addedDistance; 702 | 703 | int xduration = computeAxisDuration(dx, xvel, mCallback.getViewHorizontalDragRange(child)); 704 | int yduration = computeAxisDuration(dy, yvel, mCallback.getViewVerticalDragRange(child)); 705 | 706 | return (int) (xduration * xweight + yduration * yweight); 707 | } 708 | 709 | private int computeAxisDuration(int delta, int velocity, int motionRange) { 710 | if (delta == 0) { 711 | return 0; 712 | } 713 | 714 | final int width = mParentView.getWidth(); 715 | final int halfWidth = width / 2; 716 | final float distanceRatio = Math.min(1f, (float) Math.abs(delta) / width); 717 | final float distance = halfWidth + halfWidth 718 | * distanceInfluenceForSnapDuration(distanceRatio); 719 | 720 | int duration; 721 | velocity = Math.abs(velocity); 722 | if (velocity > 0) { 723 | duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); 724 | } else { 725 | final float range = (float) Math.abs(delta) / motionRange; 726 | duration = (int) ((range + 1) * BASE_SETTLE_DURATION); 727 | } 728 | return Math.min(duration, MAX_SETTLE_DURATION); 729 | } 730 | 731 | /** 732 | * Clamp the magnitude of value for absMin and absMax. If the value is below 733 | * the minimum, it will be clamped to zero. If the value is above the 734 | * maximum, it will be clamped to the maximum. 735 | * 736 | * @param value Value to clamp 737 | * @param absMin Absolute value of the minimum significant value to return 738 | * @param absMax Absolute value of the maximum value to return 739 | * @return The clamped value with the same sign as value 740 | */ 741 | private int clampMag(int value, int absMin, int absMax) { 742 | final int absValue = Math.abs(value); 743 | if (absValue < absMin) 744 | return 0; 745 | if (absValue > absMax) 746 | return value > 0 ? absMax : -absMax; 747 | return value; 748 | } 749 | 750 | /** 751 | * Clamp the magnitude of value for absMin and absMax. If the value is below 752 | * the minimum, it will be clamped to zero. If the value is above the 753 | * maximum, it will be clamped to the maximum. 754 | * 755 | * @param value Value to clamp 756 | * @param absMin Absolute value of the minimum significant value to return 757 | * @param absMax Absolute value of the maximum value to return 758 | * @return The clamped value with the same sign as value 759 | */ 760 | private float clampMag(float value, float absMin, float absMax) { 761 | final float absValue = Math.abs(value); 762 | if (absValue < absMin) 763 | return 0; 764 | if (absValue > absMax) 765 | return value > 0 ? absMax : -absMax; 766 | return value; 767 | } 768 | 769 | private float distanceInfluenceForSnapDuration(float f) { 770 | f -= 0.5f; // center the values about 0. 771 | f *= 0.3f * Math.PI / 2.0f; 772 | return (float) Math.sin(f); 773 | } 774 | 775 | /** 776 | * Settle the captured view based on standard free-moving fling behavior. 777 | * The caller should invoke {@link #continueSettling(boolean)} on each 778 | * subsequent frame to continue the motion until it returns false. 779 | * 780 | * @param minLeft Minimum X position for the view's left edge 781 | * @param minTop Minimum Y position for the view's top edge 782 | * @param maxLeft Maximum X position for the view's left edge 783 | * @param maxTop Maximum Y position for the view's top edge 784 | */ 785 | public void flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop) { 786 | if (!mReleaseInProgress) { 787 | throw new IllegalStateException("Cannot flingCapturedView outside of a call to " 788 | + "Callback#onViewReleased"); 789 | } 790 | 791 | mScroller.fling(mCapturedView.getLeft(), mCapturedView.getTop(), 792 | (int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId), 793 | (int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId), 794 | minLeft, maxLeft, minTop, maxTop); 795 | 796 | setDragState(STATE_SETTLING); 797 | } 798 | 799 | /** 800 | * Move the captured settling view by the appropriate amount for the current 801 | * time. If continueSettling returns true, the caller should 802 | * call it again on the next frame to continue. 803 | * 804 | * @param deferCallbacks true if state callbacks should be deferred via 805 | * posted message. Set this to true if you are calling this 806 | * method from {@link android.view.View#computeScroll()} or 807 | * similar methods invoked as part of layout or drawing. 808 | * @return true if settle is still in progress 809 | */ 810 | public boolean continueSettling(boolean deferCallbacks) { 811 | if (mDragState == STATE_SETTLING) { 812 | boolean keepGoing = mScroller.computeScrollOffset(); 813 | final int x = mScroller.getCurrX(); 814 | final int y = mScroller.getCurrY(); 815 | final int dx = x - mCapturedView.getLeft(); 816 | final int dy = y - mCapturedView.getTop(); 817 | 818 | if (dx != 0) { 819 | mCapturedView.offsetLeftAndRight(dx); 820 | } 821 | if (dy != 0) { 822 | mCapturedView.offsetTopAndBottom(dy); 823 | } 824 | 825 | if (dx != 0 || dy != 0) { 826 | mCallback.onViewPositionChanged(mCapturedView, x, y, dx, dy); 827 | } 828 | 829 | if (keepGoing && x == mScroller.getFinalX() && y == mScroller.getFinalY()) { 830 | // Close enough. The interpolator/scroller might think we're 831 | // still moving 832 | // but the user sure doesn't. 833 | mScroller.abortAnimation(); 834 | keepGoing = mScroller.isFinished(); 835 | } 836 | 837 | if (!keepGoing) { 838 | if (deferCallbacks) { 839 | mParentView.post(mSetIdleRunnable); 840 | } else { 841 | setDragState(STATE_IDLE); 842 | } 843 | } 844 | } 845 | 846 | return mDragState == STATE_SETTLING; 847 | } 848 | 849 | /** 850 | * Like all callback events this must happen on the UI thread, but release 851 | * involves some extra semantics. During a release (mReleaseInProgress) is 852 | * the only time it is valid to call {@link #settleCapturedViewAt(int, int)} 853 | * or {@link #flingCapturedView(int, int, int, int)}. 854 | */ 855 | private void dispatchViewReleased(float xvel, float yvel) { 856 | mReleaseInProgress = true; 857 | mCallback.onViewReleased(mCapturedView, xvel, yvel); 858 | mReleaseInProgress = false; 859 | 860 | if (mDragState == STATE_DRAGGING) { 861 | // onViewReleased didn't call a method that would have changed this. 862 | // Go idle. 863 | setDragState(STATE_IDLE); 864 | } 865 | } 866 | 867 | private void clearMotionHistory() { 868 | if (mInitialMotionX == null) { 869 | return; 870 | } 871 | Arrays.fill(mInitialMotionX, 0); 872 | Arrays.fill(mInitialMotionY, 0); 873 | Arrays.fill(mLastMotionX, 0); 874 | Arrays.fill(mLastMotionY, 0); 875 | Arrays.fill(mInitialEdgeTouched, 0); 876 | Arrays.fill(mEdgeDragsInProgress, 0); 877 | Arrays.fill(mEdgeDragsLocked, 0); 878 | mPointersDown = 0; 879 | } 880 | 881 | private void clearMotionHistory(int pointerId) { 882 | if (mInitialMotionX == null) { 883 | return; 884 | } 885 | mInitialMotionX[pointerId] = 0; 886 | mInitialMotionY[pointerId] = 0; 887 | mLastMotionX[pointerId] = 0; 888 | mLastMotionY[pointerId] = 0; 889 | mInitialEdgeTouched[pointerId] = 0; 890 | mEdgeDragsInProgress[pointerId] = 0; 891 | mEdgeDragsLocked[pointerId] = 0; 892 | mPointersDown &= ~(1 << pointerId); 893 | } 894 | 895 | private void ensureMotionHistorySizeForId(int pointerId) { 896 | if (mInitialMotionX == null || mInitialMotionX.length <= pointerId) { 897 | float[] imx = new float[pointerId + 1]; 898 | float[] imy = new float[pointerId + 1]; 899 | float[] lmx = new float[pointerId + 1]; 900 | float[] lmy = new float[pointerId + 1]; 901 | int[] iit = new int[pointerId + 1]; 902 | int[] edip = new int[pointerId + 1]; 903 | int[] edl = new int[pointerId + 1]; 904 | 905 | if (mInitialMotionX != null) { 906 | System.arraycopy(mInitialMotionX, 0, imx, 0, mInitialMotionX.length); 907 | System.arraycopy(mInitialMotionY, 0, imy, 0, mInitialMotionY.length); 908 | System.arraycopy(mLastMotionX, 0, lmx, 0, mLastMotionX.length); 909 | System.arraycopy(mLastMotionY, 0, lmy, 0, mLastMotionY.length); 910 | System.arraycopy(mInitialEdgeTouched, 0, iit, 0, mInitialEdgeTouched.length); 911 | System.arraycopy(mEdgeDragsInProgress, 0, edip, 0, mEdgeDragsInProgress.length); 912 | System.arraycopy(mEdgeDragsLocked, 0, edl, 0, mEdgeDragsLocked.length); 913 | } 914 | 915 | mInitialMotionX = imx; 916 | mInitialMotionY = imy; 917 | mLastMotionX = lmx; 918 | mLastMotionY = lmy; 919 | mInitialEdgeTouched = iit; 920 | mEdgeDragsInProgress = edip; 921 | mEdgeDragsLocked = edl; 922 | } 923 | } 924 | 925 | private void saveInitialMotion(float x, float y, int pointerId) { 926 | ensureMotionHistorySizeForId(pointerId); 927 | mInitialMotionX[pointerId] = mLastMotionX[pointerId] = x; 928 | mInitialMotionY[pointerId] = mLastMotionY[pointerId] = y; 929 | mInitialEdgeTouched[pointerId] = getEdgeTouched((int) x, (int) y); 930 | mPointersDown |= 1 << pointerId; 931 | } 932 | 933 | private void saveLastMotion(MotionEvent ev) { 934 | final int pointerCount = MotionEventCompat.getPointerCount(ev); 935 | for (int i = 0; i < pointerCount; i++) { 936 | final int pointerId = MotionEventCompat.getPointerId(ev, i); 937 | final float x = MotionEventCompat.getX(ev, i); 938 | final float y = MotionEventCompat.getY(ev, i); 939 | mLastMotionX[pointerId] = x; 940 | mLastMotionY[pointerId] = y; 941 | } 942 | } 943 | 944 | /** 945 | * Check if the given pointer ID represents a pointer that is currently down 946 | * (to the best of the ViewDragHelper's knowledge). 947 | *

948 | * The state used to report this information is populated by the methods 949 | * {@link #shouldInterceptTouchEvent(android.view.MotionEvent)} or 950 | * {@link #processTouchEvent(android.view.MotionEvent)}. If one of these 951 | * methods has not been called for all relevant MotionEvents to track, the 952 | * information reported by this method may be stale or incorrect. 953 | *

954 | * 955 | * @param pointerId pointer ID to check; corresponds to IDs provided by 956 | * MotionEvent 957 | * @return true if the pointer with the given ID is still down 958 | */ 959 | public boolean isPointerDown(int pointerId) { 960 | return (mPointersDown & 1 << pointerId) != 0; 961 | } 962 | 963 | void setDragState(int state) { 964 | if (mDragState != state) { 965 | mDragState = state; 966 | mCallback.onViewDragStateChanged(state); 967 | if (state == STATE_IDLE) { 968 | mCapturedView = null; 969 | } 970 | } 971 | } 972 | 973 | /** 974 | * Attempt to capture the view with the given pointer ID. The callback will 975 | * be involved. This will put us into the "dragging" state. If we've already 976 | * captured this view with this pointer this method will immediately return 977 | * true without consulting the callback. 978 | * 979 | * @param toCapture View to capture 980 | * @param pointerId Pointer to capture with 981 | * @return true if capture was successful 982 | */ 983 | boolean tryCaptureViewForDrag(View toCapture, int pointerId) { 984 | if (toCapture == mCapturedView && mActivePointerId == pointerId) { 985 | // Already done! 986 | return true; 987 | } 988 | if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) { 989 | mActivePointerId = pointerId; 990 | captureChildView(toCapture, pointerId); 991 | return true; 992 | } 993 | return false; 994 | } 995 | 996 | /** 997 | * Tests scrollability within child views of v given a delta of dx. 998 | * 999 | * @param v View to test for horizontal scrollability 1000 | * @param checkV Whether the view v passed should itself be checked for 1001 | * scrollability (true), or just its children (false). 1002 | * @param dx Delta scrolled in pixels along the X axis 1003 | * @param dy Delta scrolled in pixels along the Y axis 1004 | * @param x X coordinate of the active touch point 1005 | * @param y Y coordinate of the active touch point 1006 | * @return true if child views of v can be scrolled by delta of dx. 1007 | */ 1008 | protected boolean canScroll(View v, boolean checkV, int dx, int dy, int x, int y) { 1009 | if (v instanceof ViewGroup) { 1010 | final ViewGroup group = (ViewGroup) v; 1011 | final int scrollX = v.getScrollX(); 1012 | final int scrollY = v.getScrollY(); 1013 | final int count = group.getChildCount(); 1014 | // Count backwards - let topmost views consume scroll distance 1015 | // first. 1016 | for (int i = count - 1; i >= 0; i--) { 1017 | // TODO: Add versioned support here for transformed views. 1018 | // This will not work for transformed views in Honeycomb+ 1019 | final View child = group.getChildAt(i); 1020 | if (x + scrollX >= child.getLeft() 1021 | && x + scrollX < child.getRight() 1022 | && y + scrollY >= child.getTop() 1023 | && y + scrollY < child.getBottom() 1024 | && canScroll(child, true, dx, dy, x + scrollX - child.getLeft(), y 1025 | + scrollY - child.getTop())) { 1026 | return true; 1027 | } 1028 | } 1029 | } 1030 | 1031 | return checkV 1032 | && (ViewCompat.canScrollHorizontally(v, -dx) || ViewCompat.canScrollVertically(v, 1033 | -dy)); 1034 | } 1035 | 1036 | /** 1037 | * Check if this event as provided to the parent view's 1038 | * onInterceptTouchEvent should cause the parent to intercept the touch 1039 | * event stream. 1040 | * 1041 | * @param ev MotionEvent provided to onInterceptTouchEvent 1042 | * @return true if the parent view should return true from 1043 | * onInterceptTouchEvent 1044 | */ 1045 | public boolean shouldInterceptTouchEvent(MotionEvent ev) { 1046 | final int action = MotionEventCompat.getActionMasked(ev); 1047 | final int actionIndex = MotionEventCompat.getActionIndex(ev); 1048 | 1049 | if (action == MotionEvent.ACTION_DOWN) { 1050 | // Reset things for a new event stream, just in case we didn't get 1051 | // the whole previous stream. 1052 | cancel(); 1053 | } 1054 | 1055 | if (mVelocityTracker == null) { 1056 | mVelocityTracker = VelocityTracker.obtain(); 1057 | } 1058 | mVelocityTracker.addMovement(ev); 1059 | 1060 | switch (action) { 1061 | case MotionEvent.ACTION_DOWN: { 1062 | final float x = ev.getX(); 1063 | final float y = ev.getY(); 1064 | final int pointerId = MotionEventCompat.getPointerId(ev, 0); 1065 | saveInitialMotion(x, y, pointerId); 1066 | 1067 | final View toCapture = findTopChildUnder((int) x, (int) y); 1068 | 1069 | // Catch a settling view if possible. 1070 | if (toCapture == mCapturedView && mDragState == STATE_SETTLING) { 1071 | tryCaptureViewForDrag(toCapture, pointerId); 1072 | } 1073 | 1074 | final int edgesTouched = mInitialEdgeTouched[pointerId]; 1075 | if ((edgesTouched & mTrackingEdges) != 0) { 1076 | mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); 1077 | } 1078 | break; 1079 | } 1080 | 1081 | case MotionEventCompat.ACTION_POINTER_DOWN: { 1082 | final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex); 1083 | final float x = MotionEventCompat.getX(ev, actionIndex); 1084 | final float y = MotionEventCompat.getY(ev, actionIndex); 1085 | 1086 | saveInitialMotion(x, y, pointerId); 1087 | 1088 | // A ViewDragHelper can only manipulate one view at a time. 1089 | if (mDragState == STATE_IDLE) { 1090 | final int edgesTouched = mInitialEdgeTouched[pointerId]; 1091 | if ((edgesTouched & mTrackingEdges) != 0) { 1092 | mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); 1093 | } 1094 | } else if (mDragState == STATE_SETTLING) { 1095 | // Catch a settling view if possible. 1096 | final View toCapture = findTopChildUnder((int) x, (int) y); 1097 | if (toCapture == mCapturedView) { 1098 | tryCaptureViewForDrag(toCapture, pointerId); 1099 | } 1100 | } 1101 | break; 1102 | } 1103 | 1104 | case MotionEvent.ACTION_MOVE: { 1105 | // First to cross a touch slop over a draggable view wins. Also 1106 | // report edge drags. 1107 | final int pointerCount = MotionEventCompat.getPointerCount(ev); 1108 | for (int i = 0; i < pointerCount; i++) { 1109 | final int pointerId = MotionEventCompat.getPointerId(ev, i); 1110 | final float x = MotionEventCompat.getX(ev, i); 1111 | final float y = MotionEventCompat.getY(ev, i); 1112 | final float dx = x - mInitialMotionX[pointerId]; 1113 | final float dy = y - mInitialMotionY[pointerId]; 1114 | 1115 | reportNewEdgeDrags(dx, dy, pointerId); 1116 | if (mDragState == STATE_DRAGGING) { 1117 | // Callback might have started an edge drag 1118 | break; 1119 | } 1120 | 1121 | final View toCapture = findTopChildUnder((int) x, (int) y); 1122 | if (toCapture != null && checkTouchSlop(toCapture, dx, dy) 1123 | && tryCaptureViewForDrag(toCapture, pointerId)) { 1124 | break; 1125 | } 1126 | } 1127 | saveLastMotion(ev); 1128 | break; 1129 | } 1130 | 1131 | case MotionEventCompat.ACTION_POINTER_UP: { 1132 | final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex); 1133 | clearMotionHistory(pointerId); 1134 | break; 1135 | } 1136 | 1137 | case MotionEvent.ACTION_UP: 1138 | case MotionEvent.ACTION_CANCEL: { 1139 | cancel(); 1140 | break; 1141 | } 1142 | } 1143 | 1144 | return mDragState == STATE_DRAGGING; 1145 | } 1146 | 1147 | /** 1148 | * Process a touch event received by the parent view. This method will 1149 | * dispatch callback events as needed before returning. The parent view's 1150 | * onTouchEvent implementation should call this. 1151 | * 1152 | * @param ev The touch event received by the parent view 1153 | */ 1154 | public void processTouchEvent(MotionEvent ev) { 1155 | final int action = MotionEventCompat.getActionMasked(ev); 1156 | final int actionIndex = MotionEventCompat.getActionIndex(ev); 1157 | 1158 | if (action == MotionEvent.ACTION_DOWN) { 1159 | // Reset things for a new event stream, just in case we didn't get 1160 | // the whole previous stream. 1161 | cancel(); 1162 | } 1163 | 1164 | if (mVelocityTracker == null) { 1165 | mVelocityTracker = VelocityTracker.obtain(); 1166 | } 1167 | mVelocityTracker.addMovement(ev); 1168 | 1169 | switch (action) { 1170 | case MotionEvent.ACTION_DOWN: { 1171 | final float x = ev.getX(); 1172 | final float y = ev.getY(); 1173 | final int pointerId = MotionEventCompat.getPointerId(ev, 0); 1174 | final View toCapture = findTopChildUnder((int) x, (int) y); 1175 | 1176 | saveInitialMotion(x, y, pointerId); 1177 | 1178 | // Since the parent is already directly processing this touch 1179 | // event, 1180 | // there is no reason to delay for a slop before dragging. 1181 | // Start immediately if possible. 1182 | tryCaptureViewForDrag(toCapture, pointerId); 1183 | 1184 | final int edgesTouched = mInitialEdgeTouched[pointerId]; 1185 | if ((edgesTouched & mTrackingEdges) != 0) { 1186 | mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); 1187 | } 1188 | break; 1189 | } 1190 | 1191 | case MotionEventCompat.ACTION_POINTER_DOWN: { 1192 | final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex); 1193 | final float x = MotionEventCompat.getX(ev, actionIndex); 1194 | final float y = MotionEventCompat.getY(ev, actionIndex); 1195 | 1196 | saveInitialMotion(x, y, pointerId); 1197 | 1198 | // A ViewDragHelper can only manipulate one view at a time. 1199 | if (mDragState == STATE_IDLE) { 1200 | // If we're idle we can do anything! Treat it like a normal 1201 | // down event. 1202 | 1203 | final View toCapture = findTopChildUnder((int) x, (int) y); 1204 | tryCaptureViewForDrag(toCapture, pointerId); 1205 | 1206 | final int edgesTouched = mInitialEdgeTouched[pointerId]; 1207 | if ((edgesTouched & mTrackingEdges) != 0) { 1208 | mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); 1209 | } 1210 | } else if (isCapturedViewUnder((int) x, (int) y)) { 1211 | // We're still tracking a captured view. If the same view is 1212 | // under this 1213 | // point, we'll swap to controlling it with this pointer 1214 | // instead. 1215 | // (This will still work if we're "catching" a settling 1216 | // view.) 1217 | 1218 | tryCaptureViewForDrag(mCapturedView, pointerId); 1219 | } 1220 | break; 1221 | } 1222 | 1223 | case MotionEvent.ACTION_MOVE: { 1224 | if (mDragState == STATE_DRAGGING) { 1225 | final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId); 1226 | final float x = MotionEventCompat.getX(ev, index); 1227 | final float y = MotionEventCompat.getY(ev, index); 1228 | final int idx = (int) (x - mLastMotionX[mActivePointerId]); 1229 | final int idy = (int) (y - mLastMotionY[mActivePointerId]); 1230 | 1231 | dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy); 1232 | 1233 | saveLastMotion(ev); 1234 | } else { 1235 | // Check to see if any pointer is now over a draggable view. 1236 | final int pointerCount = MotionEventCompat.getPointerCount(ev); 1237 | for (int i = 0; i < pointerCount; i++) { 1238 | final int pointerId = MotionEventCompat.getPointerId(ev, i); 1239 | final float x = MotionEventCompat.getX(ev, i); 1240 | final float y = MotionEventCompat.getY(ev, i); 1241 | final float dx = x - mInitialMotionX[pointerId]; 1242 | final float dy = y - mInitialMotionY[pointerId]; 1243 | 1244 | reportNewEdgeDrags(dx, dy, pointerId); 1245 | if (mDragState == STATE_DRAGGING) { 1246 | // Callback might have started an edge drag. 1247 | break; 1248 | } 1249 | 1250 | final View toCapture = findTopChildUnder((int) x, (int) y); 1251 | if (checkTouchSlop(toCapture, dx, dy) 1252 | && tryCaptureViewForDrag(toCapture, pointerId)) { 1253 | break; 1254 | } 1255 | } 1256 | saveLastMotion(ev); 1257 | } 1258 | break; 1259 | } 1260 | 1261 | case MotionEventCompat.ACTION_POINTER_UP: { 1262 | final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex); 1263 | if (mDragState == STATE_DRAGGING && pointerId == mActivePointerId) { 1264 | // Try to find another pointer that's still holding on to 1265 | // the captured view. 1266 | int newActivePointer = INVALID_POINTER; 1267 | final int pointerCount = MotionEventCompat.getPointerCount(ev); 1268 | for (int i = 0; i < pointerCount; i++) { 1269 | final int id = MotionEventCompat.getPointerId(ev, i); 1270 | if (id == mActivePointerId) { 1271 | // This one's going away, skip. 1272 | continue; 1273 | } 1274 | 1275 | final float x = MotionEventCompat.getX(ev, i); 1276 | final float y = MotionEventCompat.getY(ev, i); 1277 | if (findTopChildUnder((int) x, (int) y) == mCapturedView 1278 | && tryCaptureViewForDrag(mCapturedView, id)) { 1279 | newActivePointer = mActivePointerId; 1280 | break; 1281 | } 1282 | } 1283 | 1284 | if (newActivePointer == INVALID_POINTER) { 1285 | // We didn't find another pointer still touching the 1286 | // view, release it. 1287 | releaseViewForPointerUp(); 1288 | } 1289 | } 1290 | clearMotionHistory(pointerId); 1291 | break; 1292 | } 1293 | 1294 | case MotionEvent.ACTION_UP: { 1295 | if (mDragState == STATE_DRAGGING) { 1296 | releaseViewForPointerUp(); 1297 | } 1298 | cancel(); 1299 | break; 1300 | } 1301 | 1302 | case MotionEvent.ACTION_CANCEL: { 1303 | if (mDragState == STATE_DRAGGING) { 1304 | dispatchViewReleased(0, 0); 1305 | } 1306 | cancel(); 1307 | break; 1308 | } 1309 | } 1310 | } 1311 | 1312 | private void reportNewEdgeDrags(float dx, float dy, int pointerId) { 1313 | int dragsStarted = 0; 1314 | if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_LEFT)) { 1315 | dragsStarted |= EDGE_LEFT; 1316 | } 1317 | if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_TOP)) { 1318 | dragsStarted |= EDGE_TOP; 1319 | } 1320 | if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_RIGHT)) { 1321 | dragsStarted |= EDGE_RIGHT; 1322 | } 1323 | if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_BOTTOM)) { 1324 | dragsStarted |= EDGE_BOTTOM; 1325 | } 1326 | 1327 | if (dragsStarted != 0) { 1328 | mEdgeDragsInProgress[pointerId] |= dragsStarted; 1329 | mCallback.onEdgeDragStarted(dragsStarted, pointerId); 1330 | } 1331 | } 1332 | 1333 | private boolean checkNewEdgeDrag(float delta, float odelta, int pointerId, int edge) { 1334 | final float absDelta = Math.abs(delta); 1335 | final float absODelta = Math.abs(odelta); 1336 | 1337 | if ((mInitialEdgeTouched[pointerId] & edge) != edge || (mTrackingEdges & edge) == 0 1338 | || (mEdgeDragsLocked[pointerId] & edge) == edge 1339 | || (mEdgeDragsInProgress[pointerId] & edge) == edge 1340 | || (absDelta <= mTouchSlop && absODelta <= mTouchSlop)) { 1341 | return false; 1342 | } 1343 | if (absDelta < absODelta * 0.5f && mCallback.onEdgeLock(edge)) { 1344 | mEdgeDragsLocked[pointerId] |= edge; 1345 | return false; 1346 | } 1347 | return (mEdgeDragsInProgress[pointerId] & edge) == 0 && absDelta > mTouchSlop; 1348 | } 1349 | 1350 | /** 1351 | * Check if we've crossed a reasonable touch slop for the given child view. 1352 | * If the child cannot be dragged along the horizontal or vertical axis, 1353 | * motion along that axis will not count toward the slop check. 1354 | * 1355 | * @param child Child to check 1356 | * @param dx Motion since initial position along X axis 1357 | * @param dy Motion since initial position along Y axis 1358 | * @return true if the touch slop has been crossed 1359 | */ 1360 | private boolean checkTouchSlop(View child, float dx, float dy) { 1361 | if (child == null) { 1362 | return false; 1363 | } 1364 | final boolean checkHorizontal = mCallback.getViewHorizontalDragRange(child) > 0; 1365 | final boolean checkVertical = mCallback.getViewVerticalDragRange(child) > 0; 1366 | 1367 | if (checkHorizontal && checkVertical) { 1368 | return dx * dx + dy * dy > mTouchSlop * mTouchSlop; 1369 | } else if (checkHorizontal) { 1370 | return Math.abs(dx) > mTouchSlop; 1371 | } else if (checkVertical) { 1372 | return Math.abs(dy) > mTouchSlop; 1373 | } 1374 | return false; 1375 | } 1376 | 1377 | /** 1378 | * Check if any pointer tracked in the current gesture has crossed the 1379 | * required slop threshold. 1380 | *

1381 | * This depends on internal state populated by 1382 | * {@link #shouldInterceptTouchEvent(android.view.MotionEvent)} or 1383 | * {@link #processTouchEvent(android.view.MotionEvent)}. You should only 1384 | * rely on the results of this method after all currently available touch 1385 | * data has been provided to one of these two methods. 1386 | *

1387 | * 1388 | * @param directions Combination of direction flags, see 1389 | * {@link #DIRECTION_HORIZONTAL}, {@link #DIRECTION_VERTICAL}, 1390 | * {@link #DIRECTION_ALL} 1391 | * @return true if the slop threshold has been crossed, false otherwise 1392 | */ 1393 | public boolean checkTouchSlop(int directions) { 1394 | final int count = mInitialMotionX.length; 1395 | for (int i = 0; i < count; i++) { 1396 | if (checkTouchSlop(directions, i)) { 1397 | return true; 1398 | } 1399 | } 1400 | return false; 1401 | } 1402 | 1403 | /** 1404 | * Check if the specified pointer tracked in the current gesture has crossed 1405 | * the required slop threshold. 1406 | *

1407 | * This depends on internal state populated by 1408 | * {@link #shouldInterceptTouchEvent(android.view.MotionEvent)} or 1409 | * {@link #processTouchEvent(android.view.MotionEvent)}. You should only 1410 | * rely on the results of this method after all currently available touch 1411 | * data has been provided to one of these two methods. 1412 | *

1413 | * 1414 | * @param directions Combination of direction flags, see 1415 | * {@link #DIRECTION_HORIZONTAL}, {@link #DIRECTION_VERTICAL}, 1416 | * {@link #DIRECTION_ALL} 1417 | * @param pointerId ID of the pointer to slop check as specified by 1418 | * MotionEvent 1419 | * @return true if the slop threshold has been crossed, false otherwise 1420 | */ 1421 | public boolean checkTouchSlop(int directions, int pointerId) { 1422 | if (!isPointerDown(pointerId)) { 1423 | return false; 1424 | } 1425 | 1426 | final boolean checkHorizontal = (directions & DIRECTION_HORIZONTAL) == DIRECTION_HORIZONTAL; 1427 | final boolean checkVertical = (directions & DIRECTION_VERTICAL) == DIRECTION_VERTICAL; 1428 | 1429 | final float dx = mLastMotionX[pointerId] - mInitialMotionX[pointerId]; 1430 | final float dy = mLastMotionY[pointerId] - mInitialMotionY[pointerId]; 1431 | 1432 | if (checkHorizontal && checkVertical) { 1433 | return dx * dx + dy * dy > mTouchSlop * mTouchSlop; 1434 | } else if (checkHorizontal) { 1435 | return Math.abs(dx) > mTouchSlop; 1436 | } else if (checkVertical) { 1437 | return Math.abs(dy) > mTouchSlop; 1438 | } 1439 | return false; 1440 | } 1441 | 1442 | /** 1443 | * Check if any of the edges specified were initially touched in the 1444 | * currently active gesture. If there is no currently active gesture this 1445 | * method will return false. 1446 | * 1447 | * @param edges Edges to check for an initial edge touch. See 1448 | * {@link #EDGE_LEFT}, {@link #EDGE_TOP}, {@link #EDGE_RIGHT}, 1449 | * {@link #EDGE_BOTTOM} and {@link #EDGE_ALL} 1450 | * @return true if any of the edges specified were initially touched in the 1451 | * current gesture 1452 | */ 1453 | public boolean isEdgeTouched(int edges) { 1454 | final int count = mInitialEdgeTouched.length; 1455 | for (int i = 0; i < count; i++) { 1456 | if (isEdgeTouched(edges, i)) { 1457 | return true; 1458 | } 1459 | } 1460 | return false; 1461 | } 1462 | 1463 | /** 1464 | * Check if any of the edges specified were initially touched by the pointer 1465 | * with the specified ID. If there is no currently active gesture or if 1466 | * there is no pointer with the given ID currently down this method will 1467 | * return false. 1468 | * 1469 | * @param edges Edges to check for an initial edge touch. See 1470 | * {@link #EDGE_LEFT}, {@link #EDGE_TOP}, {@link #EDGE_RIGHT}, 1471 | * {@link #EDGE_BOTTOM} and {@link #EDGE_ALL} 1472 | * @return true if any of the edges specified were initially touched in the 1473 | * current gesture 1474 | */ 1475 | public boolean isEdgeTouched(int edges, int pointerId) { 1476 | return isPointerDown(pointerId) && (mInitialEdgeTouched[pointerId] & edges) != 0; 1477 | } 1478 | 1479 | private void releaseViewForPointerUp() { 1480 | mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity); 1481 | final float xvel = clampMag( 1482 | VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId), 1483 | mMinVelocity, mMaxVelocity); 1484 | final float yvel = clampMag( 1485 | VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId), 1486 | mMinVelocity, mMaxVelocity); 1487 | dispatchViewReleased(xvel, yvel); 1488 | } 1489 | 1490 | private void dragTo(int left, int top, int dx, int dy) { 1491 | int clampedX = left; 1492 | int clampedY = top; 1493 | final int oldLeft = mCapturedView.getLeft(); 1494 | final int oldTop = mCapturedView.getTop(); 1495 | if (dx != 0) { 1496 | clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx); 1497 | mCapturedView.offsetLeftAndRight(clampedX - oldLeft); 1498 | } 1499 | if (dy != 0) { 1500 | clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy); 1501 | mCapturedView.offsetTopAndBottom(clampedY - oldTop); 1502 | } 1503 | 1504 | if (dx != 0 || dy != 0) { 1505 | final int clampedDx = clampedX - oldLeft; 1506 | final int clampedDy = clampedY - oldTop; 1507 | mCallback 1508 | .onViewPositionChanged(mCapturedView, clampedX, clampedY, clampedDx, clampedDy); 1509 | } 1510 | } 1511 | 1512 | /** 1513 | * Determine if the currently captured view is under the given point in the 1514 | * parent view's coordinate system. If there is no captured view this method 1515 | * will return false. 1516 | * 1517 | * @param x X position to test in the parent's coordinate system 1518 | * @param y Y position to test in the parent's coordinate system 1519 | * @return true if the captured view is under the given point, false 1520 | * otherwise 1521 | */ 1522 | public boolean isCapturedViewUnder(int x, int y) { 1523 | return isViewUnder(mCapturedView, x, y); 1524 | } 1525 | 1526 | /** 1527 | * Determine if the supplied view is under the given point in the parent 1528 | * view's coordinate system. 1529 | * 1530 | * @param view Child view of the parent to hit test 1531 | * @param x X position to test in the parent's coordinate system 1532 | * @param y Y position to test in the parent's coordinate system 1533 | * @return true if the supplied view is under the given point, false 1534 | * otherwise 1535 | */ 1536 | public boolean isViewUnder(View view, int x, int y) { 1537 | if (view == null) { 1538 | return false; 1539 | } 1540 | return x >= view.getLeft() && x < view.getRight() && y >= view.getTop() 1541 | && y < view.getBottom(); 1542 | } 1543 | 1544 | /** 1545 | * Find the topmost child under the given point within the parent view's 1546 | * coordinate system. The child order is determined using 1547 | * {@link me.imid.swipebacklayout.lib.ViewDragHelper.Callback#getOrderedChildIndex(int)} 1548 | * . 1549 | * 1550 | * @param x X position to test in the parent's coordinate system 1551 | * @param y Y position to test in the parent's coordinate system 1552 | * @return The topmost child view under (x, y) or null if none found. 1553 | */ 1554 | public View findTopChildUnder(int x, int y) { 1555 | final int childCount = mParentView.getChildCount(); 1556 | for (int i = childCount - 1; i >= 0; i--) { 1557 | final View child = mParentView.getChildAt(mCallback.getOrderedChildIndex(i)); 1558 | if (x >= child.getLeft() && x < child.getRight() && y >= child.getTop() 1559 | && y < child.getBottom()) { 1560 | return child; 1561 | } 1562 | } 1563 | return null; 1564 | } 1565 | 1566 | private int getEdgeTouched(int x, int y) { 1567 | int result = 0; 1568 | 1569 | if (x < mParentView.getLeft() + mEdgeSize) 1570 | result = EDGE_LEFT; 1571 | if (y < mParentView.getTop() + mEdgeSize) 1572 | result = EDGE_TOP; 1573 | if (x > mParentView.getRight() - mEdgeSize) 1574 | result = EDGE_RIGHT; 1575 | if (y > mParentView.getBottom() - mEdgeSize) 1576 | result = EDGE_BOTTOM; 1577 | 1578 | return result; 1579 | } 1580 | } 1581 | -------------------------------------------------------------------------------- /library/src/main/java/me/imid/swipebacklayout/lib/app/SwipeBackActivity.java: -------------------------------------------------------------------------------- 1 | 2 | package me.imid.swipebacklayout.lib.app; 3 | 4 | import android.os.Bundle; 5 | import android.support.v4.app.FragmentActivity; 6 | import android.support.v7.app.AppCompatActivity; 7 | import android.view.View; 8 | 9 | import me.imid.swipebacklayout.lib.SwipeBackLayout; 10 | import me.imid.swipebacklayout.lib.Utils; 11 | 12 | public class SwipeBackActivity extends AppCompatActivity implements SwipeBackActivityBase { 13 | private SwipeBackActivityHelper mHelper; 14 | 15 | @Override 16 | protected void onCreate(Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | mHelper = new SwipeBackActivityHelper(this); 19 | mHelper.onActivityCreate(); 20 | } 21 | 22 | @Override 23 | protected void onPostCreate(Bundle savedInstanceState) { 24 | super.onPostCreate(savedInstanceState); 25 | mHelper.onPostCreate(); 26 | } 27 | 28 | @Override 29 | public View findViewById(int id) { 30 | View v = super.findViewById(id); 31 | if (v == null && mHelper != null) 32 | return mHelper.findViewById(id); 33 | return v; 34 | } 35 | 36 | @Override 37 | public SwipeBackLayout getSwipeBackLayout() { 38 | return mHelper.getSwipeBackLayout(); 39 | } 40 | 41 | @Override 42 | public void setSwipeBackEnable(boolean enable) { 43 | getSwipeBackLayout().setEnableGesture(enable); 44 | } 45 | 46 | @Override 47 | public void scrollToFinishActivity() { 48 | Utils.convertActivityToTranslucent(this); 49 | getSwipeBackLayout().scrollToFinishActivity(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /library/src/main/java/me/imid/swipebacklayout/lib/app/SwipeBackActivityBase.java: -------------------------------------------------------------------------------- 1 | package me.imid.swipebacklayout.lib.app; 2 | 3 | import me.imid.swipebacklayout.lib.SwipeBackLayout; 4 | /** 5 | * @author Yrom 6 | */ 7 | public interface SwipeBackActivityBase { 8 | /** 9 | * @return the SwipeBackLayout associated with this activity. 10 | */ 11 | public abstract SwipeBackLayout getSwipeBackLayout(); 12 | 13 | public abstract void setSwipeBackEnable(boolean enable); 14 | 15 | /** 16 | * Scroll out contentView and finish the activity 17 | */ 18 | public abstract void scrollToFinishActivity(); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /library/src/main/java/me/imid/swipebacklayout/lib/app/SwipeBackActivityHelper.java: -------------------------------------------------------------------------------- 1 | package me.imid.swipebacklayout.lib.app; 2 | 3 | import android.app.Activity; 4 | import android.graphics.Color; 5 | import android.graphics.drawable.ColorDrawable; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | 9 | import me.imid.swipebacklayout.lib.SwipeBackLayout; 10 | import me.imid.swipebacklayout.lib.Utils; 11 | 12 | /** 13 | * @author Yrom 14 | */ 15 | public class SwipeBackActivityHelper { 16 | private Activity mActivity; 17 | 18 | private SwipeBackLayout mSwipeBackLayout; 19 | 20 | public SwipeBackActivityHelper(Activity activity) { 21 | mActivity = activity; 22 | } 23 | 24 | @SuppressWarnings("deprecation") 25 | public void onActivityCreate() { 26 | mActivity.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); 27 | mActivity.getWindow().getDecorView().setBackgroundDrawable(null); 28 | mSwipeBackLayout = (SwipeBackLayout) LayoutInflater.from(mActivity).inflate( 29 | me.imid.swipebacklayout.lib.R.layout.swipeback_layout, null); 30 | } 31 | 32 | public void onPostCreate() { 33 | mSwipeBackLayout.attachToActivity(mActivity); 34 | } 35 | 36 | public View findViewById(int id) { 37 | if (mSwipeBackLayout != null) { 38 | return mSwipeBackLayout.findViewById(id); 39 | } 40 | return null; 41 | } 42 | 43 | public SwipeBackLayout getSwipeBackLayout() { 44 | return mSwipeBackLayout; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /library/src/main/java/me/imid/swipebacklayout/lib/app/SwipeBackListenerActivityAdapter.java: -------------------------------------------------------------------------------- 1 | package me.imid.swipebacklayout.lib.app; 2 | 3 | import android.app.Activity; 4 | import android.support.annotation.NonNull; 5 | 6 | import java.lang.ref.WeakReference; 7 | 8 | import me.imid.swipebacklayout.lib.SwipeBackLayout; 9 | import me.imid.swipebacklayout.lib.Utils; 10 | 11 | /** 12 | * Created by laysionqet on 2018/4/24. 13 | */ 14 | public class SwipeBackListenerActivityAdapter implements SwipeBackLayout.SwipeListenerEx { 15 | private final WeakReference mActivity; 16 | 17 | public SwipeBackListenerActivityAdapter(@NonNull Activity activity) { 18 | mActivity = new WeakReference<>(activity); 19 | } 20 | 21 | @Override 22 | public void onScrollStateChange(int state, float scrollPercent) { 23 | 24 | } 25 | 26 | @Override 27 | public void onEdgeTouch(int edgeFlag) { 28 | Activity activity = mActivity.get(); 29 | if (null != activity) { 30 | Utils.convertActivityToTranslucent(activity); 31 | } 32 | } 33 | 34 | @Override 35 | public void onScrollOverThreshold() { 36 | 37 | } 38 | 39 | @Override 40 | public void onContentViewSwipedBack() { 41 | Activity activity = mActivity.get(); 42 | if (null != activity && !activity.isFinishing()) { 43 | activity.finish(); 44 | activity.overridePendingTransition(0, 0); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /library/src/main/java/me/imid/swipebacklayout/lib/app/SwipeBackPreferenceActivity.java: -------------------------------------------------------------------------------- 1 | 2 | package me.imid.swipebacklayout.lib.app; 3 | 4 | import me.imid.swipebacklayout.lib.SwipeBackLayout; 5 | import android.os.Bundle; 6 | import android.preference.PreferenceActivity; 7 | import android.view.View; 8 | 9 | public class SwipeBackPreferenceActivity extends PreferenceActivity implements SwipeBackActivityBase { 10 | private SwipeBackActivityHelper mHelper; 11 | 12 | @Override 13 | protected void onCreate(Bundle savedInstanceState) { 14 | super.onCreate(savedInstanceState); 15 | mHelper = new SwipeBackActivityHelper(this); 16 | mHelper.onActivityCreate(); 17 | } 18 | 19 | @Override 20 | protected void onPostCreate(Bundle savedInstanceState) { 21 | super.onPostCreate(savedInstanceState); 22 | mHelper.onPostCreate(); 23 | } 24 | 25 | @Override 26 | public View findViewById(int id) { 27 | View v = super.findViewById(id); 28 | if (v == null && mHelper != null) 29 | return mHelper.findViewById(id); 30 | return v; 31 | } 32 | 33 | @Override 34 | public SwipeBackLayout getSwipeBackLayout() { 35 | return mHelper.getSwipeBackLayout(); 36 | } 37 | @Override 38 | public void setSwipeBackEnable(boolean enable) { 39 | getSwipeBackLayout().setEnableGesture(enable); 40 | } 41 | 42 | @Override 43 | public void scrollToFinishActivity() { 44 | getSwipeBackLayout().scrollToFinishActivity(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /library/src/main/res/drawable-xhdpi/shadow_bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikew0ng/SwipeBackLayout/7c6b0ac424813266cec57db86d65a7c9d9ff5fa6/library/src/main/res/drawable-xhdpi/shadow_bottom.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-xhdpi/shadow_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikew0ng/SwipeBackLayout/7c6b0ac424813266cec57db86d65a7c9d9ff5fa6/library/src/main/res/drawable-xhdpi/shadow_left.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-xhdpi/shadow_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikew0ng/SwipeBackLayout/7c6b0ac424813266cec57db86d65a7c9d9ff5fa6/library/src/main/res/drawable-xhdpi/shadow_right.png -------------------------------------------------------------------------------- /library/src/main/res/layout/swipeback_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /library/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /library/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | -------------------------------------------------------------------------------- /samples/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 26 5 | buildToolsVersion '26.0.2' 6 | 7 | defaultConfig { 8 | minSdkVersion 14 9 | targetSdkVersion 26 10 | } 11 | 12 | signingConfigs { 13 | signing 14 | } 15 | 16 | buildTypes { 17 | release { 18 | if (project.hasProperty('storeFile')) { 19 | signingConfig signingConfigs.signing 20 | } 21 | } 22 | } 23 | } 24 | 25 | dependencies { 26 | implementation 'com.android.support:appcompat-v7:26.1.0' 27 | implementation project(':library') 28 | } 29 | 30 | if (project.hasProperty('storeFile')) { 31 | android.signingConfigs.signing.storeFile = file(storeFile) 32 | } 33 | 34 | if (project.hasProperty('storePassword')) { 35 | android.signingConfigs.signing.storePassword = storePassword 36 | } 37 | 38 | if (project.hasProperty('keyAlias')) { 39 | android.signingConfigs.signing.keyAlias = keyAlias 40 | } 41 | 42 | if (project.hasProperty('keyPassword')) { 43 | android.signingConfigs.signing.keyPassword = keyPassword 44 | } 45 | -------------------------------------------------------------------------------- /samples/libs/android-support-v4.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikew0ng/SwipeBackLayout/7c6b0ac424813266cec57db86d65a7c9d9ff5fa6/samples/libs/android-support-v4.jar -------------------------------------------------------------------------------- /samples/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 10 | 11 | 12 | 13 | 18 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /samples/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikew0ng/SwipeBackLayout/7c6b0ac424813266cec57db86d65a7c9d9ff5fa6/samples/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /samples/src/main/java/me/imid/swipebacklayout/demo/DemoActivity.java: -------------------------------------------------------------------------------- 1 | 2 | package me.imid.swipebacklayout.demo; 3 | 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.content.res.Resources; 7 | import android.graphics.drawable.ColorDrawable; 8 | import android.net.Uri; 9 | import android.os.Bundle; 10 | import android.os.Vibrator; 11 | import android.support.v7.widget.Toolbar; 12 | import android.view.Menu; 13 | import android.view.MenuItem; 14 | import android.view.View; 15 | import android.widget.RadioGroup; 16 | 17 | import me.imid.swipebacklayout.lib.SwipeBackLayout; 18 | import me.imid.swipebacklayout.lib.app.SwipeBackActivity; 19 | 20 | /** 21 | * Created by Issac on 8/11/13. 22 | */ 23 | public class DemoActivity extends SwipeBackActivity implements View.OnClickListener { 24 | private static final int VIBRATE_DURATION = 20; 25 | 26 | private int[] mBgColors; 27 | 28 | private static int mBgIndex = 0; 29 | 30 | private String mKeyTrackingMode; 31 | 32 | private RadioGroup mTrackingModeGroup; 33 | 34 | private SwipeBackLayout mSwipeBackLayout; 35 | 36 | private Toolbar mToolbar; 37 | 38 | @Override 39 | protected void onCreate(Bundle savedInstanceState) { 40 | super.onCreate(savedInstanceState); 41 | setContentView(R.layout.activity_demo); 42 | findViews(); 43 | changeActionBarColor(); 44 | mKeyTrackingMode = getString(R.string.key_tracking_mode); 45 | mSwipeBackLayout = getSwipeBackLayout(); 46 | 47 | mTrackingModeGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { 48 | @Override 49 | public void onCheckedChanged(RadioGroup group, int checkedId) { 50 | int edgeFlag; 51 | switch (checkedId) { 52 | case R.id.mode_left: 53 | edgeFlag = SwipeBackLayout.EDGE_LEFT; 54 | break; 55 | case R.id.mode_right: 56 | edgeFlag = SwipeBackLayout.EDGE_RIGHT; 57 | break; 58 | case R.id.mode_bottom: 59 | edgeFlag = SwipeBackLayout.EDGE_BOTTOM; 60 | break; 61 | default: 62 | edgeFlag = SwipeBackLayout.EDGE_ALL; 63 | } 64 | mSwipeBackLayout.setEdgeTrackingEnabled(edgeFlag); 65 | saveTrackingMode(edgeFlag); 66 | } 67 | }); 68 | mSwipeBackLayout.addSwipeListener(new SwipeBackLayout.SwipeListener() { 69 | @Override 70 | public void onScrollStateChange(int state, float scrollPercent) { 71 | 72 | } 73 | 74 | @Override 75 | public void onEdgeTouch(int edgeFlag) { 76 | vibrate(VIBRATE_DURATION); 77 | } 78 | 79 | @Override 80 | public void onScrollOverThreshold() { 81 | vibrate(VIBRATE_DURATION); 82 | } 83 | }); 84 | } 85 | 86 | @Override 87 | protected void onResume() { 88 | super.onResume(); 89 | restoreTrackingMode(); 90 | } 91 | 92 | private void saveTrackingMode(int flag) { 93 | PreferenceUtils.setPrefInt(getApplicationContext(), mKeyTrackingMode, flag); 94 | } 95 | 96 | private void restoreTrackingMode() { 97 | int flag = PreferenceUtils.getPrefInt(getApplicationContext(), mKeyTrackingMode, 98 | SwipeBackLayout.EDGE_LEFT); 99 | mSwipeBackLayout.setEdgeTrackingEnabled(flag); 100 | switch (flag) { 101 | case SwipeBackLayout.EDGE_LEFT: 102 | mTrackingModeGroup.check(R.id.mode_left); 103 | break; 104 | case SwipeBackLayout.EDGE_RIGHT: 105 | mTrackingModeGroup.check(R.id.mode_right); 106 | break; 107 | case SwipeBackLayout.EDGE_BOTTOM: 108 | mTrackingModeGroup.check(R.id.mode_bottom); 109 | break; 110 | case SwipeBackLayout.EDGE_ALL: 111 | mTrackingModeGroup.check(R.id.mode_all); 112 | break; 113 | } 114 | } 115 | 116 | private void changeActionBarColor() { 117 | getSupportActionBar().setBackgroundDrawable(new ColorDrawable(getColors()[mBgIndex])); 118 | mBgIndex++; 119 | if (mBgIndex >= getColors().length) { 120 | mBgIndex = 0; 121 | } 122 | } 123 | 124 | private void findViews() { 125 | setSupportActionBar((Toolbar) findViewById(R.id.toolbar)); 126 | findViewById(R.id.btn_start).setOnClickListener(this); 127 | findViewById(R.id.btn_finish).setOnClickListener(this); 128 | mTrackingModeGroup = (RadioGroup) findViewById(R.id.tracking_mode); 129 | } 130 | 131 | private int[] getColors() { 132 | if (mBgColors == null) { 133 | Resources resource = getResources(); 134 | mBgColors = new int[] { 135 | resource.getColor(R.color.androidColorA), 136 | resource.getColor(R.color.androidColorB), 137 | resource.getColor(R.color.androidColorC), 138 | resource.getColor(R.color.androidColorD), 139 | resource.getColor(R.color.androidColorE), 140 | }; 141 | } 142 | return mBgColors; 143 | } 144 | 145 | private void vibrate(long duration) { 146 | Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); 147 | long[] pattern = { 148 | 0, duration 149 | }; 150 | vibrator.vibrate(pattern, -1); 151 | } 152 | 153 | @Override 154 | public void onClick(View v) { 155 | switch (v.getId()) { 156 | case R.id.btn_start: 157 | startActivity(new Intent(DemoActivity.this, DemoActivity.class)); 158 | break; 159 | case R.id.btn_finish: 160 | scrollToFinishActivity(); 161 | break; 162 | } 163 | } 164 | 165 | @Override 166 | public boolean onCreateOptionsMenu(Menu menu) { 167 | getMenuInflater().inflate(R.menu.main, menu); 168 | return true; 169 | } 170 | 171 | @Override 172 | public boolean onOptionsItemSelected(MenuItem item) { 173 | switch (item.getItemId()) { 174 | case R.id.action_github: 175 | Intent intent = new Intent(Intent.ACTION_VIEW); 176 | intent.setData(Uri.parse("https://github.com/Issacw0ng/SwipeBackLayout")); 177 | startActivity(intent); 178 | return true; 179 | default: 180 | return super.onOptionsItemSelected(item); 181 | } 182 | } 183 | 184 | } 185 | -------------------------------------------------------------------------------- /samples/src/main/java/me/imid/swipebacklayout/demo/PreferenceUtils.java: -------------------------------------------------------------------------------- 1 | 2 | package me.imid.swipebacklayout.demo; 3 | 4 | import android.content.Context; 5 | import android.content.SharedPreferences; 6 | import android.content.SharedPreferences.Editor; 7 | import android.preference.PreferenceManager; 8 | 9 | public class PreferenceUtils { 10 | public static String getPrefString(Context context, String key, final String defaultValue) { 11 | final SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); 12 | return settings.getString(key, defaultValue); 13 | } 14 | 15 | public static void setPrefString(Context context, final String key, final String value) { 16 | final SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); 17 | settings.edit().putString(key, value).commit(); 18 | } 19 | 20 | public static boolean getPrefBoolean(Context context, final String key, 21 | final boolean defaultValue) { 22 | final SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); 23 | return settings.getBoolean(key, defaultValue); 24 | } 25 | 26 | public static boolean hasKey(Context context, final String key) { 27 | return PreferenceManager.getDefaultSharedPreferences(context).contains(key); 28 | } 29 | 30 | public static void setPrefBoolean(Context context, final String key, final boolean value) { 31 | final SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); 32 | settings.edit().putBoolean(key, value).commit(); 33 | } 34 | 35 | public static void setPrefInt(Context context, final String key, final int value) { 36 | final SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); 37 | settings.edit().putInt(key, value).commit(); 38 | } 39 | 40 | public static int getPrefInt(Context context, final String key, final int defaultValue) { 41 | final SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); 42 | return settings.getInt(key, defaultValue); 43 | } 44 | 45 | public static void setPrefFloat(Context context, final String key, final float value) { 46 | final SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); 47 | settings.edit().putFloat(key, value).commit(); 48 | } 49 | 50 | public static float getPrefFloat(Context context, final String key, final float defaultValue) { 51 | final SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); 52 | return settings.getFloat(key, defaultValue); 53 | } 54 | 55 | public static void setSettingLong(Context context, final String key, final long value) { 56 | final SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); 57 | settings.edit().putLong(key, value).commit(); 58 | } 59 | 60 | public static long getPrefLong(Context context, final String key, final long defaultValue) { 61 | final SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); 62 | return settings.getLong(key, defaultValue); 63 | } 64 | 65 | public static void clearPreference(Context context, final SharedPreferences p) { 66 | final Editor editor = p.edit(); 67 | editor.clear(); 68 | editor.commit(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /samples/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikew0ng/SwipeBackLayout/7c6b0ac424813266cec57db86d65a7c9d9ff5fa6/samples/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /samples/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikew0ng/SwipeBackLayout/7c6b0ac424813266cec57db86d65a7c9d9ff5fa6/samples/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /samples/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikew0ng/SwipeBackLayout/7c6b0ac424813266cec57db86d65a7c9d9ff5fa6/samples/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /samples/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikew0ng/SwipeBackLayout/7c6b0ac424813266cec57db86d65a7c9d9ff5fa6/samples/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /samples/src/main/res/layout/activity_demo.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | 14 |