小序
實際上RecyclerView已不是什麼新穎的話題了,至少對於用過的前輩們而言是這樣的,並且大部分人都會覺得這斯很強大,必須上。而對於剛接觸的小夥伴們,難免會遇到各種問題,或是因爲陌生,又或是因爲項目需求(譬如:側滑出現刪除按鈕,拖動與上下拉刷新等等)。雖有云“前人種樹,後人乘涼”一說,但樹依然是需要後人來灌溉的,至少這麼說是爲了鋪墊。
顯示效果
上面圖示僅是RecyclerView數據顯示的基本使用,包括Adapter、Divider,相信絕大部分人都是在Hongyang前輩的樹下乘過涼,由於其還沒有考慮到關於RecyclerView的擴展問題,所以當我們在使用的時候難免會遇到以下兩個存在的主流問題:
- 列表添加頭部(底部)視圖,也保留相應的分割線;
- 瀑布流列表Item的高度差導致的位置混亂,以及添加分割線出錯;
解決問題與效果實現
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.LayoutManager;
import android.support.v7.widget.StaggeredGridLayoutManager;
import android.view.View;
import android.widget.LinearLayout;
/**
* RecyclerView 分割符
*/
public class RecyclerVDivider extends RecyclerView.ItemDecoration {
private Drawable mDividerDraw;
private int mLeftSpace, mRightSpace, mTopSpace, mBottomSpace;
private int mSpaceColor;
/**
* @param context src/main/res/values/styles <item name="android:listDivider"/>
*/
public RecyclerVDivider(Context context) {
TypedArray a = context.obtainStyledAttributes(new int[]{android.R.attr.listDivider});
mDividerDraw = a.getDrawable(0);
a.recycle();
}
/**
* @param drawable src/main/res/drawable
*/
public RecyclerVDivider(Drawable drawable) {
mDividerDraw = drawable;
}
/**
* @param leftSpace 左側間距
*/
public void setLeftSpace(int leftSpace) {
mLeftSpace = leftSpace;
}
/**
* @param rightSpace 右側間距
*/
public void setRightSpace(int rightSpace) {
mRightSpace = rightSpace;
}
/**
* @param topSpace 頂部間距
*/
public void setTopSpace(int topSpace) {
mTopSpace = topSpace;
}
/**
* @param bottomSpace 底部間距
*/
public void setBottomSpace(int bottomSpace) {
mBottomSpace = bottomSpace;
}
/**
* @param spaceColor 分割線間距顏色,避免浮現父容器顏色
*/
public void setSpaceColor(int spaceColor) {
mSpaceColor = spaceColor;
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
if (0 != mSpaceColor) c.drawColor(mSpaceColor);
drawHorizontal(c, parent);
drawVertical(c, parent);
}
private int getSpanCount(RecyclerView parent) {
int spanCount = -1;
LayoutManager layoutManager = parent.getLayoutManager();
if (layoutManager instanceof GridLayoutManager) {
spanCount = ((GridLayoutManager) layoutManager).getSpanCount();
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
spanCount = ((StaggeredGridLayoutManager) layoutManager).getSpanCount();
}
return spanCount;
}
public void drawHorizontal(Canvas c, RecyclerView parent) {
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
final int left = child.getLeft() - params.leftMargin + mLeftSpace;
final int right = child.getRight() + params.rightMargin
+ mDividerDraw.getIntrinsicWidth() - mRightSpace;
final int top = child.getBottom() + params.bottomMargin;
final int bottom = top + mDividerDraw.getIntrinsicHeight();
mDividerDraw.setBounds(left, top, right, bottom);
mDividerDraw.draw(c);
}
}
public void drawVertical(Canvas c, RecyclerView parent) {
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
final int top = child.getTop() - params.topMargin + mTopSpace;
final int bottom = child.getBottom() + params.bottomMargin - mBottomSpace;
final int left = child.getRight() + params.rightMargin;
final int right = left + mDividerDraw.getIntrinsicWidth();
mDividerDraw.setBounds(left, top, right, bottom);
mDividerDraw.draw(c);
}
}
private boolean isVertical(LayoutManager layoutManager) {
if (layoutManager instanceof GridLayoutManager) {
return ((GridLayoutManager) layoutManager).getOrientation() == LinearLayout.VERTICAL;
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
return ((StaggeredGridLayoutManager) layoutManager).getOrientation() == LinearLayout.VERTICAL;
} else if (layoutManager instanceof LinearLayoutManager) {
return ((LinearLayoutManager) layoutManager).getOrientation() == LinearLayout.VERTICAL;
}
return true;
}
/**
* @param adapter RecyclerView.Adapter
* @return 當前遍歷Item所處列數
*/
private int getSpanIndex(RecyclerView.Adapter adapter) {
if (adapter instanceof BaseRVAdapter) {
return ((BaseRVAdapter) adapter).mSpanIndex + 1;
}
return 0;
}
/**
* @param adapter RecyclerView.Adapter
* @param spanCount 最大可顯示列數
* @return 所需繪製底部分割線的最大行數(除去底部視圖以及最後一排數據)
*/
private int getMaxRaw(RecyclerView.Adapter adapter, int spanCount) {
int childCount = adapter.getItemCount();
int maxRawSize = childCount - childCount % spanCount;
if (adapter instanceof BaseRVAdapter) {
BaseRVAdapter baseRVAdapter = (BaseRVAdapter) adapter;
if (null != baseRVAdapter.mFooterViews && null != baseRVAdapter.mHeaderViews) {
childCount = baseRVAdapter.mList.size();
maxRawSize = childCount - childCount % spanCount;
maxRawSize += baseRVAdapter.mHeaderViews.size();
}
}
return maxRawSize;
}
/**
* @param adapter RecyclerView.Adapter
* @return true_表示當前爲數據Item,false_標識當前爲頭部/底部View
*/
private boolean getItemState(RecyclerView.Adapter adapter) {
if (adapter instanceof BaseRVAdapter) {
return ((BaseRVAdapter) adapter).isMainItem;
}
return true;
}
@Override
public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
LayoutManager layoutManager = parent.getLayoutManager();
int spanCount = getSpanCount(parent);
boolean isLastRaw = isVertical(layoutManager) ?
itemPosition + 1 >= getMaxRaw(parent.getAdapter(), spanCount) :
getSpanIndex(parent.getAdapter()) == spanCount;
boolean isLastColum = isVertical(layoutManager) ?
spanCount == getSpanIndex(parent.getAdapter()) :
itemPosition + 1 >= getMaxRaw(parent.getAdapter(), spanCount);
if (getItemState(parent.getAdapter())) {
outRect.set(0, 0,
isLastColum ? 0 : mDividerDraw.getIntrinsicWidth(),// 如果是最後一列,則不需要繪製右邊
isLastRaw ? 0 : mDividerDraw.getIntrinsicHeight());// 如果是最後一行,則不需要繪製底部
}
}
}
可見分割線的繪製判斷改動比較大,且與自定義RecyclerView.Adapter的實現類BaseRVAdapter.class耦合在一起了。但爲了解決以上存在的兩個問題,這也是沒有辦法中的辦法(表示藍瘦到香菇)。以上簡單的判斷處理相信大家是能看懂,主要還是對這些判斷的數據來源比較感冒些。
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
/**
* RecyclerView 適配器
*/
public abstract class BaseRVAdapter<T> extends RecyclerView.Adapter<BaseViewHolder> {
public static final int TYPE_NORMAL = 0;
public static final int TYPE_HEADER = 1;
public static final int TYPE_FOOTER = -1;
public Context mContext;
public List<T> mList = new ArrayList<>();
public List<View> mHeaderViews = new ArrayList<>();
public List<View> mFooterViews = new ArrayList<>();
public boolean isCloseItemAnim;
public BaseRVAdapter(Context context, @NonNull List<T> list) {
this.mList = list;
this.mContext = context;
}
public T getData(int position) {
return mList.get(position);
}
public void updateAdapter(@NonNull List<T> list) {
mList.clear();
mList.addAll(list);
notifyDataSetChanged();
// notifyItemRangeChanged(startIndex(0), mList.size());
}
public void updateData(int position, T object) {
mList.set(position, object);
if (isCloseItemAnim) {
notifyDataSetChanged();
} else {
int updateIndex = startIndex(position);
notifyItemChanged(updateIndex);
notifyItemRangeChanged(updateIndex, mList.size());
}
}
public void cleanData() {
if (!isCloseItemAnim) {
mList.clear();
notifyDataSetChanged();
} else {
int cleanIndex = mHeaderViews.size();
notifyItemRangeRemoved(cleanIndex, mList.size());
mList.clear();
notifyItemRangeChanged(cleanIndex, mList.size());
}
}
public void removeData(int position) {
if (mList.size() <= position) return;
if (isCloseItemAnim) {
mList.remove(position);
notifyDataSetChanged();
} else {
notifyItemRemoved(startIndex(position));
mList.remove(position);
notifyItemRangeChanged(startIndex(position), mList.size());
}
}
public void addDataLs(final int position, @NonNull List<T> list) {
mList.addAll(position, list);
if (isCloseItemAnim) {
notifyDataSetChanged();
} else {
int addIndex = startIndex(position);
notifyItemRangeInserted(addIndex, list.size());
notifyItemRangeChanged(addIndex, mList.size());
}
}
public void addDataLs(@NonNull List<T> list) {
addDataLs(startIndex(mList.size()), list);
}
public void addData(int position, T object) {
int startIndex = startIndex(position);
mList.add(position, object);
if (isCloseItemAnim) {
notifyDataSetChanged();
} else {
notifyItemInserted(startIndex);
notifyItemRangeChanged(startIndex, mList.size());
}
}
public void addData(T object) {
addData(startIndex(mList.size()), object);
}
public void addHeaderViews(@NonNull List<View> headerViews) {
mHeaderViews.addAll(headerViews);
notifyDataSetChanged();
}
public void addHeaderView(@NonNull View headerView) {
mHeaderViews.add(headerView);
notifyDataSetChanged();
}
public void addFooterViews(@NonNull List<View> footerViews) {
mFooterViews.addAll(footerViews);
notifyDataSetChanged();
}
public void addFooterView(@NonNull View footerView) {
mFooterViews.add(footerView);
notifyDataSetChanged();
}
private int startIndex(int doneIndex) {
return doneIndex + mHeaderViews.size();
}
/**
* {@link #TYPE_HEADER} 頭部列表
* {@link #TYPE_NORMAL} 正常列表
* {@link #TYPE_FOOTER} 底部列表
*/
@Override
public int getItemViewType(int position) {
if (!mHeaderViews.isEmpty() && position < mHeaderViews.size()) {
return TYPE_HEADER + position;
} else if (!mFooterViews.isEmpty() && position >= mHeaderViews.size() + mList.size()) {
int beforeSize = mHeaderViews.size() + mList.size();
return TYPE_FOOTER - (position - beforeSize);
} else {
return TYPE_NORMAL;
}
}
@Override
public int getItemCount() {
if ((null == mList || mList.isEmpty())) {
return null == mHeaderViews ? 0 : mHeaderViews.size()
+ (null == mFooterViews ? 0 : mFooterViews.size());
} else {
return mHeaderViews.size() + mList.size() + mFooterViews.size();
}
}
@Override
public BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (!mHeaderViews.isEmpty() && viewType > TYPE_NORMAL) {
return new BaseViewHolder(mHeaderViews.get(viewType - 1));
} else if (!mFooterViews.isEmpty() && viewType <= TYPE_FOOTER) {
return new BaseViewHolder(mFooterViews.get(-viewType - 1));
} else {
View view = LayoutInflater.from(mContext).inflate(getLayoutId(viewType),
parent, false);
return new BaseViewHolder(view);
}
}
public abstract int getLayoutId(int viewType);
@Override
public void onBindViewHolder(final BaseViewHolder holder, int position) {
if (getItemViewType(position) != TYPE_NORMAL) return;
onBind(holder, position - mHeaderViews.size());
if (null != mOnItemClickListener) {
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mOnItemClickListener.itemSelect(
holder.getAdapterPosition() - mHeaderViews.size());
}
});
}
}
public abstract void onBind(BaseViewHolder holder, int position);
@Override
public void onViewRecycled(final BaseViewHolder holder) {
super.onViewRecycled(holder);
}
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
if (manager instanceof GridLayoutManager) {
final GridLayoutManager gridManager = ((GridLayoutManager) manager);
gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
return getItemViewType(position) != TYPE_NORMAL ? gridManager.getSpanCount() : 1;
}
});
}
}
/**
* @see RecyclerVDivider
* <ul>判斷是否添加分割符的標識
* <li>true_添加,false_不添加</li></ul>
* Title:另外使用分割符的情況下可刪除該變量
*/
public boolean isMainItem;
/**
* @see RecyclerVDivider
* 獲取GridLayoutManager/StaggeredGridLayoutManager當前Item項所處的列數(行數)
* 提示:另外使用分割符的情況下可刪除該變量
*/
public int mSpanIndex;
@Override
public void onViewAttachedToWindow(BaseViewHolder holder) {
super.onViewAttachedToWindow(holder);
ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
if (null == lp) return;
isMainItem = getItemViewType(holder.getLayoutPosition()) == TYPE_NORMAL;
if (lp instanceof GridLayoutManager.LayoutParams) {
GridLayoutManager.LayoutParams p = (GridLayoutManager.LayoutParams) lp;
mSpanIndex = p.getSpanIndex();
} else if (lp instanceof StaggeredGridLayoutManager.LayoutParams) {
StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp;
mSpanIndex = p.getSpanIndex();
p.setFullSpan(!isMainItem);
}
}
private OnItemClickListener mOnItemClickListener;
public void addItemClickListener(OnItemClickListener listener) {
mOnItemClickListener = listener;
}
public interface OnItemClickListener {
void itemSelect(int position);
}
}
本來還想着直接貼重點相關的代碼塊就行了,畢竟這些實現都大同小異,再說就這些也上不了排場。但是我左思右想覺得不成,這個都得貼上,這樣若是出現其他問題“怪我咯~”。大夥重點看onViewAttachedToWindow()就可以了,因爲關於分割線繪製與否的數據判定基本上就是根據這裏劫取的。
自定義RecyclerView
除此之外因RecyclerView在使用前操作過多嫌麻煩,於是自定義了一個RecyclerView的實現類,並且已在RecyclerVDivider.class當中做了相應的關聯和處理,若不嫌棄可一併拿去。
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.support.annotation.Nullable;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;
import android.util.AttributeSet;
import com.mrv.R;
import com.mrv.utils.UnitConverter;
/**
* 重定義RecyclerView使用
*/
public class MyRecyclerView extends RecyclerView {
public static final int TYPE_LIST = 0;
public static final int TYPE_GRID = 1;
public static final int TYPE_STAGGERED_GRID = 2;
/**
* <ul>列表類型
* <li>同 ListView :{@link #TYPE_LIST}(默認)</li>
* <li>同 GridView :{@link #TYPE_GRID}</li>
* <li>同 StaggeredGridView :{@link #TYPE_STAGGERED_GRID}</li></ul>
*/
private int mType;
public static final int VERTICAL = 1;
public static final int HORIZONTAL = 0;
/**
* <ul>列表類型
* <li>垂直方向 :{@link #VERTICAL}(默認)</li>
* <li>水平方向 :{@link #HORIZONTAL}</li></ul>
*/
private int mOrientation;
/**
* ?固定大小,默認固定
*/
private boolean isFixSize = true;
private static final int DEFAULT_ROW_NUM = 2;
/**
* GRID與STAGGERED_GRID顯示列數,默認{@link #DEFAULT_ROW_NUM}列
*/
private int mSpanCount;
/**
* @see RecyclerVDivider
* 分割線,默認從styles中獲取
*/
private Drawable mDividerDraw;
/**
* 分割線左側邊距,默認無邊距
*/
private int mDividerLeftSpace;
/**
* 分割線右側邊距,默認無邊距
*/
private int mDividerRightSpace;
/**
* 分割線頂部邊距,默認無邊距
*/
private int mDividerTopSpace;
/**
* 分割線底部邊距,默認無邊距
*/
private int mDividerBottomSpace;
/**
* 分割線間距顏色(即背景色),默認無
*/
private int mDividerSpaceColor;
public MyRecyclerView(Context context) {
super(context);
initView();
}
public MyRecyclerView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.recycle_view);
isFixSize = typedArray.getBoolean(R.styleable.recycle_view_fixSize, true);
mSpanCount = typedArray.getInteger(R.styleable.recycle_view_spanCount, DEFAULT_ROW_NUM);
mType = typedArray.getInt(R.styleable.recycle_view_type, TYPE_LIST);
mOrientation = typedArray.getInt(R.styleable.recycle_view_orientation, VERTICAL);
mDividerDraw = typedArray.getDrawable(R.styleable.recycle_view_divider);
mDividerLeftSpace = typedArray.getInt(R.styleable.recycle_view_dividerLeftSpace, 0);
mDividerRightSpace = typedArray.getInt(R.styleable.recycle_view_dividerRightSpace, 0);
mDividerTopSpace = typedArray.getInt(R.styleable.recycle_view_dividerTopSpace, 0);
mDividerBottomSpace = typedArray.getInt(R.styleable.recycle_view_dividerBottomSpace, 0);
mDividerSpaceColor = typedArray.getColor(R.styleable.recycle_view_dividerSpaceColor, 0);
typedArray.recycle();
initView();
}
public MyRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
private void initView() {
// 佈局樣式
switch (mType) {
case TYPE_LIST:
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(
getContext(), mOrientation, false);
this.setLayoutManager(linearLayoutManager);
break;
case TYPE_GRID:
GridLayoutManager gridLayoutManager = new GridLayoutManager(
getContext(), mSpanCount, mOrientation, false);
this.setLayoutManager(gridLayoutManager);
break;
case TYPE_STAGGERED_GRID:
StaggeredGridLayoutManager staggeredGridLayoutManager =
new StaggeredGridLayoutManager(mSpanCount, mOrientation);
this.setLayoutManager(staggeredGridLayoutManager);
break;
}
// 分割線
if (null != mDividerDraw) {
RecyclerVDivider divider = new RecyclerVDivider(mDividerDraw);
divider.setLeftSpace(UnitConverter.dip2px(getContext(), mDividerLeftSpace));
divider.setRightSpace(UnitConverter.dip2px(getContext(), mDividerRightSpace));
divider.setTopSpace(UnitConverter.dip2px(getContext(), mDividerTopSpace));
divider.setBottomSpace(UnitConverter.dip2px(getContext(), mDividerBottomSpace));
if (0 != mDividerSpaceColor) divider.setSpaceColor(mDividerSpaceColor);
this.addItemDecoration(divider);
}
// 大小固定
this.setHasFixedSize(isFixSize);
// Item出入動畫
this.setItemAnimator(new DefaultItemAnimator());
}
public int getSpanCount() {
return mSpanCount;
}
public int getOrientation() {
return mOrientation;
}
public int getType() {
return mType;
}
}
下面是關於自定義屬性的內容與對應實現功能,其中需要注意的是如果啓動了分割符的邊距設置,這需要相應的設置一下分割符的顏色,從而避免產生的邊距露出Item的父容器背景顏色。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="recycle_view">
<attr name="fixSize" format="boolean" />
<attr name="spanCount" format="integer" />
<attr name="divider" format="reference"/>
<attr name="dividerLeftSpace" format="integer" />
<attr name="dividerRightSpace" format="integer" />
<attr name="dividerTopSpace" format="integer" />
<attr name="dividerBottomSpace" format="integer" />
<attr name="dividerSpaceColor" format="color" />
<attr name="type">
<flag name="list" value="0" />
<flag name="grid" value="1" />
<flag name="staggeredGrid" value="2" />
</attr>
<attr name="orientation">
<flag name="vertical" value="1" />
<flag name="horizontal" value="0" />
</attr>
</declare-styleable>
</resources>
Name | Format | Function |
---|---|---|
fixSize | boolean | 設置true提升性能,默認true |
spanCount | integer | 網格以及流佈局顯示最大列數,默認2 |
divider | reference | 分割符資源Id,默認沒有分割符 |
dividerLeftSpace | integer | 分隔符左邊距,默認0,單位dp |
dividerRightSpace | integer | 分隔符左邊距,默認0,單位dp |
dividerTopSpace | integer | 分隔符頂部邊距,默認0,單位dp |
dividerBottomSpace | integer | 分隔符底部邊距,默認0,單位dp |
dividerSpaceColor | color | 分隔符底部顏色,默認無 |
dividerSpaceColor | color | 分隔符底部顏色,默認無 |
type | flag | list:列表;grid:網格;staggeredGrid:瀑布流 |
orientation | flag | vertical:垂直方向延伸; horizontal:水平方向延伸 |
結尾助力
以上爲本篇重點簡述內容,剩下的講講何爲”助力篇“的實際意義。一方面,個人覺得爲RecyclerView添加分割符的形式有些毛躁,因爲判斷太多直接影響性能。另一方面是自己能力尚淺,沒有辦法將其優化好,包括邏輯處理以及動畫效果的延伸。
如果你覺得上面的gif演示已經比較nice了,那麼再請你看看下面的另一個gif演示:
看完上面兩者的比較以後不難發現存在的問題了,當然如果用回notifyDataSetChanged();是可以儘量避免這些問題的。而這裏不僅是爲了將存在的問題表露出來,更多的是爲了能讓有實力的小夥伴們幫忙解決問題,或者提供一些已有的解決方案。以下爲浮現的問題:
- Item進出動畫卡頓
- 分割線配合動畫使用繪製無刷新(可能頁面顯示無更新,又或者只是繪製邏輯上出現的錯誤)
- 有些情況下使用動畫會有閃屏的現象,體驗較差。如上面gif從底部scrollToPosition()到頂部時候出現閃屏
- …
Fork助力項目地址:https://github.com/gzejia/URecyclerView
分隔符使用參考:Android RecyclerView 使用完全解析 體驗藝術般的控件
添加頭部/底部視圖參考:RecyclerView添加Header的正確方式