11
votes

I'm looking for a simple way to forget that I'm using a WebView to have justified text in my TextView. Has someone made a custom view for this? I'm well aware that I can do something like this:

 WebView view = new WebView(this);    
         view.loadData("my html with text justification","text/html","utf-8");

But it gets ugly when you want to set the size, the color or other common properties of the TextView. There must be a more convenient way of doing it.

6

6 Answers

19
votes

It was getting on my nerves, I admit it. I like the TextViews to look like TextViews in the code, and even if I'm using a WebView as the means of achieving the text-align:justified formatting, I don't want to look at it that way.

I created a custom view (ugly, probably bad) that implements the methods that I commonly use from the TextView and modifies the content of the WebView in order to reflect those changes. Wether it's useful for someone else or a potential hazard I really don't know, for me it works, I've used it in several projects and haven't run into any issues. The only minor inconvenience is that I assume it as a bigger toll memory-wise but nothing to worry about if it's just one or two (correct me if I'm wrong).

The result is the following:

enter image description here

And the code for setting it programmatically is as simple as this:

 JustifiedTextView J = new JustifiedTextView();
                   J.setText("insert your text here");

Of course it'd be stupid to leave it like that so I also added the methods for changing the font-size and the font-color which are basically all I use TextViews for. Meaning I can do something like this:

 JustifiedTextView J = new JustifiedTextView();
                   J.setText("insert your text here");
                   J.setTextColor(Color.RED);
                   J.setTextSize(30);

And obtain the following result (images are cropped):

enter image description here

But, this is not to show us how it looks, it's to share how you've done it!

I know, I know. Here's the full code. It also addresses Problems when setting transparent background and loading UTF-8 strings into the view. See the comments in reloadData() for details.

public class JustifiedTextView extends WebView{
    private String core      = "<html><body style='text-align:justify;color:rgba(%s);font-size:%dpx;margin: 0px 0px 0px 0px;'>%s</body></html>";
    private String textColor = "0,0,0,255";
    private String text      = "";
    private int textSize     = 12;
    private int backgroundColor=Color.TRANSPARENT;

    public JustifiedTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.setWebChromeClient(new WebChromeClient(){});
    }

    public void setText(String s){
        this.text = s;
        reloadData();
    }

    @SuppressLint("NewApi")
    private void reloadData(){

        // loadData(...) has a bug showing utf-8 correctly. That's why we need to set it first.
        this.getSettings().setDefaultTextEncodingName("utf-8");

        this.loadData(String.format(core,textColor,textSize,text), "text/html","utf-8");

        // set WebView's background color *after* data was loaded.
        super.setBackgroundColor(backgroundColor);

        // Hardware rendering breaks background color to work as expected.
        // Need to use software renderer in that case.
        if(android.os.Build.VERSION.SDK_INT >= 11)
            this.setLayerType(WebView.LAYER_TYPE_SOFTWARE, null);
    }

    public void setTextColor(int hex){
        String h = Integer.toHexString(hex);
        int a = Integer.parseInt(h.substring(0, 2),16);
        int r = Integer.parseInt(h.substring(2, 4),16);
        int g = Integer.parseInt(h.substring(4, 6),16);
        int b = Integer.parseInt(h.substring(6, 8),16);
        textColor = String.format("%d,%d,%d,%d", r, g, b, a); 
        reloadData();
    }

    public void setBackgroundColor(int hex){
        backgroundColor = hex;
        reloadData();
    }

    public void setTextSize(int textSize){
        this.textSize = textSize;
        reloadData();
    }
}
12
votes

In just three steps, you can justify your web view text.

1)

// Justify tag
String justifyTag = "<html><body style='text-align:justify;'>%s</body></html>";

2)

// Concatenate your string with the tag to Justify it
String dataString = String.format(Locale.US, justifyTag, "my html with text justification");

3)

// Load the data in the web view
webView.loadDataWithBaseURL("", dataString, "text/html", "UTF-8", "");
9
votes

Without webview solution is : https://github.com/merterhk/JustifiedTextView

import java.util.ArrayList;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.text.TextPaint;
import android.view.View;

public class JustifiedTextView extends View {
        String text;
        ArrayList<Line> linesCollection = new ArrayList<Line>();
        TextPaint textPaint;
        Typeface font;
        int textColor;
        float textSize = 42f, lineHeight = 57f, wordSpacing = 15f, lineSpacing = 15f;
        float onBirim, w, h;
        float leftPadding, rightPadding;

        public JustifiedTextView(Context context, String text) {
                super(context);
                this.text = text;
                init();
        }

        private void init() {
                textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
                textColor = Color.BLACK;
        }

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
                super.onMeasure(widthMeasureSpec, heightMeasureSpec);

                if (font != null) {
                        font = Typeface.createFromAsset(getContext().getAssets(), "font/Trykker-Regular.ttf");
                        textPaint.setTypeface(font);
                }
                textPaint.setColor(textColor);

                int minw = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth();
                w = resolveSizeAndState(minw, widthMeasureSpec, 1);
                h = MeasureSpec.getSize(widthMeasureSpec);

                onBirim = 0.009259259f * w;
                lineHeight = textSize + lineSpacing;
                leftPadding = 3 * onBirim + getPaddingLeft();
                rightPadding = 3 * onBirim + getPaddingRight();

                textPaint.setTextSize(textSize);

                wordSpacing = 15f;
                Line lineBuffer = new Line();
                this.linesCollection.clear();
                String[] lines = text.split("\n");
                for (String line : lines) {
                        String[] words = line.split(" ");
                        lineBuffer = new Line();
                        float lineWidth = leftPadding + rightPadding;
                        float totalWordWidth = 0;
                        for (String word : words) {
                                float ww = textPaint.measureText(word) + wordSpacing;
                                if (lineWidth + ww + (lineBuffer.getWords().size() * wordSpacing) > w) {// is
                                        lineBuffer.addWord(word);
                                        totalWordWidth += textPaint.measureText(word);
                                        lineBuffer.setSpacing((w - totalWordWidth - leftPadding - rightPadding) / (lineBuffer.getWords().size() - 1));
                                        this.linesCollection.add(lineBuffer);
                                        lineBuffer = new Line();
                                        totalWordWidth = 0;
                                        lineWidth = leftPadding + rightPadding;
                                } else {
                                        lineBuffer.setSpacing(wordSpacing);
                                        lineBuffer.addWord(word);
                                        totalWordWidth += textPaint.measureText(word);
                                        lineWidth += ww;
                                }
                        }
                        this.linesCollection.add(lineBuffer);
                }
                setMeasuredDimension((int) w, (int) ((this.linesCollection.size() + 1) * lineHeight + (10 * onBirim)));
        }

        @Override
        protected void onDraw(Canvas canvas) {
                super.onDraw(canvas);
                canvas.drawLine(0f, 10f, getMeasuredWidth(), 10f, textPaint);
                float x, y = lineHeight + onBirim;
                for (Line line : linesCollection) {
                        x = leftPadding;
                        for (String s : line.getWords()) {
                                canvas.drawText(s, x, y, textPaint);
                                x += textPaint.measureText(s) + line.spacing;
                        }
                        y += lineHeight;
                }
        }

        public String getText() {
                return text;
        }

        public void setText(String text) {
                this.text = text;
        }

        public Typeface getFont() {
                return font;
        }

        public void setFont(Typeface font) {
                this.font = font;
        }

        public float getLineHeight() {
                return lineHeight;
        }

        public void setLineHeight(float lineHeight) {
                this.lineHeight = lineHeight;
        }

        public float getLeftPadding() {
                return leftPadding;
        }

        public void setLeftPadding(float leftPadding) {
                this.leftPadding = leftPadding;
        }

        public float getRightPadding() {
                return rightPadding;
        }

        public void setRightPadding(float rightPadding) {
                this.rightPadding = rightPadding;
        }

        public void setWordSpacing(float wordSpacing) {
                this.wordSpacing = wordSpacing;
        }

        public float getWordSpacing() {
                return wordSpacing;
        }

        public float getLineSpacing() {
                return lineSpacing;
        }

        public void setLineSpacing(float lineSpacing) {
                this.lineSpacing = lineSpacing;
        }

        class Line {
                ArrayList<String> words = new ArrayList<String>();
                float spacing = 15f;

                public Line() {
                }

                public Line(ArrayList<String> words, float spacing) {
                        this.words = words;
                        this.spacing = spacing;
                }

                public void setSpacing(float spacing) {
                        this.spacing = spacing;
                }

                public float getSpacing() {
                        return spacing;
                }

                public void addWord(String s) {
                        words.add(s);
                }

                public ArrayList<String> getWords() {
                        return words;
                }
        }
}
2
votes

This is the same JustifiedTextView class given by Juan (and edited by me), but extended to work with custom xml attributes you can use in your layout xml files. Even Eclipse layout editor will show your custom attributes in the attribute table, which is cool. I put this into an additional answer, in case you want to keep things clean and don't need xml attributes.

public class JustifiedTextView extends WebView{
    private String core      = "<html><body style='text-align:justify;color:rgba(%s);font-size:%dpx;margin: 0px 0px 0px 0px;'>%s</body></html>";
    private String text;
    private int textColor;
    private int backgroundColor;
    private int textSize;

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

    public JustifiedTextView(Context context, AttributeSet attrs, int i) {
        super(context, attrs, i);
        init(attrs);
    }

    @SuppressLint("NewApi")
    public JustifiedTextView(Context context, AttributeSet attrs, int i, boolean b) {
        super(context, attrs, i, b);
        init(attrs);
    }

    private void init(AttributeSet attrs) { 
        TypedArray a=getContext().obtainStyledAttributes(
                 attrs,
                 R.styleable.JustifiedTextView);

        text = a.getString(R.styleable.JustifiedTextView_text);
        if(text==null) text="";
        textColor = a.getColor(R.styleable.JustifiedTextView_textColor, Color.BLACK);
        backgroundColor = a.getColor(R.styleable.JustifiedTextView_backgroundColor, Color.TRANSPARENT);
        textSize = a.getInt(R.styleable.JustifiedTextView_textSize, 12);

        a.recycle();

        this.setWebChromeClient(new WebChromeClient(){});
        reloadData();
    }

    public void setText(String s){
        if(s==null)
            this.text="";
        else
            this.text = s;
        reloadData();
    }

    @SuppressLint("NewApi")
    private void reloadData(){

        if(text!=null) {
            String data = String.format(core,toRgba(textColor),textSize,text);
            Log.d("test", data);
            this.loadDataWithBaseURL(null, data, "text/html","utf-8", null);
        }

        // set WebView's background color *after* data was loaded.
        super.setBackgroundColor(backgroundColor);

        // Hardware rendering breaks background color to work as expected.
        // Need to use software renderer in that case.
        if(android.os.Build.VERSION.SDK_INT >= 11)
            this.setLayerType(WebView.LAYER_TYPE_SOFTWARE, null);
    }

    public void setTextColor(int hex){
        textColor = hex; 
        reloadData();
    }

    public void setBackgroundColor(int hex){
        backgroundColor = hex;
        reloadData();
    }

    public void setTextSize(int textSize){
        this.textSize = textSize;
        reloadData();
    }

    private String toRgba(int hex) {
        String h = Integer.toHexString(hex);
        int a = Integer.parseInt(h.substring(0, 2),16);
        int r = Integer.parseInt(h.substring(2, 4),16);
        int g = Integer.parseInt(h.substring(4, 6),16);
        int b = Integer.parseInt(h.substring(6, 8),16);
        return String.format("%d,%d,%d,%d", r, g, b, a); 
    }
}

Either add this as justified_text_view_attr.xml into your res/values/ folder, or merge it into your existing attrs.xml:

<?xml version="1.0" encoding="utf-8"?>                                                                               
<resources>                                                                                                          
    <declare-styleable name="JustifiedTextView">                                                                     
        <attr name="text" format="string" localization="suggested"/>                                                 
        <attr name="textColor" format="color|reference" />                                                           
        <attr name="backgroundColor" format="color|reference" />                                                     
        <attr name="textSize" format="integer" min="1" />                                                            
    </declare-styleable>
</resources>                         

Feel free to edit if you find any bugs.

1
votes

I believe this simplest form. And I worked perfectly

package domo.suichbt.util;

import android.content.Context;
import android.text.Html;
import android.util.AttributeSet;
import android.widget.TextView;

public class JustifiedTextView extends TextView
{
    private final String CORE_TEMPLATE = "<html><body style='text-

align:justify;margin: 0px 0px 0px 0px;'>%s</body></html>";  

public JustifiedTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
    setText(Html.fromHtml(String.format(CORE_TEMPLATE,getText())));
}

public JustifiedTextView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    setText(Html.fromHtml(String.format(CORE_TEMPLATE,getText())));
}

public JustifiedTextView(Context context) {
    super(context);
    setText(Html.fromHtml(String.format(CORE_TEMPLATE,getText())));
}

public JustifiedTextView(Context context, AttributeSet attrs) {
    super(context, attrs);
    setText(Html.fromHtml(String.format(CORE_TEMPLATE,getText())));
}
}

Insert xml example

    <domo.suichbt.util.JustifiedTextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@string/sw_titulo"
    android:singleLine="false">
</domo.suichbt.util.JustifiedTextView>
0
votes

Take a look at this link. It uses a WebView to be bale to fully justify the text of a CheckBox in Android. It also can be used exactly the same in TextView, since each CheckBox is in fact a TextView and a Button. http://www.collegemobile.com/2014/09/justify-text-android-checkbox/