├── .gitignore
├── .idea
├── .name
├── compiler.xml
├── copyright
│ └── profiles_settings.xml
├── encodings.xml
├── gradle.xml
├── misc.xml
├── modules.xml
└── runConfigurations.xml
├── README.md
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── library
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── cn
│ │ └── carbs
│ │ └── android
│ │ └── expandabletextview
│ │ └── library
│ │ └── ExampleInstrumentationTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── cn
│ │ │ └── carbs
│ │ │ └── android
│ │ │ └── expandabletextview
│ │ │ └── library
│ │ │ └── ExpandableTextView.java
│ └── res
│ │ ├── values-zh-rCN
│ │ └── strings.xml
│ │ └── values
│ │ ├── attr_expandable_text_view.xml
│ │ └── strings.xml
│ └── test
│ └── java
│ └── cn
│ └── carbs
│ └── android
│ └── expandabletextview
│ └── library
│ └── ExampleUnitTest.java
├── sample
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── cn
│ │ └── carbs
│ │ └── android
│ │ └── expandabletextview
│ │ └── ExampleInstrumentationTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── cn
│ │ │ └── carbs
│ │ │ └── android
│ │ │ └── expandabletextview
│ │ │ ├── ActivityListView.java
│ │ │ └── ActivityMain.java
│ └── res
│ │ ├── layout
│ │ ├── activity_listview.xml
│ │ ├── activity_main.xml
│ │ └── item.xml
│ │ ├── mipmap-hdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-mdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-xhdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-xxhdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-xxxhdpi
│ │ └── ic_launcher.png
│ │ ├── values-w820dp
│ │ └── dimens.xml
│ │ └── values
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ └── test
│ └── java
│ └── cn
│ └── carbs
│ └── android
│ └── expandabletextview
│ └── ExampleUnitTest.java
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/workspace.xml
5 | /.idea/libraries
6 | .DS_Store
7 | /build
8 | /captures
9 |
--------------------------------------------------------------------------------
/.idea/.name:
--------------------------------------------------------------------------------
1 | ExpandableTextView
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/.idea/copyright/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
24 |
25 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | 1.8
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ExpandableTextView
2 | an ExpandableTextView on Android platform which can shrink TextView height if its line count is greater than a certain number, it also can toggle state between expand and shrink
3 |
4 | * [English](#english)
5 |
6 | # 前言:
7 | 为了保持界面UI的整洁以及将尽可能多的内容显示在有限的空间中,往往需要将长度过长的TextView进行内容截取。本控件满足了TextView可在"完整内容"与"截取内容"两种模式下进行切换的需求,且可应用在ListView/RecyclerView中并可以动态更新内容。
8 |
9 | ## 截图:
10 | #### 静态截图如下:
11 |
12 | ![hint1][1]
13 |
14 | #### 动态效果图可点击如下链接:
15 |
16 | ![hint2][2]
17 |
18 |
19 | ## 主要功能:
20 | 1. 限制行数,行尾添加`ClickSpan`,点击可以"展开"/"收起"两种状态切换;
21 | 2. 可使用在`ListView`/`RecyclerView`中,效率较高;
22 | 3. 可在任意时刻更新`ExpandableTextView`内容(布局显示之前或者显示之后);
23 | 4. 可自定义行数限制,默认最多显示2行;
24 | 5. 可自定义行尾`ClickSpan`是否显示,颜色,文字,按下的背景颜色;
25 | 6. 可添加点击此view后是否在"展开"/"收起"状态间切换;
26 | 7. 文字不足最大限制行数时,不截断文字,不显示末尾的"展开"/"收起"的指示标识;
27 | 8. 可自定义行尾省略语与行尾"展开"/"收起"的指示标识之间的gap文字;
28 |
29 | ## 说明:
30 | 1. 效果参考了jQuery的readmore.js,部分代码参考了[ReadMoreTextView][3]
31 | 2. 与Github上star数最多的[ExpandableTextView][4]实现原理及UI完全不同。
32 | 3. 暂时未添加"收缩"/"展开"时的动画效果。
33 |
34 | ## 优化:
35 | 1. 解决末尾显示的指示标识文字与原来文字宽度不一致时的显示问题(如原始文字与行尾指示标识文字为不同语言)。如当结尾指示标识文字较宽时,可能会显示到下一行。以此优化UI体验。
36 | 2. 解决末尾单词过长或者跟随标点后,换行留下的空白问题。此问题源于TextView自带的一个属性:当结尾为完整单词或者跟随标点时会连同之前的部分文字一起换行。
37 | 3. 解决文字过短时,截取文字超出边界的问题。
38 | 4. 解决任何时刻为`ExpandableTextView`更新文字的问题。
39 |
40 | ## 不具有的功能:
41 | 1. 限制字符长度。此控件只限制最大行数,不限制字符长度;
42 | 2. 省略标识的位置自定义。省略标识的位置暂时只能显示在行尾,不能够指定是否在"行首"/"行中"/"行尾"
43 | 3. 暂时未添加"收缩"/"展开"时的动画效果。
44 |
45 | ## 添加依赖
46 | ```groovy
47 | compile 'cn.carbs.android:ExpandableTextView:1.0.3'
48 | ```
49 |
50 | ## 使用方法:
51 | 有两种方法设置文字:
52 | (1)在java中更新文字
53 | ```java
54 | //普通视图中的更新
55 | etv.setText(text);
56 | //在ListView/RecyclerView中的应用
57 | etv.updateForRecyclerView(text, etvWidth, state);//etvWidth为控件的真实宽度,state是控件所处的状态,“收缩”/“伸展”状态
58 | ```
59 | (2)在xml中直接设置文字
60 | ```xml
61 |
66 | ```
67 | (3)可配置的属性有如下几项
68 | ```xml
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 | ```
89 |
90 | ## 实现原理:
91 | 1. 控件继承自`TextView`,`TextView`中的`setText(CharSequence text)`方法为 `final` 类型,且其内部最终调用了`setText(CharSequence text, BufferType type)`,因此`ExpandableTextView` Override了`setText(CharSequence text, BufferType type)`方法,且`TextView`在通过xml布局文件设置text时,同样最终是通过`setText(CharSequence text, BufferType type)`进行赋值,因此通过Override此方法达到自定义显示text的效果;
92 | 2. 采用android.text.Layout类来确定在一定宽度下,特定的文本所达到的行数,如果超过最大行数,则添加收缩/展开效果;
93 | 3. 为文本特定位置添加ClickableSpan,以此添加点击部分文本的响应效果;自定义`ClickableSpan`和`LinkMovementMethod`,达到添加点击`ClickableSpan`文字背景颜色改变的效果,感谢stackoverflow的解答;
94 | 4. 通过`Paint.measureText(String text)`方法,找到文本截取的最优位置,使得在行尾添加了ClickableSpan后,不会出现因文字宽度不同而导致的文本换行或者文本末尾空余过大的现象;
95 |
96 |
97 | ## 感谢
98 | #### [ReadMoreTextView][3]
99 | ---------------------
100 | # English
101 |
102 |
103 | ## License
104 |
105 | Copyright 2016 Carbs.Wang (ExpandableTextView)
106 |
107 | Licensed under the Apache License, Version 2.0 (the "License");
108 | you may not use this file except in compliance with the License.
109 | You may obtain a copy of the License at
110 |
111 | http://www.apache.org/licenses/LICENSE-2.0
112 |
113 | Unless required by applicable law or agreed to in writing, software
114 | distributed under the License is distributed on an "AS IS" BASIS,
115 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
116 | See the License for the specific language governing permissions and
117 | limitations under the License.
118 |
119 |
120 |
121 | [1]: https://github.com/Carbs0126/Screenshot/blob/master/expandabletextview.jpg
122 | [2]: https://github.com/Carbs0126/Screenshot/blob/master/expandabletextview.gif
123 | [3]: https://github.com/borjabravo10/ReadMoreTextView
124 | [4]: https://github.com/Manabu-GT/ExpandableTextView
125 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | repositories {
5 | jcenter()
6 | }
7 | dependencies {
8 | classpath 'com.android.tools.build:gradle:2.0.0'
9 | classpath 'com.novoda:bintray-release:0.3.4'
10 | // NOTE: Do not place your application dependencies here; they belong
11 | // in the individual module build.gradle files
12 | }
13 | }
14 |
15 | allprojects {
16 | repositories {
17 | jcenter()
18 | }
19 | }
20 |
21 | task clean(type: Delete) {
22 | delete rootProject.buildDir
23 | }
24 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | org.gradle.jvmargs=-Xmx1536m
13 |
14 | # When configured, Gradle will run in incubating parallel mode.
15 | # This option should only be used with decoupled projects. More details, visit
16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
17 | # org.gradle.parallel=true
18 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Carbs0126/ExpandableTextView/b5b464ff5f9917e9c6d0076fe920e13bfab99a78/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Dec 28 10:00:20 PST 2015
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/library/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/library/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'com.novoda.bintray-release'
3 |
4 | android {
5 | compileSdkVersion 24
6 | buildToolsVersion "24.0.0"
7 |
8 | defaultConfig {
9 | minSdkVersion 8
10 | targetSdkVersion 24
11 | versionCode 1
12 | versionName "1.0"
13 |
14 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
15 | }
16 |
17 | buildTypes {
18 | release {
19 | minifyEnabled false
20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
21 | }
22 | }
23 |
24 | lintOptions {
25 | abortOnError false
26 | }
27 | }
28 |
29 | dependencies {
30 | compile fileTree(dir: 'libs', include: ['*.jar'])
31 | compile 'com.android.support:appcompat-v7:24.0.0'
32 | testCompile 'junit:junit:4.12'
33 | androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2'
34 | androidTestCompile 'com.android.support.test:runner:0.5'
35 | androidTestCompile 'com.android.support:support-annotations:24.0.0'
36 | }
37 |
38 | publish {
39 | userOrg = 'carbs'
40 | groupId = 'cn.carbs.android'
41 | artifactId = 'ExpandableTextView'
42 | publishVersion = '1.0.2'
43 | desc = 'an ExpandableTextView on Android platform which can shrink TextView height if its line count is greater than a certain number, it also can toggle state between expand and shrink'
44 | website = 'https://github.com/Carbs0126/ExpandableTextView'
45 | }
--------------------------------------------------------------------------------
/library/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in C:\Android\SDK/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/library/src/androidTest/java/cn/carbs/android/expandabletextview/library/ExampleInstrumentationTest.java:
--------------------------------------------------------------------------------
1 | package cn.carbs.android.expandabletextview.library;
2 |
3 | import android.content.Context;
4 | import android.support.test.InstrumentationRegistry;
5 | import android.support.test.filters.MediumTest;
6 | import android.support.test.runner.AndroidJUnit4;
7 |
8 | import org.junit.Test;
9 | import org.junit.runner.RunWith;
10 |
11 |
12 | import static org.junit.Assert.*;
13 |
14 | /**
15 | * Instrumentation test, which will execute on an Android device.
16 | *
17 | * @see Testing documentation
18 | */
19 | @MediumTest
20 | @RunWith(AndroidJUnit4.class)
21 | public class ExampleInstrumentationTest {
22 | @Test
23 | public void useAppContext() throws Exception {
24 | // Context of the app under test.
25 | Context appContext = InstrumentationRegistry.getTargetContext();
26 |
27 | assertEquals("cn.carbs.android.expandabletextview.library.test", appContext.getPackageName());
28 | }
29 | }
--------------------------------------------------------------------------------
/library/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/library/src/main/java/cn/carbs/android/expandabletextview/library/ExpandableTextView.java:
--------------------------------------------------------------------------------
1 | package cn.carbs.android.expandabletextview.library;
2 |
3 | /**
4 | * Created by Carbs.Wang on 2016/7/16.
5 | * website: https://github.com/Carbs0126/
6 | *
7 | * Thanks to :
8 | * 1.ReadMoreTextView
9 | * https://github.com/borjabravo10/ReadMoreTextView
10 | * 2.TouchableSpan
11 | * http://stackoverflow.com/questions
12 | * /20856105/change-the-text-color-of-a-single-clickablespan-when-pressed-without-affecting-o
13 | * 3.FlatUI
14 | * http://www.bootcss.com/p/flat-ui/
15 | */
16 | import android.content.Context;
17 | import android.content.res.TypedArray;
18 | import android.os.Build;
19 | import android.text.DynamicLayout;
20 | import android.text.Layout;
21 | import android.text.Selection;
22 | import android.text.Spannable;
23 | import android.text.SpannableStringBuilder;
24 | import android.text.Spanned;
25 | import android.text.TextPaint;
26 | import android.text.TextUtils;
27 | import android.text.method.LinkMovementMethod;
28 | import android.text.style.ClickableSpan;
29 | import android.util.AttributeSet;
30 | import android.view.MotionEvent;
31 | import android.view.View;
32 | import android.view.ViewTreeObserver;
33 | import android.widget.TextView;
34 |
35 | import java.lang.reflect.Field;
36 |
37 | public class ExpandableTextView extends TextView{
38 |
39 | public static final int STATE_SHRINK = 0;
40 | public static final int STATE_EXPAND = 1;
41 |
42 | private static final String CLASS_NAME_VIEW = "android.view.View";
43 | private static final String CLASS_NAME_LISTENER_INFO = "android.view.View$ListenerInfo";
44 | private static final String ELLIPSIS_HINT = "..";
45 | private static final String GAP_TO_EXPAND_HINT = " ";
46 | private static final String GAP_TO_SHRINK_HINT = " ";
47 | private static final int MAX_LINES_ON_SHRINK = 2;
48 | private static final int TO_EXPAND_HINT_COLOR = 0xFF3498DB;
49 | private static final int TO_SHRINK_HINT_COLOR = 0xFFE74C3C;
50 | private static final int TO_EXPAND_HINT_COLOR_BG_PRESSED = 0x55999999;
51 | private static final int TO_SHRINK_HINT_COLOR_BG_PRESSED = 0x55999999;
52 | private static final boolean TOGGLE_ENABLE = true;
53 | private static final boolean SHOW_TO_EXPAND_HINT = true;
54 | private static final boolean SHOW_TO_SHRINK_HINT = true;
55 |
56 | private String mEllipsisHint;
57 | private String mToExpandHint;
58 | private String mToShrinkHint;
59 | private String mGapToExpandHint = GAP_TO_EXPAND_HINT;
60 | private String mGapToShrinkHint = GAP_TO_SHRINK_HINT;
61 | private boolean mToggleEnable = TOGGLE_ENABLE;
62 | private boolean mShowToExpandHint = SHOW_TO_EXPAND_HINT;
63 | private boolean mShowToShrinkHint = SHOW_TO_SHRINK_HINT;
64 | private int mMaxLinesOnShrink = MAX_LINES_ON_SHRINK;
65 | private int mToExpandHintColor = TO_EXPAND_HINT_COLOR;
66 | private int mToShrinkHintColor = TO_SHRINK_HINT_COLOR;
67 | private int mToExpandHintColorBgPressed = TO_EXPAND_HINT_COLOR_BG_PRESSED;
68 | private int mToShrinkHintColorBgPressed = TO_SHRINK_HINT_COLOR_BG_PRESSED;
69 | private int mCurrState = STATE_SHRINK;
70 |
71 | // used to add to the tail of modified text, the "shrink" and "expand" text
72 | private TouchableSpan mTouchableSpan;
73 | private BufferType mBufferType = BufferType.NORMAL;
74 | private TextPaint mTextPaint;
75 | private Layout mLayout;
76 | private int mTextLineCount = -1;
77 | private int mLayoutWidth = 0;
78 | private int mFutureTextViewWidth = 0;
79 |
80 | // the original text of this view
81 | private CharSequence mOrigText;
82 |
83 | // used to judge if the listener of corresponding to the onclick event of ExpandableTextView
84 | // is specifically for inner toggle
85 | private ExpandableClickListener mExpandableClickListener;
86 | private OnExpandListener mOnExpandListener;
87 |
88 | public ExpandableTextView(Context context) {
89 | super(context);
90 | init();
91 | }
92 |
93 | public ExpandableTextView(Context context, AttributeSet attrs) {
94 | super(context, attrs);
95 | initAttr(context,attrs);
96 | init();
97 | }
98 |
99 | public ExpandableTextView(Context context, AttributeSet attrs, int defStyleAttr) {
100 | super(context, attrs, defStyleAttr);
101 | initAttr(context,attrs);
102 | init();
103 | }
104 |
105 | private void initAttr(Context context, AttributeSet attrs) {
106 | if (attrs == null) {
107 | return;
108 | }
109 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ExpandableTextView);
110 | if (a == null) {
111 | return;
112 | }
113 | int n = a.getIndexCount();
114 | for (int i = 0; i < n; i++) {
115 | int attr = a.getIndex(i);
116 | if (attr == R.styleable.ExpandableTextView_etv_MaxLinesOnShrink) {
117 | mMaxLinesOnShrink = a.getInteger(attr, MAX_LINES_ON_SHRINK);
118 | }else if (attr == R.styleable.ExpandableTextView_etv_EllipsisHint){
119 | mEllipsisHint = a.getString(attr);
120 | }else if (attr == R.styleable.ExpandableTextView_etv_ToExpandHint) {
121 | mToExpandHint = a.getString(attr);
122 | }else if (attr == R.styleable.ExpandableTextView_etv_ToShrinkHint) {
123 | mToShrinkHint = a.getString(attr);
124 | }else if (attr == R.styleable.ExpandableTextView_etv_EnableToggle) {
125 | mToggleEnable = a.getBoolean(attr, TOGGLE_ENABLE);
126 | }else if (attr == R.styleable.ExpandableTextView_etv_ToExpandHintShow){
127 | mShowToExpandHint = a.getBoolean(attr, SHOW_TO_EXPAND_HINT);
128 | }else if (attr == R.styleable.ExpandableTextView_etv_ToShrinkHintShow){
129 | mShowToShrinkHint = a.getBoolean(attr, SHOW_TO_SHRINK_HINT);
130 | }else if (attr == R.styleable.ExpandableTextView_etv_ToExpandHintColor){
131 | mToExpandHintColor = a.getInteger(attr, TO_EXPAND_HINT_COLOR);
132 | }else if (attr == R.styleable.ExpandableTextView_etv_ToShrinkHintColor){
133 | mToShrinkHintColor = a.getInteger(attr, TO_SHRINK_HINT_COLOR);
134 | }else if (attr == R.styleable.ExpandableTextView_etv_ToExpandHintColorBgPressed){
135 | mToExpandHintColorBgPressed = a.getInteger(attr, TO_EXPAND_HINT_COLOR_BG_PRESSED);
136 | }else if (attr == R.styleable.ExpandableTextView_etv_ToShrinkHintColorBgPressed){
137 | mToShrinkHintColorBgPressed = a.getInteger(attr, TO_SHRINK_HINT_COLOR_BG_PRESSED);
138 | }else if (attr == R.styleable.ExpandableTextView_etv_InitState){
139 | mCurrState = a.getInteger(attr, STATE_SHRINK);
140 | }else if (attr == R.styleable.ExpandableTextView_etv_GapToExpandHint){
141 | mGapToExpandHint = a.getString(attr);
142 | }else if (attr == R.styleable.ExpandableTextView_etv_GapToShrinkHint){
143 | mGapToShrinkHint = a.getString(attr);
144 | }
145 | }
146 | a.recycle();
147 | }
148 |
149 | private void init() {
150 | mTouchableSpan = new TouchableSpan();
151 | setMovementMethod(new LinkTouchMovementMethod());
152 | if(TextUtils.isEmpty(mEllipsisHint)) {
153 | mEllipsisHint = ELLIPSIS_HINT;
154 | }
155 | if(TextUtils.isEmpty(mToExpandHint)){
156 | mToExpandHint = getResources().getString(R.string.to_expand_hint);
157 | }
158 | if(TextUtils.isEmpty(mToShrinkHint)){
159 | mToShrinkHint = getResources().getString(R.string.to_shrink_hint);
160 | }
161 | if(mToggleEnable){
162 | mExpandableClickListener = new ExpandableClickListener();
163 | setOnClickListener(mExpandableClickListener);
164 | }
165 | getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
166 | @Override
167 | public void onGlobalLayout() {
168 | ViewTreeObserver obs = getViewTreeObserver();
169 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
170 | obs.removeOnGlobalLayoutListener(this);
171 | } else {
172 | obs.removeGlobalOnLayoutListener(this);
173 | }
174 | setTextInternal(getNewTextByConfig(), mBufferType);
175 | }
176 | });
177 | }
178 |
179 | /**
180 | * used in ListView or RecyclerView to update ExpandableTextView
181 | * @param text
182 | * original text
183 | * @param futureTextViewWidth
184 | * the width of ExpandableTextView in px unit,
185 | * used to get max line number of original text by given the width
186 | * @param expandState
187 | * expand or shrink
188 | */
189 | public void updateForRecyclerView(CharSequence text, int futureTextViewWidth, int expandState){
190 | mFutureTextViewWidth = futureTextViewWidth;
191 | mCurrState = expandState;
192 | setText(text);
193 | }
194 |
195 | public void updateForRecyclerView(CharSequence text, BufferType type, int futureTextViewWidth){
196 | mFutureTextViewWidth = futureTextViewWidth;
197 | setText(text, type);
198 | }
199 |
200 | public void updateForRecyclerView(CharSequence text, int futureTextViewWidth){
201 | mFutureTextViewWidth = futureTextViewWidth;
202 | setText(text);
203 | }
204 |
205 | /**
206 | * get the current state of ExpandableTextView
207 | * @return
208 | * STATE_SHRINK if in shrink state
209 | * STATE_EXPAND if in expand state
210 | */
211 | public int getExpandState(){
212 | return mCurrState;
213 | }
214 |
215 | /**
216 | * refresh and get a will-be-displayed text by current configuration
217 | * @return
218 | * get a will-be-displayed text
219 | */
220 | private CharSequence getNewTextByConfig(){
221 | if(TextUtils.isEmpty(mOrigText)){
222 | return mOrigText;
223 | }
224 |
225 | mLayout = getLayout();
226 | if(mLayout != null){
227 | mLayoutWidth = mLayout.getWidth();
228 | }
229 |
230 | if(mLayoutWidth <= 0){
231 | if(getWidth() == 0) {
232 | if (mFutureTextViewWidth == 0) {
233 | return mOrigText;
234 | } else {
235 | mLayoutWidth = mFutureTextViewWidth - getPaddingLeft() - getPaddingRight();
236 | }
237 | }else{
238 | mLayoutWidth = getWidth() - getPaddingLeft() - getPaddingRight();
239 | }
240 | }
241 |
242 | mTextPaint = getPaint();
243 |
244 | mTextLineCount = -1;
245 | switch (mCurrState){
246 | case STATE_SHRINK: {
247 | mLayout = new DynamicLayout(mOrigText, mTextPaint, mLayoutWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
248 | mTextLineCount = mLayout.getLineCount();
249 |
250 | if (mTextLineCount <= mMaxLinesOnShrink) {
251 | return mOrigText;
252 | }
253 | int indexEnd = getValidLayout().getLineEnd(mMaxLinesOnShrink - 1);
254 | int indexStart = getValidLayout().getLineStart(mMaxLinesOnShrink - 1);
255 | int indexEndTrimmed = indexEnd
256 | - getLengthOfString(mEllipsisHint)
257 | - (mShowToExpandHint ? getLengthOfString(mToExpandHint) + getLengthOfString(mGapToExpandHint) : 0);
258 |
259 | if (indexEndTrimmed <= indexStart) {
260 | indexEndTrimmed = indexEnd;
261 | }
262 |
263 | int remainWidth = getValidLayout().getWidth() -
264 | (int) (mTextPaint.measureText(mOrigText.subSequence(indexStart, indexEndTrimmed).toString()) + 0.5);
265 | float widthTailReplaced = mTextPaint.measureText(getContentOfString(mEllipsisHint)
266 | + (mShowToExpandHint ? (getContentOfString(mToExpandHint) + getContentOfString(mGapToExpandHint)) : ""));
267 |
268 | int indexEndTrimmedRevised = indexEndTrimmed;
269 | if (remainWidth > widthTailReplaced) {
270 | int extraOffset = 0;
271 | int extraWidth = 0;
272 | while (remainWidth > widthTailReplaced + extraWidth) {
273 | extraOffset++;
274 | if (indexEndTrimmed + extraOffset <= mOrigText.length()) {
275 | extraWidth = (int) (mTextPaint.measureText(
276 | mOrigText.subSequence(indexEndTrimmed, indexEndTrimmed + extraOffset).toString()) + 0.5);
277 | } else {
278 | break;
279 | }
280 | }
281 | indexEndTrimmedRevised += extraOffset - 1;
282 | } else {
283 | int extraOffset = 0;
284 | int extraWidth = 0;
285 | while (remainWidth + extraWidth < widthTailReplaced) {
286 | extraOffset--;
287 | if (indexEndTrimmed + extraOffset > indexStart) {
288 | extraWidth = (int) (mTextPaint.measureText(mOrigText.subSequence(indexEndTrimmed + extraOffset, indexEndTrimmed).toString()) + 0.5);
289 | } else {
290 | break;
291 | }
292 | }
293 | indexEndTrimmedRevised += extraOffset;
294 | }
295 |
296 | CharSequence fixText = removeEndLineBreak(mOrigText.subSequence(0, indexEndTrimmedRevised));
297 | SpannableStringBuilder ssbShrink = new SpannableStringBuilder(fixText)
298 | .append(mEllipsisHint);
299 | if (mShowToExpandHint) {
300 | ssbShrink.append(getContentOfString(mGapToExpandHint) + getContentOfString(mToExpandHint));
301 | ssbShrink.setSpan(mTouchableSpan, ssbShrink.length() - getLengthOfString(mToExpandHint), ssbShrink.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
302 | }
303 | return ssbShrink;
304 | }
305 | case STATE_EXPAND: {
306 | if (!mShowToShrinkHint) {
307 | return mOrigText;
308 | }
309 | mLayout = new DynamicLayout(mOrigText, mTextPaint, mLayoutWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
310 | mTextLineCount = mLayout.getLineCount();
311 |
312 | if (mTextLineCount <= mMaxLinesOnShrink) {
313 | return mOrigText;
314 | }
315 |
316 | SpannableStringBuilder ssbExpand = new SpannableStringBuilder(mOrigText)
317 | .append(mGapToShrinkHint).append(mToShrinkHint);
318 | ssbExpand.setSpan(mTouchableSpan, ssbExpand.length() - getLengthOfString(mToShrinkHint), ssbExpand.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
319 | return ssbExpand;
320 | }
321 | }
322 | return mOrigText;
323 | }
324 |
325 | private CharSequence removeEndLineBreak(CharSequence text) {
326 | while (text.toString().endsWith("\n")) {
327 | text = text.subSequence(0, text.length() - 1);
328 | }
329 | return text;
330 | }
331 |
332 | public void setExpandListener(OnExpandListener listener){
333 | mOnExpandListener = listener;
334 | }
335 |
336 | private Layout getValidLayout(){
337 | return mLayout != null ? mLayout : getLayout();
338 | }
339 |
340 | private void toggle(){
341 | switch (mCurrState){
342 | case STATE_SHRINK:
343 | mCurrState = STATE_EXPAND;
344 | if(mOnExpandListener != null){
345 | mOnExpandListener.onExpand(this);
346 | }
347 | break;
348 | case STATE_EXPAND:
349 | mCurrState = STATE_SHRINK;
350 | if(mOnExpandListener != null){
351 | mOnExpandListener.onShrink(this);
352 | }
353 | break;
354 | }
355 | setTextInternal(getNewTextByConfig(), mBufferType);
356 | }
357 |
358 | @Override
359 | public void setText(CharSequence text, BufferType type) {
360 | mOrigText = text;
361 | mBufferType = type;
362 | setTextInternal(getNewTextByConfig(), type);
363 | }
364 |
365 | private void setTextInternal(CharSequence text, BufferType type){
366 | super.setText(text, type);
367 | }
368 |
369 | private int getLengthOfString(String string){
370 | if(string == null)
371 | return 0;
372 | return string.length();
373 | }
374 |
375 | private String getContentOfString(String string){
376 | if(string == null)
377 | return "";
378 | return string;
379 | }
380 |
381 | public interface OnExpandListener{
382 | void onExpand(ExpandableTextView view);
383 | void onShrink(ExpandableTextView view);
384 | }
385 |
386 | private class ExpandableClickListener implements View.OnClickListener{
387 | @Override
388 | public void onClick(View view) {
389 | toggle();
390 | }
391 | }
392 |
393 | public View.OnClickListener getOnClickListener(View view) {
394 | if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
395 | return getOnClickListenerV14(view);
396 | } else {
397 | return getOnClickListenerV(view);
398 | }
399 | }
400 |
401 | private View.OnClickListener getOnClickListenerV(View view) {
402 | View.OnClickListener retrievedListener = null;
403 | try {
404 | Field field = Class.forName(CLASS_NAME_VIEW).getDeclaredField("mOnClickListener");
405 | field.setAccessible(true);
406 | retrievedListener = (View.OnClickListener) field.get(view);
407 | } catch (Exception e) {
408 | e.printStackTrace();
409 | }
410 |
411 | return retrievedListener;
412 | }
413 |
414 | private View.OnClickListener getOnClickListenerV14(View view) {
415 | View.OnClickListener retrievedListener = null;
416 | try {
417 | Field listenerField = Class.forName(CLASS_NAME_VIEW).getDeclaredField("mListenerInfo");
418 | Object listenerInfo = null;
419 |
420 | if (listenerField != null) {
421 | listenerField.setAccessible(true);
422 | listenerInfo = listenerField.get(view);
423 | }
424 |
425 | Field clickListenerField = Class.forName(CLASS_NAME_LISTENER_INFO).getDeclaredField("mOnClickListener");
426 |
427 | if (clickListenerField != null && listenerInfo != null) {
428 | clickListenerField.setAccessible(true);
429 | retrievedListener = (View.OnClickListener) clickListenerField.get(listenerInfo);
430 | }
431 | } catch (Exception e) {
432 | e.printStackTrace();
433 | }
434 |
435 | return retrievedListener;
436 | }
437 |
438 |
439 | /**
440 | * Copy from:
441 | * http://stackoverflow.com/questions
442 | * /20856105/change-the-text-color-of-a-single-clickablespan-when-pressed-without-affecting-o
443 | * By:
444 | * Steven Meliopoulos
445 | */
446 | private class TouchableSpan extends ClickableSpan {
447 | private boolean mIsPressed;
448 | public void setPressed(boolean isSelected) {
449 | mIsPressed = isSelected;
450 | }
451 |
452 | @Override
453 | public void onClick(View widget) {
454 | if(hasOnClickListeners()
455 | && (getOnClickListener(ExpandableTextView.this) instanceof ExpandableClickListener)) {
456 | }else{
457 | toggle();
458 | }
459 | }
460 |
461 | @Override
462 | public void updateDrawState(TextPaint ds) {
463 | super.updateDrawState(ds);
464 | switch (mCurrState){
465 | case STATE_SHRINK:
466 | ds.setColor(mToExpandHintColor);
467 | ds.bgColor = mIsPressed ? mToExpandHintColorBgPressed : 0;
468 | break;
469 | case STATE_EXPAND:
470 | ds.setColor(mToShrinkHintColor);
471 | ds.bgColor = mIsPressed ? mToShrinkHintColorBgPressed : 0;
472 | break;
473 | }
474 | ds.setUnderlineText(false);
475 | }
476 | }
477 |
478 | /**
479 | * Copy from:
480 | * http://stackoverflow.com/questions
481 | * /20856105/change-the-text-color-of-a-single-clickablespan-when-pressed-without-affecting-o
482 | * By:
483 | * Steven Meliopoulos
484 | */
485 | public class LinkTouchMovementMethod extends LinkMovementMethod {
486 | private TouchableSpan mPressedSpan;
487 |
488 | @Override
489 | public boolean onTouchEvent(TextView textView, Spannable spannable, MotionEvent event) {
490 | if (event.getAction() == MotionEvent.ACTION_DOWN) {
491 | mPressedSpan = getPressedSpan(textView, spannable, event);
492 | if (mPressedSpan != null) {
493 | mPressedSpan.setPressed(true);
494 | Selection.setSelection(spannable, spannable.getSpanStart(mPressedSpan),
495 | spannable.getSpanEnd(mPressedSpan));
496 | }
497 | } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
498 | TouchableSpan touchedSpan = getPressedSpan(textView, spannable, event);
499 | if (mPressedSpan != null && touchedSpan != mPressedSpan) {
500 | mPressedSpan.setPressed(false);
501 | mPressedSpan = null;
502 | Selection.removeSelection(spannable);
503 | }
504 | } else {
505 | if (mPressedSpan != null) {
506 | mPressedSpan.setPressed(false);
507 | super.onTouchEvent(textView, spannable, event);
508 | }
509 | mPressedSpan = null;
510 | Selection.removeSelection(spannable);
511 | }
512 | return true;
513 | }
514 |
515 | private TouchableSpan getPressedSpan(TextView textView, Spannable spannable, MotionEvent event) {
516 |
517 | int x = (int) event.getX();
518 | int y = (int) event.getY();
519 |
520 | x -= textView.getTotalPaddingLeft();
521 | y -= textView.getTotalPaddingTop();
522 |
523 | x += textView.getScrollX();
524 | y += textView.getScrollY();
525 |
526 | Layout layout = textView.getLayout();
527 | int line = layout.getLineForVertical(y);
528 | int off = layout.getOffsetForHorizontal(line, x);
529 |
530 | TouchableSpan[] link = spannable.getSpans(off, off, TouchableSpan.class);
531 | TouchableSpan touchedSpan = null;
532 | if (link.length > 0) {
533 | touchedSpan = link[0];
534 | }
535 | return touchedSpan;
536 | }
537 | }
538 | }
539 |
--------------------------------------------------------------------------------
/library/src/main/res/values-zh-rCN/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | 展开
3 | 收起
4 |
--------------------------------------------------------------------------------
/library/src/main/res/values/attr_expandable_text_view.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 |
--------------------------------------------------------------------------------
/library/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | ExpandableTextView
3 | ExpandableTextView in ListView
4 | Expand
5 | Shrink
6 |
7 |
--------------------------------------------------------------------------------
/library/src/test/java/cn/carbs/android/expandabletextview/library/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package cn.carbs.android.expandabletextview.library;
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() throws Exception {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/sample/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/sample/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 24
5 | buildToolsVersion "24.0.0"
6 | defaultConfig {
7 | applicationId "cn.carbs.android.expandabletextview"
8 | minSdkVersion 15
9 | targetSdkVersion 24
10 | versionCode 1
11 | versionName "1.0"
12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
13 | }
14 | buildTypes {
15 | release {
16 | minifyEnabled false
17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
18 | }
19 | }
20 | }
21 |
22 | dependencies {
23 | compile project(':library')
24 | compile fileTree(dir: 'libs', include: ['*.jar'])
25 | compile 'com.android.support:appcompat-v7:24.0.0'
26 | testCompile 'junit:junit:4.12'
27 | androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2'
28 | androidTestCompile 'com.android.support.test:runner:0.5'
29 | androidTestCompile 'com.android.support:support-annotations:24.0.0'
30 | }
31 |
--------------------------------------------------------------------------------
/sample/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in C:\Android\SDK/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/sample/src/androidTest/java/cn/carbs/android/expandabletextview/ExampleInstrumentationTest.java:
--------------------------------------------------------------------------------
1 | package cn.carbs.android.expandabletextview;
2 |
3 | import android.content.Context;
4 | import android.support.test.InstrumentationRegistry;
5 | import android.support.test.filters.MediumTest;
6 | import android.support.test.runner.AndroidJUnit4;
7 |
8 | import org.junit.Test;
9 | import org.junit.runner.RunWith;
10 |
11 |
12 | import static org.junit.Assert.*;
13 |
14 | /**
15 | * Instrumentation test, which will execute on an Android device.
16 | *
17 | * @see Testing documentation
18 | */
19 | @MediumTest
20 | @RunWith(AndroidJUnit4.class)
21 | public class ExampleInstrumentationTest {
22 | @Test
23 | public void useAppContext() throws Exception {
24 | // Context of the app under test.
25 | Context appContext = InstrumentationRegistry.getTargetContext();
26 |
27 | assertEquals("cn.carbs.android.expandabletextview", appContext.getPackageName());
28 | }
29 | }
--------------------------------------------------------------------------------
/sample/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
11 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/sample/src/main/java/cn/carbs/android/expandabletextview/ActivityListView.java:
--------------------------------------------------------------------------------
1 | package cn.carbs.android.expandabletextview;
2 |
3 | import android.content.Context;
4 | import android.os.Bundle;
5 | import android.support.v7.app.AppCompatActivity;
6 | import android.util.SparseArray;
7 | import android.view.LayoutInflater;
8 | import android.view.View;
9 | import android.view.ViewGroup;
10 | import android.widget.BaseAdapter;
11 | import android.widget.Button;
12 | import android.widget.ListView;
13 |
14 | import java.util.ArrayList;
15 | import java.util.List;
16 |
17 | import cn.carbs.android.expandabletextview.library.ExpandableTextView;
18 |
19 | /**
20 | * Created by carbs on 2016/7/23.
21 | */
22 | public class ActivityListView extends AppCompatActivity implements View.OnClickListener{
23 |
24 | private ListView mListView;
25 | private TheBaseAdapter mAdapter;
26 | private Button mButton;
27 |
28 | private List mStrings = new ArrayList<>();
29 | private boolean mFlag = true;
30 | private CharSequence[] mPoems;
31 | private CharSequence[] mProses;
32 |
33 | @Override
34 | protected void onCreate(Bundle savedInstanceState) {
35 | super.onCreate(savedInstanceState);
36 | setContentView(R.layout.activity_listview);
37 | mListView = (ListView)this.findViewById(R.id.listview);
38 | mButton = (Button)this.findViewById(R.id.button_update_list);
39 |
40 | mPoems = getResources().getStringArray(R.array.poems_2);
41 | mProses = getResources().getStringArray(R.array.prose);
42 |
43 | mAdapter = new TheBaseAdapter(this, mStrings);
44 | mListView.setAdapter(mAdapter);
45 | mButton.setOnClickListener(this);
46 | getWindow().getDecorView().postDelayed(new Runnable() {
47 | @Override
48 | public void run() {
49 | inflateListViews();
50 | }
51 | }, 0);
52 | }
53 |
54 | @Override
55 | public void onClick(View view) {
56 | switch (view.getId()){
57 | case R.id.button_update_list:
58 | inflateListViews();
59 | break;
60 | }
61 | }
62 |
63 | private void inflateListViews(){
64 | mListView.smoothScrollByOffset(0);
65 | mStrings.clear();
66 | if(mFlag){
67 | for(CharSequence cs : mPoems){
68 | mStrings.add(cs.toString());
69 | }
70 | }else{
71 | for(CharSequence cs : mProses){
72 | mStrings.add(cs.toString());
73 | }
74 | }
75 | mAdapter.notifyDataSetChanged();
76 | if(mAdapter.getCount() > 0) {
77 | mListView.setSelection(0);
78 | }
79 | mFlag = !mFlag;
80 | }
81 |
82 | class TheBaseAdapter extends BaseAdapter implements ExpandableTextView.OnExpandListener{
83 |
84 | private SparseArray mPositionsAndStates = new SparseArray<>();
85 | private List mList;
86 | private LayoutInflater inflater;
87 |
88 | public TheBaseAdapter(Context context, List list){
89 | mList = list;
90 | this.inflater = LayoutInflater.from(context);
91 | }
92 |
93 | @Override
94 | public int getCount() {
95 | return mList.size();
96 | }
97 |
98 | @Override
99 | public Object getItem(int i) {
100 | return mList.get(i);
101 | }
102 |
103 | @Override
104 | public long getItemId(int i) {
105 | return i;
106 | }
107 |
108 | //只要在getview时为其赋值为准确的宽度值即可,无论采用何种方法
109 | private int etvWidth;
110 | @Override
111 | public View getView(int position, View convertView, ViewGroup parent) {
112 | final ViewHolder viewHolder;
113 | if (null == convertView){
114 | viewHolder = new ViewHolder();
115 | convertView = inflater.inflate(R.layout.item, parent, false);
116 | viewHolder.etv = (ExpandableTextView) convertView.findViewById(R.id.etv);
117 | convertView.setTag(viewHolder);
118 | }
119 | else{
120 | viewHolder = (ViewHolder) convertView.getTag();
121 | }
122 |
123 | String content = (String)getItem(position);
124 | if(etvWidth == 0){
125 | viewHolder.etv.post(new Runnable() {
126 | @Override
127 | public void run() {
128 | etvWidth = viewHolder.etv.getWidth();
129 | }
130 | });
131 | }
132 | viewHolder.etv.setTag(position);
133 | viewHolder.etv.setExpandListener(this);
134 | Integer state = mPositionsAndStates.get(position);
135 |
136 | viewHolder.etv.updateForRecyclerView(content.toString(), etvWidth, state== null ? 0 : state);//第一次getview时肯定为etvWidth为0
137 |
138 | return convertView;
139 | }
140 |
141 | @Override
142 | public void onExpand(ExpandableTextView view) {
143 | Object obj = view.getTag();
144 | if(obj != null && obj instanceof Integer){
145 | mPositionsAndStates.put((Integer)obj, view.getExpandState());
146 | }
147 | }
148 |
149 | @Override
150 | public void onShrink(ExpandableTextView view) {
151 | Object obj = view.getTag();
152 | if(obj != null && obj instanceof Integer){
153 | mPositionsAndStates.put((Integer)obj, view.getExpandState());
154 | }
155 | }
156 | }
157 |
158 | static class ViewHolder{
159 | ExpandableTextView etv;
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/sample/src/main/java/cn/carbs/android/expandabletextview/ActivityMain.java:
--------------------------------------------------------------------------------
1 | package cn.carbs.android.expandabletextview;
2 |
3 | import android.content.Intent;
4 | import android.os.Bundle;
5 | import android.support.v7.app.AppCompatActivity;
6 | import android.view.View;
7 | import android.widget.Button;
8 | import android.widget.TextView;
9 |
10 | import java.util.Random;
11 |
12 | import cn.carbs.android.expandabletextview.library.ExpandableTextView;
13 |
14 | /**
15 | * Created by carbs on 2016/7/23.
16 | */
17 | public class ActivityMain extends AppCompatActivity implements View.OnClickListener{
18 |
19 | private TextView mTVComparison;
20 | private Button mBtnUpdateText;
21 | private Button mBtnToListView;
22 |
23 | private ExpandableTextView mETV;
24 | private CharSequence[] mPoems = null;
25 |
26 | @Override
27 | protected void onCreate(Bundle savedInstanceState) {
28 | super.onCreate(savedInstanceState);
29 | setContentView(R.layout.activity_main);
30 |
31 | mPoems = getResources().getStringArray(R.array.poems);
32 |
33 | mTVComparison = (TextView)this.findViewById(R.id.tv_comparison);
34 | mBtnUpdateText = (Button)this.findViewById(R.id.button_update_text);
35 | mBtnToListView = (Button)this.findViewById(R.id.button_to_list_view);
36 | mETV = (ExpandableTextView)this.findViewById(R.id.etv);
37 |
38 | mBtnUpdateText.setOnClickListener(this);
39 | mBtnToListView.setOnClickListener(this);
40 |
41 | // 测试添加OnClickListener的情况,功能正常。添加外部的onClick事件后,原来的点击toggle功能自动屏蔽,
42 | // 点击尾部的ClickableSpan仍然有效
43 | /*mETV.setOnClickListener(new View.OnClickListener(){
44 | @Override
45 | public void onClick(View view) {
46 | switch (mETV.getExpandState()){
47 | case ExpandableTextView.STATE_SHRINK:
48 | Toast.makeText(getApplicationContext(),"ExpandableTextView clicked, STATE_SHRINK",
49 | Toast.LENGTH_SHORT).show();
50 | break;
51 | case ExpandableTextView.STATE_EXPAND:
52 | Toast.makeText(getApplicationContext(),"ExpandableTextView clicked, STATE_EXPAND",
53 | Toast.LENGTH_SHORT).show();
54 | break;
55 | }
56 | }
57 | });*/
58 | // mETV.setText(mPoems[0]);//在ExpandableTextView在创建完成之前改变文字,功能正常
59 | }
60 |
61 | @Override
62 | public void onClick(View view) {
63 | switch (view.getId()){
64 | case R.id.button_to_list_view:
65 | gotoCheckInListView();
66 | break;
67 | case R.id.button_update_text:
68 | updateText();
69 | break;
70 | }
71 | }
72 |
73 | private void gotoCheckInListView(){
74 | Intent intent = new Intent(ActivityMain.this, ActivityListView.class);
75 | startActivity(intent);
76 | }
77 |
78 | private Random mRandom = new Random();
79 | private int prevRandomInt = -1;
80 | private int currRandomInt = -1;
81 |
82 | private void updateText(){
83 | currRandomInt = mRandom.nextInt(mPoems.length);
84 | while (prevRandomInt == currRandomInt){
85 | currRandomInt = mRandom.nextInt(mPoems.length);
86 | }
87 | prevRandomInt = currRandomInt;
88 | CharSequence newCS = mPoems[currRandomInt];
89 |
90 | mTVComparison.setText(newCS);//作为对比示例
91 | mETV.setText(newCS);//效果显示
92 | }
93 | }
--------------------------------------------------------------------------------
/sample/src/main/res/layout/activity_listview.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
18 |
19 |
27 |
28 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
15 |
16 |
19 |
20 |
26 |
27 |
36 |
37 |
47 |
48 |
54 |
55 |
63 |
64 |
73 |
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/item.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
12 |
13 |
18 |
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Carbs0126/ExpandableTextView/b5b464ff5f9917e9c6d0076fe920e13bfab99a78/sample/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Carbs0126/ExpandableTextView/b5b464ff5f9917e9c6d0076fe920e13bfab99a78/sample/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Carbs0126/ExpandableTextView/b5b464ff5f9917e9c6d0076fe920e13bfab99a78/sample/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Carbs0126/ExpandableTextView/b5b464ff5f9917e9c6d0076fe920e13bfab99a78/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Carbs0126/ExpandableTextView/b5b464ff5f9917e9c6d0076fe920e13bfab99a78/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | ExpandableTextView
3 | ExpandableTextView
4 | ExpandableTextView in ListView
5 | 原始文本:
6 | 实际效果:
7 | update list
8 | 千古江山,英雄无觅,孙仲谋处。舞榭歌台,风流总被、雨打风吹去。斜阳草树,寻常巷陌,人道寄奴曾住。想当年,金戈铁马,气吞万里如虎。元嘉草草,封狼居胥,赢得仓皇北顾。四十三年,望中犹记,烽火扬州路。可堪回首,佛狸祠下,一片神鸦社鼓。凭谁问:廉颇老矣,尚能饭否?
9 |
10 | - 1.醉里挑灯看剑,梦回吹角连营。八百里分麾下炙,五十弦翻塞外声。沙场秋点兵。马作的卢飞快,弓如霹雳弦惊。了却君王天下事,赢得生前身后名。可怜白发生。
11 | - 2.庭院深深深几许,云窗雾阁春迟。为谁憔悴损芳姿。夜来清梦好,应是发南枝。玉瘦檀轻无限恨,南楼羌管休吹。浓香吹尽有谁知。暖风迟日也,别到杏花肥。
12 | - 3.淮左名都,竹西佳处,解鞍少驻初程。过春风十里。尽荠麦青青。自胡马窥江去后,废池乔木,犹厌言兵。渐黄昏,清角吹寒。都在空城。杜郎俊赏,算而今、重到须惊。纵豆蔻词工,青楼梦好,难赋深情。二十四桥仍在,波心荡、冷月无声。念桥边红药,年年知为谁生。
13 | - 4.春花秋月何时了?往事知多少。小楼昨夜又东风,故国不堪回首月明中。雕栏玉砌应犹在,只是朱颜改。问君能有几多愁?恰似一江春水向东流。
14 | - 5.无言独上西楼,月如钩。寂寞梧桐深院锁清秋。剪不断,理还乱,是离愁。别是一般滋味在心头。
15 | - 6.红酥手,黄縢酒,满城春色宫墙柳。东风恶,欢情薄。一怀愁绪,几年离索。错、错、错。春如旧,人空瘦,泪痕红浥鲛绡透。桃花落,闲池阁。山盟虽在,锦书难托。莫、莫、莫!
16 | - 7.怒发冲冠,凭栏处、潇潇雨歇。抬望眼,仰天长啸,壮怀激烈。三十功名尘与土,八千里路云和月。莫等闲、白了少年头,空悲切!靖康耻,犹未雪。臣子恨,何时灭!驾长车,踏破贺兰山缺。壮志饥餐胡虏肉,笑谈渴饮匈奴血。待从头、收拾旧山河,朝天阙。
17 | - 8.If I were to fall in love,It would have to be with youYour eyes, your smile,The way you laugh,The things you say and do Take me to the places,My heart never knew So, if I were to fall in love,It would have to be with you.
18 | - 9.Forgive me for needing you in my life; Forgive me for enjoying the beauty of your body and soul; Forgive me for wanting to be with you when I grow old
19 | - 10.My river runs to thee. Blue sea, wilt thou welcome me? My river awaits reply.Oh! sea, look graciously.
20 | - 11.You make me feel so happy;Whenever I\'m with you.You make me feel so special--This love is too good to be true.
21 | - 12.If you were a teardrop,In my eye,For fear of losing you,I would never cry.And if the golden sun,Should cease to shine its light,Just one smile from you,Would make my whole world bright.
22 | - 13.Since the first time I saw you,I felt something inside,I don\'t know if it\'s love at first sight,I do know I really like you a lot.
23 | - 14.Thoughts of you dance through my mind. Knowing, it is just a matter of time.Wondering... will u ever be mine?You are in my dreams, night... and sometimes... day.The thoughts seem to never fade away.
24 | - 15.Her gesture, motion, and her smiles, Her wit, her voice my heart beguiles,Beguiles my heart, I know not why,And yet, I\'ll love her till I die.
25 |
26 |
27 |
28 | - 1.醉里挑灯看剑,梦回吹角连营。八百里分麾下炙,五十弦翻塞外声。沙场秋点兵。马作的卢飞快,弓如霹雳弦惊。了却君王天下事,赢得生前身后名。可怜白发生。
29 | - 2.庭院深深深几许,云窗雾阁春迟。为谁憔悴损芳姿。夜来清梦好,应是发南枝。玉瘦檀轻无限恨,南楼羌管休吹。浓香吹尽有谁知。暖风迟日也,别到杏花肥。
30 | - 3.淮左名都,竹西佳处,解鞍少驻初程。过春风十里。尽荠麦青青。自胡马窥江去后,废池乔木,犹厌言兵。渐黄昏,清角吹寒。都在空城。杜郎俊赏,算而今、重到须惊。纵豆蔻词工,青楼梦好,难赋深情。二十四桥仍在,波心荡、冷月无声。念桥边红药,年年知为谁生。
31 | - 4.春花秋月何时了?往事知多少。小楼昨夜又东风,故国不堪回首月明中。雕栏玉砌应犹在,只是朱颜改。问君能有几多愁?恰似一江春水向东流。
32 | - 5.无言独上西楼,月如钩。寂寞梧桐深院锁清秋。剪不断,理还乱,是离愁。别是一般滋味在心头。
33 | - 6.红酥手,黄縢酒,满城春色宫墙柳。东风恶,欢情薄。一怀愁绪,几年离索。错、错、错。春如旧,人空瘦,泪痕红浥鲛绡透。桃花落,闲池阁。山盟虽在,锦书难托。莫、莫、莫!
34 | - 7.怒发冲冠,凭栏处、潇潇雨歇。抬望眼,仰天长啸,壮怀激烈。三十功名尘与土,八千里路云和月。莫等闲、白了少年头,空悲切!靖康耻,犹未雪。臣子恨,何时灭!驾长车,踏破贺兰山缺。壮志饥餐胡虏肉,笑谈渴饮匈奴血。待从头、收拾旧山河,朝天阙。
35 | - 8.并刀如水,吴盐胜雪,纤手破新橙。锦幄初温,兽烟不断,相对坐调笙。低声问向谁行宿,城上已三更。马滑霜浓,不如休去,直是少人行。
36 | - 9.登临送目,正故国晚秋,天气初肃。千里澄江似练,翠峰如簇。归帆去棹残阳里,背西风,酒旗斜矗。彩舟云淡,星河鹭起,画图难足。念往昔,繁华竞逐,叹门外楼头,悲恨相续。千古凭高对此,谩嗟荣辱。六朝旧事随流水,但寒烟衰草凝绿。至今商女,时时犹唱,后庭遗曲。
37 | - 10.莫听穿林打叶声,何妨吟啸且徐行。竹杖芒鞋轻胜马。谁怕!一蓑烟雨任平生。料峭春风吹酒醒,微冷,山头斜照却相迎。回首向来萧瑟处。归去,也无风雨也无晴。
38 | - 11.东风夜放花千树,更吹落星如雨。宝马雕车香满路,凤箫声动,玉壶光转,一夜鱼龙舞。蛾儿雪柳黄金缕,笑语盈盈暗香去。众里寻他千百度,蓦然回首,那人却在,灯火阑珊处。
39 | - 12.楚天千里清秋,水随天去秋无际。遥岑远目,献愁供恨,玉簪螺髻。落日楼头,断鸿声里,江南游子,把吴钩看了,阑干拍遍,无人会、登临意。休说鲈鱼堪脍,尽西风季鹰归未?求田问舍,怕应羞见,刘郎才气。可惜流年,忧愁风雨,树犹如此。倩何人唤取,红巾翠袖,揾英雄泪?
40 | - 13.塞下秋来风景异。衡阳雁去无留意。四面边声连角起。千嶂里。长烟落日孤城闭。浊酒一杯家万里。燕然未勒归无计。羌管悠悠霜满地。人不寐。将军白发征夫泪。
41 | - 14.画屏天畔,梦回依约,十洲云水。手捻红笺寄人书,写无限、伤春事。别浦高楼曾漫倚,对江南千里。楼下分流水声中,有当日、凭高泪。
42 | - 15.态浓意远。眉颦笑浅。薄罗衣窄絮风软。鬓云欺翠卷。南园花树春光暖。红香径里榆钱满。欲上秋千又惊懒。且归休怕晚。
43 | - 16.醉里挑灯看剑,梦回吹角连营。八百里分麾下炙,五十弦翻塞外声。沙场秋点兵。马作的卢飞快,弓如霹雳弦惊。了却君王天下事,赢得生前身后名。可怜白发生。
44 | - 17.庭院深深深几许,云窗雾阁春迟。为谁憔悴损芳姿。夜来清梦好,应是发南枝。玉瘦檀轻无限恨,南楼羌管休吹。浓香吹尽有谁知。暖风迟日也,别到杏花肥。
45 | - 18.淮左名都,竹西佳处,解鞍少驻初程。过春风十里。尽荠麦青青。自胡马窥江去后,废池乔木,犹厌言兵。渐黄昏,清角吹寒。都在空城。杜郎俊赏,算而今、重到须惊。纵豆蔻词工,青楼梦好,难赋深情。二十四桥仍在,波心荡、冷月无声。念桥边红药,年年知为谁生。
46 | - 19.春花秋月何时了?往事知多少。小楼昨夜又东风,故国不堪回首月明中。雕栏玉砌应犹在,只是朱颜改。问君能有几多愁?恰似一江春水向东流。
47 | - 20.无言独上西楼,月如钩。寂寞梧桐深院锁清秋。剪不断,理还乱,是离愁。别是一般滋味在心头。
48 | - 21.红酥手,黄縢酒,满城春色宫墙柳。东风恶,欢情薄。一怀愁绪,几年离索。错、错、错。春如旧,人空瘦,泪痕红浥鲛绡透。桃花落,闲池阁。山盟虽在,锦书难托。莫、莫、莫!
49 | - 22.怒发冲冠,凭栏处、潇潇雨歇。抬望眼,仰天长啸,壮怀激烈。三十功名尘与土,八千里路云和月。莫等闲、白了少年头,空悲切!靖康耻,犹未雪。臣子恨,何时灭!驾长车,踏破贺兰山缺。壮志饥餐胡虏肉,笑谈渴饮匈奴血。待从头、收拾旧山河,朝天阙。
50 | - 23.并刀如水,吴盐胜雪,纤手破新橙。锦幄初温,兽烟不断,相对坐调笙。低声问向谁行宿,城上已三更。马滑霜浓,不如休去,直是少人行。
51 | - 24.登临送目,正故国晚秋,天气初肃。千里澄江似练,翠峰如簇。归帆去棹残阳里,背西风,酒旗斜矗。彩舟云淡,星河鹭起,画图难足。念往昔,繁华竞逐,叹门外楼头,悲恨相续。千古凭高对此,谩嗟荣辱。六朝旧事随流水,但寒烟衰草凝绿。至今商女,时时犹唱,后庭遗曲。
52 | - 25.莫听穿林打叶声,何妨吟啸且徐行。竹杖芒鞋轻胜马。谁怕!一蓑烟雨任平生。料峭春风吹酒醒,微冷,山头斜照却相迎。回首向来萧瑟处。归去,也无风雨也无晴。
53 | - 26.东风夜放花千树,更吹落星如雨。宝马雕车香满路,凤箫声动,玉壶光转,一夜鱼龙舞。蛾儿雪柳黄金缕,笑语盈盈暗香去。众里寻他千百度,蓦然回首,那人却在,灯火阑珊处。
54 | - 27.楚天千里清秋,水随天去秋无际。遥岑远目,献愁供恨,玉簪螺髻。落日楼头,断鸿声里,江南游子,把吴钩看了,阑干拍遍,无人会、登临意。休说鲈鱼堪脍,尽西风季鹰归未?求田问舍,怕应羞见,刘郎才气。可惜流年,忧愁风雨,树犹如此。倩何人唤取,红巾翠袖,揾英雄泪?
55 | - 28.塞下秋来风景异。衡阳雁去无留意。四面边声连角起。千嶂里。长烟落日孤城闭。浊酒一杯家万里。燕然未勒归无计。羌管悠悠霜满地。人不寐。将军白发征夫泪。
56 | - 29.画屏天畔,梦回依约,十洲云水。手捻红笺寄人书,写无限、伤春事。别浦高楼曾漫倚,对江南千里。楼下分流水声中,有当日、凭高泪。
57 | - 30.态浓意远。眉颦笑浅。薄罗衣窄絮风软。鬓云欺翠卷。南园花树春光暖。红香径里榆钱满。欲上秋千又惊懒。且归休怕晚。
58 |
59 |
60 |
61 | - 1.《背影》
62 | - 2.我与父亲不相见已二年余了,我最不能忘记的是他的背影。那年冬天,祖母死了,父亲的差使也交卸了,正是祸不单行的日子,我从北京到徐州,打算跟着父亲奔丧回家。到徐州见着父亲,看见满院狼藉的东西,又想起祖母,不禁簌簌地流下眼泪。父亲说,“事已如此,不必难过,好在天无绝人之路!”
63 | - 3.回家变卖典质,父亲还了亏空;又借钱办了丧事。这些日子,家中光景很是惨淡,一半为了丧事,一半为了父亲赋闲。丧事完毕,父亲要到南京谋事,我也要回北京念书,我们便同行。
64 | - 4.到南京时,有朋友约去游逛,勾留了一日;
65 | - 5.第二日上午便须渡江到浦口,
66 | - 6.下午上车北去。
67 | - 7.父亲因为事忙,本已说定不送我,叫旅馆里一个熟识的茶房陪我同去。他再三嘱咐茶房,甚是仔细。但他终于不放心,怕茶房不妥帖;颇踌躇了一会。其实我那年已二十岁,北京已来往过两三次,是没有甚么要紧的了。他踌躇了一会,终于决定还是自己送我去。我两三回劝他不必去;他只说,“不要紧,他们去不好!”
68 | - 8.我们过了江,进了车站。我买票,他忙着照看行李。行李太多了,得向脚夫行些小费,才可过去。他便又忙着和他们讲价钱。我那时真是聪明过分,总觉他说话不大漂亮,非自己插嘴不可。但他终于讲定了价钱;就送我上车。他给我拣定了靠车门的一张椅子;我将他给我做的紫毛大衣铺好坐位。他嘱我路上小心,夜里警醒些,不要受凉。又嘱托茶房好好照应我。我心里暗笑他的迂;他们只认得钱,托他们直是白托!而且我这样大年纪的人,难道还不能料理自己么?唉,我现在想想,那时真是太聪明了!
69 | - 9.我说道,“爸爸,你走吧。”他望车外看了看,说,“我买几个橘子去。你就在此地,不要走动。”我看那边月台的栅栏外有几个卖东西的等着顾客。走到那边月台,须穿过铁道,须跳下去又爬上去。父亲是一个胖子,走过去自然要费事些。
70 | - 10.我本来要去的,他不肯,只好让他去。我看见他戴着黑布小帽,穿着黑布大马褂,深青布棉袍,蹒跚地走到铁道边,慢慢探身下去,尚不大难。可是他穿过铁道,要爬上那边月台,就不容易了。他用两手攀着上面,两脚再向上缩;他肥胖的身子向左微倾,显出努力的样子。这时我看见他的背影,我的泪很快地流下来了。
71 | - 11.我赶紧拭干了泪,怕他看见,也怕别人看见。
72 | - 12.我再向外看时,他已抱了朱红的橘子望回走了。过铁道时,他先将橘子散放在地上,自己慢慢爬下,再抱起橘子走。到这边时,我赶紧去搀他。他和我走到车上,将橘子一股脑儿放在我的皮大衣上。于是扑扑衣上的泥土,心里很轻松似的,过一会说,“我走了;到那边来信!”我望着他走出去。他走了几步,回过头看见我,说,“进去吧,里边没人。”等他的背影混入来来往往的人里,再找不着了,我便进来坐下,我的眼泪又来了。
73 | - 13.近几年来,父亲和我都是东奔西走,家中光景是一日不如一日。他少年出外谋生,独力支持,做了许多大事。那知老境却如此颓唐!他触目伤怀,自然情不能自已。情郁于中,自然要发之于外;家庭琐屑便往往触他之怒。他待我渐渐不同往日。但最近两年的不见,他终于忘却我的不好,只是惦记着我,惦记着我的儿子。我北来后,他写了一信给我,信中说道,“我身体平安,惟膀子疼痛利害,举箸提笔,诸多不便,大约大去之期不远矣。”我读到此处,在晶莹的泪光中,又看见那肥胖的,青布棉袍,黑布马褂的背影。唉!我不知何时再能与他相见!
74 | - 14.《荷塘月色》
75 | - 15.这几天心里颇不宁静。今晚在院子里坐着乘凉,忽然想起日日走过的荷塘,在这满月的光里,总该另有一番样子吧。月亮渐渐地升高了,墙外马路上孩子们的欢笑,已经听不见了;妻在屋里拍着闰儿,迷迷糊糊地哼着眠歌。我悄悄地披了大衫,带上门出去。
76 | - 16.沿着荷塘,是一条曲折的小煤屑路。这是一条幽僻的路;白天也少人走,夜晚更加寂寞。荷塘四面,长着许多树,蓊蓊郁郁的。路的一旁,是些杨柳,和一些不知道名字的树。没有月光的晚上,这路上阴森森的,有些怕人。今晚却很好,虽然月光也还是淡淡的。
77 | - 17.路上只我一个人,背着手踱着。这一片天地好像是我的;我也像超出了平常的自己,到了另一世界里。我爱热闹,也爱冷静;爱群居,也爱独处。像今晚上,一个人在这苍茫的月下,什么都可以想,什么都可以不想,便觉是个自由的人。白天里一定要做的事,一定要说的话,现在都可不理。这是独处的妙处,我且受用这无边的荷香月色好了。
78 | - 18.曲曲折折的荷塘上面,弥望的是田田的叶子。叶子出水很高,像亭亭的舞女的裙。层层的叶子中间,零星地点缀着些白花,有袅娜地开着的,有羞涩地打着朵儿的;正如一粒粒的明珠,又如碧天里的星星,又如刚出浴的美人。微风过处,送来缕缕清香,仿佛远处高楼上渺茫的歌声似的。这时候叶子与花也有一丝的颤动,像闪电般,霎时传过荷塘的那边去了。叶子本是肩并肩密密地挨着,这便宛然有了一道凝碧的波痕。叶子底下是脉脉的流水,遮住了,不能见一些颜色;而叶子却更见风致了。
79 | - 19.月光如流水一般,静静地泻在这一片叶子和花上。薄薄的青雾浮起在荷塘里。叶子和花仿佛在牛乳中洗过一样;又像笼着轻纱的梦。虽然是满月,天上却有一层淡淡的云,所以不能朗照;但我以为这恰是到了好处——酣眠固不可少,小睡也别有风味的。月光是隔了树照过来的,高处丛生的灌木,落下参差的斑驳的黑影,峭楞楞如鬼一般;弯弯的杨柳的稀疏的倩影,却又像是画在荷叶上。塘中的月色并不均匀;但光与影有着和谐的旋律,如梵婀玲上奏着的名曲。
80 | - 20.荷塘的四面,远远近近,高高低低都是树,而杨柳最多。这些树将一片荷塘重重围住;只在小路一旁,漏着几段空隙,像是特为月光留下的。树色一例是阴阴的,乍看像一团烟雾;但杨柳的丰姿,便在烟雾里也辨得出。树梢上隐隐约约的是一带远山,只有些大意罢了。树缝里也漏着一两点路灯光,没精打采的,是渴睡人的眼。这时候最热闹的,要数树上的蝉声与水里的蛙声;但热闹是它们的,我什么也没有。
81 | - 21.忽然想起采莲的事情来了。采莲是江南的旧俗,似乎很早就有,而六朝时为盛;从诗歌里可以约略知道。采莲的是少年的女子,她们是荡着小船,唱着艳歌去的。采莲人不用说很多,还有看采莲的人。那是一个热闹的季节,也是一个风流的季节。梁元帝《采莲赋》里说得好:
82 | - 22.于是妖童媛女,荡舟心许;鷁首徐回,兼传羽杯;欋将移而藻挂,船欲动而萍开。尔其纤腰束素,迁延顾步;夏始春余,叶嫩花初,恐沾裳而浅笑,畏倾船而敛裾。
83 | - 23.可见当时嬉游的光景了。这真是有趣的事,可惜我们现在早已无福消受了。
84 | - 24.于是又记起《西洲曲》里的句子:
85 | - 25.采莲南塘秋,莲花过人头;低头弄莲子,莲子清如水。今晚若有采莲人,这儿的莲花也算得“过人头”了;只不见一些流水的影子,是不行的。这令我到底惦着江南了。——这样想着,猛一抬头,不觉已是自己的门前;轻轻地推门进去,什么声息也没有,妻已睡熟好久了。
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/sample/src/test/java/cn/carbs/android/expandabletextview/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package cn.carbs.android.expandabletextview;
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() throws Exception {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':sample', ':library'
2 |
--------------------------------------------------------------------------------