HTML列表标记在android textview中不起作用。我能做什么?


99

HTML列表标记在android TextView中不起作用。这是我的字符串内容:

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>";

我将其加载到这样的文本视图中:

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

输出看起来像一个段落。我能做什么?有什么解决方案吗?

编辑:

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

1
应该是text / html而不是texl / html。
克洛伊(Chloe)

Answers:


156

如您在Html类源代码中所看到的,Html.fromHtml(String)不支持所有HTML标记。在这种情况下,非常,<ul>并且<li>不被支持。

从源代码中,我建立了允许的HTML标签列表:

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

因此,您最好使用WebViewloadDataWithBaseURL方法。尝试这样的事情:

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);

那我该怎么纠正呢?
Praveen 2010年

2
非常重要的一点是,此“允许”标签的某些属性也不受支持。:=(
Jorgesys

2
冷静一下...我编辑了答案,请告诉我它是否有效。
克里斯蒂安(Cristian)

6
您实际上不能以相同的方式使用WebView,因此这实际上不是解决问题的方法。
Brill Pappin

11
那怎么解决?您不能只使用WebView,而与TextView相比,它是一个非常昂贵的小部件。您不能仅对每个具有格式的文本使用WebView。
SpaceMonkey

135

我有同样的问题,我所做的是覆盖默认的TagHandler。这个为我工作。

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;
                }
            }   
        }
    }
}

并显示文字...

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

[编辑]

Kuitsi还发布了一个非常好的库,可以通过此SO链接进行操作


最后,我们使用了这种方法。任何不受支持的HTML标记,我们都会自行编写文本代码。目前,它仅是ol和ul,但是我们在堆栈中添加了处理列表嵌套和在嵌套ol时存储索引的堆栈。另外,您可以使用开幕布尔参数代替first。
JonWillis 2012年

6
@Aman Gautam非常感谢!您是否知道在多行换行时如何制表符?在第二行之后的这段代码中,文本与数字对齐,而不是在选项卡中将数字分开。我尝试了一些尝试,但无法弄清楚
RyanG

同样,列表中的换行符会导致这种方法引起麻烦。
Andreas Rudolph

与其使用粘贴的项目符号,不如使用unicode字符:output.append(“ \ n \ t \ u2022”);
马特·麦克明

感谢您提供的
出色

68

完整的示例项目位于https://bitbucket.org/Kuitsi/android-textview-html-list
示例图片可从https://kuitsi.bitbucket.io/stackoverflow3150400_screen.png获得

这个解决方案最接近玛莎的答案。一些代码也来自内部类android.text.Html.HtmlToSpannedConverter。它支持嵌套的有序列表和无序列表,但是有序列表中的文本过长仍与项目编号而不是文本对齐。混合列表(ol和ul)也需要一些工作。示例项目包含Html.TagHandler的实现,该实现传递给Html.fromHtml(String,ImageGetter,TagHandler)

编辑:对于更广泛的HTML标记支持,https://github.com/NightWhistler/HtmlSpanner也许也值得尝试。


迄今为止最好的解决方案。谢谢
peter.bartos 2013年

BitBucket存储库中没有跟踪问题,因此在此处发布:您需要在此处此处添加检查,output.length() > 0例如if (output.length() > 0 && output.charAt(output.length() - 1) != '\n')
mindeh 2015年

2
为了避免其他人在此上浪费2个小时,NightWhistler HtmlSpanner会出于未知原因删除所有带重音符号的字符。
EpicPandaForce's

@Kuitsi感谢您的解决方案。有一个问题,当html文本为“ <ul> <li>某物</ li> </ ul>”时,列表中不显示“某物”中的最后一个字母。
山姆·伯格

这是一个非常好的解决方案,但是有两个缺点:1)它不支持Android≥7,并且2)它不为列表的第一级添加起始缩进。
soshial

24

Aman Guatam代码的一个小修正。上面的函数存在渲染换行符的问题。例如:如果before <li>标签是<p>标签,则呈现2个换行符。这是升级的代码:

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;
            }
        }
    }
}

简单而有效
steven0529

那有序列表呢?
Android开发者

13

警告

截至2016年2月10日,android.text.Html实际上支持liul标记并使用了basic new BulletSpan(),这意味着在最新版本的Android中,Html.TagHandler此处发布的解决方案将被忽略

确保您的代码能够处理此更改,以防您期望BulletSpan的缺口大于默认缺口,您将需要某种解决方案来查找/替换跨度


4
但是,新的Html类仅在Android N及更高版本中可用。
Sakiboy '17

1
是的-因此您需要考虑以下事实:不同的OS版本的行为会有所不同。因此,为什么我推荐一个在HTML解析为不同范围后查找并替换BulletSpan的解决方案。N之后的版本的默认实现将使用默认边距,您可以找到它们并将其替换为所需的边距。
卡西姆(Kassim)

始终保持最新。
Kai Wang

9

使用LeadingMarginSpan的不同解决方案。处理有序列表和无序列表以及嵌套。

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 );
        }
    }
}

5
我喜欢使用Spans的想法,但无法使用此代码来嵌套列表。这两行都output.setSpan(...)java.lang.RuntimeException: PARAGRAPH span must start at paragraph boundary
Kuitsi

感谢您的好解决方案!它还缩进了多个行文本
peter.bartos

2
为什么使用Vector而不是简单的ArrayList?一个Vector用于多线程...
android开发人员

@androiddeveloper c ++程序员,我很糟糕,随时可以编辑答案
masha

1
我已经张贴片段androidsnippets.com/...
PRATIK Butani

8

如果只需要格式化列表,则使其简单并在TextView中复制/粘贴Unicode字符即可达到相同的结果。

•Unicode字符“子弹”(U + 2022)


6

我是来这里寻找TagHandler实现的。Truong Nguyen和Aman Guatam的答案都非常好,但是我需要两者的混合版本:我需要我的解决方案不要对其进行过度格式化并能够解析<ol>标签,因为我正在解析<h3>title</h3><ol><li>item</li><li>item</li><li>item</li></ol>

这是我的解决方案。

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;
                }
            }
        }
    }
}

请注意,由于每当新列表开始时我们都会重置索引值,因此,如果您嵌套列表,例如 <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

使用该代码,您将获得1, 1, 2, 3而不是1, 1, 2, 2


该代码在版本23之前有效。如何使其在24及以上版本中起作用?
Abhinav Tyagi

3

当然,有一种方法可以在Android TextView中显示项目符号。您可以使用替换<li>标签&#149;(这是项目符号的HTML代码)。

如果要尝试其他列表图标,请使用表中的首选图标。

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


没为我工作。而是在Android 7.1.1和6.0.1上,出现一个带有x的框,而不是TextView中的项目符号。
user1652110

3

您可以简单地用unicode替换“ li”

    @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");
        }
    }
}

2

沃尔德莫勋爵的回答是一个很好的起点。但是我需要ol标签显示有序列表1. 2. 3. ....而不是项目符号。同样,嵌套标签需要特殊处理才能正常工作。

在我的代码中,我维护了stack(parentList)来跟踪打开和关闭ul以及ol标签,并了解当前的打开标签。同样,levelWiseCounter在嵌套ol标签的情况下,a 用于维护不同的计数。

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
      }
   }

2

下一个代码(基于此链接)如何:

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
    {
    }
  }

1
与原始答案的格式相比,该答案的格式仅稍有不同,原始格式的创建目的是为了支持对同一问题的另一个答案:stackoverflow.com/a/17365740/262462 :)
Kuitsi 2014年

真正。没注意到。
Android开发人员

2

我遇到的问题是,在使用@Kuitsis解决方案的列表之后,我总是空行。我在handleTag()中添加了几行,现在空行消失了:

@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);
    }
}

2

您可以使用Html.TagHandler。以下可用于科特林

    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•")
    }
}

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

0

这是对卡西姆所说的证实。有碎片。我找到了解决方法。我必须重命名<li>并ul到自定义标签。所以:

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

然后在我的处理程序中,我可以查找该customTag(不执行任何操作)并使它执行某些操作。

//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");
        }
    }

这应该使其适用于所有版本的android。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.