直播app中recycleView嵌套轮播控件动画控制

解决赛事轮播中间嵌套在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;
        }
        /**
        * 。。。。。。。。。
        * 这里省略点击后向后台发送预约请求的方法
        * 。。。。。。。。。
        */
        //这里直接给我网络请求成功后刷新UI并,重新启动轮播动画的逻辑
        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);
                //刷新自定义控件的UI显示,预约按钮状态等
                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是上面的类的子view
/**
 * 赛事轮播控件的单个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);
        //省略部分findViewById和设置控件listener的模板代码
        //.........
        stateChangeListener = listener;
    }

    public String getGameId() {
        return orderInfo.getGid();
    }

    /**
     * 设置当前T的数据来源
     */
    public void setT(T info) {
        this.orderInfo = info;
        //用后台的数据T显示子view的UI数据
        //自行实现
    }

    /**
     * 赛事预约状态变化监听
     */
    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♪(・ω・)ノ

猜你喜欢

转载自blog.csdn.net/CathyChen0910/article/details/80103958