一 、先理需求
网格布局管理内放设备块,每个设备块内部有一些控件来展示设备的相关信息,有一个SwitchButton来控制设备的开关。点击/滑动开关到相应的状态时,发送指令到服务器并等待反馈,再更新UI。如下图所示当开关关闭时,图标灰显。
当开关打开时图标呈现彩色
二、开始实现,慢慢入坑
由于用到了RecyclerView,所以我们的设备的容器继承RecyclerView.Adapter。设置一个点击SwitchButton事件的监听。在Fragment监听到单击后开始执行发送消息的任务到服务器,并等待。更新设备的状态标志位,不更新UI。然后在onBindViewHolder对SwitchButton的OnCheckedChangeListener进行监听(开关状态被改变)在更新UI。编译安装后发现实际效果不对。OnCheckedChangeListener会先OnClickListener一步监听到点击事件。那么就取消OnCheckedChangeListener的监听,在OnClickListener监听完成后通过Adapter的notifyItemChanged(pos)的方式去重新绑定一次,从而刷新UI。然而SwitchButton默认设置的关闭,而SwitchButton点击事件不需要任何处理就有一个自动切换状态的动画。那就造成显示的效果为关->开->关->开。人眼明显观察到切换动画出现两次。通过查找资料和查看源码,并没有发现有设置取消自动更新动画的方法。
这个中间使用了各种方法来解决这些冲突都没有很好解决(新手一枚),国庆的一个下午和一整个晚上,中间很多过程也记不清了,多次差点放弃。终于找到了一个可算还行的方法。
三、最终实现
首先在Fragement监听OnClickListener,直接看注释即可。
myAdapter.setSwitchListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
int pos = (int) view.getTag();
//根据 当前状态判断应当发送的消息
String msg = myAdapter.getBemfaDeviceList().get(pos).getDevice().isOpen() ? DEVICE_CLOSE : DEVICE_OPEN;
//发送消息到巴法云,异步处理返回的消息
sendMsgToDevice(myAdapter.getBemfaDeviceList().get(pos), msg, new OnSendStatusListener() {
@Override
public void onSendStatus(boolean isSuccess) {
//根据发送的消息,判断接下来应当处于什么状态
boolean nextStatus = isSuccess != msg.equals(DEVICE_CLOSE);
myAdapter.getBemfaDeviceList().get(pos).getDevice().setOpenStatus(nextStatus);
//同步锁,告诉view对象,已经收到了来自服务器的消息,可以更新UI
synchronized((SwitchButton)view) {
view.setSelected(true);
view.notify();
}
}
});
}
});
接下来就是在容器内在onBindViewHolder监听OnCheckedChangeListener,如下注释很详细了
holder.grid_switch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
// buttonView.setChecked(!isChecked);
if(switchListener == null) {
return;
}
//这里就不管滑动还是点击,都当做单击处理
switchListener.onClick(holder.grid_switch);
ThreadPoolExecutor poolExecutor;
//在线程池获取一个线程(当前是处于主线程的,不在子线程通过,会直接阻塞主线程造成无响应)
poolExecutor = ExecutorUtil.getInstance().newSingleThreadExecutor();
poolExecutor.submit(new Runnable() {
@Override
public void run() {
//同步,等待数据获取完成再更新UI,条件是SwitchButton当前被选中
synchronized(holder.grid_switch) {
while(!holder.grid_switch.isSelected()) {
try {
holder.grid_switch.wait();
logcat("" + holder.grid_switch.isSelected() + Thread.currentThread().toString());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//数据获取完成后在主线程更新UI
((Activity)con).runOnUiThread(new Runnable() {
@Override
public void run() {
//重置条件,记得在最开始初始化的时候就得先重置一次
holder.grid_switch.setSelected(false);
//更新SwitchButton状态
holder.grid_switch.setCheckedNoEvent(mBemfaDeviceList.get(position).getDevice().isOpen());
//更新显示的图标
holder.grid_img.setImageResource(mBemfaDeviceList.get(position).getDevice().getImgId());
}
});
}
}
});
}
});
这里说下处理的流程:
- 不管是滑动还是点击,最先监听到OnCheckedChangeListener,现在无法判断这个来自滑动还是监听。
- 不管滑动还是点击,都当做单击处理,触发OnClickListener事件。
- 在OnClickListener中给服务器发送消息,并异步处理接收到的消息
- 在OnCheckedChangeListener等待OnClickListener收到服务器消息的通知
- OnClickListener收到消息后通知OnCheckedChangeListener的SwitchButton对象,并更新条件。
- 同步锁调节不在满足,开始更新UI。
四、问题点
这里在说明一下问题点,点击后可以正常的在接收后,在根据实际改变按钮状态。但是如果采用滑动的方式,这个就会在消息接手前就改变状态,在正常处理没啥说的,异常就回退到之前的状态,其实也还行。毕竟就设置不成功嘛。
如果在OnCheckedChangeListener内,提前屏蔽状态buttonView.setChecked(!isChecked);比如监听到改变为true,那么之前就是false。重新设置回去即可,但是这样就会在正常也出现上面提到的异常时的情况。滑到true,强制回退,在收到消息时再自动选择状态。
其实最好的函数就是禁止滑动,不知道是没有还是咋地,反正目前没找到。
2021.10.10补充
可以可以在SwitchButton布局外围加一个控制头,不在监听SwitchButton,而且监听这个控制头实现点击操作并且屏蔽了滑动操作。可以在成功/失败后在更新状态


