├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── example
│ │ └── testdemo1
│ │ └── ExampleInstrumentedTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── example
│ │ │ └── testdemo1
│ │ │ ├── AlignTextView.java
│ │ │ ├── MainActivity.java
│ │ │ ├── RApplication.java
│ │ │ └── XQJustifyTextView.java
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ └── ic_launcher_background.xml
│ │ ├── layout
│ │ ├── activity_main.xml
│ │ └── content_main.xml
│ │ ├── menu
│ │ └── menu_main.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ └── values
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ └── test
│ └── java
│ └── com
│ └── example
│ └── testdemo1
│ └── ExampleUnitTest.java
├── build.gradle
└── gradle.properties
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Panxuqin
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | ### 1.非中文单词不够一行会自动截断,用符号“-”连接起来;
3 |
4 | ### 2.适配布局的方向,使用原生TextView的属性:android:gravity=""和android:textAlignment="",gravity的优先级较高,如果同时设置这两个属性则以textAlignment的属性为准;
5 | ```
6 |
13 | ```
14 |
15 | ### 3.英文情况下使用元音字母进行截断,如果没有找到元音字母则使用默认规则截断;
16 |
17 | ### 4.依赖Library
18 | 在主项目app的build.gradle中依赖
19 | ```
20 | dependencies {
21 | ...
22 | implementation 'com.text:alginlib:1.0.1'
23 | }
24 | ```
25 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 28
5 | buildToolsVersion "28.0.3"
6 | defaultConfig {
7 | applicationId "com.example.testdemo1"
8 | minSdkVersion 21
9 | targetSdkVersion 28
10 | versionCode 1
11 | versionName "1.0"
12 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
13 | }
14 | buildTypes {
15 | release {
16 | minifyEnabled false
17 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
18 | }
19 | }
20 | }
21 |
22 | dependencies {
23 | implementation fileTree(dir: 'libs', include: ['*.jar'])
24 | implementation 'androidx.appcompat:appcompat:1.1.0'
25 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
26 | implementation 'com.google.android.material:material:1.0.0'
27 | testImplementation 'junit:junit:4.12'
28 | androidTestImplementation 'androidx.test:runner:1.2.0'
29 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
30 | }
31 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/example/testdemo1/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.example.testdemo1;
2 |
3 | import android.content.Context;
4 |
5 | import androidx.test.InstrumentationRegistry;
6 | import androidx.test.runner.AndroidJUnit4;
7 |
8 | import org.junit.Test;
9 | import org.junit.runner.RunWith;
10 |
11 | import static org.junit.Assert.*;
12 |
13 | /**
14 | * Instrumented test, which will execute on an Android device.
15 | *
16 | * @see Testing documentation
17 | */
18 | @RunWith(AndroidJUnit4.class)
19 | public class ExampleInstrumentedTest {
20 | @Test
21 | public void useAppContext() {
22 | // Context of the app under test.
23 | Context appContext = InstrumentationRegistry.getTargetContext();
24 |
25 | assertEquals("com.example.testdemo1", appContext.getPackageName());
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
13 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/testdemo1/AlignTextView.java:
--------------------------------------------------------------------------------
1 | package com.example.testdemo1;
2 |
3 | import android.content.Context;
4 | import android.content.res.TypedArray;
5 | import android.graphics.Canvas;
6 | import android.text.Layout;
7 | import android.text.StaticLayout;
8 | import android.util.AttributeSet;
9 |
10 | import androidx.annotation.Nullable;
11 | import androidx.appcompat.widget.AppCompatTextView;
12 |
13 | /**
14 | * 实现原理:通过StaticLayout来获取一行能展示多少的字符,
15 | * 然后计算剩余的宽度,进行绘制;
16 | *
17 | * 当绘制过程比较完美时,是由于计算的剩余宽度较小,看不出来效果;
18 | *
19 | * 缺点:当存在单词较少时,会存在间隙过大的问题,比如第一行只有两个单词时,会出现间距过大的问题
20 | *
21 | */
22 | public class AlignTextView extends AppCompatTextView {
23 |
24 | private boolean alignOnlyOneLine;
25 |
26 | public AlignTextView(Context context) {
27 | this(context, null);
28 | }
29 |
30 | public AlignTextView(Context context, @Nullable AttributeSet attrs) {
31 | this(context, attrs, 0);
32 | }
33 |
34 | public AlignTextView(Context context, AttributeSet attrs, int defStyleAttr) {
35 | super(context, attrs, defStyleAttr);
36 | init(context, attrs);
37 | }
38 |
39 | private void init(Context context, AttributeSet attrs) {
40 | TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.AlignTextView);
41 | alignOnlyOneLine = typedArray.getBoolean(R.styleable.AlignTextView_alignOnlyOneLine, false);
42 | typedArray.recycle();
43 | setTextColor(getCurrentTextColor());
44 | }
45 |
46 | @Override
47 | public void setTextColor(int color) {
48 | super.setTextColor(color);
49 | getPaint().setColor(color);
50 | }
51 |
52 | protected void onDraw(Canvas canvas) {
53 | CharSequence content = getText();
54 | if (!(content instanceof String)) {
55 | super.onDraw(canvas);
56 | return;
57 | }
58 | String text = (String) content;
59 | Layout layout = getLayout();
60 |
61 | for (int i = 0; i < layout.getLineCount(); ++i) {
62 | int lineBaseline = layout.getLineBaseline(i) + getPaddingTop();
63 | int lineStart = layout.getLineStart(i);
64 | int lineEnd = layout.getLineEnd(i);
65 | if (alignOnlyOneLine && layout.getLineCount() == 1) { // 只有一行
66 | String line = text.substring(lineStart, lineEnd);
67 | float width = StaticLayout.getDesiredWidth(text, lineStart, lineEnd, getPaint());
68 | this.drawScaledText(canvas, line, lineBaseline, width);
69 | } else if (i == layout.getLineCount() - 1) { // 最后一行
70 | canvas.drawText(text.substring(lineStart), getPaddingLeft(), lineBaseline, getPaint());
71 | break;
72 | } else { //中间行
73 | String line = text.substring(lineStart, lineEnd);
74 | float width = StaticLayout.getDesiredWidth(text, lineStart, lineEnd, getPaint());
75 | this.drawScaledText(canvas, line, lineBaseline, width);
76 | }
77 | }
78 |
79 | }
80 |
81 | private void drawScaledText(Canvas canvas, String line, float baseLineY, float lineWidth) {
82 | if (line.length() < 1) {
83 | return;
84 | }
85 | float x = getPaddingLeft();
86 | boolean forceNextLine = line.charAt(line.length() - 1) == 10;
87 | int length = line.length() - 1;
88 | if (forceNextLine || length == 0) {
89 | canvas.drawText(line, x, baseLineY, getPaint());
90 | return;
91 | }
92 |
93 | float d = (getMeasuredWidth() - lineWidth - getPaddingLeft() - getPaddingRight()) / length;
94 |
95 | for (int i = 0; i < line.length(); ++i) {
96 | String c = String.valueOf(line.charAt(i));
97 | float cw = StaticLayout.getDesiredWidth(c, this.getPaint());
98 | canvas.drawText(c, x, baseLineY, this.getPaint());
99 | x += cw + d;
100 | }
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/testdemo1/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.example.testdemo1;
2 |
3 | import android.os.Bundle;
4 |
5 | import androidx.appcompat.app.AppCompatActivity;
6 |
7 | public class MainActivity extends AppCompatActivity {
8 |
9 | private String text = "Hooray! It's snowing! It's time to make a snowman.James runs out. He makes a big pile of snow. He puts a big snowball on top. He adds a scarf and a hat. He adds an orange for the nose. He adds coal for the eyes and buttons.In the evening, James opens the door. What does he see? The snowman is moving! James invites him in. The snowman has never been inside a house. He says hello to the cat. He plays with paper towels.A moment later, the snowman takes James's hand and goes out.They go up, up, up into the air! They are flying! What a wonderful night!The next morning, James jumps out of bed. He runs to the door.He wants to thank the snowman. But he's gone.";
10 | private String text1 = "AppCompatActivity AppCompatActivityActivityActivityActivity";
11 | private String text2 = "For every layout expression, there is a binding adapter that makes the framework calls required to set the corresponding properties or listeners. For example, the binding adapter can take care of calling the setText() method to set the text property or call the setOnClickListener() method to add a listener to the click event. The most common binding adapters, such as the adapters for the android:text property used in the examples in this page, are available for you to use in the android.databinding.adapters package. For a list of the common binding adapters, see adapters. You can also create custom adapters, as shown in the following example:";
12 |
13 | @Override
14 | protected void onCreate(Bundle savedInstanceState) {
15 | super.onCreate(savedInstanceState);
16 | setContentView(R.layout.activity_main);
17 | XQJustifyTextView view = findViewById(R.id.just);
18 | view.setText(text2);
19 | // view.setText(text1);
20 |
21 | AlignTextView view1 = findViewById(R.id.just1);
22 | // view1.setText(text);
23 | // view1.setText(text1);
24 | }
25 |
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/testdemo1/RApplication.java:
--------------------------------------------------------------------------------
1 | package com.example.testdemo1;
2 |
3 | import android.app.Application;
4 |
5 | public class RApplication extends Application {
6 |
7 | @Override
8 | public void onCreate() {
9 | super.onCreate();
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/testdemo1/XQJustifyTextView.java:
--------------------------------------------------------------------------------
1 | package com.example.testdemo1;
2 |
3 | import android.content.Context;
4 | import android.graphics.Canvas;
5 | import android.text.Layout;
6 | import android.text.Spanned;
7 | import android.text.StaticLayout;
8 | import android.text.TextPaint;
9 | import android.text.TextUtils;
10 | import android.util.AttributeSet;
11 | import android.util.Log;
12 | import android.view.Gravity;
13 | import android.widget.TextView;
14 |
15 |
16 | import androidx.annotation.Nullable;
17 |
18 | import java.io.UnsupportedEncodingException;
19 | import java.util.ArrayList;
20 | import java.util.Arrays;
21 | import java.util.List;
22 | import java.util.Objects;
23 |
24 | /**
25 | * Description: 自定义分散对齐的TextView(中英文排版效果)
26 | * 非中文单词换行截断
27 | *
28 | * @author panxuqin
29 | */
30 | public class XQJustifyTextView extends TextView {
31 |
32 | private static final String TAG = XQJustifyTextView.class.getSimpleName();
33 |
34 | /**
35 | * 起始位置
36 | */
37 | private static final int GRAVITY_START = 1001;
38 |
39 | /**
40 | * 结尾位置
41 | */
42 | private static final int GRAVITY_END = 1002;
43 |
44 | /**
45 | * 中间位置
46 | */
47 | private static final int GRAVITY_CENTER = 1003;
48 |
49 | /**
50 | * 绘制文字的起始Y坐标
51 | */
52 | private float mLineY;
53 |
54 | /**
55 | * 文字的宽度
56 | */
57 | private int mViewWidth;
58 |
59 | /**
60 | * 段落间距
61 | */
62 | private int paragraphSpacing = dipToPx(getContext(), 15);
63 |
64 | /**
65 | * 行间距
66 | */
67 | private int lineSpacing = dipToPx(getContext(), 2);
68 |
69 | /**
70 | * 当前所有行数的集合
71 | */
72 | private ArrayList>> mParagraphLineList;
73 |
74 | /**
75 | * 当前所有的行数
76 | */
77 | private int mLineCount;
78 |
79 | /**
80 | * 每一段单词的内容集合
81 | */
82 | private ArrayList> mParagraphWordList;
83 |
84 | /**
85 | * 空格字符
86 | */
87 | private static final String BLANK = " ";
88 |
89 | /**
90 | * 英语单词元音字母
91 | */
92 | private String[] vowel = {"a", "e", "i", "o", "u"};
93 |
94 | /**
95 | * 英语单词元音字母集合
96 | */
97 | private List vowels = Arrays.asList(vowel);
98 |
99 | /**
100 | * 当前测量的间距
101 | */
102 | private int measuredWidth;
103 |
104 | /**
105 | * 左padding
106 | */
107 | private int paddingStart;
108 |
109 | /**
110 | * 右padding
111 | */
112 | private int paddingEnd;
113 |
114 | /**
115 | * 顶padding
116 | */
117 | private int paddingTop;
118 |
119 | /**
120 | * 底padding
121 | */
122 | private int paddingBottom;
123 |
124 | /**
125 | * 布局的方向
126 | */
127 | private int textGravity;
128 |
129 | public XQJustifyTextView(Context context) {
130 | this(context, null);
131 | }
132 |
133 | public XQJustifyTextView(Context context, AttributeSet attrs) {
134 | this(context, attrs, 0);
135 | }
136 |
137 | public XQJustifyTextView(Context context, AttributeSet attrs, int defStyleAttr) {
138 | this(context, attrs, defStyleAttr, 0);
139 | }
140 |
141 | public XQJustifyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
142 | super(context, attrs, defStyleAttr, defStyleRes);
143 | }
144 |
145 | @Override
146 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
147 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
148 | if (isSpan()) {
149 | return;
150 | }
151 | mParagraphLineList = null;
152 | mParagraphWordList = null;
153 | mLineY = 0;
154 | measuredWidth = getMeasuredWidth();
155 | paddingStart = getPaddingStart();
156 | paddingEnd = getPaddingEnd();
157 | paddingTop = getPaddingTop();
158 | paddingBottom = getPaddingBottom();
159 | mViewWidth = measuredWidth - paddingStart - paddingEnd;
160 | getParagraphList();
161 | for (List frontList : mParagraphWordList) {
162 | mParagraphLineList.add(getLineList(frontList));
163 | }
164 | setMeasuredDimension(measuredWidth,
165 | (mParagraphLineList.size() - 1) * paragraphSpacing
166 | + mLineCount * (getLineHeight() + lineSpacing) + paddingTop + paddingBottom);
167 | }
168 |
169 | @Override
170 | protected void onDraw(Canvas canvas) {
171 | if (isSpan()) {
172 | super.onDraw(canvas);
173 | return;
174 | }
175 | TextPaint paint = getPaint();
176 | paint.setColor(getCurrentTextColor());
177 | paint.drawableState = getDrawableState();
178 | mLineY = 0;
179 | float textSize = getTextSize();
180 | mLineY += textSize + paddingTop;
181 | Layout layout = getLayout();
182 | if (layout == null) {
183 | return;
184 | }
185 | textGravity = getTextGravity();
186 | adjust(canvas, paint);
187 | }
188 |
189 | /**
190 | * @param frontList
191 | * @return 计算每一段绘制的内容
192 | */
193 | private synchronized List> getLineList(List frontList) {
194 | Log.i(TAG, "getLineList ");
195 | StringBuilder sb = new StringBuilder();
196 | List> lineLists = new ArrayList<>();
197 | List lineList = new ArrayList<>();
198 | float width = 0;
199 | String temp = "";
200 | String front = "";
201 | for (int i = 0; i < frontList.size(); i++) {
202 |
203 | front = frontList.get(i);
204 |
205 | if (!TextUtils.isEmpty(temp)) {
206 | sb.append(temp);
207 | lineList.add(temp);
208 | if (!isCN(temp)) {
209 | sb.append(BLANK);
210 | }
211 | temp = "";
212 | }
213 |
214 | if (isCN(front)) {
215 | sb.append(front);
216 | } else {
217 | if ((i + 1) < frontList.size()) {
218 | String nextFront = frontList.get(i + 1);
219 | if (isCN(nextFront)) {
220 | sb.append(front);
221 | } else {
222 | sb.append(front).append(BLANK);
223 | }
224 | } else {
225 | sb.append(front);
226 | }
227 |
228 | }
229 |
230 | lineList.add(front);
231 | width = StaticLayout.getDesiredWidth(sb.toString(), getPaint());
232 |
233 | if (width > mViewWidth) {
234 |
235 | // 先判断最后一个单词是否是英文的,是的话则切割,否则的话就移除最后一个
236 | int lastIndex = lineList.size() - 1;
237 | String lastWord = lineList.get(lastIndex);
238 |
239 | String lastTemp = "";
240 | lineList.remove(lastIndex);
241 | if (isCN(lastWord)) {
242 |
243 | addLines(lineLists, lineList);
244 | lastTemp = lastWord;
245 | } else {
246 |
247 | // 否则的话则截取字符串
248 | String substring = sb.substring(0, sb.length() - lastWord.length() - 1);
249 | sb.delete(0, sb.toString().length());
250 | sb.append(substring).append(BLANK);
251 | String tempLastWord = "";
252 |
253 | int length = lastWord.length();
254 |
255 | if (length <= 3) {
256 | addLines(lineLists, lineList);
257 | lastTemp = lastWord;
258 | } else {
259 | int cutoffIndex = 0;
260 | for (int j = 0; j < length; j++) {
261 |
262 | tempLastWord = String.valueOf(lastWord.charAt(j));
263 | sb.append(tempLastWord);
264 | if (vowels.contains(tempLastWord)) {
265 | // 根据元音字母来进行截断
266 | if (j + 1 < length) {
267 | String nextTempLastWord = String.valueOf(lastWord.charAt(j + 1));
268 | sb.append(nextTempLastWord);
269 | width = StaticLayout.getDesiredWidth(sb.toString(), getPaint());
270 | cutoffIndex = j;
271 | if (width > mViewWidth) {
272 | if (j > 2 && j <= length - 2) {
273 | // 单词截断后,前面的字符小于2个时,则不进行截断
274 | String lastFinalWord = lastWord.substring(0, cutoffIndex + 2) + "-";
275 | lineList.add(lastFinalWord);
276 | addLines(lineLists, lineList);
277 | lastTemp = lastWord.substring(cutoffIndex + 2, length);
278 |
279 | } else {
280 | addLines(lineLists, lineList);
281 | lastTemp = lastWord;
282 | }
283 | break;
284 | }
285 | } else {
286 | addLines(lineLists, lineList);
287 | lastTemp = lastWord;
288 | break;
289 | }
290 | }
291 |
292 | width = StaticLayout.getDesiredWidth(sb.toString(), getPaint());
293 |
294 | // 找不到元音,则走默认的逻辑
295 | if (width > mViewWidth) {
296 | if (j > 2 && j <= length - 2) {
297 | // 单词截断后,前面的字符小于2个时,则不进行截断
298 | String lastFinalWord = lastWord.substring(0, j) + "-";
299 | lineList.add(lastFinalWord);
300 | addLines(lineLists, lineList);
301 | lastTemp = lastWord.substring(j, length);
302 |
303 | } else {
304 | addLines(lineLists, lineList);
305 | lastTemp = lastWord;
306 | }
307 | break;
308 | }
309 | }
310 | }
311 | }
312 |
313 | sb.delete(0, sb.toString().length());
314 | temp = lastTemp;
315 |
316 | }
317 |
318 | if (lineList.size() > 0 && i == frontList.size() - 1) {
319 | addLines(lineLists, lineList);
320 | }
321 | }
322 |
323 | if (!TextUtils.isEmpty(temp)) {
324 | lineList.add(temp);
325 | addLines(lineLists, lineList);
326 | }
327 |
328 | mLineCount += lineLists.size();
329 | return lineLists;
330 | }
331 |
332 | /**
333 | * 添加一行到单词内容
334 | *
335 | * @param lineLists 总单词集合
336 | * @param lineList 当前要添加的集合
337 | */
338 | private void addLines(List> lineLists, List lineList) {
339 | if (lineLists == null || lineList == null) {
340 | return;
341 | }
342 |
343 | List tempLines = new ArrayList<>(lineList);
344 | lineLists.add(tempLines);
345 | lineList.clear();
346 | }
347 |
348 | /**
349 | * 获取段落
350 | */
351 | private void getParagraphList() {
352 | CharSequence charSequence = getText();
353 |
354 | if (TextUtils.isEmpty(charSequence)) {
355 | return;
356 | }
357 |
358 | String text = charSequence.toString().replaceAll(" ", "").replaceAll(" ", "").replaceAll("\\r", "").trim();
359 | mLineCount = 0;
360 | String[] items = text.split("\\n");
361 | mParagraphLineList = new ArrayList<>();
362 | mParagraphWordList = new ArrayList<>();
363 | for (String item : items) {
364 | if (item.length() != 0) {
365 | mParagraphWordList.add(getWordList(item));
366 | }
367 | }
368 | }
369 |
370 | /**
371 | * 截取每一段内容的每一个单词
372 | *
373 | * @param text
374 | * @return
375 | */
376 | private synchronized List getWordList(String text) {
377 | if (TextUtils.isEmpty(text)) {
378 | return new ArrayList<>();
379 | }
380 | Log.i(TAG, "getWordList ");
381 | List frontList = new ArrayList<>();
382 | StringBuilder str = new StringBuilder();
383 | for (int i = 0; i < text.length(); i++) {
384 | String charAt = String.valueOf(text.charAt(i));
385 | if (!Objects.equals(charAt, BLANK)) {
386 | if (checkIsSymbol(charAt)) {
387 | int len = str.length();
388 | boolean isEmptyStr = str.length() == 0;
389 | str.append(charAt);
390 | // 处理连续的标点符号逻辑
391 | boolean isPreSymbol = false;
392 | if (len < str.length()) {
393 | String pre = String.valueOf(str.charAt(len));
394 | isPreSymbol = checkIsSymbol(pre);
395 | }
396 | if (!isEmptyStr && !isPreSymbol) {
397 | // 中英文都需要将字符串添加到这里;
398 | frontList.add(str.toString());
399 | str.delete(0, str.length());
400 | }
401 | } else {
402 | if (isCN(str.toString())){
403 | frontList.add(str.toString());
404 | str.delete(0, str.length());
405 | }
406 | str.append(charAt);
407 | }
408 | } else {
409 | if (!TextUtils.isEmpty(str.toString())) {
410 | frontList.add(str.toString().replaceAll(BLANK, ""));
411 | str.delete(0, str.length());
412 | }
413 | }
414 | }
415 |
416 | if (str.length() != 0) {
417 | frontList.add(str.toString());
418 | str.delete(0, str.length());
419 | }
420 |
421 | return frontList;
422 | }
423 |
424 |
425 | /**
426 | * 中英文排版效果
427 | *
428 | * @param canvas
429 | */
430 | private synchronized void adjust(Canvas canvas, TextPaint paint) {
431 |
432 | int size = mParagraphWordList.size();
433 |
434 | for (int j = 0; j < size; j++) { // 遍历每一段
435 | List> lineList = mParagraphLineList.get(j);
436 | for (int i = 0; i < lineList.size(); i++) { // 遍历每一段的每一行
437 | List lineWords = lineList.get(i);
438 | if (i == lineList.size() - 1) {
439 | drawScaledEndText(canvas, lineWords, paint);
440 | } else {
441 | drawScaledText(canvas, lineWords, paint);
442 | }
443 | mLineY += (getLineHeight() + lineSpacing);
444 | }
445 | mLineY += paragraphSpacing;
446 | }
447 | }
448 |
449 | /**
450 | * 绘制最后一行文字
451 | *
452 | * @param canvas
453 | * @param lineWords
454 | * @param paint
455 | */
456 | private void drawScaledEndText(Canvas canvas, List lineWords, TextPaint paint) {
457 | if (canvas == null || lineWords == null || paint == null) {
458 | return;
459 | }
460 | StringBuilder sb = new StringBuilder();
461 | for (String aSplit : lineWords) {
462 | if (isCN(aSplit)) {
463 | sb.append(aSplit);
464 | } else {
465 | sb.append(aSplit).append(BLANK);
466 | }
467 | }
468 | /**
469 | * 最后一行适配布局方向
470 | * android:gravity=""
471 | * android:textAlignment=""
472 | * 默认不设置则为左边
473 | * 如果同时设置gravity和textAlignment属性,则以textAlignment的属性为准
474 | * 也就是说textAlignment的属性优先级大于gravity的属性
475 | *
476 | */
477 | if (GRAVITY_START == textGravity) {
478 | canvas.drawText(sb.toString(), paddingStart, mLineY, paint);
479 | } else if (GRAVITY_END == textGravity) {
480 | float width = StaticLayout.getDesiredWidth(sb.toString(), getPaint());
481 | canvas.drawText(sb.toString(), measuredWidth - width - paddingStart, mLineY, paint);
482 | } else {
483 | float width = StaticLayout.getDesiredWidth(sb.toString(), getPaint());
484 | canvas.drawText(sb.toString(), (mViewWidth - width) / 2, mLineY, paint);
485 | }
486 | }
487 |
488 | /**
489 | * 获取布局的方向
490 | */
491 | private int getTextGravity() {
492 |
493 | final int layoutDirection = getLayoutDirection();
494 | final int absoluteGravity = Gravity.getAbsoluteGravity(getGravity(), layoutDirection);
495 | int lastGravity = absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
496 | int textAlignment = getTextAlignment();
497 |
498 | if (TEXT_ALIGNMENT_TEXT_START == textAlignment
499 | || TEXT_ALIGNMENT_VIEW_START == textAlignment
500 | || Gravity.LEFT == lastGravity) {
501 | return GRAVITY_START;
502 | } else if (TEXT_ALIGNMENT_TEXT_END == textAlignment
503 | || TEXT_ALIGNMENT_VIEW_END == textAlignment
504 | || Gravity.RIGHT == lastGravity) {
505 | return GRAVITY_END;
506 | } else {
507 | return GRAVITY_CENTER;
508 | }
509 | }
510 |
511 | /**
512 | * 绘制左右对齐效果
513 | *
514 | * @param canvas
515 | * @param line
516 | * @param paint
517 | */
518 | private void drawScaledText(Canvas canvas, List line, TextPaint paint) {
519 | if (canvas == null || line == null || paint == null) {
520 | return;
521 | }
522 | StringBuilder sb = new StringBuilder();
523 | for (String aSplit : line) {
524 | sb.append(aSplit);
525 | }
526 |
527 | float lineWidth = StaticLayout.getDesiredWidth(sb, getPaint());
528 | float cw = 0;
529 | if (GRAVITY_START == textGravity) {
530 | cw = paddingStart;
531 | } else if (GRAVITY_END == textGravity){
532 | cw = paddingEnd;
533 | } else {
534 | cw = paddingStart;
535 | }
536 | float d = (mViewWidth - lineWidth) / (line.size() - 1);
537 | for (String aSplit : line) {
538 | canvas.drawText(aSplit, cw, mLineY, getPaint());
539 | cw += StaticLayout.getDesiredWidth(aSplit + "", paint) + d;
540 | }
541 | }
542 |
543 | /**
544 | * 功能:判断字符串是否有中文
545 | *
546 | * @param str
547 | * @return
548 | */
549 | public boolean isCN(String str) {
550 | try {
551 | byte[] bytes = str.getBytes("UTF-8");
552 | if (bytes.length == str.length()) {
553 | return false;
554 | } else {
555 | return true;
556 | }
557 | } catch (UnsupportedEncodingException e) {
558 | // TODO Auto-generated catch block
559 | e.printStackTrace();
560 | }
561 | return false;
562 | }
563 |
564 | public static int dipToPx(Context var0, float var1) {
565 | float var2 = var0.getResources().getDisplayMetrics().density;
566 | return (int) (var1 * var2 + 0.5F);
567 | }
568 |
569 | /**
570 | * 判断是否包含标点符号等内容
571 | *
572 | * @param s
573 | * @return
574 | */
575 | public boolean checkIsSymbol(String s) {
576 | boolean b = false;
577 |
578 | String tmp = s;
579 | tmp = tmp.replaceAll("\\p{P}", "");
580 | if (s.length() != tmp.length()) {
581 | b = true;
582 | }
583 |
584 | return b;
585 | }
586 |
587 | /**
588 | * 判断是否是富文本
589 | */
590 | public boolean isSpan() {
591 | CharSequence charSequence = getText();
592 |
593 | return charSequence instanceof Spanned;
594 | }
595 | }
596 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
17 |
18 |
19 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/content_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_main.xml:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/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/Amterson/AlginProject/3d8271e0c950867acc5b4f20e3c7cdf52b7ffd26/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Amterson/AlginProject/3d8271e0c950867acc5b4f20e3c7cdf52b7ffd26/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Amterson/AlginProject/3d8271e0c950867acc5b4f20e3c7cdf52b7ffd26/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Amterson/AlginProject/3d8271e0c950867acc5b4f20e3c7cdf52b7ffd26/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Amterson/AlginProject/3d8271e0c950867acc5b4f20e3c7cdf52b7ffd26/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Amterson/AlginProject/3d8271e0c950867acc5b4f20e3c7cdf52b7ffd26/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Amterson/AlginProject/3d8271e0c950867acc5b4f20e3c7cdf52b7ffd26/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Amterson/AlginProject/3d8271e0c950867acc5b4f20e3c7cdf52b7ffd26/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Amterson/AlginProject/3d8271e0c950867acc5b4f20e3c7cdf52b7ffd26/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Amterson/AlginProject/3d8271e0c950867acc5b4f20e3c7cdf52b7ffd26/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #008577
4 | #00574B
5 | #D81B60
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 | 16dp
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | TestDemo1
3 | Settings
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/test/java/com/example/testdemo1/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.example.testdemo1;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | repositories {
5 | google()
6 | jcenter()
7 |
8 | }
9 | dependencies {
10 | classpath 'com.android.tools.build:gradle:3.4.1'
11 |
12 | // NOTE: Do not place your application dependencies here; they belong
13 | // in the individual module build.gradle files
14 | }
15 | }
16 |
17 | allprojects {
18 | repositories {
19 | google()
20 | jcenter()
21 |
22 | }
23 | }
24 |
25 | task clean(type: Delete) {
26 | delete rootProject.buildDir
27 | }
28 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 |
21 |
--------------------------------------------------------------------------------