- 判斷是否滑動到最后 / 前的 Item
- 添加 HeaderView 和 FooterView
- ScrollView 嵌套情況下的問題
- 數(shù)據(jù)源刷新的問題
- item 局部刷新
- 平滑的滑動置頂
- ItemTouchHelper
參考 :
Android RecyclerView 使用完全解析 體驗(yàn)藝術(shù)般的控件
深入理解 RecyclerView 系列之一:ItemDecoration
開篇先說明一下吁朦,現(xiàn)在 RecyclerView 想必都很熟悉幔欧,我在項(xiàng)目里也對這個控件做過一些封裝滞欠、遇到過一些問題俺亮,這里僅做記錄笋熬。因?yàn)檫@次時間還是比較多术吝,所以打算深入一點(diǎn)研究學(xué)習(xí)弄痹、內(nèi)容也比較多所以分出了幾篇勾效,鏈接在上面嘹悼。
簡單初始化
//設(shè)置布局管理器
mRecyclerView.setLayoutManager(layout);
//設(shè)置adapter
mRecyclerView.setAdapter(adapter)
//設(shè)置Item增加、移除動畫
mRecyclerView.setItemAnimator(new DefaultItemAnimator());
//添加分割線
mRecyclerView.addItemDecoration(new DividerItemDecoration(
getActivity(), DividerItemDecoration.HORIZONTAL_LIST));
-
監(jiān)聽 Item 的點(diǎn)擊事件
因?yàn)?RecyclerView 沒直接的事件可以監(jiān)聽层宫,需要自己去設(shè)置杨伙,
我選擇在 RecyclerView.Adapter 中設(shè)置:
//在繼承了 RecyclerView.Adapter 的內(nèi)部類中
@Override
public void onBindViewHolder(ViewHolder holder, final int position) {
if (holder instanceof ViewHolder){
//設(shè)置 TextView
holder.textView.setText(data.get(position).getTitle());
//設(shè)置 Item 的點(diǎn)擊事件
holder.itemLayout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//自己定義的一個方法,將 position 傳出去
onItemClick(position);
}
});
}
}
//重寫的 ViewHolder萌腿,使用了 ButterKnife限匣,但邏輯上沒有變化
class ViewHolder extends RecyclerView.ViewHolder{
@BindView(R.id.news_item_title)
TextView newsTitle;
@BindView(R.id.news_item_img)
ImageView newsImg;
@BindView(R.id.news_item_layout)
LinearLayout newsLayout;
public ViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this,itemView);
}
}
Item 的布局:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/news_item_layout"
android:layout_width="match_parent"
android:layout_height="110dp"
android:layout_marginBottom="2.5dp"
android:layout_marginTop="2.5dp"
android:elevation="4dp"
android:background="@drawable/news_item_bg">
<TextView
android:textColor="#000"
android:id="@+id/news_item_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="17dip"
android:layout_marginTop="10dp"
android:layout_marginBottom="5dp"
/>
</LinearLayout>
</LinearLayout>
還有一種方法就是使用 addOnItemTouchListener,通過觸摸坐標(biāo)來判斷
可以參考: RecyclerView 無法添加 onItemClickListener 最佳的高效解決方案
監(jiān)聽滑動
//OnScrollListener 是繼承自 RecyclerView.OnScrollListener 的內(nèi)部類毁菱,見下文
mNewsRecyclerView.addOnScrollListener(new OnScrollListener());
監(jiān)聽是否滑動到底部
繼承 RecyclerView.OnScrollListener米死,
在 onScrolled
獲取 RecyclerView 的最后一個 Item 的位置锌历,
在 onScrollStateChanged
判斷當(dāng)前是否滑動到最后一個 Item。
/**
* 監(jiān)聽 RecyclerView 判斷是否滑到最后一個 Item
*/
class OnScrollListener extends RecyclerView.OnScrollListener{
private int lastVisibleItem;
/**
* 判斷是否滑動了最后一個 Item
* @param recyclerView
* @param newState
*/
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if( (newState == RecyclerView.SCROLL_STATE_IDLE)
&&(lastVisibleItem + 1 == mRecyclerViewAdapter.getItemCount())){
//newState 為 0:當(dāng)前屏幕停止?jié)L動峦筒;
//1:屏幕在滾動且用戶仍在觸碰或手指還在屏幕上究西;
//2:隨用戶的操作,屏幕上產(chǎn)生的慣性滑動物喷;
//在這里執(zhí)行刷新/加載更多的操作
loadMore();
}
/**
* 在這里獲取到 RecyclerView 的最后一個 Item 的位置
* @param recyclerView
* @param dx
* @param dy
*/
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
lastVisibleItem = mLinearLayoutManager.findLastVisibleItemPosition();
}
}
特別的是卤材,當(dāng) RecyclerView 實(shí)現(xiàn)了瀑布流,
就是使用了 StaggeredGridLayoutManager 的時候峦失,因?yàn)?Item 是交錯的商膊,
所以不能使用上面的方法,而是要判斷 findLastVisibleItemPosition
返回的數(shù)組的最大值宠进,上面的 onScrolled 方法要修改為:
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
int [] lasPositions = new int[mStaggeredGridLayoutManager.getSpanCount()];
int [] all = mStaggeredGridLayoutManager
.findLastVisibleItemPositions(lasPositions);
lastVisibleItem = findMax(all);
}
private int findMax(int[] lastPositions) {
int max = lastPositions[0];
for (int value : lastPositions) {
if (value > max) {
max = value;
}
}
return max;
}
添加 HeaderView 和 FooterView
- 在 ViewHolder 中添加 HeaderView 和 FooterView
//當(dāng)前 item 的類型,0 為頭布局,1 為底部布局
public static final int TYPE_HEADER = 0;
public static final int TYPE_FOOTER = 1;
public static final int TYPE_NORMAL = 2;
//
private View mHeaderView;
private View mFooterView;
//
public void addHeaderView(View mHeaderView) {
this.mHeaderView = mHeaderView;
}
public void addFooterView(View mFooterView) {
this.mFooterView = mFooterView;
}
- 通過設(shè)置 ItemViewType 來區(qū)分 HeaderView 和 FooterView
/**
* 為每個 Item 設(shè)置類型
* 如果頭布局不為空,則將第一個 Item 設(shè)為頭布局
* @param position
* @return
*/
@Override
public int getItemViewType(int position) {
if (mHeaderView == null && mFooterView == null)
return TYPE_NORMAL;
if (mHeaderView != null && position == 0)
return TYPE_HEADER;
if (mFooterView != null && position == getItemCount() - 1)
return TYPE_FOOTER;
return TYPE_NORMAL;
}
如果 mHeaderView 不為空晕拆,則將第一個 ItemView 設(shè)為 HeaderView,
如果 mFooterView 不為空材蹬,將最后一個 ItemView 設(shè)為 FooterView实幕。
- 創(chuàng)建 ItemView
/**
* 創(chuàng)建布局的時候如果發(fā)現(xiàn)是頭布局直接返回
* @param parent
* @param viewType
* @return
*/
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (mHeaderView != null && viewType == TYPE_HEADER){
return new ViewHolder(mHeaderView);
}
if (mFooterView != null && viewType == TYPE_FOOTER){
return new ViewHolder(mFooterView);
}
View v = LayoutInflater.from(getActivity())
.inflate(R.layout.news_list_item,parent,false);
ViewHolder viewHolder = new ViewHolder(v);
return viewHolder;
}
/**
* 只有當(dāng)前的 Item 的類型是 normal 時才執(zhí)行
* realPosition = position -1 是因?yàn)?0 被頭布局占用了
* @param holder
* @param position
*/
@Override
public void onBindViewHolder(ViewHolder holder, final int position) {
final int realPosition;
if (mHeaderView != null)
realPosition = position -1;
else
realPosition = position;
if (getItemViewType(position) == TYPE_NORMAL){
if (holder instanceof ViewHolder){
holder.newsTitle.setText(data.get(realPosition).getTitle());
Glide.with(getActivity()).load(data.get(realPosition).getImages())
.into(holder.newsImg);
//設(shè)置 Item 的點(diǎn)擊事件
holder.newsLayout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
OnItemClick(realPosition);
}
});
}
}
}
- 相應(yīng)的修改
/**
* 根據(jù)是否有設(shè)置頭布局等,判斷返回的個數(shù)
* @return
*/
@Override
public int getItemCount() {
if (news == null)
return 0;
else if (mHeaderView == null && mFooterView == null)
return news.getStories().size();
else if (mHeaderView == null && mFooterView != null)
return news.getStories().size() + 1;
else if (mHeaderView != null && mFooterView == null)
return news.getStories().size() + 1;
else if (mHeaderView != null && mFooterView != null)
return news.getStories().size() + 2;
return 0;
}
//繼承 RecyclerView.ViewHolder,使用 ButterKnife
class ViewHolder extends RecyclerView.ViewHolder{
@BindView(R.id.news_item_title)
TextView newsTitle;
@BindView(R.id.news_item_img)
ImageView newsImg;
@BindView(R.id.news_item_layout)
LinearLayout newsLayout;
public ViewHolder(View itemView) {
super(itemView);
if (itemView == mHeaderView)
return;
if (itemView == mFooterView)
return;
ButterKnife.bind(this,itemView);
}
}
- 在 Activity 中調(diào)用
View banner = ......
mRecyclerViewAdapter.addHeaderView(banner);
關(guān)于滑動
- 簡單的直接定位:
mLayoutManager.scrollToPositionWithOffset(position, 0);
缺點(diǎn)是體驗(yàn)不好堤器,沒有滑動的過程昆庇。
- RecyclerView 的滑動方法
rv.scrollToPosition(index);
rv.scrollBy(int x, int y);
scrollToPosition()
將對應(yīng)的 item 滑動到屏幕內(nèi),當(dāng) item 變?yōu)榭梢姇r則停止滑動闸溃。
所以當(dāng)指定的 item 在當(dāng)前屏幕的下方時整吆,滑動后目標(biāo) item 會出現(xiàn)屏幕的最低下;當(dāng)指定 item 在屏幕可見時辉川,則完全沒有滑動表蝙。
很多時候這個方法是不符合我們預(yù)期的,一般是希望能將指定的 item 滑動到當(dāng)前的屏幕頂端或中間乓旗。
這時候可以配合 scrollBy()
來做一個判斷:
private void setSelectPosition(int index) {
//當(dāng)前可見的第一項(xiàng)和最后一項(xiàng)
int firstItem = linearLayoutManager.findFirstVisibleItemPosition();
int lastItem = linearLayoutManager.findLastVisibleItemPosition();
if (index <= firstItem) {
//當(dāng)要置頂?shù)捻?xiàng)在當(dāng)前顯示的第一個項(xiàng)的前面時府蛇,直接調(diào)用沒有問題
rv.scrollToPosition(index);
} else if (index <= lastItem) {
//當(dāng)要置頂?shù)捻?xiàng)已經(jīng)在屏幕上顯示時,計算需要滑動的距離
int top = rv.getChildAt(index - firstItem).getTop();
rv.scrollBy(0, top);
} else {
//當(dāng)指定的 item 在當(dāng)前顯示的最后一項(xiàng)的后面時
//這時候一次的滑動不足以將指定 item 放到頂端
rv.scrollToPosition(index);
//記錄當(dāng)前需要在RecyclerView滾動監(jiān)聽里面繼續(xù)第二次滾動
move = true;
}
}
class RecyclerViewListener extends RecyclerView.OnScrollListener{
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
//在這里進(jìn)行第二次滾動
if (move ){
move = false;
int n = mIndex - mLinearLayoutManager.findFirstVisibleItemPosition();
if ( 0 <= n && n < mRecyclerView.getChildCount()){
//要移動的距離
int top = mRecyclerView.getChildAt(n).getTop();
mRecyclerView.scrollBy(0, top);
}
}
}
}
上面這個方面也是看到別人的實(shí)現(xiàn)的思路屿愚,雖然沒什么問題不過還是算是取巧的一種方法汇跨,僅記錄。
其實(shí) Stack Overflow 上已經(jīng)有了更好的答案 :RecyclerView - How to smooth scroll to top of item on a certain position?
- 重寫 LinearLayoutManager
public class LinearLayoutManagerWithSmoothScroller extends LinearLayoutManager {
public LinearLayoutManagerWithSmoothScroller(Context context) {
super(context, VERTICAL, false);
}
public LinearLayoutManagerWithSmoothScroller(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}
//重點(diǎn)方法
@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
int position) {
//也就是說重點(diǎn)在于重寫 SmoothScroller,而滑動的調(diào)用為 startSmoothScroll()
RecyclerView.SmoothScroller smoothScroller = new TopSnappedSmoothScroller(recyclerView.getContext());
smoothScroller.setTargetPosition(position);
startSmoothScroll(smoothScroller);
}
private class TopSnappedSmoothScroller extends LinearSmoothScroller {
public TopSnappedSmoothScroller(Context context) {
super(context);
}
@Override
public PointF computeScrollVectorForPosition(int targetPosition) {
return LinearLayoutManagerWithSmoothScroller.this
.computeScrollVectorForPosition(targetPosition);
}
@Override
protected int getVerticalSnapPreference() {
//將指定的 item 滑動至與屏幕的頂端對齊
return SNAP_TO_START;
}
}
}
仔細(xì)看可以發(fā)現(xiàn)我們也可以選擇不重寫整個 LinearLayoutManager妆距,只要將 LinearSmoothScroller 的 getVerticalSnapPreference()
重寫也可以達(dá)到目的穷遂。
關(guān)于 getVerticalSnapPreference ()
的源碼與注釋:
/**
* When scrolling towards a child view, this method defines whether we should align the top
* or the bottom edge of the child with the parent RecyclerView.
*
* @return SNAP_TO_START, SNAP_TO_END or SNAP_TO_ANY; depending on the current target vector
* @see #SNAP_TO_START
* @see #SNAP_TO_END
* @see #SNAP_TO_ANY
*/
protected int getVerticalSnapPreference() {
return mTargetVector == null || mTargetVector.y == 0 ? SNAP_TO_ANY :
mTargetVector.y > 0 ? SNAP_TO_END : SNAP_TO_START;
}
返回的值決定了指定 item 的對齊方式,與頂部對齊 / 底部對齊娱据。
所以最終代碼:
//初始化過程
mLayoutManager= new LinearLayoutManager(getActivity());
mSmoothScroller = new LinearSmoothScroller(getActivity()) {
@Override protected int getVerticalSnapPreference() {
return LinearSmoothScroller.SNAP_TO_START;
}
@Override
public PointF computeScrollVectorForPosition(int targetPosition) {
return mLayoutManager.computeScrollVectorForPosition(targetPosition);
}
};
mRvRetail.setLayoutManager(mLayoutManager);
//移動
mSmoothScroller.setTargetPosition(position);
mLayoutManager.startSmoothScroll(mSmoothScroller);
這樣就可以實(shí)現(xiàn)平滑的滑動了蚪黑。
與 ScrollView 嵌套時慣性滑動失效
猜測是由于滑動的時候,ScrollView 將事件分發(fā)給了 RecyclerView,所以這個時候是 rv 在滑動祠锣;
當(dāng)快速滑動并離開屏幕的時候酷窥,按預(yù)期是應(yīng)該有一段慣性般滑動、緩慢停止的過程伴网,然而這時候應(yīng)該是 ScrollView 攔截了這個 ACTION_UP 的事件蓬推,導(dǎo)致 RecyclerView 無法繼續(xù)滑動。
解決這個問題可以從 RecyclerView 入手澡腾,若 RecyclerView 不再響應(yīng)事件沸伏,將事件交給 ScrollView 即可:
mLayoutManager = new LinearLayoutManager(mContext){
@Override
public boolean canScrollVertically() {
return false;
}
};
mRv.setLayoutManager(mLayoutManager);
刷新數(shù)據(jù)源導(dǎo)致的問題
比較常見是在刪除 item 的時候,可能會出現(xiàn) item 的下標(biāo)沒有刷新动分、位置錯位的問題毅糟。
更新了數(shù)據(jù)源之后最保險的方法是調(diào)用 notifyDataSetChanged()
去刷新所有的數(shù)據(jù),問題是如果只更新了少量的數(shù)據(jù)或者想要保留刪除/添加 item 的動畫澜公,這個方法都不能滿足要求姆另。
所以我們需要調(diào)用 notifyItemRemoved(position)
和 notifyItemInserted(position)
,這時候 item 的內(nèi)容已經(jīng)刷新坟乾、并且?guī)в袆赢嬓Ч75侨绻莿h除 item 的操作,上面說的下標(biāo)沒有刷新甚侣、錯位的問題就出現(xiàn)了明吩,所以接下來要調(diào)用 notifyItemRangeChanged(position, itemSize)
將可能出現(xiàn)問題的 item 都刷一遍,所以 itemSize 常取 list.size()
殷费。
item 的局部刷新
說起 RecyclerView 的局部刷新印荔,一般都是說到 notifyItemChanged(int positon)
即只刷新指定的 item 的布局,但是有時候需要針對 item 的部分控件進(jìn)行刷新详羡。
比如一個 item 里有一張圖片仍律,單調(diào)用 notifyItemChanged 的時候圖片會去重新加載,就導(dǎo)致了重復(fù)加載同樣的圖片而出現(xiàn)圖片閃爍的問題殷绍,所以這時候我們需要針對 item 去刷新改變的部分染苛、保留不變的部分鹊漠。
這里用上了
@Override
public void onBindViewHolder(ViewHolder holder, int position, List<Object> payloads) {
super.onBindViewHolder(holder, position, payloads);
}
和 notifyItemChanged(int position, Object payload)
看到這兩個方法其實(shí)方法已經(jīng)很明顯了主到,調(diào)用 notifyItemChanged()
可以傳遞一個 payload 到 onBindViewHolder
方法里面,這時候就可以通過傳遞過來的數(shù)據(jù)類型去刷新不同的控件躯概,沒有刷新到的控件將會用 ViewHolder 的實(shí)例來展示登钥。
ItemTouchHelper
ItemTouchHelper 是 RecyclerView 中輔助滑動和拖拽的實(shí)用工具類,一般用來做拖拽改變排序娶靡、滑動刪除功能牧牢。
ItemTouchHelper helper = new ItemTouchHelper(callback);
helper.attachToRecyclerView(recyclerView);
ItemTouchHelper 的構(gòu)造方法需要一個傳入 ItemTouchHelper.Callback 的實(shí)例,所以重點(diǎn)就在于這個 Callback 里的幾個方法。
class TestHelperCallback extends ItemTouchHelper.Callback{
private ItemMoveListener mListener;
public TestHelperCallback(){}
public TestHelperCallback(ItemMoveListener listener){
mListener = listener;
}
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
//兩個 flags 為 0 則無法拖動
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;//允許上下滑動
int swipeFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;//允許左右滑動
//生成并返回
int flags = makeMovementFlags(dragFlags, swipeFlags);
return flags;
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
//拖拽的回調(diào)
if (mListener != null){
return mListener.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition());
}
return false;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
//左右滑動的回調(diào)
int position = viewHolder.getAdapterPosition();
if (mListener != null){
mListener.onItemDelete(position);
}
}
@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
//在滑動和拖拽的過程中會被調(diào)用去繪制動畫塔鳍,重寫這里可以實(shí)現(xiàn)自己的動畫
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
if(actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
//左右滑動的同時改變 item 的透明度
final float alpha = 1 - Math.abs(dX) / (float)viewHolder.itemView.getWidth();
viewHolder.itemView.setAlpha(alpha);//透明度
viewHolder.itemView.setTranslationX(dX);//滑動
}
}
@Override
public boolean isLongPressDragEnabled() {
//是否可以長按拖拽
return true;
}
}
public interface ItemMoveListener {
boolean onItemMove(int fromPosition, int toPosition);
boolean onItemDelete(int position);
}
通過接口 ItemMoveListener 把回調(diào)放出去伯铣,接下來用 Adapter 實(shí)現(xiàn)并處理回調(diào):
class ItemMoveRecyclerAdapter extends CommonRecyclerAdapter<String> implements ItemMoveListener{
public ItemMoveRecyclerAdapter(Context context, List dataList, int layoutId) {
super(context, dataList, layoutId);
}
@Override
public void convert(CommonRecyclerViewHolder holder, int position, String o) {
}
@Override
public void onItemClick(CommonRecyclerViewHolder holder, int position, String o) {
}
@Override
public boolean onItemMove(int fromPosition, int toPosition) {
//交換數(shù)據(jù)
Collections.swap(mDataList, fromPosition, toPosition);
//刷新
notifyItemMoved(fromPosition, toPosition);
notifyItemRangeChanged(Math.min(fromPosition,toPosition),getItemCount());
return true;
}
@Override
public boolean onItemDelete(int position) {
mDataList.remove(position);
notifyItemRemoved(position);
notifyItemRangeChanged(position,getItemCount());
return false;
}
}
CommonRecyclerAdapter 是項(xiàng)目封裝的 Adapter,這里不是重點(diǎn)轮纫。
onItemMove()
中依次調(diào)用了 notifyItemMoved()
和 notifyItemRangeChanged()
腔寡,防止在移動過后 item 的下標(biāo)錯亂。
onItemDelete ()
中同理掌唾。
上面代碼基本就是拖拽排序和滑動刪除的代碼放前,因?yàn)楹芎唵尉蜎]有多分析,因?yàn)橐婚_始是想用 ItemTouchHelper 仿照 QQ 的側(cè)滑按鈕糯彬,但是看了一圈發(fā)現(xiàn)好像沒辦法用 ItemTouchHelper 實(shí)現(xiàn)凭语。
clipToPadding 屬性
android:clipToPadding
屬性決定子 View 是否可以在 父布局的 padding 中繪制。
在 RecyclerView 中子 View 即 item撩扒,該屬性為false 時似扔,RecyclerView 的 padding 會被 item 遮住。
比如我們在 RecyclerView 的底部上懸浮一個按鈕搓谆,按鈕會擋住下方的 item虫几,clipToPadding
+ paddingBottom
可以實(shí)現(xiàn)在 RecyclerView 拉倒底部的時候?yàn)閼腋“粹o留出空間。