├── .gitignore ├── .idea ├── codeStyles │ └── Project.xml ├── encodings.xml ├── markdown-navigator-enh.xml ├── markdown-navigator.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── litepager ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── wuyr │ │ └── litepager │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── wuyr │ │ │ └── litepager │ │ │ ├── LitePager.java │ │ │ └── ValueAnimatorUtil.java │ └── res │ │ └── values │ │ ├── attrs.xml │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── wuyr │ └── litepager │ └── ExampleUnitTest.java └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | # Android Studio Navigation editor temp files 30 | .navigation/ 31 | 32 | # Android Studio captures folder 33 | captures/ 34 | 35 | # IntelliJ 36 | *.iml 37 | .idea/workspace.xml 38 | .idea/tasks.xml 39 | .idea/gradle.xml 40 | .idea/assetWizardSettings.xml 41 | .idea/dictionaries 42 | .idea/libraries 43 | .idea/caches 44 | 45 | # Keystore files 46 | # Uncomment the following line if you do not want to check your keystore files in. 47 | #*.jks 48 | 49 | # External native build folder generated in Android Studio 2.2 and later 50 | .externalNativeBuild 51 | 52 | # Google Services (e.g. APIs or Firebase) 53 | google-services.json 54 | 55 | # Freeline 56 | freeline.py 57 | freeline/ 58 | freeline_project_description.json 59 | 60 | # fastlane 61 | fastlane/report.xml 62 | fastlane/Preview.html 63 | fastlane/screenshots 64 | fastlane/test_output 65 | fastlane/readme.md 66 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | xmlns:android 14 | 15 | ^$ 16 | 17 | 18 | 19 |
20 |
21 | 22 | 23 | 24 | xmlns:.* 25 | 26 | ^$ 27 | 28 | 29 | BY_NAME 30 | 31 |
32 |
33 | 34 | 35 | 36 | .*:id 37 | 38 | http://schemas.android.com/apk/res/android 39 | 40 | 41 | 42 |
43 |
44 | 45 | 46 | 47 | .*:name 48 | 49 | http://schemas.android.com/apk/res/android 50 | 51 | 52 | 53 |
54 |
55 | 56 | 57 | 58 | name 59 | 60 | ^$ 61 | 62 | 63 | 64 |
65 |
66 | 67 | 68 | 69 | style 70 | 71 | ^$ 72 | 73 | 74 | 75 |
76 |
77 | 78 | 79 | 80 | .* 81 | 82 | ^$ 83 | 84 | 85 | BY_NAME 86 | 87 |
88 |
89 | 90 | 91 | 92 | .* 93 | 94 | http://schemas.android.com/apk/res/android 95 | 96 | 97 | ANDROID_ATTRIBUTE_ORDER 98 | 99 |
100 |
101 | 102 | 103 | 104 | .* 105 | 106 | .* 107 | 108 | 109 | BY_NAME 110 | 111 |
112 |
113 |
114 |
115 |
116 |
-------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/markdown-navigator-enh.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.idea/markdown-navigator.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 31 | 32 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Android 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## LitePager,一个轻量级的ViewPager,仿新版网易云歌单广场 2 | ### 博客详情: 3 | 4 | ### 使用方式: 5 | #### 添加依赖: 6 | ``` 7 | implementation 'com.wuyr:litepager:1.3.1' 8 | ``` 9 | 10 | ### APIs: 11 | |Method|Description| 12 | |------|-----------| 13 | |addViews(int... layouts)|批量添加子View| 14 | |addViews(View... views)|批量添加子View| 15 | |setSelection(View target)|选中指定子View| 16 | |setSelection(int index)|根据索引选中子View| 17 | |setOrientation(int orientation)|设置滑动方向(默认: ORIENTATION_HORIZONTAL):
**ORIENTATION_HORIZONTAL**(水平)
**ORIENTATION_VERTICAL**(垂直)| 18 | |setFlingDuration(long duration)|设置动画的时长| 19 | |setTopScale(float scale)|设置**顶层**缩放比例| 20 | |setTopAlpha(float alpha)|设置**顶层**不透明度| 21 | |setMiddleScale(float scale)|设置**中层**缩放比例| 22 | |setMiddleAlpha(float alpha)|设置**中层**不透明度| 23 | |setBottomScale(float scale)|设置**底层**缩放比例| 24 | |setBottomAlpha(float alpha)|设置**底层**不透明度| 25 | |setOnScrollListener(OnScrollListener listener)|设置滚动状态监听:
**STATE_IDLE**(静止状态)
**STATE_DRAGGING_LEFT**(向左拖动)
**STATE_DRAGGING_RIGHT**(向右拖动)
**STATE_DRAGGING_TOP**(向上拖动)
**STATE_DRAGGING_BOTTOM**(向下拖动)
**STATE_SETTLING_LEFT**(向左调整)
**STATE_SETTLING_RIGHT**(向右调整)
**STATE_SETTLING_TOP**(向上调整)
**STATE_SETTLING_BOTTOM**(向下调整)
| 26 | |setOnItemSelectedListener(SelectedListener listener) |设置子View被选中的监听| 27 | |getSelectedChild() |获取当前选中的子View| 28 | |setAutoScrollEnable(boolean enable) |设置是否开启自动轮播 (默认: false)| 29 | |setAutoScrollInterval(long interval) |设置自动轮播的间隔 (默认: 5000 ms)| 30 | |setAutoScrollOrientation(int orientation) |设置自动轮播的方向(默认: SCROLL_ORIENTATION_LEFT):
**SCROLL_ORIENTATION_LEFT**(向左滚动)
**SCROLL_ORIENTATION_RIGHT**(向右滚动)
**SCROLL_ORIENTATION_UP**(向上滚动)
**SCROLL_ORIENTATION_DOWN**(向下滚动)
| 31 | |setAdapter(Adapter adapter)|使用Adapter来添加子View(见下)| 32 | 33 | ### Attributes: 34 | |Name|Format|Description| 35 | |----|-----|-----------| 36 | |orientation|enum (默认: horizontal)
**horizontal**(水平)
**vertical**(垂直)|滑动方向| 37 | |flingDuration|integer|动画时长| 38 | |topScale|float (默认: 1)|**顶层**缩放比例| 39 | |topAlpha|float (默认: 1)|**顶层**不透明度| 40 | |middleScale|float (默认: 0.8)|**中层**缩放比例| 41 | |middleAlpha|float (默认: 0.4)|**中层**不透明度| 42 | |bottomScale|float (默认: 0.6)|**底层**缩放比例| 43 | |bottomAlpha|float (默认: 0.2)|**底层**不透明度| 44 | |autoScroll|boolean (默认: false)|是否开启自动轮播| 45 | |autoScrollInterval|float (默认: 5000)|自动轮播的间隔| 46 | |autoScrollOrientation|enum (默认: left)
**left**(向左滚动)
**right**(向右滚动)
**up**(向上滚动)
**down**(向下滚动)|自动轮播的方向| 47 | 48 | ### 添加子View方式: 49 | #### 1. XML 50 | 51 | ```xml 52 | 55 | 56 | 60 | 61 | 65 | 66 | 70 | 71 | ``` 72 | 73 | #### 2. 批量添加 74 | 75 | ```java 76 | LitePager litePager = ...; 77 | View child1 = ...; 78 | View child2 = ...; 79 | View child3 = ...; 80 | 81 | litePager.addViews(child1, child2, child3); 82 | ``` 83 | 84 | #### 3. 通过布局添加 85 | ```java 86 | litePager.addViews( 87 | R.layout.view_child1 88 | R.layout.view_child2, 89 | R.layout.view_child3 90 | ); 91 | ``` 92 | 93 | #### 4. 设置适配器 94 | **示例:** 95 | Item布局: 96 | ```xml 97 | 98 | 102 | 103 | 109 | 110 | ``` 111 | Java代码: 112 | ```java 113 | litePager.setAdapter(new Adapter() { 114 | 115 | private List mData = new ArrayList<>(Arrays.asList("Item 1", "Item2", "Item3")); 116 | 117 | @Override 118 | protected ViewGroup onCreateView(@NonNull ViewGroup parent) { 119 | return (ViewGroup) LayoutInflater.from(parent.getContext()).inflate(R.layout.item_view, parent, false); 120 | } 121 | 122 | @Override 123 | protected void onBindView(@NonNull ViewGroup viewGroup, int position) { 124 | TextView textView = viewGroup.findViewById(R.id.text); 125 | textView.setText(mData.get(position)); 126 | } 127 | 128 | @Override 129 | protected int getItemCount() { 130 | return mData.size(); 131 | } 132 | }); 133 | ``` 134 | 135 |
136 | 137 | ### Demo下载: [app-debug.apk](https://github.com/wuyr/LitePager/raw/master/app-debug.apk) 138 | ### Demo源码地址: 139 | 140 | ### 效果 (图1为网易云原效果): 141 | ![preview](https://github.com/wuyr/LitePager/raw/master/previews/preview1.gif) ![preview](https://github.com/wuyr/LitePager/raw/master/previews/preview2.gif) 142 | ![preview](https://github.com/wuyr/LitePager/raw/master/previews/preview3.gif) ![preview](https://github.com/wuyr/LitePager/raw/master/previews/preview4.gif) 143 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | google() 6 | jcenter() 7 | 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.4.0' 11 | classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' 12 | classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3' 13 | 14 | // NOTE: Do not place your application dependencies here; they belong 15 | // in the individual module build.gradle files 16 | } 17 | } 18 | 19 | allprojects { 20 | repositories { 21 | google() 22 | jcenter() 23 | 24 | } 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | 15 | 16 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ifxcyr/LitePager/9d82b0d0359ee4e1e92959076e22d445c2cd57c8/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Apr 24 09:27:26 CST 2019 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-5.1.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 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 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /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 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 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 Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /litepager/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /litepager/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'com.jfrog.bintray' 3 | apply plugin: 'com.github.dcendents.android-maven' 4 | 5 | def siteUrl = 'https://github.com/wuyr/LitePager' //需要修改 6 | def gitUrl = 'https://github.com/Ifxcyr/LitePager.git' //需要修改 7 | 8 | version = "1.3.1" 9 | group = "com.wuyr" 10 | 11 | android { 12 | compileSdkVersion 29 13 | defaultConfig { 14 | minSdkVersion 14 15 | targetSdkVersion 29 16 | versionCode 4 17 | versionName "1.3.1" 18 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 19 | } 20 | buildTypes { 21 | release { 22 | minifyEnabled false 23 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 24 | } 25 | } 26 | } 27 | dependencies { 28 | implementation 'com.android.support:support-annotations:28.0.0' 29 | } 30 | 31 | Properties properties = new Properties() 32 | properties.load(project.rootProject.file('local.properties').newDataInputStream()) 33 | bintray { 34 | user = properties.getProperty("bintray.user") 35 | key = properties.getProperty("bintray.apikey") 36 | pkg { 37 | repo = 'LitePager' //需要修改 38 | name = 'LitePager' //需要修改 39 | websiteUrl = siteUrl 40 | vcsUrl = gitUrl 41 | licenses = ['Apache-2.0'] 42 | userOrg = 'wuyr' 43 | publish = true 44 | 45 | version { 46 | name = '1.3.1' 47 | desc = 'LitePager,一个轻量级的ViewPager,仿新版网易云歌单广场' //需要修改 48 | released = new Date() 49 | vcsTag = '1.3.1' 50 | attributes = ['gradle-plugin': 'com.use.less:com.use.less.gradle:gradle-useless-plugin'] 51 | } 52 | } 53 | configurations = ['archives'] 54 | } 55 | 56 | install { 57 | repositories.mavenInstaller { 58 | 59 | pom { 60 | project { 61 | packaging 'aar' 62 | 63 | name '陈小缘' 64 | description 'LitePager,一个轻量级的ViewPager,仿新版网易云歌单广场' //需要修改 65 | url siteUrl 66 | 67 | licenses { 68 | license { 69 | name 'Apache-2.0' 70 | url 'https://raw.githubusercontent.com/Ifxcyr/LitePager/master/LICENSE' //需要修改 71 | } 72 | } 73 | developers { 74 | developer { 75 | id 'ifxcyr' 76 | name '陈小缘' 77 | email 'ifxcyr@gmail.com' 78 | } 79 | } 80 | scm { 81 | connection gitUrl 82 | developerConnection gitUrl 83 | url siteUrl 84 | } 85 | } 86 | } 87 | } 88 | } 89 | task sourcesJar(type: Jar) { 90 | from android.sourceSets.main.java.srcDirs 91 | classifier = 'sources' 92 | } 93 | task javadoc(type: Javadoc) { 94 | failOnError false 95 | source = android.sourceSets.main.java.srcDirs 96 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 97 | } 98 | task javadocJar(type: Jar, dependsOn: javadoc) { 99 | classifier = 'javadoc' 100 | from javadoc.destinationDir 101 | } 102 | artifacts { 103 | archives javadocJar 104 | archives sourcesJar 105 | } 106 | javadoc { 107 | options { 108 | encoding "UTF-8" 109 | charSet 'UTF-8' 110 | author true 111 | version true 112 | links "http://docs.oracle.com/javase/8/docs/api" 113 | } 114 | } -------------------------------------------------------------------------------- /litepager/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /litepager/src/androidTest/java/com/wuyr/litepager/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.wuyr.litepager; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.wuyr.litepager.test", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /litepager/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /litepager/src/main/java/com/wuyr/litepager/LitePager.java: -------------------------------------------------------------------------------- 1 | package com.wuyr.litepager; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorListenerAdapter; 5 | import android.animation.ValueAnimator; 6 | import android.annotation.SuppressLint; 7 | import android.content.Context; 8 | import android.content.res.TypedArray; 9 | import android.graphics.Matrix; 10 | import android.support.annotation.CallSuper; 11 | import android.support.annotation.FloatRange; 12 | import android.support.annotation.IntDef; 13 | import android.support.annotation.IntRange; 14 | import android.support.annotation.LayoutRes; 15 | import android.support.annotation.NonNull; 16 | import android.support.annotation.Nullable; 17 | import android.util.AttributeSet; 18 | import android.view.LayoutInflater; 19 | import android.view.MotionEvent; 20 | import android.view.VelocityTracker; 21 | import android.view.View; 22 | import android.view.ViewConfiguration; 23 | import android.view.ViewGroup; 24 | import android.view.animation.DecelerateInterpolator; 25 | import android.view.animation.Interpolator; 26 | 27 | import java.lang.annotation.Retention; 28 | import java.lang.annotation.RetentionPolicy; 29 | import java.util.ArrayList; 30 | import java.util.List; 31 | 32 | /** 33 | * @author wuyr 34 | * @github https://github.com/wuyr/LitePager 35 | * @since 2019-04-07 下午3:29 36 | */ 37 | @SuppressWarnings("unused") 38 | public class LitePager extends ViewGroup implements Runnable { 39 | 40 | private static final float DEFAULT_TOP_SCALE = 1; 41 | private static final float DEFAULT_TOP_ALPHA = 1; 42 | 43 | private static final int DEFAULT_SCROLL_INTERVAL = 5000; 44 | private static final int DEFAULT_FLING_DURATION = 400; 45 | 46 | private static final float DEFAULT_MIDDLE_SCALE = .8F; 47 | private static final float DEFAULT_MIDDLE_ALPHA = .4F; 48 | 49 | private static final float DEFAULT_BOTTOM_SCALE = .6F; 50 | private static final float DEFAULT_BOTTOM_ALPHA = .2F; 51 | 52 | public static final int ORIENTATION_HORIZONTAL = 0;//水平方向 53 | public static final int ORIENTATION_VERTICAL = 1;//垂直方向 54 | 55 | @IntDef({ORIENTATION_HORIZONTAL, ORIENTATION_VERTICAL}) 56 | @Retention(RetentionPolicy.SOURCE) 57 | private @interface Orientation { 58 | } 59 | 60 | private static final int SCROLL_ORIENTATION_LEFT = 0; 61 | private static final int SCROLL_ORIENTATION_RIGHT = 1; 62 | private static final int SCROLL_ORIENTATION_UP = 0; 63 | private static final int SCROLL_ORIENTATION_DOWN = 1; 64 | 65 | @IntRange(from = 0, to = 1) 66 | @Retention(RetentionPolicy.SOURCE) 67 | private @interface ScrollOrientation { 68 | } 69 | 70 | private boolean mAutoScrollEnable; 71 | private int mAutoScrollOrientation; 72 | private long mAutoScrollInterval; 73 | 74 | public static final int STATE_IDLE = 0;//静止状态 75 | 76 | public static final int STATE_DRAGGING_LEFT = 1;//向左拖动 77 | public static final int STATE_DRAGGING_RIGHT = 2;//向右拖动 78 | public static final int STATE_DRAGGING_TOP = 3;//向上拖动 79 | public static final int STATE_DRAGGING_BOTTOM = 4;//向下拖动 80 | 81 | public static final int STATE_SETTLING_LEFT = 5;//向左调整 82 | public static final int STATE_SETTLING_RIGHT = 6;//向右调整 83 | public static final int STATE_SETTLING_TOP = 7;//向上调整 84 | public static final int STATE_SETTLING_BOTTOM = 8;//向下调整 85 | 86 | private int mCurrentState;//当前状态 87 | private int mOrientation;//当前方向 88 | private int mTouchSlop;//触发滑动的最小距离 89 | private boolean isBeingDragged;//是否已经开始了拖动 90 | private float mLastX, mLastY;//上一次的触摸坐标 91 | private float mDownX, mDownY;//按下时的触摸坐标 92 | private long mFlingDuration;//自动调整的动画时长 93 | private float mTopScale, mMiddleScale, mBottomScale;//缩放比例 94 | private float mTopAlpha, mMiddleAlpha, mBottomAlpha;//不透明度 95 | private float mOffsetX, mOffsetY;//水平和垂直偏移量 96 | private float mOffsetPercent;//偏移的百分比 97 | private boolean isReordered;//是否已经交换过层级顺序 98 | private boolean isAnotherActionDown;//是不是有另外的手指按下 99 | private VelocityTracker mVelocityTracker; 100 | private ValueAnimator mAnimator; 101 | private Adapter mAdapter; 102 | 103 | private OnScrollListener mOnScrollListener; 104 | private OnItemSelectedListener mOnItemSelectedListener; 105 | 106 | public LitePager(Context context) { 107 | this(context, null); 108 | } 109 | 110 | public LitePager(Context context, AttributeSet attrs) { 111 | this(context, attrs, 0); 112 | } 113 | 114 | public LitePager(Context context, AttributeSet attrs, int defStyleAttr) { 115 | super(context, attrs, defStyleAttr); 116 | initAttrs(context, attrs, defStyleAttr); 117 | mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 118 | mVelocityTracker = VelocityTracker.obtain(); 119 | } 120 | 121 | private void initAttrs(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 122 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LitePager, defStyleAttr, 0); 123 | mOrientation = a.getInteger(R.styleable.LitePager_orientation, ORIENTATION_HORIZONTAL); 124 | mFlingDuration = a.getInteger(R.styleable.LitePager_flingDuration, DEFAULT_FLING_DURATION); 125 | 126 | mTopScale = a.getFloat(R.styleable.LitePager_topScale, DEFAULT_TOP_SCALE); 127 | mTopAlpha = a.getFloat(R.styleable.LitePager_topAlpha, DEFAULT_TOP_ALPHA); 128 | 129 | mMiddleScale = a.getFloat(R.styleable.LitePager_middleScale, DEFAULT_MIDDLE_SCALE); 130 | mMiddleAlpha = a.getFloat(R.styleable.LitePager_middleAlpha, DEFAULT_MIDDLE_ALPHA); 131 | 132 | mBottomScale = a.getFloat(R.styleable.LitePager_bottomScale, DEFAULT_BOTTOM_SCALE); 133 | mBottomAlpha = a.getFloat(R.styleable.LitePager_bottomAlpha, DEFAULT_BOTTOM_ALPHA); 134 | 135 | mAutoScrollEnable = a.getBoolean(R.styleable.LitePager_autoScroll, false); 136 | mAutoScrollOrientation = a.getInteger(R.styleable.LitePager_autoScrollOrientation, SCROLL_ORIENTATION_LEFT); 137 | mAutoScrollInterval = a.getInteger(R.styleable.LitePager_autoScrollInterval, DEFAULT_SCROLL_INTERVAL); 138 | 139 | a.recycle(); 140 | fixOverflow(); 141 | 142 | 143 | } 144 | 145 | /** 146 | * 调整这些最大值和最小值,使他们在0~1范围内 147 | */ 148 | private void fixOverflow() { 149 | mMiddleScale = fixOverflow(mMiddleScale); 150 | mMiddleAlpha = fixOverflow(mMiddleAlpha); 151 | mTopScale = fixOverflow(mTopScale); 152 | mTopAlpha = fixOverflow(mTopAlpha); 153 | mBottomScale = fixOverflow(mBottomScale); 154 | mBottomAlpha = fixOverflow(mBottomAlpha); 155 | } 156 | 157 | private float fixOverflow(float value) { 158 | return value > 1 ? 1 : value < 0 ? 0 : value; 159 | } 160 | 161 | /** 162 | * 批量添加子View 163 | * 164 | * @param layouts 子View布局 165 | */ 166 | public LitePager addViews(@NonNull @LayoutRes int... layouts) { 167 | LayoutInflater inflater = LayoutInflater.from(getContext()); 168 | for (int layout : layouts) { 169 | inflater.inflate(layout, this); 170 | } 171 | return this; 172 | } 173 | 174 | /** 175 | * 批量添加子View 176 | * 177 | * @param views 目标子View 178 | */ 179 | public LitePager addViews(@NonNull View... views) { 180 | for (View view : views) { 181 | ViewGroup.LayoutParams lp = view.getLayoutParams(); 182 | if (lp != null) { 183 | if (!(lp instanceof LayoutParams)) { 184 | view.setLayoutParams(new LayoutParams(lp)); 185 | } 186 | } 187 | addView(view); 188 | } 189 | return this; 190 | } 191 | 192 | /** 193 | * 选中子View 194 | * 195 | * @param target 目标子View 196 | */ 197 | public void setSelection(View target) { 198 | setSelection(indexOfChild(target)); 199 | } 200 | 201 | private boolean isNeedPlayTwice; 202 | private int mSelectedIndex; 203 | 204 | /** 205 | * 根据索引选中子View 206 | */ 207 | public void setSelection(int index) { 208 | if (indexOfChild(getChildAt(getChildCount() - 1)) == index || 209 | getChildCount() == 0 || (mAnimator != null && mAnimator.isRunning() && !isNeedPlayTwice)) { 210 | return; 211 | } 212 | final float start, end; 213 | start = isHorizontal() ? mOffsetX : mOffsetY; 214 | if (is5Child()) { 215 | switch (index) { 216 | case 0: 217 | isNeedPlayTwice = index != mSelectedIndex; 218 | case 2: 219 | end = isHorizontal() ? getWidth() : getHeight(); 220 | break; 221 | case 1: 222 | //noinspection DuplicateBranchesInSwitch 223 | isNeedPlayTwice = index != mSelectedIndex; 224 | case 3: 225 | end = isHorizontal() ? -getWidth() : -getHeight(); 226 | break; 227 | default: 228 | return; 229 | } 230 | } else { 231 | if (index == 0) { 232 | end = isHorizontal() ? getWidth() : getHeight(); 233 | } else if (index == 1) { 234 | end = isHorizontal() ? -getWidth() : -getHeight(); 235 | } else { 236 | return; 237 | } 238 | } 239 | mSelectedIndex = index; 240 | startValueAnimator(start, end); 241 | } 242 | 243 | /** 244 | * 播放调整动画 245 | */ 246 | private void playFixingAnimation() { 247 | int childCount = getChildCount(); 248 | if (childCount == 0) { 249 | return; 250 | } 251 | float start, end; 252 | mVelocityTracker.computeCurrentVelocity(1000); 253 | float velocityX = mVelocityTracker.getXVelocity(); 254 | float velocityY = mVelocityTracker.getYVelocity(); 255 | mVelocityTracker.clear(); 256 | if (isHorizontal()) { 257 | start = mOffsetX; 258 | //优先根据滑动速率来判断,处理在Fixing的时候手指往相反方向快速滑动 259 | if (Math.abs(velocityX) > Math.abs(velocityY) && Math.abs(velocityX) > 1000) { 260 | end = velocityX < 0 ? -getWidth() : getWidth(); 261 | } else if (Math.abs(mOffsetPercent) > .5F) { 262 | end = mOffsetPercent < 0 ? -getWidth() : getWidth(); 263 | } else { 264 | end = 0; 265 | } 266 | } else { 267 | start = mOffsetY; 268 | //优先根据滑动速率来判断,处理在Fixing的时候手指往相反方向快速滑动 269 | if (Math.abs(velocityY) > Math.abs(velocityX) && Math.abs(velocityY) > 1000) { 270 | end = velocityY < 0 ? -getHeight() : getHeight(); 271 | } else if (Math.abs(mOffsetPercent) > .5F) { 272 | end = mOffsetPercent < 0 ? -getHeight() : getHeight(); 273 | } else { 274 | end = 0; 275 | } 276 | } 277 | startValueAnimator(start, end); 278 | } 279 | 280 | /** 281 | * 开始播放动画 282 | * 283 | * @param start 初始坐标 284 | * @param end 结束坐标 285 | */ 286 | private void startValueAnimator(float start, float end) { 287 | if (start == end) { 288 | return; 289 | } 290 | abortAnimation(); 291 | mAnimator = ValueAnimator.ofFloat(start, end).setDuration(mFlingDuration); 292 | mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 293 | @Override 294 | public void onAnimationUpdate(ValueAnimator animation) { 295 | float currentValue = (float) animation.getAnimatedValue(); 296 | if (isHorizontal()) { 297 | mOffsetX = currentValue; 298 | } else { 299 | mOffsetY = currentValue; 300 | } 301 | onItemMove(); 302 | } 303 | }); 304 | mAnimator.setInterpolator(mInterpolator); 305 | mAnimator.addListener(mAnimatorListener); 306 | ValueAnimatorUtil.resetDurationScale(); 307 | mAnimator.start(); 308 | } 309 | 310 | /** 311 | * 调整动画的插值器,现在是减速 312 | */ 313 | private Interpolator mInterpolator = new DecelerateInterpolator(); 314 | 315 | private Animator.AnimatorListener mAnimatorListener = new AnimatorListenerAdapter() { 316 | 317 | private boolean isCanceled; 318 | 319 | @Override 320 | public void onAnimationCancel(Animator animation) { 321 | isCanceled = true; 322 | isNeedPlayTwice = false; 323 | } 324 | 325 | @Override 326 | public void onAnimationStart(Animator animation) { 327 | isCanceled = false; 328 | } 329 | 330 | @Override 331 | public void onAnimationEnd(Animator animation) { 332 | if (!isCanceled) { 333 | if (isNeedPlayTwice) { 334 | setSelection(mSelectedIndex); 335 | } else { 336 | mSelectedIndex = -1; 337 | mCurrentState = STATE_IDLE; 338 | isAnotherActionDown = false; 339 | if (mOnScrollListener != null) { 340 | mOnScrollListener.onStateChanged(mCurrentState); 341 | } 342 | if (mOnItemSelectedListener != null) { 343 | mOnItemSelectedListener.onItemSelected(getSelectedChild()); 344 | } 345 | } 346 | if (mPostOnAnimationEnd) { 347 | mPostOnAnimationEnd = false; 348 | if (mTempAdapter != null) { 349 | updateAdapterDataNow(mTempAdapter); 350 | mTempAdapter = null; 351 | } 352 | } 353 | } 354 | } 355 | }; 356 | 357 | /** 358 | * 打断调整动画 359 | */ 360 | private void abortAnimation() { 361 | if (mAnimator != null && mAnimator.isRunning()) { 362 | mAnimator.cancel(); 363 | } 364 | } 365 | 366 | @SuppressLint("ClickableViewAccessibility") 367 | @Override 368 | public boolean onTouchEvent(MotionEvent event) { 369 | float x = event.getX(), y = event.getY(); 370 | mVelocityTracker.addMovement(event); 371 | switch (event.getAction() & event.getActionMasked()) { 372 | case MotionEvent.ACTION_POINTER_DOWN: 373 | isAnotherActionDown = true; 374 | playFixingAnimation(); 375 | return false; 376 | case MotionEvent.ACTION_DOWN: 377 | if (isSettling()) { 378 | return false; 379 | } 380 | //在空白的地方按下,会拦截,但还没标记已经开始了 381 | isBeingDragged = true; 382 | case MotionEvent.ACTION_MOVE: 383 | if (isAnotherActionDown) { 384 | return false; 385 | } 386 | abortAnimation(); 387 | float offsetX = x - mLastX; 388 | float offsetY = y - mLastY; 389 | mOffsetX += offsetX; 390 | mOffsetY += offsetY; 391 | onItemMove(); 392 | break; 393 | case MotionEvent.ACTION_UP: 394 | case MotionEvent.ACTION_CANCEL: 395 | case MotionEvent.ACTION_OUTSIDE: 396 | //因为isSettling方法不能收到isBeingDragged=false 397 | if (isSettling()) { 398 | resetDragFlag(); 399 | break; 400 | } 401 | resetDragFlag(); 402 | handleActionUp(x, y); 403 | break; 404 | default: 405 | break; 406 | } 407 | mLastX = x; 408 | mLastY = y; 409 | return true; 410 | } 411 | 412 | /** 413 | * 判断当前状态是否正在调整位置中 414 | */ 415 | private boolean isSettling() { 416 | return (mCurrentState == STATE_SETTLING_LEFT 417 | || mCurrentState == STATE_SETTLING_RIGHT 418 | || mCurrentState == STATE_SETTLING_TOP 419 | || mCurrentState == STATE_SETTLING_BOTTOM) 420 | && !isBeingDragged; 421 | } 422 | 423 | /** 424 | * 更新子View信息(位置,尺寸,透明度) 425 | */ 426 | private void onItemMove() { 427 | updateOffsetPercent(); 428 | updateFromAndTo(); 429 | updateChildOrder(); 430 | requestLayout(); 431 | } 432 | 433 | /** 434 | * 更新子View的层级顺序 435 | */ 436 | private void updateChildOrder() { 437 | if (Math.abs(mOffsetPercent) > .5F) { 438 | if (!isReordered) { 439 | if (is5Child()) { 440 | if (mOffsetPercent > 0) { 441 | reOrder(0, 3, 1, 4, 2); 442 | } else { 443 | reOrder(2, 0, 4, 1, 3); 444 | } 445 | } else { 446 | exchangeOrder(1, 2); 447 | } 448 | isReordered = true; 449 | } 450 | } else { 451 | if (isReordered) { 452 | if (is5Child()) { 453 | if (mOffsetPercent > 0) { 454 | reOrder(2, 0, 4, 1, 3); 455 | } else { 456 | reOrder(0, 3, 1, 4, 2); 457 | } 458 | } else { 459 | exchangeOrder(1, 2); 460 | } 461 | isReordered = false; 462 | } 463 | } 464 | } 465 | 466 | private List mTempViewList = new ArrayList<>(5); 467 | 468 | private void reOrder(int... indexes) { 469 | mTempViewList.clear(); 470 | for (int index : indexes) { 471 | if (index >= getChildCount()) { 472 | break; 473 | } 474 | mTempViewList.add(getChildAt(index)); 475 | } 476 | detachAllViewsFromParent(); 477 | for (int i = 0; i < mTempViewList.size(); i++) { 478 | View tmp = mTempViewList.get(i); 479 | attachViewToParent(tmp, i, tmp.getLayoutParams()); 480 | } 481 | mTempViewList.clear(); 482 | invalidate(); 483 | } 484 | 485 | 486 | /** 487 | * 更新子View的起始索引和目标索引 488 | */ 489 | private void updateFromAndTo() { 490 | if (Math.abs(mOffsetPercent) >= 1) { 491 | for (int i = 0; i < getChildCount(); i++) { 492 | View child = getChildAt(i); 493 | LayoutParams lp = (LayoutParams) child.getLayoutParams(); 494 | lp.from = lp.to; 495 | } 496 | isReordered = false; 497 | mOffsetPercent %= 1; 498 | mOffsetX %= getWidth(); 499 | mOffsetY %= getHeight(); 500 | } 501 | for (int i = 0; i < getChildCount(); i++) { 502 | View child = getChildAt(i); 503 | LayoutParams lp = (LayoutParams) child.getLayoutParams(); 504 | if (is5Child()) { 505 | switch (lp.from) { 506 | case 0: 507 | lp.to = mOffsetPercent > 0 ? 2 : 1; 508 | break; 509 | case 1: 510 | lp.to = mOffsetPercent > 0 ? 0 : 3; 511 | break; 512 | case 2: 513 | lp.to = mOffsetPercent > 0 ? 4 : 0; 514 | break; 515 | case 3: 516 | lp.to = mOffsetPercent > 0 ? 1 : 4; 517 | break; 518 | case 4: 519 | lp.to = mOffsetPercent > 0 ? 3 : 2; 520 | break; 521 | default: 522 | break; 523 | } 524 | } else { 525 | switch (lp.from) { 526 | case 0: 527 | lp.to = mOffsetPercent > 0 ? 2 : 1; 528 | break; 529 | case 1: 530 | lp.to = mOffsetPercent > 0 ? 0 : 2; 531 | break; 532 | case 2: 533 | lp.to = mOffsetPercent > 0 ? 1 : 0; 534 | break; 535 | default: 536 | break; 537 | } 538 | } 539 | } 540 | } 541 | 542 | /** 543 | * 更新偏移百分比和当前状态 544 | */ 545 | private void updateOffsetPercent() { 546 | float oldState = mCurrentState; 547 | float oldOffsetPercent = mOffsetPercent; 548 | mOffsetPercent = isHorizontal() ? mOffsetX / getWidth() : mOffsetY / getHeight(); 549 | if (isScrollFinished()) { 550 | mCurrentState = STATE_IDLE; 551 | } else if (mOffsetPercent > oldOffsetPercent) { 552 | if (isHorizontal()) { 553 | mCurrentState = isBeingDragged ? STATE_DRAGGING_RIGHT : STATE_SETTLING_RIGHT; 554 | } else { 555 | mCurrentState = isBeingDragged ? STATE_DRAGGING_BOTTOM : STATE_SETTLING_BOTTOM; 556 | } 557 | } else if (mOffsetPercent < oldOffsetPercent) { 558 | if (isHorizontal()) { 559 | mCurrentState = isBeingDragged ? STATE_DRAGGING_LEFT : STATE_SETTLING_LEFT; 560 | } else { 561 | mCurrentState = isBeingDragged ? STATE_DRAGGING_TOP : STATE_SETTLING_TOP; 562 | } 563 | } 564 | if (mCurrentState != oldState) { 565 | if (mOnScrollListener != null) { 566 | mOnScrollListener.onStateChanged(mCurrentState); 567 | } 568 | } 569 | } 570 | 571 | /** 572 | * @param view 目标view 573 | * @param points 坐标点(x, y) 574 | * @return 坐标点是否在view范围内 575 | */ 576 | private boolean pointInView(View view, float[] points) { 577 | // 像ViewGroup那样,先对齐一下Left和Top 578 | points[0] -= view.getLeft(); 579 | points[1] -= view.getTop(); 580 | // 获取View所对应的矩阵 581 | Matrix matrix = view.getMatrix(); 582 | // 如果矩阵有应用过变换 583 | if (!matrix.isIdentity()) { 584 | // 反转矩阵 585 | matrix.invert(matrix); 586 | // 映射坐标点 587 | matrix.mapPoints(points); 588 | } 589 | //判断坐标点是否在view范围内 590 | return points[0] >= 0 && points[1] >= 0 && points[0] < view.getWidth() && points[1] < view.getHeight(); 591 | } 592 | 593 | private float mInterceptLastX, mInterceptLastY; 594 | 595 | @Override 596 | public boolean dispatchTouchEvent(MotionEvent event) { 597 | float x = event.getX(), y = event.getY(); 598 | switch (event.getAction()) { 599 | case MotionEvent.ACTION_DOWN: 600 | mInterceptLastX = x; 601 | mInterceptLastY = y; 602 | getParent().requestDisallowInterceptTouchEvent(true); 603 | break; 604 | case MotionEvent.ACTION_MOVE: 605 | float offsetX = Math.abs(x - mInterceptLastX); 606 | float offsetY = Math.abs(y - mInterceptLastY); 607 | if (isHorizontal() ? offsetY > offsetX && offsetY > mTouchSlop : offsetX >= offsetY && offsetX > mTouchSlop) { 608 | getParent().requestDisallowInterceptTouchEvent(false); 609 | } 610 | break; 611 | case MotionEvent.ACTION_CANCEL: 612 | case MotionEvent.ACTION_OUTSIDE: 613 | case MotionEvent.ACTION_UP: 614 | getParent().requestDisallowInterceptTouchEvent(false); 615 | break; 616 | } 617 | return super.dispatchTouchEvent(event); 618 | } 619 | 620 | @Override 621 | public boolean onInterceptTouchEvent(MotionEvent event) { 622 | if (!isEnabled()) { 623 | return false; 624 | } 625 | if ((event.getAction() == MotionEvent.ACTION_MOVE && isBeingDragged) || super.onInterceptTouchEvent(event)) { 626 | return true; 627 | } 628 | float x = event.getX(), y = event.getY(); 629 | switch (event.getAction() & event.getActionMasked()) { 630 | case MotionEvent.ACTION_POINTER_DOWN: 631 | isAnotherActionDown = true; 632 | playFixingAnimation(); 633 | return false; 634 | case MotionEvent.ACTION_DOWN: 635 | mLastX = mDownX = x; 636 | mLastY = mDownY = y; 637 | if (isSettling()) { 638 | return false; 639 | } 640 | abortAnimation(); 641 | break; 642 | case MotionEvent.ACTION_MOVE: 643 | if (isAnotherActionDown) { 644 | return false; 645 | } 646 | float offsetX = Math.abs(x - mLastX); 647 | float offsetY = Math.abs(y - mLastY); 648 | //判断是否触发拖动事件 649 | if (isHorizontal() ? offsetX >= offsetY && offsetX > mTouchSlop : offsetY > offsetX && offsetY > mTouchSlop) { 650 | mLastX = x; 651 | mLastY = y; 652 | isBeingDragged = true; 653 | } 654 | break; 655 | case MotionEvent.ACTION_UP: 656 | case MotionEvent.ACTION_CANCEL: 657 | case MotionEvent.ACTION_OUTSIDE: 658 | //因为isSettling方法不能收到isBeingDragged=false 659 | if (isSettling()) { 660 | resetDragFlag(); 661 | break; 662 | } 663 | resetDragFlag(); 664 | return handleActionUp(x, y); 665 | } 666 | return isBeingDragged; 667 | } 668 | 669 | private void resetDragFlag() { 670 | isBeingDragged = false; 671 | isAnotherActionDown = false; 672 | } 673 | 674 | /** 675 | * 根据输入的坐标来判断是否在某个子View内 676 | * 677 | * @param x x轴坐标 678 | * @param y y轴坐标 679 | * @return 如果有,则返回这个子View,否则空 680 | */ 681 | private View findHitView(float x, float y) { 682 | for (int index = getChildCount() - 1; index >= 0; index--) { 683 | View child = getChildAt(index); 684 | if (pointInView(child, new float[]{x, y})) { 685 | return child; 686 | } 687 | } 688 | return null; 689 | } 690 | 691 | /** 692 | * 处理手指松开的事件 693 | */ 694 | private boolean handleActionUp(float x, float y) { 695 | float offsetX = x - mDownX; 696 | float offsetY = y - mDownY; 697 | //判断是否点击手势 698 | if (Math.abs(offsetX) < mTouchSlop && Math.abs(offsetY) < mTouchSlop) { 699 | //查找被点击的子View 700 | View hitView = findHitView(x, y); 701 | if (hitView != null) { 702 | if (indexOfChild(hitView) == (is5Child() ? 4 : 2)) { 703 | //点击第一个子view不用播放动画,直接不拦截 704 | return false; 705 | } else { 706 | LayoutParams lp = (LayoutParams) hitView.getLayoutParams(); 707 | setSelection(lp.from); 708 | //拦截ACTION_UP事件,内部消费 709 | return true; 710 | } 711 | } 712 | } 713 | //手指在空白地方松开 714 | playFixingAnimation(); 715 | return false; 716 | } 717 | 718 | /** 719 | * 判断是否滚动完成 720 | */ 721 | private boolean isScrollFinished() { 722 | return mOffsetPercent % 1 == 0; 723 | } 724 | 725 | @Override 726 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 727 | measureChildren(widthMeasureSpec, heightMeasureSpec); 728 | 729 | int widthSize = MeasureSpec.getSize(widthMeasureSpec); 730 | int widthMode = MeasureSpec.getMode(widthMeasureSpec); 731 | int heightSize = MeasureSpec.getSize(heightMeasureSpec); 732 | int heightMode = MeasureSpec.getMode(heightMeasureSpec); 733 | 734 | int childCount = getChildCount(); 735 | int width, height; 736 | LayoutParams layoutParams; 737 | 738 | if (widthMode == MeasureSpec.EXACTLY) { 739 | width = widthSize; 740 | } else { 741 | //如果是垂直方向的话,刚好相反:宽度取最大的子View宽 742 | int maxChildWidth = 0; 743 | for (int i = 0; i < childCount; i++) { 744 | View child = getChildAt(i); 745 | layoutParams = (LayoutParams) child.getLayoutParams(); 746 | maxChildWidth = Math.max(maxChildWidth, child.getMeasuredWidth() 747 | + layoutParams.leftMargin + layoutParams.rightMargin); 748 | } 749 | width = maxChildWidth; 750 | if (isHorizontal()) { 751 | width *= 2.5; 752 | } 753 | } 754 | if (heightMode == MeasureSpec.EXACTLY) { 755 | height = heightSize; 756 | } else { 757 | //如果高度设置了wrap_content,则取最大的子View高 758 | int maxChildHeight = 0; 759 | for (int i = 0; i < childCount; i++) { 760 | View child = getChildAt(i); 761 | layoutParams = (LayoutParams) child.getLayoutParams(); 762 | maxChildHeight = Math.max(maxChildHeight, child.getMeasuredHeight() 763 | + layoutParams.topMargin + layoutParams.bottomMargin); 764 | } 765 | height = maxChildHeight; 766 | if (!isHorizontal()) { 767 | height *= 2.5; 768 | } 769 | } 770 | 771 | setMeasuredDimension(width, height); 772 | } 773 | 774 | @Override 775 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 776 | for (int i = 0; i < getChildCount(); i++) { 777 | View child = getChildAt(i); 778 | int baseLine = getBaselineByChild(child); 779 | updateChildParamsAndLayout(child, baseLine); 780 | } 781 | } 782 | 783 | /** 784 | * 根据当前滑动距离计算出目标子View的基准线 785 | */ 786 | private int getBaselineByChild(View child) { 787 | int baseLine; 788 | if (is5Child()) { 789 | baseLine = isHorizontal() ? getHorizontalBaseLineBy5Child(child) : getVerticalBaseLineBy5Child(child); 790 | updateAlphaAndScaleBy5Child(child); 791 | } else { 792 | baseLine = isHorizontal() ? getHorizontalBaseLine(child) : getVerticalBaseLine(child); 793 | updateAlphaAndScale(child); 794 | } 795 | return baseLine; 796 | } 797 | 798 | private int getHorizontalBaseLine(View child) { 799 | int width = getWidth(); 800 | int baseLineLeft = width / 4; 801 | int baseLineCenterX = width / 2; 802 | int baseLineRight = width - baseLineLeft; 803 | return getBaseLine(child, baseLineLeft, baseLineCenterX, baseLineRight); 804 | } 805 | 806 | private int getVerticalBaseLine(View child) { 807 | int height = getHeight(); 808 | int baseLineTop = height / 4; 809 | int baseLineCenterY = height / 2; 810 | int baseLineBottom = height - baseLineTop; 811 | return getBaseLine(child, baseLineTop, baseLineCenterY, baseLineBottom); 812 | } 813 | 814 | /** 815 | * 根据当前滑动距离计算出目标子View的基准线 816 | * 817 | * @param child 目标子View 818 | * @param start 四等份中的第一条线 819 | * @param middle 四等份中的第二条线 820 | * @param end 四等份中的第三条线 821 | * @return 当前的基准线 822 | */ 823 | private int getBaseLine(View child, int start, int middle, int end) { 824 | int baseLine = 0; 825 | 826 | LayoutParams lp = (LayoutParams) child.getLayoutParams(); 827 | switch (lp.from) { 828 | case 0: 829 | switch (lp.to) { 830 | case 1: 831 | baseLine = start + (int) ((end - start) * -mOffsetPercent); 832 | break; 833 | case 2: 834 | baseLine = start + (int) ((middle - start) * mOffsetPercent); 835 | break; 836 | default: 837 | baseLine = start; 838 | break; 839 | } 840 | break; 841 | case 1: 842 | switch (lp.to) { 843 | case 0: 844 | baseLine = end + (int) ((end - start) * -mOffsetPercent/*因为是反方向*/); 845 | break; 846 | case 2: 847 | baseLine = end + (int) ((end - middle) * mOffsetPercent); 848 | break; 849 | default: 850 | baseLine = end; 851 | break; 852 | } 853 | break; 854 | case 2: 855 | switch (lp.to) { 856 | case 0: 857 | baseLine = middle + (int) ((middle - start) * mOffsetPercent); 858 | break; 859 | case 1: 860 | baseLine = middle + (int) ((end - middle) * mOffsetPercent); 861 | break; 862 | default: 863 | baseLine = middle; 864 | break; 865 | } 866 | break; 867 | default: 868 | break; 869 | } 870 | return baseLine; 871 | } 872 | 873 | /** 874 | * 跟据子View的起始索引和目标索引来更新不透明度和缩放比例 875 | * 876 | * @param child 目标子View 877 | */ 878 | private void updateAlphaAndScale(View child) { 879 | LayoutParams lp = (LayoutParams) child.getLayoutParams(); 880 | switch (lp.from) { 881 | case 0: 882 | switch (lp.to) { 883 | case 0: 884 | case 1: 885 | setAsBottom(child); 886 | lp.alpha = mMiddleAlpha; 887 | lp.scale = mMiddleScale; 888 | break; 889 | case 2: 890 | float alphaProgress; 891 | if (mOffsetPercent > .5F) { 892 | alphaProgress = (mOffsetPercent - .5F) * 2; 893 | } else { 894 | alphaProgress = 0; 895 | } 896 | lp.alpha = mMiddleAlpha + (mTopAlpha - mMiddleAlpha) * alphaProgress; 897 | lp.scale = mMiddleScale + (mTopScale - mMiddleScale) * mOffsetPercent; 898 | break; 899 | } 900 | break; 901 | case 1: 902 | switch (lp.to) { 903 | case 0: 904 | case 1: 905 | setAsBottom(child); 906 | lp.alpha = mMiddleAlpha; 907 | lp.scale = mMiddleScale; 908 | break; 909 | case 2: 910 | float alphaProgress; 911 | //要在后半段才开始改变透明度 912 | if (/*因为是向左边移动,此时Progress是负数*/-mOffsetPercent > .5F) { 913 | alphaProgress = (-mOffsetPercent - .5F) * 2; 914 | } else { 915 | alphaProgress = 0; 916 | } 917 | lp.alpha = mMiddleAlpha + (mTopAlpha - mMiddleAlpha) * alphaProgress; 918 | lp.scale = mMiddleScale + (mTopScale - mMiddleScale) * -mOffsetPercent;//因为mOffsetProgress此时是负数 919 | break; 920 | } 921 | break; 922 | case 2: 923 | float alphaProgress; 924 | float absOffsetPercent = Math.abs(mOffsetPercent); 925 | //因为现在是在中间,所以要在前半段就改变透明度 926 | if (absOffsetPercent < .5F) { 927 | alphaProgress = absOffsetPercent * 2; 928 | } else { 929 | //后半段已经不需要了 930 | alphaProgress = 1F; 931 | } 932 | lp.alpha = mTopAlpha - (mTopAlpha - mMiddleAlpha) * alphaProgress; 933 | lp.scale = mTopScale - (mTopScale - mMiddleScale) * Math.abs(mOffsetPercent); 934 | break; 935 | } 936 | } 937 | 938 | /** 939 | * 把目标子View放置到视图层级最底部 940 | */ 941 | private void setAsBottom(View child) { 942 | exchangeOrder(indexOfChild(child), 0); 943 | } 944 | 945 | private int getHorizontalBaseLineBy5Child(View child) { 946 | return getBaseLineBy5Child(child, getWidth() / 6); 947 | } 948 | 949 | private int getVerticalBaseLineBy5Child(View child) { 950 | return getBaseLineBy5Child(child, getHeight() / 6); 951 | } 952 | 953 | private int getBaseLineBy5Child(View child, int itemDistance) { 954 | int baseLine = 0; 955 | int child0Line, child1Line, child2Line, child3Line, child4Line; 956 | child0Line = itemDistance; 957 | child2Line = itemDistance * 2; 958 | child4Line = itemDistance * 3; 959 | child3Line = itemDistance * 4; 960 | child1Line = itemDistance * 5; 961 | LayoutParams lp = (LayoutParams) child.getLayoutParams(); 962 | int offset = (int) (itemDistance * mOffsetPercent); 963 | switch (lp.from) { 964 | case 0: 965 | switch (lp.to) { 966 | case 1: 967 | baseLine = child0Line - offset * 4; 968 | break; 969 | case 2: 970 | baseLine = child0Line + offset; 971 | break; 972 | default: 973 | baseLine = child0Line; 974 | break; 975 | } 976 | break; 977 | case 1: 978 | switch (lp.to) { 979 | case 0: 980 | baseLine = child1Line - offset * 4; 981 | break; 982 | case 3: 983 | baseLine = child1Line + offset; 984 | break; 985 | default: 986 | baseLine = child1Line; 987 | break; 988 | } 989 | break; 990 | case 2: 991 | switch (lp.to) { 992 | case 0: 993 | case 4: 994 | baseLine = child2Line + offset; 995 | break; 996 | default: 997 | baseLine = child2Line; 998 | break; 999 | } 1000 | break; 1001 | case 3: 1002 | switch (lp.to) { 1003 | case 1: 1004 | case 4: 1005 | baseLine = child3Line + offset; 1006 | break; 1007 | default: 1008 | baseLine = child3Line; 1009 | break; 1010 | } 1011 | break; 1012 | case 4: 1013 | switch (lp.to) { 1014 | case 2: 1015 | case 3: 1016 | baseLine = child4Line + offset; 1017 | break; 1018 | default: 1019 | baseLine = child4Line; 1020 | break; 1021 | } 1022 | break; 1023 | default: 1024 | break; 1025 | } 1026 | return baseLine; 1027 | } 1028 | 1029 | /** 1030 | * 跟据子View的起始索引和目标索引来更新不透明度和缩放比例 1031 | * 1032 | * @param child 目标子View 1033 | */ 1034 | private void updateAlphaAndScaleBy5Child(View child) { 1035 | LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1036 | switch (lp.from) { 1037 | case 0: 1038 | updateAlphaAndScaleBy5Child(child, lp, mOffsetPercent); 1039 | break; 1040 | case 1: 1041 | updateAlphaAndScaleBy5Child(child, lp, -mOffsetPercent); 1042 | break; 1043 | case 2: 1044 | updateAlphaAndScale2(lp, mOffsetPercent); 1045 | break; 1046 | case 3: 1047 | //刚好跟上面相反,因为当上面变得更不透明时,它会变得更透明 1048 | updateAlphaAndScale2(lp, -mOffsetPercent); 1049 | break; 1050 | case 4: 1051 | float absOffsetPercent = Math.abs(mOffsetPercent); 1052 | float alphaProgress; 1053 | //因为现在是在中间,所以要在前半段就改变透明度 1054 | if (absOffsetPercent < .5F) { 1055 | alphaProgress = absOffsetPercent * 2; 1056 | } else { 1057 | //后半段已经不需要了 1058 | alphaProgress = 1F; 1059 | } 1060 | lp.alpha = mTopAlpha - (mTopAlpha - mMiddleAlpha) * alphaProgress; 1061 | lp.scale = mTopScale - (mTopScale - mMiddleScale) * Math.abs(mOffsetPercent); 1062 | break; 1063 | } 1064 | } 1065 | 1066 | private void updateAlphaAndScale2(LayoutParams lp, float offsetPercent) { 1067 | float alphaProgress; 1068 | if (Math.abs(offsetPercent) > .5F) { 1069 | alphaProgress = (Math.abs(offsetPercent) - .5F) * 2; 1070 | } else { 1071 | alphaProgress = 0; 1072 | } 1073 | switch (lp.to) { 1074 | case 0: 1075 | case 1: 1076 | lp.alpha = mMiddleAlpha + (mMiddleAlpha - mBottomAlpha) * -alphaProgress; 1077 | lp.scale = mMiddleScale + (mMiddleScale - mBottomScale) * offsetPercent; 1078 | break; 1079 | case 4: 1080 | lp.alpha = mMiddleAlpha + (mTopAlpha - mMiddleAlpha) * alphaProgress; 1081 | lp.scale = mMiddleScale + (mTopScale - mMiddleScale) * offsetPercent; 1082 | break; 1083 | default: 1084 | lp.alpha = mMiddleAlpha; 1085 | lp.scale = mMiddleScale; 1086 | } 1087 | } 1088 | 1089 | private void updateAlphaAndScaleBy5Child(View child, LayoutParams lp, float offsetPercent) { 1090 | switch (lp.to) { 1091 | case 0: 1092 | case 1: 1093 | setAsBottomBy5Child(child); 1094 | lp.alpha = mBottomAlpha; 1095 | lp.scale = mBottomScale; 1096 | break; 1097 | default: 1098 | float alphaProgress; 1099 | if (offsetPercent > .5F) { 1100 | alphaProgress = (offsetPercent - .5F) * 2; 1101 | } else { 1102 | alphaProgress = 0; 1103 | } 1104 | lp.alpha = mBottomAlpha + (mMiddleAlpha - mBottomAlpha) * alphaProgress; 1105 | lp.scale = mBottomScale + (mMiddleScale - mBottomScale) * offsetPercent; 1106 | break; 1107 | } 1108 | } 1109 | 1110 | /** 1111 | * 把目标子View放置到视图层级最底部 1112 | */ 1113 | private void setAsBottomBy5Child(View target) { 1114 | //先确定现在在哪个位置 1115 | int startIndex = indexOfChild(target); 1116 | //计算一共需要几次交换,就可到达最下面 1117 | for (int i = startIndex; i >= 0; i--) { 1118 | //更新索引 1119 | int fromIndex = indexOfChild(target); 1120 | if (fromIndex == 0) { 1121 | break; 1122 | } 1123 | //目标是它的下层 1124 | int toIndex = fromIndex - 1; 1125 | //获取需要交换位置的两个子View 1126 | View from = target; 1127 | View to = getChildAt(toIndex); 1128 | 1129 | //先把它们拿出来 1130 | detachViewFromParent(fromIndex); 1131 | detachViewFromParent(toIndex); 1132 | 1133 | //再放回去,但是放回去的位置(索引)互换了 1134 | attachViewToParent(from, toIndex, from.getLayoutParams()); 1135 | attachViewToParent(to, fromIndex, to.getLayoutParams()); 1136 | } 1137 | //刷新 1138 | invalidate(); 1139 | } 1140 | 1141 | /** 1142 | * 交换子View的层级顺序 1143 | * 1144 | * @param fromIndex 原子View索引 1145 | * @param toIndex 目标子View索引 1146 | */ 1147 | private void exchangeOrder(int fromIndex, int toIndex) { 1148 | if (fromIndex == toIndex || fromIndex >= getChildCount() || toIndex >= getChildCount()) { 1149 | return; 1150 | } 1151 | if (fromIndex > toIndex) { 1152 | int temp = fromIndex; 1153 | fromIndex = toIndex; 1154 | toIndex = temp; 1155 | } 1156 | 1157 | View from = getChildAt(fromIndex); 1158 | View to = getChildAt(toIndex); 1159 | 1160 | detachViewFromParent(toIndex); 1161 | detachViewFromParent(fromIndex); 1162 | 1163 | attachViewToParent(to, fromIndex, to.getLayoutParams()); 1164 | attachViewToParent(from, toIndex, from.getLayoutParams()); 1165 | invalidate(); 1166 | } 1167 | 1168 | /** 1169 | * @return 当前是否水平方向 1170 | */ 1171 | private boolean isHorizontal() { 1172 | return mOrientation == ORIENTATION_HORIZONTAL; 1173 | } 1174 | 1175 | private boolean is5Child() { 1176 | return getChildCount() > 3; 1177 | } 1178 | 1179 | /** 1180 | * 更新子View的不透明度、缩放比例、坐标位置,并布局 1181 | */ 1182 | private void updateChildParamsAndLayout(View child, int baseLine) { 1183 | LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1184 | 1185 | child.setAlpha(lp.alpha); 1186 | child.setScaleX(lp.scale); 1187 | child.setScaleY(lp.scale); 1188 | 1189 | int childWidth; 1190 | int childHeight; 1191 | 1192 | if (child.getWidth() > 0 && child.getHeight() > 0) { 1193 | childWidth = child.getWidth(); 1194 | childHeight = child.getHeight(); 1195 | } else { 1196 | //第一次布局 1197 | childWidth = child.getMeasuredWidth(); 1198 | childHeight = child.getMeasuredHeight(); 1199 | } 1200 | 1201 | int left, top, right, bottom; 1202 | if (isHorizontal()) { 1203 | int baseLineCenterY = getHeight() / 2; 1204 | left = baseLine - childWidth / 2; 1205 | top = baseLineCenterY - childHeight / 2; 1206 | } else { 1207 | int baseLineCenterX = getWidth() / 2; 1208 | left = baseLineCenterX - childWidth / 2; 1209 | top = baseLine - childHeight / 2; 1210 | } 1211 | right = left + childWidth; 1212 | bottom = top + childHeight; 1213 | 1214 | child.layout( 1215 | left + lp.leftMargin + getPaddingLeft(), 1216 | top + lp.topMargin + getPaddingTop(), 1217 | right + lp.leftMargin - getPaddingRight(), 1218 | bottom + lp.topMargin - getPaddingBottom()); 1219 | } 1220 | 1221 | @Override 1222 | public void addView(View child, int index, ViewGroup.LayoutParams params) { 1223 | int childCount = getChildCount(); 1224 | if (childCount > 4) { 1225 | throw new IllegalStateException("LitePager can only contain 5 child!"); 1226 | } 1227 | LayoutParams lp = params instanceof LayoutParams ? (LayoutParams) params : new LayoutParams(params); 1228 | lp.from = index == -1 ? childCount : index; 1229 | if (childCount < 2) { 1230 | lp.alpha = mMiddleAlpha; 1231 | lp.scale = mMiddleScale; 1232 | } else if (childCount < 4) { 1233 | lp.alpha = mBottomAlpha; 1234 | lp.scale = mBottomScale; 1235 | } else { 1236 | lp.alpha = mTopAlpha; 1237 | lp.scale = mTopScale; 1238 | } 1239 | super.addView(child, index, params); 1240 | } 1241 | 1242 | @Override 1243 | public LayoutParams generateLayoutParams(AttributeSet attrs) { 1244 | return new LayoutParams(getContext(), attrs); 1245 | } 1246 | 1247 | @Override 1248 | protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 1249 | return new LayoutParams(p); 1250 | } 1251 | 1252 | @Override 1253 | protected LayoutParams generateDefaultLayoutParams() { 1254 | return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); 1255 | } 1256 | 1257 | @Override 1258 | protected void onAttachedToWindow() { 1259 | super.onAttachedToWindow(); 1260 | if (mAutoScrollEnable) { 1261 | run(); 1262 | } 1263 | } 1264 | 1265 | @Override 1266 | protected void onDetachedFromWindow() { 1267 | super.onDetachedFromWindow(); 1268 | if (mAutoScrollEnable) { 1269 | removeCallbacks(this); 1270 | } 1271 | } 1272 | 1273 | @Override 1274 | public void run() { 1275 | if (mAutoScrollOrientation == SCROLL_ORIENTATION_LEFT) { 1276 | setSelection(is5Child() ? 2 : 0); 1277 | } else { 1278 | setSelection(is5Child() ? 3 : 1); 1279 | } 1280 | if (mAutoScrollEnable) { 1281 | postDelayed(this, mAutoScrollInterval); 1282 | } 1283 | } 1284 | 1285 | private boolean mPostOnAnimationEnd; 1286 | private Adapter mTempAdapter; 1287 | 1288 | private void setAdapterInternal(Adapter adapter) { 1289 | if (mAnimator != null && mAnimator.isRunning()) { 1290 | mTempAdapter = adapter; 1291 | mPostOnAnimationEnd = true; 1292 | } else { 1293 | updateAdapterDataNow(adapter); 1294 | } 1295 | } 1296 | 1297 | private void updateAdapterDataNow(Adapter adapter) { 1298 | removeAllViews(); 1299 | for (int i = 0; i < adapter.getItemCount(); i++) { 1300 | View view = adapter.onCreateView(this); 1301 | //noinspection unchecked 1302 | adapter.onBindView(view, i); 1303 | ViewGroup.LayoutParams lp = view.getLayoutParams(); 1304 | if (lp != null) { 1305 | if (!(lp instanceof LayoutParams)) { 1306 | view.setLayoutParams(new LayoutParams(lp)); 1307 | } 1308 | } 1309 | addView(view); 1310 | } 1311 | } 1312 | 1313 | /** 1314 | * 设置调整动画的时长 1315 | */ 1316 | public void setFlingDuration(long flingDuration) { 1317 | mFlingDuration = flingDuration; 1318 | } 1319 | 1320 | /** 1321 | * 设置最小缩放比例 1322 | */ 1323 | public void setBottomScale(@FloatRange(from = 0, to = 1) float scale) { 1324 | mBottomScale = scale; 1325 | if (!is5Child()) { 1326 | mMiddleScale = scale; 1327 | } 1328 | requestLayout(); 1329 | } 1330 | 1331 | /** 1332 | * 设置最小不透明度 1333 | */ 1334 | public void setBottomAlpha(@FloatRange(from = 0, to = 1) float alpha) { 1335 | mBottomAlpha = alpha; 1336 | if (!is5Child()) { 1337 | mMiddleAlpha = alpha; 1338 | } 1339 | requestLayout(); 1340 | } 1341 | 1342 | /** 1343 | * 设置最大缩放比例 1344 | */ 1345 | public void setTopScale(@FloatRange(from = 0, to = 1) float scale) { 1346 | mTopScale = scale; 1347 | requestLayout(); 1348 | } 1349 | 1350 | /** 1351 | * 设置最大不透明度 1352 | */ 1353 | public void setTopAlpha(@FloatRange(from = 0, to = 1) float alpha) { 1354 | mTopAlpha = alpha; 1355 | requestLayout(); 1356 | } 1357 | 1358 | /** 1359 | * 设置中部缩放比例 1360 | */ 1361 | public void setMiddleScale(@FloatRange(from = 0, to = 1) float scale) { 1362 | mMiddleScale = scale; 1363 | requestLayout(); 1364 | } 1365 | 1366 | /** 1367 | * 设置中部不透明度 1368 | */ 1369 | public void setMiddleAlpha(@FloatRange(from = 0, to = 1) float alpha) { 1370 | mMiddleAlpha = alpha; 1371 | requestLayout(); 1372 | } 1373 | 1374 | /** 1375 | * 设置方向 1376 | */ 1377 | public void setOrientation(@Orientation int orientation) { 1378 | mOrientation = orientation; 1379 | mOffsetX = 0; 1380 | mOffsetY = 0; 1381 | mOffsetPercent = 0; 1382 | int oldState = mCurrentState; 1383 | mCurrentState = STATE_IDLE; 1384 | if (oldState != mCurrentState && mOnScrollListener != null) { 1385 | mOnScrollListener.onStateChanged(mCurrentState); 1386 | } 1387 | requestLayout(); 1388 | } 1389 | 1390 | /** 1391 | * 设置滚动状态监听 1392 | */ 1393 | public void setOnScrollListener(OnScrollListener onScrollListener) { 1394 | mOnScrollListener = onScrollListener; 1395 | } 1396 | 1397 | /** 1398 | * 获取当前选中的子View 1399 | */ 1400 | public View getSelectedChild() { 1401 | return getChildAt(getChildCount() - 1); 1402 | } 1403 | 1404 | public interface OnScrollListener { 1405 | void onStateChanged(int newState); 1406 | } 1407 | 1408 | /** 1409 | * 设置子View被选中的监听器 1410 | */ 1411 | public void setOnItemSelectedListener(OnItemSelectedListener onItemSelectedListener) { 1412 | mOnItemSelectedListener = onItemSelectedListener; 1413 | } 1414 | 1415 | public interface OnItemSelectedListener { 1416 | void onItemSelected(View selectedItem); 1417 | } 1418 | 1419 | /** 1420 | * 设置是否开启自动轮播 1421 | */ 1422 | public LitePager setAutoScrollEnable(boolean enable) { 1423 | if (mAutoScrollEnable != enable) { 1424 | mAutoScrollEnable = enable; 1425 | if (enable) { 1426 | postDelayed(this, mAutoScrollInterval); 1427 | } else { 1428 | removeCallbacks(this); 1429 | } 1430 | } 1431 | return this; 1432 | } 1433 | 1434 | /** 1435 | * 设置轮播间隔 1436 | * 1437 | * @param interval 毫秒,默认:5000 1438 | */ 1439 | public LitePager setAutoScrollInterval(long interval) { 1440 | mAutoScrollInterval = interval; 1441 | return this; 1442 | } 1443 | 1444 | /** 1445 | * 设置轮播方向 {@link ScrollOrientation} 1446 | * 1447 | * @param orientation 方向 1448 | */ 1449 | public LitePager setAutoScrollOrientation(@ScrollOrientation int orientation) { 1450 | mAutoScrollOrientation = orientation; 1451 | return this; 1452 | } 1453 | 1454 | public boolean isAutoScrollEnable() { 1455 | return mAutoScrollEnable; 1456 | } 1457 | 1458 | public long getAutoScrollInterval() { 1459 | return mAutoScrollInterval; 1460 | } 1461 | 1462 | public int getAutoScrollOrientation() { 1463 | return mAutoScrollOrientation; 1464 | } 1465 | 1466 | /** 1467 | * 设置适配器 1468 | * 1469 | * @param adapter 适配器 1470 | */ 1471 | public LitePager setAdapter(Adapter adapter) { 1472 | if (mAdapter != null) { 1473 | mAdapter.mLitePager = null; 1474 | } 1475 | if (adapter == null) { 1476 | mAdapter = null; 1477 | removeAllViews(); 1478 | return this; 1479 | } 1480 | mAdapter = adapter; 1481 | mAdapter.mLitePager = this; 1482 | setAdapterInternal(mAdapter); 1483 | return this; 1484 | } 1485 | 1486 | public Adapter getAdapter() { 1487 | return mAdapter; 1488 | } 1489 | 1490 | @SuppressWarnings("WeakerAccess") 1491 | public static abstract class Adapter { 1492 | 1493 | private LitePager mLitePager; 1494 | 1495 | @CallSuper 1496 | public void notifyDataSetChanged() { 1497 | if (mLitePager != null) { 1498 | mLitePager.setAdapterInternal(this); 1499 | } 1500 | } 1501 | 1502 | protected abstract V onCreateView(@NonNull ViewGroup parent); 1503 | 1504 | protected abstract void onBindView(@NonNull V v, int position); 1505 | 1506 | protected abstract int getItemCount(); 1507 | } 1508 | 1509 | static class LayoutParams extends MarginLayoutParams { 1510 | 1511 | int to, from; 1512 | float scale; 1513 | float alpha; 1514 | 1515 | LayoutParams(Context c, AttributeSet attrs) { 1516 | super(c, attrs); 1517 | } 1518 | 1519 | LayoutParams(int width, int height) { 1520 | super(width, height); 1521 | } 1522 | 1523 | LayoutParams(ViewGroup.LayoutParams source) { 1524 | super(source); 1525 | } 1526 | } 1527 | } -------------------------------------------------------------------------------- /litepager/src/main/java/com/wuyr/litepager/ValueAnimatorUtil.java: -------------------------------------------------------------------------------- 1 | package com.wuyr.litepager; 2 | 3 | import android.animation.ValueAnimator; 4 | import android.support.annotation.NonNull; 5 | 6 | import java.lang.reflect.Field; 7 | 8 | /** 9 | * @author wuyr 10 | * @github https://github.com/wuyr/LitePager 11 | * @since 2019-04-03 上午10:37 12 | */ 13 | class ValueAnimatorUtil { 14 | 15 | /** 16 | * 重置动画缩放时长 17 | */ 18 | static void resetDurationScale() { 19 | try { 20 | getField().setFloat(null, 1); 21 | } catch (Exception e) { 22 | e.printStackTrace(); 23 | } 24 | } 25 | 26 | private static float getDurationScale() { 27 | try { 28 | return getField().getFloat(null); 29 | } catch (Exception e) { 30 | e.printStackTrace(); 31 | return -1; 32 | } 33 | } 34 | 35 | @NonNull 36 | private static Field getField() throws NoSuchFieldException { 37 | @SuppressWarnings("JavaReflectionMemberAccess") 38 | Field field = ValueAnimator.class.getDeclaredField("sDurationScale"); 39 | field.setAccessible(true); 40 | return field; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /litepager/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 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /litepager/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | litePager 3 | -------------------------------------------------------------------------------- /litepager/src/test/java/com/wuyr/litepager/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.wuyr.litepager; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':litepager' 2 | --------------------------------------------------------------------------------