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