轉(zhuǎn)自RecyclerView系列之四實現(xiàn)回收復(fù)用
在上面文章自定義LayoutManager中講解了LayoutManager的自定義,實現(xiàn)了界面的展示和滑動剪撬。熟悉RecyclerView的都知道其緩存機制四瘫,在添加緩存的回收與復(fù)用之前,先簡單介紹下RecyclerView的緩存機制。
1逞泄、RecyclerView的回收復(fù)用原理
1.1铛漓、RecyclerView的回收
在滑動過程中,RecyclerView會將即將與之分離的ViewHolder放到mCachedViews和mRecyclerPool砸民,可使用
removeAndRecycleView(View child, Recycler recycler)
函數(shù)進行回收抵怎。
這兩級緩存的區(qū)別是:mCachedViews是第一級緩存奋救,大小默認(rèn)為2,數(shù)據(jù)結(jié)構(gòu)是ArrayList反惕。當(dāng)其中數(shù)量超過2時尝艘,會根據(jù)FIFO的原則移除元素,并將移除的元素添加到mRecyclerPool中姿染。
mRecyclerPool是一個緩存池背亥,本質(zhì)上它是SparseArray,key是itemViewType悬赏,value是一個ArrayList狡汉,value的大小默認(rèn)為5。除了上面說到兩級緩存還有mAttachedScrap闽颇,在onLayoutChildren中會調(diào)用函數(shù)
detachAndScrapAttachedViews(recycler);
將屏幕上ViewHolder進行detach盾戴,并暫存到mAttachedScrap,再重新布局時從mAttachedScrap中取出兵多,attach到RecyclerView上尖啡。
1.2、RecyclerView的復(fù)用
通過View view = recycler.getViewForPosition(position)
可以實現(xiàn)復(fù)用剩膘,根據(jù)源碼可知衅斩,在RecyclerView中,總共有四級緩存怠褐,優(yōu)先級:mAttachedScrap>mCachedViews>mViewCacheExtension>mRecyclerPool畏梆。
- mAttachedScrap:只保存當(dāng)前屏幕中detach的ViewHolder,在重新布局時復(fù)用。
- mCachedViews:緩存的是剛從RecyclerView中移除的ViewHolder(通過
removeAndRecycleView(view, recycler)方法
)惫搏,在復(fù)用時需要position或id匹配才能復(fù)用具温,所以只有在來回滑動過程中才會復(fù)用mCachedViews中的ViewHolder。如果不能匹配就需要從mRecyclerPool中取出ViewHolder并重新綁定數(shù)據(jù)筐赔。 - 復(fù)用mAttachedScrap铣猩、mCachedViews中的ViewHolder是需要精確匹配的,如果能匹配上可直接使用不需綁定數(shù)據(jù)茴丰,如果不能精確匹配达皿,即使mAttachedScrap、mCachedViews中有緩存也不能取出使用贿肩,只能從mRecyclerPool中取出使用峦椰,并且需重綁數(shù)據(jù)。如果mRecyclerPool中沒有緩存就需要調(diào)用onCreateViewHolder進行創(chuàng)建汰规。
2汤功、幾個函數(shù)
public void detachAndScrapAttachedViews(Recycler recycler)
僅用于onLayoutChildren中,在布局前將屏幕上的ViewHolder從RecyclerView中detach掉溜哮,將其放在mAttachedScrap中滔金,以供重新布局時使用色解。View view = recycler.getViewForPosition(position)
當(dāng)我們需要填充布局時,就可以調(diào)用該方法餐茵,從四個緩存容器中取出合適的View,然后添加到RecyclerView中科阎。removeAndRecycleView(child, recycler)
該函數(shù)僅用于在滑動過程中,在滾動時忿族,將滾出屏幕的ViewHolder進行remove并添加到mCachedViews或mRecyclerPool中锣笨。
可以看到,正是這三個函數(shù)的使用道批,可以讓我們自定義的LayoutManager具有復(fù)用功能错英。
另外,還有幾個常用隆豹,但經(jīng)常出錯的函數(shù):int getItemCount()
得到的是Adapter中總共有多少數(shù)據(jù)要顯示走趋,也就是總共有多少個itemint getChildCount()
得到的是當(dāng)前RecyclerView在顯示的item的個數(shù),所以這就是getChildCount()與 getItemCount()的區(qū)別View getChildAt(int position)
獲取某個可見位置的View噪伊,需要非常注意的是,它的位置索引并不是Adapter中的位置索引氮唯,而是當(dāng)前在屏幕上的位置的索引鉴吹。也就是說,要獲取當(dāng)前屏幕上在顯示的第一個item的View,應(yīng)該用getChidAt(0)惩琉,同樣豆励,如果要得到當(dāng)前屏幕上在顯示的最后一個item的View,應(yīng)該用getChildAt(getChildCount()-1)int getPosition(View view)
這個函數(shù)用于得到某個View在Adapter中的索引位置瞒渠,我們經(jīng)常將它與getChildAt(int position)聯(lián)合使用良蒸,得到某個當(dāng)前屏幕上在顯示的View在Adapter中的位置。
3伍玖、自定義LayoutManager的回收和復(fù)用原理
從上面的原理中可以看到嫩痰,回收復(fù)用主要有兩部分:
第一:在onLayoutChildren初始布局時:
- 1、在布局前調(diào)用
detachAndScrapAttachedViews(recycler)
將所有可見的ViewHolder detach窍箍。 - 2串纺、通過調(diào)用
recycler.getViewForPosition(position)
申請一個View,并添加到RecyclerView中椰棘,直到填充滿整個屏幕纺棺。
第二:在scrollVerticallyBy滑動時
- 1、判斷滾動dy后邪狞,那些ViewHolder需要回收祷蝌,然后調(diào)用
removeAndRecycleView(child, recycler)
進行回收。 - 2帆卓、然后通過調(diào)用
recycler.getViewForPosition(position)
獲取View巨朦,填充空白區(qū)域米丘。
4、為自定義LayoutManager添加回收復(fù)用
4.1罪郊、修改onLayoutChildren
上面已經(jīng)提到蠕蚜,在onLayoutChildren中,我們主要做兩件事:
- 1悔橄、在布局前調(diào)用
detachAndScrapAttachedViews(recycler)
將所有可見的ViewHolder detach靶累。 - 2、通過調(diào)用
recycler.getViewForPosition(position)
申請一個View癣疟,并添加到RecyclerView中挣柬,直到填充滿整個屏幕。
關(guān)鍵就在于如何判斷一屏能顯示多少個item睛挚,在這里每個item高度相同邪蛔,所以可以通過RecyclerView的高度處于item的高度即可
private int mItemWidth,mItemHeight;
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getItemCount() == 0) {//沒有Item,界面空著吧
detachAndScrapAttachedViews(recycler);
return;
}
detachAndScrapAttachedViews(recycler);
View childView = recycler.getViewForPosition(0);
measureChildWithMargins(childView, 0, 0);
mItemWidth = getDecoratedMeasuredWidth(childView);
mItemHeight = getDecoratedMeasuredHeight(childView);
int visiableCount = (int) Math.ceil(getVerticalSpace() * 1.0f / mItemHeight);
if (visiableCount > itemCount)
visiableCount = itemCount;
…………
}
//其中 getVerticalSpace()在上面已經(jīng)提到扎狱,得到的是RecyclerView用于顯示的高度侧到,它的定義是:
private int getVerticalSpace() {
return getHeight() - getPaddingBottom() - getPaddingTop();
}
一屏可見的item個數(shù)=(int) Math.ceil(getVerticalSpace() * 1.0f / mItemHeight);
,這里使用Math.ceil進行向上取整的原因就是:如果一屏內(nèi)可顯示1.5個item淤击,此時可見的item應(yīng)該為2才對匠抗。
除此之外,由于item高度相同污抬,為了布局方便汞贸,我們在初始化時,利用一個變量來保存在初始化時印机,在Adapter中每一個item的位置:
int offsetY = 0;
for (int i = 0; i < getItemCount(); i++) {
Rect rect = new Rect(0, offsetY, mItemWidth, offsetY + mItemHeight);
mItemRects.put(i, rect);
offsetY += mItemHeight;
}
接下來布局可見的item矢腻,不可見的item不再布局
for (int i = 0; i < visibleCount; i++) {
Rect rect = mItemRects.get(i);
View view = recycler.getViewForPosition(i);
addView(view);
//addView后一定要measure,先measure再layout
measureChildWithMargins(view, 0, 0);
layoutDecorated(view, rect.left, rect.top, rect.right, rect.bottom);
}
mTotalHeight = Math.max(offsetY, getVerticalVisibleHeight());
因為射赛,在上面我們已經(jīng)從保存了初始化狀態(tài)下每個Item的位置多柑,所以在初始化時,直接從mItemRects中取出當(dāng)前要顯示的Item的位置楣责,直接將它擺放在這個位置就可以了顷蟆。需要注意的是,因為我們在之前已經(jīng)使用detachAndScrapAttachedViews(recycler);
將所有view從RecyclerView中剝離腐魂,所以帐偎,我們需要重新通過addView(view)
添加進來。在添加進來以后蛔屹,需要走一個這個View的測量和layout邏輯削樊,先經(jīng)過測量,再將它layout到指定位置。如果我們沒有測量直接layout漫贞,會什么都出不來甸箱,因為任何view的layout都是依賴measure出來的位置信息的。
到此迅脐,完整的onLayoutChildren的代碼如下:
private int mItemWidth, mItemHeight;
private SparseArray<Rect> mItemRects = new SparseArray<>();;
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getItemCount() == 0) {//沒有Item芍殖,界面空著吧
detachAndScrapAttachedViews(recycler);
return;
}
detachAndScrapAttachedViews(recycler);
//將item的位置存儲起來
View childView = recycler.getViewForPosition(0);
measureChildWithMargins(childView, 0, 0);
mItemWidth = getDecoratedMeasuredWidth(childView);
mItemHeight = getDecoratedMeasuredHeight(childView);
int visiableCount = (int) Math.ceil(getVerticalSpace() * 1.0f / mItemHeight);
if (visiableCount > itemCount)
visiableCount = itemCount;
//定義豎直方向的偏移量
int offsetY = 0;
for (int i = 0; i < getItemCount(); i++) {
Rect rect = new Rect(0, offsetY, mItemWidth, offsetY + mItemHeight);
mItemRects.put(i, rect);
offsetY += mItemHeight;
}
for (int i = 0; i < visibleCount; i++) {
Rect rect = mItemRects.get(i);
View view = recycler.getViewForPosition(i);
addView(view);
//addView后一定要measure,先measure再layout
measureChildWithMargins(view, 0, 0);
layoutDecorated(view, rect.left, rect.top, rect.right, rect.bottom);
}
//如果所有子View的高度和沒有填滿RecyclerView的高度谴蔑,
// 則將高度設(shè)置為RecyclerView的高度
mTotalHeight = Math.max(offsetY, getVerticalSpace());
}
4.2豌骏、處理滾動
經(jīng)過上面的分析可知,我們可知隐锭,我們首先回收滾出屏幕的ViewHolder窃躲,然后再填充滾動后的空白區(qū)域。向上滾動和向下滾動雖然都是回收滾出屏幕的ViewHolder钦睡,但是處理邏輯還是有區(qū)別的蒂窒,下面就按照滾動方向分為兩種情況進行分析。
4.2.1荞怒、處理向上滾動
向上滾動時dy>0洒琢,這里先假設(shè)向上滾動dy,然后判斷哪些ViewHolder需要回收褐桌,需要新增哪些item纬凤,然后再執(zhí)行offsetChildrenVertical(-travel)
進行滑動。
因為在開始移動之前撩嚼,我們對dy做了到頂、到底的邊界判斷并進行了修正挖帘。
int travel = dy;
//如果滑動到最頂部
if (mSumDy + dy < 0) {
travel = -mSumDy;
} else if (mSumDy + dy > mTotalHeight - getVerticalSpace()) {
//如果滑動到最底部
travel = mTotalHeight - getVerticalSpace() - mSumDy;
}
所以真正移動的距離完丽,是修正后的travel。所以在進行處理回收和填充item拇舀,應(yīng)該以travel進行判斷逻族。
1、判斷回收item
在判斷向上滑動回收哪些item時骄崩,應(yīng)該遍歷當(dāng)前屏幕所有可見的item聘鳞,假設(shè)讓它們向上滑動travel,然后判斷是否已經(jīng)超出了上邊界(y=0)要拂,如果超出就進行回收抠璃。
for (int i = getChildCount() - 1; i >= 0; i--) {
View child = getChildAt(i);
if (travel > 0) {//需要回收當(dāng)前屏幕,上越界的View
if (getDecoratedBottom(child) - travel< 0) {
removeAndRecycleView(child, recycler);
}
}
}
在上面代碼中:
- 首先遍歷屏幕上所有可見的item脱惰,這里getChildCount()-1表示屏幕上可見的最后一個item搏嗡,注意和getItemCount()的區(qū)別。
- 由于是獲取屏幕上可見的item,所以可以調(diào)用getChildAt(i)直接獲取采盒。開始我不注意使用了recycler.getViewForPosition(i)去獲取了旧乞,這是從四個緩存池中獲取,很明顯不對磅氨。
-
getDecoratedBottom(child) - travel< 0
表示向上移動travel后超出了上邊界尺栖,故對進入該判斷的item進行回收。 - 注意這里使用
removeAndRecycleView(child, recycler)
方法進行回收烦租,而不是detachAndScrapAttachedViews(recycler)
方法延赌。在滾動時,滾出屏幕的ViewHolder應(yīng)該remove掉左权,而不是detach掉皮胡。在onLayoutChildren中進行布局時,需要暫存屏幕上的ViewHolder赏迟,在再次布局時使用屡贺,此時就需要使用detachAndScrapAttachedViews(recycler)
方法。
2锌杀、填充空白區(qū)域
假設(shè)向上滾動了travel后甩栈,屏幕的位置如下圖,左邊是初始狀態(tài)糕再,右邊是移動后的情況量没,其中綠色框表示屏幕。其實RecyclerView的滑動只是其中的內(nèi)容在滑動突想,這里假設(shè)內(nèi)容的位置不動殴蹄,那么屏幕相對于內(nèi)容就發(fā)生滑動。
在滾動travel后猾担,屏幕此時所在的區(qū)域如下:
private Rect getVisibleArea(int travel) {
Rect result = new Rect(getPaddingLeft(), getPaddingTop() + mSumDy + travel, getWidth() - getPaddingRight(), getHeight()-getPaddingBottom() + mSumDy + travel);
return result;
}
mSumDy表示已經(jīng)滑動的距離袭灯,travel表示即將滑動的距離。所以mSumDy + travel表示此時滑動后绑嘹,屏幕的位置稽荧。
由于在onLayoutChildren中初始化布局時,已經(jīng)記錄每個item的初始位置工腋,在拿到屏幕移動后的位置后姨丈,只需要和初始化item的位置進行比對,如果存在交集就表示在屏幕內(nèi)擅腰,否則表示已滑出了屏幕蟋恬。
分析到這里,我們還是不知道哪些item要滑入屏幕趁冈,再回看下上圖不難看出筋现,滑入屏幕的item無非就是在當(dāng)前屏幕中可見的最后一個item的下一個item一直到第itemCount個item中一些item將要滑入屏幕。
Rect visibleRect = getVisibleArea(travel);
//布局子View階段
if (travel >= 0) {
View lastView = getChildAt(getChildCount() - 1);
int minPos = getPosition(lastView) + 1;//從最后一個View+1開始吧\
//順序addChildView
for (int i = minPos; i <= getItemCount() - 1; i++) {
Rect rect = mItemRects.get(i);
if (Rect.intersects(visibleRect, rect)) {
View child = recycler.getViewForPosition(i);
addView(child);
measureChildWithMargins(child, 0, 0);
layoutDecorated(child, rect.left, rect.top - mSumDy, rect.right, rect.bottom - mSumDy);
} else {
break;
}
}
}
mSumDy += travel;
// 平移容器內(nèi)的item
offsetChildrenVertical(-travel);
我們來看看上面代碼:
首先獲取滑動后的屏幕的位置
Rect visibleRect = getVisibleArea(travel);
然后,找到移動前最后一個可見的View
View lastView = getChildAt(getChildCount() - 1);
然后矾飞,找到它之后的一個item:
int minPos = getPosition(lastView) + 1;
然后從這個item開始查詢一膨,看它和它之后的每個item是不是都在可見區(qū)域內(nèi),之后就是判斷這個item是不是在顯示區(qū)域,如果在就加進來并且布局洒沦,如果不在就退出循環(huán):
for (int i = minPos; i <= getItemCount() - 1; i++) {
Rect rect = mItemRects.get(i);
if (Rect.intersects(visibleRect, rect)) {
View child = recycler.getViewForPosition(i);
addView(child);
measureChildWithMargins(child, 0, 0);
layoutDecorated(child, rect.left, rect.top - mSumDy, rect.right, rect.bottom - mSumDy);
} else {
break;
}
}
需要注意的是:mItemRects中記錄的是item的位置是參考屏幕(0,0)豹绪,在向上滾動時,我們需要把高度減去滑動的距離申眼,這樣才能實現(xiàn)滾入屏幕瞒津。注意這個滑動距離并不包括即將滑動的距離travel,雖然我們判斷哪些item是新增顯示時括尸,假設(shè)移動了travel巷蚪,其實到目前為止并沒有發(fā)生滾動。所以我們在布局時濒翻,仍然需要按上次的移動距離來進行布局屁柏,所以這里在布局時使用是layoutDecorated(child, rect.left, rect.top - mSumDy, rect.right, rect.bottom - mSumDy),單純只是減去了mSumDy,并沒有同時減去mSumDy和travel,最后才調(diào)用offsetChildrenVertical(-travel)來整體移動布局好的item有送。這時才會把我們剛才新增布局上的item顯示出來淌喻。
所以,此時完整的scrollVerticallyBy的代碼如下:
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getChildCount() <= 0) {
return dy;
}
int travel = dy;
//如果滑動到最頂部
if (mSumDy + dy < 0) {
travel = -mSumDy;
} else if (mSumDy + dy > mTotalHeight - getVerticalSpace()) {
//如果滑動到最底部
travel = mTotalHeight - getVerticalSpace() - mSumDy;
}
//回收越界子View
for (int i = getChildCount() - 1; i >= 0; i--) {
View child = getChildAt(i);
if (travel > 0) {//需要回收當(dāng)前屏幕雀摘,上越界的View
if (getDecoratedBottom(child) - travel < 0) {
removeAndRecycleView(child, recycler);
}
}
}
Rect visibleRect = getVisibleArea(travel);
//布局子View階段
if (travel >= 0) {
View lastView = getChildAt(getChildCount() - 1);
int minPos = getPosition(lastView) + 1;//從最后一個View+1開始吧
//順序addChildView
for (int i = minPos; i <= getItemCount() - 1; i++) {
Rect rect = mItemRects.get(i);
if (Rect.intersects(visibleRect, rect)) {
View child = recycler.getViewForPosition(i);
addView(child);
measureChildWithMargins(child, 0, 0);
layoutDecorated(child, rect.left, rect.top - mSumDy, rect.right, rect.bottom - mSumDy);
} else {
break;
}
}
}
mSumDy += travel;
// 平移容器內(nèi)的item
offsetChildrenVertical(-travel);
return travel;
}
此時已經(jīng)實現(xiàn)了向上滾動的功能裸删,并在向上滾動過程中,回收滑動屏幕的ViewHolder阵赠。
4.2.2涯塔、處理向下滾動
在分析完向上滾動的處理后,向下滾動的處理就很簡單了清蚀,和向上滾動是完全相反的匕荸。
1、判斷回收item
遍歷當(dāng)前屏幕可見的item轧铁,假設(shè)向下移動travel后,判斷哪些item滑出了屏幕的底部旦棉,回收滑出的item即可齿风。
for (int i = getChildCount() - 1; i >= 0; i--) {
View child = getChildAt(i);
if (travel > 0) {//需要回收當(dāng)前屏幕,上越界的View
…………
}else if (travel < 0) {//回收當(dāng)前屏幕绑洛,下越界的View
if (getDecoratedTop(child) - travel > getHeight() - getPaddingBottom()) {
removeAndRecycleView(child, recycler);
}
}
}
getDecoratedTop(child) - travel
得到移動travel距離后item的頂部位置救斑,然后判斷是否大于屏幕底部的位置getHeight() - getPaddingBottom()
,若大于則表示滑動了屏幕真屯。
2脸候、為滾動后的空白處填充Item
向下滾動,RecyclerView的頭部位置滾動后會有空白,故可以從當(dāng)前屏幕可見的第一個item的上一個開始遍歷运沦,到第0個item結(jié)束泵额,判斷哪些item在屏幕內(nèi),將在屏幕內(nèi)的item添加進來携添。
Rect visibleRect = getVisibleArea(travel);
//布局子View階段
if (travel >= 0) {
…………
} else {
View firstView = getChildAt(0);
int maxPos = getPosition(firstView) - 1;
for (int i = maxPos; i >= 0; i--) {
Rect rect = mItemRects.get(i);
if (Rect.intersects(visibleRect, rect)) {
View child = recycler.getViewForPosition(i);
addView(child, 0);
measureChildWithMargins(child, 0, 0);
layoutDecoratedWithMargins(child, rect.left, rect.top - mSumDy, rect.right, rect.bottom - mSumDy);
} else {
break;
}
}
}
下面來看看這段代碼:
在這里嫁盲,先得到在滾動前顯示的第一個item的前一個item:
View firstView = getChildAt(0);
int maxPos = getPosition(firstView) - 1;
如果在顯示區(qū)域,那么烈掠,就將它插在第一的位置:
addView(child, 0);
同樣羞秤,在布局Item時,由于還沒有移動左敌,所以在布局時并不考慮travel的事:layoutDecoratedWithMargins(child, rect.left, rect.top - mSumDy, rect.right, rect.bottom - mSumDy)
瘾蛋。
其它的代碼都很好理解了,這里就不再講了矫限。
這樣就完整實現(xiàn)了滾動的回收和復(fù)用功能了哺哼,完整的scrollVerticallyBy代碼如下:
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getChildCount() <= 0) {
return dy;
}
int travel = dy;
//如果滑動到最頂部
if (mSumDy + dy < 0) {
travel = -mSumDy;
} else if (mSumDy + dy > mTotalHeight - getVerticalSpace()) {
//如果滑動到最底部
travel = mTotalHeight - getVerticalSpace() - mSumDy;
}
//回收越界子View
for (int i = getChildCount() - 1; i >= 0; i--) {
View child = getChildAt(i);
if (travel > 0) {//需要回收當(dāng)前屏幕,上越界的View
if (getDecoratedBottom(child) - travel < 0) {
removeAndRecycleView(child, recycler);
continue;
}
} else if (travel < 0) {//回收當(dāng)前屏幕奇唤,下越界的View
if (getDecoratedTop(child) - travel > getHeight() - getPaddingBottom()) {
removeAndRecycleView(child, recycler);
}
}
}
Rect visibleRect = getVisibleArea(travel);
//布局子View階段
if (travel >= 0) {
View lastView = getChildAt(getChildCount() - 1);
int minPos = getPosition(lastView) + 1;//從最后一個View+1開始吧
//順序addChildView
for (int i = minPos; i <= getItemCount() - 1; i++) {
Rect rect = mItemRects.get(i);
if (Rect.intersects(visibleRect, rect)) {
View child = recycler.getViewForPosition(i);
addView(child);
measureChildWithMargins(child, 0, 0);
layoutDecorated(child, rect.left, rect.top - mSumDy, rect.right, rect.bottom - mSumDy);
} else {
break;
}
}
} else {
View firstView = getChildAt(0);
int maxPos = getPosition(firstView) - 1;
for (int i = maxPos; i >= 0; i--) {
Rect rect = mItemRects.get(i);
if (Rect.intersects(visibleRect, rect)) {
View child = recycler.getViewForPosition(i);
addView(child, 0);//將View添加至RecyclerView中幸斥,childIndex為1,但是View的位置還是由layout的位置決定
measureChildWithMargins(child, 0, 0);
layoutDecoratedWithMargins(child, rect.left, rect.top - mSumDy, rect.right, rect.bottom - mSumDy);
} else {
break;
}
}
}
mSumDy += travel;
// 平移容器內(nèi)的item
offsetChildrenVertical(-travel);
return travel;
}
到此為止咬扇,我們已經(jīng)為LayoutManager增加了回收復(fù)用的功能甲葬,但是這里我們調(diào)用offsetChildrenVertical(-travel)
來實現(xiàn)平移,當(dāng)需要實現(xiàn)在平移時懈贺,改變每個item的大小经窖、角度等參數(shù)時,offsetChildrenVertical(-travel)
就無法完成了梭灿。