解决赛事轮播中间嵌套在recycleview中的回收错乱问题
首先感谢[github上下轮播控件作者的轮子](https://github.com/LeeYawei/Android-TipView)
这里是在以上链接的基础上进行改造,大家可以去参考一下
当我们在一个app中实现一个直播赛事预告的从下往上翻转的轮播控件时,
如何解决该自定义view在recycleview中被回收复用时候导致的显示错乱问题。
一睹为快,先看2个核心的自定义view源码:
/**
* 赛事轮播控件的总控view实现轮播效果,控制动画从下往上,3秒刷新一个
*/
public class RaceToolView extends FrameLayout {
/**
* 动画间隔
*/
private static final int ANIM_DELAYED_MILLIONS = 3 * 1000;
/**
* 动画持续时长
*/
private static final int ANIM_DURATION = 1000;
private Animation anim_out, anim_in;
/**
* 循环播放的消息
*/
private List<T> raceList;
private int curTipIndex = 0;
private long lastTimeMillis;
private RaceOrderView raceOut;
private RaceOrderView raceIn;
private RaceOrderView.RaceReserveStateChangeListener listener;
private boolean isRequesting = false;
private boolean isRemoveRaceIn;
public RaceToolView(@NonNull Context context) {
this(context, null);
}
public RaceToolView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public RaceToolView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initListener();
initFrameView();
initAnimation();
}
private void initListener() {
listener = new RaceOrderView.RaceReserveStateChangeListener() {
@Override
public void raceReserve(ReserveReq req) {
sendReserveReq(req, true);
}
@Override
public void raceCancel(ReserveReq req) {
sendReserveReq(req, false);
}
};
}
/**
* 初始化两个RaceOrderView进行轮播
*/
private void initFrameView() {
raceOut = new RaceOrderView(getContext(), listener);
raceIn = new RaceOrderView(getContext(), listener);
addView(raceIn);
addView(raceOut);
}
/**
* 发送赛事预定请求
*
* @param reserveReq
*/
private void sendReserveReq(final ReserveReq reserveReq, final boolean isReserve) {
isRequesting = true;
anim_out.cancel();
anim_in.cancel();
if (reserveReq.getGid().equals(raceIn.getGameId())) {
raceOut.setVisibility(GONE);
isRemoveRaceIn = false;
} else {
raceIn.setVisibility(GONE);
isRemoveRaceIn = true;
}
/**
* 。。。。。。。。。
* 这里省略点击后向后台发送预约请求的方法
* 。。。。。。。。。
*/
updateDataList(isReserve, response(这个是请求返回的response));
}
/**
* 更新gameId对应的数据预约状态值
*
* @param isReserve
* @param gameId 表示赛事对应的ID唯一标识,具体根据自己的逻辑去实现
*/
private void updateDataList(boolean isReserve, String gameId) {
for (T info : raceList) {
if (null != info.getGid() && info.getGid().equals(gameId)) {
T orderInfo = raceList.get(raceList.indexOf(info));
orderInfo.setIs_r(isReserve ? T.IS_R_RESERVED : T.IS_R_NO_RESERED);
raceList.set(raceList.indexOf(info), orderInfo);
if (raceIn.getGameId().equals(gameId)) {
raceIn.setRaceOrderInfo(orderInfo);
} else {
raceOut.setRaceOrderInfo(orderInfo);
}
break;
} else {
continue;
}
}
/**
* 这里是关键:预约请求完成后,恢复动画的轮播效果
*/
initAnimation();
if (isRemoveRaceIn) {
raceIn.setVisibility(VISIBLE);
raceIn.startAnimation(anim_in);
raceOut.setAnimation(anim_out);
} else {
raceOut.setVisibility(VISIBLE);
raceOut.setAnimation(anim_in);
raceIn.setAnimation(anim_out);
}
isRequesting = false;
}
/**
* **这里也是关键**
* 当我们的轮播控件嵌套在recycleView中时,recycleView自带的view回收复用机制,
* 会导致我们的轮播控件显示错乱
*
* 方案一:
* 如果直接在recycleView的hold里面采用调用API直接禁用当前hold的复用机制,
* 那么就会导致我们的控件在从可见状态到不可见状态,再由不可见状态变为可见状态的时候,
* 会刷新控件的显示,导致我们不能记录上一次显示的赛事item,而是每次都会从第一条重新开始。
* @Override
* public void bindData(T data, RecyclerView.ViewHolder holder) {
* 这里就是方案一所说的在adapter的bindData中设置holder为不可回收的代码
* holder.setIsRecyclable(false);
* }
*
* 方案二(我的最终方案):
* 如下所示:复写onWindowVisibilityChanged方法,在view可见的时候重新启动动画
* 在view不可见的时候取消当前动画。
* 此时即使recycleview进行了回收利用,也不会导致控件信息错乱。
*/
@Override
protected void onWindowVisibilityChanged(int visibility) {
super.onWindowVisibilityChanged(visibility);
if (VISIBLE == visibility) {
initAnimation();
if (curTipIndex % 2 == 0) {
raceIn.setVisibility(VISIBLE);
raceIn.startAnimation(anim_in);
raceOut.setAnimation(anim_out);
} else {
raceOut.setVisibility(VISIBLE);
raceOut.setAnimation(anim_in);
raceIn.setAnimation(anim_out);
}
isRequesting = false;
} else if (INVISIBLE == visibility) {
isRequesting = true;
anim_out.cancel();
anim_in.cancel();
}
}
/**
* 设置要循环播放的信息
*
* @param raceList
*/
public void setRaceList(List<RaceOrderInfo> raceList) {
this.raceList = raceList;
curTipIndex = 0;
updateRace(raceOut);
updateTipAndPlayAnimation();
}
/**
* 初始化2个动画效果
*/
private void initAnimation() {
anim_out = newAnimation(0, -1);
anim_in = newAnimation(1, 0);
anim_in.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationRepeat(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
if (isRequesting)
return;
updateTipAndPlayAnimationWithCheck();
}
});
}
private void updateTipAndPlayAnimationWithCheck() {
if (System.currentTimeMillis() - lastTimeMillis < 1000) {
return;
}
lastTimeMillis = System.currentTimeMillis();
updateTipAndPlayAnimation();
}
private void updateTipAndPlayAnimation() {
if (curTipIndex % 2 == 0) {
updateRace(raceOut);
raceIn.startAnimation(anim_out);
raceOut.startAnimation(anim_in);
this.bringChildToFront(raceIn);
} else {
updateRace(raceIn);
raceOut.startAnimation(anim_out);
raceIn.startAnimation(anim_in);
this.bringChildToFront(raceOut);
}
}
/**
* 更新raceOrderInfo数据
*
* @param raceView
*/
private void updateRace(RaceOrderView raceView) {
T race = getNextData();
if (null != race) {
raceView.setT(race);
}
}
/**
* 获取下一条消息
*
* @return
*/
private RaceOrderInfo getNextData() {
if (raceList == null || raceList.isEmpty()) {
return null;
} else {
return raceList.get(curTipIndex++ % raceList.size());
}
}
private Animation newAnimation(float fromYValue, float toYValue) {
Animation anim = new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, 0,
Animation.RELATIVE_TO_SELF, fromYValue, Animation.RELATIVE_TO_SELF, toYValue);
anim.setDuration(ANIM_DURATION);
anim.setStartOffset(ANIM_DELAYED_MILLIONS);
anim.setInterpolator(new DecelerateInterpolator());
return anim;
}
}
/**
* 赛事轮播控件的单个view
*/
public class RaceOrderView extends FrameLayout implements View.OnClickListener {
private static final String TYPE_RACE_MODULE = "0";
private static final String TYPE_RACE_ORDER_TRUE = "1";
private static final String TYPE_RACE_ORDER_FALSE = "0";
private static final String TYPE_RACE_RESERVE = "1";
private static final String TYPE_RACE_CANCEL = "2";
private T orderInfo;
private RaceReserveStateChangeListener stateChangeListener;
public RaceOrderView(@NonNull Context context, RaceReserveStateChangeListener listener) {
super(context);
LayoutInflater.from(context).inflate(R.layout.item_race_order_layout, this);
stateChangeListener = listener;
}
public String getGameId() {
return orderInfo.getGid();
}
/**
* 设置当前T的数据来源
*/
public void setT(T info) {
this.orderInfo = info;
}
/**
* 赛事预约状态变化监听
*/
interface RaceReserveStateChangeListener {
void raceReserve(ReserveReq req);
void raceCancel(ReserveReq req);
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.btn_order) {
/**
* 当子view点击预约按钮的时候才返回监听事件给上层view去处理
*/
stateChangeListener.raceCancel(reserveReq);
stateChangeListener.raceReserve(reserveReq);
}
}
/**
* 跳转到web页面
*/
private void startWebContent(String url) {
...........
}
}
- 总结,这个控件的实现复杂度并不高,但是由于需求不同你会遇到各种的问题,如何寻找合适的解决方案很重要。当发现轮播view显示错乱的时候,首先需要明确定位问题,定位出两个原因,原因一:view动画没有停止,数据源一直在更新;原因二:recycleview的复用机制导致UI显示重叠错乱。
- 这两个问题同时导致我们无法正确显示轮播控件,实现过程中定位出这两个根本原因才是关键。
- 原因确定后,就比较容易寻找相应的解决方案。
- 感谢大家的阅读。Thanks♪(・ω・)ノ