├── .gitignore ├── .idea ├── codeStyles │ └── Project.xml ├── misc.xml └── runConfigurations.xml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── liwenwei │ │ └── pinyintextview │ │ └── demo │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── liwenwei │ │ │ └── pinyintextview │ │ │ └── demo │ │ │ └── MainActivity.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.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 │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── liwenwei │ └── pinyintextview │ └── demo │ └── ExampleUnitTest.java ├── art └── shortcut.gif ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── pinyintextview ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── liwenwei │ │ └── pinyintextview │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── liwenwei │ │ │ └── pinyintextview │ │ │ ├── MultiScreenSupportUtils.java │ │ │ ├── PinyinTextView.java │ │ │ └── StringUtils.java │ └── res │ │ └── values │ │ ├── attrs.xml │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── liwenwei │ └── pinyintextview │ └── ExampleUnitTest.java └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | *.aab 5 | 6 | # Files for the ART/Dalvik VM 7 | *.dex 8 | 9 | # Java class files 10 | *.class 11 | 12 | # Generated files 13 | bin/ 14 | gen/ 15 | out/ 16 | # Uncomment the following line in case you need and you don't have the release build type files in your app 17 | # release/ 18 | 19 | # Gradle files 20 | .gradle/ 21 | build/ 22 | 23 | # Local configuration file (sdk path, etc) 24 | local.properties 25 | 26 | # Proguard folder generated by Eclipse 27 | proguard/ 28 | 29 | # Log Files 30 | *.log 31 | 32 | # Android Studio Navigation editor temp files 33 | .navigation/ 34 | 35 | # Android Studio captures folder 36 | captures/ 37 | 38 | # IntelliJ 39 | *.iml 40 | .idea/workspace.xml 41 | .idea/tasks.xml 42 | .idea/gradle.xml 43 | .idea/assetWizardSettings.xml 44 | .idea/dictionaries 45 | .idea/libraries 46 | # Android Studio 3 in .gitignore file. 47 | .idea/caches 48 | .idea/modules.xml 49 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 50 | .idea/navEditor.xml 51 | 52 | # Keystore files 53 | # Uncomment the following lines if you do not want to check your keystore files in. 54 | #*.jks 55 | #*.keystore 56 | 57 | # External native build folder generated in Android Studio 2.2 and later 58 | .externalNativeBuild 59 | .cxx/ 60 | 61 | # Google Services (e.g. APIs or Firebase) 62 | # google-services.json 63 | 64 | # Freeline 65 | freeline.py 66 | freeline/ 67 | freeline_project_description.json 68 | 69 | # fastlane 70 | fastlane/report.xml 71 | fastlane/Preview.html 72 | fastlane/screenshots 73 | fastlane/test_output 74 | fastlane/readme.md 75 | 76 | # Version control 77 | vcs.xml 78 | 79 | # lint 80 | lint/intermediates/ 81 | lint/generated/ 82 | lint/outputs/ 83 | lint/tmp/ 84 | # lint/reports/ 85 | © 2019 GitHub, Inc. -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | xmlns:android 14 | 15 | ^$ 16 | 17 | 18 | 19 |
20 |
21 | 22 | 23 | 24 | xmlns:.* 25 | 26 | ^$ 27 | 28 | 29 | BY_NAME 30 | 31 |
32 |
33 | 34 | 35 | 36 | .*:id 37 | 38 | http://schemas.android.com/apk/res/android 39 | 40 | 41 | 42 |
43 |
44 | 45 | 46 | 47 | .*:name 48 | 49 | http://schemas.android.com/apk/res/android 50 | 51 | 52 | 53 |
54 |
55 | 56 | 57 | 58 | name 59 | 60 | ^$ 61 | 62 | 63 | 64 |
65 |
66 | 67 | 68 | 69 | style 70 | 71 | ^$ 72 | 73 | 74 | 75 |
76 |
77 | 78 | 79 | 80 | .* 81 | 82 | ^$ 83 | 84 | 85 | BY_NAME 86 | 87 |
88 |
89 | 90 | 91 | 92 | .* 93 | 94 | http://schemas.android.com/apk/res/android 95 | 96 | 97 | ANDROID_ATTRIBUTE_ORDER 98 | 99 |
100 |
101 | 102 | 103 | 104 | .* 105 | 106 | .* 107 | 108 | 109 | BY_NAME 110 | 111 |
112 |
113 |
114 |
115 |
116 |
-------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.liwenwei.pinyintextview/pinyintextview/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.liwenwei.pinyintextview/pinyintextview/badge.svg) 2 | 3 | # PinyinTextView 4 | 一个简单灵活的汉语拼音Widget 5 | 6 | ![PinyinTextView shortcut](art/shortcut.gif) 7 | 8 | ## Getting started 9 | 10 | ### Setting up the dependency 11 | 12 | 首先将PinyinTextView引用到你的项目中,例如 Gradle compile dependency: 13 | ```groovy 14 | implementation "com.liwenwei.pinyintextview:pinyintextview:1.0.1" 15 | ``` 16 | 17 | ### Usage 18 | 19 | 在Activity的XML中 20 | 21 | ```xml 22 | 27 | ``` 28 | 29 | ### XML attributes 30 | 31 | Summary 32 | 33 | | XML attributes | Description | 34 | | --- | --- | 35 | | app:textSize | 字体大小 | 36 | | app:textColor | 汉字字体颜色 | 37 | | app:pinyinColor | 拼音字体颜色 | 38 | | app:pinyinTextSpace | 汉字和拼音的上下间距 | 39 | | app:horizontalSpace | 每个汉字拼音之间的间距 | 40 | | app:lineSpace | 行距 | 41 | | app:underlineVerticalSpace | 下划线垂直间距 | 42 | | app:underline | 是否显示下划线 | 43 | 44 | ### Colors 45 | 如果你想给你不同的汉字拼音设置不同的颜色,请参考 46 | ```java 47 | void setPinyinTextByTokens(List pinyinList, @PinyinMode int mode) 48 | ``` 49 | 每个拼音对应`Token`,根据给Token设置不同的`textColor`和`pinyinColor`颜色 50 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 28 5 | defaultConfig { 6 | applicationId "com.liwenwei.pinyintextview" 7 | minSdkVersion 16 8 | targetSdkVersion 28 9 | versionCode 1 10 | versionName "1.0" 11 | } 12 | buildTypes { 13 | release { 14 | minifyEnabled false 15 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 16 | } 17 | } 18 | } 19 | 20 | dependencies { 21 | implementation fileTree(dir: 'libs', include: ['*.jar']) 22 | implementation 'com.android.support:appcompat-v7:28.0.0' 23 | implementation 'androidx.appcompat:appcompat:1.1.0' 24 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 25 | implementation 'org.honorato.multistatetogglebutton:multistatetogglebutton:0.2.2' 26 | testImplementation 'junit:junit:4.12' 27 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 28 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 29 | implementation project(path: ':pinyintextview') 30 | } 31 | -------------------------------------------------------------------------------- /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/androidTest/java/com/liwenwei/pinyintextview/demo/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.liwenwei.pinyintextview.demo; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.platform.app.InstrumentationRegistry; 6 | import androidx.test.ext.junit.runners.AndroidJUnit4; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 24 | 25 | assertEquals("com.liwenwei.pinyintextview.demo", appContext.getPackageName()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/liwenwei/pinyintextview/demo/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.liwenwei.pinyintextview.demo; 2 | 3 | import android.content.Context; 4 | import android.os.Bundle; 5 | import android.util.Pair; 6 | import android.widget.CompoundButton; 7 | import android.widget.SeekBar; 8 | import android.widget.Switch; 9 | 10 | import com.liwenwei.pinyintextview.MultiScreenSupportUtils; 11 | import com.liwenwei.pinyintextview.PinyinTextView; 12 | 13 | import org.honorato.multistatetogglebutton.MultiStateToggleButton; 14 | import org.honorato.multistatetogglebutton.ToggleButton; 15 | 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | 19 | import androidx.appcompat.app.AppCompatActivity; 20 | 21 | public class MainActivity extends AppCompatActivity { 22 | 23 | private int[] mLineSpaces = {6, 10, 14}; 24 | private int[] mHorizontalSpaces = {2, 6, 10}; 25 | 26 | @Override 27 | protected void onCreate(Bundle savedInstanceState) { 28 | super.onCreate(savedInstanceState); 29 | setContentView(R.layout.activity_main); 30 | initView(); 31 | } 32 | 33 | private void initView() { 34 | final Context context = this; 35 | final PinyinTextView pTVContent = findViewById(R.id.ptv_content); 36 | pTVContent.setPinyinText(initPinyinData(), PinyinTextView.TYPE_PINYIN_AND_TEXT); 37 | 38 | // Set Text Size 39 | ((SeekBar) findViewById(R.id.seek_bar_text_size)).setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { 40 | @Override 41 | public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 42 | pTVContent.setTextSize(MultiScreenSupportUtils.sp2Px(progress, context)); 43 | } 44 | 45 | @Override 46 | public void onStartTrackingTouch(SeekBar seekBar) { 47 | 48 | } 49 | 50 | @Override 51 | public void onStopTrackingTouch(SeekBar seekBar) { 52 | 53 | } 54 | }); 55 | 56 | // Set Underline 57 | ((Switch)findViewById(R.id.switch_underline)).setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { 58 | @Override 59 | public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 60 | pTVContent.setUnderline(isChecked); 61 | } 62 | }); 63 | 64 | // Set line space 65 | MultiStateToggleButton mstbLineSpace = findViewById(R.id.mstb_line_space); 66 | mstbLineSpace.setValue(1); 67 | mstbLineSpace.setOnValueChangedListener(new ToggleButton.OnValueChangedListener() { 68 | @Override 69 | public void onValueChanged(int value) { 70 | pTVContent.setLineSpacing(MultiScreenSupportUtils.dp2Px(mLineSpaces[value], context)); 71 | } 72 | }); 73 | 74 | // Set horizontal space 75 | MultiStateToggleButton mstbHorizontalSpace = findViewById(R.id.mstb_horizontal_space); 76 | mstbHorizontalSpace.setValue(1); 77 | mstbHorizontalSpace.setOnValueChangedListener(new ToggleButton.OnValueChangedListener() { 78 | @Override 79 | public void onValueChanged(int value) { 80 | pTVContent.setHorizontalSpacing(MultiScreenSupportUtils.dp2Px(mLineSpaces[value], context)); 81 | } 82 | }); 83 | } 84 | 85 | private List> initPinyinData() { 86 | List> pinyinList = new ArrayList<>(); 87 | pinyinList.add(Pair.create("这", "zhè")); 88 | pinyinList.add(Pair.create("是", "shì")); 89 | pinyinList.add(Pair.create("一个", "yī gè")); 90 | pinyinList.add(Pair.create("拼音", "pīn yīn")); 91 | pinyinList.add(Pair.create("组件", "zǔ jiàn")); 92 | pinyinList.add(Pair.create(",", "")); 93 | pinyinList.add(Pair.create("它", "tā")); 94 | pinyinList.add(Pair.create("简单", "jiǎn dān")); 95 | pinyinList.add(Pair.create("轻便", "qīng biàn")); 96 | pinyinList.add(Pair.create(",", "")); 97 | pinyinList.add(Pair.create("可以", "kě yǐ")); 98 | pinyinList.add(Pair.create("很", "hěn")); 99 | pinyinList.add(Pair.create("灵活", "líng huó")); 100 | pinyinList.add(Pair.create("的", "de")); 101 | pinyinList.add(Pair.create("配置", "pèi zhì")); 102 | pinyinList.add(Pair.create("很多", "hěn duō")); 103 | pinyinList.add(Pair.create("设置", "shè zhì")); 104 | pinyinList.add(Pair.create(",", "")); 105 | pinyinList.add(Pair.create("同时", "tóng shí")); 106 | pinyinList.add(Pair.create("可以", "kě yǐ")); 107 | pinyinList.add(Pair.create("切换", "qiē huàn")); 108 | pinyinList.add(Pair.create("多种", "duō zhǒng")); 109 | pinyinList.add(Pair.create("模式", "mó shì")); 110 | pinyinList.add(Pair.create("。", "")); 111 | return pinyinList; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 23 | 24 | 33 | 34 | 46 | 47 | 56 | 57 | 66 | 67 | 76 | 77 | 90 | 91 | 100 | 101 | 114 | 115 | -------------------------------------------------------------------------------- /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/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liwenwei/PinyinTextView/5f06def22413b72455fcb963f4a87733f6f65eab/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liwenwei/PinyinTextView/5f06def22413b72455fcb963f4a87733f6f65eab/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liwenwei/PinyinTextView/5f06def22413b72455fcb963f4a87733f6f65eab/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liwenwei/PinyinTextView/5f06def22413b72455fcb963f4a87733f6f65eab/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liwenwei/PinyinTextView/5f06def22413b72455fcb963f4a87733f6f65eab/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liwenwei/PinyinTextView/5f06def22413b72455fcb963f4a87733f6f65eab/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liwenwei/PinyinTextView/5f06def22413b72455fcb963f4a87733f6f65eab/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liwenwei/PinyinTextView/5f06def22413b72455fcb963f4a87733f6f65eab/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liwenwei/PinyinTextView/5f06def22413b72455fcb963f4a87733f6f65eab/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liwenwei/PinyinTextView/5f06def22413b72455fcb963f4a87733f6f65eab/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #4974CE 4 | #00574B 5 | #27406D 6 | #F8F8F8 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | PinyinTextView 3 | 4 | 5 | 6 6 | 10 7 | 14 8 | 9 | 10 | 11 | 2 12 | 6 13 | 10 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/test/java/com/liwenwei/pinyintextview/demo/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.liwenwei.pinyintextview.demo; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /art/shortcut.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liwenwei/PinyinTextView/5f06def22413b72455fcb963f4a87733f6f65eab/art/shortcut.gif -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | google() 6 | jcenter() 7 | 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.5.1' 11 | 12 | // NOTE: Do not place your application dependencies here; they belong 13 | // in the individual module build.gradle files 14 | } 15 | } 16 | 17 | allprojects { 18 | repositories { 19 | google() 20 | jcenter() 21 | 22 | } 23 | } 24 | 25 | task clean(type: Delete) { 26 | delete rootProject.buildDir 27 | } 28 | -------------------------------------------------------------------------------- /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 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | 21 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liwenwei/PinyinTextView/5f06def22413b72455fcb963f4a87733f6f65eab/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Nov 05 18:19:16 CST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /pinyintextview/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /pinyintextview/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 28 5 | 6 | 7 | defaultConfig { 8 | minSdkVersion 15 9 | targetSdkVersion 28 10 | versionCode 1 11 | versionName "1.0" 12 | 13 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 14 | } 15 | 16 | buildTypes { 17 | release { 18 | minifyEnabled false 19 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 20 | } 21 | } 22 | 23 | } 24 | 25 | dependencies { 26 | implementation fileTree(dir: 'libs', include: ['*.jar']) 27 | implementation 'com.android.support:appcompat-v7:28.0.0' 28 | implementation 'com.android.support:support-annotations:28.0.0' 29 | testImplementation 'junit:junit:4.12' 30 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 31 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 32 | } 33 | 34 | -------------------------------------------------------------------------------- /pinyintextview/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liwenwei/PinyinTextView/5f06def22413b72455fcb963f4a87733f6f65eab/pinyintextview/consumer-rules.pro -------------------------------------------------------------------------------- /pinyintextview/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 | -------------------------------------------------------------------------------- /pinyintextview/src/androidTest/java/com/liwenwei/pinyintextview/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.liwenwei.pinyintextview; 2 | 3 | import android.content.Context; 4 | 5 | 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | 9 | import androidx.test.InstrumentationRegistry; 10 | import androidx.test.runner.AndroidJUnit4; 11 | 12 | import static org.junit.Assert.*; 13 | 14 | /** 15 | * Instrumented test, which will execute on an Android device. 16 | * 17 | * @see Testing documentation 18 | */ 19 | @RunWith(AndroidJUnit4.class) 20 | public class ExampleInstrumentedTest { 21 | @Test 22 | public void useAppContext() { 23 | // Context of the app under test. 24 | Context appContext = InstrumentationRegistry.getTargetContext(); 25 | 26 | assertEquals("com.liwenwei.pinyintextview.test", appContext.getPackageName()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pinyintextview/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /pinyintextview/src/main/java/com/liwenwei/pinyintextview/MultiScreenSupportUtils.java: -------------------------------------------------------------------------------- 1 | package com.liwenwei.pinyintextview; 2 | 3 | 4 | import android.content.Context; 5 | import android.util.TypedValue; 6 | 7 | public class MultiScreenSupportUtils { 8 | 9 | public static int dp2Px(int dp, Context context) { 10 | return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics()); 11 | } 12 | 13 | public static int sp2Px(int sp, Context context) { 14 | return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, context.getResources().getDisplayMetrics()); 15 | } 16 | 17 | public static float px2Dp(int px, Context context){ 18 | return (int) ((float)px / context.getResources().getDisplayMetrics().density); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /pinyintextview/src/main/java/com/liwenwei/pinyintextview/PinyinTextView.java: -------------------------------------------------------------------------------- 1 | package com.liwenwei.pinyintextview; 2 | 3 | import android.content.Context; 4 | import android.content.res.Resources; 5 | import android.content.res.TypedArray; 6 | import android.graphics.Canvas; 7 | import android.graphics.DashPathEffect; 8 | import android.graphics.Paint; 9 | import android.graphics.Rect; 10 | import android.text.Html; 11 | import android.text.Layout; 12 | import android.text.Layout.Alignment; 13 | import android.text.Spanned; 14 | import android.text.StaticLayout; 15 | import android.text.TextPaint; 16 | import android.text.TextUtils; 17 | import android.util.AttributeSet; 18 | import android.util.DisplayMetrics; 19 | import android.util.Pair; 20 | import android.util.TypedValue; 21 | import android.view.View; 22 | 23 | import java.lang.annotation.Retention; 24 | import java.lang.annotation.RetentionPolicy; 25 | import java.util.ArrayList; 26 | import java.util.List; 27 | 28 | import androidx.annotation.ColorInt; 29 | import androidx.annotation.IntDef; 30 | 31 | /** 32 | * Displays pinyin and text to the user.
33 | * Here is a example how to use this widget in xml. 34 | *
 35 |  * <com.uudove.pinyin.widget.PinyinTextView
 36 |  * android:id="@+id/pinyin_text_view"
 37 |  * android:layout_width="wrap_content"
 38 |  * android:layout_height="wrap_content"
 39 |  * app:horizontalSpacing="10dp"
 40 |  * app:verticalSpacing="10dp"
 41 |  * app:textColor="#ff0000"
 42 |  * app:textSize="20sp"/>
 43 |  * 
44 | * 45 | * @author liwenwei 46 | */ 47 | public class PinyinTextView extends View { 48 | /** 49 | * @hide 50 | */ 51 | @IntDef({TYPE_PLAIN_TEXT, TYPE_PINYIN_AND_TEXT, TYPE_PINYIN}) 52 | @Retention(RetentionPolicy.SOURCE) 53 | public @interface PinyinMode { 54 | } 55 | 56 | /** 57 | * draw only plain text 58 | */ 59 | public static final int TYPE_PLAIN_TEXT = 1; 60 | /** 61 | * draw pinyin and text 62 | */ 63 | public static final int TYPE_PINYIN_AND_TEXT = 2; 64 | /** 65 | * draw only pinyin 66 | */ 67 | public static final int TYPE_PINYIN = 3; 68 | /** 69 | * draw type. Must be one value of {@link #TYPE_PINYIN_AND_TEXT} or {@link #TYPE_PLAIN_TEXT} 70 | */ 71 | private int mDrawType = TYPE_PLAIN_TEXT; 72 | 73 | private static final float PINYIN_TEXT_SIZE_RADIO = 0.5F; 74 | 75 | /** 76 | * Text size in pixels
77 | * Def in xml app:textSize="" 78 | */ 79 | private int mTextSize; 80 | 81 | /** 82 | * Pinyin text size in pixels, default value equals {@link #mTextSize} * {@value #PINYIN_TEXT_SIZE_RADIO} 83 | */ 84 | private int mPinyinTextSize; 85 | 86 | /** 87 | * Text color.
88 | * Def attr in xml app:textColor="" 89 | */ 90 | @ColorInt 91 | private int mTextColor; 92 | 93 | /** 94 | * Text color.
95 | * Def attr in xml app:pinyinColor="" 96 | */ 97 | @ColorInt 98 | private int mPinyinColor; 99 | 100 | /** 101 | * spacing between 2 token.
102 | * Def attr in xml app:horizontalSpacing="" 103 | */ 104 | private int mHorizontalSpacing = 6; 105 | 106 | /** 107 | * Line spacing.
108 | * Def attr in xml app:verticalSpacing="" 109 | */ 110 | private int mLineSpacing = 10; 111 | 112 | /** 113 | * The vertical space(px) between text and underline 114 | */ 115 | private int mUnderlineVerticalSpacing = 14; 116 | 117 | /** 118 | * line spacing (between pinyin and text). 119 | */ 120 | private int mPinyinTextSpacing = 3; 121 | 122 | /** 123 | * Show the underline or not 124 | */ 125 | private boolean mUnderline = false; 126 | 127 | // text & pinyin string 128 | private String mPlainTextString; 129 | private String mTextString; 130 | private String mPinyinString; 131 | 132 | // calculated height of text or pinyin 133 | private int mTextHeight; 134 | private int mPinyinHeight; 135 | 136 | // Pinyin data 137 | private List mPinyinCompats = new ArrayList<>(); 138 | private List mPinyinTokens = new ArrayList<>(); 139 | 140 | // text & pinyin paint 141 | private TextPaint mPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.LINEAR_TEXT_FLAG); 142 | // underline 143 | private Paint mUnderlinePaint = new Paint(); 144 | 145 | // bounds 146 | private Rect mBounds = new Rect(); 147 | 148 | // for draw plain text 149 | private StaticLayout mStaticLayout; 150 | 151 | private boolean debugDraw = false; // for debug, set false when release 152 | private Paint mDebugPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 153 | 154 | public PinyinTextView(Context context, AttributeSet attrs, int defStyleAttr) { 155 | super(context, attrs, defStyleAttr); 156 | init(context, attrs); 157 | } 158 | 159 | public PinyinTextView(Context context, AttributeSet attrs) { 160 | super(context, attrs); 161 | init(context, attrs); 162 | } 163 | 164 | public PinyinTextView(Context context) { 165 | super(context); 166 | init(context, null); 167 | } 168 | 169 | private void init(Context context, AttributeSet attrs) { 170 | if (this.isInEditMode()) { // eclipse preview mode 171 | return; 172 | } 173 | 174 | initDefault(); // initialize default value 175 | 176 | if (attrs == null) { 177 | return; 178 | } 179 | 180 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PinyinTextView); 181 | if (a.hasValue(R.styleable.PinyinTextView_textSize)) { 182 | mTextSize = a.getDimensionPixelSize(R.styleable.PinyinTextView_textSize, mTextSize); 183 | } 184 | if (a.hasValue(R.styleable.PinyinTextView_textColor)) { 185 | mTextColor = a.getColor(R.styleable.PinyinTextView_textColor, mTextColor); 186 | } 187 | if (a.hasValue(R.styleable.PinyinTextView_pinyinColor)) { 188 | mPinyinColor = a.getColor(R.styleable.PinyinTextView_pinyinColor, mPinyinColor); 189 | } else { 190 | mPinyinColor = mTextColor; 191 | } 192 | if (a.hasValue(R.styleable.PinyinTextView_horizontalSpace)) { 193 | mHorizontalSpacing = a.getDimensionPixelSize(R.styleable.PinyinTextView_horizontalSpace, mHorizontalSpacing); 194 | } 195 | if (a.hasValue(R.styleable.PinyinTextView_lineSpace)) { 196 | mLineSpacing = a.getDimensionPixelSize(R.styleable.PinyinTextView_lineSpace, mLineSpacing); 197 | } 198 | if (a.hasValue(R.styleable.PinyinTextView_pinyinTextSpace)) { 199 | mPinyinTextSpacing = a.getDimensionPixelSize(R.styleable.PinyinTextView_pinyinTextSpace, mPinyinTextSpacing); 200 | } 201 | if (a.hasValue(R.styleable.PinyinTextView_underlineVerticalSpace)) { 202 | mUnderlineVerticalSpacing = a.getDimensionPixelSize(R.styleable.PinyinTextView_underlineVerticalSpace, mUnderlineVerticalSpacing); 203 | } 204 | if (a.hasValue(R.styleable.PinyinTextView_underline)) { 205 | mUnderline = a.getBoolean(R.styleable.PinyinTextView_underline, mUnderline); 206 | } 207 | a.recycle(); 208 | setTextSize(mTextSize); 209 | } 210 | 211 | private void initDefault() { 212 | Context c = getContext(); 213 | Resources r; 214 | 215 | if (c == null) { 216 | r = Resources.getSystem(); 217 | } else { 218 | r = c.getResources(); 219 | } 220 | 221 | DisplayMetrics dm = r.getDisplayMetrics(); 222 | 223 | // Text size default 14sp 224 | mTextSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 14, dm); 225 | mPinyinTextSize = (int) (mTextSize * PINYIN_TEXT_SIZE_RADIO); 226 | 227 | // spacing 228 | mHorizontalSpacing = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, dm); 229 | mLineSpacing = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, mLineSpacing, dm); 230 | mPinyinTextSpacing = mHorizontalSpacing / 2; 231 | 232 | // set default text color 233 | mTextColor = 0xff333333; 234 | mPinyinColor = 0xff333333; 235 | 236 | mPaint.setStyle(Paint.Style.FILL); 237 | mDebugPaint.setStyle(Paint.Style.STROKE); 238 | mUnderlinePaint.setARGB(255, 0, 0, 0); 239 | mUnderlinePaint.setStyle(Paint.Style.STROKE); 240 | mUnderlinePaint.setPathEffect(new DashPathEffect(new float[]{2, 2, 2, 2}, 0)); 241 | // The method setPathEffect is not supported by hardware acceleration. By default it is turned on (I think since Android 4.0) 242 | // turn off hardware acceleration 243 | this.setLayerType(View.LAYER_TYPE_SOFTWARE, null); 244 | } 245 | 246 | /** 247 | * Set plain text size in pixels
248 | * Def in xml app:textSize="" 249 | * 250 | * @param px - text size in pixels 251 | */ 252 | public void setTextSize(int px) { 253 | if (px < 2) { 254 | throw new IllegalArgumentException("Text size must larger than 2px"); 255 | } 256 | mTextSize = px; 257 | 258 | setPinyinTextSize((int) (px * PINYIN_TEXT_SIZE_RADIO)); 259 | } 260 | 261 | /** 262 | * Get the plain text size. 263 | * 264 | * @return plain text size. 265 | */ 266 | public int getTextSize() { 267 | return mTextSize; 268 | } 269 | 270 | public String getText() { 271 | return mPlainTextString; 272 | } 273 | 274 | /** 275 | * Set pinyin text size in pixels. If not set, pinyin text size will be the size of {@link #getTextSize()} * 276 | * {@value #PINYIN_TEXT_SIZE_RADIO}.

277 | * Attention:
278 | * Don't use{@link #setTextSize(int)} method after this method is called, which will set pinyin text size to 279 | * {@link #getTextSize()} * {@value #PINYIN_TEXT_SIZE_RADIO}. 280 | * 281 | * @param px - pinyin text size in pixels 282 | */ 283 | private void setPinyinTextSize(int px) { 284 | mPinyinTextSize = px; 285 | if (mPinyinTextSize <= 0) { 286 | throw new IllegalArgumentException("Pinyin text size must larger than 1px"); 287 | } 288 | 289 | // calculate text & pinyin height 290 | calTextHeight(); 291 | 292 | requestLayout(); 293 | invalidate(); 294 | } 295 | 296 | /** 297 | * Set text color. The color must be @ColorInt(#xxxxxx, or getColor(R.color.yourColor), 298 | * not @ColorRes(like R.color.yourColor) 299 | *

300 | * Def in xml app:textColor="" 301 | * 302 | * @param color text color. 303 | */ 304 | public void setTextColor(@ColorInt int color) { 305 | mTextColor = color; 306 | for (Token token : mPinyinTokens) { 307 | token.setTextColor(mTextColor); 308 | } 309 | if (mPinyinTokens.isEmpty()) { 310 | requestLayout(); 311 | invalidate(); 312 | } else { 313 | setPinyinTextByTokens(mPinyinTokens, mDrawType); 314 | } 315 | } 316 | 317 | /** 318 | * Set pinyin text color.
319 | * Def in xml app:pinyinColor="" 320 | * 321 | * @param color pinyin text color. 322 | */ 323 | public void setPinyinColor(@ColorInt int color) { 324 | mPinyinColor = color; 325 | for (Token token : mPinyinTokens) { 326 | token.setPinyinColor(mPinyinColor); 327 | } 328 | if (mPinyinTokens.isEmpty()) { 329 | requestLayout(); 330 | invalidate(); 331 | } else { 332 | setPinyinTextByTokens(mPinyinTokens, mDrawType); 333 | } 334 | } 335 | 336 | /** 337 | * Set line spacing in pixels.
338 | * The same as method {@link #setHorizontalSpacing(int)} 339 | * 340 | * @param px line spacing in pixels. 341 | * @see #setHorizontalSpacing(int) 342 | */ 343 | public void setLineSpacing(int px) { 344 | mLineSpacing = px; 345 | requestLayout(); 346 | invalidate(); 347 | } 348 | 349 | public void setPinyinTextSpacing(int px) { 350 | mPinyinTextSpacing = px; 351 | requestLayout(); 352 | invalidate(); 353 | } 354 | 355 | public void setUnderlineVerticalSpacing(int px) { 356 | this.mUnderlineVerticalSpacing = px; 357 | } 358 | 359 | public boolean isShowUnderline() { 360 | return this.mUnderline; 361 | } 362 | 363 | public void setUnderline(boolean isShow) { 364 | this.mUnderline = isShow; 365 | invalidate(); 366 | } 367 | 368 | /** 369 | * Set horizontal space between two tokens.
370 | * Def in xml app:horizontalSpacing="" 371 | * 372 | * @param px line spacing in pixels. 373 | */ 374 | public void setHorizontalSpacing(int px) { 375 | mHorizontalSpacing = px; 376 | mPinyinTextSpacing = mHorizontalSpacing / 2; // half of line spacing 377 | requestLayout(); 378 | invalidate(); 379 | } 380 | 381 | public void setPinyinText(List> pinyinList, @PinyinMode int mode) { 382 | mPinyinTokens.clear(); 383 | for (Pair pair : pinyinList) { 384 | Token token = new Token(); 385 | token.setText(pair.first); 386 | token.setTextColor(mTextColor); 387 | // If the toke is punctuation, the pinyin is empty, set the pinyin as punctuation 388 | // why we do this? 389 | // because we need to draw the punctuation at TYPE_PINYIN and TYPE_PLAIN_TEXT mode 390 | if (TextUtils.isEmpty(pair.second) && isPunctuation(pair.first)) { 391 | token.setPinyin(pair.first); 392 | } else { 393 | token.setPinyin(pair.second); 394 | } 395 | token.setPinyinColor(mPinyinColor); 396 | mPinyinTokens.add(token); 397 | } 398 | setPinyinTextByTokens(mPinyinTokens, mode); 399 | } 400 | 401 | /** 402 | * Set the single pinyin. 403 | */ 404 | public void setPinyinText(Pair pair, @PinyinMode int mode) { 405 | List> pairs = new ArrayList<>(); 406 | pairs.add(pair); 407 | setPinyinText(pairs, mode); 408 | } 409 | 410 | /** 411 | * Init the PinyinTextView with Chinese-Pinyin pair list. 412 | * 413 | * @param pinyinList Chinese-Pinyin pair list, like Pair.create("你", "nǐ"), if the 414 | * string is special character, set the pinyin is empty string, like 415 | * Pair.create("!", " ") 416 | * @param mode 417 | */ 418 | public void setPinyinTextByTokens(List pinyinList, @PinyinMode int mode) { 419 | mDrawType = mode; // set draw type 420 | clearAll(); // clear what is shown 421 | mPinyinTokens = pinyinList; 422 | StringBuilder plainTextBuilder = new StringBuilder(); 423 | StringBuilder textBuilder = new StringBuilder(); 424 | StringBuilder pinyinBuilder = new StringBuilder(); 425 | for (Token token : pinyinList) { 426 | String src = token.getText(); 427 | String trg = token.getPinyin(); 428 | if (src == null) { 429 | src = ""; 430 | } 431 | if (TextUtils.isEmpty(trg)) { 432 | trg = ""; 433 | } 434 | pinyinBuilder.append(convertTokenToHtml(trg, convertColorHexString(token.pinyinColor))); 435 | textBuilder.append(convertTokenToHtml(src, convertColorHexString(token.textColor))); 436 | plainTextBuilder.append(src); 437 | 438 | PinyinCompat compat = new PinyinCompat(); 439 | compat.text = src; 440 | compat.textColor = token.getTextColor() == 0 ? mTextColor : token.getTextColor(); 441 | compat.pinyin = trg; 442 | compat.pinyinColor = token.getPinyinColor() == 0 ? mPinyinColor : token.getPinyinColor(); 443 | compat.textRect = new Rect(); 444 | compat.pinyinRect = new Rect(); 445 | compat.pinyinTextRect = new Rect(); 446 | mPinyinCompats.add(compat); 447 | } 448 | 449 | // string buffer 450 | mTextString = textBuilder.toString(); 451 | mPlainTextString = plainTextBuilder.toString(); 452 | mPinyinString = pinyinBuilder.toString(); 453 | 454 | // calculate text & pinyin height 455 | calTextHeight(); 456 | requestLayout(); 457 | invalidate(); 458 | } 459 | 460 | /** 461 | * Display only plain text to user, like TextView 462 | * 463 | * @param text plain text to display. 464 | */ 465 | public void setText(String text) { 466 | mDrawType = TYPE_PLAIN_TEXT; // set draw type 467 | clearAll(); 468 | this.mPlainTextString = text; 469 | this.mTextString = text; 470 | this.mPinyinString = text; 471 | requestLayout(); 472 | invalidate(); 473 | } 474 | 475 | public void setMode(@PinyinMode int mode) { 476 | mDrawType = mode; 477 | calTextHeight(); 478 | requestLayout(); 479 | invalidate(); 480 | } 481 | 482 | /** 483 | * Set whether draw debug rect. 484 | * 485 | * @param debugDraw debug mode. 486 | */ 487 | public void setDebugDraw(boolean debugDraw) { 488 | this.debugDraw = debugDraw; 489 | } 490 | 491 | private void clearAll() { 492 | mPinyinCompats.clear(); // clear 493 | 494 | mPlainTextString = null; 495 | mTextString = null; 496 | mPinyinString = null; 497 | 498 | mTextHeight = 0; 499 | mPinyinHeight = 0; 500 | } 501 | 502 | /** 503 | * calculate text & pinyin height 504 | *

505 | * Why we calculate the text height by hard code text, not the {@link PinyinTextView#mTextString} 506 | * and {@link PinyinTextView#mPinyinString} ? 507 | *

508 | * Sometimes, we have to align multiple PinyinTextView by horizontal, if we measure different text, 509 | * we get different height, so we have to measure the same text to keep the same height 510 | */ 511 | private void calTextHeight() { 512 | // calculate text height 513 | String chinese = "你好"; 514 | mPaint.setTextSize(mTextSize); 515 | mPaint.getTextBounds(chinese, 0, chinese.length(), mBounds); 516 | mTextHeight = mBounds.height(); 517 | 518 | // calculate pinyin height 519 | String pinyin = "āáǎàaHhJjPpYyGg"; 520 | if (mDrawType == TYPE_PINYIN) { 521 | mPaint.setTextSize(mTextSize); 522 | } else { 523 | mPaint.setTextSize(mPinyinTextSize); 524 | } 525 | mPaint.getTextBounds(pinyin, 0, pinyin.length() - 1, mBounds); 526 | mPinyinHeight = mBounds.height(); 527 | } 528 | 529 | @Override 530 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 531 | if (mPinyinCompats.isEmpty()) { 532 | measurePlainText(widthMeasureSpec, heightMeasureSpec); 533 | } else { 534 | if (mDrawType == TYPE_PINYIN_AND_TEXT) { 535 | measurePinyinText(widthMeasureSpec, heightMeasureSpec); 536 | } else if (mDrawType == TYPE_PLAIN_TEXT && !TextUtils.isEmpty(mTextString)) { 537 | measurePlainText(widthMeasureSpec, heightMeasureSpec); 538 | } else if (mDrawType == TYPE_PINYIN && !TextUtils.isEmpty(mPinyinString)) { 539 | measurePinyin(widthMeasureSpec, heightMeasureSpec); 540 | } else { 541 | measureDefault(widthMeasureSpec, heightMeasureSpec); 542 | } 543 | } 544 | } 545 | 546 | private void measureDefault(int widthMeasureSpec, int heightMeasureSpec) { 547 | 548 | // max allowed width or height 549 | int sizeWidth = MeasureSpec.getSize(widthMeasureSpec); 550 | int sizeHeight = MeasureSpec.getSize(heightMeasureSpec); 551 | 552 | // mode 553 | int modeWidth = MeasureSpec.getMode(widthMeasureSpec); 554 | int modeHeight = MeasureSpec.getMode(heightMeasureSpec); 555 | 556 | // measured width and height 557 | int measuredWidth = 558 | modeWidth == MeasureSpec.EXACTLY ? sizeWidth : getPaddingLeft() + getPaddingRight(); 559 | int measuredHeight = 560 | modeHeight == MeasureSpec.EXACTLY ? sizeHeight : getPaddingTop() + getPaddingBottom(); 561 | 562 | setMeasuredDimension(measuredWidth, measuredHeight); 563 | } 564 | 565 | @SuppressWarnings("PMD") 566 | private void measurePinyinText(int widthMeasureSpec, int heightMeasureSpec) { 567 | int paddingLeft = this.getPaddingLeft(); 568 | int paddingRight = this.getPaddingRight(); 569 | int paddingTop = this.getPaddingTop(); 570 | int paddingBottom = this.getPaddingBottom(); 571 | 572 | // max allowed width or height 573 | int sizeWidth = MeasureSpec.getSize(widthMeasureSpec) - paddingLeft - paddingRight; 574 | int sizeHeight = MeasureSpec.getSize(heightMeasureSpec) - paddingTop - paddingBottom; 575 | 576 | // mode 577 | int modeWidth = MeasureSpec.getMode(widthMeasureSpec); 578 | int modeHeight = MeasureSpec.getMode(heightMeasureSpec); 579 | 580 | // measured width and height 581 | int measuredWidth = modeWidth == MeasureSpec.EXACTLY ? sizeWidth : 0; 582 | int measuredHeight = modeHeight == MeasureSpec.EXACTLY ? sizeHeight : 0; 583 | 584 | int line = 0; 585 | int col = 0; 586 | int lineLength = 0; 587 | int baseLine = 0; // top of pinyin 588 | boolean newLine = false; 589 | 590 | for (PinyinCompat compat : mPinyinCompats) { 591 | int textWidth = getTextWidth(compat.text, mTextSize); 592 | int pinyinWidth = getTextWidth(compat.pinyin, mPinyinTextSize); 593 | 594 | int maxWidth = Math.max(textWidth, pinyinWidth); 595 | 596 | if (newLine) { 597 | line++; 598 | col = 0; 599 | newLine = false; 600 | } 601 | 602 | if (lineLength + maxWidth + (col == 0 ? 0 : mHorizontalSpacing) > sizeWidth) { // new row 603 | lineLength = maxWidth; 604 | 605 | baseLine += mTextHeight + mPinyinHeight + mPinyinTextSpacing + mLineSpacing; 606 | // TODO: add the underline vertical space if show underline 607 | 608 | if (modeWidth != MeasureSpec.EXACTLY) { 609 | measuredWidth = sizeWidth; 610 | } 611 | 612 | newLine = true; 613 | } else { 614 | if (col != 0 || line != 0) { // not the first item of first row 615 | lineLength += mHorizontalSpacing; 616 | } 617 | lineLength += maxWidth; 618 | 619 | if (modeWidth != MeasureSpec.EXACTLY && measuredWidth < lineLength) { 620 | measuredWidth = lineLength; 621 | if (measuredWidth > sizeWidth) { 622 | measuredWidth = sizeWidth; 623 | } 624 | } 625 | col++; 626 | } 627 | 628 | // Center the pinyin/text 629 | int pinyinBias = 0; 630 | int textBias = 0; 631 | if (pinyinWidth < textWidth) { 632 | pinyinBias = (textWidth - pinyinWidth) / 2; 633 | } else { 634 | textBias = (pinyinWidth - textWidth) / 2; 635 | } 636 | compat.pinyinRect.left = lineLength - maxWidth + pinyinBias; 637 | compat.pinyinRect.right = compat.pinyinRect.left + pinyinWidth; 638 | compat.pinyinRect.top = baseLine; 639 | compat.pinyinRect.bottom = compat.pinyinRect.top + mPinyinHeight; 640 | 641 | compat.textRect.left = lineLength - maxWidth + textBias; 642 | compat.textRect.right = compat.textRect.left + textWidth; 643 | compat.textRect.top = compat.pinyinRect.bottom + mPinyinTextSpacing; 644 | compat.textRect.bottom = compat.textRect.top + mTextHeight; 645 | 646 | compat.pinyinTextRect.left = lineLength - maxWidth; 647 | compat.pinyinTextRect.right = compat.pinyinRect.left + Math.max(pinyinWidth, textWidth); 648 | compat.pinyinTextRect.top = baseLine; 649 | compat.pinyinTextRect.bottom = compat.pinyinRect.top + mPinyinHeight + mPinyinTextSpacing + mTextHeight; 650 | } 651 | 652 | if (modeHeight != MeasureSpec.EXACTLY) { 653 | measuredHeight = baseLine + mPinyinHeight + mPinyinTextSpacing + mTextHeight + mTextHeight / 4; 654 | } 655 | 656 | setMeasuredDimension(measuredWidth + paddingLeft + paddingRight, measuredHeight + paddingTop + paddingBottom); 657 | } 658 | 659 | private void measurePlainText(int widthMeasureSpec, int heightMeasureSpec) { 660 | measureText(widthMeasureSpec, heightMeasureSpec, mTextString, mTextSize); 661 | } 662 | 663 | private void measurePinyin(int widthMeasureSpec, int heightMeasureSpec) { 664 | measureText(widthMeasureSpec, heightMeasureSpec, mPinyinString, mTextSize); 665 | } 666 | 667 | private void measureText(int widthMeasureSpec, int heightMeasureSpec, String text, float textSize) { 668 | int paddingLeft = this.getPaddingLeft(); 669 | int paddingRight = this.getPaddingRight(); 670 | int paddingTop = this.getPaddingTop(); 671 | int paddingBottom = this.getPaddingBottom(); 672 | 673 | // max allowed width or height 674 | int sizeWidth = MeasureSpec.getSize(widthMeasureSpec) - paddingLeft - paddingRight; 675 | int sizeHeight = MeasureSpec.getSize(heightMeasureSpec) - paddingTop - paddingBottom; 676 | 677 | // mode 678 | int modeWidth = MeasureSpec.getMode(widthMeasureSpec); 679 | int modeHeight = MeasureSpec.getMode(heightMeasureSpec); 680 | 681 | // calculate text width and height 682 | mPaint.setColor(mTextColor); 683 | mPaint.setTextSize(textSize); 684 | Spanned htmlSpan = fromHtml(text); 685 | mStaticLayout = new StaticLayout(htmlSpan, mPaint, sizeWidth, Alignment.ALIGN_NORMAL, 1.0f, 0, false); 686 | 687 | // measured width and height 688 | int measuredWidth = 689 | modeWidth == MeasureSpec.EXACTLY 690 | ? sizeWidth 691 | : Math.min(sizeWidth, (int) Math.ceil(Layout.getDesiredWidth(htmlSpan, mPaint))); 692 | int measuredHeight = 693 | modeHeight == MeasureSpec.EXACTLY 694 | ? sizeHeight 695 | : mStaticLayout.getHeight(); 696 | if (mUnderline) { 697 | measuredHeight += mUnderlineVerticalSpacing; 698 | } 699 | 700 | setMeasuredDimension(measuredWidth + paddingLeft + paddingRight, measuredHeight + paddingTop + paddingBottom); 701 | } 702 | 703 | private String convertColorHexString(int color) { 704 | return String.format("#%06X", 0xFFFFFF & color); 705 | } 706 | 707 | private String convertTokenToHtml(String text, String textColor) { 708 | return String.format("%s", textColor, text); 709 | } 710 | 711 | private Spanned fromHtml(String html) { 712 | if (html == null) { 713 | html = ""; 714 | } 715 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { 716 | return Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY); 717 | } else { 718 | return Html.fromHtml(html); 719 | } 720 | } 721 | 722 | @Override 723 | protected void onDraw(Canvas canvas) { 724 | super.onDraw(canvas); 725 | if (this.isInEditMode()) { // eclipse preview mode 726 | return; 727 | } 728 | 729 | if (mPinyinCompats.isEmpty()) { 730 | drawPlainText(canvas); 731 | } else { 732 | if (mDrawType == TYPE_PINYIN_AND_TEXT) { 733 | drawPinyinAndText(canvas); 734 | } else if (mDrawType == TYPE_PLAIN_TEXT) { 735 | drawPlainText(canvas); 736 | } else if (mDrawType == TYPE_PINYIN) { 737 | drawPinyin(canvas); 738 | } 739 | } 740 | } 741 | 742 | private void drawPinyinAndText(Canvas canvas) { 743 | int paddingLeft = this.getPaddingLeft(); 744 | int paddingTop = this.getPaddingTop(); 745 | 746 | for (int i = 0; i < mPinyinCompats.size(); i++) { 747 | PinyinCompat compat = mPinyinCompats.get(i); 748 | 749 | // draw pinyin 750 | mPaint.setColor(compat.pinyinColor); 751 | mPaint.setTextSize(mPinyinTextSize); 752 | compat.pinyinRect.offset(paddingLeft, paddingTop); 753 | // If the draw mode is TYPE_PINYIN_AND_TEXT, don't draw the pinyin if it's punctuation 754 | if (!isPunctuation(compat.pinyin)) { 755 | canvas.drawText(compat.pinyin, compat.pinyinRect.left, compat.pinyinRect.bottom, mPaint); 756 | } 757 | 758 | // draw text 759 | mPaint.setColor(compat.textColor); 760 | mPaint.setTextSize(mTextSize); 761 | compat.textRect.offset(paddingLeft, paddingTop); 762 | canvas.drawText(compat.text, compat.textRect.left, compat.textRect.bottom, mPaint); 763 | 764 | if (mUnderline && !isPunctuation(compat.text)) { 765 | canvas.drawLine( 766 | compat.pinyinTextRect.left, 767 | compat.pinyinTextRect.bottom + mUnderlineVerticalSpacing, 768 | compat.pinyinTextRect.left + compat.pinyinTextRect.width() + mHorizontalSpacing, 769 | compat.pinyinTextRect.bottom + mUnderlineVerticalSpacing, 770 | mUnderlinePaint); 771 | } 772 | 773 | if (debugDraw) { 774 | mDebugPaint.setColor(mTextColor); 775 | canvas.drawRect(compat.textRect, mDebugPaint); 776 | } 777 | 778 | if (debugDraw) { 779 | mDebugPaint.setColor(mTextColor); 780 | canvas.drawRect(compat.pinyinRect, mDebugPaint); 781 | } 782 | 783 | if (debugDraw) { 784 | mDebugPaint.setColor(mTextColor); 785 | canvas.drawRect(compat.pinyinTextRect, mDebugPaint); 786 | } 787 | } 788 | } 789 | 790 | private void drawPlainText(Canvas canvas) { 791 | drawText(canvas); 792 | } 793 | 794 | // If TYPE_PINYIN or only show Pinyin, we will set the pinyin text color and text size same as the 795 | // plain text (mTextColor, mTextSize) 796 | private void drawPinyin(Canvas canvas) { 797 | drawText(canvas); 798 | } 799 | 800 | private void drawText(Canvas canvas) { 801 | if (mStaticLayout != null) { 802 | int paddingLeft = this.getPaddingLeft(); 803 | int paddingTop = this.getPaddingTop(); 804 | canvas.translate(paddingLeft, paddingTop); 805 | 806 | mStaticLayout.draw(canvas); 807 | 808 | if (mUnderline && !isPunctuation(mTextString)) { 809 | for (int i = 0; i < mStaticLayout.getLineCount(); i++) { 810 | canvas.drawLine( 811 | mStaticLayout.getLineLeft(i), 812 | mStaticLayout.getLineBottom(i), 813 | mStaticLayout.getLineRight(i), 814 | mStaticLayout.getLineBottom(i), 815 | mUnderlinePaint); 816 | } 817 | } 818 | } 819 | } 820 | 821 | private boolean isPunctuation(String text) { 822 | if (TextUtils.isEmpty(text)) { 823 | return false; 824 | } 825 | text = text.trim(); 826 | if (text.length() != 1) { 827 | return false; 828 | } 829 | return StringUtils.isPunctuation(text.charAt(0)); 830 | } 831 | 832 | private int getTextWidth(String text, int textSize) { 833 | mPaint.setTextSize(textSize); 834 | 835 | return (int) Math.ceil(Layout.getDesiredWidth(text, mPaint)); 836 | } 837 | 838 | static class PinyinCompat { 839 | String text; 840 | @ColorInt 841 | int textColor; 842 | String pinyin; 843 | @ColorInt 844 | int pinyinColor; 845 | 846 | Rect pinyinTextRect; 847 | Rect textRect; 848 | Rect pinyinRect; 849 | } 850 | 851 | public static class Token { 852 | private String text; 853 | private @ColorInt 854 | int textColor = 0; 855 | private String pinyin; 856 | private @ColorInt 857 | int pinyinColor = 0; 858 | 859 | public Token() { 860 | 861 | } 862 | 863 | public Token(String text, @ColorInt int textColor, String pinyin, @ColorInt int pinyinColor) { 864 | this.text = text; 865 | this.textColor = textColor; 866 | this.pinyin = pinyin; 867 | this.pinyinColor = pinyinColor; 868 | } 869 | 870 | public String getText() { 871 | return text; 872 | } 873 | 874 | public void setText(String text) { 875 | this.text = text; 876 | } 877 | 878 | public String getPinyin() { 879 | return pinyin; 880 | } 881 | 882 | public void setPinyin(String pinyin) { 883 | this.pinyin = pinyin; 884 | } 885 | 886 | public int getTextColor() { 887 | return textColor; 888 | } 889 | 890 | public void setTextColor(int textColor) { 891 | this.textColor = textColor; 892 | } 893 | 894 | public int getPinyinColor() { 895 | return pinyinColor; 896 | } 897 | 898 | public void setPinyinColor(int pinyinColor) { 899 | this.pinyinColor = pinyinColor; 900 | } 901 | } 902 | } 903 | -------------------------------------------------------------------------------- /pinyintextview/src/main/java/com/liwenwei/pinyintextview/StringUtils.java: -------------------------------------------------------------------------------- 1 | package com.liwenwei.pinyintextview; 2 | 3 | 4 | import android.graphics.Color; 5 | import android.text.SpannableString; 6 | import android.text.Spanned; 7 | import android.text.method.LinkMovementMethod; 8 | import android.text.style.ClickableSpan; 9 | import android.widget.TextView; 10 | 11 | import java.util.Locale; 12 | 13 | /** 14 | * StringUtils 15 | * Utils related to {@link String} operations. 16 | */ 17 | public class StringUtils { 18 | 19 | /** 20 | * logic about making link inside a TextView 21 | */ 22 | public static void makeLinks(TextView textView, String[] links, ClickableSpan[] clickableSpans) { 23 | SpannableString spannableString = new SpannableString(textView.getText()); 24 | for (int i = 0; i < links.length; i++) { 25 | ClickableSpan clickableSpan = clickableSpans[i]; 26 | String link = links[i]; 27 | 28 | int startIndexOfLink = textView.getText().toString().indexOf(link); 29 | spannableString.setSpan(clickableSpan, startIndexOfLink, 30 | startIndexOfLink + link.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 31 | } 32 | textView.setHighlightColor( 33 | Color.TRANSPARENT); // prevent TextView change background when highlight 34 | textView.setMovementMethod(LinkMovementMethod.getInstance()); 35 | textView.setText(spannableString, TextView.BufferType.SPANNABLE); 36 | } 37 | 38 | public static boolean isSymbol(char ch) { 39 | if (isCnSymbol(ch)) { 40 | return true; 41 | } 42 | if (isEnSymbol(ch)) { 43 | return true; 44 | } 45 | 46 | if (0x2010 <= ch && ch <= 0x2017) { 47 | return true; 48 | } 49 | if (0x2020 <= ch && ch <= 0x2027) { 50 | return true; 51 | } 52 | if (0x2B00 <= ch && ch <= 0x2BFF) { 53 | return true; 54 | } 55 | if (0xFF03 <= ch && ch <= 0xFF06) { 56 | return true; 57 | } 58 | if (0xFF08 <= ch && ch <= 0xFF0B) { 59 | return true; 60 | } 61 | if (ch == 0xFF0D || ch == 0xFF0F) { 62 | return true; 63 | } 64 | if (0xFF1C <= ch && ch <= 0xFF1E) { 65 | return true; 66 | } 67 | if (ch == 0xFF20 || ch == 0xFF65) { 68 | return true; 69 | } 70 | if (0xFF3B <= ch && ch <= 0xFF40) { 71 | return true; 72 | } 73 | if (0xFF5B <= ch && ch <= 0xFF60) { 74 | return true; 75 | } 76 | if (ch == 0xFF62 || ch == 0xFF63) { 77 | return true; 78 | } 79 | if (ch == 0x0020 || ch == 0x3000) { 80 | return true; 81 | } 82 | return false; 83 | 84 | } 85 | 86 | public static boolean isCnSymbol(char ch) { 87 | if (0x3004 <= ch && ch <= 0x301C) { 88 | return true; 89 | } 90 | if (0x3020 <= ch && ch <= 0x303F) { 91 | return true; 92 | } 93 | return false; 94 | } 95 | 96 | public static boolean isEnSymbol(char ch) { 97 | 98 | if (ch == 0x40) { 99 | return true; 100 | } 101 | if (ch == 0x2D || ch == 0x2F) { 102 | return true; 103 | } 104 | if (0x23 <= ch && ch <= 0x26) { 105 | return true; 106 | } 107 | if (0x28 <= ch && ch <= 0x2B) { 108 | return true; 109 | } 110 | if (0x3C <= ch && ch <= 0x3E) { 111 | return true; 112 | } 113 | if (0x5B <= ch && ch <= 0x60) { 114 | return true; 115 | } 116 | if (0x7B <= ch && ch <= 0x7E) { 117 | return true; 118 | } 119 | 120 | return false; 121 | } 122 | 123 | public static boolean isPunctuation(char ch) { 124 | if (isCjkPunc(ch)) { 125 | return true; 126 | } 127 | if (isEnPunc(ch)) { 128 | return true; 129 | } 130 | 131 | if (0x2018 <= ch && ch <= 0x201F) { 132 | return true; 133 | } 134 | if (ch == 0xFF01 || ch == 0xFF02) { 135 | return true; 136 | } 137 | if (ch == 0xFF07 || ch == 0xFF0C) { 138 | return true; 139 | } 140 | if (ch == 0xFF1A || ch == 0xFF1B) { 141 | return true; 142 | } 143 | if (ch == 0xFF1F || ch == 0xFF61) { 144 | return true; 145 | } 146 | if (ch == 0xFF0E) { 147 | return true; 148 | } 149 | if (ch == 0xFF65) { 150 | return true; 151 | } 152 | 153 | return false; 154 | } 155 | 156 | public static boolean isEnPunc(char ch) { 157 | if (0x21 <= ch && ch <= 0x22) { 158 | return true; 159 | } 160 | if (ch == 0x27 || ch == 0x2C) { 161 | return true; 162 | } 163 | if (ch == 0x2E || ch == 0x3A) { 164 | return true; 165 | } 166 | if (ch == 0x3B || ch == 0x3F) { 167 | return true; 168 | } 169 | 170 | return false; 171 | } 172 | 173 | public static boolean isCjkPunc(char ch) { 174 | if (0x3001 <= ch && ch <= 0x3003) { 175 | return true; 176 | } 177 | if (0x301D <= ch && ch <= 0x301F) { 178 | return true; 179 | } 180 | 181 | return false; 182 | } 183 | 184 | public static String toUpperFirstLetter(String str) { 185 | if (str == null || str.length() == 0) { 186 | return str; 187 | } 188 | return str.substring(0, 1).toUpperCase(Locale.getDefault()) + str.substring(1); 189 | } 190 | 191 | public static boolean isInteger(String s) { 192 | return isInteger(s, 10); 193 | } 194 | 195 | public static boolean isInteger(String s, int radix) { 196 | if (s.isEmpty()) { 197 | return false; 198 | } 199 | for (int i = 0; i < s.length(); i++) { 200 | if (i == 0 && s.charAt(i) == '-') { 201 | if (s.length() == 1) { 202 | return false; 203 | } else { 204 | continue; 205 | } 206 | } 207 | if (Character.digit(s.charAt(i), radix) < 0) { 208 | return false; 209 | } 210 | } 211 | return true; 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /pinyintextview/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /pinyintextview/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | PinyinTextView 3 | 4 | -------------------------------------------------------------------------------- /pinyintextview/src/test/java/com/liwenwei/pinyintextview/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.liwenwei.pinyintextview; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':pinyintextview' 2 | rootProject.name='PinyinTextView' 3 | --------------------------------------------------------------------------------