├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── example │ │ └── floorcountdown │ │ ├── A.java │ │ ├── FloorCountDownLib │ │ ├── Center.java │ │ └── ICountDownCenter.java │ │ ├── ListAdapter.java │ │ ├── MainActivity.java │ │ ├── MainActivityB.java │ │ └── TimeBean.java │ └── res │ ├── drawable-v24 │ ├── ic_launcher_foreground.xml │ └── pic1.jpg │ ├── drawable │ └── ic_launcher_background.xml │ ├── layout │ ├── activity_main.xml │ ├── activity_recycle.xml │ └── item.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/libraries 5 | /.idea/modules.xml 6 | /.idea/workspace.xml 7 | .DS_Store 8 | /build 9 | /captures 10 | .externalNativeBuild 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RecycleViewFloorCountDown 2 | 3 | 4 | 本文介绍在RecyclerView中使用倒计时楼层,并且每秒刷新显示倒计时。 5 | 没有纠结于样式,主要介绍代码结构和设计模式。 6 | 7 | 先看一下效果: 8 | 9 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190314162447643.gif) 10 | 11 | 我们采取的是观察者模式的方法,启动一个handler,每隔一秒去刷新所有注册过的item楼层。观察者模式的大概关系如下图: 12 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190314165911923.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0FuZHJvaWRNc2t5,size_16,color_FFFFFF,t_70) 13 | 我们并没有使用JAVA中的Observable,因为在释放Holder的时机比较难处理存在内存泄露的风险,所以我们采用WeakHashMap去保存所有Holder,但是在GC不频繁的情况下,弱引用也可以访问到无效的Holder对象,如果RecyclerView频繁更新,GC又不频繁进行,将会有通知到很多无效的Holder。 14 | 为此我们采用了均衡的两套方案, 15 | 1.在RecyclerView更新不频繁的场景下我们只在Adapter的OnCreate中注册Holder,并且除非页面销毁否则不去管理WeakHashMap中的内容,一切交给GC处理。 16 | 2在RecyclerView更新频繁的时候,我们会在onbind方法里判断是否注册了Holder,没有则加入,并且在notifyDataChange的时候解除所有Holder的注册,这样就算Holder没有被GC回收,也收不到我们的更新指令了。 17 | 选用哪种方案将在Observable初始化的时候得到确认 18 | 19 | 20 | 21 | 22 | 23 | 欢迎start fork issues提出各种问题,让项目越来越完善 谢谢。 24 | 2019.3.14 Androidmsky 25 | ## License 26 | 27 | Copyright 2019 AndroidMsky 28 | 29 | Licensed under the Apache License, Version 2.0 (the "License"); 30 | you may not use this file except in compliance with the License. 31 | You may obtain a copy of the License at 32 | 33 | http://www.apache.org/licenses/LICENSE-2.0 34 | 35 | Unless required by applicable law or agreed to in writing, software 36 | distributed under the License is distributed on an "AS IS" BASIS, 37 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 38 | See the License for the specific language governing permissions and 39 | limitations under the License. 40 | 41 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 27 5 | defaultConfig { 6 | applicationId "com.example.floorcountdown" 7 | minSdkVersion 15 8 | targetSdkVersion 27 9 | versionCode 1 10 | versionName "1.0" 11 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | implementation fileTree(dir: 'libs', include: ['*.jar']) 23 | implementation 'com.android.support:appcompat-v7:27.0.2' 24 | implementation 'com.android.support.constraint:constraint-layout:1.0.2' 25 | implementation 'com.android.support:recyclerview-v7:25.3.1' 26 | testImplementation 'junit:junit:4.12' 27 | androidTestImplementation 'com.android.support.test:runner:1.0.1' 28 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' 29 | 30 | debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.3' 31 | releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.3' 32 | // Optional, if you use support library fragments: 33 | debugImplementation 'com.squareup.leakcanary:leakcanary-support-fragment:1.6.3' 34 | } 35 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ] 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/floorcountdown/A.java: -------------------------------------------------------------------------------- 1 | package com.example.floorcountdown; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | 6 | import com.squareup.leakcanary.LeakCanary; 7 | import com.squareup.leakcanary.RefWatcher; 8 | 9 | /** 10 | * Created by air on 2019/3/14. 11 | */ 12 | public class A extends Application { 13 | 14 | //集成内存泄露检查 未发现问题。 15 | @Override 16 | public void onCreate() { 17 | super.onCreate(); 18 | // if (LeakCanary.isInAnalyzerProcess(this)) {//1 19 | // // This process is dedicated to LeakCanary for heap analysis. 20 | // // You should not init your app in this process. 21 | // return; 22 | // } 23 | // LeakCanary.install(this); 24 | } 25 | 26 | 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/floorcountdown/FloorCountDownLib/Center.java: -------------------------------------------------------------------------------- 1 | package com.example.floorcountdown.FloorCountDownLib; 2 | 3 | import android.os.Handler; 4 | import android.os.Message; 5 | import android.support.v7.widget.GridLayoutManager; 6 | import android.support.v7.widget.LinearLayoutManager; 7 | import android.support.v7.widget.RecyclerView; 8 | import android.util.Log; 9 | 10 | import java.util.Iterator; 11 | import java.util.Map; 12 | import java.util.Observable; 13 | import java.util.Observer; 14 | import java.util.WeakHashMap; 15 | 16 | /** 17 | * Created by air on 2019/3/13. 18 | */ 19 | public class Center implements ICountDownCenter { 20 | 21 | private final int BEAT_TIME; 22 | private final boolean CHANGEABLE; 23 | private volatile boolean isStart; 24 | private WeakHashMap weakHashMap = new WeakHashMap(); 25 | private PostionFL postionFL=new PostionFL(); 26 | 27 | /* 28 | * @param beatTime 更新频率 29 | * @param changeable 易变得,如果为true 30 | * 会在bind方法里访问Map,不存在则加入Map 31 | * 如果存在频繁的下拉刷新,分页加载建议设为true 32 | * 33 | * 34 | * 如果列表数据问题请设置为false,Holder会在GC后自动停止更新。 35 | * 36 | * 37 | * */ 38 | public Center(int beatTime, boolean changeable) { 39 | BEAT_TIME = beatTime; 40 | CHANGEABLE = changeable; 41 | } 42 | public static class PostionFL{ 43 | public int frist=-1; 44 | public int last=-1; 45 | 46 | } 47 | 48 | class CountDownHandler extends Handler { 49 | @Override 50 | public void handleMessage(Message msg) { 51 | switch (msg.what) { 52 | case 0: 53 | notifyHolder(); 54 | sendEmptyMessageDelayed(0, BEAT_TIME); 55 | break; 56 | 57 | default: 58 | } 59 | 60 | } 61 | } 62 | 63 | private CountDownHandler handler = new CountDownHandler(); 64 | 65 | 66 | @Override 67 | public void bindRecyclerView(RecyclerView recyclerView){ 68 | recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { 69 | @Override 70 | public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 71 | super.onScrollStateChanged(recyclerView, newState); 72 | } 73 | 74 | @Override 75 | public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 76 | super.onScrolled(recyclerView, dx, dy); 77 | int first=-1; 78 | int last=-1; 79 | 80 | RecyclerView.LayoutManager layoutManager=recyclerView.getLayoutManager(); 81 | if (layoutManager instanceof GridLayoutManager){ 82 | first=((GridLayoutManager)layoutManager).findFirstVisibleItemPosition(); 83 | last=((GridLayoutManager)layoutManager).findLastVisibleItemPosition(); 84 | } 85 | if (layoutManager instanceof LinearLayoutManager){ 86 | first=((LinearLayoutManager)layoutManager).findFirstVisibleItemPosition(); 87 | last=((LinearLayoutManager)layoutManager).findLastVisibleItemPosition(); 88 | } 89 | postionFL.frist=first; 90 | postionFL.last=last; 91 | 92 | Log.e("lmtlmt2","frist:"+first+"last:"+last); 93 | } 94 | }); 95 | } 96 | private void notifyHolder() { 97 | if (isStart) 98 | notifyObservers(); 99 | } 100 | 101 | private void notifyObservers() { 102 | 103 | Iterator iter = weakHashMap.entrySet().iterator(); 104 | while (iter.hasNext()) { 105 | Map.Entry entry = (Map.Entry) iter.next(); 106 | Observer observer = (Observer) entry.getKey(); 107 | if (observer != null) { 108 | if (postionFL.frist>-1&&postionFL.last>-1) 109 | observer.update(null, postionFL); 110 | else observer.update(null, null); 111 | } 112 | 113 | 114 | } 115 | Log.e("lmtlmt", "weakHashMap size" + weakHashMap.size()); 116 | 117 | } 118 | 119 | @Override 120 | public void startCountdown() { 121 | if (!isStart) { 122 | handler.sendEmptyMessage(0); 123 | isStart = true; 124 | } 125 | 126 | } 127 | 128 | @Override 129 | public void stopCountdown() { 130 | isStart = false; 131 | handler.removeCallbacksAndMessages(null); 132 | 133 | } 134 | 135 | @Override 136 | public void addObserver(Observer observer) { 137 | weakHashMap.put(observer, null); 138 | } 139 | 140 | @Override 141 | public void deleteObservers() { 142 | stopCountdown(); 143 | weakHashMap.clear(); 144 | } 145 | 146 | @Override 147 | public boolean containHolder(Observer observer) { 148 | //如果不是易变的 直接屏蔽次方法。 149 | if (!CHANGEABLE) return true; 150 | if (weakHashMap.containsKey(observer)) return true; 151 | return false; 152 | } 153 | 154 | @Override 155 | public void notifyAdapter() { 156 | if (!CHANGEABLE) return; 157 | deleteObservers(); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/floorcountdown/FloorCountDownLib/ICountDownCenter.java: -------------------------------------------------------------------------------- 1 | package com.example.floorcountdown.FloorCountDownLib; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | 5 | import java.util.Observer; 6 | 7 | /** 8 | * Created by air on 2019/3/14. 9 | */ 10 | public interface ICountDownCenter { 11 | void addObserver(Observer observer); 12 | void deleteObservers(); 13 | void startCountdown(); 14 | void stopCountdown(); 15 | boolean containHolder(Observer observer); 16 | void notifyAdapter(); 17 | void bindRecyclerView(RecyclerView recyclerView); 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/floorcountdown/ListAdapter.java: -------------------------------------------------------------------------------- 1 | package com.example.floorcountdown; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | import android.util.Log; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.TextView; 9 | 10 | import com.example.floorcountdown.FloorCountDownLib.Center; 11 | import com.example.floorcountdown.FloorCountDownLib.ICountDownCenter; 12 | 13 | import java.util.ArrayList; 14 | import java.util.Observable; 15 | import java.util.Observer; 16 | 17 | /** 18 | * Created by air on 2019/3/12. 19 | */ 20 | public class ListAdapter extends RecyclerView.Adapter { 21 | 22 | private ArrayList list = new ArrayList<>(); 23 | private ICountDownCenter countDownCenter; 24 | 25 | public ListAdapter(ArrayList list, ICountDownCenter countDownCenter) { 26 | this.list.addAll(list); 27 | this.countDownCenter = countDownCenter; 28 | } 29 | public void removeFloor(int i){ 30 | for (int j = 0; j < i; j++) { 31 | list.remove(0); 32 | } 33 | countDownCenter.notifyAdapter(); 34 | notifyDataSetChanged(); 35 | } 36 | 37 | @Override 38 | public int getItemViewType(int position) { 39 | return list.size(); 40 | } 41 | 42 | @Override 43 | public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 44 | View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false); 45 | ViewHolder viewHolder = new ViewHolder(view); 46 | countDownCenter.addObserver(viewHolder); 47 | countDownCenter.startCountdown(); 48 | return viewHolder; 49 | } 50 | 51 | @Override 52 | public void onBindViewHolder(ViewHolder holder, int position) { 53 | holder.lastBindPositon = position; 54 | holder.timeBean = list.get(position); 55 | bindCountView(holder.textView, holder.timeBean); 56 | if (!countDownCenter.containHolder(holder)){ 57 | countDownCenter.addObserver(holder); 58 | } 59 | 60 | } 61 | 62 | @Override 63 | public int getItemCount() { 64 | return list.size(); 65 | } 66 | 67 | static class ViewHolder extends RecyclerView.ViewHolder implements Observer { 68 | int lastBindPositon = -1; 69 | TextView textView; 70 | TimeBean timeBean; 71 | 72 | public ViewHolder(View itemView) { 73 | super(itemView); 74 | textView = itemView.findViewById(R.id.tv1); 75 | 76 | } 77 | 78 | @Override 79 | public void update(Observable o, Object arg) { 80 | 81 | if (arg!=null&&arg instanceof Center.PostionFL){ 82 | Center.PostionFL postionFL=(Center.PostionFL)arg; 83 | if (lastBindPositon>=postionFL.frist&&lastBindPositon<=postionFL.last){ 84 | Log.e("lmtlmtupdate", "update" + lastBindPositon); 85 | bindCountView(textView, timeBean); 86 | } 87 | } 88 | 89 | 90 | } 91 | 92 | //监控内存,可删除此方法实现 93 | @Override 94 | protected void finalize() throws Throwable { 95 | super.finalize(); 96 | Log.e("lmtlmt", "finalize" + lastBindPositon); 97 | } 98 | } 99 | 100 | //倒计时展示和结束逻辑 101 | private static void bindCountView(TextView textView, TimeBean timeBean) { 102 | if (timeBean == null) return; 103 | //倒计时结束 104 | if (timeBean.getRainTime() <= 0) { 105 | textView.setText("倒计时结束 活动开始"); 106 | return; 107 | } 108 | textView.setText("距开始 :" + timeBean.getRainTime() + ""); 109 | 110 | } 111 | 112 | 113 | } 114 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/floorcountdown/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.floorcountdown; 2 | 3 | import android.content.Intent; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.os.Bundle; 6 | import android.util.Log; 7 | import android.view.View; 8 | 9 | import java.util.Observable; 10 | 11 | public class MainActivity extends AppCompatActivity { 12 | 13 | @Override 14 | protected void onCreate(Bundle savedInstanceState) { 15 | super.onCreate(savedInstanceState); 16 | setContentView(R.layout.activity_main); 17 | findViewById(R.id.tv1).setOnClickListener(new View.OnClickListener() { 18 | @Override 19 | public void onClick(View v) { 20 | Intent intent = new Intent(MainActivity.this,MainActivityB.class); 21 | startActivity(intent); 22 | } 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/floorcountdown/MainActivityB.java: -------------------------------------------------------------------------------- 1 | package com.example.floorcountdown; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.support.v7.widget.GridLayoutManager; 6 | import android.support.v7.widget.LinearLayoutManager; 7 | import android.support.v7.widget.RecyclerView; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import android.widget.FrameLayout; 11 | 12 | import com.example.floorcountdown.FloorCountDownLib.Center; 13 | import com.example.floorcountdown.FloorCountDownLib.ICountDownCenter; 14 | 15 | import java.util.ArrayList; 16 | 17 | public class MainActivityB extends AppCompatActivity { 18 | 19 | private ArrayList list=new ArrayList<>(); 20 | private ICountDownCenter countDownCenter; 21 | private ListAdapter listAdapter; 22 | 23 | 24 | @Override 25 | protected void onCreate(Bundle savedInstanceState) { 26 | super.onCreate(savedInstanceState); 27 | setContentView(R.layout.activity_recycle); 28 | final RecyclerView recyclerView=new RecyclerView(this); 29 | recyclerView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); 30 | recyclerView.setLayoutManager(new LinearLayoutManager(this)); 31 | for (int i = 0; i < 100; i++) { 32 | list.add(new TimeBean(i*10)); 33 | } 34 | countDownCenter=new Center(1000,false); 35 | listAdapter=new ListAdapter(list,countDownCenter); 36 | recyclerView.setAdapter(listAdapter); 37 | final FrameLayout frameLayout=findViewById(R.id.f1); 38 | frameLayout.addView(recyclerView); 39 | countDownCenter.bindRecyclerView(recyclerView); 40 | 41 | 42 | 43 | findViewById(R.id.button).setOnClickListener(new View.OnClickListener() { 44 | @Override 45 | public void onClick(View v) { 46 | //countDownCenter.notifyAdapter(); 47 | listAdapter.removeFloor(99); 48 | // 测试重新setAdatper1 49 | // list=new ArrayList<>(); 50 | // list.add(new TimeBean(1*10000)); 51 | // countDownCenter.deleteObservers(); 52 | // countDownCenter=new Center(1000); 53 | // recyclerView.setAdapter(new ListAdapter(list,countDownCenter)); 54 | 55 | } 56 | }); 57 | 58 | 59 | 60 | } 61 | 62 | @Override 63 | protected void onDestroy() { 64 | super.onDestroy(); 65 | countDownCenter.deleteObservers(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/floorcountdown/TimeBean.java: -------------------------------------------------------------------------------- 1 | package com.example.floorcountdown; 2 | 3 | import android.os.SystemClock; 4 | 5 | /** 6 | * Created by air on 2019/3/13. 7 | */ 8 | public class TimeBean { 9 | private long elapsedRealtime; 10 | 11 | TimeBean(long remainTime) { 12 | elapsedRealtime = remainTime + SystemClock.elapsedRealtime()/1000; 13 | } 14 | 15 | 16 | public long getRainTime() { 17 | return elapsedRealtime - SystemClock.elapsedRealtime()/1000; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/pic1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidMsky/RecycleViewFloorCountDown/fee7db12ecdd7a5b28c1288b87ce94009b43d6f1/app/src/main/res/drawable-v24/pic1.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_recycle.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 |