1. 程式人生 > >Android 自定義View 解決 TextView 自動換行排版不整齊

Android 自定義View 解決 TextView 自動換行排版不整齊

第一次寫東西,內心小緊張,又不知道怎麼寫,儘量把遇到的問題和解決思路說清楚,寫的不好請見諒。

需求

專案有一個需求,很簡單,就是一個recyclerview,item裡面是兩個textview。一個TextView顯示的字串包含圓角、半形和中、英文以及數字。

想起來簡單,但是一顯示就出問題了。右側的TextView因為自動換行的問題顯示錯亂,真不行。至於原因,網上有很多介紹,下面就講一下解決過程。

解決過程

1.有問題找度娘

和大多數人一樣,發現這個問題,立馬找百度看看有可以直接用的,大致分為兩種:

1)手動拆分字串:不管是自定義view還是在view預載入時,手動對要顯示的字串進行拆分,新增換行符“\n”後再顯示。

2)自定義view,挨個畫每個字元,如果排不下就換行

以這兩種方式,都找到了可以直接用程式碼。我還是比較謹慎,先寫了demo看了,效果還行,才往專案裡寫,心想這下總解決了哇,然而高興得太早了。demo只是給了個TextView,可是專案是RecyclerView,一執行,程式能跑起來的就不錯了,還有的直接OOM,於是心灰意冷。

2.求人不如求己

既然百度不到,就只有自己動手了。解決思路採用的是自定義view,挨個畫字元。原理很簡單,直接上程式碼:

public class MyTextView extends View {
    //內容填充畫筆
    private Paint contentPaint;
    //標準的字型顏色
    private String contentNormalColor = "#737373";
    //有焦點的字型顏色
    private String contentFocuedColor = "#333333";
    //控制元件寬度
    private int viewWidth = 0;
    //控制元件高度
    private int viewHeight = 0;
    //標準的字的樣式
    public static final int TEXT_TYPE_NORMAL = 1;
    //控制元件獲取焦點的時候進行的處理
    public static final int TEXT_TYPE_FOCUED = 2;
    //預設是標準的樣式
    private int currentTextType = TEXT_TYPE_NORMAL;
    //預設當前的顏色
    private String textColor = "#333333";
    //字型大小
    private int textSize = 40;
    //內容
    private String mText = "測試的文字資訊";
    //最小view高度
    private float minHeight = 0;
    //最小view寬度
    private float minWidth = 0;
    //行間距
    private float lineSpace;
    //最大行數
    private int maxLines = 0;
    //文字測量工具
    private Paint.FontMetricsInt textFm;
    ///真實的行數
    private int realLines;
    //當前顯示的行數
    private int line;
    //在末尾是否顯示省略號
    private boolean showEllipsise;

    //文字顯示區的寬度
    private int textWidth;
    //單行文字的高度
    private int signleLineHeight;
    private int mPaddingLeft,mPaddingRight,mPaddingTop,mPaddingBottom;
    /**
     * 省略號
     **/
    private String ellipsis = "...";
    public MyTextView(Context context) {
        this(context,null);
    }

    public MyTextView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public MyTextView(Context context,  AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initAttr(context,attrs);
        init();
    }
    private boolean isFristload;

    /**
     * 初始化
     */
    private void init() {
        contentPaint = new Paint();
        contentPaint.setTextSize(textSize);
        contentPaint.setAntiAlias(true);
        contentPaint.setStrokeWidth(2);
        contentPaint.setColor(Color.parseColor(textColor));
        contentPaint.setTextAlign(Paint.Align.LEFT);
        textFm = contentPaint.getFontMetricsInt();
        signleLineHeight=Math.abs(textFm.top-textFm.bottom);
    }

    /**
     * 初始化屬性
     * @param context
     * @param attrs
     */
    private void initAttr(Context context,AttributeSet attrs) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyTextView);
        mPaddingLeft = typedArray.getDimensionPixelSize(R.styleable.MyTextView_paddingLeft, 0);
        mPaddingRight = typedArray.getDimensionPixelSize(R.styleable.MyTextView_paddingRight, 0);
        mPaddingTop = typedArray.getDimensionPixelSize(R.styleable.MyTextView_paddingTop, 0);
        mPaddingBottom = typedArray.getDimensionPixelSize(R.styleable.MyTextView_paddingBottom, 0);

        mText = typedArray.getString(R.styleable.MyTextView_text);
        textColor = typedArray.getString(R.styleable.MyTextView_textColor);
        if(textColor==null){
            textColor="#000000";
        }
        textSize = typedArray.getDimensionPixelSize(R.styleable.MyTextView_textSize, 50);
        lineSpace = typedArray.getInteger(R.styleable.MyTextView_lineSpacing, 0);
        typedArray.recycle();
    }

    public void setText(String ss){
        this.mText=ss;
        invalidate();
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec,heightMeasureSpec);
        viewWidth=getMeasuredWidth();
        textWidth=viewWidth-mPaddingLeft-mPaddingRight;
        viewHeight= (int) getViewHeight();
        setMeasuredDimension(viewWidth, viewHeight);
    }

    private float getViewHeight() {
        char[] textChars=mText.toCharArray();
        float ww=0.0f;
       int count=0;
        StringBuilder sb=new StringBuilder();
        for(int i=0;i<textChars.length;i++){
            float v = contentPaint.measureText(textChars[i] + "");
            if(ww+v<=textWidth){
                sb.append(textChars[i]);
                ww+=v;
            }
            else{
                count++;
                sb=new StringBuilder();
                ww=0.0f;
                ww+=v;
                sb.append(textChars[i]);
            }
        }
        if(sb.toString().length()!=0){
            count++;
        }
        return count*signleLineHeight+lineSpace*(count-1)+mPaddingBottom+mPaddingTop;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        drawText(canvas);
    }

    /**
     * 迴圈遍歷畫文字
     * @param canvas
     */
    private void drawText(Canvas canvas) {

        char[] textChars=mText.toCharArray();
        float ww=0.0f;
        float startL=0.0f;
        float startT=0.0f;
        startL+=mPaddingLeft;
        startT+=mPaddingTop+signleLineHeight/2+ (textFm.bottom-textFm.top)/2 - textFm.bottom;
        StringBuilder sb=new StringBuilder();

        for(int i=0;i<textChars.length;i++){
            float v = contentPaint.measureText(textChars[i] + "");
            if(ww+v<=textWidth){
                sb.append(textChars[i]);
                ww+=v;
            }
            else{
                canvas.drawText(sb.toString(),startL,startT,contentPaint);
                startT+=signleLineHeight+lineSpace;
                sb=new StringBuilder();
                ww=0.0f;
                ww+=v;
                sb.append(textChars[i]);
            }
        }

        if(sb.toString().length()>0){
            canvas.drawText(sb.toString(),startL,startT,contentPaint);
        }

    }
    
}

自定義屬性:

<declare-styleable name="MyTextView">
        <attr name="paddingLeft" format="dimension" />
        <attr name="paddingTop" format="dimension" />
        <attr name="paddingRight" format="dimension" />
        <attr name="paddingBottom" format="dimension" />
        <attr name="textSize" format="dimension" />
        <attr name="textColor" format="string" />
        <attr name="text" format="string"/>
        <attr name="lineSpacing" format="integer" />
        <attr name="maxLine" format="integer"/>
        <attr name="ellipsis" format="boolean" />
    </declare-styleable>

有的屬性的邏輯沒新增進去,或者沒用上,後面再完善。下面上一個效果圖:

有個坑

view在測量尺寸上可能還是有點問題,用在recyclerview裡面,item的佈局用的包裹,因為item 的複用關係,該view的尺寸是對的,但是item的尺寸卻錯亂了,具體原因再找一下。遮蔽方法是:繼承recyclerview,重寫onMeasure():

 @Override
    protected void onMeasure(int widthSpec, int heightSpec) {
        int customSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
        super.onMeasure(widthSpec, customSpec);
    }

佈局上在recyclerview外套一層scrollview,就正常了。後頭有空再找找原因。

8.15補充:

上面這個問題,主要是recyclerview複用後,該自定義view的尺寸沒有重新繪製,呼叫一下requestLayout(),重繪一下就對了。

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

其他的:

這個問題,好像有個比較好的解決方式,對textview.getText().toString()的字串,重新測量字元是否夠一排,如果夠的話就繪製一行,不夠的話換行。。