Android StickHeaderRecyclerView - 让recyclerview头部固定
介绍
在项目中有时会需要recyclerview滑动式时某个view滑出后会固定在头部显示,比较常用的比如手机联系人界面、地区选择界面等。 StickHeaderRecyclerView就是实现这个功能的。效果图:
这样的控件网上一抓一大把了,本控件的优点就是使用简单- lib简单 - 使用的语法也简单(之前下了2个类似开源项目,都是上万行代码。读起来麻烦、改起来麻烦就自己写了这个控件)
使用
只需要让你的adapter实现StickHeaderDecoration.StickHeaderInterface接口,方法boolean isStick(int position)中返回的值就标识当前位置的view是否需要固定。
同时需要让Adapter中的item不复用(如果怕影响性能也可以单独让需要固定的view不复用) 在adapter构造方法中setHasStableIds(true); 同时复写adapter的public long getItemId(int position) {return position;}
上代码
public class NormalAdapter extends RecyclerView.Adapter<NormalAdapter.InnerHolder> implements StickHeaderDecoration.StickHeaderInterface{ NormalAdapter(Activity activity, List<String> dates){ this.activity = activity; this.dates = dates; } @Override public boolean isStick(int position) { return position % 6 == 0; } Activity activity; private List<String> dates; @Override public InnerHolder onCreateViewHolder(ViewGroup parent, int viewType) { View inflate = LayoutInflater.from(activity).inflate(R.layout.item, parent, false); return new InnerHolder(inflate); } @Override public void onBindViewHolder(InnerHolder holder, int position) { if(isStick(position)){ holder.itemView.setBackgroundResource(R.color.colorAccent); holder.tvText.setText(position / 6 +""); }else{ holder.itemView.setBackgroundResource(R.color.white); holder.tvText.setText(dates.get(position)); } } @Override public int getItemCount() { return dates.size(); } class InnerHolder extends RecyclerView.ViewHolder{ TextView tvText; public InnerHolder(View itemView) { super(itemView); tvText = (TextView) itemView.findViewById(R.id.tvText); } } }
activity代码
public class MainActivity extends Activity { private RecyclerView recycle; private List<String> dates = new ArrayList<>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); recycle = (RecyclerView) findViewById(R.id.recycle); for(int i=0;i<66;i++){ dates.add("date : "+i); } recycle.setLayoutManager(new LinearLayoutManager(this)); recycle.setAdapter(new NormalAdapter(this, dates)); recycle.addItemDecoration(new StickHeaderDecoration(recycle)); } }
完成了
原理
先上核心类代码
public class StickHeaderDecoration extends RecyclerView.ItemDecoration { public interface StickHeaderInterface { /** * is this item need stick * @param position now item position in the recyclerView * @return true : need stick else not */ boolean isStick(int position); } private RecyclerView recyclerView; private RecyclerView.LayoutManager manager; private RecyclerView.Adapter adapter; private StickHeaderInterface stickHeaderInterface; /** * 进行一些容错检查 */ public StickHeaderDecoration(RecyclerView recyclerView) { this.recyclerView = recyclerView; this.manager = recyclerView.getLayoutManager(); this.adapter = recyclerView.getAdapter(); if (adapter == null) { throw new RuntimeException("please set Decoration after set adapter"); } if (adapter instanceof StickHeaderInterface) { stickHeaderInterface = (StickHeaderInterface) adapter; return; } throw new RuntimeException("please make your adapter implements StickHeaderInterface"); } /** * 绘制头部的stick view */ @Override public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { super.onDrawOver(c, parent, state); View childAt = parent.getChildAt(0); if (childAt == null) return; RecyclerView.ViewHolder childViewHolder = parent.getChildViewHolder(childAt); int position = childViewHolder.getPosition(); for (int i = position; i >= 0; i--) { if (stickHeaderInterface.isStick(i)) { int top = 0; if (position + 1 < adapter.getItemCount()) { if (stickHeaderInterface.isStick(position + 1)) { View childNext = parent.getChildAt(1); top = manager.getDecoratedTop(childNext) < 0 ? 0 : manager .getDecoratedTop(childNext); } } RecyclerView.ViewHolder inflate = recyclerView.getAdapter().createViewHolder(parent, recyclerView.getAdapter().getItemViewType(i)); recyclerView.getAdapter().bindViewHolder(inflate, i); int measureHeight = getMeasureHeight(inflate.itemView); c.save(); if (top < inflate.itemView.getMeasuredHeight() && top > 0) { c.translate(0, top - measureHeight); } inflate.itemView.draw(c); c.restore(); return; } } } /** * 测量控件的高度 * * @param header */ private int getMeasureHeight(View header) { int widthSpec = View.MeasureSpec.makeMeasureSpec(recyclerView.getWidth(), View .MeasureSpec.EXACTLY); int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); header.measure(widthSpec, heightSpec); header.layout(0, 0, header.getMeasuredWidth(), header.getMeasuredHeight()); return header.getMeasuredHeight(); } }
首先ItemDecoration是一个接口,通过RecyclerView的 recycle.addItemDecoration方法设置进去
其中只有6个方法其中3个过时了。我们这儿只需要对onDrawOver进行操作。
onDrawOver是当前RecyclerView绘制完毕后调用,可以其中进行绘制。我们的头部固定其实就是在这个方法中绘制进去的。
算法
1.这儿我们需要判断当前显示item的前面是否有需要固定的item(这儿取名为beforitem)如果有则绘制在顶部
2.我们还需要当第二个固定的item把前面的item慢慢顶上去的效果,这儿通过判断当前显示的第一个item的下一个item是否需要固定,如果需要则通过manager.getDecoratedTop(childNext)获取这个item距离顶部的距离然后通过计算把beforitem先上移动一定的距离。
基本原理就这样,相信代码更加有说服力,github 地址
https://github.com/LiuLinXin/StickHeaderRecyclerView-philer
待优化
头部view现在是通过ondraw绘制进去的,不能相应点击事件等。暂时没相处好的解决办法,希望有想法的朋友提示下。