├── .gitignore ├── .idea ├── copyright │ ├── Apache.xml │ └── profiles_settings.xml └── encodings.xml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── zp │ │ └── smaple │ │ ├── HoriActivity.java │ │ ├── MainActivity.java │ │ ├── StringContentUtil.java │ │ └── VertActivity.java │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── layout │ ├── activity_hori.xml │ ├── activity_main.xml │ └── activity_vert.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 ├── build.gradle ├── gradle.properties ├── images ├── selectabletextview.gif └── verticaltextview.gif ├── library ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── devilist │ │ └── advancedtextview │ │ ├── ActionMenu.java │ │ ├── CustomActionMenuCallBack.java │ │ ├── SelectableTextView.java │ │ ├── Utils.java │ │ └── VerticalTextView.java │ └── res │ └── values │ ├── attrs_selectable_text.xml │ ├── attrs_vertical_textview.xml │ └── strings.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | -------------------------------------------------------------------------------- /.idea/copyright/Apache.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /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 | # AdvancedTextView 2 | 3 | 这是一个增强的TextView库。可以实现文字的两端对齐,文字竖排,以及自定义的弹出菜单。 4 | 5 | 具体介绍请移步博客: 6 | 7 | https://blog.csdn.net/devilist/article/details/54911641 8 | 9 | https://blog.csdn.net/devilist/article/details/79236665 10 | 11 | 本库目前提供两个控件 SelectableTextView 和 VerticalTexview。 12 | 13 | # 1. SelectableTextView 14 | 15 | ![image](https://github.com/devilist/AdvancedTextView/raw/master/images/selectabletextview.gif) 16 | 17 | 在布局中引用: 18 | 19 | ``` 20 |                          // 是否启用两端对齐 33 | ``` 34 | 35 | 也可以在代码中设置: 36 | 37 | ``` 38 |        selectableTextView.setTextJustify(true); // 是否启用两端对齐 默认启用 39 |        selectableTextView.setForbiddenActionMenu(false);         // 是否禁用自定义ActionMenu 默认启用 40 |        selectableTextView.setTextHighlightColor(0xff48543e); // 文本高亮色 41 | ``` 42 | 注意:在代码中调用上述三个方法后需要 调用 inviladite() 或 postInviladite()方法通知View重绘 43 | 44 | 设置ActionMenu菜单点击监听: 45 | 46 | ``` 47 | selectableTextView.setCustomActionMenuCallBack(new CustomActionMenuCallBack() { 48 | @Override 49 | public boolean onCreateCustomActionMenu(ActionMenu menu) { 50 | menu.setActionMenuBgColor(0xff666666); // ActionMenu背景色 51 | menu.setMenuItemTextColor(0xffffffff); // ActionMenu文字颜色 52 | List titleList = new ArrayList<>(); 53 | titleList.add("翻译"); 54 | titleList.add("分享"); 55 | titleList.add("分享"); 56 | menu.addCustomMenuItem(titleList); // 添加菜单 57 | return false; // 返回false,保留默认菜单(全选/复制);返回true,移除默认菜单 58 | } 59 | 60 | @Override 61 | public void onCustomActionItemClicked(String itemTitle, String selectedContent) { 62 | Toast.makeText(this, "ActionMenu: " + itemTitle, Toast.LENGTH_SHORT).show(); 63 | } 64 | }); 65 | ``` 66 | 67 | # 2. VerticalTextView 68 | 69 | ![image](https://github.com/devilist/AdvancedTextView/raw/master/images/verticaltextview.gif) 70 | 71 | 在布局中引用: 72 | 73 | ``` 74 | // 下划线偏移量 92 | ``` 93 | 94 | 在代码中设置: 95 | 96 | ``` 97 | vtv_text_ltr.setLeftToRight(true) // 文字是否从左向右排版,默认从右向左排版 98 | .setLineSpacingExtra(10) // 行间距 99 | .setCharSpacingExtra(2) // 字符间距 100 | .setUnderLineText(true) // 是否显示下划线,默认不显示 101 | .setShowActionMenu(true) // 是否开启ActionMenu,默认关闭 102 | .setUnderLineColor(0xffCEAD53) // 下划线颜色 103 | .setUnderLineWidth(1.0f) // 下划线线宽 104 | .setUnderLineOffset(3) // 下划线偏移量 105 | .setTextHighlightColor(0xffCEAD53) // 选中文字高亮色 106 | .setCustomActionMenuCallBack(this); // ActionMenu菜单点击监听 107 | ``` 108 | 注意:在代码中调用上述方法后需要 调用 requestLayout()方法通知View重新布局 109 | 110 | 设置ActionMenu菜单点击监听和SelectableTextView一样。 111 | 112 | 113 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 zengp 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | apply plugin: 'com.android.application' 18 | 19 | android { 20 | compileSdkVersion 28 21 | defaultConfig { 22 | applicationId "com.zp.smaple" 23 | minSdkVersion 14 24 | targetSdkVersion 28 25 | versionCode 1 26 | versionName "1.0" 27 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 28 | } 29 | buildTypes { 30 | release { 31 | minifyEnabled false 32 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 33 | } 34 | debug { 35 | minifyEnabled false 36 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 37 | } 38 | } 39 | } 40 | 41 | dependencies { 42 | implementation fileTree(dir: 'libs', include: ['*.jar']) 43 | implementation 'androidx.appcompat:appcompat:1.0.2' 44 | implementation project(':library') 45 | } 46 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 25 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/zp/smaple/HoriActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 zengp 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.zp.smaple; 18 | 19 | import android.content.Context; 20 | import android.content.Intent; 21 | import android.os.Bundle; 22 | import android.text.Html; 23 | import android.util.Log; 24 | import android.view.View; 25 | import android.widget.RadioButton; 26 | import android.widget.RadioGroup; 27 | import android.widget.Toast; 28 | 29 | import androidx.appcompat.app.AppCompatActivity; 30 | 31 | import com.devilist.advancedtextview.ActionMenu; 32 | import com.devilist.advancedtextview.CustomActionMenuCallBack; 33 | import com.devilist.advancedtextview.SelectableTextView; 34 | 35 | import java.util.ArrayList; 36 | import java.util.List; 37 | 38 | /** 39 | * Created by zengp on 2017/12/2. 40 | */ 41 | 42 | public class HoriActivity extends AppCompatActivity implements 43 | RadioGroup.OnCheckedChangeListener, CustomActionMenuCallBack { 44 | 45 | private RadioGroup rg_text_gravity; 46 | private RadioGroup rg_text_content; 47 | 48 | private SelectableTextView selectableTextView; 49 | 50 | public static void start(Context context) { 51 | Intent starter = new Intent(context, HoriActivity.class); 52 | context.startActivity(starter); 53 | } 54 | 55 | @Override 56 | protected void onCreate(Bundle savedInstanceState) { 57 | super.onCreate(savedInstanceState); 58 | setContentView(R.layout.activity_hori); 59 | initView(); 60 | } 61 | 62 | private void initView() { 63 | selectableTextView = (SelectableTextView) findViewById(R.id.ctv_content); 64 | selectableTextView.setText(Html.fromHtml(StringContentUtil.str_hanzi).toString()); 65 | selectableTextView.clearFocus(); 66 | selectableTextView.setTextJustify(true); 67 | selectableTextView.setForbiddenActionMenu(false); 68 | selectableTextView.setCustomActionMenuCallBack(this); 69 | selectableTextView.setOnClickListener(new View.OnClickListener() { 70 | @Override 71 | public void onClick(View v) { 72 | Toast.makeText(HoriActivity.this, "SelectableTextView 的onClick事件", Toast.LENGTH_SHORT).show(); 73 | } 74 | }); 75 | 76 | rg_text_gravity = (RadioGroup) findViewById(R.id.rg_text_gravity); 77 | rg_text_content = (RadioGroup) findViewById(R.id.rg_text_content); 78 | ((RadioButton) findViewById(R.id.rb_justify)).setChecked(true); 79 | ((RadioButton) findViewById(R.id.rb_hanzi)).setChecked(true); 80 | rg_text_gravity.setOnCheckedChangeListener(this); 81 | rg_text_content.setOnCheckedChangeListener(this); 82 | } 83 | 84 | @Override 85 | public void onCheckedChanged(RadioGroup group, int checkedId) { 86 | switch (checkedId) { 87 | case R.id.rb_justify: 88 | selectableTextView.setTextJustify(true); 89 | selectableTextView.postInvalidate(); 90 | break; 91 | case R.id.rb_left: 92 | selectableTextView.setTextJustify(false); 93 | selectableTextView.postInvalidate(); 94 | break; 95 | case R.id.rb_hanzi: 96 | selectableTextView.setText(Html.fromHtml(StringContentUtil.str_hanzi).toString()); 97 | selectableTextView.postInvalidate(); 98 | break; 99 | case R.id.rb_en: 100 | selectableTextView.setText(Html.fromHtml(StringContentUtil.str_en).toString()); 101 | selectableTextView.postInvalidate(); 102 | break; 103 | case R.id.rb_muti: 104 | selectableTextView.setText(Html.fromHtml(StringContentUtil.str_muti).toString()); 105 | selectableTextView.postInvalidate(); 106 | break; 107 | } 108 | 109 | } 110 | 111 | @Override 112 | public boolean onCreateCustomActionMenu(ActionMenu menu) { 113 | menu.setActionMenuBgColor(0xff666666); // ActionMenu背景色 114 | menu.setMenuItemTextColor(0xffffffff); // ActionMenu文字颜色 115 | List titleList = new ArrayList<>(); 116 | titleList.add("翻译"); 117 | titleList.add("分享"); 118 | titleList.add("分享"); 119 | menu.addCustomMenuItem(titleList); // 添加菜单 120 | return false; // 返回false,保留默认菜单(全选/复制);返回true,移除默认菜单 121 | } 122 | 123 | @Override 124 | public void onCustomActionItemClicked(String itemTitle, String selectedContent) { 125 | Toast.makeText(this, "ActionMenu: " + itemTitle, Toast.LENGTH_SHORT).show(); 126 | } 127 | 128 | } -------------------------------------------------------------------------------- /app/src/main/java/com/zp/smaple/MainActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 zengp 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.zp.smaple; 18 | 19 | import android.os.Bundle; 20 | import android.view.View; 21 | import android.widget.TextView; 22 | 23 | import androidx.appcompat.app.AppCompatActivity; 24 | 25 | public class MainActivity extends AppCompatActivity implements View.OnClickListener { 26 | 27 | private TextView tv_hori, tv_vert; 28 | 29 | @Override 30 | protected void onCreate(Bundle savedInstanceState) { 31 | super.onCreate(savedInstanceState); 32 | setContentView(R.layout.activity_main); 33 | 34 | tv_hori = findViewById(R.id.tv_hori); 35 | tv_vert = findViewById(R.id.tv_vert); 36 | 37 | tv_hori.setOnClickListener(this); 38 | tv_vert.setOnClickListener(this); 39 | } 40 | 41 | @Override 42 | public void onClick(View v) { 43 | switch (v.getId()) { 44 | case R.id.tv_hori: 45 | HoriActivity.start(this); 46 | break; 47 | case R.id.tv_vert: 48 | VertActivity.start(this); 49 | break; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/com/zp/smaple/StringContentUtil.java: -------------------------------------------------------------------------------- 1 | package com.zp.smaple; 2 | 3 | /** 4 | * Created by zengpu on 2016/11/25. 5 | */ 6 | 7 | public class StringContentUtil { 8 | 9 | public static String str_hanzi = "

  壬戌之秋,七月既望,苏子与客泛舟游于赤壁之下。清风徐来,水波不兴。举酒属客,诵明月之诗,歌窈窕之章。少焉,月出于东山之上,徘徊于斗牛之间。白露横江,水光接天。纵一苇之所如,凌万顷之茫然。浩浩乎如冯虚御风,而不知其所止;飘飘乎如遗世独立,羽化而登仙。︵冯 通:凭︶

  于是饮酒乐甚,扣舷而歌之。歌曰:﹃桂棹兮兰桨,击空明兮溯流光。渺渺兮予怀,望美人兮天一方。﹄客有吹洞箫者,倚歌而和之。其声呜呜然,如怨如慕,如泣如诉;余音袅袅,不绝如缕。舞幽壑之潜蛟,泣孤舟之嫠妇。

  苏子愀然,正襟危坐,而问客曰:﹃何为其然也?﹄客曰:﹃﹁月明星稀,乌鹊南飞。﹂此非曹孟德之诗乎?西望夏口,东望武昌,山川相缪,郁乎苍苍,此非孟德之困于周郎者乎?方其破荆州,下江陵,顺流而东也,舳舻千里,旌旗蔽空,酾酒临江,横槊赋诗,固一世之雄也,而今安在哉?况吾与子渔樵于江渚之上,侣鱼虾而友麋鹿,驾一叶之扁舟,举匏樽以相属。寄蜉蝣于天地,渺沧海之一粟。哀吾生之须臾,羡长 江之无穷。挟飞仙以遨游,抱明月而长终。知不可乎骤得,托遗响于悲风。﹄

  苏子曰:﹃客亦知夫水与月乎?逝者如斯,而未尝往也;盈虚者如彼,而卒莫消长也。盖将自其变者而观之,则天地曾不能以一瞬;自其不变者而观之,则物与我皆无尽也,而又何羡乎!且夫天地之间,物各有主,苟非吾之所有,虽一毫而莫取。惟江上之清风,与山间之明月,耳得之而为声,目遇之而成色,取之无禁,用之不竭。是造物者之无尽藏也,而吾与子之所共适。﹄︵共适 一作:共食︶

  客喜而笑,洗盏更酌。肴核既尽,杯盘狼籍。相与枕藉乎舟中,不知东方之既白。

"; 10 | public static String str_cbf = "

  壬戌之秋,七月既望,蘇子與客泛舟遊於赤壁之下。清風徐來,水波不興。舉酒屬客,誦明月之詩,歌窈窕之章。少焉,月出於東山之上,徘徊於鬥牛之間。白露橫江,水光接天。縱壹葦之所如,淩萬頃之茫然。浩浩乎如馮虛禦風,而不知其所止;飄飄乎如遺世獨立,羽化而登仙。︵馮 通:憑︶

  於是飲酒樂甚,扣舷而歌之。歌曰:﹃桂桌兮蘭槳,擊空明兮溯流光。渺渺兮予懷,望美人兮天壹方。﹄客有吹洞簫者,倚歌而和之。其聲嗚嗚然,如怨如慕,如泣如訴;余音裊裊,不絕如縷。舞幽壑之潛蛟,泣孤舟之嫠婦。

  蘇子愀然,正襟危坐,而問客曰:﹃何為其然也?﹄客曰:﹃﹁月明星稀,烏鵲南飛。﹂此非曹孟德之詩乎?西望夏口,東望武昌,山川相繆,郁乎蒼蒼,此非孟德之困於周郎者乎?方其破荊州,下江陵,順流而東也,舳艫千裏,旌旗蔽空,釃酒臨江,橫槊賦詩,固壹世之雄也,而今安在哉?況吾與子漁樵於江諸之上,侶魚蝦而友麋鹿,駕壹葉之扁舟,舉匏樽以相屬。寄蜉蝣於天地,渺滄海之壹粟。哀吾生之須臾,羨長 江之無窮。挾飛仙以遨遊,抱明月而長終。知不可乎驟得,托遺響於悲風。﹄

  蘇子曰:﹃客亦知夫水與月乎?逝者如斯,而未嘗往也;盈虛者如彼,而卒莫消長也。蓋將自其變者而觀之,則天地曾不能以壹瞬;自其不變者而觀之,則物與我皆無盡也,而又何羨乎!且夫天地之間,物各有主,茍非吾之所有,雖壹毫而莫取。惟江上之清風,與山間之明月,耳得之而為聲,目遇之而成色,取之無禁,用之不竭。是造物者之無盡藏也,而吾與子之所共適。﹄︵共適 壹作:共食︶

  客喜而笑,洗盞更酌。肴核既盡,杯盤狼籍。相與枕藉乎舟中,不知東方之既白。

\n"; 11 | public static String str_en = "

renxu autumn July, Jiwang, Suzi and guest boating tours under Chibi. Xu breeze, rippleless. Wine is off, chanting the moon poetry, song slim chapter. A little while, months of Dongshan, wandering in the bullfight. Dew Yokoe, Shuiguang next day. Even a reed like, Ling Wan Qing loss. The vast Feng seems such as virtual Yufeng, and not to stop; waving as aloof, emergence and immortal. (Feng Tong)

and drinking music, I and song. The Song said: \"Zhao Gui Xi Lan paddle upstream come blow out light and air. You come to my arms, at beauty come one day.\" A blowing flute, and the Yi song. The hum of course, such as resentment, such as mu of bamboo, linger on faintly, as if weeping and complaining. Dance Youhe the cry of a hidden dragon boat.

Suzi stern, sat, and asked the guests said: \"what is it?\" The guests said: \"yuemingxingxi, Ukraine magpie flying south.\" This is not a poem of Cao Mengde? Seomang Xiakou, looking east Wuchang mountains, Miao Yu, between green and the non shuro Meng moral trapped in between? The broken Jingzhou, Jiangling, East River, a convoy of ships thousands of miles, empty word flags, pour wine, having a lance sideward and poetizing Linjiang, a hero of the age and where is also solid,? Kuangwu and son, in Jiang Nagisa, Lu fish and shrimp and the friends of the elk, riding a leaf boat, belonging to lift bottle gourd. Send ephemera in the world, a boundless sea. Sad moment of my life, the infinite envy of Yangtze river. With the fly to roam, hold the moon and long end. Don't know will suddenly left, supporting ring in Beifeng.\"

"; 12 | public static String str_muti = "

  壬戌之秋,renxu autumn July, 七月既望,Jiwang, 苏子与客泛舟游于赤壁之下。Suzi and guest boating tours under Chibi.清风徐来,水波不兴。 Xu breeze, rippleless. 举酒属客,Wine is off, 诵明月之诗,chanting the moon poetry, 歌窈窕之章。song slim chapter. 少焉,A little while, 月出于东山之上,months of Dongshan, 徘徊于斗牛之间。wandering in the bullfight. 白露横江,Dew Yokoe, 水光接天。Shuiguang next day. 纵一苇之所如,Even a reed like, 凌万顷之茫然。Ling Wan Qing loss. 浩浩乎如冯虚御风,The vast Feng seems such as virtual Yufeng, 而不知其所止;and not to stop; 飘飘乎如遗世独立,waving as aloof, 羽化而登仙。emergence and immortal. (冯 通:凭)(Feng Tong)

  于是饮酒乐甚,then drinking music very, 扣舷而歌之。 I and song. 歌曰:“桂棹兮兰桨,击空明兮溯流光。渺渺兮予怀,望美人兮天一方。”。The Song said: \"Zhao Gui Xi Lan paddle upstream come blow out light and air. You come to my arms, at beauty come one day.\"。客有吹洞箫者,A blowing flute, 倚歌而和之。 and the Yi song. 其声呜呜然,The hum of course, 如怨如慕,such as resentment, 如泣如诉;such as mu of bamboo, 余音袅袅,linger on faintly, 不绝如缕。as if weeping and complaining. 舞幽壑之潜蛟,泣孤舟之嫠妇。Dance Youhe the cry of a hidden dragon boat.

"; 13 | public static String str_hhl = "登黄鹤楼\n昔人已乘黄鹤去\n此地空余黄鹤楼\n黄鹤一去不复返\n白云千载空悠悠\n晴川历历汉阳树\n芳草萋萋鹦鹉洲\n日暮乡关何处是\n烟波江上使人愁"; 14 | public static String str_juaner = "采采卷耳,不盈頃筐。\n嗟我懷人,寘彼周行。\n︵寘 通:置︶\n陟彼崔嵬,我馬虺隤。\n我姑酌彼金罍,\n維以不永懷。\n陟彼高岡,我馬玄黃。\n我姑酌彼兕觥,\n維以不永傷。\n陟彼砠矣,我馬瘏矣,\n我仆痡矣,雲何籲矣。"; 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/zp/smaple/VertActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 zengp 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.zp.smaple; 18 | 19 | import android.content.Context; 20 | import android.content.Intent; 21 | import android.os.Bundle; 22 | import android.text.Html; 23 | import android.view.View; 24 | import android.widget.HorizontalScrollView; 25 | import android.widget.RadioGroup; 26 | import android.widget.Toast; 27 | 28 | import androidx.appcompat.app.AppCompatActivity; 29 | 30 | import com.devilist.advancedtextview.ActionMenu; 31 | import com.devilist.advancedtextview.CustomActionMenuCallBack; 32 | import com.devilist.advancedtextview.VerticalTextView; 33 | 34 | import java.util.ArrayList; 35 | import java.util.List; 36 | 37 | /** 38 | * Created by zengp on 2017/12/2. 39 | */ 40 | 41 | public class VertActivity extends AppCompatActivity implements 42 | RadioGroup.OnCheckedChangeListener, CustomActionMenuCallBack { 43 | 44 | private RadioGroup rg_text_orient, rg_text_underline; 45 | 46 | private VerticalTextView vtv_text_ltr; 47 | private HorizontalScrollView scroll_rtl; 48 | 49 | public static void start(Context context) { 50 | Intent starter = new Intent(context, VertActivity.class); 51 | context.startActivity(starter); 52 | } 53 | 54 | @Override 55 | protected void onCreate(Bundle savedInstanceState) { 56 | super.onCreate(savedInstanceState); 57 | setContentView(R.layout.activity_vert); 58 | init(); 59 | } 60 | 61 | private void init() { 62 | scroll_rtl = (HorizontalScrollView) findViewById(R.id.scroll_rtl); 63 | vtv_text_ltr = (VerticalTextView) findViewById(R.id.vtv_text_ltr); 64 | vtv_text_ltr.setText(Html.fromHtml(StringContentUtil.str_cbf).toString()); 65 | 66 | vtv_text_ltr.setLeftToRight(true) 67 | .setLineSpacingExtra(10) 68 | .setCharSpacingExtra(2) 69 | .setUnderLineText(true) 70 | .setShowActionMenu(true) 71 | .setUnderLineColor(0xffCEAD53) 72 | .setUnderLineWidth(1.0f) 73 | .setUnderLineOffset(3) 74 | .setTextHighlightColor(0xffCEAD53) 75 | .setCustomActionMenuCallBack(this); 76 | 77 | vtv_text_ltr.setOnClickListener(new View.OnClickListener() { 78 | @Override 79 | public void onClick(View v) { 80 | Toast.makeText(VertActivity.this, "onClick事件", Toast.LENGTH_SHORT).show(); 81 | } 82 | }); 83 | 84 | rg_text_orient = findViewById(R.id.rg_text_orient); 85 | rg_text_underline = findViewById(R.id.rg_text_underline); 86 | rg_text_orient.setOnCheckedChangeListener(this); 87 | rg_text_underline.setOnCheckedChangeListener(this); 88 | } 89 | 90 | @Override 91 | public void onCheckedChanged(RadioGroup group, int checkedId) { 92 | switch (checkedId) { 93 | case R.id.rb_ltr: 94 | vtv_text_ltr.setLeftToRight(true); 95 | vtv_text_ltr.requestLayout(); 96 | scroll_rtl.fullScroll(View.FOCUS_LEFT); 97 | break; 98 | case R.id.rb_rtl: 99 | vtv_text_ltr.setLeftToRight(false); 100 | vtv_text_ltr.requestLayout(); 101 | scroll_rtl.fullScroll(View.FOCUS_RIGHT); 102 | break; 103 | case R.id.rb_show: 104 | vtv_text_ltr.setUnderLineText(true); 105 | vtv_text_ltr.requestLayout(); 106 | break; 107 | case R.id.rb_hidden: 108 | vtv_text_ltr.setUnderLineText(false); 109 | vtv_text_ltr.requestLayout(); 110 | break; 111 | } 112 | } 113 | 114 | @Override 115 | public boolean onCreateCustomActionMenu(ActionMenu menu) { 116 | List titleList = new ArrayList<>(); 117 | titleList.add("翻译"); 118 | titleList.add("分享"); 119 | menu.addCustomMenuItem(titleList); 120 | return false; 121 | } 122 | 123 | @Override 124 | public void onCustomActionItemClicked(String itemTitle, String selectedContent) { 125 | Toast.makeText(this, "ActionMenu: " + itemTitle, Toast.LENGTH_SHORT).show(); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /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_hori.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 16 | 17 | 23 | 24 | 29 | 30 | 37 | 38 | 43 | 44 | 45 | 46 | 52 | 53 | 59 | 60 | 65 | 66 | 73 | 74 | 80 | 81 | 87 | 88 | 89 | 90 | 91 | 92 | 96 | 97 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 16 | 17 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_vert.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 16 | 17 | 23 | 24 | 29 | 30 | 37 | 38 | 43 | 44 | 45 | 46 | 52 | 53 | 59 | 60 | 65 | 66 | 73 | 74 | 79 | 80 | 81 | 82 | 83 | 90 | 91 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /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/devilist/AdvancedTextView/7d3b30a273cd38e570047fcda50b66be63467d45/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devilist/AdvancedTextView/7d3b30a273cd38e570047fcda50b66be63467d45/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devilist/AdvancedTextView/7d3b30a273cd38e570047fcda50b66be63467d45/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devilist/AdvancedTextView/7d3b30a273cd38e570047fcda50b66be63467d45/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devilist/AdvancedTextView/7d3b30a273cd38e570047fcda50b66be63467d45/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devilist/AdvancedTextView/7d3b30a273cd38e570047fcda50b66be63467d45/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devilist/AdvancedTextView/7d3b30a273cd38e570047fcda50b66be63467d45/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devilist/AdvancedTextView/7d3b30a273cd38e570047fcda50b66be63467d45/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devilist/AdvancedTextView/7d3b30a273cd38e570047fcda50b66be63467d45/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devilist/AdvancedTextView/7d3b30a273cd38e570047fcda50b66be63467d45/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | AdvancedTextView 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 zengp 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 18 | 19 | buildscript { 20 | 21 | repositories { 22 | google() 23 | jcenter() 24 | } 25 | dependencies { 26 | classpath 'com.android.tools.build:gradle:3.4.2' 27 | 28 | 29 | // NOTE: Do not place your application dependencies here; they belong 30 | // in the individual module build.gradle files 31 | } 32 | } 33 | 34 | allprojects { 35 | repositories { 36 | google() 37 | jcenter() 38 | } 39 | } 40 | 41 | task clean(type: Delete) { 42 | delete rootProject.buildDir 43 | } 44 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 zengp 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | # Project-wide Gradle settings. 18 | 19 | # IDE (e.g. Android Studio) users: 20 | # Gradle settings configured through the IDE *will override* 21 | # any settings specified in this file. 22 | 23 | # For more details on how to configure your build environment visit 24 | # http://www.gradle.org/docs/current/userguide/build_environment.html 25 | 26 | # Specifies the JVM arguments used for the daemon process. 27 | # The setting is particularly useful for tweaking memory settings. 28 | android.enableJetifier=true 29 | android.useAndroidX=true 30 | org.gradle.jvmargs=-Xmx2046m 31 | 32 | # When configured, Gradle will run in incubating parallel mode. 33 | # This option should only be used with decoupled projects. More details, visit 34 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 35 | # org.gradle.parallel=true 36 | -------------------------------------------------------------------------------- /images/selectabletextview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devilist/AdvancedTextView/7d3b30a273cd38e570047fcda50b66be63467d45/images/selectabletextview.gif -------------------------------------------------------------------------------- /images/verticaltextview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devilist/AdvancedTextView/7d3b30a273cd38e570047fcda50b66be63467d45/images/verticaltextview.gif -------------------------------------------------------------------------------- /library/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 zengp 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | apply plugin: 'com.android.library' 18 | 19 | android { 20 | compileSdkVersion 28 21 | 22 | defaultConfig { 23 | minSdkVersion 14 24 | targetSdkVersion 28 25 | versionCode 1 26 | versionName "1.0" 27 | 28 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 29 | 30 | } 31 | 32 | buildTypes { 33 | debug { 34 | minifyEnabled false 35 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 36 | } 37 | } 38 | buildTypes { 39 | release { 40 | minifyEnabled false 41 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 42 | } 43 | debug { 44 | minifyEnabled false 45 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 46 | } 47 | } 48 | 49 | } 50 | 51 | dependencies { 52 | implementation fileTree(dir: 'libs', include: ['*.jar']) 53 | } 54 | -------------------------------------------------------------------------------- /library/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 | -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /library/src/main/java/com/devilist/advancedtextview/ActionMenu.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 zengp 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.devilist.advancedtextview; 18 | 19 | import android.content.Context; 20 | import android.graphics.drawable.GradientDrawable; 21 | import android.util.AttributeSet; 22 | import android.view.Gravity; 23 | import android.view.View; 24 | import android.view.ViewGroup; 25 | import android.widget.LinearLayout; 26 | import android.widget.TextView; 27 | 28 | import java.util.ArrayList; 29 | import java.util.Iterator; 30 | import java.util.List; 31 | 32 | /** 33 | * 触发长按事件后弹出的ActionMenu菜单 34 | * Created by zengp on 2017/12/2. 35 | */ 36 | 37 | public class ActionMenu extends LinearLayout { 38 | 39 | public static final String DEFAULT_MENU_ITEM_TITLE_SELECT_ALL = "全选"; 40 | public static final String DEFAULT_MENU_ITEM_TITLE_COPY = "复制"; 41 | 42 | private Context mContext; 43 | private int mMenuItemMargin; 44 | private int mActionMenuBgColor = 0xbb000000; // ActionMenu背景色 45 | private int mMenuItemTextColor = 0xffffffff; // MenuItem字体颜色 46 | private List mItemTitleList; // MenuItem 标题 47 | 48 | public ActionMenu(Context context) { 49 | this(context, null); 50 | } 51 | 52 | public ActionMenu(Context context, AttributeSet attrs) { 53 | this(context, attrs, 0); 54 | } 55 | 56 | public ActionMenu(Context context, AttributeSet attrs, int defStyleAttr) { 57 | super(context, attrs, defStyleAttr); 58 | this.mContext = context; 59 | init(); 60 | } 61 | 62 | private void init() { 63 | LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 45); 64 | setLayoutParams(params); 65 | setPadding(25, 0, 25, 0); 66 | setOrientation(HORIZONTAL); 67 | setGravity(Gravity.CENTER); 68 | setActionMenuBackGround(mActionMenuBgColor); 69 | mMenuItemMargin = 25; 70 | } 71 | 72 | /** 73 | * 设置ActionMenu背景 74 | */ 75 | private void setActionMenuBackGround(int menuBgColor) { 76 | GradientDrawable gd = new GradientDrawable();//创建drawable 77 | gd.setColor(menuBgColor); 78 | gd.setCornerRadius(8); 79 | setBackgroundDrawable(gd); 80 | } 81 | 82 | /** 83 | * 添加默认MenuItem(全选,复制) 84 | */ 85 | void addDefaultMenuItem() { 86 | View item_select_all = createMenuItem(DEFAULT_MENU_ITEM_TITLE_SELECT_ALL); 87 | View item_copy = createMenuItem(DEFAULT_MENU_ITEM_TITLE_COPY); 88 | addView(item_select_all); 89 | addView(item_copy); 90 | invalidate(); 91 | } 92 | 93 | /** 94 | * 移除默认MenuItem 95 | */ 96 | public void removeDefaultMenuItem() { 97 | if (getChildCount() == 0) 98 | return; 99 | 100 | View selAllItem = findViewWithTag(DEFAULT_MENU_ITEM_TITLE_SELECT_ALL); 101 | View copyItem = findViewWithTag(DEFAULT_MENU_ITEM_TITLE_COPY); 102 | 103 | if (null != selAllItem) 104 | removeView(selAllItem); 105 | if (null != copyItem) 106 | removeView(copyItem); 107 | invalidate(); 108 | } 109 | 110 | /** 111 | * 添加自定义MenuItem标题 112 | * 113 | * @param itemTitleList MenuItem标题 114 | */ 115 | public void addCustomMenuItem(List itemTitleList) { 116 | this.mItemTitleList = itemTitleList; 117 | } 118 | 119 | /** 120 | * 添加自定义MenuItem 121 | */ 122 | void addCustomItem() { 123 | if (null == mItemTitleList || (null != mItemTitleList && mItemTitleList.size() == 0)) 124 | return; 125 | // 去重 126 | List list = new ArrayList(); 127 | for (Iterator it = mItemTitleList.iterator(); it.hasNext(); ) { 128 | String title = (String) it.next(); 129 | if (!list.contains(title)) 130 | list.add(title); 131 | } 132 | 133 | for (int i = 0; i < list.size(); i++) { 134 | final View menuItem = createMenuItem(list.get(i)); 135 | addView(menuItem); 136 | } 137 | invalidate(); 138 | } 139 | 140 | /** 141 | * 创建MenuItem 142 | */ 143 | private View createMenuItem(final String itemTitle) { 144 | final TextView menuItem = new TextView(mContext); 145 | LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); 146 | params.leftMargin = params.rightMargin = mMenuItemMargin; 147 | menuItem.setLayoutParams(params); 148 | 149 | menuItem.setTextSize(14); 150 | menuItem.setTextColor(mMenuItemTextColor); 151 | menuItem.setBackgroundColor(mContext.getResources().getColor(android.R.color.transparent)); 152 | menuItem.setGravity(Gravity.CENTER); 153 | menuItem.setText(itemTitle); 154 | menuItem.setTag(itemTitle); 155 | 156 | return menuItem; 157 | } 158 | 159 | /** 160 | * 设置MenuItem文字颜色 161 | * 162 | * @param mItemTextColor 163 | */ 164 | public void setMenuItemTextColor(int mItemTextColor) { 165 | this.mMenuItemTextColor = mItemTextColor; 166 | } 167 | 168 | /** 169 | * 设置ActionMenu背景色 170 | * 171 | * @param mMenuBgColor 172 | */ 173 | public void setActionMenuBgColor(int mMenuBgColor) { 174 | this.mActionMenuBgColor = mMenuBgColor; 175 | setActionMenuBackGround(this.mActionMenuBgColor); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /library/src/main/java/com/devilist/advancedtextview/CustomActionMenuCallBack.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 zengp 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.devilist.advancedtextview; 18 | 19 | /** 20 | * Created by zengp on 2017/12/2. 21 | */ 22 | 23 | public interface CustomActionMenuCallBack { 24 | /** 25 | * 创建ActionMenu菜单。 26 | * 返回值false,保留默认菜单;返回值true,移除默认菜单 27 | * 28 | * @param menu 29 | * @return 返回false,保留默认菜单;返回true,移除默认菜单 30 | */ 31 | boolean onCreateCustomActionMenu(ActionMenu menu); 32 | 33 | /** 34 | * ActionMenu菜单的点击事件 35 | * 36 | * @param itemTitle ActionMenu菜单item的title 37 | * @param selectedContent 选择的文字 38 | */ 39 | void onCustomActionItemClicked(String itemTitle, String selectedContent); 40 | } 41 | -------------------------------------------------------------------------------- /library/src/main/java/com/devilist/advancedtextview/SelectableTextView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 zengp 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.devilist.advancedtextview; 18 | 19 | import android.content.Context; 20 | import android.content.res.TypedArray; 21 | import android.graphics.Canvas; 22 | import android.graphics.Color; 23 | import android.graphics.Paint; 24 | import android.graphics.Path; 25 | import android.graphics.RectF; 26 | import android.graphics.drawable.ColorDrawable; 27 | import android.os.Build; 28 | import android.os.Vibrator; 29 | import android.text.Layout; 30 | import android.text.Selection; 31 | import android.text.StaticLayout; 32 | import android.text.TextPaint; 33 | import android.util.AttributeSet; 34 | import android.util.Log; 35 | import android.view.Gravity; 36 | import android.view.MotionEvent; 37 | import android.view.View; 38 | import android.view.WindowManager; 39 | import android.widget.EditText; 40 | import android.widget.PopupWindow; 41 | import android.widget.Toast; 42 | 43 | import java.util.regex.Matcher; 44 | import java.util.regex.Pattern; 45 | 46 | 47 | import static android.content.Context.VIBRATOR_SERVICE; 48 | 49 | /** 50 | * SelectableTextView ————增强版的TextView,具有以下功能: 51 | *

1:长按文字弹出ActionMenu菜单;菜单menu可以自定义;实现自定义功能(复制,全选,翻译,分享等;默认实现了全选和复制功能) 52 | *

2:文本两端对齐功能;适用于中文文本,英文文本 以及中英混合文本 53 | * Created by zengpu on 2016/11/20. 54 | */ 55 | public class SelectableTextView extends EditText { 56 | 57 | private final int TRIGGER_LONGPRESS_TIME_THRESHOLD = 300; // 触发长按事件的时间阈值 58 | private final int TRIGGER_LONGPRESS_DISTANCE_THRESHOLD = 10; // 触发长按事件的位移阈值 59 | 60 | private Context mContext; 61 | private int mScreenHeight; // 屏幕高度 62 | private int mStatusBarHeight; // 状态栏高度 63 | private int mActionMenuHeight; // 弹出菜单高度 64 | private int mTextHighlightColor;// 选中文字背景高亮颜色 65 | 66 | private float mTouchDownX = 0; 67 | private float mTouchDownY = 0; 68 | private float mTouchDownRawY = 0; 69 | 70 | private boolean isLongPress = false; // 是否发触了长按事件 71 | private boolean isLongPressTouchActionUp = false; // 长按事件结束后,标记该次事件 72 | private boolean isVibrator = false; // 是否触发过长按震动 73 | 74 | private boolean isTextJustify = true; // 是否需要两端对齐 ,默认true 75 | private boolean isForbiddenActionMenu = false; // 是否需要两端对齐 ,默认false 76 | 77 | private boolean isActionSelectAll = false; // 是否触发全选事件 78 | 79 | private int mStartLine; //action_down触摸事件 起始行 80 | private int mStartTextOffset; //action_down触摸事件 字符串开始位置的偏移值 81 | private int mCurrentLine; // action_move触摸事件 当前行 82 | private int mCurrentTextOffset; //action_move触摸事件 字符串当前位置的偏移值 83 | 84 | private int mViewTextWidth; // SelectableTextView内容的宽度(不包含padding) 85 | 86 | private Vibrator mVibrator; 87 | private PopupWindow mActionMenuPopupWindow; // 长按弹出菜单 88 | private ActionMenu mActionMenu = null; 89 | 90 | private OnClickListener mOnClickListener; 91 | private CustomActionMenuCallBack mCustomActionMenuCallBack; 92 | 93 | public SelectableTextView(Context context) { 94 | this(context, null); 95 | } 96 | 97 | public SelectableTextView(Context context, AttributeSet attrs) { 98 | this(context, attrs, 0); 99 | } 100 | 101 | public SelectableTextView(Context context, AttributeSet attrs, int defStyleAttr) { 102 | super(context, attrs, defStyleAttr); 103 | this.mContext = context; 104 | 105 | TypedArray mTypedArray = context.obtainStyledAttributes(attrs, 106 | R.styleable.SelectableTextView); 107 | isTextJustify = mTypedArray.getBoolean(R.styleable.SelectableTextView_textJustify, true); 108 | isForbiddenActionMenu = mTypedArray.getBoolean(R.styleable.SelectableTextView_forbiddenActionMenu, false); 109 | mTextHighlightColor = mTypedArray.getColor(R.styleable.SelectableTextView_textHeightColor, 0x60ffeb3b); 110 | mTypedArray.recycle(); 111 | 112 | init(); 113 | } 114 | 115 | private void init() { 116 | WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); 117 | mScreenHeight = wm.getDefaultDisplay().getHeight(); 118 | mStatusBarHeight = Utils.getStatusBarHeight(mContext); 119 | mActionMenuHeight = Utils.dp2px(mContext, 45); 120 | 121 | mVibrator = (Vibrator) mContext.getSystemService(VIBRATOR_SERVICE); 122 | 123 | if (isTextJustify) 124 | setGravity(Gravity.TOP); 125 | 126 | setTextIsSelectable(true); 127 | setCursorVisible(false); 128 | 129 | setTextHighlightColor(mTextHighlightColor); 130 | } 131 | 132 | @Override 133 | public boolean getDefaultEditable() { 134 | // 返回false,屏蔽掉系统自带的ActionMenu 135 | return false; 136 | } 137 | 138 | public void setTextJustify(boolean textJustify) { 139 | isTextJustify = textJustify; 140 | } 141 | 142 | public void setForbiddenActionMenu(boolean forbiddenActionMenu) { 143 | isForbiddenActionMenu = forbiddenActionMenu; 144 | } 145 | 146 | public void setTextHighlightColor(int color) { 147 | this.mTextHighlightColor = color; 148 | String color_hex = String.format("%08X", color); 149 | color_hex = "#40" + color_hex.substring(2); 150 | setHighlightColor(Color.parseColor(color_hex)); 151 | } 152 | 153 | @Override 154 | public void setOnClickListener(OnClickListener l) { 155 | super.setOnClickListener(l); 156 | if (null != l) { 157 | mOnClickListener = l; 158 | } 159 | } 160 | 161 | @Override 162 | public boolean onTouchEvent(MotionEvent event) { 163 | int action = event.getAction(); 164 | Layout layout = getLayout(); 165 | int currentLine; // 当前所在行 166 | 167 | switch (action) { 168 | case MotionEvent.ACTION_DOWN: 169 | Log.d("SelectableTextView", "ACTION_DOWN"); 170 | 171 | // 每次按下时,创建ActionMenu菜单,创建不成功,屏蔽长按事件 172 | if (null == mActionMenu) { 173 | mActionMenu = createActionMenu(); 174 | } 175 | mTouchDownX = event.getX(); 176 | mTouchDownY = event.getY(); 177 | mTouchDownRawY = event.getRawY(); 178 | isLongPress = false; 179 | isVibrator = false; 180 | isLongPressTouchActionUp = false; 181 | break; 182 | case MotionEvent.ACTION_MOVE: 183 | Log.d("SelectableTextView", "ACTION_MOVE"); 184 | // 先判断是否禁用了ActionMenu功能,以及ActionMenu是否创建失败, 185 | // 二者只要满足了一个条件,退出长按事件 186 | if (!isForbiddenActionMenu || mActionMenu.getChildCount() == 0) { 187 | // 手指移动过程中的字符偏移 188 | currentLine = layout.getLineForVertical(getScrollY() + (int) event.getY()); 189 | int mWordOffset_move = layout.getOffsetForHorizontal(currentLine, (int) event.getX()); 190 | // 判断是否触发长按事件 191 | if (event.getEventTime() - event.getDownTime() >= TRIGGER_LONGPRESS_TIME_THRESHOLD 192 | && Math.abs(event.getX() - mTouchDownX) < TRIGGER_LONGPRESS_DISTANCE_THRESHOLD 193 | && Math.abs(event.getY() - mTouchDownY) < TRIGGER_LONGPRESS_DISTANCE_THRESHOLD) { 194 | 195 | Log.d("SelectableTextView", "ACTION_MOVE 长按"); 196 | isLongPress = true; 197 | isLongPressTouchActionUp = false; 198 | mStartLine = currentLine; 199 | mStartTextOffset = mWordOffset_move; 200 | 201 | // 每次触发长按时,震动提示一次 202 | if (!isVibrator) { 203 | mVibrator.vibrate(30); 204 | isVibrator = true; 205 | } 206 | } 207 | if (isLongPress) { 208 | 209 | if (!isTextJustify) 210 | requestFocus(); 211 | mCurrentLine = currentLine; 212 | mCurrentTextOffset = mWordOffset_move; 213 | // 通知父布局不要拦截触摸事件 214 | getParent().requestDisallowInterceptTouchEvent(true); 215 | // 选择字符 216 | Selection.setSelection(getEditableText(), Math.min(mStartTextOffset, mWordOffset_move), 217 | Math.max(mStartTextOffset, mWordOffset_move)); 218 | } 219 | } 220 | break; 221 | case MotionEvent.ACTION_UP: 222 | Log.d("SelectableTextView", "ACTION_UP"); 223 | // 处理长按事件 224 | if (isLongPress) { 225 | currentLine = layout.getLineForVertical(getScrollY() + (int) event.getY()); 226 | int mWordOffsetEnd = layout.getOffsetForHorizontal(currentLine, (int) event.getX()); 227 | // 至少选中一个字符 228 | mCurrentLine = currentLine; 229 | mCurrentTextOffset = mWordOffsetEnd; 230 | int maxOffset = getEditableText().length() - 1; 231 | if (mStartTextOffset > maxOffset) 232 | mStartTextOffset = maxOffset; 233 | if (mCurrentTextOffset > maxOffset) 234 | mCurrentTextOffset = maxOffset; 235 | if (mCurrentTextOffset == mStartTextOffset) { 236 | if (mCurrentTextOffset == layout.getLineEnd(currentLine) - 1) 237 | mStartTextOffset -= 1; 238 | else 239 | mCurrentTextOffset += 1; 240 | } 241 | 242 | 243 | Selection.setSelection(getEditableText(), Math.min(mStartTextOffset, mCurrentTextOffset), 244 | Math.max(mStartTextOffset, mCurrentTextOffset)); 245 | // 计算菜单显示位置 246 | int mPopWindowOffsetY = calculatorActionMenuYPosition((int) mTouchDownRawY, (int) event.getRawY()); 247 | // 弹出菜单 248 | showActionMenu(mPopWindowOffsetY, mActionMenu); 249 | isLongPressTouchActionUp = true; 250 | isLongPress = false; 251 | 252 | } else if (event.getEventTime() - event.getDownTime() < TRIGGER_LONGPRESS_TIME_THRESHOLD) { 253 | // 由于onTouchEvent最终返回了true,onClick事件会被屏蔽掉,因此在这里处理onClick事件 254 | if (null != mOnClickListener) 255 | mOnClickListener.onClick(this); 256 | } 257 | // 通知父布局继续拦截触摸事件 258 | getParent().requestDisallowInterceptTouchEvent(false); 259 | break; 260 | } 261 | return true; 262 | } 263 | 264 | /* ***************************************************************************************** */ 265 | // 创建ActionMenu部分 266 | 267 | /** 268 | * 创建ActionMenu菜单 269 | * 270 | * @return 271 | */ 272 | private ActionMenu createActionMenu() { 273 | // 创建菜单 274 | ActionMenu actionMenu = new ActionMenu(mContext); 275 | // 是否需要移除默认item 276 | boolean isRemoveDefaultItem = false; 277 | if (null != mCustomActionMenuCallBack) { 278 | isRemoveDefaultItem = mCustomActionMenuCallBack.onCreateCustomActionMenu(actionMenu); 279 | } 280 | if (!isRemoveDefaultItem) 281 | actionMenu.addDefaultMenuItem(); // 添加默认item 282 | 283 | actionMenu.addCustomItem(); // 添加自定义item 284 | actionMenu.setFocusable(true); // 获取焦点 285 | actionMenu.setFocusableInTouchMode(true); 286 | 287 | if (actionMenu.getChildCount() != 0) { 288 | // item监听 289 | for (int i = 0; i < actionMenu.getChildCount(); i++) { 290 | actionMenu.getChildAt(i).setOnClickListener(mMenuClickListener); 291 | } 292 | } 293 | return actionMenu; 294 | } 295 | 296 | /** 297 | * 长按弹出菜单 298 | * 299 | * @param offsetY 300 | * @param actionMenu 301 | * @return 菜单创建成功,返回true 302 | */ 303 | private void showActionMenu(int offsetY, ActionMenu actionMenu) { 304 | 305 | mActionMenuPopupWindow = new PopupWindow(actionMenu, WindowManager.LayoutParams.WRAP_CONTENT, 306 | mActionMenuHeight, true); 307 | mActionMenuPopupWindow.setFocusable(true); 308 | mActionMenuPopupWindow.setOutsideTouchable(false); 309 | mActionMenuPopupWindow.setBackgroundDrawable(new ColorDrawable(0x00000000)); 310 | mActionMenuPopupWindow.showAtLocation(this, Gravity.TOP | Gravity.CENTER_HORIZONTAL, 0, offsetY); 311 | 312 | mActionMenuPopupWindow.setOnDismissListener(new PopupWindow.OnDismissListener() { 313 | @Override 314 | public void onDismiss() { 315 | Selection.removeSelection(getEditableText()); 316 | // 如果设置了分散对齐,ActionMenu销毁后,强制刷新一次,防止出现文字背景未消失的情况 317 | if (isTextJustify) 318 | SelectableTextView.this.postInvalidate(); 319 | } 320 | }); 321 | } 322 | 323 | /** 324 | * 隐藏菜单 325 | */ 326 | private void hideActionMenu() { 327 | if (null != mActionMenuPopupWindow) { 328 | mActionMenuPopupWindow.dismiss(); 329 | mActionMenuPopupWindow = null; 330 | } 331 | } 332 | 333 | /** 334 | * 菜单点击事件监听 335 | */ 336 | private OnClickListener mMenuClickListener = new OnClickListener() { 337 | @Override 338 | public void onClick(View v) { 339 | 340 | String menuItemTitle = (String) v.getTag(); 341 | 342 | // 选中的字符的开始和结束位置 343 | int start = getSelectionStart(); 344 | int end = getSelectionEnd(); 345 | // 获得选中的字符 346 | String selected_str; 347 | if (start < 0 || end < 0 || end <= start) { 348 | selected_str = ""; 349 | } else 350 | selected_str = getText().toString().substring(start, end); 351 | 352 | if (menuItemTitle.equals(ActionMenu.DEFAULT_MENU_ITEM_TITLE_SELECT_ALL)) { 353 | //全选事件 354 | if (isTextJustify) { 355 | mStartLine = 0; 356 | mCurrentLine = getLayout().getLineCount() - 1; 357 | mStartTextOffset = 0; 358 | mCurrentTextOffset = getLayout().getLineEnd(mCurrentLine); 359 | isActionSelectAll = true; 360 | SelectableTextView.this.invalidate(); 361 | } 362 | Selection.selectAll(getEditableText()); 363 | 364 | } else if (menuItemTitle.equals(ActionMenu.DEFAULT_MENU_ITEM_TITLE_COPY)) { 365 | // 复制事件 366 | Utils.copyText(mContext, selected_str); 367 | Toast.makeText(mContext, "复制成功!", Toast.LENGTH_SHORT).show(); 368 | hideActionMenu(); 369 | 370 | } else { 371 | // 自定义事件 372 | if (null != mCustomActionMenuCallBack) { 373 | mCustomActionMenuCallBack.onCustomActionItemClicked(menuItemTitle, selected_str); 374 | } 375 | hideActionMenu(); 376 | } 377 | } 378 | }; 379 | 380 | /** 381 | * 计算弹出菜单相对于父布局的Y向偏移 382 | * 383 | * @param yOffsetStart 所选字符的起始位置相对屏幕的Y向偏移 384 | * @param yOffsetEnd 所选字符的结束位置相对屏幕的Y向偏移 385 | * @return 386 | */ 387 | private int calculatorActionMenuYPosition(int yOffsetStart, int yOffsetEnd) { 388 | if (yOffsetStart > yOffsetEnd) { 389 | int temp = yOffsetStart; 390 | yOffsetStart = yOffsetEnd; 391 | yOffsetEnd = temp; 392 | } 393 | int actionMenuOffsetY; 394 | 395 | if (yOffsetStart < mActionMenuHeight * 3 / 2 + mStatusBarHeight) { 396 | if (yOffsetEnd > mScreenHeight - mActionMenuHeight * 3 / 2) { 397 | // 菜单显示在屏幕中间 398 | actionMenuOffsetY = mScreenHeight / 2 - mActionMenuHeight / 2; 399 | } else { 400 | // 菜单显示所选文字下方 401 | actionMenuOffsetY = yOffsetEnd + mActionMenuHeight / 2; 402 | } 403 | } else { 404 | // 菜单显示所选文字上方 405 | actionMenuOffsetY = yOffsetStart - mActionMenuHeight * 3 / 2; 406 | } 407 | return actionMenuOffsetY; 408 | } 409 | 410 | /* ***************************************************************************************** */ 411 | // 两端对齐部分 412 | 413 | @Override 414 | protected void onDraw(Canvas canvas) { 415 | Log.d("SelectableTextView", "onDraw"); 416 | if (!isTextJustify) { 417 | // 不需要两端对齐 418 | super.onDraw(canvas); 419 | 420 | } else { 421 | //textview内容的实际宽度 422 | mViewTextWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); 423 | // 重绘文字,两端对齐 424 | drawTextWithJustify(canvas); 425 | // 绘制选中文字的背景,触发以下事件时需要绘制背景: 426 | // 1.长按事件 2.全选事件 3.手指滑动过快时,进入ACTION_UP事件后, 427 | // 可能会出现背景未绘制的情况 428 | if (isLongPress | isActionSelectAll | isLongPressTouchActionUp) { 429 | drawSelectedTextBackground(canvas); 430 | isActionSelectAll = false; 431 | isLongPressTouchActionUp = false; 432 | } 433 | } 434 | } 435 | 436 | /** 437 | * 重绘文字,两端对齐 438 | * 439 | * @param canvas 440 | */ 441 | private void drawTextWithJustify(Canvas canvas) { 442 | // 文字画笔 443 | TextPaint textPaint = getPaint(); 444 | textPaint.setColor(getCurrentTextColor()); 445 | textPaint.drawableState = getDrawableState(); 446 | 447 | String text_str = getText().toString(); 448 | // 当前所在行的Y向偏移 449 | int currentLineOffsetY = getPaddingTop(); 450 | currentLineOffsetY += getTextSize(); 451 | 452 | Layout layout = getLayout(); 453 | 454 | //循环每一行,绘制文字 455 | for (int i = 0; i < layout.getLineCount(); i++) { 456 | int lineStart = layout.getLineStart(i); 457 | int lineEnd = layout.getLineEnd(i); 458 | //获取到TextView每行中的内容 459 | String line_str = text_str.substring(lineStart, lineEnd); 460 | // 获取每行字符串的宽度(不包括字符间距) 461 | float desiredWidth = StaticLayout.getDesiredWidth(text_str, lineStart, lineEnd, getPaint()); 462 | 463 | if (isLineNeedJustify(line_str)) { 464 | //最后一行不需要重绘 465 | if (i == layout.getLineCount() - 1) { 466 | canvas.drawText(line_str, getPaddingLeft(), currentLineOffsetY, textPaint); 467 | } else { 468 | drawJustifyTextForLine(canvas, line_str, desiredWidth, currentLineOffsetY); 469 | } 470 | } else { 471 | canvas.drawText(line_str, getPaddingLeft(), currentLineOffsetY, textPaint); 472 | } 473 | //更新行Y向偏移 474 | currentLineOffsetY += getLineHeight(); 475 | } 476 | } 477 | 478 | /** 479 | * 绘制选中的文字的背景 480 | * 481 | * @param canvas 482 | */ 483 | private void drawSelectedTextBackground(Canvas canvas) { 484 | if (mStartTextOffset == mCurrentTextOffset) 485 | return; 486 | 487 | // 文字背景高亮画笔 488 | Paint highlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 489 | highlightPaint.setStyle(Paint.Style.FILL); 490 | highlightPaint.setColor(mTextHighlightColor); 491 | highlightPaint.setAlpha(60); 492 | 493 | // 计算开始位置和结束位置的字符相对view最左侧的x偏移 494 | float startToLeftPosition = calculatorCharPositionToLeft(mStartLine, mStartTextOffset); 495 | float currentToLeftPosition = calculatorCharPositionToLeft(mCurrentLine, mCurrentTextOffset); 496 | 497 | // 行高 498 | int h = getLineHeight(); 499 | int paddingTop = getPaddingTop(); 500 | int paddingLeft = getPaddingLeft(); 501 | 502 | // 创建三个矩形,分别对应: 503 | // 所有选中的行对应的矩形,起始行左侧未选中文字的对应的矩形,结束行右侧未选中的文字对应的矩形 504 | RectF rect_all, rect_lt, rect_rb; 505 | // sdk版本控制 506 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 507 | if (mStartTextOffset < mCurrentTextOffset) { 508 | rect_all = new RectF(paddingLeft, mStartLine * h + paddingTop, 509 | mViewTextWidth + paddingLeft, (mCurrentLine + 1) * h + paddingTop); 510 | rect_lt = new RectF(paddingLeft, mStartLine * h + paddingTop, 511 | startToLeftPosition, (mStartLine + 1) * h + paddingTop); 512 | rect_rb = new RectF(currentToLeftPosition, mCurrentLine * h + paddingTop, 513 | mViewTextWidth + paddingLeft, (mCurrentLine + 1) * h + paddingTop); 514 | } else { 515 | rect_all = new RectF(paddingLeft, mCurrentLine * h + paddingTop, 516 | mViewTextWidth + paddingLeft, (mStartLine + 1) * h + paddingTop); 517 | rect_lt = new RectF(paddingLeft, mCurrentLine * h + paddingTop, 518 | currentToLeftPosition, (mCurrentLine + 1) * h + paddingTop); 519 | rect_rb = new RectF(startToLeftPosition, mStartLine * h + paddingTop, 520 | mViewTextWidth + paddingLeft, (mStartLine + 1) * h + paddingTop); 521 | } 522 | 523 | // 创建三个路径,分别对应上面三个矩形 524 | Path path_all = new Path(); 525 | Path path_lt = new Path(); 526 | Path path_rb = new Path(); 527 | path_all.addRect(rect_all, Path.Direction.CCW); 528 | path_lt.addRect(rect_lt, Path.Direction.CCW); 529 | path_rb.addRect(rect_rb, Path.Direction.CCW); 530 | // 将左上角和右下角的矩形从path_all中减去 531 | path_all.addRect(rect_all, Path.Direction.CCW); 532 | path_all.op(path_lt, Path.Op.DIFFERENCE); 533 | path_all.op(path_rb, Path.Op.DIFFERENCE); 534 | 535 | canvas.drawPath(path_all, highlightPaint); 536 | 537 | } else { 538 | Path path_all = new Path(); 539 | path_all.moveTo(startToLeftPosition, (mStartLine + 1) * h + paddingTop); 540 | path_all.lineTo(startToLeftPosition, mStartLine * h + paddingTop); 541 | path_all.lineTo(mViewTextWidth + paddingLeft, mStartLine * h + paddingTop); 542 | path_all.lineTo(mViewTextWidth + paddingLeft, mCurrentLine * h + paddingTop); 543 | path_all.lineTo(currentToLeftPosition, mCurrentLine * h + paddingTop); 544 | path_all.lineTo(currentToLeftPosition, (mCurrentLine + 1) * h + paddingTop); 545 | path_all.lineTo(paddingLeft, (mCurrentLine + 1) * h + paddingTop); 546 | path_all.lineTo(paddingLeft, (mStartLine + 1) * h + paddingTop); 547 | path_all.lineTo(startToLeftPosition, (mStartLine + 1) * h + paddingTop); 548 | 549 | canvas.drawPath(path_all, highlightPaint); 550 | } 551 | // canvas.restore(); 552 | } 553 | 554 | /** 555 | * 重绘此行,两端对齐 556 | * 557 | * @param canvas 558 | * @param line_str 该行所有的文字 559 | * @param desiredWidth 该行每个文字的宽度的总和 560 | * @param currentLineOffsetY 该行的Y向偏移 561 | */ 562 | private void drawJustifyTextForLine(Canvas canvas, String line_str, float desiredWidth, int currentLineOffsetY) { 563 | 564 | // 画笔X方向的偏移 565 | float lineTextOffsetX = getPaddingLeft(); 566 | // 判断是否是首行 567 | if (isFirstLineOfParagraph(line_str)) { 568 | String blanks = " "; 569 | // 画出缩进空格 570 | canvas.drawText(blanks, lineTextOffsetX, currentLineOffsetY, getPaint()); 571 | // 空格需要的宽度 572 | float blank_width = StaticLayout.getDesiredWidth(blanks, getPaint()); 573 | // 更新画笔X方向的偏移 574 | lineTextOffsetX += blank_width; 575 | line_str = line_str.substring(3); 576 | } 577 | 578 | // 计算相邻字符(或单词)之间需要填充的宽度,英文按单词处理,中文按字符处理 579 | // (TextView内容的实际宽度 - 该行字符串的宽度)/(字符或单词个数-1) 580 | if (isContentABC(line_str)) { 581 | // 该行包含英文,以空格分割单词 582 | String[] line_words = line_str.split(" "); 583 | // 计算相邻单词间需要插入的空白 584 | float insert_blank = mViewTextWidth - desiredWidth; 585 | if (line_words.length > 1) 586 | insert_blank = (mViewTextWidth - desiredWidth) / (line_words.length - 1); 587 | // 遍历单词 588 | for (int i = 0; i < line_words.length; i++) { 589 | // 判断分割后的每一个单词;如果是纯英文,按照纯英文单词处理,直接在画布上画出单词; 590 | // 如果包括汉字,则按照汉字字符处理,逐个字符绘画 591 | // 如果只有一个单词,按中文处理 592 | // 最后一个单词按照纯英文单词处理 593 | String word_i = line_words[i] + " "; 594 | if (line_words.length == 1 || (isContentHanZi(word_i) && i < line_words.length - 1)) { 595 | // 单词按照汉字字符处理 596 | // 计算单词中相邻字符间需要插入的空白 597 | float insert_blank_word_i = insert_blank; 598 | if (word_i.length() > 1) 599 | insert_blank_word_i = insert_blank / (word_i.length() - 1); 600 | // 遍历单词中字符,依次绘画 601 | for (int j = 0; j < word_i.length(); j++) { 602 | String word_i_char_j = String.valueOf(word_i.charAt(j)); 603 | float word_i_char_j_width = StaticLayout.getDesiredWidth(word_i_char_j, getPaint()); 604 | canvas.drawText(word_i_char_j, lineTextOffsetX, currentLineOffsetY, getPaint()); 605 | // 更新画笔X方向的偏移 606 | lineTextOffsetX += word_i_char_j_width + insert_blank_word_i; 607 | } 608 | } else { 609 | //单词按照纯英文处理 610 | float word_i_width = StaticLayout.getDesiredWidth(word_i, getPaint()); 611 | canvas.drawText(word_i, lineTextOffsetX, currentLineOffsetY, getPaint()); 612 | // 更新画笔X方向的偏移 613 | lineTextOffsetX += word_i_width + insert_blank; 614 | } 615 | } 616 | } else { 617 | // 该行按照中文处理 618 | float insert_blank = (mViewTextWidth - desiredWidth) / (line_str.length() - 1); 619 | for (int i = 0; i < line_str.length(); i++) { 620 | String char_i = String.valueOf(line_str.charAt(i)); 621 | float char_i_width = StaticLayout.getDesiredWidth(char_i, getPaint()); 622 | canvas.drawText(char_i, lineTextOffsetX, currentLineOffsetY, getPaint()); 623 | // 更新画笔X方向的偏移 624 | lineTextOffsetX += char_i_width + insert_blank; 625 | } 626 | } 627 | } 628 | 629 | /** 630 | * 计算字符距离控件左侧的位移 631 | * 632 | * @param line 字符所在行 633 | * @param charOffset 字符偏移量 634 | */ 635 | private float calculatorCharPositionToLeft(int line, int charOffset) { 636 | 637 | String text_str = getText().toString(); 638 | 639 | 640 | Layout layout = getLayout(); 641 | int lineStart = layout.getLineStart(line); 642 | int lineEnd = layout.getLineEnd(line); 643 | 644 | String line_str = text_str.substring(lineStart, lineEnd); 645 | 646 | if (line_str.equals("\n")) 647 | return getPaddingLeft(); 648 | // 最左侧 649 | if (lineStart == charOffset) 650 | return getPaddingLeft(); 651 | // 最右侧 652 | if (charOffset == lineEnd - 1) 653 | return mViewTextWidth + getPaddingLeft(); 654 | 655 | float desiredWidth = StaticLayout.getDesiredWidth(text_str, lineStart, lineEnd, getPaint()); 656 | 657 | // 中间位置 658 | // 计算相邻字符之间需要填充的宽度 659 | // (TextView内容的实际宽度 - 该行字符串的宽度)/(字符个数-1) 660 | float insert_blank = (mViewTextWidth - desiredWidth) / (line_str.length() - 1); 661 | // 计算当前字符左侧所有字符的宽度 662 | float allLeftCharWidth = StaticLayout.getDesiredWidth(text_str.substring(lineStart, charOffset), getPaint()); 663 | 664 | // 相邻字符之间需要填充的宽度 + 当前字符左侧所有字符的宽度 665 | return insert_blank * (charOffset - lineStart) + allLeftCharWidth + getPaddingLeft(); 666 | 667 | } 668 | 669 | /** 670 | * 判断是不是段落的第一行。一个汉字相当于一个字符,此处判断是否为第一行的依据是: 671 | * 字符长度大于3且前两个字符为空格 672 | * 673 | * @param line 674 | * @return 675 | */ 676 | private boolean isFirstLineOfParagraph(String line) { 677 | return line.length() > 3 && line.charAt(0) == ' ' && line.charAt(1) == ' '; 678 | } 679 | 680 | /** 681 | * 判断该行需不需要缩放;该行最后一个字符不是换行符的时候返回true, 682 | * 该行最后一个字符是换行符的时候返回false 683 | * 684 | * @param line_str 该行的文字 685 | * @return 686 | */ 687 | private boolean isLineNeedJustify(String line_str) { 688 | if (line_str.length() == 0) { 689 | return false; 690 | } else { 691 | return line_str.charAt(line_str.length() - 1) != '\n'; 692 | } 693 | } 694 | 695 | /** 696 | * 判断是否包含英文 697 | * 698 | * @param line_str 699 | * @return 700 | */ 701 | private boolean isContentABC(String line_str) { 702 | String regex = ".*[a-zA-Z]+.*"; 703 | Matcher m = Pattern.compile(regex).matcher(line_str); 704 | return m.matches(); 705 | } 706 | 707 | /** 708 | * 判断是否包含中文 709 | * 710 | * @param word_str 711 | * @return 712 | */ 713 | private boolean isContentHanZi(String word_str) { 714 | // String E1 = "[\u4e00-\u9fa5]";// 中文 715 | String regex = ".*[\\u4e00-\\u9fa5]+.*"; 716 | Matcher m = Pattern.compile(regex).matcher(word_str); 717 | return m.matches(); 718 | } 719 | 720 | /** 721 | * 判断是否是中文标点符号 722 | * 723 | * @param str 724 | * @return 725 | */ 726 | private boolean isUnicodeSymbol(String str) { 727 | String regex = ".*[`~!@#$^&*()=|{}':;',\\[\\].<>/?~!@#¥……&*()——|{}【】‘;:”“'。,、?]$+.*"; 728 | Matcher m = Pattern.compile(regex).matcher(str); 729 | return m.matches(); 730 | } 731 | 732 | public void setCustomActionMenuCallBack(CustomActionMenuCallBack callBack) { 733 | this.mCustomActionMenuCallBack = callBack; 734 | } 735 | } 736 | -------------------------------------------------------------------------------- /library/src/main/java/com/devilist/advancedtextview/Utils.java: -------------------------------------------------------------------------------- 1 | package com.devilist.advancedtextview; 2 | 3 | import android.content.Context; 4 | import android.os.Build; 5 | 6 | import java.lang.reflect.Field; 7 | 8 | /** 9 | * Created by zengp on 2017/12/2. 10 | */ 11 | 12 | public class Utils { 13 | 14 | 15 | /** 16 | * dp2px 17 | */ 18 | public static int dp2px(Context context, float dpValue) { 19 | final float scale = context.getResources().getDisplayMetrics().density; 20 | return (int) (dpValue * scale + 0.5f); 21 | } 22 | 23 | /** 24 | * 实现文本复制功能 25 | * 26 | * @param text 27 | */ 28 | public static void copyText(Context context, String text) { 29 | // 得到剪贴板管理器 30 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { 31 | android.text.ClipboardManager cmb = (android.text.ClipboardManager) context 32 | .getSystemService(Context.CLIPBOARD_SERVICE); 33 | cmb.setText(text.trim()); 34 | } else { 35 | android.content.ClipboardManager cmb = (android.content.ClipboardManager) context 36 | .getSystemService(Context.CLIPBOARD_SERVICE); 37 | cmb.setText(text.trim()); 38 | } 39 | } 40 | 41 | /** 42 | * 状态栏高度 43 | * 44 | * @param context 45 | * @return 46 | */ 47 | public static int getStatusBarHeight(Context context) { 48 | Class c = null; 49 | Object obj = null; 50 | Field field = null; 51 | int x = 0, statusBarHeight = 0; 52 | try { 53 | c = Class.forName("com.android.internal.R$dimen"); 54 | obj = c.newInstance(); 55 | field = c.getField("status_bar_height"); 56 | x = Integer.parseInt(field.get(obj).toString()); 57 | statusBarHeight = context.getResources().getDimensionPixelSize(x); 58 | 59 | } catch (Exception e1) { 60 | statusBarHeight = 0; 61 | e1.printStackTrace(); 62 | } 63 | return statusBarHeight; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /library/src/main/java/com/devilist/advancedtextview/VerticalTextView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 zengp 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.devilist.advancedtextview; 18 | 19 | import android.content.Context; 20 | import android.content.res.TypedArray; 21 | import android.graphics.Canvas; 22 | import android.graphics.Color; 23 | import android.graphics.Paint; 24 | import android.graphics.Path; 25 | import android.graphics.Rect; 26 | import android.graphics.drawable.ColorDrawable; 27 | import android.os.Vibrator; 28 | import android.text.TextPaint; 29 | import android.text.TextUtils; 30 | import android.util.AttributeSet; 31 | import android.util.Log; 32 | import android.util.SparseArray; 33 | import android.view.Gravity; 34 | import android.view.MotionEvent; 35 | import android.view.View; 36 | import android.view.WindowManager; 37 | import android.widget.PopupWindow; 38 | import android.widget.TextView; 39 | import android.widget.Toast; 40 | 41 | import java.lang.reflect.Field; 42 | import java.util.regex.Matcher; 43 | import java.util.regex.Pattern; 44 | 45 | 46 | import static android.content.Context.VIBRATOR_SERVICE; 47 | import static android.view.MotionEvent.ACTION_MOVE; 48 | 49 | /** 50 | * VerticalTextView ———— 实现文字竖排的TextView。 51 | *

功能: 52 | *

1.文字从上到下竖排。 53 | *

2.文字阅读方向可选择 从右向左 和 从左向右。 54 | *

3.长按可选择文本,并弹出自定义的可定制化的菜单ActionMenu 55 | *

56 | * Created by zengpu on 2017/1/20. 57 | */ 58 | public class VerticalTextView extends TextView { 59 | 60 | private static String TAG = VerticalTextView.class.getSimpleName(); 61 | 62 | private final int TRIGGER_LONGPRESS_TIME_THRESHOLD = 300; // 触发长按事件的时间阈值 63 | private final int TRIGGER_LONGPRESS_DISTANCE_THRESHOLD = 10; // 触发长按事件的位移阈值 64 | 65 | private Context mContext; 66 | private int mScreenWidth; // 屏幕宽度 67 | private int mScreenHeight; // 屏幕高度 68 | 69 | // attrs 70 | private boolean isLeftToRight; // 竖排方向,是否从左到右;默认从右到左 71 | private float mLineSpacingExtra; // 行距 默认 6px 72 | private float mCharSpacingExtra; // 字符间距 默认 6px 73 | private boolean isUnderLineText; // 是否需要下划线,默认false 74 | private int mUnderLineColor; // 下划线颜色 默认 Color.RED 75 | private float mUnderLineWidth; // 下划线线宽 默认 1.5f 76 | private float mUnderLineOffset; // 下划线偏移 默认 3px 77 | private boolean isShowActionMenu; // 是否显示ActionMenu,默认true 78 | private int mTextHighlightColor; // 选中文字背景高亮颜色 默认0x60ffeb3b 79 | 80 | // onMeasure相关 81 | private int[] mTextAreaRoughBound; // 粗略计算的文本最大显示区域(包含padding),用于view的测量和不同Gravity情况下文本的绘制 82 | private int[] mMeasureMode; // 宽高的测量模式 83 | 84 | private SparseArray mLinesOffsetArray; // 记录每一行文字的X,Y偏移量 85 | private SparseArray mLinesTextIndex; // 记录每一行文字开始和结束字符的index 86 | private int mMaxTextLine = 0; // 最大行数 87 | 88 | private int mStatusBarHeight; // 状态栏高度 89 | private int mActionMenuHeight; // 弹出菜单ActionMenu高度 90 | private String mSelectedText; // 选择的文字 91 | 92 | // onTouchEvent相关 93 | private float mTouchDownX = 0; 94 | private float mTouchDownY = 0; 95 | private float mTouchDownRawY = 0; 96 | private boolean isLongPress = false; // 是否发触了长按事件 97 | private boolean isLongPressTouchActionUp = false; // 长按事件结束后,标记该次事件,防止手指抬起后view没有重绘 98 | private boolean isVibrator = false; // 是否触发过长按震动 99 | private boolean isActionSelectAll = false; // 是否触发全选事件 100 | 101 | private int mStartLine; // 长按触摸事件 所选文字的起始行 102 | private int mCurrentLine; // 长按触摸事件 移动过程中手指所在行 103 | private float mStartTextOffset; // 长按触摸事件 所选文字开始位置的Y向偏移值 104 | private float mCurrentTextOffset; // 长按触摸事件 移动过程中所选文字结束位置的Y向偏移值 105 | 106 | private Vibrator mVibrator; 107 | private PopupWindow mActionMenuPopupWindow; // 长按弹出菜单 108 | private ActionMenu mActionMenu = null; // ActionMenu 109 | 110 | private OnClickListener mOnClickListener; 111 | private CustomActionMenuCallBack mCustomActionMenuCallBack; 112 | 113 | public VerticalTextView(Context context) { 114 | this(context, null); 115 | } 116 | 117 | public VerticalTextView(Context context, AttributeSet attrs) { 118 | this(context, attrs, 0); 119 | } 120 | 121 | public VerticalTextView(Context context, AttributeSet attrs, int defStyleAttr) { 122 | super(context, attrs, defStyleAttr); 123 | this.mContext = context; 124 | TypedArray mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.VerticalTextView); 125 | mLineSpacingExtra = mTypedArray.getDimension(R.styleable.VerticalTextView_lineSpacingExtra, 6); 126 | mCharSpacingExtra = mTypedArray.getDimension(R.styleable.VerticalTextView_charSpacingExtra, 6); 127 | isLeftToRight = mTypedArray.getBoolean(R.styleable.VerticalTextView_textLeftToRight, false); 128 | isUnderLineText = mTypedArray.getBoolean(R.styleable.VerticalTextView_underLineText, false); 129 | mUnderLineColor = mTypedArray.getColor(R.styleable.VerticalTextView_underLineColor, Color.RED); 130 | mUnderLineWidth = mTypedArray.getFloat(R.styleable.VerticalTextView_underLineWidth, 1.5f); 131 | mUnderLineOffset = mTypedArray.getDimension(R.styleable.VerticalTextView_underlineOffset, 3); 132 | mTextHighlightColor = mTypedArray.getColor(R.styleable.VerticalTextView_textHeightLightColor, 0x60ffeb3b); 133 | isShowActionMenu = mTypedArray.getBoolean(R.styleable.VerticalTextView_showActionMenu, false); 134 | mTypedArray.recycle(); 135 | 136 | mLineSpacingExtra = Math.max(6, mLineSpacingExtra); 137 | mCharSpacingExtra = Math.max(6, mCharSpacingExtra); 138 | if (isUnderLineText) { 139 | mUnderLineWidth = Math.abs(mUnderLineWidth); 140 | mUnderLineOffset = Math.min(Math.abs(mUnderLineOffset), Math.abs(mLineSpacingExtra) / 2); 141 | } 142 | 143 | init(); 144 | } 145 | 146 | private void init() { 147 | WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); 148 | mScreenWidth = wm.getDefaultDisplay().getWidth(); 149 | mScreenHeight = wm.getDefaultDisplay().getHeight(); 150 | setTextIsSelectable(false); 151 | mLinesOffsetArray = new SparseArray<>(); 152 | mLinesTextIndex = new SparseArray<>(); 153 | mTextAreaRoughBound = new int[]{0, 0}; 154 | mStatusBarHeight = Utils.getStatusBarHeight(mContext); 155 | mActionMenuHeight = Utils.dp2px(mContext, 45); 156 | mVibrator = (Vibrator) mContext.getSystemService(VIBRATOR_SERVICE); 157 | } 158 | 159 | public VerticalTextView setLeftToRight(boolean leftToRight) { 160 | isLeftToRight = leftToRight; 161 | return this; 162 | } 163 | 164 | public VerticalTextView setLineSpacingExtra(float lineSpacingExtra) { 165 | this.mLineSpacingExtra = Utils.dp2px(mContext, lineSpacingExtra); 166 | return this; 167 | } 168 | 169 | public VerticalTextView setCharSpacingExtra(float charSpacingExtra) { 170 | this.mCharSpacingExtra = Utils.dp2px(mContext, charSpacingExtra); 171 | return this; 172 | } 173 | 174 | public VerticalTextView setUnderLineText(boolean underLineText) { 175 | isUnderLineText = underLineText; 176 | return this; 177 | } 178 | 179 | public VerticalTextView setUnderLineColor(int underLineColor) { 180 | this.mUnderLineColor = underLineColor; 181 | return this; 182 | } 183 | 184 | public VerticalTextView setUnderLineWidth(float underLineWidth) { 185 | this.mUnderLineWidth = underLineWidth; 186 | return this; 187 | } 188 | 189 | public VerticalTextView setUnderLineOffset(float underLineOffset) { 190 | this.mUnderLineOffset = Utils.dp2px(mContext, underLineOffset); 191 | return this; 192 | } 193 | 194 | public VerticalTextView setShowActionMenu(boolean showActionMenu) { 195 | isShowActionMenu = showActionMenu; 196 | return this; 197 | } 198 | 199 | public VerticalTextView setTextHighlightColor(int color) { 200 | this.mTextHighlightColor = color; 201 | String color_hex = String.format("%08X", color); 202 | color_hex = "#40" + color_hex.substring(2); 203 | setHighlightColor(Color.parseColor(color_hex)); 204 | return this; 205 | } 206 | 207 | @Override 208 | public void setOnClickListener(OnClickListener l) { 209 | super.setOnClickListener(l); 210 | if (null != l) { 211 | mOnClickListener = l; 212 | } 213 | } 214 | 215 | /** 216 | * 设置ActionMenu菜单内容监听 217 | * 218 | * @param callBack 219 | */ 220 | public void setCustomActionMenuCallBack(CustomActionMenuCallBack callBack) { 221 | this.mCustomActionMenuCallBack = callBack; 222 | } 223 | 224 | /* ***************************************************************************************** */ 225 | // view测量部分 226 | @Override 227 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 228 | // view的初始测量宽高(包含padding) 229 | int widthSize = MeasureSpec.getSize(widthMeasureSpec); 230 | int heightSize = MeasureSpec.getSize(heightMeasureSpec); 231 | int widthMode = MeasureSpec.getMode(widthMeasureSpec); 232 | int heightMode = MeasureSpec.getMode(heightMeasureSpec); 233 | Log.d(TAG, "widthSize " + widthSize); 234 | Log.d(TAG, "heightSize " + heightSize); 235 | // 粗略计算文字的最大宽度和最大高度,用于修正最后的测量宽高 236 | mTextAreaRoughBound = getTextRoughSize(heightSize == 0 ? mScreenHeight : heightSize, 237 | mLineSpacingExtra, mCharSpacingExtra); 238 | 239 | int measuredWidth; 240 | int measureHeight; 241 | 242 | // if (widthSize == 0) { 243 | // // 当嵌套在HorizontalScrollView时,MeasureSpec.getSize(widthMeasureSpec)返回0,因此需要特殊处理 244 | // measuredWidth = mTextAreaRoughBound[0]; 245 | // } else if (widthSize <= mScreenWidth) { 246 | // measuredWidth = mTextAreaRoughBound[0] <= mScreenWidth ? 247 | // Math.max(widthSize, mTextAreaRoughBound[0]) : widthSize; 248 | // } else { 249 | // measuredWidth = mTextAreaRoughBound[0] <= mScreenWidth ? 250 | // mScreenWidth : Math.min(widthSize, mTextAreaRoughBound[0]); 251 | // } 252 | 253 | if (widthSize == 0) { 254 | // 当嵌套在HorizontalScrollView时,MeasureSpec.getSize(widthMeasureSpec)返回0,因此需要特殊处理 255 | measuredWidth = mTextAreaRoughBound[0]; 256 | } else { 257 | measuredWidth = widthMode == MeasureSpec.AT_MOST 258 | || widthMode == MeasureSpec.UNSPECIFIED ? 259 | mTextAreaRoughBound[0] : widthSize; 260 | } 261 | 262 | if (heightSize == 0) { 263 | // 当嵌套在ScrollView时,MeasureSpec.getSize(widthMeasureSpec)返回0,因此需要特殊处理 264 | measureHeight = mScreenHeight; 265 | } else { 266 | measureHeight = heightMode == MeasureSpec.AT_MOST 267 | || heightMode == MeasureSpec.UNSPECIFIED ? 268 | mTextAreaRoughBound[1] : heightSize; 269 | } 270 | setMeasuredDimension(measuredWidth, measureHeight); 271 | 272 | Log.d(TAG, "measuredWidth " + measuredWidth); 273 | Log.d(TAG, "measureHeight " + measureHeight); 274 | } 275 | 276 | /** 277 | * 粗略计算文本的宽度和高度(包含padding),用于修正最后的测量宽高 278 | * 279 | * @param oriHeightSize 初始测量高度 必须大于0。当等于0时,用屏幕高度代替 280 | * @param lineSpacingExtra 281 | * @param charSpacingExtra 282 | * @return int[textWidth, textHeight] 283 | */ 284 | private int[] getTextRoughSize(int oriHeightSize, float lineSpacingExtra, 285 | float charSpacingExtra) { 286 | 287 | // 将文本用换行符分隔,计算粗略的行数 288 | String[] subTextStr = getText().toString().split("\n"); 289 | int textLines = 0; 290 | // 用于计算最大高度的目标子段落 291 | String targetSubPara = ""; 292 | int tempLines = 1; 293 | float tempLength = 0; 294 | // 计算每个段落的行数,然后累加 295 | for (String aSubTextStr : subTextStr) { 296 | // 段落的粗略长度(字符间距也要考虑进去) 297 | float subParagraphLength = aSubTextStr.length() * (getTextSize() + charSpacingExtra); 298 | // 段落长度除以初始测量高度,得到粗略行数 299 | int subLines = (int) Math.ceil(subParagraphLength 300 | / Math.abs(oriHeightSize - getPaddingTop() - getPaddingBottom())); 301 | if (subLines == 0) 302 | subLines = 1; 303 | textLines += subLines; 304 | // 如果所有子段落的行数都为1,则最大高度为长度最长的子段落长度;否则最大高度为oriHeightSize; 305 | if (subLines == 1 && tempLines == 1) { 306 | if (subParagraphLength > tempLength) { 307 | tempLength = subParagraphLength; 308 | targetSubPara = aSubTextStr; 309 | } 310 | } 311 | tempLines = subLines; 312 | } 313 | // 计算文本粗略高度,包括padding 314 | int textHeight = getPaddingTop() + getPaddingBottom(); 315 | if (textLines > subTextStr.length) 316 | textHeight = oriHeightSize; 317 | else { 318 | // 计算targetSubPara长度作为高度 319 | for (int i = 0; i < targetSubPara.length(); i++) { 320 | String char_i = String.valueOf(getText().toString().charAt(i)); 321 | // 区别标点符号 和 文字 322 | if (isUnicodeSymbol(char_i)) { 323 | textHeight += 1.4f * getCharHeight(char_i, getTextPaint()) + charSpacingExtra; 324 | } else { 325 | textHeight += getTextSize() + charSpacingExtra; 326 | } 327 | } 328 | } 329 | // 计算文本的粗略宽度,包括padding, 330 | int textWidth = getPaddingLeft() + getPaddingRight() + 331 | (int) ((textLines + 1) * getTextSize() + lineSpacingExtra * (textLines - 1)); 332 | Log.d(TAG, "textRoughLines " + textLines); 333 | Log.d(TAG, "textRoughWidth " + textWidth); 334 | Log.d(TAG, "textRoughHeight " + textHeight); 335 | return new int[]{textWidth, textHeight}; 336 | } 337 | 338 | /* ***************************************************************************************** */ 339 | // 触摸事件部分。处理onclick事件,长按选择文字事件 和 弹出ActionMenu事件 340 | @Override 341 | public boolean onTouchEvent(MotionEvent event) { 342 | int action = event.getAction(); 343 | int currentLine; // 当前所在行 344 | switch (action) { 345 | case MotionEvent.ACTION_DOWN: 346 | Log.d(TAG, "ACTION_DOWN"); 347 | // 每次按下时,创建ActionMenu菜单,创建不成功,屏蔽长按事件 348 | if (null == mActionMenu) { 349 | mActionMenu = createActionMenu(); 350 | } 351 | mTouchDownX = event.getX(); 352 | mTouchDownY = event.getY(); 353 | mTouchDownRawY = event.getRawY(); 354 | isLongPress = false; 355 | isVibrator = false; 356 | isLongPressTouchActionUp = false; 357 | break; 358 | case ACTION_MOVE: 359 | Log.d(TAG, "ACTION_MOVE"); 360 | // 先判断是否禁用了ActionMenu功能,以及ActionMenu是否创建失败, 361 | // 二者只要满足了一个条件,退出长按事件 362 | if (isShowActionMenu || mActionMenu.getChildCount() == 0) { 363 | // 手指移动过程中的字符偏移 364 | currentLine = getCurrentTouchLine(event.getX(), isLeftToRight); 365 | float mWordOffset_move = event.getY(); 366 | // 判断是否触发长按事件 判断条件为: 367 | // 1.超过时间阈值;2.且超过手指移动的最小位移阈值;3.且未超过边界,在padding内 368 | boolean isTriggerTime = event.getEventTime() - event.getDownTime() >= TRIGGER_LONGPRESS_TIME_THRESHOLD; 369 | boolean isTriggerDistance = Math.abs(event.getX() - mTouchDownX) < TRIGGER_LONGPRESS_DISTANCE_THRESHOLD 370 | && Math.abs(event.getY() - mTouchDownY) < TRIGGER_LONGPRESS_DISTANCE_THRESHOLD; 371 | int[] drawPadding = getDrawPadding(isLeftToRight); 372 | boolean isInBound = event.getX() >= drawPadding[0] && event.getX() <= getWidth() - drawPadding[2] 373 | && event.getY() >= drawPadding[1] && event.getY() <= getHeight() - drawPadding[3]; 374 | if (isTriggerTime && isTriggerDistance && isInBound) { 375 | Log.d(TAG, "ACTION_MOVE 长按"); 376 | isLongPress = true; 377 | isLongPressTouchActionUp = false; 378 | mStartLine = currentLine; 379 | mStartTextOffset = mWordOffset_move; 380 | // 每次触发长按时,震动提示一次 381 | if (!isVibrator) { 382 | mVibrator.vibrate(60); 383 | isVibrator = true; 384 | } 385 | } 386 | if (isLongPress) { 387 | mCurrentLine = currentLine; 388 | mCurrentTextOffset = mWordOffset_move; 389 | // 通知父布局不要拦截触摸事件 390 | getParent().requestDisallowInterceptTouchEvent(true); 391 | // 通知view绘制所选文字背景色 392 | invalidate(); 393 | } 394 | } 395 | break; 396 | case MotionEvent.ACTION_UP: 397 | Log.d(TAG, "ACTION_UP"); 398 | // 处理长按事件 399 | if (isLongPress) { 400 | currentLine = getCurrentTouchLine(event.getX(), isLeftToRight); 401 | float mWordOffsetEnd = event.getY(); 402 | mCurrentLine = currentLine; 403 | mCurrentTextOffset = mWordOffsetEnd; 404 | // 手指抬起后选择文字 405 | selectText(mStartTextOffset, mCurrentTextOffset, mStartLine, mCurrentLine, mCharSpacingExtra, isLeftToRight); 406 | if (!TextUtils.isEmpty(mSelectedText)) { 407 | // 计算菜单显示位置 408 | int mPopWindowOffsetY = calculatorActionMenuYPosition((int) mTouchDownRawY, (int) event.getRawY()); 409 | // 弹出菜单 410 | showActionMenu(mPopWindowOffsetY, mActionMenu); 411 | } 412 | isLongPressTouchActionUp = true; 413 | isLongPress = false; 414 | 415 | } else if (event.getEventTime() - event.getDownTime() < TRIGGER_LONGPRESS_TIME_THRESHOLD) { 416 | // 由于onTouchEvent最终返回了true,onClick事件会被屏蔽掉,因此在这里处理onClick事件 417 | if (null != mOnClickListener) 418 | mOnClickListener.onClick(this); 419 | } 420 | // 通知父布局继续拦截触摸事件 421 | getParent().requestDisallowInterceptTouchEvent(false); 422 | break; 423 | } 424 | return true; 425 | } 426 | 427 | /** 428 | * 计算触摸位置所在行,最小值为1 429 | * 430 | * @param offsetX 431 | * @param isLeftToRight 432 | * @return 433 | */ 434 | private int getCurrentTouchLine(float offsetX, boolean isLeftToRight) { 435 | 436 | int currentLine = 1; 437 | float lineWidth = getTextSize() + mLineSpacingExtra; 438 | int[] drawPadding = getDrawPadding(isLeftToRight); 439 | if (isLeftToRight) { 440 | // 边界控制 441 | if (offsetX >= getWidth() - drawPadding[2]) 442 | currentLine = mMaxTextLine; 443 | else 444 | currentLine = (int) Math.ceil((offsetX - drawPadding[0]) / lineWidth); 445 | } else { 446 | if (offsetX <= drawPadding[0]) 447 | currentLine = mMaxTextLine; 448 | else 449 | currentLine = (int) Math.ceil((getWidth() - offsetX - drawPadding[2]) / lineWidth); 450 | } 451 | 452 | currentLine = currentLine <= 0 ? 1 : (currentLine > mMaxTextLine ? mMaxTextLine : currentLine); 453 | 454 | Log.d(TAG, "touch line is: " + currentLine); 455 | return currentLine; 456 | } 457 | 458 | /** 459 | * 选择选中的字符 460 | * 461 | * @param startOffsetY 462 | * @param endOffsetY 463 | * @param startLine 464 | * @param endLine 465 | * @param charSpacingExtra 466 | */ 467 | private void selectText(float startOffsetY, float endOffsetY, 468 | int startLine, int endLine, float charSpacingExtra, boolean isLeftToRight) { 469 | // 计算开始和结束的字符index 470 | int index_start = getSelectTextIndex(startOffsetY, startLine, charSpacingExtra, isLeftToRight); 471 | int index_end = getSelectTextIndex(endOffsetY, endLine, charSpacingExtra, isLeftToRight); 472 | if (index_start == index_end) 473 | mSelectedText = ""; 474 | else { 475 | String textAll = getText().toString(); 476 | if (TextUtils.isEmpty(textAll)) 477 | mSelectedText = ""; 478 | else 479 | mSelectedText = textAll.substring(Math.min(index_start, index_end), 480 | Math.max(index_start, index_end)); 481 | } 482 | Log.d(TAG, "mSelectedText " + mSelectedText); 483 | } 484 | 485 | /** 486 | * 计算所选文字起始或结束字符对应的index 487 | * 488 | * @param offsetY 489 | * @param targetLine 490 | * @param charSpacingExtra 字符间距 491 | */ 492 | private int getSelectTextIndex(float offsetY, int targetLine, float charSpacingExtra, boolean isLeftToRight) { 493 | int[] drawPadding = getDrawPadding(isLeftToRight); 494 | // 该行文字的起始和结束位置 495 | int[] lineIndex = mLinesTextIndex.get(targetLine); 496 | // 目标位置 497 | int targetIndex = lineIndex[1]; 498 | float tempY = drawPadding[1]; 499 | // 边界控制 500 | if (offsetY < drawPadding[1]) { 501 | return lineIndex[0]; 502 | } else if (offsetY > getHeight() - drawPadding[3]) { 503 | return lineIndex[1]; 504 | } 505 | /* 506 | * 循环累加每一个字符的高度,一直到tempY > offsetY 时停止,然后根据行首或行末计算index; 507 | * 如果循环完成后依然未触发tempY >= offsetY条件,返回该行的最后一个字符index,即lineIndex[1]; 508 | */ 509 | for (int i = lineIndex[0]; i < lineIndex[1]; i++) { 510 | String char_i = String.valueOf(getText().toString().charAt(i)); 511 | // 区别换行符,标点符号 和 文字 512 | if (char_i.equals("\n")) { 513 | tempY = drawPadding[1]; 514 | } else if (isUnicodeSymbol(char_i)) { 515 | tempY += 1.4f * getCharHeight(char_i, getTextPaint()) + charSpacingExtra; 516 | } else { 517 | tempY += getTextSize() + charSpacingExtra; 518 | } 519 | // 触发停止的条件 520 | if (tempY >= offsetY) { 521 | targetIndex = i; 522 | break; 523 | } 524 | } 525 | Log.d(TAG, "target index " + targetIndex); 526 | return targetIndex; 527 | } 528 | 529 | /** 530 | * 计算弹出菜单相对于父布局的Y向偏移 531 | * 532 | * @param yOffsetStart 所选字符的起始位置相对屏幕的Y向偏移 533 | * @param yOffsetEnd 所选字符的结束位置相对屏幕的Y向偏移 534 | * @return 535 | */ 536 | private int calculatorActionMenuYPosition(int yOffsetStart, int yOffsetEnd) { 537 | if (yOffsetStart > yOffsetEnd) { 538 | int temp = yOffsetStart; 539 | yOffsetStart = yOffsetEnd; 540 | yOffsetEnd = temp; 541 | } 542 | int actionMenuOffsetY; 543 | 544 | if (yOffsetStart < mActionMenuHeight * 3 / 2 + mStatusBarHeight) { 545 | if (yOffsetEnd > mScreenHeight - mActionMenuHeight * 3 / 2) { 546 | // 菜单显示在屏幕中间 547 | actionMenuOffsetY = mScreenHeight / 2 - mActionMenuHeight / 2; 548 | } else { 549 | // 菜单显示所选文字下方 550 | actionMenuOffsetY = yOffsetEnd + mActionMenuHeight / 2; 551 | } 552 | } else { 553 | // 菜单显示所选文字上方 554 | actionMenuOffsetY = yOffsetStart - mActionMenuHeight * 3 / 2; 555 | } 556 | return actionMenuOffsetY; 557 | } 558 | 559 | /* ***************************************************************************************** */ 560 | // 创建ActionMenu部分 561 | 562 | /** 563 | * 创建ActionMenu菜单 564 | * 565 | * @return 566 | */ 567 | private ActionMenu createActionMenu() { 568 | // 创建菜单 569 | ActionMenu actionMenu = new ActionMenu(mContext); 570 | // 是否需要移除默认item 571 | boolean isRemoveDefaultItem = false; 572 | if (null != mCustomActionMenuCallBack) { 573 | isRemoveDefaultItem = mCustomActionMenuCallBack.onCreateCustomActionMenu(actionMenu); 574 | } 575 | if (!isRemoveDefaultItem) 576 | actionMenu.addDefaultMenuItem(); // 添加默认item 577 | 578 | actionMenu.addCustomItem(); // 添加自定义item 579 | actionMenu.setFocusable(true); // 获取焦点 580 | actionMenu.setFocusableInTouchMode(true); 581 | 582 | if (actionMenu.getChildCount() != 0) { 583 | // item监听 584 | for (int i = 0; i < actionMenu.getChildCount(); i++) { 585 | actionMenu.getChildAt(i).setOnClickListener(mMenuClickListener); 586 | } 587 | } 588 | return actionMenu; 589 | } 590 | 591 | /** 592 | * 长按弹出菜单 593 | * 594 | * @param offsetY 595 | * @param actionMenu 596 | * @return 菜单创建成功,返回true 597 | */ 598 | private void showActionMenu(int offsetY, ActionMenu actionMenu) { 599 | 600 | mActionMenuPopupWindow = new PopupWindow(actionMenu, WindowManager.LayoutParams.WRAP_CONTENT, 601 | mActionMenuHeight, true); 602 | mActionMenuPopupWindow.setFocusable(true); 603 | mActionMenuPopupWindow.setOutsideTouchable(false); 604 | mActionMenuPopupWindow.setBackgroundDrawable(new ColorDrawable(0x00000000)); 605 | mActionMenuPopupWindow.showAtLocation(this, Gravity.TOP | Gravity.CENTER_HORIZONTAL, 0, offsetY); 606 | 607 | mActionMenuPopupWindow.setOnDismissListener(new PopupWindow.OnDismissListener() { 608 | @Override 609 | public void onDismiss() { 610 | // 清理已选的文字 611 | clearSelectedTextBackground(); 612 | } 613 | }); 614 | } 615 | 616 | /** 617 | * 隐藏菜单 618 | */ 619 | private void hideActionMenu() { 620 | if (null != mActionMenuPopupWindow) { 621 | mActionMenuPopupWindow.dismiss(); 622 | mActionMenuPopupWindow = null; 623 | } 624 | } 625 | 626 | /** 627 | * 菜单点击事件监听 628 | */ 629 | private OnClickListener mMenuClickListener = new OnClickListener() { 630 | @Override 631 | public void onClick(View v) { 632 | 633 | String menuItemTitle = (String) v.getTag(); 634 | 635 | if (menuItemTitle.equals(ActionMenu.DEFAULT_MENU_ITEM_TITLE_SELECT_ALL)) { 636 | //全选事件 637 | mSelectedText = getText().toString(); 638 | int[] drawPadding = getDrawPadding(isLeftToRight); 639 | mStartLine = 1; 640 | mCurrentLine = mMaxTextLine; 641 | mStartTextOffset = drawPadding[1]; 642 | mCurrentTextOffset = getHeight() - drawPadding[3]; 643 | isLongPressTouchActionUp = true; 644 | invalidate(); 645 | 646 | } else if (menuItemTitle.equals(ActionMenu.DEFAULT_MENU_ITEM_TITLE_COPY)) { 647 | // 复制事件 648 | Utils.copyText(mContext, mSelectedText); 649 | Toast.makeText(mContext, "复制成功!", Toast.LENGTH_SHORT).show(); 650 | hideActionMenu(); 651 | 652 | } else { 653 | // 自定义事件 654 | if (null != mCustomActionMenuCallBack) { 655 | mCustomActionMenuCallBack.onCustomActionItemClicked(menuItemTitle, mSelectedText); 656 | } 657 | hideActionMenu(); 658 | } 659 | } 660 | }; 661 | 662 | /* ***************************************************************************************** */ 663 | // 绘制部分 664 | 665 | @Override 666 | protected void onDraw(Canvas canvas) { 667 | // 绘制竖排文字 668 | drawVerticalText(canvas, mLineSpacingExtra, mCharSpacingExtra, isLeftToRight); 669 | // 绘制下划线 670 | drawTextUnderline(canvas, isLeftToRight, mUnderLineOffset, mCharSpacingExtra); 671 | // 绘制选中文字的背景,触发条件: 672 | // 1.长按事件 2.全选事件 3.手指滑动过快时,进入ACTION_UP事件后,可能会出现背景未绘制的情况 673 | if (isLongPress | isActionSelectAll | isLongPressTouchActionUp) { 674 | drawSelectedTextBackground(canvas, mStartLine, mCurrentLine, 675 | mStartTextOffset, mCurrentTextOffset, mLineSpacingExtra, mCharSpacingExtra, isLeftToRight); 676 | isActionSelectAll = false; 677 | isLongPressTouchActionUp = false; 678 | } 679 | } 680 | 681 | /** 682 | * 绘制竖排文字 683 | * 684 | * @param canvas 685 | * @param lineSpacingExtra 行距 686 | * @param charSpacingExtra 字符间距 687 | * @param isLeftToRight 文字方向 688 | */ 689 | private void drawVerticalText(Canvas canvas, float lineSpacingExtra, 690 | float charSpacingExtra, boolean isLeftToRight) { 691 | // 文字画笔 692 | TextPaint textPaint = getTextPaint(); 693 | int textStrLength = getText().length(); 694 | if (textStrLength == 0) 695 | return; 696 | // 每次绘制时初始化参数 697 | mMaxTextLine = 1; 698 | int currentLineStartIndex = 0; // 行首位置标记 699 | mLinesOffsetArray.clear(); 700 | mLinesTextIndex.clear(); 701 | int[] drawPadding = getDrawPadding(isLeftToRight); // 绘制文字的padding 702 | // 当前竖行的XY向偏移初始值 703 | float currentLineOffsetX = isLeftToRight ? 704 | drawPadding[0] : getWidth() - drawPadding[2] - getTextSize(); 705 | float currentLineOffsetY = drawPadding[1] + getTextSize(); 706 | for (int j = 0; j < textStrLength; j++) { 707 | String char_j = String.valueOf(getText().charAt(j)); 708 | /* 换行条件为: 709 | * 1:遇到换行符; 710 | * 2:该竖行是否已经写满。 711 | * 712 | * 该竖行是否已经写满,判定条件为: 713 | * 1.y向剩余的空间已经不够填下一个文字; 714 | * 2.且当前要绘制的文字不是标点符号; 715 | * 3.或当前要绘制的文字是标点符号,但标点符号的高度大于y向剩余的空间 716 | * 注意:文字是从左下角开始向上绘制的 717 | */ 718 | boolean isLineBreaks = char_j.equals("\n"); 719 | boolean isCurrentLineFinish = currentLineOffsetY > getHeight() - drawPadding[3] 720 | && (!isUnicodeSymbol(char_j) || (isUnicodeSymbol(char_j) && 721 | currentLineOffsetY + getCharHeight(char_j, textPaint) > getHeight() - drawPadding[3] + getTextSize())); 722 | 723 | if (isLineBreaks || isCurrentLineFinish) { 724 | // 记录记录偏移量,和行首行末字符的index;然后另起一行, 725 | mLinesOffsetArray.put(mMaxTextLine, new Float[]{currentLineOffsetX, currentLineOffsetY}); 726 | mLinesTextIndex.put(mMaxTextLine, new int[]{currentLineStartIndex, j}); 727 | // 另起一竖行,更新偏移量 728 | currentLineOffsetX = isLeftToRight ? 729 | currentLineOffsetX + getTextSize() + lineSpacingExtra 730 | : currentLineOffsetX - getTextSize() - lineSpacingExtra; 731 | currentLineOffsetY = drawPadding[1] + getTextSize(); 732 | mMaxTextLine++; 733 | } 734 | // 判断是否是行首,记录行首字符位置; 735 | // 判断行首的条件为:currentLineOffsetY == drawPadding[1]+getTextSize() 736 | if (currentLineOffsetY == drawPadding[1] + getTextSize()) { 737 | currentLineStartIndex = j; 738 | } 739 | 740 | // 绘制第j个字符. 741 | if (isLineBreaks) { 742 | // 如果是换行符,do nothing 743 | //char_j = ""; 744 | //canvas.drawText(char_j, currentLineOffsetX, currentLineOffsetY, textPaint); 745 | } else if (isUnicodeSymbol(char_j)) { 746 | // 如果是Y向需要补偿标点符号,加一个补偿 getTextSize() - getCharHeight. 747 | // 注意:如果该竖行第一个字符是标点符号的话,不加补偿; 748 | // 判断是否是第一个字符的条件为:offsetY == drawPadding[1] + getTextSize() 749 | float drawOffsetY = currentLineOffsetY; 750 | if (isSymbolNeedOffset(char_j)) 751 | drawOffsetY = drawOffsetY - (getTextSize() - 1.4f * getCharHeight(char_j, textPaint)); 752 | // 文字从左向右,标点符号靠右绘制,竖排标点除外 753 | float drawOffsetX = currentLineOffsetX; 754 | if (isLeftToRight && !isVerticalSymbol(char_j)) 755 | drawOffsetX = drawOffsetX + getTextSize() / 2; 756 | 757 | canvas.drawText(char_j, drawOffsetX, drawOffsetY, textPaint); 758 | currentLineOffsetY += 1.4f * getCharHeight(char_j, textPaint) + charSpacingExtra; 759 | 760 | } else { 761 | canvas.drawText(char_j, currentLineOffsetX, currentLineOffsetY, textPaint); 762 | currentLineOffsetY += getTextSize() + charSpacingExtra; 763 | } 764 | 765 | // 最后一行的偏移量和行首行末字符的index; 766 | if (j == textStrLength - 1) { 767 | mLinesOffsetArray.put(mMaxTextLine, new Float[]{currentLineOffsetX, currentLineOffsetY}); 768 | mLinesTextIndex.put(mMaxTextLine, new int[]{currentLineStartIndex, textStrLength}); 769 | } 770 | } 771 | Log.d(TAG, "mMaxTextLine is : " + mMaxTextLine); 772 | } 773 | 774 | /** 775 | * 绘制下划线 776 | * 777 | * @param canvas 778 | * @param isLeftToRight 文字方向 779 | * @param underLineOffset 下划线偏移量 >0 780 | * @param charSpacingExtra 781 | */ 782 | private void drawTextUnderline(Canvas canvas, boolean isLeftToRight, float underLineOffset, 783 | float charSpacingExtra) { 784 | 785 | if (!isUnderLineText || mUnderLineWidth == 0) 786 | return; 787 | 788 | // 下划线paint 789 | Paint underLinePaint = getPaint(); 790 | underLinePaint.setColor(mUnderLineColor); 791 | underLinePaint.setAntiAlias(true); 792 | underLinePaint.setStyle(Paint.Style.FILL); 793 | underLinePaint.setStrokeWidth(mUnderLineWidth); 794 | 795 | int[] drawPadding = getDrawPadding(isLeftToRight); // 绘制文字的padding 796 | 797 | for (int i = 0; i < mMaxTextLine; i++) { 798 | // Y向开始和结束位置 799 | float yStart = drawPadding[1]; 800 | float yEnd = mLinesOffsetArray.get(i + 1)[1] - getTextSize(); 801 | // 如果end <= start 或者 该行字符为换行符,则不绘制下划线 802 | int[] lineIndex = mLinesTextIndex.get(i + 1); 803 | String lineText = getText().toString().substring(lineIndex[0], lineIndex[1]); 804 | if (yEnd <= yStart || (lineText.equals("\n"))) 805 | continue; 806 | // Y向边界处理 807 | if (yEnd > getHeight() - drawPadding[3] - getTextSize()) 808 | yEnd = getHeight() - drawPadding[3]; 809 | // 首行缩进处理 810 | int spaceNum = getLineStartSpaceNumber(lineText); 811 | if (spaceNum > 0) { 812 | yStart = yStart + (getTextSize() + charSpacingExtra) * spaceNum; 813 | } 814 | 815 | // X向;注意不同的文字方向和下划线偏移 816 | float xStart = mLinesOffsetArray.get(i + 1)[0]; 817 | if (isLeftToRight) 818 | xStart += getTextSize() + underLineOffset; 819 | else 820 | xStart -= underLineOffset; 821 | float xEnd = xStart; 822 | 823 | canvas.drawLine(xStart, yStart, xEnd, yEnd, underLinePaint); 824 | } 825 | } 826 | 827 | /** 828 | * 绘制所选文字高亮色 829 | * 830 | * @param canvas 831 | * @param startLine 开始行 832 | * @param endLine 结束行 833 | * @param startOffsetY 开始字符Y向偏移 834 | * @param endOffsetY 结束文字Y向偏移 835 | * @param lineSpacingExtra 836 | * @param charSpacingExtra 837 | * @param isLeftToRight 838 | */ 839 | private void drawSelectedTextBackground(Canvas canvas, int startLine, int endLine, 840 | float startOffsetY, float endOffsetY, 841 | float lineSpacingExtra, float charSpacingExtra, 842 | boolean isLeftToRight) { 843 | 844 | if (startLine == endLine && Math.abs(endOffsetY - startOffsetY) == 0) { 845 | return; 846 | } 847 | // 文字背景高亮画笔 848 | Paint highlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 849 | highlightPaint.setStyle(Paint.Style.FILL); 850 | highlightPaint.setColor(mTextHighlightColor); 851 | highlightPaint.setAlpha(60); 852 | 853 | int[] drawPadding = getDrawPadding(isLeftToRight); // 绘制文字的padding 854 | 855 | // 预处理,如果startLine > endLine,交换二者 856 | if (startLine > endLine) { 857 | startLine = startLine + endLine; 858 | endLine = startLine - endLine; 859 | startLine = startLine - endLine; 860 | startOffsetY = startOffsetY + endOffsetY; 861 | endOffsetY = startOffsetY - endOffsetY; 862 | startOffsetY = startOffsetY - endOffsetY; 863 | } 864 | // 行宽 865 | int lineWidth = (int) (getTextSize() + lineSpacingExtra); 866 | // 开始行和结束行所选文字的y向偏移量 867 | int startLineOffsetY = getSelectTextPreciseOffsetY(startOffsetY, startLine, charSpacingExtra, true, isLeftToRight); 868 | int endLineOffsetY = getSelectTextPreciseOffsetY(endOffsetY, endLine, charSpacingExtra, false, isLeftToRight); 869 | // 围绕所选的文字创建一个Path闭合路径,一共八个点 870 | Path path_all = new Path(); 871 | if (isLeftToRight) { 872 | // 往左偏移半个行距 873 | int offsetLeftPadding = (int) (drawPadding[0] - lineSpacingExtra / 2); 874 | path_all.moveTo(offsetLeftPadding + (startLine - 1) * lineWidth, startLineOffsetY); 875 | path_all.lineTo(offsetLeftPadding + startLine * lineWidth, startLineOffsetY); 876 | path_all.lineTo(offsetLeftPadding + startLine * lineWidth, drawPadding[1]); 877 | path_all.lineTo(offsetLeftPadding + endLine * lineWidth, drawPadding[1]); 878 | path_all.lineTo(offsetLeftPadding + endLine * lineWidth, endLineOffsetY); 879 | path_all.lineTo(offsetLeftPadding + (endLine - 1) * lineWidth, endLineOffsetY); 880 | path_all.lineTo(offsetLeftPadding + (endLine - 1) * lineWidth, getHeight() - drawPadding[3] + charSpacingExtra); 881 | path_all.lineTo(offsetLeftPadding + (startLine - 1) * lineWidth, getHeight() - drawPadding[3] + charSpacingExtra); 882 | path_all.close(); 883 | } else { 884 | // 往右偏移半个行距 885 | int offsetRightPadding = (int) (getWidth() - drawPadding[2] + lineSpacingExtra / 2); 886 | path_all.moveTo(offsetRightPadding - (startLine - 1) * lineWidth, startLineOffsetY); 887 | path_all.lineTo(offsetRightPadding - startLine * lineWidth, startLineOffsetY); 888 | path_all.lineTo(offsetRightPadding - startLine * lineWidth, drawPadding[1]); 889 | path_all.lineTo(offsetRightPadding - endLine * lineWidth, drawPadding[1]); 890 | path_all.lineTo(offsetRightPadding - endLine * lineWidth, endLineOffsetY); 891 | path_all.lineTo(offsetRightPadding - (endLine - 1) * lineWidth, endLineOffsetY); 892 | path_all.lineTo(offsetRightPadding - (endLine - 1) * lineWidth, getHeight() - drawPadding[3] + charSpacingExtra); 893 | path_all.lineTo(offsetRightPadding - (startLine - 1) * lineWidth, getHeight() - drawPadding[3] + charSpacingExtra); 894 | path_all.close(); 895 | } 896 | canvas.drawPath(path_all, highlightPaint); 897 | canvas.save(); 898 | canvas.restore(); 899 | } 900 | 901 | /** 902 | * 根据文本的Gravity计算文字绘制时的padding 903 | * 904 | * @param isLeftToRight 文字阅读方向 905 | * @return [left, top, right, bottom] 906 | */ 907 | private int[] getDrawPadding(boolean isLeftToRight) { 908 | int textBoundWidth = mTextAreaRoughBound[0]; 909 | int textBoundHeight = mTextAreaRoughBound[1]; 910 | int left, right, top, bottom; 911 | int gravity; 912 | 913 | if (textBoundWidth < getWidth()) { 914 | // 先把水平方向的gravity解析出来 915 | gravity = getGravity() & Gravity.HORIZONTAL_GRAVITY_MASK; 916 | if (gravity == Gravity.CENTER || gravity == Gravity.CENTER_HORIZONTAL) { 917 | left = getPaddingLeft() + (getWidth() - textBoundWidth) / 2; 918 | right = getPaddingRight() + (getWidth() - textBoundWidth) / 2; 919 | } else if (gravity == Gravity.RIGHT && isLeftToRight) { 920 | left = getPaddingLeft() + getWidth() - textBoundWidth; 921 | right = getPaddingRight(); 922 | } else if (gravity == Gravity.LEFT && !isLeftToRight) { 923 | left = getPaddingLeft(); 924 | right = getPaddingRight() + getWidth() - textBoundWidth; 925 | } else { 926 | left = isLeftToRight ? getPaddingLeft() : getPaddingLeft() + getWidth() - textBoundWidth; 927 | right = isLeftToRight ? getPaddingRight() + getWidth() - textBoundWidth : getPaddingRight(); 928 | } 929 | } else { 930 | left = getPaddingLeft(); 931 | right = getPaddingRight(); 932 | } 933 | 934 | if (textBoundHeight < getHeight()) { 935 | // 先把垂直方向的gravity解析出来 936 | gravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK; 937 | if (gravity == Gravity.CENTER || gravity == Gravity.CENTER_VERTICAL) { 938 | top = getPaddingTop() + (getHeight() - textBoundHeight) / 2; 939 | bottom = getPaddingBottom() + (getHeight() - textBoundHeight) / 2; 940 | } else if (gravity == Gravity.BOTTOM) { 941 | top = getPaddingTop() + getHeight() - textBoundHeight; 942 | bottom = getPaddingBottom(); 943 | } else { 944 | top = getPaddingTop(); 945 | bottom = getPaddingBottom() + getHeight() - textBoundHeight; 946 | } 947 | } else { 948 | top = getPaddingTop(); 949 | bottom = getPaddingBottom(); 950 | } 951 | 952 | return new int[]{left, top, right, bottom}; 953 | } 954 | 955 | /** 956 | * 获取所选文字的精确Y向偏移 957 | * 958 | * @param offsetY 959 | * @param targetLine 960 | * @param charSpacingExtra 961 | * @return 962 | */ 963 | private int getSelectTextPreciseOffsetY(float offsetY, int targetLine, float charSpacingExtra, 964 | boolean isStart, boolean isLeftToRight) { 965 | 966 | int[] drawPadding = getDrawPadding(isLeftToRight); // 绘制文字的padding 967 | // 该行文字的起始和结束位置 968 | int[] lineIndex = mLinesTextIndex.get(targetLine); 969 | // 目标位置 970 | int targetOffset = drawPadding[1]; 971 | int tempY = drawPadding[1]; 972 | // 边界控制 973 | if (offsetY < drawPadding[1]) { 974 | return drawPadding[1]; 975 | } else if (offsetY > getHeight() - drawPadding[3]) { 976 | return getHeight() - drawPadding[3]; 977 | } 978 | /* 979 | * 循环累加每一个字符的高度,一直到tempY > offsetY 时停止,然后根据行首或行末计算精确的偏移量; 980 | * 如果循环完成后依然未触发tempY >= offsetY条件,返回该行的最大长度; 981 | */ 982 | for (int i = lineIndex[0]; i < lineIndex[1]; i++) { 983 | String char_i = String.valueOf(getText().toString().charAt(i)); 984 | // 区别换行符,标点符号 和 文字 985 | if (char_i.equals("\n")) { 986 | tempY = drawPadding[1]; 987 | } else if (isUnicodeSymbol(char_i)) { 988 | tempY += 1.4f * getCharHeight(char_i, getTextPaint()) + charSpacingExtra; 989 | } else { 990 | tempY += getTextSize() + charSpacingExtra; 991 | } 992 | if (tempY <= offsetY) { 993 | targetOffset = tempY; 994 | } 995 | // 触发暂停条件 996 | if (tempY > offsetY) { 997 | break; 998 | } 999 | } 1000 | return Math.max(targetOffset, drawPadding[1]); 1001 | } 1002 | 1003 | /** 1004 | * 清除所选文字的背景 1005 | */ 1006 | private void clearSelectedTextBackground() { 1007 | mSelectedText = ""; 1008 | mStartLine = mCurrentLine = 0; 1009 | mStartTextOffset = mCurrentTextOffset = 0; 1010 | invalidate(); 1011 | } 1012 | 1013 | /** 1014 | * 文字画笔 1015 | * 1016 | * @return 1017 | */ 1018 | private TextPaint getTextPaint() { 1019 | // 文字画笔 1020 | TextPaint textPaint = getPaint(); 1021 | textPaint.setColor(getCurrentTextColor()); 1022 | return textPaint; 1023 | } 1024 | 1025 | /** 1026 | * 计算首行缩进的空格数 1027 | * 1028 | * @param lineText 1029 | * @return 1030 | */ 1031 | private int getLineStartSpaceNumber(String lineText) { 1032 | if (lineText.startsWith(" ")) { 1033 | return 4; 1034 | } else if (lineText.startsWith("   ") || lineText.startsWith(" ")) { 1035 | return 3; 1036 | } else if (lineText.startsWith("  ") || lineText.startsWith(" ")) { 1037 | return 2; 1038 | } else if (lineText.startsWith(" ") || lineText.startsWith(" ")) { 1039 | return 1; 1040 | } else 1041 | return 0; 1042 | } 1043 | 1044 | /** 1045 | * 获取一个字符的高度 1046 | * 1047 | * @param target_char 1048 | * @param paint 1049 | * @return 1050 | */ 1051 | private float getCharHeight(String target_char, Paint paint) { 1052 | Rect rect = new Rect(); 1053 | paint.getTextBounds(target_char, 0, 1, rect); 1054 | return rect.height(); 1055 | } 1056 | 1057 | /** 1058 | * 获取一个字符的宽度 1059 | * 1060 | * @param target_char 1061 | * @param paint 1062 | * @return 1063 | */ 1064 | private float getCharWidth(String target_char, Paint paint) { 1065 | Rect rect = new Rect(); 1066 | paint.getTextBounds(target_char, 0, 1, rect); 1067 | return rect.width(); 1068 | } 1069 | 1070 | /** 1071 | * 判断是否是标点符号 1072 | * - - —— = + ~ 这几个不做判断 1073 | * 1074 | * @param str 1075 | * @return 1076 | */ 1077 | private boolean isUnicodeSymbol(String str) { 1078 | String regex = ".*[_\"`!@#$%^&*()|{}':;,\\[\\].<>/?!¥…()【】‘’;:”“。,、?︵ ︷︿︹︽﹁﹃︻︶︸﹀︺︾ˉ﹂﹄︼]$+.*"; 1079 | Matcher m = Pattern.compile(regex).matcher(str); 1080 | return m.matches(); 1081 | } 1082 | 1083 | /** 1084 | * 需要补偿的标点符号 1085 | * - - —— = + ~ 这几个不做补偿 1086 | * 1087 | * @param str 1088 | * @return 1089 | */ 1090 | private boolean isSymbolNeedOffset(String str) { 1091 | String regex = ".*[_!@#$%&()|{}:;,\\[\\].<>/?!¥…()【】;:。,、?︵ ︷︿︹︽﹁﹃︻]$+.*"; 1092 | Matcher m = Pattern.compile(regex).matcher(str); 1093 | return m.matches(); 1094 | } 1095 | 1096 | /** 1097 | * 是否是竖排标点符号 1098 | * 1099 | * @param str 1100 | * @return 1101 | */ 1102 | private boolean isVerticalSymbol(String str) { 1103 | String regex = ".*[︵ ︷︿︹︽﹁﹃︻︶︸﹀︺︾ˉ﹂﹄︼|]$+.*"; 1104 | Matcher m = Pattern.compile(regex).matcher(str); 1105 | return m.matches(); 1106 | } 1107 | 1108 | } 1109 | -------------------------------------------------------------------------------- /library/src/main/res/values/attrs_selectable_text.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /library/src/main/res/values/attrs_vertical_textview.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /library/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | AdvancedTextView 3 | 4 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':library' 2 | --------------------------------------------------------------------------------