├── .classpath ├── .project ├── .settings └── org.eclipse.jdt.core.prefs ├── AndroidManifest.xml ├── README.md ├── ic_launcher-web.png ├── image └── finalPic.jpg ├── libs └── android-support-v4.jar ├── proguard-project.txt ├── project.properties ├── res ├── anim │ ├── slide_left_in.xml │ └── slide_right_out.xml ├── drawable-hdpi │ └── ic_launcher.png ├── drawable-mdpi │ └── ic_launcher.png ├── drawable-xhdpi │ ├── emoji_29.png │ ├── ic_launcher.png │ └── icon.png ├── drawable-xxhdpi │ └── ic_launcher.png ├── layout │ └── activity_main.xml └── values │ ├── strings.xml │ └── styles.xml └── src └── com └── example └── mtextview ├── MTextView.java └── MainActivity.java /.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | Android_Character_Line_Feed_TextView 4 | 5 | 6 | 7 | 8 | 9 | com.android.ide.eclipse.adt.ResourceManagerBuilder 10 | 11 | 12 | 13 | 14 | com.android.ide.eclipse.adt.PreCompilerBuilder 15 | 16 | 17 | 18 | 19 | org.eclipse.jdt.core.javabuilder 20 | 21 | 22 | 23 | 24 | com.android.ide.eclipse.adt.ApkBuilder 25 | 26 | 27 | 28 | 29 | 30 | com.android.ide.eclipse.adt.AndroidNature 31 | org.eclipse.jdt.core.javanature 32 | 33 | 34 | -------------------------------------------------------------------------------- /.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 3 | org.eclipse.jdt.core.compiler.compliance=1.6 4 | org.eclipse.jdt.core.compiler.source=1.6 5 | -------------------------------------------------------------------------------- /AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 10 | 11 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Character_Line_Feed_TextView 2 | 自定义TextView,一行末尾若是英文单词,不会自动换行。 3 | 最终效果图: 4 | ![Alt text](https://github.com/xuningjack/Character_Line_Feed_TextView/raw/master/image/finalPic.jpg) 5 | -------------------------------------------------------------------------------- /ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuningjack/Character_Line_Feed_TextView/bded3386cad5fb149575a350b61a582473049d5d/ic_launcher-web.png -------------------------------------------------------------------------------- /image/finalPic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuningjack/Character_Line_Feed_TextView/bded3386cad5fb149575a350b61a582473049d5d/image/finalPic.jpg -------------------------------------------------------------------------------- /libs/android-support-v4.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuningjack/Character_Line_Feed_TextView/bded3386cad5fb149575a350b61a582473049d5d/libs/android-support-v4.jar -------------------------------------------------------------------------------- /proguard-project.txt: -------------------------------------------------------------------------------- 1 | # To enable ProGuard in your project, edit project.properties 2 | # to define the proguard.config property as described in that file. 3 | # 4 | # Add project specific ProGuard rules here. 5 | # By default, the flags in this file are appended to flags specified 6 | # in ${sdk.dir}/tools/proguard/proguard-android.txt 7 | # You can edit the include path and order by changing the ProGuard 8 | # include property in project.properties. 9 | # 10 | # For more details, see 11 | # http://developer.android.com/guide/developing/tools/proguard.html 12 | 13 | # Add any project specific keep options here: 14 | 15 | # If your project uses WebView with JS, uncomment the following 16 | # and specify the fully qualified class name to the JavaScript interface 17 | # class: 18 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 19 | # public *; 20 | #} 21 | -------------------------------------------------------------------------------- /project.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system edit 7 | # "ant.properties", and override values to adapt the script to your 8 | # project structure. 9 | # 10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): 11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt 12 | 13 | # Project target. 14 | target=android-15 15 | -------------------------------------------------------------------------------- /res/anim/slide_left_in.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | -------------------------------------------------------------------------------- /res/anim/slide_right_out.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | -------------------------------------------------------------------------------- /res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuningjack/Character_Line_Feed_TextView/bded3386cad5fb149575a350b61a582473049d5d/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuningjack/Character_Line_Feed_TextView/bded3386cad5fb149575a350b61a582473049d5d/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /res/drawable-xhdpi/emoji_29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuningjack/Character_Line_Feed_TextView/bded3386cad5fb149575a350b61a582473049d5d/res/drawable-xhdpi/emoji_29.png -------------------------------------------------------------------------------- /res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuningjack/Character_Line_Feed_TextView/bded3386cad5fb149575a350b61a582473049d5d/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /res/drawable-xhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuningjack/Character_Line_Feed_TextView/bded3386cad5fb149575a350b61a582473049d5d/res/drawable-xhdpi/icon.png -------------------------------------------------------------------------------- /res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuningjack/Character_Line_Feed_TextView/bded3386cad5fb149575a350b61a582473049d5d/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 11 | 12 | 16 | 17 | 21 | 22 | 23 | 27 | 28 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Android_Character_Line_Feed_TextView 5 | Hello world! 6 | 7 | 8 | -------------------------------------------------------------------------------- /res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 14 | 15 | 16 | 19 | 20 | -------------------------------------------------------------------------------- /src/com/example/mtextview/MTextView.java: -------------------------------------------------------------------------------- 1 | package com.example.mtextview; 2 | 3 | import java.lang.ref.SoftReference; 4 | import java.util.ArrayList; 5 | import java.util.Arrays; 6 | import java.util.Comparator; 7 | import java.util.HashMap; 8 | 9 | import android.app.Activity; 10 | import android.content.Context; 11 | import android.graphics.Canvas; 12 | import android.graphics.Color; 13 | import android.graphics.Paint; 14 | import android.graphics.Paint.FontMetrics; 15 | import android.graphics.Paint.Style; 16 | import android.graphics.Rect; 17 | import android.text.Spannable; 18 | import android.text.TextPaint; 19 | import android.text.style.BackgroundColorSpan; 20 | import android.text.style.CharacterStyle; 21 | import android.text.style.DynamicDrawableSpan; 22 | import android.util.AttributeSet; 23 | import android.util.DisplayMetrics; 24 | import android.widget.TextView; 25 | 26 | /** 27 | * 图文混排TextView,请使用{@link #setMText(CharSequence)} 28 | */ 29 | public class MTextView extends TextView { 30 | /** 31 | * 缓存测量过的数据 32 | */ 33 | private static HashMap> measuredData = 34 | new HashMap>(); 35 | private static int hashIndex = 0; 36 | /** 37 | * 存储当前文本内容,每个item为一行 38 | */ 39 | ArrayList contentList = new ArrayList(); 40 | private Context context; 41 | /** 42 | * 用于测量字符宽度 43 | */ 44 | private TextPaint paint = new TextPaint(); 45 | /** 46 | * 用于测量span高度 47 | */ 48 | private Paint.FontMetricsInt mSpanFmInt = new Paint.FontMetricsInt(); 49 | /** 50 | * 临时使用,以免在onDraw中反复生产新对象 51 | */ 52 | private FontMetrics mFontMetrics = new FontMetrics(); 53 | 54 | // private float lineSpacingMult = 0.5f; 55 | private int textColor = Color.BLACK; 56 | // 行距 57 | private float lineSpacing; 58 | private int lineSpacingDP = 5; 59 | /** 60 | * 段间距,-1为默认 61 | */ 62 | private int paragraphSpacing = -1; 63 | /** 64 | * 最大宽度 65 | */ 66 | private int maxWidth; 67 | /** 68 | * 只有一行时的宽度 69 | */ 70 | private int oneLineWidth = -1; 71 | /** 72 | * 已绘的行中最宽的一行的宽度 73 | */ 74 | private float lineWidthMax = -1; 75 | /** 76 | * 存储当前文本内容,每个item为一个字符或者一个SpanObject 77 | */ 78 | private ArrayList obList = new ArrayList(); 79 | /** 80 | * 是否使用默认{@link #onMeasure(int, int)}和 81 | * {@link #onDraw(android.graphics.Canvas)} 82 | */ 83 | private boolean useDefault = false; 84 | protected CharSequence text = ""; 85 | 86 | private int minHeight; 87 | /** 88 | * 用以获取屏幕高宽 89 | */ 90 | private DisplayMetrics displayMetrics; 91 | /** 92 | * {@link android.text.style.BackgroundColorSpan}用 93 | */ 94 | private Paint textBgColorPaint = new Paint(); 95 | /** 96 | * {@link android.text.style.BackgroundColorSpan}用 97 | */ 98 | private Rect textBgColorRect = new Rect(); 99 | 100 | public MTextView(Context context) { 101 | super(context); 102 | init(context); 103 | } 104 | 105 | public MTextView(Context context, AttributeSet attrs) { 106 | super(context, attrs); 107 | init(context); 108 | } 109 | 110 | public MTextView(Context context, AttributeSet attrs, int defStyle) { 111 | super(context, attrs, defStyle); 112 | init(context); 113 | } 114 | 115 | public void init(Context context) { 116 | this.context = context; 117 | paint.setAntiAlias(true); 118 | lineSpacing = dip2px(context, lineSpacingDP); 119 | minHeight = dip2px(context, 30); 120 | 121 | displayMetrics = new DisplayMetrics(); 122 | } 123 | 124 | public static int px2sp(Context context, float pxValue) { 125 | final float fontScale = context.getResources().getDisplayMetrics().scaledDensity; 126 | return (int) (pxValue / fontScale + 0.5f); 127 | } 128 | 129 | /** 130 | * 根据手机的分辨率从 dp 的单位 转成为 px(像素) 131 | */ 132 | public static int dip2px(Context context, float dpValue) { 133 | final float scale = context.getResources().getDisplayMetrics().density; 134 | return (int) (dpValue * scale + 0.5f); 135 | } 136 | 137 | @Override 138 | public void setMaxWidth(int maxpixels) { 139 | super.setMaxWidth(maxpixels); 140 | maxWidth = maxpixels; 141 | } 142 | 143 | @Override 144 | public void setMinHeight(int minHeight) { 145 | super.setMinHeight(minHeight); 146 | this.minHeight = minHeight; 147 | } 148 | 149 | @Override 150 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 151 | if (useDefault) { 152 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 153 | return; 154 | } 155 | int width = 0, height = 0; 156 | 157 | int widthMode = MeasureSpec.getMode(widthMeasureSpec); 158 | int heightMode = MeasureSpec.getMode(heightMeasureSpec); 159 | int widthSize = MeasureSpec.getSize(widthMeasureSpec); 160 | int heightSize = MeasureSpec.getSize(heightMeasureSpec); 161 | 162 | switch (widthMode) { 163 | case MeasureSpec.EXACTLY: 164 | width = widthSize; 165 | break; 166 | case MeasureSpec.AT_MOST: 167 | width = widthSize; 168 | break; 169 | case MeasureSpec.UNSPECIFIED: 170 | 171 | ((Activity) context).getWindowManager().getDefaultDisplay() 172 | .getMetrics(displayMetrics); 173 | width = displayMetrics.widthPixels; 174 | break; 175 | default: 176 | break; 177 | } 178 | if (maxWidth > 0) { 179 | width = Math.min(width, maxWidth); 180 | } 181 | 182 | paint.setTextSize(this.getTextSize()); 183 | paint.setColor(textColor); 184 | int realHeight = measureContentHeight((int) width); 185 | 186 | // 如果实际行宽少于预定的宽度,减少行宽以使其内容横向居中 187 | int leftPadding = getCompoundPaddingLeft(); 188 | int rightPadding = getCompoundPaddingRight(); 189 | width = Math 190 | .min(width, (int) lineWidthMax + leftPadding + rightPadding); 191 | 192 | if (oneLineWidth > -1) { 193 | width = oneLineWidth; 194 | } 195 | switch (heightMode) { 196 | case MeasureSpec.EXACTLY: 197 | height = heightSize; 198 | break; 199 | case MeasureSpec.AT_MOST: 200 | height = realHeight; 201 | break; 202 | case MeasureSpec.UNSPECIFIED: 203 | height = realHeight; 204 | break; 205 | default: 206 | break; 207 | } 208 | 209 | height += getCompoundPaddingTop() + getCompoundPaddingBottom(); 210 | 211 | height = Math.max(height, minHeight); 212 | 213 | setMeasuredDimension(width, height); 214 | } 215 | 216 | @Override 217 | protected void onDraw(Canvas canvas) { 218 | if (useDefault) { 219 | super.onDraw(canvas); 220 | return; 221 | } 222 | if (contentList.isEmpty()) { 223 | return; 224 | } 225 | int width; 226 | 227 | Object ob; 228 | 229 | int leftPadding = getCompoundPaddingLeft(); 230 | int topPadding = getCompoundPaddingTop(); 231 | 232 | float height = 0 + topPadding + lineSpacing; 233 | // 只有一行时 234 | if (oneLineWidth != -1) { 235 | height = getMeasuredHeight() / 2 - contentList.get(0).height / 2; 236 | } 237 | 238 | for (LINE aContentList : contentList) { 239 | // 绘制一行 240 | float realDrawedWidth = leftPadding; 241 | /** 是否换新段落 */ 242 | boolean newParagraph = false; 243 | for (int j = 0; j < aContentList.line.size(); j++) { 244 | ob = aContentList.line.get(j); 245 | width = aContentList.widthList.get(j); 246 | 247 | paint.getFontMetrics(mFontMetrics); 248 | float x = realDrawedWidth; 249 | float y = height + aContentList.height 250 | - paint.getFontMetrics().descent; 251 | float top = y - aContentList.height; 252 | float bottom = y + mFontMetrics.descent; 253 | if (ob instanceof String) { 254 | canvas.drawText((String) ob, realDrawedWidth, y, paint); 255 | realDrawedWidth += width; 256 | if (((String) ob).endsWith("\n") 257 | && j == aContentList.line.size() - 1) { 258 | newParagraph = true; 259 | } 260 | } else if (ob instanceof SpanObject) { 261 | Object span = ((SpanObject) ob).span; 262 | if (span instanceof DynamicDrawableSpan) { 263 | 264 | int start = ((Spannable) text).getSpanStart(span); 265 | int end = ((Spannable) text).getSpanEnd(span); 266 | ((DynamicDrawableSpan) span).draw(canvas, text, start, 267 | end, (int) x, (int) top, (int) y, (int) bottom, 268 | paint); 269 | realDrawedWidth += width; 270 | } else if (span instanceof BackgroundColorSpan) { 271 | 272 | textBgColorPaint.setColor(((BackgroundColorSpan) span) 273 | .getBackgroundColor()); 274 | textBgColorPaint.setStyle(Style.FILL); 275 | textBgColorRect.left = (int) realDrawedWidth; 276 | int textHeight = (int) getTextSize(); 277 | textBgColorRect.top = (int) (height 278 | + aContentList.height - textHeight - mFontMetrics.descent); 279 | textBgColorRect.right = textBgColorRect.left + width; 280 | textBgColorRect.bottom = (int) (height 281 | + aContentList.height + lineSpacing - mFontMetrics.descent); 282 | canvas.drawRect(textBgColorRect, textBgColorPaint); 283 | canvas.drawText(((SpanObject) ob).source.toString(), 284 | realDrawedWidth, height + aContentList.height 285 | - mFontMetrics.descent, paint); 286 | realDrawedWidth += width; 287 | } else// 做字符串处理 288 | { 289 | canvas.drawText(((SpanObject) ob).source.toString(), 290 | realDrawedWidth, height + aContentList.height 291 | - mFontMetrics.descent, paint); 292 | realDrawedWidth += width; 293 | } 294 | } 295 | } 296 | // 如果要绘制段间距 297 | if (newParagraph) { 298 | height += aContentList.height + paragraphSpacing; 299 | } else { 300 | height += aContentList.height + lineSpacing; 301 | } 302 | } 303 | } 304 | 305 | @Override 306 | public void setTextColor(int color) { 307 | super.setTextColor(color); 308 | textColor = color; 309 | } 310 | 311 | /** 312 | * 用于带ImageSpan的文本内容所占高度测量 313 | * 314 | * @param width 315 | * 预定的宽度 316 | * @return 所需的高度 317 | */ 318 | private int measureContentHeight(int width) { 319 | int cachedHeight = getCachedData(text.toString(), width); 320 | 321 | if (cachedHeight > 0) { 322 | return cachedHeight; 323 | } 324 | 325 | // 已绘的宽度 326 | float obWidth = 0; 327 | float obHeight = 0; 328 | 329 | float textSize = this.getTextSize(); 330 | FontMetrics fontMetrics = paint.getFontMetrics(); 331 | // 行高 332 | float lineHeight = fontMetrics.bottom - fontMetrics.top; 333 | // 计算出的所需高度 334 | float height = lineSpacing; 335 | 336 | int leftPadding = getCompoundPaddingLeft(); 337 | int rightPadding = getCompoundPaddingRight(); 338 | 339 | float drawedWidth = 0; 340 | 341 | boolean splitFlag = false;// BackgroundColorSpan拆分用 342 | 343 | width = width - leftPadding - rightPadding; 344 | 345 | oneLineWidth = -1; 346 | 347 | contentList.clear(); 348 | 349 | StringBuilder sb; 350 | 351 | LINE line = new LINE(); 352 | 353 | for (int i = 0; i < obList.size(); i++) { 354 | Object ob = obList.get(i); 355 | 356 | if (ob instanceof String) { 357 | obWidth = paint.measureText((String) ob); 358 | obHeight = textSize; 359 | if ("\n".equals(ob)) { // 遇到"\n"则换行 360 | obWidth = width - drawedWidth; 361 | } 362 | } else if (ob instanceof SpanObject) { 363 | Object span = ((SpanObject) ob).span; 364 | if (span instanceof DynamicDrawableSpan) { 365 | int start = ((Spannable) text).getSpanStart(span); 366 | int end = ((Spannable) text).getSpanEnd(span); 367 | obWidth = ((DynamicDrawableSpan) span).getSize(getPaint(), 368 | text, start, end, mSpanFmInt); 369 | obHeight = Math.abs(mSpanFmInt.top) 370 | + Math.abs(mSpanFmInt.bottom); 371 | if (obHeight > lineHeight) { 372 | lineHeight = obHeight; 373 | } 374 | } else if (span instanceof BackgroundColorSpan) { 375 | String str = ((SpanObject) ob).source.toString(); 376 | obWidth = paint.measureText(str); 377 | obHeight = textSize; 378 | 379 | // 如果太长,拆分 380 | int k = str.length() - 1; 381 | while (width - drawedWidth < obWidth) { 382 | obWidth = paint.measureText(str.substring(0, k--)); 383 | } 384 | if (k < str.length() - 1) { 385 | splitFlag = true; 386 | SpanObject so1 = new SpanObject(); 387 | so1.start = ((SpanObject) ob).start; 388 | so1.end = so1.start + k; 389 | so1.source = str.substring(0, k + 1); 390 | so1.span = ((SpanObject) ob).span; 391 | 392 | SpanObject so2 = new SpanObject(); 393 | so2.start = so1.end; 394 | so2.end = ((SpanObject) ob).end; 395 | so2.source = str.substring(k + 1, str.length()); 396 | so2.span = ((SpanObject) ob).span; 397 | 398 | ob = so1; 399 | obList.set(i, so2); 400 | i--; 401 | } 402 | }// 做字符串处理 403 | else { 404 | String str = ((SpanObject) ob).source.toString(); 405 | obWidth = paint.measureText(str); 406 | obHeight = textSize; 407 | } 408 | } 409 | 410 | // 这一行满了,存入contentList,新起一行 411 | if (width - drawedWidth < obWidth || splitFlag) { 412 | splitFlag = false; 413 | contentList.add(line); 414 | 415 | if (drawedWidth > lineWidthMax) { 416 | lineWidthMax = drawedWidth; 417 | } 418 | drawedWidth = 0; 419 | // 判断是否有分段 420 | int objNum = line.line.size(); 421 | if (paragraphSpacing > 0 && objNum > 0 422 | && line.line.get(objNum - 1) instanceof String 423 | && "\n".equals(line.line.get(objNum - 1))) { 424 | height += line.height + paragraphSpacing; 425 | } else { 426 | height += line.height + lineSpacing; 427 | } 428 | 429 | lineHeight = obHeight; 430 | 431 | line = new LINE(); 432 | } 433 | 434 | drawedWidth += obWidth; 435 | 436 | if (ob instanceof String && line.line.size() > 0 437 | && (line.line.get(line.line.size() - 1) instanceof String)) { 438 | int size = line.line.size(); 439 | sb = new StringBuilder(); 440 | sb.append(line.line.get(size - 1)); 441 | sb.append(ob); 442 | ob = sb.toString(); 443 | obWidth = obWidth + line.widthList.get(size - 1); 444 | line.line.set(size - 1, ob); 445 | line.widthList.set(size - 1, (int) obWidth); 446 | line.height = (int) lineHeight; 447 | } else { 448 | line.line.add(ob); 449 | line.widthList.add((int) obWidth); 450 | line.height = (int) lineHeight; 451 | } 452 | 453 | } 454 | 455 | if (drawedWidth > lineWidthMax) { 456 | lineWidthMax = drawedWidth; 457 | } 458 | 459 | if (line != null && line.line.size() > 0) { 460 | contentList.add(line); 461 | height += lineHeight + lineSpacing; 462 | } 463 | if (contentList.size() <= 1) { 464 | oneLineWidth = (int) drawedWidth + leftPadding + rightPadding; 465 | height = lineSpacing + lineHeight + lineSpacing; 466 | } 467 | 468 | cacheData(width, (int) height); 469 | return (int) height; 470 | } 471 | 472 | /** 473 | * 获取缓存的测量数据,避免多次重复测量 474 | * 475 | * @param text 476 | * @param width 477 | * @return height 478 | */ 479 | @SuppressWarnings("unchecked") 480 | private int getCachedData(String text, int width) { 481 | SoftReference cache = measuredData.get(text); 482 | if (cache == null) { 483 | return -1; 484 | } 485 | MeasuredData md = cache.get(); 486 | if (md != null && md.textSize == this.getTextSize() 487 | && width == md.width) { 488 | lineWidthMax = md.lineWidthMax; 489 | contentList = (ArrayList) md.contentList.clone(); 490 | oneLineWidth = md.oneLineWidth; 491 | 492 | StringBuilder sb = new StringBuilder(); 493 | for (int i = 0; i < contentList.size(); i++) { 494 | LINE line = contentList.get(i); 495 | sb.append(line.toString()); 496 | } 497 | return md.measuredHeight; 498 | } else { 499 | return -1; 500 | } 501 | } 502 | 503 | /** 504 | * 缓存已测量的数据 505 | * 506 | * @param width 507 | * @param height 508 | */ 509 | @SuppressWarnings("unchecked") 510 | private void cacheData(int width, int height) { 511 | MeasuredData md = new MeasuredData(); 512 | md.contentList = (ArrayList) contentList.clone(); 513 | md.textSize = this.getTextSize(); 514 | md.lineWidthMax = lineWidthMax; 515 | md.oneLineWidth = oneLineWidth; 516 | md.measuredHeight = height; 517 | md.width = width; 518 | md.hashIndex = ++hashIndex; 519 | 520 | StringBuilder sb = new StringBuilder(); 521 | for (int i = 0; i < contentList.size(); i++) { 522 | LINE line = contentList.get(i); 523 | sb.append(line.toString()); 524 | } 525 | 526 | SoftReference cache = new SoftReference(md); 527 | measuredData.put(text.toString(), cache); 528 | } 529 | 530 | /** 531 | * 用本函数代替{@link #setText(CharSequence)} 532 | * 533 | * @param cs 534 | */ 535 | public void setMText(CharSequence cs) { 536 | text = cs; 537 | 538 | obList.clear(); 539 | 540 | ArrayList isList = new ArrayList(); 541 | useDefault = false; 542 | 543 | if (cs instanceof Spannable) { 544 | CharacterStyle[] spans = ((Spannable) cs).getSpans(0, cs.length(), 545 | CharacterStyle.class); 546 | for (int i = 0; i < spans.length; i++) { 547 | int s = ((Spannable) cs).getSpanStart(spans[i]); 548 | int e = ((Spannable) cs).getSpanEnd(spans[i]); 549 | SpanObject iS = new SpanObject(); 550 | iS.span = spans[i]; 551 | iS.start = s; 552 | iS.end = e; 553 | iS.source = cs.subSequence(s, e); 554 | isList.add(iS); 555 | } 556 | } 557 | 558 | // 对span进行排序,以免不同种类的span位置错乱 559 | SpanObject[] spanArray = new SpanObject[isList.size()]; 560 | isList.toArray(spanArray); 561 | Arrays.sort(spanArray, 0, spanArray.length, new SpanObjectComparator()); 562 | isList.clear(); 563 | for (int i = 0; i < spanArray.length; i++) { 564 | isList.add(spanArray[i]); 565 | } 566 | 567 | String str = cs.toString(); 568 | 569 | for (int i = 0, j = 0; i < cs.length();) { 570 | if (j < isList.size()) { 571 | SpanObject is = isList.get(j); 572 | if (i < is.start) { 573 | Integer cp = str.codePointAt(i); 574 | // 支持增补字符 575 | if (Character.isSupplementaryCodePoint(cp)) { 576 | i += 2; 577 | } else { 578 | i++; 579 | } 580 | 581 | obList.add(new String(Character.toChars(cp))); 582 | 583 | } else if (i >= is.start) { 584 | obList.add(is); 585 | j++; 586 | i = is.end; 587 | } 588 | } else { 589 | Integer cp = str.codePointAt(i); 590 | if (Character.isSupplementaryCodePoint(cp)) { 591 | i += 2; 592 | } else { 593 | i++; 594 | } 595 | 596 | obList.add(new String(Character.toChars(cp))); 597 | } 598 | } 599 | 600 | requestLayout(); 601 | } 602 | 603 | public void setUseDefault(boolean useDefault) { 604 | this.useDefault = useDefault; 605 | if (useDefault) { 606 | this.setText(text); 607 | this.setTextColor(textColor); 608 | } 609 | } 610 | 611 | /** 612 | * 设置行距 613 | * 614 | * @param lineSpacingDP 615 | * 行距,单位dp 616 | */ 617 | public void setLineSpacingDP(int lineSpacingDP) { 618 | this.lineSpacingDP = lineSpacingDP; 619 | lineSpacing = dip2px(context, lineSpacingDP); 620 | } 621 | 622 | public void setParagraphSpacingDP(int paragraphSpacingDP) { 623 | paragraphSpacing = dip2px(context, paragraphSpacingDP); 624 | } 625 | 626 | /** 627 | * 获取行距 628 | * 629 | * @return 行距,单位dp 630 | */ 631 | public int getLineSpacingDP() { 632 | return lineSpacingDP; 633 | } 634 | 635 | /** 636 | * @author huangwei 637 | * @功能: 存储Span对象及相关信息 638 | * @2014年5月27日 639 | * @下午5:21:37 640 | */ 641 | class SpanObject { 642 | public Object span; 643 | public int start; 644 | public int end; 645 | public CharSequence source; 646 | } 647 | 648 | /** 649 | * @author huangwei 650 | * @功能: 对SpanObject进行排序 651 | * @2014年6月4日 652 | * @下午5:21:30 653 | */ 654 | class SpanObjectComparator implements Comparator { 655 | @Override 656 | public int compare(SpanObject lhs, SpanObject rhs) { 657 | 658 | return lhs.start - rhs.start; 659 | } 660 | } 661 | 662 | /** 663 | * @author huangwei 664 | * @功能: 存储测量好的一行数据 665 | * @2014年5月27日 666 | * @下午5:22:12 667 | */ 668 | class LINE { 669 | public ArrayList line = new ArrayList(); 670 | public ArrayList widthList = new ArrayList(); 671 | public float height; 672 | 673 | @Override 674 | public String toString() { 675 | StringBuilder sb = new StringBuilder("height:" + height + " "); 676 | for (int i = 0; i < line.size(); i++) { 677 | sb.append(line.get(i) + ":" + widthList.get(i)); 678 | } 679 | return sb.toString(); 680 | } 681 | } 682 | 683 | /** 684 | * @author huangwei 685 | * @功能: 缓存的数据 686 | * @2014年5月27日 687 | * @下午5:22:25 688 | */ 689 | class MeasuredData { 690 | public int measuredHeight; 691 | public float textSize; 692 | public int width; 693 | public float lineWidthMax; 694 | public int oneLineWidth; 695 | public int hashIndex; 696 | ArrayList contentList; 697 | } 698 | } -------------------------------------------------------------------------------- /src/com/example/mtextview/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.mtextview; 2 | 3 | import com.example.android_pra.R; 4 | 5 | import android.app.Activity; 6 | import android.graphics.Color; 7 | import android.os.Bundle; 8 | import android.text.SpannableString; 9 | import android.text.style.BackgroundColorSpan; 10 | import android.text.style.ImageSpan; 11 | import android.widget.TextView; 12 | 13 | public class MainActivity extends Activity { 14 | 15 | private MTextView mTextView; 16 | private TextView textView; 17 | 18 | @Override 19 | protected void onCreate(Bundle savedInstanceState) { 20 | super.onCreate(savedInstanceState); 21 | 22 | setContentView(R.layout.activity_main); 23 | mTextView = (MTextView) this.findViewById(R.id.mtextview); 24 | textView = (TextView) this.findViewById(R.id.textview); 25 | test(); 26 | testNormal(); 27 | } 28 | 29 | private void test() { 30 | mTextView.setBackgroundColor(Color.GREEN); 31 | String source = "撒反对飞王瑞芳芳vfxdsdf司法所我日xunignjackisverygood太地方个的服务4个的服务34太过分的电饭锅电饭锅打come on baby三国杀个的服务34太过分的电饭锅电饭锅打三国杀太过分的电饭锅电饭锅打三国杀水电费歌曲筒袜上课5乳房炎啊啊。"; 32 | mTextView.setMText(source); 33 | mTextView.setTextSize(20); 34 | mTextView.setTextColor(Color.BLACK); 35 | mTextView.invalidate(); 36 | } 37 | 38 | private void testNormal() { 39 | textView.setBackgroundColor(Color.BLUE); 40 | String source = "撒反对飞王瑞芳芳vfxdsdf司法所我日xunignjackisverygood太地方个的服务4个的服务34太过分的电饭锅电饭锅打come on baby三国杀个的服务34太过分的电饭锅电饭锅打三国杀太过分的电饭锅电饭锅打三国杀水电费歌曲筒袜上课5乳房炎啊啊。"; 41 | textView.setText(source); 42 | textView.setTextSize(20); 43 | textView.setTextColor(Color.BLACK); 44 | } 45 | } 46 | --------------------------------------------------------------------------------