Utilisation d'un ListAdapter pour remplir un LinearLayout dans une disposition ScrollView

95

Je suis confronté à un problème très courant: j'ai mis en page une activité et il s'avère maintenant qu'elle devrait afficher quelques éléments à l'intérieur ScrollView. La manière normale de faire cela serait d'utiliser l'existant ListAdapter, de le connecter à un ListViewet BOOM j'aurais ma liste d'articles.

MAIS vous ne devriez pas placer un imbriqué ListViewdans un ScrollViewcar il bousille le défilement - même Android Lint s'en plaint.

Alors voici ma question:

Comment connecter un ListAdapterà un LinearLayoutou quelque chose de similaire?

Je sais que cette solution ne s'adapte pas à beaucoup d'éléments, mais mes listes sont très courtes (<10 éléments), donc la réutilisation des vues n'est pas vraiment nécessaire. En termes de performances, je peux vivre en plaçant toutes les vues directement dans le LinearLayout.

Une solution que j'ai trouvée serait de placer ma disposition d'activité existante dans la section headerView du ListView. Mais cela donne l'impression d'abuser de ce mécanisme, alors je cherche une solution plus propre.

Des idées?

MISE À JOUR: Afin d'inspirer la bonne direction, j'ajoute un exemple de mise en page pour montrer mon problème:

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

MISE À JOUR 2 J'ai changé le titre pour le rendre plus facile à comprendre (j'ai obtenu un vote défavorable, doh!).

Stefan Hoth
la source
1
Avez-vous déjà trouvé une bonne solution propre à ce problème? Je vois que Google l'utilise dans leur Play Store pour les critiques. Quelqu'un sait-il comment le faire?
Zapnologica

Réponses:

96

Vous devriez probablement simplement ajouter manuellement vos éléments à 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);
}

EDIT : J'ai rejeté cette approche quand j'avais besoin d'afficher environ 200 éléments de liste non triviaux, c'est très lent - Nexus 4 avait besoin d'environ 2 secondes pour afficher ma "liste", ce qui était inacceptable. Je me suis donc tourné vers l'approche de Flo avec des en-têtes. Cela fonctionne beaucoup plus rapidement car les vues de liste sont créées à la demande lorsque l'utilisateur fait défiler, et non au moment de la création de la vue.

Resume: L'ajout manuel de vues à la mise en page est plus facile à coder (donc potentiellement moins de pièces mobiles et de bugs), mais souffre de problèmes de performances, donc si vous avez 50 vues ou plus, je vous conseille d'utiliser l'approche d'en-tête.

Exemple. Fondamentalement, la disposition de l'activité (ou du fragment) se transforme en quelque chose comme ça (plus besoin de 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"/>

Ensuite, dans onCreateView()(je vais utiliser un exemple avec un fragment), vous devez ajouter une vue d'en-tête, puis définir un adaptateur (je suppose que l'ID de ressource d'en-tête est 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.
    }
  }
});
fumer
la source
Oui, c'était la deuxième idée que j'avais, mais je n'étais pas sûr de la ListAdapteret la ListViewmagie fait plus derrière les écrans que je ne fais alors pas manuellement.
Stefan Hoth
Bien sûr, ListView fait de la magie, au moins des diviseurs entre les éléments, vous devrez les ajouter manuellement, je suppose. ListAdapter (s'il est implémenté correctement) ne devrait plus faire de magie.
smok le
2
C'est un vote défavorable pour moi. Quelqu'un a des problèmes de mémoire? Il y a une raison pour laquelle nous utilisons des adaptateurs ...
Kevin Parker
1
Quelqu'un d'autre a-t-il des problèmes avec la mise à jour des vues à l'aide de la notifyDataSetChangedméthode de l'adaptateur ? Cela fonctionne bien sur un adaptateur différent que j'ai connecté à un ListView, mais cela ne semble pas fonctionner avec celui que j'ai connecté à un LinearLayout.
jokeefe
2
c'est l'une des raisons pour lesquelles je déteste Android. Je ne vois pas pourquoi vous devez mettre en œuvre des solutions compliquées ou souffrir de problèmes de performances.
IARI
6

Je m'en tiendrai à la solution de vue d'en-tête. Il n'y a rien de mal à cela. En ce moment, je mets en œuvre une activité en utilisant exactement la même approche.

De toute évidence, la "partie élément" est plus dynamique que statique (nombre d'éléments variable par rapport au nombre d'éléments corrigés, etc.) sinon vous ne penserez pas du tout à utiliser un adaptateur. Ainsi, lorsque vous avez besoin d'un adaptateur, utilisez ListView.

L'implémentation d'une solution qui remplit un LinearLayout à partir d'un adaptateur n'est finalement rien d'autre que la construction d'un ListView avec une disposition personnalisée.

Juste mes 2 cents.

Flo
la source
Un inconvénient de cette approche est que vous devez déplacer la quasi-totalité de la mise en page de votre activité (voir ci-dessus) dans un autre élément de mise en page afin de pouvoir la référencer en tant que ListHeaderView et créer une autre mise en page contenant simplement le ListView. Pas sûr que j'aime ce travail de patch.
Stefan Hoth
1
Oui, je sais ce que vous voulez dire, avant de commencer à utiliser cette approche, je n'aimais pas non plus l'idée de séparer la disposition de mon activité en plusieurs fichiers. Mais quand j'ai vu que la mise en page générale ressemblait à ce à quoi je m'attendais, cela ne me dérangeait pas pour la séparation car elle n'est séparée qu'en deux fichiers. Une chose que vous devez savoir, le ListView ne doit pas avoir une vue vide car cela masquera votre en-tête de liste lorsqu'il n'y a pas d'éléments de liste.
Flo
3
De plus, si vous souhaitez avoir plusieurs listes sur une page, cela ne fonctionne pas vraiment. Quelqu'un a-t-il un exemple de vue d'adaptateur personnalisée qui génère des éléments dans une liste "plate", c'est-à-dire une liste qui ne défile pas.
murtuza
0

Définissez votre vue sur main.xml onCreate, puis gonflez à partir de 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
la source
Je pense que vous avez mal compris la question. Il ne veut pas remplir une liste avec LinearLayouts.
Flo
"Comment connecter un ListAdapter à un LinearLayout ou quelque chose de similaire?" Juste répondre à ce que l'OP a demandé
fasheikh
2
Non, il ne veut pas utiliser un ListView. Il souhaite uniquement utiliser un adaptateur et l'utiliser pour remplir un LinearLayout avec des entrées.
Flo
Merci pour votre réponse mais @Flo a raison. Je ne peux pas utiliser un ListView car la disposition externe est déjà un ScrollView. Veuillez vérifier mon texte et l'exemple de mise en page.
Stefan Hoth
pourquoi ce scrollView est-il nécessaire?
fasheikh le
0

J'utilise le code suivant qui réplique la fonctionnalité de l'adaptateur avec ViewGroupet TabLayout. La bonne chose à ce sujet est que si vous modifiez votre liste et liez à nouveau, cela n'affectera que les éléments modifiés:

Usage:

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

Pour 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])) } }
}

Car TabLayoutj'ai ceci:

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
la source