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