TextView 自動換行,每行排滿的自定義TextView

madreain發表於2019-08-07

效果圖是下面的,而用傳統的Textview不行

效果圖

實際上用TextView

TextView 自動換行,每行排滿的自定義TextView

直接自定義TextView,上程式碼

package com.madreian.hulk.view;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;

import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.FontMetrics;
import android.graphics.Paint.Style;
import android.graphics.Rect;
import android.text.Spannable;
import android.text.TextPaint;
import android.text.style.BackgroundColorSpan;
import android.text.style.CharacterStyle;
import android.text.style.DynamicDrawableSpan;
import android.util.AttributeSet;
import android.util.DisplayMetrics;


/**
 * @author madreain
 * @date 2019-08-06.
 * module:圖文混排TextView,請使用{@link #setMText(CharSequence)}
 * description:
 */

public class MTextView extends android.support.v7.widget.AppCompatTextView {
    /**
     * 快取測量過的資料
     */
    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();
    /**
     * 用於測量span高度
     */
    private Paint.FontMetricsInt mSpanFmInt = new Paint.FontMetricsInt();
    /**
     * 臨時使用,以免在onDraw中反覆生產新物件
     */
    private FontMetrics mFontMetrics = new FontMetrics();

    // private float lineSpacingMult = 0.5f;
    private int textColor = Color.BLACK;
    // 行距
    private float lineSpacing;
    private int lineSpacingDP = 5;
    /**
     * 段間距,-1為預設
     */
    private int paragraphSpacing = -1;
    /**
     * 最大寬度
     */
    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(android.graphics.Canvas)}
     */
    private boolean useDefault = false;
    protected 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);
        init(context);
    }

    public MTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public MTextView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context);
    }

    public void init(Context context) {
        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;
            /** 是否換新段落 */
            boolean newParagraph = false;
            for (int j = 0; j < aContentList.line.size(); j++) {
                ob = aContentList.line.get(j);
                width = aContentList.widthList.get(j);

                paint.getFontMetrics(mFontMetrics);
                float x = realDrawedWidth;
                float y = height + aContentList.height
                        - paint.getFontMetrics().descent;
                float top = y - aContentList.height;
                float bottom = y + mFontMetrics.descent;
                if (ob instanceof String) {
                    canvas.drawText((String) ob, realDrawedWidth, y, paint);
                    realDrawedWidth += width;
                    if (((String) ob).endsWith("\n")
                            && j == aContentList.line.size() - 1) {
                        newParagraph = true;
                    }
                } else if (ob instanceof SpanObject) {
                    Object span = ((SpanObject) ob).span;
                    if (span instanceof DynamicDrawableSpan) {

                        int start = ((Spannable) text).getSpanStart(span);
                        int end = ((Spannable) text).getSpanEnd(span);
                        ((DynamicDrawableSpan) span).draw(canvas, text, start,
                                end, (int) x, (int) top, (int) y, (int) bottom,
                                paint);
                        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 - mFontMetrics.descent);
                        textBgColorRect.right = textBgColorRect.left + width;
                        textBgColorRect.bottom = (int) (height
                                + aContentList.height + lineSpacing - mFontMetrics.descent);
                        canvas.drawRect(textBgColorRect, textBgColorPaint);
                        canvas.drawText(((SpanObject) ob).source.toString(),
                                realDrawedWidth, height + aContentList.height
                                        - mFontMetrics.descent, paint);
                        realDrawedWidth += width;
                    } else// 做字串處理
                    {
                        canvas.drawText(((SpanObject) ob).source.toString(),
                                realDrawedWidth, height + aContentList.height
                                        - mFontMetrics.descent, paint);
                        realDrawedWidth += width;
                    }
                }
            }
            // 如果要繪製段間距
            if (newParagraph) {
                height += aContentList.height + paragraphSpacing;
            } else {
                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;
                if ("\n".equals(ob)) { // 遇到"\n"則換行
                    obWidth = width - drawedWidth;
                }
            } else if (ob instanceof SpanObject) {
                Object span = ((SpanObject) ob).span;
                if (span instanceof DynamicDrawableSpan) {
                    int start = ((Spannable) text).getSpanStart(span);
                    int end = ((Spannable) text).getSpanEnd(span);
                    obWidth = ((DynamicDrawableSpan) span).getSize(getPaint(),
                            text, start, end, mSpanFmInt);
                    obHeight = Math.abs(mSpanFmInt.top)
                            + Math.abs(mSpanFmInt.bottom);
                    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;
                // 判斷是否有分段
                int objNum = line.line.size();
                if (paragraphSpacing > 0 && objNum > 0
                        && line.line.get(objNum - 1) instanceof String
                        && "\n".equals(line.line.get(objNum - 1))) {
                    height += line.height + paragraphSpacing;
                } else {
                    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<SpanObject>();
        useDefault = false;

        if (cs instanceof Spannable) {
            CharacterStyle[] spans = ((Spannable) cs).getSpans(0, cs.length(),
                    CharacterStyle.class);
            for (int i = 0; i < spans.length; i++) {
                int s = ((Spannable) cs).getSpanStart(spans[i]);
                int e = ((Spannable) cs).getSpanEnd(spans[i]);
                SpanObject iS = new SpanObject();
                iS.span = spans[i];
                iS.start = s;
                iS.end = e;
                iS.source = cs.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);
    }

    public void setParagraphSpacingDP(int paragraphSpacingDP) {
        paragraphSpacing = dip2px(context, paragraphSpacingDP);
    }

    /**
     * 獲取行距
     *
     * @return 行距,單位dp
     */
    public int getLineSpacingDP() {
        return lineSpacingDP;
    }

    /**
     * 儲存Span物件及相關資訊
     */
    class SpanObject {
        public Object span;
        public int start;
        public int end;
        public CharSequence source;
    }

    /**
     * 對SpanObject進行排序
     */
    class SpanObjectComparator implements Comparator<SpanObject> {
        @Override
        public int compare(SpanObject lhs, SpanObject rhs) {

            return lhs.start - rhs.start;
        }
    }

    /**
     * 儲存測量好的一行資料
     */
    class LINE {
        public ArrayList<Object> line = new ArrayList<Object>();
        public ArrayList<Integer> widthList = new ArrayList<Integer>();
        public float 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();
        }
    }

    /**
     * 快取的資料
     */
    class MeasuredData {
        public int measuredHeight;
        public float textSize;
        public int width;
        public float lineWidthMax;
        public int oneLineWidth;
        public int hashIndex;
        ArrayList<LINE> contentList;
    }
}


複製程式碼

設定的時候使用setMText

相關文章