概述
在Android中做图文混排,一般都是选择用TextView+ImageSpan来实现,但是在ImageSpan中,对齐方式只有ImageSpan.ALIGN_BASELINE,ImageSpan.ALIGN_BOTTOM两种对齐方式,不能相对于文本居中,如果我们图片小于文字,或者大于文字,都会导致排版问题出现。
居中
下面是抄自wangkunlin同学都一个居中实现代码,封装的很好,就拿过来直接用了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
| package com.wkl.imagespan;
import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.support.annotation.IntDef; import android.text.style.ImageSpan;
import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference;
/** * Created by wangkunlin * On 2017-03-15 */
public class AlignImageSpan extends ImageSpan {
/** * 顶部对齐 */ public static final int ALIGN_TOP = 3; /** * 垂直居中 */ public static final int ALIGN_CENTER = 4;
@IntDef({ALIGN_BOTTOM, ALIGN_BASELINE, ALIGN_TOP, ALIGN_CENTER}) @Retention(RetentionPolicy.SOURCE) public @interface Alignment { }
public AlignImageSpan(Drawable d) { this(d, ALIGN_CENTER); }
public AlignImageSpan(Drawable d, @Alignment int verticalAlignment) { super(d, verticalAlignment); }
@Override public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) { Drawable d = getCachedDrawable(); Rect rect = d.getBounds(); if (fm != null) { Paint.FontMetrics fmPaint = paint.getFontMetrics(); // 顶部 leading float topLeading = fmPaint.top - fmPaint.ascent; // 底部 leading float bottomLeading = fmPaint.bottom - fmPaint.descent; // drawable 的高度 int drHeight = rect.height();
switch (mVerticalAlignment) { case ALIGN_CENTER: { // drawable 的中间与 行中间对齐 // 当前行 的高度 float fontHeight = fmPaint.descent - fmPaint.ascent; // 整行的 y方向上的中间 y 坐标 float center = fmPaint.descent - fontHeight / 2;
// 算出 ascent 和 descent float ascent = center - drHeight / 2; float descent = center + drHeight / 2;
fm.ascent = (int) ascent; fm.top = (int) (ascent + topLeading); fm.descent = (int) descent; fm.bottom = (int) (descent + bottomLeading); break; } case ALIGN_BASELINE: { // drawable 的底部与 baseline 对齐 // 所以 ascent 的值就是 负的 drawable 的高度 float ascent = -drHeight; fm.ascent = -drHeight; fm.top = (int) (ascent + topLeading); break; } case ALIGN_TOP: { // drawable 的顶部与 行的顶部 对齐 // 算出 descent float descent = drHeight + fmPaint.ascent; fm.descent = (int) descent; fm.bottom = (int) (descent + bottomLeading); break; } case ALIGN_BOTTOM: // drawable 的底部与 行的底部 对齐 default: { // 算出 ascent float ascent = fmPaint.descent - drHeight; fm.ascent = (int) ascent; fm.top = (int) (ascent + topLeading); } } } return rect.right; }
/** * 这里的 x, y, top 以及 bottom 都是基于整个 TextView 的坐标系的坐标 * * @param x drawable 绘制的起始 x 坐标 * @param top 当前行最高处,在 TextView 中的 y 坐标 * @param y 当前行的 BaseLine 在 TextView 中的 y 坐标 * @param bottom 当前行最低处,在 TextView 中的 y 坐标 */ @Override public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) { Drawable drawable = getDrawable(); Rect rect = drawable.getBounds(); float transY; switch (mVerticalAlignment) { case ALIGN_BASELINE: transY = y - rect.height(); break; case ALIGN_CENTER: transY = ((bottom - top) - rect.height()) / 2 + top; break; case ALIGN_TOP: transY = top; break; case ALIGN_BOTTOM: default: transY = bottom - rect.height(); } canvas.save(); // 这里如果不移动画布,drawable 就会在 Textview 的左上角出现 canvas.translate(x, transY); drawable.draw(canvas); canvas.restore(); }
private Drawable getCachedDrawable() { WeakReference<Drawable> wr = mDrawableRef; Drawable d = null;
if (wr != null) d = wr.get();
if (d == null) { d = getDrawable(); mDrawableRef = new WeakReference<>(d); }
return d; }
private WeakReference<Drawable> mDrawableRef; }
|
截断处理
图片被截断,在概述里我们说明了原因,那怎么解决呢?这里我们使用了TextUtils.ellipsize,它相当于 TextView的xml中ellipsize,是在获取TextView已setMaxLines后,显示出来的带有…结尾的内容。
所以我们就可以通过计算,获取到最终显示的文本,然后直接显示了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| //获取图片大小 int wh = mContext.getResources().getDimensionPixelOffset(R.dimen.28); textView.post(() -> { int width = textView.getWidth(); //获取textview宽度 int padding = textView.getPaddingLeft()+textView.getPaddingRight();//获取textview 内边距 int lineWidth = width - padding-wh*2;//获取实际展示的但航内容宽度 //通过TextUtils.ellipsize获取截断后的实际文本 String ellipsizeStr = TextUtils.ellipsize(signature, textView.getPaint(), lineWidth*2, TextUtils.TruncateAt.END).toString(); textView.setText(getSignatureText(". "+ellipsizeStr+" .",wh)); });
private SpannableString getSignatureText(String selfDesc,int wh){ Drawable drawableLeft = mContext.getResources().getDrawable(R.drawable.mark_left); drawableLeft.setBounds(0, 0, wh, wh); AlignImageSpan imgSpanLeft = new AlignImageSpan(drawableLeft, AlignImageSpan.ALIGN_CENTER);
Drawable drawableRight = mContext.getResources().getDrawable(R.drawable.mark_right); drawableRight.setBounds(0, 0, wh, wh); AlignImageSpan imgSpanRight = new AlignImageSpan(drawableRight, AlignImageSpan.ALIGN_CENTER);
SpannableString spannableString = new SpannableString(selfDesc); spannableString.setSpan(imgSpanLeft, 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); spannableString.setSpan(imgSpanRight, selfDesc.length()-1, selfDesc.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); return spannableString; }
|
注意
“. “+ellipsizeStr+” .”,我们在这里加入图片的地方加了点和空格,点是为了占位,空格是为了设置与文字之间的间距。为啥要用点占位,不加占位的话,首个文字会被开始的图片给截取点,导致少展示了一个开始的文字。(但是直接用系统ImageSpan,不用TextUtils.ellipsize的时候就不会,感兴趣的朋友可以验证下原因)。
自动换行后,结尾图片显示正常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| textView.post(() -> { int ellipsisCount = textView.getLayout().getEllipsisCount(textView.getLineCount() - 1); if (ellipsisCount > 0) { int endIndex = getSubLength(text,textView,ellipsisCount); text = text.substring(0, endIndex) + "... img"; } });
/** * 计算字符截取的point length,可以处理混排换行或数字,字母结尾图片展示不全的情况 * @param text * @param textView * @param ellipsisCount * @return */ private int getSubLength(String text, TextView textView, int ellipsisCount) { int endIndex = text.length() - ellipsisCount; //默认减去一个字符代替图片位置 if (textView == null || textView.getPaint() == null) { return endIndex - 1; } //精确计算需要展示的字符length String subText = text.substring(0, endIndex); char[] chars = subText.toCharArray(); float textWidth = 0; Paint paint = textView.getPaint(); float point = paint.measureText("..."); for (int i = chars.length - 1; ; i--) { textWidth = textWidth + paint.measureText(String.valueOf(chars[i])); endIndex = i; if (textWidth >= mImgSize + point) { break; } } return endIndex; }
|
Html.fromHtml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| String texts = "<img src=\"startimg\"/> " + text +"... <img src=\"endimg\"/>"; Spanned spanned = Html.fromHtml(texts, new Html.ImageGetter() { @Override public Drawable getDrawable(String source) { if("startimg".equals(source)){ //根据id从资源文件中获取图片对象 Drawable d = mContext.getResources().getDrawable(R.drawable.friend_list_card_selfdesc_mark_left); d.setBounds(0, 0, wh,wh); return d; }else{ Drawable d = mContext.getResources().getDrawable(R.drawable.friend_list_card_selfdesc_mark_right); d.setBounds(0, 0, wh,wh); return d; } } },null); textView.setText(spanned);
|
html也不支持混排。
自动换行
通过计算每个字符的宽度,来自动添加\n换行,且xml中不需要设置ellipsize。暂支持只两行,其他情况原理相同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| textView.post(() -> { String endText = getAutoSplitText(textView,signature,2); textView.setText(getSignatureText(endText,wh)); }); private String getAutoSplitText(TextView textView, String signature, int maxLineCount) { int imgSize = mContext.getResources().getDimensionPixelOffset(R.dimen.px28); String rawText = signature.replaceAll("\r", ""); final Paint tvPaint = textView.getPaint(); float space = tvPaint.measureText(" "); float dot = tvPaint.measureText("..."); float tvWidth = textView.getWidth() - textView.getPaddingLeft() - textView.getPaddingRight(); //控件可用宽度 //如果整行宽度超过控件可用宽度,则按字符测量,在超过可用宽度的前一个字符处手动换行 StringBuilder startText = new StringBuilder(); float lineWidth = imgSize + space; int lineCount = 0; for (int count = 0; count < rawText.length(); ++count) { char ch = rawText.charAt(count); lineWidth += tvPaint.measureText(String.valueOf(ch)); if (lineWidth <= tvWidth) { startText.append(ch); } else { startText.append("\n"); lineCount++; if (lineCount == maxLineCount) { break; } tvWidth = tvWidth - imgSize - space - dot - tvPaint.measureText(String.valueOf(ch)); lineWidth = 0; --count; } } //把结尾多余的\n去掉 if (startText.toString().endsWith("\n")) { startText.deleteCharAt(startText.length() - 1); } StringBuilder endText = new StringBuilder(); if(lineCount < 2){ endText.append("img ").append(startText).append(" img"); }else{ endText.append("img ").append(startText).append("... img"); } return endText.toString(); }
|
最终效果
参考资料
TextView 图文混排,解决图片被截断
TextView图文混排图片被截断的问题以及Android省略号只有一个点的问题
SpannableString的用法详解
Ursprünglicher Link: http://nunu03.github.io/2019/09/26/TextView图文混排居中、图片被截断问题/
Copyright-Erklärung: 转载请注明出处.