Loading core/java/com/android/internal/view/ListViewCaptureHelper.java 0 → 100644 +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); } } core/java/com/android/internal/view/RecyclerViewCaptureHelper.java +52 −120 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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) { Loading @@ -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(); } } } core/java/com/android/internal/view/ScrollCaptureInternal.java +6 −0 Original line number Diff line number Diff line Loading @@ -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. Loading Loading @@ -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: Loading core/java/com/android/internal/view/ScrollCaptureViewSupport.java +108 −2 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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()) { Loading Loading
core/java/com/android/internal/view/ListViewCaptureHelper.java 0 → 100644 +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); } }
core/java/com/android/internal/view/RecyclerViewCaptureHelper.java +52 −120 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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) { Loading @@ -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(); } } }
core/java/com/android/internal/view/ScrollCaptureInternal.java +6 −0 Original line number Diff line number Diff line Loading @@ -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. Loading Loading @@ -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: Loading
core/java/com/android/internal/view/ScrollCaptureViewSupport.java +108 −2 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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()) { Loading