Android?手寫RecyclerView實(shí)現(xiàn)列表加載
前言
我相信一點(diǎn),只要我們的產(chǎn)品中,涉及到列表的需求,肯定第一時(shí)間想到RecyclerView,即便是自定義View,那么RecyclerView也會(huì)是首選,為什么會(huì)選擇RecyclerView而不是ListView,主要就是RecyclerView的內(nèi)存復(fù)用機(jī)制,這也是RecyclerView的核心

當(dāng)RecyclerView展示列表信息的時(shí)候,獲取ItemView的來源有2個(gè):一個(gè)是從適配器拿,另一個(gè)是從復(fù)用池中去拿;一開始的時(shí)候就是從復(fù)用池去拿,如果復(fù)用池中沒有,那么就從Adapter中去拿,這個(gè)時(shí)候就是通過onCreateViewHolder來創(chuàng)建一個(gè)ItemView。
1 RecyclerView的加載流程

首先,當(dāng)加載第一屏的時(shí)候,RecyclerView會(huì)向復(fù)用池中請(qǐng)求獲取View,這個(gè)時(shí)候復(fù)用池中是空的,因此就需要我們自己創(chuàng)建的Adapter,調(diào)用onCreateViewHolder創(chuàng)建ItemView,然后onBindViewHolder綁定數(shù)據(jù),展示在列表上

當(dāng)我們滑動(dòng)的時(shí)候第一個(gè)ItemView移出屏幕時(shí),會(huì)被放到復(fù)用池中;同時(shí),底部空出位置需要加載新的ItemView,觸發(fā)加載機(jī)制,這個(gè)時(shí)候復(fù)用池不為空,拿到復(fù)用的ItemView,調(diào)用Adapter的onBIndViewHolder方法刷新數(shù)據(jù),加載到尾部;
這里有個(gè)問題,放在復(fù)用池的僅僅是View嗎?其實(shí)不是的,因?yàn)镽ecyclerView可以根據(jù)type類型加載不同的ItemView,那么放在復(fù)用池中的ItemView也是根據(jù)type進(jìn)行歸類,當(dāng)復(fù)用的時(shí)候,根據(jù)type取出不同類型的ItemView;
例如ItemView07的類型是ImageView,那么ItemView01在復(fù)用池中的類型是TextView,那么在加載ItemView07時(shí),從復(fù)用池中是取不到的,需要Adapter新建一個(gè)ImageView類型的ItemView。
2 自定義RecyclerView
其實(shí)RecyclerView,我們?cè)谑褂玫臅r(shí)候,知道怎么去用它,但是內(nèi)部的原理并不清楚,而且就算是看了源碼,時(shí)間久了就很容易忘記,所以只有當(dāng)自己自定義RecyclerView之后才能真正了解其中的原理。
2.1 RecyclerView三板斧
通過第一節(jié)的加載流程,我們知道RecyclerView有3個(gè)重要的角色:RecyclerView、適配器、復(fù)用池,所以在自定義RecyclerView的時(shí)候,就需要先創(chuàng)建這3個(gè)角色;
/**
* 自定義RecyclerView
*/
public class MyRecyclerView extends ViewGroup {
public MyRecyclerView(Context context) {
super(context);
}
public MyRecyclerView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return super.onInterceptTouchEvent(ev);
}
@Override
public void scrollBy(int x, int y) {
super.scrollBy(x, y);
}
interface Adapter<VH extends ViewHolder>{
VH onCreateViewHolder(ViewGroup parent,int viewType);
void onBindViewHolder(VH holder,int position);
int getItemCount();
int getItemViewType(int position);
}
}/**
* 復(fù)用池
*/
public class MyRecyclerViewPool {
}/**
* Rv的ViewHolder
*/
public class ViewHolder {
private View itemView;
public ViewHolder(View itemView) {
this.itemView = itemView;
}
}真正在應(yīng)用層使用到的就是MyRecyclerView,通過設(shè)置Adapter實(shí)現(xiàn)View的展示
2.2 初始化工作
從加載流程中,我們可以看到,RecyclerView是協(xié)調(diào)Adapter和復(fù)用池的關(guān)系,因此在RecyclerView內(nèi)部是持有這兩個(gè)對(duì)象的引用的。
//持有Adapter和復(fù)用池的引用 private Adapter mAdapter; private MyRecyclerViewPool myRecyclerViewPool; //Rv的寬高 private int mWidth; private int mHeight; //itemView的高度 private int[] heights;
那么這些變量的初始化,是在哪里做的呢?首先肯定不是在構(gòu)造方法中做的,我們?cè)谑褂肁dapter的時(shí)候,會(huì)調(diào)用setAdapter,其實(shí)就是在這個(gè)時(shí)候,進(jìn)行初始化的操作。
public void setAdapter(Adapter mAdapter) {
this.mAdapter = mAdapter;
this.needLayout = true;
//刷新頁面
requestLayout();
}
/**
* 對(duì)子View進(jìn)行位置計(jì)算擺放
* @param changed
* @param l
* @param t
* @param r
* @param b
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if(changed || needLayout){
needLayout = false;
mWidth = r - l;
mHeight = b - t;
}
}每次調(diào)用setAdapter的時(shí)候,都會(huì)調(diào)用requestLayout刷新重新布局,這個(gè)時(shí)候會(huì)調(diào)用onLayout,因?yàn)閛nLayout的調(diào)用很頻繁非常耗性能,因此我們通知設(shè)置一個(gè)標(biāo)志位needLayout,只有當(dāng)需要刷新的時(shí)候,才能刷新重新擺放子View
2.3 ItemView的獲取與擺放
其實(shí)在RecyclerView當(dāng)中,是對(duì)每個(gè)子View進(jìn)行了測量,得到了它們的寬高,然后根據(jù)每個(gè)ItemView的高度擺放,這里我們就寫死了高度是200,僅做測試使用,后續(xù)優(yōu)化。
那么在擺放的時(shí)候,比如我們有200條數(shù)據(jù),肯定不會(huì)把200條數(shù)據(jù)全部加載進(jìn)來,默認(rèn)就展示一屏的數(shù)據(jù),所以需要判斷如果最后一個(gè)ItemView的bottom超過了屏幕的高度,就停止加載。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if(changed || needLayout){
needLayout = false;
if(mAdapter != null){
mWidth = r - l;
mHeight = b - t;
//計(jì)算每個(gè)ItemView的寬高,然后擺放位置
rowCount = mAdapter.getItemCount();
//這里假定每個(gè)ItemView的高度為200,實(shí)際Rv是需要測量每個(gè)ItemView的高度
heights = new int[rowCount];
for (int i = 0; i < rowCount; i++) {
heights[i] = 200;
}
//擺放 -- 滿第一屏就停止擺放
for (int i = 0; i < rowCount; i++) {
bottom = top + heights[i];
//獲取View
ViewHolder holder = getItemView(i,0,top,mWidth,bottom);
viewHolders.add(holder);
//第二個(gè)top就是第一個(gè)的bottom
top = bottom;
}
}
}
}我們先拿到之前的圖,確定下子View的位置

其實(shí)每個(gè)子View的left都是0,right都是RecyclerView的寬度,變量就是top和bottom,其實(shí)從第2個(gè)ItemView開始,top都是上一個(gè)ItemView的bottom,那么bottom就是 top + ItemView的高度
在確定了子View的位置參數(shù)之后,就可以獲取子View來進(jìn)行擺放,其實(shí)在應(yīng)用層是對(duì)子View做了一層包裝 --- ViewHolder,因此這里獲取到的也是ViewHolder。
private ViewHolder getItemView(int row,int left, int top, int right, int bottom) {
ViewHolder viewHolder = obtainViewHolder(row,right - left,bottom - top);
viewHolder.itemView.layout(left,top,right,bottom);
return viewHolder;
}
private ViewHolder obtainViewHolder(int row, int width, int height) {
ViewHolder viewHolder = null;
//首先從復(fù)用池中查找
//如果找不到,那么就通過適配器生成
if(mAdapter !=null){
viewHolder = mAdapter.onCreateViewHolder(this,mAdapter.getItemViewType(row));
}
return viewHolder;
}通過調(diào)用obtainViewHolder來獲取ViewHolder對(duì)象,其實(shí)是分2步的,首先 是從緩存池中去拿,在第一節(jié)加載流程中提及到,緩存池中不只是存了一個(gè)ItemView的布局,而是通過type標(biāo)注了ItemView,所以從緩存池中需要根據(jù)type來獲取,如果沒有獲取到,那么就調(diào)用Adapter的onCreateViewHolder獲取,這種避免了每個(gè)ItemView都通過onCreateViewHolder創(chuàng)建,浪費(fèi)系統(tǒng)資源;
在拿到了ViewHolder之后,調(diào)用根布局ItemView的layout方法進(jìn)行位置擺放。
2.4 復(fù)用池
前面我們提到,在復(fù)用池中不僅僅是緩存了一個(gè)布局,而是每個(gè)type都對(duì)應(yīng)一組回收的Holder,所以在復(fù)用池中存在一個(gè)容器存儲(chǔ)ViewHolder
/**
* 復(fù)用池
*/
public class MyRecyclerViewPool {
static class scrapData{
List<ViewHolder> viewHolders = new ArrayList<>();
}
private SparseArray<scrapData> array = new SparseArray<>();
/**
* 從緩存中獲取ViewHolder
* @param type ViewHolder的類型,用戶自己設(shè)置
* @return ViewHolder
*/
public ViewHolder getRecyclerView(int type){
}
/**
* 將ViewHolder放入緩存池中
* @param holder
*/
public void putRecyclerView(ViewHolder holder){
}
}當(dāng)RecyclerView觸發(fā)加載機(jī)制的時(shí)候,首先會(huì)從緩存池中取出對(duì)應(yīng)type的ViewHolder;當(dāng)ItemView移出屏幕之后,相應(yīng)的ViewHolder會(huì)被放在緩存池中,因此存在對(duì)應(yīng)的2個(gè)方法,添加及獲取
/**
* 從緩存中獲取ViewHolder
*
* @param type ViewHolder的類型,用戶自己設(shè)置
* @return ViewHolder
*/
public static ViewHolder getRecyclerView(int type) {
//首先判斷type
if (array.get(type) != null && !array.get(type).viewHolders.isEmpty()) {
//將最后一個(gè)ViewHolder從列表中移除
List<ViewHolder> scrapData = array.get(type).viewHolders;
for (int i = scrapData.size() - 1; i >= 0; i--) {
return scrapData.remove(i);
}
}
return null;
}
/**
* 將ViewHolder放入緩存池中
*
* @param holder
*/
public static void putRecyclerView(ViewHolder holder) {
int key = holder.getItemViewType();
//獲取集合
List<ViewHolder> viewHolders = getScrapData(key).viewHolders;
viewHolders.add(holder);
}
private static ScrapData getScrapData(int key) {
ScrapData scrapData = array.get(key);
if(scrapData == null){
scrapData = new ScrapData();
array.put(key,scrapData);
}
return scrapData;
}2.5 數(shù)據(jù)更新
無論是從緩存池中拿到了緩存的ViewHolder,還是通過適配器創(chuàng)建了ViewHolder,最終都需要將ViewHolder進(jìn)行數(shù)據(jù)填充
private ViewHolder obtainViewHolder(int row, int width, int height) {
int itemViewType = mAdapter.getItemViewType(row);
//首先從復(fù)用池中查找
ViewHolder viewHolder = MyRecyclerViewPool.getRecyclerView(itemViewType);
//如果找不到,那么就通過適配器生成
if(viewHolder == null){
viewHolder = mAdapter.onCreateViewHolder(this,itemViewType);
}
//更新數(shù)據(jù)
if (mAdapter != null) {
mAdapter.onBindViewHolder(viewHolder, row);
//設(shè)置ViewHOlder的類型
viewHolder.setItemViewType(itemViewType);
//測量
viewHolder.itemView.measure(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
);
addView(viewHolder.itemView);
}
return viewHolder;
}如果跟到這里,我們其實(shí)已經(jīng)完成了RecyclerView的基礎(chǔ)功能,一個(gè)首屏列表的展示
3 RecyclerView滑動(dòng)事件處理
3.1 點(diǎn)擊事件與滑動(dòng)事件
對(duì)于RecyclerView來說,我們需要的其實(shí)是對(duì)于滑動(dòng)事件的處理,對(duì)于點(diǎn)擊事件來說,通常是子View來響應(yīng),做相應(yīng)的跳轉(zhuǎn)或者其他操作,所以對(duì)于點(diǎn)擊事件和滑動(dòng)事件,RecyclerView需要做定向的處理。
那么如何區(qū)分點(diǎn)擊事件和滑動(dòng)事件?
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_MOVE:
return true;
}
return false;
}在容器中,如果碰到MOVE事件就攔截就認(rèn)為是滑動(dòng)事件,這種靠譜嗎?顯然 不是的,當(dāng)手指點(diǎn)擊到屏幕上時(shí),首先系統(tǒng)會(huì)接收到一次ACTION_DWON時(shí)間,在手指抬起之前,ACTION_DWON只會(huì)響應(yīng)一次,而且ACTION_MOVE會(huì)有無數(shù)次,因?yàn)槿梭w手指是有面積的,當(dāng)我們點(diǎn)下去肯定不是一個(gè)點(diǎn),而是一個(gè)面肯定會(huì)存在ACTION_MOVE事件,但這種我們會(huì)認(rèn)為是點(diǎn)擊事件;
所以對(duì)于滑動(dòng)事件,我們會(huì)認(rèn)為當(dāng)手指移動(dòng)一段距離之后,超出某個(gè)距離就是滑動(dòng)事件,這個(gè)最小滑動(dòng)距離通過ViewConfiguration來獲取。
private void init(Context context) {
ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
this.touchSlop = viewConfiguration.getScaledTouchSlop();
}因?yàn)榱斜砦覀冋J(rèn)為是豎直方向滑動(dòng)的,所以我們需要記錄手指在豎直方向上的滑動(dòng)距離。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//判斷是否攔截
boolean intercept = false;
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
mCurrentY = (int) ev.getY();
break;
case MotionEvent.ACTION_MOVE:
//y值在不停改變
int y = (int) ev.getY();
if(Math.abs(y - mCurrentY) > touchSlop){
//認(rèn)為是滑動(dòng)了
intercept = true;
}
break;
}
return intercept;
}我們通過intercept標(biāo)志位,來判斷當(dāng)前是否在進(jìn)行滑動(dòng),如果滑動(dòng)的距離超出了touchSlop,那么就將事件攔截,在onTouchEvent中消費(fèi)這個(gè)事件。
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE: {
//判斷滑動(dòng)的方向
int diff = (int) (mCurrentY - event.getRawY());
if(Math.abs(diff) > touchSlop){
Log.e(TAG,"diff --- "+diff);
scrollBy(0, diff);
mCurrentY = (int) event.getRawY();
}
break;
}
}
return super.onTouchEvent(event);
}3.2 scrollBy和scrollTo
在onTouchEvent中,我們使用了scrollBy進(jìn)行滑動(dòng),那么scrollBy和scrollTo有什么區(qū)別,那就根據(jù)Android的坐標(biāo)系開始說起

scrollBy滑動(dòng),其實(shí)是滑動(dòng)的偏移量,相對(duì)于上一次View所在的位置,例如上圖中,View上滑,偏移量就是(200 - 100 = 100),所以調(diào)用scrollBy(0,100)就是向上滑動(dòng),反之就是上下滑動(dòng);
scrollTo滑動(dòng),滑動(dòng)的是絕對(duì)距離,例如上圖中,View上滑,那么需要傳入詳細(xì)的坐標(biāo)scrollTo(200,100),下滑scrollTo(200,300),其實(shí)scrollBy內(nèi)部調(diào)用也是調(diào)用的scrollTo,所以偏移量就是用來計(jì)算絕對(duì)位置的。
3.3 滑動(dòng)帶來的View回收
當(dāng)滑動(dòng)屏幕的時(shí)候,有一部分View會(huì)被滑出到屏幕外,那么就涉及到了View的回收和View的重新擺放。
首先分析向上滑動(dòng)的操作,首先我們用scrollY來標(biāo)記,屏幕中第一個(gè)子View左上角距離屏幕左上角的距離,默認(rèn)就是0.
@Override
public void scrollBy(int x, int y) {
super.scrollBy(x, y);
scrollY += y;
if (scrollY > 0) {
Log.e(TAG, "上滑");
//防止一次滑動(dòng)多個(gè)子View出去
while (scrollY > heights[firstRow]) {
//被移除,放入回收池
if (!viewHolders.isEmpty()) {
removeView(viewHolders.remove(0));
}
scrollY -= heights[firstRow];
firstRow++;
}
} else {
Log.e(TAG, "下滑");
}
}
當(dāng)ItemView1移出屏幕之后,因?yàn)樯匣瑂crollY > 0,所以scrollY肯定會(huì)超過Itemiew 的高度,這里有個(gè)情況就是,如果一次滑出去多個(gè)ItemView,那么高度肯定是超過單個(gè)ItemView的高度,這里用firstRow來標(biāo)記,當(dāng)前子View在數(shù)據(jù)集合中的位置,所以這里使用的是while循環(huán)。
/**
* 移除ViewHolder,放入回收池
*
* @param holder
*/
private void removeView(ViewHolder holder) {
MyRecyclerViewPool.putRecyclerView(holder);
//系統(tǒng)方法,從RecyclerView中移除這個(gè)View
removeView(holder.itemView);
viewHolders.remove(holder);
}如果滑出去多個(gè)子View,那么就循環(huán)從viewHolders(當(dāng)前屏幕展示的View的集合)中移除,移除的ViewHolder就被放在了回收池中,然后從當(dāng)前屏幕中移除;
3.4 加載機(jī)制
既然有移除,那么就會(huì)有新增,當(dāng)?shù)撞砍霈F(xiàn)空缺的時(shí)候,就會(huì)觸發(fā)加載機(jī)制,那么每次移除一個(gè)元素,都會(huì)有一個(gè)元素添加進(jìn)來嗎?其實(shí)不然

像ItemView1移除之后,最底部的ItemView還沒有完全展示出來,其實(shí)是沒有觸發(fā)加載的,那么什么時(shí)候觸發(fā)加載呢?
在當(dāng)前屏幕中展示的View其實(shí)是在緩存中的,那么只要計(jì)算緩存中全部ItemView的高度跟屏幕的高度比較,如果不足就需要填充。
//如果小于屏幕的高度
while (getRealHeight(firstRow) <= mHeight) {
//觸發(fā)加載機(jī)制
int addIndex = firstRow + viewHolders.size();
ViewHolder viewHolder = obtainViewHolder(addIndex, mWidth, heights[addIndex]);
viewHolders.add(viewHolders.size(), viewHolder);
Log.e(TAG,"添加一個(gè)View");
}
/**
* 獲取實(shí)際展示的高度
*
* @param firstIndex
* @return
*/
private int getRealHeight(int firstIndex) {
return getSumArray(firstRow, viewHolders.size()) - scrollY;
}
private int getSumArray(int firstIndex, int count) {
int totalHeight = 0;
count+= firstIndex;
for (int i = firstIndex; i < count; i++) {
totalHeight += heights[i];
}
return totalHeight;
}這樣其實(shí)就實(shí)現(xiàn)了,一個(gè)View移除屏幕之后,會(huì)有一個(gè)新的View添加進(jìn)來
/**
* 重新擺放View
*/
private void repositionViews() {
int left = 0;
int top = -scrollY;
int right = mWidth;
int bottom = 0;
int index = firstRow;
for (int i = 0; i < viewHolders.size(); i++) {
bottom = top + heights[index++];
viewHolders.get(i).itemView.layout(left,top,right,bottom);
top = bottom;
}
}當(dāng)然新的View只要添加進(jìn)來,就需要對(duì)他進(jìn)行重新擺放,這樣上滑就實(shí)現(xiàn)了(只有上滑哦)

3.5 RecyclerView下滑處理
在此之前,我們處理了上滑的事件,頂部的View移出,下部分的View添加進(jìn)來,那么下滑正好相反。

那么下滑添加View的時(shí)機(jī)是什么呢?就是scrollY小于0的時(shí)候,會(huì)有新的View添加進(jìn)來
//下滑頂部添加View
while (scrollY < 0) {
//獲取ViewHolder
ViewHolder viewHolder = obtainViewHolder(firstRow - 1, mWidth, heights[firstRow - 1]);
//放到屏幕緩存ViewHolder最頂部的位置
viewHolders.add(0, viewHolder);
firstRow--;
//當(dāng)頂部ItemView完全加進(jìn)來之后,需要改變scrollY的值
scrollY += heights[firstRow];
}此時(shí)需要將添加的View,放在屏幕展示View緩存的首位,然后firstRow需要-1;
那么當(dāng)新的View添加進(jìn)來之后,底部View需要移除,那么移除的時(shí)機(jī)是什么呢?先把尾部最后一個(gè)View的高度拋開,繼續(xù)往下滑動(dòng),如果當(dāng)前屏幕展示的View的高度超過了屏幕高度,那么就需要移除
//底部移除View
while (!viewHolders.isEmpty() &&
getRealHeight(firstRow) - viewHolders.get(viewHolders.size() - 1).itemView.getHeight() >= mHeight) {
//需要移除
removeView(viewHolders.remove(viewHolders.size() - 1));
}3.6 邊界問題
當(dāng)我們上滑或者下滑的時(shí)候,firstRow都在遞增或者遞減,但是firstRow肯定是有邊界的,例如滑到最上端的時(shí)候,firstRow最小就是0,如果再-1,那么就會(huì)數(shù)組越界,最下端也有邊界,那就是數(shù)組的最大長度。
/**
* @param scrollY
* @param firstRow
*/
private void scrollBounds(int scrollY, int firstRow) {
if (scrollY > 0) {
//上滑
if (getSumArray(firstRow, heights.length - firstRow) - scrollY > mHeight) {
this.scrollY = scrollY;
} else {
this.scrollY = getSumArray(firstRow, heights.length - firstRow) - mHeight;
}
} else {
//下滑
this.scrollY = Math.max(scrollY, -getSumArray(0, firstRow));
}
}首先看下滑,這個(gè)時(shí)候firstRow > 0,這個(gè)時(shí)候getSumArray的值是逐漸減小的,等到最頂部,也就是滑到firstRow = 0的時(shí)候,這個(gè)時(shí)候getSumArray = 0,那么再往下滑其實(shí)還是能滑的,這個(gè)時(shí)候我們需要做限制,取scrollY 和 getSumArray的最大值,如果一致下滑,getSumArray一致都是0,然后scrollY < 0,最終scrollY = 0,不會(huì)再執(zhí)行下滑的操作了。
接下來看上滑,正常情況下,如果200條數(shù)據(jù),那么當(dāng)firstRow = 10的時(shí)候,剩下190個(gè)ItemView的高度(減去上滑的高度)肯定是高于屏幕高度的,那么一直滑,當(dāng)發(fā)現(xiàn)剩余的ItemView的高度不足以占滿整個(gè)屏幕的時(shí)候,就是沒有數(shù)據(jù)了,這個(gè)時(shí)候,其實(shí)就可以把scrollY設(shè)置為0,不能再繼續(xù)滑動(dòng)了。
@Override
public void scrollBy(int x, int y) {
// super.scrollBy(x, y);
scrollY += y;
scrollBounds(scrollY, firstRow);
if (scrollY > 0) {
Log.e(TAG, "上滑");
//防止一次滑動(dòng)多個(gè)子View出去
while (scrollY > heights[firstRow]) {
//被移除,放入回收池
if (!viewHolders.isEmpty()) {
removeView(viewHolders.remove(0));
}
scrollY -= heights[firstRow];
firstRow++;
Log.e("scrollBy", "scrollBy 移除一個(gè)View size =="+viewHolders.size());
}
//如果小于屏幕的高度
while (getRealHeight(firstRow) < mHeight) {
//觸發(fā)加載機(jī)制
int addIndex = firstRow + viewHolders.size();
ViewHolder viewHolder = obtainViewHolder(addIndex, mWidth, heights[addIndex]);
viewHolders.add(viewHolders.size(), viewHolder);
Log.e("scrollBy", "scrollBy 添加一個(gè)View size=="+viewHolders.size());
}
//重新擺放
repositionViews();
} else {
Log.e(TAG, "下滑");
//底部移除View
while (!viewHolders.isEmpty() &&
getRealHeight(firstRow) - viewHolders.get(viewHolders.size() - 1).itemView.getHeight() >= mHeight) {
//需要移除
removeView(viewHolders.remove(viewHolders.size() - 1));
}
//下滑頂部添加View
while (scrollY < 0) {
//獲取ViewHolder
ViewHolder viewHolder = obtainViewHolder(firstRow - 1, mWidth, heights[firstRow - 1]);
//放到屏幕緩存ViewHolder最頂部的位置
viewHolders.add(0, viewHolder);
firstRow--;
//當(dāng)頂部ItemView完全加進(jìn)來之后,需要改變scrollY的值
scrollY += heights[firstRow];
}
}
}OK,這其實(shí)跟RecyclerView的源碼相比,簡直就是一個(gè)窮人版的RecyclerView,但是其中的思想我們是可以借鑒的,尤其是回收池的思想,在開發(fā)中是可以借鑒的,下面展示的就是最后的成果

到此這篇關(guān)于Android 手寫RecyclerView實(shí)現(xiàn)列表加載的文章就介紹到這了,更多相關(guān)Android RecyclerView 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Android實(shí)現(xiàn)RecyclerView嵌套流式布局的詳細(xì)過程
- Android RecyclerView實(shí)現(xiàn)吸頂動(dòng)態(tài)效果流程分析
- Android RecyclerView四級(jí)緩存源碼層詳細(xì)分析
- Android RecyclerView緩存復(fù)用原理解析
- Android RecyclerView使用入門介紹
- Android開發(fā)RecyclerView單獨(dú)刷新使用技巧
- Android開發(fā)RecyclerView實(shí)現(xiàn)折線圖效果
- Android獲取RecyclerView滑動(dòng)距離方法詳細(xì)講解
相關(guān)文章
React Native中Android物理back鍵按兩次返回鍵即退出應(yīng)用
這篇文章主要給大家介紹了關(guān)于React Native中Android物理back鍵按兩次返回鍵即退出應(yīng)用的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2017-10-10
Android學(xué)習(xí)之AppWidget筆記分享
這篇文章主要為大家詳細(xì)介紹了Android學(xué)習(xí)筆記之AppWidget的相關(guān)資料,感興趣的小伙伴們可以參考一下2016-08-08
android用java動(dòng)態(tài)增添刪除修改布局
這篇文章主要介紹了android用java動(dòng)態(tài)增添刪除修改布局,感興趣的小伙伴們可以參考一下2016-03-03
Android的ListView多選刪除操作實(shí)現(xiàn)代碼
這篇文章主要為大家詳細(xì)介紹了Android的ListView多選刪除操作實(shí)現(xiàn)代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-05-05
Android實(shí)現(xiàn)ListView數(shù)據(jù)動(dòng)態(tài)加載的方法
這篇文章主要介紹了Android實(shí)現(xiàn)ListView數(shù)據(jù)動(dòng)態(tài)加載的方法,通過ListView控件綁定setOnScrollListener方法簡單實(shí)現(xiàn)動(dòng)態(tài)加載數(shù)據(jù)的功能,需要的朋友可以參考下2016-01-01
Android程序開發(fā)之給背景圖加上移動(dòng)的手勢
這篇文章主要介紹了Android程序開發(fā)之給背景圖加上移動(dòng)的手勢 的相關(guān)資料,需要的朋友可以參考下2016-03-03
Android新建水平節(jié)點(diǎn)進(jìn)度條示例
這篇文章主要為大家介紹了Android新建水平節(jié)點(diǎn)進(jìn)度條示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06
Android控件之使用ListView實(shí)現(xiàn)時(shí)間軸效果
這篇文章主要介紹了Android基礎(chǔ)控件之使用ListView實(shí)現(xiàn)時(shí)間軸效果的相關(guān)資料,本文是以查看物流信息為例,給大家介紹了listview時(shí)間軸的實(shí)現(xiàn)代碼,需要的朋友可以參考下2016-11-11
Android利用RenderScript實(shí)現(xiàn)毛玻璃模糊效果示例
毛玻璃效果(亦稱磨砂效果),近兩年在移動(dòng)端的UI設(shè)計(jì)上越來越流行,下面這篇文章主要介紹了Android利用RenderScript實(shí)現(xiàn)毛玻璃模糊效果的相關(guān)資料,文中給出了詳細(xì)的示例代碼,需要的朋友可以參考學(xué)習(xí),下面來一起看看吧。2017-03-03

