• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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