How to display horizontal lines between the text of html content on android single Textview?

925 Views Asked by At

I have some HTML content with something like

<html>
<h3>nested unordered lists</h3>
<ul>
  <li>
     first level
     <ul>
        <li>
           second level
           <ul>
              <li>
                 third level
                 <ul>
                    <li>
                       fourth level
                       <ul>
                          <li>fifth level</li>
                       </ul>
                    </li>
                    <li>fourth level</li>
                 </ul>
              </li>
              <li>third level</li>
           </ul>
        </li>
        <li>second level</li>
        <li>second level: this should be a long enough text that will be wrapped into multiple lines</li>
     </ul>
  </li>
  <li>first level</li>
</ul> 

<hr>
<h3>nested ordered lists</h3>
<ol>
  <li>
     first level
     <ol>
        <li>
           second level
           <ol>
              <li>
                 third level
                 <ol>
                    <li>
                       fourth level
                       <ol>
                          <li>fifth level</li>
                       </ol>
                    </li>
                    <li>fourth level</li>
                 </ol>
              </li>
              <li>third level</li>
           </ol>
        </li>
        <li>second level</li>
        <li>second level: this should be a long enough text that will be wrapped into multiple lines</li>
     </ol>
  </li>
  <li>first level</li>
</ol>

<hr>
<h3>Mixed (ol and ul) nested lists:</h3>
<ul>
  <li>
     first unordered
     <ol>
        <li>first ordered</li>
        <li>
           second ordered
           <ul>
              <li>
                 unordered in second ordered
                 <ol>
                    <li>ordered in "unordered in second ordered"</li>
                    <li>another ordered in ""unordered in second ordered"</li>
                 </ol>
              </li>
           </ul>
        </li>
        <li>third ordered with some other formatting: <b>bold</b> and <i>italics</i></li>
     </ol>
  </li>
  <li>second unordered</li>
</ul>
</html>

Now I am trying to display HTML content on single Textview. Code snippet is given below

textView.setText(Html.fromHtml(htmlContent, null, htmlTagHandler));

Here

  • htmlTagHandler is HtmlTagHandler reference which is used to support <ul>, <ol>, <li> tags.

HtmlTagHandler.java:

    public class HtmlTagHandler implements Html.TagHandler {
    /**
    * Keeps track of lists (ol, ul). On bottom of Stack is the outermost list
    * and on top of Stack is the most nested list
    */
    Stack<String> lists = new Stack<String>();
    /**
    * Tracks indexes of ordered lists so that after a nested list ends
    * we can continue with correct index of outer list
    */
    Stack<Integer> olNextIndex = new Stack<Integer>();
    /**
    * List indentation in pixels. Nested lists use multiple of this.
    */
    private static final int indent = 10;
    private static final int listItemIndent = indent * 2;
    private static final BulletSpan bullet = new BulletSpan(indent);

    @Override
    public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) {
    if (tag.equalsIgnoreCase("ul")) {
        if (opening) {
            lists.push(tag);
        } else {
            lists.pop();
        }
    } else if (tag.equalsIgnoreCase("ol")) {
        if (opening) {
            lists.push(tag);
            olNextIndex.push(Integer.valueOf(1)).toString();//TODO: add support for lists starting other index than 1
        } else {
            lists.pop();
            olNextIndex.pop().toString();
        }
    } else if (tag.equalsIgnoreCase("li")) {
        if (opening) {
            if (output.length() > 0 && output.charAt(output.length() - 1) != '\n') {
                output.append("\n");
            }
            String parentList = lists.peek();
            if (parentList.equalsIgnoreCase("ol")) {
                start(output, new Ol());
                output.append(olNextIndex.peek().toString() + ". ");
                olNextIndex.push(Integer.valueOf(olNextIndex.pop().intValue() + 1));
            } else if (parentList.equalsIgnoreCase("ul")) {
                start(output, new Ul());
            }
        } else {
            if (lists.peek().equalsIgnoreCase("ul")) {
                if ( output.length() > 0 && output.charAt(output.length() - 1) != '\n' ) {
                    output.append("\n");
                }
                // Nested BulletSpans increases distance between bullet and text, so we must prevent it.
                int bulletMargin = indent;
                if (lists.size() > 1) {
                    bulletMargin = indent-bullet.getLeadingMargin(true);
                    if (lists.size() > 2) {
                        // This get's more complicated when we add a LeadingMarginSpan into the same line:
                        // we have also counter it's effect to BulletSpan
                        bulletMargin -= (lists.size() - 2) * listItemIndent;
                    }
                }
                BulletSpan newBullet = new BulletSpan(bulletMargin);
                end(output,
                    Ul.class,
                    new LeadingMarginSpan.Standard(listItemIndent * (lists.size() - 1)),
                    newBullet);
            } else if (lists.peek().equalsIgnoreCase("ol")) {
                if ( output.length() > 0 && output.charAt(output.length() - 1) != '\n' ) {
                    output.append("\n");
                }
                int numberMargin = listItemIndent * (lists.size() - 1);
                if (lists.size() > 2) {
                    // Same as in ordered lists: counter the effect of nested Spans
                    numberMargin -= (lists.size() - 2) * listItemIndent;
                }
                end(output,
                    Ol.class,
                    new LeadingMarginSpan.Standard(numberMargin));
            }
        }
    } else {
        if (opening) Log.d("TagHandler", "Found an unsupported tag " + tag);
      }
    }


    private static void start(Editable text, Object mark) {
    int len = text.length();
    text.setSpan(mark, len, len, Spanned.SPAN_MARK_MARK);
    }

    private static void end(Editable text, Class<?> kind, Object... replaces) {
    int len = text.length();
    Object obj = getLast(text, kind);
    int where = text.getSpanStart(obj);
    text.removeSpan(obj);
    if (where != len) {
        for (Object replace: replaces) {
            text.setSpan(replace, where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
         }
       }
      return;
     }


    private static Object getLast(Spanned text, Class<?> kind) {
    /*
     * This knows that the last returned object from getSpans()
     * will be the most recently added.
     */
       Object[] objs = text.getSpans(0, text.length(), kind);
       if (objs.length == 0) {
               return null;
        }
       return objs[objs.length - 1];
      }

     private static class Ul { }
     private static class Ol { }
    }

Here I am able to display bullets and numbers by using Html.fromHtml(htmlContent, htmlImageGet, htmlTagHandler) but not horizontal lines. Can anyone guide me how to display horizontal line or supporting <hr> tag in Html.fromHtml() method by the help of HtmlTagHandler or else any other approach.

Expected output like below screen shot.

enter image description here

Everything is working fine expect that horizontal lines. Can anybody help on this issue.

1

There are 1 best solutions below

0
On

This is how I've solved your question. It's not a very elegant solution, but it works.

Html.TagHandler:

import org.xml.sax.XMLReader;
import android.text.Editable;
import android.text.Html;
import android.text.Spannable;
import android.text.Spanned;
import android.util.Log;

public class CustomTagHandler implements Html.TagHandler {

    @Override
    public void handleTag(final boolean opening, final String tag,
                          Editable output, final XMLReader xmlReader) {
        if(tag.equals("hr")) {
            handleHRTag(opening,output);
        }
    }

    private void handleHRTag(boolean opening, Editable output) {
        if(!opening) return;
        int start = output.length();
        // The space is necessary, the block requires some content:
        output.append(" \n"); 
        output.setSpan(new CustomHRSpan(0xff000000,5.0f,2.0f),
                       start, output.length(), 0);
    }
}

Then create a CustomHRSpan class:

import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.text.style.LineBackgroundSpan;
import android.text.style.LineHeightSpan;


public class CustomHRSpan implements LineBackgroundSpan, LineHeightSpan {
    private final int color;          // Color of line
    private final float height;       // Height of HR element
    private final float line;         // Line size
    private final float marginLeft;   // Margin of line, left side
    private final float marginRight;  // Margin of line, right side

    public CustomHRSpan(int color, float height, float line) {
        this.color        = color;
        this.height       = height;
        this.line         = line;
        this.marginLeft   = 5.0f;
        this.marginRight  = 5.0f;
    }

    @Override
    public void drawBackground(Canvas c, Paint p, int left, int right, int top, int baseline, int bottom,
                               CharSequence text, int start, int end, int lnum) {
        int paintColor = p.getColor();
        float y = (float)(top+(bottom-top)/2) - line*0.5f;
        RectF r = new RectF((float)left+marginLeft, y,
                            (float)(right-left)-marginRight, y+line);
        p.setColor(color);
        c.drawRect(r, p);
        p.setColor(paintColor);
    }

    @Override
    public void chooseHeight(CharSequence text, int start, int end, int spanstartv, int v, Paint.FontMetricsInt fm) {
        fm.descent  = (int)height / 2;
        fm.ascent   = (int)height - fm.descent;
        fm.leading  = 0;
        fm.top      = fm.ascent;
        fm.bottom   = fm.descent;
    }
}