Merge "Adds scroll capture support for RecyclerView"
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -173,6 +188,7 @@ public class ScrollCaptureViewSupport<V extends View> implements ScrollCaptureCa
|
||||
}
|
||||
|
||||
public void setSurface(Surface surface) {
|
||||
mSurface = surface;
|
||||
mRenderer.setSurface(surface);
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
@@ -244,6 +278,7 @@ public class ScrollCaptureViewSupport<V extends View> implements ScrollCaptureCa
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
mSurface = null;
|
||||
mRenderer.destroy();
|
||||
}
|
||||
|
||||
@@ -254,6 +289,5 @@ public class ScrollCaptureViewSupport<V extends View> implements ScrollCaptureCa
|
||||
mTempMatrix.mapRect(mTempRectF);
|
||||
mTempRectF.round(outRect);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
| |
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
/*
|
||||
* 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 android.view.ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
|
||||
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import android.app.Instrumentation;
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Rect;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.test.annotation.UiThreadTest;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
|
||||
import com.android.internal.view.ScrollCaptureViewHelper.ScrollResult;
|
||||
import com.android.internal.widget.LinearLayoutManager;
|
||||
import com.android.internal.widget.RecyclerView;
|
||||
|
||||
import com.google.common.truth.Truth;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class RecyclerViewCaptureHelperTest {
|
||||
private static final int CHILD_VIEWS = 12;
|
||||
private static final int CHILD_VIEW_HEIGHT = 300;
|
||||
private static final int WINDOW_WIDTH = 800;
|
||||
private static final int WINDOW_HEIGHT = 1200;
|
||||
private static final int CAPTURE_HEIGHT = 600;
|
||||
|
||||
private FrameLayout mParent;
|
||||
private RecyclerView mTarget;
|
||||
private WindowManager mWm;
|
||||
|
||||
private WindowManager.LayoutParams mWindowLayoutParams;
|
||||
|
||||
private Context mContext;
|
||||
private float mDensity;
|
||||
private LinearLayoutManager mLinearLayoutManager;
|
||||
private Instrumentation mInstrumentation;
|
||||
|
||||
@Before
|
||||
@UiThreadTest
|
||||
public void setUp() {
|
||||
mInstrumentation = InstrumentationRegistry.getInstrumentation();
|
||||
mContext = mInstrumentation.getContext();
|
||||
mDensity = mContext.getResources().getDisplayMetrics().density;
|
||||
|
||||
mParent = new FrameLayout(mContext);
|
||||
|
||||
mTarget = new RecyclerView(mContext);
|
||||
mParent.addView(mTarget, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
|
||||
|
||||
mTarget.setAdapter(new TestAdapter());
|
||||
mLinearLayoutManager =
|
||||
new LinearLayoutManager(mContext, LinearLayoutManager.VERTICAL, false);
|
||||
mTarget.setLayoutManager(mLinearLayoutManager);
|
||||
mWm = mContext.getSystemService(WindowManager.class);
|
||||
|
||||
// Setup the window that we are going to use
|
||||
mWindowLayoutParams = new WindowManager.LayoutParams(WINDOW_WIDTH, WINDOW_HEIGHT,
|
||||
TYPE_APPLICATION_OVERLAY, FLAG_NOT_TOUCHABLE, PixelFormat.OPAQUE);
|
||||
mWindowLayoutParams.setTitle("ScrollViewCaptureHelper");
|
||||
mWindowLayoutParams.gravity = Gravity.CENTER;
|
||||
mWm.addView(mParent, mWindowLayoutParams);
|
||||
}
|
||||
|
||||
@After
|
||||
@UiThreadTest
|
||||
public void tearDown() {
|
||||
mWm.removeViewImmediate(mParent);
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void onScrollRequested_up_fromTop() {
|
||||
mTarget.scrollBy(0, -(WINDOW_HEIGHT * 3));
|
||||
// mTarget.createSnapshot(new ViewDebug.HardwareCanvasProvider(), false);
|
||||
|
||||
RecyclerViewCaptureHelper rvc = new RecyclerViewCaptureHelper();
|
||||
Rect scrollBounds = rvc.onComputeScrollBounds(mTarget);
|
||||
rvc.onPrepareForStart(mTarget, scrollBounds);
|
||||
|
||||
assertThat(scrollBounds.height()).isGreaterThan(CAPTURE_HEIGHT);
|
||||
|
||||
Rect request = new Rect(0, -CAPTURE_HEIGHT, scrollBounds.width(), 0);
|
||||
|
||||
ScrollResult scrollResult = rvc.onScrollRequested(mTarget,
|
||||
scrollBounds, request);
|
||||
|
||||
// The result is an empty rectangle and no scrolling, since it
|
||||
// is not possible to physically scroll further up to make the
|
||||
// requested area visible at all (it doesn't exist).
|
||||
assertEmpty(scrollResult.availableArea);
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void onScrollRequested_down_fromTop() {
|
||||
mTarget.scrollBy(0, -(WINDOW_HEIGHT * 3));
|
||||
|
||||
RecyclerViewCaptureHelper rvc = new RecyclerViewCaptureHelper();
|
||||
Rect scrollBounds = rvc.onComputeScrollBounds(mTarget);
|
||||
rvc.onPrepareForStart(mTarget, scrollBounds);
|
||||
|
||||
assertThat(scrollBounds.height()).isGreaterThan(CAPTURE_HEIGHT);
|
||||
|
||||
// Capture between y = +1200 to +1800 pixels BELOW current top
|
||||
Rect request = new Rect(0, WINDOW_HEIGHT, scrollBounds.width(),
|
||||
WINDOW_HEIGHT + CAPTURE_HEIGHT);
|
||||
|
||||
ScrollResult scrollResult = rvc.onScrollRequested(mTarget, scrollBounds, request);
|
||||
assertThat(request).isEqualTo(scrollResult.requestedArea);
|
||||
assertThat(request).isEqualTo(scrollResult.availableArea);
|
||||
assertThat(scrollResult.scrollDelta).isEqualTo(CAPTURE_HEIGHT);
|
||||
assertAvailableAreaCompletelyVisible(scrollResult, mTarget);
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void onScrollRequested_up_fromMiddle() {
|
||||
mTarget.scrollBy(0, WINDOW_HEIGHT);
|
||||
|
||||
RecyclerViewCaptureHelper helper = new RecyclerViewCaptureHelper();
|
||||
Rect scrollBounds = helper.onComputeScrollBounds(mTarget);
|
||||
helper.onPrepareForStart(mTarget, scrollBounds);
|
||||
|
||||
Rect request = new Rect(0, -CAPTURE_HEIGHT, scrollBounds.width(), 0);
|
||||
|
||||
ScrollResult scrollResult = helper.onScrollRequested(mTarget, scrollBounds, request);
|
||||
assertThat(request).isEqualTo(scrollResult.requestedArea);
|
||||
assertThat(request).isEqualTo(scrollResult.availableArea);
|
||||
assertThat(scrollResult.scrollDelta).isEqualTo(-CAPTURE_HEIGHT);
|
||||
assertAvailableAreaCompletelyVisible(scrollResult, mTarget);
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void onScrollRequested_down_fromMiddle() {
|
||||
mTarget.scrollBy(0, WINDOW_HEIGHT);
|
||||
|
||||
RecyclerViewCaptureHelper helper = new RecyclerViewCaptureHelper();
|
||||
Rect scrollBounds = helper.onComputeScrollBounds(mTarget);
|
||||
helper.onPrepareForStart(mTarget, scrollBounds);
|
||||
|
||||
Rect request = new Rect(0, WINDOW_HEIGHT, scrollBounds.width(),
|
||||
WINDOW_HEIGHT + CAPTURE_HEIGHT);
|
||||
|
||||
ScrollResult scrollResult = helper.onScrollRequested(mTarget, scrollBounds, request);
|
||||
assertThat(request).isEqualTo(scrollResult.requestedArea);
|
||||
assertThat(request).isEqualTo(scrollResult.availableArea);
|
||||
assertThat(scrollResult.scrollDelta).isEqualTo(CAPTURE_HEIGHT);
|
||||
assertAvailableAreaCompletelyVisible(scrollResult, mTarget);
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void onScrollRequested_up_fromBottom() {
|
||||
mTarget.scrollBy(0, WINDOW_HEIGHT * 2);
|
||||
|
||||
RecyclerViewCaptureHelper helper = new RecyclerViewCaptureHelper();
|
||||
Rect scrollBounds = helper.onComputeScrollBounds(mTarget);
|
||||
helper.onPrepareForStart(mTarget, scrollBounds);
|
||||
|
||||
Rect request = new Rect(0, -CAPTURE_HEIGHT, scrollBounds.width(), 0);
|
||||
|
||||
ScrollResult scrollResult = helper.onScrollRequested(mTarget, scrollBounds, request);
|
||||
assertThat(request).isEqualTo(scrollResult.requestedArea);
|
||||
assertThat(request).isEqualTo(scrollResult.availableArea);
|
||||
assertThat(scrollResult.scrollDelta).isEqualTo(-CAPTURE_HEIGHT);
|
||||
assertAvailableAreaCompletelyVisible(scrollResult, mTarget);
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void onScrollRequested_down_fromBottom() {
|
||||
mTarget.scrollBy(0, WINDOW_HEIGHT * 3);
|
||||
|
||||
RecyclerViewCaptureHelper rvc = new RecyclerViewCaptureHelper();
|
||||
Rect scrollBounds = rvc.onComputeScrollBounds(mTarget);
|
||||
rvc.onPrepareForStart(mTarget, scrollBounds);
|
||||
|
||||
Rect request = new Rect(0, WINDOW_HEIGHT, scrollBounds.width(),
|
||||
WINDOW_HEIGHT + CAPTURE_HEIGHT);
|
||||
|
||||
ScrollResult scrollResult = rvc.onScrollRequested(mTarget,
|
||||
scrollBounds, request);
|
||||
Truth.assertThat(request).isEqualTo(scrollResult.requestedArea);
|
||||
|
||||
// The result is an empty rectangle and no scrolling, since it
|
||||
// is not possible to physically scroll further down to make the
|
||||
// requested area visible at all (it doesn't exist).
|
||||
assertEmpty(scrollResult.availableArea);
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void onScrollRequested_offTopEdge() {
|
||||
mTarget.scrollBy(0, -(WINDOW_HEIGHT * 3));
|
||||
|
||||
RecyclerViewCaptureHelper helper = new RecyclerViewCaptureHelper();
|
||||
Rect scrollBounds = helper.onComputeScrollBounds(mTarget);
|
||||
helper.onPrepareForStart(mTarget, scrollBounds);
|
||||
|
||||
// Create a request which lands halfway off the top of the content
|
||||
//from -1500 to -900, (starting at 1200 = -300 to +300 within the content)
|
||||
int top = 0;
|
||||
Rect request = new Rect(
|
||||
0, top - (CAPTURE_HEIGHT / 2),
|
||||
scrollBounds.width(), top + (CAPTURE_HEIGHT / 2));
|
||||
|
||||
ScrollResult scrollResult = helper.onScrollRequested(mTarget, scrollBounds, request);
|
||||
assertThat(request).isEqualTo(scrollResult.requestedArea);
|
||||
|
||||
ScrollResult result = helper.onScrollRequested(mTarget, scrollBounds, request);
|
||||
// The result is a partial result
|
||||
Rect expectedResult = new Rect(request);
|
||||
expectedResult.top += (CAPTURE_HEIGHT / 2); // top half clipped
|
||||
assertThat(expectedResult).isEqualTo(result.availableArea);
|
||||
assertThat(scrollResult.scrollDelta).isEqualTo(0);
|
||||
assertAvailableAreaPartiallyVisible(scrollResult, mTarget);
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void onScrollRequested_offBottomEdge() {
|
||||
mTarget.scrollBy(0, WINDOW_HEIGHT * 2);
|
||||
|
||||
RecyclerViewCaptureHelper helper = new RecyclerViewCaptureHelper();
|
||||
Rect scrollBounds = helper.onComputeScrollBounds(mTarget);
|
||||
helper.onPrepareForStart(mTarget, scrollBounds);
|
||||
|
||||
// Create a request which lands halfway off the bottom of the content
|
||||
//from 600 to to 1200, (starting at 2400 = 3000 to 3600 within the content)
|
||||
|
||||
int bottom = WINDOW_HEIGHT;
|
||||
Rect request = new Rect(
|
||||
0, bottom - (CAPTURE_HEIGHT / 2),
|
||||
scrollBounds.width(), bottom + (CAPTURE_HEIGHT / 2));
|
||||
|
||||
ScrollResult result = helper.onScrollRequested(mTarget, scrollBounds, request);
|
||||
|
||||
Rect expectedResult = new Rect(request);
|
||||
expectedResult.bottom -= 300; // bottom half clipped
|
||||
assertThat(expectedResult).isEqualTo(result.availableArea);
|
||||
assertThat(result.scrollDelta).isEqualTo(0);
|
||||
assertAvailableAreaPartiallyVisible(result, mTarget);
|
||||
}
|
||||
|
||||
static final class TestViewHolder extends RecyclerView.ViewHolder {
|
||||
TestViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
}
|
||||
}
|
||||
|
||||
static final class TestAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
private final Random mRandom = new Random();
|
||||
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
return new TestViewHolder(new TextView(parent.getContext()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
|
||||
TextView view = (TextView) holder.itemView;
|
||||
view.setText("Child #" + position);
|
||||
view.setTextColor(Color.WHITE);
|
||||
view.setTextSize(30f);
|
||||
view.setBackgroundColor(Color.rgb(mRandom.nextFloat(), mRandom.nextFloat(),
|
||||
mRandom.nextFloat()));
|
||||
view.setMinHeight(CHILD_VIEW_HEIGHT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return CHILD_VIEWS;
|
||||
}
|
||||
}
|
||||
|
||||
static void assertEmpty(Rect r) {
|
||||
if (r != null && !r.isEmpty()) {
|
||||
fail("Not true that " + r + " is empty");
|
||||
}
|
||||
}
|
||||
|
||||
static Rect getVisibleRect(View v) {
|
||||
Rect r = new Rect(0, 0, v.getWidth(), v.getHeight());
|
||||
v.getLocalVisibleRect(r);
|
||||
return r;
|
||||
}
|
||||
|
||||
static void assertAvailableAreaCompletelyVisible(ScrollResult result, View container) {
|
||||
Rect requested = new Rect(result.availableArea);
|
||||
requested.offset(0, -result.scrollDelta); // make relative
|
||||
Rect localVisible = getVisibleRect(container);
|
||||
if (!localVisible.contains(requested)) {
|
||||
fail("Not true that all of " + requested + " is contained by " + localVisible);
|
||||
}
|
||||
}
|
||||
|
||||
static void assertAvailableAreaPartiallyVisible(ScrollResult result, View container) {
|
||||
Rect requested = new Rect(result.availableArea);
|
||||
requested.offset(0, -result.scrollDelta); // make relative
|
||||
Rect localVisible = getVisibleRect(container);
|
||||
if (!Rect.intersects(localVisible, requested)) {
|
||||
fail("Not true that any of " + requested + " is contained by " + localVisible);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,6 +82,10 @@
|
||||
|
||||
<activity android:name="com.android.systemui.screenshot.ScrollViewActivity"
|
||||
android:exported="false" />
|
||||
|
||||
<activity android:name="com.android.systemui.screenshot.RecyclerViewActivity"
|
||||
android:exported="false" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.lifecycle.ProcessLifecycleOwnerInitializer"
|
||||
tools:replace="android:authorities"
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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.systemui.screenshot;
|
||||
|
||||
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.internal.widget.LinearLayoutManager;
|
||||
import com.android.internal.widget.RecyclerView;
|
||||
import com.android.internal.widget.RecyclerView.LayoutParams;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
public class RecyclerViewActivity extends Activity {
|
||||
public static final int CHILD_VIEW_HEIGHT = 300;
|
||||
private static final int CHILD_VIEWS = 12;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
RecyclerView recyclerView = new RecyclerView(this);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||
recyclerView.setAdapter(new TestAdapter());
|
||||
recyclerView.setLayoutParams(new LayoutParams(MATCH_PARENT, MATCH_PARENT));
|
||||
setContentView(recyclerView);
|
||||
}
|
||||
|
||||
static final class TestViewHolder extends RecyclerView.ViewHolder {
|
||||
TestViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
}
|
||||
}
|
||||
|
||||
static final class TestAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
private final Random mRandom = new Random();
|
||||
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
return new TestViewHolder(new TextView(parent.getContext()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
|
||||
TextView view = (TextView) holder.itemView;
|
||||
view.setText("Child #" + position);
|
||||
view.setTextColor(Color.WHITE);
|
||||
view.setTextSize(30f);
|
||||
view.setBackgroundColor(
|
||||
Color.rgb(mRandom.nextFloat(), mRandom.nextFloat(), mRandom.nextFloat()));
|
||||
view.setMinHeight(CHILD_VIEW_HEIGHT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return CHILD_VIEWS;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user