diff --git a/core/java/com/android/internal/view/ListViewCaptureHelper.java b/core/java/com/android/internal/view/ListViewCaptureHelper.java new file mode 100644 index 0000000000000..c25b4b59930d0 --- /dev/null +++ b/core/java/com/android/internal/view/ListViewCaptureHelper.java @@ -0,0 +1,125 @@ +/* + * 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 { + 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); + } +} diff --git a/core/java/com/android/internal/view/RecyclerViewCaptureHelper.java b/core/java/com/android/internal/view/RecyclerViewCaptureHelper.java index 75bf1ce136c93..b29cf1c2cb1a6 100644 --- a/core/java/com/android/internal/view/RecyclerViewCaptureHelper.java +++ b/core/java/com/android/internal/view/RecyclerViewCaptureHelper.java @@ -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 { - - // 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 0) { + adjustedContainerBounds.inset(0, -remainingHeight / 2); } - Rect available = new Rect(requestedContainerBounds); - available.offset(-scrollBounds.left, -scrollBounds.top); - available.offset(0, mScrollDelta); - result.availableArea = available; - Log.d(TAG, "availableArea: " + result.availableArea); + + 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(recyclerView, scrollAmount); + int refTop = refView.getTop(); + + // 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); + + // Note: requestChildRectangleOnScreen may modify rectangle, must pass pass in a copy here + Rect request = new Rect(requestedContentBounds); + recyclerView.requestChildRectangleOnScreen(refView, request, true); + + 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"); + } + + // Update, post-scroll + requestedContainerBounds = new Rect( + transformFromRequestToContainer(mScrollDelta, scrollBounds, requestRect)); + + // in case it might have changed (nested scrolling) + recyclerView.getLocalVisibleRect(recyclerLocalVisible); + if (requestedContainerBounds.intersect(recyclerLocalVisible)) { + result.availableArea = transformFromContainerToRequest( + mScrollDelta, scrollBounds, requestedContainerBounds); + } + Log.d(TAG, "-----------------------------------------------------------"); return result; } - /** - * 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 parent - * @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 centerDistance = Math.abs(targetRect.centerY() - frame.centerY()); - Log.d(TAG, "child #" + i + " : center to center: " + centerDistance + "px"); - - 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; - } - } - return selected; - } - - @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(); - } } } diff --git a/core/java/com/android/internal/view/ScrollCaptureInternal.java b/core/java/com/android/internal/view/ScrollCaptureInternal.java index 4b9a1606975b3..ffee16a151df8 100644 --- a/core/java/com/android/internal/view/ScrollCaptureInternal.java +++ b/core/java/com/android/internal/view/ScrollCaptureInternal.java @@ -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: diff --git a/core/java/com/android/internal/view/ScrollCaptureViewSupport.java b/core/java/com/android/internal/view/ScrollCaptureViewSupport.java index 9e09006f608d6..8d63a400c63c5 100644 --- a/core/java/com/android/internal/view/ScrollCaptureViewSupport.java +++ b/core/java/com/android/internal/view/ScrollCaptureViewSupport.java @@ -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 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. + *

+ * 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 onReady) { if (signal.isCanceled()) {