先看下效果图:

上面是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. Paying for upgrades, by Bob Arnson

    Following content is reprinted from here, please go to the original website for more information. Au ...

  2. PHP的基础计算器

    设计一个计算的功能,该功能能够完成运算并且能够对不合理的数据进行验证并且给出错误提示. 规则: 第一个数,第二个数不能够为空 如果操作符是/,第二个数数不能够为0. <?php header(' ...

  3. Linux C 程序 指针和字符串函数(11)

    指向字符串的指针 C语言访问字符串很多方法:1.用字符数组存放一个字符串 char string[] = "Linux C"; printf("%s\n".st ...

  4. Repeater中将int类型和bool类型的字段以字符显示出来

    图一 图二 比如将图一中是否显示中的列显示以图二中的方式显示: 方法1: 1.在后台编写方法:a.aspx.cs代码如下 //IsShow字段显示的方法public string GetStrIsSh ...

  5. AngularJS(9)-表单

    AngularJS 表单是输入控件的集合 <!DOCTYPE html> <html lang="en"> <head> <meta ch ...

  6. python(四)数据持久化操作 文件存储

    1.写入 导入pickle包 然后组织一个列表my_list,保存为pkl格式,可以是任意格式 在磁盘下回出现一个保存的文件 2.读取

  7. phpmailer 实现发送邮件

    在注册的时候,常常会用到邮件验证,一直想弄明白这是怎么实现的,记得2年前曾经试过这个问题,没有实现,今天困到不行的时候开始决定搞明白这个,然后,然后就出来了. <?php require(&qu ...

  8. Import和SQL*Loader这2个工具的异同

    问:请讲述Import和SQL*Loader这2个工具的异同? 解答: 相同点:这两个ORACLE工具都是用来将数据导入数据库的. 区别是: IMPORT工具只能处理由另一个ORACLE工具EXPOR ...

  9. easy ui 下拉级联效果 ,下拉框绑定数据select控件

    html代码: ①两个下拉框,一个是省,另一个市 <tr> <td>省:</td> <td> <select id="ProvinceI ...

  10. Objective-C中的数据类型、常量、变量、运算符与表达式

    1.Objective-C中的数据类型: Objective-C中的基本数据类型有:int.char(-128-127).float.double.BOOL,Byte(0-255) Id类型相当于(等 ...