|
|
@@ -0,0 +1,589 @@
|
|
|
+package kr.co.zumo.app.lifeplus.view.screen.main;
|
|
|
+
|
|
|
+import android.graphics.PointF;
|
|
|
+import android.support.annotation.NonNull;
|
|
|
+import android.support.annotation.Nullable;
|
|
|
+import android.support.v7.widget.LinearSmoothScroller;
|
|
|
+import android.support.v7.widget.OrientationHelper;
|
|
|
+import android.support.v7.widget.RecyclerView;
|
|
|
+import android.support.v7.widget.RecyclerView.LayoutManager;
|
|
|
+import android.support.v7.widget.RecyclerView.SmoothScroller;
|
|
|
+import android.support.v7.widget.RecyclerView.SmoothScroller.ScrollVectorProvider;
|
|
|
+import android.support.v7.widget.SnapHelper;
|
|
|
+import android.util.DisplayMetrics;
|
|
|
+import android.util.Log;
|
|
|
+import android.view.View;
|
|
|
+import android.view.animation.DecelerateInterpolator;
|
|
|
+import android.widget.Scroller;
|
|
|
+
|
|
|
+/**
|
|
|
+ * MainCategorySnapper
|
|
|
+ * <pre>
|
|
|
+ * </pre>
|
|
|
+ *
|
|
|
+ * @author 하세미
|
|
|
+ * @version 1.0
|
|
|
+ * @history 하세미 [2018-11-27] [최초 작성]
|
|
|
+ * @since 2018-11-27
|
|
|
+ */
|
|
|
+public class MainCategorySnapper extends RecyclerView.OnFlingListener {
|
|
|
+
|
|
|
+ private final int snapOffset;
|
|
|
+ private int minVelocity = 200;
|
|
|
+
|
|
|
+ private static final int TIME_MAX = 990;
|
|
|
+ private static final int TIME_MIN = 200;
|
|
|
+
|
|
|
+ static final float MILLISECONDS_PER_INCH = 100f;
|
|
|
+
|
|
|
+ RecyclerView mRecyclerView;
|
|
|
+ private Scroller mGravityScroller;
|
|
|
+
|
|
|
+ private static final float INVALID_DISTANCE = 1f;
|
|
|
+
|
|
|
+ // Orientation helpers are lazily created per LayoutManager.
|
|
|
+ @Nullable
|
|
|
+ private OrientationHelper mVerticalHelper;
|
|
|
+ @Nullable
|
|
|
+ private OrientationHelper mHorizontalHelper;
|
|
|
+
|
|
|
+ private int targetChildPosition = 0;
|
|
|
+
|
|
|
+ // Handles the snap on scroll case.
|
|
|
+ private final RecyclerView.OnScrollListener mScrollListener =
|
|
|
+ new RecyclerView.OnScrollListener() {
|
|
|
+ boolean mScrolled = false;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
|
|
|
+ super.onScrollStateChanged(recyclerView, newState);
|
|
|
+ if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
|
|
|
+ mScrolled = false;
|
|
|
+ snapToTargetExistingView();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
|
|
|
+// Log.i("APP# MainCategorySnapper | onScrolled", "|" + "dx: " + dx);
|
|
|
+ if (dx != 0 || dy != 0) {
|
|
|
+ mScrolled = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 메인의 세로 스크롤으 스냅을 조절한다.
|
|
|
+ *
|
|
|
+ * @param minVelocity velocity 최소값, 이 이상의 속도에서만 fling 을 처리한다.
|
|
|
+ * @param snapOffset snap 지점의 offset, - (음수) 이면 화면의 위쪽으로 이동.
|
|
|
+ */
|
|
|
+ public MainCategorySnapper(int minVelocity, int snapOffset) {
|
|
|
+ this.minVelocity = minVelocity;
|
|
|
+ this.snapOffset = snapOffset;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public boolean onFling(int velocityX, int velocityY) {
|
|
|
+ Log.w("APP# MainCategorySnapper | onFling", "|" + "velocityY: " + velocityY);
|
|
|
+ LayoutManager layoutManager = mRecyclerView.getLayoutManager();
|
|
|
+ if (layoutManager == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
|
|
|
+ if (adapter == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ // fixme ------
|
|
|
+ int minFlingVelocity = 0; //mRecyclerView.getMinFlingVelocity();
|
|
|
+ return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity)
|
|
|
+ && snapFromFling(layoutManager, velocityX, velocityY);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Attaches the {@link SnapHelper} 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 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 LayoutManager layoutManager, int velocityX,
|
|
|
+ int velocityY) {
|
|
|
+ if (!(layoutManager instanceof ScrollVectorProvider)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ SmoothScroller smoothScroller = createScroller(layoutManager);
|
|
|
+ if (smoothScroller == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
|
|
|
+ Log.w("APP# MainCategorySnapper | snapFromFling2", "|" + "targetPosition: " + targetPosition);
|
|
|
+ if (targetPosition == RecyclerView.NO_POSITION) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ targetChildPosition = targetPosition;
|
|
|
+
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ LayoutManager layoutManager = mRecyclerView.getLayoutManager();
|
|
|
+ if (layoutManager == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * 마지막 view (더보기)는 폭이 좁아서 포커스를 유지할 수 없기에 제외
|
|
|
+ */
|
|
|
+ Log.w("APP# MainCategorySnapper | snapFromFling2", "|" + "targetChildPosition: " + targetChildPosition);
|
|
|
+
|
|
|
+ View snapView = findSnapView(layoutManager);
|
|
|
+// if (targetChildPosition < layoutManager.getChildCount() - 1) {
|
|
|
+// snapView = findSnapView(layoutManager);
|
|
|
+// }
|
|
|
+// else {
|
|
|
+// snapView = layoutManager.getChildAt(targetChildPosition);
|
|
|
+// }
|
|
|
+
|
|
|
+ Log.i("APP# MainCategorySnapper | snapToTargetExistingView", "|" + snapView);
|
|
|
+
|
|
|
+ if (snapView == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 현재 스크롤 포지션에 가장 가까운 뷰로 스크롤 시킨다.
|
|
|
+ * - recyclerView 의 스크롤을 이용하지않고 직접 스크롤시킨다.
|
|
|
+ * - recyclerView 는 너무 빠름.
|
|
|
+ */
|
|
|
+ int position = layoutManager.getPosition(snapView);
|
|
|
+
|
|
|
+ SmoothScroller smoothScroller = createScroller(layoutManager);
|
|
|
+ if (smoothScroller == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ smoothScroller.setTargetPosition(position);
|
|
|
+ layoutManager.startSmoothScroll(smoothScroller);
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Creates a scroller to be used in the snapping implementation.
|
|
|
+ *
|
|
|
+ * @param layoutManager The {@link LayoutManager} associated with the attached
|
|
|
+ * {@link RecyclerView}.
|
|
|
+ * @return a {@link SmoothScroller} which will handle the scrolling.
|
|
|
+ */
|
|
|
+ @Nullable
|
|
|
+ protected SmoothScroller createScroller(LayoutManager layoutManager) {
|
|
|
+ return createSnapScroller(layoutManager);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Creates a scroller to be used in the snapping implementation.
|
|
|
+ *
|
|
|
+ * @param layoutManager The {@link LayoutManager} associated with the attached
|
|
|
+ * {@link RecyclerView}.
|
|
|
+ * @return a {@link LinearSmoothScroller} which will handle the scrolling.
|
|
|
+ * @deprecated use {@link #createScroller(LayoutManager)} instead.
|
|
|
+ */
|
|
|
+ @Nullable
|
|
|
+ @Deprecated
|
|
|
+ protected LinearSmoothScroller createSnapScroller(LayoutManager layoutManager) {
|
|
|
+ if (!(layoutManager instanceof ScrollVectorProvider)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return new LinearSmoothScroller(mRecyclerView.getContext()) {
|
|
|
+ @Override
|
|
|
+ protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
|
|
|
+ if (mRecyclerView == null) {
|
|
|
+ // The associated RecyclerView has been removed so there is no action to take.
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(), targetView);
|
|
|
+ final int dx = snapDistances[0] - snapOffset;
|
|
|
+ final int dy = snapDistances[1] - snapOffset;
|
|
|
+ final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));
|
|
|
+ if (time > 0) {
|
|
|
+ int newTime = Math.min(TIME_MAX, Math.max(time, TIME_MIN));
|
|
|
+ action.update(dx, dy, newTime, mDecelerateInterpolator);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
|
|
|
+ return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 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 SnapHelper} 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 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 LayoutManager layoutManager, @NonNull View targetView) {
|
|
|
+ int[] out = new int[2];
|
|
|
+ if (layoutManager.canScrollHorizontally()) {
|
|
|
+ out[0] = distanceToTop(layoutManager, targetView, getHorizontalHelper(layoutManager));
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ out[0] = 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (layoutManager.canScrollVertically()) {
|
|
|
+ out[1] = distanceToTop(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 SnapHelper} 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 SnapHelper} 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 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(LayoutManager layoutManager) {
|
|
|
+ if (layoutManager.canScrollVertically()) {
|
|
|
+ return findTopView(layoutManager, getVerticalHelper(layoutManager));
|
|
|
+ }
|
|
|
+ else if (layoutManager.canScrollHorizontally()) {
|
|
|
+ return findTopView(layoutManager, getHorizontalHelper(layoutManager));
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Return the child view that is currently closest to the top of this parent.
|
|
|
+ *
|
|
|
+ * @param layoutManager The {@link 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 top of this parent.
|
|
|
+ */
|
|
|
+ @Nullable
|
|
|
+ private View findTopView(LayoutManager layoutManager, OrientationHelper helper) {
|
|
|
+ int childCount = layoutManager.getChildCount();
|
|
|
+ if (childCount == 0) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ View closestChild = null;
|
|
|
+ final int top;
|
|
|
+ if (layoutManager.getClipToPadding()) {
|
|
|
+ top = helper.getStartAfterPadding();
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ top = 0;
|
|
|
+ }
|
|
|
+ int absClosest = Integer.MAX_VALUE;
|
|
|
+
|
|
|
+ for (int i = 0; i < childCount; i++) {
|
|
|
+ final View child = layoutManager.getChildAt(i);
|
|
|
+ int childStart = helper.getDecoratedStart(child);
|
|
|
+ int childEnd = helper.getDecoratedEnd(child);
|
|
|
+ int childSize = Math.abs(childEnd - childStart) + snapOffset;
|
|
|
+ int childCenter = childStart + (helper.getDecoratedMeasurement(child) >> 2);
|
|
|
+ int absDistance = Math.abs(childCenter - (top + (childSize >> 2)));
|
|
|
+
|
|
|
+ /** if child top is closer than previous closest, set it as closest **/
|
|
|
+ if (absDistance < absClosest) {
|
|
|
+ absClosest = absDistance;
|
|
|
+ closestChild = child;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return closestChild;
|
|
|
+ }
|
|
|
+
|
|
|
+ private int distanceToTop(@NonNull LayoutManager layoutManager, @NonNull View targetView, OrientationHelper helper) {
|
|
|
+ /**
|
|
|
+ * targetView 의 시작 위치(Top)를 반환한다.
|
|
|
+ */
|
|
|
+ return helper.getDecoratedStart(targetView);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Estimates a position to which SnapHelper will try to scroll to in response to a fling.
|
|
|
+ *
|
|
|
+ * @param layoutManager The {@link LayoutManager} associated with the attached
|
|
|
+ * {@link RecyclerView}.
|
|
|
+ * @param helper The {@link OrientationHelper} that is created from the LayoutManager.
|
|
|
+ * @param velocityX The velocity on the x axis.
|
|
|
+ * @param velocityY The velocity on the y axis.
|
|
|
+ * @return The diff between the target scroll position and the current position.
|
|
|
+ */
|
|
|
+ private int estimateNextPositionDiffForFling(LayoutManager layoutManager,
|
|
|
+ OrientationHelper helper, int velocityX, int velocityY) {
|
|
|
+ int[] distances = calculateScrollDistance(velocityX, velocityY);
|
|
|
+ float distancePerChild = computeDistancePerChild(layoutManager, helper);
|
|
|
+ if (distancePerChild <= 0) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ int distance =
|
|
|
+ Math.abs(distances[0]) > Math.abs(distances[1]) ? distances[0] : distances[1];
|
|
|
+// return (int) Math.round(distance / distancePerChild);
|
|
|
+
|
|
|
+ // fixme ------
|
|
|
+ int min = 0;
|
|
|
+ // 속도가 minVelocity 보다 크면 기본 1칸 이동
|
|
|
+ if (Math.abs(velocityX) > minVelocity || Math.abs(velocityY) > minVelocity) {
|
|
|
+ min = 1;
|
|
|
+ }
|
|
|
+ int result = Math.round(distance / distancePerChild);
|
|
|
+ if (result == 0 && min > 0) {
|
|
|
+ result = distance > 0 ? min : -min;
|
|
|
+ }
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Override to provide a particular adapter target position for snapping.
|
|
|
+ *
|
|
|
+ * @param layoutManager the {@link 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(LayoutManager layoutManager, int velocityX,
|
|
|
+ int velocityY) {
|
|
|
+ if (!(layoutManager instanceof ScrollVectorProvider)) {
|
|
|
+ return RecyclerView.NO_POSITION;
|
|
|
+ }
|
|
|
+
|
|
|
+ final int itemCount = layoutManager.getItemCount();
|
|
|
+ if (itemCount == 0) {
|
|
|
+ return RecyclerView.NO_POSITION;
|
|
|
+ }
|
|
|
+
|
|
|
+ final View currentView = findSnapView(layoutManager);
|
|
|
+ if (currentView == null) {
|
|
|
+ return RecyclerView.NO_POSITION;
|
|
|
+ }
|
|
|
+
|
|
|
+ final int currentPosition = layoutManager.getPosition(currentView);
|
|
|
+ if (currentPosition == RecyclerView.NO_POSITION) {
|
|
|
+ return RecyclerView.NO_POSITION;
|
|
|
+ }
|
|
|
+
|
|
|
+ ScrollVectorProvider vectorProvider =
|
|
|
+ (ScrollVectorProvider) layoutManager;
|
|
|
+ // deltaJumps sign comes from the velocity which may not match the order of children in
|
|
|
+ // the LayoutManager. To overcome this, we ask for a vector from the LayoutManager to
|
|
|
+ // get the direction.
|
|
|
+ PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);
|
|
|
+ if (vectorForEnd == null) {
|
|
|
+ // cannot get a vector for the given position.
|
|
|
+ return RecyclerView.NO_POSITION;
|
|
|
+ }
|
|
|
+
|
|
|
+ int vDeltaJump, hDeltaJump;
|
|
|
+ if (layoutManager.canScrollHorizontally()) {
|
|
|
+ hDeltaJump = estimateNextPositionDiffForFling(layoutManager,
|
|
|
+ getHorizontalHelper(layoutManager), velocityX, 0);
|
|
|
+ if (vectorForEnd.x < 0) {
|
|
|
+ hDeltaJump = -hDeltaJump;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ hDeltaJump = 0;
|
|
|
+ }
|
|
|
+ if (layoutManager.canScrollVertically()) {
|
|
|
+ vDeltaJump = estimateNextPositionDiffForFling(layoutManager,
|
|
|
+ getVerticalHelper(layoutManager), 0, velocityY);
|
|
|
+ if (vectorForEnd.y < 0) {
|
|
|
+ vDeltaJump = -vDeltaJump;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ vDeltaJump = 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ int deltaJump = layoutManager.canScrollVertically() ? vDeltaJump : hDeltaJump;
|
|
|
+ if (deltaJump == 0) {
|
|
|
+ return RecyclerView.NO_POSITION;
|
|
|
+ }
|
|
|
+
|
|
|
+ int targetPos = currentPosition + deltaJump;
|
|
|
+ if (targetPos < 0) {
|
|
|
+ targetPos = 0;
|
|
|
+ }
|
|
|
+ if (targetPos >= itemCount) {
|
|
|
+ targetPos = itemCount - 1;
|
|
|
+ }
|
|
|
+ return targetPos;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Computes an average pixel value to pass a single child.
|
|
|
+ * <p>
|
|
|
+ * Returns a negative value if it cannot be calculated.
|
|
|
+ *
|
|
|
+ * @param layoutManager The {@link LayoutManager} associated with the attached
|
|
|
+ * {@link RecyclerView}.
|
|
|
+ * @param helper The relevant {@link OrientationHelper} for the attached
|
|
|
+ * {@link LayoutManager}.
|
|
|
+ * @return A float value that is the average number of pixels needed to scroll by one view in
|
|
|
+ * the relevant direction.
|
|
|
+ */
|
|
|
+ private float computeDistancePerChild(LayoutManager layoutManager,
|
|
|
+ OrientationHelper helper) {
|
|
|
+ View minPosView = null;
|
|
|
+ View maxPosView = null;
|
|
|
+ int minPos = Integer.MAX_VALUE;
|
|
|
+ int maxPos = Integer.MIN_VALUE;
|
|
|
+ int childCount = layoutManager.getChildCount();
|
|
|
+ if (childCount == 0) {
|
|
|
+ return INVALID_DISTANCE;
|
|
|
+ }
|
|
|
+
|
|
|
+ for (int i = 0; i < childCount; i++) {
|
|
|
+ View child = layoutManager.getChildAt(i);
|
|
|
+ final int pos = layoutManager.getPosition(child);
|
|
|
+ if (pos == RecyclerView.NO_POSITION) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (pos < minPos) {
|
|
|
+ minPos = pos;
|
|
|
+ minPosView = child;
|
|
|
+ }
|
|
|
+ if (pos > maxPos) {
|
|
|
+ maxPos = pos;
|
|
|
+ maxPosView = child;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (minPosView == null || maxPosView == null) {
|
|
|
+ return INVALID_DISTANCE;
|
|
|
+ }
|
|
|
+ int start = Math.min(helper.getDecoratedStart(minPosView),
|
|
|
+ helper.getDecoratedStart(maxPosView));
|
|
|
+ int end = Math.max(helper.getDecoratedEnd(minPosView),
|
|
|
+ helper.getDecoratedEnd(maxPosView));
|
|
|
+ int distance = end - start;
|
|
|
+ if (distance == 0) {
|
|
|
+ return INVALID_DISTANCE;
|
|
|
+ }
|
|
|
+ return 1f * distance / ((maxPos - minPos) + 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ @NonNull
|
|
|
+ private OrientationHelper getVerticalHelper(@NonNull LayoutManager layoutManager) {
|
|
|
+ if (mVerticalHelper == null || mVerticalHelper.getLayoutManager() != layoutManager) {
|
|
|
+ mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager);
|
|
|
+ }
|
|
|
+ return mVerticalHelper;
|
|
|
+ }
|
|
|
+
|
|
|
+ @NonNull
|
|
|
+ private OrientationHelper getHorizontalHelper(
|
|
|
+ @NonNull LayoutManager layoutManager) {
|
|
|
+ if (mHorizontalHelper == null || mHorizontalHelper.getLayoutManager() != layoutManager) {
|
|
|
+ mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
|
|
|
+ }
|
|
|
+ return mHorizontalHelper;
|
|
|
+ }
|
|
|
+}
|