给RecyclerView绑定一个PagerSnapHelper,可以实现ViewPager一次滑动一个item的效果,但ViewPager可以通过setPageTransformer方法动态改变每个Item的大小,而RecyclerView没有。
RecyclerView通过监听滑动事件,在onScrolled回调方法里判断滑动距离,动态设置Item的Scale大小。
如果直接使用PagerSnapHelper不好处理,直接写一个自己的SnapHelper,使用PagerSnapHelper的方法,由于是在滑动事件里改变Item大小,当你刷新适配器时,每个item大小又恢复默认大小,需要手动去更新,重置动态改变item大小。
点击item滑动到对应的item在中间显示:
mAdapter.setOnItemClickListener(new BaseAdapter.OnItemClickListener() {
@Override
public void onClick(int position, Object o) {
snapView = snapHelper.findSnapView(layoutManager);
int mPosition = layoutManager.getPosition(snapView);
if (mPosition == position) {
// snapHelper.refreshView(position);
} else {
layoutManager.scrollToPosition(position);
}
}
});
完整代码如下
package com.hj.common;
import android.graphics.PointF;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.View;
import android.view.animation.DecelerateInterpolator;
import android.widget.Scroller;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearSmoothScroller;
import androidx.recyclerview.widget.OrientationHelper;
import androidx.recyclerview.widget.RecyclerView;
public class ViewPagerSnapHelper extends RecyclerView.OnFlingListener {
private static final float MAX_SCALE = 1.0f;
private static final float MIN_SCALE = 0.8f;
private static final float MILLISECONDS_PER_INCH = 100f;
private static final int MAX_SCROLL_ON_FLING_DURATION = 100; // ms
// Orientation helpers are lazily created per LayoutManager.
@Nullable
private OrientationHelper mVerticalHelper;
@Nullable
private OrientationHelper mHorizontalHelper;
RecyclerView mRecyclerView;
private Scroller mGravityScroller;
private boolean mScrolled = false;
// Handles the snap on scroll case.
private final RecyclerView.OnScrollListener mScrollListener =
new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
mScrolled = false;
snapToTargetExistingView();
} else if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
scrollX = 0;
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (dx != 0 || dy != 0) {
mScrolled = true;
}
scrollX += dx;
refreshView(Math.abs(scrollX), dx > 0);
}
};
private int scrollX;
private View snapView;
//position位置为中间显示的item
public void refreshView(int position) {
RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
if (layoutManager == null) {
return;
}
for (int i = 0; i < layoutManager.getItemCount(); i++) {
View v = layoutManager.findViewByPosition(i);
if (v == null) {
continue;
}
if (i == position) {
v.setScaleX(MAX_SCALE);
v.setScaleY(MAX_SCALE);
} else {
v.setScaleX(MIN_SCALE);
v.setScaleY(MIN_SCALE);
}
}
}
public void refreshView(float mScrollX, boolean isToRight) {
RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
if (layoutManager == null) {
return;
}
snapView = findSnapView(layoutManager);
if (snapView != null) {
boolean isToNextView = false;
int w = snapView.getWidth();
if (mScrollX > w) {
//设置为每次滑动只能滑动一个item
mRecyclerView.suppressLayout(true);
mRecyclerView.suppressLayout(false);
snapToTargetExistingView();
}
if (mScrollX > w / 2) {
isToNextView = true;
mScrollX = w / 2;
}
float scaleFactor = mScrollX * 2 / w * (MAX_SCALE - MIN_SCALE);
int p = layoutManager.getPosition(snapView);
for (int i = 0; i < layoutManager.getItemCount(); i++) {
View v = layoutManager.findViewByPosition(i);
if (v == null) {
continue;
}
if (isToNextView) {
if (i == p) {
v.setScaleX(MAX_SCALE);
v.setScaleY(MAX_SCALE);
} else {
v.setScaleX(MIN_SCALE);
v.setScaleY(MIN_SCALE);
}
} else {
if (i == p) {
v.setScaleX(MAX_SCALE - scaleFactor);
v.setScaleY(MAX_SCALE - scaleFactor);
} else if ((isToRight && i == p + 1) || (!isToRight && i == p - 1)) {
v.setScaleX(MIN_SCALE + scaleFactor);
v.setScaleY(MIN_SCALE + scaleFactor);
} else {
v.setScaleX(MIN_SCALE);
v.setScaleY(MIN_SCALE);
}
}
}
} else {
}
}
@Override
public boolean onFling(int velocityX, int velocityY) {
RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
if (layoutManager == null) {
return false;
}
RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
if (adapter == null) {
return false;
}
int minFlingVelocity = mRecyclerView.getMinFlingVelocity();
return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity)
&& snapFromFling(layoutManager, velocityX, velocityY);
}
/**
* Attaches the {@link ViewPagerSnapHelper} to the provided RecyclerView, by calling
* {@link RecyclerView#setOnFlingListener(RecyclerView.OnFlingListener)}.
* You can call this method with {@code null} to detach it from the current RecyclerView.
*
* @param recyclerView The RecyclerView instance to which you want to add this helper or
* {@code null} if you want to remove SnapHelper from the current
* RecyclerView.
* @throws IllegalArgumentException if there is already a {@link RecyclerView.OnFlingListener}
* attached to the provided {@link RecyclerView}.
*/
public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
throws IllegalStateException {
if (mRecyclerView == recyclerView) {
return; // nothing to do
}
if (mRecyclerView != null) {
destroyCallbacks();
}
mRecyclerView = recyclerView;
if (mRecyclerView != null) {
setupCallbacks();
mGravityScroller = new Scroller(mRecyclerView.getContext(),
new DecelerateInterpolator());
snapToTargetExistingView();
}
}
/**
* Called when an instance of a {@link RecyclerView} is attached.
*/
private void setupCallbacks() throws IllegalStateException {
if (mRecyclerView.getOnFlingListener() != null) {
throw new IllegalStateException("An instance of OnFlingListener already set.");
}
mRecyclerView.addOnScrollListener(mScrollListener);
mRecyclerView.setOnFlingListener(this);
}
/**
* Called when the instance of a {@link RecyclerView} is detached.
*/
private void destroyCallbacks() {
mRecyclerView.removeOnScrollListener(mScrollListener);
mRecyclerView.setOnFlingListener(null);
}
/**
* Calculated the estimated scroll distance in each direction given velocities on both axes.
*
* @param velocityX Fling velocity on the horizontal axis.
* @param velocityY Fling velocity on the vertical axis.
* @return array holding the calculated distances in x and y directions
* respectively.
*/
public int[] calculateScrollDistance(int velocityX, int velocityY) {
int[] outDist = new int[2];
mGravityScroller.fling(0, 0, velocityX, velocityY,
Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
outDist[0] = mGravityScroller.getFinalX();
outDist[1] = mGravityScroller.getFinalY();
return outDist;
}
/**
* Helper method to facilitate for snapping triggered by a fling.
*
* @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached
* {@link RecyclerView}.
* @param velocityX Fling velocity on the horizontal axis.
* @param velocityY Fling velocity on the vertical axis.
* @return true if it is handled, false otherwise.
*/
private boolean snapFromFling(@NonNull RecyclerView.LayoutManager layoutManager, int velocityX,
int velocityY) {
if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
return false;
}
RecyclerView.SmoothScroller smoothScroller = createScroller(layoutManager);
if (smoothScroller == null) {
return false;
}
int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
if (targetPosition == RecyclerView.NO_POSITION) {
return false;
}
smoothScroller.setTargetPosition(targetPosition);
layoutManager.startSmoothScroll(smoothScroller);
return true;
}
/**
* Snaps to a target view which currently exists in the attached {@link RecyclerView}. This
* method is used to snap the view when the {@link RecyclerView} is first attached; when
* snapping was triggered by a scroll and when the fling is at its final stages.
*/
void snapToTargetExistingView() {
if (mRecyclerView == null) {
return;
}
RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
if (layoutManager == null) {
return;
}
View snapView = findSnapView(layoutManager);
if (snapView == null) {
return;
}
int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
if (snapDistance[0] != 0 || snapDistance[1] != 0) {
mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
}
}
/**
* Creates a scroller to be used in the snapping implementation.
*
* @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached
* {@link RecyclerView}.
* @return a {@link RecyclerView.SmoothScroller} which will handle the scrolling.
*/
@Nullable
protected RecyclerView.SmoothScroller createScroller(RecyclerView.LayoutManager layoutManager) {
return createSnapScroller(layoutManager);
}
/**
* Override this method to snap to a particular point within the target view or the container
* view on any axis.
* <p>
* This method is called when the {@link ViewPagerSnapHelper} has intercepted a fling and it needs
* to know the exact distance required to scroll by in order to snap to the target view.
*
* @param layoutManager the {@link RecyclerView.LayoutManager} associated with the attached
* {@link RecyclerView}
* @param targetView the target view that is chosen as the view to snap
* @return the output coordinates the put the result into. out[0] is the distance
* on horizontal axis and out[1] is the distance on vertical axis.
*/
@SuppressWarnings("WeakerAccess")
@Nullable
public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager,
@NonNull View targetView) {
int[] out = new int[2];
if (layoutManager.canScrollHorizontally()) {
out[0] = distanceToCenter(layoutManager, targetView,
getHorizontalHelper(layoutManager));
} else {
out[0] = 0;
}
if (layoutManager.canScrollVertically()) {
out[1] = distanceToCenter(layoutManager, targetView,
getVerticalHelper(layoutManager));
} else {
out[1] = 0;
}
return out;
}
/**
* Override this method to provide a particular target view for snapping.
* <p>
* This method is called when the {@link ViewPagerSnapHelper} is ready to start snapping and requires
* a target view to snap to. It will be explicitly called when the scroll state becomes idle
* after a scroll. It will also be called when the {@link ViewPagerSnapHelper} is preparing to snap
* after a fling and requires a reference view from the current set of child views.
* <p>
* If this method returns {@code null}, SnapHelper will not snap to any view.
*
* @param layoutManager the {@link RecyclerView.LayoutManager} associated with the attached
* {@link RecyclerView}
* @return the target view to which to snap on fling or end of scroll
*/
@SuppressWarnings("WeakerAccess")
@Nullable
public View findSnapView(RecyclerView.LayoutManager layoutManager) {
if (layoutManager.canScrollVertically()) {
return findCenterView(layoutManager, getVerticalHelper(layoutManager));
} else if (layoutManager.canScrollHorizontally()) {
return findCenterView(layoutManager, getHorizontalHelper(layoutManager));
}
return null;
}
/**
* Override to provide a particular adapter target position for snapping.
*
* @param layoutManager the {@link RecyclerView.LayoutManager} associated with the attached
* {@link RecyclerView}
* @param velocityX fling velocity on the horizontal axis
* @param velocityY fling velocity on the vertical axis
* @return the target adapter position to you want to snap or {@link RecyclerView#NO_POSITION}
* if no snapping should happen
*/
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
int velocityY) {
final int itemCount = layoutManager.getItemCount();
if (itemCount == 0) {
return RecyclerView.NO_POSITION;
}
final OrientationHelper orientationHelper = getOrientationHelper(layoutManager);
if (orientationHelper == null) {
return RecyclerView.NO_POSITION;
}
// A child that is exactly in the center is eligible for both before and after
View closestChildBeforeCenter = null;
int distanceBefore = Integer.MIN_VALUE;
View closestChildAfterCenter = null;
int distanceAfter = Integer.MAX_VALUE;
// Find the first view before the center, and the first view after the center
final int childCount = layoutManager.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = layoutManager.getChildAt(i);
if (child == null) {
continue;
}
final int distance = distanceToCenter(layoutManager, child, orientationHelper);
if (distance <= 0 && distance > distanceBefore) {
// Child is before the center and closer then the previous best
distanceBefore = distance;
closestChildBeforeCenter = child;
}
if (distance >= 0 && distance < distanceAfter) {
// Child is after the center and closer then the previous best
distanceAfter = distance;
closestChildAfterCenter = child;
}
}
// Return the position of the first child from the center, in the direction of the fling
final boolean forwardDirection = isForwardFling(layoutManager, velocityX, velocityY);
if (forwardDirection && closestChildAfterCenter != null) {
return layoutManager.getPosition(closestChildAfterCenter);
} else if (!forwardDirection && closestChildBeforeCenter != null) {
return layoutManager.getPosition(closestChildBeforeCenter);
}
// There is no child in the direction of the fling. Either it doesn't exist (start/end of
// the list), or it is not yet attached (very rare case when children are larger then the
// viewport). Extrapolate from the child that is visible to get the position of the view to
// snap to.
View visibleView = forwardDirection ? closestChildBeforeCenter : closestChildAfterCenter;
if (visibleView == null) {
return RecyclerView.NO_POSITION;
}
int visiblePosition = layoutManager.getPosition(visibleView);
int snapToPosition = visiblePosition
+ (isReverseLayout(layoutManager) == forwardDirection ? -1 : +1);
if (snapToPosition < 0 || snapToPosition >= itemCount) {
return RecyclerView.NO_POSITION;
}
return snapToPosition;
}
private boolean isForwardFling(RecyclerView.LayoutManager layoutManager, int velocityX,
int velocityY) {
if (layoutManager.canScrollHorizontally()) {
return velocityX > 0;
} else {
return velocityY > 0;
}
}
private boolean isReverseLayout(RecyclerView.LayoutManager layoutManager) {
final int itemCount = layoutManager.getItemCount();
if ((layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider =
(RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager;
PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);
if (vectorForEnd != null) {
return vectorForEnd.x < 0 || vectorForEnd.y < 0;
}
}
return false;
}
/**
* Creates a scroller to be used in the snapping implementation.
*
* @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached
* {@link RecyclerView}.
* @return a {@link LinearSmoothScroller} which will handle the scrolling.
*/
protected LinearSmoothScroller createSnapScroller(RecyclerView.LayoutManager layoutManager) {
if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
return null;
}
return new LinearSmoothScroller(mRecyclerView.getContext()) {
@Override
protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(),
targetView);
final int dx = snapDistances[0];
final int dy = snapDistances[1];
final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));
if (time > 0) {
action.update(dx, dy, time, mDecelerateInterpolator);
}
}
@Override
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}
@Override
protected int calculateTimeForScrolling(int dx) {
return Math.min(MAX_SCROLL_ON_FLING_DURATION, super.calculateTimeForScrolling(dx));
}
};
}
private int distanceToCenter(@NonNull RecyclerView.LayoutManager layoutManager,
@NonNull View targetView, OrientationHelper helper) {
final int childCenter = helper.getDecoratedStart(targetView)
+ (helper.getDecoratedMeasurement(targetView) / 2);
final int containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
return childCenter - containerCenter;
}
/**
* Return the child view that is currently closest to the center of this parent.
*
* @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached
* {@link RecyclerView}.
* @param helper The relevant {@link OrientationHelper} for the attached {@link RecyclerView}.
* @return the child view that is currently closest to the center of this parent.
*/
@Nullable
private View findCenterView(RecyclerView.LayoutManager layoutManager,
OrientationHelper helper) {
int childCount = layoutManager.getChildCount();
if (childCount == 0) {
return null;
}
View closestChild = null;
final int center;
if (layoutManager.getClipToPadding()) {
center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
} else {
center = helper.getEnd() / 2;
}
int absClosest = Integer.MAX_VALUE;
for (int i = 0; i < childCount; i++) {
final View child = layoutManager.getChildAt(i);
int childCenter = helper.getDecoratedStart(child)
+ (helper.getDecoratedMeasurement(child) / 2);
int absDistance = Math.abs(childCenter - center);
/* if child center is closer than previous closest, set it as closest */
if (absDistance < absClosest) {
absClosest = absDistance;
closestChild = child;
}
}
return closestChild;
}
@Nullable
private OrientationHelper getOrientationHelper(RecyclerView.LayoutManager layoutManager) {
if (layoutManager.canScrollVertically()) {
return getVerticalHelper(layoutManager);
} else if (layoutManager.canScrollHorizontally()) {
return getHorizontalHelper(layoutManager);
} else {
return null;
}
}
@NonNull
private OrientationHelper getVerticalHelper(@NonNull RecyclerView.LayoutManager layoutManager) {
if (mVerticalHelper == null || mVerticalHelper.getLayoutManager() != layoutManager) {
mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager);
}
return mVerticalHelper;
}
@NonNull
private OrientationHelper getHorizontalHelper(
@NonNull RecyclerView.LayoutManager layoutManager) {
if (mHorizontalHelper == null || mHorizontalHelper.getLayoutManager() != layoutManager) {
mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
}
return mHorizontalHelper;
}
}