Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Correct way to implement onMeasure() and onLayout() in custom AdapterView

I know that I should measure children in onMeasure() and layout them in onLayout(). The question is in what of these methods should I add/recycle views so I could measure all children together with an eye on how they are mutually positioned (i.e. grid, list or whatever)?

My first approach was to add/recycle views in onLayout() but from that point I can't measure my children because they aren't added to AdapterView yet and getChildCount() returns 0 in onMeasure(). And I can't measure AdapterView itself without children being already layouted because it really depends upon their mutual positions, right?

I'm really confused with android layouting process in AdapterView when childrens are added/removed dynamically.

like image 492
Vsevolod Ganin Avatar asked Jul 19 '15 12:07

Vsevolod Ganin


1 Answers

I can't post a comment because I'm a new user, but can you describe WHAT you're trying to do, as opposed to HOW you're trying to do it? Frequently, you will find that this is an issue of design as opposed to coding. Especially if you're coming from a different platform (example, iOS). From experience, I found that measuring and manual layouts in Android is mostly unnecessary if you design your layout properly in light of your business need.

EDIT: As I mentioned this can be solved using some design decisions. I will use your Nodes/List example (hoping that this is your actual use case, but the solution can be expanded for a more general problem).

So if we think about your Header as a comment in a forum, and the List as replies to your comment, we can make the following assumption:

  1. One list is enough, not two. Each item in the list can either be a header (comment) or a list item (reply). Each reply is a comment, but not all comments are replies.

  2. For item n, I know if it's a comment or a reply (i.e. is it a header or an item in your list).

  3. For item n, I have a boolean member isVisible (default false; View.GONE).

Now, you can use the following components:

  1. One extended adapter class
  2. Two Layout XMLs: One for your Comment, one for your reply. You can have unlimited comments and each comment can have unlimited replies. Both those satisfy your requirements.
  3. Your fragment or activity container class that implements OnItemClickListener to show/hide your list.

So let's look at some code, shall we?

First, your XML files:

Comment row (your header)

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/overall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true">

<TextView
    android:id="@+id/comment_row_label"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>
</RelativeLayout>

Now your reply row (an element in your list)

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/overall"
android:layout_width="match_parent"
android:layout_height="wrap_content"> <!-- this is important -->
<TextView
    android:id="@+id/reply_row_label"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:visibility="gone"/>  <!-- important -->
</RelativeLayout>

Ok, now your adapter class

public class CommentsListAdapter extends BaseAdapter implements OnClickListener
{

public static String TAG = "CommentsListAdapter";

private final int NORMAL_COMMENT_TYPE = 0;
private final int REPLY_COMMENT_TYPE = 1;

private Context context = null;
private List<Comment> commentEntries = null;
private LayoutInflater inflater = null;

//All replies are comments, but not all comments are replies. The commentsList includes all your data. (Remember that the refresh method allows you to add items to the list at runtime.
public CommentsListAdapter(Context context, List<Comment> commentsList)
{
    super();

    this.context = context;
    this.inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    this.commentEntries = commentsList;

}
//For our first XML layout file
public static class CommentViewHolder
{
    public RelativeLayout overall;
    public TextView label;
}

//For our second XML
public static class ReplyViewHolder
{
    public RelativeView replyOverall;
    public TextView replyLabel;
}

@Override
public int getViewTypeCount()
{
    return 2; //Important. We have two views, Comment and reply.
}

//Change the following method to determine if the current item is a header or a list item.
@Override
public int getItemViewType(int position)
{
    int type = -1;
    if(commentEntries.get(position).getParentKey() == null)
        type = NORMAL_COMMENT_TYPE;
    else if(commentEntries.get(position).getParentKey() == 0L)
        type = NORMAL_COMMENT_TYPE;
    else
        type = REPLY_COMMENT_TYPE;

    return type;
}

@Override
public int getCount()
{
    return this.commentEntries.size(); //all data
}

@Override
public Object getItem(int position)
{
    return this.commentEntries.get(position);
}

@Override
public long getItemId(int position)
{
    return this.commentEntries.indexOf(this.commentEntries.get(position));
}

@Override
public View getView(int position, View convertView, ViewGroup parent)
{
    CommentViewHolder holder = null;
    ReplyViewHolder replyHolder = null;

    int type = getItemViewType(position);

    if(convertView == null)
    {
        if(type == NORMAL_COMMENT_TYPE)
        {
            convertView = inflater.inflate(R.layout.row_comment_entry, null);
            holder = new CommentViewHolder();
            holder.label =(TextView)convertView.findViewById(R.id.comment_row_label);
            convertView.setTag(holder);
        }
        else if(type == REPLY_COMMENT_TYPE)
        {
            convertView = inflater.inflate(R.layout.row_comment_reply_entry, null);
            replyHolder = new ReplyViewHolder();
            replyHolder.replyLable = (TextView)convertView.findViewById(R.id.reply_row_label);
            convertView.setTag(replyHolder);
        }
    }
    else
    {
        if(type == NORMAL_COMMENT_TYPE)
        {
            holder = (CommentViewHolder)convertView.getTag();
        }
        else if(type == REPLY_COMMENT_TYPE)
        {
            replyHolder = (ReplyViewHolder)convertView.getTag();
        }
    }
    //Now, set the values of your labels
    if(type == NORMAL_COMMENT_TYPE)
    {
        holder.label.setTag((Integer)position); //Important for onClick handling

        //your data model object
        Comment entry = (Comment)getItem(position);
        holder.label.setText(entry.getLabel());
    }
    else if(type == REPLY_COMMENT_TYPE)
    {
        replyHolder = (ReplyViewHolder)convertView.getTag(); //if you want to implement onClick for list items.

        //Or another data model if you decide to use multiple Lists
        Comment entry = (Comment)getItem(position);
        replyHolder.replyLabel.setText(entry.getLabel()));

        //This is the key
        if(entry.getVisible() == true)
           replyHolder.replyLabel.setVisibility(View.VISIBLE);
        else
          replyHolder.replyLabel.setVisibility(View.GONE);
    }

    return convertView;

}

//You can use this method to add items to your list. Remember that if you are using two data models, then you will have to send the correct model list here and create another refresh method for the other list.
public void refresh(List<Comment> commentsList)
{
    try
    {
        this.commentEntries = commentsList;
        notifyDataSetChanged();
    }
    catch(Exception e)
    {
        e.printStackTrace();
        Log.d(TAG, "::Error refreshing comments list.");        
    }
}

//Utility method to show/hide your list items
public void changeVisibility(int position)
{
    if(this.commentEntries == null || this.commentEntries.size() == 0)
         return;
    Comment parent = (Comment)getItem(position);
    for(Comment entry : this.commentEntries)
    {
        if(entry.getParent().isEqual(parent))
           entry.setVisible(!entry.getVisible()); //if it's shown, hide it. Show it otherwise.
    }
    notifyDataSetChanged(); //redraw
}

}

Ok great, now we have a list of headers with hidden children (remember, we set the default visibility of children to 'gone'). Not what we wanted, so let's fix that.

Your container class (fragment or activity) you will have the following XML definition

<!-- the @null divider means transparent -->
<ListView
    android:id="@+id/comments_entries_list"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:divider="@null"
    android:dividerHeight="5dp" />

And your onCreateView will implement OnItemClickListener and have the following

private ListView commentsListView = null;
private List<Comment>comments = null;
private static CommentsListAdapter adapter = null;
....
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
 ...
 //comments list can be null here, and you can use adapter.refresh(data) to set the data
 adapter = new CommentsListAdapter(getActivity(), comments);
 this.commentsListView.setAdapter(adapter);
 this.commentsListView.setOnClickListener(this); //to show your list
}

Now to show your list when you click a header

@Override
public void onItemClick(AdapterView<?> parent, View view, int position,
        long id)
{

     adapter.changeVisibility(position);

}

Now, if an item is clicked and that item has a parent (i.e. list item), it will be shown/hidden according to its current state.

Some comments about the code:

  1. I wrote this on WordPad as I don't have a dev environment handy. Sorry for any compilation errors.

  2. This code can be optimized: If you have a very large data set, this code would be slow since you're redrawing the entire list on every call to changeVisibility(). You can maintain two lists (one for headers, one for list items) and in changeVisibility you can query over the list items only).

  3. I re-enforce that idea that some design decisions would make your life a lot easier. For example, if your list items were actually just a list of labels, then you can have one custom XML file (for your header) and a ListView view within it that you can set to View.GONE. This will make all other views pretend that it's not even there and your layout will work properly.

Hope this helps.

like image 125
Fayez Avatar answered Oct 14 '22 03:10

Fayez