You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Firebase按日期分组返回记录,优化Android足球赛程ListView UI

嘿,这个同日期赛程分组的需求太常见啦!我给你分享两种适配现有ListView场景的实现方案,你可以根据需求挑选:

方案一:用ExpandableListView实现分组(推荐,原生支持展开收起)

ExpandableListView是Android原生控件,天生支持“组-子项”的结构,完美匹配你要的分组展示效果,还自带展开/收起的交互,不用自己折腾逻辑。

步骤1:预处理Firebase数据,按日期分组

从Firebase拿到所有赛程数据后,先把同日期的赛程归为一组,用TreeMap可以自动按日期排序,避免分组顺序混乱:

// 假设你的赛程实体类是Match,包含date、team1、team2、matchTime等字段
List<Match> allMatches = fetchMatchesFromFirebase(); // 从Firebase获取原始数据
// TreeMap会自动按key(日期)排序,保证分组的时间顺序
Map<String, List<Match>> groupedMatches = new TreeMap<>();

for (Match match : allMatches) {
    String matchDate = match.getDate(); // 确保日期格式统一,比如"2024-05-20"
    if (!groupedMatches.containsKey(matchDate)) {
        groupedMatches.put(matchDate, new ArrayList<>());
    }
    groupedMatches.get(matchDate).add(match);
}

// 提取所有日期作为分组标题的数据源
List<String> dateGroups = new ArrayList<>(groupedMatches.keySet());

步骤2:自定义ExpandableListAdapter

继承BaseExpandableListAdapter,分别实现分组标题(日期)和子项(赛程)的视图绑定:

public class MatchExpandableAdapter extends BaseExpandableListAdapter {
    private Context mContext;
    private List<String> mDateGroups;
    private Map<String, List<Match>> mGroupedMatches;

    public MatchExpandableAdapter(Context context, List<String> dateGroups, Map<String, List<Match>> groupedMatches) {
        mContext = context;
        mDateGroups = dateGroups;
        mGroupedMatches = groupedMatches;
    }

    // 返回分组总数
    @Override
    public int getGroupCount() {
        return mDateGroups.size();
    }

    // 返回某分组下的子项数
    @Override
    public int getChildrenCount(int groupPosition) {
        String targetDate = mDateGroups.get(groupPosition);
        return mGroupedMatches.get(targetDate).size();
    }

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

    @Override
    public Object getChild(int groupPosition, int childPosition) {
        String targetDate = mDateGroups.get(groupPosition);
        return mGroupedMatches.get(targetDate).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 false;
    }

    // 绑定分组标题视图(日期栏)
    @Override
    public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
        String date = (String) getGroup(groupPosition);
        if (convertView == null) {
            LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            convertView = inflater.inflate(R.layout.item_group_date, parent, false);
        }

        TextView tvDateTitle = convertView.findViewById(R.id.tv_date_title);
        ImageView ivExpandArrow = convertView.findViewById(R.id.iv_expand_arrow);

        tvDateTitle.setText(date);
        // 根据展开状态切换箭头图标
        ivExpandArrow.setImageResource(isExpanded ? R.drawable.ic_arrow_up : R.drawable.ic_arrow_down);

        return convertView;
    }

    // 绑定子项视图(单条赛程)
    @Override
    public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
        Match match = (Match) getChild(groupPosition, childPosition);
        if (convertView == null) {
            LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            // 这里用你原来的单条赛程布局即可
            convertView = inflater.inflate(R.layout.item_single_match, parent, false);
        }

        TextView tvTeam1 = convertView.findViewById(R.id.tv_team1);
        TextView tvTeam2 = convertView.findViewById(R.id.tv_team2);
        TextView tvMatchTime = convertView.findViewById(R.id.tv_match_time);

        tvTeam1.setText(match.getTeam1());
        tvTeam2.setText(match.getTeam2());
        tvMatchTime.setText(match.getMatchTime());

        return convertView;
    }

    @Override
    public boolean isChildSelectable(int groupPosition, int childPosition) {
        return true; // 允许点击单条赛程
    }
}

步骤3:布局文件调整

把原来的ListView换成ExpandableListView,并分别创建分组标题布局和子项布局:

分组标题布局(item_group_date.xml)

<?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="wrap_content"
    android:padding="16dp"
    android:background="#f8f8f8"
    android:orientation="horizontal"
    android:gravity="center_vertical">

    <TextView
        android:id="@+id/tv_date_title"
        android:layout_width="0dp"
        android:layout_weight="1"
        android:layout_height="wrap_content"
        android:textSize="16sp"
        android:textStyle="bold"/>

    <ImageView
        android:id="@+id/iv_expand_arrow"
        android:layout_width="24dp"
        android:layout_height="24dp"
        android:src="@drawable/ic_arrow_down"/>
</LinearLayout>

单条赛程布局(item_single_match.xml)

直接复用你原来的ListView项布局就好,不用改。

步骤4:在Activity/Fragment中绑定Adapter

ExpandableListView expandableListView = findViewById(R.id.expandable_list_match);
MatchExpandableAdapter adapter = new MatchExpandableAdapter(this, dateGroups, groupedMatches);
expandableListView.setAdapter(adapter);

// 可选:默认展开所有分组
for (int i = 0; i < adapter.getGroupCount(); i++) {
    expandableListView.expandGroup(i);
}
方案二:继续使用普通ListView,自定义混合Adapter

如果不想替换现有ListView控件,可以把日期标题作为特殊的列表项,和赛程项混合在同一个数据源里,通过Adapter判断类型来加载不同视图。

步骤1:构建混合数据源

List<Object> mixedMatchList = new ArrayList<>();
for (String date : dateGroups) {
    mixedMatchList.add(date); // 添加日期标题项
    mixedMatchList.addAll(groupedMatches.get(date)); // 添加当天所有赛程
}

步骤2:自定义ListView Adapter

public class MatchGroupAdapter extends BaseAdapter {
    private Context mContext;
    private List<Object> mMixedList;
    private LayoutInflater mInflater;

    public MatchGroupAdapter(Context context, List<Object> mixedList) {
        mContext = context;
        mMixedList = mixedList;
        mInflater = LayoutInflater.from(context);
    }

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

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

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

    // 区分两种item类型:日期标题、赛程项
    @Override
    public int getItemViewType(int position) {
        return mMixedList.get(position) instanceof String ? 0 : 1;
    }

    @Override
    public int getViewTypeCount() {
        return 2; // 两种视图类型
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        int viewType = getItemViewType(position);
        if (viewType == 0) {
            // 日期标题项
            DateViewHolder dateHolder;
            if (convertView == null || !(convertView.getTag() instanceof DateViewHolder)) {
                convertView = mInflater.inflate(R.layout.item_group_date, parent, false);
                dateHolder = new DateViewHolder();
                dateHolder.tvDate = convertView.findViewById(R.id.tv_date_title);
                convertView.setTag(dateHolder);
            } else {
                dateHolder = (DateViewHolder) convertView.getTag();
            }
            dateHolder.tvDate.setText((String) mMixedList.get(position));
        } else {
            // 赛程项
            MatchViewHolder matchHolder;
            Match match = (Match) mMixedList.get(position);
            if (convertView == null || !(convertView.getTag() instanceof MatchViewHolder)) {
                convertView = mInflater.inflate(R.layout.item_single_match, parent, false);
                matchHolder = new MatchViewHolder();
                matchHolder.tvTeam1 = convertView.findViewById(R.id.tv_team1);
                matchHolder.tvTeam2 = convertView.findViewById(R.id.tv_team2);
                matchHolder.tvMatchTime = convertView.findViewById(R.id.tv_match_time);
                convertView.setTag(matchHolder);
            } else {
                matchHolder = (MatchViewHolder) convertView.getTag();
            }
            matchHolder.tvTeam1.setText(match.getTeam1());
            matchHolder.tvTeam2.setText(match.getTeam2());
            matchHolder.tvMatchTime.setText(match.getMatchTime());
        }
        return convertView;
    }

    // 定义两个ViewHolder,优化列表性能
    static class DateViewHolder {
        TextView tvDate;
    }

    static class MatchViewHolder {
        TextView tvTeam1;
        TextView tvTeam2;
        TextView tvMatchTime;
    }
}

步骤3:绑定Adapter到原ListView

ListView matchListView = findViewById(R.id.list_match);
MatchGroupAdapter adapter = new MatchGroupAdapter(this, mixedMatchList);
matchListView.setAdapter(adapter);
注意事项
  1. 日期格式统一:确保从Firebase获取的日期格式一致(比如yyyy-MM-dd),这样TreeMap才能正确按时间排序分组。如果日期格式不统一,建议先转换成标准格式再分组。
  2. 性能优化:两种方案都用到了ViewHolder,避免重复创建视图,保证列表滚动流畅。
  3. 交互需求:如果需要展开收起功能,优先选方案一;如果只是静态分组展示,方案二更轻量。

内容的提问来源于stack exchange,提问作者Carl Bruiners

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.21 08:42:34