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.Point;
21 import android.graphics.Rect;
22 import android.os.CancellationSignal;
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 ScrollView and <i>ScrollView-like</i> ViewGroups.
31  * <p>
32  * Requirements for proper operation:
33  * <ul>
34  * <li>contains at most 1 child.
35  * <li>scrolls to absolute positions with {@link View#scrollTo(int, int)}.
36  * <li>has a finite, known content height and scrolling range
37  * <li>correctly implements {@link View#canScrollVertically(int)}
38  * <li>correctly implements {@link ViewParent#requestChildRectangleOnScreen(View,
39  * Rect, boolean)}
40  * </ul>
41  *
42  * @see ScrollCaptureViewSupport
43  */
44 public class ScrollViewCaptureHelper implements ScrollCaptureViewHelper<ViewGroup> {
45     private int mStartScrollY;
46     private boolean mScrollBarEnabled;
47     private int mOverScrollMode;
48 
onAcceptSession(@onNull ViewGroup view)49     public boolean onAcceptSession(@NonNull ViewGroup view) {
50         return view.isVisibleToUser()
51                 && (view.canScrollVertically(UP) || view.canScrollVertically(DOWN));
52     }
53 
onPrepareForStart(@onNull ViewGroup view, Rect scrollBounds)54     public void onPrepareForStart(@NonNull ViewGroup view, Rect scrollBounds) {
55         mStartScrollY = view.getScrollY();
56         mOverScrollMode = view.getOverScrollMode();
57         if (mOverScrollMode != View.OVER_SCROLL_NEVER) {
58             view.setOverScrollMode(View.OVER_SCROLL_NEVER);
59         }
60         mScrollBarEnabled = view.isVerticalScrollBarEnabled();
61         if (mScrollBarEnabled) {
62             view.setVerticalScrollBarEnabled(false);
63         }
64     }
65 
onScrollRequested(@onNull ViewGroup view, Rect scrollBounds, Rect requestRect, CancellationSignal signal, Consumer<ScrollResult> resultConsumer)66     public void onScrollRequested(@NonNull ViewGroup view, Rect scrollBounds,
67             Rect requestRect, CancellationSignal signal, Consumer<ScrollResult> resultConsumer) {
68         /*
69                +---------+ <----+ Content [25,25 - 275,1025] (w=250,h=1000)
70                |         |
71             ...|.........|...  startScrollY=100
72                |         |
73             +--+---------+---+ <--+ Container View [0,0 - 300,300] (scrollY=200)
74             |  .         .   |
75         --- |  . +-----+   <------+ Scroll Bounds [50,50 - 250,250] (200x200)
76          ^  |  . |     | .   |      (Local to Container View, fixed/un-scrolled)
77          |  |  . |     | .   |
78          |  |  . |     | .   |
79          |  |  . +-----+ .   |
80          |  |  .         .   |
81          |  +--+---------+---+
82          |     |         |
83         -+-    | +-----+ |
84                | |#####| |   <--+ Requested Bounds [0,300 - 200,400] (200x100)
85                | +-----+ |       (Local to Scroll Bounds, fixed/un-scrolled)
86                |         |
87                +---------+
88 
89         Container View (ScrollView) [0,0 - 300,300] (scrollY = 200)
90         \__ Content [25,25 - 275,1025]  (250x1000) (contentView)
91         \__ Scroll Bounds[50,50 - 250,250]  (w=200,h=200)
92             \__ Requested Bounds[0,300 - 200,400] (200x100)
93        */
94 
95         // 0) adjust the requestRect to account for scroll change since start
96         //
97         //  Scroll Bounds[50,50 - 250,250]  (w=200,h=200)
98         //  \__ Requested Bounds[0,200 - 200,300] (200x100)
99 
100         // (y-100) (scrollY - mStartScrollY)
101         int scrollDelta = view.getScrollY() - mStartScrollY;
102 
103         final ScrollResult result = new ScrollResult();
104         result.requestedArea = new Rect(requestRect);
105         result.scrollDelta = scrollDelta;
106         result.availableArea = new Rect();
107 
108         final View contentView = view.getChildAt(0); // returns null, does not throw IOOBE
109         if (contentView == null) {
110             // No child view? Cannot continue.
111             resultConsumer.accept(result);
112             return;
113         }
114 
115         //  1) Translate request rect to make it relative to container view
116         //
117         //  Container View [0,0 - 300,300] (scrollY=200)
118         //  \__ Requested Bounds[50,250 - 250,350] (w=250, h=100)
119 
120         // (x+50,y+50)
121         Rect requestedContainerBounds = new Rect(requestRect);
122         requestedContainerBounds.offset(0, -scrollDelta);
123         requestedContainerBounds.offset(scrollBounds.left, scrollBounds.top);
124 
125         //  2) Translate from container to contentView relative (applying container scrollY)
126         //
127         //  Container View [0,0 - 300,300] (scrollY=200)
128         //  \__ Content [25,25 - 275,1025]  (250x1000) (contentView)
129         //      \__ Requested Bounds[25,425 - 200,525] (w=250, h=100)
130 
131         // (x-25,y+175) (scrollY - content.top)
132         Rect requestedContentBounds = new Rect(requestedContainerBounds);
133         requestedContentBounds.offset(
134                 view.getScrollX() - contentView.getLeft(),
135                 view.getScrollY() - contentView.getTop());
136 
137         Rect input = new Rect(requestedContentBounds);
138 
139         // Expand input rect to get the requested rect to be in the center
140         int remainingHeight = view.getHeight() - view.getPaddingTop()
141                 - view.getPaddingBottom() - input.height();
142         if (remainingHeight > 0) {
143             input.inset(0, -remainingHeight / 2);
144         }
145 
146         // requestRect is now local to contentView as requestedContentBounds
147         // contentView (and each parent in turn if possible) will be scrolled
148         // (if necessary) to make all of requestedContent visible, (if possible!)
149         contentView.requestRectangleOnScreen(input, true);
150 
151         // update new offset between starting and current scroll position
152         scrollDelta = view.getScrollY() - mStartScrollY;
153         result.scrollDelta = scrollDelta;
154 
155         // TODO: crop capture area to avoid occlusions/minimize scroll changes
156 
157         Point offset = new Point();
158         final Rect available = new Rect(requestedContentBounds);
159         if (!view.getChildVisibleRect(contentView, available, offset)) {
160             available.setEmpty();
161             result.availableArea = available;
162             resultConsumer.accept(result);
163             return;
164         }
165         // Transform back from global to content-view local
166         available.offset(-offset.x, -offset.y);
167 
168         // Then back to container view
169         available.offset(
170                 contentView.getLeft() - view.getScrollX(),
171                 contentView.getTop() - view.getScrollY());
172 
173 
174         // And back to relative to scrollBounds
175         available.offset(-scrollBounds.left, -scrollBounds.top);
176 
177         // Apply scrollDelta again to return to make `available` relative to `scrollBounds` at
178         // the scroll position at start of capture.
179         available.offset(0, scrollDelta);
180 
181         result.availableArea = new Rect(available);
182         resultConsumer.accept(result);
183     }
184 
onPrepareForEnd(@onNull ViewGroup view)185     public void onPrepareForEnd(@NonNull ViewGroup view) {
186         view.scrollTo(0, mStartScrollY);
187         if (mOverScrollMode != View.OVER_SCROLL_NEVER) {
188             view.setOverScrollMode(mOverScrollMode);
189         }
190         if (mScrollBarEnabled) {
191             view.setVerticalScrollBarEnabled(true);
192         }
193     }
194 }
195