1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.internal.view; 18 19 import android.annotation.NonNull; 20 import android.graphics.Rect; 21 import android.util.Log; 22 import android.view.View; 23 import android.view.ViewGroup; 24 import android.view.ViewParent; 25 26 /** 27 * ScrollCapture for RecyclerView and <i>RecyclerView-like</i> ViewGroups. 28 * <p> 29 * Requirements for proper operation: 30 * <ul> 31 * <li>at least one visible child view</li> 32 * <li>scrolls by pixels in response to {@link View#scrollBy(int, int)}. 33 * <li>reports ability to scroll with {@link View#canScrollVertically(int)} 34 * <li>properly implements {@link ViewParent#requestChildRectangleOnScreen(View, Rect, boolean)} 35 * </ul> 36 * 37 * @see ScrollCaptureViewSupport 38 */ 39 public class RecyclerViewCaptureHelper implements ScrollCaptureViewHelper<ViewGroup> { 40 private static final String TAG = "RVCaptureHelper"; 41 42 private int mScrollDelta; 43 private boolean mScrollBarWasEnabled; 44 private int mOverScrollMode; 45 46 @Override onPrepareForStart(@onNull ViewGroup view, Rect scrollBounds)47 public void onPrepareForStart(@NonNull ViewGroup view, Rect scrollBounds) { 48 mScrollDelta = 0; 49 50 mOverScrollMode = view.getOverScrollMode(); 51 view.setOverScrollMode(View.OVER_SCROLL_NEVER); 52 53 mScrollBarWasEnabled = view.isVerticalScrollBarEnabled(); 54 view.setVerticalScrollBarEnabled(false); 55 } 56 57 @Override onScrollRequested(@onNull ViewGroup recyclerView, Rect scrollBounds, Rect requestRect)58 public ScrollResult onScrollRequested(@NonNull ViewGroup recyclerView, Rect scrollBounds, 59 Rect requestRect) { 60 ScrollResult result = new ScrollResult(); 61 result.requestedArea = new Rect(requestRect); 62 result.scrollDelta = mScrollDelta; 63 result.availableArea = new Rect(); // empty 64 65 if (!recyclerView.isVisibleToUser() || recyclerView.getChildCount() == 0) { 66 Log.w(TAG, "recyclerView is empty or not visible, cannot continue"); 67 return result; // result.availableArea == empty Rect 68 } 69 70 // move from scrollBounds-relative to parent-local coordinates 71 Rect requestedContainerBounds = new Rect(requestRect); 72 requestedContainerBounds.offset(0, -mScrollDelta); 73 requestedContainerBounds.offset(scrollBounds.left, scrollBounds.top); 74 // requestedContainerBounds is now in recyclerview-local coordinates 75 76 // Save a copy for later 77 View anchor = findChildNearestTarget(recyclerView, requestedContainerBounds); 78 if (anchor == null) { 79 Log.w(TAG, "Failed to locate anchor view"); 80 return result; // result.availableArea == empty rect 81 } 82 83 Rect requestedContentBounds = new Rect(requestedContainerBounds); 84 recyclerView.offsetRectIntoDescendantCoords(anchor, requestedContentBounds); 85 86 int prevAnchorTop = anchor.getTop(); 87 // Note: requestChildRectangleOnScreen may modify rectangle, must pass pass in a copy here 88 Rect input = new Rect(requestedContentBounds); 89 // Expand input rect to get the requested rect to be in the center 90 int remainingHeight = recyclerView.getHeight() - recyclerView.getPaddingTop() 91 - recyclerView.getPaddingBottom() - input.height(); 92 if (remainingHeight > 0) { 93 input.inset(0, -remainingHeight / 2); 94 } 95 96 if (recyclerView.requestChildRectangleOnScreen(anchor, input, true)) { 97 int scrolled = prevAnchorTop - anchor.getTop(); // inverse of movement 98 mScrollDelta += scrolled; // view.top-- is equivalent to parent.scrollY++ 99 result.scrollDelta = mScrollDelta; 100 } 101 102 requestedContainerBounds.set(requestedContentBounds); 103 recyclerView.offsetDescendantRectToMyCoords(anchor, requestedContainerBounds); 104 105 Rect recyclerLocalVisible = new Rect(scrollBounds); 106 recyclerView.getLocalVisibleRect(recyclerLocalVisible); 107 108 if (!requestedContainerBounds.intersect(recyclerLocalVisible)) { 109 // Requested area is still not visible 110 return result; 111 } 112 Rect available = new Rect(requestedContainerBounds); 113 available.offset(-scrollBounds.left, -scrollBounds.top); 114 available.offset(0, mScrollDelta); 115 result.availableArea = available; 116 return result; 117 } 118 119 /** 120 * Find a view that is located "closest" to targetRect. Returns the first view to fully 121 * vertically overlap the target targetRect. If none found, returns the view with an edge 122 * nearest the target targetRect. 123 * 124 * @param parent the parent vertical layout 125 * @param targetRect a rectangle in local coordinates of <code>parent</code> 126 * @return a child view within parent matching the criteria or null 127 */ findChildNearestTarget(ViewGroup parent, Rect targetRect)128 static View findChildNearestTarget(ViewGroup parent, Rect targetRect) { 129 View selected = null; 130 int minCenterDistance = Integer.MAX_VALUE; 131 int maxOverlap = 0; 132 133 // allowable center-center distance, relative to targetRect. 134 // if within this range, taller views are preferred 135 final float preferredRangeFromCenterPercent = 0.25f; 136 final int preferredDistance = 137 (int) (preferredRangeFromCenterPercent * targetRect.height()); 138 139 Rect parentLocalVis = new Rect(); 140 parent.getLocalVisibleRect(parentLocalVis); 141 142 Rect frame = new Rect(); 143 for (int i = 0; i < parent.getChildCount(); i++) { 144 final View child = parent.getChildAt(i); 145 child.getHitRect(frame); 146 147 if (child.getVisibility() != View.VISIBLE) { 148 continue; 149 } 150 151 int centerDistance = Math.abs(targetRect.centerY() - frame.centerY()); 152 153 if (centerDistance < minCenterDistance) { 154 // closer to center 155 minCenterDistance = centerDistance; 156 selected = child; 157 } else if (frame.intersect(targetRect) && (frame.height() > preferredDistance)) { 158 // within X% pixels of center, but taller 159 selected = child; 160 } 161 } 162 return selected; 163 } 164 165 @Override onPrepareForEnd(@onNull ViewGroup view)166 public void onPrepareForEnd(@NonNull ViewGroup view) { 167 // Restore original position and state 168 view.scrollBy(0, -mScrollDelta); 169 view.setOverScrollMode(mOverScrollMode); 170 view.setVerticalScrollBarEnabled(mScrollBarWasEnabled); 171 } 172 } 173