最近项目中需要一个类似微博个人信息页的布局,网上找了一些资料但都不符合自己的需求,故自己实现一个。
一.模仿布局样式原型:
上图是新浪微博个人信息页的整体布局,可以看到整个布局是一个可以滚动列表结构,上边是一个个人信息布局和一个tab选择栏布局,看到这个布局之后我们可以想到用scrollview+listview来完成,但由于使用这种嵌套方式之后需重写listview高度而导致item无法复用,故我们使用另一种方式来完成,也就是RecyclerView充当整个布局的方式,个人信息布局和tab选择栏作为header插入到列表中。
下边来让我们思考一下即将实现布局将会遇到的问题:
1.RecyclerView如何加header和footer进去?
2.怎样做到悬浮tab栏效果?
3.怎么在同一个列表中加载不同的样式?
4.如何在点tab栏切换数据的时候保持列表处于上一次的位置?
问题一:RecyclerView如何加header和footer进去?
我们都知道RecylerView不像Listview一样有addheaderview和addfooterview两个方法,既然没有这两个方法我们就需要自己去写这两个方法并且去实现了,我们知道RecyclerView的adapter中有一个叫getItemViewType(int position)的方法,我们可以通过重写这个方法来让RecyclerView的item根据条件不同加载不同种类的布局,那我们便可以通过重写这个方法将header和footer作为item项加入到RecyclerView中从而达到加header和footer的效果。适配器的代码如下:
package com.ifeng.art.common.widget.recyclerview;
import android.content.Context;
import android.support.v4.util.SparseArrayCompat;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
/**
* Created by lvzishen on 16/10/8.
*/
public abstract class WrapperRecyclerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
protected List list = new ArrayList<>();
protected Context context;
// /**
// * 头布局
// */
// private View headView;
// /**
// * 头布局Tab
// */
// private View headViewTab;
/**
* 头布局
* key: Integer; value: object
*/
private SparseArrayCompat<View> mHeaderViews = new SparseArrayCompat<>();
/**
* 头布局Tab
*/
private View headViewTab;
/**
* 脚布局
*/
private FooterView footView;
// /**
// * 头布局数量
// */
// public int headViewCount = 0;
/**
* 脚布局数量
*/
public int footViewCount = 0;
public int getFootViewCount() {
return footViewCount;
}
private static final int TYPE_HEADER = 1;
private static final int TYPE_FOOTER = 3;
private FootViewHolder footViewHolder;
public void setList(List listData) {
this.list.clear();
this.list.addAll(listData);
this.notifyDataSetChanged();
}
public WrapperRecyclerAdapter(Context context) {
this.context = context;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (mHeaderViews.get(viewType) != null) {
return new HeadViewHolder(mHeaderViews.get(viewType));
}
else if (viewType == TYPE_FOOTER) {
return footViewHolder;
} else {
return onCreateViewHolderNormal(parent, viewType);
}
}
public abstract RecyclerView.ViewHolder onCreateViewHolderNormal(ViewGroup parent, int viewType);
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
position = getRealPosition(position);
onBindViewHolderNormal(holder, position);
}
public abstract void onBindViewHolderNormal(RecyclerView.ViewHolder holder, int position);
@Override
public int getItemCount() {
return list.size() + mHeaderViews.size() + footViewCount;
}
public void addHeaderView(View head) {
this.mHeaderViews.put(mHeaderViews.size() + TYPE_HEADER, head);
notifyItemInserted(mHeaderViews.size() - 1);
}
public void addFooterView(FooterView footView) {
this.footView = footView;
footViewCount++;
footViewHolder = new FootViewHolder(footView);
notifyItemInserted(getItemCount() - 1);
}
public void removeFooterView() {
if (footView != null) {
footViewCount = 0;
footViewHolder = null;
footView = null;
notifyItemRemoved(getItemCount() - 1);
}
}
public void removeHeaderView() {
if (mHeaderViews.size() > 0) {
// headViewCount = 0;
mHeaderViews.clear();
for (int i = 0; i < mHeaderViews.size(); i++) {
notifyItemRemoved(i);
}
}
}
@Override
public int getItemViewType(int position) {
if (position < mHeaderViews.size()) {
return mHeaderViews.keyAt(position);
}
if (position == getItemCount() - 1 && footView != null) {
//最后一个,应该加载Footer
return TYPE_FOOTER;
}
return getItemViewTypeNormal(getRealPosition(position));
}
public abstract int getItemViewTypeNormal(int position);
/**
* 得到真实的position
*
* @return
*/
private int getRealPosition(int position) {
return position = position - mHeaderViews.size();
}
public void setState(int status) {
if (footViewHolder != null) {
footView.setState(status);
}
}
/**
* @param errorListener 脚布局加载失败
*/
public void setErrorListener(FooterView.ErrorListener errorListener) {
footView.setOnErrorListener(errorListener);
}
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if (layoutManager instanceof GridLayoutManager) {
final GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
int spanSize = 1; //每个griditem 占1份 一行有3个网格item 每个spansize为1 一个item占满则需要返回3
if (position == 0 && mHeaderViews != null && mHeaderViews.size() > 0) {
return gridLayoutManager.getSpanCount();
}
if (position == getItemCount() - 1 && footView != null && footViewCount > 0) {
return gridLayoutManager.getSpanCount();
}
return spanSize;
}
});
}
}
public int getState() {
if (footView != null) {
return footView.getStatus();
}
return 0;
}
class HeadViewHolder extends RecyclerView.ViewHolder {
public HeadViewHolder(View itemView) {
super(itemView);
}
}
class FootViewHolder extends RecyclerView.ViewHolder {
public FootViewHolder(View itemView) {
super(itemView);
}
}
}
这里讲的不是很详细,只是给了大家思路并且简单的实现了一下效果,因为网上已经有很多关于这方面的资料了,如果有不明白的地方可以评论留言给我或者自行查阅资料。这个adapter中我还加入了一些关于footer状态的地方,是因为我们要控制footer去根据不同的状态加载不同的布局类型,具体可以看最后的demo代码。
问题二:怎样做到悬浮tab栏效果?
可以看到当我们向上拖动列表的时候会出现一个跟头布局相同的tab悬浮栏悬浮在最上方。这里我们采用一个简单的方法,RecyclerView中可以通过设置 RecyclerView.OnScrollListener这个接口去监听到RecyclerView的滚动状态,第一个显示的item,总共显示的item个数等等,我们可以利用这个抽象类去达到效果。
具体的思路为在RecyclerView的根布局中加一个和headview tab栏中布局相同的tab,并且隐藏这个tab。当我们滚动列表时,第一个可见的item为headview
tab的位置的时候(此布局position为1)我们就将隐藏的tab显示出来从而给用户造成一个tab悬浮在最上层的假象。因为我们这个隐藏显示的tab和headview中的tab并不是同一个tab,所以当我们改变其中一个tab的状态的时候要对应的改变另一个tab,这样才能让数据同步。下边给出OnScrollListener的代码:
package com.ifeng.art.common.widget.recyclerview;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
/**
* Created by lvzishen on 16/10/9.
*/
public abstract class RecyclerViewLoadData extends RecyclerView.OnScrollListener {
private RecyclerView.LayoutManager layoutManager;
private int lastVisibleItem;
private int firstVisbleItem;
private int totalItemCount;
private int visibleItemCount;
public RecyclerViewLoadData(RecyclerView.LayoutManager layoutManager) {
this.layoutManager = layoutManager;
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
// 获取到最后一个可见的item
if (layoutManager instanceof LinearLayoutManager) {
lastVisibleItem = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
//得到当前显示的第一个item的位置
firstVisbleItem = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
//如果第一个可见的item为大于等于1则显示悬浮tab
if (firstVisbleItem >= 1) {
showTab();
} else {
hideTab();
}
getFirstVisiblePosition(firstVisbleItem);
}
if (layoutManager instanceof GridLayoutManager) {
lastVisibleItem = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition();
}
}
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
// 获取item的总数
totalItemCount = layoutManager.getItemCount();
//可见的item数
visibleItemCount = layoutManager.getChildCount();
// Log.e("totalItemCount", totalItemCount + "");
// Log.e("visibleItemCount", visibleItemCount + "");
if (newState == RecyclerView.SCROLL_STATE_IDLE && lastVisibleItem >= totalItemCount - 1 && visibleItemCount > 0) {
// 此处调用加载更多回调接口的回调方法
int state = RecyclerViewStateUtils.getFooterViewState(recyclerView);
if (state == FooterView.Loading || state == FooterView.TheEnd) {
Log.d("state not allow", "the state is Loading/The End, just wait..");
return;
}
loadMoreData(recyclerView);
}
}
public abstract void loadMoreData(RecyclerView recyclerView);
public abstract void getFirstVisiblePosition(int firstVisiblePosition);
public abstract void showTab();
public abstract void hideTab();
}
问题三:怎么在同一个列表中加载不同的样式?
这个问题的答案和我们第一个加headview和footview的思路相同,再次不再多说,要注意adapter中并不是只能加载一种类型的数据,我们可以根据加载的数据类型不同加载不同类型的布局从而达到像微博列表中那样的一个列表是正常的列表布局(多个list集合,根据tab的切换让adapter加载不同的list集合),一个列表是网格形布局的情况,其实只不过是加载了不同的布局(第一种是加载正常布局item,第二种是加载三个imageview并排的item),下边给出适配器的代码:
package com.ifeng.art.adapter;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.ifeng.art.R;
import com.ifeng.art.common.widget.recyclerview.WrapperRecyclerAdapter;
import com.ifeng.art.entity.UserNickname;
import com.ifeng.art.entity.UserNumber;
/**
* Created by lvzishen on 16/11/2.
*/
public class DemoWrapperRecyclerAdapter extends WrapperRecyclerAdapter {
private final int TYPE_ONE = 10;
private final int TYPE_TWO = 11;
public DemoWrapperRecyclerAdapter(Context context) {
super(context);
}
@Override
public RecyclerView.ViewHolder onCreateViewHolderNormal(ViewGroup parent, int viewType) {
if (viewType == TYPE_ONE) {
View view = LayoutInflater.from(context).inflate(R.layout.item_normal, parent, false);
return new NormalHolder(view);
}
if (viewType == TYPE_TWO) {
View view2 = LayoutInflater.from(context).inflate(R.layout.item_normal_two, parent, false);
return new NormalHolder2(view2);
}
return null;
}
@Override
public void onBindViewHolderNormal(RecyclerView.ViewHolder holder, final int position) {
if (holder instanceof NormalHolder) {
final UserNumber art = (UserNumber) list.get(position);
((NormalHolder) holder).textView.setText(art.follows);
((NormalHolder) holder).textView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e("position", position + "");
}
});
}
if (holder instanceof NormalHolder2) {
final UserNickname huodong = (UserNickname) list.get(position);
((NormalHolder2) holder).textView.setText(huodong.uname);
// Glide.with(context)
// .load(huodong.image)
// .placeholder(R.color.default_image)
// .into(((NormalHolder2) holder).imageView);
((NormalHolder2) holder).textView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e("position", position + "");
}
});
}
}
@Override
public int getItemViewTypeNormal(int position) {
if (list.get(position) instanceof UserNickname) {
return TYPE_TWO;
} else {
return TYPE_ONE;
}
}
class NormalHolder extends RecyclerView.ViewHolder {
TextView textView;
public NormalHolder(View itemView) {
super(itemView);
textView = (TextView) itemView.findViewById(R.id.item_textview);
}
}
class NormalHolder2 extends RecyclerView.ViewHolder {
TextView textView;
ImageView imageView;
public NormalHolder2(View itemView) {
super(itemView);
textView = (TextView) itemView.findViewById(R.id.item_textview);
imageView = (ImageView) itemView.findViewById(R.id.image);
}
}
}
问题四:如何在点tab栏切换数据的时候保持列表处于上一次的位置?
当我们点击tab栏切换不同的集合时需要保持列表处于上一次显示的位置,我们可以获取到第一个可见的item位置并且记录下来,然后使用scrollToPositionWithOffset(int position, int offset)这个方法来让RecyclerView去切换到指定的位置上,其中的逻辑判断比较繁琐,具体的逻辑可以看最后demo中的代码。
最后让我们看一下实现出来的效果
最后给出代码地址
github.com/lvzishen/We…