使用包含节标题的自定义列表适配器自定义ListView过滤

时间:2017-05-27 11:36:57

标签: android listview android-filterable

我正在尝试使用自定义适配器过滤自定义列表视图。当搜索参数更改或变为空时,我在复制原始数据并将其放回列表时遇到问题。过滤适用于第一个输入字符,但如果更改,则不会再次搜索整个数据集。我知道这是因为我需要一份原始数据的重复列表,但我无法真正开始工作,因为我不知道如何正确实现它,因为我使用自定义类作为我的数据类型。我只使用它的名称和类别属性,名称是实际项目,它也按类别排序。

我的适配器不在此示例中:https://gist.github.com/fjfish/3024308

这是List Adapter的代码:

class DataListAdapter extends BaseAdapter implements Filterable {

    private Context mContext;
    private List<Object> originalData = null;
    private List<Object> filteredData = null;
    private static final int CARRIER = 0;
    private static final int HEADER = 1;
    private LayoutInflater inflater;
    private ItemFilter mFilter = new ItemFilter();

    DataListAdapter(Context context, List<Object> input) {
        this.mContext = context;
        this.originalData = input;
        this.filteredData = input;
        this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }

    @Override
    public int getItemViewType(int position) {
        if (originalData.get(position) instanceof Carrier) {
            return CARRIER;
        } else {
            return HEADER;
        }
    }

    @Override
    public int getViewTypeCount() {
        return 2;
    }

    @Override
    public int getCount() {
        return originalData.size();
    }

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

    @Override
    public long getItemId(int position) {
        return position;
    }

    @SuppressLint("InflateParams")
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        if (convertView == null) {
            switch (getItemViewType(position)) {
                case CARRIER:
                    convertView = inflater.inflate(R.layout.listview_item_data_layout, null);
                    break;
                case HEADER:
                    convertView = inflater.inflate(R.layout.listview_header_data_layout, null);
                    break;
            }
        }

        switch (getItemViewType(position)) {
            case CARRIER:
                TextView name = (TextView) convertView.findViewById(R.id.fragment_data_list_view_carrier_name);
                name.setText(((Carrier) originalData.get(position)).get_name());
                break;
            case HEADER:
                TextView category = (TextView) convertView.findViewById(R.id.fragment_data_list_view_category);
                category.setText((String) originalData.get(position));
                break;
        }

        return convertView;
    }

    @Override
    public Filter getFilter() {
        return mFilter;
    }

    private class ItemFilter extends Filter {
        @Override
        protected FilterResults performFiltering(CharSequence constraint) {

            DatabaseHelper dbHelper = new DatabaseHelper(mContext, null, null, 1);
            String filterString = constraint.toString().toLowerCase();
            FilterResults results = new FilterResults();
            final List<Object> list = originalData;
            int count = list.size();
            final List<Object> nlist = new ArrayList<>(count);
            String filterableString = "";

            for (int i = 0; i < count; i++) {
                switch (getItemViewType(i)) {
                    case CARRIER:
                        filterableString = ((Carrier)list.get(i)).get_name();
                        break;
                    case HEADER:
                        filterableString = "";
                        break;
                }
                if(filterableString.toLowerCase().contains(filterString)) {
                    nlist.add(dbHelper.getCarriersWithName(filterableString).get(0));
                }
            }

            results.values = nlist;
            results.count = nlist.size();

            return results;
        }

        @SuppressWarnings("unchecked")
        @Override
        protected void publishResults(CharSequence constraint, FilterResults results) {
            if(results.count == 0) {
                notifyDataSetInvalidated();
            } else {
                originalData = (List<Object>)results.values;
                notifyDataSetChanged();
            }

        }
    }
}

我的主要活动显然看起来像这样,应该没问题。问题出现在过滤后的数据列表中,我无法开始工作。

List<Object> combinedCategoryCarrierList = dbHelper.getCombinedCategoryCarrierList();
adapter = new DataListAdapter(mContext, combinedCategoryCarrierList);
listView.setAdapter(adapter);

listView.setTextFilterEnabled(true);

searchEditText = (EditText) view.findViewById(R.id.fragment_data_search);
searchEditText.addTextChangedListener(new TextWatcher() {
    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {

    }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {

    }

    @Override
    public void afterTextChanged(Editable s) {
        adapter.getFilter().filter(searchEditText.getText().toString());
    }
});

如果有人能向我展示如何使用自定义数据类型和节标题组合的示例,我将不胜感激。甚至改变我的代码:)我无法找到所有这些都适用的例子。

修改: The screen looks like this, so I want to keep the category headers when filtering.

1 个答案:

答案 0 :(得分:2)

我没有找到原始问题的解决方案,但我想出了一个更好的解决方案。我不知道Android中有ExpandableListView可用。这基本上是一个ListView,但这些项目分为组和它们的Childs,它们是可扩展和可折叠的,所以正是我想要的。

以下是我使用工作过滤器和组实现它的方法:

所以,首先,这是我的主要布局文件。请注意我正在使用Fragments,这就是为什么代码在获取上下文方面有点不同。该组件的功能保持不变。

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <EditText
        android:id="@+id/fragment_data_search"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="text"
        android:hint="@string/data_search_hint"
        android:layout_marginTop="8dp"
        android:layout_marginBottom="8dp"
        android:layout_marginStart="10dp"
        android:layout_marginEnd="10dp" />

    <ExpandableListView
        android:id="@+id/fragment_data_expandable_list_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:groupIndicator="@null" />

</LinearLayout>

您的标题/组项目和子项目还需要两个布局文件。我的标题项有一个显示类别名称的TextView和一个显示+或 - 的ImageView,用于显示类别是折叠还是展开。

这是我的标题布局文件:

<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/colorAccent"
    android:descendantFocusability="blocksDescendants" >

    <TextView
        android:id="@+id/fragment_data_list_view_category"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentStart="true"
        android:gravity="start"
        android:textStyle="bold"
        android:textSize="18sp"
        android:paddingStart="16dp"
        android:paddingEnd="16dp"
        android:paddingBottom="8dp"
        android:paddingTop="8dp"
        android:textColor="@android:color/primary_text_light"
        android:text="@string/placeholder_header_listview"
        android:maxLines="1"
        android:ellipsize="end" />

    <ImageView
        android:id="@+id/fragment_data_list_view_category_icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:layout_gravity="end"
        android:paddingStart="16dp"
        android:paddingEnd="16dp"
        android:paddingBottom="8dp"
        android:paddingTop="8dp"
        android:contentDescription="@string/content_description_list_view_header"
        android:src="@drawable/ic_remove_black_24dp"
        android:tag="maximized"/>

</RelativeLayout>

当我尝试设置onItemClickListener时,属性android:descendantFocusability="blocksDescendants"修复了一个错误。如果您遇到此问题,请尝试将RelativeLayout用于您的子布局(如果您尚未使用)。它为我修复了它,onClickItemListener没有使用LinearLayout执行。

这是我的子项目的布局文件:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:paddingStart="16dp"
    android:paddingEnd="16dp"
    android:paddingTop="8dp"
    android:paddingBottom="8dp"
    android:descendantFocusability="blocksDescendants" >

        <TextView
            android:id="@+id/fragment_data_list_view_carrier_name"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/placeholder_item_listview"
            android:textSize="18sp"
            android:textStyle="normal"
            android:textColor="@android:color/primary_text_light"
            android:maxLines="1"
            android:ellipsize="end" />

</RelativeLayout>

以下代码来自我的fragment类,它处理ExpandableListView的所有逻辑:

public class Fragment_Data extends Fragment {

    private Context mContext;

    private ExpandableListView expandableListView;
    private List<String> categories_list;
    private HashMap<String, List<Carrier>> carriers_list;
    private DataExpandableListAdapter adapter;

    private DatabaseHelper dbHelper;

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        getActivity().setTitle(R.string.nav_item_data);
    }

第一部分显示了所需变量的声明和必要的方法onViewCreatedCarrier类是一个自定义对象,具有名称,类别等属性。 DatabaseHelper也是一个自定义类,它处理我的数据库并为我获取所有数据,这些数据被转换为Carrier对象。您当然可以使用任何您喜欢的数据类型。

@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {

    View view = inflater.inflate(R.layout.fragment_data_layout, container, false);
    mContext = getContext();
    expandableListView = (ExpandableListView) view.findViewById(R.id.fragment_data_expandable_list_view);
    dbHelper = new DatabaseHelper(mContext, null, null, 1);

    adapter = new DataExpandableListAdapter(mContext, categories_list, carriers_list);

    displayList();

    expandAllGroups();

    EditText searchEditText = (EditText) view.findViewById(R.id.fragment_data_search);
    searchEditText.addTextChangedListener(new TextWatcher() {
        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {

        }

        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
            adapter.filterData(s.toString());
            expandAllGroups();
        }

        @Override
        public void afterTextChanged(Editable s) {

        }
    });

    expandableListView.setOnItemLongClickListener(deleteSelectedItem);
    expandableListView.setOnChildClickListener(editSelectedItem);

    return view;
}

onCreate方法处理所有重要的事情,例如设置适配器,为项目充气布局和设置onClick事件以及搜索字段的onTextChange事件。

private void expandAllGroups() {
    for(int i = 0; i < adapter.getGroupCount(); i++) {
        expandableListView.expandGroup(i);
    }
}

private void displayList() {
    prepareListData();

    adapter = new DataExpandableListAdapter(mContext, categories_list, carriers_list);
    expandableListView.setAdapter(adapter);

    expandAllGroups();
}

private void prepareListData() {
    categories_list = new ArrayList<>();
    carriers_list = new HashMap<>();

    categories_list = dbHelper.getCategoryList();

    for(int i = 0; i < categories_list.size(); i++) {
        List<Carrier> carrierList = dbHelper.getCarriersWithCategory(categories_list.get(i));
        carriers_list.put(categories_list.get(i), carrierList);
    }
}

使用expandAllGroups(),您只需展开所有群组,因为默认情况下它们会折叠。 displayList()只为ExpandableListView设置适配器并调用prepareListData(),它填充类别(组)列表和运营商(子)列表。请注意,子List是一个散列映射,其中键是类别,值本身是载波列表,因此适配器知道哪些子项属于哪个子项。

以下是Adapter的代码:

class DataExpandableListAdapter extends BaseExpandableListAdapter {

private Context mContext;
private List<String> list_categories = new ArrayList<>();
private List<String> list_categories_original = new ArrayList<>();
private HashMap<String, List<Carrier>> list_carriers = new HashMap<>();
private HashMap<String, List<Carrier>> list_carriers_original = new HashMap<>();

DataExpandableListAdapter(Context context, List<String> categories, HashMap<String, List<Carrier>> carriers) {
    this.mContext = context;
    this.list_categories = categories;
    this.list_categories_original = categories;
    this.list_carriers = carriers;
    this.list_carriers_original = carriers;
}

如果要使用过滤,则需要拥有两个原始列表的副本。这用于在搜索查询为空或再次或简单地不同时还原所有数据。过滤器将删除与原始列表不匹配的所有项目。

@Override
public int getGroupCount() {
    return this.list_categories.size();
}

@Override
public int getChildrenCount(int groupPosition) {
    return this.list_carriers.get(this.list_categories.get(groupPosition)).size();
}

@Override
public Object getGroup(int groupPosition) {
    return this.list_categories.get(groupPosition);
}

@Override
public Object getChild(int groupPosition, int childPosition) {
    return this.list_carriers.get(this.list_categories.get(groupPosition)).get(childPosition);
}

@Override
public long getGroupId(int groupPosition) {
    return groupPosition;
}

@Override
public long getChildId(int groupPosition, int childPosition) {
    return childPosition;
}

@Override
public boolean hasStableIds() {
    return true;
}

@Override
    public boolean isChildSelectable(int groupPosition, int childPosition) {
        return true;
    }

展开BaseExpandableListAdapter时,需要覆盖这些方法。您可以使用类似的内容替换所有return null;语句,具体取决于您的数据。

@SuppressLint("InflateParams")
@Override
public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {

    String headerTitle = (String) getGroup(groupPosition);

    if (convertView == null) {
        LayoutInflater inflater = (LayoutInflater) this.mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        convertView = inflater.inflate(R.layout.listview_header_data_layout, null);
    }

    TextView lblListHeader = (TextView) convertView.findViewById(R.id.fragment_data_list_view_category);
    lblListHeader.setText(headerTitle);

    ImageView expandIcon = (ImageView) convertView.findViewById(R.id.fragment_data_list_view_category_icon);
    if(isExpanded) {
        expandIcon.setImageResource(R.drawable.ic_remove_black_24dp);
    } else {
        expandIcon.setImageResource(R.drawable.ic_add_black_24dp);
    }

    return convertView;
}

此覆盖方法只是为每个标题/组/类别项目的布局增加,并根据组的状态(如果它已折叠或展开)设置文本和图像。

@SuppressLint("InflateParams")
@Override
public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {

        final String carrierName = ((Carrier)getChild(groupPosition, childPosition)).get_name();

        if (convertView == null) {
            LayoutInflater inflater = (LayoutInflater) this.mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            convertView = inflater.inflate(R.layout.listview_item_data_layout, null);
        }

        TextView txtListChild = (TextView) convertView.findViewById(R.id.fragment_data_list_view_carrier_name);

        txtListChild.setText(carrierName);
        return convertView;
}

与儿童用品相同。

现在终于过滤了: 我使用此自定义方法筛选出我需要与搜索查询匹配的所有项目。请记住,每次EditText的文本更改时都会调用此方法。

void filterData(String query) {
        query = query.toLowerCase();
        list_categories = new ArrayList<>(); 
        list_carriers = new HashMap<>();

    DatabaseHelper dbHelper = new DatabaseHelper(mContext, null, null, 1);

    if(query.trim().isEmpty()) {
        list_categories = new ArrayList<>(list_categories_original);
        list_carriers = new HashMap<>(list_carriers_original);
        notifyDataSetInvalidated();
    }
    else {
        //Filter all data with the given search query. Yes, it's complicated
        List<String> new_categories_list = new ArrayList<>();
        HashMap<String, List<Carrier>> new_carriers_list = new HashMap<>();
        List<String> all_categories_list = dbHelper.getCategoryList();
        for(int i = 0; i < all_categories_list.size(); i++) {
            List<Carrier> carriersWithCategoryList = dbHelper.getCarriersWithCategory(all_categories_list.get(i));
            List<Carrier> matchingCarriersInCategory = new ArrayList<>();
            for(Carrier carrierInCategory : carriersWithCategoryList) {
                if(carrierInCategory.get_name().toLowerCase().contains(query)) {
                    matchingCarriersInCategory.add(carrierInCategory);
                    if(!new_categories_list.contains(all_categories_list.get(i))) {
                        new_categories_list.add(all_categories_list.get(i));
                    }
                }
            }
            new_carriers_list.put(all_categories_list.get(i), matchingCarriersInCategory);
        }

        if(new_categories_list.size() > 0 && new_carriers_list.size() > 0) {
            list_categories.clear();
            list_categories.addAll(new_categories_list);
            list_carriers.clear();
            list_carriers.putAll(new_carriers_list);
        }

        notifyDataSetChanged();
    }
}`

这可能非常令人困惑,但由于我的数据结构,在我的情况下需要这么复杂。在你的情况下可能会更容易。

这基本上是做的,它首先检查搜索查询是否为空。如果它为空,则将两个列表重置为我在构造函数中指定的“备份”列表。然后我打电话给notifyDataSetInvalidated();告诉适配器它的内容将被重新填充。它可能与notifyDataSetChanged();一起使用,我没有测试过,但它应该是因为我们将原始列表设置回原来的状态。

现在,如果搜索查询不为空,我会浏览每个类别,看看该特定类别是否包含与搜索查询匹配的任何项目。如果是这种情况,那么该项目将被添加到新的子列表中,并且它的类别/父级也将被添加到新的父级列表中(如果它尚未存在)。

最后但并非最不重要的是,该方法检查两个列表是否都不为空。如果它们不为空,则清空原始列表并放入新的过滤数据,并通过调用notifyDataSetChanged();通知适配器

我希望这对任何人都有帮助。