RecyclerView中倒计时item的优雅方案

本文探讨在RecyclerView中实现倒计时item的解决方案,采用观察者模式,通过Handler每秒更新。利用WeakHashMap避免内存泄露,同时提供两种策略应对更新频率,确保CPU效率。详细介绍了代码实现,包括Adapter和ViewHolder的交互,以及如何在不同场景下选择合适方案。

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

先看一下效果:

在这里插入图片描述

我们采取的是观察者模式的方法,启动一个handler,每隔一秒去刷新所有注册过的item楼层。观察者模式的大概关系如下图:
在这里插入图片描述
我们并没有使用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

欢迎关注我的博客和GitHub

源码下载地址: https://pan.quark.cn/s/a4b39357ea24 在电磁模拟技术中,CST(Computer Simulation Technology)是一种被广泛采纳的软件工具,它主要用于电磁场、微波、天线以及射频系统的设计工作。本资料将详细分析CST软件中离散端口的具体配置方法,这些方法对于提升仿真结果的精确度和专业水准具有决定性作用。离散端口在CST软件中扮演着模拟信号输入或输出的重要角色,它们构成了仿真模型不可或缺的部分。在配置离散端口时,一个核心的原则是保证端口的方向与网格线保持一致,这是因为这样做能够有效降低计算过程中产生的误差,并确保仿真数据的有效性。如果未能遵循这一指导原则,可能会引发未知的计算问题,进而导致仿真结果失去可靠性。 在CST软件中配置离散端口,通常需要借助“Pick Points”这一功能。通过选择“Pick Edge Center”选项,端口将被设定在模型边缘的中心位置上。然而,这种做法并不总是能够确保端口与网格线保持平行。在某些特定情形下,模型的几何构造可能不允许直接选取一个与网格线平行的边作为端口的安装位置。 为了克服这一挑战,可以采用多种不同的策略。如果模型本身已经包含一条与馈电口平行的边,那么可以直接利用这条边来建立端口,此时CST软件会自动调整端口使其与网格线对齐。另一种可选的方法是,当模型不具备现成的平行边时,用户可以手动构建一个几何结构,比如一个立方体,并使其边缘与馈电口平行。通过这种方式,新建立的几何结构的边缘就可以作为端口的位置,从而确保端口与网格线的平行关系。 在实施上述操作时,必须关注端口尺寸的合理性和物理意义的一致性。端口的尺寸应当依据实际天线馈电部分的尺寸进行适当调整,过大的端口或...
代码下载链接: https://pan.quark.cn/s/a4b39357ea24 【使用TensorFlow进行图像识别】 图像识别作为计算机视觉领域的关键任务之一,其核心在于通过算法解析和理解图像所包含的信息。在此资源中,我们将集中探讨如何借助功能强大的深度学习框架TensorFlow来执行手写数字识别。手写数字识别构成了众多实际应用的基础,例如自动支票处理、光学字符识别(OCR)等场景。 TensorFlow是由Google创建的一个开源库,它主要用于数值运算和机器学习,尤其在深度学习方面表现卓越。其核心优势在于可以构建并训练复杂的神经网络架构,并且在多种硬件环境中实现高效执行,涵盖CPU和GPU平台。 在此实践项目中,我们将运用TensorFlow来构建一个卷积神经网络(CNN)模型,这种架构是处理图像数据的理想选择。CNNs通过模仿人脑视觉皮层的运作机制,能够自主地提取图像中的关键特征,进而达成识别目标。在手写数字识别的特定情境下,这些特征可能涉及笔画的几何形态、走向以及相互间的连接模式。 对于CNN的基础结构,我们需要具备相应的认知,其通常由卷积层、池化层、全连接层以及激活函数等部分组成。卷积层借助滤波器(亦称卷积核)对图像进行扫描,以捕捉局部特征;池化层则用于降低数据维度,同时保留核心信息;全连接层将特征向量映射至各类别的概率分布;而激活函数如ReLU则通过引入非线性元素,使模型能够学习更为复杂的模式。 在此案例中,建议采用MNIST数据集,这是一个广泛用于手写数字识别的标准测试集。该数据集包含60,000个训练样本和10,000个测试样本,每个样本均为28x28像素的灰度图像,代表0到9这十个数字中的某一个。为了训练模型,必须首先加载数据,并...
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值