網站首頁 編程語言 正文
前言
我相信一點,只要我們的產品中,涉及到列表的需求,肯定第一時間想到RecyclerView,即便是自定義View,那么RecyclerView也會是首選,為什么會選擇RecyclerView而不是ListView,主要就是RecyclerView的內存復用機制,這也是RecyclerView的核心?
?當RecyclerView展示列表信息的時候,獲取ItemView的來源有2個:一個是從適配器拿,另一個是從復用池中去拿;一開始的時候就是從復用池去拿,如果復用池中沒有,那么就從Adapter中去拿,這個時候就是通過onCreateViewHolder來創(chuàng)建一個ItemView。
1 RecyclerView的加載流程
?首先,當加載第一屏的時候,RecyclerView會向復用池中請求獲取View,這個時候復用池中是空的,因此就需要我們自己創(chuàng)建的Adapter,調用onCreateViewHolder創(chuàng)建ItemView,然后onBindViewHolder綁定數據,展示在列表上?
當我們滑動的時候第一個ItemView移出屏幕時,會被放到復用池中;同時,底部空出位置需要加載新的ItemView,觸發(fā)加載機制,這個時候復用池不為空,拿到復用的ItemView,調用Adapter的onBIndViewHolder方法刷新數據,加載到尾部;
這里有個問題,放在復用池的僅僅是View嗎?其實不是的,因為RecyclerView可以根據type類型加載不同的ItemView,那么放在復用池中的ItemView也是根據type進行歸類,當復用的時候,根據type取出不同類型的ItemView;
例如ItemView07的類型是ImageView,那么ItemView01在復用池中的類型是TextView,那么在加載ItemView07時,從復用池中是取不到的,需要Adapter新建一個ImageView類型的ItemView。
2 自定義RecyclerView
其實RecyclerView,我們在使用的時候,知道怎么去用它,但是內部的原理并不清楚,而且就算是看了源碼,時間久了就很容易忘記,所以只有當自己自定義RecyclerView之后才能真正了解其中的原理。
2.1 RecyclerView三板斧
通過第一節(jié)的加載流程,我們知道RecyclerView有3個重要的角色:RecyclerView、適配器、復用池,所以在自定義RecyclerView的時候,就需要先創(chuàng)建這3個角色;
/**
* 自定義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);
}
}
/**
* 復用池
*/
public class MyRecyclerViewPool {
}
/**
* Rv的ViewHolder
*/
public class ViewHolder {
private View itemView;
public ViewHolder(View itemView) {
this.itemView = itemView;
}
}
真正在應用層使用到的就是MyRecyclerView,通過設置Adapter實現View的展示
2.2 初始化工作
從加載流程中,我們可以看到,RecyclerView是協調Adapter和復用池的關系,因此在RecyclerView內部是持有這兩個對象的引用的。
//持有Adapter和復用池的引用
private Adapter mAdapter;
private MyRecyclerViewPool myRecyclerViewPool;
//Rv的寬高
private int mWidth;
private int mHeight;
//itemView的高度
private int[] heights;
那么這些變量的初始化,是在哪里做的呢?首先肯定不是在構造方法中做的,我們在使用Adapter的時候,會調用setAdapter,其實就是在這個時候,進行初始化的操作。
public void setAdapter(Adapter mAdapter) {
this.mAdapter = mAdapter;
this.needLayout = true;
//刷新頁面
requestLayout();
}
/**
* 對子View進行位置計算擺放
* @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;
}
}
每次調用setAdapter的時候,都會調用requestLayout刷新重新布局,這個時候會調用onLayout,因為onLayout的調用很頻繁非常耗性能,因此我們通知設置一個標志位needLayout,只有當需要刷新的時候,才能刷新重新擺放子View
2.3 ItemView的獲取與擺放
其實在RecyclerView當中,是對每個子View進行了測量,得到了它們的寬高,然后根據每個ItemView的高度擺放,這里我們就寫死了高度是200,僅做測試使用,后續(xù)優(yōu)化。
那么在擺放的時候,比如我們有200條數據,肯定不會把200條數據全部加載進來,默認就展示一屏的數據,所以需要判斷如果最后一個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;
//計算每個ItemView的寬高,然后擺放位置
rowCount = mAdapter.getItemCount();
//這里假定每個ItemView的高度為200,實際Rv是需要測量每個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);
//第二個top就是第一個的bottom
top = bottom;
}
}
}
}
我們先拿到之前的圖,確定下子View的位置?
?其實每個子View的left都是0,right都是RecyclerView的寬度,變量就是top和bottom,其實從第2個ItemView開始,top都是上一個ItemView的bottom,那么bottom就是 top + ItemView的高度
在確定了子View的位置參數之后,就可以獲取子View來進行擺放,其實在應用層是對子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;
//首先從復用池中查找
//如果找不到,那么就通過適配器生成
if(mAdapter !=null){
viewHolder = mAdapter.onCreateViewHolder(this,mAdapter.getItemViewType(row));
}
return viewHolder;
}
通過調用obtainViewHolder來獲取ViewHolder對象,其實是分2步的,首先 是從緩存池中去拿,在第一節(jié)加載流程中提及到,緩存池中不只是存了一個ItemView的布局,而是通過type標注了ItemView,所以從緩存池中需要根據type來獲取,如果沒有獲取到,那么就調用Adapter的onCreateViewHolder獲取,這種避免了每個ItemView都通過onCreateViewHolder創(chuàng)建,浪費系統(tǒng)資源;
在拿到了ViewHolder之后,調用根布局ItemView的layout方法進行位置擺放。
2.4 復用池
前面我們提到,在復用池中不僅僅是緩存了一個布局,而是每個type都對應一組回收的Holder,所以在復用池中存在一個容器存儲ViewHolder
/**
* 復用池
*/
public class MyRecyclerViewPool {
static class scrapData{
List<ViewHolder> viewHolders = new ArrayList<>();
}
private SparseArray<scrapData> array = new SparseArray<>();
/**
* 從緩存中獲取ViewHolder
* @param type ViewHolder的類型,用戶自己設置
* @return ViewHolder
*/
public ViewHolder getRecyclerView(int type){
}
/**
* 將ViewHolder放入緩存池中
* @param holder
*/
public void putRecyclerView(ViewHolder holder){
}
}
當RecyclerView觸發(fā)加載機制的時候,首先會從緩存池中取出對應type的ViewHolder;當ItemView移出屏幕之后,相應的ViewHolder會被放在緩存池中,因此存在對應的2個方法,添加及獲取
/**
* 從緩存中獲取ViewHolder
*
* @param type ViewHolder的類型,用戶自己設置
* @return ViewHolder
*/
public static ViewHolder getRecyclerView(int type) {
//首先判斷type
if (array.get(type) != null && !array.get(type).viewHolders.isEmpty()) {
//將最后一個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 數據更新
無論是從緩存池中拿到了緩存的ViewHolder,還是通過適配器創(chuàng)建了ViewHolder,最終都需要將ViewHolder進行數據填充
private ViewHolder obtainViewHolder(int row, int width, int height) {
int itemViewType = mAdapter.getItemViewType(row);
//首先從復用池中查找
ViewHolder viewHolder = MyRecyclerViewPool.getRecyclerView(itemViewType);
//如果找不到,那么就通過適配器生成
if(viewHolder == null){
viewHolder = mAdapter.onCreateViewHolder(this,itemViewType);
}
//更新數據
if (mAdapter != null) {
mAdapter.onBindViewHolder(viewHolder, row);
//設置ViewHOlder的類型
viewHolder.setItemViewType(itemViewType);
//測量
viewHolder.itemView.measure(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
);
addView(viewHolder.itemView);
}
return viewHolder;
}
如果跟到這里,我們其實已經完成了RecyclerView的基礎功能,一個首屏列表的展示
3 RecyclerView滑動事件處理
3.1 點擊事件與滑動事件
對于RecyclerView來說,我們需要的其實是對于滑動事件的處理,對于點擊事件來說,通常是子View來響應,做相應的跳轉或者其他操作,所以對于點擊事件和滑動事件,RecyclerView需要做定向的處理。
那么如何區(qū)分點擊事件和滑動事件?
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_MOVE:
return true;
}
return false;
}
在容器中,如果碰到MOVE事件就攔截就認為是滑動事件,這種靠譜嗎?顯然 不是的,當手指點擊到屏幕上時,首先系統(tǒng)會接收到一次ACTION_DWON時間,在手指抬起之前,ACTION_DWON只會響應一次,而且ACTION_MOVE會有無數次,因為人體手指是有面積的,當我們點下去肯定不是一個點,而是一個面肯定會存在ACTION_MOVE事件,但這種我們會認為是點擊事件;
所以對于滑動事件,我們會認為當手指移動一段距離之后,超出某個距離就是滑動事件,這個最小滑動距離通過ViewConfiguration來獲取。
private void init(Context context) {
ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
this.touchSlop = viewConfiguration.getScaledTouchSlop();
}
因為列表我們認為是豎直方向滑動的,所以我們需要記錄手指在豎直方向上的滑動距離。
@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){
//認為是滑動了
intercept = true;
}
break;
}
return intercept;
}
我們通過intercept標志位,來判斷當前是否在進行滑動,如果滑動的距離超出了touchSlop,那么就將事件攔截,在onTouchEvent中消費這個事件。
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE: {
//判斷滑動的方向
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進行滑動,那么scrollBy和scrollTo有什么區(qū)別,那就根據Android的坐標系開始說起?
?scrollBy滑動,其實是滑動的偏移量,相對于上一次View所在的位置,例如上圖中,View上滑,偏移量就是(200 - 100 = 100),所以調用scrollBy(0,100)就是向上滑動,反之就是上下滑動;
scrollTo滑動,滑動的是絕對距離,例如上圖中,View上滑,那么需要傳入詳細的坐標scrollTo(200,100),下滑scrollTo(200,300),其實scrollBy內部調用也是調用的scrollTo,所以偏移量就是用來計算絕對位置的。
3.3 滑動帶來的View回收
當滑動屏幕的時候,有一部分View會被滑出到屏幕外,那么就涉及到了View的回收和View的重新擺放。
首先分析向上滑動的操作,首先我們用scrollY來標記,屏幕中第一個子View左上角距離屏幕左上角的距離,默認就是0.
@Override
public void scrollBy(int x, int y) {
super.scrollBy(x, y);
scrollY += y;
if (scrollY > 0) {
Log.e(TAG, "上滑");
//防止一次滑動多個子View出去
while (scrollY > heights[firstRow]) {
//被移除,放入回收池
if (!viewHolders.isEmpty()) {
removeView(viewHolders.remove(0));
}
scrollY -= heights[firstRow];
firstRow++;
}
} else {
Log.e(TAG, "下滑");
}
}
?當ItemView1移出屏幕之后,因為上滑scrollY > 0,所以scrollY肯定會超過Itemiew 的高度,這里有個情況就是,如果一次滑出去多個ItemView,那么高度肯定是超過單個ItemView的高度,這里用firstRow來標記,當前子View在數據集合中的位置,所以這里使用的是while循環(huán)。
/**
* 移除ViewHolder,放入回收池
*
* @param holder
*/
private void removeView(ViewHolder holder) {
MyRecyclerViewPool.putRecyclerView(holder);
//系統(tǒng)方法,從RecyclerView中移除這個View
removeView(holder.itemView);
viewHolders.remove(holder);
}
如果滑出去多個子View,那么就循環(huán)從viewHolders(當前屏幕展示的View的集合)中移除,移除的ViewHolder就被放在了回收池中,然后從當前屏幕中移除;
3.4 加載機制
既然有移除,那么就會有新增,當底部出現空缺的時候,就會觸發(fā)加載機制,那么每次移除一個元素,都會有一個元素添加進來嗎?其實不然?
?像ItemView1移除之后,最底部的ItemView還沒有完全展示出來,其實是沒有觸發(fā)加載的,那么什么時候觸發(fā)加載呢?
在當前屏幕中展示的View其實是在緩存中的,那么只要計算緩存中全部ItemView的高度跟屏幕的高度比較,如果不足就需要填充。
//如果小于屏幕的高度
while (getRealHeight(firstRow) <= mHeight) {
//觸發(fā)加載機制
int addIndex = firstRow + viewHolders.size();
ViewHolder viewHolder = obtainViewHolder(addIndex, mWidth, heights[addIndex]);
viewHolders.add(viewHolders.size(), viewHolder);
Log.e(TAG,"添加一個View");
}
/**
* 獲取實際展示的高度
*
* @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;
}
這樣其實就實現了,一個View移除屏幕之后,會有一個新的View添加進來
/**
* 重新擺放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;
}
}
當然新的View只要添加進來,就需要對他進行重新擺放,這樣上滑就實現了(只有上滑哦)?
3.5 RecyclerView下滑處理
在此之前,我們處理了上滑的事件,頂部的View移出,下部分的View添加進來,那么下滑正好相反。?
?那么下滑添加View的時機是什么呢?就是scrollY小于0的時候,會有新的View添加進來
//下滑頂部添加View
while (scrollY < 0) {
//獲取ViewHolder
ViewHolder viewHolder = obtainViewHolder(firstRow - 1, mWidth, heights[firstRow - 1]);
//放到屏幕緩存ViewHolder最頂部的位置
viewHolders.add(0, viewHolder);
firstRow--;
//當頂部ItemView完全加進來之后,需要改變scrollY的值
scrollY += heights[firstRow];
}
此時需要將添加的View,放在屏幕展示View緩存的首位,然后firstRow需要-1;
那么當新的View添加進來之后,底部View需要移除,那么移除的時機是什么呢?先把尾部最后一個View的高度拋開,繼續(xù)往下滑動,如果當前屏幕展示的View的高度超過了屏幕高度,那么就需要移除
//底部移除View
while (!viewHolders.isEmpty() &&
getRealHeight(firstRow) - viewHolders.get(viewHolders.size() - 1).itemView.getHeight() >= mHeight) {
//需要移除
removeView(viewHolders.remove(viewHolders.size() - 1));
}
3.6 邊界問題
當我們上滑或者下滑的時候,firstRow都在遞增或者遞減,但是firstRow肯定是有邊界的,例如滑到最上端的時候,firstRow最小就是0,如果再-1,那么就會數組越界,最下端也有邊界,那就是數組的最大長度。
/**
* @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));
}
}
首先看下滑,這個時候firstRow > 0,這個時候getSumArray的值是逐漸減小的,等到最頂部,也就是滑到firstRow = 0的時候,這個時候getSumArray = 0,那么再往下滑其實還是能滑的,這個時候我們需要做限制,取scrollY 和 getSumArray的最大值,如果一致下滑,getSumArray一致都是0,然后scrollY < 0,最終scrollY = 0,不會再執(zhí)行下滑的操作了。
接下來看上滑,正常情況下,如果200條數據,那么當firstRow = 10的時候,剩下190個ItemView的高度(減去上滑的高度)肯定是高于屏幕高度的,那么一直滑,當發(fā)現剩余的ItemView的高度不足以占滿整個屏幕的時候,就是沒有數據了,這個時候,其實就可以把scrollY設置為0,不能再繼續(xù)滑動了。
@Override
public void scrollBy(int x, int y) {
// super.scrollBy(x, y);
scrollY += y;
scrollBounds(scrollY, firstRow);
if (scrollY > 0) {
Log.e(TAG, "上滑");
//防止一次滑動多個子View出去
while (scrollY > heights[firstRow]) {
//被移除,放入回收池
if (!viewHolders.isEmpty()) {
removeView(viewHolders.remove(0));
}
scrollY -= heights[firstRow];
firstRow++;
Log.e("scrollBy", "scrollBy 移除一個View size =="+viewHolders.size());
}
//如果小于屏幕的高度
while (getRealHeight(firstRow) < mHeight) {
//觸發(fā)加載機制
int addIndex = firstRow + viewHolders.size();
ViewHolder viewHolder = obtainViewHolder(addIndex, mWidth, heights[addIndex]);
viewHolders.add(viewHolders.size(), viewHolder);
Log.e("scrollBy", "scrollBy 添加一個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--;
//當頂部ItemView完全加進來之后,需要改變scrollY的值
scrollY += heights[firstRow];
}
}
}
OK,這其實跟RecyclerView的源碼相比,簡直就是一個窮人版的RecyclerView,但是其中的思想我們是可以借鑒的,尤其是回收池的思想,在開發(fā)中是可以借鑒的,下面展示的就是最后的成果?
原文鏈接:https://juejin.cn/post/7103054421856092168
相關推薦
- 2022-01-26 使用Guzzle拓展包請求接口失敗重試
- 2022-06-19 mybatis-plus的sql語句打印問題小結_MsSql
- 2022-12-09 Python?keras.metrics源代碼分析_python
- 2022-09-29 基于Python3編寫一個GUI翻譯器_python
- 2022-09-15 教你如何將應用從docker-compose遷移到k8s中_docker
- 2022-05-05 Android開發(fā)之自定義加載動畫詳解_Android
- 2022-08-13 Spring Boot 攔截器
- 2022-01-02 無法將“node.exe”項識別為 cmdlet、函數、腳本文件或可運行程序的名稱
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細win安裝深度學習環(huán)境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發(fā)現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態(tài)字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支