-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
5 changed files
with
407 additions
and
35 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
197 changes: 197 additions & 0 deletions
197
lib/java/com/google/android/material/carousel/CarouselSnapHelper.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
/* | ||
* Copyright 2023 The Android Open Source Project | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package com.google.android.material.carousel; | ||
|
||
import android.graphics.PointF; | ||
import androidx.recyclerview.widget.RecyclerView; | ||
import androidx.recyclerview.widget.RecyclerView.LayoutManager; | ||
import androidx.recyclerview.widget.SnapHelper; | ||
import android.view.View; | ||
import androidx.annotation.NonNull; | ||
import androidx.annotation.Nullable; | ||
|
||
/** | ||
* Implementation of the {@link SnapHelper} that supports snapping items to the carousel keylines | ||
* according to the strategy. | ||
*/ | ||
public class CarouselSnapHelper extends SnapHelper { | ||
|
||
private final boolean disableFling; | ||
|
||
public CarouselSnapHelper() { | ||
this(true); | ||
} | ||
|
||
public CarouselSnapHelper(boolean disableFling) { | ||
this.disableFling = disableFling; | ||
} | ||
|
||
@Nullable | ||
@Override | ||
public int[] calculateDistanceToFinalSnap( | ||
@NonNull LayoutManager layoutManager, @NonNull View view) { | ||
// If the layout manager is not a CarouselLayoutManager, we return with a zero offset | ||
// as there are no keylines to snap to. | ||
if (!(layoutManager instanceof CarouselLayoutManager)) { | ||
return new int[] {0, 0}; | ||
} | ||
|
||
int offset = 0; | ||
if (layoutManager.canScrollHorizontally()) { | ||
offset = distanceToFirstFocalKeyline(view, (CarouselLayoutManager) layoutManager); | ||
} | ||
// TODO(b/279088745): Implement snap helper for vertical scrolling. | ||
return new int[] {offset, 0}; | ||
} | ||
|
||
private int distanceToFirstFocalKeyline( | ||
@NonNull View targetView, CarouselLayoutManager layoutManager) { | ||
return layoutManager.getOffsetToScrollToPosition(layoutManager.getPosition(targetView)); | ||
} | ||
|
||
@Nullable | ||
@Override | ||
public View findSnapView(LayoutManager layoutManager) { | ||
// TODO(b/279088745): Implement snap helper for vertical scrolling. | ||
if (layoutManager.canScrollHorizontally()) { | ||
return findViewNearestFirstKeyline(layoutManager); | ||
} | ||
return null; | ||
} | ||
|
||
/** | ||
* Return the child view that is currently closest to the first focal keyline. | ||
* | ||
* @param layoutManager The {@link LayoutManager} associated with the attached {@link | ||
* RecyclerView}. | ||
* @return the child view that is currently closest to the first focal keyline. | ||
*/ | ||
@Nullable | ||
private View findViewNearestFirstKeyline(LayoutManager layoutManager) { | ||
int childCount = layoutManager.getChildCount(); | ||
if (childCount == 0 || !(layoutManager instanceof CarouselLayoutManager)) { | ||
return null; | ||
} | ||
View closestChild = null; | ||
int absClosest = Integer.MAX_VALUE; | ||
|
||
CarouselLayoutManager carouselLayoutManager = (CarouselLayoutManager) layoutManager; | ||
for (int i = 0; i < childCount; i++) { | ||
final View child = layoutManager.getChildAt(i); | ||
final int position = layoutManager.getPosition(child); | ||
final int offset = | ||
Math.abs(carouselLayoutManager.getOffsetToScrollToPosition(position)); | ||
|
||
// If child center is closer than previous closest, set it as closest | ||
if (offset < absClosest) { | ||
absClosest = offset; | ||
closestChild = child; | ||
} | ||
} | ||
return closestChild; | ||
} | ||
|
||
@Override | ||
public int findTargetSnapPosition(LayoutManager layoutManager, int velocityX, int velocityY) { | ||
if (!disableFling) { | ||
return RecyclerView.NO_POSITION; | ||
} | ||
|
||
final int itemCount = layoutManager.getItemCount(); | ||
if (itemCount == 0) { | ||
return RecyclerView.NO_POSITION; | ||
} | ||
|
||
// A child that is exactly centered on the first focal keyline is eligible | ||
// for both before and after | ||
View closestChildBeforeKeyline = null; | ||
int distanceBefore = Integer.MIN_VALUE; | ||
View closestChildAfterKeyline = null; | ||
int distanceAfter = Integer.MAX_VALUE; | ||
|
||
// Find the first view before the first focal keyline, and the first view after it | ||
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 = | ||
distanceToFirstFocalKeyline(child, (CarouselLayoutManager) layoutManager); | ||
|
||
if (distance <= 0 && distance > distanceBefore) { | ||
// Child is before the keyline and closer then the previous best | ||
distanceBefore = distance; | ||
closestChildBeforeKeyline = child; | ||
} | ||
if (distance >= 0 && distance < distanceAfter) { | ||
// Child is after the keyline and closer then the previous best | ||
distanceAfter = distance; | ||
closestChildAfterKeyline = child; | ||
} | ||
} | ||
|
||
// Return the position of the closest child from the first focal keyline, in the direction of | ||
// the fling | ||
final boolean forwardDirection = isForwardFling(layoutManager, velocityX, velocityY); | ||
if (forwardDirection && closestChildAfterKeyline != null) { | ||
return layoutManager.getPosition(closestChildAfterKeyline); | ||
} else if (!forwardDirection && closestChildBeforeKeyline != null) { | ||
return layoutManager.getPosition(closestChildBeforeKeyline); | ||
} | ||
|
||
// There is no child in the direction of the fling (eg. start/end of list). | ||
// Extrapolate from the child that is visible to get the position of the view to | ||
// snap to. | ||
View visibleView = forwardDirection ? closestChildBeforeKeyline : closestChildAfterKeyline; | ||
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; | ||
} | ||
} | ||
|
||
// Calculates the direction of the layout based on the direction of the scroll vector when | ||
// scrolling to the end of the list. This is not equivalent to `isRtl` because the recyclerview | ||
// layout manager may set `reverseLayout`. | ||
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.