9

We are working on an app with multiple components that we want to integrate into the detail view of a post (see mockup.) Each of the pieces is expandable (usually with an ajax call) and the entire screen is scrollable to see all the content.

listview mockup

We're not sure of the best way to solve this problem because, according to Google, listviews should not be put into scrollviews. It seems there are people doing this anyway as in How can I put a ListView into a ScrollView without it collapsing?.

In general the amount of content that is in a post is small enough that it could all be inflated at once, but it is possible to get posts with 100+ photos or 300+ comments where memory might be a concern.

My question is what is the best, if not official, way to build such a layout. I'm interested in hearing recommendations for the entire layout structure so I understand how the more static content like title plays with the photos or comments.

I would like to optimize performance/ease of implementation for the small posts (few photos, 20-50 comments) - but we have to be able to handle the large posts without crashing the app.

Community
  • 1
  • 1
Yehosef
  • 17,987
  • 7
  • 35
  • 56
  • Note - I mentioned listviews in the question - but it could be relativeLayouts would be better - I just would prefer something that I can easily add new elements via something like an adaptor. – Yehosef Nov 19 '13 at 12:39
  • try to use listview with addHeaderView() or addFooterView() implementation.. – Haresh Chhelana Nov 19 '13 at 12:39
  • @Haresh - there are multiple potential listviews here - could you elaborate? – Yehosef Nov 19 '13 at 15:33

4 Answers4

9

At first I thought it was a complex layout for tablets, but if it is only 1 list, it can be implemented by using a custom list adapter.

Imagine this layout as a single list with different views. So the post title is a list item of type 1, the photo view is of type 3, the button is of type 4. Here is your layout with item positions marked by red numbers and view types marked by blue text.

OP layout with marked item positions

You should create model classes for each item with a viewType property, so for 11 view types there will be 11 classes (or a base class and few derived classes). Then add these models in a proper order to your adapter like this:

    this.add(new ListItemModelBase(ListItemViewTypes.POST_TITLE));
    this.add(new PhotoViewItemModel("image1.jpg", "Image 1"));
    this.add(new PhotoViewItemModel("image2.jpg", "Image 2"));
    this.add(new PhotoViewItemModel("image3.jpg", "Image 3"));
    this.add(new ListItemModelBase(ListItemViewTypes.SEE_MORE_PHOTOS_BUTTON));
    // other models

Then you can update/add/remove list items by changing models. For example, if you click "See more text" button, call the following function:

private void onSeeMoreTextButtonClicked(ListItemModelBase item){
    item.setViewType(ListItemViewTypes.POST_TITLE_EXPANDED);
    this.notifyDataSetChanged();
}

If you click "See more photos" button, call this function:

private void onSeeMorePhotosButtonClicked(ListItemModelBase item){      
    int index = this.getPosition(item);

    this.remove(item);
    this.insert(new PhotoViewItemModel("image4.jpg", "Image 4"), index);
    this.insert(new PhotoViewItemModel("image5.jpg", "Image 5"), index + 1);
    this.insert(new PhotoViewItemModel("image6.jpg", "Image 6"), index + 2);
}

I think the idea is clear.

Here is a full example for 4 view types:

public class ListItemViewTypes {
    public static final int VIEW_TYPES_COUNT = 4;

    // Must start from 0
    public static final int POST_TITLE = 0;
    public static final int POST_TITLE_EXPANDED = 1;
    public static final int PHOTO_VIEW = 2;
    public static final int SEE_MORE_PHOTOS_BUTTON = 3;
}

public class ListItemModelBase {
    private int mViewType;

    public ListItemModelBase(int viewType){
        mViewType = viewType;
    }

    public int getViewType() {
        return mViewType;
    }

    public void setViewType(int viewType) {
        mViewType = viewType;
    }
}

public class PhotoViewItemModel extends ListItemModelBase {
    private final String mUrl;
    private final String mDescription;

    public PhotoViewItemModel(String url, String description) {
        super(ListItemViewTypes.PHOTO_VIEW);
        mUrl = url;
        mDescription = description;
    }

    public String getUrl() {
        return mUrl;
    }

    public String getDescription() {
        return mDescription;
    }
}

public class TestListAdapter extends ArrayAdapter<ListItemModelBase> {
    private final LayoutInflater mInflater;

    public TestListAdapter(Context context) {
        super(context, -1);

        mInflater = LayoutInflater.from(context);

        this.add(new ListItemModelBase(ListItemViewTypes.POST_TITLE));
        this.add(new PhotoViewItemModel("image1.jpg", "Image 1"));
        this.add(new PhotoViewItemModel("image2.jpg", "Image 2"));
        this.add(new PhotoViewItemModel("image3.jpg", "Image 3"));
        this.add(new ListItemModelBase(ListItemViewTypes.SEE_MORE_PHOTOS_BUTTON));
    }

    @Override
    public int getViewTypeCount() {
        return ListItemViewTypes.VIEW_TYPES_COUNT;
    }

    @Override
    public int getItemViewType(int position) {
        // important method so that convertView has a correct type
        return this.getItem(position).getViewType();
    }

    @Override
    public boolean isEnabled(int position) {
        // enable context menu for photo views, comments and related posts
        // or always return false if context menu is not needed at all 
        if (this.getItem(position).getViewType() == ListItemViewTypes.PHOTO_VIEW) {
            return true;
        }

        return false;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        final ListItemModelBase item = this.getItem(position);
        int viewType = item.getViewType();

        if (convertView == null) {
            // different layouts for different `viewType`
            int layoutId = -1;
            if (viewType == ListItemViewTypes.POST_TITLE) {
                layoutId = R.layout.post_title;
            } else if (viewType == ListItemViewTypes.POST_TITLE_EXPANDED) {
                layoutId = R.layout.post_title_expanded;
            } else if (viewType == ListItemViewTypes.PHOTO_VIEW) {
                layoutId = R.layout.photo_view;
            } else if (viewType == ListItemViewTypes.SEE_MORE_PHOTOS_BUTTON) {
                layoutId = R.layout.more_photos_button;
            }

            convertView = this.mInflater.inflate(layoutId, null);
        }

        // update the current view based on data from the current model
        if (viewType == ListItemViewTypes.POST_TITLE) {
            Button seeMoreTextButton = (Button)convertView.findViewById(R.id.button_see_more_text);
            seeMoreTextButton.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    onSeeMoreTextButtonClicked(item);
                }
            });
        } else if (viewType == ListItemViewTypes.SEE_MORE_PHOTOS_BUTTON) {
            Button seeMorePhotosButton = (Button)convertView.findViewById(R.id.button_see_more_photos);
            seeMorePhotosButton.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    onSeeMorePhotosButtonClicked(item);
                }
            });
        } else if (viewType == ListItemViewTypes.PHOTO_VIEW){
            PhotoViewItemModel photoViewItem = (PhotoViewItemModel)item;
            TextView photoDescriptionText = (TextView)convertView.findViewById(R.id.photo_view_text);
            photoDescriptionText.setText(photoViewItem.getDescription());
        }

        return convertView;
    }

    private void onSeeMoreTextButtonClicked(ListItemModelBase item){
        item.setViewType(ListItemViewTypes.POST_TITLE_EXPANDED);
        this.notifyDataSetChanged();
    }

    private void onSeeMorePhotosButtonClicked(ListItemModelBase item){      
        int index = this.getPosition(item);

        this.remove(item);
        this.insert(new PhotoViewItemModel("image4.jpg", "Image 4"), index);
        this.insert(new PhotoViewItemModel("image5.jpg", "Image 5"), index + 1);
        this.insert(new PhotoViewItemModel("image6.jpg", "Image 6"), index + 2);
    }
}

public class MainActivity extends ListActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //setContentView(R.layout.activity_main);

        this.getListView().setChoiceMode(ListView.CHOICE_MODE_NONE);
        this.getListView().setDivider(null);

        TestListAdapter adapter = new TestListAdapter(this.getApplicationContext());
        this.getListView().setAdapter(adapter);
    }
}
vortexwolf
  • 13,967
  • 2
  • 54
  • 72
  • Thanks! this is the approach we've take thus far. At first I was very skeptical of the approach - it feels very unelegant. One advantage it has is that if the list is long (eg dozens of photos, hundreds of comments) you can still have good performance because of the listview. I was secretly hoping for a different solution - but I appreciate the concurring approach. – Yehosef Dec 09 '13 at 11:14
  • 1
    @Yenosef It's the only way that handles large lists efficiently in regards of performance. Another solution would be to use `WebView` and develop UI by using html/css/javascript, but web UI is often worse than native UI. – vortexwolf Dec 09 '13 at 16:41
3

Looking at the requirement you have I can think of these 3 possible solutions on the top of my head :

  1. Use Expandable List View - I think this will help you looking at the images above, have a look here - http://www.androidhive.info/2013/07/android-expandable-list-view-tutorial/

  2. Write dynamic layouts through code, create a new layout on click.

  3. Use different row items in list view using the getViewTypeCount() method. More on this here - http://android.amberfog.com/?p=296 Android ListView with different layouts for each row and http://www.jiahaoliuliu.com/2012/08/android-list-view-with-different-views.html

I personally see you using a combination of 1 & 3 to solve your current problem

I would strongly advise against the use of listview within a listview or a listview within a scrollview. They will be loads of performance issues and other stuff which you really don't want to get into. Hope this helps.

Community
  • 1
  • 1
Adnan Mulla
  • 2,872
  • 3
  • 25
  • 35
  • Thanks - does the expandable list view give me anything that a single list view with different views doesn't already? (I have to have multiple views types with an expandable list view anyway - no?) – Yehosef Dec 09 '13 at 11:21
  • I assume you wanted expandable option for your list view, the normal list view won't provide you the expand and close functionality out of the box where as the expandable list view will. Pardon me if I didn't understand your requirement correctly. – Adnan Mulla Dec 09 '13 at 11:51
  • I see - you're talking about the animation? It's not important for our needs. Also - I don't know if it matters - but for most of the components I won't know the "expandable" elements until after I make an api request (it will not be preloaded) - not sure if that matters for expandable lists. – Yehosef Dec 09 '13 at 11:59
  • Ugh as I mentioned my bad, I thought that those 2nd elements were displayed after you expanded the 1st list. The expandable list won't work out :( I believe vorrtex's answer is the closest ! – Adnan Mulla Dec 09 '13 at 17:52
0

Graphical layout may do the trick easily. first vertical then relative layout with widget property of scrolling item.

learner1
  • 123
  • 2
  • 3
  • 13
0

custom list view with different adapters will solve your problem

Suhail Mehta
  • 5,514
  • 2
  • 23
  • 37
  • Can you elaborate, I'm not sure I follow. You mean that there are multiple listviews or one listview will all the different pieces (post text, member, photos, comments, related posts)? – Yehosef Nov 20 '13 at 16:14