如何开发Android蓝牙设置风格的双ListView单Activity页面?
嘿,这个场景我太熟悉了——蓝牙设置里的「已配对设备」+「新发现设备」双列表,确实不能把ListView硬塞ScrollView里,不然会碰到滚动冲突、Item测量不准、滑动卡顿一堆坑。给你几个生产环境常用的实现方案,按推荐程度排序:
1. 用RecyclerView + MergeAdapter(首推!)
这是AndroidX官方提供的最优雅的解决方案,完美适配这种多列表拼接的场景。核心思路是:给「已配对设备」和「新发现设备」各自写独立的RecyclerView Adapter,然后用MergeAdapter把这两个Adapter合并成一个,最终绑定到同一个RecyclerView上。这样整个界面就是一个统一的滚动容器,完全没有嵌套问题,还能保留每个列表各自的逻辑独立性。
举个简单的代码示例:
// 先准备好两个独立的Adapter PairedDeviceAdapter pairedAdapter = new PairedDeviceAdapter(pairedDeviceList); DiscoveredDeviceAdapter discoveredAdapter = new DiscoveredDeviceAdapter(discoveredDeviceList); // 用MergeAdapter合并 MergeAdapter mergeAdapter = new MergeAdapter(pairedAdapter, discoveredAdapter); // 绑定到RecyclerView recyclerView.setAdapter(mergeAdapter);
注意要确保你的项目依赖了AndroidX RecyclerView库:
implementation 'androidx.recyclerview:recyclerview:1.3.2' // 用最新版本即可
后续如果某一个列表的数据更新(比如新发现了设备),直接调用对应Adapter的notifyDataSetChanged()(或者更高效的DiffUtil)就行,MergeAdapter会自动处理刷新。
2. 单个RecyclerView实现多类型Item
如果不想引入MergeAdapter,也可以在同一个RecyclerView Adapter里处理两种不同的Item类型。核心是通过getItemViewType()区分「已配对」和「新发现」的Item,然后在onCreateViewHolder()和onBindViewHolder()里分别处理不同的布局和数据绑定。
示例代码片段:
public class BluetoothDeviceAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private static final int TYPE_PAIRED = 0; private static final int TYPE_DISCOVERED = 1; private List<BluetoothDevice> pairedDevices; private List<BluetoothDevice> discoveredDevices; // 构造方法、数据更新方法省略... @Override public int getItemViewType(int position) { // 前pairedDevices.size()个是已配对设备,后面是新发现设备 return position < pairedDevices.size() ? TYPE_PAIRED : TYPE_DISCOVERED; } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { LayoutInflater inflater = LayoutInflater.from(parent.getContext()); if (viewType == TYPE_PAIRED) { View view = inflater.inflate(R.layout.item_paired_device, parent, false); return new PairedDeviceViewHolder(view); } else { View view = inflater.inflate(R.layout.item_discovered_device, parent, false); return new DiscoveredDeviceViewHolder(view); } } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { if (getItemViewType(position) == TYPE_PAIRED) { BluetoothDevice device = pairedDevices.get(position); ((PairedDeviceViewHolder) holder).bind(device); } else { BluetoothDevice device = discoveredDevices.get(position - pairedDevices.size()); ((DiscoveredDeviceViewHolder) holder).bind(device); } } @Override public int getItemCount() { return pairedDevices.size() + discoveredDevices.size(); } // 两个ViewHolder类省略... }
这种方案的好处是不需要额外依赖,所有逻辑都在一个Adapter里,但如果两个列表的逻辑比较复杂,代码会显得臃肿,不如MergeAdapter清晰。
3. 动态计算ListView高度(应急方案,不推荐)
如果你的项目已经大量使用ListView,不想迁移到RecyclerView,可以通过手动计算每个Item的高度,把ListView的高度固定下来,然后把两个ListView放在一个LinearLayout里(不需要套ScrollView,或者套也不会有冲突)。
计算高度的工具方法:
public static void setListViewHeight(ListView listView) { ListAdapter adapter = listView.getAdapter(); if (adapter == null) return; int totalHeight = 0; // 遍历所有Item计算总高度 for (int i = 0; i < adapter.getCount(); i++) { View item = adapter.getView(i, null, listView); item.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)); totalHeight += item.getMeasuredHeight(); } // 加上分割线的高度 totalHeight += listView.getDividerHeight() * (adapter.getCount() - 1); ViewGroup.LayoutParams params = listView.getLayoutParams(); params.height = totalHeight; listView.setLayoutParams(params); }
使用时,在给ListView设置完Adapter后调用这个方法:
pairedListView.setAdapter(pairedAdapter); setListViewHeight(pairedListView); discoveredListView.setAdapter(discoveredAdapter); setListViewHeight(discoveredListView);
这个方案的缺点很明显:如果Item高度不固定(比如有长文本换行),计算出来的高度会不准;如果列表很长,遍历计算高度会有性能损耗;而且ListView本身失去了滚动能力,只能靠外层容器滚动,体验不如RecyclerView。所以只适合应急,不推荐长期使用。
额外注意点
不管用哪种方案,蓝牙设备列表都是动态更新的(比如扫描到新设备、配对状态变化),建议用DiffUtil来做高效的数据刷新,避免全量刷新导致的卡顿,提升用户体验。
内容的提问来源于stack exchange,提问作者meDarq

