| /* |
| * 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; |
| } |
| } |