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