Android ListView 标头

Posted

技术标签:

【中文标题】Android ListView 标头【英文标题】:Android ListView headers 【发布时间】:2012-11-15 10:50:06 【问题描述】:

我的 ListView 上有一些事件。事件按天排序,我希望每天都有带有日期的标题,然后事件在下面收听。

这是我填充该列表的方式:

ArrayList<TwoText> crs = new ArrayList<TwoText>();

crs.add(new TwoText("This will be header", event.getDate()));

for (Event event : events) 
    crs.add(new TwoText(event.getStartString() + "-" + event.getEndString(), event.getSubject()));


arrayAdapter = new TwoTextArrayAdapter(this, R.layout.my_list_item, crs);
lv1.setAdapter(arrayAdapter);

这就是我的类 TwoText 的样子:

public class TwoText 
    public String classID;
    public String state;

    public TwoText(String classID, String state) 
        this.classID = classID;
        this.state = state;
    

这就是我的 TwoTextArrayAdapter 类的外观:

import java.util.ArrayList;
import android.app.Activity;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;

public class TwoTextArrayAdapter extends ArrayAdapter<TwoText> 

    private ArrayList<TwoText> classes;
    private Activity con;
    TextView seperator;

    public TwoTextArrayAdapter(Activity context, int textViewResourceId, ArrayList<TwoText> classes) 
        super(context, textViewResourceId, classes);
        this.con = context;
        this.classes = classes;

    

    @Override

    public View getView(int position, View convertView, ViewGroup parent) 

        View v = convertView;

        if (v == null) 

            LayoutInflater vi = (LayoutInflater) con.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

            v = vi.inflate(R.layout.my_list_item, null);

        

        TwoText user = classes.get(position);

        if (user != null) 

            TextView content1 = (TextView) v.findViewById(R.id.list_content1);

            TextView content2 = (TextView) v.findViewById(R.id.list_content2);

            if (content1 != null) 

                content1.setText(user.classID);
               
            if(content2 != null) 

                content2.setText(user.state);
            
        
        return v;
    

这是 my_list_item.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_
    android:layout_
    android:orientation="vertical" >

    <TextView
        style="?android:attr/listSeparatorTextViewStyle"
        android:id="@+id/separator"
        android:text="Header"
        android:layout_
        android:layout_
        android:background="#757678"
        android:textColor="#f5c227" />

    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_
        android:layout_
        android:orientation="horizontal" >

        <TextView
            android:id="@+id/list_content1"
            android:layout_
            android:layout_
            android:layout_margin="5dip"
            android:clickable="false"
            android:gravity="center"
            android:longClickable="false"
            android:paddingBottom="1dip"
            android:paddingTop="1dip"
            android:text="sample"
            android:textColor="#ff7f1d"
            android:textSize="17dip"
            android:textStyle="bold" />

        <TextView
            android:id="@+id/list_content2"
            android:layout_
            android:layout_
            android:layout_margin="5dip"
            android:clickable="false"
            android:gravity="center"
            android:linksClickable="false"
            android:longClickable="false"
            android:paddingBottom="1dip"
            android:paddingTop="1dip"
            android:text="sample"
            android:textColor="#6d6d6d"
            android:textSize="17dip" />
    </LinearLayout>

</LinearLayout>

我现在所做的是将标题添加为常规列表对象,但我希望它作为标题并且在我的情况下有一个日期。

我的 xml 中有此代码作为标题:

<TextView
        style="?android:attr/listSeparatorTextViewStyle"
        android:id="@+id/separator"
        android:text="Header"
        android:layout_
        android:layout_
        android:background="#757678"
        android:textColor="#f5c227" />

我尝试在不必要时隐藏它并在必要时显示它,但我只是弄乱了我的其余代码。我尝试了更多教程,但它们也有相同的效果。

谁能指导我如何做到这一点?

【问题讨论】:

【参考方案1】:

我是这样做的,键是Adapter 类中的getItemViewType 和getViewTypeCount。 getViewTypeCount 返回列表中有多少类型的项目,在这种情况下,我们有一个标题项目和一个事件项目,所以两个。 getItemViewType 应该返回我们在输入 position 处拥有的 View 的类型。

Android 会自动在convertView 中为您传递正确类型的View

以下代码的结果如下所示:

首先我们有一个接口,我们的两个列表项类型将实现

public interface Item 
    public int getViewType();
    public View getView(LayoutInflater inflater, View convertView);

然后我们有一个适配器,它接受Item 的列表

public class TwoTextArrayAdapter extends ArrayAdapter<Item> 
    private LayoutInflater mInflater;

    public enum RowType 
        LIST_ITEM, HEADER_ITEM
    

    public TwoTextArrayAdapter(Context context, List<Item> items) 
        super(context, 0, items);
        mInflater = LayoutInflater.from(context);
    

    @Override
    public int getViewTypeCount() 
        return RowType.values().length;

    

    @Override
    public int getItemViewType(int position) 
        return getItem(position).getViewType();
    
@Override
public View getView(int position, View convertView, ViewGroup parent) 
   return getItem(position).getView(mInflater, convertView);

编辑 Better For Performance.. 可以在滚动时注意到

private static final int TYPE_ITEM = 0; 
private static final int TYPE_SEPARATOR = 1; 

public View getView(int position, View convertView, ViewGroup parent)  
    ViewHolder holder = null;
    int rowType = getItemViewType(position);
    View View;
    if (convertView == null) 
        holder = new ViewHolder();
        switch (rowType) 
            case TYPE_ITEM:
                convertView = mInflater.inflate(R.layout.task_details_row, null);
                holder.View=getItem(position).getView(mInflater, convertView);
                break;
            case TYPE_SEPARATOR:
                convertView = mInflater.inflate(R.layout.task_detail_header, null);
                holder.View=getItem(position).getView(mInflater, convertView);
                break;
        
        convertView.setTag(holder);
    
    else
    
        holder = (ViewHolder) convertView.getTag();
    
    return convertView; 
 

public static class ViewHolder 
    public  View View;  

然后我们有实现Item 的类并膨胀正确的布局。在您的情况下,您将拥有类似 Header 类和 ListItem 类的东西。

   public class Header implements Item 
    private final String         name;

    public Header(String name) 
        this.name = name;
    

    @Override
    public int getViewType() 
        return RowType.HEADER_ITEM.ordinal();
    

    @Override
    public View getView(LayoutInflater inflater, View convertView) 
        View view;
        if (convertView == null) 
            view = (View) inflater.inflate(R.layout.header, null);
            // Do some initialization
         else 
            view = convertView;
        

        TextView text = (TextView) view.findViewById(R.id.separator);
        text.setText(name);

        return view;
    


然后是ListItem

    public class ListItem implements Item 
    private final String         str1;
    private final String         str2;

    public ListItem(String text1, String text2) 
        this.str1 = text1;
        this.str2 = text2;
    

    @Override
    public int getViewType() 
        return RowType.LIST_ITEM.ordinal();
    

    @Override
    public View getView(LayoutInflater inflater, View convertView) 
        View view;
        if (convertView == null) 
            view = (View) inflater.inflate(R.layout.my_list_item, null);
            // Do some initialization
         else 
            view = convertView;
        

        TextView text1 = (TextView) view.findViewById(R.id.list_content1);
        TextView text2 = (TextView) view.findViewById(R.id.list_content2);
        text1.setText(str1);
        text2.setText(str2);

        return view;
    


还有一个简单的Activity 来显示它

public class MainActivity extends ListActivity 

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

        List<Item> items = new ArrayList<Item>();
        items.add(new Header("Header 1"));
        items.add(new ListItem("Text 1", "Rabble rabble"));
        items.add(new ListItem("Text 2", "Rabble rabble"));
        items.add(new ListItem("Text 3", "Rabble rabble"));
        items.add(new ListItem("Text 4", "Rabble rabble"));
        items.add(new Header("Header 2"));
        items.add(new ListItem("Text 5", "Rabble rabble"));
        items.add(new ListItem("Text 6", "Rabble rabble"));
        items.add(new ListItem("Text 7", "Rabble rabble"));
        items.add(new ListItem("Text 8", "Rabble rabble"));

        TwoTextArrayAdapter adapter = new TwoTextArrayAdapter(this, items);
        setListAdapter(adapter);
    


R.layout.header 的布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_
    android:layout_
    android:orientation="horizontal" >

    <TextView
        style="?android:attr/listSeparatorTextViewStyle"
        android:id="@+id/separator"
        android:text="Header"
        android:layout_
        android:layout_
        android:background="#757678"
        android:textColor="#f5c227" />

</LinearLayout>

R.layout.my_list_item 的布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_
    android:layout_
    android:orientation="horizontal" >

    <TextView
        android:id="@+id/list_content1"
        android:layout_
        android:layout_
        android:layout_margin="5dip"
        android:clickable="false"
        android:gravity="center"
        android:longClickable="false"
        android:paddingBottom="1dip"
        android:paddingTop="1dip"
        android:text="sample"
        android:textColor="#ff7f1d"
        android:textSize="17dip"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/list_content2"
        android:layout_
        android:layout_
        android:layout_margin="5dip"
        android:clickable="false"
        android:gravity="center"
        android:linksClickable="false"
        android:longClickable="false"
        android:paddingBottom="1dip"
        android:paddingTop="1dip"
        android:text="sample"
        android:textColor="#6d6d6d"
        android:textSize="17dip" />

</LinearLayout>

R.layout.activity_main.xml 的布局

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_
    android:layout_
    tools:context=".MainActivity" >

    <ListView
        android:id="@android:id/list"
        android:layout_
        android:layout_ />

</RelativeLayout>

您还可以更高级地使用ViewHolders、异步加载内容或任何您喜欢的方式。

【讨论】:

喜欢您的解决方案,但由于您正在扩展 ArrayAdapter,因此您不应跟踪自己的项目列表。只需使用 ArrayAdapter 中的内部跟踪。否则,存储物品的内存量会增加一倍 优秀的解决方案。如果标题不可点击(可以在构造函数中设置),我只会将此添加到适配器中 @Override public boolean isEnabled(int position) return (getItem(position).getViewType() == RowType.LIST_ITEM .ordinal()); 滚动列表视图时行会随机化吗?谁能指导一下 为什么 Google 不能只用 3 行代码实现这一目标? 这个答案是我在 SO 上找到的最佳答案之一 - 清晰、简洁且解释清楚。但是,我确实遇到了 ListView 的结果是半随机顺序的问题(第一个标题和项目还可以,后面的项目搞砸了),我设法找出了问题所在。 'EDIT Better For Performance.. 可以在滚动时注意到'下面的代码块让我搞砸了 - 从自定义 ArrayAdapter 类中删除它为我解决了这个问题。我建议每个得到随机结果的人都试试这个。不过,感谢您的出色回答。真的帮了我大忙!【参考方案2】:

您可能正在寻找一个ExpandableListView,它具有用于分隔项目(子项)的标题(组)。

关于这个主题的好教程:here。

【讨论】:

我不希望它们可扩展 如果这是唯一的问题,您可以覆盖 onItemClick 方法以防止展开/折叠视图。 但我仍然需要它们可以点击以用于其他目的 Err...我实际上是想说onGroupClick 只处理点击“标题”,你不需要禁用点击或类似的东西,只需取消任何折叠操作并设置所有要从头开始扩展的组。 我同意ExpandableListView 在大多数情况下是最好的。但是,我有时想在我的活动中显示一个平面列表,并在其他时间显示一个带有标题的列表。可悲的是,ExpandableListAdapter 接口没有扩展ListAdapter 接口,因此对于多态性,我不得不使用@antew 的解决方案。【参考方案3】:

作为替代方案,有一个很好的3rd party library 专门为此用例设计。因此,您需要根据存储在适配器中的数据生成标头。它们被称为 Rolodex 适配器,与ExpandableListViews 一起使用。它们可以很容易地被定制成一个带有标题的普通列表。

使用 OP 的 Event 对象并知道标头基于与之关联的 Date...代码如下所示:

活动

    //There's no need to pre-compute what the headers are. Just pass in your List of objects. 
    EventDateAdapter adapter = new EventDateAdapter(this, mEvents);
    mExpandableListView.setAdapter(adapter);

适配器

private class EventDateAdapter extends NFRolodexArrayAdapter<Date, Event> 

    public EventDateAdapter(Context activity, Collection<Event> items) 
        super(activity, items);
    

    @Override
    public Date createGroupFor(Event childItem) 
        //This is how the adapter determines what the headers are and what child items belong to it
        return (Date) childItem.getDate().clone();
    

    @Override
    public View getChildView(LayoutInflater inflater, int groupPosition, int childPosition,
                             boolean isLastChild, View convertView, ViewGroup parent) 
        //Inflate your view

        //Gets the Event data for this view
        Event event = getChild(groupPosition, childPosition);

        //Fill view with event data
    

    @Override
    public View getGroupView(LayoutInflater inflater, int groupPosition, boolean isExpanded,
                             View convertView, ViewGroup parent) 
        //Inflate your header view

        //Gets the Date for this view
        Date date = getGroup(groupPosition);

        //Fill view with date data
    

    @Override
    public boolean hasAutoExpandingGroups() 
        //This forces our group views (headers) to always render expanded.
        //Even attempting to programmatically collapse a group will not work.
        return true;
    

    @Override
    public boolean isGroupSelectable(int groupPosition) 
        //This prevents a user from seeing any touch feedback when a group (header) is clicked.
        return false;
    

【讨论】:

【参考方案4】:

我做了什么来将日期(例如 2016 年 12 月 1 日)作为标题。 我使用了 StickyHeaderListView 库

https://github.com/emilsjolander/StickyListHeaders

将日期转换为以毫秒为单位的长[不包括时间]并将其作为标题ID。

@Override
public long getHeaderId(int position) 
    return <date in millis>;

【讨论】:

【参考方案5】:

Here is a sample project,基于 antew 的详细而有用的答案,实现了一个带有多个标题的 ListView,其中包含视图持有者以提高滚动性能。

在这个项目中,ListView 中表示的对象是类HeaderItem 或类RowItem 的实例,它们都是抽象类Item 的子类。 Item 的每个子类对应于自定义适配器中的不同视图类型ItemAdapterItemAdapter 上的 getView() 方法将每个列表项的视图创建委托给 HeaderItemRowItem 上的个性化 getView() 方法,具体取决于传递给getView() 适配器上的方法。每个Item 子类都提供了自己的视图持有者。

视图持有者的实现如下。 Item 子类上的 getView() 方法检查传递给 ItemAdapter 上的 getView() 方法的 View 对象是否为空。如果是这样,适当的布局被膨胀,并且视图持有者对象被实例化并通过View.setTag()与膨胀的视图相关联。如果View 对象不为空,则视图持有者对象已经与视图相关联,并且通过View.getTag() 检索视图持有者。视图持有者的使用方式可以在HeaderItem的以下代码sn-p中看到:

@Override
View getView(LayoutInflater i, View v) 
    ViewHolder h;
    if (v == null) 
        v = i.inflate(R.layout.header, null);
        h = new ViewHolder(v);
        v.setTag(h);
     else 
        h = (ViewHolder) v.getTag();
    
    h.category.setText(text());
    return v;


private class ViewHolder 
    final TextView category;

    ViewHolder(View v) 
        category = v.findViewById(R.id.category);
    

ListView 的完整实现如下。这是Java代码:

import android.app.ListActivity;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class MainActivity extends ListActivity 
    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setListAdapter(new ItemAdapter(getItems()));
    

    class ItemAdapter extends ArrayAdapter<Item> 
        final private List<Class<?>> viewTypes;

        ItemAdapter(List<Item> items) 
            super(MainActivity.this, 0, items);
            if (items.contains(null))
                throw new IllegalArgumentException("null item");
            viewTypes = getViewTypes(items);
        

        private List<Class<?>> getViewTypes(List<Item> items) 
            Set<Class<?>> set = new HashSet<>();
            for (Item i : items) 
                set.add(i.getClass());
            List<Class<?>> list = new ArrayList<>(set);
            return Collections.unmodifiableList(list);
        

        @Override
        public int getViewTypeCount() 
            return viewTypes.size();
        

        @Override
        public int getItemViewType(int position) 
            Item t = getItem(position);
            return viewTypes.indexOf(t.getClass());
        

        @Override
        public View getView(int position, View v, ViewGroup unused) 
            return getItem(position).getView(getLayoutInflater(), v);
        
    

    abstract private class Item 
        final private String text;

        Item(String text) 
            this.text = text;
        

        String text()  return text; 

        abstract View getView(LayoutInflater i, View v);
    

    private class HeaderItem extends Item 
        HeaderItem(String text) 
            super(text);
        

        @Override
        View getView(LayoutInflater i, View v) 
            ViewHolder h;
            if (v == null) 
                v = i.inflate(R.layout.header, null);
                h = new ViewHolder(v);
                v.setTag(h);
             else 
                h = (ViewHolder) v.getTag();
            
            h.category.setText(text());
            return v;
        

        private class ViewHolder 
            final TextView category;

            ViewHolder(View v) 
                category = v.findViewById(R.id.category);
            
        
    

    private class RowItem extends Item 
        RowItem(String text) 
            super(text);
        

        @Override
        View getView(LayoutInflater i, View v) 
            ViewHolder h;
            if (v == null) 
                v = i.inflate(R.layout.row, null);
                h = new ViewHolder(v);
                v.setTag(h);
             else 
                h = (ViewHolder) v.getTag();
            
            h.option.setText(text());
            return v;
        

        private class ViewHolder 
            final TextView option;

            ViewHolder(View v) 
                option = v.findViewById(R.id.option);
            
        
    

    private List<Item> getItems() 
        List<Item> t = new ArrayList<>();
        t.add(new HeaderItem("Header 1"));
        t.add(new RowItem("Row 2"));
        t.add(new HeaderItem("Header 3"));
        t.add(new RowItem("Row 4"));

        t.add(new HeaderItem("Header 5"));
        t.add(new RowItem("Row 6"));
        t.add(new HeaderItem("Header 7"));
        t.add(new RowItem("Row 8"));

        t.add(new HeaderItem("Header 9"));
        t.add(new RowItem("Row 10"));
        t.add(new HeaderItem("Header 11"));
        t.add(new RowItem("Row 12"));

        t.add(new HeaderItem("Header 13"));
        t.add(new RowItem("Row 14"));
        t.add(new HeaderItem("Header 15"));
        t.add(new RowItem("Row 16"));

        t.add(new HeaderItem("Header 17"));
        t.add(new RowItem("Row 18"));
        t.add(new HeaderItem("Header 19"));
        t.add(new RowItem("Row 20"));

        t.add(new HeaderItem("Header 21"));
        t.add(new RowItem("Row 22"));
        t.add(new HeaderItem("Header 23"));
        t.add(new RowItem("Row 24"));

        t.add(new HeaderItem("Header 25"));
        t.add(new RowItem("Row 26"));
        t.add(new HeaderItem("Header 27"));
        t.add(new RowItem("Row 28"));
        t.add(new RowItem("Row 29"));
        t.add(new RowItem("Row 30"));

        t.add(new HeaderItem("Header 31"));
        t.add(new RowItem("Row 32"));
        t.add(new HeaderItem("Header 33"));
        t.add(new RowItem("Row 34"));
        t.add(new RowItem("Row 35"));
        t.add(new RowItem("Row 36"));

        return t;
    


还有两个列表项布局,每个 Item 子类一个。这是 HeaderItem 使用的布局header

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_
    android:layout_
    android:background="#FFAAAAAA"
    >
    <TextView
        android:id="@+id/category"
        android:layout_
        android:layout_
        android:layout_margin="4dp"
        android:textColor="#FF000000"
        android:textSize="20sp"
        android:textStyle="bold"
        />
 </LinearLayout>

这是 RowItem 使用的布局 row

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_
    android:layout_
    android:minHeight="?android:attr/listPreferredItemHeight"
    >
    <TextView
        android:id="@+id/option"
        android:layout_
        android:layout_
        android:textSize="15sp"
        />
</LinearLayout>

这是生成的 ListView 的一部分的图像:

【讨论】:

以上是关于Android ListView 标头的主要内容,如果未能解决你的问题,请参考以下文章

Android 添加动态 ListView 标头

android问题及其解决-优化listView卡顿和怎样禁用ListView的fling

Android App 在片段中创建 ListView 引用时关闭

Android ListView随手缓慢滑动时有残影现象

Android——ViewHolder的作用与用法

如何在app小部件中使用ListView并在android中填充数据?