摘要:有时候在做开发的时候,遇到需要自定义View的时候,相信很多人的第一反应就是到github上去搜一下有没有相关的已经做好的View,如果搜到了就暗暗窃喜,搜不到就悲剧了,我曾经也是其中一员,但是残酷的现实就是就算搜到了用的飞快的时候,bug来了,然后又是陷入看源码,改源码,不行再改的恶性循环中,好一个累的成狗的程序猿啊!!!
于是决定痛下决心,一改从前的恶习,自己一步步踩坑,不断调试打造一个属于自己的RecyclerView,这样不仅把ViewGroup的事件分发搞清楚了,而且还增强了一个开发者傲视一切的自信心!只要跨过这一层,就好像打通了任督二脉一样,尽情的把自定义View踩在脚下吧。
本篇博客分享的是一个菜鸟程序员如何练级的,碰到的坑也分享给大家。
一个RecyclerView,再次封装,需要哪些功能。
1.下拉刷新
2.加载更多
3.跟listview一样利用addHeaderView(),addFooterView()来任意添加头部和底部View。
4.添加蒙层。
先把不那么复杂的先给做了。先做第3个吧。headerView和footerView都是RecyclerView的一个item,RecyclerView通过Adapter的viewType来创建不同的ViewHolder,因此这个ViewType很重要。
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
public int getItemCount()
public int getItemViewType(int position)
通过观察,很容易得出结论,通过position返回一个ViewType,可以通过viewType来创建一个ViewHolder。position->viewType->View->ViewHolder。那么很容易联想到用HashMap来存储view,key是viewType,value是View。这里采用的是SparseArrayCompat来代替HashMap,既然android推荐用这个东西,自然有用它的道理。其内部实现了压缩算法,可以进行矩阵压缩,大大减少了存储空间,节约内存。此外它的查找算法是二分法,提高了查找的效率。
public void addHeaderView(@NonNull View view) {
mHeaderViews.put(mHeaderViews.size() + VIEW_TYPE_NORMAL_HEADER, view);
}
public void addFooterView(@NonNull View view) {
mFooterViews.put(mFooterViews.size() + VIEW_TYPE_NORMAL_FOOTER, view);
}
自然的写出了这样一串代码。
然后来看看getItemViewType是如何做的
@Override
public int getItemViewType(int position) {
if (position == 0) {
return VIEW_TYPE_REFRESH_HEADER;
} else if (isFooter(position)) {
return VIEW_TYPE_LOAD_MORE_FOOTER;
} else if (isHeaders(position)) {
return mHeaderViews.keyAt(position - 1);
} else if (isContents(position)) {
return VIEW_TYPE_CONTENT;
} else if (isFooters(position)) {
if (adapter != null) {
return mFooterViews.keyAt(position - mHeaderViews.size() - adapter.getItemCount() - 1);
} else {
return mFooterViews.keyAt(position - mHeaderViews.size() - 1);
}
} else {
throw new RuntimeException("invalid Item type");
}
}
public boolean isFooter(int position) {
return position == getItemCount() - 1;
}
public boolean isHeaders(int position) {
return mHeaderViews != null && mHeaderViews.size() > 0
&& position <= mHeaderViews.size();
}
public boolean isContents(int position) {
return adapter != null && adapter.getItemCount() > 0
&& position <= mHeaderViews.size() + adapter.getItemCount();
}
public boolean isFooters(int position) {
if (adapter == null) {
return mFooterViews.size() > 0 &&
position <= mHeaderViews.size() + mFooterViews.size();
} else {
return mFooterViews.size() > 0 && position <= mHeaderViews.size()
+ adapter.getItemCount() + mFooterViews.size();
}
}
把key值当成viewType传过去,通过viewType拿到View。一切万事大吉了。
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
RecyclerView.ViewHolder viewHolder = null;
if (viewType == VIEW_TYPE_REFRESH_HEADER) {
viewHolder = createHeaderViewHolder(parent);
} else if (viewType == VIEW_TYPE_LOAD_MORE_FOOTER) {
viewHolder = createFooterViewHolder(parent);
} else if (viewType == VIEW_TYPE_CONTENT) {
return adapter.onCreateViewHolder(parent, viewType);
} else {
if (mHeaderViews.get(viewType) != null) {
viewHolder = new ViewHolder(mHeaderViews.get(viewType)) {
@Override
public String toString() {
return super.toString();
}
};
}
if (mFooterViews.get(viewType) != null) {
viewHolder = new ViewHolder(mFooterViews.get(viewType)) {
@Override
public String toString() {
return super.toString();
}
};
}
}
return viewHolder;
}
值得注意的是我们把自己要定义的子Adapter传进来。item的个数就很好算了。
@Override
public int getItemCount() {
int itemCount = 0;
if (adapter != null) {
itemCount = adapter.getItemCount();
}
return mHeaderViews.size() + mFooterViews.size() + 2 + itemCount;
}
adapter写好了,我们需要重写RecyclerView的setAdapter这个方法。
@Override
public void setAdapter(Adapter adapter) {
mWrapperAdapter = new WrapperAdapter(adapter);
super.setAdapter(mWrapperAdapter);
adapter.registerAdapterDataObserver(dataObserver);
dataObserver.onChanged();
}
贴上整个Adapter的代码。
private class WrapperAdapter extends RecyclerView.Adapter {
private Adapter adapter;
public WrapperAdapter(Adapter adapter) {
this.adapter = adapter;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
RecyclerView.ViewHolder viewHolder = null;
if (viewType == VIEW_TYPE_REFRESH_HEADER) {
viewHolder = createHeaderViewHolder(parent);
} else if (viewType == VIEW_TYPE_LOAD_MORE_FOOTER) {
viewHolder = createFooterViewHolder(parent);
} else if (viewType == VIEW_TYPE_CONTENT) {
return adapter.onCreateViewHolder(parent, viewType);
} else {
if (mHeaderViews.get(viewType) != null) {
viewHolder = new ViewHolder(mHeaderViews.get(viewType)) {
@Override
public String toString() {
return super.toString();
}
};
}
if (mFooterViews.get(viewType) != null) {
viewHolder = new ViewHolder(mFooterViews.get(viewType)) {
@Override
public String toString() {
return super.toString();
}
};
}
}
return viewHolder;
}
private ViewHolder createFooterViewHolder(ViewGroup parent) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_productlist_footer, parent, false);
LoadingFooterHolder footerHolder = new LoadingFooterHolder(view);
loadingFooterHolder = footerHolder;
return footerHolder;
}
private ViewHolder createHeaderViewHolder(ViewGroup parent) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.view_loading_header, parent, false);
LoadingHeaderHolder headerHolder = new LoadingHeaderHolder(view);
loadingHeaderHolder = headerHolder;
return headerHolder;
}
@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) {
if (position == 0){
return gridManager.getSpanCount();
}else if (isHeaders(position)){
return gridManager.getSpanCount();
}else if (isContents(position)){
return 1;
}else{
return gridManager.getSpanCount();
}
}
});
}
}
@Override
public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
super.onViewAttachedToWindow(holder);
ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
if (lp != null
&& lp instanceof StaggeredGridLayoutManager.LayoutParams
&& (isHeaders(holder.getLayoutPosition()) ||
isFooter(holder.getLayoutPosition()) ||
holder.getLayoutPosition() == 0 ||
isFooters(holder.getLayoutPosition()))) {
StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp;
p.setFullSpan(true);
}
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
}
@Override
public int getItemCount() {
int itemCount = 0;
if (adapter != null) {
itemCount = adapter.getItemCount();
}
return mHeaderViews.size() + mFooterViews.size() + 2 + itemCount;
}
@Override
public int getItemViewType(int position) {
if (position == 0) {
return VIEW_TYPE_REFRESH_HEADER;
} else if (isFooter(position)) {
return VIEW_TYPE_LOAD_MORE_FOOTER;
} else if (isHeaders(position)) {
return mHeaderViews.keyAt(position - 1);
} else if (isContents(position)) {
return VIEW_TYPE_CONTENT;
} else if (isFooters(position)) {
if (adapter != null) {
return mFooterViews.keyAt(position - mHeaderViews.size() - adapter.getItemCount() - 1);
} else {
return mFooterViews.keyAt(position - mHeaderViews.size() - 1);
}
} else {
throw new RuntimeException("invalid Item type");
}
}
public boolean isFooter(int position) {
return position == getItemCount() - 1;
}
public boolean isHeaders(int position) {
return mHeaderViews != null && mHeaderViews.size() > 0
&& position <= mHeaderViews.size();
}
public boolean isContents(int position) {
return adapter != null && adapter.getItemCount() > 0
&& position <= mHeaderViews.size() + adapter.getItemCount();
}
public boolean isFooters(int position) {
if (adapter == null) {
return mFooterViews.size() > 0 &&
position <= mHeaderViews.size() + mFooterViews.size();
} else {
return mFooterViews.size() > 0 && position <= mHeaderViews.size()
+ adapter.getItemCount() + mFooterViews.size();
}
}
}
接下来就要来实现下拉刷新了,思路是这样的,我们先把刷新布局的高度设为1px,然后重写onTouchEvent,拿到dy,通过dy来改变刷新布局的高度,当UP的时候,通过动画平移到原来的样子。可能这样实现不好,但是我当时就是这样写的。如果有什么更好的办法可以@我。
来看看onTouchEvent的代码
@Override
public boolean onTouchEvent(MotionEvent e) {
if (shouldTouch()) {
switch (e.getActionMasked()) {
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (status == PULLING || status == LOADING) {
stopRefresh();
} else if (status == PENDINGLOADING) {
if (onRefreshListener != null) {
onRefreshListener.onPullRefresh();
}
animateToStatus(LOADING);
}
initialX = INVALID;
initialY = INVALID;
lastY = INVALID;
dragging = false;
return super.onTouchEvent(e);
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_POINTER_DOWN:
lastY = INVALID;
break;
case MotionEvent.ACTION_MOVE:
int index = e.getActionIndex();
if (lastY == INVALID) {
lastY = e.getY(index);
}
if (initialX == INVALID) {
initialX = e.getX(index);
initialY = e.getY(index);
}
float x = e.getX(index);
float y = e.getY(index);
if (!dragging && status == IDLE && y - initialY > touchSlop && y - initialY > x - initialX) {
dragging = true;
setStatus(PULLING);
}
boolean consumed = false;
if (dragging && (status == PULLING || status == PENDINGLOADING)) {
float dy = y - lastY;
refreshLoadingViewHeight(dy);
consumed = true;
}
lastY = e.getY(index);
if (consumed) {
return true;
}
break;
}
return super.onTouchEvent(e);
}
return super.onTouchEvent(e);
}
如果需要详细看的话,详见github,地址会在最后贴出来。
我们还可以添加蒙层。
@Override
protected void dispatchDraw(Canvas canvas) {
Paint paint = new Paint();
paint.setColor(Color.parseColor("#FF4081"));
canvas.drawRect(0, getRootView().getTranslationY(), getWidth(), getHeight(), paint);
super.dispatchDraw(canvas);
}
贴上地址:https://github.com/Eddieyuan123/XRecyclerView-chelizi
欢迎测试。