Solve the problem of recycling disorder nested in recycleview in the middle of event rotation
首先感谢[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;
}
}
The following custom view is a subview of the above class
/**
* 赛事轮播控件的单个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) {
...........
}
}
In conclusion, the implementation complexity of this control is not high, but you will encounter various problems due to different requirements, and how to find a suitable solution is very important. When it is found that the carousel view is displayed in a disorderly manner, first of all, it is necessary to clarify the positioning problem. There are two reasons for positioning. Reason one: the view animation does not stop, and the data source has been updated; reason two: the reuse mechanism of the recycleview causes the UI display to overlap and disorderly.
These two problems also prevent us from displaying the carousel controls correctly. It is the key to locate these two root causes during the implementation process.
Once the cause is identified, it is easier to find the appropriate solution.
Thanks everyone for reading. Thanks♪(・ω・)ノ