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

Commit 82f0979e authored by Mark Renouf's avatar Mark Renouf Committed by Android (Google) Code Review
Browse files

Merge "Adds long screenshot support for ListView" into sc-dev

parents 328ba5dc 021fd547
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()) {