├── .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 |
6 |
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 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/.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 |
29 |
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 |  
142 |  
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 |
--------------------------------------------------------------------------------