diff options
author | Chris Craik <ccraik@google.com> | 2016-03-02 18:52:33 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2016-03-02 18:52:35 +0000 |
commit | b67985f337e1592f621e1b85f525098887804ccd (patch) | |
tree | fb73aa42cc5c678f1922a7efe971a3701ed9e0a7 | |
parent | b4672ef6dfbaf416981539d515ad1489d7d0bc87 (diff) | |
parent | a748c08241e43fc68c7c34767d819aef5183936e (diff) |
Merge changes I4f72448f,I74b7233c into nyc-dev
* changes:
Fix ripple positioning within scrolled node
Clip projected ripples to outlines
-rw-r--r-- | libs/hwui/BakedOpDispatcher.cpp | 7 | ||||
-rw-r--r-- | libs/hwui/BakedOpState.cpp | 21 | ||||
-rw-r--r-- | libs/hwui/BakedOpState.h | 5 | ||||
-rw-r--r-- | libs/hwui/FrameBuilder.cpp | 44 | ||||
-rw-r--r-- | libs/hwui/LayerBuilder.cpp | 5 | ||||
-rw-r--r-- | libs/hwui/OpenGLRenderer.cpp | 6 | ||||
-rw-r--r-- | libs/hwui/Snapshot.cpp | 10 | ||||
-rw-r--r-- | libs/hwui/Snapshot.h | 8 | ||||
-rw-r--r-- | libs/hwui/tests/unit/FrameBuilderTests.cpp | 151 | ||||
-rw-r--r-- | tests/HwAccelerationTest/res/layout/projection_clipping.xml | 36 |
10 files changed, 247 insertions, 46 deletions
diff --git a/libs/hwui/BakedOpDispatcher.cpp b/libs/hwui/BakedOpDispatcher.cpp index 1aab3c79dcfd..80180068c44a 100644 --- a/libs/hwui/BakedOpDispatcher.cpp +++ b/libs/hwui/BakedOpDispatcher.cpp @@ -30,6 +30,7 @@ #include <algorithm> #include <math.h> #include <SkPaintDefaults.h> +#include <SkPathOps.h> namespace android { namespace uirenderer { @@ -527,6 +528,12 @@ void BakedOpDispatcher::onOvalOp(BakedOpRenderer& renderer, const OvalOp& op, co SkPath path; SkRect rect = getBoundsOfFill(op); path.addOval(rect); + + if (state.computedState.localProjectionPathMask != nullptr) { + // Mask the ripple path by the local space projection mask in local space. + // Note that this can create CCW paths. + Op(path, *state.computedState.localProjectionPathMask, kIntersect_SkPathOp, &path); + } renderConvexPath(renderer, state, path, *(op.paint)); } } diff --git a/libs/hwui/BakedOpState.cpp b/libs/hwui/BakedOpState.cpp index 682bd045098d..26653f77daeb 100644 --- a/libs/hwui/BakedOpState.cpp +++ b/libs/hwui/BakedOpState.cpp @@ -63,9 +63,22 @@ ResolvedRenderState::ResolvedRenderState(LinearAllocator& allocator, Snapshot& s clipState = nullptr; clippedBounds.setEmpty(); } else { - // Not rejected! compute true clippedBounds and clipSideFlags + // Not rejected! compute true clippedBounds, clipSideFlags, and path mask clipSideFlags = computeClipSideFlags(clipRect, clippedBounds); clippedBounds.doIntersect(clipRect); + + if (CC_UNLIKELY(snapshot.projectionPathMask)) { + // map projection path mask from render target space into op space, + // so intersection with op geometry is possible + Matrix4 inverseTransform; + inverseTransform.loadInverse(transform); + SkMatrix skInverseTransform; + inverseTransform.copyTo(skInverseTransform); + + auto localMask = allocator.create<SkPath>(); + snapshot.projectionPathMask->transform(skInverseTransform, localMask); + localProjectionPathMask = localMask; + } } } @@ -73,13 +86,15 @@ ResolvedRenderState::ResolvedRenderState(LinearAllocator& allocator, Snapshot& s : transform(*snapshot.transform) , clipState(snapshot.mutateClipArea().serializeClip(allocator)) , clippedBounds(clipState->rect) - , clipSideFlags(OpClipSideFlags::Full) {} + , clipSideFlags(OpClipSideFlags::Full) + , localProjectionPathMask(nullptr) {} ResolvedRenderState::ResolvedRenderState(const ClipRect* clipRect, const Rect& dstRect) : transform(Matrix4::identity()) , clipState(clipRect) , clippedBounds(dstRect) - , clipSideFlags(computeClipSideFlags(clipRect->rect, dstRect)) { + , clipSideFlags(computeClipSideFlags(clipRect->rect, dstRect)) + , localProjectionPathMask(nullptr) { clippedBounds.doIntersect(clipRect->rect); } diff --git a/libs/hwui/BakedOpState.h b/libs/hwui/BakedOpState.h index 4365ef870dda..ffe2901782a9 100644 --- a/libs/hwui/BakedOpState.h +++ b/libs/hwui/BakedOpState.h @@ -88,6 +88,7 @@ public: const ClipBase* clipState = nullptr; Rect clippedBounds; int clipSideFlags = 0; + const SkPath* localProjectionPathMask = nullptr; }; /** @@ -154,7 +155,6 @@ public: // simple state (straight pointer/value storage): const float alpha; const RoundRectClipState* roundRectClipState; - const ProjectionPathMask* projectionPathMask; const RecordedOp* op; private: @@ -165,21 +165,18 @@ private: : computedState(allocator, snapshot, recordedOp, expandForStroke) , alpha(snapshot.alpha) , roundRectClipState(snapshot.roundRectClipState) - , projectionPathMask(snapshot.projectionPathMask) , op(&recordedOp) {} BakedOpState(LinearAllocator& allocator, Snapshot& snapshot, const ShadowOp* shadowOpPtr) : computedState(allocator, snapshot) , alpha(snapshot.alpha) , roundRectClipState(snapshot.roundRectClipState) - , projectionPathMask(snapshot.projectionPathMask) , op(shadowOpPtr) {} BakedOpState(const ClipRect* clipRect, const Rect& dstRect, const RecordedOp& recordedOp) : computedState(clipRect, dstRect) , alpha(1.0f) , roundRectClipState(nullptr) - , projectionPathMask(nullptr) , op(&recordedOp) {} }; diff --git a/libs/hwui/FrameBuilder.cpp b/libs/hwui/FrameBuilder.cpp index 04de98afe85c..1bc37e25f934 100644 --- a/libs/hwui/FrameBuilder.cpp +++ b/libs/hwui/FrameBuilder.cpp @@ -389,34 +389,38 @@ void FrameBuilder::deferShadow(const RenderNodeOp& casterNodeOp) { } void FrameBuilder::deferProjectedChildren(const RenderNode& renderNode) { - const SkPath* projectionReceiverOutline = renderNode.properties().getOutline().getPath(); int count = mCanvasState.save(SaveFlags::MatrixClip); + const SkPath* projectionReceiverOutline = renderNode.properties().getOutline().getPath(); - // can't be null, since DL=null node rejection happens before deferNodePropsAndOps - const DisplayList& displayList = *(renderNode.getDisplayList()); + SkPath transformedMaskPath; // on stack, since BakedOpState makes a deep copy + if (projectionReceiverOutline) { + // transform the mask for this projector into render target space + // TODO: consider combining both transforms by stashing transform instead of applying + SkMatrix skCurrentTransform; + mCanvasState.currentTransform()->copyTo(skCurrentTransform); + projectionReceiverOutline->transform( + skCurrentTransform, + &transformedMaskPath); + mCanvasState.setProjectionPathMask(mAllocator, &transformedMaskPath); + } - const RecordedOp* op = (displayList.getOps()[displayList.projectionReceiveIndex]); - const RenderNodeOp* backgroundOp = static_cast<const RenderNodeOp*>(op); - const RenderProperties& backgroundProps = backgroundOp->renderNode->properties(); + for (size_t i = 0; i < renderNode.mProjectedNodes.size(); i++) { + RenderNodeOp* childOp = renderNode.mProjectedNodes[i]; + RenderNode& childNode = *childOp->renderNode; - // Transform renderer to match background we're projecting onto - // (by offsetting canvas by translationX/Y of background rendernode, since only those are set) - mCanvasState.translate(backgroundProps.getTranslationX(), backgroundProps.getTranslationY()); + // Draw child if it has content, but ignore state in childOp - matrix already applied to + // transformFromCompositingAncestor, and record-time clip is ignored when projecting + if (!childNode.nothingToDraw()) { + int restoreTo = mCanvasState.save(SaveFlags::MatrixClip); - // If the projection receiver has an outline, we mask projected content to it - // (which we know, apriori, are all tessellated paths) - mCanvasState.setProjectionPathMask(mAllocator, projectionReceiverOutline); + // Apply transform between ancestor and projected descendant + mCanvasState.concatMatrix(childOp->transformFromCompositingAncestor); - // draw projected nodes - for (size_t i = 0; i < renderNode.mProjectedNodes.size(); i++) { - RenderNodeOp* childOp = renderNode.mProjectedNodes[i]; + deferNodePropsAndOps(childNode); - int restoreTo = mCanvasState.save(SaveFlags::Matrix); - mCanvasState.concatMatrix(childOp->transformFromCompositingAncestor); - deferRenderNodeOpImpl(*childOp); - mCanvasState.restoreToCount(restoreTo); + mCanvasState.restoreToCount(restoreTo); + } } - mCanvasState.restoreToCount(count); } diff --git a/libs/hwui/LayerBuilder.cpp b/libs/hwui/LayerBuilder.cpp index bc39621f2cb2..c5af279653d9 100644 --- a/libs/hwui/LayerBuilder.cpp +++ b/libs/hwui/LayerBuilder.cpp @@ -140,7 +140,10 @@ public: // Identical round rect clip state means both ops will clip in the same way, or not at all. // As the state objects are const, we can compare their pointers to determine mergeability if (lhs->roundRectClipState != rhs->roundRectClipState) return false; - if (lhs->projectionPathMask != rhs->projectionPathMask) return false; + + // Local masks prevent merge, since they're potentially in different coordinate spaces + if (lhs->computedState.localProjectionPathMask + || rhs->computedState.localProjectionPathMask) return false; /* Clipping compatibility check * diff --git a/libs/hwui/OpenGLRenderer.cpp b/libs/hwui/OpenGLRenderer.cpp index b7a5923cdd65..7693fdcbe817 100644 --- a/libs/hwui/OpenGLRenderer.cpp +++ b/libs/hwui/OpenGLRenderer.cpp @@ -1148,7 +1148,9 @@ bool OpenGLRenderer::storeDisplayState(DeferredDisplayState& state, int stateDef // always store/restore, since these are just pointers state.mRoundRectClipState = currentSnapshot()->roundRectClipState; +#if !HWUI_NEW_OPS state.mProjectionPathMask = currentSnapshot()->projectionPathMask; +#endif return false; } @@ -1156,7 +1158,9 @@ void OpenGLRenderer::restoreDisplayState(const DeferredDisplayState& state, bool setGlobalMatrix(state.mMatrix); writableSnapshot()->alpha = state.mAlpha; writableSnapshot()->roundRectClipState = state.mRoundRectClipState; +#if !HWUI_NEW_OPS writableSnapshot()->projectionPathMask = state.mProjectionPathMask; +#endif if (state.mClipValid && !skipClipRestore) { writableSnapshot()->setClip(state.mClip.left, state.mClip.top, @@ -1833,6 +1837,7 @@ void OpenGLRenderer::drawCircle(float x, float y, float radius, const SkPaint* p path.addCircle(x, y, radius); } +#if !HWUI_NEW_OPS if (CC_UNLIKELY(currentSnapshot()->projectionPathMask != nullptr)) { // mask ripples with projection mask SkPath maskPath = *(currentSnapshot()->projectionPathMask->projectionMask); @@ -1852,6 +1857,7 @@ void OpenGLRenderer::drawCircle(float x, float y, float radius, const SkPaint* p // in local space. Note that this can create CCW paths. Op(path, maskPath, kIntersect_SkPathOp, &path); } +#endif drawConvexPath(path, p); } diff --git a/libs/hwui/Snapshot.cpp b/libs/hwui/Snapshot.cpp index 27fea1ff59f5..cf5e69a1e6ae 100644 --- a/libs/hwui/Snapshot.cpp +++ b/libs/hwui/Snapshot.cpp @@ -146,6 +146,9 @@ void Snapshot::resetTransform(float x, float y, float z) { } void Snapshot::buildScreenSpaceTransform(Matrix4* outTransform) const { +#if HWUI_NEW_OPS + LOG_ALWAYS_FATAL("not supported - not needed by new ops"); +#else // build (reverse ordered) list of the stack of snapshots, terminated with a NULL Vector<const Snapshot*> snapshotList; snapshotList.push(nullptr); @@ -171,6 +174,7 @@ void Snapshot::buildScreenSpaceTransform(Matrix4* outTransform) const { outTransform->multiply(*(current->transform)); } } +#endif } /////////////////////////////////////////////////////////////////////////////// @@ -223,15 +227,19 @@ void Snapshot::setClippingRoundRect(LinearAllocator& allocator, const Rect& boun } void Snapshot::setProjectionPathMask(LinearAllocator& allocator, const SkPath* path) { +#if HWUI_NEW_OPS + // TODO: remove allocator param for HWUI_NEW_OPS + projectionPathMask = path; +#else if (path) { ProjectionPathMask* mask = new (allocator) ProjectionPathMask; mask->projectionMask = path; buildScreenSpaceTransform(&(mask->projectionMaskTransform)); - projectionPathMask = mask; } else { projectionPathMask = nullptr; } +#endif } /////////////////////////////////////////////////////////////////////////////// diff --git a/libs/hwui/Snapshot.h b/libs/hwui/Snapshot.h index b03643f06f1c..3a01d049109c 100644 --- a/libs/hwui/Snapshot.h +++ b/libs/hwui/Snapshot.h @@ -63,6 +63,7 @@ public: float radius; }; +// TODO: remove for HWUI_NEW_OPS class ProjectionPathMask { public: static void* operator new(size_t size) = delete; @@ -219,6 +220,7 @@ public: * Fills outTransform with the current, total transform to screen space, * across layer boundaries. */ + // TODO: remove for HWUI_NEW_OPS void buildScreenSpaceTransform(Matrix4* outTransform) const; /** @@ -294,9 +296,13 @@ public: const RoundRectClipState* roundRectClipState; /** - * Current projection masking path - used exclusively to mask tessellated circles. + * Current projection masking path - used exclusively to mask projected, tessellated circles. */ +#if HWUI_NEW_OPS + const SkPath* projectionPathMask; +#else const ProjectionPathMask* projectionPathMask; +#endif void dump() const; diff --git a/libs/hwui/tests/unit/FrameBuilderTests.cpp b/libs/hwui/tests/unit/FrameBuilderTests.cpp index f86898fd669a..8802d07de753 100644 --- a/libs/hwui/tests/unit/FrameBuilderTests.cpp +++ b/libs/hwui/tests/unit/FrameBuilderTests.cpp @@ -990,21 +990,26 @@ TEST(FrameBuilder, projectionReorder) { EXPECT_EQ(Rect(100, 100), op.unmappedBounds); EXPECT_EQ(SK_ColorWHITE, op.paint->getColor()); expectedMatrix.loadIdentity(); + EXPECT_EQ(nullptr, state.computedState.localProjectionPathMask); break; case 1: EXPECT_EQ(Rect(-10, -10, 60, 60), op.unmappedBounds); EXPECT_EQ(SK_ColorDKGRAY, op.paint->getColor()); - expectedMatrix.loadTranslate(50, 50, 0); // TODO: should scroll be respected here? + expectedMatrix.loadTranslate(50 - scrollX, 50 - scrollY, 0); + ASSERT_NE(nullptr, state.computedState.localProjectionPathMask); + EXPECT_EQ(Rect(-35, -30, 45, 50), + Rect(state.computedState.localProjectionPathMask->getBounds())); break; case 2: EXPECT_EQ(Rect(100, 50), op.unmappedBounds); EXPECT_EQ(SK_ColorBLUE, op.paint->getColor()); expectedMatrix.loadTranslate(-scrollX, 50 - scrollY, 0); + EXPECT_EQ(nullptr, state.computedState.localProjectionPathMask); break; default: ADD_FAILURE(); } - EXPECT_MATRIX_APPROX_EQ(expectedMatrix, state.computedState.transform); + EXPECT_EQ(expectedMatrix, state.computedState.transform); } }; @@ -1045,6 +1050,9 @@ TEST(FrameBuilder, projectionReorder) { }); auto parent = TestUtils::createNode(0, 0, 100, 100, [&receiverBackground, &child](RenderProperties& properties, RecordingCanvas& canvas) { + // Set a rect outline for the projecting ripple to be masked against. + properties.mutableOutline().setRoundRect(10, 10, 90, 90, 5, 1.0f); + canvas.save(SaveFlags::MatrixClip); canvas.translate(-scrollX, -scrollY); // Apply scroll (note: bg undoes this internally) canvas.drawRenderNode(receiverBackground.get()); @@ -1059,6 +1067,145 @@ TEST(FrameBuilder, projectionReorder) { EXPECT_EQ(3, renderer.getIndex()); } +RENDERTHREAD_TEST(FrameBuilder, projectionHwLayer) { + static const int scrollX = 5; + static const int scrollY = 10; + class ProjectionHwLayerTestRenderer : public TestRendererBase { + public: + void startRepaintLayer(OffscreenBuffer* offscreenBuffer, const Rect& repaintRect) override { + EXPECT_EQ(0, mIndex++); + } + void onArcOp(const ArcOp& op, const BakedOpState& state) override { + EXPECT_EQ(1, mIndex++); + ASSERT_EQ(nullptr, state.computedState.localProjectionPathMask); + } + void endLayer() override { + EXPECT_EQ(2, mIndex++); + } + void onRectOp(const RectOp& op, const BakedOpState& state) override { + EXPECT_EQ(3, mIndex++); + ASSERT_EQ(nullptr, state.computedState.localProjectionPathMask); + } + void onOvalOp(const OvalOp& op, const BakedOpState& state) override { + EXPECT_EQ(4, mIndex++); + ASSERT_NE(nullptr, state.computedState.localProjectionPathMask); + Matrix4 expected; + expected.loadTranslate(100 - scrollX, 100 - scrollY, 0); + EXPECT_EQ(expected, state.computedState.transform); + EXPECT_EQ(Rect(-85, -80, 295, 300), + Rect(state.computedState.localProjectionPathMask->getBounds())); + } + void onLayerOp(const LayerOp& op, const BakedOpState& state) override { + EXPECT_EQ(5, mIndex++); + ASSERT_EQ(nullptr, state.computedState.localProjectionPathMask); + } + }; + auto receiverBackground = TestUtils::createNode(0, 0, 400, 400, + [](RenderProperties& properties, RecordingCanvas& canvas) { + properties.setProjectionReceiver(true); + // scroll doesn't apply to background, so undone via translationX/Y + // NOTE: translationX/Y only! no other transform properties may be set for a proj receiver! + properties.setTranslationX(scrollX); + properties.setTranslationY(scrollY); + + canvas.drawRect(0, 0, 400, 400, SkPaint()); + }); + auto projectingRipple = TestUtils::createNode(0, 0, 200, 200, + [](RenderProperties& properties, RecordingCanvas& canvas) { + properties.setProjectBackwards(true); + properties.setClipToBounds(false); + canvas.drawOval(100, 100, 300, 300, SkPaint()); // drawn mostly out of layer bounds + }); + auto child = TestUtils::createNode(100, 100, 300, 300, + [&projectingRipple](RenderProperties& properties, RecordingCanvas& canvas) { + properties.mutateLayerProperties().setType(LayerType::RenderLayer); + canvas.drawRenderNode(projectingRipple.get()); + canvas.drawArc(0, 0, 200, 200, 0.0f, 280.0f, true, SkPaint()); + }); + auto parent = TestUtils::createNode(0, 0, 400, 400, + [&receiverBackground, &child](RenderProperties& properties, RecordingCanvas& canvas) { + // Set a rect outline for the projecting ripple to be masked against. + properties.mutableOutline().setRoundRect(10, 10, 390, 390, 0, 1.0f); + canvas.translate(-scrollX, -scrollY); // Apply scroll (note: bg undoes this internally) + canvas.drawRenderNode(receiverBackground.get()); + canvas.drawRenderNode(child.get()); + }); + + OffscreenBuffer** layerHandle = child->getLayerHandle(); + + // create RenderNode's layer here in same way prepareTree would, setting windowTransform + OffscreenBuffer layer(renderThread.renderState(), Caches::getInstance(), 200, 200); + Matrix4 windowTransform; + windowTransform.loadTranslate(100, 100, 0); // total transform of layer's origin + layer.setWindowTransform(windowTransform); + *layerHandle = &layer; + + auto syncedList = TestUtils::createSyncedNodeList(parent); + LayerUpdateQueue layerUpdateQueue; // Note: enqueue damage post-sync, so bounds are valid + layerUpdateQueue.enqueueLayerWithDamage(child.get(), Rect(200, 200)); + FrameBuilder frameBuilder(layerUpdateQueue, SkRect::MakeWH(400, 400), 400, 400, + syncedList, sLightGeometry, nullptr); + ProjectionHwLayerTestRenderer renderer; + frameBuilder.replayBakedOps<TestDispatcher>(renderer); + EXPECT_EQ(6, renderer.getIndex()); + + // clean up layer pointer, so we can safely destruct RenderNode + *layerHandle = nullptr; +} + +RENDERTHREAD_TEST(FrameBuilder, projectionChildScroll) { + static const int scrollX = 500000; + static const int scrollY = 0; + class ProjectionChildScrollTestRenderer : public TestRendererBase { + public: + void onRectOp(const RectOp& op, const BakedOpState& state) override { + EXPECT_EQ(0, mIndex++); + EXPECT_TRUE(state.computedState.transform.isIdentity()); + } + void onOvalOp(const OvalOp& op, const BakedOpState& state) override { + EXPECT_EQ(1, mIndex++); + ASSERT_NE(nullptr, state.computedState.clipState); + ASSERT_EQ(ClipMode::Rectangle, state.computedState.clipState->mode); + ASSERT_EQ(Rect(400, 400), state.computedState.clipState->rect); + EXPECT_TRUE(state.computedState.transform.isIdentity()); + } + }; + auto receiverBackground = TestUtils::createNode(0, 0, 400, 400, + [](RenderProperties& properties, RecordingCanvas& canvas) { + properties.setProjectionReceiver(true); + canvas.drawRect(0, 0, 400, 400, SkPaint()); + }); + auto projectingRipple = TestUtils::createNode(0, 0, 200, 200, + [](RenderProperties& properties, RecordingCanvas& canvas) { + // scroll doesn't apply to background, so undone via translationX/Y + // NOTE: translationX/Y only! no other transform properties may be set for a proj receiver! + properties.setTranslationX(scrollX); + properties.setTranslationY(scrollY); + properties.setProjectBackwards(true); + properties.setClipToBounds(false); + canvas.drawOval(0, 0, 200, 200, SkPaint()); + }); + auto child = TestUtils::createNode(0, 0, 400, 400, + [&projectingRipple](RenderProperties& properties, RecordingCanvas& canvas) { + // Record time clip will be ignored by projectee + canvas.clipRect(100, 100, 300, 300, SkRegion::kIntersect_Op); + + canvas.translate(-scrollX, -scrollY); // Apply scroll (note: bg undoes this internally) + canvas.drawRenderNode(projectingRipple.get()); + }); + auto parent = TestUtils::createNode(0, 0, 400, 400, + [&receiverBackground, &child](RenderProperties& properties, RecordingCanvas& canvas) { + canvas.drawRenderNode(receiverBackground.get()); + canvas.drawRenderNode(child.get()); + }); + + FrameBuilder frameBuilder(sEmptyLayerUpdateQueue, SkRect::MakeWH(400, 400), 400, 400, + TestUtils::createSyncedNodeList(parent), sLightGeometry, nullptr); + ProjectionChildScrollTestRenderer renderer; + frameBuilder.replayBakedOps<TestDispatcher>(renderer); + EXPECT_EQ(2, renderer.getIndex()); +} + // creates a 100x100 shadow casting node with provided translationZ static sp<RenderNode> createWhiteRectShadowCaster(float translationZ) { return TestUtils::createNode(0, 0, 100, 100, diff --git a/tests/HwAccelerationTest/res/layout/projection_clipping.xml b/tests/HwAccelerationTest/res/layout/projection_clipping.xml index 1f2b93946f48..1ea9f9cd49f6 100644 --- a/tests/HwAccelerationTest/res/layout/projection_clipping.xml +++ b/tests/HwAccelerationTest/res/layout/projection_clipping.xml @@ -3,24 +3,32 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> - <FrameLayout + <ScrollView + android:orientation="vertical" android:translationX="50dp" android:translationY="50dp" android:elevation="30dp" android:layout_width="200dp" android:layout_height="200dp" android:background="@drawable/round_rect_background"> - <View - android:id="@+id/clickable1" - android:layout_width="100dp" - android:layout_height="100dp" - android:background="?android:attr/selectableItemBackgroundBorderless"/> - <View - android:id="@+id/clickable2" - android:translationX="50dp" - android:translationY="10dp" - android:layout_width="150dp" - android:layout_height="100dp" - android:background="?android:attr/selectableItemBackgroundBorderless"/> - </FrameLayout> + <FrameLayout + android:layout_width="200dp" + android:layout_height="wrap_content"> + <View + android:layout_width="200dp" + android:layout_height="2000dp"/> + <View + android:id="@+id/clickable1" + android:layout_width="100dp" + android:layout_height="100dp" + android:background="?android:attr/selectableItemBackgroundBorderless"/> + <View + android:id="@+id/clickable2" + android:translationX="50dp" + android:translationY="10dp" + android:layout_width="150dp" + android:layout_height="100dp" + android:background="?android:attr/selectableItemBackgroundBorderless"/> + </FrameLayout> + </ScrollView> </LinearLayout> |