您的位置:首页 > 移动开发 > Android开发

Android 自绘TextView解决提前换行问题,支持图文混排

2014-11-14 14:13 435 查看
本文转载自:Android 自绘TextView解决提前换行问题,支持图文混排

先看下效果图:


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

上代码

[java] view
plaincopy





</pre><pre>

[java] view
plaincopy





/**

* @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 = 0;

/**

* 存储当前文本内容,每个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 = 5;

/**

* 最大宽度

*/

private int maxWidth;

/**

* 只有一行时的宽度

*/

private int oneLineWidth = -1;

/**

* 已绘的行中最宽的一行的宽度

*/

private float lineWidthMax = -1;

/**

* 存储当前文本内容,每个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, 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();

}

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 = 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;

}

if (contentList.isEmpty())

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 (LINE aContentList : contentList)

{

//绘制一行

float realDrawedWidth = leftPadding;

for (int j = 0; 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 > 0)

{

return cachedHeight;

}

// 已绘的宽度

float obWidth = 0;

float obHeight = 0;

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 = 0;

boolean splitFlag = false;//BackgroundColorSpan拆分用

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 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()-1;

while(width - drawedWidth < obWidth)

{

obWidth = paint.measureText(str.substring(0,k--));

}

if(k < str.length()-1)

{

splitFlag = true;

SpanObject so1 = new SpanObject();

so1.start = ((SpanObject) ob).start;

so1.end = so1.start + k;

so1.source = str.substring(0,k+1);

so1.span = ((SpanObject) ob).span;

SpanObject so2 = new SpanObject();

so2.start = so1.end;

so2.end = ((SpanObject) ob).end;

so2.source = str.substring(k+1,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 = 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 (drawedWidth > lineWidthMax)

{

lineWidthMax = drawedWidth;

}

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();

ArrayList<SpanObject> isList = new ArrayList<MTextView.SpanObject>();

useDefault = false;

if (cs instanceof SpannableString)

{

SpannableString ss = (SpannableString) cs;

CharacterStyle[] spans = ss.getSpans(0, ss.length(), CharacterStyle.class);

for (int i = 0; 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,0,spanArray.length,new SpanObjectComparator());

isList.clear();

for(int i=0;i<spanArray.length;i++)

{

isList.add(spanArray[i]);

}

String str = cs.toString();

for (int i = 0, j = 0; 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 += 2;

}

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 += 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);

}

}

/**

* 设置行距

* @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 = 0; 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显示文本不换行的问题确实不美观,这篇文章可以作为参考,github上还有类似的工程:android-justifiedtextview
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐