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

上代码

/**
 * @功能 图文混排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

Android 自绘TextView解决提前换行问题,支持图文混排,布布扣,bubuko.com

时间: 2024-10-25 17:59:03

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

android 你所不知道的类SpannableStringBuilder的总结--实现图文混排,查看更多,下划线等等

今天无意中看到的一个类SpannableStringBuilder,查了下感觉很牛叉: 1.看下实现的效果 第一种 引用自:使用android SpannableStringBuilder实现图文混排,查看更多 第二种 实现的代码见   使用的模板 2.简单的使用模板: 引用自  http://blog.it985.com/14433.html public class MainActivity extends Activity implements OnClickListener { priv

android:如何在TextView实现图文混排

我们通常在TextView文本中设置文字.可是如何设置图文混排呢? 我就在这里写一个例子 .我们需要用到一点简单的HTML知识 在TextView中预订了一些类似HTML的标签,通过标签可以使TextView控件显示不同颜色,大小,字体的文字 <font>:设置颜色和字体 <big>:设置大号 <small>:设置小号 <i>\<b>:斜体.粗体 <a>:链接地址 <img>:插入图片 在drawable中存入我们的图片.

Android Json数据的解析+ListView图文混排+缓存算法Lrucache 仿知乎

前几天心血来潮,打算根据看知乎的API自己做一个小知乎,定制的过程遇到ListView的优化问题及图片未缓存重加载等等许多问题,解决了以后打算和博友分享一下. 接口数据:http://api.kanzhihu.com/getpostanswers/20150925/archive 首先,Json数据太常用,相信每一位开发者Json的解析都是必备的.我们要准备以下知识: JavaBean,枚举你需要的元素,用来存储数据. 异步加载网络内容的必备途径,多线程加载+AsyncTask两种方式. Jso

使用android SpannableStringBuilder实现图文混排,查看更多

项目开发中需要实现这种效果 多余两行,两行最后是省略号,省略号后面是下拉更多 之前用过的是Html.fromHtml去处理图文混排的,仅仅是文字后图片或者文字颜色字体什么的, 但是这里需要在最后文字的省略号后面添加图片. 直接上代码吧,代码注释很多,慢慢研究 private void toggleEllipsize(final TextView tv,final String desc){ if(desc == null){ return; } tv.getViewTreeObserver().

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

一直想实现像小米便签那样的图文混排效果,收集网上的办法无非三种: 1.自定义布局,每张图片是一个ImageView,插入图片后插入EditText,缺点是实现复杂,不能像小米便签那样同时选中图片和文字 2.通过Html.fromHtml(source),可以将图片加载写进ImageGetter,实现后无bug,但是只能显示Html,当EditText setText后,想取出之前的HTML格式      图片得到的是一个obj的字符,查看了很多博客,包括stackoverflow也没给出办法从e

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

前段时间做了一个Android会议管理系统,项目需求涉及到EditText的图文混排,如图: 在上图的"会议详情"中.须要支持文本和图片的混合插入,下图演示输入的演示样例: 当会议创建完毕以后,保存数据到server.然后查看刚刚创建好的会议.如图: 一.明白需求 首先.点击"会议详情"文本框中,正常输入文本,然后点击左下角的图片图标.进入系统的相冊用来选择一张图片并插入到文本框中,你还能够将光标停留在随意的文字中间,完毕图片的插入.回退建即能够逐个删除文字,也能够

Android图文混排(一)-实现EditText图文混合插入上传

前段时间做了一个Android会议管理系统,项目需求涉及到EditText的图文混排,如图: 在上图的"会议详情"中,需要支持文本和图片的混合插入,下图演示输入的示例: 当会议创建完成以后,保存数据到服务器,然后查看刚刚创建好的会议,如图: 一.明确需求 首先,点击"会议详情"文本框中,正常输入文本,然后点击左下角的图片图标,进入系统的相册用来选择一张图片并插入到文本框中,你还可以将光标停留在任意的文字中间,完成图片的插入,回退建即可以逐个删除文字,也可以删除图片.

Android 图文混排 通过webview实现并实现点击图片

在一个开源项目看到是用的webview 实现的 1. 这是在asset中的一个模板html <html> <head> <title>News Detail</title> <meta name="viewport" content="width=device-width, minimum-scale=0.5, initial-scale=1.2, maximum-scale=2.0, user-scalable=1&qu

【转】Android TextView SpannableStringBuilder 图文混排颜色斜体粗体下划线删除线

spannableStringBuilder 用法详解: SpannableString ss = new SpannableString("红色打电话斜体删除线绿色下划线图片:.");           //用颜色标记文本         ss.setSpan(new ForegroundColorSpan(Color.RED), 0, 2,                   //setSpan时需要指定的 flag,Spanned.SPAN_EXCLUSIVE_EXCLUSIV