使用RecyclerView实现一个画廊效果,主要是使用support库中最新加入的PagerSnapHelper类,通过计算滑动偏移来计算scale的值。
基本实现
首先需要为RecyclerView添加一个滚动监听,然后为RecyclerView的第一个与最后一个itemView添加一个ItemDecoration,使位于第一个与最后一个itemView的位置居中对齐。
public void attachToRecyclerView(final RecyclerView recyclerView) { this.recyclerView = recyclerView; snapHelper.attachToRecyclerView(recyclerView); recyclerView.addOnScrollListener(scrollListener); recyclerView.addItemDecoration(new ScalableCardItemDecoration()); recyclerView.post(new Runnable() { @Override public void run() { pageScrolled(); } }); }
ScalableCardItemDecoration用于计算itemView左右剩余空间,然后为第一个itemView与最后一个itemView添加偏移量。
private static class ScalableCardItemDecoration extends RecyclerView.ItemDecoration { @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { RecyclerView.ViewHolder holder = parent.getChildViewHolder(view); int position = holder.getAdapterPosition() == RecyclerView.NO_POSITION ? holder.getOldPosition() : holder.getAdapterPosition(); RecyclerView.LayoutManager layoutManager = parent.getLayoutManager(); int itemCount = layoutManager.getItemCount(); if(position != 0 && position != itemCount - 1){ return; } int peekWidth = getPeekWidth(parent, view); boolean isVertical = layoutManager.canScrollVertically(); //移除item时adapter position为-1。 if (isVertical) { if (position == 0) { outRect.set(0, peekWidth, 0, 0); } else if (position == itemCount - 1) { outRect.set(0, 0, 0, peekWidth); } else { outRect.set(0, 0, 0, 0); } } else { if (position == 0) { outRect.set(peekWidth, 0, 0, 0); } else if (position == itemCount - 1) { outRect.set(0, 0, peekWidth, 0); } else { outRect.set(0, 0, 0, 0); } } } }
在为item添加ItemDecoration时,需要计算两边的空间。这里需要手动测量itemView的宽高, 然后计算第一个itemView左边的偏移与最后一个itemView右边的偏移。这里碰到个问题,如果LayoutMangaer的方面是垂直或水平的且RecyclerView对应的的高度或宽度设为wrap_content时,这里计算的值不会正确。
public static int getPeekWidth(RecyclerView recyclerView, View itemView) { RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); boolean isVertical = layoutManager.canScrollVertically(); int position = recyclerView.getChildAdapterPosition(itemView); //TODO RecyclerView使用wrap_content时,获取的宽度可能会是0。 int parentWidth = recyclerView.getMeasuredWidth(); int parentHeight = recyclerView.getMeasuredHeight(); //有时会拿到0 parentWidth = parentWidth == 0 ? recyclerView.getWidth() : parentWidth; parentHeight = parentHeight == 0 ? recyclerView.getHeight() : parentHeight; int parentEnd = isVertical ? parentHeight : parentWidth; int parentCenter = parentEnd / 2; int itemSize = isVertical ? itemView.getMeasuredHeight() : itemView.getMeasuredWidth(); if (itemSize == 0) { ViewGroup.LayoutParams layoutParams = itemView.getLayoutParams(); int widthMeasureSpec = RecyclerView.LayoutManager.getChildMeasureSpec(parentWidth, layoutManager.getWidthMode(), recyclerView.getPaddingLeft() + recyclerView.getPaddingRight(), layoutParams.width, layoutManager.canScrollHorizontally()); int heightMeasureSpec = RecyclerView.LayoutManager.getChildMeasureSpec(parentHeight, layoutManager.getHeightMode(), recyclerView.getPaddingTop() + recyclerView.getPaddingBottom(), layoutParams.height, layoutManager.canScrollVertically()); itemView.measure(widthMeasureSpec, heightMeasureSpec); itemSize = isVertical ? itemView.getMeasuredHeight() : itemView.getMeasuredWidth(); } /* 计算ItemDecoration的大小,确保插入的大小正好使view的start + itemSize / 2等于parentCenter。 */ int startOffset = parentCenter - itemSize / 2; int endOffset = parentEnd - (startOffset + itemSize); return position == 0 ? startOffset : endOffset; }
添加完ItemDecoration后,我们需要在RecyclerView每次滚动的时候计算左、中、右3个itemView的缩放比例。
private void pageScrolled() { if (recyclerView == null || recyclerView.getChildCount() == 0) return; RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); View snapingView = snapHelper.findSnapView(layoutManager); int snapingViewPosition = recyclerView.getChildAdapterPosition(snapingView); View leftSnapingView = layoutManager.findViewByPosition(snapingViewPosition - 1); View rightSnapingView = layoutManager.findViewByPosition(snapingViewPosition + 1); float leftSnapingOffset = calculateOffset(recyclerView, leftSnapingView); float rightSnapingOffset = calculateOffset(recyclerView, rightSnapingView); float currentSnapingOffset = calculateOffset(recyclerView, snapingView); if (snapingView != null) { snapingView.setScaleX(currentSnapingOffset); snapingView.setScaleY(currentSnapingOffset); } if (leftSnapingView != null) { leftSnapingView.setScaleX(leftSnapingOffset); leftSnapingView.setScaleY(leftSnapingOffset); } if (rightSnapingView != null) { rightSnapingView.setScaleX(rightSnapingOffset); rightSnapingView.setScaleY(rightSnapingOffset); } if(snapingView != null && currentSnapingOffset >= 1){ OnPageChangeListener listener = pageChangeListenerRef != null ? pageChangeListenerRef.get(): null; if(listener != null) listener.onPageSelected(snapingViewPosition); } Log.d(TAG, String.format("left: %f, right: %f, current: %f", leftSnapingOffset, rightSnapingOffset, currentSnapingOffset)); }
计算缩放比例时是根据左、中、右三个itemView的中间点与RecyclerView中间点的距离来计算的。
private float calculateOffset(RecyclerView recyclerView, View view) { if (view == null) return -1; RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); boolean isVertical = layoutManager.canScrollVertically(); int viewStart = isVertical ? view.getTop() : view.getLeft(); int viewEnd = isVertical ? view.getBottom() : view.getRight(); int centerX = isVertical ? recyclerView.getHeight() / 2 : recyclerView.getWidth() / 2; int childCenter = (viewStart + viewEnd) / 2; int distance = Math.abs(childCenter - centerX); if (distance > centerX) return STAY_SCALE; float offset = 1.f - (distance / (float) centerX); return (1.f - STAY_SCALE) * offset + STAY_SCALE; }
项目地址: https://github.com/yjwfn/recyclerview-gallery