fix(P): Android 首页常用设备不再因控制而重排/闪动

- 会话内顺序稳定: 记录上次渲染顺序, 新设备追加末尾, 后端顺序变化也不跳位
- 切家庭时重置顺序基准
- 点击卡片开关时通知页面: DP 回传期间跳过整表刷新, 4s 后延迟对齐一次
This commit is contained in:
2995840376 2026-10-28 11:11:46 +08:00
parent 8a11c78d22
commit f3aaf8fc35
2 changed files with 122 additions and 2 deletions

View File

@ -108,6 +108,9 @@ public class HomeFragment extends Fragment {
activityRefreshHandler.removeCallbacks(activityRefreshTask);
activityRefreshTask = null;
}
if (deviceRefreshHandler != null) {
deviceRefreshHandler.removeCallbacks(alignRefreshTask);
}
}
@Override
@ -195,7 +198,14 @@ public class HomeFragment extends Fragment {
public void onDpUpdate(String devId, Map<String, Object> dps) {
android.util.Log.i("HomeFragment", "DP回调[" + devId + "] dps=" + dps);
if (rvDevices != null) {
rvDevices.post(HomeFragment.this::scheduleDeviceRefresh);
if (isSelfInitiatedControl(devId)) {
// 用户刚点了这张卡片的开关:胶囊已按乐观值显示,
// 此时整表重载会让"常用设备"整块闪一下(顺序已稳定,但仍有重绘)。
// 推迟到窗口结束后再对齐一次即可。
scheduleDelayedAlignRefresh();
} else {
rvDevices.post(HomeFragment.this::scheduleDeviceRefresh);
}
}
// DP 变化统一上报后端(source=panel):
// 这是蓝牙 Mesh 本地通道控制的唯一上报路径——手机蓝牙直连 publishDps
@ -215,6 +225,39 @@ public class HomeFragment extends Fragment {
}
};
// ==================== 控制后延迟对齐(避免常用设备整表闪动) ====================
/** 用户刚点击控制的设备ID */
private String pendingControlDeviceId;
/** 用户刚点击控制的时间戳 */
private long pendingControlAt;
/** 控制后延迟对齐刷新的等待时间 */
private static final long CONTROL_ALIGN_DELAY_MS = 4000L;
/** 控制后延迟对齐刷新任务 */
private final Runnable alignRefreshTask = new Runnable() {
@Override
public void run() {
if (isAdded()) {
loadDevicesFromBackend();
}
}
};
/** 该 DP 变化是否来自用户刚在首页点下的控制 */
private boolean isSelfInitiatedControl(String devId) {
return devId != null && devId.equals(pendingControlDeviceId)
&& System.currentTimeMillis() - pendingControlAt < CONTROL_ALIGN_DELAY_MS;
}
/** 控制后延迟对齐刷新:等指令回传稳定,再拉一次列表让服务端状态覆盖乐观值 */
private void scheduleDelayedAlignRefresh() {
if (rvDevices == null) return;
rvDevices.post(() -> {
deviceRefreshHandler.removeCallbacks(alignRefreshTask);
deviceRefreshHandler.postDelayed(alignRefreshTask, CONTROL_ALIGN_DELAY_MS);
});
}
private void initViews(View root) {
rvDevices = root.findViewById(R.id.rv_devices);
progressBar = root.findViewById(R.id.progress_devices);
@ -265,6 +308,11 @@ public class HomeFragment extends Fragment {
adapter = new HomeDeviceAdapter(new ArrayList<>(), (device, position) ->
openDevicePanel(device));
adapter.setAccentColor(accent);
// 用户点了卡片开关:记录下来,DP 回调时不做整表刷新(避免"常用设备"闪动/跳位)
adapter.setOnControlStartListener(deviceId -> {
pendingControlDeviceId = deviceId;
pendingControlAt = System.currentTimeMillis();
});
// 长按设备:编辑(重命名)/ 删除
adapter.setOnDeviceLongClickListener((device, position) ->
DeviceActionDialogs.showActions(requireActivity(),
@ -410,6 +458,8 @@ public class HomeFragment extends Fragment {
Toast.makeText(requireContext(),
"已切换到「" + (home.getName() != null ? home.getName() : "家庭") + "」",
Toast.LENGTH_SHORT).show();
// 换家庭:上一次的设备顺序不再适用,重置后按新家庭顺序展示
lastDeviceOrder.clear();
loadDevicesFromBackend();
loadRecentActivities();
}
@ -1160,11 +1210,62 @@ public class HomeFragment extends Fragment {
}
result.add(td);
}
// 在线设备排前面,离线排后面(组内保持原顺序)
// 1) 先按「上一次显示的顺序」重排:设备位置对用户是固定的,
// 不能因为一次控制上报就让常用设备换位置(JD2BTSA 跳到 JD6BTSA 前面)。
// 新增设备按后端返回顺序追加到末尾。
applyStableOrder(result);
// 2) 在线设备排前面,离线排后面(稳定排序,组内保持上一步的相对顺序)
result.sort((a, b) -> Boolean.compare(!a.isOnline(), !b.isOnline()));
return result;
}
/** 上一次渲染的设备顺序(设备ID),用于保持列表位置稳定 */
private final List<String> lastDeviceOrder = new ArrayList<>();
/**
* 按上次顺序重排设备列表。
*
* <p>后端已对 member 视角改为按 id 升序(位置固定),这里再加一层会话内兜底:
* 万一后端顺序变化(如新设备插入、后台排序调整),也不让已在屏上的设备跳位。</p>
*/
private void applyStableOrder(List<TuyaDevice> devices) {
if (devices == null || devices.isEmpty()) {
lastDeviceOrder.clear();
return;
}
if (lastDeviceOrder.isEmpty()) {
// 首次加载:以本次顺序为基准
lastDeviceOrder.clear();
for (TuyaDevice d : devices) {
lastDeviceOrder.add(d.getId());
}
return;
}
java.util.Map<String, TuyaDevice> remain = new java.util.LinkedHashMap<>();
for (TuyaDevice d : devices) {
remain.put(d.getId(), d);
}
List<TuyaDevice> ordered = new ArrayList<>(devices.size());
for (String id : lastDeviceOrder) {
TuyaDevice d = remain.remove(id);
if (d != null) {
ordered.add(d);
}
}
// 新设备追加在后面
for (TuyaDevice d : devices) {
if (remain.containsKey(d.getId())) {
ordered.add(d);
}
}
devices.clear();
devices.addAll(ordered);
lastDeviceOrder.clear();
for (TuyaDevice d : ordered) {
lastDeviceOrder.add(d.getId());
}
}
private void showLoading(boolean show) {
progressBar.setVisibility(show ? View.VISIBLE : View.GONE);
}

View File

@ -52,6 +52,21 @@ public class HomeDeviceAdapter extends RecyclerView.Adapter<HomeDeviceAdapter.Vi
void onLongClick(TuyaDevice device, int position);
}
/**
* 控制发起回调
*
* <p>用户在卡片上点了开关时通知页面,便于页面推迟整表刷新(避免列表重排/闪动)。</p>
*/
public interface OnControlStart {
void onControlStart(String deviceId);
}
private OnControlStart controlStartListener;
public void setOnControlStartListener(OnControlStart l) {
this.controlStartListener = l;
}
public HomeDeviceAdapter(List<TuyaDevice> devices, OnDeviceClick listener) {
this.devices = devices;
this.listener = listener;
@ -215,6 +230,10 @@ public class HomeDeviceAdapter extends RecyclerView.Adapter<HomeDeviceAdapter.Vi
holder.pill.setOnUserToggleListener(checked -> {
// 锁定乐观值:下发结果回传前,忽略列表刷新带回来的旧状态
lockPendingToggle(device.getId(), checked);
// 通知页面:用户刚点了开关,别急着整表刷新(会闪动/跳位)
if (controlStartListener != null) {
controlStartListener.onControlStart(device.getId());
}
Map<String, Object> commands = new HashMap<>();
commands.put(switchDpId, checked);
PlatformServiceManager.getInstance(holder.itemView.getContext())