先看下效果图:

上面是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

上代码

/**
* @author huangwei
* @version SocialClient 1.2.0
* @功能 图文混排TextView,请使用{@link #setMText(CharSequence)}
* @2014年5月27日
* @下午5:29:27
*/
public class MTextView extends TextView
{
/**
* 缓存测量过的数据
*/
private static HashMap<String, SoftReference<MeasuredData>> measuredData = new HashMap<String, SoftReference<MeasuredData>>();
private static int hashIndex = ;
/**
* 存储当前文本内容,每个item为一行
*/
ArrayList<LINE> contentList = new ArrayList<LINE>();
private Context context;
/**
* 用于测量字符宽度
*/
private TextPaint paint = new TextPaint(); // private float lineSpacingMult = 0.5f;
private int textColor = Color.BLACK;
//行距
private float lineSpacing;
private int lineSpacingDP = ;
/**
* 最大宽度
*/
private int maxWidth;
/**
* 只有一行时的宽度
*/
private int oneLineWidth = -;
/**
* 已绘的行中最宽的一行的宽度
*/
private float lineWidthMax = -;
/**
* 存储当前文本内容,每个item为一个字符或者一个SpanObject
*/
private ArrayList<Object> obList = new ArrayList<Object>();
/**
* 是否使用默认{@link #onMeasure(int, int)}和{@link #onDraw(Canvas)}
*/
private boolean useDefault = false;
private CharSequence text = ""; private int minHeight;
/**
* 用以获取屏幕高宽
*/
private DisplayMetrics displayMetrics;
/**
* {@link android.text.style.BackgroundColorSpan}用
*/
private Paint textBgColorPaint = new Paint();
/**
* {@link android.text.style.BackgroundColorSpan}用
*/
private Rect textBgColorRect = new Rect(); public MTextView(Context context)
{
super(context);
this.context = context;
paint.setAntiAlias(true);
lineSpacing = dip2px(context, lineSpacingDP);
minHeight = dip2px(context, ); 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, ); displayMetrics = new DisplayMetrics();
} 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);
} @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 = , height = ; 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 > )
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 > -)
{
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;
}
if (contentList.isEmpty())
return;
int width; Object ob; int leftPadding = getCompoundPaddingLeft();
int topPadding = getCompoundPaddingTop(); float height = + topPadding + lineSpacing;
//只有一行时
if (oneLineWidth != -)
{
height = getMeasuredHeight() / - contentList.get().height / ;
} for (LINE aContentList : contentList)
{
//绘制一行
float realDrawedWidth = leftPadding;
for (int j = ; j < aContentList.line.size(); j++)
{
ob = aContentList.line.get(j);
width = aContentList.widthList.get(j); if (ob instanceof String)
{
canvas.drawText((String) ob, realDrawedWidth, height + aContentList.height - paint.getFontMetrics().descent, paint);
realDrawedWidth += width;
}
else if (ob instanceof SpanObject)
{
Object span = ((SpanObject) ob).span;
if(span instanceof ImageSpan)
{
ImageSpan is = (ImageSpan) span;
Drawable d = is.getDrawable(); int left = (int) (realDrawedWidth);
int top = (int) height;
int right = (int) (realDrawedWidth + width);
int bottom = (int) (height + aContentList.height);
d.setBounds(left, top, right, bottom);
d.draw(canvas);
realDrawedWidth += width;
}
else if(span instanceof BackgroundColorSpan)
{ textBgColorPaint.setColor(((BackgroundColorSpan) span).getBackgroundColor());
textBgColorPaint.setStyle(Style.FILL);
textBgColorRect.left = (int) realDrawedWidth;
int textHeight = (int) getTextSize();
textBgColorRect.top = (int) (height + aContentList.height - textHeight - paint.getFontMetrics().descent);
textBgColorRect.right = textBgColorRect.left+width;
textBgColorRect.bottom = (int) (height + aContentList.height + lineSpacing - paint.getFontMetrics().descent);
canvas.drawRect(textBgColorRect, textBgColorPaint);
canvas.drawText(((SpanObject) ob).source.toString(), realDrawedWidth, height + aContentList.height - paint.getFontMetrics().descent, paint);
realDrawedWidth += width;
}
else//做字符串处理
{
canvas.drawText(((SpanObject) ob).source.toString(), realDrawedWidth, height + aContentList.height - paint.getFontMetrics().descent, paint);
realDrawedWidth += width;
}
} }
height += aContentList.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 > )
{
return cachedHeight;
} // 已绘的宽度
float obWidth = ;
float obHeight = ; float textSize = this.getTextSize();
FontMetrics fontMetrics = paint.getFontMetrics();
//行高
float lineHeight = fontMetrics.bottom - fontMetrics.top;
//计算出的所需高度
float height = lineSpacing; int leftPadding = getCompoundPaddingLeft();
int rightPadding = getCompoundPaddingRight(); float drawedWidth = ; boolean splitFlag = false;//BackgroundColorSpan拆分用 width = width - leftPadding - rightPadding; oneLineWidth = -; contentList.clear(); StringBuilder sb; LINE line = new LINE(); for (int i = ; i < obList.size(); i++)
{
Object ob = obList.get(i); if (ob instanceof String)
{ obWidth = paint.measureText((String) ob);
obHeight = textSize;
}
else if (ob instanceof SpanObject)
{
Object span = ((SpanObject) ob).span;
if(span instanceof ImageSpan)
{
Rect r = ((ImageSpan)span).getDrawable().getBounds();
obWidth = r.right - r.left;
obHeight = r.bottom - r.top;
if (obHeight > lineHeight)
lineHeight = obHeight;
}
else if(span instanceof BackgroundColorSpan)
{
String str = ((SpanObject) ob).source.toString();
obWidth = paint.measureText(str);
obHeight = textSize; //如果太长,拆分
int k= str.length()-;
while(width - drawedWidth < obWidth)
{
obWidth = paint.measureText(str.substring(,k--));
}
if(k < str.length()-)
{
splitFlag = true;
SpanObject so1 = new SpanObject();
so1.start = ((SpanObject) ob).start;
so1.end = so1.start + k;
so1.source = str.substring(,k+);
so1.span = ((SpanObject) ob).span; SpanObject so2 = new SpanObject();
so2.start = so1.end;
so2.end = ((SpanObject) ob).end;
so2.source = str.substring(k+,str.length());
so2.span = ((SpanObject) ob).span; ob = so1;
obList.set(i,so2);
i--;
}
}//做字符串处理
else
{
String str = ((SpanObject) ob).source.toString();
obWidth = paint.measureText(str);
obHeight = textSize;
}
} //这一行满了,存入contentList,新起一行
if (width - drawedWidth < obWidth || splitFlag)
{
splitFlag = false;
contentList.add(line); if (drawedWidth > lineWidthMax)
{
lineWidthMax = drawedWidth;
}
drawedWidth = ;
height += line.height + lineSpacing; lineHeight = obHeight; line = new LINE();
} drawedWidth += obWidth; if (ob instanceof String && line.line.size() > && (line.line.get(line.line.size() - ) instanceof String))
{
int size = line.line.size();
sb = new StringBuilder();
sb.append(line.line.get(size - ));
sb.append(ob);
ob = sb.toString();
obWidth = obWidth + line.widthList.get(size - );
line.line.set(size - , ob);
line.widthList.set(size - , (int) obWidth);
line.height = (int) lineHeight; }
else
{
line.line.add(ob);
line.widthList.add((int) obWidth);
line.height = (int) lineHeight;
} } if (drawedWidth > lineWidthMax)
{
lineWidthMax = drawedWidth;
} if (line != null && line.line.size() > )
{
contentList.add(line);
height += lineHeight + lineSpacing;
}
if (contentList.size() <= )
{
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 -;
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 = ; i < contentList.size(); i++)
{
LINE line = contentList.get(i);
sb.append(line.toString());
}
return md.measuredHeight;
}
else
return -;
} /**
* 缓存已测量的数据
* @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 = ; 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(); ArrayList<SpanObject> isList = new ArrayList<MTextView.SpanObject>();
useDefault = false;
if (cs instanceof SpannableString)
{
SpannableString ss = (SpannableString) cs;
CharacterStyle[] spans = ss.getSpans(, ss.length(), CharacterStyle.class);
for (int i = ; i < spans.length; i++)
{ int s = ss.getSpanStart(spans[i]);
int e = ss.getSpanEnd(spans[i]);
SpanObject iS = new SpanObject();
iS.span = spans[i];
iS.start = s;
iS.end = e;
iS.source = ss.subSequence(s, e);
isList.add(iS);
}
} //对span进行排序,以免不同种类的span位置错乱
SpanObject[] spanArray = new SpanObject[isList.size()];
isList.toArray(spanArray);
Arrays.sort(spanArray,,spanArray.length,new SpanObjectComparator());
isList.clear();
for(int i=;i<spanArray.length;i++)
{
isList.add(spanArray[i]);
} String str = cs.toString(); for (int i = , j = ; i < cs.length(); )
{
if (j < isList.size())
{
SpanObject is = isList.get(j);
if (i < is.start)
{
Integer cp = str.codePointAt(i);
//支持增补字符
if (Character.isSupplementaryCodePoint(cp))
{
i += ;
}
else
{
i++;
} obList.add(new String(Character.toChars(cp))); }
else if (i >= is.start)
{
obList.add(is);
j++;
i = is.end;
}
}
else
{
Integer cp = str.codePointAt(i);
if (Character.isSupplementaryCodePoint(cp))
{
i += ;
}
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);
}
}
/**
* 设置行距
* @param lineSpacingDP 行距,单位dp
*/
public void setLineSpacingDP(int lineSpacingDP)
{
this.lineSpacingDP = lineSpacingDP;
lineSpacing = dip2px(context, lineSpacingDP);
}
/**
* 获取行距
* @return 行距,单位dp
*/
public int getLineSpacingDP()
{
return lineSpacingDP;
}
/**
* @author huangwei
* @version SocialClient 1.2.0
* @功能: 存储Span对象及相关信息
* @2014年5月27日
* @下午5:21:37
*/
class SpanObject
{
public Object span;
public int start;
public int end;
public CharSequence source;
}
/**
* @功能: 对SpanObject进行排序
* @author huangwei
* @2014年6月4日
* @下午5:21:30
* @version SocialClient 1.2.0
*/
class SpanObjectComparator implements Comparator<SpanObject>
{
@Override
public int compare(SpanObject lhs, SpanObject rhs)
{ return lhs.start - rhs.start;
} }
/**
* @author huangwei
* @version SocialClient 1.2.0
* @功能: 存储测量好的一行数据
* @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 = ; i < line.size(); i++)
{
sb.append(line.get(i) + ":" + widthList.get(i));
}
return sb.toString();
} } /**
* @author huangwei
* @version SocialClient 1.2.0
* @功能: 缓存的数据
* @2014年5月27日
* @下午5:22:25
*/
class MeasuredData
{
public int measuredHeight;
public float textSize;
public int width;
public float lineWidthMax;
public int oneLineWidth;
public int hashIndex;
ArrayList<LINE> contentList; }

为方便在ListView中使用(ListView反复上下滑动会多次重新onMeasure),加了缓存,相同的情况下可以不用重复在测量一次。

对于SpannableString,只支持了ImageSpan,有其它需要者可自行扩展

Demo:http://download.csdn.net/detail/yellowcath/7421147 或:https://github.com/yellowcath/MTextView.git (2014/6/4 更新 添加对BackGroundColorSpan的支持,修复一个会导致最后一行最后一个图形显示不全的bug)

代码:这里

Android 自绘TextView解决提前换行问题,支持图文混排的更多相关文章

  1. Android TextView中图文混排设置行间距导致高度不一致问题解决

    最近项目中需要实现一个评论带表情的功能,刚开始一切顺利,非常easy,突然有一天发现文字跟表情混排的时候,TextView中图文高度不一致,excuse...什么鬼,之前明明测试过图文混排,不存在这个 ...

  2. Android中Textview显示Html,图文混排,支持图片点击放大

    本文首发于网易云社区 对于呈现Html文本来说,Android提供的Webview控件可以得到很好的效果,但使用Webview控件的弊端是效率相对比较低,对于呈现简单的html文本的话,杀鸡不必使用牛 ...

  3. android:怎样在TextView实现图文混排

    我们通常在TextView文本中设置文字.但是怎样设置图文混排呢? 我就在这里写一个样例 .我们须要用到一点简单的HTML知识 在TextView中预订了一些类似HTML的标签,通过标签能够使Text ...

  4. 使用android SpannableStringBuilder实现图文混排

    项目开发中需要实现这种效果 多余两行,两行最后是省略号,省略号后面是下拉更多 之前用过的是Html.fromHtml去处理图文混排的,仅仅是文字后图片或者文字颜色字体什么的, 但是这里需要在最后文字的 ...

  5. 使用android SpannableStringBuilder实现图文混排,看到许多其他

    项目开发需要达到这种效果 watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvZmFuY3lsb3ZlamF2YQ==/font/5a6L5L2T/fontsiz ...

  6. TextView + Spanned实现图文混排以及图片点击交互

    最近要实现图文混排的需求,webview过大,所以想到了用SpannableStringBuilder来实现. 不过参考了大量国内文章,大多数是教你如何实现图文混排,并没有提及图片点击交互的.有翻阅了 ...

  7. Android自动解析html带图片,实现图文混排

    在android中,如何将html代码转换为text,然后显示在textview中呢,有一个简单直接的方法: textView.setText(Html.fromHtml(content)); 然而用 ...

  8. 仿小米便签图文混排 EditText解决尾部插入文字bug

    一直想实现像小米便签那样的图文混排效果,收集网上的办法无非三种: 1.自定义布局,每张图片是一个ImageView,插入图片后插入EditText,缺点是实现复杂,不能像小米便签那样同时选中图片和文字 ...

  9. Android图文混排-实现EditText图文混合插入上传

    前段时间做了一个Android会议管理系统,项目需求涉及到EditText的图文混排,如图: 在上图的"会议详情"中.须要支持文本和图片的混合插入,下图演示输入的演示样例: 当会议 ...

随机推荐

  1. MSDN 2005 安装问题

    在安装玩Visual Studio  MSDN 2005时,经常会出现这种问题:“无法显示 Microsoft 文档资源管理器,因为指定的帮助集合“ms-help://MS.MSDNQTR.v” 网上 ...

  2. 对于观察者模式,Reactor模式,Proactor模式的一点理解

    最近就服务器程序IO效率这一块了解一下设计模式中的Reacotr模式和proactor模式,感觉跟观察者模式有些类似的地方,网上也看了一些其他人对三者之间区别的理解,都讲得很仔细,在此根据自己的理解做 ...

  3. ubuntu后台配置无线网络

    一.静态配置: 1.编辑 /etc/network/interfaces: auto loiface lo inet loopback auto wlan0iface wlan0 inet stati ...

  4. 利用WeX5集成百度地图

    最近做一个地图类的app经过几天的摸索,终于完成百度地图集成的界面先看效果:1.加载完成之后,页面加载制定位置的地图2.顶部能够输入地图的关键字,地图显示符合条件的下拉列表3.用户选择了相应的选项后, ...

  5. android eclipse集成环境

    Android开发工具(ADT)是一个插件为Eclipse IDE,它的目的是给你一个强大的,集成的环境来构建Android应用程序. ADT扩展了Eclipse的功能使用Android SDK工具, ...

  6. Javascript中最常用的55个经典技巧

    Javascript中最常用的55个经典技巧1. oncontextmenu="window.event.returnValue=false" 将彻底屏蔽鼠标右键<table ...

  7. ARM-Linux S5PV210 UART驱动(1)----用户手册中的硬件知识

    一.概述 The Universal Asynchronous Receiver and Transmitter (UART) in S5PV210 provide four independent ...

  8. 转几篇WPF文章

    How to view word document in WPF application (CSVSTOViewWordInWPF) WPF 浏览PDF 文件 如何保存RichTextBox的文本到数 ...

  9. 百度地图 获取矩形point

    http://developer.baidu.com/map/jsdemo.htm#f0_7  鼠标绘制点线面 <!DOCTYPE html><html><head> ...

  10. 【maven项目结构】module 生成独立的jar

    生成jar 生成jar的过程会出现以下问题: clean完了之后就会出现以下问题: install [INFO] Scanning for projects... [INFO] [INFO] ---- ...