diff --git a/core/java/com/android/internal/view/RecyclerViewCaptureHelper.java b/core/java/com/android/internal/view/RecyclerViewCaptureHelper.java new file mode 100644 index 0000000000000..461e116685097 --- /dev/null +++ b/core/java/com/android/internal/view/RecyclerViewCaptureHelper.java @@ -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 RecyclerView-like ViewGroups. + *

+ * Requirements for proper operation: + *

+ * + * @see ScrollCaptureViewSupport + */ +public class RecyclerViewCaptureHelper implements ScrollCaptureViewHelper { + + // Experiment + private static final boolean DISABLE_ANIMATORS = false; + // Experiment + private static final boolean STOP_RENDER_THREAD = false; + + private static final String TAG = "RVCaptureHelper"; + private int mScrollDelta; + private boolean mScrollBarWasEnabled; + private int mOverScrollMode; + private float mDurationScale; + + @Override + public void onPrepareForStart(@NonNull ViewGroup view, Rect scrollBounds) { + 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 parent + * @return a child view within parent matching the criteria or null + */ + static View findChildNearestTarget(ViewGroup parent, Rect targetRect) { + View selected = null; + int minCenterDistance = Integer.MAX_VALUE; + int maxOverlap = 0; + + // allowable center-center distance, relative to targetRect. + // if within this range, taller views are preferred + final float preferredRangeFromCenterPercent = 0.25f; + final int preferredDistance = + (int) (preferredRangeFromCenterPercent * targetRect.height()); + + Rect parentLocalVis = new Rect(); + parent.getLocalVisibleRect(parentLocalVis); + Log.d(TAG, "findChildNearestTarget: parentVis=" + parentLocalVis + + " targetRect=" + targetRect); + + Rect frame = new Rect(); + for (int i = 0; i < parent.getChildCount(); i++) { + final View child = parent.getChildAt(i); + child.getHitRect(frame); + Log.d(TAG, "child #" + i + " hitRect=" + frame); + + if (child.getVisibility() != View.VISIBLE) { + Log.d(TAG, "child #" + i + " is not visible"); + continue; + } + + int centerDistance = Math.abs(targetRect.centerY() - frame.centerY()); + Log.d(TAG, "child #" + i + " : center to center: " + centerDistance + "px"); + + if (centerDistance < minCenterDistance) { + // closer to center + minCenterDistance = centerDistance; + selected = child; + } else if (frame.intersect(targetRect) && (frame.height() > preferredDistance)) { + // within X% pixels of center, but taller + selected = child; + } + } + return selected; + } + + + @Override + public void onPrepareForEnd(@NonNull ViewGroup view) { + // Restore original position and state + view.scrollBy(0, mScrollDelta); + view.setOverScrollMode(mOverScrollMode); + view.setVerticalScrollBarEnabled(mScrollBarWasEnabled); + if (DISABLE_ANIMATORS) { + ValueAnimator.setDurationScale(mDurationScale); + } + if (STOP_RENDER_THREAD) { + view.getThreadedRenderer().start(); + } + } +} diff --git a/core/java/com/android/internal/view/ScrollCaptureInternal.java b/core/java/com/android/internal/view/ScrollCaptureInternal.java index c589afdeaa1a2..ae1a815910ed4 100644 --- a/core/java/com/android/internal/view/ScrollCaptureInternal.java +++ b/core/java/com/android/internal/view/ScrollCaptureInternal.java @@ -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; + } } diff --git a/core/java/com/android/internal/view/ScrollCaptureViewHelper.java b/core/java/com/android/internal/view/ScrollCaptureViewHelper.java index a92e978b2fc13..9829d0b7ae8d6 100644 --- a/core/java/com/android/internal/view/ScrollCaptureViewHelper.java +++ b/core/java/com/android/internal/view/ScrollCaptureViewHelper.java @@ -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 { * @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 { * * @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 { * @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 { * 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. diff --git a/core/java/com/android/internal/view/ScrollCaptureViewSupport.java b/core/java/com/android/internal/view/ScrollCaptureViewSupport.java index 7b4f73ffde2b8..85fa791b429c4 100644 --- a/core/java/com/android/internal/view/ScrollCaptureViewSupport.java +++ b/core/java/com/android/internal/view/ScrollCaptureViewSupport.java @@ -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 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 mWeakView; private final ScrollCaptureViewHelper mViewHelper; private ViewRenderer mRenderer; @@ -99,12 +103,16 @@ public class ScrollCaptureViewSupport 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 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 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 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 implements ScrollCaptureCa } public void setSurface(Surface surface) { + mSurface = surface; mRenderer.setSurface(surface); } @@ -223,20 +239,38 @@ public class ScrollCaptureViewSupport 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 implements ScrollCaptureCa } public void destroy() { + mSurface = null; mRenderer.destroy(); } @@ -254,6 +289,5 @@ public class ScrollCaptureViewSupport implements ScrollCaptureCa mTempMatrix.mapRect(mTempRectF); mTempRectF.round(outRect); } - } } diff --git a/core/java/com/android/internal/view/ScrollViewCaptureHelper.java b/core/java/com/android/internal/view/ScrollViewCaptureHelper.java index 1514b9a285dd8..a1d202e3a39f7 100644 --- a/core/java/com/android/internal/view/ScrollViewCaptureHelper.java +++ b/core/java/com/android/internal/view/ScrollViewCaptureHelper.java @@ -57,10 +57,6 @@ public class ScrollViewCaptureHelper implements ScrollCaptureViewHelper { + 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); + } + } +} diff --git a/packages/SystemUI/tests/AndroidManifest.xml b/packages/SystemUI/tests/AndroidManifest.xml index db878458721e7..2b4ed4e8f1cdb 100644 --- a/packages/SystemUI/tests/AndroidManifest.xml +++ b/packages/SystemUI/tests/AndroidManifest.xml @@ -82,6 +82,10 @@ + + + { + 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; + } + } +}