一 、先理需求
网格布局管理内放设备块,每个设备块内部有一些控件来展示设备的相关信息,有一个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,而且监听这个控制头实现点击操作并且屏蔽了滑动操作。可以在成功/失败后在更新状态