La etiqueta de lista HTML no funciona en la vista de texto de Android. ¿que puedo hacer?

99

La etiqueta de lista HTML no funciona en Android TextView. Este es mi contenido de cadena:

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

Lo cargué en una vista de texto como esta:

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

La salida parece un párrafo. ¿Que puedo hacer? ¿Existe alguna solución para ello?

Editar:

webview.loadData(str,"text/html","utf-8");
Praveen
fuente
1
Debe ser texto / html, no texl / html.
Chloe

Respuestas:

156

Como puede ver en el Htmlcódigo fuente de la clase , Html.fromHtml(String)no es compatible con todas las etiquetas HTML. En este mismo caso, <ul>y <li>no son compatibles.

A partir del código fuente, he creado una lista de etiquetas HTML permitidas:

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

Así que es mejor utilizar WebViewy su loadDataWithBaseURLmétodo. Intente algo como esto:

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);
Cristian
fuente
entonces, ¿qué puedo hacer para rectificarlo?
Praveen
2
Es muy importante tener en cuenta que algunos atributos de estas etiquetas "permitidas" tampoco son compatibles. : = (
Jorgesys
2
Cálmate ... Edité mi respuesta, por favor avísame si funciona.
Cristian
6
En realidad, no puede usar WebView de la misma manera, por lo que en realidad no es una solución para el problema.
Brill Pappin
11
¿Cómo es eso una solución? no se puede simplemente usar un WebView, es un widget muy caro en comparación con un TextView. No puede usar WebView para cada texto formateado que tenga.
SpaceMonkey
135

Tengo el mismo problema, lo que hice fue anular el TagHandler predeterminado . Este funcionó para mí.

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

y para mostrar el texto ...

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

[Editar]

Kuitsi también ha publicado una biblioteca realmente buena que hace lo mismo, la obtuve de este enlace SO .

Aman Gautam
fuente
Usamos este enfoque al final. Cualquier etiqueta HTML no compatible, la codificamos nosotros mismos en texto. Por ahora es solo ol y ul, pero agregamos pilas para manejar el anidamiento de listas y almacenar índices al anidar ol. Además, puede utilizar el parámetro booleano de apertura en sustitución de first.
JonWillis
6
@Aman Gautam, ¡gracias por esto! ¿Tiene alguna idea de cómo tabular el texto cuando envuelve más de una línea? Con este código después de la segunda línea, el texto se alinea con el número en lugar de tabularse para mantener el número separado.
Intenté
Lo mismo aquí, los saltos de línea en una lista causan problemas con este enfoque.
Andreas Rudolph
En lugar de usar el carácter de viñeta pegado, puede ser mejor usar el carácter Unicode: output.append ("\ n \ t \ u2022");
Matt McMinn
Gracias por este buen código, pero no puedo usarlo hasta que encontremos una solución para arreglar la sangría de varias líneas
peter.bartos
68

El proyecto de muestra completo se encuentra en https://bitbucket.org/Kuitsi/android-textview-html-list .
La imagen de muestra está disponible en https://kuitsi.bitbucket.io/stackoverflow3150400_screen.png

Esta solución es la más cercana a la respuesta de masha . También se toma parte de código de la clase interna android.text.Html.HtmlToSpannedConverter. Admite listas ordenadas y desordenadas anidadas, pero los textos demasiado largos en listas ordenadas todavía se alinean con el número de artículo en lugar del texto. Las listas mixtas (ol y ul) también necesitan algo de trabajo. El proyecto de muestra contiene la implementación de Html.TagHandler que se pasa a Html.fromHtml (String, ImageGetter, TagHandler) .

Editar: para un soporte más amplio de etiquetas HTML, también puede valer la pena probar https://github.com/NightWhistler/HtmlSpanner .

Kuitsi
fuente
Hasta ahora la mejor solución. Gracias
peter.bartos
No hay seguimiento de problemas en el repositorio de BitBucket, así que publique aquí: debe agregar cheques aquí y aquí para output.length() > 0como enif (output.length() > 0 && output.charAt(output.length() - 1) != '\n')
mindeh
2
Solo para evitar que otras personas pierdan 2 horas en esto, NightWhistler HtmlSpanner elimina todos los caracteres acentuados por cualquier motivo desconocido.
EpicPandaForce
@Kuitsi gracias por la solución. Hay un problema con esto, cuando el texto html es "<ul> <li> algo </li> </ul>", entonces la última letra de "algo" no se muestra en la lista.
Sam Berg
Esta es una muy buena solución, PERO dos inconvenientes: 1) no es compatible con Android ≥ 7 y 2) no pone una sangría inicial para el primer nivel de la lista.
soshial
24

Una pequeña corrección para el código de Aman Guatam. La función anterior tiene el problema de representar el carácter de nueva línea. Por ejemplo: si antes de la <li>etiqueta es una <p>etiqueta, se representan 2 caracteres de nueva línea. Aquí está el código actualizado:

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;
            }
        }
    }
}
Truong Nguyen
fuente
Simple pero efectivo
steven0529
¿Qué pasa con la lista ordenada?
desarrollador de Android
13

ADVERTENCIA

a partir del 10 de febrero de 2016, en android.text.Htmlrealidad admite liy uletiqueta y usa un básico new BulletSpan(), lo que significa que en las últimas versiones de Android las Html.TagHandlersoluciones publicadas aquí serán ignoradas

asegúrese de que su código maneje este cambio en caso de que esté esperando un BulletSpan con un espacio mayor que el predeterminado, tendrá que tener algún tipo de solución que busque / reemplace los tramos

kassim
fuente
4
Pero esa nueva clase Html solo está disponible en Android Ny versiones posteriores.
Sakiboy
1
Sí, debe tener en cuenta el hecho de que las diferentes versiones del sistema operativo se comportarán de manera diferente. Por eso recomiendo una solución que encuentre y reemplace BulletSpan después de que el HTML se haya analizado en diferentes intervalos. La implementación predeterminada de las versiones posteriores a la N utilizará un margen predeterminado, puede buscarlos y reemplazarlos con el margen que desee.
kassim
Manténgase siempre actualizado.
Kai Wang
9

Solución diferente usando LeadingMarginSpan. Maneja listas ordenadas y desordenadas, así como anidaciones.

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 );
        }
    }
}
masha
fuente
5
Me gusta la idea de usar Spans pero no puedo hacer que la lista anidada funcione con este código. Ambas líneas output.setSpan(...)se java.lang.RuntimeException: PARAGRAPH span must start at paragraph boundary
bloquean
¡Gracias por la buena solución! También aplica
sangría a
2
¿Por qué usas un vector en lugar de un simple ArrayList? un vector es para subprocesos múltiples ...
desarrollador de Android
@androiddeveloper programador de c ++, mi mal, siéntase libre de editar la respuesta
masha
1
He publicado como Snippet androidsnippets.com/…
Pratik Butani
8

Si solo necesita formatear una lista, manténgalo simple y copie / pegue un carácter Unicode en su TextView para lograr el mismo resultado.

• Carácter Unicode 'BULLET' (U + 2022)

Naku
fuente
6

Vine aquí buscando implementaciones de TagHandler. Las respuestas de Truong Nguyen y Aman Guatam son muy agradables, pero necesitaba una versión mixta de ambas: necesitaba mi solución para no sobreformatearla y poder resolver <ol>etiquetas, ya que estoy analizando algo como <h3>title</h3><ol><li>item</li><li>item</li><li>item</li></ol>.

Esta es mi solución.

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

Tenga en cuenta que, dado que estamos restableciendo el valor del índice cada vez que comienza una nueva lista, NO funcionará si anida listas como en <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

Con ese código, obtendría en 1, 1, 2, 3lugar de 1, 1, 2, 2.

Charlie-Blake
fuente
Este código funciona hasta la versión 23. ¿Cómo hacerlo funcionar para 24 y superior?
Abhinav Tyagi
3

Claro, hay una forma de mostrar viñetas en Android TextView. Puede reemplazar las <li>etiquetas con &#149;(que es código HTML para viñeta).

Si desea probar otros íconos de lista, use el preferido de la tabla que es este enlace;

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

Taner
fuente
No funcionó para mí. En cambio, en Android 7.1.1 y 6.0.1, aparece un cuadro con una x en lugar de la viñeta en TextView.
user1652110
3

Simplemente puede reemplazar el "li" con 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");
        }
    }
}
hojalata
fuente
2

La respuesta de Lord Voldermort es un buen punto de partida. Sin embargo, necesitaba que la oletiqueta mostrara una lista ordenada en 1. 2. 3. ....lugar de viñetas. Además, las etiquetas anidadas necesitan un manejo especial para funcionar correctamente.

En mi código, he mantenido pila (ParentList) para realizar un seguimiento de abierta y cerrada uly oletiquetas, y también para conocer la etiqueta abierta en ese momento. Además, levelWiseCounterse utiliza para mantener diferentes recuentos en caso de oletiquetas anidadas .

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
      }
   }
Kshitij
fuente
2

¿Qué tal el siguiente código (basado en este enlace ):

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
    {
    }
  }
desarrollador de Android
fuente
1
Esta respuesta solo tiene un formato ligeramente diferente en comparación con la fuente original de esto, que se creó para admitir otra respuesta a esta misma pregunta: stackoverflow.com/a/17365740/262462 :)
Kuitsi
cierto. no me di cuenta de eso.
desarrollador de Android
2

Tuve el problema de que siempre obtenía una línea vacía después de una lista con la solución @Kuitsis. Agregué algunas líneas en handleTag () y ahora las líneas vacías se han ido:

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

Puede utilizar Html.TagHandler. A continuación se puede utilizar para 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•")
    }
}

y

textView.setText(Html.fromHtml(myHtmlText, null, UlTagHandler()));
Shalu TD
fuente
0

esto es una confirmación de lo que ha dicho kassim. hay fragmentación. Encontré cómo resolver esto. tengo que cambiar el nombre <li>y ul a una etiqueta personalizada. entonces:

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

luego, en mi controlador, puedo buscar esa etiqueta personalizada (que no hace nada) y hacer que haga algo.

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

esto debería hacer que funcione para todas las versiones de Android.

j2emanue
fuente