Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit db211ddf authored by Mark Renouf's avatar Mark Renouf Committed by Automerger Merge Worker
Browse files

Merge "Adds long screenshot support for ListView" into sc-dev am: 82f0979e

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/15083160

Change-Id: I78cb8c1e7f50168e80a3e6cadd1e2c9a0d023b16
parents bc94ec1f 82f0979e
Loading
Loading
Loading
Loading
+125 −0
Original line number Diff line number Diff line
/*
 * 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 static com.android.internal.view.ScrollCaptureViewSupport.computeScrollAmount;
import static com.android.internal.view.ScrollCaptureViewSupport.findScrollingReferenceView;
import static com.android.internal.view.ScrollCaptureViewSupport.transformFromContainerToRequest;
import static com.android.internal.view.ScrollCaptureViewSupport.transformFromRequestToContainer;

import android.annotation.NonNull;
import android.graphics.Rect;
import android.util.Log;
import android.view.View;
import android.widget.ListView;

/**
 * Scroll capture support for ListView.
 *
 * @see ScrollCaptureViewSupport
 */
public class ListViewCaptureHelper implements ScrollCaptureViewHelper<ListView> {
    private static final String TAG = "LVCaptureHelper";
    private int mScrollDelta;
    private boolean mScrollBarWasEnabled;
    private int mOverScrollMode;

    @Override
    public void onPrepareForStart(@NonNull ListView view, Rect scrollBounds) {
        mScrollDelta = 0;

        mOverScrollMode = view.getOverScrollMode();
        view.setOverScrollMode(View.OVER_SCROLL_NEVER);

        mScrollBarWasEnabled = view.isVerticalScrollBarEnabled();
        view.setVerticalScrollBarEnabled(false);
    }

    @Override
    public ScrollResult onScrollRequested(@NonNull ListView listView, Rect scrollBounds,
            Rect requestRect) {
        Log.d(TAG, "-----------------------------------------------------------");
        Log.d(TAG, "onScrollRequested(scrollBounds=" + scrollBounds + ", "
                + "requestRect=" + requestRect + ")");

        ScrollResult result = new ScrollResult();
        result.requestedArea = new Rect(requestRect);
        result.scrollDelta = mScrollDelta;
        result.availableArea = new Rect(); // empty

        if (!listView.isVisibleToUser() || listView.getChildCount() == 0) {
            Log.w(TAG, "listView is empty or not visible, cannot continue");
            return result; // result.availableArea == empty Rect
        }

        // Make requestRect relative to RecyclerView (from scrollBounds)
        Rect requestedContainerBounds =
                transformFromRequestToContainer(mScrollDelta, scrollBounds, requestRect);

        Rect recyclerLocalVisible = new Rect();
        listView.getLocalVisibleRect(recyclerLocalVisible);

        // Expand request rect match visible bounds to center the requested rect vertically
        Rect adjustedContainerBounds = new Rect(requestedContainerBounds);
        int remainingHeight = recyclerLocalVisible.height() -  requestedContainerBounds.height();
        if (remainingHeight > 0) {
            adjustedContainerBounds.inset(0, -remainingHeight / 2);
        }

        int scrollAmount = computeScrollAmount(recyclerLocalVisible, adjustedContainerBounds);
        if (scrollAmount < 0) {
            Log.d(TAG, "About to scroll UP (content moves down within parent)");
        } else if (scrollAmount > 0) {
            Log.d(TAG, "About to scroll DOWN (content moves up within parent)");
        }
        Log.d(TAG, "scrollAmount: " + scrollAmount);

        View refView = findScrollingReferenceView(listView, scrollAmount);
        int refTop = refView.getTop();

        listView.scrollListBy(scrollAmount);
        int scrollDistance = refTop - refView.getTop();
        Log.d(TAG, "Parent view has scrolled vertically by " + scrollDistance + " px");

        mScrollDelta += scrollDistance;
        result.scrollDelta = mScrollDelta;
        if (scrollDistance != 0) {
            Log.d(TAG, "Scroll delta is now " + mScrollDelta + " px");
        }

        // Update, post-scroll
        requestedContainerBounds = new Rect(
                transformFromRequestToContainer(mScrollDelta, scrollBounds, requestRect));

        listView.getLocalVisibleRect(recyclerLocalVisible);
        if (requestedContainerBounds.intersect(recyclerLocalVisible)) {
            result.availableArea = transformFromContainerToRequest(
                    mScrollDelta, scrollBounds, requestedContainerBounds);
        }
        Log.d(TAG, "-----------------------------------------------------------");
        return result;
    }


    @Override
    public void onPrepareForEnd(@NonNull ListView listView) {
        // Restore original position and state
        listView.scrollListBy(-mScrollDelta);
        listView.setOverScrollMode(mOverScrollMode);
        listView.setVerticalScrollBarEnabled(mScrollBarWasEnabled);
    }
}
+52 −120
Original line number Diff line number Diff line
@@ -16,7 +16,11 @@

package com.android.internal.view;

import android.animation.ValueAnimator;
import static com.android.internal.view.ScrollCaptureViewSupport.computeScrollAmount;
import static com.android.internal.view.ScrollCaptureViewSupport.findScrollingReferenceView;
import static com.android.internal.view.ScrollCaptureViewSupport.transformFromContainerToRequest;
import static com.android.internal.view.ScrollCaptureViewSupport.transformFromRequestToContainer;

import android.annotation.NonNull;
import android.graphics.Rect;
import android.util.Log;
@@ -38,17 +42,10 @@ import android.view.ViewParent;
 * @see ScrollCaptureViewSupport
 */
public class RecyclerViewCaptureHelper implements ScrollCaptureViewHelper<ViewGroup> {

    // Experiment
    private static final boolean DISABLE_ANIMATORS = false;
    // Experiment
    private static final boolean STOP_RENDER_THREAD = false;

    private static final String TAG = "RVCaptureHelper";
    private int mScrollDelta;
    private boolean mScrollBarWasEnabled;
    private int mOverScrollMode;
    private float mDurationScale;

    @Override
    public void onPrepareForStart(@NonNull ViewGroup view, Rect scrollBounds) {
@@ -59,152 +56,87 @@ public class RecyclerViewCaptureHelper implements ScrollCaptureViewHelper<ViewGr

        mScrollBarWasEnabled = view.isVerticalScrollBarEnabled();
        view.setVerticalScrollBarEnabled(false);
        if (DISABLE_ANIMATORS) {
            mDurationScale = ValueAnimator.getDurationScale();
            ValueAnimator.setDurationScale(0);
        }
        if (STOP_RENDER_THREAD) {
            view.getThreadedRenderer().stop();
        }
    }

    @Override
    public ScrollResult onScrollRequested(@NonNull ViewGroup recyclerView, Rect scrollBounds,
            Rect requestRect) {
        Log.d(TAG, "-----------------------------------------------------------");
        Log.d(TAG, "onScrollRequested(scrollBounds=" + scrollBounds + ", "
                + "requestRect=" + requestRect + ")");

        ScrollResult result = new ScrollResult();
        result.requestedArea = new Rect(requestRect);
        result.scrollDelta = mScrollDelta;
        result.availableArea = new Rect(); // empty

        Log.d(TAG, "current scrollDelta: " + mScrollDelta);
        if (!recyclerView.isVisibleToUser() || recyclerView.getChildCount() == 0) {
            Log.w(TAG, "recyclerView is empty or not visible, cannot continue");
            return result; // result.availableArea == empty Rect
        }

        // move from scrollBounds-relative to parent-local coordinates
        Rect requestedContainerBounds = new Rect(requestRect);
        requestedContainerBounds.offset(0, -mScrollDelta);
        requestedContainerBounds.offset(scrollBounds.left, scrollBounds.top);
        // Make requestRect relative to RecyclerView (from scrollBounds)
        Rect requestedContainerBounds =
                transformFromRequestToContainer(mScrollDelta, scrollBounds, requestRect);

        // requestedContainerBounds is now in recyclerview-local coordinates
        Log.d(TAG, "requestedContainerBounds: " + requestedContainerBounds);

        // Save a copy for later
        View anchor = findChildNearestTarget(recyclerView, requestedContainerBounds);
        if (anchor == null) {
            Log.d(TAG, "Failed to locate anchor view");
            return result; // result.availableArea == null
        }

        Log.d(TAG, "Anchor view:" + anchor);
        Rect requestedContentBounds = new Rect(requestedContainerBounds);
        recyclerView.offsetRectIntoDescendantCoords(anchor, requestedContentBounds);
        Rect recyclerLocalVisible = new Rect();
        recyclerView.getLocalVisibleRect(recyclerLocalVisible);

        Log.d(TAG, "requestedContentBounds = " + requestedContentBounds);
        int prevAnchorTop = anchor.getTop();
        // Note: requestChildRectangleOnScreen may modify rectangle, must pass pass in a copy here
        Rect input = new Rect(requestedContentBounds);
        // Expand input rect to get the requested rect to be in the center
        int remainingHeight = recyclerView.getHeight() - recyclerView.getPaddingTop()
                - recyclerView.getPaddingBottom() - input.height();
        // Expand request rect match visible bounds to center the requested rect vertically
        Rect adjustedContainerBounds = new Rect(requestedContainerBounds);
        int remainingHeight = recyclerLocalVisible.height() -  requestedContainerBounds.height();
        if (remainingHeight > 0) {
            input.inset(0, -remainingHeight / 2);
            adjustedContainerBounds.inset(0, -remainingHeight / 2);
        }
        Log.d(TAG, "input (post center adjustment) = " + input);

        if (recyclerView.requestChildRectangleOnScreen(anchor, input, true)) {
            int scrolled = prevAnchorTop - anchor.getTop(); // inverse of movement
            Log.d(TAG, "RecyclerView scrolled by " + scrolled + " px");
            mScrollDelta += scrolled; // view.top-- is equivalent to parent.scrollY++
            result.scrollDelta = mScrollDelta;
            Log.d(TAG, "requestedContentBounds, (post-request-rect) = " + requestedContentBounds);
        int scrollAmount = computeScrollAmount(recyclerLocalVisible, adjustedContainerBounds);
        if (scrollAmount < 0) {
            Log.d(TAG, "About to scroll UP (content moves down within parent)");
        } else if (scrollAmount > 0) {
            Log.d(TAG, "About to scroll DOWN (content moves up within parent)");
        }
        Log.d(TAG, "scrollAmount: " + scrollAmount);

        requestedContainerBounds.set(requestedContentBounds);
        recyclerView.offsetDescendantRectToMyCoords(anchor, requestedContainerBounds);
        Log.d(TAG, "requestedContainerBounds, (post-scroll): " + requestedContainerBounds);
        View refView = findScrollingReferenceView(recyclerView, scrollAmount);
        int refTop = refView.getTop();

        Rect recyclerLocalVisible = new Rect(scrollBounds);
        recyclerView.getLocalVisibleRect(recyclerLocalVisible);
        Log.d(TAG, "recyclerLocalVisible: " + recyclerLocalVisible);
        // Map the request into the child view coords
        Rect requestedContentBounds = new Rect(adjustedContainerBounds);
        recyclerView.offsetRectIntoDescendantCoords(refView, requestedContentBounds);
        Log.d(TAG, "request rect, in child view space = " + requestedContentBounds);

        if (!requestedContainerBounds.intersect(recyclerLocalVisible)) {
            // Requested area is still not visible
            Log.d(TAG, "requested bounds not visible!");
            return result;
        }
        Rect available = new Rect(requestedContainerBounds);
        available.offset(-scrollBounds.left, -scrollBounds.top);
        available.offset(0, mScrollDelta);
        result.availableArea = available;
        Log.d(TAG, "availableArea: " + result.availableArea);
        return result;
    }
        // Note: requestChildRectangleOnScreen may modify rectangle, must pass pass in a copy here
        Rect request = new Rect(requestedContentBounds);
        recyclerView.requestChildRectangleOnScreen(refView, request, true);

    /**
     * Find a view that is located "closest" to targetRect. Returns the first view to fully
     * vertically overlap the target targetRect. If none found, returns the view with an edge
     * nearest the target targetRect.
     *
     * @param parent the parent vertical layout
     * @param targetRect a rectangle in local coordinates of <code>parent</code>
     * @return a child view within parent matching the criteria or null
     */
    static View findChildNearestTarget(ViewGroup parent, Rect targetRect) {
        View selected = null;
        int minCenterDistance = Integer.MAX_VALUE;
        int maxOverlap = 0;

        // allowable center-center distance, relative to targetRect.
        // if within this range, taller views are preferred
        final float preferredRangeFromCenterPercent = 0.25f;
        final int preferredDistance =
                (int) (preferredRangeFromCenterPercent * targetRect.height());

        Rect parentLocalVis = new Rect();
        parent.getLocalVisibleRect(parentLocalVis);
        Log.d(TAG, "findChildNearestTarget: parentVis=" + parentLocalVis
                + " targetRect=" + targetRect);

        Rect frame = new Rect();
        for (int i = 0; i < parent.getChildCount(); i++) {
            final View child = parent.getChildAt(i);
            child.getHitRect(frame);
            Log.d(TAG, "child #" + i + " hitRect=" + frame);

            if (child.getVisibility() != View.VISIBLE) {
                Log.d(TAG, "child #" + i + " is not visible");
                continue;
        int scrollDistance = refTop - refView.getTop();
        Log.d(TAG, "Parent view scrolled vertically by " + scrollDistance + " px");

        mScrollDelta += scrollDistance;
        result.scrollDelta = mScrollDelta;
        if (scrollDistance != 0) {
            Log.d(TAG, "Scroll delta is now " + mScrollDelta + " px");
        }

            int centerDistance = Math.abs(targetRect.centerY() - frame.centerY());
            Log.d(TAG, "child #" + i + " : center to center: " + centerDistance + "px");
        // Update, post-scroll
        requestedContainerBounds = new Rect(
                transformFromRequestToContainer(mScrollDelta, scrollBounds, requestRect));

            if (centerDistance < minCenterDistance) {
                // closer to center
                minCenterDistance = centerDistance;
                selected = child;
            } else if (frame.intersect(targetRect) && (frame.height() > preferredDistance)) {
                // within X% pixels of center, but taller
                selected = child;
            }
        // in case it might have changed (nested scrolling)
        recyclerView.getLocalVisibleRect(recyclerLocalVisible);
        if (requestedContainerBounds.intersect(recyclerLocalVisible)) {
            result.availableArea = transformFromContainerToRequest(
                    mScrollDelta, scrollBounds, requestedContainerBounds);
        }
        return selected;
        Log.d(TAG, "-----------------------------------------------------------");
        return result;
    }


    @Override
    public void onPrepareForEnd(@NonNull ViewGroup view) {
        // Restore original position and state
        view.scrollBy(0, -mScrollDelta);
        view.setOverScrollMode(mOverScrollMode);
        view.setVerticalScrollBarEnabled(mScrollBarWasEnabled);
        if (DISABLE_ANIMATORS) {
            ValueAnimator.setDurationScale(mDurationScale);
        }
        if (STOP_RENDER_THREAD) {
            view.getThreadedRenderer().start();
        }
    }
}
+6 −0
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ import android.util.Log;
import android.view.ScrollCaptureCallback;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ListView;

/**
 * Provides built-in framework level Scroll Capture support for standard scrolling Views.
@@ -180,6 +181,11 @@ public class ScrollCaptureInternal {
                            + "[" + 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_FIXED:
+108 −2
Original line number Diff line number Diff line
@@ -21,10 +21,8 @@ import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.graphics.HardwareRenderer;
import android.graphics.Matrix;
import android.graphics.RecordingCanvas;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.RenderNode;
import android.os.CancellationSignal;
import android.provider.Settings;
@@ -35,6 +33,7 @@ import android.view.ScrollCaptureCallback;
import android.view.ScrollCaptureSession;
import android.view.Surface;
import android.view.View;
import android.view.ViewGroup;

import com.android.internal.view.ScrollCaptureViewHelper.ScrollResult;

@@ -88,6 +87,113 @@ public class ScrollCaptureViewSupport<V extends View> implements ScrollCaptureCa
        return colorMode;
    }

    /**
     * Maps a rect in request bounds relative space  (relative to requestBounds) to container-local
     * space, accounting for the provided value of scrollY.
     *
     * @param scrollY the current scroll offset to apply to rect
     * @param requestBounds defines the local coordinate space of rect, within the container
     * @param requestRect the rectangle to transform to container-local coordinates
     * @return the same rectangle mapped to container bounds
     */
    public static Rect transformFromRequestToContainer(int scrollY, Rect requestBounds,
            Rect requestRect) {
        Rect requestedContainerBounds = new Rect(requestRect);
        requestedContainerBounds.offset(0, -scrollY);
        requestedContainerBounds.offset(requestBounds.left, requestBounds.top);
        return requestedContainerBounds;
    }

    /**
     * Maps a rect in container-local coordinate space to request space (relative to
     * requestBounds), accounting for the provided value of scrollY.
     *
     * @param scrollY the current scroll offset of the container
     * @param requestBounds defines the local coordinate space of rect, within the container
     * @param containerRect the rectangle within the container local coordinate space
     * @return the same rectangle mapped to within request bounds
     */
    public static Rect transformFromContainerToRequest(int scrollY, Rect requestBounds,
            Rect containerRect) {
        Rect requestRect = new Rect(containerRect);
        requestRect.offset(-requestBounds.left, -requestBounds.top);
        requestRect.offset(0, scrollY);
        return requestRect;
    }

    /**
     * Implements the core contract of requestRectangleOnScreen. Given a bounding rect and
     * another rectangle, return the minimum scroll distance that will maximize the visible area
     * of the requested rectangle.
     *
     * @param parentVisibleBounds the visible area
     * @param requested the requested area
     */
    public static int computeScrollAmount(Rect parentVisibleBounds, Rect requested) {
        final int height = parentVisibleBounds.height();
        final int top = parentVisibleBounds.top;
        final int bottom = parentVisibleBounds.bottom;
        int scrollYDelta = 0;

        if (requested.bottom > bottom && requested.top > top) {
            // need to scroll DOWN (move views up) to get it in view:
            // move just enough so that the entire rectangle is in view
            // (or at least the first screen size chunk).

            if (requested.height() > height) {
                // just enough to get screen size chunk on
                scrollYDelta += (requested.top - top);
            } else {
                // entire rect at bottom
                scrollYDelta += (requested.bottom - bottom);
            }
        } else if (requested.top < top && requested.bottom < bottom) {
            // need to scroll UP (move views down) to get it in view:
            // move just enough so that entire rectangle is in view
            // (or at least the first screen size chunk of it).

            if (requested.height() > height) {
                // screen size chunk
                scrollYDelta -= (bottom - requested.bottom);
            } else {
                // entire rect at top
                scrollYDelta -= (top - requested.top);
            }
        }
        return scrollYDelta;
    }

    /**
     * Locate a view to use as a reference, given an anticipated scrolling movement.
     * <p>
     * This view will be used to measure the actual movement of child views after scrolling.
     * When scrolling down, the last (max(y)) view is used, otherwise the first (min(y)
     * view. This helps to avoid recycling the reference view as a side effect of scrolling.
     *
     * @param parent the scrolling container
     * @param expectedScrollDistance the amount of scrolling to perform
     */
    public static View findScrollingReferenceView(ViewGroup parent, int expectedScrollDistance) {
        View selected = null;
        Rect parentLocalVisible = new Rect();
        parent.getLocalVisibleRect(parentLocalVisible);

        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = parent.getChildAt(i);
            if (selected == null) {
                selected = child;
            } else if (expectedScrollDistance < 0) {
                if (child.getTop() < selected.getTop()) {
                    selected = child;
                }
            } else if (child.getBottom() > selected.getBottom()) {
                selected = child;
            }
        }
        return selected;
    }

    @Override
    public final void onScrollCaptureSearch(CancellationSignal signal, Consumer<Rect> onReady) {
        if (signal.isCanceled()) {