├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── styles.xml │ │ │ │ ├── strings.xml │ │ │ │ └── dimens.xml │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── layout │ │ │ │ ├── main_activity.xml │ │ │ │ └── stack_view_item.xml │ │ │ └── drawable │ │ │ │ └── stack_view_item_bg.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── horizontalstackview │ │ │ ├── MainActivity.java │ │ │ └── adapter │ │ │ └── StackViewAdapter.java │ └── androidTest │ │ └── java │ │ └── com │ │ └── horizontalstackview │ │ └── ApplicationTest.java ├── proguard-rules.pro ├── build.gradle └── app.iml ├── .idea ├── .name ├── copyright │ └── profiles_settings.xml ├── dictionaries │ └── binary.xml ├── vcs.xml ├── modules.xml ├── gradle.xml ├── compiler.xml └── misc.xml ├── stackview ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ └── values │ │ │ │ ├── strings.xml │ │ │ │ └── attrs.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── stackview │ │ │ ├── AdapterViewAnimator.java │ │ │ ├── AdapterView.java │ │ │ └── StackViewVertical.java │ └── androidTest │ │ └── java │ │ └── com │ │ └── stackview │ │ └── ApplicationTest.java ├── build.gradle ├── proguard-rules.pro └── stackview.iml ├── settings.gradle ├── README.md ├── .gitignore ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── HorizontalStackView.iml ├── gradlew.bat └── gradlew /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | HorizontalStackView -------------------------------------------------------------------------------- /stackview/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':stackview' 2 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HorizontalStackView 2 | 3 | 4 | ![Alt text](http://i.giphy.com/3o85xJuAsQLEH1iB7q.gif) 5 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/dictionaries/binary.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /stackview/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | StackView 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/workspace.xml 4 | /.idea/libraries 5 | .DS_Store 6 | /build 7 | /captures 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | HorizontalStackView 3 | 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryroot/HorizontalStackView/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryroot/HorizontalStackView/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryroot/HorizontalStackView/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryroot/HorizontalStackView/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binaryroot/HorizontalStackView/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1dp 4 | 1dp 5 | -------------------------------------------------------------------------------- /stackview/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Apr 10 15:27:10 PDT 2013 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.2.1-all.zip 7 | -------------------------------------------------------------------------------- /stackview/src/androidTest/java/com/stackview/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.stackview; 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 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/horizontalstackview/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.horizontalstackview; 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 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/main_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/stack_view_item_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /stackview/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 21 5 | buildToolsVersion "22.0.1" 6 | 7 | defaultConfig { 8 | minSdkVersion 14 9 | targetSdkVersion 21 10 | versionCode 1 11 | versionName "1.0" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | compile fileTree(dir: 'libs', include: ['*.jar']) 23 | compile 'com.android.support:appcompat-v7:22.0.0' 24 | } 25 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 19 | -------------------------------------------------------------------------------- /app/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 /Users/binary/Library/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 | -------------------------------------------------------------------------------- /stackview/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 /Users/binary/Library/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 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 21 5 | buildToolsVersion "22.0.1" 6 | 7 | defaultConfig { 8 | applicationId "com.horizontalstackview" 9 | minSdkVersion 14 10 | targetSdkVersion 21 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(dir: 'libs', include: ['*.jar']) 24 | compile 'com.android.support:appcompat-v7:22.0.0' 25 | compile project(':stackview') 26 | compile 'com.squareup.picasso:picasso:2.3.2' 27 | } 28 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /HorizontalStackView.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/layout/stack_view_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 12 | 13 | 18 | 19 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/horizontalstackview/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.horizontalstackview; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | 6 | import com.horizontalstackview.adapter.StackViewAdapter; 7 | import com.stackview.StackViewVertical; 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | public class MainActivity extends Activity { 13 | 14 | private final List mImagesLink = new ArrayList<>(); 15 | 16 | { 17 | mImagesLink.add("http://www.blastr.com/sites/blastr/files/1400308574-iron-man.jpg"); 18 | mImagesLink.add("http://www.kevinwebb22.com/wp-content/uploads/2012/05/Hulk_Avengers_Movie_Poster.jpg"); 19 | mImagesLink.add("https://kinobox.in.ua/uploads/551c051011597_1427899664.jpg"); 20 | mImagesLink.add("http://relbox.ru/images/movies/mstiteli-2/mstiteli-2-02.jpg"); 21 | mImagesLink.add("http://dlm4.meta.ua/pic/0/69/105/bSj0nWx9ar.jpg"); 22 | } 23 | 24 | /*********************************************************************/ 25 | /**************************** Activity *******************************/ 26 | /*********************************************************************/ 27 | 28 | @Override 29 | protected void onCreate(Bundle savedInstanceState) { 30 | super.onCreate(savedInstanceState); 31 | setContentView(R.layout.main_activity); 32 | 33 | initStackView(); 34 | } 35 | 36 | private void initStackView() { 37 | StackViewVertical stackViewVertical = (StackViewVertical) findViewById(R.id.stackViewVertical); 38 | stackViewVertical.setAdapter(new StackViewAdapter(MainActivity.this, mImagesLink)); 39 | } 40 | } -------------------------------------------------------------------------------- /stackview/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 23 | 24 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Android Lint 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 36 | 37 | 38 | 39 | 40 | Android API 16 Platform 41 | 42 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /app/src/main/java/com/horizontalstackview/adapter/StackViewAdapter.java: -------------------------------------------------------------------------------- 1 | package com.horizontalstackview.adapter; 2 | 3 | import android.content.Context; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.BaseAdapter; 8 | import android.widget.ImageView; 9 | import android.widget.TextView; 10 | 11 | import com.horizontalstackview.R; 12 | import com.squareup.picasso.Picasso; 13 | 14 | import java.util.List; 15 | 16 | public class StackViewAdapter extends BaseAdapter { 17 | 18 | private Context mContext; 19 | private List mPhotos; 20 | 21 | public StackViewAdapter(Context context, List photos) { 22 | mContext = context; 23 | mPhotos = photos; 24 | } 25 | 26 | /*********************************************************************/ 27 | /**************************** BaseAdapter ****************************/ 28 | /*********************************************************************/ 29 | 30 | @Override 31 | public int getCount() { 32 | if(mPhotos == null){ 33 | return 0; 34 | } else { 35 | return mPhotos.size(); 36 | } 37 | } 38 | 39 | @Override 40 | public String getItem(int position) { 41 | return mPhotos.get(position); 42 | } 43 | 44 | @Override 45 | public long getItemId(int position) { 46 | return 0; 47 | } 48 | 49 | @Override 50 | public View getView(final int position, View view, ViewGroup parent) { 51 | final ViewHolder viewHolder; 52 | if (view == null) { 53 | LayoutInflater vi = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 54 | view = vi.inflate(R.layout.stack_view_item, null, false); 55 | viewHolder = new ViewHolder(); 56 | viewHolder.mPhotoView = (ImageView) view.findViewById(R.id.slider_item_image); 57 | viewHolder.mCounter = (TextView) view.findViewById(R.id.slider_item_count); 58 | view.setTag(viewHolder); 59 | } else { 60 | viewHolder = (ViewHolder) view.getTag(); 61 | } 62 | 63 | Picasso.with(mContext).load(mPhotos.get(position)).fit().centerCrop().into(viewHolder.mPhotoView); 64 | viewHolder.mCounter.setText((position+1)+"/"+getCount()); 65 | return view; 66 | } 67 | 68 | private static final class ViewHolder { 69 | private static ImageView mPhotoView; 70 | private static TextView mCounter; 71 | } 72 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /stackview/stackview.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /app/app.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /stackview/src/main/java/com/stackview/AdapterViewAnimator.java: -------------------------------------------------------------------------------- 1 | package com.stackview; 2 | 3 | /** 4 | * Created by binary on 5/19/15. 5 | */ 6 | 7 | import android.animation.AnimatorInflater; 8 | import android.animation.ObjectAnimator; 9 | import android.annotation.TargetApi; 10 | import android.content.Context; 11 | import android.content.res.TypedArray; 12 | import android.os.Build; 13 | import android.os.Handler; 14 | import android.os.Parcel; 15 | import android.os.Parcelable; 16 | import android.util.AttributeSet; 17 | import android.view.MotionEvent; 18 | import android.view.View; 19 | import android.view.ViewConfiguration; 20 | import android.view.ViewGroup; 21 | import android.widget.Adapter; 22 | import android.widget.Advanceable; 23 | import android.widget.FrameLayout; 24 | 25 | import java.util.ArrayList; 26 | import java.util.HashMap; 27 | 28 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN) 29 | public abstract class AdapterViewAnimator extends AdapterView 30 | implements Advanceable { 31 | private static final String TAG = "RemoteViewAnimator"; 32 | 33 | /** 34 | * The index of the current child, which appears anywhere from the beginning 35 | * to the end of the current set of children, as specified by {@link #mActiveOffset} 36 | */ 37 | int mWhichChild = 0; 38 | 39 | /** 40 | * The index of the child to restore after the asynchronous connection from the 41 | * RemoteViewsAdapter has been. 42 | */ 43 | private int mRestoreWhichChild = -1; 44 | 45 | /** 46 | * Whether or not the first view(s) should be animated in 47 | */ 48 | boolean mAnimateFirstTime = true; 49 | 50 | /** 51 | * Represents where the in the current window of 52 | * views the current mDisplayedChild sits 53 | */ 54 | int mActiveOffset = 0; 55 | 56 | /** 57 | * The number of views that the {@link AdapterViewAnimator} keeps as children at any 58 | * given time (not counting views that are pending removal, see {@link #mPreviousViews}). 59 | */ 60 | int mMaxNumActiveViews = 1; 61 | 62 | /** 63 | * Map of the children of the {@link AdapterViewAnimator}. 64 | */ 65 | HashMap mViewsMap = new HashMap(); 66 | 67 | /** 68 | * List of views pending removal from the {@link AdapterViewAnimator} 69 | */ 70 | ArrayList mPreviousViews; 71 | 72 | /** 73 | * The index, relative to the adapter, of the beginning of the window of views 74 | */ 75 | int mCurrentWindowStart = 0; 76 | 77 | /** 78 | * The index, relative to the adapter, of the end of the window of views 79 | */ 80 | int mCurrentWindowEnd = -1; 81 | 82 | /** 83 | * The same as {@link #mCurrentWindowStart}, except when the we have bounded 84 | * {@link #mCurrentWindowStart} to be non-negative 85 | */ 86 | int mCurrentWindowStartUnbounded = 0; 87 | 88 | /** 89 | * Listens for data changes from the adapter 90 | */ 91 | AdapterDataSetObserver mDataSetObserver; 92 | 93 | /** 94 | * The {@link Adapter} for this {@link AdapterViewAnimator} 95 | */ 96 | Adapter mAdapter; 97 | 98 | 99 | /** 100 | * The remote adapter containing the data to be displayed by this view to be set 101 | */ 102 | boolean mDeferNotifyDataSetChanged = false; 103 | 104 | /** 105 | * Specifies whether this is the first time the animator is showing views 106 | */ 107 | boolean mFirstTime = true; 108 | 109 | /** 110 | * Specifies if the animator should wrap from 0 to the end and vice versa 111 | * or have hard boundaries at the beginning and end 112 | */ 113 | boolean mLoopViews = true; 114 | 115 | /** 116 | * The width and height of some child, used as a size reference in-case our 117 | * dimensions are unspecified by the parent. 118 | */ 119 | int mReferenceChildWidth = -1; 120 | int mReferenceChildHeight = -1; 121 | 122 | /** 123 | * In and out animations. 124 | */ 125 | ObjectAnimator mInAnimation; 126 | ObjectAnimator mOutAnimation; 127 | 128 | /** 129 | * Current touch state. 130 | */ 131 | private int mTouchMode = TOUCH_MODE_NONE; 132 | 133 | /** 134 | * Private touch states. 135 | */ 136 | static final int TOUCH_MODE_NONE = 0; 137 | static final int TOUCH_MODE_DOWN_IN_CURRENT_VIEW = 1; 138 | static final int TOUCH_MODE_HANDLED = 2; 139 | 140 | private Runnable mPendingCheckForTap; 141 | 142 | private static final int DEFAULT_ANIMATION_DURATION = 200; 143 | 144 | public AdapterViewAnimator(Context context) { 145 | this(context, null); 146 | } 147 | 148 | public AdapterViewAnimator(Context context, AttributeSet attrs) { 149 | this(context, attrs, 0); 150 | } 151 | 152 | public AdapterViewAnimator(Context context, AttributeSet attrs, int defStyleAttr) { 153 | super(context, attrs, defStyleAttr); 154 | 155 | TypedArray a = context.obtainStyledAttributes(attrs, 156 | R.styleable.AdapterViewAnimatorHorizontal, defStyleAttr, 0); 157 | int resource = a.getResourceId( 158 | R.styleable.AdapterViewAnimatorHorizontal_inAnimationHorizontal, 0); 159 | if (resource > 0) { 160 | setInAnimation(context, resource); 161 | } else { 162 | setInAnimation(getDefaultInAnimation()); 163 | } 164 | 165 | resource = a.getResourceId(R.styleable.AdapterViewAnimatorHorizontal_outAnimationHorizontal, 0); 166 | if (resource > 0) { 167 | setOutAnimation(context, resource); 168 | } else { 169 | setOutAnimation(getDefaultOutAnimation()); 170 | } 171 | 172 | boolean flag = a.getBoolean( 173 | R.styleable.AdapterViewAnimatorHorizontal_animateFirstViewHorizontal, true); 174 | setAnimateFirstView(flag); 175 | 176 | mLoopViews = a.getBoolean( 177 | R.styleable.AdapterViewAnimatorHorizontal_loopViewsHorizontal, false); 178 | 179 | a.recycle(); 180 | 181 | initViewAnimator(); 182 | } 183 | 184 | /** 185 | * Initialize this {@link AdapterViewAnimator} 186 | */ 187 | private void initViewAnimator() { 188 | mPreviousViews = new ArrayList(); 189 | } 190 | 191 | class ViewAndMetaData { 192 | View view; 193 | int relativeIndex; 194 | int adapterPosition; 195 | long itemId; 196 | 197 | ViewAndMetaData(View view, int relativeIndex, int adapterPosition, long itemId) { 198 | this.view = view; 199 | this.relativeIndex = relativeIndex; 200 | this.adapterPosition = adapterPosition; 201 | this.itemId = itemId; 202 | } 203 | } 204 | 205 | /** 206 | * This method is used by subclasses to configure the animator to display the 207 | * desired number of views, and specify the offset 208 | * 209 | * @param numVisibleViews The number of views the animator keeps in the {@link ViewGroup} 210 | * @param activeOffset This parameter specifies where the current index ({@link #mWhichChild}) 211 | * sits within the window. For example if activeOffset is 1, and numVisibleViews is 3, 212 | * and {@link #setDisplayedChild(int)} is called with 10, then the effective window will 213 | * be the indexes 9, 10, and 11. In the same example, if activeOffset were 0, then the 214 | * window would instead contain indexes 10, 11 and 12. 215 | */ 216 | void configureViewAnimator(int numVisibleViews, int activeOffset) { 217 | if (activeOffset > numVisibleViews - 1) { 218 | // Throw an exception here. 219 | } 220 | mMaxNumActiveViews = numVisibleViews; 221 | mActiveOffset = activeOffset; 222 | mPreviousViews.clear(); 223 | mViewsMap.clear(); 224 | removeAllViewsInLayout(); 225 | mCurrentWindowStart = 0; 226 | mCurrentWindowEnd = -1; 227 | } 228 | 229 | /** 230 | * This class should be overridden by subclasses to customize view transitions within 231 | * the set of visible views 232 | * 233 | * @param fromIndex The relative index within the window that the view was in, -1 if it wasn't 234 | * in the window 235 | * @param toIndex The relative index within the window that the view is going to, -1 if it is 236 | * being removed 237 | * @param view The view that is being animated 238 | */ 239 | void transformViewForTransition(int fromIndex, int toIndex, View view, boolean animate) { 240 | if (fromIndex == -1) { 241 | mInAnimation.setTarget(view); 242 | mInAnimation.start(); 243 | } else if (toIndex == -1) { 244 | mOutAnimation.setTarget(view); 245 | mOutAnimation.start(); 246 | } 247 | } 248 | 249 | ObjectAnimator getDefaultInAnimation() { 250 | ObjectAnimator anim = ObjectAnimator.ofFloat(null, "alpha", 0.0f, 1.0f); 251 | anim.setDuration(DEFAULT_ANIMATION_DURATION); 252 | return anim; 253 | } 254 | 255 | ObjectAnimator getDefaultOutAnimation() { 256 | ObjectAnimator anim = ObjectAnimator.ofFloat(null, "alpha", 1.0f, 0.0f); 257 | anim.setDuration(DEFAULT_ANIMATION_DURATION); 258 | return anim; 259 | } 260 | 261 | /** 262 | * Sets which child view will be displayed. 263 | * 264 | * @param whichChild the index of the child view to display 265 | */ 266 | public void setDisplayedChild(int whichChild) { 267 | setDisplayedChild(whichChild, true); 268 | } 269 | 270 | private void setDisplayedChild(int whichChild, boolean animate) { 271 | if (mAdapter != null) { 272 | mWhichChild = whichChild; 273 | if (whichChild >= getWindowSize()) { 274 | mWhichChild = mLoopViews ? 0 : getWindowSize() - 1; 275 | } else if (whichChild < 0) { 276 | mWhichChild = mLoopViews ? getWindowSize() - 1 : 0; 277 | } 278 | 279 | boolean hasFocus = getFocusedChild() != null; 280 | // This will clear old focus if we had it 281 | showOnly(mWhichChild, animate); 282 | if (hasFocus) { 283 | // Try to retake focus if we had it 284 | requestFocus(FOCUS_FORWARD); 285 | } 286 | } 287 | } 288 | 289 | /** 290 | * To be overridden by subclasses. This method applies a view / index specific 291 | * transform to the child view. 292 | * 293 | * @param child 294 | * @param relativeIndex 295 | */ 296 | void applyTransformForChildAtIndex(View child, int relativeIndex) { 297 | } 298 | 299 | /** 300 | * Returns the index of the currently displayed child view. 301 | */ 302 | public int getDisplayedChild() { 303 | return mWhichChild; 304 | } 305 | 306 | /** 307 | * Manually shows the next child. 308 | */ 309 | public void showNext() { 310 | setDisplayedChild(mWhichChild + 1); 311 | } 312 | 313 | /** 314 | * Manually shows the previous child. 315 | */ 316 | public void showPrevious() { 317 | setDisplayedChild(mWhichChild - 1); 318 | } 319 | 320 | int modulo(int pos, int size) { 321 | if (size > 0) { 322 | return (size + (pos % size)) % size; 323 | } else { 324 | return 0; 325 | } 326 | } 327 | 328 | /** 329 | * Get the view at this index relative to the current window's start 330 | * 331 | * @param relativeIndex Position relative to the current window's start 332 | * @return View at this index, null if the index is outside the bounds 333 | */ 334 | View getViewAtRelativeIndex(int relativeIndex) { 335 | if (relativeIndex >= 0 && relativeIndex <= getNumActiveViews() - 1 && mAdapter != null) { 336 | int i = modulo(mCurrentWindowStartUnbounded + relativeIndex, getWindowSize()); 337 | if (mViewsMap.get(i) != null) { 338 | return mViewsMap.get(i).view; 339 | } 340 | } 341 | return null; 342 | } 343 | 344 | int getNumActiveViews() { 345 | if (mAdapter != null) { 346 | return Math.min(getCount() + 1, mMaxNumActiveViews); 347 | } else { 348 | return mMaxNumActiveViews; 349 | } 350 | } 351 | 352 | int getWindowSize() { 353 | if (mAdapter != null) { 354 | int adapterCount = getCount(); 355 | if (adapterCount <= getNumActiveViews() && mLoopViews) { 356 | return adapterCount * mMaxNumActiveViews; 357 | } else { 358 | return adapterCount; 359 | } 360 | } else { 361 | return 0; 362 | } 363 | } 364 | 365 | private ViewAndMetaData getMetaDataForChild(View child) { 366 | for (ViewAndMetaData vm : mViewsMap.values()) { 367 | if (vm.view == child) { 368 | return vm; 369 | } 370 | } 371 | return null; 372 | } 373 | 374 | LayoutParams createOrReuseLayoutParams(View v) { 375 | final ViewGroup.LayoutParams currentLp = v.getLayoutParams(); 376 | if (currentLp instanceof ViewGroup.LayoutParams) { 377 | LayoutParams lp = (LayoutParams) currentLp; 378 | return lp; 379 | } 380 | return new ViewGroup.LayoutParams(0, 0); 381 | } 382 | 383 | void refreshChildren() { 384 | if (mAdapter == null) return; 385 | for (int i = mCurrentWindowStart; i <= mCurrentWindowEnd; i++) { 386 | int index = modulo(i, getWindowSize()); 387 | 388 | int adapterCount = getCount(); 389 | // get the fresh child from the adapter 390 | final View updatedChild = mAdapter.getView(modulo(i, adapterCount), null, this); 391 | 392 | if (mViewsMap.containsKey(index)) { 393 | final FrameLayout fl = (FrameLayout) mViewsMap.get(index).view; 394 | // add the new child to the frame, if it exists 395 | if (updatedChild != null) { 396 | // flush out the old child 397 | fl.removeAllViewsInLayout(); 398 | fl.addView(updatedChild); 399 | } 400 | } 401 | } 402 | } 403 | 404 | /** 405 | * This method can be overridden so that subclasses can provide a custom frame in which their 406 | * children can live. For example, StackView adds padding to its childrens' frames so as to 407 | * accomodate for the highlight effect. 408 | * 409 | * @return The FrameLayout into which children can be placed. 410 | */ 411 | FrameLayout getFrameForChild() { 412 | return new FrameLayout(getContext()); 413 | } 414 | 415 | /** 416 | * Shows only the specified child. The other displays Views exit the screen, 417 | * optionally with the with the {@link #getOutAnimation() out animation} and 418 | * the specified child enters the screen, optionally with the 419 | * {@link #getInAnimation() in animation}. 420 | * 421 | * @param childIndex The index of the child to be shown. 422 | * @param animate Whether or not to use the in and out animations, defaults 423 | * to true. 424 | */ 425 | void showOnly(int childIndex, boolean animate) { 426 | if (mAdapter == null) return; 427 | final int adapterCount = getCount(); 428 | if (adapterCount == 0) return; 429 | 430 | for (int i = 0; i < mPreviousViews.size(); i++) { 431 | View viewToRemove = mViewsMap.get(mPreviousViews.get(i)).view; 432 | mViewsMap.remove(mPreviousViews.get(i)); 433 | viewToRemove.clearAnimation(); 434 | if (viewToRemove instanceof ViewGroup) { 435 | ViewGroup vg = (ViewGroup) viewToRemove; 436 | vg.removeAllViewsInLayout(); 437 | } 438 | // applyTransformForChildAtIndex here just allows for any cleanup 439 | // associated with this view that may need to be done by a subclass 440 | applyTransformForChildAtIndex(viewToRemove, -1); 441 | 442 | removeViewInLayout(viewToRemove); 443 | } 444 | mPreviousViews.clear(); 445 | int newWindowStartUnbounded = childIndex - mActiveOffset; 446 | int newWindowEndUnbounded = newWindowStartUnbounded + getNumActiveViews() - 1; 447 | int newWindowStart = Math.max(0, newWindowStartUnbounded); 448 | int newWindowEnd = Math.min(adapterCount - 1, newWindowEndUnbounded); 449 | 450 | if (mLoopViews) { 451 | newWindowStart = newWindowStartUnbounded; 452 | newWindowEnd = newWindowEndUnbounded; 453 | } 454 | int rangeStart = modulo(newWindowStart, getWindowSize()); 455 | int rangeEnd = modulo(newWindowEnd, getWindowSize()); 456 | 457 | boolean wrap = false; 458 | if (rangeStart > rangeEnd) { 459 | wrap = true; 460 | } 461 | 462 | // This section clears out any items that are in our active views list 463 | // but are outside the effective bounds of our window (this is becomes an issue 464 | // at the extremities of the list, eg. where newWindowStartUnbounded < 0 or 465 | // newWindowEndUnbounded > adapterCount - 1 466 | for (Integer index : mViewsMap.keySet()) { 467 | boolean remove = false; 468 | if (!wrap && (index < rangeStart || index > rangeEnd)) { 469 | remove = true; 470 | } else if (wrap && (index > rangeEnd && index < rangeStart)) { 471 | remove = true; 472 | } 473 | 474 | if (remove) { 475 | View previousView = mViewsMap.get(index).view; 476 | int oldRelativeIndex = mViewsMap.get(index).relativeIndex; 477 | 478 | mPreviousViews.add(index); 479 | transformViewForTransition(oldRelativeIndex, -1, previousView, animate); 480 | } 481 | } 482 | 483 | // If the window has changed 484 | if (!(newWindowStart == mCurrentWindowStart && newWindowEnd == mCurrentWindowEnd && 485 | newWindowStartUnbounded == mCurrentWindowStartUnbounded)) { 486 | // Run through the indices in the new range 487 | for (int i = newWindowStart; i <= newWindowEnd; i++) { 488 | 489 | int index = modulo(i, getWindowSize()); 490 | int oldRelativeIndex; 491 | if (mViewsMap.containsKey(index)) { 492 | oldRelativeIndex = mViewsMap.get(index).relativeIndex; 493 | } else { 494 | oldRelativeIndex = -1; 495 | } 496 | int newRelativeIndex = i - newWindowStartUnbounded; 497 | 498 | // If this item is in the current window, great, we just need to apply 499 | // the transform for it's new relative position in the window, and animate 500 | // between it's current and new relative positions 501 | boolean inOldRange = mViewsMap.containsKey(index) && !mPreviousViews.contains(index); 502 | 503 | if (inOldRange) { 504 | View view = mViewsMap.get(index).view; 505 | mViewsMap.get(index).relativeIndex = newRelativeIndex; 506 | applyTransformForChildAtIndex(view, newRelativeIndex); 507 | transformViewForTransition(oldRelativeIndex, newRelativeIndex, view, animate); 508 | 509 | // Otherwise this view is new to the window 510 | } else { 511 | // Get the new view from the adapter, add it and apply any transform / animation 512 | final int adapterPosition = modulo(i, adapterCount); 513 | View newView = mAdapter.getView(adapterPosition, null, this); 514 | long itemId = mAdapter.getItemId(adapterPosition); 515 | 516 | // We wrap the new view in a FrameLayout so as to respect the contract 517 | // with the adapter, that is, that we don't modify this view directly 518 | FrameLayout fl = getFrameForChild(); 519 | 520 | // If the view from the adapter is null, we still keep an empty frame in place 521 | if (newView != null) { 522 | fl.addView(newView); 523 | } 524 | mViewsMap.put(index, new ViewAndMetaData(fl, newRelativeIndex, 525 | adapterPosition, itemId)); 526 | addChild(fl); 527 | applyTransformForChildAtIndex(fl, newRelativeIndex); 528 | transformViewForTransition(-1, newRelativeIndex, fl, animate); 529 | } 530 | mViewsMap.get(index).view.bringToFront(); 531 | } 532 | mCurrentWindowStart = newWindowStart; 533 | mCurrentWindowEnd = newWindowEnd; 534 | mCurrentWindowStartUnbounded = newWindowStartUnbounded; 535 | } 536 | requestLayout(); 537 | invalidate(); 538 | } 539 | 540 | private void addChild(View child) { 541 | addViewInLayout(child, -1, createOrReuseLayoutParams(child)); 542 | 543 | // This code is used to obtain a reference width and height of a child in case we need 544 | // to decide our own size. TODO: Do we want to update the size of the child that we're 545 | // using for reference size? If so, when? 546 | if (mReferenceChildWidth == -1 || mReferenceChildHeight == -1) { 547 | int measureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 548 | child.measure(measureSpec, measureSpec); 549 | mReferenceChildWidth = child.getMeasuredWidth(); 550 | mReferenceChildHeight = child.getMeasuredHeight(); 551 | } 552 | } 553 | 554 | void showTapFeedback(View v) { 555 | v.setPressed(true); 556 | } 557 | 558 | void hideTapFeedback(View v) { 559 | v.setPressed(false); 560 | } 561 | 562 | void cancelHandleClick() { 563 | View v = getCurrentView(); 564 | if (v != null) { 565 | hideTapFeedback(v); 566 | } 567 | mTouchMode = TOUCH_MODE_NONE; 568 | } 569 | 570 | final class CheckForTap implements Runnable { 571 | public void run() { 572 | if (mTouchMode == TOUCH_MODE_DOWN_IN_CURRENT_VIEW) { 573 | View v = getCurrentView(); 574 | showTapFeedback(v); 575 | } 576 | } 577 | } 578 | 579 | @Override 580 | public boolean onTouchEvent(MotionEvent ev) { 581 | int action = ev.getAction(); 582 | boolean handled = false; 583 | switch (action) { 584 | case MotionEvent.ACTION_DOWN: { 585 | View v = getCurrentView(); 586 | if (v != null) { 587 | if (isTransformedTouchPointInView(ev.getX(), ev.getY(), v, null)) { 588 | if (mPendingCheckForTap == null) { 589 | mPendingCheckForTap = new CheckForTap(); 590 | } 591 | mTouchMode = TOUCH_MODE_DOWN_IN_CURRENT_VIEW; 592 | postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); 593 | } 594 | } 595 | break; 596 | } 597 | case MotionEvent.ACTION_MOVE: 598 | break; 599 | case MotionEvent.ACTION_POINTER_UP: 600 | break; 601 | case MotionEvent.ACTION_UP: { 602 | if (mTouchMode == TOUCH_MODE_DOWN_IN_CURRENT_VIEW) { 603 | final View v = getCurrentView(); 604 | final ViewAndMetaData viewData = getMetaDataForChild(v); 605 | if (v != null) { 606 | if (isTransformedTouchPointInView(ev.getX(), ev.getY(), v, null)) { 607 | final Handler handler = getHandler(); 608 | if (handler != null) { 609 | handler.removeCallbacks(mPendingCheckForTap); 610 | } 611 | showTapFeedback(v); 612 | postDelayed(new Runnable() { 613 | public void run() { 614 | hideTapFeedback(v); 615 | post(new Runnable() { 616 | public void run() { 617 | if (viewData != null) { 618 | performItemClick(v, viewData.adapterPosition, 619 | viewData.itemId); 620 | } else { 621 | performItemClick(v, 0, 0); 622 | } 623 | } 624 | }); 625 | } 626 | }, ViewConfiguration.getPressedStateDuration()); 627 | handled = true; 628 | } 629 | } 630 | } 631 | mTouchMode = TOUCH_MODE_NONE; 632 | break; 633 | } 634 | case MotionEvent.ACTION_CANCEL: { 635 | View v = getCurrentView(); 636 | if (v != null) { 637 | hideTapFeedback(v); 638 | } 639 | mTouchMode = TOUCH_MODE_NONE; 640 | } 641 | } 642 | return handled; 643 | } 644 | 645 | private void measureChildren() { 646 | final int count = getChildCount(); 647 | final int childWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); 648 | final int childHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom(); 649 | 650 | for (int i = 0; i < count; i++) { 651 | final View child = getChildAt(i); 652 | child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY), 653 | MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY)); 654 | } 655 | } 656 | 657 | @Override 658 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 659 | int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); 660 | int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); 661 | final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); 662 | final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); 663 | 664 | boolean haveChildRefSize = (mReferenceChildWidth != -1 && mReferenceChildHeight != -1); 665 | 666 | // We need to deal with the case where our parent hasn't told us how 667 | // big we should be. In this case we try to use the desired size of the first 668 | // child added. 669 | if (heightSpecMode == MeasureSpec.UNSPECIFIED) { 670 | heightSpecSize = haveChildRefSize ? mReferenceChildHeight + getPaddingTop() + 671 | getPaddingBottom() : 0; 672 | } else if (heightSpecMode == MeasureSpec.AT_MOST) { 673 | if (haveChildRefSize) { 674 | int height = mReferenceChildHeight + getPaddingTop() + getPaddingBottom(); 675 | if (height > heightSpecSize) { 676 | heightSpecSize |= MEASURED_STATE_TOO_SMALL; 677 | } else { 678 | heightSpecSize = height; 679 | } 680 | } 681 | } 682 | 683 | if (widthSpecMode == MeasureSpec.UNSPECIFIED) { 684 | widthSpecSize = haveChildRefSize ? mReferenceChildWidth + getPaddingLeft() + 685 | getPaddingRight() : 0; 686 | } else if (heightSpecMode == MeasureSpec.AT_MOST) { 687 | if (haveChildRefSize) { 688 | int width = mReferenceChildWidth + getPaddingLeft() + getPaddingRight(); 689 | if (width > widthSpecSize) { 690 | widthSpecSize |= MEASURED_STATE_TOO_SMALL; 691 | } else { 692 | widthSpecSize = width; 693 | } 694 | } 695 | } 696 | 697 | setMeasuredDimension(widthSpecSize, heightSpecSize); 698 | measureChildren(); 699 | } 700 | 701 | void checkForAndHandleDataChanged() { 702 | boolean dataChanged = mDataChanged; 703 | if (dataChanged) { 704 | post(new Runnable() { 705 | public void run() { 706 | handleDataChanged(); 707 | // if the data changes, mWhichChild might be out of the bounds of the adapter 708 | // in this case, we reset mWhichChild to the beginning 709 | if (mWhichChild >= getWindowSize()) { 710 | mWhichChild = 0; 711 | 712 | showOnly(mWhichChild, false); 713 | } else if (mOldItemCount != getCount()) { 714 | showOnly(mWhichChild, false); 715 | } 716 | refreshChildren(); 717 | requestLayout(); 718 | } 719 | }); 720 | } 721 | mDataChanged = false; 722 | } 723 | 724 | @Override 725 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 726 | checkForAndHandleDataChanged(); 727 | 728 | final int childCount = getChildCount(); 729 | for (int i = 0; i < childCount; i++) { 730 | final View child = getChildAt(i); 731 | 732 | int childRight = getPaddingLeft() + child.getMeasuredWidth(); 733 | int childBottom = getPaddingTop() + child.getMeasuredHeight(); 734 | 735 | child.layout(getPaddingLeft(), getPaddingTop(), childRight, childBottom); 736 | } 737 | } 738 | 739 | static class SavedState extends BaseSavedState { 740 | int whichChild; 741 | 742 | /** 743 | * Constructor called from {@link AdapterViewAnimator#onSaveInstanceState()} 744 | */ 745 | SavedState(Parcelable superState, int whichChild) { 746 | super(superState); 747 | this.whichChild = whichChild; 748 | } 749 | 750 | /** 751 | * Constructor called from {@link #CREATOR} 752 | */ 753 | private SavedState(Parcel in) { 754 | super(in); 755 | this.whichChild = in.readInt(); 756 | } 757 | 758 | @Override 759 | public void writeToParcel(Parcel out, int flags) { 760 | super.writeToParcel(out, flags); 761 | out.writeInt(this.whichChild); 762 | } 763 | 764 | @Override 765 | public String toString() { 766 | return "AdapterViewAnimator.SavedState{ whichChild = " + this.whichChild + " }"; 767 | } 768 | 769 | public static final Parcelable.Creator CREATOR 770 | = new Parcelable.Creator() { 771 | public SavedState createFromParcel(Parcel in) { 772 | return new SavedState(in); 773 | } 774 | 775 | public SavedState[] newArray(int size) { 776 | return new SavedState[size]; 777 | } 778 | }; 779 | } 780 | 781 | @Override 782 | public Parcelable onSaveInstanceState() { 783 | Parcelable superState = super.onSaveInstanceState(); 784 | return new SavedState(superState, mWhichChild); 785 | } 786 | 787 | @Override 788 | public void onRestoreInstanceState(Parcelable state) { 789 | SavedState ss = (SavedState) state; 790 | super.onRestoreInstanceState(ss.getSuperState()); 791 | 792 | // Here we set mWhichChild in addition to setDisplayedChild 793 | // We do the former in case mAdapter is null, and hence setDisplayedChild won't 794 | // set mWhichChild 795 | mWhichChild = ss.whichChild; 796 | 797 | // When using RemoteAdapters, the async connection process can lead to 798 | // onRestoreInstanceState to be called before setAdapter(), so we need to save the previous 799 | // values to restore the list position after we connect, and can skip setting the displayed 800 | // child until then. 801 | if (mAdapter == null) { 802 | mRestoreWhichChild = mWhichChild; 803 | } else { 804 | setDisplayedChild(mWhichChild, false); 805 | } 806 | } 807 | 808 | /** 809 | * Returns the View corresponding to the currently displayed child. 810 | * 811 | * @return The View currently displayed. 812 | * @see #getDisplayedChild() 813 | */ 814 | public View getCurrentView() { 815 | return getViewAtRelativeIndex(mActiveOffset); 816 | } 817 | 818 | /** 819 | * Returns the current animation used to animate a View that enters the screen. 820 | * 821 | * @return An Animation or null if none is set. 822 | * @see #setInAnimation(android.animation.ObjectAnimator) 823 | * @see #setInAnimation(android.content.Context, int) 824 | */ 825 | public ObjectAnimator getInAnimation() { 826 | return mInAnimation; 827 | } 828 | 829 | /** 830 | * Specifies the animation used to animate a View that enters the screen. 831 | * 832 | * @param inAnimation The animation started when a View enters the screen. 833 | * @see #getInAnimation() 834 | * @see #setInAnimation(android.content.Context, int) 835 | */ 836 | public void setInAnimation(ObjectAnimator inAnimation) { 837 | mInAnimation = inAnimation; 838 | } 839 | 840 | /** 841 | * Returns the current animation used to animate a View that exits the screen. 842 | * 843 | * @return An Animation or null if none is set. 844 | * @see #setOutAnimation(android.animation.ObjectAnimator) 845 | * @see #setOutAnimation(android.content.Context, int) 846 | */ 847 | public ObjectAnimator getOutAnimation() { 848 | return mOutAnimation; 849 | } 850 | 851 | /** 852 | * Specifies the animation used to animate a View that exit the screen. 853 | * 854 | * @param outAnimation The animation started when a View exit the screen. 855 | * @see #getOutAnimation() 856 | * @see #setOutAnimation(android.content.Context, int) 857 | */ 858 | public void setOutAnimation(ObjectAnimator outAnimation) { 859 | mOutAnimation = outAnimation; 860 | } 861 | 862 | /** 863 | * Specifies the animation used to animate a View that enters the screen. 864 | * 865 | * @param context The application's environment. 866 | * @param resourceID The resource id of the animation. 867 | * @see #getInAnimation() 868 | * @see #setInAnimation(android.animation.ObjectAnimator) 869 | */ 870 | public void setInAnimation(Context context, int resourceID) { 871 | setInAnimation((ObjectAnimator) AnimatorInflater.loadAnimator(context, resourceID)); 872 | } 873 | 874 | /** 875 | * Specifies the animation used to animate a View that exit the screen. 876 | * 877 | * @param context The application's environment. 878 | * @param resourceID The resource id of the animation. 879 | * @see #getOutAnimation() 880 | * @see #setOutAnimation(android.animation.ObjectAnimator) 881 | */ 882 | public void setOutAnimation(Context context, int resourceID) { 883 | setOutAnimation((ObjectAnimator) AnimatorInflater.loadAnimator(context, resourceID)); 884 | } 885 | 886 | /** 887 | * Indicates whether the current View should be animated the first time 888 | * the ViewAnimation is displayed. 889 | * 890 | * @param animate True to animate the current View the first time it is displayed, 891 | * false otherwise. 892 | */ 893 | public void setAnimateFirstView(boolean animate) { 894 | mAnimateFirstTime = animate; 895 | } 896 | 897 | @Override 898 | public int getBaseline() { 899 | return (getCurrentView() != null) ? getCurrentView().getBaseline() : super.getBaseline(); 900 | } 901 | 902 | @Override 903 | public Adapter getAdapter() { 904 | return mAdapter; 905 | } 906 | 907 | @Override 908 | public void setAdapter(Adapter adapter) { 909 | if (mAdapter != null && mDataSetObserver != null) { 910 | mAdapter.unregisterDataSetObserver(mDataSetObserver); 911 | } 912 | 913 | mAdapter = adapter; 914 | checkFocus(); 915 | 916 | if (mAdapter != null) { 917 | mDataSetObserver = new AdapterDataSetObserver(); 918 | mAdapter.registerDataSetObserver(mDataSetObserver); 919 | mItemCount = mAdapter.getCount(); 920 | } 921 | setFocusable(true); 922 | mWhichChild = 0; 923 | showOnly(mWhichChild, false); 924 | } 925 | 926 | 927 | @Override 928 | public void setSelection(int position) { 929 | setDisplayedChild(position); 930 | } 931 | 932 | @Override 933 | public View getSelectedView() { 934 | return getViewAtRelativeIndex(mActiveOffset); 935 | } 936 | 937 | /** 938 | * This defers a notifyDataSetChanged on the pending RemoteViewsAdapter if it has not 939 | * connected yet. 940 | */ 941 | public void deferNotifyDataSetChanged() { 942 | mDeferNotifyDataSetChanged = true; 943 | } 944 | 945 | 946 | /** 947 | * Called back when the adapter disconnects from the RemoteViewsService. 948 | */ 949 | public void onRemoteAdapterDisconnected() { 950 | // If the remote adapter disconnects, we keep it around 951 | // since the currently displayed items are still cached. 952 | // Further, we want the service to eventually reconnect 953 | // when necessary, as triggered by this view requesting 954 | // items from the Adapter. 955 | } 956 | 957 | /** 958 | * Called by an {@link android.appwidget.AppWidgetHost} in order to advance the current view when 959 | * it is being used within an app widget. 960 | */ 961 | public void advance() { 962 | showNext(); 963 | } 964 | 965 | /** 966 | * Called by an {@link android.appwidget.AppWidgetHost} to indicate that it will be 967 | * automatically advancing the views of this {@link AdapterViewAnimator} by calling 968 | * {@link AdapterViewAnimator#advance()} at some point in the future. This allows subclasses to 969 | * perform any required setup, for example, to stop automatically advancing their children. 970 | */ 971 | public void fyiWillBeAdvancedByHostKThx() { 972 | } 973 | } -------------------------------------------------------------------------------- /stackview/src/main/java/com/stackview/AdapterView.java: -------------------------------------------------------------------------------- 1 | package com.stackview; 2 | 3 | import android.content.Context; 4 | import android.database.DataSetObserver; 5 | import android.graphics.PointF; 6 | import android.graphics.Rect; 7 | import android.os.Parcelable; 8 | import android.os.SystemClock; 9 | import android.util.AttributeSet; 10 | import android.util.SparseArray; 11 | import android.view.ContextMenu; 12 | import android.view.ContextMenu.ContextMenuInfo; 13 | import android.view.SoundEffectConstants; 14 | import android.view.View; 15 | import android.view.ViewDebug; 16 | import android.view.ViewGroup; 17 | import android.view.accessibility.AccessibilityEvent; 18 | import android.view.accessibility.AccessibilityNodeInfo; 19 | import android.widget.Adapter; 20 | import android.widget.ListView; 21 | 22 | 23 | public abstract class AdapterView extends ViewGroup { 24 | 25 | /** 26 | * The item view type returned by {@link Adapter#getItemViewType(int)} when 27 | * the adapter does not want the item's view recycled. 28 | */ 29 | public static final int ITEM_VIEW_TYPE_IGNORE = -1; 30 | 31 | /** 32 | * The item view type returned by {@link Adapter#getItemViewType(int)} when 33 | * the item is a header or footer. 34 | */ 35 | public static final int ITEM_VIEW_TYPE_HEADER_OR_FOOTER = -2; 36 | 37 | /** 38 | * The position of the first child displayed 39 | */ 40 | @ViewDebug.ExportedProperty(category = "scrolling") 41 | int mFirstPosition = 0; 42 | 43 | /** 44 | * The offset in pixels from the top of the AdapterView to the top 45 | * of the view to select during the next layout. 46 | */ 47 | int mSpecificTop; 48 | 49 | /** 50 | * Position from which to start looking for mSyncRowId 51 | */ 52 | int mSyncPosition; 53 | 54 | /** 55 | * Row id to look for when data has changed 56 | */ 57 | long mSyncRowId = INVALID_ROW_ID; 58 | 59 | /** 60 | * Height of the view when mSyncPosition and mSyncRowId where set 61 | */ 62 | long mSyncHeight; 63 | 64 | /** 65 | * True if we need to sync to mSyncRowId 66 | */ 67 | boolean mNeedSync = false; 68 | 69 | /** 70 | * Indicates whether to sync based on the selection or position. Possible 71 | * values are {@link #SYNC_SELECTED_POSITION} or 72 | * {@link #SYNC_FIRST_POSITION}. 73 | */ 74 | int mSyncMode; 75 | 76 | /** 77 | * Our height after the last layout 78 | */ 79 | private int mLayoutHeight; 80 | 81 | /** 82 | * Sync based on the selected child 83 | */ 84 | static final int SYNC_SELECTED_POSITION = 0; 85 | 86 | /** 87 | * Sync based on the first child displayed 88 | */ 89 | static final int SYNC_FIRST_POSITION = 1; 90 | 91 | /** 92 | * Maximum amount of time to spend in {@link #findSyncPosition()} 93 | */ 94 | static final int SYNC_MAX_DURATION_MILLIS = 100; 95 | 96 | /** 97 | * Indicates that this view is currently being laid out. 98 | */ 99 | boolean mInLayout = false; 100 | 101 | /** 102 | * The listener that receives notifications when an item is selected. 103 | */ 104 | OnItemSelectedListener mOnItemSelectedListener; 105 | 106 | /** 107 | * The listener that receives notifications when an item is clicked. 108 | */ 109 | OnItemClickListener mOnItemClickListener; 110 | 111 | /** 112 | * The listener that receives notifications when an item is long clicked. 113 | */ 114 | OnItemLongClickListener mOnItemLongClickListener; 115 | 116 | /** 117 | * True if the data has changed since the last layout 118 | */ 119 | boolean mDataChanged; 120 | 121 | /** 122 | * The position within the adapter's data set of the item to select 123 | * during the next layout. 124 | */ 125 | @ViewDebug.ExportedProperty(category = "list") 126 | int mNextSelectedPosition = INVALID_POSITION; 127 | 128 | /** 129 | * The item id of the item to select during the next layout. 130 | */ 131 | long mNextSelectedRowId = INVALID_ROW_ID; 132 | 133 | /** 134 | * The position within the adapter's data set of the currently selected item. 135 | */ 136 | @ViewDebug.ExportedProperty(category = "list") 137 | int mSelectedPosition = INVALID_POSITION; 138 | 139 | /** 140 | * The item id of the currently selected item. 141 | */ 142 | long mSelectedRowId = INVALID_ROW_ID; 143 | 144 | /** 145 | * View to show if there are no items to show. 146 | */ 147 | private View mEmptyView; 148 | 149 | /** 150 | * The number of items in the current adapter. 151 | */ 152 | @ViewDebug.ExportedProperty(category = "list") 153 | int mItemCount; 154 | 155 | /** 156 | * The number of items in the adapter before a data changed event occurred. 157 | */ 158 | int mOldItemCount; 159 | 160 | /** 161 | * Represents an invalid position. All valid positions are in the range 0 to 1 less than the 162 | * number of items in the current adapter. 163 | */ 164 | public static final int INVALID_POSITION = -1; 165 | 166 | /** 167 | * Represents an empty or invalid row id 168 | */ 169 | public static final long INVALID_ROW_ID = Long.MIN_VALUE; 170 | 171 | /** 172 | * The last selected position we used when notifying 173 | */ 174 | int mOldSelectedPosition = INVALID_POSITION; 175 | 176 | /** 177 | * The id of the last selected position we used when notifying 178 | */ 179 | long mOldSelectedRowId = INVALID_ROW_ID; 180 | 181 | /** 182 | * Indicates what focusable state is requested when calling setFocusable(). 183 | * In addition to this, this view has other criteria for actually 184 | * determining the focusable state (such as whether its empty or the text 185 | * filter is shown). 186 | * 187 | * @see #setFocusable(boolean) 188 | * @see #checkFocus() 189 | */ 190 | private boolean mDesiredFocusableState; 191 | private boolean mDesiredFocusableInTouchModeState; 192 | 193 | private SelectionNotifier mSelectionNotifier; 194 | /** 195 | * When set to true, calls to requestLayout() will not propagate up the parent hierarchy. 196 | * This is used to layout the children during a layout pass. 197 | */ 198 | boolean mBlockLayoutRequests = false; 199 | 200 | public AdapterView(Context context) { 201 | super(context); 202 | } 203 | 204 | public AdapterView(Context context, AttributeSet attrs) { 205 | super(context, attrs); 206 | } 207 | 208 | public AdapterView(Context context, AttributeSet attrs, int defStyle) { 209 | super(context, attrs, defStyle); 210 | } 211 | 212 | /** 213 | * Interface definition for a callback to be invoked when an item in this 214 | * AdapterView has been clicked. 215 | */ 216 | public interface OnItemClickListener { 217 | 218 | /** 219 | * Callback method to be invoked when an item in this AdapterView has 220 | * been clicked. 221 | *

222 | * Implementers can call getItemAtPosition(position) if they need 223 | * to access the data associated with the selected item. 224 | * 225 | * @param parent The AdapterView where the click happened. 226 | * @param view The view within the AdapterView that was clicked (this 227 | * will be a view provided by the adapter) 228 | * @param position The position of the view in the adapter. 229 | * @param id The row id of the item that was clicked. 230 | */ 231 | void onItemClick(AdapterView parent, View view, int position, long id); 232 | } 233 | 234 | /** 235 | * Register a callback to be invoked when an item in this AdapterView has 236 | * been clicked. 237 | * 238 | * @param listener The callback that will be invoked. 239 | */ 240 | public void setOnItemClickListener(OnItemClickListener listener) { 241 | mOnItemClickListener = listener; 242 | } 243 | 244 | /** 245 | * @return The callback to be invoked with an item in this AdapterView has 246 | * been clicked, or null id no callback has been set. 247 | */ 248 | public final OnItemClickListener getOnItemClickListener() { 249 | return mOnItemClickListener; 250 | } 251 | 252 | /** 253 | * Call the OnItemClickListener, if it is defined. 254 | * 255 | * @param view The view within the AdapterView that was clicked. 256 | * @param position The position of the view in the adapter. 257 | * @param id The row id of the item that was clicked. 258 | * @return True if there was an assigned OnItemClickListener that was 259 | * called, false otherwise is returned. 260 | */ 261 | public boolean performItemClick(View view, int position, long id) { 262 | if (mOnItemClickListener != null) { 263 | playSoundEffect(SoundEffectConstants.CLICK); 264 | if (view != null) { 265 | view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); 266 | } 267 | mOnItemClickListener.onItemClick(this, view, position, id); 268 | return true; 269 | } 270 | 271 | return false; 272 | } 273 | 274 | /** 275 | * Interface definition for a callback to be invoked when an item in this 276 | * view has been clicked and held. 277 | */ 278 | public interface OnItemLongClickListener { 279 | /** 280 | * Callback method to be invoked when an item in this view has been 281 | * clicked and held. 282 | *

283 | * Implementers can call getItemAtPosition(position) if they need to access 284 | * the data associated with the selected item. 285 | * 286 | * @param parent The AbsListView where the click happened 287 | * @param view The view within the AbsListView that was clicked 288 | * @param position The position of the view in the list 289 | * @param id The row id of the item that was clicked 290 | * @return true if the callback consumed the long click, false otherwise 291 | */ 292 | boolean onItemLongClick(AdapterView parent, View view, int position, long id); 293 | } 294 | 295 | 296 | /** 297 | * Register a callback to be invoked when an item in this AdapterView has 298 | * been clicked and held 299 | * 300 | * @param listener The callback that will run 301 | */ 302 | public void setOnItemLongClickListener(OnItemLongClickListener listener) { 303 | if (!isLongClickable()) { 304 | setLongClickable(true); 305 | } 306 | mOnItemLongClickListener = listener; 307 | } 308 | 309 | /** 310 | * @return The callback to be invoked with an item in this AdapterView has 311 | * been clicked and held, or null id no callback as been set. 312 | */ 313 | public final OnItemLongClickListener getOnItemLongClickListener() { 314 | return mOnItemLongClickListener; 315 | } 316 | 317 | /** 318 | * Interface definition for a callback to be invoked when 319 | * an item in this view has been selected. 320 | */ 321 | public interface OnItemSelectedListener { 322 | /** 323 | *

Callback method to be invoked when an item in this view has been 324 | * selected. This callback is invoked only when the newly selected 325 | * position is different from the previously selected position or if 326 | * there was no selected item.

327 | *

328 | * Impelmenters can call getItemAtPosition(position) if they need to access the 329 | * data associated with the selected item. 330 | * 331 | * @param parent The AdapterView where the selection happened 332 | * @param view The view within the AdapterView that was clicked 333 | * @param position The position of the view in the adapter 334 | * @param id The row id of the item that is selected 335 | */ 336 | void onItemSelected(AdapterView parent, View view, int position, long id); 337 | 338 | /** 339 | * Callback method to be invoked when the selection disappears from this 340 | * view. The selection can disappear for instance when touch is activated 341 | * or when the adapter becomes empty. 342 | * 343 | * @param parent The AdapterView that now contains no selected item. 344 | */ 345 | void onNothingSelected(AdapterView parent); 346 | } 347 | 348 | 349 | /** 350 | * Register a callback to be invoked when an item in this AdapterView has 351 | * been selected. 352 | * 353 | * @param listener The callback that will run 354 | */ 355 | public void setOnItemSelectedListener(OnItemSelectedListener listener) { 356 | mOnItemSelectedListener = listener; 357 | } 358 | 359 | public final OnItemSelectedListener getOnItemSelectedListener() { 360 | return mOnItemSelectedListener; 361 | } 362 | 363 | /** 364 | * Extra menu information provided to the 365 | * {@link android.view.View.OnCreateContextMenuListener#onCreateContextMenu(ContextMenu, View, ContextMenuInfo) } 366 | * callback when a context menu is brought up for this AdapterView. 367 | */ 368 | public static class AdapterContextMenuInfo implements ContextMenu.ContextMenuInfo { 369 | 370 | public AdapterContextMenuInfo(View targetView, int position, long id) { 371 | this.targetView = targetView; 372 | this.position = position; 373 | this.id = id; 374 | } 375 | 376 | /** 377 | * The child view for which the context menu is being displayed. This 378 | * will be one of the children of this AdapterView. 379 | */ 380 | public View targetView; 381 | 382 | /** 383 | * The position in the adapter for which the context menu is being 384 | * displayed. 385 | */ 386 | public int position; 387 | 388 | /** 389 | * The row id of the item for which the context menu is being displayed. 390 | */ 391 | public long id; 392 | } 393 | 394 | /** 395 | * Returns the adapter currently associated with this widget. 396 | * 397 | * @return The adapter used to provide this view's content. 398 | */ 399 | public abstract T getAdapter(); 400 | 401 | /** 402 | * Sets the adapter that provides the data and the views to represent the data 403 | * in this widget. 404 | * 405 | * @param adapter The adapter to use to create this view's content. 406 | */ 407 | public abstract void setAdapter(T adapter); 408 | 409 | /** 410 | * This method is not supported and throws an UnsupportedOperationException when called. 411 | * 412 | * @param child Ignored. 413 | * @throws UnsupportedOperationException Every time this method is invoked. 414 | */ 415 | @Override 416 | public void addView(View child) { 417 | throw new UnsupportedOperationException("addView(View) is not supported in AdapterView"); 418 | } 419 | 420 | /** 421 | * This method is not supported and throws an UnsupportedOperationException when called. 422 | * 423 | * @param child Ignored. 424 | * @param index Ignored. 425 | * @throws UnsupportedOperationException Every time this method is invoked. 426 | */ 427 | @Override 428 | public void addView(View child, int index) { 429 | throw new UnsupportedOperationException("addView(View, int) is not supported in AdapterView"); 430 | } 431 | 432 | /** 433 | * This method is not supported and throws an UnsupportedOperationException when called. 434 | * 435 | * @param child Ignored. 436 | * @param params Ignored. 437 | * @throws UnsupportedOperationException Every time this method is invoked. 438 | */ 439 | @Override 440 | public void addView(View child, LayoutParams params) { 441 | throw new UnsupportedOperationException("addView(View, LayoutParams) " 442 | + "is not supported in AdapterView"); 443 | } 444 | 445 | /** 446 | * This method is not supported and throws an UnsupportedOperationException when called. 447 | * 448 | * @param child Ignored. 449 | * @param index Ignored. 450 | * @param params Ignored. 451 | * @throws UnsupportedOperationException Every time this method is invoked. 452 | */ 453 | @Override 454 | public void addView(View child, int index, LayoutParams params) { 455 | throw new UnsupportedOperationException("addView(View, int, LayoutParams) " 456 | + "is not supported in AdapterView"); 457 | } 458 | 459 | /** 460 | * This method is not supported and throws an UnsupportedOperationException when called. 461 | * 462 | * @param child Ignored. 463 | * @throws UnsupportedOperationException Every time this method is invoked. 464 | */ 465 | @Override 466 | public void removeView(View child) { 467 | throw new UnsupportedOperationException("removeView(View) is not supported in AdapterView"); 468 | } 469 | 470 | /** 471 | * This method is not supported and throws an UnsupportedOperationException when called. 472 | * 473 | * @param index Ignored. 474 | * @throws UnsupportedOperationException Every time this method is invoked. 475 | */ 476 | @Override 477 | public void removeViewAt(int index) { 478 | throw new UnsupportedOperationException("removeViewAt(int) is not supported in AdapterView"); 479 | } 480 | 481 | /** 482 | * This method is not supported and throws an UnsupportedOperationException when called. 483 | * 484 | * @throws UnsupportedOperationException Every time this method is invoked. 485 | */ 486 | @Override 487 | public void removeAllViews() { 488 | throw new UnsupportedOperationException("removeAllViews() is not supported in AdapterView"); 489 | } 490 | 491 | @Override 492 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 493 | mLayoutHeight = getHeight(); 494 | } 495 | 496 | /** 497 | * Return the position of the currently selected item within the adapter's data set 498 | * 499 | * @return int Position (starting at 0), or {@link #INVALID_POSITION} if there is nothing selected. 500 | */ 501 | @ViewDebug.CapturedViewProperty 502 | public int getSelectedItemPosition() { 503 | return mNextSelectedPosition; 504 | } 505 | 506 | /** 507 | * @return The id corresponding to the currently selected item, or {@link #INVALID_ROW_ID} 508 | * if nothing is selected. 509 | */ 510 | @ViewDebug.CapturedViewProperty 511 | public long getSelectedItemId() { 512 | return mNextSelectedRowId; 513 | } 514 | 515 | /** 516 | * @return The view corresponding to the currently selected item, or null 517 | * if nothing is selected 518 | */ 519 | public abstract View getSelectedView(); 520 | 521 | /** 522 | * @return The data corresponding to the currently selected item, or 523 | * null if there is nothing selected. 524 | */ 525 | public Object getSelectedItem() { 526 | T adapter = getAdapter(); 527 | int selection = getSelectedItemPosition(); 528 | if (adapter != null && adapter.getCount() > 0 && selection >= 0) { 529 | return adapter.getItem(selection); 530 | } else { 531 | return null; 532 | } 533 | } 534 | 535 | /** 536 | * @return The number of items owned by the Adapter associated with this 537 | * AdapterView. (This is the number of data items, which may be 538 | * larger than the number of visible views.) 539 | */ 540 | @ViewDebug.CapturedViewProperty 541 | public int getCount() { 542 | return mItemCount; 543 | } 544 | 545 | /** 546 | * Get the position within the adapter's data set for the view, where view is a an adapter item 547 | * or a descendant of an adapter item. 548 | * 549 | * @param view an adapter item, or a descendant of an adapter item. This must be visible in this 550 | * AdapterView at the time of the call. 551 | * @return the position within the adapter's data set of the view, or {@link #INVALID_POSITION} 552 | * if the view does not correspond to a list item (or it is not currently visible). 553 | */ 554 | public int getPositionForView(View view) { 555 | View listItem = view; 556 | try { 557 | View v; 558 | while (!(v = (View) listItem.getParent()).equals(this)) { 559 | listItem = v; 560 | } 561 | } catch (ClassCastException e) { 562 | // We made it up to the window without find this list view 563 | return INVALID_POSITION; 564 | } 565 | 566 | // Search the children for the list item 567 | final int childCount = getChildCount(); 568 | for (int i = 0; i < childCount; i++) { 569 | if (getChildAt(i).equals(listItem)) { 570 | return mFirstPosition + i; 571 | } 572 | } 573 | 574 | // Child not found! 575 | return INVALID_POSITION; 576 | } 577 | 578 | /** 579 | * Returns the position within the adapter's data set for the first item 580 | * displayed on screen. 581 | * 582 | * @return The position within the adapter's data set 583 | */ 584 | public int getFirstVisiblePosition() { 585 | return mFirstPosition; 586 | } 587 | 588 | /** 589 | * Returns the position within the adapter's data set for the last item 590 | * displayed on screen. 591 | * 592 | * @return The position within the adapter's data set 593 | */ 594 | public int getLastVisiblePosition() { 595 | return mFirstPosition + getChildCount() - 1; 596 | } 597 | 598 | /** 599 | * Sets the currently selected item. To support accessibility subclasses that 600 | * override this method must invoke the overriden super method first. 601 | * 602 | * @param position Index (starting at 0) of the data item to be selected. 603 | */ 604 | public abstract void setSelection(int position); 605 | 606 | /** 607 | * Sets the view to show if the adapter is empty 608 | */ 609 | 610 | public void setEmptyView(View emptyView) { 611 | mEmptyView = emptyView; 612 | 613 | final T adapter = getAdapter(); 614 | final boolean empty = ((adapter == null) || adapter.isEmpty()); 615 | updateEmptyStatus(empty); 616 | } 617 | 618 | /** 619 | * When the current adapter is empty, the AdapterView can display a special view 620 | * call the empty view. The empty view is used to provide feedback to the user 621 | * that no data is available in this AdapterView. 622 | * 623 | * @return The view to show if the adapter is empty. 624 | */ 625 | public View getEmptyView() { 626 | return mEmptyView; 627 | } 628 | 629 | /** 630 | * Indicates whether this view is in filter mode. Filter mode can for instance 631 | * be enabled by a user when typing on the keyboard. 632 | * 633 | * @return True if the view is in filter mode, false otherwise. 634 | */ 635 | boolean isInFilterMode() { 636 | return false; 637 | } 638 | 639 | @Override 640 | public void setFocusable(boolean focusable) { 641 | final T adapter = getAdapter(); 642 | final boolean empty = adapter == null || adapter.getCount() == 0; 643 | 644 | mDesiredFocusableState = focusable; 645 | if (!focusable) { 646 | mDesiredFocusableInTouchModeState = false; 647 | } 648 | 649 | super.setFocusable(focusable && (!empty || isInFilterMode())); 650 | } 651 | 652 | @Override 653 | public void setFocusableInTouchMode(boolean focusable) { 654 | final T adapter = getAdapter(); 655 | final boolean empty = adapter == null || adapter.getCount() == 0; 656 | 657 | mDesiredFocusableInTouchModeState = focusable; 658 | if (focusable) { 659 | mDesiredFocusableState = true; 660 | } 661 | 662 | super.setFocusableInTouchMode(focusable && (!empty || isInFilterMode())); 663 | } 664 | 665 | void checkFocus() { 666 | final T adapter = getAdapter(); 667 | final boolean empty = adapter == null || adapter.getCount() == 0; 668 | final boolean focusable = !empty || isInFilterMode(); 669 | // The order in which we set focusable in touch mode/focusable may matter 670 | // for the client, see View.setFocusableInTouchMode() comments for more 671 | // details 672 | super.setFocusableInTouchMode(focusable && mDesiredFocusableInTouchModeState); 673 | super.setFocusable(focusable && mDesiredFocusableState); 674 | if (mEmptyView != null) { 675 | updateEmptyStatus((adapter == null) || adapter.isEmpty()); 676 | } 677 | } 678 | 679 | /** 680 | * Update the status of the list based on the empty parameter. If empty is true and 681 | * we have an empty view, display it. In all the other cases, make sure that the listview 682 | * is VISIBLE and that the empty view is GONE (if it's not null). 683 | */ 684 | private void updateEmptyStatus(boolean empty) { 685 | if (isInFilterMode()) { 686 | empty = false; 687 | } 688 | 689 | if (empty) { 690 | if (mEmptyView != null) { 691 | mEmptyView.setVisibility(View.VISIBLE); 692 | setVisibility(View.GONE); 693 | } else { 694 | // If the caller just removed our empty view, make sure the list view is visible 695 | setVisibility(View.VISIBLE); 696 | } 697 | 698 | // We are now GONE, so pending layouts will not be dispatched. 699 | // Force one here to make sure that the state of the list matches 700 | // the state of the adapter. 701 | if (mDataChanged) { 702 | // onLayout(false, getLeft(), getTop(), getRight(), getBottom()); 703 | mLayoutHeight = getHeight(); 704 | } 705 | } else { 706 | if (mEmptyView != null) mEmptyView.setVisibility(View.GONE); 707 | setVisibility(View.VISIBLE); 708 | } 709 | } 710 | 711 | /** 712 | * Gets the data associated with the specified position in the list. 713 | * 714 | * @param position Which data to get 715 | * @return The data associated with the specified position in the list 716 | */ 717 | public Object getItemAtPosition(int position) { 718 | T adapter = getAdapter(); 719 | return (adapter == null || position < 0) ? null : adapter.getItem(position); 720 | } 721 | 722 | public long getItemIdAtPosition(int position) { 723 | T adapter = getAdapter(); 724 | return (adapter == null || position < 0) ? INVALID_ROW_ID : adapter.getItemId(position); 725 | } 726 | 727 | @Override 728 | public void setOnClickListener(OnClickListener l) { 729 | throw new RuntimeException("Don't call setOnClickListener for an AdapterView. " 730 | + "You probably want setOnItemClickListener instead"); 731 | } 732 | 733 | /** 734 | * Override to prevent freezing of any views created by the adapter. 735 | */ 736 | @Override 737 | protected void dispatchSaveInstanceState(SparseArray container) { 738 | dispatchFreezeSelfOnly(container); 739 | } 740 | 741 | /** 742 | * Override to prevent thawing of any views created by the adapter. 743 | */ 744 | @Override 745 | protected void dispatchRestoreInstanceState(SparseArray container) { 746 | dispatchThawSelfOnly(container); 747 | } 748 | 749 | class AdapterDataSetObserver extends DataSetObserver { 750 | 751 | private Parcelable mInstanceState = null; 752 | 753 | @Override 754 | public void onChanged() { 755 | mDataChanged = true; 756 | mOldItemCount = mItemCount; 757 | mItemCount = getAdapter().getCount(); 758 | 759 | // Detect the case where a cursor that was previously invalidated has 760 | // been repopulated with new data. 761 | if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null 762 | && mOldItemCount == 0 && mItemCount > 0) { 763 | AdapterView.this.onRestoreInstanceState(mInstanceState); 764 | mInstanceState = null; 765 | } else { 766 | rememberSyncState(); 767 | } 768 | checkFocus(); 769 | requestLayout(); 770 | } 771 | 772 | @Override 773 | public void onInvalidated() { 774 | mDataChanged = true; 775 | 776 | if (AdapterView.this.getAdapter().hasStableIds()) { 777 | // Remember the current state for the case where our hosting activity is being 778 | // stopped and later restarted 779 | mInstanceState = AdapterView.this.onSaveInstanceState(); 780 | } 781 | 782 | // Data is invalid so we should reset our state 783 | mOldItemCount = mItemCount; 784 | mItemCount = 0; 785 | mSelectedPosition = INVALID_POSITION; 786 | mSelectedRowId = INVALID_ROW_ID; 787 | mNextSelectedPosition = INVALID_POSITION; 788 | mNextSelectedRowId = INVALID_ROW_ID; 789 | mNeedSync = false; 790 | 791 | checkFocus(); 792 | requestLayout(); 793 | } 794 | 795 | public void clearSavedState() { 796 | mInstanceState = null; 797 | } 798 | } 799 | 800 | @Override 801 | protected void onDetachedFromWindow() { 802 | super.onDetachedFromWindow(); 803 | removeCallbacks(mSelectionNotifier); 804 | } 805 | 806 | private class SelectionNotifier implements Runnable { 807 | public void run() { 808 | if (mDataChanged) { 809 | // Data has changed between when this SelectionNotifier 810 | // was posted and now. We need to wait until the AdapterView 811 | // has been synched to the new data. 812 | if (getAdapter() != null) { 813 | post(this); 814 | } 815 | } else { 816 | fireOnSelected(); 817 | } 818 | } 819 | } 820 | 821 | void selectionChanged() { 822 | if (mOnItemSelectedListener != null) { 823 | if (mInLayout || mBlockLayoutRequests) { 824 | // If we are in a layout traversal, defer notification 825 | // by posting. This ensures that the view tree is 826 | // in a consistent state and is able to accomodate 827 | // new layout or invalidate requests. 828 | if (mSelectionNotifier == null) { 829 | mSelectionNotifier = new SelectionNotifier(); 830 | } 831 | post(mSelectionNotifier); 832 | } else { 833 | fireOnSelected(); 834 | } 835 | } 836 | 837 | // we fire selection events here not in View 838 | if (mSelectedPosition != ListView.INVALID_POSITION && isShown() && !isInTouchMode()) { 839 | sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); 840 | } 841 | } 842 | 843 | private void fireOnSelected() { 844 | if (mOnItemSelectedListener == null) 845 | return; 846 | 847 | int selection = this.getSelectedItemPosition(); 848 | if (selection >= 0) { 849 | View v = getSelectedView(); 850 | mOnItemSelectedListener.onItemSelected(this, v, selection, 851 | getAdapter().getItemId(selection)); 852 | } else { 853 | mOnItemSelectedListener.onNothingSelected(this); 854 | } 855 | } 856 | 857 | @Override 858 | public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 859 | View selectedView = getSelectedView(); 860 | if (selectedView != null && selectedView.getVisibility() == VISIBLE 861 | && selectedView.dispatchPopulateAccessibilityEvent(event)) { 862 | return true; 863 | } 864 | return false; 865 | } 866 | 867 | @Override 868 | public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) { 869 | if (super.onRequestSendAccessibilityEvent(child, event)) { 870 | // Add a record for ourselves as well. 871 | AccessibilityEvent record = AccessibilityEvent.obtain(); 872 | onInitializeAccessibilityEvent(record); 873 | // Populate with the text of the requesting child. 874 | child.dispatchPopulateAccessibilityEvent(record); 875 | event.appendRecord(record); 876 | return true; 877 | } 878 | return false; 879 | } 880 | 881 | @Override 882 | public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 883 | super.onInitializeAccessibilityNodeInfo(info); 884 | info.setScrollable(isScrollableForAccessibility()); 885 | View selectedView = getSelectedView(); 886 | if (selectedView != null) { 887 | info.setEnabled(selectedView.isEnabled()); 888 | } 889 | } 890 | 891 | @Override 892 | public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 893 | super.onInitializeAccessibilityEvent(event); 894 | event.setScrollable(isScrollableForAccessibility()); 895 | View selectedView = getSelectedView(); 896 | if (selectedView != null) { 897 | event.setEnabled(selectedView.isEnabled()); 898 | } 899 | event.setCurrentItemIndex(getSelectedItemPosition()); 900 | event.setFromIndex(getFirstVisiblePosition()); 901 | event.setToIndex(getLastVisiblePosition()); 902 | event.setItemCount(getCount()); 903 | } 904 | 905 | private boolean isScrollableForAccessibility() { 906 | T adapter = getAdapter(); 907 | if (adapter != null) { 908 | final int itemCount = adapter.getCount(); 909 | return itemCount > 0 910 | && (getFirstVisiblePosition() > 0 || getLastVisiblePosition() < itemCount - 1); 911 | } 912 | return false; 913 | } 914 | 915 | @Override 916 | protected boolean canAnimate() { 917 | return super.canAnimate() && mItemCount > 0; 918 | } 919 | 920 | void handleDataChanged() { 921 | final int count = mItemCount; 922 | boolean found = false; 923 | 924 | if (count > 0) { 925 | 926 | int newPos; 927 | 928 | // Find the row we are supposed to sync to 929 | if (mNeedSync) { 930 | // Update this first, since setNextSelectedPositionInt inspects 931 | // it 932 | mNeedSync = false; 933 | 934 | // See if we can find a position in the new data with the same 935 | // id as the old selection 936 | newPos = findSyncPosition(); 937 | if (newPos >= 0) { 938 | // Verify that new selection is selectable 939 | int selectablePos = lookForSelectablePosition(newPos, true); 940 | if (selectablePos == newPos) { 941 | // Same row id is selected 942 | setNextSelectedPositionInt(newPos); 943 | found = true; 944 | } 945 | } 946 | } 947 | if (!found) { 948 | // Try to use the same position if we can't find matching data 949 | newPos = getSelectedItemPosition(); 950 | 951 | // Pin position to the available range 952 | if (newPos >= count) { 953 | newPos = count - 1; 954 | } 955 | if (newPos < 0) { 956 | newPos = 0; 957 | } 958 | 959 | // Make sure we select something selectable -- first look down 960 | int selectablePos = lookForSelectablePosition(newPos, true); 961 | if (selectablePos < 0) { 962 | // Looking down didn't work -- try looking up 963 | selectablePos = lookForSelectablePosition(newPos, false); 964 | } 965 | if (selectablePos >= 0) { 966 | setNextSelectedPositionInt(selectablePos); 967 | checkSelectionChanged(); 968 | found = true; 969 | } 970 | } 971 | } 972 | if (!found) { 973 | // Nothing is selected 974 | mSelectedPosition = INVALID_POSITION; 975 | mSelectedRowId = INVALID_ROW_ID; 976 | mNextSelectedPosition = INVALID_POSITION; 977 | mNextSelectedRowId = INVALID_ROW_ID; 978 | mNeedSync = false; 979 | checkSelectionChanged(); 980 | } 981 | } 982 | 983 | void checkSelectionChanged() { 984 | if ((mSelectedPosition != mOldSelectedPosition) || (mSelectedRowId != mOldSelectedRowId)) { 985 | selectionChanged(); 986 | mOldSelectedPosition = mSelectedPosition; 987 | mOldSelectedRowId = mSelectedRowId; 988 | } 989 | } 990 | 991 | /** 992 | * Searches the adapter for a position matching mSyncRowId. The search starts at mSyncPosition 993 | * and then alternates between moving up and moving down until 1) we find the right position, or 994 | * 2) we run out of time, or 3) we have looked at every position 995 | * 996 | * @return Position of the row that matches mSyncRowId, or {@link #INVALID_POSITION} if it can't 997 | * be found 998 | */ 999 | int findSyncPosition() { 1000 | int count = mItemCount; 1001 | 1002 | if (count == 0) { 1003 | return INVALID_POSITION; 1004 | } 1005 | 1006 | long idToMatch = mSyncRowId; 1007 | int seed = mSyncPosition; 1008 | 1009 | // If there isn't a selection don't hunt for it 1010 | if (idToMatch == INVALID_ROW_ID) { 1011 | return INVALID_POSITION; 1012 | } 1013 | 1014 | // Pin seed to reasonable values 1015 | seed = Math.max(0, seed); 1016 | seed = Math.min(count - 1, seed); 1017 | 1018 | long endTime = SystemClock.uptimeMillis() + SYNC_MAX_DURATION_MILLIS; 1019 | 1020 | long rowId; 1021 | 1022 | // first position scanned so far 1023 | int first = seed; 1024 | 1025 | // last position scanned so far 1026 | int last = seed; 1027 | 1028 | // True if we should move down on the next iteration 1029 | boolean next = false; 1030 | 1031 | // True when we have looked at the first item in the data 1032 | boolean hitFirst; 1033 | 1034 | // True when we have looked at the last item in the data 1035 | boolean hitLast; 1036 | 1037 | // Get the item ID locally (instead of getItemIdAtPosition), so 1038 | // we need the adapter 1039 | T adapter = getAdapter(); 1040 | if (adapter == null) { 1041 | return INVALID_POSITION; 1042 | } 1043 | 1044 | while (SystemClock.uptimeMillis() <= endTime) { 1045 | rowId = adapter.getItemId(seed); 1046 | if (rowId == idToMatch) { 1047 | // Found it! 1048 | return seed; 1049 | } 1050 | 1051 | hitLast = last == count - 1; 1052 | hitFirst = first == 0; 1053 | 1054 | if (hitLast && hitFirst) { 1055 | // Looked at everything 1056 | break; 1057 | } 1058 | 1059 | if (hitFirst || (next && !hitLast)) { 1060 | // Either we hit the top, or we are trying to move down 1061 | last++; 1062 | seed = last; 1063 | // Try going up next time 1064 | next = false; 1065 | } else if (hitLast || (!next && !hitFirst)) { 1066 | // Either we hit the bottom, or we are trying to move up 1067 | first--; 1068 | seed = first; 1069 | // Try going down next time 1070 | next = true; 1071 | } 1072 | 1073 | } 1074 | 1075 | return INVALID_POSITION; 1076 | } 1077 | 1078 | /** 1079 | * Find a position that can be selected (i.e., is not a separator). 1080 | * 1081 | * @param position The starting position to look at. 1082 | * @param lookDown Whether to look down for other positions. 1083 | * @return The next selectable position starting at position and then searching either up or 1084 | * down. Returns {@link #INVALID_POSITION} if nothing can be found. 1085 | */ 1086 | int lookForSelectablePosition(int position, boolean lookDown) { 1087 | return position; 1088 | } 1089 | 1090 | /** 1091 | * Utility to keep mSelectedPosition and mSelectedRowId in sync 1092 | * 1093 | * @param position Our current position 1094 | */ 1095 | void setSelectedPositionInt(int position) { 1096 | mSelectedPosition = position; 1097 | mSelectedRowId = getItemIdAtPosition(position); 1098 | } 1099 | 1100 | /** 1101 | * Utility to keep mNextSelectedPosition and mNextSelectedRowId in sync 1102 | * 1103 | * @param position Intended value for mSelectedPosition the next time we go 1104 | * through layout 1105 | */ 1106 | void setNextSelectedPositionInt(int position) { 1107 | mNextSelectedPosition = position; 1108 | mNextSelectedRowId = getItemIdAtPosition(position); 1109 | // If we are trying to sync to the selection, update that too 1110 | if (mNeedSync && mSyncMode == SYNC_SELECTED_POSITION && position >= 0) { 1111 | mSyncPosition = position; 1112 | mSyncRowId = mNextSelectedRowId; 1113 | } 1114 | } 1115 | 1116 | /** 1117 | * Remember enough information to restore the screen state when the data has 1118 | * changed. 1119 | */ 1120 | void rememberSyncState() { 1121 | if (getChildCount() > 0) { 1122 | mNeedSync = true; 1123 | mSyncHeight = mLayoutHeight; 1124 | if (mSelectedPosition >= 0) { 1125 | // Sync the selection state 1126 | View v = getChildAt(mSelectedPosition - mFirstPosition); 1127 | mSyncRowId = mNextSelectedRowId; 1128 | mSyncPosition = mNextSelectedPosition; 1129 | if (v != null) { 1130 | mSpecificTop = v.getTop(); 1131 | } 1132 | mSyncMode = SYNC_SELECTED_POSITION; 1133 | } else { 1134 | // Sync the based on the offset of the first view 1135 | View v = getChildAt(0); 1136 | T adapter = getAdapter(); 1137 | if (mFirstPosition >= 0 && mFirstPosition < adapter.getCount()) { 1138 | mSyncRowId = adapter.getItemId(mFirstPosition); 1139 | } else { 1140 | mSyncRowId = NO_ID; 1141 | } 1142 | mSyncPosition = mFirstPosition; 1143 | if (v != null) { 1144 | mSpecificTop = v.getTop(); 1145 | } 1146 | mSyncMode = SYNC_FIRST_POSITION; 1147 | } 1148 | } 1149 | } 1150 | 1151 | protected boolean isTransformedTouchPointInView(float x, float y, View child, PointF outLocalPoint) { 1152 | //Log.d(TAG, "isTranformedTouchPointInView()"); 1153 | final Rect frame = new Rect(); 1154 | child.getHitRect(frame); 1155 | if (frame.contains((int) x, (int) y)) { 1156 | return true; 1157 | } 1158 | return false; 1159 | } 1160 | } -------------------------------------------------------------------------------- /stackview/src/main/java/com/stackview/StackViewVertical.java: -------------------------------------------------------------------------------- 1 | package com.stackview; 2 | 3 | /** 4 | * Created by binary on 5/19/15. 5 | */ 6 | 7 | import android.animation.ObjectAnimator; 8 | import android.animation.PropertyValuesHolder; 9 | import android.content.Context; 10 | import android.content.res.TypedArray; 11 | import android.graphics.Bitmap; 12 | import android.graphics.BlurMaskFilter; 13 | import android.graphics.Canvas; 14 | import android.graphics.Matrix; 15 | import android.graphics.Paint; 16 | import android.graphics.PorterDuff; 17 | import android.graphics.PorterDuffXfermode; 18 | import android.graphics.Rect; 19 | import android.graphics.RectF; 20 | import android.graphics.Region; 21 | import android.util.AttributeSet; 22 | import android.util.DisplayMetrics; 23 | import android.util.Log; 24 | import android.view.InputDevice; 25 | import android.view.MotionEvent; 26 | import android.view.VelocityTracker; 27 | import android.view.View; 28 | import android.view.ViewConfiguration; 29 | import android.view.ViewGroup; 30 | import android.view.WindowManager; 31 | import android.view.animation.LinearInterpolator; 32 | import android.widget.FrameLayout; 33 | import android.widget.ImageView; 34 | 35 | import java.lang.ref.WeakReference; 36 | 37 | 38 | /** 39 | * A view that displays its children in a stack and allows users to discretely swipe 40 | * through the children. 41 | */ 42 | public class StackViewVertical extends AdapterViewAnimator { 43 | private final String TAG = "StackView"; 44 | 45 | /** 46 | * Default animation parameters 47 | */ 48 | private static final int DEFAULT_ANIMATION_DURATION = 400; 49 | private static final int MINIMUM_ANIMATION_DURATION = 50; 50 | private static final int STACK_RELAYOUT_DURATION = 100; 51 | 52 | /** 53 | * Parameters effecting the perspective visuals 54 | */ 55 | private static final float PERSPECTIVE_SHIFT_FACTOR_Y = 0.04f; 56 | private static final float PERSPECTIVE_SHIFT_FACTOR_X = 0.00f; 57 | 58 | private float mPerspectiveShiftX; 59 | private float mPerspectiveShiftY; 60 | private float mNewPerspectiveShiftX; 61 | private float mNewPerspectiveShiftY; 62 | 63 | @SuppressWarnings({"FieldCanBeLocal"}) 64 | private static final float PERSPECTIVE_SCALE_FACTOR = 0.03f; 65 | 66 | /** 67 | * Represent the two possible stack modes, one where items slide up, and the other 68 | * where items slide down. The perspective is also inverted between these two modes. 69 | */ 70 | private static final int ITEMS_SLIDE_UP = 0; 71 | private static final int ITEMS_SLIDE_DOWN = 1; 72 | 73 | /** 74 | * These specify the different gesture states 75 | */ 76 | private static final int GESTURE_NONE = 0; 77 | private static final int GESTURE_SLIDE_UP = 1; 78 | private static final int GESTURE_SLIDE_DOWN = 2; 79 | 80 | 81 | /** 82 | * Specifies how far you need to swipe (up or down) before it 83 | * will be consider a completed gesture when you lift your finger 84 | */ 85 | private static final float SWIPE_THRESHOLD_RATIO = 0.2f; 86 | 87 | /** 88 | * Specifies the total distance, relative to the size of the stack, 89 | * that views will be slid, either up or down 90 | */ 91 | private static final float SLIDE_UP_RATIO = 0.7f; 92 | 93 | /** 94 | * Sentinel value for no current active pointer. 95 | * Used by {@link #mActivePointerId}. 96 | */ 97 | private static final int INVALID_POINTER = -1; 98 | 99 | /** 100 | * Number of active views in the stack. One fewer view is actually visible, as one is hidden. 101 | */ 102 | private static final int NUM_ACTIVE_VIEWS = 5; 103 | 104 | private static final int FRAME_PADDING = 4; 105 | 106 | private final Rect mTouchRect = new Rect(); 107 | 108 | private static final int MIN_TIME_BETWEEN_INTERACTION_AND_AUTOADVANCE = 5000; 109 | 110 | private static final long MIN_TIME_BETWEEN_SCROLLS = 100; 111 | 112 | /** 113 | * These variables are all related to the current state of touch interaction 114 | * with the stack 115 | */ 116 | private float mInitialY; 117 | private float mInitialX; 118 | private int mActivePointerId; 119 | private int mYVelocity = 0; 120 | private int mSwipeGestureType = GESTURE_NONE; 121 | private int mSlideAmount; 122 | private int mSwipeThreshold; 123 | private int mTouchSlop; 124 | private int mMaximumVelocity; 125 | private VelocityTracker mVelocityTracker; 126 | private boolean mTransitionIsSetup = false; 127 | private int mResOutColor; 128 | private int mClickColor; 129 | 130 | private static HolographicHelper sHolographicHelper; 131 | private ImageView mHighlight; 132 | private ImageView mClickFeedback; 133 | private boolean mClickFeedbackIsValid = false; 134 | private StackSlider mStackSlider; 135 | private boolean mFirstLayoutHappened = false; 136 | private long mLastInteractionTime = 0; 137 | private long mLastScrollTime; 138 | private int mStackMode; 139 | private int mFramePadding; 140 | private final Rect stackInvalidateRect = new Rect(); 141 | 142 | private final WindowManager WINDOWS_MANAGER = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); 143 | private final DisplayMetrics DISPLAY_METRICS = new DisplayMetrics(); 144 | 145 | /** 146 | * {@inheritDoc} 147 | */ 148 | public StackViewVertical(Context context) { 149 | this(context, null); 150 | } 151 | 152 | /** 153 | * {@inheritDoc} 154 | */ 155 | public StackViewVertical(Context context, AttributeSet attrs) { 156 | this(context, attrs, R.attr.stackViewStyleHorizontal); 157 | } 158 | 159 | /** 160 | * {@inheritDoc} 161 | */ 162 | public StackViewVertical(Context context, AttributeSet attrs, int defStyleAttr) { 163 | super(context, attrs, defStyleAttr); 164 | WINDOWS_MANAGER.getDefaultDisplay().getMetrics(DISPLAY_METRICS); 165 | TypedArray a = context.obtainStyledAttributes(attrs, 166 | R.styleable.StackViewHorizontal, defStyleAttr, 0); 167 | 168 | mResOutColor = a.getColor( 169 | R.styleable.StackViewHorizontal_resOutColorHorizontal, 0); 170 | mClickColor = a.getColor( 171 | R.styleable.StackViewHorizontal_clickColorHorizontal, 0); 172 | 173 | a.recycle(); 174 | initStackView(); 175 | } 176 | 177 | private void initStackView() { 178 | configureViewAnimator(NUM_ACTIVE_VIEWS, 1); 179 | setStaticTransformationsEnabled(true); 180 | final ViewConfiguration configuration = ViewConfiguration.get(getContext()); 181 | mTouchSlop = configuration.getScaledTouchSlop(); 182 | mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 183 | mActivePointerId = INVALID_POINTER; 184 | 185 | mHighlight = new ImageView(getContext()); 186 | mHighlight.setLayoutParams(new LayoutParams(mHighlight)); 187 | addViewInLayout(mHighlight, -1, new LayoutParams(mHighlight)); 188 | 189 | mClickFeedback = new ImageView(getContext()); 190 | mClickFeedback.setLayoutParams(new LayoutParams(mClickFeedback)); 191 | addViewInLayout(mClickFeedback, -1, new LayoutParams(mClickFeedback)); 192 | mClickFeedback.setVisibility(INVISIBLE); 193 | 194 | mStackSlider = new StackSlider(); 195 | 196 | if (sHolographicHelper == null) { 197 | sHolographicHelper = new HolographicHelper(getContext()); 198 | } 199 | setClipChildren(false); 200 | setClipToPadding(false); 201 | 202 | // This sets the form of the StackView, which is currently to have the perspective-shifted 203 | // views above the active view, and have items slide down when sliding out. The opposite is 204 | // available by using ITEMS_SLIDE_UP. 205 | mStackMode = ITEMS_SLIDE_DOWN; 206 | 207 | // This is a flag to indicate the the stack is loading for the first time 208 | mWhichChild = -1; 209 | 210 | // Adjust the frame padding based on the density, since the highlight changes based 211 | // on the density 212 | final float density = getResources().getDisplayMetrics().density; 213 | mFramePadding = (int) Math.ceil(density * FRAME_PADDING); 214 | } 215 | 216 | /** 217 | * Animate the views between different relative indexes within the {@link AdapterViewAnimator} 218 | */ 219 | void transformViewForTransition(int fromIndex, int toIndex, final View view, boolean animate) { 220 | if (!animate) { 221 | ((StackFrame) view).cancelSliderAnimator(); 222 | view.setRotationX(0f); 223 | LayoutParams lp = (LayoutParams) view.getLayoutParams(); 224 | lp.setVerticalOffset(0); 225 | lp.setHorizontalOffset(0); 226 | } 227 | 228 | if (fromIndex == -1 && toIndex == getNumActiveViews() - 1) { 229 | transformViewAtIndex(toIndex, view, false); 230 | view.setVisibility(VISIBLE); 231 | view.setAlpha(1.0f); 232 | } else if (fromIndex == 0 && toIndex == 1) { 233 | // Slide item in 234 | ((StackFrame) view).cancelSliderAnimator(); 235 | view.setVisibility(VISIBLE); 236 | 237 | int duration = Math.round(mStackSlider.getDurationForNeutralPosition(mYVelocity)); 238 | StackSlider animationSlider = new StackSlider(mStackSlider); 239 | animationSlider.setView(view); 240 | 241 | if (animate) { 242 | PropertyValuesHolder slideInY = PropertyValuesHolder.ofFloat("YProgress", 0.0f); 243 | PropertyValuesHolder slideInX = PropertyValuesHolder.ofFloat("XProgress", 0.0f); 244 | ObjectAnimator slideIn = ObjectAnimator.ofPropertyValuesHolder(animationSlider, 245 | slideInX, slideInY); 246 | slideIn.setDuration(duration); 247 | slideIn.setInterpolator(new LinearInterpolator()); 248 | ((StackFrame) view).setSliderAnimator(slideIn); 249 | slideIn.start(); 250 | } else { 251 | animationSlider.setYProgress(0f); 252 | animationSlider.setXProgress(0f); 253 | } 254 | } else if (fromIndex == 1 && toIndex == 0) { 255 | // Slide item out 256 | ((StackFrame) view).cancelSliderAnimator(); 257 | int duration = Math.round(mStackSlider.getDurationForOffscreenPosition(mYVelocity)); 258 | 259 | StackSlider animationSlider = new StackSlider(mStackSlider); 260 | animationSlider.setView(view); 261 | if (animate) { 262 | PropertyValuesHolder slideOutY = PropertyValuesHolder.ofFloat("YProgress", 1.0f); 263 | PropertyValuesHolder slideOutX = PropertyValuesHolder.ofFloat("XProgress", 0.0f); 264 | ObjectAnimator slideOut = ObjectAnimator.ofPropertyValuesHolder(animationSlider, 265 | slideOutX, slideOutY); 266 | slideOut.setDuration(duration); 267 | slideOut.setInterpolator(new LinearInterpolator()); 268 | ((StackFrame) view).setSliderAnimator(slideOut); 269 | slideOut.start(); 270 | } else { 271 | animationSlider.setYProgress(1.0f); 272 | animationSlider.setXProgress(0f); 273 | } 274 | } else if (toIndex == 0) { 275 | // Make sure this view that is "waiting in the wings" is invisible 276 | view.setAlpha(0.0f); 277 | view.setVisibility(INVISIBLE); 278 | } else if ((fromIndex == 0 || fromIndex == 1) && toIndex > 1) { 279 | view.setVisibility(VISIBLE); 280 | view.setAlpha(1.0f); 281 | view.setRotationX(0f); 282 | LayoutParams lp = (LayoutParams) view.getLayoutParams(); 283 | lp.setVerticalOffset(0); 284 | lp.setHorizontalOffset(0); 285 | } else if (fromIndex == -1) { 286 | view.setAlpha(1.0f); 287 | view.setVisibility(VISIBLE); 288 | } else if (toIndex == -1) { 289 | if (animate) { 290 | postDelayed(new Runnable() { 291 | public void run() { 292 | view.setAlpha(0); 293 | } 294 | }, STACK_RELAYOUT_DURATION); 295 | } else { 296 | view.setAlpha(0f); 297 | } 298 | } 299 | 300 | // Implement the faked perspective 301 | if (toIndex != -1) { 302 | transformViewAtIndex(toIndex, view, animate); 303 | } 304 | } 305 | 306 | private void transformViewAtIndex(int index, final View view, boolean animate) { 307 | 308 | //TODO: fix this workaround, item doesn't show when adapter size == 1 309 | if (getAdapter().getCount() == 1) { 310 | return; 311 | } 312 | 313 | final float maxPerspectiveShiftY = mPerspectiveShiftY; 314 | final float maxPerspectiveShiftX = mPerspectiveShiftX; 315 | 316 | if (mStackMode == ITEMS_SLIDE_DOWN) { 317 | index = getNumActiveViews() - index - 1; 318 | if (index == getNumActiveViews() - 1) index--; 319 | } else { 320 | index--; 321 | if (index < 0) index++; 322 | } 323 | 324 | float r = (index * 1f) / (getNumActiveViews() - 2); 325 | 326 | float scale = 1f - PERSPECTIVE_SCALE_FACTOR * (1 - r); 327 | 328 | float perspectiveTranslationY = r * maxPerspectiveShiftY; 329 | float scaleShiftCorrectionY = (scale - 1) * 330 | (getMeasuredHeight() * (1 - PERSPECTIVE_SHIFT_FACTOR_Y) / 2.0f); 331 | final float transY = perspectiveTranslationY + scaleShiftCorrectionY; 332 | 333 | float perspectiveTranslationX = (1 - r) * maxPerspectiveShiftX; 334 | float scaleShiftCorrectionX = (1 - scale) * 335 | (getMeasuredWidth() * (1 - PERSPECTIVE_SHIFT_FACTOR_X) / 2.0f); 336 | final float transX = perspectiveTranslationX + (scaleShiftCorrectionX / 10); 337 | 338 | // If this view is currently being animated for a certain position, we need to cancel 339 | // this animation so as not to interfere with the new transformation. 340 | if (view instanceof StackFrame) { 341 | ((StackFrame) view).cancelTransformAnimator(); 342 | } 343 | 344 | if (animate) { 345 | PropertyValuesHolder translationX = PropertyValuesHolder.ofFloat("translationX", transX); 346 | PropertyValuesHolder translationY = PropertyValuesHolder.ofFloat("translationY", transY); 347 | PropertyValuesHolder scalePropX = PropertyValuesHolder.ofFloat("scaleX", scale); 348 | PropertyValuesHolder scalePropY = PropertyValuesHolder.ofFloat("scaleY", scale); 349 | 350 | ObjectAnimator oa = ObjectAnimator.ofPropertyValuesHolder(view, scalePropX, scalePropY, 351 | translationY, translationX); 352 | oa.setDuration(STACK_RELAYOUT_DURATION); 353 | if (view instanceof StackFrame) { 354 | ((StackFrame) view).setTransformAnimator(oa); 355 | } 356 | oa.start(); 357 | } else { 358 | view.setTranslationX(transX); 359 | view.setTranslationY(transY); 360 | view.setScaleX(scale); 361 | view.setScaleY(scale); 362 | } 363 | } 364 | 365 | private void setupStackSlider(View v, int mode) { 366 | mStackSlider.setMode(mode); 367 | if (v != null) { 368 | mHighlight.setImageBitmap(sHolographicHelper.createResOutline(v, mResOutColor)); 369 | // mHighlight.setRotation(v.getRotation()); 370 | mHighlight.setTranslationY(v.getTranslationY()); 371 | mHighlight.setTranslationX(v.getTranslationX()); 372 | mHighlight.bringToFront(); 373 | v.bringToFront(); 374 | mStackSlider.setView(v); 375 | 376 | v.setVisibility(VISIBLE); 377 | } 378 | } 379 | 380 | /** 381 | * {@inheritDoc} 382 | */ 383 | @Override 384 | public void showNext() { 385 | if (mSwipeGestureType != GESTURE_NONE) return; 386 | if (!mTransitionIsSetup) { 387 | View v = getViewAtRelativeIndex(1); 388 | if (v != null) { 389 | setupStackSlider(v, StackSlider.NORMAL_MODE); 390 | mStackSlider.setYProgress(0); 391 | mStackSlider.setXProgress(0); 392 | } 393 | } 394 | super.showNext(); 395 | } 396 | 397 | /** 398 | * {@inheritDoc} 399 | */ 400 | @Override 401 | public void showPrevious() { 402 | if (mSwipeGestureType != GESTURE_NONE) return; 403 | if (!mTransitionIsSetup) { 404 | View v = getViewAtRelativeIndex(0); 405 | if (v != null) { 406 | setupStackSlider(v, StackSlider.NORMAL_MODE); 407 | mStackSlider.setYProgress(1); 408 | mStackSlider.setXProgress(0); 409 | } 410 | } 411 | super.showPrevious(); 412 | } 413 | 414 | @Override 415 | void showOnly(int childIndex, boolean animate) { 416 | super.showOnly(childIndex, animate); 417 | 418 | // Here we need to make sure that the z-order of the children is correct 419 | for (int i = mCurrentWindowEnd; i >= mCurrentWindowStart; i--) { 420 | int index = modulo(i, getWindowSize()); 421 | ViewAndMetaData vm = mViewsMap.get(index); 422 | if (vm != null) { 423 | View v = mViewsMap.get(index).view; 424 | if (v != null) v.bringToFront(); 425 | } 426 | } 427 | if (mHighlight != null) { 428 | mHighlight.bringToFront(); 429 | } 430 | mTransitionIsSetup = false; 431 | mClickFeedbackIsValid = false; 432 | } 433 | 434 | void updateClickFeedback() { 435 | if (!mClickFeedbackIsValid) { 436 | View v = getViewAtRelativeIndex(1); 437 | if (v != null) { 438 | mClickFeedback.setImageBitmap( 439 | sHolographicHelper.createClickOutline(v, mClickColor)); 440 | mClickFeedback.setTranslationX(v.getTranslationX()); 441 | mClickFeedback.setTranslationY(v.getTranslationY()); 442 | } 443 | mClickFeedbackIsValid = true; 444 | } 445 | } 446 | 447 | @Override 448 | void showTapFeedback(View v) { 449 | updateClickFeedback(); 450 | mClickFeedback.setVisibility(VISIBLE); 451 | mClickFeedback.bringToFront(); 452 | invalidate(); 453 | } 454 | 455 | @Override 456 | void hideTapFeedback(View v) { 457 | mClickFeedback.setVisibility(INVISIBLE); 458 | invalidate(); 459 | } 460 | 461 | private void updateChildTransforms() { 462 | for (int i = 0; i < getNumActiveViews(); i++) { 463 | View v = getViewAtRelativeIndex(i); 464 | if (v != null) { 465 | transformViewAtIndex(i, v, false); 466 | } 467 | } 468 | } 469 | 470 | private static class StackFrame extends FrameLayout { 471 | WeakReference transformAnimator; 472 | WeakReference sliderAnimator; 473 | 474 | public StackFrame(Context context) { 475 | super(context); 476 | } 477 | 478 | void setTransformAnimator(ObjectAnimator oa) { 479 | transformAnimator = new WeakReference(oa); 480 | } 481 | 482 | void setSliderAnimator(ObjectAnimator oa) { 483 | sliderAnimator = new WeakReference(oa); 484 | } 485 | 486 | boolean cancelTransformAnimator() { 487 | if (transformAnimator != null) { 488 | ObjectAnimator oa = transformAnimator.get(); 489 | if (oa != null) { 490 | oa.cancel(); 491 | return true; 492 | } 493 | } 494 | return false; 495 | } 496 | 497 | boolean cancelSliderAnimator() { 498 | if (sliderAnimator != null) { 499 | ObjectAnimator oa = sliderAnimator.get(); 500 | if (oa != null) { 501 | oa.cancel(); 502 | return true; 503 | } 504 | } 505 | return false; 506 | } 507 | } 508 | 509 | @Override 510 | FrameLayout getFrameForChild() { 511 | StackFrame fl = new StackFrame(getContext()); 512 | fl.setPadding(mFramePadding, mFramePadding, mFramePadding, mFramePadding); 513 | return fl; 514 | } 515 | 516 | /** 517 | * Apply any necessary tranforms for the child that is being added. 518 | */ 519 | void applyTransformForChildAtIndex(View child, int relativeIndex) { 520 | } 521 | 522 | @Override 523 | protected void dispatchDraw(Canvas canvas) { 524 | boolean expandClipRegion = false; 525 | 526 | canvas.getClipBounds(stackInvalidateRect); 527 | final int childCount = getChildCount(); 528 | for (int i = 0; i < childCount; i++) { 529 | final View child = getChildAt(i); 530 | LayoutParams lp = (LayoutParams) child.getLayoutParams(); 531 | if ((lp.horizontalOffset == 0 && lp.verticalOffset == 0) || 532 | child.getAlpha() == 0f || child.getVisibility() != VISIBLE) { 533 | lp.resetInvalidateRect(); 534 | } 535 | Rect childInvalidateRect = lp.getInvalidateRect(); 536 | if (!childInvalidateRect.isEmpty()) { 537 | expandClipRegion = true; 538 | stackInvalidateRect.union(childInvalidateRect); 539 | } 540 | } 541 | 542 | // We only expand the clip bounds if necessary. 543 | if (expandClipRegion) { 544 | canvas.save(Canvas.CLIP_SAVE_FLAG); 545 | canvas.clipRect(stackInvalidateRect, Region.Op.UNION); 546 | super.dispatchDraw(canvas); 547 | canvas.restore(); 548 | } else { 549 | super.dispatchDraw(canvas); 550 | } 551 | } 552 | 553 | public void onLayoutView() { 554 | if (!mFirstLayoutHappened) { 555 | mFirstLayoutHappened = true; 556 | updateChildTransforms(); 557 | } 558 | 559 | final int newSlideAmount = Math.round(SLIDE_UP_RATIO * getMeasuredHeight()); 560 | if (mSlideAmount != newSlideAmount) { 561 | mSlideAmount = newSlideAmount; 562 | mSwipeThreshold = Math.round(SWIPE_THRESHOLD_RATIO * newSlideAmount); 563 | } 564 | 565 | if (Float.compare(mPerspectiveShiftY, mNewPerspectiveShiftY) != 0 || 566 | Float.compare(mPerspectiveShiftX, mNewPerspectiveShiftX) != 0) { 567 | 568 | mPerspectiveShiftY = mNewPerspectiveShiftY; 569 | mPerspectiveShiftX = mNewPerspectiveShiftX; 570 | updateChildTransforms(); 571 | } 572 | } 573 | 574 | @Override 575 | public boolean onGenericMotionEvent(MotionEvent event) { 576 | if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { 577 | switch (event.getAction()) { 578 | case MotionEvent.ACTION_SCROLL: { 579 | final float vscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL); 580 | if (vscroll < 0) { 581 | pacedScroll(false); 582 | return true; 583 | } else if (vscroll > 0) { 584 | pacedScroll(true); 585 | return true; 586 | } 587 | } 588 | } 589 | } 590 | return super.onGenericMotionEvent(event); 591 | } 592 | 593 | // This ensures that the frequency of stack flips caused by scrolls is capped 594 | private void pacedScroll(boolean up) { 595 | long timeSinceLastScroll = System.currentTimeMillis() - mLastScrollTime; 596 | if (timeSinceLastScroll > MIN_TIME_BETWEEN_SCROLLS) { 597 | if (up) { 598 | showPrevious(); 599 | } else { 600 | showNext(); 601 | } 602 | mLastScrollTime = System.currentTimeMillis(); 603 | } 604 | } 605 | 606 | /** 607 | * {@inheritDoc} 608 | */ 609 | @Override 610 | public boolean onInterceptTouchEvent(MotionEvent ev) { 611 | int action = ev.getAction(); 612 | switch (action & MotionEvent.ACTION_MASK) { 613 | case MotionEvent.ACTION_DOWN: { 614 | if (mActivePointerId == INVALID_POINTER) { 615 | mInitialX = ev.getX(); 616 | mInitialY = ev.getY(); 617 | mActivePointerId = ev.getPointerId(0); 618 | } 619 | break; 620 | } 621 | // case MotionEvent.ACTION_MOVE: { 622 | // int pointerIndex = ev.findPointerIndex(mActivePointerId); 623 | // if (pointerIndex == INVALID_POINTER) { 624 | // // no data for our primary pointer, this shouldn't happen, log it 625 | // Log.d(TAG, "Error: No data for our primary pointer."); 626 | // return false; 627 | // } 628 | // float newY = ev.getY(pointerIndex); 629 | // float deltaY = newY - mInitialY; 630 | // 631 | // float newX = ev.getX(pointerIndex); 632 | // float deltaX = newX - mInitialX; 633 | // 634 | // // beginGestureIfNeeded(deltaX); 635 | // break; 636 | // } 637 | case MotionEvent.ACTION_POINTER_UP: { 638 | onSecondaryPointerUp(ev); 639 | break; 640 | } 641 | case MotionEvent.ACTION_UP: 642 | case MotionEvent.ACTION_CANCEL: { 643 | mActivePointerId = INVALID_POINTER; 644 | mSwipeGestureType = GESTURE_NONE; 645 | } 646 | } 647 | 648 | return mSwipeGestureType != GESTURE_NONE; 649 | } 650 | 651 | private void beginGestureIfNeeded(float deltaY) { 652 | if ((int) Math.abs(deltaY) > mTouchSlop && mSwipeGestureType == GESTURE_NONE) { 653 | 654 | final int swipeGestureTypeY = deltaY < 0 ? GESTURE_SLIDE_UP : GESTURE_SLIDE_DOWN; 655 | 656 | 657 | cancelLongPress(); 658 | requestDisallowInterceptTouchEvent(true); 659 | 660 | if (getAdapter() == null) return; 661 | final int adapterCount = getCount(); 662 | 663 | int activeIndex; 664 | 665 | if (mStackMode != ITEMS_SLIDE_UP) { 666 | activeIndex = (swipeGestureTypeY == GESTURE_SLIDE_DOWN) ? 0 : 1; 667 | } else { 668 | activeIndex = (swipeGestureTypeY == GESTURE_SLIDE_DOWN) ? 1 : 0; 669 | } 670 | 671 | boolean endOfStack = mLoopViews && adapterCount == 1 && 672 | ((mStackMode == ITEMS_SLIDE_UP && swipeGestureTypeY == GESTURE_SLIDE_UP) || 673 | (mStackMode == ITEMS_SLIDE_DOWN && swipeGestureTypeY == GESTURE_SLIDE_DOWN)); 674 | 675 | 676 | boolean beginningOfStack = mLoopViews && adapterCount == 1 && 677 | ((mStackMode == ITEMS_SLIDE_DOWN && swipeGestureTypeY == GESTURE_SLIDE_UP) || 678 | (mStackMode == ITEMS_SLIDE_UP && swipeGestureTypeY == GESTURE_SLIDE_DOWN)); 679 | 680 | 681 | int stackMode; 682 | if (mLoopViews && !beginningOfStack && !endOfStack) { 683 | stackMode = StackSlider.NORMAL_MODE; 684 | } else if (mCurrentWindowStartUnbounded + activeIndex == -1 || beginningOfStack) { 685 | 686 | activeIndex++; 687 | stackMode = StackSlider.BEGINNING_OF_STACK_MODE; 688 | } else if (mCurrentWindowStartUnbounded + activeIndex == adapterCount - 1 || endOfStack) { 689 | stackMode = StackSlider.END_OF_STACK_MODE; 690 | } else { 691 | stackMode = StackSlider.NORMAL_MODE; 692 | } 693 | 694 | mTransitionIsSetup = stackMode == StackSlider.NORMAL_MODE; 695 | 696 | View v = getViewAtRelativeIndex(activeIndex); 697 | if (v == null) return; 698 | 699 | setupStackSlider(v, stackMode); 700 | 701 | // We only register this gesture if we've made it this far without a problem 702 | mSwipeGestureType = swipeGestureTypeY; 703 | cancelHandleClick(); 704 | } 705 | } 706 | 707 | 708 | /** 709 | * {@inheritDoc} 710 | */ 711 | @Override 712 | public boolean onTouchEvent(MotionEvent ev) { 713 | super.onTouchEvent(ev); 714 | 715 | int action = ev.getAction(); 716 | int pointerIndex = ev.findPointerIndex(mActivePointerId); 717 | if (pointerIndex == INVALID_POINTER) { 718 | // no data for our primary pointer, this shouldn't happen, log it 719 | Log.d(TAG, "Error: No data for our primary pointer."); 720 | return false; 721 | } 722 | 723 | float newY = ev.getY(pointerIndex); 724 | float newX = ev.getX(pointerIndex); 725 | float deltaY = newY - mInitialY; 726 | float deltaX = newX - mInitialX; 727 | if (mVelocityTracker == null) { 728 | mVelocityTracker = VelocityTracker.obtain(); 729 | } 730 | mVelocityTracker.addMovement(ev); 731 | 732 | switch (action & MotionEvent.ACTION_MASK) { 733 | case MotionEvent.ACTION_MOVE: { 734 | beginGestureIfNeeded(deltaX); 735 | 736 | float rx = deltaX / (mSlideAmount * 1.0f); 737 | 738 | if (mSwipeGestureType == GESTURE_SLIDE_DOWN) { 739 | float r = (deltaX - mTouchSlop * 1.0f) / mSlideAmount * 1.0f; 740 | 741 | if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r; 742 | mStackSlider.setYProgress(r); 743 | // mStackSlider.setXProgress(rx); 744 | // mStackSlider.setYProgress(rx); 745 | // mStackSlider.setXProgress(1 - r); 746 | return true; 747 | } else if (mSwipeGestureType == GESTURE_SLIDE_UP) { 748 | float r = -(deltaX + mTouchSlop * 1.0f) / mSlideAmount * 1.0f; 749 | if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r; 750 | mStackSlider.setYProgress(1 - r); 751 | // mStackSlider.setXProgress(rx); 752 | return true; 753 | } 754 | break; 755 | } 756 | case MotionEvent.ACTION_UP: { 757 | handlePointerUp(ev); 758 | break; 759 | } 760 | case MotionEvent.ACTION_POINTER_UP: { 761 | // onSecondaryPointerUp(ev); 762 | break; 763 | } 764 | case MotionEvent.ACTION_CANCEL: { 765 | mActivePointerId = INVALID_POINTER; 766 | mSwipeGestureType = GESTURE_NONE; 767 | break; 768 | } 769 | } 770 | return true; 771 | } 772 | 773 | private void onSecondaryPointerUp(MotionEvent ev) { 774 | final int activePointerIndex = ev.getActionIndex(); 775 | final int pointerId = ev.getPointerId(activePointerIndex); 776 | if (pointerId == mActivePointerId) { 777 | 778 | int activeViewIndex = (mSwipeGestureType == GESTURE_SLIDE_DOWN) ? 0 : 1; 779 | 780 | View v = getViewAtRelativeIndex(activeViewIndex); 781 | if (v == null) return; 782 | 783 | // Our primary pointer has gone up -- let's see if we can find 784 | // another pointer on the view. If so, then we should replace 785 | // our primary pointer with this new pointer and adjust things 786 | // so that the view doesn't jump 787 | for (int index = 0; index < ev.getPointerCount(); index++) { 788 | if (index != activePointerIndex) { 789 | 790 | float x = ev.getX(index); 791 | float y = ev.getY(index); 792 | 793 | mTouchRect.set(v.getLeft(), v.getTop(), v.getRight(), v.getBottom()); 794 | if (mTouchRect.contains(Math.round(x), Math.round(y))) { 795 | float oldX = ev.getX(activePointerIndex); 796 | float oldY = ev.getY(activePointerIndex); 797 | 798 | // adjust our frame of reference to avoid a jump 799 | mInitialY += (y - oldY); 800 | mInitialX += (x - oldX); 801 | 802 | mActivePointerId = ev.getPointerId(index); 803 | if (mVelocityTracker != null) { 804 | mVelocityTracker.clear(); 805 | } 806 | // ok, we're good, we found a new pointer which is touching the active view 807 | return; 808 | } 809 | } 810 | } 811 | // if we made it this far, it means we didn't find a satisfactory new pointer :(, 812 | // so end the gesture 813 | handlePointerUp(ev); 814 | } 815 | } 816 | 817 | private void handlePointerUp(MotionEvent ev) { 818 | int pointerIndex = ev.findPointerIndex(mActivePointerId); 819 | float newX = ev.getX(pointerIndex); 820 | int deltaX = (int) (newX - mInitialY); 821 | mLastInteractionTime = System.currentTimeMillis(); 822 | 823 | if (mVelocityTracker != null) { 824 | mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 825 | mYVelocity = (int) mVelocityTracker.getYVelocity(mActivePointerId); 826 | } 827 | 828 | if (mVelocityTracker != null) { 829 | mVelocityTracker.recycle(); 830 | mVelocityTracker = null; 831 | } 832 | 833 | 834 | Log.d("==", "mSwipeGestureType :: " + mSwipeGestureType); 835 | 836 | if (deltaX < mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_UP 837 | && mStackSlider.mMode == StackSlider.NORMAL_MODE) { 838 | Log.d("===", "1"); 839 | // We reset the gesture variable, because otherwise we will ignore showPrevious() / 840 | // showNext(); 841 | mSwipeGestureType = GESTURE_NONE; 842 | 843 | // Swipe threshold exceeded, swipe down 844 | if (mStackMode == ITEMS_SLIDE_UP) { 845 | showPrevious(); 846 | } else { 847 | showNext(); 848 | } 849 | mHighlight.bringToFront(); 850 | } else if (deltaX > mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_DOWN 851 | && mStackSlider.mMode == StackSlider.NORMAL_MODE) { 852 | // 853 | Log.d("===", "2"); 854 | // We reset the gesture variable, because otherwise we will ignore showPrevious() / 855 | // showNext(); 856 | mSwipeGestureType = GESTURE_NONE; 857 | 858 | // Swipe threshold exceeded, swipe up 859 | if (mStackMode == ITEMS_SLIDE_UP) { 860 | showNext(); 861 | } else { 862 | showPrevious(); 863 | } 864 | 865 | mHighlight.bringToFront(); 866 | } else if (mSwipeGestureType == GESTURE_SLIDE_DOWN) { 867 | Log.d("===", "3"); 868 | // Didn't swipe up far enough, snap back down 869 | int duration; 870 | float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 1 : 0; 871 | if (mStackMode == ITEMS_SLIDE_UP || mStackSlider.mMode != StackSlider.NORMAL_MODE) { 872 | duration = Math.round(mStackSlider.getDurationForNeutralPosition()); 873 | } else { 874 | duration = Math.round(mStackSlider.getDurationForOffscreenPosition()); 875 | } 876 | 877 | StackSlider animationSlider = new StackSlider(mStackSlider); 878 | PropertyValuesHolder snapBackY = PropertyValuesHolder.ofFloat("YProgress", finalYProgress); 879 | PropertyValuesHolder snapBackX = PropertyValuesHolder.ofFloat("XProgress", 0.0f); 880 | ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider, 881 | snapBackX, snapBackY); 882 | pa.setDuration(duration); 883 | pa.setInterpolator(new LinearInterpolator()); 884 | pa.start(); 885 | } else if (mSwipeGestureType == GESTURE_SLIDE_UP) { 886 | Log.d("===", "4"); 887 | // Didn't swipe down far enough, snap back up 888 | float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 0 : 1; 889 | int duration; 890 | if (mStackMode == ITEMS_SLIDE_DOWN || mStackSlider.mMode != StackSlider.NORMAL_MODE) { 891 | duration = Math.round(mStackSlider.getDurationForOffscreenPosition()); 892 | } else { 893 | duration = Math.round(mStackSlider.getDurationForNeutralPosition()); 894 | 895 | } 896 | 897 | StackSlider animationSlider = new StackSlider(mStackSlider); 898 | PropertyValuesHolder snapBackY = 899 | PropertyValuesHolder.ofFloat("YProgress", finalYProgress); 900 | PropertyValuesHolder snapBackX = PropertyValuesHolder.ofFloat("XProgress", 0.0f); 901 | ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider, 902 | snapBackX, snapBackY); 903 | pa.setDuration(duration); 904 | pa.start(); 905 | } 906 | 907 | mActivePointerId = INVALID_POINTER; 908 | mSwipeGestureType = GESTURE_NONE; 909 | } 910 | 911 | private class StackSlider { 912 | View mView; 913 | float mYProgress; 914 | float mXProgress; 915 | 916 | static final int NORMAL_MODE = 0; 917 | static final int BEGINNING_OF_STACK_MODE = 1; 918 | static final int END_OF_STACK_MODE = 2; 919 | 920 | int mMode = NORMAL_MODE; 921 | 922 | public StackSlider() { 923 | } 924 | 925 | public StackSlider(StackSlider copy) { 926 | mView = copy.mView; 927 | mYProgress = copy.mYProgress; 928 | mXProgress = copy.mXProgress; 929 | mMode = copy.mMode; 930 | } 931 | 932 | private float cubic(float r) { 933 | return (float) (Math.pow(2 * r - 1, 3) + 1) / 2.0f; 934 | } 935 | 936 | private float highlightAlphaInterpolator(float r) { 937 | float pivot = 0.3f; 938 | if (r < pivot) { 939 | return 0.45f * cubic(r / pivot); 940 | } else { 941 | return 0.45f * cubic(1 - (r - pivot) / (1 - pivot)); 942 | } 943 | } 944 | 945 | private float viewAlphaInterpolator(float r) { 946 | float pivot = 0.01f; 947 | if (r > pivot) { 948 | return (r - pivot) / (1 - pivot); 949 | } else { 950 | return 0; 951 | } 952 | } 953 | 954 | private float rotationInterpolator(float r) { 955 | float pivot = 0.2f; 956 | if (r < pivot) { 957 | return 0; 958 | } else { 959 | return (r - pivot) / (1 - pivot); 960 | } 961 | } 962 | 963 | void setView(View v) { 964 | mView = v; 965 | } 966 | 967 | public void setYProgress(float r) { 968 | 969 | // enforce r between 0 and 1 970 | r = Math.min(1.0f, r); 971 | r = Math.max(0, r); 972 | 973 | mYProgress = r; 974 | if (mView == null) return; 975 | 976 | final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams(); 977 | final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams(); 978 | 979 | int stackDirection = (mStackMode == ITEMS_SLIDE_UP) ? -1 : 1; 980 | 981 | // We need to prevent any clipping issues which may arise by setting a layer type. 982 | // This doesn't come for free however, so we only want to enable it when required. 983 | if (Float.compare(0f, mYProgress) != 0 && Float.compare(1.0f, mYProgress) != 0) { 984 | if (mView.getLayerType() == LAYER_TYPE_NONE) { 985 | mView.setLayerType(LAYER_TYPE_HARDWARE, null); 986 | } 987 | } else { 988 | if (mView.getLayerType() != LAYER_TYPE_NONE) { 989 | mView.setLayerType(LAYER_TYPE_NONE, null); 990 | } 991 | } 992 | 993 | switch (mMode) { 994 | case NORMAL_MODE: 995 | // viewLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount)); 996 | // highlightLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount)); 997 | viewLp.setHorizontalOffset(Math.round(-r * stackDirection * mSlideAmount)); 998 | highlightLp.setHorizontalOffset(Math.round(-r * stackDirection * mSlideAmount)); 999 | mHighlight.setAlpha(highlightAlphaInterpolator(r)); 1000 | 1001 | float alpha = viewAlphaInterpolator(1 - r); 1002 | 1003 | // We make sure that views which can't be seen (have 0 alpha) are also invisible 1004 | // so that they don't interfere with click events. 1005 | if (mView.getAlpha() == 0 && alpha != 0 && mView.getVisibility() != VISIBLE) { 1006 | mView.setVisibility(VISIBLE); 1007 | } else if (alpha == 0 && mView.getAlpha() != 0 1008 | && mView.getVisibility() == VISIBLE) { 1009 | mView.setVisibility(INVISIBLE); 1010 | } 1011 | 1012 | mView.setAlpha(alpha); 1013 | // mView.setRotationX(stackDirection * 90.0f * rotationInterpolator(r)); 1014 | // mHighlight.setRotationX(stackDirection * 90.0f * rotationInterpolator(r)); 1015 | break; 1016 | case END_OF_STACK_MODE: 1017 | r = r * 0.2f; 1018 | viewLp.setHorizontalOffset(Math.round(-stackDirection * r * mSlideAmount)); 1019 | highlightLp.setHorizontalOffset(Math.round(-stackDirection * r * mSlideAmount)); 1020 | mHighlight.setAlpha(highlightAlphaInterpolator(r)); 1021 | break; 1022 | case BEGINNING_OF_STACK_MODE: 1023 | r = (1 - r) * 0.2f; 1024 | viewLp.setHorizontalOffset(Math.round(stackDirection * r * mSlideAmount)); 1025 | highlightLp.setHorizontalOffset(Math.round(stackDirection * r * mSlideAmount)); 1026 | mHighlight.setAlpha(highlightAlphaInterpolator(r)); 1027 | break; 1028 | } 1029 | } 1030 | 1031 | public void setXProgress(float r) { 1032 | // enforce r between 0 and 1 1033 | r = Math.min(2.0f, r); 1034 | r = Math.max(-2.0f, r); 1035 | 1036 | mXProgress = r; 1037 | 1038 | if (mView == null) return; 1039 | final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams(); 1040 | final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams(); 1041 | 1042 | 1043 | int stackDirection = (mStackMode == ITEMS_SLIDE_UP) ? 1 : -1; 1044 | if (Float.compare(0f, mYProgress) != 0 && Float.compare(1.0f, mYProgress) != 0) { 1045 | if (mView.getLayerType() == LAYER_TYPE_NONE) { 1046 | mView.setLayerType(LAYER_TYPE_HARDWARE, null); 1047 | } 1048 | } else { 1049 | if (mView.getLayerType() != LAYER_TYPE_NONE) { 1050 | mView.setLayerType(LAYER_TYPE_NONE, null); 1051 | } 1052 | } 1053 | 1054 | switch (mMode) { 1055 | case NORMAL_MODE: 1056 | case END_OF_STACK_MODE: 1057 | case BEGINNING_OF_STACK_MODE: 1058 | } 1059 | 1060 | 1061 | r *= 0.2f; 1062 | viewLp.setVerticalOffset(Math.round(r * mSlideAmount)); 1063 | highlightLp.setVerticalOffset(Math.round(r * mSlideAmount)); 1064 | } 1065 | 1066 | void setMode(int mode) { 1067 | mMode = mode; 1068 | } 1069 | 1070 | float getDurationForNeutralPosition() { 1071 | return getDuration(false, 0); 1072 | } 1073 | 1074 | float getDurationForOffscreenPosition() { 1075 | return getDuration(true, 0); 1076 | } 1077 | 1078 | float getDurationForNeutralPosition(float velocity) { 1079 | return getDuration(false, velocity); 1080 | } 1081 | 1082 | float getDurationForOffscreenPosition(float velocity) { 1083 | return getDuration(true, velocity); 1084 | } 1085 | 1086 | private float getDuration(boolean invert, float velocity) { 1087 | if (mView != null) { 1088 | final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams(); 1089 | 1090 | float d = (float) Math.sqrt(Math.pow(viewLp.horizontalOffset, 2) + 1091 | Math.pow(viewLp.verticalOffset, 2)); 1092 | float maxd = (float) Math.sqrt(Math.pow(mSlideAmount, 2) + 1093 | Math.pow(0.4f * mSlideAmount, 2)); 1094 | 1095 | if (velocity == 0) { 1096 | return (invert ? (1 - d / maxd) : d / maxd) * DEFAULT_ANIMATION_DURATION; 1097 | } else { 1098 | float duration = invert ? d / Math.abs(velocity) : 1099 | (maxd - d) / Math.abs(velocity); 1100 | if (duration < MINIMUM_ANIMATION_DURATION || 1101 | duration > DEFAULT_ANIMATION_DURATION) { 1102 | return getDuration(invert, 0); 1103 | } else { 1104 | return duration; 1105 | } 1106 | } 1107 | } 1108 | return 0; 1109 | } 1110 | 1111 | // Used for animations 1112 | @SuppressWarnings({"UnusedDeclaration"}) 1113 | public float getYProgress() { 1114 | return mYProgress; 1115 | } 1116 | 1117 | // Used for animations 1118 | @SuppressWarnings({"UnusedDeclaration"}) 1119 | public float getXProgress() { 1120 | return mXProgress; 1121 | } 1122 | } 1123 | 1124 | LayoutParams createOrReuseLayoutParams(View v) { 1125 | final ViewGroup.LayoutParams currentLp = v.getLayoutParams(); 1126 | if (currentLp instanceof LayoutParams) { 1127 | LayoutParams lp = (LayoutParams) currentLp; 1128 | lp.setHorizontalOffset(0); 1129 | lp.setVerticalOffset(0); 1130 | lp.width = 0; 1131 | lp.width = 0; 1132 | return lp; 1133 | } 1134 | return new LayoutParams(v); 1135 | } 1136 | 1137 | @Override 1138 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 1139 | checkForAndHandleDataChanged(); 1140 | 1141 | final int childCount = getChildCount(); 1142 | for (int i = 0; i < childCount; i++) { 1143 | final View child = getChildAt(i); 1144 | 1145 | int childRight = getPaddingLeft() + child.getMeasuredWidth(); 1146 | int childBottom = getPaddingTop() + child.getMeasuredHeight(); 1147 | LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1148 | 1149 | child.layout(getPaddingLeft() + lp.horizontalOffset, getPaddingTop() + lp.verticalOffset, 1150 | childRight + lp.horizontalOffset, childBottom + lp.verticalOffset); 1151 | 1152 | } 1153 | onLayoutView(); 1154 | } 1155 | 1156 | @Override 1157 | public void advance() { 1158 | long timeSinceLastInteraction = System.currentTimeMillis() - mLastInteractionTime; 1159 | 1160 | if (mAdapter == null) return; 1161 | final int adapterCount = getCount(); 1162 | if (adapterCount == 1 && mLoopViews) return; 1163 | 1164 | if (mSwipeGestureType == GESTURE_NONE && 1165 | timeSinceLastInteraction > MIN_TIME_BETWEEN_INTERACTION_AND_AUTOADVANCE) { 1166 | showNext(); 1167 | } 1168 | } 1169 | 1170 | private void measureChildren() { 1171 | final int count = getChildCount(); 1172 | 1173 | final int measuredWidth = getMeasuredWidth(); 1174 | final int measuredHeight = getMeasuredHeight(); 1175 | 1176 | final int childWidth = Math.round(measuredWidth * (1 - PERSPECTIVE_SHIFT_FACTOR_X)) 1177 | - getPaddingLeft() - getPaddingRight(); 1178 | final int childHeight = Math.round(measuredHeight * (1 - PERSPECTIVE_SHIFT_FACTOR_Y)) 1179 | - getPaddingTop() - getPaddingBottom(); 1180 | 1181 | int maxWidth = 0; 1182 | int maxHeight = 0; 1183 | 1184 | for (int i = 0; i < count; i++) { 1185 | final View child = getChildAt(i); 1186 | child.measure(View.MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY), 1187 | View.MeasureSpec.makeMeasureSpec(childHeight, View.MeasureSpec.EXACTLY)); 1188 | 1189 | if (child != mHighlight && child != mClickFeedback) { 1190 | final int childMeasuredWidth = child.getMeasuredWidth(); 1191 | final int childMeasuredHeight = child.getMeasuredHeight(); 1192 | if (childMeasuredWidth > maxWidth) { 1193 | maxWidth = childMeasuredWidth; 1194 | } 1195 | if (childMeasuredHeight > maxHeight) { 1196 | maxHeight = childMeasuredHeight; 1197 | } 1198 | } 1199 | } 1200 | 1201 | mNewPerspectiveShiftX = PERSPECTIVE_SHIFT_FACTOR_X * measuredWidth; 1202 | mNewPerspectiveShiftY = PERSPECTIVE_SHIFT_FACTOR_Y * measuredHeight; 1203 | 1204 | // If we have extra space, we try and spread the items out 1205 | if (maxWidth > 0 && count > 0 && maxWidth < childWidth) { 1206 | mNewPerspectiveShiftX = measuredWidth - maxWidth; 1207 | } 1208 | 1209 | if (maxHeight > 0 && count > 0 && maxHeight < childHeight) { 1210 | mNewPerspectiveShiftY = measuredHeight - maxHeight; 1211 | } 1212 | } 1213 | 1214 | 1215 | @Override 1216 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1217 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1218 | int widthSpecSize = View.MeasureSpec.getSize(widthMeasureSpec); 1219 | ; 1220 | int heightInPX = DISPLAY_METRICS.heightPixels / 3; 1221 | int heightSpecSize = MeasureSpec.makeMeasureSpec(heightInPX, MeasureSpec.AT_MOST); 1222 | final int widthSpecMode = View.MeasureSpec.getMode(widthMeasureSpec); 1223 | final int heightSpecMode = MeasureSpec.makeMeasureSpec(heightInPX, MeasureSpec.AT_MOST); 1224 | boolean haveChildRefSize = (mReferenceChildWidth != -1 && mReferenceChildHeight != -1); 1225 | 1226 | // We need to deal with the case where our parent hasn't told us how 1227 | // big we should be. In this case we should 1228 | float factorY = 1 / (1 - PERSPECTIVE_SHIFT_FACTOR_Y); 1229 | if (heightSpecMode == View.MeasureSpec.UNSPECIFIED) { 1230 | heightSpecSize = haveChildRefSize ? 1231 | Math.round(mReferenceChildHeight * (1 + factorY)) + 1232 | getPaddingTop() + getPaddingBottom() : 0; 1233 | } else if (heightSpecMode == View.MeasureSpec.AT_MOST) { 1234 | if (haveChildRefSize) { 1235 | int height = Math.round(mReferenceChildHeight * (1 + factorY)) 1236 | + getPaddingTop() + getPaddingBottom(); 1237 | if (height <= heightSpecSize) { 1238 | heightSpecSize = height; 1239 | } else { 1240 | heightSpecSize |= View.MEASURED_STATE_TOO_SMALL; 1241 | 1242 | } 1243 | } else { 1244 | heightSpecSize = 0; 1245 | } 1246 | } 1247 | 1248 | float factorX = 1 / (1 - PERSPECTIVE_SHIFT_FACTOR_X); 1249 | if (widthSpecMode == View.MeasureSpec.UNSPECIFIED) { 1250 | 1251 | widthSpecSize = haveChildRefSize ? 1252 | Math.round(mReferenceChildWidth * (1 + factorX)) + 1253 | getPaddingLeft() + getPaddingRight() : 0; 1254 | } else if (heightSpecMode == View.MeasureSpec.AT_MOST) { 1255 | 1256 | if (haveChildRefSize) { 1257 | int width = mReferenceChildWidth + getPaddingLeft() + getPaddingRight(); 1258 | if (width <= widthSpecSize) { 1259 | widthSpecSize = width; 1260 | } else { 1261 | widthSpecSize |= MEASURED_STATE_TOO_SMALL; 1262 | } 1263 | } else { 1264 | widthSpecSize = 0; 1265 | } 1266 | } 1267 | 1268 | setMeasuredDimension(widthSpecSize, heightSpecSize); 1269 | measureChildren(); 1270 | } 1271 | 1272 | class LayoutParams extends ViewGroup.LayoutParams { 1273 | int horizontalOffset; 1274 | int verticalOffset; 1275 | View mView; 1276 | private final Rect parentRect = new Rect(); 1277 | private final Rect invalidateRect = new Rect(); 1278 | private final RectF invalidateRectf = new RectF(); 1279 | private final Rect globalInvalidateRect = new Rect(); 1280 | 1281 | LayoutParams(View view) { 1282 | super(0, 0); 1283 | width = 0; 1284 | height = 0; 1285 | horizontalOffset = 0; 1286 | verticalOffset = 0; 1287 | mView = view; 1288 | } 1289 | 1290 | LayoutParams(Context c, AttributeSet attrs) { 1291 | super(c, attrs); 1292 | horizontalOffset = 0; 1293 | verticalOffset = 0; 1294 | width = 0; 1295 | height = 0; 1296 | } 1297 | 1298 | void invalidateGlobalRegion(View v, Rect r) { 1299 | // We need to make a new rect here, so as not to modify the one passed 1300 | globalInvalidateRect.set(r); 1301 | globalInvalidateRect.union(0, 0, getWidth(), getHeight()); 1302 | View p = v; 1303 | if (!(v.getParent() != null && v.getParent() instanceof View)) return; 1304 | 1305 | boolean firstPass = true; 1306 | parentRect.set(0, 0, 0, 0); 1307 | while (p.getParent() != null && p.getParent() instanceof View 1308 | && !parentRect.contains(globalInvalidateRect)) { 1309 | if (!firstPass) { 1310 | globalInvalidateRect.offset(p.getLeft() - p.getScrollX(), p.getTop() 1311 | - p.getScrollY()); 1312 | } 1313 | firstPass = false; 1314 | p = (View) p.getParent(); 1315 | parentRect.set(p.getScrollX(), p.getScrollY(), 1316 | p.getWidth() + p.getScrollX(), p.getHeight() + p.getScrollY()); 1317 | p.invalidate(globalInvalidateRect.left, globalInvalidateRect.top, 1318 | globalInvalidateRect.right, globalInvalidateRect.bottom); 1319 | } 1320 | 1321 | p.invalidate(globalInvalidateRect.left, globalInvalidateRect.top, 1322 | globalInvalidateRect.right, globalInvalidateRect.bottom); 1323 | } 1324 | 1325 | Rect getInvalidateRect() { 1326 | return invalidateRect; 1327 | } 1328 | 1329 | void resetInvalidateRect() { 1330 | invalidateRect.set(0, 0, 0, 0); 1331 | } 1332 | 1333 | // This is public so that ObjectAnimator can access it 1334 | public void setVerticalOffset(int newVerticalOffset) { 1335 | setOffsets(horizontalOffset, newVerticalOffset); 1336 | } 1337 | 1338 | public void setHorizontalOffset(int newHorizontalOffset) { 1339 | setOffsets(newHorizontalOffset, verticalOffset); 1340 | } 1341 | 1342 | public void setOffsets(int newHorizontalOffset, int newVerticalOffset) { 1343 | int horizontalOffsetDelta = newHorizontalOffset - horizontalOffset; 1344 | horizontalOffset = newHorizontalOffset; 1345 | int verticalOffsetDelta = newVerticalOffset - verticalOffset; 1346 | verticalOffset = newVerticalOffset; 1347 | 1348 | if (mView != null) { 1349 | mView.requestLayout(); 1350 | int left = Math.min(mView.getLeft() + horizontalOffsetDelta, mView.getLeft()); 1351 | int right = Math.max(mView.getRight() + horizontalOffsetDelta, mView.getRight()); 1352 | int top = Math.min(mView.getTop() + verticalOffsetDelta, mView.getTop()); 1353 | int bottom = Math.max(mView.getBottom() + verticalOffsetDelta, mView.getBottom()); 1354 | 1355 | invalidateRectf.set(left, top, right, bottom); 1356 | 1357 | float xoffset = -invalidateRectf.left; 1358 | float yoffset = -invalidateRectf.top; 1359 | invalidateRectf.offset(xoffset, yoffset); 1360 | mView.getMatrix().mapRect(invalidateRectf); 1361 | invalidateRectf.offset(-xoffset, -yoffset); 1362 | 1363 | invalidateRect.set((int) Math.floor(invalidateRectf.left), 1364 | (int) Math.floor(invalidateRectf.top), 1365 | (int) Math.ceil(invalidateRectf.right), 1366 | (int) Math.ceil(invalidateRectf.bottom)); 1367 | 1368 | invalidateGlobalRegion(mView, invalidateRect); 1369 | } 1370 | } 1371 | } 1372 | 1373 | private static class HolographicHelper { 1374 | private final Paint mHolographicPaint = new Paint(); 1375 | private final Paint mErasePaint = new Paint(); 1376 | private final Paint mBlurPaint = new Paint(); 1377 | private static final int RES_OUT = 0; 1378 | private static final int CLICK_FEEDBACK = 1; 1379 | private float mDensity; 1380 | private BlurMaskFilter mSmallBlurMaskFilter; 1381 | private BlurMaskFilter mLargeBlurMaskFilter; 1382 | private final Canvas mCanvas = new Canvas(); 1383 | private final Canvas mMaskCanvas = new Canvas(); 1384 | private final int[] mTmpXY = new int[2]; 1385 | private final Matrix mIdentityMatrix = new Matrix(); 1386 | 1387 | HolographicHelper(Context context) { 1388 | mDensity = context.getResources().getDisplayMetrics().density; 1389 | 1390 | mHolographicPaint.setFilterBitmap(true); 1391 | // mHolographicPaint.setMaskFilter(TableMaskFilter.CreateClipTable(0, 30)); 1392 | mErasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); 1393 | mErasePaint.setFilterBitmap(true); 1394 | 1395 | mSmallBlurMaskFilter = new BlurMaskFilter(2 * mDensity, BlurMaskFilter.Blur.NORMAL); 1396 | mLargeBlurMaskFilter = new BlurMaskFilter(4 * mDensity, BlurMaskFilter.Blur.NORMAL); 1397 | } 1398 | 1399 | Bitmap createClickOutline(View v, int color) { 1400 | return createOutline(v, CLICK_FEEDBACK, color); 1401 | } 1402 | 1403 | Bitmap createResOutline(View v, int color) { 1404 | return createOutline(v, RES_OUT, color); 1405 | } 1406 | 1407 | Bitmap createOutline(View v, int type, int color) { 1408 | mHolographicPaint.setColor(color); 1409 | if (type == RES_OUT) { 1410 | mBlurPaint.setMaskFilter(mSmallBlurMaskFilter); 1411 | } else if (type == CLICK_FEEDBACK) { 1412 | mBlurPaint.setMaskFilter(mLargeBlurMaskFilter); 1413 | } 1414 | 1415 | if (v.getMeasuredWidth() == 0 || v.getMeasuredHeight() == 0) { 1416 | return null; 1417 | } 1418 | 1419 | Bitmap bitmap = Bitmap.createBitmap(v.getMeasuredWidth(), v.getMeasuredHeight(), 1420 | Bitmap.Config.ARGB_8888); 1421 | mCanvas.setBitmap(bitmap); 1422 | 1423 | float rotationX = v.getRotationX(); 1424 | float rotation = v.getRotation(); 1425 | float translationY = v.getTranslationY(); 1426 | float translationX = v.getTranslationX(); 1427 | v.setRotationX(0); 1428 | // v.setRotation(0); 1429 | v.setTranslationY(0); 1430 | v.setTranslationX(0); 1431 | v.draw(mCanvas); 1432 | v.setRotationX(rotationX); 1433 | // v.setRotation(rotation); 1434 | v.setTranslationY(translationY); 1435 | v.setTranslationX(translationX); 1436 | 1437 | drawOutline(mCanvas, bitmap); 1438 | mCanvas.setBitmap(null); 1439 | return bitmap; 1440 | } 1441 | 1442 | void drawOutline(Canvas dest, Bitmap src) { 1443 | final int[] xy = mTmpXY; 1444 | Bitmap mask = src.extractAlpha(mBlurPaint, xy); 1445 | mMaskCanvas.setBitmap(mask); 1446 | mMaskCanvas.drawBitmap(src, -xy[0], -xy[1], mErasePaint); 1447 | dest.drawColor(0, PorterDuff.Mode.CLEAR); 1448 | dest.setMatrix(mIdentityMatrix); 1449 | dest.drawBitmap(mask, xy[0], xy[1], mHolographicPaint); 1450 | mMaskCanvas.setBitmap(null); 1451 | mask.recycle(); 1452 | } 1453 | } 1454 | } --------------------------------------------------------------------------------