├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── zzt │ │ └── cycleviewpager │ │ └── MainActivity.java │ └── res │ ├── layout │ ├── activity_main.xml │ ├── page1.xml │ ├── page2.xml │ └── page3.xml │ ├── menu │ └── menu_main.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ ├── pic1.jpg │ ├── pic2.jpg │ └── pic3.jpg │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── library ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── zzt │ └── library │ └── CycleViewPager.java ├── screenshot.gif └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/workspace.xml 4 | /.idea/libraries 5 | .DS_Store 6 | /build 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CycleViewPager 2 | 拓展ViewPager使之能循环滚动 3 | 4 | # 说明 5 | viewpager最少要有3个item 6 | 7 | # ScreenShot 8 | ![image](https://github.com/zhaozhentao/CycleViewPager/blob/master/screenshot.gif) 9 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 22 5 | buildToolsVersion "22.0.1" 6 | 7 | defaultConfig { 8 | applicationId "com.zzt.cycleviewpager" 9 | minSdkVersion 11 10 | targetSdkVersion 22 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.1.1' 25 | compile project(':library') 26 | } 27 | -------------------------------------------------------------------------------- /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 H:\AndroidDev\androidsdk/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/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzt/cycleviewpager/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.zzt.cycleviewpager; 2 | 3 | import android.support.v4.view.PagerAdapter; 4 | import android.support.v7.app.ActionBarActivity; 5 | import android.os.Bundle; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.Toast; 10 | 11 | import com.zzt.library.CycleViewPager; 12 | 13 | import java.util.ArrayList; 14 | 15 | 16 | public class MainActivity extends ActionBarActivity { 17 | 18 | private CycleViewPager cycleViewPager; 19 | ArrayList pages = new ArrayList<>(); 20 | 21 | @Override 22 | protected void onCreate(Bundle savedInstanceState) { 23 | super.onCreate(savedInstanceState); 24 | setContentView(R.layout.activity_main); 25 | 26 | cycleViewPager = (CycleViewPager)findViewById(R.id.pp); 27 | pages.add(LayoutInflater.from(this).inflate(R.layout.page1, null)); 28 | pages.add(LayoutInflater.from(this).inflate(R.layout.page2, null)); 29 | pages.add(LayoutInflater.from(this).inflate(R.layout.page3, null)); 30 | 31 | cycleViewPager.setAdapter(new PagerAdapter() { 32 | @Override 33 | public int getCount() { 34 | return pages.size(); 35 | } 36 | 37 | @Override 38 | public boolean isViewFromObject(View view, Object object) { 39 | return view == object; 40 | } 41 | 42 | @Override 43 | public void destroyItem(ViewGroup container, int position, 44 | Object object) { 45 | //Toast.makeText(MainActivity.this, "destroyItem:"+position, Toast.LENGTH_SHORT).show(); 46 | ((CycleViewPager) container).removeView(pages.get(position)); 47 | } 48 | 49 | @Override 50 | public Object instantiateItem(ViewGroup container, int position) { 51 | ((CycleViewPager) container).addView(pages.get(position)); 52 | return pages.get(position); 53 | } 54 | }); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/layout/page1.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/layout/page2.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/layout/page3.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhaozhentao/CycleViewPager/73d6f0c50cb88a8adda15dabd7f3a6c2398942cc/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhaozhentao/CycleViewPager/73d6f0c50cb88a8adda15dabd7f3a6c2398942cc/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhaozhentao/CycleViewPager/73d6f0c50cb88a8adda15dabd7f3a6c2398942cc/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/pic1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhaozhentao/CycleViewPager/73d6f0c50cb88a8adda15dabd7f3a6c2398942cc/app/src/main/res/mipmap-xhdpi/pic1.jpg -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/pic2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhaozhentao/CycleViewPager/73d6f0c50cb88a8adda15dabd7f3a6c2398942cc/app/src/main/res/mipmap-xhdpi/pic2.jpg -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/pic3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhaozhentao/CycleViewPager/73d6f0c50cb88a8adda15dabd7f3a6c2398942cc/app/src/main/res/mipmap-xhdpi/pic3.jpg -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhaozhentao/CycleViewPager/73d6f0c50cb88a8adda15dabd7f3a6c2398942cc/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | CycleViewPager 3 | 4 | Hello world! 5 | Settings 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:1.1.0' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhaozhentao/CycleViewPager/73d6f0c50cb88a8adda15dabd7f3a6c2398942cc/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /library/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 22 5 | buildToolsVersion "22.0.1" 6 | 7 | defaultConfig { 8 | minSdkVersion 11 9 | targetSdkVersion 22 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.1.1' 24 | } 25 | -------------------------------------------------------------------------------- /library/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 H:/AndroidDev/androidsdk/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 | -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /library/src/main/java/com/zzt/library/CycleViewPager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.zzt.library; 18 | 19 | import android.content.Context; 20 | import android.content.res.Resources; 21 | import android.content.res.TypedArray; 22 | import android.database.DataSetObserver; 23 | import android.graphics.Canvas; 24 | import android.graphics.Rect; 25 | import android.graphics.drawable.Drawable; 26 | import android.os.Build; 27 | import android.os.Bundle; 28 | import android.os.Parcel; 29 | import android.os.Parcelable; 30 | import android.os.SystemClock; 31 | import android.support.annotation.DrawableRes; 32 | import android.support.v4.os.ParcelableCompat; 33 | import android.support.v4.os.ParcelableCompatCreatorCallbacks; 34 | import android.support.v4.view.AccessibilityDelegateCompat; 35 | import android.support.v4.view.KeyEventCompat; 36 | import android.support.v4.view.MotionEventCompat; 37 | import android.support.v4.view.PagerAdapter; 38 | import android.support.v4.view.VelocityTrackerCompat; 39 | import android.support.v4.view.ViewCompat; 40 | import android.support.v4.view.ViewConfigurationCompat; 41 | import android.support.v4.view.accessibility.AccessibilityEventCompat; 42 | import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; 43 | import android.support.v4.view.accessibility.AccessibilityRecordCompat; 44 | import android.support.v4.widget.EdgeEffectCompat; 45 | import android.util.AttributeSet; 46 | import android.util.Log; 47 | import android.view.FocusFinder; 48 | import android.view.Gravity; 49 | import android.view.KeyEvent; 50 | import android.view.MotionEvent; 51 | import android.view.SoundEffectConstants; 52 | import android.view.VelocityTracker; 53 | import android.view.View; 54 | import android.view.ViewConfiguration; 55 | import android.view.ViewGroup; 56 | import android.view.ViewParent; 57 | import android.view.accessibility.AccessibilityEvent; 58 | import android.view.animation.Interpolator; 59 | import android.widget.Scroller; 60 | 61 | import java.lang.reflect.Method; 62 | import java.util.ArrayList; 63 | import java.util.Collections; 64 | import java.util.Comparator; 65 | 66 | /** 67 | * Layout manager that allows the user to flip left and right 68 | * through pages of data. You supply an implementation of a 69 | * {@link PagerAdapter} to generate the pages that the view shows. 70 | * 71 | *

Note this class is currently under early design and 72 | * development. The API will likely change in later updates of 73 | * the compatibility library, requiring changes to the source code 74 | * of apps when they are compiled against the newer version.

75 | * 76 | *

ViewPager is most often used in conjunction with {@link android.app.Fragment}, 77 | * which is a convenient way to supply and manage the lifecycle of each page. 78 | * There are standard adapters implemented for using fragments with the ViewPager, 79 | * which cover the most common use cases. These are 80 | * {@link android.support.v4.app.FragmentPagerAdapter} and 81 | * {@link android.support.v4.app.FragmentStatePagerAdapter}; each of these 82 | * classes have simple code showing how to build a full user interface 83 | * with them. 84 | * 85 | *

For more information about how to use ViewPager, read Creating Swipe Views with 87 | * Tabs.

88 | * 89 | *

Below is a more complicated example of ViewPager, using it in conjunction 90 | * with {@link android.app.ActionBar} tabs. You can find other examples of using 91 | * ViewPager in the API 4+ Support Demos and API 13+ Support Demos sample code. 92 | * 93 | * {@sample development/samples/Support13Demos/src/com/example/android/supportv13/app/ActionBarTabsPager.java 94 | * complete} 95 | */ 96 | public class CycleViewPager extends ViewGroup { 97 | private static final String TAG = "ViewPager"; 98 | private static final boolean DEBUG = false; 99 | 100 | private static final boolean USE_CACHE = false; 101 | 102 | private static final int DEFAULT_OFFSCREEN_PAGES = 1; 103 | private static final int MAX_SETTLE_DURATION = 600; // ms 104 | private static final int MIN_DISTANCE_FOR_FLING = 25; // dips 105 | 106 | private static final int DEFAULT_GUTTER_SIZE = 16; // dips 107 | 108 | private static final int MIN_FLING_VELOCITY = 400; // dips 109 | 110 | private static final int[] LAYOUT_ATTRS = new int[] { 111 | android.R.attr.layout_gravity 112 | }; 113 | 114 | /** 115 | * Used to track what the expected number of items in the adapter should be. 116 | * If the app changes this when we don't expect it, we'll throw a big obnoxious exception. 117 | */ 118 | private int mExpectedAdapterCount; 119 | 120 | static class ItemInfo { 121 | Object object; 122 | int position; 123 | boolean scrolling; 124 | float widthFactor; 125 | float offset; 126 | } 127 | 128 | private static final Comparator COMPARATOR = new Comparator(){ 129 | @Override 130 | public int compare(ItemInfo lhs, ItemInfo rhs) { 131 | return lhs.position - rhs.position; 132 | } 133 | }; 134 | 135 | private static final Interpolator sInterpolator = new Interpolator() { 136 | public float getInterpolation(float t) { 137 | t -= 1.0f; 138 | return t * t * t * t * t + 1.0f; 139 | } 140 | }; 141 | 142 | private final ArrayList mItems = new ArrayList(); 143 | private final ItemInfo mTempItem = new ItemInfo(); 144 | 145 | private final Rect mTempRect = new Rect(); 146 | 147 | private PagerAdapter mAdapter; 148 | private int mCurItem; // Index of currently displayed page. 149 | private int mRestoredCurItem = -1; 150 | private Parcelable mRestoredAdapterState = null; 151 | private ClassLoader mRestoredClassLoader = null; 152 | private Scroller mScroller; 153 | private PagerObserver mObserver; 154 | 155 | private int mPageMargin; 156 | private Drawable mMarginDrawable; 157 | private int mTopPageBounds; 158 | private int mBottomPageBounds; 159 | 160 | // Offsets of the first and last items, if known. 161 | // Set during population, used to determine if we are at the beginning 162 | // or end of the pager data set during touch scrolling. 163 | private float mFirstOffset = -Float.MAX_VALUE; 164 | private float mLastOffset = Float.MAX_VALUE; 165 | 166 | private int mChildWidthMeasureSpec; 167 | private int mChildHeightMeasureSpec; 168 | private boolean mInLayout; 169 | 170 | private boolean mScrollingCacheEnabled; 171 | 172 | private boolean mPopulatePending; 173 | private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES; 174 | 175 | private boolean mIsBeingDragged; 176 | private boolean mIsUnableToDrag; 177 | private int mDefaultGutterSize; 178 | private int mGutterSize; 179 | private int mTouchSlop; 180 | /** 181 | * Position of the last motion event. 182 | */ 183 | private float mLastMotionX; 184 | private float mLastMotionY; 185 | private float mInitialMotionX; 186 | private float mInitialMotionY; 187 | /** 188 | * ID of the active pointer. This is used to retain consistency during 189 | * drags/flings if multiple pointers are used. 190 | */ 191 | private int mActivePointerId = INVALID_POINTER; 192 | /** 193 | * Sentinel value for no current active pointer. 194 | * Used by {@link #mActivePointerId}. 195 | */ 196 | private static final int INVALID_POINTER = -1; 197 | 198 | /** 199 | * Determines speed during touch scrolling 200 | */ 201 | private VelocityTracker mVelocityTracker; 202 | private int mMinimumVelocity; 203 | private int mMaximumVelocity; 204 | private int mFlingDistance; 205 | private int mCloseEnough; 206 | 207 | // If the pager is at least this close to its final position, complete the scroll 208 | // on touch down and let the user interact with the content inside instead of 209 | // "catching" the flinging pager. 210 | private static final int CLOSE_ENOUGH = 2; // dp 211 | 212 | private boolean mFakeDragging; 213 | private long mFakeDragBeginTime; 214 | 215 | private EdgeEffectCompat mLeftEdge; 216 | private EdgeEffectCompat mRightEdge; 217 | 218 | private boolean mFirstLayout = true; 219 | private boolean mNeedCalculatePageOffsets = false; 220 | private boolean mCalledSuper; 221 | private int mDecorChildCount; 222 | 223 | private OnPageChangeListener mOnPageChangeListener; 224 | private OnPageChangeListener mInternalPageChangeListener; 225 | private OnAdapterChangeListener mAdapterChangeListener; 226 | private PageTransformer mPageTransformer; 227 | private Method mSetChildrenDrawingOrderEnabled; 228 | 229 | private static final int DRAW_ORDER_DEFAULT = 0; 230 | private static final int DRAW_ORDER_FORWARD = 1; 231 | private static final int DRAW_ORDER_REVERSE = 2; 232 | private int mDrawingOrder; 233 | private ArrayList mDrawingOrderedChildren; 234 | private static final ViewPositionComparator sPositionComparator = new ViewPositionComparator(); 235 | 236 | /** 237 | * Indicates that the pager is in an idle, settled state. The current page 238 | * is fully in view and no animation is in progress. 239 | */ 240 | public static final int SCROLL_STATE_IDLE = 0; 241 | 242 | /** 243 | * Indicates that the pager is currently being dragged by the user. 244 | */ 245 | public static final int SCROLL_STATE_DRAGGING = 1; 246 | 247 | /** 248 | * Indicates that the pager is in the process of settling to a final position. 249 | */ 250 | public static final int SCROLL_STATE_SETTLING = 2; 251 | 252 | private final Runnable mEndScrollRunnable = new Runnable() { 253 | public void run() { 254 | setScrollState(SCROLL_STATE_IDLE); 255 | populate(); 256 | } 257 | }; 258 | 259 | private int mScrollState = SCROLL_STATE_IDLE; 260 | 261 | /** 262 | * Callback interface for responding to changing state of the selected page. 263 | */ 264 | public interface OnPageChangeListener { 265 | 266 | /** 267 | * This method will be invoked when the current page is scrolled, either as part 268 | * of a programmatically initiated smooth scroll or a user initiated touch scroll. 269 | * 270 | * @param position Position index of the first page currently being displayed. 271 | * Page position+1 will be visible if positionOffset is nonzero. 272 | * @param positionOffset Value from [0, 1) indicating the offset from the page at position. 273 | * @param positionOffsetPixels Value in pixels indicating the offset from position. 274 | */ 275 | public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels); 276 | 277 | /** 278 | * This method will be invoked when a new page becomes selected. Animation is not 279 | * necessarily complete. 280 | * 281 | * @param position Position index of the new selected page. 282 | */ 283 | public void onPageSelected(int position); 284 | 285 | /** 286 | * Called when the scroll state changes. Useful for discovering when the user 287 | * begins dragging, when the pager is automatically settling to the current page, 288 | * or when it is fully stopped/idle. 289 | * 290 | * @param state The new scroll state. 291 | * @see CycleViewPager#SCROLL_STATE_IDLE 292 | * @see CycleViewPager#SCROLL_STATE_DRAGGING 293 | * @see CycleViewPager#SCROLL_STATE_SETTLING 294 | */ 295 | public void onPageScrollStateChanged(int state); 296 | } 297 | 298 | /** 299 | * Simple implementation of the {@link OnPageChangeListener} interface with stub 300 | * implementations of each method. Extend this if you do not intend to override 301 | * every method of {@link OnPageChangeListener}. 302 | */ 303 | public static class SimpleOnPageChangeListener implements OnPageChangeListener { 304 | @Override 305 | public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 306 | // This space for rent 307 | } 308 | 309 | @Override 310 | public void onPageSelected(int position) { 311 | // This space for rent 312 | } 313 | 314 | @Override 315 | public void onPageScrollStateChanged(int state) { 316 | // This space for rent 317 | } 318 | } 319 | 320 | /** 321 | * A PageTransformer is invoked whenever a visible/attached page is scrolled. 322 | * This offers an opportunity for the application to apply a custom transformation 323 | * to the page views using animation properties. 324 | * 325 | *

As property animation is only supported as of Android 3.0 and forward, 326 | * setting a PageTransformer on a ViewPager on earlier platform versions will 327 | * be ignored.

328 | */ 329 | public interface PageTransformer { 330 | /** 331 | * Apply a property transformation to the given page. 332 | * 333 | * @param page Apply the transformation to this page 334 | * @param position Position of page relative to the current front-and-center 335 | * position of the pager. 0 is front and center. 1 is one full 336 | * page position to the right, and -1 is one page position to the left. 337 | */ 338 | public void transformPage(View page, float position); 339 | } 340 | 341 | /** 342 | * Used internally to monitor when adapters are switched. 343 | */ 344 | interface OnAdapterChangeListener { 345 | public void onAdapterChanged(PagerAdapter oldAdapter, PagerAdapter newAdapter); 346 | } 347 | 348 | /** 349 | * Used internally to tag special types of child views that should be added as 350 | * pager decorations by default. 351 | */ 352 | interface Decor {} 353 | 354 | public CycleViewPager(Context context) { 355 | super(context); 356 | initViewPager(); 357 | } 358 | 359 | public CycleViewPager(Context context, AttributeSet attrs) { 360 | super(context, attrs); 361 | initViewPager(); 362 | } 363 | 364 | void initViewPager() { 365 | setWillNotDraw(false); 366 | setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); 367 | setFocusable(true); 368 | final Context context = getContext(); 369 | mScroller = new Scroller(context, sInterpolator); 370 | final ViewConfiguration configuration = ViewConfiguration.get(context); 371 | final float density = context.getResources().getDisplayMetrics().density; 372 | 373 | mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration); 374 | mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density); 375 | mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 376 | mLeftEdge = new EdgeEffectCompat(context); 377 | mRightEdge = new EdgeEffectCompat(context); 378 | 379 | mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density); 380 | mCloseEnough = (int) (CLOSE_ENOUGH * density); 381 | mDefaultGutterSize = (int) (DEFAULT_GUTTER_SIZE * density); 382 | 383 | ViewCompat.setAccessibilityDelegate(this, new MyAccessibilityDelegate()); 384 | 385 | if (ViewCompat.getImportantForAccessibility(this) 386 | == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { 387 | ViewCompat.setImportantForAccessibility(this, 388 | ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); 389 | } 390 | } 391 | 392 | @Override 393 | protected void onDetachedFromWindow() { 394 | removeCallbacks(mEndScrollRunnable); 395 | super.onDetachedFromWindow(); 396 | } 397 | 398 | private void setScrollState(int newState) { 399 | if (mScrollState == newState) { 400 | return; 401 | } 402 | 403 | mScrollState = newState; 404 | if (mPageTransformer != null) { 405 | // PageTransformers can do complex things that benefit from hardware layers. 406 | enableLayers(newState != SCROLL_STATE_IDLE); 407 | } 408 | if (mOnPageChangeListener != null) { 409 | mOnPageChangeListener.onPageScrollStateChanged(newState); 410 | } 411 | } 412 | 413 | /** 414 | * Set a PagerAdapter that will supply views for this pager as needed. 415 | * 416 | * @param adapter Adapter to use 417 | */ 418 | public void setAdapter(PagerAdapter adapter) { 419 | if (mAdapter != null) { 420 | mAdapter.unregisterDataSetObserver(mObserver); 421 | mAdapter.startUpdate(this); 422 | for (int i = 0; i < mItems.size(); i++) { 423 | final ItemInfo ii = mItems.get(i); 424 | mAdapter.destroyItem(this, ii.position, ii.object); 425 | } 426 | mAdapter.finishUpdate(this); 427 | mItems.clear(); 428 | removeNonDecorViews(); 429 | mCurItem = 0; 430 | scrollTo(0, 0); 431 | } 432 | 433 | final PagerAdapter oldAdapter = mAdapter; 434 | mAdapter = adapter; 435 | mExpectedAdapterCount = 0; 436 | 437 | if (mAdapter != null) { 438 | if (mObserver == null) { 439 | mObserver = new PagerObserver(); 440 | } 441 | mAdapter.registerDataSetObserver(mObserver); 442 | mPopulatePending = false; 443 | final boolean wasFirstLayout = mFirstLayout; 444 | mFirstLayout = true; 445 | mExpectedAdapterCount = mAdapter.getCount(); 446 | if (mRestoredCurItem >= 0) { 447 | mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader); 448 | setCurrentItemInternal(mRestoredCurItem, false, true); 449 | mRestoredCurItem = -1; 450 | mRestoredAdapterState = null; 451 | mRestoredClassLoader = null; 452 | } else if (!wasFirstLayout) { 453 | populate(); 454 | } else { 455 | requestLayout(); 456 | } 457 | } 458 | 459 | if (mAdapterChangeListener != null && oldAdapter != adapter) { 460 | mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter); 461 | } 462 | } 463 | 464 | private void removeNonDecorViews() { 465 | for (int i = 0; i < getChildCount(); i++) { 466 | final View child = getChildAt(i); 467 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 468 | if (!lp.isDecor) { 469 | removeViewAt(i); 470 | i--; 471 | } 472 | } 473 | } 474 | 475 | /** 476 | * Retrieve the current adapter supplying pages. 477 | * 478 | * @return The currently registered PagerAdapter 479 | */ 480 | public PagerAdapter getAdapter() { 481 | return mAdapter; 482 | } 483 | 484 | void setOnAdapterChangeListener(OnAdapterChangeListener listener) { 485 | mAdapterChangeListener = listener; 486 | } 487 | 488 | private int getClientWidth() { 489 | return getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); 490 | } 491 | 492 | /** 493 | * Set the currently selected page. If the ViewPager has already been through its first 494 | * layout with its current adapter there will be a smooth animated transition between 495 | * the current item and the specified item. 496 | * 497 | * @param item Item index to select 498 | */ 499 | public void setCurrentItem(int item) { 500 | mPopulatePending = false; 501 | setCurrentItemInternal(item, !mFirstLayout, false); 502 | } 503 | 504 | /** 505 | * Set the currently selected page. 506 | * 507 | * @param item Item index to select 508 | * @param smoothScroll True to smoothly scroll to the new item, false to transition immediately 509 | */ 510 | public void setCurrentItem(int item, boolean smoothScroll) { 511 | mPopulatePending = false; 512 | setCurrentItemInternal(item, smoothScroll, false); 513 | } 514 | 515 | public int getCurrentItem() { 516 | return mCurItem; 517 | } 518 | 519 | void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) { 520 | setCurrentItemInternal(item, smoothScroll, always, 0); 521 | } 522 | 523 | void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) { 524 | if (mAdapter == null || mAdapter.getCount() <= 0) { 525 | setScrollingCacheEnabled(false); 526 | return; 527 | } 528 | if (!always && mCurItem == item && mItems.size() != 0) { 529 | setScrollingCacheEnabled(false); 530 | return; 531 | } 532 | 533 | final int pageLimit = mOffscreenPageLimit; 534 | if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) { 535 | // We are doing a jump by more than one page. To avoid 536 | // glitches, we want to keep all current pages in the view 537 | // until the scroll ends. 538 | for (int i=0; i= 0) { 573 | newItem = item % mAdapter.getCount(); 574 | }else{ 575 | if((item % mAdapter.getCount()) != 0) 576 | newItem = mAdapter.getCount() - (-item)%mAdapter.getCount(); 577 | else 578 | newItem = 0; 579 | } 580 | 581 | if (smoothScroll) { 582 | smoothScrollTo(destX, 0, velocity); 583 | if (dispatchSelected && mOnPageChangeListener != null) { 584 | mOnPageChangeListener.onPageSelected(newItem); 585 | } 586 | if (dispatchSelected && mInternalPageChangeListener != null) { 587 | mInternalPageChangeListener.onPageSelected(newItem); 588 | } 589 | } else { 590 | if (dispatchSelected && mOnPageChangeListener != null) { 591 | mOnPageChangeListener.onPageSelected(newItem); 592 | } 593 | if (dispatchSelected && mInternalPageChangeListener != null) { 594 | mInternalPageChangeListener.onPageSelected(newItem); 595 | } 596 | completeScroll(false); 597 | scrollTo(destX, 0); 598 | pageScrolled(destX); 599 | } 600 | } 601 | 602 | /** 603 | * Set a listener that will be invoked whenever the page changes or is incrementally 604 | * scrolled. See {@link OnPageChangeListener}. 605 | * 606 | * @param listener Listener to set 607 | */ 608 | public void setOnPageChangeListener(OnPageChangeListener listener) { 609 | mOnPageChangeListener = listener; 610 | } 611 | 612 | /** 613 | * Set a {@link PageTransformer} that will be called for each attached page whenever 614 | * the scroll position is changed. This allows the application to apply custom property 615 | * transformations to each page, overriding the default sliding look and feel. 616 | * 617 | *

Note: Prior to Android 3.0 the property animation APIs did not exist. 618 | * As a result, setting a PageTransformer prior to Android 3.0 (API 11) will have no effect.

619 | * 620 | * @param reverseDrawingOrder true if the supplied PageTransformer requires page views 621 | * to be drawn from last to first instead of first to last. 622 | * @param transformer PageTransformer that will modify each page's animation properties 623 | */ 624 | public void setPageTransformer(boolean reverseDrawingOrder, PageTransformer transformer) { 625 | if (Build.VERSION.SDK_INT >= 11) { 626 | final boolean hasTransformer = transformer != null; 627 | final boolean needsPopulate = hasTransformer != (mPageTransformer != null); 628 | mPageTransformer = transformer; 629 | setChildrenDrawingOrderEnabledCompat(hasTransformer); 630 | if (hasTransformer) { 631 | mDrawingOrder = reverseDrawingOrder ? DRAW_ORDER_REVERSE : DRAW_ORDER_FORWARD; 632 | } else { 633 | mDrawingOrder = DRAW_ORDER_DEFAULT; 634 | } 635 | if (needsPopulate) populate(); 636 | } 637 | } 638 | 639 | void setChildrenDrawingOrderEnabledCompat(boolean enable) { 640 | if (Build.VERSION.SDK_INT >= 7) { 641 | if (mSetChildrenDrawingOrderEnabled == null) { 642 | try { 643 | mSetChildrenDrawingOrderEnabled = ViewGroup.class.getDeclaredMethod( 644 | "setChildrenDrawingOrderEnabled", new Class[] { Boolean.TYPE }); 645 | } catch (NoSuchMethodException e) { 646 | Log.e(TAG, "Can't find setChildrenDrawingOrderEnabled", e); 647 | } 648 | } 649 | try { 650 | mSetChildrenDrawingOrderEnabled.invoke(this, enable); 651 | } catch (Exception e) { 652 | Log.e(TAG, "Error changing children drawing order", e); 653 | } 654 | } 655 | } 656 | 657 | @Override 658 | protected int getChildDrawingOrder(int childCount, int i) { 659 | final int index = mDrawingOrder == DRAW_ORDER_REVERSE ? childCount - 1 - i : i; 660 | final int result = ((LayoutParams) mDrawingOrderedChildren.get(index).getLayoutParams()).childIndex; 661 | return result; 662 | } 663 | 664 | /** 665 | * Set a separate OnPageChangeListener for internal use by the support library. 666 | * 667 | * @param listener Listener to set 668 | * @return The old listener that was set, if any. 669 | */ 670 | OnPageChangeListener setInternalPageChangeListener(OnPageChangeListener listener) { 671 | OnPageChangeListener oldListener = mInternalPageChangeListener; 672 | mInternalPageChangeListener = listener; 673 | return oldListener; 674 | } 675 | 676 | /** 677 | * Returns the number of pages that will be retained to either side of the 678 | * current page in the view hierarchy in an idle state. Defaults to 1. 679 | * 680 | * @return How many pages will be kept offscreen on either side 681 | * @see #setOffscreenPageLimit(int) 682 | */ 683 | public int getOffscreenPageLimit() { 684 | return mOffscreenPageLimit; 685 | } 686 | 687 | /** 688 | * Set the number of pages that should be retained to either side of the 689 | * current page in the view hierarchy in an idle state. Pages beyond this 690 | * limit will be recreated from the adapter when needed. 691 | * 692 | *

This is offered as an optimization. If you know in advance the number 693 | * of pages you will need to support or have lazy-loading mechanisms in place 694 | * on your pages, tweaking this setting can have benefits in perceived smoothness 695 | * of paging animations and interaction. If you have a small number of pages (3-4) 696 | * that you can keep active all at once, less time will be spent in layout for 697 | * newly created view subtrees as the user pages back and forth.

698 | * 699 | *

You should keep this limit low, especially if your pages have complex layouts. 700 | * This setting defaults to 1.

701 | * 702 | * @param limit How many pages will be kept offscreen in an idle state. 703 | */ 704 | public void setOffscreenPageLimit(int limit) { 705 | if (limit < DEFAULT_OFFSCREEN_PAGES) { 706 | Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " + 707 | DEFAULT_OFFSCREEN_PAGES); 708 | limit = DEFAULT_OFFSCREEN_PAGES; 709 | } 710 | if (limit != mOffscreenPageLimit) { 711 | mOffscreenPageLimit = limit; 712 | populate(); 713 | } 714 | } 715 | 716 | /** 717 | * Set the margin between pages. 718 | * 719 | * @param marginPixels Distance between adjacent pages in pixels 720 | * @see #getPageMargin() 721 | * @see #setPageMarginDrawable(android.graphics.drawable.Drawable) 722 | * @see #setPageMarginDrawable(int) 723 | */ 724 | public void setPageMargin(int marginPixels) { 725 | final int oldMargin = mPageMargin; 726 | mPageMargin = marginPixels; 727 | 728 | final int width = getWidth(); 729 | recomputeScrollPosition(width, width, marginPixels, oldMargin); 730 | 731 | requestLayout(); 732 | } 733 | 734 | /** 735 | * Return the margin between pages. 736 | * 737 | * @return The size of the margin in pixels 738 | */ 739 | public int getPageMargin() { 740 | return mPageMargin; 741 | } 742 | 743 | /** 744 | * Set a drawable that will be used to fill the margin between pages. 745 | * 746 | * @param d Drawable to display between pages 747 | */ 748 | public void setPageMarginDrawable(Drawable d) { 749 | mMarginDrawable = d; 750 | if (d != null) refreshDrawableState(); 751 | setWillNotDraw(d == null); 752 | invalidate(); 753 | } 754 | 755 | /** 756 | * Set a drawable that will be used to fill the margin between pages. 757 | * 758 | * @param resId Resource ID of a drawable to display between pages 759 | */ 760 | public void setPageMarginDrawable(@DrawableRes int resId) { 761 | setPageMarginDrawable(getContext().getResources().getDrawable(resId)); 762 | } 763 | 764 | @Override 765 | protected boolean verifyDrawable(Drawable who) { 766 | return super.verifyDrawable(who) || who == mMarginDrawable; 767 | } 768 | 769 | @Override 770 | protected void drawableStateChanged() { 771 | super.drawableStateChanged(); 772 | final Drawable d = mMarginDrawable; 773 | if (d != null && d.isStateful()) { 774 | d.setState(getDrawableState()); 775 | } 776 | } 777 | 778 | // We want the duration of the page snap animation to be influenced by the distance that 779 | // the screen has to travel, however, we don't want this duration to be effected in a 780 | // purely linear fashion. Instead, we use this method to moderate the effect that the distance 781 | // of travel has on the overall snap duration. 782 | float distanceInfluenceForSnapDuration(float f) { 783 | f -= 0.5f; // center the values about 0. 784 | f *= 0.3f * Math.PI / 2.0f; 785 | return (float) Math.sin(f); 786 | } 787 | 788 | /** 789 | * Like {@link android.view.View#scrollBy}, but scroll smoothly instead of immediately. 790 | * 791 | * @param x the number of pixels to scroll by on the X axis 792 | * @param y the number of pixels to scroll by on the Y axis 793 | */ 794 | void smoothScrollTo(int x, int y) { 795 | smoothScrollTo(x, y, 0); 796 | } 797 | 798 | /** 799 | * Like {@link android.view.View#scrollBy}, but scroll smoothly instead of immediately. 800 | * 801 | * @param x the number of pixels to scroll by on the X axis 802 | * @param y the number of pixels to scroll by on the Y axis 803 | * @param velocity the velocity associated with a fling, if applicable. (0 otherwise) 804 | */ 805 | void smoothScrollTo(int x, int y, int velocity) { 806 | if (getChildCount() == 0) { 807 | // Nothing to do. 808 | setScrollingCacheEnabled(false); 809 | return; 810 | } 811 | int sx = getScrollX(); 812 | int sy = getScrollY(); 813 | int dx = x - sx; 814 | int dy = y - sy; 815 | if (dx == 0 && dy == 0) { 816 | completeScroll(false); 817 | populate(); 818 | setScrollState(SCROLL_STATE_IDLE); 819 | return; 820 | } 821 | 822 | setScrollingCacheEnabled(true); 823 | setScrollState(SCROLL_STATE_SETTLING); 824 | 825 | final int width = getClientWidth(); 826 | final int halfWidth = width / 2; 827 | final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width); 828 | final float distance = halfWidth + halfWidth * 829 | distanceInfluenceForSnapDuration(distanceRatio); 830 | 831 | int duration = 0; 832 | velocity = Math.abs(velocity); 833 | if (velocity > 0) { 834 | duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); 835 | } else { 836 | final float pageWidth = width * mAdapter.getPageWidth(mCurItem); 837 | final float pageDelta = (float) Math.abs(dx) / (pageWidth + mPageMargin); 838 | duration = (int) ((pageDelta + 1) * 100); 839 | } 840 | duration = Math.min(duration, MAX_SETTLE_DURATION); 841 | 842 | mScroller.startScroll(sx, sy, dx, dy, duration); 843 | ViewCompat.postInvalidateOnAnimation(this); 844 | } 845 | 846 | ItemInfo addNewItem(int position, int index) { 847 | ItemInfo ii = new ItemInfo(); 848 | ii.position = position; 849 | if(position >=0) 850 | ii.object = mAdapter.instantiateItem(this, position % mAdapter.getCount()); 851 | else { 852 | int pos; 853 | if((position % mAdapter.getCount()) != 0) 854 | pos = mAdapter.getCount() - (-position)%mAdapter.getCount(); 855 | else 856 | pos = 0; 857 | ii.object = mAdapter.instantiateItem(this, pos); 858 | } 859 | 860 | ii.widthFactor = mAdapter.getPageWidth(position); 861 | if (index < 0 || index >= mItems.size()) { 862 | mItems.add(ii); 863 | } else { 864 | mItems.add(index, ii); 865 | } 866 | return ii; 867 | } 868 | 869 | void dataSetChanged() { 870 | // This method only gets called if our observer is attached, so mAdapter is non-null. 871 | 872 | final int adapterCount = mAdapter.getCount(); 873 | mExpectedAdapterCount = adapterCount; 874 | boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 && 875 | mItems.size() < adapterCount; 876 | int newCurrItem = mCurItem; 877 | 878 | boolean isUpdating = false; 879 | for (int i = 0; i < mItems.size(); i++) { 880 | final ItemInfo ii = mItems.get(i); 881 | final int newPos = mAdapter.getItemPosition(ii.object); 882 | 883 | if (newPos == PagerAdapter.POSITION_UNCHANGED) { 884 | continue; 885 | } 886 | 887 | if (newPos == PagerAdapter.POSITION_NONE) { 888 | mItems.remove(i); 889 | i--; 890 | 891 | if (!isUpdating) { 892 | mAdapter.startUpdate(this); 893 | isUpdating = true; 894 | } 895 | 896 | mAdapter.destroyItem(this, ii.position, ii.object); 897 | needPopulate = true; 898 | 899 | if (mCurItem == ii.position) { 900 | // Keep the current item in the valid range 901 | newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1)); 902 | needPopulate = true; 903 | } 904 | continue; 905 | } 906 | 907 | if (ii.position != newPos) { 908 | if (ii.position == mCurItem) { 909 | // Our current item changed position. Follow it. 910 | newCurrItem = newPos; 911 | } 912 | 913 | ii.position = newPos; 914 | needPopulate = true; 915 | } 916 | } 917 | 918 | if (isUpdating) { 919 | mAdapter.finishUpdate(this); 920 | } 921 | 922 | Collections.sort(mItems, COMPARATOR); 923 | 924 | if (needPopulate) { 925 | // Reset our known page widths; populate will recompute them. 926 | final int childCount = getChildCount(); 927 | for (int i = 0; i < childCount; i++) { 928 | final View child = getChildAt(i); 929 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 930 | if (!lp.isDecor) { 931 | lp.widthFactor = 0.f; 932 | } 933 | } 934 | 935 | setCurrentItemInternal(newCurrItem, false, true); 936 | requestLayout(); 937 | } 938 | } 939 | 940 | void populate() { 941 | populate(mCurItem); 942 | } 943 | 944 | void populate(int newCurrentItem) { 945 | ItemInfo oldCurInfo = null; 946 | int focusDirection = View.FOCUS_FORWARD; 947 | if (mCurItem != newCurrentItem) { 948 | focusDirection = mCurItem < newCurrentItem ? View.FOCUS_RIGHT : View.FOCUS_LEFT; 949 | oldCurInfo = infoForPosition(mCurItem); 950 | mCurItem = newCurrentItem; 951 | } 952 | 953 | if (mAdapter == null) { 954 | sortChildDrawingOrder(); 955 | return; 956 | } 957 | 958 | // Bail now if we are waiting to populate. This is to hold off 959 | // on creating views from the time the user releases their finger to 960 | // fling to a new position until we have finished the scroll to 961 | // that position, avoiding glitches from happening at that point. 962 | if (mPopulatePending) { 963 | if (DEBUG) Log.i(TAG, "populate is pending, skipping for now..."); 964 | sortChildDrawingOrder(); 965 | return; 966 | } 967 | 968 | // Also, don't populate until we are attached to a window. This is to 969 | // avoid trying to populate before we have restored our view hierarchy 970 | // state and conflicting with what is restored. 971 | if (getWindowToken() == null) { 972 | return; 973 | } 974 | 975 | mAdapter.startUpdate(this); 976 | 977 | final int pageLimit = mOffscreenPageLimit; 978 | final int startPos = Math.max(0, mCurItem - pageLimit); 979 | final int N = mAdapter.getCount(); 980 | final int endPos = Math.min(N-1, mCurItem + pageLimit); 981 | 982 | if (N != mExpectedAdapterCount) { 983 | String resName; 984 | try { 985 | resName = getResources().getResourceName(getId()); 986 | } catch (Resources.NotFoundException e) { 987 | resName = Integer.toHexString(getId()); 988 | } 989 | throw new IllegalStateException("The application's PagerAdapter changed the adapter's" + 990 | " contents without calling PagerAdapter#notifyDataSetChanged!" + 991 | " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N + 992 | " Pager id: " + resName + 993 | " Pager class: " + getClass() + 994 | " Problematic adapter: " + mAdapter.getClass()); 995 | } 996 | 997 | // Locate the currently focused item or add it if needed. 998 | int curIndex = -1; 999 | ItemInfo curItem = null; 1000 | for (curIndex = 0; curIndex < mItems.size(); curIndex++) { 1001 | final ItemInfo ii = mItems.get(curIndex); 1002 | if (ii.position >= mCurItem) { 1003 | if (ii.position == mCurItem) curItem = ii; 1004 | break; 1005 | } 1006 | } 1007 | 1008 | if (curItem == null && N > 0) { 1009 | curItem = addNewItem(mCurItem, curIndex); 1010 | } 1011 | 1012 | // Fill 3x the available width or up to the number of offscreen 1013 | // pages requested to either side, whichever is larger. 1014 | // If we have no current item we have no work to do. 1015 | if (curItem != null) { 1016 | float extraWidthLeft = 0.f; 1017 | int itemIndex = curIndex - 1; 1018 | ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 1019 | final int clientWidth = getClientWidth(); 1020 | final float leftWidthNeeded = clientWidth <= 0 ? 0 : 1021 | 2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth; 1022 | 1023 | final int nl = mCurItem - 2; 1024 | for (int pos = mCurItem - 1; pos >= 0 || pos >= nl; pos--) { 1025 | if (extraWidthLeft >= leftWidthNeeded && pos < startPos) { 1026 | if (ii == null) { 1027 | break; 1028 | } 1029 | if (pos == ii.position && !ii.scrolling) { 1030 | mItems.remove(itemIndex); 1031 | 1032 | if(pos >= 0) { 1033 | mAdapter.destroyItem(this, pos % N, ii.object); 1034 | }else{ 1035 | int pPos; 1036 | if((pos % mAdapter.getCount()) != 0) 1037 | pPos = mAdapter.getCount() - (-pos)%mAdapter.getCount(); 1038 | else 1039 | pPos = 0; 1040 | mAdapter.destroyItem(this, pPos, ii.object); 1041 | } 1042 | 1043 | if (DEBUG) { 1044 | Log.i(TAG, "populate() - destroyItem() with pos: " + pos + 1045 | " view: " + ((View) ii.object)); 1046 | } 1047 | itemIndex--; 1048 | curIndex--; 1049 | ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 1050 | } 1051 | } else if (ii != null && pos == ii.position) { 1052 | extraWidthLeft += ii.widthFactor; 1053 | itemIndex--; 1054 | ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 1055 | if(itemIndex < 0){ 1056 | int id; 1057 | if((itemIndex % mItems.size()) != 0) 1058 | id = mItems.size() - (-itemIndex)%mItems.size(); 1059 | else 1060 | id = 0; 1061 | ii = mItems.get(id); 1062 | } 1063 | } else { 1064 | //检查要准备加载出来的这一项是否已经被加载,如果是则要先销毁 1065 | int tempPos; 1066 | if(pos >= 0) { 1067 | tempPos = pos % N; 1068 | }else{ 1069 | if((pos % mAdapter.getCount()) != 0) 1070 | tempPos = mAdapter.getCount() - (-pos)%mAdapter.getCount(); 1071 | else 1072 | tempPos = 0; 1073 | } 1074 | for(int j=0; j= 0) { 1078 | tempItemPosition = itemPosition % N; 1079 | }else{ 1080 | if((itemPosition % mAdapter.getCount()) != 0) 1081 | tempItemPosition = mAdapter.getCount() - (-itemPosition)%mAdapter.getCount(); 1082 | else 1083 | tempItemPosition = 0; 1084 | } 1085 | if(tempPos == tempItemPosition){ 1086 | mAdapter.destroyItem(this, tempPos, mItems.get(j)); 1087 | mItems.remove(j); 1088 | } 1089 | } 1090 | ii = addNewItem(pos, itemIndex + 1); 1091 | extraWidthLeft += ii.widthFactor; 1092 | curIndex++; 1093 | ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 1094 | } 1095 | } 1096 | 1097 | float extraWidthRight = curItem.widthFactor; 1098 | itemIndex = curIndex + 1; 1099 | if (extraWidthRight < 2.f) { 1100 | ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 1101 | final float rightWidthNeeded = clientWidth <= 0 ? 0 : 1102 | (float) getPaddingRight() / (float) clientWidth + 2.f; 1103 | final int n = mCurItem + 2; 1104 | for (int pos = mCurItem + 1; pos < N || pos <= n; pos++) { 1105 | if (extraWidthRight >= rightWidthNeeded && pos > endPos) { 1106 | if (ii == null) { 1107 | break; 1108 | } 1109 | if (pos == ii.position && !ii.scrolling) { 1110 | mItems.remove(itemIndex); 1111 | if(pos >=0) 1112 | mAdapter.destroyItem(this, pos % N, ii.object); 1113 | else { 1114 | int pPos; 1115 | if((pos % mAdapter.getCount()) != 0) 1116 | pPos = mAdapter.getCount() - (-pos)%mAdapter.getCount(); 1117 | else 1118 | pPos = 0; 1119 | mAdapter.destroyItem(this, pPos, ii.object); 1120 | } 1121 | 1122 | if (DEBUG) { 1123 | Log.i(TAG, "populate() - destroyItem() with pos: " + pos + 1124 | " view: " + ((View) ii.object)); 1125 | } 1126 | ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 1127 | } 1128 | } else if (ii != null && pos == ii.position) { 1129 | extraWidthRight += ii.widthFactor; 1130 | itemIndex++; 1131 | ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 1132 | } else { 1133 | ii = addNewItem(pos, itemIndex); 1134 | itemIndex++; 1135 | extraWidthRight += ii.widthFactor; 1136 | ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 1137 | } 1138 | } 1139 | } 1140 | 1141 | calculatePageOffsets(curItem, curIndex, oldCurInfo); 1142 | } 1143 | 1144 | if (DEBUG) { 1145 | Log.i(TAG, "Current page list:"); 1146 | for (int i=0; i(); 1194 | } else { 1195 | mDrawingOrderedChildren.clear(); 1196 | } 1197 | final int childCount = getChildCount(); 1198 | for (int i = 0; i < childCount; i++) { 1199 | final View child = getChildAt(i); 1200 | mDrawingOrderedChildren.add(child); 1201 | } 1202 | Collections.sort(mDrawingOrderedChildren, sPositionComparator); 1203 | } 1204 | } 1205 | 1206 | private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) { 1207 | final int N = mAdapter.getCount(); 1208 | final int width = getClientWidth(); 1209 | final float marginOffset = width > 0 ? (float) mPageMargin / width : 0; 1210 | // Fix up offsets for later layout. 1211 | if (oldCurInfo != null) { 1212 | final int oldCurPosition = oldCurInfo.position; 1213 | // Base offsets off of oldCurInfo. 1214 | if (oldCurPosition < curItem.position) { 1215 | int itemIndex = 0; 1216 | ItemInfo ii = null; 1217 | float offset = oldCurInfo.offset + oldCurInfo.widthFactor + marginOffset; 1218 | for (int pos = oldCurPosition + 1; 1219 | pos <= curItem.position && itemIndex < mItems.size(); pos++) { 1220 | ii = mItems.get(itemIndex); 1221 | while (pos > ii.position && itemIndex < mItems.size() - 1) { 1222 | itemIndex++; 1223 | ii = mItems.get(itemIndex); 1224 | } 1225 | while (pos < ii.position) { 1226 | // We don't have an item populated for this, 1227 | // ask the adapter for an offset. 1228 | offset += mAdapter.getPageWidth(pos) + marginOffset; 1229 | pos++; 1230 | } 1231 | ii.offset = offset; 1232 | offset += ii.widthFactor + marginOffset; 1233 | } 1234 | } else if (oldCurPosition > curItem.position) { 1235 | int itemIndex = mItems.size() - 1; 1236 | ItemInfo ii = null; 1237 | float offset = oldCurInfo.offset; 1238 | for (int pos = oldCurPosition - 1; 1239 | pos >= curItem.position && itemIndex >= 0; pos--) { 1240 | ii = mItems.get(itemIndex); 1241 | while (pos < ii.position && itemIndex > 0) { 1242 | itemIndex--; 1243 | ii = mItems.get(itemIndex); 1244 | } 1245 | while (pos > ii.position) { 1246 | // We don't have an item populated for this, 1247 | // ask the adapter for an offset. 1248 | offset -= mAdapter.getPageWidth(pos) + marginOffset; 1249 | pos--; 1250 | } 1251 | offset -= ii.widthFactor + marginOffset; 1252 | ii.offset = offset; 1253 | } 1254 | } 1255 | } 1256 | 1257 | // Base all offsets off of curItem. 1258 | final int itemCount = mItems.size(); 1259 | float offset = curItem.offset; 1260 | int pos = curItem.position - 1; 1261 | 1262 | // Previous pages 1263 | for (int i = curIndex - 1; i >= 0; i--, pos--) { 1264 | final ItemInfo ii = mItems.get(i); 1265 | while (pos > ii.position) { 1266 | offset -= mAdapter.getPageWidth(pos--) + marginOffset; 1267 | } 1268 | offset -= ii.widthFactor + marginOffset; 1269 | ii.offset = offset; 1270 | } 1271 | offset = curItem.offset + curItem.widthFactor + marginOffset; 1272 | pos = curItem.position + 1; 1273 | // Next pages 1274 | for (int i = curIndex + 1; i < itemCount; i++, pos++) { 1275 | final ItemInfo ii = mItems.get(i); 1276 | while (pos < ii.position) { 1277 | offset += mAdapter.getPageWidth(pos++) + marginOffset; 1278 | } 1279 | 1280 | ii.offset = offset; 1281 | offset += ii.widthFactor + marginOffset; 1282 | } 1283 | 1284 | mNeedCalculatePageOffsets = false; 1285 | } 1286 | 1287 | /** 1288 | * This is the persistent state that is saved by ViewPager. Only needed 1289 | * if you are creating a sublass of ViewPager that must save its own 1290 | * state, in which case it should implement a subclass of this which 1291 | * contains that state. 1292 | */ 1293 | public static class SavedState extends BaseSavedState { 1294 | int position; 1295 | Parcelable adapterState; 1296 | ClassLoader loader; 1297 | 1298 | public SavedState(Parcelable superState) { 1299 | super(superState); 1300 | } 1301 | 1302 | @Override 1303 | public void writeToParcel(Parcel out, int flags) { 1304 | super.writeToParcel(out, flags); 1305 | out.writeInt(position); 1306 | out.writeParcelable(adapterState, flags); 1307 | } 1308 | 1309 | @Override 1310 | public String toString() { 1311 | return "FragmentPager.SavedState{" 1312 | + Integer.toHexString(System.identityHashCode(this)) 1313 | + " position=" + position + "}"; 1314 | } 1315 | 1316 | public static final Creator CREATOR 1317 | = ParcelableCompat.newCreator(new ParcelableCompatCreatorCallbacks() { 1318 | @Override 1319 | public SavedState createFromParcel(Parcel in, ClassLoader loader) { 1320 | return new SavedState(in, loader); 1321 | } 1322 | 1323 | @Override 1324 | public SavedState[] newArray(int size) { 1325 | return new SavedState[size]; 1326 | } 1327 | }); 1328 | 1329 | SavedState(Parcel in, ClassLoader loader) { 1330 | super(in); 1331 | if (loader == null) { 1332 | loader = getClass().getClassLoader(); 1333 | } 1334 | position = in.readInt(); 1335 | adapterState = in.readParcelable(loader); 1336 | this.loader = loader; 1337 | } 1338 | } 1339 | 1340 | @Override 1341 | public Parcelable onSaveInstanceState() { 1342 | Parcelable superState = super.onSaveInstanceState(); 1343 | SavedState ss = new SavedState(superState); 1344 | ss.position = mCurItem; 1345 | if (mAdapter != null) { 1346 | ss.adapterState = mAdapter.saveState(); 1347 | } 1348 | return ss; 1349 | } 1350 | 1351 | @Override 1352 | public void onRestoreInstanceState(Parcelable state) { 1353 | if (!(state instanceof SavedState)) { 1354 | super.onRestoreInstanceState(state); 1355 | return; 1356 | } 1357 | 1358 | SavedState ss = (SavedState)state; 1359 | super.onRestoreInstanceState(ss.getSuperState()); 1360 | 1361 | if (mAdapter != null) { 1362 | mAdapter.restoreState(ss.adapterState, ss.loader); 1363 | setCurrentItemInternal(ss.position, false, true); 1364 | } else { 1365 | mRestoredCurItem = ss.position; 1366 | mRestoredAdapterState = ss.adapterState; 1367 | mRestoredClassLoader = ss.loader; 1368 | } 1369 | } 1370 | 1371 | @Override 1372 | public void addView(View child, int index, ViewGroup.LayoutParams params) { 1373 | if (!checkLayoutParams(params)) { 1374 | params = generateLayoutParams(params); 1375 | } 1376 | final LayoutParams lp = (LayoutParams) params; 1377 | lp.isDecor |= child instanceof Decor; 1378 | if (mInLayout) { 1379 | if (lp != null && lp.isDecor) { 1380 | throw new IllegalStateException("Cannot add pager decor view during layout"); 1381 | } 1382 | lp.needsMeasure = true; 1383 | addViewInLayout(child, index, params); 1384 | } else { 1385 | super.addView(child, index, params); 1386 | } 1387 | 1388 | if (USE_CACHE) { 1389 | if (child.getVisibility() != GONE) { 1390 | child.setDrawingCacheEnabled(mScrollingCacheEnabled); 1391 | } else { 1392 | child.setDrawingCacheEnabled(false); 1393 | } 1394 | } 1395 | } 1396 | 1397 | @Override 1398 | public void removeView(View view) { 1399 | if (mInLayout) { 1400 | removeViewInLayout(view); 1401 | } else { 1402 | super.removeView(view); 1403 | } 1404 | } 1405 | 1406 | ItemInfo infoForChild(View child) { 1407 | for (int i=0; i 0 && !mItems.isEmpty()) { 1550 | final int widthWithMargin = width - getPaddingLeft() - getPaddingRight() + margin; 1551 | final int oldWidthWithMargin = oldWidth - getPaddingLeft() - getPaddingRight() 1552 | + oldMargin; 1553 | final int xpos = getScrollX(); 1554 | final float pageOffset = (float) xpos / oldWidthWithMargin; 1555 | final int newOffsetPixels = (int) (pageOffset * widthWithMargin); 1556 | 1557 | scrollTo(newOffsetPixels, getScrollY()); 1558 | if (!mScroller.isFinished()) { 1559 | // We now return to your regularly scheduled scroll, already in progress. 1560 | final int newDuration = mScroller.getDuration() - mScroller.timePassed(); 1561 | ItemInfo targetInfo = infoForPosition(mCurItem); 1562 | mScroller.startScroll(newOffsetPixels, 0, 1563 | (int) (targetInfo.offset * width), 0, newDuration); 1564 | } 1565 | } else { 1566 | final ItemInfo ii = infoForPosition(mCurItem); 1567 | final float scrollOffset = ii != null ? Math.min(ii.offset, mLastOffset) : 0; 1568 | final int scrollPos = (int) (scrollOffset * 1569 | (width - getPaddingLeft() - getPaddingRight())); 1570 | if (scrollPos != getScrollX()) { 1571 | completeScroll(false); 1572 | scrollTo(scrollPos, getScrollY()); 1573 | } 1574 | } 1575 | } 1576 | 1577 | @Override 1578 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 1579 | final int count = getChildCount(); 1580 | int width = r - l; 1581 | int height = b - t; 1582 | int paddingLeft = getPaddingLeft(); 1583 | int paddingTop = getPaddingTop(); 1584 | int paddingRight = getPaddingRight(); 1585 | int paddingBottom = getPaddingBottom(); 1586 | final int scrollX = getScrollX(); 1587 | 1588 | int decorCount = 0; 1589 | 1590 | // First pass - decor views. We need to do this in two passes so that 1591 | // we have the proper offsets for non-decor views later. 1592 | for (int i = 0; i < count; i++) { 1593 | final View child = getChildAt(i); 1594 | if (child.getVisibility() != GONE) { 1595 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1596 | int childLeft = 0; 1597 | int childTop = 0; 1598 | if (lp.isDecor) { 1599 | final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; 1600 | final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; 1601 | switch (hgrav) { 1602 | default: 1603 | childLeft = paddingLeft; 1604 | break; 1605 | case Gravity.LEFT: 1606 | childLeft = paddingLeft; 1607 | paddingLeft += child.getMeasuredWidth(); 1608 | break; 1609 | case Gravity.CENTER_HORIZONTAL: 1610 | childLeft = Math.max((width - child.getMeasuredWidth()) / 2, 1611 | paddingLeft); 1612 | break; 1613 | case Gravity.RIGHT: 1614 | childLeft = width - paddingRight - child.getMeasuredWidth(); 1615 | paddingRight += child.getMeasuredWidth(); 1616 | break; 1617 | } 1618 | switch (vgrav) { 1619 | default: 1620 | childTop = paddingTop; 1621 | break; 1622 | case Gravity.TOP: 1623 | childTop = paddingTop; 1624 | paddingTop += child.getMeasuredHeight(); 1625 | break; 1626 | case Gravity.CENTER_VERTICAL: 1627 | childTop = Math.max((height - child.getMeasuredHeight()) / 2, 1628 | paddingTop); 1629 | break; 1630 | case Gravity.BOTTOM: 1631 | childTop = height - paddingBottom - child.getMeasuredHeight(); 1632 | paddingBottom += child.getMeasuredHeight(); 1633 | break; 1634 | } 1635 | childLeft += scrollX; 1636 | child.layout(childLeft, childTop, 1637 | childLeft + child.getMeasuredWidth(), 1638 | childTop + child.getMeasuredHeight()); 1639 | decorCount++; 1640 | } 1641 | } 1642 | } 1643 | 1644 | final int childWidth = width - paddingLeft - paddingRight; 1645 | // Page views. Do this once we have the right padding offsets from above. 1646 | for (int i = 0; i < count; i++) { 1647 | final View child = getChildAt(i); 1648 | if (child.getVisibility() != GONE) { 1649 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1650 | ItemInfo ii; 1651 | if (!lp.isDecor && (ii = infoForChild(child)) != null) { 1652 | int loff = (int) (childWidth * ii.offset); 1653 | int childLeft = paddingLeft + loff; 1654 | int childTop = paddingTop; 1655 | if (lp.needsMeasure) { 1656 | // This was added during layout and needs measurement. 1657 | // Do it now that we know what we're working with. 1658 | lp.needsMeasure = false; 1659 | final int widthSpec = MeasureSpec.makeMeasureSpec( 1660 | (int) (childWidth * lp.widthFactor), 1661 | MeasureSpec.EXACTLY); 1662 | final int heightSpec = MeasureSpec.makeMeasureSpec( 1663 | (int) (height - paddingTop - paddingBottom), 1664 | MeasureSpec.EXACTLY); 1665 | child.measure(widthSpec, heightSpec); 1666 | } 1667 | if (DEBUG) Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object 1668 | + ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth() 1669 | + "x" + child.getMeasuredHeight()); 1670 | child.layout(childLeft, childTop, 1671 | childLeft + child.getMeasuredWidth(), 1672 | childTop + child.getMeasuredHeight()); 1673 | } 1674 | } 1675 | } 1676 | mTopPageBounds = paddingTop; 1677 | mBottomPageBounds = height - paddingBottom; 1678 | mDecorChildCount = decorCount; 1679 | 1680 | if (mFirstLayout) { 1681 | scrollToItem(mCurItem, false, 0, false); 1682 | } 1683 | mFirstLayout = false; 1684 | } 1685 | 1686 | @Override 1687 | public void computeScroll() { 1688 | if (!mScroller.isFinished() && mScroller.computeScrollOffset()) { 1689 | int oldX = getScrollX(); 1690 | int oldY = getScrollY(); 1691 | int x = mScroller.getCurrX(); 1692 | int y = mScroller.getCurrY(); 1693 | 1694 | if (oldX != x || oldY != y) { 1695 | scrollTo(x, y); 1696 | if (!pageScrolled(x)) { 1697 | mScroller.abortAnimation(); 1698 | scrollTo(0, y); 1699 | } 1700 | } 1701 | 1702 | // Keep on drawing until the animation has finished. 1703 | ViewCompat.postInvalidateOnAnimation(this); 1704 | return; 1705 | } 1706 | 1707 | // Done with scroll, clean up state. 1708 | completeScroll(true); 1709 | } 1710 | 1711 | private boolean pageScrolled(int xpos) { 1712 | if (mItems.size() == 0) { 1713 | mCalledSuper = false; 1714 | onPageScrolled(0, 0, 0); 1715 | if (!mCalledSuper) { 1716 | throw new IllegalStateException( 1717 | "onPageScrolled did not call superclass implementation"); 1718 | } 1719 | return false; 1720 | } 1721 | final ItemInfo ii = infoForCurrentScrollPosition(); 1722 | final int width = getClientWidth(); 1723 | final int widthWithMargin = width + mPageMargin; 1724 | final float marginOffset = (float) mPageMargin / width; 1725 | final int currentPage = ii.position; 1726 | final float pageOffset = (((float) xpos / width) - ii.offset) / 1727 | (ii.widthFactor + marginOffset); 1728 | final int offsetPixels = (int) (pageOffset * widthWithMargin); 1729 | 1730 | mCalledSuper = false; 1731 | onPageScrolled(currentPage, pageOffset, offsetPixels); 1732 | if (!mCalledSuper) { 1733 | throw new IllegalStateException( 1734 | "onPageScrolled did not call superclass implementation"); 1735 | } 1736 | return true; 1737 | } 1738 | 1739 | /** 1740 | * This method will be invoked when the current page is scrolled, either as part 1741 | * of a programmatically initiated smooth scroll or a user initiated touch scroll. 1742 | * If you override this method you must call through to the superclass implementation 1743 | * (e.g. super.onPageScrolled(position, offset, offsetPixels)) before onPageScrolled 1744 | * returns. 1745 | * 1746 | * @param position Position index of the first page currently being displayed. 1747 | * Page position+1 will be visible if positionOffset is nonzero. 1748 | * @param offset Value from [0, 1) indicating the offset from the page at position. 1749 | * @param offsetPixels Value in pixels indicating the offset from position. 1750 | */ 1751 | protected void onPageScrolled(int position, float offset, int offsetPixels) { 1752 | // Offset any decor views if needed - keep them on-screen at all times. 1753 | if (mDecorChildCount > 0) { 1754 | final int scrollX = getScrollX(); 1755 | int paddingLeft = getPaddingLeft(); 1756 | int paddingRight = getPaddingRight(); 1757 | final int width = getWidth(); 1758 | final int childCount = getChildCount(); 1759 | for (int i = 0; i < childCount; i++) { 1760 | final View child = getChildAt(i); 1761 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1762 | if (!lp.isDecor) continue; 1763 | 1764 | final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; 1765 | int childLeft = 0; 1766 | switch (hgrav) { 1767 | default: 1768 | childLeft = paddingLeft; 1769 | break; 1770 | case Gravity.LEFT: 1771 | childLeft = paddingLeft; 1772 | paddingLeft += child.getWidth(); 1773 | break; 1774 | case Gravity.CENTER_HORIZONTAL: 1775 | childLeft = Math.max((width - child.getMeasuredWidth()) / 2, 1776 | paddingLeft); 1777 | break; 1778 | case Gravity.RIGHT: 1779 | childLeft = width - paddingRight - child.getMeasuredWidth(); 1780 | paddingRight += child.getMeasuredWidth(); 1781 | break; 1782 | } 1783 | childLeft += scrollX; 1784 | 1785 | final int childOffset = childLeft - child.getLeft(); 1786 | if (childOffset != 0) { 1787 | child.offsetLeftAndRight(childOffset); 1788 | } 1789 | } 1790 | } 1791 | 1792 | if (mOnPageChangeListener != null) { 1793 | mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels); 1794 | } 1795 | if (mInternalPageChangeListener != null) { 1796 | mInternalPageChangeListener.onPageScrolled(position, offset, offsetPixels); 1797 | } 1798 | 1799 | if (mPageTransformer != null) { 1800 | final int scrollX = getScrollX(); 1801 | final int childCount = getChildCount(); 1802 | for (int i = 0; i < childCount; i++) { 1803 | final View child = getChildAt(i); 1804 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1805 | 1806 | if (lp.isDecor) continue; 1807 | 1808 | final float transformPos = (float) (child.getLeft() - scrollX) / getClientWidth(); 1809 | mPageTransformer.transformPage(child, transformPos); 1810 | } 1811 | } 1812 | 1813 | mCalledSuper = true; 1814 | } 1815 | 1816 | private void completeScroll(boolean postEvents) { 1817 | boolean needPopulate = mScrollState == SCROLL_STATE_SETTLING; 1818 | if (needPopulate) { 1819 | // Done with scroll, no longer want to cache view drawing. 1820 | setScrollingCacheEnabled(false); 1821 | mScroller.abortAnimation(); 1822 | int oldX = getScrollX(); 1823 | int oldY = getScrollY(); 1824 | int x = mScroller.getCurrX(); 1825 | int y = mScroller.getCurrY(); 1826 | if (oldX != x || oldY != y) { 1827 | scrollTo(x, y); 1828 | if (x != oldX) { 1829 | pageScrolled(x); 1830 | } 1831 | } 1832 | } 1833 | mPopulatePending = false; 1834 | for (int i=0; i 0) || (x > getWidth() - mGutterSize && dx < 0); 1852 | } 1853 | 1854 | private void enableLayers(boolean enable) { 1855 | final int childCount = getChildCount(); 1856 | for (int i = 0; i < childCount; i++) { 1857 | final int layerType = enable ? 1858 | ViewCompat.LAYER_TYPE_HARDWARE : ViewCompat.LAYER_TYPE_NONE; 1859 | ViewCompat.setLayerType(getChildAt(i), layerType, null); 1860 | } 1861 | } 1862 | 1863 | @Override 1864 | public boolean onInterceptTouchEvent(MotionEvent ev) { 1865 | /* 1866 | * This method JUST determines whether we want to intercept the motion. 1867 | * If we return true, onMotionEvent will be called and we do the actual 1868 | * scrolling there. 1869 | */ 1870 | 1871 | final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; 1872 | 1873 | // Always take care of the touch gesture being complete. 1874 | if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { 1875 | // Release the drag. 1876 | if (DEBUG) Log.v(TAG, "Intercept done!"); 1877 | mIsBeingDragged = false; 1878 | mIsUnableToDrag = false; 1879 | mActivePointerId = INVALID_POINTER; 1880 | if (mVelocityTracker != null) { 1881 | mVelocityTracker.recycle(); 1882 | mVelocityTracker = null; 1883 | } 1884 | return false; 1885 | } 1886 | 1887 | // Nothing more to do here if we have decided whether or not we 1888 | // are dragging. 1889 | if (action != MotionEvent.ACTION_DOWN) { 1890 | if (mIsBeingDragged) { 1891 | if (DEBUG) Log.v(TAG, "Intercept returning true!"); 1892 | return true; 1893 | } 1894 | if (mIsUnableToDrag) { 1895 | if (DEBUG) Log.v(TAG, "Intercept returning false!"); 1896 | return false; 1897 | } 1898 | } 1899 | 1900 | switch (action) { 1901 | case MotionEvent.ACTION_MOVE: { 1902 | /* 1903 | * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check 1904 | * whether the user has moved far enough from his original down touch. 1905 | */ 1906 | 1907 | /* 1908 | * Locally do absolute value. mLastMotionY is set to the y value 1909 | * of the down event. 1910 | */ 1911 | final int activePointerId = mActivePointerId; 1912 | if (activePointerId == INVALID_POINTER) { 1913 | // If we don't have a valid id, the touch down wasn't on content. 1914 | break; 1915 | } 1916 | 1917 | final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId); 1918 | final float x = MotionEventCompat.getX(ev, pointerIndex); 1919 | final float dx = x - mLastMotionX; 1920 | final float xDiff = Math.abs(dx); 1921 | final float y = MotionEventCompat.getY(ev, pointerIndex); 1922 | final float yDiff = Math.abs(y - mInitialMotionY); 1923 | if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); 1924 | 1925 | if (dx != 0 && !isGutterDrag(mLastMotionX, dx) && 1926 | canScroll(this, false, (int) dx, (int) x, (int) y)) { 1927 | // Nested view has scrollable area under this point. Let it be handled there. 1928 | mLastMotionX = x; 1929 | mLastMotionY = y; 1930 | mIsUnableToDrag = true; 1931 | return false; 1932 | } 1933 | if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) { 1934 | if (DEBUG) Log.v(TAG, "Starting drag!"); 1935 | mIsBeingDragged = true; 1936 | requestParentDisallowInterceptTouchEvent(true); 1937 | setScrollState(SCROLL_STATE_DRAGGING); 1938 | mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop : 1939 | mInitialMotionX - mTouchSlop; 1940 | mLastMotionY = y; 1941 | setScrollingCacheEnabled(true); 1942 | } else if (yDiff > mTouchSlop) { 1943 | // The finger has moved enough in the vertical 1944 | // direction to be counted as a drag... abort 1945 | // any attempt to drag horizontally, to work correctly 1946 | // with children that have scrolling containers. 1947 | if (DEBUG) Log.v(TAG, "Starting unable to drag!"); 1948 | mIsUnableToDrag = true; 1949 | } 1950 | if (mIsBeingDragged) { 1951 | // Scroll to follow the motion event 1952 | if (performDrag(x)) { 1953 | ViewCompat.postInvalidateOnAnimation(this); 1954 | } 1955 | } 1956 | break; 1957 | } 1958 | 1959 | case MotionEvent.ACTION_DOWN: { 1960 | /* 1961 | * Remember location of down touch. 1962 | * ACTION_DOWN always refers to pointer index 0. 1963 | */ 1964 | mLastMotionX = mInitialMotionX = ev.getX(); 1965 | mLastMotionY = mInitialMotionY = ev.getY(); 1966 | mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 1967 | mIsUnableToDrag = false; 1968 | 1969 | mScroller.computeScrollOffset(); 1970 | if (mScrollState == SCROLL_STATE_SETTLING && 1971 | Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) { 1972 | // Let the user 'catch' the pager as it animates. 1973 | mScroller.abortAnimation(); 1974 | mPopulatePending = false; 1975 | populate(); 1976 | mIsBeingDragged = true; 1977 | requestParentDisallowInterceptTouchEvent(true); 1978 | setScrollState(SCROLL_STATE_DRAGGING); 1979 | } else { 1980 | completeScroll(false); 1981 | mIsBeingDragged = false; 1982 | } 1983 | 1984 | if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY 1985 | + " mIsBeingDragged=" + mIsBeingDragged 1986 | + "mIsUnableToDrag=" + mIsUnableToDrag); 1987 | break; 1988 | } 1989 | 1990 | case MotionEventCompat.ACTION_POINTER_UP: 1991 | onSecondaryPointerUp(ev); 1992 | break; 1993 | } 1994 | 1995 | if (mVelocityTracker == null) { 1996 | mVelocityTracker = VelocityTracker.obtain(); 1997 | } 1998 | mVelocityTracker.addMovement(ev); 1999 | 2000 | /* 2001 | * The only time we want to intercept motion events is if we are in the 2002 | * drag mode. 2003 | */ 2004 | return mIsBeingDragged; 2005 | } 2006 | 2007 | @Override 2008 | public boolean onTouchEvent(MotionEvent ev) { 2009 | if (mFakeDragging) { 2010 | // A fake drag is in progress already, ignore this real one 2011 | // but still eat the touch events. 2012 | // (It is likely that the user is multi-touching the screen.) 2013 | return true; 2014 | } 2015 | 2016 | if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) { 2017 | // Don't handle edge touches immediately -- they may actually belong to one of our 2018 | // descendants. 2019 | return false; 2020 | } 2021 | 2022 | if (mAdapter == null || mAdapter.getCount() == 0) { 2023 | // Nothing to present or scroll; nothing to touch. 2024 | return false; 2025 | } 2026 | 2027 | if (mVelocityTracker == null) { 2028 | mVelocityTracker = VelocityTracker.obtain(); 2029 | } 2030 | mVelocityTracker.addMovement(ev); 2031 | 2032 | final int action = ev.getAction(); 2033 | boolean needsInvalidate = false; 2034 | 2035 | switch (action & MotionEventCompat.ACTION_MASK) { 2036 | case MotionEvent.ACTION_DOWN: { 2037 | mScroller.abortAnimation(); 2038 | mPopulatePending = false; 2039 | populate(); 2040 | 2041 | // Remember where the motion event started 2042 | mLastMotionX = mInitialMotionX = ev.getX(); 2043 | mLastMotionY = mInitialMotionY = ev.getY(); 2044 | mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 2045 | break; 2046 | } 2047 | case MotionEvent.ACTION_MOVE: 2048 | if (!mIsBeingDragged) { 2049 | final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); 2050 | final float x = MotionEventCompat.getX(ev, pointerIndex); 2051 | final float xDiff = Math.abs(x - mLastMotionX); 2052 | final float y = MotionEventCompat.getY(ev, pointerIndex); 2053 | final float yDiff = Math.abs(y - mLastMotionY); 2054 | if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); 2055 | if (xDiff > mTouchSlop && xDiff > yDiff) { 2056 | if (DEBUG) Log.v(TAG, "Starting drag!"); 2057 | mIsBeingDragged = true; 2058 | requestParentDisallowInterceptTouchEvent(true); 2059 | mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop : 2060 | mInitialMotionX - mTouchSlop; 2061 | mLastMotionY = y; 2062 | setScrollState(SCROLL_STATE_DRAGGING); 2063 | setScrollingCacheEnabled(true); 2064 | 2065 | // Disallow Parent Intercept, just in case 2066 | ViewParent parent = getParent(); 2067 | if (parent != null) { 2068 | parent.requestDisallowInterceptTouchEvent(true); 2069 | } 2070 | } 2071 | } 2072 | // Not else! Note that mIsBeingDragged can be set above. 2073 | if (mIsBeingDragged) { 2074 | // Scroll to follow the motion event 2075 | final int activePointerIndex = MotionEventCompat.findPointerIndex( 2076 | ev, mActivePointerId); 2077 | final float x = MotionEventCompat.getX(ev, activePointerIndex); 2078 | needsInvalidate |= performDrag(x); 2079 | } 2080 | break; 2081 | case MotionEvent.ACTION_UP: 2082 | if (mIsBeingDragged) { 2083 | final VelocityTracker velocityTracker = mVelocityTracker; 2084 | velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 2085 | int initialVelocity = (int) VelocityTrackerCompat.getXVelocity( 2086 | velocityTracker, mActivePointerId); 2087 | mPopulatePending = true; 2088 | final int width = getClientWidth(); 2089 | final int scrollX = getScrollX(); 2090 | final ItemInfo ii = infoForCurrentScrollPosition(); 2091 | final int currentPage = ii.position; 2092 | final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor; 2093 | final int activePointerIndex = 2094 | MotionEventCompat.findPointerIndex(ev, mActivePointerId); 2095 | final float x = MotionEventCompat.getX(ev, activePointerIndex); 2096 | final int totalDelta = (int) (x - mInitialMotionX); 2097 | int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, 2098 | totalDelta); 2099 | setCurrentItemInternal(nextPage, true, true, initialVelocity); 2100 | 2101 | mActivePointerId = INVALID_POINTER; 2102 | endDrag(); 2103 | needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease(); 2104 | } 2105 | break; 2106 | case MotionEvent.ACTION_CANCEL: 2107 | if (mIsBeingDragged) { 2108 | scrollToItem(mCurItem, true, 0, false); 2109 | mActivePointerId = INVALID_POINTER; 2110 | endDrag(); 2111 | needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease(); 2112 | } 2113 | break; 2114 | case MotionEventCompat.ACTION_POINTER_DOWN: { 2115 | final int index = MotionEventCompat.getActionIndex(ev); 2116 | final float x = MotionEventCompat.getX(ev, index); 2117 | mLastMotionX = x; 2118 | mActivePointerId = MotionEventCompat.getPointerId(ev, index); 2119 | break; 2120 | } 2121 | case MotionEventCompat.ACTION_POINTER_UP: 2122 | onSecondaryPointerUp(ev); 2123 | mLastMotionX = MotionEventCompat.getX(ev, 2124 | MotionEventCompat.findPointerIndex(ev, mActivePointerId)); 2125 | break; 2126 | } 2127 | if (needsInvalidate) { 2128 | ViewCompat.postInvalidateOnAnimation(this); 2129 | } 2130 | return true; 2131 | } 2132 | 2133 | private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) { 2134 | final ViewParent parent = getParent(); 2135 | if (parent != null) { 2136 | parent.requestDisallowInterceptTouchEvent(disallowIntercept); 2137 | } 2138 | } 2139 | 2140 | private boolean performDrag(float x) { 2141 | boolean needsInvalidate = false; 2142 | 2143 | final float deltaX = mLastMotionX - x; 2144 | mLastMotionX = x; 2145 | 2146 | float oldScrollX = getScrollX(); 2147 | float scrollX = oldScrollX + deltaX; 2148 | final int width = getClientWidth(); 2149 | 2150 | float leftBound = width * mFirstOffset; 2151 | float rightBound = width * mLastOffset; 2152 | boolean leftAbsolute = true; 2153 | boolean rightAbsolute = true; 2154 | 2155 | final ItemInfo firstItem = mItems.get(0); 2156 | final ItemInfo lastItem = mItems.get(mItems.size() - 1); 2157 | if (firstItem.position != 0) { 2158 | leftAbsolute = false; 2159 | leftBound = firstItem.offset * width; 2160 | } 2161 | if (lastItem.position != mAdapter.getCount() - 1) { 2162 | rightAbsolute = false; 2163 | rightBound = lastItem.offset * width; 2164 | } 2165 | 2166 | if (scrollX < leftBound) { 2167 | if (leftAbsolute) { 2168 | float over = leftBound - scrollX; 2169 | needsInvalidate = mLeftEdge.onPull(Math.abs(over) / width); 2170 | } 2171 | scrollX = leftBound; 2172 | } else if (scrollX > rightBound) { 2173 | if (rightAbsolute) { 2174 | float over = scrollX - rightBound; 2175 | needsInvalidate = mRightEdge.onPull(Math.abs(over) / width); 2176 | } 2177 | scrollX = rightBound; 2178 | } 2179 | // Don't lose the rounded component 2180 | mLastMotionX += scrollX - (int) scrollX; 2181 | scrollTo((int) scrollX, getScrollY()); 2182 | pageScrolled((int) scrollX); 2183 | 2184 | return needsInvalidate; 2185 | } 2186 | 2187 | /** 2188 | * @return Info about the page at the current scroll position. 2189 | * This can be synthetic for a missing middle page; the 'object' field can be null. 2190 | */ 2191 | private ItemInfo infoForCurrentScrollPosition() { 2192 | final int width = getClientWidth(); 2193 | final float scrollOffset = width > 0 ? (float) getScrollX() / width : 0; 2194 | final float marginOffset = width > 0 ? (float) mPageMargin / width : 0; 2195 | int lastPos = -1; 2196 | float lastOffset = 0.f; 2197 | float lastWidth = 0.f; 2198 | boolean first = true; 2199 | 2200 | ItemInfo lastItem = null; 2201 | for (int i = 0; i < mItems.size(); i++) { 2202 | ItemInfo ii = mItems.get(i); 2203 | float offset; 2204 | if (!first && ii.position != lastPos + 1) { 2205 | // Create a synthetic item for a missing page. 2206 | ii = mTempItem; 2207 | ii.offset = lastOffset + lastWidth + marginOffset; 2208 | ii.position = lastPos + 1; 2209 | ii.widthFactor = mAdapter.getPageWidth(ii.position); 2210 | i--; 2211 | } 2212 | offset = ii.offset; 2213 | 2214 | final float leftBound = offset; 2215 | final float rightBound = offset + ii.widthFactor + marginOffset; 2216 | if (first || scrollOffset >= leftBound) { 2217 | if (scrollOffset < rightBound || i == mItems.size() - 1) { 2218 | return ii; 2219 | } 2220 | } else { 2221 | return lastItem; 2222 | } 2223 | first = false; 2224 | lastPos = ii.position; 2225 | lastOffset = offset; 2226 | lastWidth = ii.widthFactor; 2227 | lastItem = ii; 2228 | } 2229 | 2230 | return lastItem; 2231 | } 2232 | 2233 | private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) { 2234 | int targetPage; 2235 | if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) { 2236 | targetPage = velocity > 0 ? currentPage : currentPage + 1; 2237 | } else { 2238 | final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f; 2239 | targetPage = (int) (currentPage + pageOffset + truncator); 2240 | } 2241 | 2242 | if (mItems.size() > 0) { 2243 | final ItemInfo firstItem = mItems.get(0); 2244 | final ItemInfo lastItem = mItems.get(mItems.size() - 1); 2245 | 2246 | // Only let the user target pages we have items for 2247 | targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position)); 2248 | } 2249 | 2250 | return targetPage; 2251 | } 2252 | 2253 | @Override 2254 | public void draw(Canvas canvas) { 2255 | super.draw(canvas); 2256 | boolean needsInvalidate = false; 2257 | 2258 | final int overScrollMode = ViewCompat.getOverScrollMode(this); 2259 | if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS || 2260 | (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && 2261 | mAdapter != null && mAdapter.getCount() > 1)) { 2262 | if (!mLeftEdge.isFinished()) { 2263 | final int restoreCount = canvas.save(); 2264 | final int height = getHeight() - getPaddingTop() - getPaddingBottom(); 2265 | final int width = getWidth(); 2266 | 2267 | canvas.rotate(270); 2268 | canvas.translate(-height + getPaddingTop(), mFirstOffset * width); 2269 | mLeftEdge.setSize(height, width); 2270 | needsInvalidate |= mLeftEdge.draw(canvas); 2271 | canvas.restoreToCount(restoreCount); 2272 | } 2273 | if (!mRightEdge.isFinished()) { 2274 | final int restoreCount = canvas.save(); 2275 | final int width = getWidth(); 2276 | final int height = getHeight() - getPaddingTop() - getPaddingBottom(); 2277 | 2278 | canvas.rotate(90); 2279 | canvas.translate(-getPaddingTop(), -(mLastOffset + 1) * width); 2280 | mRightEdge.setSize(height, width); 2281 | needsInvalidate |= mRightEdge.draw(canvas); 2282 | canvas.restoreToCount(restoreCount); 2283 | } 2284 | } else { 2285 | mLeftEdge.finish(); 2286 | mRightEdge.finish(); 2287 | } 2288 | 2289 | if (needsInvalidate) { 2290 | // Keep animating 2291 | ViewCompat.postInvalidateOnAnimation(this); 2292 | } 2293 | } 2294 | 2295 | @Override 2296 | protected void onDraw(Canvas canvas) { 2297 | super.onDraw(canvas); 2298 | 2299 | // Draw the margin drawable between pages if needed. 2300 | if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) { 2301 | final int scrollX = getScrollX(); 2302 | final int width = getWidth(); 2303 | 2304 | final float marginOffset = (float) mPageMargin / width; 2305 | int itemIndex = 0; 2306 | ItemInfo ii = mItems.get(0); 2307 | float offset = ii.offset; 2308 | final int itemCount = mItems.size(); 2309 | final int firstPos = ii.position; 2310 | final int lastPos = mItems.get(itemCount - 1).position; 2311 | for (int pos = firstPos; pos < lastPos; pos++) { 2312 | while (pos > ii.position && itemIndex < itemCount) { 2313 | ii = mItems.get(++itemIndex); 2314 | } 2315 | 2316 | float drawAt; 2317 | if (pos == ii.position) { 2318 | drawAt = (ii.offset + ii.widthFactor) * width; 2319 | offset = ii.offset + ii.widthFactor + marginOffset; 2320 | } else { 2321 | float widthFactor = mAdapter.getPageWidth(pos); 2322 | drawAt = (offset + widthFactor) * width; 2323 | offset += widthFactor + marginOffset; 2324 | } 2325 | 2326 | if (drawAt + mPageMargin > scrollX) { 2327 | mMarginDrawable.setBounds((int) drawAt, mTopPageBounds, 2328 | (int) (drawAt + mPageMargin + 0.5f), mBottomPageBounds); 2329 | mMarginDrawable.draw(canvas); 2330 | } 2331 | 2332 | if (drawAt > scrollX + width) { 2333 | break; // No more visible, no sense in continuing 2334 | } 2335 | } 2336 | } 2337 | } 2338 | 2339 | /** 2340 | * Start a fake drag of the pager. 2341 | * 2342 | *

A fake drag can be useful if you want to synchronize the motion of the ViewPager 2343 | * with the touch scrolling of another view, while still letting the ViewPager 2344 | * control the snapping motion and fling behavior. (e.g. parallax-scrolling tabs.) 2345 | * Call {@link #fakeDragBy(float)} to simulate the actual drag motion. Call 2346 | * {@link #endFakeDrag()} to complete the fake drag and fling as necessary. 2347 | * 2348 | *

During a fake drag the ViewPager will ignore all touch events. If a real drag 2349 | * is already in progress, this method will return false. 2350 | * 2351 | * @return true if the fake drag began successfully, false if it could not be started. 2352 | * 2353 | * @see #fakeDragBy(float) 2354 | * @see #endFakeDrag() 2355 | */ 2356 | public boolean beginFakeDrag() { 2357 | if (mIsBeingDragged) { 2358 | return false; 2359 | } 2360 | mFakeDragging = true; 2361 | setScrollState(SCROLL_STATE_DRAGGING); 2362 | mInitialMotionX = mLastMotionX = 0; 2363 | if (mVelocityTracker == null) { 2364 | mVelocityTracker = VelocityTracker.obtain(); 2365 | } else { 2366 | mVelocityTracker.clear(); 2367 | } 2368 | final long time = SystemClock.uptimeMillis(); 2369 | final MotionEvent ev = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0); 2370 | mVelocityTracker.addMovement(ev); 2371 | ev.recycle(); 2372 | mFakeDragBeginTime = time; 2373 | return true; 2374 | } 2375 | 2376 | /** 2377 | * End a fake drag of the pager. 2378 | * 2379 | * @see #beginFakeDrag() 2380 | * @see #fakeDragBy(float) 2381 | */ 2382 | public void endFakeDrag() { 2383 | if (!mFakeDragging) { 2384 | throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first."); 2385 | } 2386 | 2387 | final VelocityTracker velocityTracker = mVelocityTracker; 2388 | velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 2389 | int initialVelocity = (int) VelocityTrackerCompat.getXVelocity( 2390 | velocityTracker, mActivePointerId); 2391 | mPopulatePending = true; 2392 | final int width = getClientWidth(); 2393 | final int scrollX = getScrollX(); 2394 | final ItemInfo ii = infoForCurrentScrollPosition(); 2395 | final int currentPage = ii.position; 2396 | final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor; 2397 | final int totalDelta = (int) (mLastMotionX - mInitialMotionX); 2398 | int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, 2399 | totalDelta); 2400 | setCurrentItemInternal(nextPage, true, true, initialVelocity); 2401 | endDrag(); 2402 | 2403 | mFakeDragging = false; 2404 | } 2405 | 2406 | /** 2407 | * Fake drag by an offset in pixels. You must have called {@link #beginFakeDrag()} first. 2408 | * 2409 | * @param xOffset Offset in pixels to drag by. 2410 | * @see #beginFakeDrag() 2411 | * @see #endFakeDrag() 2412 | */ 2413 | public void fakeDragBy(float xOffset) { 2414 | if (!mFakeDragging) { 2415 | throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first."); 2416 | } 2417 | 2418 | mLastMotionX += xOffset; 2419 | 2420 | float oldScrollX = getScrollX(); 2421 | float scrollX = oldScrollX - xOffset; 2422 | final int width = getClientWidth(); 2423 | 2424 | float leftBound = width * mFirstOffset; 2425 | float rightBound = width * mLastOffset; 2426 | 2427 | final ItemInfo firstItem = mItems.get(0); 2428 | final ItemInfo lastItem = mItems.get(mItems.size() - 1); 2429 | if (firstItem.position != 0) { 2430 | leftBound = firstItem.offset * width; 2431 | } 2432 | if (lastItem.position != mAdapter.getCount() - 1) { 2433 | rightBound = lastItem.offset * width; 2434 | } 2435 | 2436 | if (scrollX < leftBound) { 2437 | scrollX = leftBound; 2438 | } else if (scrollX > rightBound) { 2439 | scrollX = rightBound; 2440 | } 2441 | // Don't lose the rounded component 2442 | mLastMotionX += scrollX - (int) scrollX; 2443 | scrollTo((int) scrollX, getScrollY()); 2444 | pageScrolled((int) scrollX); 2445 | 2446 | // Synthesize an event for the VelocityTracker. 2447 | final long time = SystemClock.uptimeMillis(); 2448 | final MotionEvent ev = MotionEvent.obtain(mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE, 2449 | mLastMotionX, 0, 0); 2450 | mVelocityTracker.addMovement(ev); 2451 | ev.recycle(); 2452 | } 2453 | 2454 | /** 2455 | * Returns true if a fake drag is in progress. 2456 | * 2457 | * @return true if currently in a fake drag, false otherwise. 2458 | * 2459 | * @see #beginFakeDrag() 2460 | * @see #fakeDragBy(float) 2461 | * @see #endFakeDrag() 2462 | */ 2463 | public boolean isFakeDragging() { 2464 | return mFakeDragging; 2465 | } 2466 | 2467 | private void onSecondaryPointerUp(MotionEvent ev) { 2468 | final int pointerIndex = MotionEventCompat.getActionIndex(ev); 2469 | final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); 2470 | if (pointerId == mActivePointerId) { 2471 | // This was our active pointer going up. Choose a new 2472 | // active pointer and adjust accordingly. 2473 | final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 2474 | mLastMotionX = MotionEventCompat.getX(ev, newPointerIndex); 2475 | mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); 2476 | if (mVelocityTracker != null) { 2477 | mVelocityTracker.clear(); 2478 | } 2479 | } 2480 | } 2481 | 2482 | private void endDrag() { 2483 | mIsBeingDragged = false; 2484 | mIsUnableToDrag = false; 2485 | 2486 | if (mVelocityTracker != null) { 2487 | mVelocityTracker.recycle(); 2488 | mVelocityTracker = null; 2489 | } 2490 | } 2491 | 2492 | private void setScrollingCacheEnabled(boolean enabled) { 2493 | if (mScrollingCacheEnabled != enabled) { 2494 | mScrollingCacheEnabled = enabled; 2495 | if (USE_CACHE) { 2496 | final int size = getChildCount(); 2497 | for (int i = 0; i < size; ++i) { 2498 | final View child = getChildAt(i); 2499 | if (child.getVisibility() != GONE) { 2500 | child.setDrawingCacheEnabled(enabled); 2501 | } 2502 | } 2503 | } 2504 | } 2505 | } 2506 | 2507 | public boolean canScrollHorizontally(int direction) { 2508 | if (mAdapter == null) { 2509 | return false; 2510 | } 2511 | 2512 | final int width = getClientWidth(); 2513 | final int scrollX = getScrollX(); 2514 | if (direction < 0) { 2515 | return (scrollX > (int) (width * mFirstOffset)); 2516 | } else if (direction > 0) { 2517 | return (scrollX < (int) (width * mLastOffset)); 2518 | } else { 2519 | return false; 2520 | } 2521 | } 2522 | 2523 | /** 2524 | * Tests scrollability within child views of v given a delta of dx. 2525 | * 2526 | * @param v View to test for horizontal scrollability 2527 | * @param checkV Whether the view v passed should itself be checked for scrollability (true), 2528 | * or just its children (false). 2529 | * @param dx Delta scrolled in pixels 2530 | * @param x X coordinate of the active touch point 2531 | * @param y Y coordinate of the active touch point 2532 | * @return true if child views of v can be scrolled by delta of dx. 2533 | */ 2534 | protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) { 2535 | if (v instanceof ViewGroup) { 2536 | final ViewGroup group = (ViewGroup) v; 2537 | final int scrollX = v.getScrollX(); 2538 | final int scrollY = v.getScrollY(); 2539 | final int count = group.getChildCount(); 2540 | // Count backwards - let topmost views consume scroll distance first. 2541 | for (int i = count - 1; i >= 0; i--) { 2542 | // TODO: Add versioned support here for transformed views. 2543 | // This will not work for transformed views in Honeycomb+ 2544 | final View child = group.getChildAt(i); 2545 | if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && 2546 | y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && 2547 | canScroll(child, true, dx, x + scrollX - child.getLeft(), 2548 | y + scrollY - child.getTop())) { 2549 | return true; 2550 | } 2551 | } 2552 | } 2553 | 2554 | return checkV && ViewCompat.canScrollHorizontally(v, -dx); 2555 | } 2556 | 2557 | @Override 2558 | public boolean dispatchKeyEvent(KeyEvent event) { 2559 | // Let the focused view and/or our descendants get the key first 2560 | return super.dispatchKeyEvent(event) || executeKeyEvent(event); 2561 | } 2562 | 2563 | /** 2564 | * You can call this function yourself to have the scroll view perform 2565 | * scrolling from a key event, just as if the event had been dispatched to 2566 | * it by the view hierarchy. 2567 | * 2568 | * @param event The key event to execute. 2569 | * @return Return true if the event was handled, else false. 2570 | */ 2571 | public boolean executeKeyEvent(KeyEvent event) { 2572 | boolean handled = false; 2573 | if (event.getAction() == KeyEvent.ACTION_DOWN) { 2574 | switch (event.getKeyCode()) { 2575 | case KeyEvent.KEYCODE_DPAD_LEFT: 2576 | handled = arrowScroll(FOCUS_LEFT); 2577 | break; 2578 | case KeyEvent.KEYCODE_DPAD_RIGHT: 2579 | handled = arrowScroll(FOCUS_RIGHT); 2580 | break; 2581 | case KeyEvent.KEYCODE_TAB: 2582 | if (Build.VERSION.SDK_INT >= 11) { 2583 | // The focus finder had a bug handling FOCUS_FORWARD and FOCUS_BACKWARD 2584 | // before Android 3.0. Ignore the tab key on those devices. 2585 | if (KeyEventCompat.hasNoModifiers(event)) { 2586 | handled = arrowScroll(FOCUS_FORWARD); 2587 | } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_SHIFT_ON)) { 2588 | handled = arrowScroll(FOCUS_BACKWARD); 2589 | } 2590 | } 2591 | break; 2592 | } 2593 | } 2594 | return handled; 2595 | } 2596 | 2597 | public boolean arrowScroll(int direction) { 2598 | View currentFocused = findFocus(); 2599 | if (currentFocused == this) { 2600 | currentFocused = null; 2601 | } else if (currentFocused != null) { 2602 | boolean isChild = false; 2603 | for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup; 2604 | parent = parent.getParent()) { 2605 | if (parent == this) { 2606 | isChild = true; 2607 | break; 2608 | } 2609 | } 2610 | if (!isChild) { 2611 | // This would cause the focus search down below to fail in fun ways. 2612 | final StringBuilder sb = new StringBuilder(); 2613 | sb.append(currentFocused.getClass().getSimpleName()); 2614 | for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup; 2615 | parent = parent.getParent()) { 2616 | sb.append(" => ").append(parent.getClass().getSimpleName()); 2617 | } 2618 | Log.e(TAG, "arrowScroll tried to find focus based on non-child " + 2619 | "current focused view " + sb.toString()); 2620 | currentFocused = null; 2621 | } 2622 | } 2623 | 2624 | boolean handled = false; 2625 | 2626 | View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, 2627 | direction); 2628 | if (nextFocused != null && nextFocused != currentFocused) { 2629 | if (direction == View.FOCUS_LEFT) { 2630 | // If there is nothing to the left, or this is causing us to 2631 | // jump to the right, then what we really want to do is page left. 2632 | final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left; 2633 | final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left; 2634 | if (currentFocused != null && nextLeft >= currLeft) { 2635 | handled = pageLeft(); 2636 | } else { 2637 | handled = nextFocused.requestFocus(); 2638 | } 2639 | } else if (direction == View.FOCUS_RIGHT) { 2640 | // If there is nothing to the right, or this is causing us to 2641 | // jump to the left, then what we really want to do is page right. 2642 | final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left; 2643 | final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left; 2644 | if (currentFocused != null && nextLeft <= currLeft) { 2645 | handled = pageRight(); 2646 | } else { 2647 | handled = nextFocused.requestFocus(); 2648 | } 2649 | } 2650 | } else if (direction == FOCUS_LEFT || direction == FOCUS_BACKWARD) { 2651 | // Trying to move left and nothing there; try to page. 2652 | handled = pageLeft(); 2653 | } else if (direction == FOCUS_RIGHT || direction == FOCUS_FORWARD) { 2654 | // Trying to move right and nothing there; try to page. 2655 | handled = pageRight(); 2656 | } 2657 | if (handled) { 2658 | playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); 2659 | } 2660 | return handled; 2661 | } 2662 | 2663 | private Rect getChildRectInPagerCoordinates(Rect outRect, View child) { 2664 | if (outRect == null) { 2665 | outRect = new Rect(); 2666 | } 2667 | if (child == null) { 2668 | outRect.set(0, 0, 0, 0); 2669 | return outRect; 2670 | } 2671 | outRect.left = child.getLeft(); 2672 | outRect.right = child.getRight(); 2673 | outRect.top = child.getTop(); 2674 | outRect.bottom = child.getBottom(); 2675 | 2676 | ViewParent parent = child.getParent(); 2677 | while (parent instanceof ViewGroup && parent != this) { 2678 | final ViewGroup group = (ViewGroup) parent; 2679 | outRect.left += group.getLeft(); 2680 | outRect.right += group.getRight(); 2681 | outRect.top += group.getTop(); 2682 | outRect.bottom += group.getBottom(); 2683 | 2684 | parent = group.getParent(); 2685 | } 2686 | return outRect; 2687 | } 2688 | 2689 | boolean pageLeft() { 2690 | if (mCurItem > 0) { 2691 | setCurrentItem(mCurItem-1, true); 2692 | return true; 2693 | } 2694 | return false; 2695 | } 2696 | 2697 | boolean pageRight() { 2698 | if (mAdapter != null && mCurItem < (mAdapter.getCount()-1)) { 2699 | setCurrentItem(mCurItem+1, true); 2700 | return true; 2701 | } 2702 | return false; 2703 | } 2704 | 2705 | /** 2706 | * We only want the current page that is being shown to be focusable. 2707 | */ 2708 | @Override 2709 | public void addFocusables(ArrayList views, int direction, int focusableMode) { 2710 | final int focusableCount = views.size(); 2711 | 2712 | final int descendantFocusability = getDescendantFocusability(); 2713 | 2714 | if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) { 2715 | for (int i = 0; i < getChildCount(); i++) { 2716 | final View child = getChildAt(i); 2717 | if (child.getVisibility() == VISIBLE) { 2718 | ItemInfo ii = infoForChild(child); 2719 | if (ii != null && ii.position == mCurItem) { 2720 | child.addFocusables(views, direction, focusableMode); 2721 | } 2722 | } 2723 | } 2724 | } 2725 | 2726 | // we add ourselves (if focusable) in all cases except for when we are 2727 | // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable. this is 2728 | // to avoid the focus search finding layouts when a more precise search 2729 | // among the focusable children would be more interesting. 2730 | if ( 2731 | descendantFocusability != FOCUS_AFTER_DESCENDANTS || 2732 | // No focusable descendants 2733 | (focusableCount == views.size())) { 2734 | // Note that we can't call the superclass here, because it will 2735 | // add all views in. So we need to do the same thing View does. 2736 | if (!isFocusable()) { 2737 | return; 2738 | } 2739 | if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE && 2740 | isInTouchMode() && !isFocusableInTouchMode()) { 2741 | return; 2742 | } 2743 | if (views != null) { 2744 | views.add(this); 2745 | } 2746 | } 2747 | } 2748 | 2749 | /** 2750 | * We only want the current page that is being shown to be touchable. 2751 | */ 2752 | @Override 2753 | public void addTouchables(ArrayList views) { 2754 | // Note that we don't call super.addTouchables(), which means that 2755 | // we don't call View.addTouchables(). This is okay because a ViewPager 2756 | // is itself not touchable. 2757 | for (int i = 0; i < getChildCount(); i++) { 2758 | final View child = getChildAt(i); 2759 | if (child.getVisibility() == VISIBLE) { 2760 | ItemInfo ii = infoForChild(child); 2761 | if (ii != null && ii.position == mCurItem) { 2762 | child.addTouchables(views); 2763 | } 2764 | } 2765 | } 2766 | } 2767 | 2768 | /** 2769 | * We only want the current page that is being shown to be focusable. 2770 | */ 2771 | @Override 2772 | protected boolean onRequestFocusInDescendants(int direction, 2773 | Rect previouslyFocusedRect) { 2774 | int index; 2775 | int increment; 2776 | int end; 2777 | int count = getChildCount(); 2778 | if ((direction & FOCUS_FORWARD) != 0) { 2779 | index = 0; 2780 | increment = 1; 2781 | end = count; 2782 | } else { 2783 | index = count - 1; 2784 | increment = -1; 2785 | end = -1; 2786 | } 2787 | for (int i = index; i != end; i += increment) { 2788 | View child = getChildAt(i); 2789 | if (child.getVisibility() == VISIBLE) { 2790 | ItemInfo ii = infoForChild(child); 2791 | if (ii != null && ii.position == mCurItem) { 2792 | if (child.requestFocus(direction, previouslyFocusedRect)) { 2793 | return true; 2794 | } 2795 | } 2796 | } 2797 | } 2798 | return false; 2799 | } 2800 | 2801 | @Override 2802 | public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 2803 | // Dispatch scroll events from this ViewPager. 2804 | if (event.getEventType() == AccessibilityEventCompat.TYPE_VIEW_SCROLLED) { 2805 | return super.dispatchPopulateAccessibilityEvent(event); 2806 | } 2807 | 2808 | // Dispatch all other accessibility events from the current page. 2809 | final int childCount = getChildCount(); 2810 | for (int i = 0; i < childCount; i++) { 2811 | final View child = getChildAt(i); 2812 | if (child.getVisibility() == VISIBLE) { 2813 | final ItemInfo ii = infoForChild(child); 2814 | if (ii != null && ii.position == mCurItem && 2815 | child.dispatchPopulateAccessibilityEvent(event)) { 2816 | return true; 2817 | } 2818 | } 2819 | } 2820 | 2821 | return false; 2822 | } 2823 | 2824 | @Override 2825 | protected ViewGroup.LayoutParams generateDefaultLayoutParams() { 2826 | return new LayoutParams(); 2827 | } 2828 | 2829 | @Override 2830 | protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 2831 | return generateDefaultLayoutParams(); 2832 | } 2833 | 2834 | @Override 2835 | protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 2836 | return p instanceof LayoutParams && super.checkLayoutParams(p); 2837 | } 2838 | 2839 | @Override 2840 | public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 2841 | return new LayoutParams(getContext(), attrs); 2842 | } 2843 | 2844 | class MyAccessibilityDelegate extends AccessibilityDelegateCompat { 2845 | 2846 | @Override 2847 | public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { 2848 | super.onInitializeAccessibilityEvent(host, event); 2849 | event.setClassName(CycleViewPager.class.getName()); 2850 | final AccessibilityRecordCompat recordCompat = AccessibilityRecordCompat.obtain(); 2851 | recordCompat.setScrollable(canScroll()); 2852 | if (event.getEventType() == AccessibilityEventCompat.TYPE_VIEW_SCROLLED 2853 | && mAdapter != null) { 2854 | recordCompat.setItemCount(mAdapter.getCount()); 2855 | recordCompat.setFromIndex(mCurItem); 2856 | recordCompat.setToIndex(mCurItem); 2857 | } 2858 | } 2859 | 2860 | @Override 2861 | public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { 2862 | super.onInitializeAccessibilityNodeInfo(host, info); 2863 | info.setClassName(CycleViewPager.class.getName()); 2864 | info.setScrollable(canScroll()); 2865 | if (canScrollHorizontally(1)) { 2866 | info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); 2867 | } 2868 | if (canScrollHorizontally(-1)) { 2869 | info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); 2870 | } 2871 | } 2872 | 2873 | @Override 2874 | public boolean performAccessibilityAction(View host, int action, Bundle args) { 2875 | if (super.performAccessibilityAction(host, action, args)) { 2876 | return true; 2877 | } 2878 | switch (action) { 2879 | case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: { 2880 | if (canScrollHorizontally(1)) { 2881 | setCurrentItem(mCurItem + 1); 2882 | return true; 2883 | } 2884 | } return false; 2885 | case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: { 2886 | if (canScrollHorizontally(-1)) { 2887 | setCurrentItem(mCurItem - 1); 2888 | return true; 2889 | } 2890 | } return false; 2891 | } 2892 | return false; 2893 | } 2894 | 2895 | private boolean canScroll() { 2896 | return (mAdapter != null) && (mAdapter.getCount() > 1); 2897 | } 2898 | } 2899 | 2900 | private class PagerObserver extends DataSetObserver { 2901 | @Override 2902 | public void onChanged() { 2903 | dataSetChanged(); 2904 | } 2905 | @Override 2906 | public void onInvalidated() { 2907 | dataSetChanged(); 2908 | } 2909 | } 2910 | 2911 | /** 2912 | * Layout parameters that should be supplied for views added to a 2913 | * ViewPager. 2914 | */ 2915 | public static class LayoutParams extends ViewGroup.LayoutParams { 2916 | /** 2917 | * true if this view is a decoration on the pager itself and not 2918 | * a view supplied by the adapter. 2919 | */ 2920 | public boolean isDecor; 2921 | 2922 | /** 2923 | * Gravity setting for use on decor views only: 2924 | * Where to position the view page within the overall ViewPager 2925 | * container; constants are defined in {@link android.view.Gravity}. 2926 | */ 2927 | public int gravity; 2928 | 2929 | /** 2930 | * Width as a 0-1 multiplier of the measured pager width 2931 | */ 2932 | float widthFactor = 0.f; 2933 | 2934 | /** 2935 | * true if this view was added during layout and needs to be measured 2936 | * before being positioned. 2937 | */ 2938 | boolean needsMeasure; 2939 | 2940 | /** 2941 | * Adapter position this view is for if !isDecor 2942 | */ 2943 | int position; 2944 | 2945 | /** 2946 | * Current child index within the ViewPager that this view occupies 2947 | */ 2948 | int childIndex; 2949 | 2950 | public LayoutParams() { 2951 | super(FILL_PARENT, FILL_PARENT); 2952 | } 2953 | 2954 | public LayoutParams(Context context, AttributeSet attrs) { 2955 | super(context, attrs); 2956 | 2957 | final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS); 2958 | gravity = a.getInteger(0, Gravity.TOP); 2959 | a.recycle(); 2960 | } 2961 | } 2962 | 2963 | static class ViewPositionComparator implements Comparator { 2964 | @Override 2965 | public int compare(View lhs, View rhs) { 2966 | final LayoutParams llp = (LayoutParams) lhs.getLayoutParams(); 2967 | final LayoutParams rlp = (LayoutParams) rhs.getLayoutParams(); 2968 | if (llp.isDecor != rlp.isDecor) { 2969 | return llp.isDecor ? 1 : -1; 2970 | } 2971 | return llp.position - rlp.position; 2972 | } 2973 | } 2974 | } 2975 | -------------------------------------------------------------------------------- /screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhaozhentao/CycleViewPager/73d6f0c50cb88a8adda15dabd7f3a6c2398942cc/screenshot.gif -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':library' 2 | --------------------------------------------------------------------------------