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.os.CancellationSignal; 22 import android.util.Log; 23 import android.view.View; 24 import android.view.ViewGroup; 25 import android.view.ViewParent; 26 27 import java.util.function.Consumer; 28 29 /** 30 * ScrollCapture for RecyclerView and <i>RecyclerView-like</i> ViewGroups. 31 * <p> 32 * Requirements for proper operation: 33 * <ul> 34 * <li>at least one visible child view</li> 35 * <li>scrolls by pixels in response to {@link View#scrollBy(int, int)}. 36 * <li>reports ability to scroll with {@link View#canScrollVertically(int)} 37 * <li>properly implements {@link ViewParent#requestChildRectangleOnScreen(View, Rect, boolean)} 38 * </ul> 39 * 40 * @see ScrollCaptureViewSupport 41 */ 42 public class RecyclerViewCaptureHelper implements ScrollCaptureViewHelper<ViewGroup> { 43 private static final String TAG = "RVCaptureHelper"; 44 45 private int mScrollDelta; 46 private boolean mScrollBarWasEnabled; 47 private int mOverScrollMode; 48 49 @Override onAcceptSession(@onNull ViewGroup view)50 public boolean onAcceptSession(@NonNull ViewGroup view) { 51 return view.isVisibleToUser() 52 && (view.canScrollVertically(UP) || view.canScrollVertically(DOWN)); 53 } 54 55 @Override onPrepareForStart(@onNull ViewGroup view, Rect scrollBounds)56 public void onPrepareForStart(@NonNull ViewGroup view, Rect scrollBounds) { 57 mScrollDelta = 0; 58 59 mOverScrollMode = view.getOverScrollMode(); 60 view.setOverScrollMode(View.OVER_SCROLL_NEVER); 61 62 mScrollBarWasEnabled = view.isVerticalScrollBarEnabled(); 63 view.setVerticalScrollBarEnabled(false); 64 } 65 66 @Override onScrollRequested(@onNull ViewGroup recyclerView, Rect scrollBounds, Rect requestRect, CancellationSignal signal, Consumer<ScrollResult> resultConsumer)67 public void onScrollRequested(@NonNull ViewGroup recyclerView, Rect scrollBounds, 68 Rect requestRect, CancellationSignal signal, Consumer<ScrollResult> resultConsumer) { 69 ScrollResult result = new ScrollResult(); 70 result.requestedArea = new Rect(requestRect); 71 result.scrollDelta = mScrollDelta; 72 result.availableArea = new Rect(); // empty 73 74 if (!recyclerView.isVisibleToUser() || recyclerView.getChildCount() == 0) { 75 Log.w(TAG, "recyclerView is empty or not visible, cannot continue"); 76 resultConsumer.accept(result); // result.availableArea == empty Rect 77 return; 78 } 79 80 // move from scrollBounds-relative to parent-local coordinates 81 Rect requestedContainerBounds = new Rect(requestRect); 82 requestedContainerBounds.offset(0, -mScrollDelta); 83 requestedContainerBounds.offset(scrollBounds.left, scrollBounds.top); 84 // requestedContainerBounds is now in recyclerview-local coordinates 85 86 // Save a copy for later 87 View anchor = findChildNearestTarget(recyclerView, requestedContainerBounds); 88 if (anchor == null) { 89 Log.w(TAG, "Failed to locate anchor view"); 90 resultConsumer.accept(result); // result.availableArea == empty rect 91 return; 92 } 93 94 Rect requestedContentBounds = new Rect(requestedContainerBounds); 95 recyclerView.offsetRectIntoDescendantCoords(anchor, requestedContentBounds); 96 97 int prevAnchorTop = anchor.getTop(); 98 // Note: requestChildRectangleOnScreen may modify rectangle, must pass pass in a copy here 99 Rect input = new Rect(requestedContentBounds); 100 // Expand input rect to get the requested rect to be in the center 101 int remainingHeight = recyclerView.getHeight() - recyclerView.getPaddingTop() 102 - recyclerView.getPaddingBottom() - input.height(); 103 if (remainingHeight > 0) { 104 input.inset(0, -remainingHeight / 2); 105 } 106 107 if (recyclerView.requestChildRectangleOnScreen(anchor, input, true)) { 108 int scrolled = prevAnchorTop - anchor.getTop(); // inverse of movement 109 mScrollDelta += scrolled; // view.top-- is equivalent to parent.scrollY++ 110 result.scrollDelta = mScrollDelta; 111 } 112 113 requestedContainerBounds.set(requestedContentBounds); 114 recyclerView.offsetDescendantRectToMyCoords(anchor, requestedContainerBounds); 115 116 Rect recyclerLocalVisible = new Rect(scrollBounds); 117 recyclerView.getLocalVisibleRect(recyclerLocalVisible); 118 119 if (!requestedContainerBounds.intersect(recyclerLocalVisible)) { 120 // Requested area is still not visible 121 resultConsumer.accept(result); 122 return; 123 } 124 Rect available = new Rect(requestedContainerBounds); 125 available.offset(-scrollBounds.left, -scrollBounds.top); 126 available.offset(0, mScrollDelta); 127 result.availableArea = available; 128 resultConsumer.accept(result); 129 } 130 131 /** 132 * Find a view that is located "closest" to targetRect. Returns the first view to fully 133 * vertically overlap the target targetRect. If none found, returns the view with an edge 134 * nearest the target targetRect. 135 * 136 * @param parent the parent vertical layout 137 * @param targetRect a rectangle in local coordinates of <code>parent</code> 138 * @return a child view within parent matching the criteria or null 139 */ findChildNearestTarget(ViewGroup parent, Rect targetRect)140 static View findChildNearestTarget(ViewGroup parent, Rect targetRect) { 141 View selected = null; 142 int minCenterDistance = Integer.MAX_VALUE; 143 int maxOverlap = 0; 144 145 // allowable center-center distance, relative to targetRect. 146 // if within this range, taller views are preferred 147 final float preferredRangeFromCenterPercent = 0.25f; 148 final int preferredDistance = 149 (int) (preferredRangeFromCenterPercent * targetRect.height()); 150 151 Rect parentLocalVis = new Rect(); 152 parent.getLocalVisibleRect(parentLocalVis); 153 154 Rect frame = new Rect(); 155 for (int i = 0; i < parent.getChildCount(); i++) { 156 final View child = parent.getChildAt(i); 157 child.getHitRect(frame); 158 159 if (child.getVisibility() != View.VISIBLE) { 160 continue; 161 } 162 163 int centerDistance = Math.abs(targetRect.centerY() - frame.centerY()); 164 165 if (centerDistance < minCenterDistance) { 166 // closer to center 167 minCenterDistance = centerDistance; 168 selected = child; 169 } else if (frame.intersect(targetRect) && (frame.height() > preferredDistance)) { 170 // within X% pixels of center, but taller 171 selected = child; 172 } 173 } 174 return selected; 175 } 176 177 @Override onPrepareForEnd(@onNull ViewGroup view)178 public void onPrepareForEnd(@NonNull ViewGroup view) { 179 // Restore original position and state 180 view.scrollBy(0, -mScrollDelta); 181 view.setOverScrollMode(mOverScrollMode); 182 view.setVerticalScrollBarEnabled(mScrollBarWasEnabled); 183 } 184 } 185