├── .gitignore ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── library ├── build.gradle ├── gradle.properties └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── fr │ └── castorflex │ └── android │ └── verticalviewpager │ └── VerticalViewPager.java ├── mvn_push.gradle ├── sample ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── fr │ │ └── castorflex │ │ └── android │ │ └── verticalviewpager │ │ └── sample │ │ ├── MainActivity.java │ │ └── verticaltablayout │ │ ├── QTabIndicator.java │ │ ├── QTabView.java │ │ ├── TabAdapter.java │ │ ├── TabIndicator.java │ │ ├── TabView.java │ │ └── VerticalTabLayout.java │ └── res │ ├── layout │ ├── activity_main.xml │ ├── fragment_layout.xml │ └── list_item.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── raw │ ├── screenshot1.gif │ ├── screenshot2.gif │ └── screenshot3.gif │ └── values │ ├── attrs.xml │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Eclipse 2 | .project 3 | .classpath 4 | .settings 5 | .checkstyle 6 | 7 | # IntelliJ IDEA 8 | .idea 9 | *.iml 10 | *.ipr 11 | *.iws 12 | classes 13 | gen-external-apklibs 14 | 15 | # Gradle 16 | .gradle 17 | build 18 | 19 | # Maven 20 | target 21 | release.properties 22 | pom.xml.* 23 | 24 | # Ant 25 | bin 26 | gen 27 | build.xml 28 | ant.properties 29 | local.properties 30 | proguard-project.txt 31 | 32 | #Crashlytics 33 | .crashlytics_data 34 | com_crashlytics_export_strings.xml 35 | 36 | # Other 37 | out/ 38 | .DS_Store 39 | tmp 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 在开发中,我们常常需要ViewPager结合Fragment一起使用.我们可以使用三方开源的PagerSlidingTabStrip去实现,或者viewpagerindicator。现在我们可以使用Design support library库的TabLayout去实现了。 3 | 4 | TabLayout+ViewPager+Fragment成为了实现如下效果的标配 5 | (效果图来自 暴风体育Android APP) 6 | ![这里写图片描述](https://github.com/soulrelay/VerticalViewPagerWithTabLayout/blob/master/sample/src/main/res/raw/screenshot1.gif) 7 | 8 | 9 | 不过这不是重点,重点是我们要实现的下图所示效果:垂直的TabLayout以及垂直的ViewPager,并完成二者的联动: 10 | 11 | ![这里写图片描述](https://github.com/soulrelay/VerticalViewPagerWithTabLayout/blob/master/sample/src/main/res/raw/screenshot2.gif) 12 | 13 | 14 | 下面这张是我分享的Demo示意图: 15 | 16 | ![这里写图片描述](https://github.com/soulrelay/VerticalViewPagerWithTabLayout/blob/master/sample/src/main/res/raw/screenshot3.gif) 17 | 18 | 19 | ## 问题 20 | 这里不对相关代码做过多说明,使用的Github上造好的轮子,然后根据自己的业务需求做的相关改动,因为时间比较紧,这里聊聊期间碰到的困难 21 | >* 方案调研过程中,第一套方案采用的VerticalViewPager继承自ViewPager,通过将Event的横向和纵向坐标进行交换完成ViewPager的垂直效果,但是会出现滑动冲突,即配合嵌套有RecyclerView或者ListView的Fragment会出现向下滑动不灵敏的问题,指定时间内解决效果不理想,用户体验不好 ,暂时放弃、 22 | >* 第二套解决方案使用的VerticalViewPager继承自ViewGroup,按照作者的说明(Small library allowing you to have a VerticalViewPager. It's just a copy paste from the v19 ViewPager available in the support lib, where I changed all the left/right into top/bottom and X into Y.),同时代码解决了(VerticalViewPager scroll doesnt work when listview is used inside one of the fragment)的问题,结合VerticalTabLayout完美使用 23 | >* 通过VerticalTabLayout的OnTabSelectedListener与VerticalViewPager的OnPageChangeListener完成二者之间的联动,通过VerticalViewPager的PageTransformer完成垂直ViewPager的自定义切换效果 24 | >* VerticalTabLayout和VerticalViewPager通过线性布局水平放置,使用layout_weight进行比例分割,注意layout_width设置为0,否则可能导致右边VerticalViewPager中嵌套的数据显示不居中的问题 25 | >* 期间设置Fragment时碰到一个没有解决的问题(Fragment with ViewPager setCustomAnimations not working),有知识的大大望不吝赐教 26 | >* 还有就是业务逻辑 接口设计方面的问题,我觉得如果有相关竞品,相对成熟的可以作为参考(反编译APK查看布局代码,抓包查看相关接口和请求机制),为我们提供一种思路,然后基于我们自己的需求取其精华弃其糟粕 27 | 28 | ## 参考链接 29 | [VerticalTabLayout](https://github.com/qstumn/VerticalTabLayout) 30 | 31 | [VerticalViewPager](https://github.com/castorflex/VerticalViewPager) 32 | 33 | [android design library提供的TabLayout的用法](http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0731/3247.html) -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | } 5 | dependencies { 6 | classpath 'com.android.tools.build:gradle:2.2.2' 7 | } 8 | } 9 | 10 | allprojects { 11 | version = VERSION_NAME 12 | group = GROUP 13 | 14 | repositories { 15 | mavenCentral() 16 | } 17 | } 18 | 19 | 20 | apply plugin: 'android-reporting' -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | VERSION_NAME=19.0.1 2 | VERSION_CODE=2 3 | GROUP=com.github.castorflex.verticalviewpager 4 | 5 | #storeFile=nice try 6 | #keyAlias=nice try 7 | #storePassword=nice try 8 | #keyPassword=nice try 9 | 10 | POM_DESCRIPTION=Android Library to have a vertical ViewPager 11 | POM_URL=https://github.com/castorflex/VerticalViewPager 12 | POM_SCM_URL=https://github.com/castorflex/VerticalViewPager 13 | POM_SCM_CONNECTION=scm:git@github.com:castorflex/VerticalViewPager.git 14 | POM_SCM_DEV_CONNECTION=scm:git@github.com:castorflex/VerticalViewPager.git 15 | POM_LICENCE_NAME=THE BEER-WARE LICENSE, Revision 42 16 | POM_LICENCE_URL=https://en.wikipedia.org/wiki/Beerware 17 | POM_LICENCE_DIST=repo 18 | POM_DEVELOPER_ID=castorflex 19 | POM_DEVELOPER_NAME=Antoine Merle 20 | 21 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soulrelay/VerticalViewPagerWithTabLayout/1188cfb010811a263aa16deedd7cc11bda1fe90a/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Nov 03 16:40:35 CST 2016 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.14.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | repositories { 4 | mavenCentral() 5 | } 6 | 7 | dependencies { 8 | compile 'com.android.support:support-v4:20.0.0' 9 | } 10 | 11 | android { 12 | compileSdkVersion 23 13 | buildToolsVersion "23.0.3" 14 | 15 | defaultConfig { 16 | 17 | minSdkVersion 14 18 | targetSdkVersion 20 19 | versionCode 1 20 | versionName "1.0" 21 | } 22 | buildTypes { 23 | release { 24 | //runProguard false 25 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 26 | } 27 | } 28 | compileOptions { 29 | sourceCompatibility JavaVersion.VERSION_1_7 30 | targetCompatibility JavaVersion.VERSION_1_7 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /library/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=VerticalViewPager Library 2 | POM_ARTIFACT_ID=library 3 | POM_PACKAGING=aar -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /library/src/main/java/fr/castorflex/android/verticalviewpager/VerticalViewPager.java: -------------------------------------------------------------------------------- 1 | package fr.castorflex.android.verticalviewpager; 2 | 3 | import android.content.Context; 4 | import android.content.res.Resources; 5 | import android.content.res.TypedArray; 6 | import android.database.DataSetObserver; 7 | import android.graphics.Canvas; 8 | import android.graphics.Rect; 9 | import android.graphics.drawable.Drawable; 10 | import android.os.Build; 11 | import android.os.Bundle; 12 | import android.os.Parcel; 13 | import android.os.Parcelable; 14 | import android.os.SystemClock; 15 | import android.support.v4.os.ParcelableCompat; 16 | import android.support.v4.os.ParcelableCompatCreatorCallbacks; 17 | import android.support.v4.view.AccessibilityDelegateCompat; 18 | import android.support.v4.view.KeyEventCompat; 19 | import android.support.v4.view.MotionEventCompat; 20 | import android.support.v4.view.PagerAdapter; 21 | import android.support.v4.view.VelocityTrackerCompat; 22 | import android.support.v4.view.ViewCompat; 23 | import android.support.v4.view.ViewConfigurationCompat; 24 | import android.support.v4.view.ViewPager; 25 | import android.support.v4.view.accessibility.AccessibilityEventCompat; 26 | import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; 27 | import android.support.v4.view.accessibility.AccessibilityRecordCompat; 28 | import android.support.v4.widget.EdgeEffectCompat; 29 | import android.util.AttributeSet; 30 | import android.util.Log; 31 | import android.view.FocusFinder; 32 | import android.view.Gravity; 33 | import android.view.KeyEvent; 34 | import android.view.MotionEvent; 35 | import android.view.SoundEffectConstants; 36 | import android.view.VelocityTracker; 37 | import android.view.View; 38 | import android.view.ViewConfiguration; 39 | import android.view.ViewGroup; 40 | import android.view.ViewParent; 41 | import android.view.accessibility.AccessibilityEvent; 42 | import android.view.animation.Interpolator; 43 | import android.widget.Scroller; 44 | 45 | import java.lang.reflect.Method; 46 | import java.util.ArrayList; 47 | import java.util.Collections; 48 | import java.util.Comparator; 49 | 50 | /** 51 | * Created by castorflex on 12/29/13. 52 | * Just a copy of the original ViewPager modified to support vertical Scrolling 53 | */ 54 | public class VerticalViewPager extends ViewGroup { 55 | private static final String TAG = "ViewPager"; 56 | private static final boolean DEBUG = false; 57 | 58 | private static final boolean USE_CACHE = false; 59 | 60 | private static final int DEFAULT_OFFSCREEN_PAGES = 1; 61 | private static final int MAX_SETTLE_DURATION = 600; // ms 62 | private static final int MIN_DISTANCE_FOR_FLING = 25; // dips 63 | 64 | private static final int DEFAULT_GUTTER_SIZE = 16; // dips 65 | 66 | private static final int MIN_FLING_VELOCITY = 400; // dips 67 | 68 | private static final int[] LAYOUT_ATTRS = new int[]{ 69 | android.R.attr.layout_gravity 70 | }; 71 | 72 | /** 73 | * Used to track what the expected number of items in the adapter should be. 74 | * If the app changes this when we don't expect it, we'll throw a big obnoxious exception. 75 | */ 76 | private int mExpectedAdapterCount; 77 | 78 | static class ItemInfo { 79 | Object object; 80 | int position; 81 | boolean scrolling; 82 | float heightFactor; 83 | float offset; 84 | } 85 | 86 | private static final Comparator COMPARATOR = new Comparator() { 87 | @Override 88 | public int compare(ItemInfo lhs, ItemInfo rhs) { 89 | return lhs.position - rhs.position; 90 | } 91 | }; 92 | 93 | private static final Interpolator sInterpolator = new Interpolator() { 94 | public float getInterpolation(float t) { 95 | t -= 1.0f; 96 | return t * t * t * t * t + 1.0f; 97 | } 98 | }; 99 | 100 | private final ArrayList mItems = new ArrayList<>(); 101 | private final ItemInfo mTempItem = new ItemInfo(); 102 | 103 | private final Rect mTempRect = new Rect(); 104 | 105 | private PagerAdapter mAdapter; 106 | private int mCurItem; // Index of currently displayed page. 107 | private int mRestoredCurItem = -1; 108 | private Parcelable mRestoredAdapterState = null; 109 | private ClassLoader mRestoredClassLoader = null; 110 | private Scroller mScroller; 111 | private PagerObserver mObserver; 112 | 113 | private int mPageMargin; 114 | private Drawable mMarginDrawable; 115 | private int mLeftPageBounds; 116 | private int mRightPageBounds; 117 | 118 | // Offsets of the first and last items, if known. 119 | // Set during population, used to determine if we are at the beginning 120 | // or end of the pager data set during touch scrolling. 121 | private float mFirstOffset = -Float.MAX_VALUE; 122 | private float mLastOffset = Float.MAX_VALUE; 123 | 124 | private int mChildWidthMeasureSpec; 125 | private int mChildHeightMeasureSpec; 126 | private boolean mInLayout; 127 | 128 | private boolean mScrollingCacheEnabled; 129 | 130 | private boolean mPopulatePending; 131 | private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES; 132 | 133 | private boolean mIsBeingDragged; 134 | private boolean mIsUnableToDrag; 135 | private boolean mIgnoreGutter; 136 | private int mDefaultGutterSize; 137 | private int mGutterSize; 138 | private int mTouchSlop; 139 | /** 140 | * Position of the last motion event. 141 | */ 142 | private float mLastMotionX; 143 | private float mLastMotionY; 144 | private float mInitialMotionX; 145 | private float mInitialMotionY; 146 | /** 147 | * ID of the active pointer. This is used to retain consistency during 148 | * drags/flings if multiple pointers are used. 149 | */ 150 | private int mActivePointerId = INVALID_POINTER; 151 | /** 152 | * Sentinel value for no current active pointer. 153 | * Used by {@link #mActivePointerId}. 154 | */ 155 | private static final int INVALID_POINTER = -1; 156 | 157 | /** 158 | * Determines speed during touch scrolling 159 | */ 160 | private VelocityTracker mVelocityTracker; 161 | private int mMinimumVelocity; 162 | private int mMaximumVelocity; 163 | private int mFlingDistance; 164 | private int mCloseEnough; 165 | 166 | // If the pager is at least this close to its final position, complete the scroll 167 | // on touch down and let the user interact with the content inside instead of 168 | // "catching" the flinging pager. 169 | private static final int CLOSE_ENOUGH = 2; // dp 170 | 171 | private boolean mFakeDragging; 172 | private long mFakeDragBeginTime; 173 | 174 | private EdgeEffectCompat mTopEdge; 175 | private EdgeEffectCompat mBottomEdge; 176 | 177 | private boolean mFirstLayout = true; 178 | private boolean mNeedCalculatePageOffsets = false; 179 | private boolean mCalledSuper; 180 | private int mDecorChildCount; 181 | 182 | private ViewPager.OnPageChangeListener mOnPageChangeListener; 183 | private ViewPager.OnPageChangeListener mInternalPageChangeListener; 184 | private OnAdapterChangeListener mAdapterChangeListener; 185 | private ViewPager.PageTransformer mPageTransformer; 186 | private Method mSetChildrenDrawingOrderEnabled; 187 | 188 | private static final int DRAW_ORDER_DEFAULT = 0; 189 | private static final int DRAW_ORDER_FORWARD = 1; 190 | private static final int DRAW_ORDER_REVERSE = 2; 191 | private int mDrawingOrder; 192 | private ArrayList mDrawingOrderedChildren; 193 | private static final ViewPositionComparator sPositionComparator = new ViewPositionComparator(); 194 | 195 | /** 196 | * Indicates that the pager is in an idle, settled state. The current page 197 | * is fully in view and no animation is in progress. 198 | */ 199 | public static final int SCROLL_STATE_IDLE = 0; 200 | 201 | /** 202 | * Indicates that the pager is currently being dragged by the user. 203 | */ 204 | public static final int SCROLL_STATE_DRAGGING = 1; 205 | 206 | /** 207 | * Indicates that the pager is in the process of settling to a final position. 208 | */ 209 | public static final int SCROLL_STATE_SETTLING = 2; 210 | 211 | private final Runnable mEndScrollRunnable = new Runnable() { 212 | public void run() { 213 | setScrollState(SCROLL_STATE_IDLE); 214 | populate(); 215 | } 216 | }; 217 | 218 | private int mScrollState = SCROLL_STATE_IDLE; 219 | 220 | /** 221 | * Used internally to monitor when adapters are switched. 222 | */ 223 | interface OnAdapterChangeListener { 224 | public void onAdapterChanged(PagerAdapter oldAdapter, PagerAdapter newAdapter); 225 | } 226 | 227 | /** 228 | * Used internally to tag special types of child views that should be added as 229 | * pager decorations by default. 230 | */ 231 | interface Decor { 232 | } 233 | 234 | public VerticalViewPager(Context context) { 235 | super(context); 236 | initViewPager(); 237 | } 238 | 239 | public VerticalViewPager(Context context, AttributeSet attrs) { 240 | super(context, attrs); 241 | initViewPager(); 242 | } 243 | 244 | void initViewPager() { 245 | setWillNotDraw(false); 246 | setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); 247 | setFocusable(true); 248 | final Context context = getContext(); 249 | mScroller = new Scroller(context, sInterpolator); 250 | final ViewConfiguration configuration = ViewConfiguration.get(context); 251 | final float density = context.getResources().getDisplayMetrics().density; 252 | 253 | mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration); 254 | mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density); 255 | mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 256 | mTopEdge = new EdgeEffectCompat(context); 257 | mBottomEdge = new EdgeEffectCompat(context); 258 | 259 | mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density); 260 | mCloseEnough = (int) (CLOSE_ENOUGH * density); 261 | mDefaultGutterSize = (int) (DEFAULT_GUTTER_SIZE * density); 262 | 263 | ViewCompat.setAccessibilityDelegate(this, new MyAccessibilityDelegate()); 264 | 265 | if (ViewCompat.getImportantForAccessibility(this) 266 | == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { 267 | ViewCompat.setImportantForAccessibility(this, 268 | ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); 269 | } 270 | } 271 | 272 | @Override 273 | protected void onDetachedFromWindow() { 274 | removeCallbacks(mEndScrollRunnable); 275 | super.onDetachedFromWindow(); 276 | } 277 | 278 | private void setScrollState(int newState) { 279 | if (mScrollState == newState) { 280 | return; 281 | } 282 | 283 | mScrollState = newState; 284 | if (mPageTransformer != null) { 285 | // PageTransformers can do complex things that benefit from hardware layers. 286 | enableLayers(newState != SCROLL_STATE_IDLE); 287 | } 288 | if (mOnPageChangeListener != null) { 289 | mOnPageChangeListener.onPageScrollStateChanged(newState); 290 | } 291 | } 292 | 293 | /** 294 | * Set a PagerAdapter that will supply views for this pager as needed. 295 | * 296 | * @param adapter Adapter to use 297 | */ 298 | public void setAdapter(PagerAdapter adapter) { 299 | if (mAdapter != null) { 300 | mAdapter.unregisterDataSetObserver(mObserver); 301 | mAdapter.startUpdate(this); 302 | for (int i = 0; i < mItems.size(); i++) { 303 | final ItemInfo ii = mItems.get(i); 304 | mAdapter.destroyItem(this, ii.position, ii.object); 305 | } 306 | mAdapter.finishUpdate(this); 307 | mItems.clear(); 308 | removeNonDecorViews(); 309 | mCurItem = 0; 310 | scrollTo(0, 0); 311 | } 312 | 313 | final PagerAdapter oldAdapter = mAdapter; 314 | mAdapter = adapter; 315 | mExpectedAdapterCount = 0; 316 | 317 | if (mAdapter != null) { 318 | if (mObserver == null) { 319 | mObserver = new PagerObserver(); 320 | } 321 | mAdapter.registerDataSetObserver(mObserver); 322 | mPopulatePending = false; 323 | final boolean wasFirstLayout = mFirstLayout; 324 | mFirstLayout = true; 325 | mExpectedAdapterCount = mAdapter.getCount(); 326 | if (mRestoredCurItem >= 0 && mRestoredAdapterState != null && mRestoredClassLoader != null) { 327 | mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader); 328 | setCurrentItemInternal(mRestoredCurItem, false, true); 329 | mRestoredCurItem = -1; 330 | mRestoredAdapterState = null; 331 | mRestoredClassLoader = null; 332 | } else if (!wasFirstLayout) { 333 | populate(); 334 | } else { 335 | requestLayout(); 336 | } 337 | } 338 | 339 | if (mAdapterChangeListener != null && oldAdapter != adapter) { 340 | mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter); 341 | } 342 | } 343 | 344 | private void removeNonDecorViews() { 345 | for (int i = 0; i < getChildCount(); i++) { 346 | final View child = getChildAt(i); 347 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 348 | if (!lp.isDecor) { 349 | removeViewAt(i); 350 | i--; 351 | } 352 | } 353 | } 354 | 355 | /** 356 | * Retrieve the current adapter supplying pages. 357 | * 358 | * @return The currently registered PagerAdapter 359 | */ 360 | public PagerAdapter getAdapter() { 361 | return mAdapter; 362 | } 363 | 364 | void setOnAdapterChangeListener(OnAdapterChangeListener listener) { 365 | mAdapterChangeListener = listener; 366 | } 367 | 368 | // private int getClientWidth() { 369 | // return getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); 370 | // } 371 | 372 | private int getClientHeight() { 373 | return getMeasuredHeight() - getPaddingTop() - getPaddingBottom(); 374 | } 375 | 376 | 377 | /** 378 | * Set the currently selected page. If the ViewPager has already been through its first 379 | * layout with its current adapter there will be a smooth animated transition between 380 | * the current item and the specified item. 381 | * 382 | * @param item Item index to select 383 | */ 384 | public void setCurrentItem(int item) { 385 | mPopulatePending = false; 386 | setCurrentItemInternal(item, !mFirstLayout, false); 387 | } 388 | 389 | /** 390 | * Set the currently selected page. 391 | * 392 | * @param item Item index to select 393 | * @param smoothScroll True to smoothly scroll to the new item, false to transition immediately 394 | */ 395 | public void setCurrentItem(int item, boolean smoothScroll) { 396 | mPopulatePending = false; 397 | setCurrentItemInternal(item, smoothScroll, false); 398 | } 399 | 400 | public int getCurrentItem() { 401 | return mCurItem; 402 | } 403 | 404 | void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) { 405 | setCurrentItemInternal(item, smoothScroll, always, 0); 406 | } 407 | 408 | void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) { 409 | if (mAdapter == null || mAdapter.getCount() <= 0) { 410 | setScrollingCacheEnabled(false); 411 | return; 412 | } 413 | if (!always && mCurItem == item && mItems.size() != 0) { 414 | setScrollingCacheEnabled(false); 415 | return; 416 | } 417 | 418 | if (item < 0) { 419 | item = 0; 420 | } else if (item >= mAdapter.getCount()) { 421 | item = mAdapter.getCount() - 1; 422 | } 423 | final int pageLimit = mOffscreenPageLimit; 424 | if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) { 425 | // We are doing a jump by more than one page. To avoid 426 | // glitches, we want to keep all current pages in the view 427 | // until the scroll ends. 428 | for (int i = 0; i < mItems.size(); i++) { 429 | mItems.get(i).scrolling = true; 430 | } 431 | } 432 | final boolean dispatchSelected = mCurItem != item; 433 | 434 | if (mFirstLayout) { 435 | // We don't have any idea how big we are yet and shouldn't have any pages either. 436 | // Just set things up and let the pending layout handle things. 437 | mCurItem = item; 438 | if (dispatchSelected && mOnPageChangeListener != null) { 439 | mOnPageChangeListener.onPageSelected(item); 440 | } 441 | if (dispatchSelected && mInternalPageChangeListener != null) { 442 | mInternalPageChangeListener.onPageSelected(item); 443 | } 444 | requestLayout(); 445 | } else { 446 | populate(item); 447 | scrollToItem(item, smoothScroll, velocity, dispatchSelected); 448 | } 449 | } 450 | 451 | private void scrollToItem(int item, boolean smoothScroll, int velocity, 452 | boolean dispatchSelected) { 453 | final ItemInfo curInfo = infoForPosition(item); 454 | int destY = 0; 455 | if (curInfo != null) { 456 | final int height = getClientHeight(); 457 | destY = (int) (height * Math.max(mFirstOffset, 458 | Math.min(curInfo.offset, mLastOffset))); 459 | } 460 | if (smoothScroll) { 461 | smoothScrollTo(0, destY, velocity); 462 | if (dispatchSelected && mOnPageChangeListener != null) { 463 | mOnPageChangeListener.onPageSelected(item); 464 | } 465 | if (dispatchSelected && mInternalPageChangeListener != null) { 466 | mInternalPageChangeListener.onPageSelected(item); 467 | } 468 | } else { 469 | if (dispatchSelected && mOnPageChangeListener != null) { 470 | mOnPageChangeListener.onPageSelected(item); 471 | } 472 | if (dispatchSelected && mInternalPageChangeListener != null) { 473 | mInternalPageChangeListener.onPageSelected(item); 474 | } 475 | completeScroll(false); 476 | scrollTo(0, destY); 477 | pageScrolled(destY); 478 | } 479 | } 480 | 481 | /** 482 | * Set a listener that will be invoked whenever the page changes or is incrementally 483 | * scrolled. See {@link android.support.v4.view.ViewPager.OnPageChangeListener}. 484 | * 485 | * @param listener Listener to set 486 | */ 487 | public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) { 488 | mOnPageChangeListener = listener; 489 | } 490 | 491 | /** 492 | * Set a {@link android.support.v4.view.ViewPager.PageTransformer} that will be called for each attached page whenever 493 | * the scroll position is changed. This allows the application to apply custom property 494 | * transformations to each page, overriding the default sliding look and feel. 495 | *

496 | *

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

498 | * 499 | * @param reverseDrawingOrder true if the supplied PageTransformer requires page views 500 | * to be drawn from last to first instead of first to last. 501 | * @param transformer PageTransformer that will modify each page's animation properties 502 | */ 503 | public void setPageTransformer(boolean reverseDrawingOrder, ViewPager.PageTransformer transformer) { 504 | if (Build.VERSION.SDK_INT >= 11) { 505 | final boolean hasTransformer = transformer != null; 506 | final boolean needsPopulate = hasTransformer != (mPageTransformer != null); 507 | mPageTransformer = transformer; 508 | setChildrenDrawingOrderEnabledCompat(hasTransformer); 509 | if (hasTransformer) { 510 | mDrawingOrder = reverseDrawingOrder ? DRAW_ORDER_REVERSE : DRAW_ORDER_FORWARD; 511 | } else { 512 | mDrawingOrder = DRAW_ORDER_DEFAULT; 513 | } 514 | if (needsPopulate) populate(); 515 | } 516 | } 517 | 518 | void setChildrenDrawingOrderEnabledCompat(boolean enable) { 519 | if (Build.VERSION.SDK_INT >= 7) { 520 | if (mSetChildrenDrawingOrderEnabled == null) { 521 | try { 522 | mSetChildrenDrawingOrderEnabled = ViewGroup.class.getDeclaredMethod( 523 | "setChildrenDrawingOrderEnabled", new Class[]{Boolean.TYPE}); 524 | } catch (NoSuchMethodException e) { 525 | Log.e(TAG, "Can't find setChildrenDrawingOrderEnabled", e); 526 | } 527 | } 528 | try { 529 | mSetChildrenDrawingOrderEnabled.invoke(this, enable); 530 | } catch (Exception e) { 531 | Log.e(TAG, "Error changing children drawing order", e); 532 | } 533 | } 534 | } 535 | 536 | @Override 537 | protected int getChildDrawingOrder(int childCount, int i) { 538 | final int index = mDrawingOrder == DRAW_ORDER_REVERSE ? childCount - 1 - i : i; 539 | final int result = ((LayoutParams) mDrawingOrderedChildren.get(index).getLayoutParams()).childIndex; 540 | return result; 541 | } 542 | 543 | /** 544 | * Set a separate OnPageChangeListener for internal use by the support library. 545 | * 546 | * @param listener Listener to set 547 | * @return The old listener that was set, if any. 548 | */ 549 | ViewPager.OnPageChangeListener setInternalPageChangeListener(ViewPager.OnPageChangeListener listener) { 550 | ViewPager.OnPageChangeListener oldListener = mInternalPageChangeListener; 551 | mInternalPageChangeListener = listener; 552 | return oldListener; 553 | } 554 | 555 | /** 556 | * Returns the number of pages that will be retained to either side of the 557 | * current page in the view hierarchy in an idle state. Defaults to 1. 558 | * 559 | * @return How many pages will be kept offscreen on either side 560 | * @see #setOffscreenPageLimit(int) 561 | */ 562 | public int getOffscreenPageLimit() { 563 | return mOffscreenPageLimit; 564 | } 565 | 566 | /** 567 | * Set the number of pages that should be retained to either side of the 568 | * current page in the view hierarchy in an idle state. Pages beyond this 569 | * limit will be recreated from the adapter when needed. 570 | *

571 | *

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

577 | *

578 | *

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

580 | * 581 | * @param limit How many pages will be kept offscreen in an idle state. 582 | */ 583 | public void setOffscreenPageLimit(int limit) { 584 | if (limit < DEFAULT_OFFSCREEN_PAGES) { 585 | Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " + 586 | DEFAULT_OFFSCREEN_PAGES); 587 | limit = DEFAULT_OFFSCREEN_PAGES; 588 | } 589 | if (limit != mOffscreenPageLimit) { 590 | mOffscreenPageLimit = limit; 591 | populate(); 592 | } 593 | } 594 | 595 | /** 596 | * Set the margin between pages. 597 | * 598 | * @param marginPixels Distance between adjacent pages in pixels 599 | * @see #getPageMargin() 600 | * @see #setPageMarginDrawable(Drawable) 601 | * @see #setPageMarginDrawable(int) 602 | */ 603 | public void setPageMargin(int marginPixels) { 604 | final int oldMargin = mPageMargin; 605 | mPageMargin = marginPixels; 606 | 607 | final int height = getHeight(); 608 | recomputeScrollPosition(height, height, marginPixels, oldMargin); 609 | 610 | requestLayout(); 611 | } 612 | 613 | /** 614 | * Return the margin between pages. 615 | * 616 | * @return The size of the margin in pixels 617 | */ 618 | public int getPageMargin() { 619 | return mPageMargin; 620 | } 621 | 622 | /** 623 | * Set a drawable that will be used to fill the margin between pages. 624 | * 625 | * @param d Drawable to display between pages 626 | */ 627 | public void setPageMarginDrawable(Drawable d) { 628 | mMarginDrawable = d; 629 | if (d != null) refreshDrawableState(); 630 | setWillNotDraw(d == null); 631 | invalidate(); 632 | } 633 | 634 | /** 635 | * Set a drawable that will be used to fill the margin between pages. 636 | * 637 | * @param resId Resource ID of a drawable to display between pages 638 | */ 639 | public void setPageMarginDrawable(int resId) { 640 | setPageMarginDrawable(getContext().getResources().getDrawable(resId)); 641 | } 642 | 643 | @Override 644 | protected boolean verifyDrawable(Drawable who) { 645 | return super.verifyDrawable(who) || who == mMarginDrawable; 646 | } 647 | 648 | @Override 649 | protected void drawableStateChanged() { 650 | super.drawableStateChanged(); 651 | final Drawable d = mMarginDrawable; 652 | if (d != null && d.isStateful()) { 653 | d.setState(getDrawableState()); 654 | } 655 | } 656 | 657 | // We want the duration of the page snap animation to be influenced by the distance that 658 | // the screen has to travel, however, we don't want this duration to be effected in a 659 | // purely linear fashion. Instead, we use this method to moderate the effect that the distance 660 | // of travel has on the overall snap duration. 661 | float distanceInfluenceForSnapDuration(float f) { 662 | f -= 0.5f; // center the values about 0. 663 | f *= 0.3f * Math.PI / 2.0f; 664 | return (float) Math.sin(f); 665 | } 666 | 667 | /** 668 | * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. 669 | * 670 | * @param x the number of pixels to scroll by on the X axis 671 | * @param y the number of pixels to scroll by on the Y axis 672 | */ 673 | void smoothScrollTo(int x, int y) { 674 | smoothScrollTo(x, y, 0); 675 | } 676 | 677 | /** 678 | * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. 679 | * 680 | * @param x the number of pixels to scroll by on the X axis 681 | * @param y the number of pixels to scroll by on the Y axis 682 | * @param velocity the velocity associated with a fling, if applicable. (0 otherwise) 683 | */ 684 | void smoothScrollTo(int x, int y, int velocity) { 685 | if (getChildCount() == 0) { 686 | // Nothing to do. 687 | setScrollingCacheEnabled(false); 688 | return; 689 | } 690 | int sx = getScrollX(); 691 | int sy = getScrollY(); 692 | int dx = x - sx; 693 | int dy = y - sy; 694 | if (dx == 0 && dy == 0) { 695 | completeScroll(false); 696 | populate(); 697 | setScrollState(SCROLL_STATE_IDLE); 698 | return; 699 | } 700 | 701 | setScrollingCacheEnabled(true); 702 | setScrollState(SCROLL_STATE_SETTLING); 703 | 704 | final int height = getClientHeight(); 705 | final int halfHeight = height / 2; 706 | final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / height); 707 | final float distance = halfHeight + halfHeight * 708 | distanceInfluenceForSnapDuration(distanceRatio); 709 | 710 | int duration = 0; 711 | velocity = Math.abs(velocity); 712 | if (velocity > 0) { 713 | duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); 714 | } else { 715 | final float pageHeight = height * mAdapter.getPageWidth(mCurItem); 716 | final float pageDelta = (float) Math.abs(dx) / (pageHeight + mPageMargin); 717 | duration = (int) ((pageDelta + 1) * 100); 718 | } 719 | duration = Math.min(duration, MAX_SETTLE_DURATION); 720 | 721 | mScroller.startScroll(sx, sy, dx, dy, duration); 722 | ViewCompat.postInvalidateOnAnimation(this); 723 | } 724 | 725 | ItemInfo addNewItem(int position, int index) { 726 | ItemInfo ii = new ItemInfo(); 727 | ii.position = position; 728 | ii.object = mAdapter.instantiateItem(this, position); 729 | ii.heightFactor = mAdapter.getPageWidth(position); 730 | if (index < 0 || index >= mItems.size()) { 731 | mItems.add(ii); 732 | } else { 733 | mItems.add(index, ii); 734 | } 735 | return ii; 736 | } 737 | 738 | void dataSetChanged() { 739 | // This method only gets called if our observer is attached, so mAdapter is non-null. 740 | 741 | final int adapterCount = mAdapter.getCount(); 742 | mExpectedAdapterCount = adapterCount; 743 | boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 && 744 | mItems.size() < adapterCount; 745 | int newCurrItem = mCurItem; 746 | 747 | boolean isUpdating = false; 748 | for (int i = 0; i < mItems.size(); i++) { 749 | final ItemInfo ii = mItems.get(i); 750 | final int newPos = mAdapter.getItemPosition(ii.object); 751 | 752 | if (newPos == PagerAdapter.POSITION_UNCHANGED) { 753 | continue; 754 | } 755 | 756 | if (newPos == PagerAdapter.POSITION_NONE) { 757 | mItems.remove(i); 758 | i--; 759 | 760 | if (!isUpdating) { 761 | mAdapter.startUpdate(this); 762 | isUpdating = true; 763 | } 764 | 765 | mAdapter.destroyItem(this, ii.position, ii.object); 766 | needPopulate = true; 767 | 768 | if (mCurItem == ii.position) { 769 | // Keep the current item in the valid range 770 | newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1)); 771 | needPopulate = true; 772 | } 773 | continue; 774 | } 775 | 776 | if (ii.position != newPos) { 777 | if (ii.position == mCurItem) { 778 | // Our current item changed position. Follow it. 779 | newCurrItem = newPos; 780 | } 781 | 782 | ii.position = newPos; 783 | needPopulate = true; 784 | } 785 | } 786 | 787 | if (isUpdating) { 788 | mAdapter.finishUpdate(this); 789 | } 790 | 791 | Collections.sort(mItems, COMPARATOR); 792 | 793 | if (needPopulate) { 794 | // Reset our known page widths; populate will recompute them. 795 | final int childCount = getChildCount(); 796 | for (int i = 0; i < childCount; i++) { 797 | final View child = getChildAt(i); 798 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 799 | if (!lp.isDecor) { 800 | lp.heightFactor = 0.f; 801 | } 802 | } 803 | 804 | setCurrentItemInternal(newCurrItem, false, true); 805 | requestLayout(); 806 | } 807 | } 808 | 809 | void populate() { 810 | populate(mCurItem); 811 | } 812 | 813 | void populate(int newCurrentItem) { 814 | ItemInfo oldCurInfo = null; 815 | int focusDirection = View.FOCUS_FORWARD; 816 | if (mCurItem != newCurrentItem) { 817 | focusDirection = mCurItem < newCurrentItem ? View.FOCUS_DOWN : View.FOCUS_UP; 818 | oldCurInfo = infoForPosition(mCurItem); 819 | mCurItem = newCurrentItem; 820 | } 821 | 822 | if (mAdapter == null) { 823 | sortChildDrawingOrder(); 824 | return; 825 | } 826 | 827 | // Bail now if we are waiting to populate. This is to hold off 828 | // on creating views from the time the user releases their finger to 829 | // fling to a new position until we have finished the scroll to 830 | // that position, avoiding glitches from happening at that point. 831 | if (mPopulatePending) { 832 | if (DEBUG) Log.i(TAG, "populate is pending, skipping for now..."); 833 | sortChildDrawingOrder(); 834 | return; 835 | } 836 | 837 | // Also, don't populate until we are attached to a window. This is to 838 | // avoid trying to populate before we have restored our view hierarchy 839 | // state and conflicting with what is restored. 840 | if (getWindowToken() == null) { 841 | return; 842 | } 843 | 844 | mAdapter.startUpdate(this); 845 | 846 | final int pageLimit = mOffscreenPageLimit; 847 | final int startPos = Math.max(0, mCurItem - pageLimit); 848 | final int N = mAdapter.getCount(); 849 | final int endPos = Math.min(N - 1, mCurItem + pageLimit); 850 | 851 | if (N != mExpectedAdapterCount) { 852 | String resName; 853 | try { 854 | resName = getResources().getResourceName(getId()); 855 | } catch (Resources.NotFoundException e) { 856 | resName = Integer.toHexString(getId()); 857 | } 858 | throw new IllegalStateException("The application's PagerAdapter changed the adapter's" + 859 | " contents without calling PagerAdapter#notifyDataSetChanged!" + 860 | " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N + 861 | " Pager id: " + resName + 862 | " Pager class: " + getClass() + 863 | " Problematic adapter: " + mAdapter.getClass()); 864 | } 865 | 866 | // Locate the currently focused item or add it if needed. 867 | int curIndex = -1; 868 | ItemInfo curItem = null; 869 | for (curIndex = 0; curIndex < mItems.size(); curIndex++) { 870 | final ItemInfo ii = mItems.get(curIndex); 871 | if (ii.position >= mCurItem) { 872 | if (ii.position == mCurItem) curItem = ii; 873 | break; 874 | } 875 | } 876 | 877 | if (curItem == null && N > 0) { 878 | curItem = addNewItem(mCurItem, curIndex); 879 | } 880 | 881 | // Fill 3x the available width or up to the number of offscreen 882 | // pages requested to either side, whichever is larger. 883 | // If we have no current item we have no work to do. 884 | if (curItem != null) { 885 | float extraHeightTop = 0.f; 886 | int itemIndex = curIndex - 1; 887 | ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 888 | final int clientHeight = getClientHeight(); 889 | final float topHeightNeeded = clientHeight <= 0 ? 0 : 890 | 2.f - curItem.heightFactor + (float) getPaddingLeft() / (float) clientHeight; 891 | for (int pos = mCurItem - 1; pos >= 0; pos--) { 892 | if (extraHeightTop >= topHeightNeeded && pos < startPos) { 893 | if (ii == null) { 894 | break; 895 | } 896 | if (pos == ii.position && !ii.scrolling) { 897 | mItems.remove(itemIndex); 898 | mAdapter.destroyItem(this, pos, ii.object); 899 | if (DEBUG) { 900 | Log.i(TAG, "populate() - destroyItem() with pos: " + pos + 901 | " view: " + ((View) ii.object)); 902 | } 903 | itemIndex--; 904 | curIndex--; 905 | ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 906 | } 907 | } else if (ii != null && pos == ii.position) { 908 | extraHeightTop += ii.heightFactor; 909 | itemIndex--; 910 | ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 911 | } else { 912 | ii = addNewItem(pos, itemIndex + 1); 913 | extraHeightTop += ii.heightFactor; 914 | curIndex++; 915 | ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 916 | } 917 | } 918 | 919 | float extraHeightBottom = curItem.heightFactor; 920 | itemIndex = curIndex + 1; 921 | if (extraHeightBottom < 2.f) { 922 | ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 923 | final float bottomHeightNeeded = clientHeight <= 0 ? 0 : 924 | (float) getPaddingRight() / (float) clientHeight + 2.f; 925 | for (int pos = mCurItem + 1; pos < N; pos++) { 926 | if (extraHeightBottom >= bottomHeightNeeded && pos > endPos) { 927 | if (ii == null) { 928 | break; 929 | } 930 | if (pos == ii.position && !ii.scrolling) { 931 | mItems.remove(itemIndex); 932 | mAdapter.destroyItem(this, pos, ii.object); 933 | if (DEBUG) { 934 | Log.i(TAG, "populate() - destroyItem() with pos: " + pos + 935 | " view: " + ((View) ii.object)); 936 | } 937 | ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 938 | } 939 | } else if (ii != null && pos == ii.position) { 940 | extraHeightBottom += ii.heightFactor; 941 | itemIndex++; 942 | ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 943 | } else { 944 | ii = addNewItem(pos, itemIndex); 945 | itemIndex++; 946 | extraHeightBottom += ii.heightFactor; 947 | ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 948 | } 949 | } 950 | } 951 | 952 | calculatePageOffsets(curItem, curIndex, oldCurInfo); 953 | } 954 | 955 | if (DEBUG) { 956 | Log.i(TAG, "Current page list:"); 957 | for (int i = 0; i < mItems.size(); i++) { 958 | Log.i(TAG, "#" + i + ": page " + mItems.get(i).position); 959 | } 960 | } 961 | 962 | mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null); 963 | 964 | mAdapter.finishUpdate(this); 965 | 966 | // Check width measurement of current pages and drawing sort order. 967 | // Update LayoutParams as needed. 968 | final int childCount = getChildCount(); 969 | for (int i = 0; i < childCount; i++) { 970 | final View child = getChildAt(i); 971 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 972 | lp.childIndex = i; 973 | if (!lp.isDecor && lp.heightFactor == 0.f) { 974 | // 0 means requery the adapter for this, it doesn't have a valid width. 975 | final ItemInfo ii = infoForChild(child); 976 | if (ii != null) { 977 | lp.heightFactor = ii.heightFactor; 978 | lp.position = ii.position; 979 | } 980 | } 981 | } 982 | sortChildDrawingOrder(); 983 | 984 | if (hasFocus()) { 985 | View currentFocused = findFocus(); 986 | ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null; 987 | if (ii == null || ii.position != mCurItem) { 988 | for (int i = 0; i < getChildCount(); i++) { 989 | View child = getChildAt(i); 990 | ii = infoForChild(child); 991 | if (ii != null && ii.position == mCurItem) { 992 | if (child.requestFocus(focusDirection)) { 993 | break; 994 | } 995 | } 996 | } 997 | } 998 | } 999 | } 1000 | 1001 | private void sortChildDrawingOrder() { 1002 | if (mDrawingOrder != DRAW_ORDER_DEFAULT) { 1003 | if (mDrawingOrderedChildren == null) { 1004 | mDrawingOrderedChildren = new ArrayList(); 1005 | } else { 1006 | mDrawingOrderedChildren.clear(); 1007 | } 1008 | final int childCount = getChildCount(); 1009 | for (int i = 0; i < childCount; i++) { 1010 | final View child = getChildAt(i); 1011 | mDrawingOrderedChildren.add(child); 1012 | } 1013 | Collections.sort(mDrawingOrderedChildren, sPositionComparator); 1014 | } 1015 | } 1016 | 1017 | private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) { 1018 | final int N = mAdapter.getCount(); 1019 | final int height = getClientHeight(); 1020 | final float marginOffset = height > 0 ? (float) mPageMargin / height : 0; 1021 | // Fix up offsets for later layout. 1022 | if (oldCurInfo != null) { 1023 | final int oldCurPosition = oldCurInfo.position; 1024 | // Base offsets off of oldCurInfo. 1025 | if (oldCurPosition < curItem.position) { 1026 | int itemIndex = 0; 1027 | ItemInfo ii = null; 1028 | float offset = oldCurInfo.offset + oldCurInfo.heightFactor + marginOffset; 1029 | for (int pos = oldCurPosition + 1; 1030 | pos <= curItem.position && itemIndex < mItems.size(); pos++) { 1031 | ii = mItems.get(itemIndex); 1032 | while (pos > ii.position && itemIndex < mItems.size() - 1) { 1033 | itemIndex++; 1034 | ii = mItems.get(itemIndex); 1035 | } 1036 | while (pos < ii.position) { 1037 | // We don't have an item populated for this, 1038 | // ask the adapter for an offset. 1039 | offset += mAdapter.getPageWidth(pos) + marginOffset; 1040 | pos++; 1041 | } 1042 | ii.offset = offset; 1043 | offset += ii.heightFactor + marginOffset; 1044 | } 1045 | } else if (oldCurPosition > curItem.position) { 1046 | int itemIndex = mItems.size() - 1; 1047 | ItemInfo ii = null; 1048 | float offset = oldCurInfo.offset; 1049 | for (int pos = oldCurPosition - 1; 1050 | pos >= curItem.position && itemIndex >= 0; pos--) { 1051 | ii = mItems.get(itemIndex); 1052 | while (pos < ii.position && itemIndex > 0) { 1053 | itemIndex--; 1054 | ii = mItems.get(itemIndex); 1055 | } 1056 | while (pos > ii.position) { 1057 | // We don't have an item populated for this, 1058 | // ask the adapter for an offset. 1059 | offset -= mAdapter.getPageWidth(pos) + marginOffset; 1060 | pos--; 1061 | } 1062 | offset -= ii.heightFactor + marginOffset; 1063 | ii.offset = offset; 1064 | } 1065 | } 1066 | } 1067 | 1068 | // Base all offsets off of curItem. 1069 | final int itemCount = mItems.size(); 1070 | float offset = curItem.offset; 1071 | int pos = curItem.position - 1; 1072 | mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE; 1073 | mLastOffset = curItem.position == N - 1 ? 1074 | curItem.offset + curItem.heightFactor - 1 : Float.MAX_VALUE; 1075 | // Previous pages 1076 | for (int i = curIndex - 1; i >= 0; i--, pos--) { 1077 | final ItemInfo ii = mItems.get(i); 1078 | while (pos > ii.position) { 1079 | offset -= mAdapter.getPageWidth(pos--) + marginOffset; 1080 | } 1081 | offset -= ii.heightFactor + marginOffset; 1082 | ii.offset = offset; 1083 | if (ii.position == 0) mFirstOffset = offset; 1084 | } 1085 | offset = curItem.offset + curItem.heightFactor + marginOffset; 1086 | pos = curItem.position + 1; 1087 | // Next pages 1088 | for (int i = curIndex + 1; i < itemCount; i++, pos++) { 1089 | final ItemInfo ii = mItems.get(i); 1090 | while (pos < ii.position) { 1091 | offset += mAdapter.getPageWidth(pos++) + marginOffset; 1092 | } 1093 | if (ii.position == N - 1) { 1094 | mLastOffset = offset + ii.heightFactor - 1; 1095 | } 1096 | ii.offset = offset; 1097 | offset += ii.heightFactor + marginOffset; 1098 | } 1099 | 1100 | mNeedCalculatePageOffsets = false; 1101 | } 1102 | 1103 | /** 1104 | * This is the persistent state that is saved by ViewPager. Only needed 1105 | * if you are creating a sublass of ViewPager that must save its own 1106 | * state, in which case it should implement a subclass of this which 1107 | * contains that state. 1108 | */ 1109 | public static class SavedState extends BaseSavedState { 1110 | int position; 1111 | Parcelable adapterState; 1112 | ClassLoader loader; 1113 | 1114 | public SavedState(Parcelable superState) { 1115 | super(superState); 1116 | } 1117 | 1118 | @Override 1119 | public void writeToParcel(Parcel out, int flags) { 1120 | super.writeToParcel(out, flags); 1121 | out.writeInt(position); 1122 | out.writeParcelable(adapterState, flags); 1123 | } 1124 | 1125 | @Override 1126 | public String toString() { 1127 | return "FragmentPager.SavedState{" 1128 | + Integer.toHexString(System.identityHashCode(this)) 1129 | + " position=" + position + "}"; 1130 | } 1131 | 1132 | public static final Parcelable.Creator CREATOR 1133 | = ParcelableCompat.newCreator(new ParcelableCompatCreatorCallbacks() { 1134 | @Override 1135 | public SavedState createFromParcel(Parcel in, ClassLoader loader) { 1136 | return new SavedState(in, loader); 1137 | } 1138 | 1139 | @Override 1140 | public SavedState[] newArray(int size) { 1141 | return new SavedState[size]; 1142 | } 1143 | }); 1144 | 1145 | SavedState(Parcel in, ClassLoader loader) { 1146 | super(in); 1147 | if (loader == null) { 1148 | loader = getClass().getClassLoader(); 1149 | } 1150 | position = in.readInt(); 1151 | adapterState = in.readParcelable(loader); 1152 | this.loader = loader; 1153 | } 1154 | } 1155 | 1156 | @Override 1157 | public Parcelable onSaveInstanceState() { 1158 | Parcelable superState = super.onSaveInstanceState(); 1159 | SavedState ss = new SavedState(superState); 1160 | ss.position = mCurItem; 1161 | if (mAdapter != null) { 1162 | ss.adapterState = mAdapter.saveState(); 1163 | } 1164 | return ss; 1165 | } 1166 | 1167 | @Override 1168 | public void onRestoreInstanceState(Parcelable state) { 1169 | if (!(state instanceof SavedState)) { 1170 | super.onRestoreInstanceState(state); 1171 | return; 1172 | } 1173 | 1174 | SavedState ss = (SavedState) state; 1175 | super.onRestoreInstanceState(ss.getSuperState()); 1176 | 1177 | if (mAdapter != null) { 1178 | mAdapter.restoreState(ss.adapterState, ss.loader); 1179 | setCurrentItemInternal(ss.position, false, true); 1180 | } else { 1181 | mRestoredCurItem = ss.position; 1182 | mRestoredAdapterState = ss.adapterState; 1183 | mRestoredClassLoader = ss.loader; 1184 | } 1185 | } 1186 | 1187 | @Override 1188 | public void addView(View child, int index, ViewGroup.LayoutParams params) { 1189 | if (!checkLayoutParams(params)) { 1190 | params = generateLayoutParams(params); 1191 | } 1192 | final LayoutParams lp = (LayoutParams) params; 1193 | lp.isDecor |= child instanceof Decor; 1194 | if (mInLayout) { 1195 | if (lp != null && lp.isDecor) { 1196 | throw new IllegalStateException("Cannot add pager decor view during layout"); 1197 | } 1198 | lp.needsMeasure = true; 1199 | addViewInLayout(child, index, params); 1200 | } else { 1201 | super.addView(child, index, params); 1202 | } 1203 | 1204 | if (USE_CACHE) { 1205 | if (child.getVisibility() != GONE) { 1206 | child.setDrawingCacheEnabled(mScrollingCacheEnabled); 1207 | } else { 1208 | child.setDrawingCacheEnabled(false); 1209 | } 1210 | } 1211 | } 1212 | 1213 | @Override 1214 | public void removeView(View view) { 1215 | if (mInLayout) { 1216 | removeViewInLayout(view); 1217 | } else { 1218 | super.removeView(view); 1219 | } 1220 | } 1221 | 1222 | ItemInfo infoForChild(View child) { 1223 | for (int i = 0; i < mItems.size(); i++) { 1224 | ItemInfo ii = mItems.get(i); 1225 | if (mAdapter.isViewFromObject(child, ii.object)) { 1226 | return ii; 1227 | } 1228 | } 1229 | return null; 1230 | } 1231 | 1232 | ItemInfo infoForAnyChild(View child) { 1233 | ViewParent parent; 1234 | while ((parent = child.getParent()) != this) { 1235 | if (parent == null || !(parent instanceof View)) { 1236 | return null; 1237 | } 1238 | child = (View) parent; 1239 | } 1240 | return infoForChild(child); 1241 | } 1242 | 1243 | ItemInfo infoForPosition(int position) { 1244 | for (int i = 0; i < mItems.size(); i++) { 1245 | ItemInfo ii = mItems.get(i); 1246 | if (ii.position == position) { 1247 | return ii; 1248 | } 1249 | } 1250 | return null; 1251 | } 1252 | 1253 | @Override 1254 | protected void onAttachedToWindow() { 1255 | super.onAttachedToWindow(); 1256 | mFirstLayout = true; 1257 | } 1258 | 1259 | @Override 1260 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1261 | // For simple implementation, our internal size is always 0. 1262 | // We depend on the container to specify the layout size of 1263 | // our view. We can't really know what it is since we will be 1264 | // adding and removing different arbitrary views and do not 1265 | // want the layout to change as this happens. 1266 | setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), 1267 | getDefaultSize(0, heightMeasureSpec)); 1268 | 1269 | final int measuredHeight = getMeasuredHeight(); 1270 | final int maxGutterSize = measuredHeight / 10; 1271 | mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize); 1272 | 1273 | // Children are just made to fill our space. 1274 | int childWidthSize = getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); 1275 | int childHeightSize = measuredHeight - getPaddingTop() - getPaddingBottom(); 1276 | 1277 | /* 1278 | * Make sure all children have been properly measured. Decor views first. 1279 | * Right now we cheat and make this less complicated by assuming decor 1280 | * views won't intersect. We will pin to edges based on gravity. 1281 | */ 1282 | int size = getChildCount(); 1283 | for (int i = 0; i < size; ++i) { 1284 | final View child = getChildAt(i); 1285 | if (child.getVisibility() != GONE) { 1286 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1287 | if (lp != null && lp.isDecor) { 1288 | final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; 1289 | final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; 1290 | int widthMode = MeasureSpec.AT_MOST; 1291 | int heightMode = MeasureSpec.AT_MOST; 1292 | boolean consumeVertical = vgrav == Gravity.TOP || vgrav == Gravity.BOTTOM; 1293 | boolean consumeHorizontal = hgrav == Gravity.LEFT || hgrav == Gravity.RIGHT; 1294 | 1295 | if (consumeVertical) { 1296 | widthMode = MeasureSpec.EXACTLY; 1297 | } else if (consumeHorizontal) { 1298 | heightMode = MeasureSpec.EXACTLY; 1299 | } 1300 | 1301 | int widthSize = childWidthSize; 1302 | int heightSize = childHeightSize; 1303 | if (lp.width != LayoutParams.WRAP_CONTENT) { 1304 | widthMode = MeasureSpec.EXACTLY; 1305 | if (lp.width != LayoutParams.FILL_PARENT) { 1306 | widthSize = lp.width; 1307 | } 1308 | } 1309 | if (lp.height != LayoutParams.WRAP_CONTENT) { 1310 | heightMode = MeasureSpec.EXACTLY; 1311 | if (lp.height != LayoutParams.FILL_PARENT) { 1312 | heightSize = lp.height; 1313 | } 1314 | } 1315 | final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode); 1316 | final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode); 1317 | child.measure(widthSpec, heightSpec); 1318 | 1319 | if (consumeVertical) { 1320 | childHeightSize -= child.getMeasuredHeight(); 1321 | } else if (consumeHorizontal) { 1322 | childWidthSize -= child.getMeasuredWidth(); 1323 | } 1324 | } 1325 | } 1326 | } 1327 | 1328 | mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY); 1329 | mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY); 1330 | 1331 | // Make sure we have created all fragments that we need to have shown. 1332 | mInLayout = true; 1333 | populate(); 1334 | mInLayout = false; 1335 | 1336 | // Page views next. 1337 | size = getChildCount(); 1338 | for (int i = 0; i < size; ++i) { 1339 | final View child = getChildAt(i); 1340 | if (child.getVisibility() != GONE) { 1341 | if (DEBUG) Log.v(TAG, "Measuring #" + i + " " + child 1342 | + ": " + mChildWidthMeasureSpec); 1343 | 1344 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1345 | if (lp == null || !lp.isDecor) { 1346 | final int heightSpec = MeasureSpec.makeMeasureSpec( 1347 | (int) (childHeightSize * lp.heightFactor), MeasureSpec.EXACTLY); 1348 | child.measure(mChildWidthMeasureSpec, heightSpec); 1349 | } 1350 | } 1351 | } 1352 | } 1353 | 1354 | @Override 1355 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 1356 | super.onSizeChanged(w, h, oldw, oldh); 1357 | 1358 | // Make sure scroll position is set correctly. 1359 | if (h != oldh) { 1360 | recomputeScrollPosition(h, oldh, mPageMargin, mPageMargin); 1361 | } 1362 | } 1363 | 1364 | private void recomputeScrollPosition(int height, int oldHeight, int margin, int oldMargin) { 1365 | if (oldHeight > 0 && !mItems.isEmpty()) { 1366 | final int heightWithMargin = height - getPaddingTop() - getPaddingBottom() + margin; 1367 | final int oldHeightWithMargin = oldHeight - getPaddingTop() - getPaddingBottom() 1368 | + oldMargin; 1369 | final int ypos = getScrollY(); 1370 | final float pageOffset = (float) ypos / oldHeightWithMargin; 1371 | final int newOffsetPixels = (int) (pageOffset * heightWithMargin); 1372 | 1373 | scrollTo(getScrollX(), newOffsetPixels); 1374 | if (!mScroller.isFinished()) { 1375 | // We now return to your regularly scheduled scroll, already in progress. 1376 | final int newDuration = mScroller.getDuration() - mScroller.timePassed(); 1377 | ItemInfo targetInfo = infoForPosition(mCurItem); 1378 | mScroller.startScroll(0, newOffsetPixels, 1379 | 0, (int) (targetInfo.offset * height), newDuration); 1380 | } 1381 | } else { 1382 | final ItemInfo ii = infoForPosition(mCurItem); 1383 | final float scrollOffset = ii != null ? Math.min(ii.offset, mLastOffset) : 0; 1384 | final int scrollPos = (int) (scrollOffset * 1385 | (height - getPaddingTop() - getPaddingBottom())); 1386 | if (scrollPos != getScrollY()) { 1387 | completeScroll(false); 1388 | scrollTo(getScrollX(), scrollPos); 1389 | } 1390 | } 1391 | } 1392 | 1393 | @Override 1394 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 1395 | final int count = getChildCount(); 1396 | int width = r - l; 1397 | int height = b - t; 1398 | int paddingLeft = getPaddingLeft(); 1399 | int paddingTop = getPaddingTop(); 1400 | int paddingRight = getPaddingRight(); 1401 | int paddingBottom = getPaddingBottom(); 1402 | final int scrollY = getScrollY(); 1403 | 1404 | int decorCount = 0; 1405 | 1406 | // First pass - decor views. We need to do this in two passes so that 1407 | // we have the proper offsets for non-decor views later. 1408 | for (int i = 0; i < count; i++) { 1409 | final View child = getChildAt(i); 1410 | if (child.getVisibility() != GONE) { 1411 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1412 | int childLeft = 0; 1413 | int childTop = 0; 1414 | if (lp.isDecor) { 1415 | final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; 1416 | final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; 1417 | switch (hgrav) { 1418 | default: 1419 | childLeft = paddingLeft; 1420 | break; 1421 | case Gravity.LEFT: 1422 | childLeft = paddingLeft; 1423 | paddingLeft += child.getMeasuredWidth(); 1424 | break; 1425 | case Gravity.CENTER_HORIZONTAL: 1426 | childLeft = Math.max((width - child.getMeasuredWidth()) / 2, 1427 | paddingLeft); 1428 | break; 1429 | case Gravity.RIGHT: 1430 | childLeft = width - paddingRight - child.getMeasuredWidth(); 1431 | paddingRight += child.getMeasuredWidth(); 1432 | break; 1433 | } 1434 | switch (vgrav) { 1435 | default: 1436 | childTop = paddingTop; 1437 | break; 1438 | case Gravity.TOP: 1439 | childTop = paddingTop; 1440 | paddingTop += child.getMeasuredHeight(); 1441 | break; 1442 | case Gravity.CENTER_VERTICAL: 1443 | childTop = Math.max((height - child.getMeasuredHeight()) / 2, 1444 | paddingTop); 1445 | break; 1446 | case Gravity.BOTTOM: 1447 | childTop = height - paddingBottom - child.getMeasuredHeight(); 1448 | paddingBottom += child.getMeasuredHeight(); 1449 | break; 1450 | } 1451 | childTop += scrollY; 1452 | child.layout(childLeft, childTop, 1453 | childLeft + child.getMeasuredWidth(), 1454 | childTop + child.getMeasuredHeight()); 1455 | decorCount++; 1456 | } 1457 | } 1458 | } 1459 | 1460 | final int childHeight = height - paddingTop - paddingBottom; 1461 | // Page views. Do this once we have the right padding offsets from above. 1462 | for (int i = 0; i < count; i++) { 1463 | final View child = getChildAt(i); 1464 | if (child.getVisibility() != GONE) { 1465 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1466 | ItemInfo ii; 1467 | if (!lp.isDecor && (ii = infoForChild(child)) != null) { 1468 | int toff = (int) (childHeight * ii.offset); 1469 | int childLeft = paddingLeft; 1470 | int childTop = paddingTop + toff; 1471 | if (lp.needsMeasure) { 1472 | // This was added during layout and needs measurement. 1473 | // Do it now that we know what we're working with. 1474 | lp.needsMeasure = false; 1475 | final int widthSpec = MeasureSpec.makeMeasureSpec( 1476 | (int) (width - paddingLeft - paddingRight), 1477 | MeasureSpec.EXACTLY); 1478 | final int heightSpec = MeasureSpec.makeMeasureSpec( 1479 | (int) (childHeight * lp.heightFactor), 1480 | MeasureSpec.EXACTLY); 1481 | child.measure(widthSpec, heightSpec); 1482 | } 1483 | if (DEBUG) Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object 1484 | + ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth() 1485 | + "x" + child.getMeasuredHeight()); 1486 | child.layout(childLeft, childTop, 1487 | childLeft + child.getMeasuredWidth(), 1488 | childTop + child.getMeasuredHeight()); 1489 | } 1490 | } 1491 | } 1492 | mLeftPageBounds = paddingLeft; 1493 | mRightPageBounds = width - paddingRight; 1494 | mDecorChildCount = decorCount; 1495 | 1496 | if (mFirstLayout) { 1497 | scrollToItem(mCurItem, false, 0, false); 1498 | } 1499 | mFirstLayout = false; 1500 | } 1501 | 1502 | @Override 1503 | public void computeScroll() { 1504 | if (!mScroller.isFinished() && mScroller.computeScrollOffset()) { 1505 | int oldX = getScrollX(); 1506 | int oldY = getScrollY(); 1507 | int x = mScroller.getCurrX(); 1508 | int y = mScroller.getCurrY(); 1509 | 1510 | if (oldX != x || oldY != y) { 1511 | scrollTo(x, y); 1512 | if (!pageScrolled(y)) { 1513 | mScroller.abortAnimation(); 1514 | scrollTo(x, 0); 1515 | } 1516 | } 1517 | 1518 | // Keep on drawing until the animation has finished. 1519 | ViewCompat.postInvalidateOnAnimation(this); 1520 | return; 1521 | } 1522 | 1523 | // Done with scroll, clean up state. 1524 | completeScroll(true); 1525 | } 1526 | 1527 | private boolean pageScrolled(int ypos) { 1528 | if (mItems.size() == 0) { 1529 | mCalledSuper = false; 1530 | onPageScrolled(0, 0, 0); 1531 | if (!mCalledSuper) { 1532 | throw new IllegalStateException( 1533 | "onPageScrolled did not call superclass implementation"); 1534 | } 1535 | return false; 1536 | } 1537 | final ItemInfo ii = infoForCurrentScrollPosition(); 1538 | final int height = getClientHeight(); 1539 | final int heightWithMargin = height + mPageMargin; 1540 | final float marginOffset = (float) mPageMargin / height; 1541 | final int currentPage = ii.position; 1542 | final float pageOffset = (((float) ypos / height) - ii.offset) / 1543 | (ii.heightFactor + marginOffset); 1544 | final int offsetPixels = (int) (pageOffset * heightWithMargin); 1545 | 1546 | mCalledSuper = false; 1547 | onPageScrolled(currentPage, pageOffset, offsetPixels); 1548 | if (!mCalledSuper) { 1549 | throw new IllegalStateException( 1550 | "onPageScrolled did not call superclass implementation"); 1551 | } 1552 | return true; 1553 | } 1554 | 1555 | /** 1556 | * This method will be invoked when the current page is scrolled, either as part 1557 | * of a programmatically initiated smooth scroll or a user initiated touch scroll. 1558 | * If you override this method you must call through to the superclass implementation 1559 | * (e.g. super.onPageScrolled(position, offset, offsetPixels)) before onPageScrolled 1560 | * returns. 1561 | * 1562 | * @param position Position index of the first page currently being displayed. 1563 | * Page position+1 will be visible if positionOffset is nonzero. 1564 | * @param offset Value from [0, 1) indicating the offset from the page at position. 1565 | * @param offsetPixels Value in pixels indicating the offset from position. 1566 | */ 1567 | protected void onPageScrolled(int position, float offset, int offsetPixels) { 1568 | // Offset any decor views if needed - keep them on-screen at all times. 1569 | if (mDecorChildCount > 0) { 1570 | final int scrollY = getScrollY(); 1571 | int paddingTop = getPaddingTop(); 1572 | int paddingBottom = getPaddingBottom(); 1573 | final int height = getHeight(); 1574 | final int childCount = getChildCount(); 1575 | for (int i = 0; i < childCount; i++) { 1576 | final View child = getChildAt(i); 1577 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1578 | if (!lp.isDecor) continue; 1579 | 1580 | final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; 1581 | int childTop = 0; 1582 | switch (vgrav) { 1583 | default: 1584 | childTop = paddingTop; 1585 | break; 1586 | case Gravity.TOP: 1587 | childTop = paddingTop; 1588 | paddingTop += child.getHeight(); 1589 | break; 1590 | case Gravity.CENTER_VERTICAL: 1591 | childTop = Math.max((height - child.getMeasuredHeight()) / 2, 1592 | paddingTop); 1593 | break; 1594 | case Gravity.BOTTOM: 1595 | childTop = height - paddingBottom - child.getMeasuredHeight(); 1596 | paddingBottom += child.getMeasuredHeight(); 1597 | break; 1598 | } 1599 | childTop += scrollY; 1600 | 1601 | final int childOffset = childTop - child.getTop(); 1602 | if (childOffset != 0) { 1603 | child.offsetTopAndBottom(childOffset); 1604 | } 1605 | } 1606 | } 1607 | 1608 | if (mOnPageChangeListener != null) { 1609 | mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels); 1610 | } 1611 | if (mInternalPageChangeListener != null) { 1612 | mInternalPageChangeListener.onPageScrolled(position, offset, offsetPixels); 1613 | } 1614 | 1615 | if (mPageTransformer != null) { 1616 | final int scrollY = getScrollY(); 1617 | final int childCount = getChildCount(); 1618 | for (int i = 0; i < childCount; i++) { 1619 | final View child = getChildAt(i); 1620 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1621 | 1622 | if (lp.isDecor) continue; 1623 | 1624 | final float transformPos = (float) (child.getTop() - scrollY) / getClientHeight(); 1625 | mPageTransformer.transformPage(child, transformPos); 1626 | } 1627 | } 1628 | 1629 | mCalledSuper = true; 1630 | } 1631 | 1632 | private void completeScroll(boolean postEvents) { 1633 | boolean needPopulate = mScrollState == SCROLL_STATE_SETTLING; 1634 | if (needPopulate) { 1635 | // Done with scroll, no longer want to cache view drawing. 1636 | setScrollingCacheEnabled(false); 1637 | mScroller.abortAnimation(); 1638 | int oldX = getScrollX(); 1639 | int oldY = getScrollY(); 1640 | int x = mScroller.getCurrX(); 1641 | int y = mScroller.getCurrY(); 1642 | if (oldX != x || oldY != y) { 1643 | scrollTo(x, y); 1644 | } 1645 | } 1646 | mPopulatePending = false; 1647 | for (int i = 0; i < mItems.size(); i++) { 1648 | ItemInfo ii = mItems.get(i); 1649 | if (ii.scrolling) { 1650 | needPopulate = true; 1651 | ii.scrolling = false; 1652 | } 1653 | } 1654 | if (needPopulate) { 1655 | if (postEvents) { 1656 | ViewCompat.postOnAnimation(this, mEndScrollRunnable); 1657 | } else { 1658 | mEndScrollRunnable.run(); 1659 | } 1660 | } 1661 | } 1662 | 1663 | private boolean isGutterDrag(float y, float dy) { 1664 | return (y < mGutterSize && dy > 0) || (y > getHeight() - mGutterSize && dy < 0); 1665 | } 1666 | 1667 | private void enableLayers(boolean enable) { 1668 | final int childCount = getChildCount(); 1669 | for (int i = 0; i < childCount; i++) { 1670 | final int layerType = enable ? 1671 | ViewCompat.LAYER_TYPE_HARDWARE : ViewCompat.LAYER_TYPE_NONE; 1672 | ViewCompat.setLayerType(getChildAt(i), layerType, null); 1673 | } 1674 | } 1675 | 1676 | @Override 1677 | public boolean onInterceptTouchEvent(MotionEvent ev) { 1678 | /* 1679 | * This method JUST determines whether we want to intercept the motion. 1680 | * If we return true, onMotionEvent will be called and we do the actual 1681 | * scrolling there. 1682 | */ 1683 | 1684 | final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; 1685 | 1686 | // Always take care of the touch gesture being complete. 1687 | if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { 1688 | // Release the drag. 1689 | if (DEBUG) Log.v(TAG, "Intercept done!"); 1690 | mIsBeingDragged = false; 1691 | mIsUnableToDrag = false; 1692 | mActivePointerId = INVALID_POINTER; 1693 | if (mVelocityTracker != null) { 1694 | mVelocityTracker.recycle(); 1695 | mVelocityTracker = null; 1696 | } 1697 | return false; 1698 | } 1699 | 1700 | // Nothing more to do here if we have decided whether or not we 1701 | // are dragging. 1702 | if (action != MotionEvent.ACTION_DOWN) { 1703 | if (mIsBeingDragged) { 1704 | if (DEBUG) Log.v(TAG, "Intercept returning true!"); 1705 | return true; 1706 | } 1707 | if (mIsUnableToDrag) { 1708 | if (DEBUG) Log.v(TAG, "Intercept returning false!"); 1709 | return false; 1710 | } 1711 | } 1712 | 1713 | switch (action) { 1714 | case MotionEvent.ACTION_MOVE: { 1715 | /* 1716 | * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check 1717 | * whether the user has moved far enough from his original down touch. 1718 | */ 1719 | 1720 | /* 1721 | * Locally do absolute value. mLastMotionY is set to the y value 1722 | * of the down event. 1723 | */ 1724 | final int activePointerId = mActivePointerId; 1725 | if (activePointerId == INVALID_POINTER) { 1726 | // If we don't have a valid id, the touch down wasn't on content. 1727 | break; 1728 | } 1729 | 1730 | final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId); 1731 | final float y = MotionEventCompat.getY(ev, pointerIndex); 1732 | final float dy = y - mLastMotionY; 1733 | final float yDiff = Math.abs(dy); 1734 | final float x = MotionEventCompat.getX(ev, pointerIndex); 1735 | final float xDiff = Math.abs(x - mInitialMotionX); 1736 | if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); 1737 | 1738 | if (dy != 0 && (allowDragDown(dy) || allowDragUp(dy)) && !canScroll(this, true, (int) dy, (int) x, (int) y)) { 1739 | if (DEBUG) Log.v(TAG, "Starting drag!"); 1740 | mIsBeingDragged = true; 1741 | requestParentDisallowInterceptTouchEvent(true); 1742 | setScrollState(SCROLL_STATE_DRAGGING); 1743 | mLastMotionY = dy > 0 ? mInitialMotionY + mTouchSlop : 1744 | mInitialMotionY - mTouchSlop; 1745 | mLastMotionX = x; 1746 | setScrollingCacheEnabled(true); 1747 | } else if (xDiff > mTouchSlop) { 1748 | // The finger has moved enough in the vertical 1749 | // direction to be counted as a drag... abort 1750 | // any attempt to drag horizontally, to work correctly 1751 | // with children that have scrolling containers. 1752 | if (DEBUG) Log.v(TAG, "Starting unable to drag!"); 1753 | mIsUnableToDrag = true; 1754 | } 1755 | if (mIsBeingDragged) { 1756 | // Scroll to follow the motion event 1757 | if (performDrag(y)) { 1758 | ViewCompat.postInvalidateOnAnimation(this); 1759 | } 1760 | } 1761 | break; 1762 | } 1763 | 1764 | case MotionEvent.ACTION_DOWN: { 1765 | /* 1766 | * Remember location of down touch. 1767 | * ACTION_DOWN always refers to pointer index 0. 1768 | */ 1769 | mLastMotionX = mInitialMotionX = ev.getX(); 1770 | mLastMotionY = mInitialMotionY = ev.getY(); 1771 | mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 1772 | mIsUnableToDrag = false; 1773 | 1774 | mScroller.computeScrollOffset(); 1775 | if (mScrollState == SCROLL_STATE_SETTLING && 1776 | Math.abs(mScroller.getFinalY() - mScroller.getCurrY()) > mCloseEnough) { 1777 | // Let the user 'catch' the pager as it animates. 1778 | mScroller.abortAnimation(); 1779 | mPopulatePending = false; 1780 | populate(); 1781 | mIsBeingDragged = true; 1782 | requestParentDisallowInterceptTouchEvent(true); 1783 | setScrollState(SCROLL_STATE_DRAGGING); 1784 | } else { 1785 | completeScroll(false); 1786 | mIsBeingDragged = false; 1787 | } 1788 | 1789 | if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY 1790 | + " mIsBeingDragged=" + mIsBeingDragged 1791 | + "mIsUnableToDrag=" + mIsUnableToDrag); 1792 | break; 1793 | } 1794 | 1795 | case MotionEventCompat.ACTION_POINTER_UP: 1796 | onSecondaryPointerUp(ev); 1797 | break; 1798 | } 1799 | 1800 | if (mVelocityTracker == null) { 1801 | mVelocityTracker = VelocityTracker.obtain(); 1802 | } 1803 | mVelocityTracker.addMovement(ev); 1804 | 1805 | /* 1806 | * The only time we want to intercept motion events is if we are in the 1807 | * drag mode. 1808 | */ 1809 | return mIsBeingDragged; 1810 | } 1811 | 1812 | @Override 1813 | public boolean onTouchEvent(MotionEvent ev) { 1814 | if (mFakeDragging) { 1815 | // A fake drag is in progress already, ignore this real one 1816 | // but still eat the touch events. 1817 | // (It is likely that the user is multi-touching the screen.) 1818 | return true; 1819 | } 1820 | 1821 | if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) { 1822 | // Don't handle edge touches immediately -- they may actually belong to one of our 1823 | // descendants. 1824 | return false; 1825 | } 1826 | 1827 | if (mAdapter == null || mAdapter.getCount() == 0) { 1828 | // Nothing to present or scroll; nothing to touch. 1829 | return false; 1830 | } 1831 | 1832 | if (mVelocityTracker == null) { 1833 | mVelocityTracker = VelocityTracker.obtain(); 1834 | } 1835 | mVelocityTracker.addMovement(ev); 1836 | 1837 | final int action = ev.getAction(); 1838 | boolean needsInvalidate = false; 1839 | 1840 | switch (action & MotionEventCompat.ACTION_MASK) { 1841 | case MotionEvent.ACTION_DOWN: { 1842 | mScroller.abortAnimation(); 1843 | mPopulatePending = false; 1844 | populate(); 1845 | 1846 | // Remember where the motion event started 1847 | mLastMotionX = mInitialMotionX = ev.getX(); 1848 | mLastMotionY = mInitialMotionY = ev.getY(); 1849 | mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 1850 | break; 1851 | } 1852 | case MotionEvent.ACTION_MOVE: 1853 | if (!mIsBeingDragged) { 1854 | final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); 1855 | final float y = MotionEventCompat.getY(ev, pointerIndex); 1856 | final float yDiff = Math.abs(y - mLastMotionY); 1857 | final float x = MotionEventCompat.getX(ev, pointerIndex); 1858 | final float xDiff = Math.abs(x - mLastMotionX); 1859 | if (DEBUG) 1860 | Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); 1861 | if (yDiff > mTouchSlop && yDiff > xDiff) { 1862 | if (DEBUG) Log.v(TAG, "Starting drag!"); 1863 | mIsBeingDragged = true; 1864 | requestParentDisallowInterceptTouchEvent(true); 1865 | mLastMotionY = y - mInitialMotionY > 0 ? mInitialMotionY + mTouchSlop : 1866 | mInitialMotionY - mTouchSlop; 1867 | mLastMotionX = x; 1868 | setScrollState(SCROLL_STATE_DRAGGING); 1869 | setScrollingCacheEnabled(true); 1870 | 1871 | // Disallow Parent Intercept, just in case 1872 | ViewParent parent = getParent(); 1873 | if (parent != null) { 1874 | parent.requestDisallowInterceptTouchEvent(true); 1875 | } 1876 | } 1877 | } 1878 | // Not else! Note that mIsBeingDragged can be set above. 1879 | if (mIsBeingDragged) { 1880 | // Scroll to follow the motion event 1881 | final int activePointerIndex = MotionEventCompat.findPointerIndex( 1882 | ev, mActivePointerId); 1883 | final float y = MotionEventCompat.getY(ev, activePointerIndex); 1884 | needsInvalidate |= performDrag(y); 1885 | } 1886 | break; 1887 | case MotionEvent.ACTION_UP: 1888 | if (mIsBeingDragged) { 1889 | final VelocityTracker velocityTracker = mVelocityTracker; 1890 | velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 1891 | int initialVelocity = (int) VelocityTrackerCompat.getYVelocity( 1892 | velocityTracker, mActivePointerId); 1893 | mPopulatePending = true; 1894 | final int height = getClientHeight(); 1895 | final int scrollY = getScrollY(); 1896 | final ItemInfo ii = infoForCurrentScrollPosition(); 1897 | final int currentPage = ii.position; 1898 | final float pageOffset = (((float) scrollY / height) - ii.offset) / ii.heightFactor; 1899 | final int activePointerIndex = 1900 | MotionEventCompat.findPointerIndex(ev, mActivePointerId); 1901 | final float y = MotionEventCompat.getY(ev, activePointerIndex); 1902 | final int totalDelta = (int) (y - mInitialMotionY); 1903 | int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, 1904 | totalDelta); 1905 | setCurrentItemInternal(nextPage, true, true, initialVelocity); 1906 | 1907 | mActivePointerId = INVALID_POINTER; 1908 | endDrag(); 1909 | needsInvalidate = mTopEdge.onRelease() | mBottomEdge.onRelease(); 1910 | } 1911 | break; 1912 | case MotionEvent.ACTION_CANCEL: 1913 | if (mIsBeingDragged) { 1914 | scrollToItem(mCurItem, true, 0, false); 1915 | mActivePointerId = INVALID_POINTER; 1916 | endDrag(); 1917 | needsInvalidate = mTopEdge.onRelease() | mBottomEdge.onRelease(); 1918 | } 1919 | break; 1920 | case MotionEventCompat.ACTION_POINTER_DOWN: { 1921 | final int index = MotionEventCompat.getActionIndex(ev); 1922 | final float y = MotionEventCompat.getY(ev, index); 1923 | mLastMotionY = y; 1924 | mActivePointerId = MotionEventCompat.getPointerId(ev, index); 1925 | break; 1926 | } 1927 | case MotionEventCompat.ACTION_POINTER_UP: 1928 | onSecondaryPointerUp(ev); 1929 | mLastMotionY = MotionEventCompat.getY(ev, 1930 | MotionEventCompat.findPointerIndex(ev, mActivePointerId)); 1931 | break; 1932 | } 1933 | if (needsInvalidate) { 1934 | ViewCompat.postInvalidateOnAnimation(this); 1935 | } 1936 | return true; 1937 | } 1938 | 1939 | private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) { 1940 | final ViewParent parent = getParent(); 1941 | if (parent != null) { 1942 | parent.requestDisallowInterceptTouchEvent(disallowIntercept); 1943 | } 1944 | } 1945 | 1946 | private boolean performDrag(float y) { 1947 | boolean needsInvalidate = false; 1948 | 1949 | final float deltaY = mLastMotionY - y; 1950 | mLastMotionY = y; 1951 | 1952 | float oldScrollY = getScrollY(); 1953 | float scrollY = oldScrollY + deltaY; 1954 | final int height = getClientHeight(); 1955 | 1956 | float topBound = height * mFirstOffset; 1957 | float bottomBound = height * mLastOffset; 1958 | boolean topAbsolute = true; 1959 | boolean bottomAbsolute = true; 1960 | 1961 | final ItemInfo firstItem = mItems.get(0); 1962 | final ItemInfo lastItem = mItems.get(mItems.size() - 1); 1963 | if (firstItem.position != 0) { 1964 | topAbsolute = false; 1965 | topBound = firstItem.offset * height; 1966 | } 1967 | if (lastItem.position != mAdapter.getCount() - 1) { 1968 | bottomAbsolute = false; 1969 | bottomBound = lastItem.offset * height; 1970 | } 1971 | 1972 | if (scrollY < topBound) { 1973 | if (topAbsolute) { 1974 | float over = topBound - scrollY; 1975 | needsInvalidate = mTopEdge.onPull(Math.abs(over) / height); 1976 | } 1977 | scrollY = topBound; 1978 | } else if (scrollY > bottomBound) { 1979 | if (bottomAbsolute) { 1980 | float over = scrollY - bottomBound; 1981 | needsInvalidate = mBottomEdge.onPull(Math.abs(over) / height); 1982 | } 1983 | scrollY = bottomBound; 1984 | } 1985 | // Don't lose the rounded component 1986 | mLastMotionX += scrollY - (int) scrollY; 1987 | scrollTo(getScrollX(), (int) scrollY); 1988 | pageScrolled((int) scrollY); 1989 | 1990 | return needsInvalidate; 1991 | } 1992 | 1993 | /** 1994 | * @return Info about the page at the current scroll position. 1995 | * This can be synthetic for a missing middle page; the 'object' field can be null. 1996 | */ 1997 | private ItemInfo infoForCurrentScrollPosition() { 1998 | final int height = getClientHeight(); 1999 | final float scrollOffset = height > 0 ? (float) getScrollY() / height : 0; 2000 | final float marginOffset = height > 0 ? (float) mPageMargin / height : 0; 2001 | int lastPos = -1; 2002 | float lastOffset = 0.f; 2003 | float lastHeight = 0.f; 2004 | boolean first = true; 2005 | 2006 | ItemInfo lastItem = null; 2007 | for (int i = 0; i < mItems.size(); i++) { 2008 | ItemInfo ii = mItems.get(i); 2009 | float offset; 2010 | if (!first && ii.position != lastPos + 1) { 2011 | // Create a synthetic item for a missing page. 2012 | ii = mTempItem; 2013 | ii.offset = lastOffset + lastHeight + marginOffset; 2014 | ii.position = lastPos + 1; 2015 | ii.heightFactor = mAdapter.getPageWidth(ii.position); 2016 | i--; 2017 | } 2018 | offset = ii.offset; 2019 | 2020 | final float topBound = offset; 2021 | final float bottomBound = offset + ii.heightFactor + marginOffset; 2022 | if (first || scrollOffset >= topBound) { 2023 | if (scrollOffset < bottomBound || i == mItems.size() - 1) { 2024 | return ii; 2025 | } 2026 | } else { 2027 | return lastItem; 2028 | } 2029 | first = false; 2030 | lastPos = ii.position; 2031 | lastOffset = offset; 2032 | lastHeight = ii.heightFactor; 2033 | lastItem = ii; 2034 | } 2035 | 2036 | return lastItem; 2037 | } 2038 | 2039 | private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaY) { 2040 | int targetPage; 2041 | if (Math.abs(deltaY) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) { 2042 | targetPage = velocity > 0 ? currentPage : currentPage + 1; 2043 | } else { 2044 | final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f; 2045 | targetPage = (int) (currentPage + pageOffset + truncator); 2046 | } 2047 | 2048 | if (mItems.size() > 0) { 2049 | final ItemInfo firstItem = mItems.get(0); 2050 | final ItemInfo lastItem = mItems.get(mItems.size() - 1); 2051 | 2052 | // Only let the user target pages we have items for 2053 | targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position)); 2054 | } 2055 | 2056 | return targetPage; 2057 | } 2058 | 2059 | @Override 2060 | public void draw(Canvas canvas) { 2061 | super.draw(canvas); 2062 | boolean needsInvalidate = false; 2063 | 2064 | final int overScrollMode = ViewCompat.getOverScrollMode(this); 2065 | if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS || 2066 | (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && 2067 | mAdapter != null && mAdapter.getCount() > 1)) { 2068 | if (!mTopEdge.isFinished()) { 2069 | final int restoreCount = canvas.save(); 2070 | final int height = getHeight(); 2071 | final int width = getWidth() - getPaddingLeft() - getPaddingRight(); 2072 | 2073 | canvas.translate(getPaddingLeft(), mFirstOffset * height); 2074 | mTopEdge.setSize(width, height); 2075 | needsInvalidate |= mTopEdge.draw(canvas); 2076 | canvas.restoreToCount(restoreCount); 2077 | } 2078 | if (!mBottomEdge.isFinished()) { 2079 | final int restoreCount = canvas.save(); 2080 | final int height = getHeight(); 2081 | final int width = getWidth() - getPaddingLeft() - getPaddingRight(); 2082 | 2083 | canvas.rotate(180); 2084 | canvas.translate(-width - getPaddingLeft(), -(mLastOffset + 1) * height); 2085 | mBottomEdge.setSize(width, height); 2086 | needsInvalidate |= mBottomEdge.draw(canvas); 2087 | canvas.restoreToCount(restoreCount); 2088 | } 2089 | } else { 2090 | mTopEdge.finish(); 2091 | mBottomEdge.finish(); 2092 | } 2093 | 2094 | if (needsInvalidate) { 2095 | // Keep animating 2096 | ViewCompat.postInvalidateOnAnimation(this); 2097 | } 2098 | } 2099 | 2100 | @Override 2101 | protected void onDraw(Canvas canvas) { 2102 | super.onDraw(canvas); 2103 | 2104 | // Draw the margin drawable between pages if needed. 2105 | if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) { 2106 | final int scrollY = getScrollY(); 2107 | final int height = getHeight(); 2108 | 2109 | final float marginOffset = (float) mPageMargin / height; 2110 | int itemIndex = 0; 2111 | ItemInfo ii = mItems.get(0); 2112 | float offset = ii.offset; 2113 | final int itemCount = mItems.size(); 2114 | final int firstPos = ii.position; 2115 | final int lastPos = mItems.get(itemCount - 1).position; 2116 | for (int pos = firstPos; pos < lastPos; pos++) { 2117 | while (pos > ii.position && itemIndex < itemCount) { 2118 | ii = mItems.get(++itemIndex); 2119 | } 2120 | 2121 | float drawAt; 2122 | if (pos == ii.position) { 2123 | drawAt = (ii.offset + ii.heightFactor) * height; 2124 | offset = ii.offset + ii.heightFactor + marginOffset; 2125 | } else { 2126 | float heightFactor = mAdapter.getPageWidth(pos); 2127 | drawAt = (offset + heightFactor) * height; 2128 | offset += heightFactor + marginOffset; 2129 | } 2130 | 2131 | if (drawAt + mPageMargin > scrollY) { 2132 | mMarginDrawable.setBounds(mLeftPageBounds, (int) drawAt, 2133 | mRightPageBounds, (int) (drawAt + mPageMargin + 0.5f)); 2134 | mMarginDrawable.draw(canvas); 2135 | } 2136 | 2137 | if (drawAt > scrollY + height) { 2138 | break; // No more visible, no sense in continuing 2139 | } 2140 | } 2141 | } 2142 | } 2143 | 2144 | /** 2145 | * Start a fake drag of the pager. 2146 | *

2147 | *

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

2153 | *

During a fake drag the ViewPager will ignore all touch events. If a real drag 2154 | * is already in progress, this method will return false. 2155 | * 2156 | * @return true if the fake drag began successfully, false if it could not be started. 2157 | * @see #fakeDragBy(float) 2158 | * @see #endFakeDrag() 2159 | */ 2160 | public boolean beginFakeDrag() { 2161 | if (mIsBeingDragged) { 2162 | return false; 2163 | } 2164 | mFakeDragging = true; 2165 | setScrollState(SCROLL_STATE_DRAGGING); 2166 | mInitialMotionY = mLastMotionY = 0; 2167 | if (mVelocityTracker == null) { 2168 | mVelocityTracker = VelocityTracker.obtain(); 2169 | } else { 2170 | mVelocityTracker.clear(); 2171 | } 2172 | final long time = SystemClock.uptimeMillis(); 2173 | final MotionEvent ev = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0); 2174 | mVelocityTracker.addMovement(ev); 2175 | ev.recycle(); 2176 | mFakeDragBeginTime = time; 2177 | return true; 2178 | } 2179 | 2180 | /** 2181 | * End a fake drag of the pager. 2182 | * 2183 | * @see #beginFakeDrag() 2184 | * @see #fakeDragBy(float) 2185 | */ 2186 | public void endFakeDrag() { 2187 | if (!mFakeDragging) { 2188 | throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first."); 2189 | } 2190 | 2191 | final VelocityTracker velocityTracker = mVelocityTracker; 2192 | velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 2193 | int initialVelocity = (int) VelocityTrackerCompat.getYVelocity( 2194 | velocityTracker, mActivePointerId); 2195 | mPopulatePending = true; 2196 | final int height = getClientHeight(); 2197 | final int scrollY = getScrollY(); 2198 | final ItemInfo ii = infoForCurrentScrollPosition(); 2199 | final int currentPage = ii.position; 2200 | final float pageOffset = (((float) scrollY / height) - ii.offset) / ii.heightFactor; 2201 | final int totalDelta = (int) (mLastMotionY - mInitialMotionY); 2202 | int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, 2203 | totalDelta); 2204 | setCurrentItemInternal(nextPage, true, true, initialVelocity); 2205 | endDrag(); 2206 | 2207 | mFakeDragging = false; 2208 | } 2209 | 2210 | /** 2211 | * Fake drag by an offset in pixels. You must have called {@link #beginFakeDrag()} first. 2212 | * 2213 | * @param yOffset Offset in pixels to drag by. 2214 | * @see #beginFakeDrag() 2215 | * @see #endFakeDrag() 2216 | */ 2217 | public void fakeDragBy(float yOffset) { 2218 | if (!mFakeDragging) { 2219 | throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first."); 2220 | } 2221 | 2222 | mLastMotionY += yOffset; 2223 | 2224 | float oldScrollY = getScrollY(); 2225 | float scrollY = oldScrollY - yOffset; 2226 | final int height = getClientHeight(); 2227 | 2228 | float topBound = height * mFirstOffset; 2229 | float bottomBound = height * mLastOffset; 2230 | 2231 | final ItemInfo firstItem = mItems.get(0); 2232 | final ItemInfo lastItem = mItems.get(mItems.size() - 1); 2233 | if (firstItem.position != 0) { 2234 | topBound = firstItem.offset * height; 2235 | } 2236 | if (lastItem.position != mAdapter.getCount() - 1) { 2237 | bottomBound = lastItem.offset * height; 2238 | } 2239 | 2240 | if (scrollY < topBound) { 2241 | scrollY = topBound; 2242 | } else if (scrollY > bottomBound) { 2243 | scrollY = bottomBound; 2244 | } 2245 | // Don't lose the rounded component 2246 | mLastMotionY += scrollY - (int) scrollY; 2247 | scrollTo(getScrollX(), (int) scrollY); 2248 | pageScrolled((int) scrollY); 2249 | 2250 | // Synthesize an event for the VelocityTracker. 2251 | final long time = SystemClock.uptimeMillis(); 2252 | final MotionEvent ev = MotionEvent.obtain(mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE, 2253 | 0, mLastMotionY, 0); 2254 | mVelocityTracker.addMovement(ev); 2255 | ev.recycle(); 2256 | } 2257 | 2258 | /** 2259 | * Returns true if a fake drag is in progress. 2260 | * 2261 | * @return true if currently in a fake drag, false otherwise. 2262 | * @see #beginFakeDrag() 2263 | * @see #fakeDragBy(float) 2264 | * @see #endFakeDrag() 2265 | */ 2266 | public boolean isFakeDragging() { 2267 | return mFakeDragging; 2268 | } 2269 | 2270 | private void onSecondaryPointerUp(MotionEvent ev) { 2271 | final int pointerIndex = MotionEventCompat.getActionIndex(ev); 2272 | final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); 2273 | if (pointerId == mActivePointerId) { 2274 | // This was our active pointer going up. Choose a new 2275 | // active pointer and adjust accordingly. 2276 | final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 2277 | mLastMotionY = MotionEventCompat.getY(ev, newPointerIndex); 2278 | mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); 2279 | if (mVelocityTracker != null) { 2280 | mVelocityTracker.clear(); 2281 | } 2282 | } 2283 | } 2284 | 2285 | private void endDrag() { 2286 | mIsBeingDragged = false; 2287 | mIsUnableToDrag = false; 2288 | 2289 | if (mVelocityTracker != null) { 2290 | mVelocityTracker.recycle(); 2291 | mVelocityTracker = null; 2292 | } 2293 | } 2294 | 2295 | private void setScrollingCacheEnabled(boolean enabled) { 2296 | if (mScrollingCacheEnabled != enabled) { 2297 | mScrollingCacheEnabled = enabled; 2298 | if (USE_CACHE) { 2299 | final int size = getChildCount(); 2300 | for (int i = 0; i < size; ++i) { 2301 | final View child = getChildAt(i); 2302 | if (child.getVisibility() != GONE) { 2303 | child.setDrawingCacheEnabled(enabled); 2304 | } 2305 | } 2306 | } 2307 | } 2308 | } 2309 | 2310 | public boolean internalCanScrollVertically(int direction) { 2311 | if (mAdapter == null) { 2312 | return false; 2313 | } 2314 | 2315 | final int height = getClientHeight(); 2316 | final int scrollY = getScrollY(); 2317 | if (direction < 0) { 2318 | return (scrollY > (int) (height * mFirstOffset)); 2319 | } else if (direction > 0) { 2320 | return (scrollY < (int) (height * mLastOffset)); 2321 | } else { 2322 | return false; 2323 | } 2324 | } 2325 | 2326 | /** 2327 | * Tests scrollability within child views of v given a delta of dx. 2328 | * 2329 | * @param v View to test for horizontal scrollability 2330 | * @param checkV Whether the view v passed should itself be checked for scrollability (true), 2331 | * or just its children (false). 2332 | * @param dy Delta scrolled in pixels 2333 | * @param x X coordinate of the active touch point 2334 | * @param y Y coordinate of the active touch point 2335 | * @return true if child views of v can be scrolled by delta of dx. 2336 | */ 2337 | protected boolean canScroll(View v, boolean checkV, int dy, int x, int y) { 2338 | if (v instanceof ViewGroup) { 2339 | final ViewGroup group = (ViewGroup) v; 2340 | final int scrollX = v.getScrollX(); 2341 | final int scrollY = v.getScrollY(); 2342 | final int count = group.getChildCount(); 2343 | // Count backwards - let topmost views consume scroll distance first. 2344 | for (int i = count - 1; i >= 0; i--) { 2345 | // TODO: Add versioned support here for transformed views. 2346 | // This will not work for transformed views in Honeycomb+ 2347 | final View child = group.getChildAt(i); 2348 | if (y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && 2349 | x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && 2350 | canScroll(child, true, dy, x + scrollX - child.getLeft(), 2351 | y + scrollY - child.getTop())) { 2352 | return true; 2353 | } 2354 | } 2355 | } 2356 | 2357 | return checkV && ViewCompat.canScrollVertically(v, -dy); 2358 | } 2359 | 2360 | /** 2361 | * Return false if: 2362 | * If the adapter is in the first row 2363 | * & (If the child is a viewgroup) If the child is in the first row 2364 | * & If the scroll is trying to go up 2365 | */ 2366 | protected boolean allowDragDown(float dy) { 2367 | return mCurItem != 0 && dy > 0; 2368 | } 2369 | 2370 | protected boolean allowDragUp(float dy) { 2371 | return mCurItem != getAdapter().getCount() - 1 && dy < 0; 2372 | } 2373 | 2374 | @Override 2375 | public boolean dispatchKeyEvent(KeyEvent event) { 2376 | // Let the focused view and/or our descendants get the key first 2377 | return super.dispatchKeyEvent(event) || executeKeyEvent(event); 2378 | } 2379 | 2380 | /** 2381 | * You can call this function yourself to have the scroll view perform 2382 | * scrolling from a key event, just as if the event had been dispatched to 2383 | * it by the view hierarchy. 2384 | * 2385 | * @param event The key event to execute. 2386 | * @return Return true if the event was handled, else false. 2387 | */ 2388 | public boolean executeKeyEvent(KeyEvent event) { 2389 | boolean handled = false; 2390 | if (event.getAction() == KeyEvent.ACTION_DOWN) { 2391 | switch (event.getKeyCode()) { 2392 | case KeyEvent.KEYCODE_DPAD_LEFT: 2393 | handled = arrowScroll(FOCUS_LEFT); 2394 | break; 2395 | case KeyEvent.KEYCODE_DPAD_RIGHT: 2396 | handled = arrowScroll(FOCUS_RIGHT); 2397 | break; 2398 | case KeyEvent.KEYCODE_TAB: 2399 | if (Build.VERSION.SDK_INT >= 11) { 2400 | // The focus finder had a bug handling FOCUS_FORWARD and FOCUS_BACKWARD 2401 | // before Android 3.0. Ignore the tab key on those devices. 2402 | if (KeyEventCompat.hasNoModifiers(event)) { 2403 | handled = arrowScroll(FOCUS_FORWARD); 2404 | } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_SHIFT_ON)) { 2405 | handled = arrowScroll(FOCUS_BACKWARD); 2406 | } 2407 | } 2408 | break; 2409 | } 2410 | } 2411 | return handled; 2412 | } 2413 | 2414 | public boolean arrowScroll(int direction) { 2415 | View currentFocused = findFocus(); 2416 | if (currentFocused == this) { 2417 | currentFocused = null; 2418 | } else if (currentFocused != null) { 2419 | boolean isChild = false; 2420 | for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup; 2421 | parent = parent.getParent()) { 2422 | if (parent == this) { 2423 | isChild = true; 2424 | break; 2425 | } 2426 | } 2427 | if (!isChild) { 2428 | // This would cause the focus search down below to fail in fun ways. 2429 | final StringBuilder sb = new StringBuilder(); 2430 | sb.append(currentFocused.getClass().getSimpleName()); 2431 | for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup; 2432 | parent = parent.getParent()) { 2433 | sb.append(" => ").append(parent.getClass().getSimpleName()); 2434 | } 2435 | Log.e(TAG, "arrowScroll tried to find focus based on non-child " + 2436 | "current focused view " + sb.toString()); 2437 | currentFocused = null; 2438 | } 2439 | } 2440 | 2441 | boolean handled = false; 2442 | 2443 | View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, 2444 | direction); 2445 | if (nextFocused != null && nextFocused != currentFocused) { 2446 | if (direction == View.FOCUS_UP) { 2447 | // If there is nothing to the left, or this is causing us to 2448 | // jump to the right, then what we really want to do is page left. 2449 | final int nextTop = getChildRectInPagerCoordinates(mTempRect, nextFocused).top; 2450 | final int currTop = getChildRectInPagerCoordinates(mTempRect, currentFocused).top; 2451 | if (currentFocused != null && nextTop >= currTop) { 2452 | handled = pageUp(); 2453 | } else { 2454 | handled = nextFocused.requestFocus(); 2455 | } 2456 | } else if (direction == View.FOCUS_DOWN) { 2457 | // If there is nothing to the right, or this is causing us to 2458 | // jump to the left, then what we really want to do is page right. 2459 | final int nextDown = getChildRectInPagerCoordinates(mTempRect, nextFocused).bottom; 2460 | final int currDown = getChildRectInPagerCoordinates(mTempRect, currentFocused).bottom; 2461 | if (currentFocused != null && nextDown <= currDown) { 2462 | handled = pageDown(); 2463 | } else { 2464 | handled = nextFocused.requestFocus(); 2465 | } 2466 | } 2467 | } else if (direction == FOCUS_UP || direction == FOCUS_BACKWARD) { 2468 | // Trying to move left and nothing there; try to page. 2469 | handled = pageUp(); 2470 | } else if (direction == FOCUS_DOWN || direction == FOCUS_FORWARD) { 2471 | // Trying to move right and nothing there; try to page. 2472 | handled = pageDown(); 2473 | } 2474 | if (handled) { 2475 | playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); 2476 | } 2477 | return handled; 2478 | } 2479 | 2480 | private Rect getChildRectInPagerCoordinates(Rect outRect, View child) { 2481 | if (outRect == null) { 2482 | outRect = new Rect(); 2483 | } 2484 | if (child == null) { 2485 | outRect.set(0, 0, 0, 0); 2486 | return outRect; 2487 | } 2488 | outRect.left = child.getLeft(); 2489 | outRect.right = child.getRight(); 2490 | outRect.top = child.getTop(); 2491 | outRect.bottom = child.getBottom(); 2492 | 2493 | ViewParent parent = child.getParent(); 2494 | while (parent instanceof ViewGroup && parent != this) { 2495 | final ViewGroup group = (ViewGroup) parent; 2496 | outRect.left += group.getLeft(); 2497 | outRect.right += group.getRight(); 2498 | outRect.top += group.getTop(); 2499 | outRect.bottom += group.getBottom(); 2500 | 2501 | parent = group.getParent(); 2502 | } 2503 | return outRect; 2504 | } 2505 | 2506 | boolean pageUp() { 2507 | if (mCurItem > 0) { 2508 | setCurrentItem(mCurItem - 1, true); 2509 | return true; 2510 | } 2511 | return false; 2512 | } 2513 | 2514 | boolean pageDown() { 2515 | if (mAdapter != null && mCurItem < (mAdapter.getCount() - 1)) { 2516 | setCurrentItem(mCurItem + 1, true); 2517 | return true; 2518 | } 2519 | return false; 2520 | } 2521 | 2522 | /** 2523 | * We only want the current page that is being shown to be focusable. 2524 | */ 2525 | @Override 2526 | public void addFocusables(ArrayList views, int direction, int focusableMode) { 2527 | final int focusableCount = views.size(); 2528 | 2529 | final int descendantFocusability = getDescendantFocusability(); 2530 | 2531 | if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) { 2532 | for (int i = 0; i < getChildCount(); i++) { 2533 | final View child = getChildAt(i); 2534 | if (child.getVisibility() == VISIBLE) { 2535 | ItemInfo ii = infoForChild(child); 2536 | if (ii != null && ii.position == mCurItem) { 2537 | child.addFocusables(views, direction, focusableMode); 2538 | } 2539 | } 2540 | } 2541 | } 2542 | 2543 | // we add ourselves (if focusable) in all cases except for when we are 2544 | // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable. this is 2545 | // to avoid the focus search finding layouts when a more precise search 2546 | // among the focusable children would be more interesting. 2547 | if ( 2548 | descendantFocusability != FOCUS_AFTER_DESCENDANTS || 2549 | // No focusable descendants 2550 | (focusableCount == views.size())) { 2551 | // Note that we can't call the superclass here, because it will 2552 | // add all views in. So we need to do the same thing View does. 2553 | if (!isFocusable()) { 2554 | return; 2555 | } 2556 | if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE && 2557 | isInTouchMode() && !isFocusableInTouchMode()) { 2558 | return; 2559 | } 2560 | if (views != null) { 2561 | views.add(this); 2562 | } 2563 | } 2564 | } 2565 | 2566 | /** 2567 | * We only want the current page that is being shown to be touchable. 2568 | */ 2569 | @Override 2570 | public void addTouchables(ArrayList views) { 2571 | // Note that we don't call super.addTouchables(), which means that 2572 | // we don't call View.addTouchables(). This is okay because a ViewPager 2573 | // is itself not touchable. 2574 | for (int i = 0; i < getChildCount(); i++) { 2575 | final View child = getChildAt(i); 2576 | if (child.getVisibility() == VISIBLE) { 2577 | ItemInfo ii = infoForChild(child); 2578 | if (ii != null && ii.position == mCurItem) { 2579 | child.addTouchables(views); 2580 | } 2581 | } 2582 | } 2583 | } 2584 | 2585 | /** 2586 | * We only want the current page that is being shown to be focusable. 2587 | */ 2588 | @Override 2589 | protected boolean onRequestFocusInDescendants(int direction, 2590 | Rect previouslyFocusedRect) { 2591 | int index; 2592 | int increment; 2593 | int end; 2594 | int count = getChildCount(); 2595 | if ((direction & FOCUS_FORWARD) != 0) { 2596 | index = 0; 2597 | increment = 1; 2598 | end = count; 2599 | } else { 2600 | index = count - 1; 2601 | increment = -1; 2602 | end = -1; 2603 | } 2604 | for (int i = index; i != end; i += increment) { 2605 | View child = getChildAt(i); 2606 | if (child.getVisibility() == VISIBLE) { 2607 | ItemInfo ii = infoForChild(child); 2608 | if (ii != null && ii.position == mCurItem) { 2609 | if (child.requestFocus(direction, previouslyFocusedRect)) { 2610 | return true; 2611 | } 2612 | } 2613 | } 2614 | } 2615 | return false; 2616 | } 2617 | 2618 | @Override 2619 | public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 2620 | // Dispatch scroll events from this ViewPager. 2621 | if (event.getEventType() == AccessibilityEventCompat.TYPE_VIEW_SCROLLED) { 2622 | return super.dispatchPopulateAccessibilityEvent(event); 2623 | } 2624 | 2625 | // Dispatch all other accessibility events from the current page. 2626 | final int childCount = getChildCount(); 2627 | for (int i = 0; i < childCount; i++) { 2628 | final View child = getChildAt(i); 2629 | if (child.getVisibility() == VISIBLE) { 2630 | final ItemInfo ii = infoForChild(child); 2631 | if (ii != null && ii.position == mCurItem && 2632 | child.dispatchPopulateAccessibilityEvent(event)) { 2633 | return true; 2634 | } 2635 | } 2636 | } 2637 | 2638 | return false; 2639 | } 2640 | 2641 | @Override 2642 | protected ViewGroup.LayoutParams generateDefaultLayoutParams() { 2643 | return new LayoutParams(); 2644 | } 2645 | 2646 | @Override 2647 | protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 2648 | return generateDefaultLayoutParams(); 2649 | } 2650 | 2651 | @Override 2652 | protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 2653 | return p instanceof LayoutParams && super.checkLayoutParams(p); 2654 | } 2655 | 2656 | @Override 2657 | public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 2658 | return new LayoutParams(getContext(), attrs); 2659 | } 2660 | 2661 | class MyAccessibilityDelegate extends AccessibilityDelegateCompat { 2662 | 2663 | @Override 2664 | public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { 2665 | super.onInitializeAccessibilityEvent(host, event); 2666 | event.setClassName(ViewPager.class.getName()); 2667 | final AccessibilityRecordCompat recordCompat = AccessibilityRecordCompat.obtain(); 2668 | recordCompat.setScrollable(canScroll()); 2669 | if (event.getEventType() == AccessibilityEventCompat.TYPE_VIEW_SCROLLED 2670 | && mAdapter != null) { 2671 | recordCompat.setItemCount(mAdapter.getCount()); 2672 | recordCompat.setFromIndex(mCurItem); 2673 | recordCompat.setToIndex(mCurItem); 2674 | } 2675 | } 2676 | 2677 | @Override 2678 | public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { 2679 | super.onInitializeAccessibilityNodeInfo(host, info); 2680 | info.setClassName(ViewPager.class.getName()); 2681 | info.setScrollable(canScroll()); 2682 | if (internalCanScrollVertically(1)) { 2683 | info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); 2684 | } 2685 | if (internalCanScrollVertically(-1)) { 2686 | info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); 2687 | } 2688 | } 2689 | 2690 | @Override 2691 | public boolean performAccessibilityAction(View host, int action, Bundle args) { 2692 | if (super.performAccessibilityAction(host, action, args)) { 2693 | return true; 2694 | } 2695 | switch (action) { 2696 | case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: { 2697 | if (internalCanScrollVertically(1)) { 2698 | setCurrentItem(mCurItem + 1); 2699 | return true; 2700 | } 2701 | } 2702 | return false; 2703 | case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: { 2704 | if (internalCanScrollVertically(-1)) { 2705 | setCurrentItem(mCurItem - 1); 2706 | return true; 2707 | } 2708 | } 2709 | return false; 2710 | } 2711 | return false; 2712 | } 2713 | 2714 | private boolean canScroll() { 2715 | return (mAdapter != null) && (mAdapter.getCount() > 1); 2716 | } 2717 | } 2718 | 2719 | private class PagerObserver extends DataSetObserver { 2720 | @Override 2721 | public void onChanged() { 2722 | dataSetChanged(); 2723 | } 2724 | 2725 | @Override 2726 | public void onInvalidated() { 2727 | dataSetChanged(); 2728 | } 2729 | } 2730 | 2731 | /** 2732 | * Layout parameters that should be supplied for views added to a 2733 | * ViewPager. 2734 | */ 2735 | public static class LayoutParams extends ViewGroup.LayoutParams { 2736 | /** 2737 | * true if this view is a decoration on the pager itself and not 2738 | * a view supplied by the adapter. 2739 | */ 2740 | public boolean isDecor; 2741 | 2742 | /** 2743 | * Gravity setting for use on decor views only: 2744 | * Where to position the view page within the overall ViewPager 2745 | * container; constants are defined in {@link android.view.Gravity}. 2746 | */ 2747 | public int gravity; 2748 | 2749 | /** 2750 | * Width as a 0-1 multiplier of the measured pager width 2751 | */ 2752 | float heightFactor = 0.f; 2753 | 2754 | /** 2755 | * true if this view was added during layout and needs to be measured 2756 | * before being positioned. 2757 | */ 2758 | boolean needsMeasure; 2759 | 2760 | /** 2761 | * Adapter position this view is for if !isDecor 2762 | */ 2763 | int position; 2764 | 2765 | /** 2766 | * Current child index within the ViewPager that this view occupies 2767 | */ 2768 | int childIndex; 2769 | 2770 | public LayoutParams() { 2771 | super(FILL_PARENT, FILL_PARENT); 2772 | } 2773 | 2774 | public LayoutParams(Context context, AttributeSet attrs) { 2775 | super(context, attrs); 2776 | 2777 | final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS); 2778 | gravity = a.getInteger(0, Gravity.TOP); 2779 | a.recycle(); 2780 | } 2781 | } 2782 | 2783 | static class ViewPositionComparator implements Comparator { 2784 | @Override 2785 | public int compare(View lhs, View rhs) { 2786 | final LayoutParams llp = (LayoutParams) lhs.getLayoutParams(); 2787 | final LayoutParams rlp = (LayoutParams) rhs.getLayoutParams(); 2788 | if (llp.isDecor != rlp.isDecor) { 2789 | return llp.isDecor ? 1 : -1; 2790 | } 2791 | return llp.position - rlp.position; 2792 | } 2793 | } 2794 | } 2795 | -------------------------------------------------------------------------------- /mvn_push.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Chris Banes 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 | apply plugin: 'maven' 18 | apply plugin: 'signing' 19 | 20 | def isReleaseBuild() { 21 | return version.contains("SNAPSHOT") == false 22 | } 23 | 24 | def getReleaseRepositoryUrl() { 25 | return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL 26 | : "https://oss.sonatype.org/service/local/staging/deploy/maven2/" 27 | } 28 | 29 | def getSnapshotRepositoryUrl() { 30 | return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL 31 | : "https://oss.sonatype.org/content/repositories/snapshots/" 32 | } 33 | 34 | def getRepositoryUsername() { 35 | return hasProperty('NEXUS_USERNAME') ? NEXUS_USERNAME : "" 36 | } 37 | 38 | def getRepositoryPassword() { 39 | return hasProperty('NEXUS_PASSWORD') ? NEXUS_PASSWORD : "" 40 | } 41 | 42 | afterEvaluate { project -> 43 | uploadArchives { 44 | repositories { 45 | mavenDeployer { 46 | beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } 47 | 48 | pom.artifactId = POM_ARTIFACT_ID 49 | 50 | repository(url: getReleaseRepositoryUrl()) { 51 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) 52 | } 53 | snapshotRepository(url: getSnapshotRepositoryUrl()) { 54 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) 55 | } 56 | 57 | pom.project { 58 | name POM_NAME 59 | packaging POM_PACKAGING 60 | description POM_DESCRIPTION 61 | url POM_URL 62 | 63 | scm { 64 | url POM_SCM_URL 65 | connection POM_SCM_CONNECTION 66 | developerConnection POM_SCM_DEV_CONNECTION 67 | } 68 | 69 | licenses { 70 | license { 71 | name POM_LICENCE_NAME 72 | url POM_LICENCE_URL 73 | distribution POM_LICENCE_DIST 74 | } 75 | } 76 | 77 | developers { 78 | developer { 79 | id POM_DEVELOPER_ID 80 | name POM_DEVELOPER_NAME 81 | } 82 | } 83 | } 84 | } 85 | } 86 | } 87 | 88 | signing { 89 | required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") } 90 | sign configurations.archives 91 | } 92 | 93 | task androidJavadocs(type: Javadoc) { 94 | source = android.sourceSets.main.allJava 95 | } 96 | 97 | task androidJavadocsJar(type: Jar) { 98 | classifier = 'javadoc' 99 | from androidJavadocs.destinationDir 100 | } 101 | 102 | task androidSourcesJar(type: Jar) { 103 | classifier = 'sources' 104 | from android.sourceSets.main.allSource 105 | } 106 | 107 | artifacts { 108 | archives androidSourcesJar 109 | archives androidJavadocsJar 110 | } 111 | } -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | repositories { 4 | mavenCentral() 5 | } 6 | 7 | dependencies { 8 | compile fileTree(dir: 'libs', include: ['*.jar']) 9 | compile 'com.android.support:support-v13:20.0.0' 10 | compile 'com.android.support:support-v4:23.4.0' 11 | compile project(':library') 12 | } 13 | 14 | android { 15 | compileSdkVersion 23 16 | buildToolsVersion "23.0.3" 17 | 18 | defaultConfig { 19 | applicationId "mobileplus.verticalviewpager" 20 | minSdkVersion 14 21 | targetSdkVersion 20 22 | versionCode 1 23 | versionName "1.0" 24 | } 25 | buildTypes { 26 | release { 27 | // runProguard false 28 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 29 | } 30 | } 31 | compileOptions { 32 | sourceCompatibility JavaVersion.VERSION_1_7 33 | targetCompatibility JavaVersion.VERSION_1_7 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /sample/src/main/java/fr/castorflex/android/verticalviewpager/sample/MainActivity.java: -------------------------------------------------------------------------------- 1 | package fr.castorflex.android.verticalviewpager.sample; 2 | 3 | import android.app.Activity; 4 | import android.app.Fragment; 5 | import android.app.FragmentManager; 6 | import android.graphics.Color; 7 | import android.graphics.drawable.ColorDrawable; 8 | import android.os.Bundle; 9 | import android.support.v13.app.FragmentPagerAdapter; 10 | import android.support.v4.view.ViewPager; 11 | import android.util.Log; 12 | import android.view.LayoutInflater; 13 | import android.view.View; 14 | import android.view.ViewGroup; 15 | import android.widget.ArrayAdapter; 16 | import android.widget.ListView; 17 | 18 | import java.util.ArrayList; 19 | import java.util.Collections; 20 | import java.util.List; 21 | 22 | import fr.castorflex.android.verticalviewpager.VerticalViewPager; 23 | import fr.castorflex.android.verticalviewpager.sample.verticaltablayout.QTabView; 24 | import fr.castorflex.android.verticalviewpager.sample.verticaltablayout.TabAdapter; 25 | import fr.castorflex.android.verticalviewpager.sample.verticaltablayout.TabView; 26 | import fr.castorflex.android.verticalviewpager.sample.verticaltablayout.VerticalTabLayout; 27 | 28 | public class MainActivity extends Activity { 29 | 30 | private static final float MIN_SCALE = 0.75f; 31 | private static final float MIN_ALPHA = 0.75f; 32 | 33 | @Override 34 | protected void onCreate(Bundle savedInstanceState) { 35 | super.onCreate(savedInstanceState); 36 | setContentView(R.layout.activity_main); 37 | final VerticalViewPager verticalViewPager = (VerticalViewPager) findViewById(R.id.verticalviewpager); 38 | final VerticalTabLayout tablayout = (VerticalTabLayout) findViewById(R.id.tablayout); 39 | tablayout.setTabAdapter(new MyTabAdapter()); 40 | tablayout.addOnTabSelectedListener(new VerticalTabLayout.OnTabSelectedListener() { 41 | @Override 42 | public void onTabSelected(TabView tab, int position) { 43 | verticalViewPager.setCurrentItem(position); 44 | } 45 | 46 | @Override 47 | public void onTabReselected(TabView tab, int position) { 48 | 49 | } 50 | }); 51 | verticalViewPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() { 52 | @Override 53 | public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 54 | 55 | } 56 | 57 | @Override 58 | public void onPageSelected(int position) { 59 | tablayout.setTabSelected(position); 60 | } 61 | 62 | @Override 63 | public void onPageScrollStateChanged(int state) { 64 | 65 | } 66 | }); 67 | verticalViewPager.setAdapter(new DummyAdapter(getFragmentManager())); 68 | verticalViewPager.setPageMargin(getResources(). 69 | getDimensionPixelSize(R.dimen.pagemargin)); 70 | verticalViewPager.setPageMarginDrawable(new ColorDrawable( 71 | getResources().getColor(android.R.color.holo_green_dark))); 72 | 73 | verticalViewPager.setPageTransformer(true, new ViewPager.PageTransformer() { 74 | @Override 75 | public void transformPage(View view, float position) { 76 | int pageWidth = view.getWidth(); 77 | int pageHeight = view.getHeight(); 78 | 79 | if (position < -1) { // [-Infinity,-1) 80 | // This page is way off-screen to the left. 81 | view.setAlpha(0); 82 | 83 | } else if (position <= 1) { // [-1,1] 84 | // Modify the default slide transition to shrink the page as well 85 | float scaleFactor = Math.max(MIN_SCALE, 1 - Math.abs(position)); 86 | float vertMargin = pageHeight * (1 - scaleFactor) / 2; 87 | float horzMargin = pageWidth * (1 - scaleFactor) / 2; 88 | if (position < 0) { 89 | view.setTranslationY(vertMargin - horzMargin / 2); 90 | } else { 91 | view.setTranslationY(-vertMargin + horzMargin / 2); 92 | } 93 | 94 | // Scale the page down (between MIN_SCALE and 1) 95 | view.setScaleX(scaleFactor); 96 | view.setScaleY(scaleFactor); 97 | 98 | // Fade the page relative to its size. 99 | view.setAlpha(MIN_ALPHA + 100 | (scaleFactor - MIN_SCALE) / 101 | (1 - MIN_SCALE) * (1 - MIN_ALPHA)); 102 | 103 | } else { // (1,+Infinity] 104 | // This page is way off-screen to the right. 105 | view.setAlpha(0); 106 | } 107 | } 108 | }); 109 | } 110 | 111 | public class DummyAdapter extends FragmentPagerAdapter { 112 | List fragments = new ArrayList<>(); 113 | 114 | public DummyAdapter(FragmentManager fm) { 115 | super(fm); 116 | 117 | for (int i = 0; i < 5; i++) { 118 | fragments.add(PlaceholderFragment.newInstance(i)); 119 | } 120 | } 121 | 122 | @Override 123 | public Fragment getItem(int position) { 124 | // getItem is called to instantiate the fragment for the given page. 125 | // Return a PlaceholderFragment (defined as a static inner class below). 126 | //return PlaceholderFragment.newInstance(position + 1); 127 | return fragments.get(position); 128 | } 129 | 130 | @Override 131 | public int getCount() { 132 | // Show 3 total pages. 133 | return 5; 134 | } 135 | 136 | @Override 137 | public CharSequence getPageTitle(int position) { 138 | switch (position) { 139 | case 0: 140 | return "PAGE 0"; 141 | case 1: 142 | return "PAGE 1"; 143 | case 2: 144 | return "PAGE 2"; 145 | case 3: 146 | return "PAGE 3"; 147 | case 4: 148 | return "PAGE 4"; 149 | } 150 | return null; 151 | } 152 | } 153 | 154 | /** 155 | * A placeholder fragment containing a simple view. 156 | */ 157 | public static class PlaceholderFragment extends Fragment { 158 | String[] array = new String[]{"Android 1", "Android 2", "Android 3", 159 | "Android 4", "Android 5", "Android 6", "Android 7", "Android 8", 160 | "Android 9", "Android 10", "Android 11", "Android 12", "Android 13", 161 | "Android 14", "Android 15", "Android 16"}; 162 | 163 | /** 164 | * The fragment argument representing the section number for this 165 | * fragment. 166 | */ 167 | private static final String ARG_SECTION_NUMBER = "section_number"; 168 | 169 | /** 170 | * Returns a new instance of this fragment for the given section 171 | * number. 172 | */ 173 | public static PlaceholderFragment newInstance(int sectionNumber) { 174 | PlaceholderFragment fragment = new PlaceholderFragment(); 175 | Bundle args = new Bundle(); 176 | args.putInt(ARG_SECTION_NUMBER, sectionNumber); 177 | fragment.setArguments(args); 178 | return fragment; 179 | } 180 | 181 | public PlaceholderFragment() { 182 | } 183 | 184 | @Override 185 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 186 | Bundle savedInstanceState) { 187 | final View rootView = inflater.inflate(R.layout.fragment_layout, container, false); 188 | 189 | Log.d("Debug", "creating fragment " 190 | + getArguments().getInt(ARG_SECTION_NUMBER)); 191 | 192 | switch (getArguments().getInt(ARG_SECTION_NUMBER)) { 193 | case 0: 194 | break; 195 | 196 | case 1: 197 | rootView.setBackgroundColor(Color.BLACK); 198 | break; 199 | 200 | case 2: 201 | rootView.setBackgroundColor(Color.BLUE); 202 | break; 203 | 204 | case 3: 205 | rootView.setBackgroundColor(Color.GREEN); 206 | break; 207 | 208 | case 4: 209 | rootView.setBackgroundColor(Color.RED); 210 | break; 211 | } 212 | final ListView listView = (ListView) rootView.findViewById(R.id.listView); 213 | listView.setAdapter(new ArrayAdapter<>(getActivity(), 214 | R.layout.list_item, R.id.text1, array)); 215 | 216 | return rootView; 217 | } 218 | } 219 | 220 | class MyTabAdapter implements TabAdapter { 221 | 222 | List titles; 223 | 224 | { 225 | titles = new ArrayList<>(); 226 | Collections.addAll(titles, "Android", "IOS", "Web", "JAVA", "C++" 227 | ); 228 | } 229 | 230 | @Override 231 | public int getCount() { 232 | return 5; 233 | } 234 | 235 | @Override 236 | public int getBadge(int position) { 237 | if (position == 5) return position; 238 | return 0; 239 | } 240 | 241 | @Override 242 | public QTabView.TabIcon getIcon(int position) { 243 | return null; 244 | } 245 | 246 | @Override 247 | public QTabView.TabTitle getTitle(int position) { 248 | return new QTabView.TabTitle.Builder(MainActivity.this) 249 | .setContent(titles.get(position)) 250 | .setTextColor(Color.BLUE, Color.BLACK) 251 | .build(); 252 | } 253 | 254 | @Override 255 | public int getBackground(int position) { 256 | return 0; 257 | } 258 | } 259 | 260 | } 261 | -------------------------------------------------------------------------------- /sample/src/main/java/fr/castorflex/android/verticalviewpager/sample/verticaltablayout/QTabIndicator.java: -------------------------------------------------------------------------------- 1 | package fr.castorflex.android.verticalviewpager.sample.verticaltablayout; 2 | 3 | /** 4 | * Created by chqiu on 2016/9/8. 5 | */ 6 | public class QTabIndicator extends TabIndicator{ 7 | 8 | 9 | 10 | public QTabIndicator(){ 11 | 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /sample/src/main/java/fr/castorflex/android/verticalviewpager/sample/verticaltablayout/QTabView.java: -------------------------------------------------------------------------------- 1 | package fr.castorflex.android.verticalviewpager.sample.verticaltablayout; 2 | 3 | import android.content.Context; 4 | import android.graphics.drawable.GradientDrawable; 5 | import android.text.TextUtils; 6 | import android.view.Gravity; 7 | import android.view.View; 8 | import android.widget.ImageView; 9 | import android.widget.LinearLayout; 10 | import android.widget.TextView; 11 | 12 | import fr.castorflex.android.verticalviewpager.sample.R; 13 | 14 | 15 | /** 16 | * @author chqiu 17 | * Email:qstumn@163.com 18 | */ 19 | public class QTabView extends TabView { 20 | private Context mContext; 21 | private ImageView mIcon; 22 | private TextView mTitle; 23 | private TextView mBadge; 24 | private int mMinHeight; 25 | private TabIcon mTabIcon; 26 | private TabTitle mTabTitle; 27 | private boolean mChecked; 28 | private LinearLayout mContainer; 29 | private GradientDrawable gd; 30 | 31 | public QTabView(Context context) { 32 | super(context); 33 | mContext = context; 34 | gd = new GradientDrawable(); 35 | gd.setColor(0xFFE84E40); 36 | mMinHeight = dp2px(30); 37 | mTabIcon = new TabIcon.Builder().build(); 38 | mTabTitle = new TabTitle.Builder(context).build(); 39 | initView(); 40 | } 41 | 42 | @Override 43 | protected int[] onCreateDrawableState(int extraSpace) { 44 | final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); 45 | if (isChecked()) { 46 | mergeDrawableStates(drawableState, new int[]{android.R.attr.state_checked}); 47 | } 48 | return drawableState; 49 | } 50 | 51 | private void initView() { 52 | initContainer(); 53 | initIconView(); 54 | initTitleView(); 55 | initBadge(); 56 | addView(mContainer); 57 | addView(mBadge); 58 | } 59 | 60 | private void initContainer() { 61 | mContainer = new LinearLayout(mContext); 62 | mContainer.setOrientation(LinearLayout.HORIZONTAL); 63 | mContainer.setMinimumHeight(mMinHeight); 64 | mContainer.setPadding(dp2px(5), dp2px(5), dp2px(5), dp2px(5)); 65 | mContainer.setGravity(Gravity.CENTER); 66 | } 67 | 68 | private void initBadge() { 69 | mBadge = new TextView(mContext); 70 | LayoutParams params2 = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); 71 | params2.gravity = Gravity.RIGHT | Gravity.TOP; 72 | params2.setMargins(0, dp2px(5), dp2px(5), 0); 73 | mBadge.setLayoutParams(params2); 74 | mBadge.setGravity(Gravity.CENTER); 75 | mBadge.setTextColor(0xFFFFFFFF); 76 | mBadge.setTextSize(9); 77 | setBadge(0); 78 | } 79 | 80 | private void initTitleView() { 81 | if (mTitle != null) mContainer.removeView(mTitle); 82 | mTitle = new TextView(mContext); 83 | LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); 84 | mTitle.setLayoutParams(params); 85 | mTitle.setTextColor(mTabTitle.mColorNormal); 86 | mTitle.setTextSize(mTabTitle.mTitleTextSize); 87 | mTitle.setText(mTabTitle.mContent); 88 | mTitle.setGravity(Gravity.CENTER); 89 | mTitle.setSingleLine(); 90 | mTitle.setEllipsize(TextUtils.TruncateAt.END); 91 | // mTitle.setPadding(dp2px(5), dp2px(5), dp2px(5), dp2px(5)); 92 | requestContainerLayout(mTabIcon.mIconGravity); 93 | } 94 | 95 | private void initIconView() { 96 | if (mIcon != null) mContainer.removeView(mIcon); 97 | mIcon = new ImageView(mContext); 98 | LayoutParams params = new LayoutParams(mTabIcon.mIconWidth, mTabIcon.mIconHeight); 99 | mIcon.setLayoutParams(params); 100 | if (mTabIcon.mNormalIcon != 0) { 101 | mIcon.setImageResource(mTabIcon.mNormalIcon); 102 | } else { 103 | mIcon.setVisibility(View.GONE); 104 | } 105 | requestContainerLayout(mTabIcon.mIconGravity); 106 | } 107 | 108 | private void setBadgeImp(int num) { 109 | LayoutParams lp = (LayoutParams) mBadge.getLayoutParams(); 110 | if (num <= 9) { 111 | lp.width = dp2px(12); 112 | lp.height = dp2px(12); 113 | gd.setShape(GradientDrawable.OVAL); 114 | mBadge.setPadding(0, 0, 0, 0); 115 | } else { 116 | lp.width = LayoutParams.WRAP_CONTENT; 117 | lp.height = LayoutParams.WRAP_CONTENT; 118 | mBadge.setPadding(dp2px(3), 0, dp2px(3), 0); 119 | gd.setShape(GradientDrawable.RECTANGLE); 120 | gd.setCornerRadius(dp2px(6)); 121 | } 122 | mBadge.setLayoutParams(lp); 123 | mBadge.setBackgroundDrawable(gd); 124 | mBadge.setText(String.valueOf(num)); 125 | mBadge.setVisibility(View.VISIBLE); 126 | } 127 | 128 | @Override 129 | public QTabView setBadge(int num) { 130 | if (num > 0) { 131 | setBadgeImp(num); 132 | } else { 133 | mBadge.setText(""); 134 | mBadge.setVisibility(View.GONE); 135 | } 136 | return this; 137 | } 138 | 139 | public QTabView setIcon(TabIcon icon) { 140 | if (icon != null) 141 | mTabIcon = icon; 142 | initIconView(); 143 | setChecked(mChecked); 144 | return this; 145 | } 146 | 147 | public QTabView setTitle(TabTitle title) { 148 | if (title != null) 149 | mTabTitle = title; 150 | initTitleView(); 151 | setChecked(mChecked); 152 | return this; 153 | } 154 | 155 | public QTabView setBackground(int resId) { 156 | super.setBackgroundResource(resId); 157 | return this; 158 | } 159 | 160 | private void requestContainerLayout(int gravity) { 161 | mContainer.removeAllViews(); 162 | switch (gravity) { 163 | case Gravity.LEFT: 164 | mContainer.setOrientation(LinearLayout.HORIZONTAL); 165 | if (mIcon != null) { 166 | mContainer.addView(mIcon); 167 | LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mIcon.getLayoutParams(); 168 | lp.setMargins(0, 0, mTabIcon.mMargin, 0); 169 | mIcon.setLayoutParams(lp); 170 | } 171 | if (mTitle != null) 172 | mContainer.addView(mTitle); 173 | break; 174 | case Gravity.TOP: 175 | mContainer.setOrientation(LinearLayout.VERTICAL); 176 | if (mIcon != null) { 177 | mContainer.addView(mIcon); 178 | LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mIcon.getLayoutParams(); 179 | lp.setMargins(0, 0, 0, mTabIcon.mMargin); 180 | mIcon.setLayoutParams(lp); 181 | } 182 | if (mTitle != null) 183 | mContainer.addView(mTitle); 184 | break; 185 | case Gravity.RIGHT: 186 | mContainer.setOrientation(LinearLayout.HORIZONTAL); 187 | if (mTitle != null) 188 | mContainer.addView(mTitle); 189 | if (mIcon != null) { 190 | mContainer.addView(mIcon); 191 | LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mIcon.getLayoutParams(); 192 | lp.setMargins(mTabIcon.mMargin, 0, 0, 0); 193 | mIcon.setLayoutParams(lp); 194 | } 195 | 196 | break; 197 | case Gravity.BOTTOM: 198 | mContainer.setOrientation(LinearLayout.VERTICAL); 199 | if (mTitle != null) 200 | mContainer.addView(mTitle); 201 | if (mIcon != null) { 202 | mContainer.addView(mIcon); 203 | LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mIcon.getLayoutParams(); 204 | lp.setMargins(0, mTabIcon.mMargin, 0, 0); 205 | mIcon.setLayoutParams(lp); 206 | } 207 | break; 208 | } 209 | } 210 | 211 | protected int dp2px(float dp) { 212 | final float scale = mContext.getResources().getDisplayMetrics().density; 213 | return (int) (dp * scale + 0.5f); 214 | } 215 | 216 | @Override 217 | public void setChecked(boolean checked) { 218 | mChecked = checked; 219 | refreshDrawableState(); 220 | if (mChecked) { 221 | mTitle.setTextColor(mTabTitle.mColorSelected); 222 | if (mTabIcon.mSelectedIcon != 0) { 223 | mIcon.setVisibility(View.VISIBLE); 224 | mIcon.setImageResource(mTabIcon.mSelectedIcon); 225 | } else { 226 | mIcon.setVisibility(View.GONE); 227 | } 228 | } else { 229 | mTitle.setTextColor(mTabTitle.mColorNormal); 230 | if (mTabIcon.mNormalIcon != 0) { 231 | mIcon.setVisibility(View.VISIBLE); 232 | mIcon.setImageResource(mTabIcon.mNormalIcon); 233 | } else { 234 | mIcon.setVisibility(View.GONE); 235 | } 236 | } 237 | } 238 | 239 | @Override 240 | public boolean isChecked() { 241 | return mChecked; 242 | } 243 | 244 | @Override 245 | public void toggle() { 246 | setChecked(!mChecked); 247 | } 248 | 249 | public static class TabIcon { 250 | public int mSelectedIcon; 251 | public int mNormalIcon; 252 | public int mIconGravity; 253 | public int mIconWidth; 254 | public int mIconHeight; 255 | public int mMargin; 256 | 257 | private TabIcon(int mSelectedIcon, int mNormalIcon, int mIconGravity, int mIconWidth, int mIconHeight, int mMargin) { 258 | this.mSelectedIcon = mSelectedIcon; 259 | this.mNormalIcon = mNormalIcon; 260 | this.mIconGravity = mIconGravity; 261 | this.mIconWidth = mIconWidth; 262 | this.mIconHeight = mIconHeight; 263 | this.mMargin = mMargin; 264 | } 265 | 266 | public static class Builder { 267 | private int mSelectedIcon; 268 | private int mNormalIcon; 269 | private int mIconGravity; 270 | private int mIconWidth; 271 | private int mIconHeight; 272 | public int mMargin; 273 | 274 | public Builder() { 275 | mSelectedIcon = 0; 276 | mNormalIcon = 0; 277 | mIconWidth = LayoutParams.WRAP_CONTENT; 278 | mIconHeight = LayoutParams.WRAP_CONTENT; 279 | mIconGravity = Gravity.LEFT; 280 | mMargin = 0; 281 | } 282 | 283 | public Builder setIcon(int selectIconResId, int normalIconResId) { 284 | mSelectedIcon = selectIconResId; 285 | mNormalIcon = normalIconResId; 286 | return this; 287 | } 288 | 289 | public Builder setIconSize(int width, int height) { 290 | mIconWidth = width; 291 | mIconHeight = height; 292 | return this; 293 | } 294 | 295 | public Builder setIconGravity(int gravity) { 296 | if (gravity != Gravity.LEFT && gravity != Gravity.RIGHT 297 | & gravity != Gravity.TOP & gravity != Gravity.BOTTOM) { 298 | throw new IllegalStateException("iconGravity only support Gravity.LEFT " + 299 | "or Gravity.RIGHT or Gravity.TOP or Gravity.BOTTOM"); 300 | } 301 | mIconGravity = gravity; 302 | return this; 303 | } 304 | 305 | public Builder setIconMargin(int margin) { 306 | mMargin = margin; 307 | return this; 308 | } 309 | 310 | public TabIcon build() { 311 | return new TabIcon(mSelectedIcon, mNormalIcon, mIconGravity, mIconWidth, mIconHeight, mMargin); 312 | } 313 | } 314 | } 315 | 316 | public static class TabTitle { 317 | public int mColorSelected; 318 | public int mColorNormal; 319 | public int mTitleTextSize; 320 | public String mContent; 321 | 322 | private TabTitle(int mColorSelected, int mColorNormal, int mTitleTextSize, String mContent) { 323 | this.mColorSelected = mColorSelected; 324 | this.mColorNormal = mColorNormal; 325 | this.mTitleTextSize = mTitleTextSize; 326 | this.mContent = mContent; 327 | } 328 | 329 | public static class Builder { 330 | private int mColorSelected; 331 | private int mColorNormal; 332 | private int mTitleTextSize; 333 | private String mContent; 334 | 335 | public Builder(Context context) { 336 | this.mColorSelected = context.getResources().getColor(R.color.colorAccent); 337 | this.mColorNormal = 0xFF757575; 338 | this.mTitleTextSize = 16; 339 | this.mContent = "title"; 340 | } 341 | 342 | public Builder setTextColor(int colorSelected, int colorNormal) { 343 | mColorSelected = colorSelected; 344 | mColorNormal = colorNormal; 345 | return this; 346 | } 347 | 348 | public Builder setTextSize(int sizeSp) { 349 | mTitleTextSize = sizeSp; 350 | return this; 351 | } 352 | 353 | public Builder setContent(String content) { 354 | mContent = content; 355 | return this; 356 | } 357 | 358 | public TabTitle build() { 359 | return new TabTitle(mColorSelected, mColorNormal, mTitleTextSize, mContent); 360 | } 361 | } 362 | } 363 | } -------------------------------------------------------------------------------- /sample/src/main/java/fr/castorflex/android/verticalviewpager/sample/verticaltablayout/TabAdapter.java: -------------------------------------------------------------------------------- 1 | package fr.castorflex.android.verticalviewpager.sample.verticaltablayout; 2 | 3 | 4 | /** 5 | * @author chqiu 6 | * Email:qstumn@163.com 7 | */ 8 | public interface TabAdapter { 9 | int getCount(); 10 | 11 | int getBadge(int position); 12 | 13 | QTabView.TabIcon getIcon(int position); 14 | 15 | QTabView.TabTitle getTitle(int position); 16 | 17 | int getBackground(int position); 18 | } 19 | -------------------------------------------------------------------------------- /sample/src/main/java/fr/castorflex/android/verticalviewpager/sample/verticaltablayout/TabIndicator.java: -------------------------------------------------------------------------------- 1 | package fr.castorflex.android.verticalviewpager.sample.verticaltablayout; 2 | 3 | import android.graphics.drawable.GradientDrawable; 4 | 5 | /** 6 | * Created by chqiu on 2016/9/8. 7 | */ 8 | public abstract class TabIndicator extends GradientDrawable { 9 | protected int mIndicatorWidth; 10 | protected int mIndicatorGravity; 11 | protected float mIndicatorCorners; 12 | 13 | 14 | } 15 | -------------------------------------------------------------------------------- /sample/src/main/java/fr/castorflex/android/verticalviewpager/sample/verticaltablayout/TabView.java: -------------------------------------------------------------------------------- 1 | package fr.castorflex.android.verticalviewpager.sample.verticaltablayout; 2 | 3 | import android.content.Context; 4 | import android.widget.Checkable; 5 | import android.widget.FrameLayout; 6 | 7 | /** 8 | * @author chqiu 9 | * Email:qstumn@163.com 10 | */ 11 | public abstract class TabView extends FrameLayout implements Checkable { 12 | 13 | public TabView(Context context) { 14 | super(context); 15 | } 16 | 17 | public abstract TabView setBadge(int num); 18 | } 19 | -------------------------------------------------------------------------------- /sample/src/main/java/fr/castorflex/android/verticalviewpager/sample/verticaltablayout/VerticalTabLayout.java: -------------------------------------------------------------------------------- 1 | package fr.castorflex.android.verticalviewpager.sample.verticaltablayout; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorListenerAdapter; 5 | import android.animation.ValueAnimator; 6 | import android.content.Context; 7 | import android.content.res.TypedArray; 8 | import android.database.DataSetObserver; 9 | import android.graphics.Canvas; 10 | import android.graphics.Paint; 11 | import android.graphics.RectF; 12 | import android.support.annotation.Nullable; 13 | import android.support.v4.view.PagerAdapter; 14 | import android.support.v4.view.ViewPager; 15 | import android.util.AttributeSet; 16 | import android.view.Gravity; 17 | import android.view.View; 18 | import android.widget.LinearLayout; 19 | import android.widget.ScrollView; 20 | 21 | import java.util.ArrayList; 22 | import java.util.List; 23 | 24 | import fr.castorflex.android.verticalviewpager.sample.R; 25 | 26 | /** 27 | * @author chqiu 28 | * Email:qstumn@163.com 29 | */ 30 | public class VerticalTabLayout extends ScrollView { 31 | private Context mContext; 32 | private TabStrip mTabStrip; 33 | private int mColorIndicator; 34 | private TabView mSelectedTab; 35 | private int mTabMargin; 36 | private int mIndicatorWidth; 37 | private int mIndicatorGravity; 38 | private float mIndicatorCorners; 39 | private TabIndicator mIndicator; 40 | private int mTabMode; 41 | private int mTabHeight; 42 | 43 | public static int TAB_MODE_FIXED = 10; 44 | public static int TAB_MODE_SCROLLABLE = 11; 45 | 46 | private ViewPager mViewPager; 47 | private PagerAdapter mPagerAdapter; 48 | private TabAdapter mTabAdapter; 49 | 50 | private List mTabSelectedListeners; 51 | private OnTabPageChangeListener mTabPageChangeListener; 52 | private DataSetObserver mPagerAdapterObserver; 53 | 54 | public VerticalTabLayout(Context context) { 55 | this(context, null); 56 | } 57 | 58 | public VerticalTabLayout(Context context, AttributeSet attrs) { 59 | this(context, attrs, 0); 60 | } 61 | 62 | public VerticalTabLayout(Context context, AttributeSet attrs, int defStyleAttr) { 63 | super(context, attrs, defStyleAttr); 64 | mContext = context; 65 | setFillViewport(true); 66 | mTabSelectedListeners = new ArrayList<>(); 67 | TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.VerticalTabLayout); 68 | mColorIndicator = typedArray.getColor(R.styleable.VerticalTabLayout_indicator_color, 69 | context.getResources().getColor(R.color.colorAccent)); 70 | mIndicatorWidth = (int) typedArray.getDimension(R.styleable.VerticalTabLayout_indicator_width, dp2px(3)); 71 | mIndicatorCorners = typedArray.getDimension(R.styleable.VerticalTabLayout_indicator_corners, 0); 72 | mIndicatorGravity = typedArray.getInteger(R.styleable.VerticalTabLayout_indicator_gravity, Gravity.LEFT); 73 | mTabMargin = (int) typedArray.getDimension(R.styleable.VerticalTabLayout_tab_margin, 0); 74 | mTabMode = typedArray.getInteger(R.styleable.VerticalTabLayout_tab_mode, TAB_MODE_FIXED); 75 | mTabHeight = (int) typedArray.getDimension(R.styleable.VerticalTabLayout_tab_height, LayoutParams.WRAP_CONTENT); 76 | typedArray.recycle(); 77 | } 78 | 79 | @Override 80 | protected void onFinishInflate() { 81 | super.onFinishInflate(); 82 | if (getChildCount() > 0) removeAllViews(); 83 | initTabStrip(); 84 | } 85 | 86 | private void initTabStrip() { 87 | mTabStrip = new TabStrip(mContext); 88 | addView(mTabStrip, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); 89 | } 90 | 91 | public void removeAllTabs() { 92 | mTabStrip.removeAllViews(); 93 | mSelectedTab = null; 94 | } 95 | 96 | public TabView getTabAt(int position) { 97 | return (TabView) mTabStrip.getChildAt(position); 98 | } 99 | 100 | private void addTabWithMode(TabView tabView) { 101 | LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); 102 | initTabWithMode(params); 103 | mTabStrip.addView(tabView, params); 104 | if (mTabStrip.indexOfChild(tabView) == 0) { 105 | tabView.setChecked(true); 106 | params = (LinearLayout.LayoutParams) tabView.getLayoutParams(); 107 | params.setMargins(0, 0, 0, 0); 108 | tabView.setLayoutParams(params); 109 | mSelectedTab = tabView; 110 | } 111 | } 112 | 113 | private void initTabWithMode(LinearLayout.LayoutParams params) { 114 | if (mTabMode == TAB_MODE_FIXED) { 115 | params.height = 0; 116 | params.weight = 1.0f; 117 | params.setMargins(0, 0, 0, 0); 118 | } else if (mTabMode == TAB_MODE_SCROLLABLE) { 119 | params.height = mTabHeight; 120 | params.weight = 0f; 121 | params.setMargins(0, mTabMargin, 0, 0); 122 | } 123 | } 124 | 125 | private void scrollToTab(int position) { 126 | final TabView tabView = getTabAt(position); 127 | tabView.post(new Runnable() { 128 | @Override 129 | public void run() { 130 | int y = getScrollY(); 131 | int tabTop = tabView.getTop() + tabView.getHeight() / 2 - y; 132 | int target = getHeight() / 2; 133 | if (tabTop > target) { 134 | smoothScrollBy(0, tabTop - target); 135 | } else if (tabTop < target) { 136 | smoothScrollBy(0, tabTop - target); 137 | } 138 | } 139 | }); 140 | } 141 | 142 | private float mLastPositionOffset; 143 | 144 | private void scrollByTab(int position, final float positionOffset) { 145 | final TabView tabView = getTabAt(position); 146 | int y = getScrollY(); 147 | int tabTop = tabView.getTop() + tabView.getHeight() / 2 - y; 148 | int target = getHeight() / 2; 149 | int nextScrollY = tabView.getHeight() + mTabMargin; 150 | if (positionOffset > 0) { 151 | float percent = positionOffset - mLastPositionOffset; 152 | if (tabTop > target) { 153 | smoothScrollBy(0, (int) (nextScrollY * percent)); 154 | } 155 | } 156 | mLastPositionOffset = positionOffset; 157 | } 158 | 159 | public void addTab(TabView tabView) { 160 | if (tabView != null) { 161 | addTabWithMode(tabView); 162 | tabView.setOnClickListener(new OnClickListener() { 163 | @Override 164 | public void onClick(View view) { 165 | int position = mTabStrip.indexOfChild(view); 166 | setTabSelected(position); 167 | } 168 | }); 169 | } else { 170 | throw new IllegalStateException("tabview can't be null"); 171 | } 172 | } 173 | 174 | public void setTabSelected(int position) { 175 | TabView view = getTabAt(position); 176 | for (int i = 0; i < mTabSelectedListeners.size(); i++) { 177 | OnTabSelectedListener listener = mTabSelectedListeners.get(i); 178 | if (listener != null) { 179 | if (view == mSelectedTab) { 180 | listener.onTabReselected(view, position); 181 | } else { 182 | listener.onTabSelected(view, position); 183 | } 184 | } 185 | } 186 | if (view != mSelectedTab) { 187 | mSelectedTab.setChecked(false); 188 | view.setChecked(true); 189 | // if (mViewPager == null) 190 | mTabStrip.moveIndicator(position); 191 | mSelectedTab = view; 192 | scrollToTab(position); 193 | } 194 | } 195 | 196 | public void setTabBadge(int tabPosition, int badgeNum) { 197 | getTabAt(tabPosition).setBadge(badgeNum); 198 | } 199 | 200 | public void setTabMode(int mode) { 201 | if (mode != TAB_MODE_FIXED && mode != TAB_MODE_SCROLLABLE) { 202 | throw new IllegalStateException("only support TAB_MODE_FIXED or TAB_MODE_SCROLLABLE"); 203 | } 204 | if (mode == mTabMode) return; 205 | mTabMode = mode; 206 | for (int i = 0; i < mTabStrip.getChildCount(); i++) { 207 | View view = mTabStrip.getChildAt(i); 208 | LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) view.getLayoutParams(); 209 | initTabWithMode(params); 210 | if (i == 0) { 211 | params.setMargins(0, 0, 0, 0); 212 | } 213 | view.setLayoutParams(params); 214 | } 215 | mTabStrip.invalidate(); 216 | mTabStrip.post(new Runnable() { 217 | @Override 218 | public void run() { 219 | mTabStrip.updataIndicatorMargin(); 220 | } 221 | }); 222 | } 223 | 224 | /** 225 | * only in TAB_MODE_SCROLLABLE mode will be supported 226 | * 227 | * @param margin margin 228 | */ 229 | public void setTabMargin(int margin) { 230 | if (margin == mTabMargin) return; 231 | mTabMargin = margin; 232 | if (mTabMode == TAB_MODE_FIXED) return; 233 | for (int i = 0; i < mTabStrip.getChildCount(); i++) { 234 | View view = mTabStrip.getChildAt(i); 235 | LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) view.getLayoutParams(); 236 | params.setMargins(0, i == 0 ? 0 : mTabMargin, 0, 0); 237 | view.setLayoutParams(params); 238 | } 239 | mTabStrip.invalidate(); 240 | mTabStrip.post(new Runnable() { 241 | @Override 242 | public void run() { 243 | mTabStrip.updataIndicatorMargin(); 244 | } 245 | }); 246 | } 247 | 248 | /** 249 | * only in TAB_MODE_SCROLLABLE mode will be supported 250 | * 251 | * @param height height 252 | */ 253 | public void setTabHeight(int height) { 254 | if (height == mTabHeight) return; 255 | mTabHeight = height; 256 | if (mTabMode == TAB_MODE_FIXED) return; 257 | for (int i = 0; i < mTabStrip.getChildCount(); i++) { 258 | View view = mTabStrip.getChildAt(i); 259 | LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) view.getLayoutParams(); 260 | params.height = mTabHeight; 261 | view.setLayoutParams(params); 262 | } 263 | mTabStrip.invalidate(); 264 | mTabStrip.post(new Runnable() { 265 | @Override 266 | public void run() { 267 | mTabStrip.updataIndicatorMargin(); 268 | } 269 | }); 270 | } 271 | 272 | public void setIndicatorColor(int color) { 273 | mColorIndicator = color; 274 | mTabStrip.invalidate(); 275 | } 276 | 277 | public void setIndicatorWidth(int width) { 278 | mIndicatorWidth = width; 279 | mTabStrip.setIndicatorGravity(); 280 | } 281 | 282 | public void setIndicatorCorners(int corners) { 283 | mIndicatorCorners = corners; 284 | mTabStrip.invalidate(); 285 | } 286 | 287 | /** 288 | * @param gravity only support Gravity.LEFT,Gravity.RIGHT,Gravity.FILL 289 | */ 290 | public void setIndicatorGravity(int gravity) { 291 | if (gravity == Gravity.LEFT || gravity == Gravity.RIGHT || Gravity.FILL == gravity) { 292 | mIndicatorGravity = gravity; 293 | mTabStrip.setIndicatorGravity(); 294 | } else { 295 | throw new IllegalStateException("only support Gravity.LEFT,Gravity.RIGHT,Gravity.FILL"); 296 | } 297 | } 298 | 299 | public void addOnTabSelectedListener(OnTabSelectedListener listener) { 300 | if (listener != null) { 301 | mTabSelectedListeners.add(listener); 302 | } 303 | } 304 | 305 | public void setTabAdapter(TabAdapter adapter) { 306 | removeAllTabs(); 307 | if (adapter != null) { 308 | mTabAdapter = adapter; 309 | for (int i = 0; i < adapter.getCount(); i++) { 310 | addTab(new QTabView(mContext).setIcon(adapter.getIcon(i)) 311 | .setTitle(adapter.getTitle(i)).setBadge(adapter.getBadge(i)) 312 | .setBackground(adapter.getBackground(i))); 313 | } 314 | } else { 315 | removeAllTabs(); 316 | } 317 | } 318 | 319 | public void setupWithViewPager(@Nullable ViewPager viewPager) { 320 | if (mViewPager != null && mTabPageChangeListener != null) { 321 | mViewPager.removeOnPageChangeListener(mTabPageChangeListener); 322 | } 323 | 324 | if (viewPager != null) { 325 | final PagerAdapter adapter = viewPager.getAdapter(); 326 | if (adapter == null) { 327 | throw new IllegalArgumentException("ViewPager does not have a PagerAdapter set"); 328 | } 329 | 330 | mViewPager = viewPager; 331 | 332 | if (mTabPageChangeListener == null) { 333 | mTabPageChangeListener = new OnTabPageChangeListener(); 334 | } 335 | viewPager.addOnPageChangeListener(mTabPageChangeListener); 336 | 337 | addOnTabSelectedListener(new OnTabSelectedListener() { 338 | @Override 339 | public void onTabSelected(TabView tab, int position) { 340 | if (mViewPager != null && mViewPager.getAdapter().getCount() >= position) { 341 | mViewPager.setCurrentItem(position); 342 | } 343 | } 344 | 345 | @Override 346 | public void onTabReselected(TabView tab, int position) { 347 | } 348 | }); 349 | 350 | setPagerAdapter(adapter, true); 351 | } else { 352 | mViewPager = null; 353 | setPagerAdapter(null, true); 354 | } 355 | } 356 | 357 | private void setPagerAdapter(@Nullable final PagerAdapter adapter, final boolean addObserver) { 358 | if (mPagerAdapter != null && mPagerAdapterObserver != null) { 359 | mPagerAdapter.unregisterDataSetObserver(mPagerAdapterObserver); 360 | } 361 | 362 | mPagerAdapter = adapter; 363 | 364 | if (addObserver && adapter != null) { 365 | if (mPagerAdapterObserver == null) { 366 | mPagerAdapterObserver = new PagerAdapterObserver(); 367 | } 368 | adapter.registerDataSetObserver(mPagerAdapterObserver); 369 | } 370 | 371 | populateFromPagerAdapter(); 372 | } 373 | 374 | private void populateFromPagerAdapter() { 375 | removeAllTabs(); 376 | if (mPagerAdapter != null) { 377 | final int adapterCount = mPagerAdapter.getCount(); 378 | for (int i = 0; i < adapterCount; i++) { 379 | if (mPagerAdapter instanceof TabAdapter) { 380 | mTabAdapter = (TabAdapter) mPagerAdapter; 381 | addTab(new QTabView(mContext).setIcon(mTabAdapter.getIcon(i)) 382 | .setTitle(mTabAdapter.getTitle(i)).setBadge(mTabAdapter.getBadge(i)) 383 | .setBackground(mTabAdapter.getBackground(i))); 384 | } else { 385 | String title = mPagerAdapter.getPageTitle(i) == null ? "tab" + i : mPagerAdapter.getPageTitle(i).toString(); 386 | addTab(new QTabView(mContext).setTitle( 387 | new QTabView.TabTitle.Builder(mContext).setContent(title).build())); 388 | } 389 | } 390 | 391 | // Make sure we reflect the currently set ViewPager item 392 | if (mViewPager != null && adapterCount > 0) { 393 | final int curItem = mViewPager.getCurrentItem(); 394 | if (curItem != getSelectedTabPosition() && curItem < getTabCount()) { 395 | setTabSelected(curItem); 396 | } 397 | } 398 | } else { 399 | removeAllTabs(); 400 | } 401 | } 402 | 403 | private int getTabCount() { 404 | return mTabStrip.getChildCount(); 405 | } 406 | 407 | private int getSelectedTabPosition() { 408 | // if (mViewPager != null) return mViewPager.getCurrentItem(); 409 | int index = mTabStrip.indexOfChild(mSelectedTab); 410 | return index == -1 ? 0 : index; 411 | } 412 | 413 | 414 | private class TabStrip extends LinearLayout { 415 | private float mIndicatorY; 416 | private float mIndicatorX; 417 | private float mIndicatorBottomY; 418 | private int mLastWidth; 419 | private int mIndicatorHeight; 420 | private Paint mIndicatorPaint; 421 | //record invalidate count,used to initialize on mIndicatorBottomY 422 | private long mInvalidateCount; 423 | 424 | public TabStrip(Context context) { 425 | super(context); 426 | setWillNotDraw(false); 427 | setOrientation(LinearLayout.VERTICAL); 428 | mIndicatorPaint = new Paint(); 429 | mIndicatorGravity = mIndicatorGravity == 0 ? Gravity.LEFT : mIndicatorGravity; 430 | setIndicatorGravity(); 431 | } 432 | 433 | @Override 434 | protected void onFinishInflate() { 435 | super.onFinishInflate(); 436 | mIndicatorBottomY = mIndicatorHeight; 437 | } 438 | 439 | @Override 440 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 441 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 442 | if (getChildCount() > 0) { 443 | View childView = getChildAt(0); 444 | mIndicatorHeight = childView.getMeasuredHeight(); 445 | if (mInvalidateCount == 0) { 446 | mIndicatorBottomY = mIndicatorHeight; 447 | } 448 | mInvalidateCount++; 449 | } 450 | } 451 | 452 | protected void updataIndicatorMargin() { 453 | int index = getSelectedTabPosition(); 454 | mIndicatorY = calcIndicatorY(index); 455 | mIndicatorBottomY = mIndicatorY + mIndicatorHeight; 456 | invalidate(); 457 | } 458 | 459 | 460 | protected void setIndicatorGravity() { 461 | if (mIndicatorGravity == Gravity.LEFT) { 462 | mIndicatorX = 0; 463 | if (mLastWidth != 0) mIndicatorWidth = mLastWidth; 464 | setPadding(mIndicatorWidth, 0, 0, 0); 465 | } else if (mIndicatorGravity == Gravity.RIGHT) { 466 | if (mLastWidth != 0) mIndicatorWidth = mLastWidth; 467 | setPadding(0, 0, mIndicatorWidth, 0); 468 | } else if (mIndicatorGravity == Gravity.FILL) { 469 | mIndicatorX = 0; 470 | setPadding(0, 0, 0, 0); 471 | } 472 | post(new Runnable() { 473 | @Override 474 | public void run() { 475 | if (mIndicatorGravity == Gravity.RIGHT) { 476 | mIndicatorX = getWidth() - mIndicatorWidth; 477 | } else if (mIndicatorGravity == Gravity.FILL) { 478 | mLastWidth = mIndicatorWidth; 479 | mIndicatorWidth = getWidth(); 480 | } 481 | invalidate(); 482 | } 483 | }); 484 | } 485 | 486 | private float calcIndicatorY(float offset) { 487 | if (mTabMode == TAB_MODE_FIXED) 488 | return offset * mIndicatorHeight; 489 | return offset * (mIndicatorHeight + mTabMargin); 490 | } 491 | 492 | 493 | protected void moveIndicator(float offset) { 494 | mIndicatorY = calcIndicatorY(offset); 495 | mIndicatorBottomY = mIndicatorY + mIndicatorHeight; 496 | invalidate(); 497 | } 498 | 499 | /** 500 | * move indicator to a tab location 501 | * 502 | * @param index tab location's index 503 | */ 504 | protected void moveIndicator(final int index) { 505 | final int direction = index - getSelectedTabPosition(); 506 | final float target = calcIndicatorY(index); 507 | final float targetBottom = target + mIndicatorHeight; 508 | if (mIndicatorY == target) return; 509 | post(new Runnable() { 510 | @Override 511 | public void run() { 512 | ValueAnimator anime = null; 513 | if (direction > 0) { 514 | anime = ValueAnimator.ofFloat(mIndicatorBottomY, targetBottom); 515 | anime.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 516 | @Override 517 | public void onAnimationUpdate(ValueAnimator animation) { 518 | float value = Float.parseFloat(animation.getAnimatedValue().toString()); 519 | mIndicatorBottomY = value; 520 | invalidate(); 521 | } 522 | }); 523 | anime.addListener(new AnimatorListenerAdapter() { 524 | @Override 525 | public void onAnimationEnd(Animator animation) { 526 | ValueAnimator anime2 = ValueAnimator.ofFloat(mIndicatorY, target); 527 | anime2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 528 | @Override 529 | public void onAnimationUpdate(ValueAnimator animation) { 530 | float value = Float.parseFloat(animation.getAnimatedValue().toString()); 531 | mIndicatorY = value; 532 | invalidate(); 533 | } 534 | }); 535 | anime2.setDuration(100).start(); 536 | } 537 | }); 538 | 539 | } else if (direction < 0) { 540 | anime = ValueAnimator.ofFloat(mIndicatorY, target); 541 | anime.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 542 | @Override 543 | public void onAnimationUpdate(ValueAnimator animation) { 544 | float value = Float.parseFloat(animation.getAnimatedValue().toString()); 545 | mIndicatorY = value; 546 | invalidate(); 547 | } 548 | }); 549 | anime.addListener(new AnimatorListenerAdapter() { 550 | @Override 551 | public void onAnimationEnd(Animator animation) { 552 | ValueAnimator anime2 = ValueAnimator.ofFloat(mIndicatorBottomY, targetBottom); 553 | anime2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 554 | @Override 555 | public void onAnimationUpdate(ValueAnimator animation) { 556 | float value = Float.parseFloat(animation.getAnimatedValue().toString()); 557 | mIndicatorBottomY = value; 558 | invalidate(); 559 | } 560 | }); 561 | anime2.setDuration(100).start(); 562 | } 563 | }); 564 | } 565 | if (anime != null) { 566 | anime.setDuration(100).start(); 567 | } 568 | } 569 | }); 570 | } 571 | 572 | @Override 573 | protected void onDraw(Canvas canvas) { 574 | super.onDraw(canvas); 575 | mIndicatorPaint.setColor(mColorIndicator); 576 | RectF r = new RectF(mIndicatorX, mIndicatorY, 577 | mIndicatorX + mIndicatorWidth, mIndicatorBottomY); 578 | if (mIndicatorCorners != 0) { 579 | canvas.drawRoundRect(r, mIndicatorCorners, mIndicatorCorners, mIndicatorPaint); 580 | } else { 581 | canvas.drawRect(r, mIndicatorPaint); 582 | } 583 | } 584 | 585 | } 586 | 587 | protected int dp2px(float dp) { 588 | final float scale = mContext.getResources().getDisplayMetrics().density; 589 | return (int) (dp * scale + 0.5f); 590 | } 591 | 592 | private class OnTabPageChangeListener implements ViewPager.OnPageChangeListener { 593 | 594 | public OnTabPageChangeListener() { 595 | } 596 | 597 | @Override 598 | public void onPageScrollStateChanged(int state) { 599 | 600 | } 601 | 602 | @Override 603 | public void onPageScrolled(int position, float positionOffset, 604 | int positionOffsetPixels) { 605 | // mTabStrip.moveIndicator(positionOffset + position); 606 | } 607 | 608 | @Override 609 | public void onPageSelected(int position) { 610 | if (position != getSelectedTabPosition() && !getTabAt(position).isPressed()) { 611 | setTabSelected(position); 612 | } 613 | } 614 | } 615 | 616 | private class PagerAdapterObserver extends DataSetObserver { 617 | @Override 618 | public void onChanged() { 619 | populateFromPagerAdapter(); 620 | } 621 | 622 | @Override 623 | public void onInvalidated() { 624 | populateFromPagerAdapter(); 625 | } 626 | } 627 | 628 | public interface OnTabSelectedListener { 629 | 630 | void onTabSelected(TabView tab, int position); 631 | 632 | void onTabReselected(TabView tab, int position); 633 | } 634 | } 635 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 14 | 25 | 26 | 31 | 32 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/fragment_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 14 | 15 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soulrelay/VerticalViewPagerWithTabLayout/1188cfb010811a263aa16deedd7cc11bda1fe90a/sample/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soulrelay/VerticalViewPagerWithTabLayout/1188cfb010811a263aa16deedd7cc11bda1fe90a/sample/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soulrelay/VerticalViewPagerWithTabLayout/1188cfb010811a263aa16deedd7cc11bda1fe90a/sample/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soulrelay/VerticalViewPagerWithTabLayout/1188cfb010811a263aa16deedd7cc11bda1fe90a/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soulrelay/VerticalViewPagerWithTabLayout/1188cfb010811a263aa16deedd7cc11bda1fe90a/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/raw/screenshot1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soulrelay/VerticalViewPagerWithTabLayout/1188cfb010811a263aa16deedd7cc11bda1fe90a/sample/src/main/res/raw/screenshot1.gif -------------------------------------------------------------------------------- /sample/src/main/res/raw/screenshot2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soulrelay/VerticalViewPagerWithTabLayout/1188cfb010811a263aa16deedd7cc11bda1fe90a/sample/src/main/res/raw/screenshot2.gif -------------------------------------------------------------------------------- /sample/src/main/res/raw/screenshot3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soulrelay/VerticalViewPagerWithTabLayout/1188cfb010811a263aa16deedd7cc11bda1fe90a/sample/src/main/res/raw/screenshot3.gif -------------------------------------------------------------------------------- /sample/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /sample/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /sample/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | -------------------------------------------------------------------------------- /sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | VerticalViewPager 5 | 6 | 7 | -------------------------------------------------------------------------------- /sample/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':sample' 2 | include ':library' 3 | --------------------------------------------------------------------------------