• 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 static android.view.flags.Flags.scrollCaptureRelaxScrollViewCriteria;
20 
21 import android.annotation.Nullable;
22 import android.content.Context;
23 import android.content.res.Resources;
24 import android.graphics.Point;
25 import android.graphics.Rect;
26 import android.util.Log;
27 import android.view.ScrollCaptureCallback;
28 import android.view.View;
29 import android.view.ViewGroup;
30 import android.webkit.WebView;
31 import android.widget.ListView;
32 
33 /**
34  * Provides built-in framework level Scroll Capture support for standard scrolling Views.
35  */
36 public class ScrollCaptureInternal {
37     private static final String TAG = "ScrollCaptureInternal";
38 
39     // Log found scrolling views
40     private static final boolean DEBUG = false;
41 
42     // Log all investigated views, as well as heuristic checks
43     private static final boolean DEBUG_VERBOSE = false;
44 
45     private static final int UP = -1;
46     private static final int DOWN = 1;
47 
48     /**
49      * Cannot scroll according to {@link View#canScrollVertically}.
50      */
51     public static final int TYPE_FIXED = 0;
52 
53     /**
54      * Moves the viewport across absolute positioned child views using the scrollY property.
55      */
56     public static final int TYPE_SCROLLING = 1;
57 
58     /**
59      * Slides child views through the viewport by translating their layout positions with {@link
60      * View#offsetTopAndBottom(int)}. Manages Child view lifecycle, creating as needed and
61      * binding views to data from an adapter. Views are reused whenever possible.
62      */
63     public static final int TYPE_RECYCLING = 2;
64 
65     /**
66      * Unknown scrollable view with no child views (or not a subclass of ViewGroup).
67      */
68     public static final int TYPE_OPAQUE = 3;
69 
70     /**
71      * Performs tests on the given View and determines:
72      * 1. If scrolling is possible
73      * 2. What mechanisms are used for scrolling.
74      * <p>
75      * This needs to be fast and not alloc memory. It's called on everything in the tree not marked
76      * as excluded during scroll capture search.
77      */
detectScrollingType(View view)78     public static int detectScrollingType(View view) {
79         // Confirm that it can scroll.
80         if (!(view.canScrollVertically(DOWN) || view.canScrollVertically(UP))) {
81             // Nothing to scroll here, move along.
82             if (DEBUG_VERBOSE) {
83                 Log.v(TAG, "hint: cannot be scrolled");
84             }
85             return TYPE_FIXED;
86         }
87         if (DEBUG_VERBOSE) {
88             Log.v(TAG, "hint: can be scrolled up or down");
89         }
90         // Must be a ViewGroup
91         if (!(view instanceof ViewGroup)) {
92             if (DEBUG_VERBOSE) {
93                 Log.v(TAG, "hint: not a subclass of ViewGroup");
94             }
95             return TYPE_OPAQUE;
96         }
97         if (DEBUG_VERBOSE) {
98             Log.v(TAG, "hint: is a subclass of ViewGroup");
99         }
100         // Flag: Optionally allow ScrollView-like ViewGroups which have more than one child view.
101         if (!scrollCaptureRelaxScrollViewCriteria()) {
102             // ScrollViews accept only a single child.
103             if (((ViewGroup) view).getChildCount() > 1) {
104                 if (DEBUG_VERBOSE) {
105                     Log.v(TAG, "hint: scrollable with multiple children");
106                 }
107                 return TYPE_RECYCLING;
108             }
109         }
110         // At least one child view is required.
111         if (((ViewGroup) view).getChildCount() == 0) {
112             Log.w(TAG, "scrollable but no children!");
113             return TYPE_OPAQUE;
114         }
115         if (DEBUG_VERBOSE) {
116             Log.v(TAG, "hint: single child view");
117         }
118         // Because recycling containers don't use scrollY, a non-zero value means Scroll view.
119         if (view.getScrollY() != 0) {
120             if (DEBUG_VERBOSE) {
121                 Log.v(TAG, "hint: scrollY != 0");
122             }
123             return TYPE_SCROLLING;
124         }
125         Log.v(TAG, "hint: scrollY == 0");
126         // Since scrollY cannot be negative, this means a Recycling view.
127         if (view.canScrollVertically(UP)) {
128             if (DEBUG_VERBOSE) {
129                 Log.v(TAG, "hint: able to scroll up");
130             }
131             return TYPE_RECYCLING;
132         }
133         if (DEBUG_VERBOSE) {
134             Log.v(TAG, "hint: cannot be scrolled up");
135         }
136 
137         // canScrollVertically(UP) == false, getScrollY() == 0, getChildCount() >= 1.
138         // For Recycling containers, this should be a no-op (RecyclerView logs a warning)
139         view.scrollTo(view.getScrollX(), 1);
140 
141         // A scrolling container would have moved by 1px.
142         if (view.getScrollY() == 1) {
143             view.scrollTo(view.getScrollX(), 0);
144             if (DEBUG_VERBOSE) {
145                 Log.v(TAG, "hint: scrollTo caused scrollY to change");
146             }
147             return TYPE_SCROLLING;
148         }
149         if (DEBUG_VERBOSE) {
150             Log.v(TAG, "hint: scrollTo did not cause scrollY to change");
151         }
152         return TYPE_RECYCLING;
153     }
154 
155     /**
156      * Creates a scroll capture callback for the given view if possible.
157      *
158      * @param view             the view to capture
159      * @param localVisibleRect the visible area of the given view in local coordinates, as supplied
160      *                         by the view parent
161      * @param positionInWindow the offset of localVisibleRect within the window
162      * @return a new callback or null if the View isn't supported
163      */
164     @Nullable
requestCallback(View view, Rect localVisibleRect, Point positionInWindow)165     public ScrollCaptureCallback requestCallback(View view, Rect localVisibleRect,
166             Point positionInWindow) {
167         // Nothing to see here yet.
168         if (DEBUG_VERBOSE) {
169             Log.v(TAG, "scroll capture: checking " + view.getClass().getName()
170                     + "[" + resolveId(view.getContext(), view.getId()) + "]");
171         }
172         int i = detectScrollingType(view);
173         switch (i) {
174             case TYPE_SCROLLING:
175                 if (DEBUG) {
176                     Log.d(TAG, "scroll capture: FOUND " + view.getClass().getName()
177                             + "[" + resolveId(view.getContext(), view.getId()) + "]"
178                             + " -> TYPE_SCROLLING");
179                 }
180                 return new ScrollCaptureViewSupport<>((ViewGroup) view,
181                         new ScrollViewCaptureHelper());
182             case TYPE_RECYCLING:
183                 if (DEBUG) {
184                     Log.d(TAG, "scroll capture: FOUND " + view.getClass().getName()
185                             + "[" + resolveId(view.getContext(), view.getId()) + "]"
186                             + " -> TYPE_RECYCLING");
187                 }
188                 if (view instanceof ListView) {
189                     // ListView is special.
190                     return new ScrollCaptureViewSupport<>((ListView) view,
191                             new ListViewCaptureHelper());
192                 }
193                 return new ScrollCaptureViewSupport<>((ViewGroup) view,
194                         new RecyclerViewCaptureHelper());
195             case TYPE_OPAQUE:
196                 if (DEBUG) {
197                     Log.d(TAG, "scroll capture: FOUND " + view.getClass().getName()
198                             + "[" + resolveId(view.getContext(), view.getId()) + "]"
199                             + " -> TYPE_OPAQUE");
200                 }
201                 if (view instanceof WebView) {
202                     Log.d(TAG, "scroll capture: Using WebView support");
203                     return new ScrollCaptureViewSupport<>((WebView) view,
204                             new WebViewCaptureHelper());
205                 }
206                 break;
207             case TYPE_FIXED:
208                 // ignore
209                 break;
210 
211         }
212         return null;
213     }
214 
215     // Lifted from ViewDebug (package protected)
216 
formatIntToHexString(int value)217     private static String formatIntToHexString(int value) {
218         return "0x" + Integer.toHexString(value).toUpperCase();
219     }
220 
resolveId(Context context, int id)221     static String resolveId(Context context, int id) {
222         String fieldValue;
223         final Resources resources = context.getResources();
224         if (id >= 0) {
225             try {
226                 fieldValue = resources.getResourceTypeName(id) + '/'
227                         + resources.getResourceEntryName(id);
228             } catch (Resources.NotFoundException e) {
229                 fieldValue = "id/" + formatIntToHexString(id);
230             }
231         } else {
232             fieldValue = "NO_ID";
233         }
234         return fieldValue;
235     }
236 }
237