Html List tag not working in android textview. What can I do?

Android

Android Problem Overview


Html List tag not working in android TextView. This is my string content:

String str="A dressy take on classic gingham in a soft, textured weave of stripes that resembles twill.  Take a closer look at this one.<ul><li>Trim, tailored fit for a bespoke feel</li><li>Medium spread collar, one-button mitered barrel cuffs</li><li>Applied placket with genuine mother-of-pearl buttons</li><li>;Split back yoke, rear side pleats</li><li>Made in the U.S.A. of 100% imported cotton.</li></ul>";

I loaded it in a text view like this:

textview.setText(Html.fromHtml(str));

The output looks like a paragraph. What can I do? Is there any solution for it?

Edit:

webview.loadData(str,"text/html","utf-8");

Android Solutions


Solution 1 - Android

As you can see in the Html class source code, Html.fromHtml(String) does not support all HTML tags. In this very case, <ul> and <li> are not supported.

From the source code I have built a list of allowed HTML tags:

  • br
  • p
  • div
  • em
  • b
  • strong
  • cite
  • dfn
  • i
  • big
  • small
  • font
  • blockquote
  • tt
  • monospace
  • a
  • u
  • sup
  • sub

So you better use WebView and its loadDataWithBaseURL method. Try something like this:

String str="<html><body>A dressy take on classic gingham in a soft, textured weave of stripes that resembles twill.  Take a closer look at this one.<ul><li>Trim, tailored fit for a bespoke feel</li><li>Medium spread collar, one-button mitered barrel cuffs</li><li>Applied placket with genuine mother-of-pearl buttons</li><li>;Split back yoke, rear side pleats</li><li>Made in the U.S.A. of 100% imported cotton.</li></ul></body></html>";
webView.loadDataWithBaseURL(null, str, "text/html", "utf-8", null);

Solution 2 - Android

I was having the same problem, and what I did is overriding the default TagHandler. This one worked for me.

public class MyTagHandler implements TagHandler {

	boolean first = true;
	String parent = null;
	int index = 1;
	@Override
	public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) {
		
		if (tag.equals("ul")) {
            parent = "ul";
        } else if (tag.equals("ol")) {
            parent = "ol";
        }

		if (tag.equals("li")) {
			if (parent.equals("ul")) {
				if (first) {
					output.append("\n\t•");
					first = false;
				} else {
					first = true;
				}
			} else{
				if (first) {
					output.append("\n\t"+index+". ");
					first = false;
					index++;
				} else {
					first = true;
				}
			}	
		}
	}
}

and for displaying the text...

myTextView.setText(Html.fromHtml("<ul><li>I am an Android developer</li><li>Another Item</li></ul>", null, new MyTagHandler()));

[Edit]

Kuitsi has also posted an really good library that does the same, got it from this SO link.

Solution 3 - Android

Full sample project is located at https://bitbucket.org/Kuitsi/android-textview-html-list.
Sample picture is available at https://kuitsi.bitbucket.io/stackoverflow3150400_screen.png

This solution is closest to masha's answer. Some code is also taken from inner class android.text.Html.HtmlToSpannedConverter. It supports nested ordered and unordered lists but too long texts in ordered lists are still aligned with item number rather than text. Mixed lists (ol and ul) needs some work too. Sample project contains implementation of Html.TagHandler which is passed to Html.fromHtml(String, ImageGetter, TagHandler).

Edit: For wider HTML tag support, https://github.com/NightWhistler/HtmlSpanner might also be worth trying.

Solution 4 - Android

A small fix to Aman Guatam code. The function above has problem of rendering newline character. For example: if before <li> tag is a <p> tag, 2 newline characters are rendered. Here is upgraded code:

import org.xml.sax.XMLReader;

import android.text.Editable;
import android.text.Html.TagHandler;

public class ListTagHandler implements TagHandler {
	boolean first = true;

	@Override
	public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) {

		// TODO Auto-generated method stub
		if (tag.equals("li")) {
			char lastChar = 0;
			if (output.length() > 0)
				lastChar = output.charAt(output.length() - 1);
			if (first) {
				if (lastChar == '\n')
					output.append("\t•  ");
				else
					output.append("\n\t•  ");
				first = false;
			} else {
				first = true;
			}
		}
	}
}

Solution 5 - Android

WARNING

As of Android 7 android.text.Html actually supports li and ul tags and uses a basic BulletSpan(), which means in the latest versions of Android the Html.TagHandlersolutions posted here will be ignored

Make sure your code handles this change. In case you want a BulletSpan with a larger gap than the default, you can can replace it with another span:

val html = SpannableStringBuilder(HtmlCompat.fromHtml(source, HtmlCompat.FROM_HTML_MODE_COMPACT))
val bulletSpans = html.getSpans<BulletSpan>(0, html.length)
bulletSpans.forEach {
    val spanStart = html.getSpanStart(it)
    val spanEnd = html.getSpanEnd(it)
    html.removeSpan(it)
    val bulletSpan = BulletSpan(gapWidthInDp, context.getColor(R.color.textColorBlack))
    html.setSpan(bulletSpan, spanStart, spanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}

Solution 6 - Android

Different solution using LeadingMarginSpan. Handles ordered and unordered lists as well as nesting.

public class ListTagHandler implements TagHandler
{
	private int					m_index		= 0;
	private List< String >	m_parents	= new ArrayList< String >( );

	@Override
	public void handleTag( final boolean opening, final String tag, Editable output,    final XMLReader xmlReader )
	{
		if( tag.equals( "ul" ) || tag.equals( "ol" ) || tag.equals( "dd" ) )
		{
			if( opening )
			{
				m_parents.add( tag );
    		}
			else m_parents.remove( tag );

			m_index = 0;
    	}
	    else if( tag.equals( "li" ) && !opening ) handleListTag( output );
    }

    private void handleListTag( Editable output )
    {
	    if( m_parents.get(m_parents.size()-1 ).equals( "ul" ) )
	    {
	    	output.append( "\n" );
	    	String[ ] split = output.toString( ).split( "\n" );

	    	int lastIndex = split.length - 1;
	       	int start = output.length( ) - split[ lastIndex ].length( ) - 1;
	       	output.setSpan( new BulletSpan( 15 * m_parents.size( ) ), start, output.length( ), 0 );
        }
    	else if( m_parents.get(m_parents.size()-1).equals( "ol" ) )
    	{
    		m_index++ ;

	    	output.append( "\n" );
	    	String[ ] split = output.toString( ).split( "\n" );

	    	int lastIndex = split.length - 1;
	    	int start = output.length( ) - split[ lastIndex ].length( ) - 1;
	    	output.insert( start, m_index + ". " );
	    	output.setSpan( new LeadingMarginSpan.Standard( 15 * m_parents.size( ) ), start, output.length( ), 0 );
    	}
    }
}

Solution 7 - Android

If you only need to format a list, keep it simple and copy/paste a unicode character in your TextView to achieve the same result.

• Unicode Character 'BULLET' (U+2022)

Solution 8 - Android

I came here looking for TagHandler implementations. Both Truong Nguyen and Aman Guatam answers are very nice, but I needed a mixed version of both: I needed my solution not to overformat it and to be able to ressolve <ol> tags, since I'm parsing something like <h3>title</h3><ol><li>item</li><li>item</li><li>item</li></ol>.

Here's my solution.

import org.xml.sax.XMLReader;

import android.text.Editable;
import android.text.Html.TagHandler;

public class MyTagHandler implements TagHandler {
	boolean first = true;
	String parent = null;
	int index = 1;

	public void handleTag(final boolean opening, final String tag,
			final Editable output, final XMLReader xmlReader) {

		if (tag.equals("ul")) {
			parent = "ul";
                    index = 1;
		} else if (tag.equals("ol")) {
			parent = "ol";
                    index = 1;
		}
		if (tag.equals("li")) {
			char lastChar = 0;
			if (output.length() > 0) {
				lastChar = output.charAt(output.length() - 1);
			}
			if (parent.equals("ul")) {
				if (first) {
					if (lastChar == '\n') {
						output.append("\t•  ");
					} else {
						output.append("\n\t•  ");
					}
					first = false;
				} else {
					first = true;
				}
			} else {
				if (first) {
					if (lastChar == '\n') {
						output.append("\t" + index + ". ");
					} else {
						output.append("\n\t" + index + ". ");
					}
					first = false;
					index++;
				} else {
					first = true;
				}
			}
		}
	}
}

Note that, since we are resetting the index value whenever a new list starts, it WILL NOT work if you nest lists like in <ol><li>1<ol><li>1.1</li><li>1.2</li></ol><li>2</li></ol>

  1. 1
    1. 1.1
    2. 1.2
  2. 2

With that code, you would get 1, 1, 2, 3 instead of 1, 1, 2, 2.

Solution 9 - Android

You can simply replace the "li" with unicodes

    @Override
public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) {

    if (tag.equalsIgnoreCase("li")) {
        if (opening) {
            output.append("\u2022 ");
        } else {
            output.append("\n");
        }
    }
}

Solution 10 - Android

Sure, there ise a way of showing bullets in Android TextView. You can replace <li> tags with &#149; (which is HTML code for bullet).

If you want to try other list icons, use the preferred one from the table is this link;

http://www.ascii-code.com/

Solution 11 - Android

Lord Voldermort's answer is a good starting point. However I required ol tag to display ordered list 1. 2. 3. .... instead of bullets. Also, nested tags need special handling to work properly.

In my code, I have maintained stack(parentList) to keep track of opened and closed ul and ol tags and also to know the current open tag. Also, a levelWiseCounter is used to maintain different counts in case of nested ol tags.

myTextView.setText(Html.fromHtml("your string", null, new CustomTagHandler()));

. . .

private static class CustomTagHandler implements TagHandler
   {
      int level = 0;
      private LinkedList<Tag> parentList = new LinkedList<DetailFragment.CustomTagHandler.Tag>();
      private HashMap<Integer, Integer> levelWiseCounter = new HashMap<Integer, Integer>();

      @Override
      public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader)
      {
         if (tag.equalsIgnoreCase("ul") || tag.equalsIgnoreCase("ol"))
         {
            if (opening)
            {
               if (tag.equalsIgnoreCase("ul"))
               {
                  parentList.push(Tag.UL);
               }
               else
               {
                  parentList.push(Tag.OL);
               }
               level++;
            }
            else
            {
               if (!parentList.isEmpty())
               {
                  parentList.pop();

                  //remove counter at that level, in any present.
                  levelWiseCounter.remove(level);
               }
               level--;
               if (level < 0)
               {
                  level = 0;
               }
            }
         }
         else if (tag.equalsIgnoreCase("li"))
         {
            if (opening && level > 0)
            {
               //new line check
               int length = output.toString().length();
               if (length > 0 && (output.toString().charAt(length - 1) == '\n'))
               {
               }
               else
               {
                  output.append("\n");
               }

               //add tabs as per current level of li
               for (int i = 0; i < level; i++)
               {
                  output.append("\t");
               }

               // append dot or numbers based on parent tag
               if (Tag.UL == parentList.peek())
               {
                  output.append("•");
               }
               else
               {
                  //parent is OL. Check current level and retreive counter from levelWiseCounter
                  int counter = 1;
                  if (levelWiseCounter.get(level) == null)
                  {
                     levelWiseCounter.put(level, 1);
                  }
                  else
                  {
                     counter = levelWiseCounter.get(level) + 1;
                     levelWiseCounter.put(level, counter);
                  }
                  output.append(padInt(counter) + ".");
               }

               //trailing tab
               output.append("\t");

            }
         }
      }

      /**
       * Add padding so that all numbers are aligned properly. Currently supports padding from 1-99.
       * 
       * @param num
       * @return
       */
      private static String padInt(int num)
      {
         if (num < 10)
         {
            return " " + num;
         }
         return "" + num;
      }

      private enum Tag
      {
         UL, OL
      }
   }

Solution 12 - Android

How about the next code (based on this link) :

public class TextViewHtmlTagHandler implements 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(final boolean opening,final String tag,final Editable output,final 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");
        final 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.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;
          }
        final 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.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(final Editable text,final Object mark)
    {
    final int len=text.length();
    text.setSpan(mark,len,len,Spanned.SPAN_MARK_MARK);
    }

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

  private static Object getLast(final Spanned text,final Class<?> kind)
    {
    /*
     * This knows that the last returned object from getSpans()
     * will be the most recently added.
     */
    final 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
    {
    }
  }

Solution 13 - Android

I had the problem, that I always got an empty line after a list with @Kuitsis solution. I added a few lines in handleTag() and now the empty lines are gone:

@Override
public void handleTag(final boolean opening, final String tag, final Editable output, final XMLReader xmlReader) {
	if (UL_TAG.equalsIgnoreCase(tag)) {
		if (opening) {   // handle <ul>
			lists.push(new Ul());
		} else {   // handle </ul>
			lists.pop();
			if (output.length() > 0 && output.charAt(output.length() - 1) == '\n') {
				output.delete(output.length() - 1, output.length());
			}
		}
	} else if (OL_TAG.equalsIgnoreCase(tag)) {
		if (opening) {   // handle <ol>
			lists.push(new Ol()); // use default start index of 1
		} else {   // handle </ol>
			lists.pop();
			if (output.length() > 0 && output.charAt(output.length() - 1) == '\n') {
				output.delete(output.length() - 1, output.length());
			}
		}
	} else if (LI_TAG.equalsIgnoreCase(tag)) {
		if (opening) {   // handle <li>
			lists.peek().openItem(output);
		} else {   // handle </li>
			lists.peek().closeItem(output, lists.size());
		}
	} else {
		Log.d("TagHandler", "Found an unsupported tag " + tag);
	}
}

Solution 14 - Android

You can use Html.TagHandler. Below can be used for kotlin

    class UlTagHandler : Html.TagHandler {
    override fun handleTag(
        opening: Boolean, tag: String, output: Editable,
        xmlReader: XMLReader
    ) {
        if (tag == "ul" && !opening) output.append("\n")
        if (tag == "li" && opening) output.append("\n\t•")
    }
}

and

textView.setText(Html.fromHtml(myHtmlText, null, UlTagHandler()));

Solution 15 - Android

this is a confirmation to what kassim has stated. there is fragmentation. i found how to resolve this. i have to rename <li> and ul to a custom tag. so:

myHTML.replaceAll("</ul>","</customTag>").replaceAll("<ul>","<customTag>");
//likewise for li

then in my handler i can look for that customTag (which does nothing) and make it do something.

//now my handler can handle the customtags. it was ignoring them after nougat. 
 public class UlTagHandler implements Html.TagHandler {
        //for ul in nougat and up this tagHandler is completely ignored
        @Override
        public void handleTag(boolean opening, String tag, Editable output,
                              XMLReader xmlReader) {
      
            if (tag.equals("customtag2") && opening)
            output.append("\n\t\u25CF\t");
        if (tag.equals("customtag2") && !opening)
            output.append("\n");
        }
    }

this should make it work for all versions of android.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionPraveenView Question on Stackoverflow
Solution 1 - AndroidCristianView Answer on Stackoverflow
Solution 2 - AndroidAman GautamView Answer on Stackoverflow
Solution 3 - AndroidKuitsiView Answer on Stackoverflow
Solution 4 - AndroidTruong NguyenView Answer on Stackoverflow
Solution 5 - AndroidkassimView Answer on Stackoverflow
Solution 6 - AndroidmashaView Answer on Stackoverflow
Solution 7 - AndroidNakuView Answer on Stackoverflow
Solution 8 - AndroidCharlie-BlakeView Answer on Stackoverflow
Solution 9 - AndroidsheetalView Answer on Stackoverflow
Solution 10 - AndroidTanerView Answer on Stackoverflow
Solution 11 - AndroidKshitijView Answer on Stackoverflow
Solution 12 - Androidandroid developerView Answer on Stackoverflow
Solution 13 - AndroidJensJensenView Answer on Stackoverflow
Solution 14 - AndroidShalu T DView Answer on Stackoverflow
Solution 15 - Androidj2emanueView Answer on Stackoverflow