blob: 72b5488f4bac411bb5425dcc792ea256eff5b8d4 [file] [log] [blame]
/*
* 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;
}
}