Android 自繪TextView解決提前換行問題,支援圖文混排

來源:互聯網
上載者:User

標籤: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();}@Overridepublic void setMaxWidth(int maxpixels){super.setMaxWidth(maxpixels);maxWidth = maxpixels;}@Overridepublic void setMinHeight(int minHeight){super.setMinHeight(minHeight);this.minHeight = minHeight;}@Overrideprotected 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);}@Overrideprotected 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;}}@Overridepublic 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;}elsereturn -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;@Overridepublic 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

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.