Loading core/java/com/android/internal/view/RecyclerViewCaptureHelper.java 0 → 100644 +202 −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 android.animation.ValueAnimator; import android.annotation.NonNull; import android.graphics.Rect; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; /** * ScrollCapture for RecyclerView and <i>RecyclerView-like</i> ViewGroups. * <p> * Requirements for proper operation: * <ul> * <li>at least one visible child view</li> * <li>scrolls by pixels in response to {@link View#scrollBy(int, int)}. * <li>reports ability to scroll with {@link View#canScrollVertically(int)} * <li>properly implements {@link ViewParent#requestChildRectangleOnScreen(View, Rect, boolean)} * </ul> * * @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) { mScrollDelta = 0; mOverScrollMode = view.getOverScrollMode(); view.setOverScrollMode(View.OVER_SCROLL_NEVER); 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) { 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); // 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); 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); 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); } requestedContainerBounds.set(requestedContentBounds); recyclerView.offsetDescendantRectToMyCoords(anchor, requestedContainerBounds); Log.d(TAG, "requestedContainerBounds, (post-scroll): " + requestedContainerBounds); Rect recyclerLocalVisible = new Rect(scrollBounds); recyclerView.getLocalVisibleRect(recyclerLocalVisible); Log.d(TAG, "recyclerLocalVisible: " + recyclerLocalVisible); 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; } /** * 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 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(); } } } core/java/com/android/internal/view/ScrollCaptureInternal.java +88 −3 Original line number Diff line number Diff line Loading @@ -17,8 +17,11 @@ package com.android.internal.view; import android.annotation.Nullable; import android.content.Context; import android.content.res.Resources; import android.graphics.Point; import android.graphics.Rect; import android.util.Log; import android.view.ScrollCaptureCallback; import android.view.View; import android.view.ViewGroup; Loading @@ -29,6 +32,12 @@ import android.view.ViewGroup; public class ScrollCaptureInternal { private static final String TAG = "ScrollCaptureInternal"; // Log found scrolling views private static final boolean DEBUG = true; // Log all investigated views, as well as heuristic checks private static final boolean DEBUG_VERBOSE = false; private static final int UP = -1; private static final int DOWN = 1; Loading Loading @@ -57,38 +66,72 @@ public class ScrollCaptureInternal { * This needs to be fast and not alloc memory. It's called on everything in the tree not marked * as excluded during scroll capture search. */ public static int detectScrollingType(View view) { private static int detectScrollingType(View view) { // Must be a ViewGroup if (!(view instanceof ViewGroup)) { if (DEBUG_VERBOSE) { Log.v(TAG, "hint: not a subclass of ViewGroup"); } return TYPE_FIXED; } if (DEBUG_VERBOSE) { Log.v(TAG, "hint: is a subclass of ViewGroup"); } // Confirm that it can scroll. if (!(view.canScrollVertically(DOWN) || view.canScrollVertically(UP))) { // Nothing to scroll here, move along. if (DEBUG_VERBOSE) { Log.v(TAG, "hint: cannot be scrolled"); } return TYPE_FIXED; } if (DEBUG_VERBOSE) { Log.v(TAG, "hint: can be scrolled up or down"); } // ScrollViews accept only a single child. if (((ViewGroup) view).getChildCount() > 1) { if (DEBUG_VERBOSE) { Log.v(TAG, "hint: scrollable with multiple children"); } return TYPE_RECYCLING; } if (DEBUG_VERBOSE) { Log.v(TAG, "hint: less than two child views"); } //Because recycling containers don't use scrollY, a non-zero value means Scroll view. if (view.getScrollY() != 0) { if (DEBUG_VERBOSE) { Log.v(TAG, "hint: scrollY != 0"); } return TYPE_SCROLLING; } Log.v(TAG, "hint: scrollY == 0"); // Since scrollY cannot be negative, this means a Recycling view. if (view.canScrollVertically(UP)) { if (DEBUG_VERBOSE) { Log.v(TAG, "hint: able to scroll up"); } return TYPE_RECYCLING; } // canScrollVertically(UP) == false, getScrollY() == 0, getChildCount() == 1. if (DEBUG_VERBOSE) { Log.v(TAG, "hint: cannot be scrolled up"); } // canScrollVertically(UP) == false, getScrollY() == 0, getChildCount() == 1. // For Recycling containers, this should be a no-op (RecyclerView logs a warning) view.scrollTo(view.getScrollX(), 1); // A scrolling container would have moved by 1px. if (view.getScrollY() == 1) { view.scrollTo(view.getScrollX(), 0); if (DEBUG_VERBOSE) { Log.v(TAG, "hint: scrollTo caused scrollY to change"); } return TYPE_SCROLLING; } if (DEBUG_VERBOSE) { Log.v(TAG, "hint: scrollTo did not cause scrollY to change"); } return TYPE_RECYCLING; } Loading @@ -99,19 +142,61 @@ public class ScrollCaptureInternal { * @param localVisibleRect the visible area of the given view in local coordinates, as supplied * by the view parent * @param positionInWindow the offset of localVisibleRect within the window * * @return a new callback or null if the View isn't supported */ @Nullable public ScrollCaptureCallback requestCallback(View view, Rect localVisibleRect, Point positionInWindow) { // Nothing to see here yet. if (DEBUG_VERBOSE) { Log.v(TAG, "scroll capture: checking " + view.getClass().getName() + "[" + resolveId(view.getContext(), view.getId()) + "]"); } int i = detectScrollingType(view); switch (i) { case TYPE_SCROLLING: if (DEBUG) { Log.d(TAG, "scroll capture: FOUND " + view.getClass().getName() + "[" + resolveId(view.getContext(), view.getId()) + "]" + " -> TYPE_SCROLLING"); } return new ScrollCaptureViewSupport<>((ViewGroup) view, new ScrollViewCaptureHelper()); case TYPE_RECYCLING: if (DEBUG) { Log.d(TAG, "scroll capture: FOUND " + view.getClass().getName() + "[" + resolveId(view.getContext(), view.getId()) + "]" + " -> TYPE_RECYCLING"); } return new ScrollCaptureViewSupport<>((ViewGroup) view, new RecyclerViewCaptureHelper()); case TYPE_FIXED: // ignore break; } return null; } // Lifted from ViewDebug (package protected) private static String formatIntToHexString(int value) { return "0x" + Integer.toHexString(value).toUpperCase(); } static String resolveId(Context context, int id) { String fieldValue; final Resources resources = context.getResources(); if (id >= 0) { try { fieldValue = resources.getResourceTypeName(id) + '/' + resources.getResourceEntryName(id); } catch (Resources.NotFoundException e) { fieldValue = "id/" + formatIntToHexString(id); } } else { fieldValue = "NO_ID"; } return fieldValue; } } core/java/com/android/internal/view/ScrollCaptureViewHelper.java +7 −6 Original line number Diff line number Diff line Loading @@ -17,7 +17,6 @@ package com.android.internal.view; import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Rect; import android.view.View; Loading Loading @@ -62,8 +61,8 @@ interface ScrollCaptureViewHelper<V extends View> { * @param view the view being captured * @return true if the callback should respond to a request with scroll bounds */ default boolean onAcceptSession(@Nullable V view) { return view != null && view.isVisibleToUser() default boolean onAcceptSession(@NonNull V view) { return view.isVisibleToUser() && (view.canScrollVertically(UP) || view.canScrollVertically(DOWN)); } Loading @@ -73,7 +72,7 @@ interface ScrollCaptureViewHelper<V extends View> { * * @param view the view being captured */ default Rect onComputeScrollBounds(@Nullable V view) { @NonNull default Rect onComputeScrollBounds(@NonNull V view) { return new Rect(view.getPaddingLeft(), view.getPaddingTop(), view.getWidth() - view.getPaddingRight(), view.getHeight() - view.getPaddingBottom()); Loading @@ -88,7 +87,7 @@ interface ScrollCaptureViewHelper<V extends View> { * @param view the view being captured * @param scrollBounds the bounds within {@code view} where content scrolls */ void onPrepareForStart(@NonNull V view, Rect scrollBounds); void onPrepareForStart(@NonNull V view, @NonNull Rect scrollBounds); /** * Map the request onto the screen. Loading @@ -105,7 +104,9 @@ interface ScrollCaptureViewHelper<V extends View> { * content to capture for the request * @return the result of the request as a {@link ScrollResult} */ ScrollResult onScrollRequested(@NonNull V view, Rect scrollBounds, Rect requestRect); @NonNull ScrollResult onScrollRequested(@NonNull V view, @NonNull Rect scrollBounds, @NonNull Rect requestRect); /** * Restore the target after capture. Loading core/java/com/android/internal/view/ScrollCaptureViewSupport.java +53 −19 Original line number Diff line number Diff line Loading @@ -23,8 +23,8 @@ import android.graphics.Rect; import android.graphics.RectF; import android.graphics.RenderNode; import android.os.Handler; import android.os.SystemClock; import android.util.DisplayMetrics; import android.util.Log; import android.view.ScrollCaptureCallback; import android.view.ScrollCaptureSession; import android.view.Surface; Loading @@ -46,8 +46,12 @@ import java.util.function.Consumer; */ public class ScrollCaptureViewSupport<V extends View> implements ScrollCaptureCallback { public static final long NO_FRAME_PRODUCED = -1; private static final String TAG = "ScrollCaptureViewSupport"; private static final boolean WAIT_FOR_ANIMATION = true; private final WeakReference<V> mWeakView; private final ScrollCaptureViewHelper<V> mViewHelper; private ViewRenderer mRenderer; Loading Loading @@ -99,12 +103,16 @@ public class ScrollCaptureViewSupport<V extends View> implements ScrollCaptureCa V view = mWeakView.get(); if (view == null || !view.isVisibleToUser()) { // Signal to the controller that we have a problem and can't continue. session.notifyBufferSent(0, null); session.notifyBufferSent(NO_FRAME_PRODUCED, new Rect()); return; } // Ask the view to scroll as needed to bring this area into view. ScrollResult scrollResult = mViewHelper.onScrollRequested(view, session.getScrollBounds(), requestRect); if (scrollResult.availableArea.isEmpty()) { session.notifyBufferSent(NO_FRAME_PRODUCED, scrollResult.availableArea); return; } view.invalidate(); // don't wait for vsync // For image capture, shift back by scrollDelta to arrive at the location within the view Loading @@ -112,8 +120,19 @@ public class ScrollCaptureViewSupport<V extends View> implements ScrollCaptureCa Rect viewCaptureArea = new Rect(scrollResult.availableArea); viewCaptureArea.offset(0, -scrollResult.scrollDelta); mRenderer.renderView(view, viewCaptureArea, mUiHandler, (frameNumber) -> session.notifyBufferSent(frameNumber, scrollResult.availableArea)); if (WAIT_FOR_ANIMATION) { Log.d(TAG, "render: delaying until animation"); view.postOnAnimation(() -> { Log.d(TAG, "postOnAnimation(): rendering now"); long resultFrame = mRenderer.renderView(view, viewCaptureArea); Log.d(TAG, "notifyBufferSent: " + scrollResult.availableArea); session.notifyBufferSent(resultFrame, new Rect(scrollResult.availableArea)); }); } else { long resultFrame = mRenderer.renderView(view, viewCaptureArea); session.notifyBufferSent(resultFrame, new Rect(scrollResult.availableArea)); } } @Override Loading @@ -132,8 +151,7 @@ public class ScrollCaptureViewSupport<V extends View> implements ScrollCaptureCa /** * Internal helper class which assists in rendering sections of the view hierarchy relative to a * given view. Used by framework implementations of ScrollCaptureHandler to render and dispatch * image requests. * given view. */ static final class ViewRenderer { // alpha, "reasonable default" from Javadoc Loading @@ -157,14 +175,11 @@ public class ScrollCaptureViewSupport<V extends View> implements ScrollCaptureCa private final Matrix mTempMatrix = new Matrix(); private final int[] mTempLocation = new int[2]; private long mLastRenderedSourceDrawingId = -1; public interface FrameCompleteListener { void onFrameComplete(long frameNumber); } private Surface mSurface; ViewRenderer() { mRenderer = new HardwareRenderer(); mRenderer.setName("ScrollCapture"); mCaptureRenderNode = new RenderNode("ScrollCaptureRoot"); mRenderer.setContentRoot(mCaptureRenderNode); Loading @@ -173,6 +188,7 @@ public class ScrollCaptureViewSupport<V extends View> implements ScrollCaptureCa } public void setSurface(Surface surface) { mSurface = surface; mRenderer.setSurface(surface); } Loading Loading @@ -223,20 +239,38 @@ public class ScrollCaptureViewSupport<V extends View> implements ScrollCaptureCa mCaptureRenderNode.endRecording(); } public void renderView(View view, Rect sourceRect, Handler handler, FrameCompleteListener frameListener) { public long renderView(View view, Rect sourceRect) { if (updateForView(view)) { setupLighting(view); } view.invalidate(); updateRootNode(view, sourceRect); HardwareRenderer.FrameRenderRequest request = mRenderer.createRenderRequest(); request.setVsyncTime(SystemClock.elapsedRealtimeNanos()); // private API b/c request.setFrameCommitCallback does not provide access to frameNumber mRenderer.setFrameCompleteCallback( frameNr -> handler.post(() -> frameListener.onFrameComplete(frameNr))); long timestamp = System.nanoTime(); request.setVsyncTime(timestamp); // Would be nice to access nextFrameNumber from HwR without having to hold on to Surface final long frameNumber = mSurface.getNextFrameNumber(); // Block until a frame is presented to the Surface request.setWaitForPresent(true); request.syncAndDraw(); switch (request.syncAndDraw()) { case HardwareRenderer.SYNC_OK: case HardwareRenderer.SYNC_REDRAW_REQUESTED: return frameNumber; case HardwareRenderer.SYNC_FRAME_DROPPED: Log.e(TAG, "syncAndDraw(): SYNC_FRAME_DROPPED !"); break; case HardwareRenderer.SYNC_LOST_SURFACE_REWARD_IF_FOUND: Log.e(TAG, "syncAndDraw(): SYNC_LOST_SURFACE !"); break; case HardwareRenderer.SYNC_CONTEXT_IS_STOPPED: Log.e(TAG, "syncAndDraw(): SYNC_CONTEXT_IS_STOPPED !"); break; } return NO_FRAME_PRODUCED; } public void trimMemory() { Loading @@ -244,6 +278,7 @@ public class ScrollCaptureViewSupport<V extends View> implements ScrollCaptureCa } public void destroy() { mSurface = null; mRenderer.destroy(); } Loading @@ -254,6 +289,5 @@ public class ScrollCaptureViewSupport<V extends View> implements ScrollCaptureCa mTempMatrix.mapRect(mTempRectF); mTempRectF.round(outRect); } } } core/java/com/android/internal/view/ScrollViewCaptureHelper.java +12 −8 Original line number Diff line number Diff line Loading @@ -57,10 +57,6 @@ public class ScrollViewCaptureHelper implements ScrollCaptureViewHelper<ViewGrou public ScrollResult onScrollRequested(@NonNull ViewGroup view, Rect scrollBounds, Rect requestRect) { final View contentView = view.getChildAt(0); // returns null, does not throw IOOBE if (contentView == null) { return null; } /* +---------+ <----+ Content [25,25 - 275,1025] (w=250,h=1000) | | Loading Loading @@ -88,9 +84,6 @@ public class ScrollViewCaptureHelper implements ScrollCaptureViewHelper<ViewGrou \__ Requested Bounds[0,300 - 200,400] (200x100) */ ScrollResult result = new ScrollResult(); result.requestedArea = new Rect(requestRect); // 0) adjust the requestRect to account for scroll change since start // // Scroll Bounds[50,50 - 250,250] (w=200,h=200) Loading @@ -99,6 +92,17 @@ public class ScrollViewCaptureHelper implements ScrollCaptureViewHelper<ViewGrou // (y-100) (scrollY - mStartScrollY) int scrollDelta = view.getScrollY() - mStartScrollY; final ScrollResult result = new ScrollResult(); result.requestedArea = new Rect(requestRect); result.scrollDelta = scrollDelta; result.availableArea = new Rect(); final View contentView = view.getChildAt(0); // returns null, does not throw IOOBE if (contentView == null) { // No child view? Cannot continue. return result; } // 1) Translate request rect to make it relative to container view // // Container View [0,0 - 300,300] (scrollY=200) Loading Loading @@ -133,7 +137,7 @@ public class ScrollViewCaptureHelper implements ScrollCaptureViewHelper<ViewGrou // TODO: crop capture area to avoid occlusions/minimize scroll changes Point offset = new Point(); final Rect available = new Rect(requestedContentBounds); // empty final Rect available = new Rect(requestedContentBounds); if (!view.getChildVisibleRect(contentView, available, offset)) { available.setEmpty(); result.availableArea = available; Loading Loading
core/java/com/android/internal/view/RecyclerViewCaptureHelper.java 0 → 100644 +202 −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 android.animation.ValueAnimator; import android.annotation.NonNull; import android.graphics.Rect; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; /** * ScrollCapture for RecyclerView and <i>RecyclerView-like</i> ViewGroups. * <p> * Requirements for proper operation: * <ul> * <li>at least one visible child view</li> * <li>scrolls by pixels in response to {@link View#scrollBy(int, int)}. * <li>reports ability to scroll with {@link View#canScrollVertically(int)} * <li>properly implements {@link ViewParent#requestChildRectangleOnScreen(View, Rect, boolean)} * </ul> * * @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) { mScrollDelta = 0; mOverScrollMode = view.getOverScrollMode(); view.setOverScrollMode(View.OVER_SCROLL_NEVER); 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) { 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); // 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); 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); 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); } requestedContainerBounds.set(requestedContentBounds); recyclerView.offsetDescendantRectToMyCoords(anchor, requestedContainerBounds); Log.d(TAG, "requestedContainerBounds, (post-scroll): " + requestedContainerBounds); Rect recyclerLocalVisible = new Rect(scrollBounds); recyclerView.getLocalVisibleRect(recyclerLocalVisible); Log.d(TAG, "recyclerLocalVisible: " + recyclerLocalVisible); 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; } /** * 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 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(); } } }
core/java/com/android/internal/view/ScrollCaptureInternal.java +88 −3 Original line number Diff line number Diff line Loading @@ -17,8 +17,11 @@ package com.android.internal.view; import android.annotation.Nullable; import android.content.Context; import android.content.res.Resources; import android.graphics.Point; import android.graphics.Rect; import android.util.Log; import android.view.ScrollCaptureCallback; import android.view.View; import android.view.ViewGroup; Loading @@ -29,6 +32,12 @@ import android.view.ViewGroup; public class ScrollCaptureInternal { private static final String TAG = "ScrollCaptureInternal"; // Log found scrolling views private static final boolean DEBUG = true; // Log all investigated views, as well as heuristic checks private static final boolean DEBUG_VERBOSE = false; private static final int UP = -1; private static final int DOWN = 1; Loading Loading @@ -57,38 +66,72 @@ public class ScrollCaptureInternal { * This needs to be fast and not alloc memory. It's called on everything in the tree not marked * as excluded during scroll capture search. */ public static int detectScrollingType(View view) { private static int detectScrollingType(View view) { // Must be a ViewGroup if (!(view instanceof ViewGroup)) { if (DEBUG_VERBOSE) { Log.v(TAG, "hint: not a subclass of ViewGroup"); } return TYPE_FIXED; } if (DEBUG_VERBOSE) { Log.v(TAG, "hint: is a subclass of ViewGroup"); } // Confirm that it can scroll. if (!(view.canScrollVertically(DOWN) || view.canScrollVertically(UP))) { // Nothing to scroll here, move along. if (DEBUG_VERBOSE) { Log.v(TAG, "hint: cannot be scrolled"); } return TYPE_FIXED; } if (DEBUG_VERBOSE) { Log.v(TAG, "hint: can be scrolled up or down"); } // ScrollViews accept only a single child. if (((ViewGroup) view).getChildCount() > 1) { if (DEBUG_VERBOSE) { Log.v(TAG, "hint: scrollable with multiple children"); } return TYPE_RECYCLING; } if (DEBUG_VERBOSE) { Log.v(TAG, "hint: less than two child views"); } //Because recycling containers don't use scrollY, a non-zero value means Scroll view. if (view.getScrollY() != 0) { if (DEBUG_VERBOSE) { Log.v(TAG, "hint: scrollY != 0"); } return TYPE_SCROLLING; } Log.v(TAG, "hint: scrollY == 0"); // Since scrollY cannot be negative, this means a Recycling view. if (view.canScrollVertically(UP)) { if (DEBUG_VERBOSE) { Log.v(TAG, "hint: able to scroll up"); } return TYPE_RECYCLING; } // canScrollVertically(UP) == false, getScrollY() == 0, getChildCount() == 1. if (DEBUG_VERBOSE) { Log.v(TAG, "hint: cannot be scrolled up"); } // canScrollVertically(UP) == false, getScrollY() == 0, getChildCount() == 1. // For Recycling containers, this should be a no-op (RecyclerView logs a warning) view.scrollTo(view.getScrollX(), 1); // A scrolling container would have moved by 1px. if (view.getScrollY() == 1) { view.scrollTo(view.getScrollX(), 0); if (DEBUG_VERBOSE) { Log.v(TAG, "hint: scrollTo caused scrollY to change"); } return TYPE_SCROLLING; } if (DEBUG_VERBOSE) { Log.v(TAG, "hint: scrollTo did not cause scrollY to change"); } return TYPE_RECYCLING; } Loading @@ -99,19 +142,61 @@ public class ScrollCaptureInternal { * @param localVisibleRect the visible area of the given view in local coordinates, as supplied * by the view parent * @param positionInWindow the offset of localVisibleRect within the window * * @return a new callback or null if the View isn't supported */ @Nullable public ScrollCaptureCallback requestCallback(View view, Rect localVisibleRect, Point positionInWindow) { // Nothing to see here yet. if (DEBUG_VERBOSE) { Log.v(TAG, "scroll capture: checking " + view.getClass().getName() + "[" + resolveId(view.getContext(), view.getId()) + "]"); } int i = detectScrollingType(view); switch (i) { case TYPE_SCROLLING: if (DEBUG) { Log.d(TAG, "scroll capture: FOUND " + view.getClass().getName() + "[" + resolveId(view.getContext(), view.getId()) + "]" + " -> TYPE_SCROLLING"); } return new ScrollCaptureViewSupport<>((ViewGroup) view, new ScrollViewCaptureHelper()); case TYPE_RECYCLING: if (DEBUG) { Log.d(TAG, "scroll capture: FOUND " + view.getClass().getName() + "[" + resolveId(view.getContext(), view.getId()) + "]" + " -> TYPE_RECYCLING"); } return new ScrollCaptureViewSupport<>((ViewGroup) view, new RecyclerViewCaptureHelper()); case TYPE_FIXED: // ignore break; } return null; } // Lifted from ViewDebug (package protected) private static String formatIntToHexString(int value) { return "0x" + Integer.toHexString(value).toUpperCase(); } static String resolveId(Context context, int id) { String fieldValue; final Resources resources = context.getResources(); if (id >= 0) { try { fieldValue = resources.getResourceTypeName(id) + '/' + resources.getResourceEntryName(id); } catch (Resources.NotFoundException e) { fieldValue = "id/" + formatIntToHexString(id); } } else { fieldValue = "NO_ID"; } return fieldValue; } }
core/java/com/android/internal/view/ScrollCaptureViewHelper.java +7 −6 Original line number Diff line number Diff line Loading @@ -17,7 +17,6 @@ package com.android.internal.view; import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Rect; import android.view.View; Loading Loading @@ -62,8 +61,8 @@ interface ScrollCaptureViewHelper<V extends View> { * @param view the view being captured * @return true if the callback should respond to a request with scroll bounds */ default boolean onAcceptSession(@Nullable V view) { return view != null && view.isVisibleToUser() default boolean onAcceptSession(@NonNull V view) { return view.isVisibleToUser() && (view.canScrollVertically(UP) || view.canScrollVertically(DOWN)); } Loading @@ -73,7 +72,7 @@ interface ScrollCaptureViewHelper<V extends View> { * * @param view the view being captured */ default Rect onComputeScrollBounds(@Nullable V view) { @NonNull default Rect onComputeScrollBounds(@NonNull V view) { return new Rect(view.getPaddingLeft(), view.getPaddingTop(), view.getWidth() - view.getPaddingRight(), view.getHeight() - view.getPaddingBottom()); Loading @@ -88,7 +87,7 @@ interface ScrollCaptureViewHelper<V extends View> { * @param view the view being captured * @param scrollBounds the bounds within {@code view} where content scrolls */ void onPrepareForStart(@NonNull V view, Rect scrollBounds); void onPrepareForStart(@NonNull V view, @NonNull Rect scrollBounds); /** * Map the request onto the screen. Loading @@ -105,7 +104,9 @@ interface ScrollCaptureViewHelper<V extends View> { * content to capture for the request * @return the result of the request as a {@link ScrollResult} */ ScrollResult onScrollRequested(@NonNull V view, Rect scrollBounds, Rect requestRect); @NonNull ScrollResult onScrollRequested(@NonNull V view, @NonNull Rect scrollBounds, @NonNull Rect requestRect); /** * Restore the target after capture. Loading
core/java/com/android/internal/view/ScrollCaptureViewSupport.java +53 −19 Original line number Diff line number Diff line Loading @@ -23,8 +23,8 @@ import android.graphics.Rect; import android.graphics.RectF; import android.graphics.RenderNode; import android.os.Handler; import android.os.SystemClock; import android.util.DisplayMetrics; import android.util.Log; import android.view.ScrollCaptureCallback; import android.view.ScrollCaptureSession; import android.view.Surface; Loading @@ -46,8 +46,12 @@ import java.util.function.Consumer; */ public class ScrollCaptureViewSupport<V extends View> implements ScrollCaptureCallback { public static final long NO_FRAME_PRODUCED = -1; private static final String TAG = "ScrollCaptureViewSupport"; private static final boolean WAIT_FOR_ANIMATION = true; private final WeakReference<V> mWeakView; private final ScrollCaptureViewHelper<V> mViewHelper; private ViewRenderer mRenderer; Loading Loading @@ -99,12 +103,16 @@ public class ScrollCaptureViewSupport<V extends View> implements ScrollCaptureCa V view = mWeakView.get(); if (view == null || !view.isVisibleToUser()) { // Signal to the controller that we have a problem and can't continue. session.notifyBufferSent(0, null); session.notifyBufferSent(NO_FRAME_PRODUCED, new Rect()); return; } // Ask the view to scroll as needed to bring this area into view. ScrollResult scrollResult = mViewHelper.onScrollRequested(view, session.getScrollBounds(), requestRect); if (scrollResult.availableArea.isEmpty()) { session.notifyBufferSent(NO_FRAME_PRODUCED, scrollResult.availableArea); return; } view.invalidate(); // don't wait for vsync // For image capture, shift back by scrollDelta to arrive at the location within the view Loading @@ -112,8 +120,19 @@ public class ScrollCaptureViewSupport<V extends View> implements ScrollCaptureCa Rect viewCaptureArea = new Rect(scrollResult.availableArea); viewCaptureArea.offset(0, -scrollResult.scrollDelta); mRenderer.renderView(view, viewCaptureArea, mUiHandler, (frameNumber) -> session.notifyBufferSent(frameNumber, scrollResult.availableArea)); if (WAIT_FOR_ANIMATION) { Log.d(TAG, "render: delaying until animation"); view.postOnAnimation(() -> { Log.d(TAG, "postOnAnimation(): rendering now"); long resultFrame = mRenderer.renderView(view, viewCaptureArea); Log.d(TAG, "notifyBufferSent: " + scrollResult.availableArea); session.notifyBufferSent(resultFrame, new Rect(scrollResult.availableArea)); }); } else { long resultFrame = mRenderer.renderView(view, viewCaptureArea); session.notifyBufferSent(resultFrame, new Rect(scrollResult.availableArea)); } } @Override Loading @@ -132,8 +151,7 @@ public class ScrollCaptureViewSupport<V extends View> implements ScrollCaptureCa /** * Internal helper class which assists in rendering sections of the view hierarchy relative to a * given view. Used by framework implementations of ScrollCaptureHandler to render and dispatch * image requests. * given view. */ static final class ViewRenderer { // alpha, "reasonable default" from Javadoc Loading @@ -157,14 +175,11 @@ public class ScrollCaptureViewSupport<V extends View> implements ScrollCaptureCa private final Matrix mTempMatrix = new Matrix(); private final int[] mTempLocation = new int[2]; private long mLastRenderedSourceDrawingId = -1; public interface FrameCompleteListener { void onFrameComplete(long frameNumber); } private Surface mSurface; ViewRenderer() { mRenderer = new HardwareRenderer(); mRenderer.setName("ScrollCapture"); mCaptureRenderNode = new RenderNode("ScrollCaptureRoot"); mRenderer.setContentRoot(mCaptureRenderNode); Loading @@ -173,6 +188,7 @@ public class ScrollCaptureViewSupport<V extends View> implements ScrollCaptureCa } public void setSurface(Surface surface) { mSurface = surface; mRenderer.setSurface(surface); } Loading Loading @@ -223,20 +239,38 @@ public class ScrollCaptureViewSupport<V extends View> implements ScrollCaptureCa mCaptureRenderNode.endRecording(); } public void renderView(View view, Rect sourceRect, Handler handler, FrameCompleteListener frameListener) { public long renderView(View view, Rect sourceRect) { if (updateForView(view)) { setupLighting(view); } view.invalidate(); updateRootNode(view, sourceRect); HardwareRenderer.FrameRenderRequest request = mRenderer.createRenderRequest(); request.setVsyncTime(SystemClock.elapsedRealtimeNanos()); // private API b/c request.setFrameCommitCallback does not provide access to frameNumber mRenderer.setFrameCompleteCallback( frameNr -> handler.post(() -> frameListener.onFrameComplete(frameNr))); long timestamp = System.nanoTime(); request.setVsyncTime(timestamp); // Would be nice to access nextFrameNumber from HwR without having to hold on to Surface final long frameNumber = mSurface.getNextFrameNumber(); // Block until a frame is presented to the Surface request.setWaitForPresent(true); request.syncAndDraw(); switch (request.syncAndDraw()) { case HardwareRenderer.SYNC_OK: case HardwareRenderer.SYNC_REDRAW_REQUESTED: return frameNumber; case HardwareRenderer.SYNC_FRAME_DROPPED: Log.e(TAG, "syncAndDraw(): SYNC_FRAME_DROPPED !"); break; case HardwareRenderer.SYNC_LOST_SURFACE_REWARD_IF_FOUND: Log.e(TAG, "syncAndDraw(): SYNC_LOST_SURFACE !"); break; case HardwareRenderer.SYNC_CONTEXT_IS_STOPPED: Log.e(TAG, "syncAndDraw(): SYNC_CONTEXT_IS_STOPPED !"); break; } return NO_FRAME_PRODUCED; } public void trimMemory() { Loading @@ -244,6 +278,7 @@ public class ScrollCaptureViewSupport<V extends View> implements ScrollCaptureCa } public void destroy() { mSurface = null; mRenderer.destroy(); } Loading @@ -254,6 +289,5 @@ public class ScrollCaptureViewSupport<V extends View> implements ScrollCaptureCa mTempMatrix.mapRect(mTempRectF); mTempRectF.round(outRect); } } }
core/java/com/android/internal/view/ScrollViewCaptureHelper.java +12 −8 Original line number Diff line number Diff line Loading @@ -57,10 +57,6 @@ public class ScrollViewCaptureHelper implements ScrollCaptureViewHelper<ViewGrou public ScrollResult onScrollRequested(@NonNull ViewGroup view, Rect scrollBounds, Rect requestRect) { final View contentView = view.getChildAt(0); // returns null, does not throw IOOBE if (contentView == null) { return null; } /* +---------+ <----+ Content [25,25 - 275,1025] (w=250,h=1000) | | Loading Loading @@ -88,9 +84,6 @@ public class ScrollViewCaptureHelper implements ScrollCaptureViewHelper<ViewGrou \__ Requested Bounds[0,300 - 200,400] (200x100) */ ScrollResult result = new ScrollResult(); result.requestedArea = new Rect(requestRect); // 0) adjust the requestRect to account for scroll change since start // // Scroll Bounds[50,50 - 250,250] (w=200,h=200) Loading @@ -99,6 +92,17 @@ public class ScrollViewCaptureHelper implements ScrollCaptureViewHelper<ViewGrou // (y-100) (scrollY - mStartScrollY) int scrollDelta = view.getScrollY() - mStartScrollY; final ScrollResult result = new ScrollResult(); result.requestedArea = new Rect(requestRect); result.scrollDelta = scrollDelta; result.availableArea = new Rect(); final View contentView = view.getChildAt(0); // returns null, does not throw IOOBE if (contentView == null) { // No child view? Cannot continue. return result; } // 1) Translate request rect to make it relative to container view // // Container View [0,0 - 300,300] (scrollY=200) Loading Loading @@ -133,7 +137,7 @@ public class ScrollViewCaptureHelper implements ScrollCaptureViewHelper<ViewGrou // TODO: crop capture area to avoid occlusions/minimize scroll changes Point offset = new Point(); final Rect available = new Rect(requestedContentBounds); // empty final Rect available = new Rect(requestedContentBounds); if (!view.getChildVisibleRect(contentView, available, offset)) { available.setEmpty(); result.availableArea = available; Loading