96

I'm facing a very common problem: I layed out an activity and now it turns out it should display a few items within this ScrollView. The normal way to do that would be to use the existing ListAdapter, connect it to a ListView and BOOM I'd have my list of items.

BUT You should not place a nested ListView in a ScrollView as it screws up the scrolling - even Android Lint complains about it.

So here's my question:

How do I connect a ListAdapter to a LinearLayout or something similar?

I know this solution won't scale for a lot of items but my lists is very short (< 10 items) so reusage of views is not really needed. Performance wise I can live with placing all views directly into the LinearLayout.

One solution I came up with would be to place my existing activity layout in the headerView section of the ListView. But this feels like abusing this mechanism so I'm looking for a cleaner solution.

Ideas?

UPDATE: In order to inspire the right direction I add a sample layout to show my problem:

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

UPDATE 2 I changed the headline to make it easier to understand (got a downvote, doh!).

Stefan Hoth
  • 2,675
  • 3
  • 22
  • 33
  • 1
    Did you ever find a nice clean solution to this problem? I see Google uses it in their play store for reviews. Anyone know how they do it? – Zapnologica Oct 15 '14 at 16:58

4 Answers4

97

You probably should just manually add your items to 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: I rejected this approach when I needed to display about 200 non-trivial list items, it is very slow - Nexus 4 needed about 2 seconds to display my "list", that was unacceptable. So I turned to Flo's approach with headers. It works much faster because list views are created on demand when user scrolls, not at the time the view is created.

Resume: The manual addition of views to layout is easier to code (thus potentially less moving parts and bugs), but suffers from performance problems, so if you have like 50 views or more, I advise to use the header approach.

Example. Basically the activity (or fragment) layout transforms to something like this (no ScrollView needed anymore):

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

Then in onCreateView() (I'll use an example with a fragment) you need to add a header view and then set an adapter (I assume the header resource ID is 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.
    }
  }
});
smok
  • 1,658
  • 13
  • 13
  • Yes, that was the second idea I had but i wasn't sure of the `ListAdapter` and the `ListView` does more magic behind the screens which I then don't do manually. – Stefan Hoth Sep 13 '12 at 12:49
  • Of course ListView does some magic, at least dividers between elements, you would have to add them manually, I guess. ListAdapter (if it is implemented correctly) should not do any more magic. – smok Sep 13 '12 at 13:27
  • I went with this solution as it fits my purpose better (bigger layout, small list). If it would be the other way around I'd go with the solution provided by Flo as it allows reusage of elements in the `ListView` for better performance. – Stefan Hoth Sep 17 '12 at 09:48
  • @smok `LinearLayout` has dividers too. – Dandre Allison Jan 19 '13 at 02:53
  • 2
    This is a down vote for me. Memory issues anyone? There is a reason we use adapters... – Kevin Parker May 02 '13 at 18:14
  • @Kevin What memory issues? – smok May 30 '13 at 15:08
  • @DandreAllison dividers? Can you please post a link? – smok May 30 '13 at 15:09
  • http://developer.android.com/reference/android/widget/LinearLayout.html `android:divider` and `android:showDividers`, looks like it was added in 3.0. @smok – Dandre Allison May 30 '13 at 16:35
  • @smok http://stackoverflow.com/questions/15128652/android-add-divider-to-linear-layout has suggestions for adding dividers without 3.0. Stuff can be translated to Java since you're code does things dynamically. – Dandre Allison May 30 '13 at 16:37
  • @Kevin, would you please elaborate on the memory issues and how adapters mitigate the issue? – StockB Jul 05 '13 at 16:12
  • @StockB, I can suggest, that Kevin meant the second parameter of GetView() adapter function. It should be used to 'reuse' views instead of creating a new one for each element in the list. With large amount of items in the ListAdapter it becomes a problem. – Karatheodory Jul 10 '13 at 02:56
  • @Karatheodory the author said that the list has <10 items, memory won't be a problem. – smok Jul 22 '13 at 20:44
  • @Smok, yes, I agree. I just tried to figure out what troubles with memory Kevin spoke about. – Karatheodory Aug 07 '13 at 04:31
  • 1
    Does anyone else have problems with updating the views using the adapter's `notifyDataSetChanged` method? This works fine on a different adapter I have hooked up to a `ListView`, but it doesn't seem to be working with the one I have hooked up to a `LinearLayout`. – jokeefe Nov 12 '13 at 04:48
  • @jokeefe I suspect that `ListView` registers itself as the adapter's observer via `registerDataSetObserver()` method. You should do that in your activity too, and in `DataSetObserver` implementation just clean the `LinearLayout` and add the views again. – smok Dec 05 '13 at 12:15
  • @smok Thanks. I figured that it was an issue with observer registration, so I actually just went back to using a `ListView` and made use of the header and footer views. It does everything I need now! – jokeefe Dec 05 '13 at 15:31
  • 2
    this is one of the reason I hate android for. I don't see why you have to implement complicated solutions, or suffer from performance problems. – IARI Feb 27 '17 at 19:13
6

I would stick with the header view solution. There's nothing wrong with it. At the moment I implementing an activity using the exact same approach.

Obviously the "item part" is more dynamically than static (varying item count vs. fix item count etc.) otherwise you won't think about using an adapter at all. So when you need an adapter then use the ListView.

Implementing a solution which populates a LinearLayout from an adapter is in the end nothing else than building a ListView with a custom layout.

Just my 2 cents.

Flo
  • 27,355
  • 15
  • 87
  • 125
  • One drawback of this approach is that you have to move almost your whole activity layout (see above) into another piece of layout so you can reference it as a ListHeaderView and create another layout one just including the `ListView`. Not sure I like this patch work. – Stefan Hoth Sep 13 '12 at 12:47
  • 1
    Yes I know what you mean, before I started to use this approach I also didn't like the idea of having my activity layout separated into multiple files. But when I saw that the overall layout looked as I expected to look I didn't mind about the separation as it's only separated into two files. One thing you must be aware of, the ListView must not have an empty view as this will hide your list header when there are no list items. – Flo Sep 13 '12 at 14:00
  • 3
    Also, if you want to have multiple lists on a page this doesn't really work. Does anyone have an example of a custom adapter view that outputs items into a "flat" list, i.e one that does not scroll. – murtuza Jul 12 '14 at 20:45
0

Set your view to main.xml onCreate, then inflate from 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
  • 419
  • 3
  • 19
  • I think you miss understood the question. He don't want to populate a list with LinearLayouts. – Flo Sep 13 '12 at 12:13
  • "How do I connect a ListAdapter to a LinearLayout or something similar?" Just answering what the OP asked – fasheikh Sep 13 '12 at 12:17
  • 2
    No he don't want to use a ListView. He only want to use an adapter and use it to populate a LinearLayout with entries. – Flo Sep 13 '12 at 12:29
  • Thanks for your answer but @Flo is correct. I **can't** use a ListView because the outer layout is already a ScrollView. Please check my text and the sample layout. – Stefan Hoth Sep 13 '12 at 13:30
  • why is that scrollView needed? – fasheikh Sep 13 '12 at 13:36
  • @fasheikh It's needed because by "list content" just a part of the info I want to display on that screen. Gallery, TextView etc comes before the list content. – Stefan Hoth Sep 13 '12 at 14:44
  • Sorry I don't understand. Basically, you want a textView above the list? – fasheikh Sep 13 '12 at 14:46
  • @fasheikh I have a full layout and with a lot of content (so I need the `ScrollView`) and now I just want to add *some* data via a `ListAdapter`. – Stefan Hoth Sep 14 '12 at 08:12
  • sorry @StefanHoth I'm all outta ideas :| I did think to make the primary layout a relative one, and set it to Scrollable but i'm not sure that'll work. – fasheikh Sep 14 '12 at 10:00
0

I use following code which replicate adapter functionality with ViewGroup and TabLayout. Good thing about this is that if you change your list and bind again, it will only affect changed items:

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

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

For TabLayout I have this:

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
  • 17,204
  • 10
  • 66
  • 103