RecyclerView中倒计时item的优雅方案

RecyclerView中倒计时item的优雅方案_第1张图片

在电商、秒杀类产品里经常有在列表里展示倒计时的需求,本文来自一位朋友的投稿,介绍了实现这种需求的一种方案,思路还是不错的,但使用 Handler 实现倒计时在主线程卡顿时可能会有延迟,建议结合 Java 定时任务类 ScheduledExecutorService 使用。欢迎有兴趣的朋友一起探讨。

作者:AndroidMsky原文:https://blog.csdn.net/AndroidMsky/article/details/88557445


本文介绍在RecyclerView中使用倒计时楼层,并且每秒刷新显示倒计时。没有纠结于样式,主要介绍代码结构和设计模式。

先看一下效果:

RecyclerView中倒计时item的优雅方案_第2张图片

我们采取的是观察者模式的方法,启动一个handler,每隔一秒去刷新所有注册过的item楼层。观察者模式的大概关系如下图:

RecyclerView中倒计时item的优雅方案_第3张图片

我们并没有使用JAVA中的Observable,因为在释放Holder的时机比较难处理存在内存泄露的风险,所以我们采用WeakHashMap去保存所有Holder,但是在GC不频繁的情况下,弱引用也可以访问到无效的Holder对象,如果RecyclerView频繁更新,GC又不频繁进行,将会有通知到很多无效的Holder。为此我们采用了均衡的两套方案:

  1. 在RecyclerView更新不频繁的场景下我们只在Adapter的OnCreate中注册Holder,并且除非页面销毁否则不去管理WeakHashMap中的内容,一切交给GC处理。

  2. 在RecyclerView更新频繁的时候,我们会在onbind方法里判断是否注册了Holder,没有则加入,并且在notifyDataChange的时候解除所有Holder的注册,这样就算Holder没有被GC回收,也收不到我们的更新指令了。

选用哪种方案将在Observable初始化的时候得到确认:

 /*
    * @param beatTime 更新频率
    * @param changeable 易变得,如果为true
    * 会在bind方法里访问Map,不存在则加入Map
    * 如果存在频繁的下拉刷新,分页加载建议设为true
    * 
    * 如果列表数据不频繁更新请设置为false,
    * 只会在onCreate时候注册观察者。
    * Holder会在GC发生后自动停止更新。
    *
    *
    * */
    private final int BEAT_TIME;
    private final boolean CHANGEABLE;
    ...
    public Center(int beatTime, boolean changeable) {
        BEAT_TIME = beatTime;
        CHANGEABLE = changeable;
    }

注册方法:

 private WeakHashMap<Observer, Object> weakHashMap = new WeakHashMap();
 ...
 @Override
    public void addObserver(Observer observer) {
        weakHashMap.put(observer, null);
    }

通过bindRecyclerView获取到当前展示的item位置范围:

public void bindRecyclerView(RecyclerView recyclerView){
        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
            }

            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                int first=-1;
                int last=-1;

                RecyclerView.LayoutManager layoutManager=recyclerView.getLayoutManager();
                if (layoutManager instanceof GridLayoutManager){
                     first=((GridLayoutManager)layoutManager).findFirstVisibleItemPosition();
                     last=((GridLayoutManager)layoutManager).findLastVisibleItemPosition();
                }
                if (layoutManager instanceof LinearLayoutManager){
                     first=((LinearLayoutManager)layoutManager).findFirstVisibleItemPosition();
                     last=((LinearLayoutManager)layoutManager).findLastVisibleItemPosition();
                }
                postionFL.frist=first;
                postionFL.last=last;

                Log.e("lmtlmt2","frist:"+first+"last:"+last);
            }
        });
    }

通知所有holder更新数据并将位置访问传递给所有观察者,观察者拿到显示范围,并和自己做比较,决定是否更新UI:

 private void notifyObservers() {

        Iterator iter = weakHashMap.entrySet().iterator();
        while (iter.hasNext()) {
            Map.Entry entry = (Map.Entry) iter.next();
            Observer observer = (Observer) entry.getKey();
            if (observer != null) {
                if (postionFL.frist>-1&&postionFL.last>-1)
                observer.update(null, postionFL);
                else observer.update(null, null);
            }


        }
        Log.e("lmtlmt", "weakHashMap size" + weakHashMap.size());

    }

停止和开始倒计时:

 @Override
    public void startCountdown() {
        if (!isStart) {
            handler.sendEmptyMessage(0);
            isStart = true;
        }

    }

    @Override
    public void stopCountdown() {
        isStart = false;
        handler.removeCallbacksAndMessages(null);

    }

通知更新和判断是否加入Map,仅在易变模式下有效:

 @Override
    public boolean containHolder(Observer observer) {
        //如果不是易变的 直接屏蔽次方法。
        if (!CHANGEABLE) return true;
        if (weakHashMap.containsKey(observer)) return true;
        return false;
    }

    @Override
    public void notifyAdapter() {
        if (!CHANGEABLE) return;
        deleteObservers();
    }

倒计时model代码,这里注意我们使用的是elapsedRealtime(可以理解为开机后cpu的增长时钟),不会因为用户调整系统时间而导致倒计时不准确。

public class TimeBean {
    private long elapsedRealtime;

    TimeBean(long remainTime) {
        elapsedRealtime = remainTime + SystemClock.elapsedRealtime()/1000;
    }


    public long getRainTime() {
        return elapsedRealtime - SystemClock.elapsedRealtime()/1000;
    }

下面是Adapter的代码:ViewHolder实现Observer接口等待更新信号,只更新在范围内的ViewHolder

static class ViewHolder extends RecyclerView.ViewHolder implements Observer {
        int lastBindPositon = -1;
        TextView textView;
        TimeBean timeBean;

        public ViewHolder(View itemView) {
            super(itemView);
            textView = itemView.findViewById(R.id.tv1);

        }

        @Override
        public void update(Observable o, Object arg) {
          if (arg!=null&&arg instanceof Center.PostionFL){
                Center.PostionFL postionFL=(Center.PostionFL)arg;
                if (lastBindPositon>=postionFL.frist&&lastBindPositon<=postionFL.last){
                    Log.e("lmtlmtupdate", "update" + lastBindPositon);
                    bindCountView(textView, timeBean);
                }
            }

        }

两个关键方法:注意在非易变模式下,bind中的containHolder方法恒为true。

@Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false);
        ViewHolder viewHolder = new ViewHolder(view);
        countDownCenter.addObserver(viewHolder);
        countDownCenter.startCountdown();
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        holder.lastBindPositon = position;
        holder.timeBean = list.get(position);
        bindCountView(holder.textView, holder.timeBean);
        if (!countDownCenter.containHolder(holder)){
            countDownCenter.addObserver(holder);
        }

    }

测试代码片段:

 for (int i = 0; i < 100; i++) {
            list.add(new TimeBean(i*10));
        }
        countDownCenter=new Center(1000,false);
        listAdapter=new ListAdapter(list,countDownCenter);
        recyclerView.setAdapter(listAdapter);
        final FrameLayout frameLayout=findViewById(R.id.f1);
        frameLayout.addView(recyclerView);

方案总结:

  1. 倒计时信号恒定时间准确。

  2. 使用弱引用即便操作不当也不会产生内存泄露。

  3. 提供两种权衡方法,根据项目自身情况最大程度避免CPU无效开销。

  4. 可根据Activity或Fragment生命周期进行暂停和重开。

  5. 入侵性小,介入简便。

代码地址:https://github.com/AndroidMsky/RecycleViewFloorCountDown

你可能感兴趣的:(RecyclerView中倒计时item的优雅方案)