效果图
分析
先来分析一下,可以看到这是一个按月份分组的2行图片列表,列表顶部一个悬浮栏,会随着列表滑动而刷新,点击顶部栏,弹出了一个筛选框。
思路
1.列表部分
可以用RecyclerView
+GridLayoutManager
,月份的标题栏可以使用多布局
首先是主体item的布局
复制代码
然后是月份标题的布局
复制代码
PictureAdapter
这里使用了 ,需要继承BaseMultiItemQuickAdapter
,关于adapter多布局的使用,篇幅所限,这里不再细述。
public class PictureAdapter extends BaseMultiItemQuickAdapter{ public PictureAdapter(@Nullable List data) { super(data); addItemType(PictureModel.PICTURE_CONTENT, R.layout.item_pictures); addItemType(PictureModel.PICTURE_TITLE, R.layout.item_picture_month); } @Override protected void convert(BaseViewHolder helper, PictureModel item) { if (helper.getItemViewType() == PictureModel.PICTURE_CONTENT) { //标题/数量 helper.setText(R.id.tv_pictrue_title, item.getTitle() + "(" + item.getPicture_count() + ")"); //时间 helper.setText(R.id.tv_pictrue_time, item.getDate()); //封面图 GlideUtils.loadImg(mContext, item.getCover_image(), (ImageView) helper.getView(R.id.iv_pictrue)); } else if (helper.getItemViewType() == PictureModel.PICTURE_TITLE) { helper.setText(R.id.tv_picture_month, item.getDate()); } } @Override public void onAttachedToRecyclerView(RecyclerView recyclerView) { super.onAttachedToRecyclerView(recyclerView); FullSpanUtil.onAttachedToRecyclerView(recyclerView, this, PictureModel.PICTURE_TITLE); } @Override public void onViewDetachedFromWindow(@NonNull BaseViewHolder holder) { super.onViewDetachedFromWindow(holder); FullSpanUtil.onViewAttachedToWindow(holder, this, PictureModel.PICTURE_TITLE); }}复制代码
其中,由于月份的标题需要占满一行,需要重写onAttachedToRecyclerView
和onViewDetachedFromWindow
方法。
public class FullSpanUtil { public static void onAttachedToRecyclerView(RecyclerView recyclerView, final RecyclerView.Adapter adapter, final int pinnedHeaderType) { // 如果是网格布局,这里处理标签的布局占满一行 final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); if (layoutManager instanceof GridLayoutManager) { final GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager; final GridLayoutManager.SpanSizeLookup oldSizeLookup = gridLayoutManager.getSpanSizeLookup(); gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { if (adapter.getItemViewType(position) == pinnedHeaderType) { return gridLayoutManager.getSpanCount(); } if (oldSizeLookup != null) { return oldSizeLookup.getSpanSize(position); } return 1; } }); } } public static void onViewAttachedToWindow(RecyclerView.ViewHolder holder, RecyclerView.Adapter adapter, int pinnedHeaderType) { // 如果是瀑布流布局,这里处理标签的布局占满一行 final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); if (lp instanceof StaggeredGridLayoutManager.LayoutParams) { final StaggeredGridLayoutManager.LayoutParams slp = (StaggeredGridLayoutManager.LayoutParams) lp; slp.setFullSpan(adapter.getItemViewType(holder.getLayoutPosition()) == pinnedHeaderType); } }}复制代码
PictureModel
需要继承MultiItemEntity
,然后重写getItemType
方法,adapter
即可通过model
的type
来判断该使用哪个布局。 注:get和set方法这里就不贴了
public class PictureModel implements MultiItemEntity { public static final int PICTURE_TITLE = 1; public static final int PICTURE_CONTENT = 0; private int type; private String id; private String title; private String date_time; private String create_time; private String picture_count; private String status; private String cover_image; private String date; public PictureModel(int type) { this.type = type; } public int getType() { return type; } public void setType(int type) { this.type = type; } @Override public int getItemType() { return type; } }复制代码
最后,是在Activity
使用
pictureAdapter = new PictureAdapter(null); rvPictrues.setLayoutManager(new GridLayoutManager(context, 2)); SpaceDecoration spaceDecoration = new SpaceDecoration(dp2px(context, 10)); spaceDecoration.setPaddingStart(false); rvPictrues.addItemDecoration(spaceDecoration); rvPictrues.setAdapter(pictureAdapter); pictureAdapter.bindToRecyclerView(rvPictrues);复制代码
/** * dp转px * * @param context * @param dpVal * @return */ public static int dp2px(Context context, float dpVal) { return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpVal, context.getResources().getDisplayMetrics()); }复制代码
SpaceDecoration
public class SpaceDecoration extends RecyclerView.ItemDecoration { private int space; private int headerCount = -1; private int footerCount = Integer.MAX_VALUE; private boolean mPaddingEdgeSide = true; private boolean mPaddingStart = true; private boolean mPaddingHeaderFooter = false; private ColorDrawable mColorDrawable; private boolean mDrawLastItem = true; private boolean mDrawHeaderFooter = false; public SpaceDecoration(int space) { this.mColorDrawable = new ColorDrawable(Color.parseColor("#e7e7e7")); this.space = space; } public SpaceDecoration(int space, int color) { this.mColorDrawable = new ColorDrawable(color); this.space = space; } public void setPaddingEdgeSide(boolean mPaddingEdgeSide) { this.mPaddingEdgeSide = mPaddingEdgeSide; } public void setPaddingStart(boolean mPaddingStart) { this.mPaddingStart = mPaddingStart; } public void setPaddingHeaderFooter(boolean mPaddingHeaderFooter) { this.mPaddingHeaderFooter = mPaddingHeaderFooter; } @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { int position = parent.getChildAdapterPosition(view); int spanCount = 0; int orientation = 0; int spanIndex = 0; int headerCount = 0, footerCount = 0; if (parent.getAdapter() instanceof BaseQuickAdapter) { headerCount = ((BaseQuickAdapter) parent.getAdapter()).getHeaderLayoutCount(); footerCount = ((BaseQuickAdapter) parent.getAdapter()).getFooterLayoutCount(); } RecyclerView.LayoutManager layoutManager = parent.getLayoutManager(); if (layoutManager instanceof StaggeredGridLayoutManager) { orientation = ((StaggeredGridLayoutManager) layoutManager).getOrientation(); spanCount = ((StaggeredGridLayoutManager) layoutManager).getSpanCount(); spanIndex = ((StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams()).getSpanIndex(); } else if (layoutManager instanceof GridLayoutManager) { orientation = ((GridLayoutManager) layoutManager).getOrientation(); spanCount = ((GridLayoutManager) layoutManager).getSpanCount(); spanIndex = ((GridLayoutManager.LayoutParams) view.getLayoutParams()).getSpanIndex(); } else if (layoutManager instanceof LinearLayoutManager) { orientation = ((LinearLayoutManager) layoutManager).getOrientation(); spanCount = 1; spanIndex = 0; } /** * 普通Item的尺寸 */ if ((position >= headerCount && position < parent.getAdapter().getItemCount() - footerCount)) { if (orientation == VERTICAL) { float expectedWidth = (float) (parent.getWidth() - space * (spanCount + (mPaddingEdgeSide ? 1 : -1))) / spanCount; float originWidth = (float) parent.getWidth() / spanCount; float expectedX = (mPaddingEdgeSide ? space : 0) + (expectedWidth + space) * spanIndex; float originX = originWidth * spanIndex; outRect.left = (int) (expectedX - originX); outRect.right = (int) (originWidth - outRect.left - expectedWidth); if (position - headerCount < spanCount && mPaddingStart) { outRect.top = space; } outRect.bottom = space; return; } else { float expectedHeight = (float) (parent.getHeight() - space * (spanCount + (mPaddingEdgeSide ? 1 : -1))) / spanCount; float originHeight = (float) parent.getHeight() / spanCount; float expectedY = (mPaddingEdgeSide ? space : 0) + (expectedHeight + space) * spanIndex; float originY = originHeight * spanIndex; outRect.bottom = (int) (expectedY - originY); outRect.top = (int) (originHeight - outRect.bottom - expectedHeight); if (position - headerCount < spanCount && mPaddingStart) { outRect.left = space; } outRect.right = space; return; } } else if (mPaddingHeaderFooter) { if (orientation == VERTICAL) { outRect.right = outRect.left = mPaddingEdgeSide ? space : 0; outRect.top = mPaddingStart ? space : 0; } else { outRect.top = outRect.bottom = mPaddingEdgeSide ? space : 0; outRect.left = mPaddingStart ? space : 0; } } } @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { if (parent.getAdapter() == null) { return; } int orientation = 0; int headerCount = 0, footerCount = 0, dataCount; if (parent.getAdapter() instanceof BaseQuickAdapter) { headerCount = ((BaseQuickAdapter) parent.getAdapter()).getHeaderLayoutCount(); footerCount = ((BaseQuickAdapter) parent.getAdapter()).getFooterLayoutCount(); dataCount = parent.getAdapter().getItemCount(); } else { dataCount = parent.getAdapter().getItemCount(); } int dataStartPosition = headerCount; int dataEndPosition = headerCount + dataCount; RecyclerView.LayoutManager layoutManager = parent.getLayoutManager(); if (layoutManager instanceof StaggeredGridLayoutManager) { orientation = ((StaggeredGridLayoutManager) layoutManager).getOrientation(); } else if (layoutManager instanceof GridLayoutManager) { orientation = ((GridLayoutManager) layoutManager).getOrientation(); } else if (layoutManager instanceof LinearLayoutManager) { orientation = ((LinearLayoutManager) layoutManager).getOrientation(); } int start, end; if (orientation == OrientationHelper.VERTICAL) { start = parent.getPaddingLeft(); end = parent.getWidth() - parent.getPaddingRight(); } else { start = parent.getPaddingTop(); end = parent.getHeight() - parent.getPaddingBottom(); } int childCount = parent.getChildCount(); for (int i = 0; i < childCount; i++) { View child = parent.getChildAt(i); int position = parent.getChildAdapterPosition(child); if (position >= dataStartPosition && position < dataEndPosition - 1//数据项除了最后一项 || (position == dataEndPosition - 1 && mDrawLastItem)//数据项最后一项 || (!(position >= dataStartPosition && position < dataEndPosition) && mDrawHeaderFooter)//header&footer且可绘制 ) { if (orientation == OrientationHelper.VERTICAL) { RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams(); int top = child.getBottom() + params.bottomMargin; int bottom = top; mColorDrawable.setBounds(start, top, end, bottom); mColorDrawable.draw(c); } else { RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams(); int left = child.getRight() + params.rightMargin; int right = left; mColorDrawable.setBounds(left, start, right, end); mColorDrawable.draw(c); } } } }}复制代码
2.顶部栏部分
这里可以使用ItemDecoration
,难点在于如何设置点击点击事件。感谢作者,提供了一种新的思路 ,这里只说如何使用,原理阅读作者源码即可。 按照这个思路,我们可以将头部布局单独出来,这样的话,处理点击事件就很简单。 activity布局
复制代码
StickyHeadContainer
public class StickyHeadContainer extends ViewGroup { private int mOffset; private int mLastOffset = Integer.MIN_VALUE; private int mLastStickyHeadPosition = Integer.MIN_VALUE; private int mLeft; private int mRight; private int mTop; private int mBottom; private DataCallback mDataCallback; public StickyHeadContainer(Context context) { this(context, null); } public StickyHeadContainer(Context context, AttributeSet attrs) { this(context, attrs, 0); } public StickyHeadContainer(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // TODO: 2017/1/9 屏蔽点击事件 } }); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int desireHeight; int desireWidth; int count = getChildCount(); if (count != 1) { throw new IllegalArgumentException("只允许容器添加1个子View!"); } final View child = getChildAt(0); // 测量子元素并考虑外边距 measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); // 获取子元素的布局参数 final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); // 计算子元素宽度,取子控件最大宽度 desireWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; // 计算子元素高度 desireHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; // 考虑父容器内边距 desireWidth += getPaddingLeft() + getPaddingRight(); desireHeight += getPaddingTop() + getPaddingBottom(); // 尝试比较建议最小值和期望值的大小并取大值 desireWidth = Math.max(desireWidth, getSuggestedMinimumWidth()); desireHeight = Math.max(desireHeight, getSuggestedMinimumHeight()); // 设置最终测量值 setMeasuredDimension(resolveSize(desireWidth, widthMeasureSpec), resolveSize(desireHeight, heightMeasureSpec)); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { final View child = getChildAt(0); MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); final int paddingLeft = getPaddingLeft(); final int paddingTop = getPaddingTop(); mLeft = paddingLeft + lp.leftMargin; mRight = child.getMeasuredWidth() + mLeft; mTop = paddingTop + lp.topMargin + mOffset; mBottom = child.getMeasuredHeight() + mTop; child.layout(mLeft, mTop, mRight, mBottom); } // 生成默认的布局参数 @Override protected LayoutParams generateDefaultLayoutParams() { return super.generateDefaultLayoutParams(); } // 生成布局参数,将布局参数包装成我们的 @Override protected LayoutParams generateLayoutParams(LayoutParams p) { return new MarginLayoutParams(p); } // 生成布局参数,从属性配置中生成我们的布局参数 @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(), attrs); } // 查当前布局参数是否是我们定义的类型这在code声明布局参数时常常用到 @Override protected boolean checkLayoutParams(LayoutParams p) { return p instanceof MarginLayoutParams; } public void scrollChild(int offset) { if (mLastOffset != offset) { mOffset = offset; ViewCompat.offsetTopAndBottom(getChildAt(0), mOffset - mLastOffset); } mLastOffset = mOffset; } protected int getChildHeight() { return getChildAt(0).getHeight(); } protected void onDataChange(int stickyHeadPosition) { if (mDataCallback != null && mLastStickyHeadPosition != stickyHeadPosition) { mDataCallback.onDataChange(stickyHeadPosition); } mLastStickyHeadPosition = stickyHeadPosition; } public void reset() { mLastStickyHeadPosition = Integer.MIN_VALUE; } public interface DataCallback { void onDataChange(int pos); } public void setDataCallback(DataCallback dataCallback) { mDataCallback = dataCallback; }}复制代码
在activity
中完整使用
StickyItemDecoration stickyItemDecoration = new StickyItemDecoration(shcPictrues, PictureModel.PICTURE_TITLE); stickyItemDecoration.setOnStickyChangeListener(new OnStickyChangeListener() { @Override public void onScrollable(int offset) { //可见时 shcPictrues.scrollChild(offset); shcPictrues.setVisibility(View.VISIBLE); } @Override public void onInVisible() { //不可见时 shcPictrues.reset(); shcPictrues.setVisibility(View.INVISIBLE); } }); shcPictrues.setDataCallback(new StickyHeadContainer.DataCallback() { @Override public void onDataChange(int pos) { //数据更新 ListlistModels = pictureAdapter.getData(); if (listModels.size() > pos) { tvPictureTime.setText(listModels.get(pos).getDate()); } } }); //添加至rv rvPictrues.addItemDecoration(stickyItemDecoration); pictureAdapter = new PictureAdapter(null); rvPictrues.setLayoutManager(new GridLayoutManager(context, 2)); rvPictrues.addItemDecoration(stickyItemDecoration); SpaceDecoration spaceDecoration = new SpaceDecoration(DensityUtils.dp2px(context, 10)); spaceDecoration.setPaddingStart(false); rvPictrues.addItemDecoration(spaceDecoration); rvPictrues.setAdapter(pictureAdapter); pictureAdapter.bindToRecyclerView(rvPictrues);复制代码
StickyItemDecoration
public class StickyItemDecoration extends RecyclerView.ItemDecoration { private int mStickyHeadType; private int mFirstVisiblePosition; // private int mFirstCompletelyVisiblePosition; private int mStickyHeadPosition; private int[] mInto; private RecyclerView.Adapter mAdapter; private StickyHeadContainer mStickyHeadContainer; private boolean mEnableStickyHead = true; private OnStickyChangeListener mOnStickyChangeListener; public void setOnStickyChangeListener(OnStickyChangeListener onStickyChangeListener){ this.mOnStickyChangeListener = onStickyChangeListener; } public StickyItemDecoration(StickyHeadContainer stickyHeadContainer, int stickyHeadType) { mStickyHeadContainer = stickyHeadContainer; mStickyHeadType = stickyHeadType; } // 当我们调用mRecyclerView.addItemDecoration()方法添加decoration的时候,RecyclerView在绘制的时候,去会绘制decorator,即调用该类的onDraw和onDrawOver方法, // 1.onDraw方法先于drawChildren // 2.onDrawOver在drawChildren之后,一般我们选择复写其中一个即可。 // 3.getItemOffsets 可以通过outRect.set()为每个Item设置一定的偏移量,主要用于绘制Decorator。 @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { super.onDraw(c, parent, state); checkCache(parent); if (mAdapter == null) { // checkCache的话RecyclerView未设置之前mAdapter为空 return; } calculateStickyHeadPosition(parent); if (mEnableStickyHead /*&& mFirstCompletelyVisiblePosition > mStickyHeadPosition*/ && mFirstVisiblePosition >= mStickyHeadPosition && mStickyHeadPosition != -1) { View belowView = parent.findChildViewUnder(c.getWidth() / 2, mStickyHeadContainer.getChildHeight() + 0.01f); mStickyHeadContainer.onDataChange(mStickyHeadPosition); int offset; if (isStickyHead(parent, belowView) && belowView.getTop() > 0) { offset = belowView.getTop() - mStickyHeadContainer.getChildHeight(); } else { offset = 0; } if (mOnStickyChangeListener!=null){ mOnStickyChangeListener.onScrollable(offset); } } else { if (mOnStickyChangeListener!=null){ mOnStickyChangeListener.onInVisible(); } } } public void enableStickyHead(boolean enableStickyHead) { mEnableStickyHead = enableStickyHead; if (!mEnableStickyHead) { mStickyHeadContainer.setVisibility(View.INVISIBLE); } } private void calculateStickyHeadPosition(RecyclerView parent) { final RecyclerView.LayoutManager layoutManager = parent.getLayoutManager(); // mFirstCompletelyVisiblePosition = findFirstCompletelyVisiblePosition(layoutManager); // 获取第一个可见的item位置 mFirstVisiblePosition = findFirstVisiblePosition(layoutManager); // 获取标签的位置, int stickyHeadPosition = findStickyHeadPosition(mFirstVisiblePosition); if (stickyHeadPosition >= 0 && mStickyHeadPosition != stickyHeadPosition) { // 标签位置有效并且和缓存的位置不同 mStickyHeadPosition = stickyHeadPosition; } } /** * 从传入位置递减找出标签的位置 * * @param formPosition * @return */ private int findStickyHeadPosition(int formPosition) { for (int position = formPosition; position >= 0; position--) { // 位置递减,只要查到位置是标签,立即返回此位置 final int type = mAdapter.getItemViewType(position); if (isStickyHeadType(type)) { return position; } } return -1; } /** * 通过适配器告知类型是否为标签 * * @param type * @return */ private boolean isStickyHeadType(int type) { return mStickyHeadType == type; } /** * 找出第一个可见的Item的位置 * * @param layoutManager * @return */ private int findFirstVisiblePosition(RecyclerView.LayoutManager layoutManager) { int firstVisiblePosition = 0; if (layoutManager instanceof GridLayoutManager) { firstVisiblePosition = ((GridLayoutManager) layoutManager).findFirstVisibleItemPosition(); } else if (layoutManager instanceof LinearLayoutManager) { firstVisiblePosition = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition(); } else if (layoutManager instanceof StaggeredGridLayoutManager) { mInto = new int[((StaggeredGridLayoutManager) layoutManager).getSpanCount()]; ((StaggeredGridLayoutManager) layoutManager).findFirstVisibleItemPositions(mInto); firstVisiblePosition = Integer.MAX_VALUE; for (int pos : mInto) { firstVisiblePosition = Math.min(pos, firstVisiblePosition); } } return firstVisiblePosition; } /** * 找出第一个完全可见的Item的位置 * * @param layoutManager * @return */ private int findFirstCompletelyVisiblePosition(RecyclerView.LayoutManager layoutManager) { int firstVisiblePosition = 0; if (layoutManager instanceof GridLayoutManager) { firstVisiblePosition = ((GridLayoutManager) layoutManager).findFirstCompletelyVisibleItemPosition(); } else if (layoutManager instanceof LinearLayoutManager) { firstVisiblePosition = ((LinearLayoutManager) layoutManager).findFirstCompletelyVisibleItemPosition(); } else if (layoutManager instanceof StaggeredGridLayoutManager) { mInto = new int[((StaggeredGridLayoutManager) layoutManager).getSpanCount()]; ((StaggeredGridLayoutManager) layoutManager).findFirstCompletelyVisibleItemPositions(mInto); firstVisiblePosition = Integer.MAX_VALUE; for (int pos : mInto) { firstVisiblePosition = Math.min(pos, firstVisiblePosition); } } return firstVisiblePosition; } /** * 检查缓存 * * @param parent */ private void checkCache(final RecyclerView parent) { final RecyclerView.Adapter adapter = parent.getAdapter(); if (mAdapter != adapter) { mAdapter = adapter; // 适配器为null或者不同,清空缓存 mStickyHeadPosition = -1; mAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { @Override public void onChanged() { reset(); } @Override public void onItemRangeChanged(int positionStart, int itemCount) { reset(); } @Override public void onItemRangeChanged(int positionStart, int itemCount, Object payload) { reset(); } @Override public void onItemRangeInserted(int positionStart, int itemCount) { reset(); } @Override public void onItemRangeRemoved(int positionStart, int itemCount) { reset(); } @Override public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { reset(); } }); } } private void reset() { mStickyHeadContainer.reset(); } /** * 查找到view对应的位置从而判断出是否标签类型 * * @param parent * @param view * @return */ private boolean isStickyHead(RecyclerView parent, View view) { final int position = parent.getChildAdapterPosition(view); if (position == RecyclerView.NO_POSITION) { return false; } final int type = mAdapter.getItemViewType(position); return isStickyHeadType(type); }}复制代码
3.点击顶部栏弹窗
这里就偷个懒,不贴代码了。