android - 在自定义 AdapterView 中实现 onMeasure() 和 onLayout() 的正确方法

标签 android android-layout android-custom-view android-adapterview

我知道我应该在 onMeasure() 中测量子项并在 onLayout() 中对它们进行布局。问题是我应该添加/回收 View 中的哪些方法,以便我可以一起测量所有子项并观察它们如何相互定位(即网格、列表或其他)?

我的第一个方法是在 onLayout() 中添加/回收 View ,但从那时起我无法测量我的 child ,因为它们还没有添加到 AdapterView 和 getChildCount() onMeasure() 中返回 0。如果 child 已经布局,我无法测量 AdapterView 本身,因为它真的取决于他们的相互位置,对吧?

当动态添加/删除子项时,我真的很困惑 AdapterView 中的 android 布局过程。

最佳答案

我不能发表评论,因为我是新用户,但你能描述一下你想做什么,而不是你想怎么做吗?通常,您会发现这是一个设计问题,而不是编码问题。特别是如果您来自不同的平台(例如 iOS)。根据经验,我发现如果您根据业务需要正确设计布局,那么在 Android 中测量和手动布局几乎是不必要的。

编辑: 正如我提到的,这可以通过一些设计决策来解决。我将使用您的节点/列表示例(希望这是您的实际用例,但解决方案可以针对更一般的问题进行扩展)。

因此,如果我们将您的 Header 视为论坛中的评论,而将 List 视为对您评论的回复,我们可以做出以下假设:

  1. 一份 list 就够了,两份不行。列表中的每一项都可以是标题(评论)或列表项(回复)。每个回复都是评论,但并非所有评论都是回复。

  2. 对于项目 n,我知道它是评论还是回复(即它是标题还是列表中的项目)。

  3. 对于项目 n,我有一个 bool 值成员 isVisible(默认为 false;View.GONE)。

现在,您可以使用以下组件:

  1. 一个扩展适配器类
  2. 两个布局 XML:一个用于您的评论,一个用于您的回复。您可以有无限的评论,每条评论可以有无限的回复。两者都满足您的要求。
  3. 实现 OnItemClickListener 以显示/隐藏列表的 fragment 或 Activity 容器类。

那么让我们看一些代码,好吗?

首先,您的 XML 文件:

评论行(你的标题)

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

现在你的回复行(你列表中的一个元素)

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

好的,现在你的适配器类

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
}

}

好的,现在我们有一个包含隐藏子项的标题列表(记住,我们将子项的默认可见性设置为“消失”)。这不是我们想要的,所以让我们解决这个问题。

您的容器类( fragment 或 Activity )您将具有以下 XML 定义

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

并且您的 onCreateView 将实现 OnItemClickListener 并具有以下内容

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
}

现在单击标题时显示您的列表

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

     adapter.changeVisibility(position);

}

现在,如果单击一个项目并且该项目有一个父项(即列表项),它将根据其当前状态显示/隐藏。

关于代码的一些评论:

  1. 我手边没有开发环境,所以我在写字板上写了这篇文章。对于任何编译错误,我们深表歉意。

  2. 此代码可以优化:如果您有一个非常大的数据集,此代码会很慢,因为您在每次调用 changeVisibility() 时都会重新绘制整个列表。您可以维护两个列表(一个用于标题,一个用于列表项),并且在 changeVisibility 中您只能查询列表项。

  3. 我再次强调一些设计决策会让您的生活更轻松的想法。例如,如果您的列表项实际上只是一个标签列表,那么您可以拥有一个自定义 XML 文件(用于您的标题)和其中的一个 ListView View ,您可以将其设置为 View.GONE。这将使所有其他 View 假装它甚至不存在,并且您的布局将正常工作。

希望这对您有所帮助。

关于android - 在自定义 AdapterView 中实现 onMeasure() 和 onLayout() 的正确方法,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/31501092/

相关文章:

java - getContext() 不存在

android - 为 Android 相机预览添加效果

android - 如何在 Microsoft Band 的自定义布局中创建图标?

android - 在 android 中以圆形方式设置卫星菜单项

android - 当 state_pressed = true 时更改 <selector> 中的文本大小

android - onDraw 不会在自定义 View 上被调用?

android - 如何在android中解析<div class ="abc"><h4 class ="cvb">india</h4></div>

android - 在文本更改监听器上添加破折号时无法从 EditText 字段中删除文本

android - 带有膨胀 xml 和子节点的自定义 LinearLayout

java - 如何创建可以动态接受角半径和颜色并相应更改的自定义按钮 View