1. 程式人生 > >Android 自繪TextView解決提前換行問題,支援圖文混排

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

上程式碼

</pre><pre>
/**
 * @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,有其它需要者可自行擴充套件

或: