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