先看下效果图:
上面是MTextView,下面是默认的TextView。
一、原因
用最简单的全英文句子为例,如果有一个很长的单词,这一行剩余的空间显示不下了,那么规则就是不打断单词,而是把整个单词丢到下一行开始显示。这样本来没有错。一是咱们中国人都是方块字,怎么都放得下,不存在英文的这个问题。所以不习惯那个排版。二是如果TextView里面有图片,如图,不知道判断单词的代码是怎么弄得,总之它觉得最后一个啦字和后面的一串表情应该是一个整体,不能分开,就一起丢到第二行了,也就造成了这种难看的排版。要验证这个说法也很简单,自己去QQ里试一试,在每个表情之间都加一个空格,就会发现排版一下子正常了。
二、解决方法
最简单的就是表情之间加空格,如果不想这么做,就只有自己来画啦。
先给初学的朋友解释一下View绘制的流程,首先是onMeasure(int widthMeasureSpec, int heightMeasureSpec),onMeasure执行的时候,就是父View在问你,小朋友,你要占多大的地儿呀?当然,问你的时候,会给你个限制条件,就是那两参数,以widthMeasureSpec为例,这参数不能直接用,得先拆开,用int widthMode = MeasureSpec.getMode(widthMeasureSpec) 和 int widthSize = MeasureSpec.getSize(widthMeasureSpec);widthMode就三种情况:
MeasureSpec.EXACTLY:你就widthSize那么宽就行了。
MeasureSpec.AT_MOST:你最多只能widthSize那么宽。
MeasureSpec.UNSPECIFIED:未指定,你爱多宽多宽。
当然,其实这只父View给你的建议,遵不遵守你自己看着办,但是自己乱来导致显示不全就不是父View的错了。
最终你听取了建议,思量了一番,觉得自己应该有width那么宽,height那么高,最后就得用setMeasuredDimension(width, height)这个函数真正确定自己的高宽。然后onMeasure()的工作就完了。
然后就是onDraw(Canvas canvas),这个就简单了,canvas就是父View给的一块画布,爱在上面画啥都行,比如写个字drawText(String text,float x, float y, Paint paint),
text是要写的字,paint是写字的笔,值得注意的是x,y坐标是相对于你自己这一小块画布的左上角的。最左上就是0,0右下是width,height
上代码
/** * @功能 图文混排TextView,请使用{@link #setMText(CharSequence)} * @author huangwei * @2014年5月27日 * @下午5:29:27 */ public class MTextView extends TextView { private Context context; /** * 用于测量字符宽度 */ private Paint paint = new Paint(); private int textColor = Color.BLACK; //行距 private float lineSpacing; private int lineSpacingDP = 2; // private float lineSpacingMult = 0.5f; /** * 最大宽度 */ private int maxWidth; /** * 只有一行时的宽度 */ private int oneLineWidth = -1; /** * 已绘的行中最宽的一行的宽度 */ private float lineWidthMax = -1; /** * 存储当前文本内容,每个item为一个字符或者一个ImageSpan */ private ArrayList<Object> obList = new ArrayList<Object>(); /** * 是否使用默认{@link #onMeasure(int, int)}和{@link #onDraw(Canvas)} */ private boolean useDefault = false; /** * 存储当前文本内容,每个item为一行 */ ArrayList<LINE> contentList = new ArrayList<LINE>(); /** * 缓存测量过的数据 */ private static HashMap<String, SoftReference<MeasuredData>> measuredData = new HashMap<String, SoftReference<MeasuredData>>(); private static int hashIndex = 0; private CharSequence text = ""; /** * 最小高度 */ private int minHeight; /** * 用以获取屏幕高宽 */ private DisplayMetrics displayMetrics; public MTextView(Context context) { super(context); this.context = context; paint.setAntiAlias(true); lineSpacing = dip2px(context, lineSpacingDP); minHeight = dip2px(context, 30); displayMetrics = new DisplayMetrics(); } public MTextView(Context context, AttributeSet attrs) { super(context, attrs); this.context = context; paint.setAntiAlias(true); lineSpacing = dip2px(context, lineSpacingDP); minHeight = dip2px(context, 30); displayMetrics = new DisplayMetrics(); } @Override public void setMaxWidth(int maxpixels) { super.setMaxWidth(maxpixels); maxWidth = maxpixels; } @Override public void setMinHeight(int minHeight) { super.setMinHeight(minHeight); this.minHeight = minHeight; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (useDefault) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); return; } int width = 0, height = 0; int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); switch (widthMode) { case MeasureSpec.EXACTLY: width = widthSize; break; case MeasureSpec.AT_MOST: width = widthSize; break; case MeasureSpec.UNSPECIFIED: ((Activity) context).getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); width = displayMetrics.widthPixels; break; default: break; } if (maxWidth > 0) width = Math.min(width, maxWidth); paint.setTextSize(this.getTextSize()); paint.setColor(textColor); int realHeight = measureContentHeight((int) width); //如果实际行宽少于预定的宽度,减少行宽以使其内容横向居中 int leftPadding = getCompoundPaddingLeft(); int rightPadding = getCompoundPaddingRight(); width = Math.min(width, (int) lineWidthMax + leftPadding+ rightPadding); if (oneLineWidth > -1) { width = oneLineWidth; } switch (heightMode) { case MeasureSpec.EXACTLY: height = heightSize; break; case MeasureSpec.AT_MOST: height = realHeight; break; case MeasureSpec.UNSPECIFIED: height = realHeight; break; default: break; } height += getCompoundPaddingTop() + getCompoundPaddingBottom(); height = Math.max(height,minHeight); setMeasuredDimension(width, height); } @Override protected void onDraw(Canvas canvas) { if (useDefault) { super.onDraw(canvas); return; } int width; Object ob; int leftPadding = getCompoundPaddingLeft(); int topPadding = getCompoundPaddingTop(); float height = 0 + topPadding + lineSpacing; //只有一行时 if(oneLineWidth != -1) { height = getMeasuredHeight() /2 - contentList.get(0).height/2; } for (int i = 0; i < contentList.size(); i++) { //绘制一行 float realDrawedWidth = 0 + leftPadding; LINE line = contentList.get(i); for (int j = 0; j < line.line.size(); j++) { ob = line.line.get(j); width = line.widthList.get(j); if (ob instanceof String) { canvas.drawText((String) ob, realDrawedWidth, height + line.height, paint); realDrawedWidth += width; } else if (ob instanceof ImageSpan) { ImageSpan is = (ImageSpan) ob; Drawable d = is.getDrawable(); int left = (int) (realDrawedWidth); int top = (int) height; int right = (int) (realDrawedWidth + width); int bottom = (int) (height + line.height); d.setBounds(left, top, right, bottom); d.draw(canvas); realDrawedWidth += width; } } height += line.height + lineSpacing; } } @Override public void setTextColor(int color) { super.setTextColor(color); textColor = color; } /** * 用于带ImageSpan的文本内容所占高度测量 * @param width 预定的宽度 * @return 所需的高度 */ private int measureContentHeight(int width) { int cachedHeight = getCachedData(text.toString(), width); if(cachedHeight > 0) { return cachedHeight; } // 已绘的宽度 float obWidth = 0; float obHeight = 0; float textSize = this.getTextSize(); //行高 float lineHeight = textSize; //计算出的所需高度 float height = lineSpacing; int leftPadding = getCompoundPaddingLeft(); int rightPadding = getCompoundPaddingRight(); float drawedWidth = 0; width = width - leftPadding - rightPadding; oneLineWidth = -1; contentList.clear(); StringBuilder sb; LINE line = new LINE(); for (int i = 0; i < obList.size(); i++) { Object ob = obList.get(i); if (ob instanceof String) { obWidth = paint.measureText((String) ob); obHeight = textSize; } else if (ob instanceof ImageSpan) { Rect r = ((ImageSpan) ob).getDrawable().getBounds(); obWidth = r.right - r.left; obHeight = r.bottom - r.top; if (obHeight > lineHeight) lineHeight = obHeight; } //这一行满了,存入contentList,新起一行 if (width - drawedWidth < obWidth) { contentList.add(line); if (drawedWidth > lineWidthMax) { lineWidthMax = drawedWidth; } drawedWidth = 0; height += line.height + lineSpacing; lineHeight = obHeight; line = new LINE(); } drawedWidth += obWidth; if (ob instanceof String && line.line.size() > 0 && (line.line.get(line.line.size() - 1) instanceof String)) { int size = line.line.size(); sb = new StringBuilder(); sb.append(line.line.get(size - 1)); sb.append(ob); ob = sb.toString(); obWidth = obWidth + line.widthList.get(size - 1); line.line.set(size - 1, ob); line.widthList.set(size - 1, (int) obWidth); line.height = (int) lineHeight; } else { line.line.add(ob); line.widthList.add((int) obWidth); line.height = (int) lineHeight; } } if (line != null && line.line.size() > 0) { contentList.add(line); height += lineHeight + lineSpacing; } if (contentList.size() <= 1) { oneLineWidth = (int) drawedWidth + leftPadding + rightPadding; height = lineSpacing + lineHeight + lineSpacing; } cacheData(width,(int) height); return (int) height; } /** * 获取缓存的测量数据,避免多次重复测量 * @param text * @param width * @return height */ @SuppressWarnings("unchecked") private int getCachedData(String text, int width) { SoftReference<MeasuredData> cache = measuredData.get(text); if(cache == null) return -1; MeasuredData md = cache.get(); if (md != null && md.textSize == this.getTextSize() && width == md.width) { lineWidthMax = md.lineWidthMax; contentList = (ArrayList<LINE>) md.contentList.clone(); oneLineWidth = md.oneLineWidth; StringBuilder sb = new StringBuilder(); for(int i=0;i<contentList.size();i++) { LINE line = contentList.get(i); sb.append(line.toString()); } return md.measuredHeight; } else return -1; } /** * 缓存已测量的数据 * @param width * @param height */ @SuppressWarnings("unchecked") private void cacheData(int width, int height) { MeasuredData md = new MeasuredData(); md.contentList = (ArrayList<LINE>) contentList.clone(); md.textSize = this.getTextSize(); md.lineWidthMax = lineWidthMax; md.oneLineWidth = oneLineWidth; md.measuredHeight = height; md.width = width; md.hashIndex = ++hashIndex; StringBuilder sb = new StringBuilder(); for(int i=0;i<contentList.size();i++) { LINE line = contentList.get(i); sb.append(line.toString()); } SoftReference<MeasuredData> cache = new SoftReference<MeasuredData>(md); measuredData.put(text.toString(),cache); } /** * 用本函数代替{@link #setText(CharSequence)} * @param cs */ public void setMText(CharSequence cs) { text = cs; obList.clear(); // contentList.clear(); ArrayList<IS> isList = new ArrayList<MTextView.IS>(); useDefault = false; if (cs instanceof SpannableString) { SpannableString ss = (SpannableString) cs; ImageSpan[] imageSpans = ss.getSpans(0, ss.length(), ImageSpan.class); for (int i = 0; i < imageSpans.length; i++) { int s = ss.getSpanStart(imageSpans[i]); int e = ss.getSpanEnd(imageSpans[i]); IS iS = new IS(); iS.is = imageSpans[i]; iS.start = s; iS.end = e; isList.add(iS); } } String str = cs.toString(); for (int i = 0, j = 0; i < cs.length();) { if (j < isList.size()) { IS is = isList.get(j); if (i < is.start) { Integer cp = str.codePointAt(i); //支持增补字符 if (Character.isSupplementaryCodePoint(cp)) { i += 2; } else { i++; } obList.add(new String(Character.toChars(cp))); } else if (i >= is.start) { obList.add(is.is); j++; i = is.end; } } else { Integer cp = str.codePointAt(i); if (Character.isSupplementaryCodePoint(cp)) { i += 2; } else { i++; } obList.add(new String(Character.toChars(cp))); } } requestLayout(); } public void setUseDefault(boolean useDefault) { this.useDefault = useDefault; if (useDefault) { this.setText(text); this.setTextColor(textColor); } } public static int px2sp(Context context, float pxValue) { final float fontScale = context.getResources().getDisplayMetrics().scaledDensity; return (int) (pxValue / fontScale + 0.5f); } /** * 根据手机的分辨率从 dp 的单位 转成为 px(像素) */ public static int dip2px(Context context, float dpValue) { final float scale = context.getResources().getDisplayMetrics().density; return (int) (dpValue * scale + 0.5f); } /** * @功能: 存储ImageSpan及其开始结束位置 * @author huangwei * @2014年5月27日 * @下午5:21:37 */ class IS { public ImageSpan is; public int start; public int end; } /** * @功能: 存储测量好的一行数据 * @author huangwei * @2014年5月27日 * @下午5:22:12 */ class LINE { public ArrayList<Object> line = new ArrayList<Object>(); public ArrayList<Integer> widthList = new ArrayList<Integer>(); public int height; @Override public String toString() { StringBuilder sb = new StringBuilder("height:"+height+" "); for(int i=0;i<line.size();i++) { sb.append(line.get(i)+":"+widthList.get(i)); } return sb.toString(); } } /** * @功能: 缓存的数据 * @author huangwei * @2014年5月27日 * @下午5:22:25 */ class MeasuredData { public int measuredHeight; public float textSize; public int width; public float lineWidthMax; ArrayList<LINE> contentList; public int oneLineWidth; public int hashIndex; } }
为方便在ListView中使用(ListView反复上下滑动会多次重新onMeasure),加了缓存,相同的情况下可以不用重复在测量一次。
对于SpannableString,只支持了ImageSpan,有其它需要者可自行扩展
Demo:http://download.csdn.net/detail/yellowcath/7421147
或https://github.com/yellowcath/MTextView.git