Search code examples
androidtextviewscrollview

How to fix Cutoff big text when I using custom TextView to justify text?


I found on GitHub a code by Nyakokishi a link that allows to justify the text in a TextView through a custom class, works perfectly for the initial purpose, but creates another problem, when the text is too large is not shown all.

Custom TextView class (All the gratitude to Droidroid for creating and Nyakokishi for great demo):

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.Build;
import android.text.Layout;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.util.AttributeSet;

import androidx.appcompat.widget.AppCompatTextView;

import mz.ac.ucm.fgti.newsfgti.android_client.R;

/**
 * Created by Droidroid on 2016/3/24.
 */
public class JustifyTextView extends AppCompatTextView  {

private TextPaint mPaint;
private Layout mLayout;
// Row height
private int mTextHeight;
// Y-axis
private float mLineY;
// View content width
private int mContentWidth;
// Character spacing
private float mCharacterSpace;
// Half of the character spacing
private float mGapWidth;
// Line spacing
private int mLineSpace;
// Full-width character width
private float mSBCwith;
// Aligned
private boolean isToAlignChars;

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

private void init(Context context, AttributeSet attrs) {
    TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.JustifyTextView);
    mCharacterSpace = ta.getDimensionPixelSize(ta.getIndex(R.styleable.JustifyTextView_character_space)
            , 0);
    mLineSpace = ta.getDimensionPixelSize(ta.getIndex(R.styleable.JustifyTextView_line_space), 1);
    isToAlignChars = ta.getBoolean(ta.getIndex(R.styleable.JustifyTextView_align_chars), true);
    ta.recycle();
    mGapWidth = mCharacterSpace / 2;
    String text = getText().toString();
    if (text.charAt(text.length() - 1) == 10) {
        text = text.substring(0, text.length() - 1);
        setText(text);
    }
}

@Override
protected void onDraw(Canvas canvas) {
    mPaint = getPaint();
    mPaint.setColor(getCurrentTextColor());
    mPaint.drawableState = getDrawableState();

    String text = getText().toString();
    mLayout = getLayout();
    // layout.getLayout() has a NullPointerException in 4.4.3
    if (mLayout == null) {
        return;
    }

    Paint.FontMetrics fm = mPaint.getFontMetrics();
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
        mTextHeight = (int) (Math.ceil(fm.descent - fm.ascent) + getLineSpacingExtra());
    } else {
        mTextHeight = (int) (Math.ceil(fm.descent - fm.ascent) + mLineSpace);
    }
    mTextHeight = (int) (mTextHeight * mLayout.getSpacingMultiplier() + mLayout.getSpacingAdd());

    // The width of the view content
    mContentWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
    // Full-width character width
    mSBCwith = getSBCwidth(text);

    if (mSBCwith > 0) {
        if (isToAlignChars) {
            drawAligmentLines(canvas, text);
        } else {
            drawSBCLines(canvas, text);
        }
    } else {
        drawDBCLines(canvas, text);
    }
}

private void drawSBCLines(Canvas canvas, String text) {

    mLineY = getTextSize() + getPaddingTop();
    float x = getPaddingLeft();
    for (int i = 0; i < text.length(); i++) {
        String c = String.valueOf(text.charAt(i));
        if (c.equals(" ")) {
            continue;
        }
        float cw = StaticLayout.getDesiredWidth(c, getPaint());
        canvas.drawText(c, x, mLineY, getPaint());
        x += cw + mCharacterSpace;
        if (x + cw - mCharacterSpace > mContentWidth + getPaddingLeft() || c.equals("\n")) {
            mLineY += mTextHeight;
            x = getPaddingLeft();
        }
        // The last character in a line can squeeze
        else if (x + cw > mContentWidth + getPaddingLeft()
                && x - mCharacterSpace + cw <= mContentWidth + getPaddingLeft()) {
            x -= mCharacterSpace;
        }
    }
}

private void drawAligmentLines(Canvas canvas, String text) {

    mLineY = getTextSize() + getPaddingTop();
    float x = getPaddingLeft();
    for (int i = 0; i < text.length(); i++) {
        String c = String.valueOf(text.charAt(i));
        if (c.equals(" ")) {
            continue;
        }
        float cw = StaticLayout.getDesiredWidth(c, getPaint());
        float currentGapWidth = getGapWidth(text.charAt(i));
        canvas.drawText(c, x + currentGapWidth, mLineY, getPaint());
        if (i < text.length() - 1) {
            x += cw + 2 * currentGapWidth;
            float nextCw = StaticLayout.getDesiredWidth(String.valueOf(text.charAt(i + 1)), getPaint());
            float nextGapWidth = getGapWidth(text.charAt(i + 1));
            if (x + nextCw + 2 * nextGapWidth > mContentWidth + getPaddingLeft() || c.equals("\n")) {
                mLineY += mTextHeight;
                x = getPaddingLeft();
            }
        }
    }
}

private void drawDBCLines(Canvas canvas, String text) {
    mLineY = getTextSize() + getPaddingTop();
    float x = getPaddingLeft();

    int lineCount = mLayout.getLineCount();
    if (lineCount > 1) {
        for (int i = 0; i < lineCount; i++) {
            int lineStart = mLayout.getLineStart(i);
            int lineEnd = mLayout.getLineEnd(i);
            float lineWidth = mLayout.getLineWidth(i);
            String line = text.substring(lineStart, lineEnd);
            float wordSpace = (mContentWidth - lineWidth) / (getBlankCount(line) - 1);
            for (int j = 0; j < line.length(); j++) {
                String c = String.valueOf(line.charAt(j));
                float cw = StaticLayout.getDesiredWidth(c, getPaint());
                canvas.drawText(c, x, mLineY, getPaint());
                if (i < lineCount - 1 && line.charAt(j) == 32) {
                    x += cw + wordSpace;
                } else {
                    x += cw;
                }
            }
            mLineY += mTextHeight;
            x = getPaddingLeft();
        }
    } else {
        int lineStart = mLayout.getLineStart(0);
        int lineEnd = mLayout.getLineEnd(0);
        String line = text.substring(lineStart, lineEnd);
        for (int j = 0; j < line.length(); j++) {
            String c = String.valueOf(line.charAt(j));
            float cw = StaticLayout.getDesiredWidth(c, getPaint());
            canvas.drawText(c, x, mLineY, getPaint());
            x += cw;
        }
    }
}

private int getBlankCount(String line) {
    int count = 0;
    for (char c : line.toCharArray()) {
        if (c == 32) {
            count++;
        }
    }
    return count;
}

private float getSBCwidth(String text) {
    for (int i = 0; i < text.length(); i++) {
        String c = String.valueOf(text.charAt(i));
        if (Character.UnicodeBlock.of(text.charAt(i)) == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS) {
            return StaticLayout.getDesiredWidth(c, getPaint());
        }
    }
    return -1;
}

private float getGapWidth(char c) {
    float gapWidth = (mSBCwith - StaticLayout.getDesiredWidth(String.valueOf(c)
            , getPaint())) / 2 + mGapWidth;
    return gapWidth;
}

}

The XML file where I use the JustifyTextView (I use only on TextViews were I want it justified):

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:clickable="true"
android:paddingLeft="10sp"
android:paddingRight="10sp">

<ScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginTop="10sp"
    android:scrollbarAlwaysDrawHorizontalTrack="false"
    android:scrollbars="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="5sp"
        android:orientation="vertical"
        android:overScrollMode="never">

         <mz.ac.ucm.fgti.newsfgti.android_client.view.adapters.JustifyTextView
            android:id="@+id/col_titulo_detalhe"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:editable="false"
            android:enabled="false"
            android:fontFamily="@font/ubuntu_bold"
            android:gravity="center|top"
            android:inputType="textMultiLine"
            android:justificationMode="inter_word"
            android:lineSpacingExtra="8sp"
            android:text="texto para nao dar erro com JustifiedTextViee, sera substituido"
            android:textColor="@color/colorPrimary"
            android:textSize="24sp"
            android:textStyle="bold" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_margin="5sp"
            android:orientation="horizontal"
            android:overScrollMode="never">


            <TextView
                android:id="@+id/col_categoria_detalhe"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginRight="5.5sp"
                android:layout_weight="1"
                android:editable="false"
                android:enabled="false"
                android:fontFamily="@font/ubuntu_light"
                android:gravity="right"
                android:textColor="@color/colorBlack"
                android:textSize="12sp" />

            <TextView
                android:id="@+id/col_privilegio_detalhe"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:editable="false"
                android:enabled="false"
                android:fontFamily="@font/ubuntu_light"
                android:textColor="@color/colorBlack"
                android:textSize="12sp" />
        </LinearLayout>

        <TextView
            android:id="@+id/col_data_detalhe"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="20sp"
            android:editable="false"
            android:enabled="false"
            android:fontFamily="@font/ubuntu_light_italic"
            android:gravity="center"
            android:textColor="@color/colorBlack"
            android:textSize="12sp"
            android:textStyle="italic" />


        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            android:overScrollMode="never">

             <mz.ac.ucm.fgti.newsfgti.android_client.view.adapters.JustifyTextView
                android:id="@+id/col_conteudo_detalhe"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@android:color/transparent"
                android:editable="false"
                android:ems="10"
                android:enabled="false"
                android:fontFamily="@font/ubuntu_light"
                android:inputType="textMultiLine"
                android:justificationMode="inter_word"
                android:lineSpacingExtra="8sp"
                android:paddingBottom="10sp"
                android:text="texto para nao dar erro com JustifiedTextViee, sera substituido"
                android:textColor="@color/colorBlack"
                android:textSize="18sp" />
        </LinearLayout>
    </LinearLayout>
</ScrollView>
</RelativeLayout>

Fragment for inflate the previous XML:

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import androidx.fragment.app.Fragment;

import mz.ac.ucm.fgti.newsfgti.android_client.R;
import mz.ac.ucm.fgti.newsfgti.android_client.view.adapters.JustifyTextView;

public class TbInformacaoDetalheFragment extends Fragment {

public JustifyTextView col_titulo_detalhe;
public TextView col_privilegio_detalhe;
public TextView col_categoria_detalhe;
public TextView col_data_detalhe;
public JustifyTextView col_conteudo_detalhe;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

    View itemView = LayoutInflater.from(container.getContext())
            .inflate(R.layout.fragment_tb_informacao_detalhe, container, false);

    Bundle bundle = getActivity().getIntent().getExtras();

    col_titulo_detalhe = (JustifyTextView) itemView.findViewById(R.id.col_titulo_detalhe);
    col_privilegio_detalhe = (TextView) itemView.findViewById(R.id.col_privilegio_detalhe);
    col_categoria_detalhe = (TextView) itemView.findViewById(R.id.col_categoria_detalhe);
    col_data_detalhe = (TextView) itemView.findViewById(R.id.col_data_detalhe);
    col_conteudo_detalhe = (JustifyTextView) itemView.findViewById(R.id.col_conteudo_detalhe);

    col_titulo_detalhe.setText(getArguments().getString("titulo"));
    //A BARRA ANTES DO TEXTO SERVE PARA DEFINIR UMA SEPARACAO VISUAL ENTRE A CATEGORIA E O PRIVILEGIO
    col_privilegio_detalhe.setText("|  " + getArguments().getString("privilegio"));
    col_categoria_detalhe.setText(getArguments().getString("categoria"));
    col_data_detalhe.setText(getArguments().getString("data"));
    col_conteudo_detalhe.setText(getArguments().getString("conteudo"));


    return itemView;
}
}

Note that when I use a embedded TextView the whole text is shown.

Thank you in advance.


Solution

  • Try this solution

    Create a custom class and use this

    JustifyTextView.java

    import android.content.Context;
    import android.graphics.Canvas;
    import android.graphics.Paint;
    import android.text.Layout;
    import android.text.StaticLayout;
    import android.text.TextPaint;
    import android.util.AttributeSet;
    import android.widget.TextView;
    
    
    public class JustifyTextView extends TextView {
    
        private int mLineY;
        private int mViewWidth;
        public static final String TWO_CHINESE_BLANK = "  ";
    
        public JustifyTextView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        @Override
        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
            super.onLayout(changed, left, top, right, bottom);
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            TextPaint paint = getPaint();
            paint.setColor(getCurrentTextColor());
            paint.drawableState = getDrawableState();
            mViewWidth = getMeasuredWidth();
            String text = getText().toString();
            mLineY = 0;
            mLineY += getTextSize();
            Layout layout = getLayout();
    
            // layout.getLayout()在4.4.3出现NullPointerException
            if (layout == null) {
                return;
            }
    
            Paint.FontMetrics fm = paint.getFontMetrics();
    
            int textHeight = (int) (Math.ceil(fm.descent - fm.ascent));
            textHeight = (int) (textHeight * layout.getSpacingMultiplier() + layout.getSpacingAdd());
    
            for (int i = 0; i < layout.getLineCount(); i++) {
                int lineStart = layout.getLineStart(i);
                int lineEnd = layout.getLineEnd(i);
                float width = StaticLayout.getDesiredWidth(text, lineStart, lineEnd, getPaint());
                String line = text.substring(lineStart, lineEnd);
                if (needScale(line) && i < layout.getLineCount() -1) {
                    drawScaledText(canvas, lineStart, line, width);
                } else {
                    canvas.drawText(line, 0, mLineY, paint);
                }
                mLineY += textHeight;
            }
        }
    
        private void drawScaledText(Canvas canvas, int lineStart, String line, float lineWidth) {
            float x = 0;
            if (isFirstLineOfParagraph(lineStart, line)) {
                String blanks = "  ";
                canvas.drawText(blanks, x, mLineY, getPaint());
                float bw = StaticLayout.getDesiredWidth(blanks, getPaint());
                x += bw;
    
                line = line.substring(3);
            }
    
            int gapCount = line.length() - 1;
            int i = 0;
            if (line.length() > 2 && line.charAt(0) == 12288 && line.charAt(1) == 12288) {
                String substring = line.substring(0, 2);
                float cw = StaticLayout.getDesiredWidth(substring, getPaint());
                canvas.drawText(substring, x, mLineY, getPaint());
                x += cw;
                i += 2;
            }
    
            float d = (mViewWidth - lineWidth) / gapCount;
            for (; i < line.length(); i++) {
                String c = String.valueOf(line.charAt(i));
                float cw = StaticLayout.getDesiredWidth(c, getPaint());
                canvas.drawText(c, x, mLineY, getPaint());
                x += cw + d;
            }
        }
    
        private boolean isFirstLineOfParagraph(int lineStart, String line) {
            return line.length() > 3 && line.charAt(0) == ' ' && line.charAt(1) == ' ';
        }
    
        private boolean needScale(String line) {
            if (line == null || line.length() == 0) {
                return false;
            } else {
                return line.charAt(line.length() - 1) != '\n';
            }
        }
    
    }