├── .gitignore ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── recyclerviewenhanced ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── nikhilpanju │ │ └── recyclerviewenhanced │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── nikhilpanju │ │ │ └── recyclerviewenhanced │ │ │ ├── OnActivityTouchListener.java │ │ │ └── RecyclerTouchListener.java │ └── res │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── nikhilpanju │ └── recyclerviewenhanced │ └── ExampleUnitTest.java ├── sample ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── nikhilpanju │ │ └── recyclerviewsample │ │ └── ApplicationTest.java │ ├── common │ └── images │ │ └── Demo.gif │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── nikhilpanju │ │ │ └── recyclerviewsample │ │ │ ├── MainActivity.java │ │ │ ├── RowModel.java │ │ │ └── ToastUtil.java │ └── res │ │ ├── drawable │ │ ├── ic_add_black_24dp.xml │ │ ├── ic_build_black_24dp.xml │ │ └── ic_mode_edit_black_24dp.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ └── recycler_row.xml │ │ ├── menu │ │ └── menu_main.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── nikhilpanju │ └── recyclerviewsample │ └── ExampleUnitTest.java └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/* 5 | .DS_Store 6 | /build 7 | /captures 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RecyclerViewEnhanced 2 | Android Library to provide swipe, click and other functionality to RecyclerView 3 | ## Usage 4 | 5 | Add this to your build.gradle file 6 | 7 | ``` 8 | dependencies { 9 | compile 'com.nikhilpanju.recyclerviewenhanced:recyclerviewenhanced:1.1.0' 10 | } 11 | ``` 12 | 13 | ## Features 14 | * Supports API 14+ (Earlier APIs not tested 15 | * Supports any view for "Swipe Options" 16 | * Doesn't require any new adapters or new views. Works with any existing RecyclerViews. 17 | * Requires adding `OnItemTouchListener` to the RecyclerView 18 | * Supports clicking and swiping functionalities. 19 | * Supports disabling clicking and swiping for particular items/rows. 20 | * Supports `independentViews` in your items/rows (Read below for more information) 21 | * Supports `fadeViews` in your items/rows (Read below for more information) 22 | 23 | ## Demo 24 | Build the sample application to try RecyclerViewEnhanced 25 | ![alt text](https://github.com/nikhilpanju/RecyclerViewEnhanced/blob/master/sample/src/common/images/Demo.gif "Demo") 26 | 27 | ## Configuring 28 | * #### Create an instance of `RecyclerTouchListener` 29 | `onTouchListener = new RecyclerTouchListener(this, mRecyclerView);` 30 | 31 | * #### Set `IndependentViews` and `FadeViews` (If required) 32 | `IndependentViews` are views which can be clicked separately from the entire row. Their clicks have different functionality from row clicks. `FadeViews` are views which fade in and out as the rows are swiped closed and opened respectively. 33 | 34 | ``` 35 | onTouchListener.setIndependentViews(R.id.rowButton) 36 | .setViewsToFade(R.id.rowButton) 37 | ``` 38 | 39 | * #### Implement `OnRowClickListener` using `setClickable()` 40 | `setClickable()` will enable clicks for the recycler view items and the `IndependentViews` 41 | 42 | ``` 43 | .setClickable(new RecyclerTouchListener.OnRowClickListener() { 44 | @Override 45 | public void onRowClicked(int position) { 46 | // Do something 47 | } 48 | 49 | @Override 50 | public void onIndependentViewClicked(int independentViewID, int position) { 51 | // Do something 52 | } 53 | }) 54 | ``` 55 | 56 | * #### Enable Swipe Functionality 57 | 58 | Set the views for which you require a click listener and enable swiping by using `setSwipeable()` 59 | ``` 60 | .setSwipeOptionViews(R.id.add, R.id.edit, R.id.change) 61 | .setSwipeable(R.id.rowFG, R.id.rowBG, new RecyclerTouchListener.OnSwipeOptionsClickListener() { 62 | @Override 63 | public void onSwipeOptionClicked(int viewID, int position) { 64 | if (viewID == R.id.add) { 65 | // Do something 66 | } else if (viewID == R.id.edit) { 67 | // Do something 68 | } else if (viewID == R.id.change) { 69 | // Do something 70 | } 71 | } 72 | }); 73 | ``` 74 | 75 | * #### Adding the listener to the RecyclerView 76 | 77 | In `onResume()` add the listener: 78 | ``` 79 | mRecyclerView.addOnItemTouchListener(onTouchListener); 80 | ``` 81 | In `onPause()` remove the listener: 82 | ``` 83 | mRecyclerView.removeOnItemTouchListener(onTouchListener); 84 | ``` 85 | 86 | ## Additional Functionality 87 | * Use `onRowLongClickListener` to receive long click events 88 | ``` 89 | .setLongClickable(true, new RecyclerTouchListener.OnRowLongClickListener() { 90 | @Override 91 | public void onRowLongClicked(int position) { 92 | ToastUtil.makeToast(getApplicationContext(), "Row " + (position + 1) + " long clicked!"); 93 | } 94 | }) 95 | ``` 96 | 97 | * Use `setUnSwipeableRows()` to disable certain rows from swiping. Using this also displays an "difficult-to-slide" animation when trying to slide an unswipeable row. 98 | * Use `setUnClickableRows()` to disable click actions for certain rows. (Note: This also prevents the independentViews from being clicked). 99 | * `openSwipeOptions()` opens the swipe options for a specific row. 100 | * `closeVisibleBG()` closes any open options. 101 | * Implement `OnSwipeListener` to get `onSwipeOptionsClosed()` and `onSwipeOptionsOpened()` events. 102 | 103 | 104 | ### Closing swipe options when clicked anywhere outside of the recyclerView: 105 | * Make your Activity implement `RecyclerTouchListener.RecyclerTouchListenerHelper` and store the touchListener 106 | ``` 107 | private OnActivityTouchListener touchListener; 108 | 109 | @Override 110 | public void setOnActivityTouchListener(OnActivityTouchListener listener) { 111 | this.touchListener = listener; 112 | } 113 | ``` 114 | * Override `dispatchTouchEvent()` of your Activity and pass the `MotionEvent` variable to the `touchListener` 115 | ``` 116 | @Override 117 | public boolean dispatchTouchEvent(MotionEvent ev) { 118 | if (touchListener != null) touchListener.getTouchCoordinates(ev); 119 | return super.dispatchTouchEvent(ev); 120 | } 121 | ``` 122 | ## Author 123 | * Nikhil Panju ([Github](https://github.com/nikhilpanju)) 124 | 125 | 126 | ## License 127 | Copyright 2016 Nikhil Panju 128 | 129 | Licensed under the Apache License, Version 2.0 (the "License"); 130 | you may not use this file except in compliance with the License. 131 | You may obtain a copy of the License at 132 | 133 | ([http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)) 134 | 135 | Unless required by applicable law or agreed to in writing, software 136 | distributed under the License is distributed on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 138 | See the License for the specific language governing permissions and 139 | limitations under the License. 140 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:2.1.0' 9 | classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.4' 10 | classpath 'com.github.dcendents:android-maven-gradle-plugin:1.3' 11 | 12 | // NOTE: Do not place your application dependencies here; they belong 13 | // in the individual module build.gradle files 14 | } 15 | } 16 | 17 | allprojects { 18 | repositories { 19 | jcenter() 20 | } 21 | } 22 | 23 | task clean(type: Delete) { 24 | delete rootProject.buildDir 25 | } 26 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikhilpanju/RecyclerViewEnhanced/da8117365dded9508be7cb96c83ca99d64907065/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Dec 28 10:00:20 PST 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-2.10-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 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /recyclerviewenhanced/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /recyclerviewenhanced/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | ext { 4 | bintrayRepo = 'maven' 5 | bintrayName = 'recyclerviewenhanced' 6 | 7 | publishedGroupId = 'com.nikhilpanju.recyclerviewenhanced' 8 | libraryName = 'recyclerviewenhanced' 9 | artifact = 'recyclerviewenhanced' 10 | 11 | libraryDescription = 'Android Library to provide swipe, click and other functionality to RecyclerView' 12 | 13 | siteUrl = 'https://github.com/nikhilpanju/RecyclerViewEnhanced' 14 | gitUrl = 'https://github.com/nikhilpanju/RecyclerViewEnhanced.git' 15 | 16 | libraryVersion = '1.0.1' 17 | 18 | developerId = 'nikhilpanju' 19 | developerName = 'Nikhil Panju' 20 | developerEmail = 'nikhilpanju22@gmail.com' 21 | 22 | licenseName = 'The Apache Software License, Version 2.0' 23 | licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt' 24 | allLicenses = '' 25 | } 26 | 27 | android { 28 | compileSdkVersion 23 29 | buildToolsVersion "23.0.3" 30 | 31 | defaultConfig { 32 | minSdkVersion 14 33 | targetSdkVersion 23 34 | versionCode 1 35 | versionName "1.0.1" 36 | } 37 | buildTypes { 38 | release { 39 | minifyEnabled false 40 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 41 | } 42 | } 43 | } 44 | 45 | dependencies { 46 | compile fileTree(dir: 'libs', include: ['*.jar']) 47 | testCompile 'junit:junit:4.12' 48 | compile 'com.android.support:appcompat-v7:23.4.0' 49 | compile 'com.android.support:recyclerview-v7:23.4.0' 50 | } 51 | 52 | // Place it at the end of the file 53 | apply from: 'https://raw.githubusercontent.com/nuuneoi/JCenter/master/installv1.gradle' 54 | apply from: 'https://raw.githubusercontent.com/nuuneoi/JCenter/master/bintrayv1.gradle' 55 | -------------------------------------------------------------------------------- /recyclerviewenhanced/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in C:\Users\nikhi_000\AppData\Local\Android\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /recyclerviewenhanced/src/androidTest/java/com/nikhilpanju/recyclerviewenhanced/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.nikhilpanju.recyclerviewenhanced; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /recyclerviewenhanced/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /recyclerviewenhanced/src/main/java/com/nikhilpanju/recyclerviewenhanced/OnActivityTouchListener.java: -------------------------------------------------------------------------------- 1 | package com.nikhilpanju.recyclerviewenhanced; 2 | 3 | import android.view.MotionEvent; 4 | 5 | public interface OnActivityTouchListener { 6 | void getTouchCoordinates(MotionEvent ev); 7 | } -------------------------------------------------------------------------------- /recyclerviewenhanced/src/main/java/com/nikhilpanju/recyclerviewenhanced/RecyclerTouchListener.java: -------------------------------------------------------------------------------- 1 | package com.nikhilpanju.recyclerviewenhanced; 2 | 3 | import android.animation.Animator; 4 | import android.animation.ObjectAnimator; 5 | import android.app.Activity; 6 | import android.content.Context; 7 | import android.graphics.Rect; 8 | import android.os.Handler; 9 | import android.os.Vibrator; 10 | import android.support.v7.widget.RecyclerView; 11 | import android.util.DisplayMetrics; 12 | import android.util.Log; 13 | import android.view.MotionEvent; 14 | import android.view.VelocityTracker; 15 | import android.view.View; 16 | import android.view.ViewConfiguration; 17 | import android.view.animation.DecelerateInterpolator; 18 | import android.widget.ListView; 19 | 20 | import java.util.ArrayList; 21 | import java.util.Arrays; 22 | import java.util.HashSet; 23 | import java.util.List; 24 | import java.util.Set; 25 | 26 | public class RecyclerTouchListener implements RecyclerView.OnItemTouchListener, OnActivityTouchListener { 27 | private static final String TAG = "RecyclerTouchListener"; 28 | final Handler handler = new Handler(); 29 | Activity act; 30 | List unSwipeableRows; 31 | /* 32 | * independentViews are views on the foreground layer which when clicked, act "independent" from the foreground 33 | * ie, they are treated separately from the "row click" action 34 | */ 35 | List independentViews; 36 | List unClickableRows; 37 | List optionViews; 38 | Set ignoredViewTypes; 39 | // Cached ViewConfiguration and system-wide constant values 40 | private int touchSlop; 41 | private int minFlingVel; 42 | private int maxFlingVel; 43 | private long ANIMATION_STANDARD = 300; 44 | private long ANIMATION_CLOSE = 150; 45 | // Fixed properties 46 | private RecyclerView rView; 47 | // private SwipeListener mSwipeListener; 48 | private int bgWidth = 1, bgWidthLeft = 1; // 1 and not 0 to prevent dividing by zero 49 | // Transient properties 50 | // private List mPendingDismisses = new ArrayList<>(); 51 | private int mDismissAnimationRefCount = 0; 52 | private float touchedX; 53 | private float touchedY; 54 | private boolean isFgSwiping; 55 | private int mSwipingSlop; 56 | private VelocityTracker mVelocityTracker; 57 | private int touchedPosition; 58 | private View touchedView; 59 | private boolean mPaused; 60 | private boolean bgVisible, fgPartialViewClicked; 61 | private int bgVisiblePosition; 62 | private View bgVisibleView; 63 | private boolean isRViewScrolling; 64 | private int heightOutsideRView, screenHeight; 65 | private boolean mLongClickPerformed; 66 | // Foreground view (to be swiped), Background view (to show) 67 | private View fgView; 68 | private View bgView; 69 | //view ID 70 | private int fgViewID; 71 | private int bgViewID, bgViewIDLeft; 72 | private ArrayList fadeViews; 73 | private OnRowClickListener mRowClickListener; 74 | private OnRowLongClickListener mRowLongClickListener; 75 | private OnSwipeOptionsClickListener mBgClickListener, mBgClickListenerLeft; 76 | // user choices 77 | private boolean clickable = false; 78 | private boolean longClickable = false; 79 | private boolean swipeable = false, swipeableLeftOptions = false; 80 | private int LONG_CLICK_DELAY = 800; 81 | private boolean longClickVibrate; 82 | Runnable mLongPressed = new Runnable() { 83 | public void run() { 84 | if (!longClickable) 85 | return; 86 | 87 | mLongClickPerformed = true; 88 | 89 | if (!bgVisible && touchedPosition >= 0 && !unClickableRows.contains(touchedPosition) && !isRViewScrolling) { 90 | if (longClickVibrate) { 91 | Vibrator vibe = (Vibrator) act.getSystemService(Context.VIBRATOR_SERVICE); 92 | vibe.vibrate(100); 93 | } 94 | mRowLongClickListener.onRowLongClicked(touchedPosition); 95 | } 96 | } 97 | }; 98 | 99 | private RecyclerTouchListener() { 100 | } 101 | 102 | public RecyclerTouchListener(Activity a, RecyclerView recyclerView) { 103 | this.act = a; 104 | ViewConfiguration vc = ViewConfiguration.get(recyclerView.getContext()); 105 | touchSlop = vc.getScaledTouchSlop(); 106 | minFlingVel = vc.getScaledMinimumFlingVelocity() * 16; 107 | maxFlingVel = vc.getScaledMaximumFlingVelocity(); 108 | rView = recyclerView; 109 | bgVisible = false; 110 | bgVisiblePosition = -1; 111 | bgVisibleView = null; 112 | fgPartialViewClicked = false; 113 | unSwipeableRows = new ArrayList<>(); 114 | unClickableRows = new ArrayList<>(); 115 | ignoredViewTypes = new HashSet<>(); 116 | independentViews = new ArrayList<>(); 117 | optionViews = new ArrayList<>(); 118 | fadeViews = new ArrayList<>(); 119 | isRViewScrolling = false; 120 | 121 | // mSwipeListener = listener; 122 | 123 | rView.addOnScrollListener(new RecyclerView.OnScrollListener() { 124 | @Override 125 | public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 126 | /** 127 | * This will ensure that this RecyclerTouchListener is paused during recycler view scrolling. 128 | * If a scroll listener is already assigned, the caller should still pass scroll changes through 129 | * to this listener. 130 | */ 131 | setEnabled(newState != RecyclerView.SCROLL_STATE_DRAGGING); 132 | 133 | /** 134 | * This is used so that clicking a row cannot be done while scrolling 135 | */ 136 | isRViewScrolling = newState != RecyclerView.SCROLL_STATE_IDLE; 137 | } 138 | 139 | @Override 140 | public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 141 | 142 | } 143 | }); 144 | } 145 | 146 | /** 147 | * Enables or disables (pauses or resumes) watching for swipe-to-dismiss gestures. 148 | * 149 | * @param enabled Whether or not to watch for gestures. 150 | */ 151 | public void setEnabled(boolean enabled) { 152 | mPaused = !enabled; 153 | } 154 | 155 | @Override 156 | public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent motionEvent) { 157 | return handleTouchEvent(motionEvent); 158 | } 159 | 160 | @Override 161 | public void onTouchEvent(RecyclerView rv, MotionEvent motionEvent) { 162 | handleTouchEvent(motionEvent); 163 | } 164 | 165 | /*////////////// Clickable ////////////////////*/ 166 | 167 | public RecyclerTouchListener setClickable(OnRowClickListener listener) { 168 | this.clickable = true; 169 | this.mRowClickListener = listener; 170 | return this; 171 | } 172 | 173 | public RecyclerTouchListener setClickable(boolean clickable) { 174 | this.clickable = clickable; 175 | return this; 176 | } 177 | 178 | public RecyclerTouchListener setLongClickable(boolean vibrate, OnRowLongClickListener listener) { 179 | this.longClickable = true; 180 | this.mRowLongClickListener = listener; 181 | this.longClickVibrate = vibrate; 182 | return this; 183 | } 184 | public RecyclerTouchListener setLongClickable(boolean longClickable) { 185 | this.longClickable = longClickable; 186 | return this; 187 | } 188 | 189 | public RecyclerTouchListener setIndependentViews(Integer... viewIds) { 190 | this.independentViews = new ArrayList<>(Arrays.asList(viewIds)); 191 | return this; 192 | } 193 | 194 | public RecyclerTouchListener setUnClickableRows(Integer... rows) { 195 | this.unClickableRows = new ArrayList<>(Arrays.asList(rows)); 196 | return this; 197 | } 198 | 199 | public RecyclerTouchListener setIgnoredViewTypes(Integer... viewTypes) { 200 | ignoredViewTypes.clear(); 201 | ignoredViewTypes.addAll(Arrays.asList(viewTypes)); 202 | return this; 203 | } 204 | 205 | //////////////// Swipeable //////////////////// 206 | 207 | public RecyclerTouchListener setSwipeable(int foregroundID, int backgroundID, OnSwipeOptionsClickListener listener) { 208 | this.swipeable = true; 209 | if (fgViewID != 0 && foregroundID != fgViewID) 210 | throw new IllegalArgumentException("foregroundID does not match previously set ID"); 211 | fgViewID = foregroundID; 212 | bgViewID = backgroundID; 213 | this.mBgClickListener = listener; 214 | 215 | if (act instanceof RecyclerTouchListenerHelper) 216 | ((RecyclerTouchListenerHelper) act).setOnActivityTouchListener(this); 217 | 218 | DisplayMetrics displaymetrics = new DisplayMetrics(); 219 | act.getWindowManager().getDefaultDisplay().getMetrics(displaymetrics); 220 | screenHeight = displaymetrics.heightPixels; 221 | 222 | return this; 223 | } 224 | 225 | /*public RecyclerTouchListener setLeftToRightSwipeable(int foregroundID, int backgroundID, OnSwipeOptionsClickListener listener) { 226 | this.swipeableLeftOptions = true; 227 | if (fgViewID != 0 && foregroundID != fgViewID) 228 | throw new IllegalArgumentException("foregroundID does not match previously set ID"); 229 | fgViewID = foregroundID; 230 | bgViewIDLeft = backgroundID; 231 | this.mBgClickListenerLeft = listener; 232 | 233 | if (act instanceof RecyclerTouchListenerHelper) 234 | ((RecyclerTouchListenerHelper) act).setOnActivityTouchListener(this); 235 | 236 | DisplayMetrics displaymetrics = new DisplayMetrics(); 237 | act.getWindowManager().getDefaultDisplay().getMetrics(displaymetrics); 238 | screenHeight = displaymetrics.heightPixels; 239 | 240 | return this; 241 | }*/ 242 | 243 | public RecyclerTouchListener setSwipeable(boolean value) { 244 | this.swipeable = value; 245 | if (!value) 246 | invalidateSwipeOptions(); 247 | return this; 248 | } 249 | 250 | public RecyclerTouchListener setSwipeOptionViews(Integer... viewIds) { 251 | this.optionViews = new ArrayList<>(Arrays.asList(viewIds)); 252 | return this; 253 | } 254 | 255 | public RecyclerTouchListener setUnSwipeableRows(Integer... rows) { 256 | this.unSwipeableRows = new ArrayList<>(Arrays.asList(rows)); 257 | return this; 258 | } 259 | 260 | //////////////// Fade Views //////////////////// 261 | 262 | // Set views which are faded out as fg is opened 263 | public RecyclerTouchListener setViewsToFade(Integer... viewIds) { 264 | this.fadeViews = new ArrayList<>(Arrays.asList(viewIds)); 265 | return this; 266 | } 267 | 268 | // the entire foreground is faded out as it is opened 269 | public RecyclerTouchListener setFgFade() { 270 | if (!fadeViews.contains(fgViewID)) 271 | this.fadeViews.add(fgViewID); 272 | return this; 273 | } 274 | 275 | //-------------- Checkers for preventing ---------------// 276 | 277 | private boolean isIndependentViewClicked(MotionEvent motionEvent) { 278 | for (int i = 0; i < independentViews.size(); i++) { 279 | if (touchedView != null) { 280 | Rect rect = new Rect(); 281 | int x = (int) motionEvent.getRawX(); 282 | int y = (int) motionEvent.getRawY(); 283 | touchedView.findViewById(independentViews.get(i)).getGlobalVisibleRect(rect); 284 | if (rect.contains(x, y)) { 285 | return false; 286 | } 287 | } 288 | } 289 | return true; 290 | } 291 | 292 | private int getOptionViewID(MotionEvent motionEvent) { 293 | for (int i = 0; i < optionViews.size(); i++) { 294 | if (touchedView != null) { 295 | Rect rect = new Rect(); 296 | int x = (int) motionEvent.getRawX(); 297 | int y = (int) motionEvent.getRawY(); 298 | touchedView.findViewById(optionViews.get(i)).getGlobalVisibleRect(rect); 299 | if (rect.contains(x, y)) { 300 | return optionViews.get(i); 301 | } 302 | } 303 | } 304 | return -1; 305 | } 306 | 307 | private int getIndependentViewID(MotionEvent motionEvent) { 308 | for (int i = 0; i < independentViews.size(); i++) { 309 | if (touchedView != null) { 310 | Rect rect = new Rect(); 311 | int x = (int) motionEvent.getRawX(); 312 | int y = (int) motionEvent.getRawY(); 313 | touchedView.findViewById(independentViews.get(i)).getGlobalVisibleRect(rect); 314 | if (rect.contains(x, y)) { 315 | return independentViews.get(i); 316 | } 317 | } 318 | } 319 | return -1; 320 | } 321 | 322 | @Override 323 | public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { 324 | 325 | } 326 | 327 | public void invalidateSwipeOptions() { 328 | bgWidth = 1; 329 | } 330 | 331 | public void openSwipeOptions(int position) { 332 | if (!swipeable || rView.getChildAt(position) == null 333 | || unSwipeableRows.contains(position) || shouldIgnoreAction(position)) 334 | return; 335 | if (bgWidth < 2) { 336 | if (act.findViewById(bgViewID) != null) 337 | bgWidth = act.findViewById(bgViewID).getWidth(); 338 | heightOutsideRView = screenHeight - rView.getHeight(); 339 | } 340 | touchedPosition = position; 341 | touchedView = rView.getChildAt(position); 342 | fgView = touchedView.findViewById(fgViewID); 343 | bgView = touchedView.findViewById(bgViewID); 344 | bgView.setMinimumHeight(fgView.getHeight()); 345 | 346 | closeVisibleBG(null); 347 | animateFG(touchedView, Animation.OPEN, ANIMATION_STANDARD); 348 | bgVisible = true; 349 | bgVisibleView = fgView; 350 | bgVisiblePosition = touchedPosition; 351 | } 352 | 353 | @Deprecated 354 | public void closeVisibleBG() { 355 | if (bgVisibleView == null) { 356 | Log.e(TAG, "No rows found for which background options are visible"); 357 | return; 358 | } 359 | bgVisibleView.animate() 360 | .translationX(0) 361 | .setDuration(ANIMATION_CLOSE) 362 | .setListener(null); 363 | 364 | animateFadeViews(bgVisibleView, 1f, ANIMATION_CLOSE); 365 | bgVisible = false; 366 | bgVisibleView = null; 367 | bgVisiblePosition = -1; 368 | } 369 | 370 | public void closeVisibleBG(final OnSwipeListener mSwipeCloseListener) { 371 | if (bgVisibleView == null) { 372 | Log.e(TAG, "No rows found for which background options are visible"); 373 | return; 374 | } 375 | final ObjectAnimator translateAnimator = ObjectAnimator.ofFloat(bgVisibleView, 376 | View.TRANSLATION_X, 0f); 377 | translateAnimator.setDuration(ANIMATION_CLOSE); 378 | translateAnimator.addListener(new Animator.AnimatorListener() { 379 | @Override 380 | public void onAnimationStart(Animator animation) { 381 | } 382 | 383 | @Override 384 | public void onAnimationEnd(Animator animation) { 385 | if (mSwipeCloseListener != null) 386 | mSwipeCloseListener.onSwipeOptionsClosed(); 387 | translateAnimator.removeAllListeners(); 388 | } 389 | 390 | @Override 391 | public void onAnimationCancel(Animator animation) { 392 | } 393 | 394 | @Override 395 | public void onAnimationRepeat(Animator animation) { 396 | } 397 | }); 398 | translateAnimator.start(); 399 | 400 | animateFadeViews(bgVisibleView, 1f, ANIMATION_CLOSE); 401 | bgVisible = false; 402 | bgVisibleView = null; 403 | bgVisiblePosition = -1; 404 | } 405 | 406 | private void animateFadeViews(View downView, float alpha, long duration) { 407 | if (fadeViews != null) { 408 | for (final int viewID : fadeViews) { 409 | downView.findViewById(viewID).animate() 410 | .alpha(alpha) 411 | .setDuration(duration); 412 | } 413 | } 414 | } 415 | 416 | private void animateFG(View downView, Animation animateType, long duration) { 417 | if (animateType == Animation.OPEN) { 418 | ObjectAnimator translateAnimator = ObjectAnimator.ofFloat( 419 | fgView, View.TRANSLATION_X, -bgWidth); 420 | translateAnimator.setDuration(duration); 421 | translateAnimator.setInterpolator(new DecelerateInterpolator(1.5f)); 422 | translateAnimator.start(); 423 | animateFadeViews(downView, 0f, duration); 424 | } else if (animateType == Animation.CLOSE) { 425 | ObjectAnimator translateAnimator = ObjectAnimator.ofFloat( 426 | fgView, View.TRANSLATION_X, 0f); 427 | translateAnimator.setDuration(duration); 428 | translateAnimator.setInterpolator(new DecelerateInterpolator(1.5f)); 429 | translateAnimator.start(); 430 | animateFadeViews(downView, 1f, duration); 431 | } 432 | } 433 | 434 | private void animateFG(View downView, final Animation animateType, long duration, 435 | final OnSwipeListener mSwipeCloseListener) { 436 | final ObjectAnimator translateAnimator; 437 | if (animateType == Animation.OPEN) { 438 | translateAnimator = ObjectAnimator.ofFloat(fgView, View.TRANSLATION_X, -bgWidth); 439 | translateAnimator.setDuration(duration); 440 | translateAnimator.setInterpolator(new DecelerateInterpolator(1.5f)); 441 | translateAnimator.start(); 442 | animateFadeViews(downView, 0f, duration); 443 | } else /*if (animateType == Animation.CLOSE)*/ { 444 | translateAnimator = ObjectAnimator.ofFloat(fgView, View.TRANSLATION_X, 0f); 445 | translateAnimator.setDuration(duration); 446 | translateAnimator.setInterpolator(new DecelerateInterpolator(1.5f)); 447 | translateAnimator.start(); 448 | animateFadeViews(downView, 1f, duration); 449 | } 450 | 451 | translateAnimator.addListener(new Animator.AnimatorListener() { 452 | @Override 453 | public void onAnimationStart(Animator animation) { 454 | } 455 | 456 | @Override 457 | public void onAnimationEnd(Animator animation) { 458 | if (mSwipeCloseListener != null) { 459 | if (animateType == Animation.OPEN) 460 | mSwipeCloseListener.onSwipeOptionsOpened(); 461 | else if (animateType == Animation.CLOSE) 462 | mSwipeCloseListener.onSwipeOptionsClosed(); 463 | } 464 | translateAnimator.removeAllListeners(); 465 | } 466 | 467 | @Override 468 | public void onAnimationCancel(Animator animation) { 469 | } 470 | 471 | @Override 472 | public void onAnimationRepeat(Animator animation) { 473 | } 474 | }); 475 | } 476 | 477 | private boolean handleTouchEvent(MotionEvent motionEvent) { 478 | if (swipeable && bgWidth < 2) { 479 | // bgWidth = rView.getWidth(); 480 | if (act.findViewById(bgViewID) != null) 481 | bgWidth = act.findViewById(bgViewID).getWidth(); 482 | 483 | heightOutsideRView = screenHeight - rView.getHeight(); 484 | } 485 | 486 | switch (motionEvent.getActionMasked()) { 487 | 488 | // When finger touches screen 489 | case MotionEvent.ACTION_DOWN: { 490 | if (mPaused) { 491 | break; 492 | } 493 | 494 | // Find the child view that was touched (perform a hit test) 495 | Rect rect = new Rect(); 496 | int childCount = rView.getChildCount(); 497 | int[] listViewCoords = new int[2]; 498 | rView.getLocationOnScreen(listViewCoords); 499 | // x and y values respective to the recycler view 500 | int x = (int) motionEvent.getRawX() - listViewCoords[0]; 501 | int y = (int) motionEvent.getRawY() - listViewCoords[1]; 502 | View child; 503 | 504 | /* 505 | * check for every child (row) in the recycler view whether the touched co-ordinates belong to that 506 | * respective child and if it does, register that child as the touched view (touchedView) 507 | */ 508 | for (int i = 0; i < childCount; i++) { 509 | child = rView.getChildAt(i); 510 | child.getHitRect(rect); 511 | if (rect.contains(x, y)) { 512 | touchedView = child; 513 | break; 514 | } 515 | } 516 | 517 | if (touchedView != null) { 518 | touchedX = motionEvent.getRawX(); 519 | touchedY = motionEvent.getRawY(); 520 | touchedPosition = rView.getChildAdapterPosition(touchedView); 521 | 522 | if (shouldIgnoreAction(touchedPosition)) { 523 | touchedPosition = ListView.INVALID_POSITION; 524 | return false; // <-- guard here allows for ignoring events, allowing more than one view type and preventing NPE 525 | } 526 | 527 | if (longClickable) { 528 | mLongClickPerformed = false; 529 | handler.postDelayed(mLongPressed, LONG_CLICK_DELAY); 530 | } 531 | if (swipeable) { 532 | mVelocityTracker = VelocityTracker.obtain(); 533 | mVelocityTracker.addMovement(motionEvent); 534 | fgView = touchedView.findViewById(fgViewID); 535 | bgView = touchedView.findViewById(bgViewID); 536 | // bgView.getLayoutParams().height = fgView.getHeight(); 537 | bgView.setMinimumHeight(fgView.getHeight()); 538 | 539 | /* 540 | * bgVisible is true when the options menu is opened 541 | * This block is to register fgPartialViewClicked status - Partial view is the view that is still 542 | * shown on the screen if the options width is < device width 543 | */ 544 | if (bgVisible && fgView != null) { 545 | handler.removeCallbacks(mLongPressed); 546 | x = (int) motionEvent.getRawX(); 547 | y = (int) motionEvent.getRawY(); 548 | fgView.getGlobalVisibleRect(rect); 549 | fgPartialViewClicked = rect.contains(x, y); 550 | } else { 551 | fgPartialViewClicked = false; 552 | } 553 | } 554 | } 555 | 556 | /* 557 | * If options menu is shown and the touched position is not the same as the row for which the 558 | * options is displayed - close the options menu for the row which is displaying it 559 | * (bgVisibleView and bgVisiblePosition is used for this purpose which registers which view and 560 | * which position has it's options menu opened) 561 | */ 562 | x = (int) motionEvent.getRawX(); 563 | y = (int) motionEvent.getRawY(); 564 | rView.getHitRect(rect); 565 | if (swipeable && bgVisible && touchedPosition != bgVisiblePosition) { 566 | handler.removeCallbacks(mLongPressed); 567 | closeVisibleBG(null); 568 | } 569 | break; 570 | } 571 | 572 | case MotionEvent.ACTION_CANCEL: { 573 | handler.removeCallbacks(mLongPressed); 574 | if (mLongClickPerformed) 575 | break; 576 | 577 | if (mVelocityTracker == null) { 578 | break; 579 | } 580 | if (swipeable) { 581 | if (touchedView != null && isFgSwiping) { 582 | // cancel 583 | animateFG(touchedView, Animation.CLOSE, ANIMATION_STANDARD); 584 | } 585 | mVelocityTracker.recycle(); 586 | mVelocityTracker = null; 587 | isFgSwiping = false; 588 | bgView = null; 589 | } 590 | touchedX = 0; 591 | touchedY = 0; 592 | touchedView = null; 593 | touchedPosition = ListView.INVALID_POSITION; 594 | break; 595 | } 596 | 597 | // When finger is lifted off the screen (after clicking, flinging, swiping, etc..) 598 | case MotionEvent.ACTION_UP: { 599 | handler.removeCallbacks(mLongPressed); 600 | if (mLongClickPerformed) 601 | break; 602 | 603 | if (mVelocityTracker == null && swipeable) { 604 | break; 605 | } 606 | if (touchedPosition < 0) 607 | break; 608 | 609 | // swipedLeft and swipedRight are true if the user swipes in the respective direction (no conditions) 610 | boolean swipedLeft = false; 611 | boolean swipedRight = false; 612 | /* 613 | * swipedLeftProper and swipedRightProper are true if user swipes in the respective direction 614 | * and if certain conditions are satisfied (given some few lines below) 615 | */ 616 | boolean swipedLeftProper = false; 617 | boolean swipedRightProper = false; 618 | 619 | float mFinalDelta = motionEvent.getRawX() - touchedX; 620 | 621 | // mVelocityTracker.addMovement(motionEvent); 622 | // mVelocityTracker.computeCurrentVelocity(1000); 623 | // float velocityX = mVelocityTracker.getXVelocity(); 624 | // float absVelocityX = Math.abs(velocityX); 625 | // float absVelocityY = Math.abs(mVelocityTracker.getYVelocity()); 626 | 627 | // if swiped in a direction, make that respective variable true 628 | if (isFgSwiping) { 629 | swipedLeft = mFinalDelta < 0; 630 | swipedRight = mFinalDelta > 0; 631 | } 632 | 633 | /* 634 | * If the user has swiped more than half of the width of the options menu, or if the 635 | * velocity of swiping is between min and max fling values 636 | * "proper" variable are set true 637 | */ 638 | if (Math.abs(mFinalDelta) > bgWidth / 2 && isFgSwiping) { 639 | swipedLeftProper = mFinalDelta < 0; 640 | swipedRightProper = mFinalDelta > 0; 641 | } else if (swipeable) { 642 | mVelocityTracker.addMovement(motionEvent); 643 | mVelocityTracker.computeCurrentVelocity(1000); 644 | float velocityX = mVelocityTracker.getXVelocity(); 645 | float absVelocityX = Math.abs(velocityX); 646 | float absVelocityY = Math.abs(mVelocityTracker.getYVelocity()); 647 | if (minFlingVel <= absVelocityX && absVelocityX <= maxFlingVel 648 | && absVelocityY < absVelocityX && isFgSwiping) { 649 | // dismiss only if flinging in the same direction as dragging 650 | swipedLeftProper = (velocityX < 0) == (mFinalDelta < 0); 651 | swipedRightProper = (velocityX > 0) == (mFinalDelta > 0); 652 | } 653 | } 654 | 655 | ///////// Manipulation of view based on the 4 variables mentioned above /////////// 656 | 657 | // if swiped left properly and options menu isn't already visible, animate the foreground to the left 658 | if (swipeable && !swipedRight && swipedLeftProper && touchedPosition != RecyclerView.NO_POSITION 659 | && !unSwipeableRows.contains(touchedPosition) && !bgVisible) { 660 | 661 | final View downView = touchedView; // touchedView gets null'd before animation ends 662 | final int downPosition = touchedPosition; 663 | ++mDismissAnimationRefCount; 664 | //TODO - speed 665 | animateFG(touchedView, Animation.OPEN, ANIMATION_STANDARD); 666 | bgVisible = true; 667 | bgVisibleView = fgView; 668 | bgVisiblePosition = downPosition; 669 | } 670 | // else if swiped right properly when options menu is visible, close the menu and bring the foreground 671 | // to it's original position 672 | else if (swipeable && !swipedLeft && swipedRightProper && touchedPosition != RecyclerView.NO_POSITION 673 | && !unSwipeableRows.contains(touchedPosition) && bgVisible) { 674 | // dismiss 675 | final View downView = touchedView; // touchedView gets null'd before animation ends 676 | final int downPosition = touchedPosition; 677 | 678 | ++mDismissAnimationRefCount; 679 | //TODO - speed 680 | animateFG(touchedView, Animation.CLOSE, ANIMATION_STANDARD); 681 | bgVisible = false; 682 | bgVisibleView = null; 683 | bgVisiblePosition = -1; 684 | } 685 | // else if swiped left incorrectly (not satisfying the above conditions), animate the foreground back to 686 | // it's original position (spring effect) 687 | else if (swipeable && swipedLeft && !bgVisible) { 688 | // cancel 689 | final View tempBgView = bgView; 690 | animateFG(touchedView, Animation.CLOSE, ANIMATION_STANDARD, new OnSwipeListener() { 691 | @Override 692 | public void onSwipeOptionsClosed() { 693 | if (tempBgView != null) 694 | tempBgView.setVisibility(View.VISIBLE); 695 | } 696 | 697 | @Override 698 | public void onSwipeOptionsOpened() { 699 | 700 | } 701 | }); 702 | 703 | bgVisible = false; 704 | bgVisibleView = null; 705 | bgVisiblePosition = -1; 706 | } 707 | // else if swiped right incorrectly (not satisfying the above conditions), animate the foreground to 708 | // it's open position (spring effect) 709 | else if (swipeable && swipedRight && bgVisible) { 710 | // cancel 711 | animateFG(touchedView, Animation.OPEN, ANIMATION_STANDARD); 712 | bgVisible = true; 713 | bgVisibleView = fgView; 714 | bgVisiblePosition = touchedPosition; 715 | } 716 | // This case deals with an error where the user can swipe left, then right 717 | // really fast and the fg is stuck open - so in that case we close the fg 718 | else if (swipeable && swipedRight && !bgVisible) { 719 | // cancel 720 | animateFG(touchedView, Animation.CLOSE, ANIMATION_STANDARD); 721 | bgVisible = false; 722 | bgVisibleView = null; 723 | bgVisiblePosition = -1; 724 | } 725 | // This case deals with an error where the user can swipe right, then left 726 | // really fast and the fg is stuck open - so in that case we open the fg 727 | else if (swipeable && swipedLeft && bgVisible) { 728 | // cancel 729 | animateFG(touchedView, Animation.OPEN, ANIMATION_STANDARD); 730 | bgVisible = true; 731 | bgVisibleView = fgView; 732 | bgVisiblePosition = touchedPosition; 733 | } 734 | 735 | // if clicked 736 | else if (!swipedRight && !swipedLeft) { 737 | // if partial foreground view is clicked (see ACTION_DOWN) bring foreground back to original position 738 | // bgVisible is true automatically since it's already checked in ACTION_DOWN block 739 | if (swipeable && fgPartialViewClicked) { 740 | animateFG(touchedView, Animation.CLOSE, ANIMATION_STANDARD); 741 | bgVisible = false; 742 | bgVisibleView = null; 743 | bgVisiblePosition = -1; 744 | } 745 | // On Click listener for rows 746 | else if (clickable && !bgVisible && touchedPosition >= 0 && !unClickableRows.contains(touchedPosition) 747 | && isIndependentViewClicked(motionEvent) && !isRViewScrolling) { 748 | mRowClickListener.onRowClicked(touchedPosition); 749 | } 750 | // On Click listener for independent views inside the rows 751 | else if (clickable && !bgVisible && touchedPosition >= 0 && !unClickableRows.contains(touchedPosition) 752 | && !isIndependentViewClicked(motionEvent) && !isRViewScrolling) { 753 | final int independentViewID = getIndependentViewID(motionEvent); 754 | if (independentViewID >= 0) 755 | mRowClickListener.onIndependentViewClicked(independentViewID, touchedPosition); 756 | } 757 | // On Click listener for background options 758 | else if (swipeable && bgVisible && !fgPartialViewClicked) { 759 | final int optionID = getOptionViewID(motionEvent); 760 | if (optionID >= 0 && touchedPosition >= 0) { 761 | final int downPosition = touchedPosition; 762 | closeVisibleBG(new OnSwipeListener() { 763 | @Override 764 | public void onSwipeOptionsClosed() { 765 | mBgClickListener.onSwipeOptionClicked(optionID, downPosition); 766 | } 767 | 768 | @Override 769 | public void onSwipeOptionsOpened() { 770 | 771 | } 772 | }); 773 | } 774 | } 775 | } 776 | } 777 | // if clicked and not swiped 778 | 779 | if (swipeable) { 780 | mVelocityTracker.recycle(); 781 | mVelocityTracker = null; 782 | } 783 | touchedX = 0; 784 | touchedY = 0; 785 | touchedView = null; 786 | touchedPosition = ListView.INVALID_POSITION; 787 | isFgSwiping = false; 788 | bgView = null; 789 | break; 790 | 791 | // when finger is moving across the screen (and not yet lifted) 792 | case MotionEvent.ACTION_MOVE: { 793 | if (mLongClickPerformed) 794 | break; 795 | if (mVelocityTracker == null || mPaused || !swipeable) { 796 | break; 797 | } 798 | 799 | mVelocityTracker.addMovement(motionEvent); 800 | float deltaX = motionEvent.getRawX() - touchedX; 801 | float deltaY = motionEvent.getRawY() - touchedY; 802 | 803 | /* 804 | * isFgSwiping variable which is set to true here is used to alter the swipedLeft, swipedRightProper 805 | * variables in "ACTION_UP" block by checking if user is actually swiping at present or not 806 | */ 807 | if (!isFgSwiping && Math.abs(deltaX) > touchSlop && Math.abs(deltaY) < Math.abs(deltaX) / 2) { 808 | handler.removeCallbacks(mLongPressed); 809 | isFgSwiping = true; 810 | mSwipingSlop = (deltaX > 0 ? touchSlop : -touchSlop); 811 | } 812 | 813 | // This block moves the foreground along with the finger when swiping 814 | if (swipeable && isFgSwiping && !unSwipeableRows.contains(touchedPosition)) { 815 | if (bgView == null) { 816 | bgView = touchedView.findViewById(bgViewID); 817 | bgView.setVisibility(View.VISIBLE); 818 | } 819 | // if fg is being swiped left 820 | if (deltaX < touchSlop && !bgVisible) { 821 | float translateAmount = deltaX - mSwipingSlop; 822 | // if ((Math.abs(translateAmount) > bgWidth ? -bgWidth : translateAmount) <= 0) { 823 | // swipe fg till width of bg. If swiped further, nothing happens (stalls at width of bg) 824 | fgView.setTranslationX(Math.abs(translateAmount) > bgWidth ? -bgWidth : translateAmount); 825 | if (fgView.getTranslationX() > 0) fgView.setTranslationX(0); 826 | // } 827 | 828 | // fades all the fadeViews gradually to 0 alpha as dragged 829 | if (fadeViews != null) { 830 | for (int viewID : fadeViews) { 831 | touchedView.findViewById(viewID).setAlpha(1 - (Math.abs(translateAmount) / bgWidth)); 832 | } 833 | } 834 | } 835 | // if fg is being swiped right 836 | else if (deltaX > 0 && bgVisible) { 837 | // for closing rightOptions 838 | if (bgVisible) { 839 | float translateAmount = (deltaX - mSwipingSlop) - bgWidth; 840 | 841 | // swipe fg till it reaches original position. If swiped further, nothing happens (stalls at 0) 842 | fgView.setTranslationX(translateAmount > 0 ? 0 : translateAmount); 843 | 844 | // fades all the fadeViews gradually to 0 alpha as dragged 845 | if (fadeViews != null) { 846 | for (int viewID : fadeViews) { 847 | touchedView.findViewById(viewID).setAlpha(1 - (Math.abs(translateAmount) / bgWidth)); 848 | } 849 | } 850 | } 851 | // for opening leftOptions 852 | else { 853 | float translateAmount = (deltaX - mSwipingSlop) - bgWidth; 854 | 855 | // swipe fg till it reaches original position. If swiped further, nothing happens (stalls at 0) 856 | fgView.setTranslationX(translateAmount > 0 ? 0 : translateAmount); 857 | 858 | // fades all the fadeViews gradually to 0 alpha as dragged 859 | if (fadeViews != null) { 860 | for (int viewID : fadeViews) { 861 | touchedView.findViewById(viewID).setAlpha(1 - (Math.abs(translateAmount) / bgWidth)); 862 | } 863 | } 864 | } 865 | } 866 | return true; 867 | } 868 | // moves the fg slightly to give the illusion of an "unswipeable" row 869 | else if (swipeable && isFgSwiping && unSwipeableRows.contains(touchedPosition)) { 870 | if (deltaX < touchSlop && !bgVisible) { 871 | float translateAmount = deltaX - mSwipingSlop; 872 | if (bgView == null) 873 | bgView = touchedView.findViewById(bgViewID); 874 | 875 | if (bgView != null) 876 | bgView.setVisibility(View.GONE); 877 | 878 | // swipe fg till width of bg. If swiped further, nothing happens (stalls at width of bg) 879 | fgView.setTranslationX(translateAmount / 5); 880 | if (fgView.getTranslationX() > 0) fgView.setTranslationX(0); 881 | 882 | // fades all the fadeViews gradually to 0 alpha as dragged 883 | // if (fadeViews != null) { 884 | // for (int viewID : fadeViews) { 885 | // touchedView.findViewById(viewID).setAlpha(1 - (Math.abs(translateAmount) / bgWidth)); 886 | // } 887 | // } 888 | } 889 | return true; 890 | } 891 | break; 892 | } 893 | } 894 | return false; 895 | } 896 | 897 | /** 898 | * Gets coordinates from Activity and closes any 899 | * swiped rows if touch happens outside the recycler view 900 | */ 901 | @Override 902 | public void getTouchCoordinates(MotionEvent ev) { 903 | int y = (int) ev.getRawY(); 904 | if (swipeable && bgVisible && ev.getActionMasked() == MotionEvent.ACTION_DOWN 905 | && y < heightOutsideRView) closeVisibleBG(null); 906 | } 907 | 908 | private boolean shouldIgnoreAction(int touchedPosition) { 909 | return rView == null || ignoredViewTypes.contains(rView.getAdapter().getItemViewType(touchedPosition)); 910 | } 911 | 912 | private enum Animation { 913 | OPEN, CLOSE 914 | } 915 | 916 | /////////////////////////////////////////////////////////////////////////////////////// 917 | //////////////////////////////////// Interfaces ///////////////////////////////////// 918 | /////////////////////////////////////////////////////////////////////////////////////// 919 | 920 | public interface OnRowClickListener { 921 | void onRowClicked(int position); 922 | 923 | void onIndependentViewClicked(int independentViewID, int position); 924 | } 925 | 926 | public interface OnRowLongClickListener { 927 | void onRowLongClicked(int position); 928 | } 929 | 930 | public interface OnSwipeOptionsClickListener { 931 | void onSwipeOptionClicked(int viewID, int position); 932 | } 933 | 934 | public interface RecyclerTouchListenerHelper { 935 | void setOnActivityTouchListener(OnActivityTouchListener listener); 936 | } 937 | 938 | public interface OnSwipeListener { 939 | void onSwipeOptionsClosed(); 940 | 941 | void onSwipeOptionsOpened(); 942 | } 943 | } 944 | -------------------------------------------------------------------------------- /recyclerviewenhanced/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | RecyclerViewEnhanced 3 | 4 | -------------------------------------------------------------------------------- /recyclerviewenhanced/src/test/java/com/nikhilpanju/recyclerviewenhanced/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.nikhilpanju.recyclerviewenhanced; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * To work on unit tests, switch the Test Artifact in the Build Variants view. 9 | */ 10 | public class ExampleUnitTest { 11 | @Test 12 | public void addition_isCorrect() throws Exception { 13 | assertEquals(4, 2 + 2); 14 | } 15 | } -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "23.0.3" 6 | 7 | defaultConfig { 8 | applicationId "com.nikhilpanju.recyclerviewsample" 9 | minSdkVersion 14 10 | targetSdkVersion 23 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(include: ['*.jar'], dir: 'libs') 24 | testCompile 'junit:junit:4.12' 25 | compile 'com.android.support:appcompat-v7:23.4.0' 26 | compile 'com.android.support:recyclerview-v7:23.4.0' 27 | compile project(':recyclerviewenhanced') 28 | } 29 | -------------------------------------------------------------------------------- /sample/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in C:\Users\nikhi_000\AppData\Local\Android\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /sample/src/androidTest/java/com/nikhilpanju/recyclerviewsample/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.nikhilpanju.recyclerviewsample; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /sample/src/common/images/Demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikhilpanju/RecyclerViewEnhanced/da8117365dded9508be7cb96c83ca99d64907065/sample/src/common/images/Demo.gif -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /sample/src/main/java/com/nikhilpanju/recyclerviewsample/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.nikhilpanju.recyclerviewsample; 2 | 3 | import android.content.Context; 4 | import android.content.DialogInterface; 5 | import android.os.Bundle; 6 | import android.support.v7.app.AlertDialog; 7 | import android.support.v7.app.AppCompatActivity; 8 | import android.support.v7.widget.LinearLayoutManager; 9 | import android.support.v7.widget.RecyclerView; 10 | import android.view.LayoutInflater; 11 | import android.view.Menu; 12 | import android.view.MenuItem; 13 | import android.view.MotionEvent; 14 | import android.view.View; 15 | import android.view.ViewGroup; 16 | import android.widget.TextView; 17 | 18 | import com.nikhilpanju.recyclerviewenhanced.OnActivityTouchListener; 19 | import com.nikhilpanju.recyclerviewenhanced.RecyclerTouchListener; 20 | 21 | import java.util.ArrayList; 22 | import java.util.List; 23 | 24 | public class MainActivity extends AppCompatActivity implements RecyclerTouchListener.RecyclerTouchListenerHelper { 25 | 26 | RecyclerView mRecyclerView; 27 | MainAdapter mAdapter; 28 | String[] dialogItems; 29 | List unclickableRows, unswipeableRows; 30 | private RecyclerTouchListener onTouchListener; 31 | private int openOptionsPosition; 32 | private OnActivityTouchListener touchListener; 33 | 34 | @Override 35 | protected void onCreate(Bundle savedInstanceState) { 36 | super.onCreate(savedInstanceState); 37 | setContentView(R.layout.activity_main); 38 | 39 | if (getSupportActionBar() != null) 40 | getSupportActionBar().setTitle("RecyclerViewEnhanced"); 41 | 42 | unclickableRows = new ArrayList<>(); 43 | unswipeableRows = new ArrayList<>(); 44 | dialogItems = new String[25]; 45 | for (int i = 0; i < 25; i++) { 46 | dialogItems[i] = String.valueOf(i + 1); 47 | } 48 | 49 | mRecyclerView = (RecyclerView) findViewById(R.id.recyclerView); 50 | mAdapter = new MainAdapter(this, getData()); 51 | mRecyclerView.setAdapter(mAdapter); 52 | mRecyclerView.setLayoutManager(new LinearLayoutManager(this)); 53 | 54 | onTouchListener = new RecyclerTouchListener(this, mRecyclerView); 55 | onTouchListener 56 | .setIndependentViews(R.id.rowButton) 57 | .setViewsToFade(R.id.rowButton) 58 | .setClickable(new RecyclerTouchListener.OnRowClickListener() { 59 | @Override 60 | public void onRowClicked(int position) { 61 | ToastUtil.makeToast(getApplicationContext(), "Row " + (position + 1) + " clicked!"); 62 | } 63 | 64 | @Override 65 | public void onIndependentViewClicked(int independentViewID, int position) { 66 | ToastUtil.makeToast(getApplicationContext(), "Button in row " + (position + 1) + " clicked!"); 67 | } 68 | }) 69 | .setLongClickable(true, new RecyclerTouchListener.OnRowLongClickListener() { 70 | @Override 71 | public void onRowLongClicked(int position) { 72 | ToastUtil.makeToast(getApplicationContext(), "Row " + (position + 1) + " long clicked!"); 73 | } 74 | }) 75 | .setSwipeOptionViews(R.id.add, R.id.edit, R.id.change) 76 | .setSwipeable(R.id.rowFG, R.id.rowBG, new RecyclerTouchListener.OnSwipeOptionsClickListener() { 77 | @Override 78 | public void onSwipeOptionClicked(int viewID, int position) { 79 | String message = ""; 80 | if (viewID == R.id.add) { 81 | message += "Add"; 82 | } else if (viewID == R.id.edit) { 83 | message += "Edit"; 84 | } else if (viewID == R.id.change) { 85 | message += "Change"; 86 | } 87 | message += " clicked for row " + (position + 1); 88 | ToastUtil.makeToast(getApplicationContext(), message); 89 | } 90 | }); 91 | } 92 | 93 | @Override 94 | protected void onResume() { 95 | super.onResume(); 96 | mRecyclerView.addOnItemTouchListener(onTouchListener); } 97 | 98 | @Override 99 | protected void onPause() { 100 | super.onPause(); 101 | mRecyclerView.removeOnItemTouchListener(onTouchListener); 102 | } 103 | 104 | private List getData() { 105 | List list = new ArrayList<>(25); 106 | for (int i = 0; i < 25; i++) { 107 | list.add(new RowModel("Row " + (i + 1), "Some Text... ")); 108 | } 109 | return list; 110 | } 111 | 112 | @Override 113 | public boolean onCreateOptionsMenu(Menu menu) { 114 | getMenuInflater().inflate(R.menu.menu_main, menu); 115 | return super.onCreateOptionsMenu(menu); 116 | } 117 | 118 | @Override 119 | public boolean onOptionsItemSelected(MenuItem item) { 120 | boolean currentState = false; 121 | if (item.isCheckable()) { 122 | currentState = item.isChecked(); 123 | item.setChecked(!currentState); 124 | } 125 | switch (item.getItemId()) { 126 | case R.id.menu_swipeable: 127 | onTouchListener.setSwipeable(!currentState); 128 | return true; 129 | case R.id.menu_clickable: 130 | onTouchListener.setClickable(!currentState); 131 | return true; 132 | case R.id.menu_unclickableRows: 133 | showMultiSelectDialog(unclickableRows, item.getItemId()); 134 | return true; 135 | case R.id.menu_unswipeableRows: 136 | showMultiSelectDialog(unswipeableRows, item.getItemId()); 137 | return true; 138 | case R.id.menu_openOptions: 139 | showSingleSelectDialog(); 140 | return true; 141 | default: 142 | return super.onOptionsItemSelected(item); 143 | } 144 | } 145 | 146 | private void showMultiSelectDialog(final List list, final int menuId) { 147 | boolean[] checkedItems = new boolean[25]; 148 | for (int i = 0; i < list.size(); i++) { 149 | checkedItems[list.get(i)] = true; 150 | } 151 | 152 | String title = "Select {} Rows"; 153 | if (menuId == R.id.menu_unclickableRows) title = title.replace("{}", "Unclickable"); 154 | else title = title.replace("{}", "Unswipeable"); 155 | 156 | AlertDialog.Builder builder = new AlertDialog.Builder(this) 157 | .setTitle(title) 158 | .setMultiChoiceItems(dialogItems, checkedItems, new DialogInterface.OnMultiChoiceClickListener() { 159 | @Override 160 | public void onClick(DialogInterface dialog, int which, boolean isChecked) { 161 | if (isChecked) 162 | list.add(which); 163 | else 164 | list.remove(which); 165 | } 166 | }) 167 | .setPositiveButton("Ok", new DialogInterface.OnClickListener() { 168 | @Override 169 | public void onClick(DialogInterface dialog, int which) { 170 | Integer[] tempArray = new Integer[list.size()]; 171 | if (menuId == R.id.menu_unclickableRows) 172 | onTouchListener.setUnClickableRows(list.toArray(tempArray)); 173 | else 174 | onTouchListener.setUnSwipeableRows(list.toArray(tempArray)); 175 | } 176 | }); 177 | builder.create().show(); 178 | } 179 | 180 | private void showSingleSelectDialog() { 181 | AlertDialog.Builder builder = new AlertDialog.Builder(this) 182 | .setTitle("Open Swipe Options for row: ") 183 | .setSingleChoiceItems(dialogItems, 0, new DialogInterface.OnClickListener() { 184 | @Override 185 | public void onClick(DialogInterface dialog, int which) { 186 | openOptionsPosition = which; 187 | } 188 | }) 189 | .setPositiveButton("Ok", new DialogInterface.OnClickListener() { 190 | @Override 191 | public void onClick(DialogInterface dialog, int which) { 192 | onTouchListener.openSwipeOptions(openOptionsPosition); 193 | } 194 | }); 195 | builder.create().show(); 196 | } 197 | 198 | @Override 199 | public boolean dispatchTouchEvent(MotionEvent ev) { 200 | if (touchListener != null) touchListener.getTouchCoordinates(ev); 201 | return super.dispatchTouchEvent(ev); 202 | } 203 | 204 | @Override 205 | public void setOnActivityTouchListener(OnActivityTouchListener listener) { 206 | this.touchListener = listener; 207 | } 208 | 209 | private class MainAdapter extends RecyclerView.Adapter { 210 | LayoutInflater inflater; 211 | List modelList; 212 | 213 | public MainAdapter(Context context, List list) { 214 | inflater = LayoutInflater.from(context); 215 | modelList = new ArrayList<>(list); 216 | } 217 | 218 | @Override 219 | public MainViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 220 | View view = inflater.inflate(R.layout.recycler_row, parent, false); 221 | return new MainViewHolder(view); 222 | } 223 | 224 | @Override 225 | public void onBindViewHolder(MainViewHolder holder, int position) { 226 | holder.bindData(modelList.get(position)); 227 | } 228 | 229 | @Override 230 | public int getItemCount() { 231 | return modelList.size(); 232 | } 233 | 234 | class MainViewHolder extends RecyclerView.ViewHolder { 235 | 236 | TextView mainText, subText; 237 | 238 | public MainViewHolder(View itemView) { 239 | super(itemView); 240 | mainText = (TextView) itemView.findViewById(R.id.mainText); 241 | subText = (TextView) itemView.findViewById(R.id.subText); 242 | } 243 | 244 | public void bindData(RowModel rowModel) { 245 | mainText.setText(rowModel.getMainText()); 246 | subText.setText(rowModel.getSubText()); 247 | } 248 | } 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /sample/src/main/java/com/nikhilpanju/recyclerviewsample/RowModel.java: -------------------------------------------------------------------------------- 1 | package com.nikhilpanju.recyclerviewsample; 2 | 3 | public class RowModel { 4 | String mainText, subText; 5 | 6 | public RowModel(String mainText, String subText) { 7 | this.mainText = mainText; 8 | this.subText = subText; 9 | } 10 | 11 | public String getMainText() { 12 | return mainText; 13 | } 14 | 15 | public void setMainText(String mainText) { 16 | this.mainText = mainText; 17 | } 18 | 19 | public String getSubText() { 20 | return subText; 21 | } 22 | 23 | public void setSubText(String subText) { 24 | this.subText = subText; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /sample/src/main/java/com/nikhilpanju/recyclerviewsample/ToastUtil.java: -------------------------------------------------------------------------------- 1 | package com.nikhilpanju.recyclerviewsample; 2 | 3 | import android.content.Context; 4 | import android.widget.Toast; 5 | 6 | public class ToastUtil { 7 | public static void makeToast(Context context, String message){ 8 | Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); 9 | } 10 | public static void makeToast(Context context, String message, int duration){ 11 | Toast.makeText(context, message, duration).show(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_add_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_build_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_mode_edit_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/recycler_row.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 16 | 17 | 18 | 26 | 27 | 34 | 35 | 41 | 42 | 55 | 56 | 57 | 58 | 66 | 67 | 74 | 75 | 81 | 82 | 95 | 96 | 97 | 98 | 106 | 107 | 114 | 115 | 121 | 122 | 135 | 136 | 137 | 138 | 139 | 149 | 150 | 158 | 159 | 165 | 166 | 175 | 176 | 183 | 184 | 185 |