├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ ├── colors.xml
│ │ │ │ ├── styles.xml
│ │ │ │ └── attrs.xml
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── layout
│ │ │ │ └── activity_main.xml
│ │ │ ├── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ │ └── drawable
│ │ │ │ └── ic_launcher_background.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── chenlittleping
│ │ │ │ └── filltextview
│ │ │ │ ├── MainActivity.kt
│ │ │ │ └── FillTextView.kt
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── chenlittleping
│ │ │ └── filltextview
│ │ │ └── ExampleUnitTest.kt
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── chenlittleping
│ │ └── filltextview
│ │ └── ExampleInstrumentedTest.kt
├── proguard-rules.pro
└── build.gradle
├── settings.gradle
├── gif
└── demo.gif
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .idea
├── codeStyles
│ ├── codeStyleConfig.xml
│ └── Project.xml
├── markdown-navigator
│ └── profiles_settings.xml
├── runConfigurations.xml
├── gradle.xml
└── misc.xml
├── .gitignore
├── README.md
├── gradle.properties
├── gradlew.bat
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------
/gif/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenLittlePing/FillTextView/HEAD/gif/demo.gif
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | FillTextView
3 |
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenLittlePing/FillTextView/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenLittlePing/FillTextView/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenLittlePing/FillTextView/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenLittlePing/FillTextView/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenLittlePing/FillTextView/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenLittlePing/FillTextView/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenLittlePing/FillTextView/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenLittlePing/FillTextView/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenLittlePing/FillTextView/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenLittlePing/FillTextView/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenLittlePing/FillTextView/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/markdown-navigator/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches/build_file_checksums.ser
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | .DS_Store
9 | /build
10 | /captures
11 | .externalNativeBuild
12 | /.idea
13 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | 一个Android自定义的填空题控件,类似"学习强国"App中的填空题。
3 |
4 | 详情请查看文章:[一步步教你如何定制一个「填空题」控件(仿学习强国填空题控件)](https://www.jianshu.com/p/3cf87168ed49)
5 |
6 | 
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #008577
4 | #00574B
5 | #D81B60
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/test/java/com/chenlittleping/filltextview/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.chenlittleping.filltextview
2 |
3 | import org.junit.Assert.assertEquals
4 | import org.junit.Test
5 |
6 | /**
7 | * Example local unit test, which will execute on the development machine (host).
8 | *
9 | * See [testing documentation](http://d.android.com/tools/testing).
10 | */
11 | class ExampleUnitTest {
12 | @Test
13 | fun addition_isCorrect() {
14 | assertEquals(4, 2 + 2)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chenlittleping/filltextview/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.chenlittleping.filltextview
2 |
3 | import android.os.Bundle
4 | import android.support.v7.app.AppCompatActivity
5 | import android.view.View
6 | import kotlinx.android.synthetic.main.activity_main.*
7 |
8 | class MainActivity : AppCompatActivity() {
9 |
10 | override fun onCreate(savedInstanceState: Bundle?) {
11 | super.onCreate(savedInstanceState)
12 | setContentView(R.layout.activity_main)
13 | }
14 |
15 | fun clickBtn(view: View) {
16 | var t = ""
17 | for (text in fillText.getFillTexts()) {
18 | t += text
19 | t +=","
20 | }
21 | tv_fills.text = t.subSequence(0, t.length - 1)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/chenlittleping/filltextview/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.chenlittleping.filltextview
2 |
3 | import android.support.test.InstrumentationRegistry
4 | import android.support.test.runner.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getTargetContext()
22 | assertEquals("com.chenlittleping.filltextview", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/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 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/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 | # Kotlin code style for this project: "official" or "obsolete":
15 | kotlin.code.style=official
16 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | apply plugin: 'kotlin-android'
4 |
5 | apply plugin: 'kotlin-android-extensions'
6 |
7 | android {
8 | compileSdkVersion 28
9 | defaultConfig {
10 | applicationId "com.chenlittleping.filltextview"
11 | minSdkVersion 19
12 | targetSdkVersion 28
13 | versionCode 1
14 | versionName "1.0"
15 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
16 | }
17 | buildTypes {
18 | release {
19 | minifyEnabled false
20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
21 | }
22 | }
23 | }
24 |
25 | dependencies {
26 | implementation fileTree(dir: 'libs', include: ['*.jar'])
27 | implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
28 | implementation 'com.android.support:appcompat-v7:28.0.0'
29 | implementation 'com.android.support.constraint:constraint-layout:1.1.3'
30 | testImplementation 'junit:junit:4.12'
31 | androidTestImplementation 'com.android.support.test:runner:1.0.2'
32 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
33 | }
34 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
21 |
22 |
30 |
31 |
38 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
10 |
12 |
14 |
16 |
18 |
20 |
22 |
24 |
26 |
28 |
30 |
32 |
34 |
36 |
38 |
40 |
42 |
44 |
46 |
48 |
50 |
52 |
54 |
56 |
58 |
60 |
62 |
64 |
66 |
68 |
70 |
72 |
74 |
75 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chenlittleping/filltextview/FillTextView.kt:
--------------------------------------------------------------------------------
1 | package com.chenlittleping.filltextview
2 |
3 | import android.content.Context
4 | import android.graphics.Canvas
5 | import android.graphics.Color
6 | import android.graphics.Paint
7 | import android.graphics.Rect
8 | import android.os.Handler
9 | import android.text.InputType
10 | import android.util.AttributeSet
11 | import android.view.KeyEvent
12 | import android.view.KeyEvent.KEYCODE_DEL
13 | import android.view.MotionEvent
14 | import android.view.View
15 | import android.view.inputmethod.BaseInputConnection
16 | import android.view.inputmethod.EditorInfo
17 | import android.view.inputmethod.InputConnection
18 | import android.view.inputmethod.InputMethodManager
19 | import java.util.regex.Pattern
20 |
21 |
22 | /**
23 | * 文本填空控件
24 | *
25 | * @author ChenLittlePing (562818444@qq.com)
26 | * @Datetime 2019-04-28 15:02
27 | *
28 | */
29 | class FillTextView: View, MyInputConnection.InputListener, View.OnKeyListener {
30 |
31 | //编辑字段标记
32 | private var EDIT_TAG = ""
33 |
34 | //编辑字段替换
35 | private var EDIT_REPLACEMENT = "【 】"
36 |
37 | //可编辑空白
38 | private val BLANKS = " "
39 |
40 | //可编辑开始符
41 | private var mEditStartTag = "【"
42 |
43 | //可编辑结束符
44 | private var mEditEndTag = "】"
45 |
46 | //文本
47 | private var mText = StringBuffer()
48 |
49 | //存放文字段的列表,根据分割为多个字段
50 | private var mTextList = arrayListOf()
51 |
52 | //正在输入的字段
53 | private var mEditingText: AText? = null
54 |
55 | //当前正在编辑的文本行数
56 | private var mEditTextRow = 1
57 |
58 | //光标[0]:x坐标,[1]:文字的基准线
59 | private var mCursor = arrayOf(-1f, -1f)
60 |
61 | //光标所在文字索引
62 | private var mCursorIndex = 0
63 |
64 | //光标闪烁标志
65 | private var mHideCursor = true
66 |
67 | //控件宽度
68 | private var mWidth = 0
69 |
70 | //文字画笔
71 | private val mNormalPaint = Paint()
72 |
73 | //普通文字颜色
74 | private var mNormalColor = Color.BLACK
75 |
76 | //文字画笔
77 | private val mFillPaint = Paint()
78 |
79 | //填写文字颜色
80 | private var mFillColor = Color.BLACK
81 |
82 | //光标画笔
83 | private val mCursorPain = Paint()
84 |
85 | //光标宽度1dp
86 | private var mCursorWidth = 1f
87 |
88 | //一个汉字的宽度
89 | private var mOneWordWidth = 0f
90 |
91 | //一行最大的文字数
92 | private var mMaxSizeOneLine = 0
93 |
94 | //字体大小
95 | private var mTextSize = sp2px(16f).toFloat()
96 |
97 | //当前绘制到第几行
98 | private var mCurDrawRow = 1
99 |
100 | //获取文字的起始位置
101 | private var mStartIndex = 0
102 |
103 | //获取文字的结束位置
104 | private var mEndIndex = 0
105 |
106 | //存放每行的文字,用于计算文字长度
107 | private var mOneRowText = StringBuffer()
108 |
109 | //一行字包含的字段:普通字段,可编辑字段
110 | private var mOneRowTexts = arrayListOf()
111 |
112 | //默认行距2dp,也是最小行距(用户设置的行距在此基础上叠加,即:2 + cst)
113 | private var mRowSpace = dp2px(2f).toFloat()
114 |
115 | //是否显示下划线
116 | private var mUnderlineVisible = false
117 |
118 | //下划线画笔
119 | private val mUnderlinePain = Paint().apply {
120 | strokeWidth = dp2px(1f).toFloat()
121 | color = Color.BLACK
122 | isAntiAlias = true
123 | }
124 |
125 | constructor(context: Context): super(context) {
126 | init()
127 | }
128 |
129 | constructor(context: Context, attrs: AttributeSet): super(context, attrs) {
130 | getAttrs(attrs)
131 | init()
132 | }
133 |
134 | constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int): super(context, attrs, defStyleAttr) {
135 | getAttrs(attrs)
136 | init()
137 | }
138 |
139 | private fun getAttrs(attrs: AttributeSet) {
140 | val ta = context.obtainStyledAttributes(attrs, R.styleable.filled_text)
141 | mTextSize = ta.getDimension(R.styleable.filled_text_fillTextSize, mTextSize)
142 | mText = mText.append(ta.getText(R.styleable.filled_text_filledText)?: "")
143 | mNormalColor = ta.getColor(R.styleable.filled_text_normalColor, Color.BLACK)
144 | mFillColor = ta.getColor(R.styleable.filled_text_fillColor, Color.BLACK)
145 | mRowSpace += ta.getDimension(R.styleable.filled_text_rowSpace, 0f)
146 | ta.recycle()
147 | }
148 |
149 | private fun init() {
150 | isFocusable = true
151 | initCursorPaint()
152 | initTextPaint()
153 | initFillPaint()
154 | splitTexts()
155 | initHandler()
156 | setOnKeyListener(this)
157 | }
158 |
159 | /**
160 | * 初始化光标画笔
161 | */
162 | private fun initCursorPaint() {
163 | mCursorWidth = dp2px(mCursorWidth).toFloat()
164 | mCursorPain.strokeWidth = mCursorWidth
165 | mCursorPain.color = mFillColor
166 | mCursorPain.isAntiAlias = true
167 | }
168 |
169 | /**
170 | * 初始化文字画笔
171 | */
172 | private fun initTextPaint() {
173 | // mTextSize = sp2px(mTextSize).toFloat()
174 | // mRowSpace = dp2px(mRowSpace).toFloat()
175 |
176 | mNormalPaint.color = mNormalColor
177 | mNormalPaint.textSize = mTextSize
178 | mNormalPaint.isAntiAlias = true
179 |
180 | mOneWordWidth = measureTextLength("测")
181 | }
182 |
183 | private fun initFillPaint() {
184 | mFillPaint.color = mFillColor
185 | mFillPaint.textSize = mTextSize
186 | mFillPaint.isAntiAlias = true
187 | }
188 |
189 | private fun dp2px(dp: Float): Int {
190 | val density = resources.displayMetrics.density
191 | return (dp * density + 0.5).toInt()
192 | }
193 |
194 | private fun sp2px(sp: Float): Int {
195 | val density = resources.displayMetrics.scaledDensity
196 | return (sp * density + 0.5).toInt()
197 | }
198 |
199 | /**
200 | * 拆分文字,普通文字和可编辑文字
201 | */
202 | private fun splitTexts() {
203 | mTextList.clear()
204 | val texts = mText.split(EDIT_TAG)
205 | for (i in 0 until texts.size - 1) {
206 | var text = texts[i]
207 | if (i > 0) {
208 | text = mEditEndTag + text
209 | }
210 | text += mEditStartTag
211 | mTextList.add(AText(text))
212 | mTextList.add(AText(BLANKS, true))
213 | }
214 | mTextList.add(AText(mEditEndTag + texts[texts.size - 1]))
215 | }
216 |
217 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
218 | super.onMeasure(widthMeasureSpec, heightMeasureSpec)
219 | val widthMode = MeasureSpec.getMode(widthMeasureSpec)
220 | val widthSize = MeasureSpec.getSize(widthMeasureSpec)
221 | val heightMode = MeasureSpec.getMode(heightMeasureSpec)
222 | val heightSize = MeasureSpec.getSize(heightMeasureSpec)
223 |
224 | var width = widthSize
225 | var height = heightSize
226 |
227 | val realText = StringBuffer()
228 | for (aText in mTextList) {
229 | realText.append(aText.text)
230 | }
231 | when(widthMode) {
232 | MeasureSpec.EXACTLY -> {
233 | width = widthSize
234 | //用户指定宽高
235 | mWidth = width
236 | mMaxSizeOneLine = (width / mOneWordWidth).toInt()
237 | }
238 | MeasureSpec.UNSPECIFIED, MeasureSpec.AT_MOST -> {
239 | //绘制宽高为文字最大长度,如果长度超过,则使用父布局可用的最大长度
240 | width = if (mText.isEmpty()) 0
241 | else Math.min(widthSize, measureTextLength(realText.toString()).toInt())
242 |
243 | //设配最大宽高
244 | mWidth = widthSize
245 | mMaxSizeOneLine = (widthSize / mOneWordWidth).toInt()
246 | }
247 | }
248 |
249 | when(heightMode) {
250 | MeasureSpec.EXACTLY -> height = heightSize
251 | MeasureSpec.UNSPECIFIED, MeasureSpec.AT_MOST ->
252 | height = if (realText.isEmpty()) 0
253 | else //其中mRowSpace + mNormalPaint.fontMetrics.descent是最后一行距离底部的间距
254 | (getRowHeight() * (mCurDrawRow - 1) + mRowSpace + mNormalPaint.fontMetrics.descent).toInt()
255 | }
256 | setMeasuredDimension(width, height)
257 | }
258 |
259 | override fun draw(canvas: Canvas) {
260 | clear()
261 | canvas.save()
262 | mStartIndex = 0
263 | mEndIndex = mMaxSizeOneLine
264 | for (i in 0 until mTextList.size) {
265 | val aText = mTextList[i]
266 | val text = aText.text
267 | while (true) {
268 | if (mEndIndex > text.length) {
269 | mEndIndex = text.length
270 | }
271 | addEditStartPos(aText) //记录编辑初始位置
272 |
273 | val cs = text.subSequence(mStartIndex, mEndIndex)
274 | mOneRowTexts.add(AText(cs.toString(), aText.isFill))
275 | mOneRowText.append(cs)
276 |
277 | val textWidth = measureTextLength(mOneRowText.toString())
278 | if (textWidth <= mWidth) {
279 | val left = mWidth - textWidth
280 | val textCount = left / mOneWordWidth
281 | if (mEndIndex < text.length) {
282 | mStartIndex = mEndIndex
283 | mEndIndex += textCount.toInt()
284 | if (mStartIndex == mEndIndex) {
285 | val one = measureTextLength(text.substring(mEndIndex, mEndIndex + 1))
286 | if (one + textWidth < mWidth) { //可以放多一个字
287 | mEndIndex++
288 | } else {
289 | //绘制文字
290 | addEditEndPos(aText)
291 | drawOneRow(canvas)
292 | addEditStartPosFromZero(aText, mStartIndex) //编辑的段落可能进入下一行
293 | }
294 | }
295 | } else { //进入下一段文字
296 | addEditEndPos(aText) //记录编辑结束位置
297 | if (i < mTextList.size - 1) {
298 | mStartIndex = 0
299 | mEndIndex = textCount.toInt()
300 | if (mStartIndex == mEndIndex) {
301 | val one = measureTextLength(mTextList[i + 1].text.substring(0, 1))
302 | if (one + textWidth < mWidth) { //可以放多一个字
303 | mEndIndex = 1 //只读下一段文字第一个字
304 | } else {
305 | //绘制文字
306 | drawOneRow(canvas)
307 | }
308 | }
309 | } else {
310 | //绘制文字
311 | drawOneRow(canvas)
312 | }
313 | break
314 | }
315 | } else {
316 | //绘制文字
317 | drawOneRow(canvas)
318 | }
319 | }
320 | }
321 | if (isFocused) {
322 | drawCursor(canvas)
323 | }
324 | super.draw(canvas)
325 | canvas.restore()
326 | }
327 |
328 | private var mHandler: Handler? = null
329 |
330 | /**
331 | * 光标闪烁定时
332 | */
333 | private fun initHandler() {
334 | mHandler = Handler(Handler.Callback {
335 | mHideCursor = !mHideCursor
336 | mHandler!!.sendEmptyMessageDelayed(1, 500)
337 | invalidate()
338 | true
339 | })
340 | mHandler!!.sendEmptyMessageDelayed(1, 500)
341 | }
342 |
343 | override fun onFocusChanged(gainFocus: Boolean, direction: Int, previouslyFocusedRect: Rect?) {
344 | super.onFocusChanged(gainFocus, direction, previouslyFocusedRect)
345 | if (gainFocus) {
346 | mHandler?.removeMessages(1)
347 | mHandler?.sendEmptyMessageDelayed(1, 500)
348 | } else {//失去焦点时,停止刷新光标
349 | mHandler?.removeMessages(1)
350 | }
351 | }
352 |
353 | /**
354 | * 清除过期状态
355 | */
356 | private fun clear() {
357 | mCurDrawRow = 1
358 | mStartIndex = 0
359 | mEndIndex = 0
360 | mOneRowText.delete(0, mOneRowText.length)
361 | mOneRowTexts.clear()
362 | mEditingText?.posInfo?.clear()
363 | }
364 |
365 | /**
366 | * 绘制一行文字
367 | */
368 | private fun drawOneRow(canvas: Canvas) {
369 | //drawText中的y坐标为文字基线
370 | // canvas.drawText(mOneRowText.toString(), 0f, getRowHeight()*mCurDrawRow, mNormalPaint)
371 | val fm = mNormalPaint.fontMetrics //文字基准线问题
372 | var x = 0f
373 | for (aText in mOneRowTexts) {
374 | canvas.drawText(aText.text, x, getRowHeight()*mCurDrawRow,
375 | if (aText.isFill) mFillPaint else mNormalPaint)
376 |
377 | val lineStartX = x
378 | x += measureTextLength(aText.text)
379 |
380 | if (aText.isFill && mUnderlineVisible) {
381 | canvas.drawLine(lineStartX, getRowHeight()*mCurDrawRow + fm.descent,
382 | x, (getRowHeight()*mCurDrawRow + fm.descent), mUnderlinePain)
383 | }
384 | }
385 |
386 | mCurDrawRow++
387 | mEndIndex += mMaxSizeOneLine
388 | mOneRowText.delete(0, mOneRowText.length)
389 | mOneRowTexts.clear()
390 | requestLayout()
391 | }
392 |
393 | /**
394 | * 绘制光标
395 | */
396 | private fun drawCursor(canvas: Canvas) {
397 | if (mHideCursor) {
398 | mCursorPain.alpha = 0
399 | } else {
400 | mCursorPain.alpha = 255
401 | }
402 |
403 | if (mCursor[0] >= 0 && mCursor[1] >= 0) {
404 | if (mEditingText?.text == BLANKS && //光标可能需要换到上一行
405 | (mCursor[0] == 0f || (mCursor[0] == mCursorWidth && mEditingText!!.posInfo.size > 1))) {
406 | if (mEditingText!!.posInfo.size > 1) {
407 | mEditTextRow = mEditingText!!.getStartPos() //得到可编辑字段最上面一行的起始位置
408 | val posInfo = mEditingText!!.posInfo[mEditTextRow]
409 | mCursor[0] = posInfo!!.rect.left.toFloat()
410 | mCursor[1] = posInfo!!.rect.bottom.toFloat()
411 | if (mCursor[0] <= 0) mCursor[0] = mCursorWidth //矫正光标X轴坐标
412 | }
413 | }
414 |
415 | val fm = mNormalPaint.fontMetrics //文字基准线问题
416 | canvas.drawLine(mCursor[0], mCursor[1] + fm.ascent,
417 | mCursor[0], (mCursor[1] + fm.descent), mCursorPain)
418 | }
419 | }
420 |
421 | /**
422 | * 添加编辑字段起始位置
423 | */
424 | private fun addEditStartPos(aText: AText) {
425 | if (aText.isFill && mStartIndex == 0) {
426 | aText.posInfo.clear()
427 | val width = measureTextLength(mOneRowText.toString()).toInt()
428 | val rect = Rect(width, (getRowHeight()*(mCurDrawRow - 1) + mRowSpace/*加上行距*/).toInt(), 0, 0)
429 | val info = EditPosInfo(mStartIndex, rect)
430 | aText.posInfo[mCurDrawRow] = info
431 | }
432 | }
433 |
434 | /**
435 | * 添加编辑字段起始位置(换行的情况)
436 | */
437 | private fun addEditStartPosFromZero(aText: AText, index: Int) {
438 | if (aText.isFill) {
439 | val rect = Rect(0, (getRowHeight()*(mCurDrawRow - 1) + mRowSpace/*加上行距*/).toInt(), 0, 0)
440 | val info = EditPosInfo(index, rect)
441 | aText.posInfo[mCurDrawRow] = info
442 | }
443 | }
444 |
445 | /**
446 | * 添加编辑字段结束位置
447 | */
448 | private fun addEditEndPos(aText: AText) {
449 | if (aText.isFill) {
450 | val width = measureTextLength(mOneRowText.toString())
451 | aText.posInfo[mCurDrawRow]?.rect?.right = width.toInt()
452 | aText.posInfo[mCurDrawRow]?.rect?.bottom = (getRowHeight()*mCurDrawRow).toInt()
453 | }
454 | }
455 |
456 | /**
457 | * 计算文字长度:px
458 | */
459 | private fun measureTextLength(text: String): Float {
460 | return mNormalPaint.measureText(text)
461 | }
462 |
463 | /**
464 | * 获取一行高度
465 | */
466 | private fun getRowHeight(): Float {
467 | return mTextSize + mRowSpace
468 | }
469 |
470 | override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
471 | super.onWindowFocusChanged(hasWindowFocus)
472 | if (!hasWindowFocus) {
473 | hideInput()
474 | }
475 | }
476 |
477 | /**
478 | * 隐藏输入法
479 | */
480 | private fun hideInput() {
481 | val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
482 | imm.hideSoftInputFromWindow(windowToken, 0)
483 | }
484 |
485 | override fun onTouchEvent(event: MotionEvent): Boolean {
486 | isFocusableInTouchMode = true
487 | requestFocus()
488 | when (event.action) {
489 | MotionEvent.ACTION_DOWN -> {
490 | if (touchCollision(event)) {
491 | isFocusableInTouchMode = true //important
492 | isFocusable = true
493 | requestFocus()
494 | try {
495 | val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
496 | imm.showSoftInput(this, InputMethodManager.RESULT_SHOWN)
497 | imm.restartInput(this)
498 | } catch (ignore: Exception) {
499 | }
500 | return true
501 | }
502 | }
503 | }
504 |
505 | return super.onTouchEvent(event)
506 | }
507 |
508 | /**
509 | * 检测点击碰撞
510 | */
511 | private fun touchCollision(event: MotionEvent): Boolean {
512 | for (aText in mTextList) {
513 | if (aText.isFill) {
514 | for ((row, posInfo) in aText.posInfo) {
515 | if (event.x > posInfo.rect.left && event.x < posInfo.rect.right &&
516 | event.y > posInfo.rect.top && event.y < posInfo.rect.bottom) {
517 | mEditTextRow = row
518 | if (aText.text == BLANKS) {
519 | val firstRow = aText.getStartPos()
520 | if (firstRow >= 0) { //可能存在换行
521 | mEditTextRow = firstRow
522 | }
523 | }
524 | mEditingText = aText
525 | calculateCursorPos(event, aText.posInfo[mEditTextRow]!!, aText.text)
526 | return true
527 | }
528 | }
529 | }
530 | }
531 | return false
532 | }
533 |
534 | /**
535 | * 计算光标位置
536 | */
537 | private fun calculateCursorPos(event: MotionEvent, posInfo: EditPosInfo, text: String) {
538 | val eX = event.x
539 | var innerWidth = eX - posInfo.rect.left
540 | var nWord = (innerWidth / mOneWordWidth).toInt()
541 | var wordsWidth = 0
542 | if (nWord <= 0) nWord = 1
543 | if (text == BLANKS) {
544 | mCursor[0] = posInfo.rect.left.toFloat()
545 | mCursor[1] = posInfo.rect.bottom.toFloat()
546 | mCursorIndex = 0
547 | } else {
548 | //循环计算,直到最后一个真正超过显示范围的文字(因为汉字和英文数字占位不一样,这里以汉字作为初始占位)
549 | do {
550 | wordsWidth = measureTextLength(text.substring(posInfo.index, posInfo.index + nWord)).toInt()
551 | nWord++
552 | } while (wordsWidth < innerWidth && posInfo.index + nWord <= text.length)
553 | mCursorIndex = posInfo.index + nWord - 1
554 | val leftWidth = wordsWidth - innerWidth //计算点击位置是否超过所点击文字的一半
555 | if (leftWidth > measureTextLength(text.substring(mCursorIndex - 1, mCursorIndex))/2) {
556 | mCursorIndex--
557 | }
558 |
559 | mCursor[0] = posInfo.rect.left + measureTextLength(text.substring(posInfo.index, mCursorIndex))
560 | mCursor[1] = posInfo.rect.bottom.toFloat()
561 | }
562 | invalidate()
563 | }
564 |
565 | /**
566 | * 键盘输入
567 | */
568 | override fun onTextInput(text: CharSequence) {
569 | if (mEditingText != null) {
570 | val filledText = StringBuffer(mEditingText!!.text.replace(BLANKS, ""))
571 | if (filledText.isEmpty()) {
572 | filledText.append(text)
573 | mCursorIndex = text.length
574 | } else {
575 | filledText.insert(mCursorIndex, text)
576 | mCursorIndex += text.length
577 | }
578 | mEditingText!!.text = filledText.toString()
579 | if (mCursor[0] + measureTextLength(text.toString()) > mWidth) {//计算实际可以放多少字
580 | var restCount = ((mWidth - mCursor[0])/mOneWordWidth).toInt()
581 | var realWidth = mCursor[0] + measureTextLength(text.substring(0, restCount))
582 |
583 | //循环计算,直到最后一个真正超过显示范围的文字(因为汉字和英文数字占位不一样,这里以汉字作为初始占位)
584 | while (realWidth <= mWidth && restCount < text.length) {
585 | restCount++
586 | realWidth = mCursor[0] + measureTextLength(text.substring(0, restCount))
587 | }
588 | mEditTextRow += ((mCursor[0] + measureTextLength(text.toString()))/mWidth).toInt()
589 | if (mEditTextRow < 1) mEditTextRow = 1
590 | val realCount = if (restCount - 1 > 0) restCount -1 else 0
591 | mCursor[0] = measureTextLength(text.substring(realCount, text.length))
592 | mCursor[1] = getRowHeight() * (mEditTextRow)
593 | } else {
594 | mCursor[0] += measureTextLength(text.toString())
595 | }
596 | invalidate()
597 | }
598 | }
599 |
600 | override fun onCheckIsTextEditor(): Boolean {
601 | return true
602 | }
603 |
604 | override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection {
605 | outAttrs.inputType = InputType.TYPE_CLASS_TEXT
606 | outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE
607 | return MyInputConnection(this, false, this)
608 | }
609 |
610 | override fun onKey(view: View?, keyCode: Int, keyEvent: KeyEvent): Boolean {
611 | if (keyCode == KeyEvent.KEYCODE_DEL &&
612 | keyEvent.action == KeyEvent.ACTION_DOWN) {
613 | onDeleteWord()
614 | return true
615 | }
616 | return false
617 | }
618 |
619 | override fun onDeleteWord() {
620 | if (mEditingText != null) {
621 | val text = StringBuffer(mEditingText?.text)
622 | if (!text.isNullOrEmpty() &&
623 | text.toString() != BLANKS &&
624 | mCursorIndex >= 1) {
625 | var cursorPos = (mCursor[0] - measureTextLength(text.substring(mCursorIndex - 1, mCursorIndex))).toInt()
626 | if (cursorPos > 0 ||
627 | (cursorPos == 0 && mEditingText!!.posInfo.size == 1)) {//光标仍然在同一行
628 | mCursor[0] = cursorPos.toFloat()
629 | } else { //光标回到上一行
630 | mEditTextRow--
631 | val posInfo = mEditingText!!.posInfo[mEditTextRow]!!
632 | mCursor[0] = posInfo.rect.left + measureTextLength(text.substring(posInfo.index, mCursorIndex - 1))
633 | mCursor[1] = getRowHeight() * (mEditTextRow)
634 | }
635 | mEditingText?.text = text.replace(mCursorIndex - 1, mCursorIndex, "").toString()
636 | mCursorIndex--
637 |
638 | if (mEditingText?.text?.length?:0 <= 0) {
639 | if (text.toString() != BLANKS) {
640 | mEditingText?.text = BLANKS
641 | mCursorIndex = 1
642 | val firstRow = mEditingText!!.getStartPos()
643 | if (firstRow > 0) {//可能存在换行
644 | mEditTextRow = firstRow
645 | }
646 | mCursor[0] = mEditingText!!.posInfo[mEditTextRow]!!.rect.left.toFloat()
647 | mCursor[1] = getRowHeight() * (mEditTextRow)
648 | }
649 | }
650 |
651 | invalidate()
652 | }
653 | }
654 | }
655 |
656 | /**
657 | * 设置文本
658 | */
659 | fun setText(text: String) {
660 | mText = StringBuffer(text)
661 | splitTexts()
662 | invalidate()
663 | }
664 |
665 | /**
666 | * 设置字体大小,单位sp
667 | */
668 | fun setTextSize(sp: Float) {
669 | mTextSize = sp2px(sp).toFloat()
670 | initTextPaint()
671 | initFillPaint()
672 | invalidate()
673 | }
674 |
675 | /**
676 | * 设置行距,单位dp
677 | */
678 | fun setRowSpace(dp: Float) {
679 | mRowSpace = dp2px(2+dp).toFloat()
680 | invalidate()
681 | }
682 |
683 | /**
684 | * 设置可编辑标记的开始和结束符
685 | */
686 | fun setEditTag(startTag: String, endTag: String) {
687 | mEditStartTag = startTag
688 | mEditEndTag = endTag
689 | invalidate()
690 | }
691 |
692 | /**
693 | * 设置是否显示可编辑字段下划线
694 | */
695 | fun displayUnderline(visible: Boolean) {
696 | mUnderlineVisible = visible
697 | }
698 |
699 | /**
700 | * 设置下划线颜色
701 | */
702 | fun setUnderlineColor(color: Int) {
703 | mUnderlinePain.color = color
704 | invalidate()
705 | }
706 |
707 | /**
708 | * 获取填写的文本内容
709 | */
710 | fun getFillTexts(): List {
711 | val list = arrayListOf()
712 | for (value in mTextList) {
713 | if (value.isFill) {
714 | list.add(value.text)
715 | }
716 | }
717 | return list
718 | }
719 | }
720 |
721 | internal class MyInputConnection(targetView: View, fullEditor: Boolean, private val mListener: InputListener) : BaseInputConnection(targetView, fullEditor) {
722 | override fun commitText(text: CharSequence, newCursorPosition: Int): Boolean {
723 | if (!isEmoji(text)) { //过滤emoji表情
724 | mListener.onTextInput(text)
725 | }
726 | return super.commitText(text, newCursorPosition)
727 | }
728 |
729 | private fun isEmoji(string: CharSequence): Boolean {
730 | //过滤Emoji表情
731 | val p = Pattern.compile("[^\\u0000-\\uFFFF]")
732 | //过滤Emoji表情和颜文字
733 | //Pattern p = Pattern.compile("[\\ud83c\\udc00-\\ud83c\\udfff]|[\\ud83d\\udc00-\\ud83d\\udfff]|[\\u2600-\\u27ff]|[\\ud83e\\udd00-\\ud83e\\uddff]|[\\u2300-\\u23ff]|[\\u2500-\\u25ff]|[\\u2100-\\u21ff]|[\\u0000-\\u00ff]|[\\u2b00-\\u2bff]|[\\u2d06]|[\\u3030]")
734 | val m = p.matcher(string)
735 | return m.find()
736 | }
737 |
738 | override fun deleteSurroundingText(beforeLength: Int, afterLength: Int): Boolean {
739 | //软键盘的删除键 DEL 无法直接监听,自己发送del事件
740 | return if (beforeLength == 1 && afterLength == 0) {
741 | super.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KEYCODE_DEL)) && super.sendKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL))
742 | } else super.deleteSurroundingText(beforeLength, afterLength)
743 | }
744 |
745 | interface InputListener {
746 | fun onTextInput(text: CharSequence)
747 | fun onDeleteWord()
748 | }
749 | }
750 |
751 | /**
752 | * 文字段落
753 | *
754 | * @author ChenLittlePing (562818444@qq.com)
755 | * @Datetime 2019-04-29 09:27
756 | *
757 | */
758 | internal class AText(text: String, isFill: Boolean = false) {
759 | //段落文字
760 | var text: String = text
761 |
762 | //是否为填写字段
763 | var isFill = isFill
764 |
765 | //文本位置信息<行,文本框>
766 | var posInfo: MutableMap = mutableMapOf()
767 |
768 | fun getStartPos(): Int {
769 | if (posInfo.isEmpty()) return -1
770 | var firstRow = Int.MAX_VALUE
771 | for ((row, _) in posInfo) {
772 | if (firstRow > row) {
773 | firstRow = row
774 | }
775 | }
776 | return firstRow
777 | }
778 | }
779 |
780 | data class EditPosInfo(var index: Int,
781 | var rect: Rect)
--------------------------------------------------------------------------------