需求:單一畫面顯示多組資料,每組包含標題與多行明細,使用者嘗試用「大 ListView 嵌套小 ListView」實現,導致資料錯亂、位置偏移、回收機制失效。
Android `BaseAdapter` 提供 `getViewTypeCount()` 與 `getItemViewType(position)` 機制,支援同一列表內不同佈局:
// 基礎介面
interface ListItem {
int getType();
}
// 區塊標題
class SectionHeader implements ListItem {
String title;
public int getType() { return 0; }
}
// 明細項目
class DetailItem implements ListItem {
String name, price;
public int getType() { return 1; }
}
// 使用 List<ListItem> 作為統一資料源
List<ListItem> items = new ArrayList<>();
items.add(new SectionHeader("湘菜"));
items.add(new DetailItem("小炒肉", "28元"));
items.add(new DetailItem("紅燒肉", "48元"));
items.add(new SectionHeader("川菜"));
items.add(new DetailItem("麻婆豆腐", "18元"));
public class MultiTypeAdapter extends BaseAdapter {
private final List<ListItem> data;
private static final int TYPE_HEADER = 0;
private static final int TYPE_DETAIL = 1;
public MultiTypeAdapter(List<ListItem> data) { this.data = data; }
@Override public int getViewTypeCount() { return 2; }
@Override public int getItemViewType(int position) {
return data.get(position).getType();
}
@Override public int getCount() { return data.size(); }
@Override public Object getItem(int p) { return data.get(p); }
@Override public long getItemId(int p) { return p; }
@Override public View getView(int position, View convertView, ViewGroup parent) {
int type = getItemViewType(position);
ViewHolder holder;
if (convertView == null) {
holder = new ViewHolder();
if (type == TYPE_HEADER) {
convertView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_header, parent, false);
holder.tvTitle = convertView.findViewById(R.id.tv_header);
} else {
convertView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_detail, parent, false);
holder.tvName = convertView.findViewById(R.id.tv_name);
holder.tvPrice = convertView.findViewById(R.id.tv_price);
}
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
ListItem item = data.get(position);
if (type == TYPE_HEADER) {
holder.tvTitle.setText(((SectionHeader)item).title);
} else {
DetailItem d = (DetailItem) item;
holder.tvName.setText(d.name);
holder.tvPrice.setText(d.price);
}
return convertView;
}
static class ViewHolder {
TextView tvTitle, tvName, tvPrice;
}
}
<!-- item_header.xml -->
<TextView android:id="@+id/tv_header" style="@style/SectionHeader" />
<!-- item_detail.xml -->
<LinearLayout android:orientation="horizontal">
<TextView android:id="@+id/tv_name" layout_weight="1"/>
<TextView android:id="@+id/tv_price" layout_width="wrap_content"/>
</LinearLayout>
同樣原理適用於 `RecyclerView.Adapter`,且效能更佳:
public class MultiTypeRecyclerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
// ViewHolder 分類
static class HeaderVH extends RecyclerView.ViewHolder { TextView tv; ... }
static class DetailVH extends RecyclerView.ViewHolder { TextView name, price; ... }
@Override public int getItemViewType(int pos) { return data.get(pos).getType(); }
@Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == TYPE_HEADER)
return new HeaderVH(inflate(R.layout.item_header, parent));
else
return new DetailVH(inflate(R.layout.item_detail, parent));
}
@Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int pos) {
ListItem item = data.get(pos);
if (holder instanceof HeaderVH) ((HeaderVH)holder).bind((SectionHeader)item);
else ((DetailVH)holder).bind((DetailItem)item);
}
}
| 方案 | 複雜度 | 效能 | 維護性 | 推薦度 |
|---|---|---|---|---|
| ListView 嵌套 ListView | 低(表象) | 極差 | 極差 | ❌ 禁止 |
| 單一 ListView + ViewType | 中 | 良好 | 良好 | ✅ 可用 |
| RecyclerView + 多型 ViewHolder | 中高 | 最佳 | 最佳 | 🌟 推薦 |