Usando un ListAdapter para llenar un LinearLayout dentro de un diseño ScrollView

95

Me enfrento a un problema muy común: diseñé una actividad y ahora resulta que debería mostrar algunos elementos dentro de ella ScrollView. La forma normal de hacerlo sería usar el existente ListAdapter, conectarlo a ListViewy BOOM tendría mi lista de elementos.

PERO no debe colocar un anidado ListViewen un ScrollViewya que arruina el desplazamiento, incluso Android Lint se queja de ello.

Entonces esta es mi pregunta:

¿Cómo conecto un ListAdaptera un LinearLayouto algo similar?

Sé que esta solución no se escalará para muchos elementos, pero mis listas son muy cortas (<10 elementos), por lo que no es necesario reutilizar las vistas. En cuanto al rendimiento, puedo vivir colocando todas las vistas directamente en el archivo LinearLayout.

Una solución que se me ocurrió sería colocar mi diseño de actividad existente en la sección headerView del archivo ListView. Pero se siente como abusar de este mecanismo, así que estoy buscando una solución más limpia.

Ideas?

ACTUALIZACIÓN: Para inspirar la dirección correcta, agrego un diseño de muestra para mostrar mi problema:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:id="@+id/news_detail_layout"
              android:layout_width="fill_parent"
              android:layout_height="fill_parent"
              android:orientation="vertical"
              android:visibility="visible">


    <ScrollView
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:background="#FFF"
            >

        <LinearLayout
                android:layout_width="fill_parent"
                android:layout_height="fill_parent"
                android:orientation="vertical"
                android:paddingLeft="@dimen/news_detail_layout_side_padding"
                android:paddingRight="@dimen/news_detail_layout_side_padding"
                android:paddingTop="@dimen/news_detail_layout_vertical_padding"
                android:paddingBottom="@dimen/news_detail_layout_vertical_padding"
                >

            <TextView
                    android:id="@+id/news_detail_date"
                    android:layout_height="wrap_content"
                    android:layout_width="fill_parent"
                    android:gravity="center_horizontal"
                    android:text="LALALA"
                    android:textSize="@dimen/news_detail_date_height"
                    android:textColor="@color/font_black"
                    />

            <Gallery
                    android:id="@+id/news_detail_image"
                    android:layout_height="wrap_content"
                    android:layout_width="fill_parent"
                    android:paddingTop="5dip"
                    android:paddingBottom="5dip"
                    />

            <TextView
                    android:id="@+id/news_detail_headline"
                    android:layout_height="wrap_content"
                    android:layout_width="fill_parent"
                    android:gravity="center_horizontal"
                    android:text="Some awesome headline"
                    android:textSize="@dimen/news_detail_headline_height"
                    android:textColor="@color/font_black"
                    android:paddingTop="@dimen/news_detail_headline_paddingTop"
                    android:paddingBottom="@dimen/news_detail_headline_paddingBottom"
                    />

            <TextView
                    android:id="@+id/news_detail_content"
                    android:layout_height="wrap_content"
                    android:layout_width="fill_parent"
                    android:text="Here comes a lot of text so the scrollview is really needed."
                    android:textSize="@dimen/news_detail_content_height"
                    android:textColor="@color/font_black"
                    />

            <!---
                HERE I NEED THE LIST OF ITEMS PROVIDED BY THE EXISTING ADAPTER. 
                They should be positioned at the end of the content, so making the scrollview smaller is not an option.
            ---->                        

        </LinearLayout>
    </ScrollView>
</LinearLayout>

ACTUALIZACIÓN 2 Cambié el título para que sea más fácil de entender (obtuve un voto negativo, ¡doh!).

Stefan Hoth
fuente
1
¿Alguna vez encontró una solución limpia y agradable a este problema? Veo que Google lo usa en su tienda de juegos para reseñas. ¿Alguien sabe cómo lo hacen?
Zapnologica

Respuestas:

96

Probablemente debería agregar manualmente sus elementos a LinearLayout:

LinearLayout layout = ... // Your linear layout.
ListAdapter adapter = ... // Your adapter.

final int adapterCount = adapter.getCount();

for (int i = 0; i < adapterCount; i++) {
  View item = adapter.getView(i, null, null);
  layout.addView(item);
}

EDITAR : Rechacé este enfoque cuando necesitaba mostrar alrededor de 200 elementos de lista no triviales, es muy lento - Nexus 4 necesitó alrededor de 2 segundos para mostrar mi "lista", eso era inaceptable. Así que recurrí al enfoque de Flo con encabezados. Funciona mucho más rápido porque las vistas de lista se crean a pedido cuando el usuario se desplaza, no en el momento en que se crea la vista.

Reanudar: La adición manual de vistas al diseño es más fácil de codificar (por lo tanto, potencialmente menos partes móviles y errores), pero tiene problemas de rendimiento, por lo que si tiene como 50 vistas o más, le recomiendo usar el enfoque de encabezado.

Ejemplo. Básicamente, el diseño de la actividad (o fragmento) se transforma en algo como esto (ya no se necesita ScrollView):

<ListView xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/my_top_layout"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"/>

Luego, en onCreateView()(usaré un ejemplo con un fragmento), debe agregar una vista de encabezado y luego configurar un adaptador (supongo que el ID del recurso del encabezado es header_layout):

ListView listView = (ListView) inflater.inflate(R.layout.my_top_layout, container, false);
View header = inflater.inflate(R.layout.header_layout, null);
// Initialize your header here.
listView.addHeaderView(header, null, false);

BaseAdapter adapter = // ... Initialize your adapter.
listView.setAdapter(adapter);

// Just as a bonus - if you want to do something with your list items:
view.setOnItemClickListener(new AdapterView.OnItemClickListener() {
  @Override
  public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    // You can just use listView instead of parent casted to ListView.
    if (position >= ((ListView) parent).getHeaderViewsCount()) {
      // Note the usage of getItemAtPosition() instead of adapter's getItem() because
      // the latter does not take into account the header (which has position 0).
      Object obj = parent.getItemAtPosition(position);
      // Do something with your object.
    }
  }
});
fumar
fuente
Sí, esa fue la segunda idea que tuve, pero no estaba seguro ListAdaptery ListViewhace más magia detrás de las pantallas que luego no hago manualmente.
Stefan Hoth
Por supuesto, ListView hace algo de magia, al menos divisores entre elementos, tendrías que agregarlos manualmente, supongo. ListAdapter (si se implementa correctamente) no debería hacer más magia.
Smok
2
Este es un voto negativo para mí. ¿Alguien tiene problemas de memoria? Hay una razón por la que usamos adaptadores ...
Kevin Parker
1
¿Alguien más tiene problemas para actualizar las vistas usando el notifyDataSetChangedmétodo del adaptador ? Esto funciona bien en un adaptador diferente que he conectado a ListView, pero no parece funcionar con el que he conectado a LinearLayout.
jokeefe
2
esta es una de las razones por las que odio a Android. No veo por qué tiene que implementar soluciones complicadas o sufrir problemas de rendimiento.
IARI
6

Me quedaría con la solución de vista de encabezado. No tiene nada de malo. En este momento estoy implementando una actividad usando exactamente el mismo enfoque.

Obviamente, la "parte del elemento" es más dinámica que estática (variando el recuento de elementos frente al recuento de elementos arreglados, etc.); de lo contrario, no pensará en usar un adaptador en absoluto. Entonces, cuando necesite un adaptador, use ListView.

Implementar una solución que rellene un LinearLayout desde un adaptador no es nada más que construir un ListView con un diseño personalizado.

Solo mis 2 centavos.

Flo
fuente
Un inconveniente de este enfoque es que tiene que mover casi todo el diseño de su actividad (ver arriba) a otro diseño para que pueda hacer referencia a él como ListHeaderView y crear otro diseño que solo incluya el ListView. No estoy seguro de que me guste este parche.
Stefan Hoth
1
Sí, sé lo que quieres decir, antes de comenzar a usar este enfoque tampoco me gustaba la idea de tener mi diseño de actividad separado en varios archivos. Pero cuando vi que el diseño general se veía como esperaba, no me importó la separación, ya que solo está dividida en dos archivos. Una cosa que debe tener en cuenta, ListView no debe tener una vista vacía ya que esto ocultará el encabezado de su lista cuando no haya elementos de la lista.
Flo
3
Además, si desea tener varias listas en una página, esto realmente no funciona. ¿Alguien tiene un ejemplo de una vista de adaptador personalizada que genera elementos en una lista "plana", es decir, una que no se desplaza?
murtuza
0

Establezca su vista en main.xml onCreate, luego infle desde row.xml

main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="450dp" >

        <ListView
            android:id="@+id/mainListView"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:layout_above="@+id/size"
            android:layout_below="@+id/editText1"
            android:gravity="fill_vertical|fill_horizontal"
            android:horizontalSpacing="15dp"
            android:isScrollContainer="true"
            android:numColumns="1"
            android:padding="5dp"
            android:scrollbars="vertical"
            android:smoothScrollbar="true"
            android:stretchMode="columnWidth" >

</ListView>

    <TextView
        android:id="@+id/size"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentRight="true"
        android:background="#ff444444"
        android:gravity="center"
        android:text="TextView"
        android:textColor="#D3D3D3"
        android:textStyle="italic" />

    </EditText>

</RelativeLayout> 

row.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent" 
  android:paddingTop="3dp">

<TextView
  android:id="@+id/rowTextView"
  android:layout_width="0dip"
  android:layout_height="41dp"
  android:layout_margin="4dp"
  android:layout_weight="2.83"
  android:ellipsize="end"
  android:gravity="center_vertical"
  android:lines="1"
  android:text="John Doe"
  android:textColor="@color/color_white"
  android:textSize="23dp" >
</TextView>

</LinearLayout>
fasheikh
fuente
Creo que extrañaste entendiste la pregunta. No quiere llenar una lista con LinearLayouts.
Flo
"¿Cómo conecto un ListAdapter a un LinearLayout o algo similar?" Solo respondiendo lo que preguntó el OP
fasheikh
2
No, no quiere usar ListView. Solo quiere usar un adaptador y usarlo para completar un LinearLayout con entradas.
Flo
Gracias por tu respuesta pero @Flo es correcto. No puedo usar un ListView porque el diseño exterior ya es un ScrollView. Por favor revise mi texto y el diseño de muestra.
Stefan Hoth
¿Por qué se necesita scrollView?
Fasheikh
0

Utilizo el siguiente código que replica la funcionalidad del adaptador con ViewGroupy TabLayout. Lo bueno de esto es que si cambia su lista y vuelve a vincular, solo afectará a los elementos modificados:

Uso:

val list = mutableListOf<Person>()
layout.bindChildren(list, { it.personId }, { bindView(it) }, {d, t ->bindView(d, t)})
list.removeAt(0)
list+=newPerson
layout.bindChildren(list, { it.personId }, { bindView(it) }, {d, t ->bindView(d, t)})

Para ViewGroups:

fun <Item, Key> ViewGroup.bindChildren(items: List<Item>, id: (Item) -> Key, view: (Item) -> View, bind: (Item, View) -> Unit) {
    val old = children.map { it.tag as Key }.toList().filter { it != null }
    val new = items.map(id)

    val add = new - old
    val remove = old - new
    val keep = new.intersect(old)

    val tagToChildren = children.associateBy { it.tag as Key }
    val idToItem = items.associateBy(id)

    remove.forEach { tagToChildren[it].let { removeView(it) } }
    keep.forEach { bind(idToItem[it]!!, tagToChildren[it]!!) }
    add.forEach { id -> view(idToItem[id]!!).also { it.tag = id }.also { addView(it, items.indexOf(idToItem[id])) } }
}

Porque TabLayouttengo esto:

fun <Item, Key> TabLayout.bindTabs(items: List<Item>, toKey: (Item) -> Key, tab: (Item) -> TabLayout.Tab, bind: (Item, TabLayout.Tab) -> Unit) {
    val old = (0 until tabCount).map { getTabAt(it)?.tag as Key }
    val new = items.map(toKey)

    val add = new - old
    val remove = old - new
    val keep = new.intersect(old)

    val tagToChildren = (0 until tabCount).map { getTabAt(it) }.associateBy { it?.tag as Key }
    val idToItem = items.associateBy(toKey)

    remove.forEach { tagToChildren[it].let { removeTab(it) } }
    keep.forEach { bind(idToItem[it]!!, tagToChildren[it]!!) }
    add.forEach { key -> tab(idToItem[key]!!).also { it.tag = key }.also { addTab(it, items.indexOf(idToItem[key])) } }
}
M-WaJeEh
fuente