mirror of
https://github.com/material-components/material-components-android.git
synced 2026-01-16 18:01:42 +08:00
262 lines
9.6 KiB
Java
262 lines
9.6 KiB
Java
/*
|
|
* 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 static java.lang.Math.max;
|
|
|
|
import android.graphics.PointF;
|
|
import androidx.recyclerview.widget.LinearSmoothScroller;
|
|
import androidx.recyclerview.widget.RecyclerView;
|
|
import androidx.recyclerview.widget.RecyclerView.LayoutManager;
|
|
import androidx.recyclerview.widget.RecyclerView.SmoothScroller;
|
|
import androidx.recyclerview.widget.SnapHelper;
|
|
import android.util.DisplayMetrics;
|
|
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 static final float HORIZONTAL_SNAP_SPEED = 100F;
|
|
|
|
private static final float VERTICAL_SNAP_SPEED = 50F;
|
|
|
|
private final boolean disableFling;
|
|
private RecyclerView recyclerView;
|
|
|
|
public CarouselSnapHelper() {
|
|
this(true);
|
|
}
|
|
|
|
public CarouselSnapHelper(boolean disableFling) {
|
|
this.disableFling = disableFling;
|
|
}
|
|
|
|
@Override
|
|
public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {
|
|
super.attachToRecyclerView(recyclerView);
|
|
this.recyclerView = recyclerView;
|
|
}
|
|
|
|
@Nullable
|
|
@Override
|
|
public int[] calculateDistanceToFinalSnap(
|
|
@NonNull LayoutManager layoutManager, @NonNull View view) {
|
|
return calculateDistanceToSnap(layoutManager, view, false);
|
|
}
|
|
|
|
private int[] calculateDistanceToSnap(
|
|
@NonNull LayoutManager layoutManager, @NonNull View view, boolean partialSnap) {
|
|
// 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 =
|
|
distanceToFirstFocalKeyline(view, (CarouselLayoutManager) layoutManager, partialSnap);
|
|
if (layoutManager.canScrollHorizontally()) {
|
|
return new int[] {offset, 0};
|
|
}
|
|
|
|
if (layoutManager.canScrollVertically()) {
|
|
return new int[] {0, offset};
|
|
}
|
|
return new int[] {0, 0};
|
|
}
|
|
|
|
private int distanceToFirstFocalKeyline(
|
|
@NonNull View targetView, CarouselLayoutManager layoutManager, boolean partialSnap) {
|
|
return layoutManager.getOffsetToScrollToPositionForSnap(
|
|
layoutManager.getPosition(targetView), partialSnap);
|
|
}
|
|
|
|
@Nullable
|
|
@Override
|
|
public View findSnapView(LayoutManager layoutManager) {
|
|
return findViewNearestFirstKeyline(layoutManager);
|
|
}
|
|
|
|
/**
|
|
* 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.getOffsetToScrollToPositionForSnap(position, false));
|
|
|
|
// 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, false);
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*
|
|
* <p>This is mostly a copy of {@code SnapHelper#createSnapScroller} with a slight adjustment to
|
|
* call {@link CarouselSnapHelper#calculateDistanceToSnap(LayoutManager, View, boolean)}
|
|
* (LayoutManager, View)}. We want to do a partial snap since the correct target keyline state may
|
|
* not have updated yet since this gets called before the keylines shift.
|
|
*/
|
|
@Nullable
|
|
@Override
|
|
protected SmoothScroller createScroller(@NonNull LayoutManager layoutManager) {
|
|
return layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider
|
|
? new LinearSmoothScroller(recyclerView.getContext()) {
|
|
@Override
|
|
protected void onTargetFound(
|
|
View targetView,
|
|
RecyclerView.State state,
|
|
RecyclerView.SmoothScroller.Action action) {
|
|
if (recyclerView != null) {
|
|
int[] snapDistances =
|
|
calculateDistanceToSnap(recyclerView.getLayoutManager(), targetView, true);
|
|
int dx = snapDistances[0];
|
|
int dy = snapDistances[1];
|
|
int time = this.calculateTimeForDeceleration(max(Math.abs(dx), Math.abs(dy)));
|
|
if (time > 0) {
|
|
action.update(dx, dy, time, this.mDecelerateInterpolator);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
|
|
// If the carousel orientation is vertical, we want the scroll speed to be faster.
|
|
if (layoutManager.canScrollVertically()) {
|
|
return VERTICAL_SNAP_SPEED / (float) displayMetrics.densityDpi;
|
|
}
|
|
return HORIZONTAL_SNAP_SPEED / (float) displayMetrics.densityDpi;
|
|
}
|
|
}
|
|
: null;
|
|
}
|
|
}
|