先看下效果图:
上面是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)
代码:这里