blob: d14adf6f3c2040d697a052f3ef1baecf80f40bb2 [file] [log] [blame]
/*
* Copyright (C) 2020 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
*
* http://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.android.internal.view;
import android.annotation.NonNull;
import android.graphics.Rect;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
/**
* ScrollCapture for RecyclerView and <i>RecyclerView-like</i> ViewGroups.
* <p>
* Requirements for proper operation:
* <ul>
* <li>at least one visible child view</li>
* <li>scrolls by pixels in response to {@link View#scrollBy(int, int)}.
* <li>reports ability to scroll with {@link View#canScrollVertically(int)}
* <li>properly implements {@link ViewParent#requestChildRectangleOnScreen(View, Rect, boolean)}
* </ul>
*
* @see ScrollCaptureViewSupport
*/
public class RecyclerViewCaptureHelper implements ScrollCaptureViewHelper<ViewGroup> {
private static final String TAG = "RVCaptureHelper";
private int mScrollDelta;
private boolean mScrollBarWasEnabled;
private int mOverScrollMode;
@Override
public void onPrepareForStart(@NonNull ViewGroup view, Rect scrollBounds) {
mScrollDelta = 0;
mOverScrollMode = view.getOverScrollMode();
view.setOverScrollMode(View.OVER_SCROLL_NEVER);
mScrollBarWasEnabled = view.isVerticalScrollBarEnabled();
view.setVerticalScrollBarEnabled(false);
}
@Override
public ScrollResult onScrollRequested(@NonNull ViewGroup recyclerView, Rect scrollBounds,
Rect requestRect) {
ScrollResult result = new ScrollResult();
result.requestedArea = new Rect(requestRect);
result.scrollDelta = mScrollDelta;
result.availableArea = new Rect(); // empty
if (!recyclerView.isVisibleToUser() || recyclerView.getChildCount() == 0) {
Log.w(TAG, "recyclerView is empty or not visible, cannot continue");
return result; // result.availableArea == empty Rect
}
// move from scrollBounds-relative to parent-local coordinates
Rect requestedContainerBounds = new Rect(requestRect);
requestedContainerBounds.offset(0, -mScrollDelta);
requestedContainerBounds.offset(scrollBounds.left, scrollBounds.top);
// requestedContainerBounds is now in recyclerview-local coordinates
// Save a copy for later
View anchor = findChildNearestTarget(recyclerView, requestedContainerBounds);
if (anchor == null) {
Log.w(TAG, "Failed to locate anchor view");
return result; // result.availableArea == empty rect
}
Rect requestedContentBounds = new Rect(requestedContainerBounds);
recyclerView.offsetRectIntoDescendantCoords(anchor, requestedContentBounds);
int prevAnchorTop = anchor.getTop();
// Note: requestChildRectangleOnScreen may modify rectangle, must pass pass in a copy here
Rect input = new Rect(requestedContentBounds);
// Expand input rect to get the requested rect to be in the center
int remainingHeight = recyclerView.getHeight() - recyclerView.getPaddingTop()
- recyclerView.getPaddingBottom() - input.height();
if (remainingHeight > 0) {
input.inset(0, -remainingHeight / 2);
}
if (recyclerView.requestChildRectangleOnScreen(anchor, input, true)) {
int scrolled = prevAnchorTop - anchor.getTop(); // inverse of movement
mScrollDelta += scrolled; // view.top-- is equivalent to parent.scrollY++
result.scrollDelta = mScrollDelta;
}
requestedContainerBounds.set(requestedContentBounds);
recyclerView.offsetDescendantRectToMyCoords(anchor, requestedContainerBounds);
Rect recyclerLocalVisible = new Rect(scrollBounds);
recyclerView.getLocalVisibleRect(recyclerLocalVisible);
if (!requestedContainerBounds.intersect(recyclerLocalVisible)) {
// Requested area is still not visible
return result;
}
Rect available = new Rect(requestedContainerBounds);
available.offset(-scrollBounds.left, -scrollBounds.top);
available.offset(0, mScrollDelta);
result.availableArea = available;
return result;
}
/**
* Find a view that is located "closest" to targetRect. Returns the first view to fully
* vertically overlap the target targetRect. If none found, returns the view with an edge
* nearest the target targetRect.
*
* @param parent the parent vertical layout
* @param targetRect a rectangle in local coordinates of <code>parent</code>
* @return a child view within parent matching the criteria or null
*/
static View findChildNearestTarget(ViewGroup parent, Rect targetRect) {
View selected = null;
int minCenterDistance = Integer.MAX_VALUE;
int maxOverlap = 0;
// allowable center-center distance, relative to targetRect.
// if within this range, taller views are preferred
final float preferredRangeFromCenterPercent = 0.25f;
final int preferredDistance =
(int) (preferredRangeFromCenterPercent * targetRect.height());
Rect parentLocalVis = new Rect();
parent.getLocalVisibleRect(parentLocalVis);
Rect frame = new Rect();
for (int i = 0; i < parent.getChildCount(); i++) {
final View child = parent.getChildAt(i);
child.getHitRect(frame);
if (child.getVisibility() != View.VISIBLE) {
continue;
}
int centerDistance = Math.abs(targetRect.centerY() - frame.centerY());
if (centerDistance < minCenterDistance) {
// closer to center
minCenterDistance = centerDistance;
selected = child;
} else if (frame.intersect(targetRect) && (frame.height() > preferredDistance)) {
// within X% pixels of center, but taller
selected = child;
}
}
return selected;
}
@Override
public void onPrepareForEnd(@NonNull ViewGroup view) {
// Restore original position and state
view.scrollBy(0, -mScrollDelta);
view.setOverScrollMode(mOverScrollMode);
view.setVerticalScrollBarEnabled(mScrollBarWasEnabled);
}
}