RecyclerView使用StaggeredGridLayoutManager布局的粘性头部实现

介绍RecyclerView中使用StaggeredGridLayoutManager布局时粘性头部的实现

我们经常在使用App的过程中有城市的列表选择,我们会发现App在滑动的过程中,会有粘性头部一直悬浮在最上端,其中实现的方法有很多种,当然,这里我们不讨论使用ListView或者GridView控件,也不讨论RecyclerView使用LinearLayoutManager或者GridLayoutManager布局时的实现方式。本文讨论的重点是使用StaggeredGridLayoutManager布局时的粘性头部的实现方式。本文已展示本地图片库的图片作为例子来展开。

先上一下效果图

粘性头部

扫描本地图片库

  1. public LocalPictureDateResult loadAllLocalPictures() {
  2. LocalPictureDateResult result = new LocalPictureDateResult(mContext);
  3. Uri mImageUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
  4. ContentResolver mContentResolver = mContext.getContentResolver();
  5. Log.i(TAG, mImageUri.getPath());
  6. // query only jpeg and png image type files
  7. Cursor mCursor = mContentResolver.query(mImageUri, null,
  8. MediaStore.Images.Media.MIME_TYPE + "=? or "
  9. + MediaStore.Images.Media.MIME_TYPE + "=?",
  10. new String[]{"image/jpeg", "image/png"}, MediaStore.Images.Media.DATE_MODIFIED + " DESC");
  11. if (mCursor != null) {
  12. while (mCursor.moveToNext()) {
  13. //retrive image path
  14. String path = mCursor.getString(mCursor
  15. .getColumnIndex(MediaStore.Images.Media.DATA));
  16. int width = mCursor.getInt(mCursor
  17. .getColumnIndex(MediaStore.Images.Media.WIDTH));
  18. int height = mCursor.getInt(mCursor
  19. .getColumnIndex(MediaStore.Images.Media.HEIGHT));
  20. long modifiedData = mCursor.getLong(mCursor.
  21. getColumnIndex(MediaStore.Images.Media.DATE_MODIFIED));
  22. long addedData = mCursor.getLong(mCursor.
  23. getColumnIndex(MediaStore.Images.Media.DATE_ADDED));
  24. if (path==null || "".equals(path) || width == 0 || height == 0 || modifiedData == 0) {
  25. continue;
  26. }
  27. LocalPictureDetailInfo lpi = new LocalPictureDetailInfo(path, width, height, addedData * 1000, modifiedData * 1000);
  28. result.add(lpi);
  29. }
  30. mCursor.close();
  31. }
  32. return result;
  33. }

这里返回一个LocalPictureDateResult对象,里面存放了扫描出来的本地图片文件信息集合

这里我们看下上面代码中的result.add(lpi)方法

  1. **LocalPictureDateResult.java**
  2. /**
  3. * save the positions of title in the data collections
  4. */
  5. ArrayList mArrayTitlePos;
  6. /**
  7. * title collection
  8. */
  9. private HashSet<String> titleSet ;
  10. /**
  11. * pictures collection,include title and picture details
  12. */
  13. ArrayList<WrapLocalPictureDetailInfo> mLocalPictureInfos;
  14. /**
  15. * add local picture details
  16. *
  17. * @param detailInfo
  18. */
  19. public void add(LocalPictureDetailInfo detailInfo) {
  20. String dateTimeStr = mDateParseFilter.parse(detailInfo.getModifiedDate());
  21. if (!titleSet.contains(dateTimeStr)) {
  22. titleSet.add(dateTimeStr);
  23. // save title position
  24. mArrayTitlePos.add(mLocalPictureInfos.size());
  25. // add title object
  26. WrapLocalPictureDetailInfo titleInfo = new WrapLocalPictureDetailInfo(dateTimeStr, DATA_TYPE_TITLE);
  27. mLocalPictureInfos.add(titleInfo);
  28. // add content object
  29. WrapLocalPictureDetailInfo contentInfo = new WrapLocalPictureDetailInfo(detailInfo);
  30. mLocalPictureInfos.add(contentInfo);
  31. } else {
  32. mLocalPictureInfos.add(new WrapLocalPictureDetailInfo(detailInfo));
  33. }
  34. }

上面这段代码,把扫描出来的本地图片集,通过图片的最后修改时间,分离出时间段名称,并且把时间段名称作为数据的一部分加入到最后的图片集合中,这样图片集合中就包含了图片数据和标题数据,并且我们也有了每个标题在图片集合中的位置,这样有利于我们后面计算和寻找标题做准备。

主页面布局

  1. <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:layout_width="match_parent"
  3. android:layout_height="match_parent">
  4. <android.support.v4.widget.SwipeRefreshLayout
  5. android:id="@+id/srl_root"
  6. android:layout_width="match_parent"
  7. android:layout_height="match_parent"
  8. android:orientation="vertical">
  9. <android.support.v7.widget.RecyclerView
  10. android:id="@+id/rv_content"
  11. android:layout_width="match_parent"
  12. android:layout_height="match_parent">
  13. </android.support.v7.widget.RecyclerView>
  14. </android.support.v4.widget.SwipeRefreshLayout>
  15. <LinearLayout
  16. android:id="@+id/header_one"
  17. android:layout_width="match_parent"
  18. android:layout_height="wrap_content"
  19. android:visibility="invisible">
  20. <include layout="@layout/item_title_local_picture" />
  21. </LinearLayout>
  22. </FrameLayout>

上面我们看到最外层用了个FrameLayout,用了个LinearLayout作为粘性头部,引入的头部布局和RecyclerView中的头部布局一样

RecyclerView的普通头部制作

头部布局文件

  1. <TextView xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:id="@+id/tv_title"
  3. android:layout_width="match_parent"
  4. android:layout_height="wrap_content"
  5. android:background="@android:color/holo_red_light"
  6. android:gravity="center_vertical"
  7. android:textColor="@android:color/black"
  8. android:textSize="20sp">
  9. </TextView>

设置RecylerView的adapter和layoutmanager

  1. int mColumns = 3;
  2. //
  3. mLayoutManager = new StaggeredGridLayoutManager(mColumns, StaggeredGridLayoutManager.VERTICAL);
  4. mAdapter = new LocalPicturesAdapter(this, mMainPresenter.getLocalPictureDatas(), mColumns);
  5. // set RecylerView's adapter and layoutmanager
  6. mContentView.setLayoutManager(mLayoutManager);
  7. mContentView.setAdapter(mAdapter);
  8. mContentView.setHasFixedSize(true);

LocalPicturesAdapter类,主要根据数据中的type类型来显示不同的

  1. public class LocalPicturesAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements View.OnClickListener {
  2. Activity mContext;
  3. List<WrapLocalPictureDetailInfo> mImagePaths;
  4. int columnNums = 1;
  5. private OnRecyclerViewItemClickListener mOnItemClickListener = null;
  6. public LocalPicturesAdapter(Activity context, List<WrapLocalPictureDetailInfo> imagePaths, int columnNums) {
  7. this.mContext = context;
  8. this.mImagePaths = imagePaths;
  9. this.columnNums = columnNums;
  10. }
  11. public void setImagePaths(List<WrapLocalPictureDetailInfo> imagePaths){
  12. this.mImagePaths = imagePaths;
  13. }
  14. @Override
  15. public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
  16. RecyclerView.ViewHolder viewHolder;
  17. View view;
  18. if (viewType == DATA_TYPE_TITLE) {
  19. view = mContext.getLayoutInflater().inflate(R.layout.item_title_local_picture,null);
  20. viewHolder = new LocalPicturesTitleHolder(view);
  21. } else if (viewType == DATA_TYPE_CONTENT) {
  22. view = mContext.getLayoutInflater().inflate(R.layout.item_content_local_picture,null);
  23. viewHolder = new LocalPicturesContentHolder(view);
  24. } else {
  25. view = mContext.getLayoutInflater().inflate(R.layout.item_content_local_picture,null);
  26. viewHolder = new LocalPicturesContentHolder(view);
  27. }
  28. if (view != null) {
  29. // add listener
  30. view.setOnClickListener(this);
  31. }
  32. return viewHolder;
  33. }
  34. @Override
  35. public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
  36. if (viewHolder.getItemViewType() == DATA_TYPE_TITLE) {
  37. LocalPicturesTitleHolder holder = (LocalPicturesTitleHolder) viewHolder;
  38. holder.itemView.setTag(position);
  39. if (holder.mTvTitle != null) {
  40. WrapLocalPictureDetailInfo pictureDetailInfo = mImagePaths.get(position);
  41. // set title layoutparams
  42. StaggeredGridLayoutManager.LayoutParams layoutParams = new StaggeredGridLayoutManager.LayoutParams(
  43. ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
  44. // set title full span
  45. layoutParams.setFullSpan(true);
  46. holder.itemView.setLayoutParams(layoutParams);
  47. holder.mTvTitle.setText(pictureDetailInfo.getDataTitle());
  48. }
  49. } else if (viewHolder.getItemViewType() == DATA_TYPE_CONTENT) {
  50. LocalPicturesContentHolder holder = (LocalPicturesContentHolder) viewHolder;
  51. holder.itemView.setTag(position);
  52. if (holder.mImageView != null) {
  53. WrapLocalPictureDetailInfo pictureDetailInfo = mImagePaths.get(position);
  54. ........
  55. ........
  56. ........
  57. // use glide load the image into ImageView
  58. Glide.with(mContext).load(new File(pictureDetailInfo.getPath())).into(holder.mImageView);
  59. }
  60. }
  61. }
  62. ......
  63. ......
  64. ......
  65. }

我们通过显示普通title的方法是使用设置itemview的StaggeredGridLayoutManager.LayoutParams来实现的,并且setFUllSpan(true)用来让title显示一整行

RecylerView滚动监听

  1. public RecyclerView.OnScrollListener onScrollListener = new RecyclerView.OnScrollListener() {
  2. // get the recyclerview first visable item's position,return the size same as span count
  3. int mFirstVisiblePosition[] = new int[mColumns];
  4. // get the recyclerview last visable item's position,return the size same as span count
  5. int mLastVisiblePosition[] = new int[mColumns];
  6. // sticky head view height
  7. private int mStickyHeadHeight = 0;
  8. @Override
  9. public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
  10. super.onScrollStateChanged(recyclerView, newState);
  11. }
  12. @Override
  13. public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
  14. super.onScrolled(recyclerView, dx, dy);
  15. if (dy == 0) {
  16. // the first time show the recyclerview items
  17. mLayoutManager.findFirstVisibleItemPositions(mFirstVisiblePosition);
  18. int titlePosision = getMinVisablePosition(mFirstVisiblePosition);
  19. if (titlePosision >= 0) {
  20. // get the item object
  21. WrapLocalPictureDetailInfo wrapLocalPictureDetailInfo = mAdapter.getItemObject(titlePosision);
  22. if (wrapLocalPictureDetailInfo != null) {
  23. // set the first title view text content
  24. ((TextView) mHeaderOneView.getChildAt(0)).setText(wrapLocalPictureDetailInfo.getDataTitle());
  25. mHeaderOneView.setVisibility(View.VISIBLE);
  26. // set the current title position into view tag object
  27. mHeaderOneView.setTag(titlePosision);
  28. // retrive the title view height,and set into variable
  29. mStickyHeadHeight = mHeaderOneView.getMeasuredHeight();
  30. }
  31. }
  32. } else if (dy != 0) { // pull down the recyclerview then dy<0 , pull up the recyclerview then dy>0
  33. mLayoutManager.findFirstVisibleItemPositions(mFirstVisiblePosition);
  34. int minVisablePosition = getMinVisablePosition(mFirstVisiblePosition);
  35. mLayoutManager.findLastVisibleItemPositions(mLastVisiblePosition);
  36. int maxVisablePosition = getMaxVisablePosition(mLastVisiblePosition);
  37. if (minVisablePosition < 0) {
  38. return;
  39. }
  40. /**
  41. * get one title position before current minimum visable item position
  42. */
  43. int beforeFirstItemTitlePosition = mMainPresenter.findBeforeTitlePosition(minVisablePosition);
  44. /**
  45. * get the title position after current minimum visable item position
  46. */
  47. int afterFirstItemTitlePosition = mMainPresenter.findAfterTitlePosition(minVisablePosition);
  48. // when next title item position after the current minimum visable exist,and not equals current item position
  49. if (afterFirstItemTitlePosition != Integer.MIN_VALUE && afterFirstItemTitlePosition != minVisablePosition) {
  50. // determine whether next title item after the current minimum visable position is visable in the recyclerview
  51. if (afterFirstItemTitlePosition <= maxVisablePosition) {
  52. // it means that next title item is visable in the recycleview now
  53. // find that suitable next title view
  54. View nextTitleView = findView(afterFirstItemTitlePosition);
  55. if (nextTitleView != null) {
  56. float yxis = nextTitleView.getY();
  57. // if next title view is scroll into first title view's area
  58. if (yxis <= mStickyHeadHeight) {
  59. // then fix the first title scroll y
  60. mHeaderOneView.scrollTo(0, (int) (mStickyHeadHeight - yxis));
  61. } else {
  62. // others,next title away from the first title view
  63. // then fix first title view scroll y
  64. mHeaderOneView.scrollTo(0, 0);
  65. }
  66. // set visable to the header view always
  67. mHeaderOneView.setVisibility(View.VISIBLE);
  68. }
  69. }
  70. }
  71. /**
  72. * Determine whether need to change the title
  73. */
  74. // when title item before the current maxinum visable position exist,and position not equals the current header view's tag
  75. if (beforeFirstItemTitlePosition != Integer.MIN_VALUE && mHeaderOneView.getTag() != null && (int) mHeaderOneView.getTag() != beforeFirstItemTitlePosition) {
  76. /**
  77. * it means that should change the title content
  78. */
  79. ((TextView) mHeaderOneView.getChildAt(0)).setText("");
  80. // always show the title item that before the current maxinum visable position
  81. WrapLocalPictureDetailInfo wpdi = mMainPresenter.getLocalPictureDatas().get(beforeFirstItemTitlePosition);
  82. // set new title content
  83. ((TextView) mHeaderOneView.getChildAt(0)).setText(wpdi.getDataTitle());
  84. if (dy > 0) {
  85. // if user pull up the recyclerview,fix the scroll y.Sometimes,because of the next title item may be not visable in current recyclerview,
  86. // so,we should force set header view visable once again
  87. mHeaderOneView.setVisibility(View.VISIBLE);
  88. mHeaderOneView.scrollTo(0, 0);
  89. }
  90. // set title position in the tag
  91. mHeaderOneView.setTag(beforeFirstItemTitlePosition);
  92. // retrive the sticky head height
  93. mStickyHeadHeight = mHeaderOneView.getMeasuredHeight();
  94. }
  95. }
  96. }
  97. ......
  98. ......
  99. ......
  100. };

其中最主要的原理是在RecylerView滚动的时候,判断dy的大小,查找当前RecylerView中用户可视的最小的item位置,通过之前我们初始化数据的时候,分离出的每个标题在数据集中的位置,底层使用二分查找方法,找到当前可视的最小item位置之前(下文称item之前)和之后(item之后)的标题item的位置信息。如果存在item之前的标题信息,判断和当前已经显示的粘性头部tag中存储的位置不一样,那么用新的头部信息替换当前粘性头部;如果在当前页面搜索到item之后的标题视图,获取该视图对象,判断与已经显示的粘性头部底部位置判断,如果超过了粘性头部,则把粘性头部用scrollTo方法进行向上滑动,就会出现粘性头部滑动的效果,如果没有超过,那么什么都不做

总结

可能看代码还是有点抽象,总结起来就是在初始化数据时,将标题也作为数据项加入到数据集合中,然后在Adapter中动态的判断是标题还是普通内容,采用不同的布局页面和布局,结合滚动时,通过当前页面最小位置的判断之前和之后标题item的位置,来控制滑动或者切换粘性头部,我把示例工程放在了github上RecyclerStaggeredStickyHeaderView,欢迎fork或者start。最后如果发现文章有什么问题或者写的不好的,欢迎留言交流,当然,也欢迎点赞~~~