介绍RecyclerView中使用StaggeredGridLayoutManager布局时粘性头部的实现
我们经常在使用App的过程中有城市的列表选择,我们会发现App在滑动的过程中,会有粘性头部一直悬浮在最上端,其中实现的方法有很多种,当然,这里我们不讨论使用ListView或者GridView控件,也不讨论RecyclerView使用LinearLayoutManager或者GridLayoutManager布局时的实现方式。本文讨论的重点是使用StaggeredGridLayoutManager布局时的粘性头部的实现方式。本文已展示本地图片库的图片作为例子来展开。
先上一下效果图
扫描本地图片库
public LocalPictureDateResult loadAllLocalPictures() {
LocalPictureDateResult result = new LocalPictureDateResult(mContext);
Uri mImageUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
ContentResolver mContentResolver = mContext.getContentResolver();
Log.i(TAG, mImageUri.getPath());
// query only jpeg and png image type files
Cursor mCursor = mContentResolver.query(mImageUri, null,
MediaStore.Images.Media.MIME_TYPE + "=? or "
+ MediaStore.Images.Media.MIME_TYPE + "=?",
new String[]{"image/jpeg", "image/png"}, MediaStore.Images.Media.DATE_MODIFIED + " DESC");
if (mCursor != null) {
while (mCursor.moveToNext()) {
//retrive image path
String path = mCursor.getString(mCursor
.getColumnIndex(MediaStore.Images.Media.DATA));
int width = mCursor.getInt(mCursor
.getColumnIndex(MediaStore.Images.Media.WIDTH));
int height = mCursor.getInt(mCursor
.getColumnIndex(MediaStore.Images.Media.HEIGHT));
long modifiedData = mCursor.getLong(mCursor.
getColumnIndex(MediaStore.Images.Media.DATE_MODIFIED));
long addedData = mCursor.getLong(mCursor.
getColumnIndex(MediaStore.Images.Media.DATE_ADDED));
if (path==null || "".equals(path) || width == 0 || height == 0 || modifiedData == 0) {
continue;
}
LocalPictureDetailInfo lpi = new LocalPictureDetailInfo(path, width, height, addedData * 1000, modifiedData * 1000);
result.add(lpi);
}
mCursor.close();
}
return result;
}
这里返回一个LocalPictureDateResult对象,里面存放了扫描出来的本地图片文件信息集合
这里我们看下上面代码中的result.add(lpi)
方法
**LocalPictureDateResult.java**
/**
* save the positions of title in the data collections
*/
ArrayList mArrayTitlePos;
/**
* title collection
*/
private HashSet<String> titleSet ;
/**
* pictures collection,include title and picture details
*/
ArrayList<WrapLocalPictureDetailInfo> mLocalPictureInfos;
/**
* add local picture details
*
* @param detailInfo
*/
public void add(LocalPictureDetailInfo detailInfo) {
String dateTimeStr = mDateParseFilter.parse(detailInfo.getModifiedDate());
if (!titleSet.contains(dateTimeStr)) {
titleSet.add(dateTimeStr);
// save title position
mArrayTitlePos.add(mLocalPictureInfos.size());
// add title object
WrapLocalPictureDetailInfo titleInfo = new WrapLocalPictureDetailInfo(dateTimeStr, DATA_TYPE_TITLE);
mLocalPictureInfos.add(titleInfo);
// add content object
WrapLocalPictureDetailInfo contentInfo = new WrapLocalPictureDetailInfo(detailInfo);
mLocalPictureInfos.add(contentInfo);
} else {
mLocalPictureInfos.add(new WrapLocalPictureDetailInfo(detailInfo));
}
}
上面这段代码,把扫描出来的本地图片集,通过图片的最后修改时间,分离出时间段名称,并且把时间段名称作为数据的一部分加入到最后的图片集合中,这样图片集合中就包含了图片数据和标题数据,并且我们也有了每个标题在图片集合中的位置,这样有利于我们后面计算和寻找标题做准备。
主页面布局
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/srl_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.RecyclerView
android:id="@+id/rv_content"
android:layout_width="match_parent"
android:layout_height="match_parent">
</android.support.v7.widget.RecyclerView>
</android.support.v4.widget.SwipeRefreshLayout>
<LinearLayout
android:id="@+id/header_one"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="invisible">
<include layout="@layout/item_title_local_picture" />
</LinearLayout>
</FrameLayout>
上面我们看到最外层用了个FrameLayout,用了个LinearLayout作为粘性头部,引入的头部布局和RecyclerView中的头部布局一样
RecyclerView的普通头部制作
头部布局文件
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/tv_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/holo_red_light"
android:gravity="center_vertical"
android:textColor="@android:color/black"
android:textSize="20sp">
</TextView>
设置RecylerView的adapter和layoutmanager
int mColumns = 3;
//
mLayoutManager = new StaggeredGridLayoutManager(mColumns, StaggeredGridLayoutManager.VERTICAL);
mAdapter = new LocalPicturesAdapter(this, mMainPresenter.getLocalPictureDatas(), mColumns);
// set RecylerView's adapter and layoutmanager
mContentView.setLayoutManager(mLayoutManager);
mContentView.setAdapter(mAdapter);
mContentView.setHasFixedSize(true);
LocalPicturesAdapter类,主要根据数据中的type类型来显示不同的
public class LocalPicturesAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements View.OnClickListener {
Activity mContext;
List<WrapLocalPictureDetailInfo> mImagePaths;
int columnNums = 1;
private OnRecyclerViewItemClickListener mOnItemClickListener = null;
public LocalPicturesAdapter(Activity context, List<WrapLocalPictureDetailInfo> imagePaths, int columnNums) {
this.mContext = context;
this.mImagePaths = imagePaths;
this.columnNums = columnNums;
}
public void setImagePaths(List<WrapLocalPictureDetailInfo> imagePaths){
this.mImagePaths = imagePaths;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
RecyclerView.ViewHolder viewHolder;
View view;
if (viewType == DATA_TYPE_TITLE) {
view = mContext.getLayoutInflater().inflate(R.layout.item_title_local_picture,null);
viewHolder = new LocalPicturesTitleHolder(view);
} else if (viewType == DATA_TYPE_CONTENT) {
view = mContext.getLayoutInflater().inflate(R.layout.item_content_local_picture,null);
viewHolder = new LocalPicturesContentHolder(view);
} else {
view = mContext.getLayoutInflater().inflate(R.layout.item_content_local_picture,null);
viewHolder = new LocalPicturesContentHolder(view);
}
if (view != null) {
// add listener
view.setOnClickListener(this);
}
return viewHolder;
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
if (viewHolder.getItemViewType() == DATA_TYPE_TITLE) {
LocalPicturesTitleHolder holder = (LocalPicturesTitleHolder) viewHolder;
holder.itemView.setTag(position);
if (holder.mTvTitle != null) {
WrapLocalPictureDetailInfo pictureDetailInfo = mImagePaths.get(position);
// set title layoutparams
StaggeredGridLayoutManager.LayoutParams layoutParams = new StaggeredGridLayoutManager.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
// set title full span
layoutParams.setFullSpan(true);
holder.itemView.setLayoutParams(layoutParams);
holder.mTvTitle.setText(pictureDetailInfo.getDataTitle());
}
} else if (viewHolder.getItemViewType() == DATA_TYPE_CONTENT) {
LocalPicturesContentHolder holder = (LocalPicturesContentHolder) viewHolder;
holder.itemView.setTag(position);
if (holder.mImageView != null) {
WrapLocalPictureDetailInfo pictureDetailInfo = mImagePaths.get(position);
........
........
........
// use glide load the image into ImageView
Glide.with(mContext).load(new File(pictureDetailInfo.getPath())).into(holder.mImageView);
}
}
}
......
......
......
}
我们通过显示普通title的方法是使用设置itemview的StaggeredGridLayoutManager.LayoutParams来实现的,并且setFUllSpan(true)用来让title显示一整行
RecylerView滚动监听
public RecyclerView.OnScrollListener onScrollListener = new RecyclerView.OnScrollListener() {
// get the recyclerview first visable item's position,return the size same as span count
int mFirstVisiblePosition[] = new int[mColumns];
// get the recyclerview last visable item's position,return the size same as span count
int mLastVisiblePosition[] = new int[mColumns];
// sticky head view height
private int mStickyHeadHeight = 0;
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (dy == 0) {
// the first time show the recyclerview items
mLayoutManager.findFirstVisibleItemPositions(mFirstVisiblePosition);
int titlePosision = getMinVisablePosition(mFirstVisiblePosition);
if (titlePosision >= 0) {
// get the item object
WrapLocalPictureDetailInfo wrapLocalPictureDetailInfo = mAdapter.getItemObject(titlePosision);
if (wrapLocalPictureDetailInfo != null) {
// set the first title view text content
((TextView) mHeaderOneView.getChildAt(0)).setText(wrapLocalPictureDetailInfo.getDataTitle());
mHeaderOneView.setVisibility(View.VISIBLE);
// set the current title position into view tag object
mHeaderOneView.setTag(titlePosision);
// retrive the title view height,and set into variable
mStickyHeadHeight = mHeaderOneView.getMeasuredHeight();
}
}
} else if (dy != 0) { // pull down the recyclerview then dy<0 , pull up the recyclerview then dy>0
mLayoutManager.findFirstVisibleItemPositions(mFirstVisiblePosition);
int minVisablePosition = getMinVisablePosition(mFirstVisiblePosition);
mLayoutManager.findLastVisibleItemPositions(mLastVisiblePosition);
int maxVisablePosition = getMaxVisablePosition(mLastVisiblePosition);
if (minVisablePosition < 0) {
return;
}
/**
* get one title position before current minimum visable item position
*/
int beforeFirstItemTitlePosition = mMainPresenter.findBeforeTitlePosition(minVisablePosition);
/**
* get the title position after current minimum visable item position
*/
int afterFirstItemTitlePosition = mMainPresenter.findAfterTitlePosition(minVisablePosition);
// when next title item position after the current minimum visable exist,and not equals current item position
if (afterFirstItemTitlePosition != Integer.MIN_VALUE && afterFirstItemTitlePosition != minVisablePosition) {
// determine whether next title item after the current minimum visable position is visable in the recyclerview
if (afterFirstItemTitlePosition <= maxVisablePosition) {
// it means that next title item is visable in the recycleview now
// find that suitable next title view
View nextTitleView = findView(afterFirstItemTitlePosition);
if (nextTitleView != null) {
float yxis = nextTitleView.getY();
// if next title view is scroll into first title view's area
if (yxis <= mStickyHeadHeight) {
// then fix the first title scroll y
mHeaderOneView.scrollTo(0, (int) (mStickyHeadHeight - yxis));
} else {
// others,next title away from the first title view
// then fix first title view scroll y
mHeaderOneView.scrollTo(0, 0);
}
// set visable to the header view always
mHeaderOneView.setVisibility(View.VISIBLE);
}
}
}
/**
* Determine whether need to change the title
*/
// when title item before the current maxinum visable position exist,and position not equals the current header view's tag
if (beforeFirstItemTitlePosition != Integer.MIN_VALUE && mHeaderOneView.getTag() != null && (int) mHeaderOneView.getTag() != beforeFirstItemTitlePosition) {
/**
* it means that should change the title content
*/
((TextView) mHeaderOneView.getChildAt(0)).setText("");
// always show the title item that before the current maxinum visable position
WrapLocalPictureDetailInfo wpdi = mMainPresenter.getLocalPictureDatas().get(beforeFirstItemTitlePosition);
// set new title content
((TextView) mHeaderOneView.getChildAt(0)).setText(wpdi.getDataTitle());
if (dy > 0) {
// if user pull up the recyclerview,fix the scroll y.Sometimes,because of the next title item may be not visable in current recyclerview,
// so,we should force set header view visable once again
mHeaderOneView.setVisibility(View.VISIBLE);
mHeaderOneView.scrollTo(0, 0);
}
// set title position in the tag
mHeaderOneView.setTag(beforeFirstItemTitlePosition);
// retrive the sticky head height
mStickyHeadHeight = mHeaderOneView.getMeasuredHeight();
}
}
}
......
......
......
};
其中最主要的原理是在RecylerView滚动的时候,判断dy的大小,查找当前RecylerView中用户可视的最小的item位置,通过之前我们初始化数据的时候,分离出的每个标题在数据集中的位置,底层使用二分查找方法,找到当前可视的最小item位置之前(下文称item之前)和之后(item之后)的标题item的位置信息。如果存在item之前的标题信息,判断和当前已经显示的粘性头部tag中存储的位置不一样,那么用新的头部信息替换当前粘性头部;如果在当前页面搜索到item之后的标题视图,获取该视图对象,判断与已经显示的粘性头部底部位置判断,如果超过了粘性头部,则把粘性头部用scrollTo方法进行向上滑动,就会出现粘性头部滑动的效果,如果没有超过,那么什么都不做
总结
可能看代码还是有点抽象,总结起来就是在初始化数据时,将标题也作为数据项加入到数据集合中,然后在Adapter中动态的判断是标题还是普通内容,采用不同的布局页面和布局,结合滚动时,通过当前页面最小位置的判断之前和之后标题item的位置,来控制滑动或者切换粘性头部,我把示例工程放在了github上RecyclerStaggeredStickyHeaderView,欢迎fork或者start。最后如果发现文章有什么问题或者写的不好的,欢迎留言交流,当然,也欢迎点赞~~~