Justificar texto dentro de un TextView en Android

Entonces, como la mayoría de ustedes saben, no hay texto que justifique dentro de un TextView en Android. Entonces, construí un TextView personalizado para solucionar el problema. Sin embargo, por alguna razón, a veces los signos de puntuación rompen la línea por alguna razón en algunos dispositivos. Probé en un LG G3 y un emulador (Nexus 4 con la última versión) y una coma "," por ejemplo, rompe la justificación en el LG G3 pero no en el emulador.

Si agrego un inicio y final de relleno (o izquierda y derecha) de al menos 2, el problema está resuelto. Esto me parece muy arbitrario.

Básicamente, mi lógica era que para justificar el texto, necesitaría conocer el ancho de TextView en sí, construir el texto en líneas que tengan una longitud máxima. Luego, al encontrar el número de espacios en la línea y el espacio vacío restante, estire los caracteres "" (espacio) a escalar de acuerdo con los píxeles restantes (o espacio en la vista).

Funciona casi perfectamente, y la mayoría de las veces también admite texto RTL.

Aquí hay algunas imágenes del texto (un simple lorem impsum) con y sin las marcas ofensivas (la primera está en el emulador nexus 4 con 7.1.1, la segunda está en LG G3 con v5.0)

Aquí está el código:

public class DTextView extends AppCompatTextView {

    private boolean justify;

    public DTextView(Context context) {
        super(context);
    }

    public DTextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(attrs);
    }

    public DTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(attrs);
    }

    private void setJustify(boolean justify) {
        this.justify = justify;
        if (justify) {
            justify();
        }
    }

    private void init(@Nullable AttributeSet attrs) {
        TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.DTextView, 0, 0);
        justify = ta.getBoolean(R.styleable.DTextView_justify, false);

        ta.recycle();
    }

    private SpannableStringBuilder justifyText() {

        String[] words = getText().toString().split(" ");
        setText("");

        int maxLineWidth = getWidth() - getPaddingLeft() - getPaddingRight();

        SpannableStringBuilder justifiedTextSpannable = new SpannableStringBuilder();

        //This will build the new text with the lines rearranged so that they will have a width
        //bigger than the View's own width
        ArrayList<String> lines = new ArrayList<>(0);
        String line = "";
        for (String word : words) {
            if (getWordWidth(line + word) < maxLineWidth) {
                line += word + " ";
            } else {
                line = line.substring(0, line.length() - 1);
                lines.add(line);
                line = word + " ";
            }
        }
        //Add the last line
        lines.add(line);

        for (int i = 0; i < lines.size() - 1; i++) {
            justifiedTextSpannable.append(justifyLine(lines.get(i), maxLineWidth));
            justifiedTextSpannable.append("\n");
        }

        justifiedTextSpannable.append(lines.get(lines.size() - 1));


        return justifiedTextSpannable;
    }

    private SpannableString justifyLine(String line, float maxWidth) {

        SpannableString sLine = new SpannableString(line);
        float spaces = line.split(" ").length - 1;

        float spaceCharSize = getWordWidth(" ");
        float emptySpace = maxWidth - getWordWidth(line);
        float newSpaceSize = (emptySpace / spaces) + spaceCharSize;
        float scaleX = newSpaceSize / spaceCharSize;

        for (int i = 0; i < line.length(); i++) {
            if (line.charAt(i) == ' ') {
                sLine.setSpan(new ScaleXSpan(scaleX), i, i + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
            }
        }

        return sLine;
    }

    private void justify() {
        justify = false;
        setText(justifyText());
        invalidate();
    }

    private float getWordWidth(String word) {
        return getPaint().measureText(word);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (!justify)
            super.onDraw(canvas);
        else
            justify();
    }
}

Apreciaría mucho a cualquiera que pueda arrojar algo de luz sobre esto.

Respuestas a la pregunta(3)

Su respuesta a la pregunta