├── .gitignore ├── .idea ├── .name ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── encodings.xml ├── gradle.xml ├── misc.xml ├── modules.xml ├── scopes │ └── scope_settings.xml └── vcs.xml ├── Library ├── .gitignore ├── Library.iml ├── build.gradle ├── build_for_jcenter_publish.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── dk_exp │ │ └── library │ │ └── ApplicationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── dk │ │ └── view │ │ └── patheffect │ │ ├── MatchPath.java │ │ └── PathTextView.java │ └── res │ └── values │ ├── strings.xml │ └── styles.xml ├── PathEffectTextView.iml ├── README.md ├── app ├── .gitignore ├── app.iml ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── dk_exp │ │ └── patheffecttextview │ │ └── ApplicationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── dk │ │ └── view │ │ └── demo │ │ └── patheffect │ │ └── MainActivity.java │ └── res │ ├── layout │ └── activity_main.xml │ ├── menu │ └── menu_main.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── path1.gif ├── path2.gif ├── path3.gif ├── path4.gif └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # files for the dex VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # generated files 12 | bin/ 13 | gen/ 14 | 15 | # Local configuration file (sdk path, etc) 16 | local.properties 17 | 18 | # Eclipse project files 19 | .classpath 20 | .project 21 | 22 | # Proguard folder generated by Eclipse 23 | proguard/ 24 | 25 | # Intellij project files 26 | *.iml 27 | *.ipr 28 | *.iws 29 | .idea/ 30 | 31 | .gradle/ 32 | gradle/ 33 | gradlew 34 | gradlew.bat 35 | build/ 36 | .DS_Store 37 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | PathEffectTextView -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 1.7 38 | 39 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/scopes/scope_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Library/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /Library/Library.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /Library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 21 5 | buildToolsVersion "21.1.2" 6 | 7 | defaultConfig { 8 | minSdkVersion 15 9 | targetSdkVersion 21 10 | versionCode 1 11 | versionName "1.0" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | compile fileTree(dir: 'libs', include: ['*.jar']) 23 | compile 'com.android.support:appcompat-v7:21.+' 24 | } 25 | -------------------------------------------------------------------------------- /Library/build_for_jcenter_publish.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | } 5 | dependencies { 6 | classpath 'com.android.tools.build:gradle:1.3.0' 7 | classpath 'com.github.dcendents:android-maven-plugin:1.2' 8 | classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:1.0" 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | apply plugin: 'com.android.library' 16 | apply plugin: 'com.github.dcendents.android-maven' 17 | apply plugin: 'com.jfrog.bintray' 18 | 19 | 20 | version = "0.1.1" 21 | 22 | android { 23 | compileSdkVersion 23 24 | buildToolsVersion "23.0.0" 25 | resourcePrefix "PathEffectTextView" 26 | 27 | defaultConfig { 28 | minSdkVersion 15 29 | targetSdkVersion 23 30 | versionCode 21 31 | versionName "0.1.1" 32 | } 33 | buildTypes { 34 | release { 35 | minifyEnabled false 36 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 37 | } 38 | } 39 | } 40 | 41 | def siteUrl = "https://github.com/dkmeteor/PathEffectTextView" 42 | def gitUrl = "https://github.com/dkmeteor/PathEffectTextView.git" 43 | 44 | //填写唯一包名 45 | group = "com.dk.view.patheffect" 46 | 47 | install { 48 | repositories.mavenInstaller { 49 | // This generates POM.xml with proper paramters 50 | pom { 51 | project { 52 | packaging 'aar' 53 | 54 | //添加项目描述 55 | name 'Add path effect on text' 56 | url siteUrl 57 | 58 | //设置开源证书信息 59 | licenses { 60 | license { 61 | name 'The Apache Software License, Version 2.0' 62 | url 'http://www.apache.org/licenses/LICENSE-2.0.txt' 63 | } 64 | } 65 | //添加开发者信息 66 | developers { 67 | developer { 68 | id 'dkmeteor' 69 | name 'Dean' 70 | email '93440331@qq.com' 71 | } 72 | } 73 | 74 | scm { 75 | connection gitUrl 76 | developerConnection gitUrl 77 | url siteUrl 78 | } 79 | } 80 | } 81 | } 82 | } 83 | 84 | dependencies { 85 | compile fileTree(dir: 'libs', include: ['*.jar']) 86 | } 87 | 88 | task sourcesJar(type: Jar) { 89 | from android.sourceSets.main.java.srcDirs 90 | classifier = 'sources' 91 | } 92 | 93 | task javadoc(type: Javadoc) { 94 | source = android.sourceSets.main.java.srcDirs 95 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 96 | } 97 | 98 | task javadocJar(type: Jar, dependsOn: javadoc) { 99 | classifier = 'javadoc' 100 | from javadoc.destinationDir 101 | } 102 | 103 | artifacts { 104 | archives javadocJar 105 | archives sourcesJar 106 | } 107 | 108 | Properties properties = new Properties() 109 | properties.load(project.rootProject.file('local.properties').newDataInputStream()) 110 | 111 | //配置上传Bintray相关信息 112 | bintray { 113 | //读取Bintray帐号和密码。 114 | //一般的为了保密和安全性,在项目的local.properties文件中添加两行句话即可: 115 | //bintray.user=username 116 | //bintray.apikey=apikey 117 | user = properties.getProperty("bintray.user") 118 | key = properties.getProperty("bintray.apikey") 119 | 120 | configurations = ['archives'] 121 | 122 | pkg { 123 | repo = "maven"//上传的中央仓库名称 124 | name = "PathEffectTextView"//上传的项目的名字 125 | websiteUrl = siteUrl 126 | vcsUrl = gitUrl 127 | licenses = ["Apache-2.0"] 128 | publish = true //是否发布 129 | } 130 | } -------------------------------------------------------------------------------- /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:/Users/DK/Documents/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/com/dk_exp/library/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.dk_exp.library; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /Library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Library/src/main/java/com/dk/view/patheffect/MatchPath.java: -------------------------------------------------------------------------------- 1 | package com.dk.view.patheffect; 2 | 3 | import android.util.SparseArray; 4 | 5 | import java.util.ArrayList; 6 | 7 | /** 8 | * This code comes from https://github.com/liaohuqiu/android-Ultra-Pull-To-Refresh/ 9 | */ 10 | public class MatchPath { 11 | 12 | private static final SparseArray sPointList; 13 | 14 | public static final char V_LEFT = '#'; 15 | public static final char H_TOP_BOTTOM = '$'; 16 | public static final char V_RIGHT = '%'; 17 | 18 | 19 | static { 20 | sPointList = new SparseArray(); 21 | float[][] LETTERS = new float[][]{ 22 | new float[]{ 23 | // A 24 | 24, 0, 1, 22, 25 | 1, 22, 1, 72, 26 | 24, 0, 47, 22, 27 | 47, 22, 47, 72, 28 | 1, 48, 47, 48 29 | }, 30 | 31 | new float[]{ 32 | // B 33 | 0, 0, 0, 72, 34 | 0, 0, 37, 0, 35 | 37, 0, 47, 11, 36 | 47, 11, 47, 26, 37 | 47, 26, 38, 36, 38 | 38, 36, 0, 36, 39 | 38, 36, 47, 46, 40 | 47, 46, 47, 61, 41 | 47, 61, 38, 71, 42 | 37, 72, 0, 72, 43 | }, 44 | 45 | new float[]{ 46 | // C 47 | 47, 0, 0, 0, 48 | 0, 0, 0, 72, 49 | 0, 72, 47, 72, 50 | }, 51 | 52 | new float[]{ 53 | // D 54 | 0, 0, 0, 72, 55 | 0, 0, 24, 0, 56 | 24, 0, 47, 22, 57 | 47, 22, 47, 48, 58 | 47, 48, 23, 72, 59 | 23, 72, 0, 72, 60 | }, 61 | 62 | new float[]{ 63 | // E 64 | 0, 0, 0, 72, 65 | 0, 0, 47, 0, 66 | 0, 36, 37, 36, 67 | 0, 72, 47, 72, 68 | }, 69 | 70 | new float[]{ 71 | // F 72 | 0, 0, 0, 72, 73 | 0, 0, 47, 0, 74 | 0, 36, 37, 36, 75 | }, 76 | 77 | new float[]{ 78 | // G 79 | 47, 23, 47, 0, 80 | 47, 0, 0, 0, 81 | 0, 0, 0, 72, 82 | 0, 72, 47, 72, 83 | 47, 72, 47, 48, 84 | 47, 48, 24, 48, 85 | }, 86 | 87 | new float[]{ 88 | // H 89 | 0, 0, 0, 72, 90 | 0, 36, 47, 36, 91 | 47, 0, 47, 72, 92 | }, 93 | 94 | new float[]{ 95 | // I 96 | 0, 0, 47, 0, 97 | 24, 0, 24, 72, 98 | 0, 72, 47, 72, 99 | }, 100 | 101 | new float[]{ 102 | // J 103 | 47, 0, 47, 72, 104 | 47, 72, 24, 72, 105 | 24, 72, 0, 48, 106 | }, 107 | 108 | new float[]{ 109 | // K 110 | 0, 0, 0, 72, 111 | 47, 0, 3, 33, 112 | 3, 38, 47, 72, 113 | }, 114 | 115 | new float[]{ 116 | // L 117 | 0, 0, 0, 72, 118 | 0, 72, 47, 72, 119 | }, 120 | 121 | new float[]{ 122 | // M 123 | 0, 0, 0, 72, 124 | 0, 0, 24, 23, 125 | 24, 23, 47, 0, 126 | 47, 0, 47, 72, 127 | }, 128 | 129 | new float[]{ 130 | // N 131 | 0, 0, 0, 72, 132 | 0, 0, 47, 72, 133 | 47, 72, 47, 0, 134 | }, 135 | 136 | new float[]{ 137 | // O 138 | 0, 0, 0, 72, 139 | 0, 72, 47, 72, 140 | 47, 72, 47, 0, 141 | 47, 0, 0, 0, 142 | }, 143 | 144 | new float[]{ 145 | // P 146 | 0, 0, 0, 72, 147 | 0, 0, 47, 0, 148 | 47, 0, 47, 36, 149 | 47, 36, 0, 36, 150 | }, 151 | 152 | new float[]{ 153 | // Q 154 | 0, 0, 0, 72, 155 | 0, 72, 23, 72, 156 | 23, 72, 47, 48, 157 | 47, 48, 47, 0, 158 | 47, 0, 0, 0, 159 | 24, 28, 47, 71, 160 | }, 161 | 162 | new float[]{ 163 | // R 164 | 0, 0, 0, 72, 165 | 0, 0, 47, 0, 166 | 47, 0, 47, 36, 167 | 47, 36, 0, 36, 168 | 0, 37, 47, 72, 169 | }, 170 | 171 | new float[]{ 172 | // S 173 | 47, 0, 0, 0, 174 | 0, 0, 0, 36, 175 | 0, 36, 47, 36, 176 | 47, 36, 47, 72, 177 | 47, 72, 0, 72, 178 | }, 179 | 180 | new float[]{ 181 | // T 182 | 0, 0, 47, 0, 183 | 24, 0, 24, 72, 184 | }, 185 | 186 | new float[]{ 187 | // U 188 | 0, 0, 0, 72, 189 | 0, 72, 47, 72, 190 | 47, 72, 47, 0, 191 | }, 192 | 193 | new float[]{ 194 | // V 195 | 0, 0, 24, 72, 196 | 24, 72, 47, 0, 197 | }, 198 | 199 | new float[]{ 200 | // W 201 | 0, 0, 0, 72, 202 | 0, 72, 24, 49, 203 | 24, 49, 47, 72, 204 | 47, 72, 47, 0 205 | }, 206 | 207 | new float[]{ 208 | // X 209 | 0, 0, 47, 72, 210 | 47, 0, 0, 72 211 | }, 212 | 213 | new float[]{ 214 | // Y 215 | 0, 0, 24, 23, 216 | 47, 0, 24, 23, 217 | 24, 23, 24, 72 218 | }, 219 | 220 | new float[]{ 221 | // Z 222 | 0, 0, 47, 0, 223 | 47, 0, 0, 72, 224 | 0, 72, 47, 72 225 | }, 226 | }; 227 | final float[][] NUMBERS = new float[][]{ 228 | new float[]{ 229 | // 0 230 | 0, 0, 0, 72, 231 | 0, 72, 47, 72, 232 | 47, 72, 47, 0, 233 | 47, 0, 0, 0, 234 | }, 235 | new float[]{ 236 | // 1 237 | 24, 0, 24, 72, 238 | }, 239 | 240 | new float[]{ 241 | // 2 242 | 0, 0, 47, 0, 243 | 47, 0, 47, 36, 244 | 47, 36, 0, 36, 245 | 0, 36, 0, 72, 246 | 0, 72, 47, 72 247 | }, 248 | 249 | new float[]{ 250 | // 3 251 | 0, 0, 47, 0, 252 | 47, 0, 47, 36, 253 | 47, 36, 0, 36, 254 | 47, 36, 47, 72, 255 | 47, 72, 0, 72, 256 | }, 257 | 258 | new float[]{ 259 | // 4 260 | 0, 0, 0, 36, 261 | 0, 36, 47, 36, 262 | 47, 0, 47, 72, 263 | }, 264 | 265 | new float[]{ 266 | // 5 267 | 0, 0, 0, 36, 268 | 0, 36, 47, 36, 269 | 47, 36, 47, 72, 270 | 47, 72, 0, 72, 271 | 0, 0, 47, 0 272 | }, 273 | 274 | new float[]{ 275 | // 6 276 | 0, 0, 0, 72, 277 | 0, 72, 47, 72, 278 | 47, 72, 47, 36, 279 | 47, 36, 0, 36 280 | }, 281 | 282 | new float[]{ 283 | // 7 284 | 0, 0, 47, 0, 285 | 47, 0, 47, 72 286 | }, 287 | 288 | new float[]{ 289 | // 8 290 | 0, 0, 0, 72, 291 | 0, 72, 47, 72, 292 | 47, 72, 47, 0, 293 | 47, 0, 0, 0, 294 | 0, 36, 47, 36 295 | }, 296 | 297 | new float[]{ 298 | // 9 299 | 47, 0, 0, 0, 300 | 0, 0, 0, 36, 301 | 0, 36, 47, 36, 302 | 47, 0, 47, 72, 303 | } 304 | }; 305 | // A - Z 306 | for (int i = 0; i < LETTERS.length; i++) { 307 | sPointList.append(i + 65, LETTERS[i]); 308 | } 309 | // a - z 310 | for (int i = 0; i < LETTERS.length; i++) { 311 | sPointList.append(i + 65 + 32, LETTERS[i]); 312 | } 313 | // 0 - 9 314 | for (int i = 0; i < NUMBERS.length; i++) { 315 | sPointList.append(i + 48, NUMBERS[i]); 316 | } 317 | // blank 318 | addChar(' ', new float[]{}); 319 | // - 320 | addChar('-', new float[]{ 321 | 0, 36, 47, 36 322 | }); 323 | // . 324 | addChar('.', new float[]{ 325 | 24, 60, 24, 72 326 | }); 327 | 328 | // 329 | addChar(V_LEFT, new float[]{ 330 | -12, 120, -12, 38, 331 | -12, 38, -12, -45 332 | }); 333 | // 334 | addChar(H_TOP_BOTTOM, new float[]{ 335 | 0, -45, 23, -45, 336 | 23, -45, 67, -45, 337 | 0, 120, 23, 120, 338 | 23, 120, 67, 120 339 | }); 340 | 341 | // 342 | addChar(V_RIGHT, new float[]{ 343 | 79, -45, 79, 38, 344 | 79, 38, 79, 120 345 | }); 346 | } 347 | 348 | public static void addChar(char c, float[] points) { 349 | sPointList.append(c, points); 350 | } 351 | 352 | public static ArrayList getPath(String str) { 353 | return getPath(str, 1, 14); 354 | } 355 | 356 | public static boolean isButtonModle; 357 | 358 | /** 359 | * @param str 360 | * @param scale 361 | * @param gapBetweenLetter 362 | * @return ArrayList of float[] {x1, y1, x2, y2} 363 | */ 364 | public static ArrayList getPath(String str, float scale, int gapBetweenLetter) { 365 | ArrayList list = new ArrayList(); 366 | float offsetForWidth = 0; 367 | for (int i = 0; i < str.length(); i++) { 368 | int pos = str.charAt(i); 369 | int key = sPointList.indexOfKey(pos); 370 | if (key == -1) { 371 | continue; 372 | } 373 | float[] points = sPointList.get(pos); 374 | 375 | if (isButtonModle) { 376 | float[] points1 = new float[points.length + 16]; 377 | for (int j = 0; j < sPointList.get(H_TOP_BOTTOM).length; j++) { 378 | points1[j] = sPointList.get(H_TOP_BOTTOM)[j]; 379 | } 380 | for (int j = 0; j < points.length; j++) { 381 | points1[j + 16] = points[j]; 382 | } 383 | points = points1; 384 | } 385 | 386 | int pointCount = points.length / 4; 387 | for (int j = 0; j < pointCount; j++) { 388 | float[] line = new float[4]; 389 | for (int k = 0; k < 4; k++) { 390 | float l = points[j * 4 + k]; 391 | // x 392 | if (k % 2 == 0) { 393 | line[k] = (l + offsetForWidth) * scale; 394 | } 395 | // y 396 | else { 397 | line[k] = l * scale; 398 | } 399 | } 400 | list.add(line); 401 | } 402 | offsetForWidth += 57 + gapBetweenLetter; 403 | } 404 | 405 | if (isButtonModle) { 406 | isButtonModle = false; 407 | } 408 | return list; 409 | } 410 | } 411 | -------------------------------------------------------------------------------- /Library/src/main/java/com/dk/view/patheffect/PathTextView.java: -------------------------------------------------------------------------------- 1 | package com.dk.view.patheffect; 2 | 3 | import android.animation.ObjectAnimator; 4 | import android.content.Context; 5 | import android.graphics.Canvas; 6 | import android.graphics.Color; 7 | import android.graphics.Paint; 8 | import android.graphics.Path; 9 | import android.graphics.PathMeasure; 10 | import android.os.Build; 11 | import android.util.AttributeSet; 12 | import android.view.View; 13 | 14 | import java.util.ArrayList; 15 | 16 | /** 17 | * Created by DK on 2015/4/1. 18 | */ 19 | public class PathTextView extends View { 20 | private static final float BASE_SQUARE_UNIT = 72f; 21 | private String mText = "FUCK"; 22 | private ArrayList mDatas; 23 | private ArrayList mPaths = new ArrayList(); 24 | private Paint mPaint = new Paint(); 25 | private ObjectAnimator mSvgAnimator; 26 | private final Object mSvgLock = new Object(); 27 | private float mPhase; 28 | private Type mType = Type.SINGLE; 29 | private float mScaleFactor = 1.0f; 30 | 31 | private int mTextColor = Color.BLACK; 32 | private float mTextSize = BASE_SQUARE_UNIT; 33 | private float mTextWeight = 2; 34 | private float mShadowDy = 0; 35 | 36 | private int mDuration = 3000; 37 | 38 | public enum Type { 39 | SINGLE, MULTIPLY 40 | } 41 | 42 | public PathTextView(Context context, AttributeSet attrs) { 43 | super(context, attrs); 44 | mPaint.setStyle(Paint.Style.STROKE); 45 | mPaint.setColor(mTextColor); 46 | mPaint.setStrokeWidth(mTextWeight); 47 | 48 | /** 49 | * refer to http://stackoverflow.com/questions/27926105/android-paint-setshadowlayer-ignoring-shadowcolor 50 | * 51 | * The shadowLayer works only it the hardware acceleration is disabled 52 | */ 53 | 54 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 55 | setLayerType(LAYER_TYPE_SOFTWARE, mPaint); 56 | } 57 | } 58 | 59 | public void setTextColor(int color) { 60 | mTextColor = color; 61 | mPaint.setColor(color); 62 | } 63 | 64 | public void setTextWeight(int weight) { 65 | mTextWeight = weight; 66 | mPaint.setStrokeWidth(mTextWeight); 67 | } 68 | 69 | public void setTextSize(float size) { 70 | mTextSize = size; 71 | mScaleFactor = mTextSize / BASE_SQUARE_UNIT; 72 | } 73 | 74 | public void setPaintType(Type type) { 75 | mType = type; 76 | } 77 | 78 | public void setDuration(int duration) { 79 | mDuration = duration; 80 | } 81 | 82 | /** 83 | * This draws a shadow layer below the main layer, with the specified 84 | * offset and color, and blur radius. If radius is 0, then the shadow 85 | * layer is removed. 86 | *

87 | * Can be used to create a blurred shadow underneath text. Support for use 88 | * with other drawing operations is constrained to the software rendering 89 | * pipeline. 90 | *

91 | * The alpha of the shadow will be the paint's alpha if the shadow color is 92 | * opaque, or the alpha from the shadow color if not. 93 | */ 94 | public void setShadow(int radius, int dx, int dy, int color) { 95 | // mPaint.clearShadowLayer(); 96 | mShadowDy = dy; 97 | mPaint.setShadowLayer(radius, dx, dy, color); 98 | } 99 | 100 | @Override 101 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 102 | setMeasuredDimension(measureWidth(widthMeasureSpec), 103 | measureHeight(heightMeasureSpec)); 104 | } 105 | 106 | public void init(String text) { 107 | if (text == null || text.length() == 0) 108 | return; 109 | 110 | requestLayout(); 111 | invalidate(); 112 | 113 | mText = text; 114 | mDatas = MatchPath.getPath(mText); 115 | mSvgAnimator = ObjectAnimator.ofFloat(this, "phase", 0.0f, 1.0f).setDuration(mDuration); 116 | mSvgAnimator.start(); 117 | } 118 | 119 | 120 | @Override 121 | protected void onDraw(Canvas canvas) { 122 | super.onDraw(canvas); 123 | if (mPaths == null) 124 | return; 125 | synchronized (mSvgLock) { 126 | for (int i = 0; i < mPaths.size(); i++) 127 | canvas.drawPath(mPaths.get(i), mPaint); 128 | } 129 | } 130 | 131 | 132 | private void updatePathsPhaseLocked() { 133 | mPaths.clear(); 134 | float singlefactor = mPhase * mDatas.size(); 135 | for (int i = 0; i < mDatas.size(); i++) { 136 | Path path = new Path(); 137 | path.moveTo(mDatas.get(i)[0] * mScaleFactor + mTextWeight, mDatas.get(i)[1] * mScaleFactor + mTextWeight); 138 | path.lineTo(mDatas.get(i)[2] * mScaleFactor + mTextWeight, mDatas.get(i)[3] * mScaleFactor + mTextWeight); 139 | 140 | if (mType == Type.MULTIPLY) { 141 | PathMeasure measure = new PathMeasure(path, false); 142 | Path dst = new Path(); 143 | measure.getSegment(0.0f, mPhase * measure.getLength(), dst, true); 144 | mPaths.add(dst); 145 | } else { 146 | //Fuck! can't compare float and int 147 | //Sometimes, at the end of animation , the value is -9.5176697E-4 or other tiny value. 148 | if (singlefactor - (i + 1) >= -0.01) 149 | mPaths.add(path); 150 | else if (i - Math.floor(singlefactor) < 0.0001) { 151 | Path dst = new Path(); 152 | PathMeasure measure = new PathMeasure(path, false); 153 | measure.getSegment(0.0f, (singlefactor % 1) * measure.getLength(), dst, true); 154 | mPaths.add(dst); 155 | } 156 | } 157 | 158 | } 159 | } 160 | 161 | public float getPhase() { 162 | return mPhase; 163 | } 164 | 165 | public void setPhase(float phase) { 166 | mPhase = phase; 167 | synchronized (mSvgLock) { 168 | updatePathsPhaseLocked(); 169 | } 170 | invalidate(); 171 | } 172 | 173 | private int measureWidth(int measureSpec) { 174 | int result = 0; 175 | int specMode = MeasureSpec.getMode(measureSpec); 176 | int specSize = MeasureSpec.getSize(measureSpec); 177 | 178 | if (specMode == MeasureSpec.EXACTLY) { 179 | // We were told how big to be 180 | result = specSize; 181 | } else { 182 | // Measure the text 183 | result = (int) (mText.length() * BASE_SQUARE_UNIT * mScaleFactor + getPaddingLeft() 184 | + getPaddingRight() + mTextWeight * 2); 185 | if (specMode == MeasureSpec.AT_MOST) { 186 | // Respect AT_MOST value if that was what is called for by measureSpec 187 | result = Math.min(result, specSize); 188 | } 189 | } 190 | 191 | return result; 192 | } 193 | 194 | private int measureHeight(int measureSpec) { 195 | int result = 0; 196 | int specMode = MeasureSpec.getMode(measureSpec); 197 | int specSize = MeasureSpec.getSize(measureSpec); 198 | 199 | if (specMode == MeasureSpec.EXACTLY) { 200 | // We were told how big to be 201 | result = specSize; 202 | } else { 203 | // Text wight(stoke width) may cause it a litter bigger 204 | result = (int) (BASE_SQUARE_UNIT * mScaleFactor) + getPaddingTop() 205 | + getPaddingBottom() + (int) (mTextWeight * 2) + (int) mShadowDy; 206 | if (specMode == MeasureSpec.AT_MOST) { 207 | // Respect AT_MOST value if that was what is called for by measureSpec 208 | result = Math.min(result, specSize); 209 | } 210 | } 211 | return result; 212 | } 213 | 214 | } 215 | -------------------------------------------------------------------------------- /Library/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Library 3 | 4 | -------------------------------------------------------------------------------- /Library/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /PathEffectTextView.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Screenshot 2 | 3 | Please waiting for loading the gif... 4 | 5 | ![](/path1.gif) 6 | 7 | ![](/path2.gif) 8 | 9 | ![](/path3.gif) 10 | 11 | ![] (/path4.gif) 12 | 13 | # How to use 14 | 15 | Step 1: add denpendence 16 | 17 | 18 | compile('com.dk.view.patheffect:Library:0.1.1@aar') 19 | 20 | 21 | 22 | If you are still using `Eclipse`, you can just copy source code or jar file to you project. 23 | 24 | 25 | 26 | Step 2: add view to your layout: 27 | 28 | 32 | 33 | step 3: call `init` method like this: 34 | 35 | PathTextView mPathTextView = (PathTextView) findViewById(R.id.path); 36 | mPathTextView.init("Hello World"); 37 | 38 | Option settings: 39 | 40 | mPathTextView.setPaintType(PathTextView.Type.MULTIPLY); 41 | mPathTextView.setTextColor(color); 42 | mPathTextView.setTextSize(size); 43 | mPathTextView.setTextWeight(weight); 44 | mPathTextView.setDuration(duration); 45 | mPathTextView.setShadow(radius, dx, dy, shadowColor); 46 | 47 | # NOTE 48 | 49 | - Only Support capital letter, you can check this file for [`Path Data`](https://github.com/dkmeteor/PathEffectTextView/blob/master/Library/src/main/java/com/dk/view/patheffect/MatchPath.java), the data comes from [android-Ultra-Pull-To-Refresh](https://github.com/liaohuqiu/android-Ultra-Pull-To-Refresh/) 50 | 51 | - the size unit is px , 72 means 72px*72px. 52 | 53 | - the text weight unit is px. 54 | 55 | 56 | # License 57 | 58 | Copyright 2015 Dean Ding 59 | 60 | Licensed under the Apache License, Version 2.0 (the "License"); 61 | you may not use this file except in compliance with the License. 62 | You may obtain a copy of the License at 63 | 64 | http://www.apache.org/licenses/LICENSE-2.0 65 | 66 | Unless required by applicable law or agreed to in writing, software 67 | distributed under the License is distributed on an "AS IS" BASIS, 68 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 69 | See the License for the specific language governing permissions and 70 | limitations under the License. 71 | 72 | --- 73 | Developed By 74 | 75 | 76 | Dean <93440331@qq.com> 77 | 78 | Weibo:http://weibo.com/u/2699012760 79 | 80 | ![](https://avatars0.githubusercontent.com/u/5019523?v=3&s=460) -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/app.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 21 5 | buildToolsVersion "21.1.2" 6 | 7 | defaultConfig { 8 | applicationId "com.dk.path.effect.textview" 9 | minSdkVersion 15 10 | targetSdkVersion 21 11 | versionCode 1 12 | versionName "1.0" 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 fileTree(dir: 'libs', include: ['*.jar']) 24 | compile 'com.android.support:appcompat-v7:21.+' 25 | compile project(':Library') 26 | // compile('com.dk.view.patheffect:Library:0.1.1@aar') 27 | } 28 | -------------------------------------------------------------------------------- /app/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:\Users\DK\Documents\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 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/dk_exp/patheffecttextview/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.dk_exp.patheffecttextview; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/dk/view/demo/patheffect/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.dk.view.demo.patheffect; 2 | 3 | import android.graphics.Color; 4 | import android.os.Bundle; 5 | import android.support.v7.app.ActionBarActivity; 6 | import android.view.View; 7 | import android.widget.EditText; 8 | import android.widget.RadioButton; 9 | 10 | import com.dk.view.patheffect.PathTextView; 11 | 12 | public class MainActivity extends ActionBarActivity { 13 | private PathTextView mPathTextView; 14 | private EditText mEditText, mColorEditText, mSizeEditText, mWeightEditText,mShadowWeight,mShadowColor; 15 | private RadioButton mSingle, mMultiply; 16 | 17 | @Override 18 | protected void onCreate(Bundle savedInstanceState) { 19 | super.onCreate(savedInstanceState); 20 | setContentView(R.layout.activity_main); 21 | mPathTextView = (PathTextView) findViewById(R.id.path); 22 | mEditText = (EditText) findViewById(R.id.input); 23 | mColorEditText = (EditText) findViewById(R.id.color); 24 | mSizeEditText = (EditText) findViewById(R.id.size); 25 | mWeightEditText = (EditText) findViewById(R.id.weight); 26 | mShadowWeight = (EditText) findViewById(R.id.shadow_weight); 27 | mShadowColor = (EditText) findViewById(R.id.shadow_color); 28 | mSingle = (RadioButton) findViewById(R.id.radio_single); 29 | mMultiply = (RadioButton) findViewById(R.id.radio_multiply); 30 | 31 | 32 | findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() { 33 | @Override 34 | public void onClick(View v) { 35 | String colorText = mColorEditText.getText().toString(); 36 | String sizeText = mSizeEditText.getText().toString(); 37 | String weightText = mWeightEditText.getText().toString(); 38 | 39 | int color = Color.BLACK; 40 | try { 41 | if (colorText.contains("#")) { 42 | // colorText = colorText.replace("#",""); 43 | color = Color.parseColor(colorText); 44 | } else { 45 | color = Integer.parseInt(colorText); 46 | } 47 | } catch (NumberFormatException e) { 48 | // e.printStackTrace(); 49 | } 50 | int size = 72; 51 | try { 52 | size = Integer.parseInt(sizeText); 53 | } catch (NumberFormatException e) { 54 | // e.printStackTrace(); 55 | } 56 | int weight = 2; 57 | try { 58 | weight = Integer.parseInt(weightText); 59 | } catch (NumberFormatException e) { 60 | // e.printStackTrace(); 61 | } 62 | 63 | String shadowWeightText = mShadowWeight.getText().toString(); 64 | 65 | int shadowWeight = weight / 2; 66 | try { 67 | shadowWeight = Integer.parseInt(shadowWeightText); 68 | } catch (NumberFormatException e) { 69 | // e.printStackTrace(); 70 | } 71 | 72 | String shdowColorText =mShadowColor.getText().toString(); 73 | int shadowColor = Color.GRAY; 74 | try { 75 | if (shdowColorText.contains("#")) { 76 | // colorText = colorText.replace("#",""); 77 | shadowColor = Color.parseColor(shdowColorText); 78 | } else { 79 | shadowColor = Integer.parseInt(shdowColorText); 80 | } 81 | } catch (NumberFormatException e) { 82 | // e.printStackTrace(); 83 | } 84 | 85 | 86 | 87 | if (mSingle.isChecked()) 88 | mPathTextView.setPaintType(PathTextView.Type.SINGLE); 89 | else 90 | mPathTextView.setPaintType(PathTextView.Type.MULTIPLY); 91 | 92 | mPathTextView.setTextColor(color); 93 | mPathTextView.setTextSize(size); 94 | mPathTextView.setTextWeight(weight); 95 | mPathTextView.setDuration(2000); 96 | mPathTextView.setShadow(shadowWeight,shadowWeight,shadowWeight,shadowColor); 97 | mPathTextView.init(mEditText.getText().toString()); 98 | } 99 | }); 100 | 101 | mPathTextView.setTextWeight(8); 102 | mPathTextView.setTextColor(Color.BLUE); 103 | mPathTextView.setTextSize(144); 104 | } 105 | 106 | 107 | } 108 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 16 | 17 | 25 | 26 |