Update hole punch logic in HWUI

--Updated HWUI holepunch logic for SurfaceView to
also apply the stretch to the hole punch
--Updated RenderNode callbacks to also include
an offset from the ancestor RenderNode that also
has a stretch configured on it
--Added new test activity to verify hole punch
logic

Bug: 179047472
Test: manual
Change-Id: Ibbaf8248a31839ba9dc352ecb9fef54e1276918e
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index ac70dff..4f0e1af 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -146,8 +146,9 @@
     private static native void nativeSetBlurRegions(long transactionObj, long nativeObj,
             float[][] regions, int length);
     private static native void nativeSetStretchEffect(long transactionObj, long nativeObj,
-            float left, float top, float right, float bottom, float vecX, float vecY,
-            float maxStretchAmount);
+            float width, float height, float vecX, float vecY,
+            float maxStretchAmountX, float maxStretchAmountY, float childRelativeLeft,
+            float childRelativeTop, float childRelativeRight, float childRelativeBottom);
 
     private static native boolean nativeClearContentFrameStats(long nativeObject);
     private static native boolean nativeGetContentFrameStats(long nativeObject, WindowContentFrameStats outStats);
@@ -3038,11 +3039,14 @@
         /**
          * @hide
          */
-        public Transaction setStretchEffect(SurfaceControl sc, float left, float top, float right,
-                float bottom, float vecX, float vecY, float maxStretchAmount) {
+        public Transaction setStretchEffect(SurfaceControl sc, float width, float height,
+                float vecX, float vecY, float maxStretchAmountX,
+                float maxStretchAmountY, float childRelativeLeft, float childRelativeTop, float childRelativeRight,
+                float childRelativeBottom) {
             checkPreconditions(sc);
-            nativeSetStretchEffect(mNativeObject, sc.mNativeObject, left, top, right, bottom,
-                    vecX, vecY, maxStretchAmount);
+            nativeSetStretchEffect(mNativeObject, sc.mNativeObject, width, height,
+                    vecX, vecY, maxStretchAmountX, maxStretchAmountY, childRelativeLeft, childRelativeTop,
+                    childRelativeRight, childRelativeBottom);
             return this;
         }
 
diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java
index 7bdf5cf..2fce434 100644
--- a/core/java/android/view/SurfaceView.java
+++ b/core/java/android/view/SurfaceView.java
@@ -1471,10 +1471,13 @@
         }
 
         @Override
-        public void applyStretch(long frameNumber, float left, float top, float right,
-                float bottom, float vecX, float vecY, float maxStretch) {
-            mRtTransaction.setStretchEffect(mSurfaceControl, left, top, right, bottom, vecX, vecY,
-                    maxStretch);
+        public void applyStretch(long frameNumber, float width, float height,
+                float vecX, float vecY, float maxStretchX, float maxStretchY,
+                float childRelativeLeft, float childRelativeTop, float childRelativeRight,
+                float childRelativeBottom) {
+            mRtTransaction.setStretchEffect(mSurfaceControl, width, height, vecX, vecY,
+                    maxStretchX, maxStretchY, childRelativeLeft, childRelativeTop,
+                    childRelativeRight, childRelativeBottom);
             applyOrMergeTransaction(mRtTransaction, frameNumber);
         }
 
diff --git a/core/java/android/widget/EdgeEffect.java b/core/java/android/widget/EdgeEffect.java
index 4d2d9e8..9398d38 100644
--- a/core/java/android/widget/EdgeEffect.java
+++ b/core/java/android/widget/EdgeEffect.java
@@ -641,14 +641,10 @@
             boolean hasValidVectors = Float.isFinite(vecX) && Float.isFinite(vecY);
             if (right > left && bottom > top && mWidth > 0 && mHeight > 0 && hasValidVectors) {
                 renderNode.stretch(
-                        left,
-                        top,
-                        right,
-                        bottom,
-                        vecX,
-                        vecY,
-                        mWidth,
-                        mHeight
+                        vecX, // horizontal stretch intensity
+                        vecY, // vertical stretch intensity
+                        mWidth, // max horizontal stretch in pixels
+                        mHeight // max vertical stretch in pixels
                 );
             }
         } else {
diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp
index 27f82f1..d528428 100644
--- a/core/jni/android_view_SurfaceControl.cpp
+++ b/core/jni/android_view_SurfaceControl.cpp
@@ -626,12 +626,24 @@
 }
 
 static void nativeSetStretchEffect(JNIEnv* env, jclass clazz, jlong transactionObj,
-                                   jlong nativeObject, jfloat left, jfloat top, jfloat right,
-                                   jfloat bottom, jfloat vecX, jfloat vecY,
-                                   jfloat maxStretchAmount) {
+                                   jlong nativeObject, jfloat width, jfloat height,
+                                   jfloat vecX, jfloat vecY,
+                                   jfloat maxStretchAmountX, jfloat maxStretchAmountY,
+                                   jfloat childRelativeLeft, jfloat childRelativeTop,
+                                   jfloat childRelativeRight, jfloat childRelativeBottom) {
     auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj);
-    SurfaceControl* const ctrl = reinterpret_cast<SurfaceControl*>(nativeObject);
-    transaction->setStretchEffect(ctrl, left, top, right, bottom, vecX, vecY, maxStretchAmount);
+    auto* const ctrl = reinterpret_cast<SurfaceControl*>(nativeObject);
+    auto stretch = StretchEffect{
+      .width = width,
+      .height = height,
+      .vectorX = vecX,
+      .vectorY = vecY,
+      .maxAmountX = maxStretchAmountX,
+      .maxAmountY = maxStretchAmountY,
+      .mappedChildBounds = FloatRect(
+          childRelativeLeft, childRelativeTop, childRelativeRight, childRelativeBottom)
+    };
+    transaction->setStretchEffect(ctrl, stretch);
 }
 
 static void nativeSetSize(JNIEnv* env, jclass clazz, jlong transactionObj,
@@ -1829,7 +1841,7 @@
             (void*)nativeSetLayerStack },
     {"nativeSetBlurRegions", "(JJ[[FI)V",
             (void*)nativeSetBlurRegions },
-    {"nativeSetStretchEffect", "(JJFFFFFFF)V",
+    {"nativeSetStretchEffect", "(JJFFFFFFFFFF)V",
             (void*) nativeSetStretchEffect },
     {"nativeSetShadowRadius", "(JJF)V",
             (void*)nativeSetShadowRadius },
diff --git a/graphics/java/android/graphics/RenderNode.java b/graphics/java/android/graphics/RenderNode.java
index 6fcd8d0..01fd231 100644
--- a/graphics/java/android/graphics/RenderNode.java
+++ b/graphics/java/android/graphics/RenderNode.java
@@ -280,8 +280,10 @@
          *
          * @hide
          */
-        default void applyStretch(long frameNumber, float left, float top, float right,
-                float bottom, float vecX, float vecY, float maxStretch) { }
+        default void applyStretch(long frameNumber, float width, float height,
+                float vecX, float vecY,
+                float maxStretchX, float maxStretchY, float childRelativeLeft,
+                float childRelativeTop, float childRelativeRight, float childRelativeBottom) { }
 
         /**
          * Called by native on RenderThread to notify that the view is no longer in the
@@ -326,10 +328,13 @@
         }
 
         @Override
-        public void applyStretch(long frameNumber, float left, float top, float right, float bottom,
-                float vecX, float vecY, float maxStretch) {
+        public void applyStretch(long frameNumber, float width, float height,
+                float vecX, float vecY, float maxStretchX, float maxStretchY, float childRelativeLeft,
+                float childRelativeTop, float childRelativeRight, float childRelativeBottom) {
             for (PositionUpdateListener pul : mListeners) {
-                pul.applyStretch(frameNumber, left, top, right, bottom, vecX, vecY, maxStretch);
+                pul.applyStretch(frameNumber, width, height, vecX, vecY, maxStretchX,
+                        maxStretchY, childRelativeLeft, childRelativeTop, childRelativeRight,
+                        childRelativeBottom);
             }
         }
     }
@@ -719,19 +724,15 @@
     }
 
     /** @hide */
-    public boolean stretch(float left, float top, float right, float bottom,
-            float vecX, float vecY, float maxStretchAmountX, float maxStretchAmountY) {
+    public boolean stretch(float vecX, float vecY,
+        float maxStretchAmountX, float maxStretchAmountY) {
         if (Float.isInfinite(vecX) || Float.isNaN(vecX)) {
             throw new IllegalArgumentException("vecX must be a finite, non-NaN value " + vecX);
         }
         if (Float.isInfinite(vecY) || Float.isNaN(vecY)) {
             throw new IllegalArgumentException("vecY must be a finite, non-NaN value " + vecY);
         }
-        if (top >= bottom || left >= right) {
-            throw new IllegalArgumentException(
-                    "Stretch region must not be empty, got "
-                            + new RectF(left, top, right, bottom).toString());
-        }
+
         if (maxStretchAmountX <= 0.0f) {
             throw new IllegalArgumentException(
                     "The max horizontal stretch amount must be >0, got " + maxStretchAmountX);
@@ -742,10 +743,6 @@
         }
         return nStretch(
                 mNativeRenderNode,
-                left,
-                top,
-                right,
-                bottom,
                 vecX,
                 vecY,
                 maxStretchAmountX,
@@ -1701,8 +1698,8 @@
     private static native boolean nClearStretch(long renderNode);
 
     @CriticalNative
-    private static native boolean nStretch(long renderNode, float left, float top, float right,
-            float bottom, float vecX, float vecY, float maxStretchX, float maxStretchY);
+    private static native boolean nStretch(long renderNode, float vecX, float vecY,
+            float maxStretchX, float maxStretchY);
 
     @CriticalNative
     private static native boolean nHasShadow(long renderNode);
diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp
index f2c48bb..0212309 100644
--- a/libs/hwui/Android.bp
+++ b/libs/hwui/Android.bp
@@ -467,6 +467,7 @@
         "pipeline/skia/HolePunch.cpp",
         "pipeline/skia/SkiaDisplayList.cpp",
         "pipeline/skia/SkiaRecordingCanvas.cpp",
+        "pipeline/skia/StretchMask.cpp",
         "pipeline/skia/RenderNodeDrawable.cpp",
         "pipeline/skia/ReorderBarrierDrawables.cpp",
         "pipeline/skia/TransformCanvas.cpp",
diff --git a/libs/hwui/DamageAccumulator.cpp b/libs/hwui/DamageAccumulator.cpp
index 0bf9480..94fe243 100644
--- a/libs/hwui/DamageAccumulator.cpp
+++ b/libs/hwui/DamageAccumulator.cpp
@@ -197,6 +197,27 @@
     }
 }
 
+static void computeTransformImpl(const DirtyStack* frame, const DirtyStack* end,
+                                 Matrix4* outMatrix) {
+  while (frame != end) {
+    switch (frame->type) {
+        case TransformRenderNode:
+            frame->renderNode->applyViewPropertyTransforms(*outMatrix);
+            break;
+        case TransformMatrix4:
+            outMatrix->multiply(*frame->matrix4);
+            break;
+        case TransformNone:
+            // nothing to be done
+            break;
+        default:
+            LOG_ALWAYS_FATAL("Tried to compute transform with an invalid type: %d",
+                             frame->type);
+    }
+    frame = frame->prev;
+  }
+}
+
 void DamageAccumulator::applyRenderNodeTransform(DirtyStack* frame) {
     if (frame->pendingDirty.isEmpty()) {
         return;
@@ -249,19 +270,38 @@
     mHead->pendingDirty.setEmpty();
 }
 
-const StretchEffect* DamageAccumulator::findNearestStretchEffect() const {
+DamageAccumulator::StretchResult DamageAccumulator::findNearestStretchEffect() const {
     DirtyStack* frame = mHead;
     while (frame->prev != frame) {
-        frame = frame->prev;
         if (frame->type == TransformRenderNode) {
+            const auto& renderNode = frame->renderNode;
+            const auto& frameRenderNodeProperties = renderNode->properties();
             const auto& effect =
-                    frame->renderNode->properties().layerProperties().getStretchEffect();
+                    frameRenderNodeProperties.layerProperties().getStretchEffect();
+            const float width = (float) renderNode->getWidth();
+            const float height = (float) renderNode->getHeight();
             if (!effect.isEmpty()) {
-                return &effect;
+                Matrix4 stretchMatrix;
+                computeTransformImpl(mHead, frame, &stretchMatrix);
+                Rect stretchRect = Rect(0.f, 0.f, width, height);
+                stretchMatrix.mapRect(stretchRect);
+
+                return StretchResult{
+                    .stretchEffect = &effect,
+                    .childRelativeBounds = SkRect::MakeLTRB(
+                        stretchRect.left,
+                        stretchRect.top,
+                        stretchRect.right,
+                        stretchRect.bottom
+                    ),
+                    .width = width,
+                    .height = height
+                };
             }
         }
+        frame = frame->prev;
     }
-    return nullptr;
+    return StretchResult{};
 }
 
 } /* namespace uirenderer */
diff --git a/libs/hwui/DamageAccumulator.h b/libs/hwui/DamageAccumulator.h
index 89ee0e3..90a3517 100644
--- a/libs/hwui/DamageAccumulator.h
+++ b/libs/hwui/DamageAccumulator.h
@@ -21,6 +21,7 @@
 
 #include <SkMatrix.h>
 #include <SkRect.h>
+#include <effects/StretchEffect.h>
 
 #include "utils/Macros.h"
 
@@ -35,7 +36,6 @@
 struct DirtyStack;
 class RenderNode;
 class Matrix4;
-class StretchEffect;
 
 class DamageAccumulator {
     PREVENT_COPY_AND_ASSIGN(DamageAccumulator);
@@ -63,7 +63,29 @@
 
     void finish(SkRect* totalDirty);
 
-    const StretchEffect* findNearestStretchEffect() const;
+    struct StretchResult {
+        /**
+         * Stretch parameters configured on the stretch container
+         */
+        const StretchEffect* stretchEffect;
+
+        /**
+         * Bounds of the child relative to the stretch container
+         */
+        const SkRect childRelativeBounds;
+
+        /**
+         * Width of the stretch container
+         */
+        const float width;
+
+        /**
+         * Height of the stretch container
+         */
+        const float height;
+    };
+
+    [[nodiscard]] StretchResult findNearestStretchEffect() const;
 
 private:
     void pushCommon();
diff --git a/libs/hwui/RenderNode.cpp b/libs/hwui/RenderNode.cpp
index e9eae3d..fce2e1f 100644
--- a/libs/hwui/RenderNode.cpp
+++ b/libs/hwui/RenderNode.cpp
@@ -194,6 +194,9 @@
     SkRect dirty;
     info.damageAccumulator->peekAtDirty(&dirty);
     info.layerUpdateQueue->enqueueLayerWithDamage(this, dirty);
+    if (!dirty.isEmpty()) {
+      mStretchMask.markDirty();
+    }
 
     // There might be prefetched layers that need to be accounted for.
     // That might be us, so tell CanvasContext that this layer is in the
@@ -302,6 +305,12 @@
         damageSelf(info);
         info.damageAccumulator->popTransform();
         syncProperties();
+
+        const StretchEffect& stagingStretch =
+            mProperties.layerProperties().getStretchEffect();
+        if (stagingStretch.isEmpty()) {
+            mStretchMask.clear();
+        }
         // We could try to be clever and only re-damage if the matrix changed.
         // However, we don't need to worry about that. The cost of over-damaging
         // here is only going to be a single additional map rect of this node
diff --git a/libs/hwui/RenderNode.h b/libs/hwui/RenderNode.h
index 988141f..6a0b1aa 100644
--- a/libs/hwui/RenderNode.h
+++ b/libs/hwui/RenderNode.h
@@ -40,6 +40,7 @@
 #include "pipeline/skia/SkiaLayer.h"
 
 #include <vector>
+#include <pipeline/skia/StretchMask.h>
 
 class SkBitmap;
 class SkPaint;
@@ -127,6 +128,8 @@
         }
     }
 
+    StretchMask& getStretchMask() { return mStretchMask; }
+
     VirtualLightRefBase* getUserContext() const { return mUserContext.get(); }
 
     void setUserContext(VirtualLightRefBase* context) { mUserContext = context; }
@@ -286,6 +289,7 @@
     UsageHint mUsageHint = UsageHint::Unknown;
 
     bool mHasHolePunches;
+    StretchMask mStretchMask;
 
     // METHODS & FIELDS ONLY USED BY THE SKIA RENDERER
 public:
diff --git a/libs/hwui/effects/StretchEffect.cpp b/libs/hwui/effects/StretchEffect.cpp
index 6eb6e1e..1519d69 100644
--- a/libs/hwui/effects/StretchEffect.cpp
+++ b/libs/hwui/effects/StretchEffect.cpp
@@ -189,17 +189,12 @@
 static const float CONTENT_DISTANCE_STRETCHED = 1.f;
 static const float INTERPOLATION_STRENGTH_VALUE = 0.7f;
 
-sk_sp<SkShader> StretchEffect::getShader(const sk_sp<SkImage>& snapshotImage) const {
+sk_sp<SkShader> StretchEffect::getShader(float width, float height,
+                                         const sk_sp<SkImage>& snapshotImage) const {
     if (isEmpty()) {
         return nullptr;
     }
 
-    if (mStretchShader != nullptr) {
-        return mStretchShader;
-    }
-
-    float viewportWidth = stretchArea.width();
-    float viewportHeight = stretchArea.height();
     float normOverScrollDistX = mStretchDirection.x();
     float normOverScrollDistY = mStretchDirection.y();
     float distanceStretchedX = CONTENT_DISTANCE_STRETCHED / (1 + abs(normOverScrollDistX));
@@ -228,12 +223,10 @@
     mBuilder->uniform("uOverscrollY").set(&normOverScrollDistY, 1);
     mBuilder->uniform("uScrollX").set(&ZERO, 1);
     mBuilder->uniform("uScrollY").set(&ZERO, 1);
-    mBuilder->uniform("viewportWidth").set(&viewportWidth, 1);
-    mBuilder->uniform("viewportHeight").set(&viewportHeight, 1);
+    mBuilder->uniform("viewportWidth").set(&width, 1);
+    mBuilder->uniform("viewportHeight").set(&height, 1);
 
-    mStretchShader = mBuilder->makeShader(nullptr, false);
-
-    return mStretchShader;
+    return mBuilder->makeShader(nullptr, false);
 }
 
 sk_sp<SkRuntimeEffect> StretchEffect::getStretchEffect() {
diff --git a/libs/hwui/effects/StretchEffect.h b/libs/hwui/effects/StretchEffect.h
index 546d53b..61537f0 100644
--- a/libs/hwui/effects/StretchEffect.h
+++ b/libs/hwui/effects/StretchEffect.h
@@ -26,19 +26,15 @@
 
 namespace android::uirenderer {
 
-// TODO: Inherit from base RenderEffect type?
 class StretchEffect {
 public:
-    enum class StretchInterpolator {
-        SmoothStep,
-    };
 
-    StretchEffect(const SkRect& area, const SkVector& direction, float maxStretchAmountX,
+    StretchEffect(const SkVector& direction,
+                  float maxStretchAmountX,
                   float maxStretchAmountY)
-            : stretchArea(area)
-            , maxStretchAmountX(maxStretchAmountX)
+            : maxStretchAmountX(maxStretchAmountX)
             , maxStretchAmountY(maxStretchAmountY)
-            , mStretchDirection(direction) {}
+            , mStretchDirection(direction) { }
 
     StretchEffect() {}
 
@@ -51,14 +47,18 @@
     }
 
     StretchEffect& operator=(const StretchEffect& other) {
-        this->stretchArea = other.stretchArea;
         this->mStretchDirection = other.mStretchDirection;
-        this->mStretchShader = other.mStretchShader;
         this->maxStretchAmountX = other.maxStretchAmountX;
         this->maxStretchAmountY = other.maxStretchAmountY;
         return *this;
     }
 
+    bool operator==(const StretchEffect& other) const {
+        return mStretchDirection == other.mStretchDirection &&
+                maxStretchAmountX == other.maxStretchAmountX &&
+                maxStretchAmountY == other.maxStretchAmountY;
+    }
+
     void mergeWith(const StretchEffect& other) {
         if (other.isEmpty()) {
             return;
@@ -67,33 +67,26 @@
             *this = other;
             return;
         }
-        setStretchDirection(mStretchDirection + other.mStretchDirection);
+        mStretchDirection += other.mStretchDirection;
         if (isEmpty()) {
             return setEmpty();
         }
-        stretchArea.join(other.stretchArea);
         maxStretchAmountX = std::max(maxStretchAmountX, other.maxStretchAmountX);
         maxStretchAmountY = std::max(maxStretchAmountY, other.maxStretchAmountY);
     }
 
-    sk_sp<SkShader> getShader(const sk_sp<SkImage>& snapshotImage) const;
+    sk_sp<SkShader> getShader(float width, float height,
+                              const sk_sp<SkImage>& snapshotImage) const;
 
-    SkRect stretchArea {0, 0, 0, 0};
     float maxStretchAmountX = 0;
     float maxStretchAmountY = 0;
 
-    void setStretchDirection(const SkVector& direction) {
-        mStretchShader = nullptr;
-        mStretchDirection = direction;
-    }
-
     const SkVector getStretchDirection() const { return mStretchDirection; }
 
 private:
     static sk_sp<SkRuntimeEffect> getStretchEffect();
     mutable SkVector mStretchDirection{0, 0};
     mutable std::unique_ptr<SkRuntimeShaderBuilder> mBuilder;
-    mutable sk_sp<SkShader> mStretchShader;
 };
 
 } // namespace android::uirenderer
diff --git a/libs/hwui/jni/android_graphics_RenderNode.cpp b/libs/hwui/jni/android_graphics_RenderNode.cpp
index fffa806..5131c64 100644
--- a/libs/hwui/jni/android_graphics_RenderNode.cpp
+++ b/libs/hwui/jni/android_graphics_RenderNode.cpp
@@ -180,12 +180,10 @@
 }
 
 static jboolean android_view_RenderNode_stretch(CRITICAL_JNI_PARAMS_COMMA jlong renderNodePtr,
-                                                jfloat left, jfloat top, jfloat right,
-                                                jfloat bottom, jfloat vX, jfloat vY, jfloat maxX,
+                                                jfloat vX, jfloat vY, jfloat maxX,
                                                 jfloat maxY) {
-    StretchEffect effect = StretchEffect(SkRect::MakeLTRB(left, top, right, bottom),
-                                         {.fX = vX, .fY = vY}, maxX, maxY);
-    RenderNode* renderNode = reinterpret_cast<RenderNode*>(renderNodePtr);
+    auto* renderNode = reinterpret_cast<RenderNode*>(renderNodePtr);
+    StretchEffect effect = StretchEffect({.fX = vX, .fY = vY}, maxX, maxY);
     renderNode->mutateStagingProperties().mutateLayerProperties().mutableStretchEffect().mergeWith(
             effect);
     renderNode->setPropertyFieldsDirty(RenderNode::GENERIC);
@@ -643,13 +641,15 @@
 
         void handleStretchEffect(const TreeInfo& info, const Matrix4& transform) {
             // Search up to find the nearest stretcheffect parent
-            const StretchEffect* effect = info.damageAccumulator->findNearestStretchEffect();
+            const DamageAccumulator::StretchResult result =
+                info.damageAccumulator->findNearestStretchEffect();
+            const StretchEffect* effect = result.stretchEffect;
             if (!effect) {
                 return;
             }
 
-            uirenderer::Rect area = effect->stretchArea;
-            transform.mapRect(area);
+            const auto& childRelativeBounds = result.childRelativeBounds;
+
             JNIEnv* env = jnienv();
 
             jobject localref = env->NewLocalRef(mWeakRef);
@@ -661,9 +661,17 @@
 #ifdef __ANDROID__  // Layoutlib does not support CanvasContext
             SkVector stretchDirection = effect->getStretchDirection();
             env->CallVoidMethod(localref, gPositionListener_ApplyStretchMethod,
-                                info.canvasContext.getFrameNumber(), area.left, area.top,
-                                area.right, area.bottom, stretchDirection.fX, stretchDirection.fY,
-                                effect->maxStretchAmountX, effect->maxStretchAmountY);
+                                info.canvasContext.getFrameNumber(),
+                                result.width,
+                                result.height,
+                                stretchDirection.fX,
+                                stretchDirection.fY,
+                                effect->maxStretchAmountX,
+                                effect->maxStretchAmountY,
+                                childRelativeBounds.left(),
+                                childRelativeBounds.top(),
+                                childRelativeBounds.right(),
+                                childRelativeBounds.bottom());
 #endif
             env->DeleteLocalRef(localref);
         }
@@ -739,7 +747,7 @@
         {"nSetOutlineEmpty", "(J)Z", (void*)android_view_RenderNode_setOutlineEmpty},
         {"nSetOutlineNone", "(J)Z", (void*)android_view_RenderNode_setOutlineNone},
         {"nClearStretch", "(J)Z", (void*)android_view_RenderNode_clearStretch},
-        {"nStretch", "(JFFFFFFFF)Z", (void*)android_view_RenderNode_stretch},
+        {"nStretch", "(JFFFF)Z", (void*)android_view_RenderNode_stretch},
         {"nHasShadow", "(J)Z", (void*)android_view_RenderNode_hasShadow},
         {"nSetSpotShadowColor", "(JI)Z", (void*)android_view_RenderNode_setSpotShadowColor},
         {"nGetSpotShadowColor", "(J)I", (void*)android_view_RenderNode_getSpotShadowColor},
@@ -814,7 +822,7 @@
     gPositionListener_PositionChangedMethod = GetMethodIDOrDie(env, clazz,
             "positionChanged", "(JIIII)V");
     gPositionListener_ApplyStretchMethod =
-            GetMethodIDOrDie(env, clazz, "applyStretch", "(JFFFFFFF)V");
+            GetMethodIDOrDie(env, clazz, "applyStretch", "(JFFFFFFFFFF)V");
     gPositionListener_PositionLostMethod = GetMethodIDOrDie(env, clazz,
             "positionLost", "(J)V");
     return RegisterMethodsOrDie(env, kClassPathName, gMethods, NELEM(gMethods));
diff --git a/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp b/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp
index 77d99a6..1ae06d0 100644
--- a/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp
+++ b/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp
@@ -16,6 +16,7 @@
 
 #include "RenderNodeDrawable.h"
 #include <SkPaintFilterCanvas.h>
+#include "StretchMask.h"
 #include "RenderNode.h"
 #include "SkiaDisplayList.h"
 #include "TransformCanvas.h"
@@ -245,17 +246,37 @@
                     "SurfaceID|%" PRId64, renderNode->uniqueId()).c_str(), nullptr);
             }
 
-            if (renderNode->hasHolePunches()) {
-                TransformCanvas transformCanvas(canvas);
-                displayList->draw(&transformCanvas);
-            }
-
             const StretchEffect& stretch = properties.layerProperties().getStretchEffect();
             if (stretch.isEmpty()) {
+                // If we don't have any stretch effects, issue the filtered
+                // canvas draw calls to make sure we still punch a hole
+                // with the same canvas transformation + clip into the target
+                // canvas then draw the layer on top
+                if (renderNode->hasHolePunches()) {
+                    TransformCanvas transformCanvas(canvas, SkBlendMode::kClear);
+                    displayList->draw(&transformCanvas);
+                }
                 canvas->drawImageRect(snapshotImage, bounds, bounds, sampling, &paint,
                                       SkCanvas::kStrict_SrcRectConstraint);
             } else {
-                sk_sp<SkShader> stretchShader = stretch.getShader(snapshotImage);
+                // If we do have stretch effects and have hole punches,
+                // then create a mask and issue the filtered draw calls to
+                // get the corresponding hole punches.
+                // Then apply the stretch to the mask and draw the mask to
+                // the destination
+                if (renderNode->hasHolePunches()) {
+                    GrRecordingContext* context = canvas->recordingContext();
+                    StretchMask& stretchMask = renderNode->getStretchMask();
+                    stretchMask.draw(context,
+                                     stretch,
+                                     bounds,
+                                     displayList,
+                                     canvas);
+                }
+
+                sk_sp<SkShader> stretchShader = stretch.getShader(bounds.width(),
+                                                                  bounds.height(),
+                                                                  snapshotImage);
                 paint.setShader(stretchShader);
                 canvas->drawRect(bounds, paint);
             }
diff --git a/libs/hwui/pipeline/skia/StretchMask.cpp b/libs/hwui/pipeline/skia/StretchMask.cpp
new file mode 100644
index 0000000..2bbd8a4
--- /dev/null
+++ b/libs/hwui/pipeline/skia/StretchMask.cpp
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+#include "StretchMask.h"
+#include "SkSurface.h"
+#include "SkCanvas.h"
+#include "TransformCanvas.h"
+#include "SkiaDisplayList.h"
+
+using android::uirenderer::StretchMask;
+
+void StretchMask::draw(GrRecordingContext* context,
+                       const StretchEffect& stretch,
+                       const SkRect& bounds,
+                       skiapipeline::SkiaDisplayList* displayList,
+                       SkCanvas* canvas) {
+    float width = bounds.width();
+    float height = bounds.height();
+    if (mMaskSurface == nullptr || mMaskSurface->width() != width ||
+        mMaskSurface->height() != height) {
+        // Create a new surface if we don't have one or our existing size does
+        // not match.
+        mMaskSurface = SkSurface::MakeRenderTarget(
+            context,
+            SkBudgeted::kYes,
+            SkImageInfo::Make(
+                width,
+                height,
+                SkColorType::kAlpha_8_SkColorType,
+                SkAlphaType::kPremul_SkAlphaType)
+        );
+        mIsDirty = true;
+    }
+
+    if (mIsDirty) {
+        SkCanvas* maskCanvas = mMaskSurface->getCanvas();
+        maskCanvas->drawColor(0, SkBlendMode::kClear);
+        TransformCanvas transformCanvas(maskCanvas, SkBlendMode::kSrcOver);
+        displayList->draw(&transformCanvas);
+    }
+
+    sk_sp<SkImage> maskImage = mMaskSurface->makeImageSnapshot();
+    sk_sp<SkShader> maskStretchShader = stretch.getShader(
+        width, height, maskImage);
+
+    SkPaint maskPaint;
+    maskPaint.setShader(maskStretchShader);
+    maskPaint.setBlendMode(SkBlendMode::kDstOut);
+    canvas->drawRect(bounds, maskPaint);
+
+    mIsDirty = false;
+}
\ No newline at end of file
diff --git a/libs/hwui/pipeline/skia/StretchMask.h b/libs/hwui/pipeline/skia/StretchMask.h
new file mode 100644
index 0000000..dc698b8
--- /dev/null
+++ b/libs/hwui/pipeline/skia/StretchMask.h
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+#pragma once
+
+#include "GrRecordingContext.h"
+#include <effects/StretchEffect.h>
+#include <SkSurface.h>
+#include "SkiaDisplayList.h"
+
+namespace android::uirenderer {
+
+/**
+ * Helper class used to create/cache an SkSurface instance
+ * to create a mask that is used to draw a stretched hole punch
+ */
+class StretchMask {
+ public:
+  /**
+   * Release the current surface used for the stretch mask
+   */
+  void clear() {
+      mMaskSurface = nullptr;
+  }
+
+  /**
+   * Reset the dirty flag to re-create the stretch mask on the next draw
+   * pass
+   */
+  void markDirty() {
+      mIsDirty = true;
+  }
+
+  /**
+   * Draws the stretch mask into the given target canvas
+   * @param context GrRecordingContext used to create the surface if necessary
+   * @param stretch StretchEffect to apply to the mask
+   * @param bounds Target bounds to draw into the given canvas
+   * @param displayList List of drawing commands to render into the stretch mask
+   * @param canvas Target canvas to draw the mask into
+   */
+  void draw(GrRecordingContext* context,
+            const StretchEffect& stretch, const SkRect& bounds,
+            skiapipeline::SkiaDisplayList* displayList, SkCanvas* canvas);
+private:
+  sk_sp<SkSurface> mMaskSurface;
+  bool mIsDirty = true;
+};
+
+}
diff --git a/libs/hwui/pipeline/skia/TransformCanvas.cpp b/libs/hwui/pipeline/skia/TransformCanvas.cpp
index a6e4c4c..6777c00 100644
--- a/libs/hwui/pipeline/skia/TransformCanvas.cpp
+++ b/libs/hwui/pipeline/skia/TransformCanvas.cpp
@@ -28,8 +28,8 @@
         SkRRect roundRect = SkRRect::MakeRectXY(rect, radiusX, radiusY);
 
         SkPaint paint;
-        paint.setColor(0);
-        paint.setBlendMode(SkBlendMode::kClear);
+        paint.setColor(SkColors::kBlack);
+        paint.setBlendMode(mHolePunchBlendMode);
         mWrappedCanvas->drawRRect(roundRect, paint);
     }
 }
diff --git a/libs/hwui/pipeline/skia/TransformCanvas.h b/libs/hwui/pipeline/skia/TransformCanvas.h
index 47f77f1..685b71d 100644
--- a/libs/hwui/pipeline/skia/TransformCanvas.h
+++ b/libs/hwui/pipeline/skia/TransformCanvas.h
@@ -17,10 +17,12 @@
 
 #include <include/core/SkCanvas.h>
 #include "SkPaintFilterCanvas.h"
+#include <effects/StretchEffect.h>
 
 class TransformCanvas : public SkPaintFilterCanvas {
 public:
-    TransformCanvas(SkCanvas* target) : SkPaintFilterCanvas(target), mWrappedCanvas(target) {}
+    TransformCanvas(SkCanvas* target, SkBlendMode blendmode) :
+        SkPaintFilterCanvas(target), mWrappedCanvas(target), mHolePunchBlendMode(blendmode) {}
 
 protected:
     bool onFilter(SkPaint& paint) const override;
@@ -32,4 +34,5 @@
 private:
     // We don't own the canvas so just maintain a raw pointer to it
     SkCanvas* mWrappedCanvas;
+    const SkBlendMode mHolePunchBlendMode;
 };
diff --git a/tests/HwAccelerationTest/AndroidManifest.xml b/tests/HwAccelerationTest/AndroidManifest.xml
index 6bf4492..04a55d6 100644
--- a/tests/HwAccelerationTest/AndroidManifest.xml
+++ b/tests/HwAccelerationTest/AndroidManifest.xml
@@ -409,6 +409,15 @@
             </intent-filter>
         </activity>
 
+        <activity android:name="ScrollingStretchSurfaceViewActivity"
+                  android:label="SurfaceView/Scrolling Stretched SurfaceView"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="com.android.test.hwui.TEST"/>
+            </intent-filter>
+        </activity>
+
         <activity android:name="GetBitmapSurfaceViewActivity"
              android:label="SurfaceView/GetBitmap with Camera source"
              android:exported="true">
diff --git a/tests/HwAccelerationTest/res/layout/scrolling_stretch_surfaceview.xml b/tests/HwAccelerationTest/res/layout/scrolling_stretch_surfaceview.xml
new file mode 100644
index 0000000..77f5e60
--- /dev/null
+++ b/tests/HwAccelerationTest/res/layout/scrolling_stretch_surfaceview.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 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.
+  -->
+
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+            android:overScrollMode="always"
+            android:fillViewport="true"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical"
+    >
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="100dp"
+            android:layout_marginTop="100dp"
+            android:orientation="horizontal"
+        >
+
+            <ImageView
+                android:id="@+id/vertical_imageview"
+                android:layout_width="0dp"
+                android:layout_weight="1"
+                android:layout_height="match_parent"/>
+            <FrameLayout
+                android:id="@+id/vertical_surfaceview_container"
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_weight="1"/>
+        </LinearLayout>
+
+        <HorizontalScrollView
+            android:overScrollMode="always"
+            android:layout_width="400dp"
+            android:layout_height="0dp"
+            android:background="#FF0000"
+            android:layout_weight="1"
+        >
+            <LinearLayout
+                android:layout_width="400dp"
+                android:layout_height="400dp"
+                android:layout_marginLeft="100dp"
+                android:orientation="vertical">
+
+                <ImageView
+                    android:id="@+id/horizontal_imageview"
+                    android:layout_width="100dp"
+                    android:layout_weight="1"
+                    android:layout_height="0dp"/>
+
+                <FrameLayout
+                    android:id="@+id/horizontal_surfaceview_container"
+                    android:layout_width="100dp"
+                    android:layout_height="0dp"
+                    android:layout_weight="1"/>
+
+            </LinearLayout>
+        </HorizontalScrollView>
+    </LinearLayout>
+</ScrollView>
\ No newline at end of file
diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/PositionListenerActivity.java b/tests/HwAccelerationTest/src/com/android/test/hwui/PositionListenerActivity.java
index 6b6287d..2ad034c 100644
--- a/tests/HwAccelerationTest/src/com/android/test/hwui/PositionListenerActivity.java
+++ b/tests/HwAccelerationTest/src/com/android/test/hwui/PositionListenerActivity.java
@@ -74,12 +74,10 @@
                 float maxStretchAmount = 100f;
                 // Although we could do this in a single call, the real one won't be - so mimic that
                 if (dir.x != 0f) {
-                    node.stretch(0f, 0f, (float) getWidth(), (float) getHeight(),
-                            dir.x, 0f, maxStretchAmount, maxStretchAmount);
+                    node.stretch(dir.x, 0f, maxStretchAmount, maxStretchAmount);
                 }
                 if (dir.y != 0f) {
-                    node.stretch(0f, 0f, (float) getWidth(), (float) getHeight(),
-                            0f, dir.y, maxStretchAmount, maxStretchAmount);
+                    node.stretch(0f, dir.y, maxStretchAmount, maxStretchAmount);
                 }
             }
         };
@@ -94,10 +92,13 @@
         int mCurrentCount = 0;
         int mTranslateY = 0;
         Rect mPosition = new Rect();
-        RectF mStretchArea = new RectF();
+        float mWidth = 0f;
+        float mHeight = 0f;
+        RectF mMappedBounds = new RectF();
         float mStretchX = 0.0f;
         float mStretchY = 0.0f;
-        float mStretchMax = 0.0f;
+        float mStretchMaxX = 0.0f;
+        float mStretchMaxY = 0.0f;
 
         MyPositionReporter(Context c) {
             super(c);
@@ -128,9 +129,12 @@
         }
 
         void updateText() {
-            setText(String.format("%d: Position %s, stretch area %s, vec %f,%f, amount %f",
-                    mCurrentCount, mPosition.toShortString(), mStretchArea.toShortString(),
-                    mStretchX, mStretchY, mStretchMax));
+            String posText =
+              "%d: Position %s, stretch width %f, height %f, vec %f,%f, amountX %f amountY %f mappedBounds %s";
+            setText(String.format(posText,
+                    mCurrentCount, mPosition.toShortString(), mWidth, mHeight,
+                    mStretchX, mStretchY, mStretchMaxX, mStretchMaxY,
+                    mMappedBounds.toShortString()));
         }
 
         @Override
@@ -143,13 +147,19 @@
         }
 
         @Override
-        public void applyStretch(long frameNumber, float left, float top, float right, float bottom,
-                float vecX, float vecY, float maxStretch) {
+        public void applyStretch(long frameNumber, float width, float height,
+                float vecX, float vecY,
+                float maxStretchX, float maxStretchY, float childRelativeLeft,
+                float childRelativeTop, float childRelativeRight, float childRelativeBottom) {
             getHandler().postAtFrontOfQueue(() -> {
-                mStretchArea.set(left, top, right, bottom);
+                mWidth = width;
+                mHeight = height;
                 mStretchX = vecX;
                 mStretchY = vecY;
-                mStretchMax = maxStretch;
+                mStretchMaxX = maxStretchX;
+                mStretchMaxY = maxStretchY;
+                mMappedBounds.set(childRelativeLeft, childRelativeTop, childRelativeRight,
+                        childRelativeBottom);
                 updateText();
             });
         }
diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/ScrollingStretchSurfaceViewActivity.java b/tests/HwAccelerationTest/src/com/android/test/hwui/ScrollingStretchSurfaceViewActivity.java
new file mode 100644
index 0000000..040bff5
--- /dev/null
+++ b/tests/HwAccelerationTest/src/com/android/test/hwui/ScrollingStretchSurfaceViewActivity.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2021 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.test.hwui;
+
+import android.app.Activity;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.view.SurfaceHolder;
+import android.view.SurfaceHolder.Callback;
+import android.view.SurfaceView;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+public class ScrollingStretchSurfaceViewActivity extends Activity implements Callback {
+
+    SurfaceView mVerticalSurfaceView;
+    SurfaceView mHorizontalSurfaceView;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.scrolling_stretch_surfaceview);
+
+        mVerticalSurfaceView = new SurfaceView(this);
+        mVerticalSurfaceView.getHolder().addCallback(this);
+
+        mHorizontalSurfaceView = new SurfaceView(this);
+        mHorizontalSurfaceView.getHolder().addCallback(this);
+
+        FrameLayout verticalContainer = findViewById(R.id.vertical_surfaceview_container);
+        verticalContainer.addView(mVerticalSurfaceView,
+            new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
+                FrameLayout.LayoutParams.MATCH_PARENT));
+
+        FrameLayout horizontalContainer = findViewById(R.id.horizontal_surfaceview_container);
+        horizontalContainer.addView(mHorizontalSurfaceView,
+            new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
+                FrameLayout.LayoutParams.MATCH_PARENT));
+
+        ImageView verticalImageView = findViewById(R.id.vertical_imageview);
+        verticalImageView.setImageDrawable(new LineDrawable());
+
+        ImageView horizontalImageView = findViewById(R.id.horizontal_imageview);
+        horizontalImageView.setImageDrawable(new LineDrawable());
+    }
+
+    @Override
+    public void surfaceCreated(SurfaceHolder holder) {
+    }
+
+    @Override
+    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+        Canvas canvas = holder.lockCanvas();
+
+        drawLine(canvas, width, height);
+        holder.unlockCanvasAndPost(canvas);
+    }
+
+    private static void drawLine(Canvas canvas, int width, int height) {
+        canvas.drawColor(Color.GRAY);
+
+        Paint paint = new Paint();
+        paint.setAntiAlias(true);
+        paint.setColor(Color.GREEN);
+        paint.setStyle(Paint.Style.STROKE);
+        paint.setStrokeWidth(10f);
+        canvas.drawLine(0, 0, width, height, paint);
+    }
+
+    private static class LineDrawable extends Drawable {
+        @Override
+        public void draw(Canvas canvas) {
+            drawLine(canvas, getBounds().width(), getBounds().height());
+        }
+
+        @Override
+        public void setAlpha(int alpha) {
+            // NO-OP
+        }
+
+        @Override
+        public void setColorFilter(ColorFilter colorFilter) {
+            // NO-OP
+        }
+
+        @Override
+        public int getOpacity() {
+            return 0;
+        }
+    }
+
+    @Override
+    public void surfaceDestroyed(SurfaceHolder holder) {
+    }
+}
diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/StretchShaderActivity.java b/tests/HwAccelerationTest/src/com/android/test/hwui/StretchShaderActivity.java
index ade94a9..3307c36 100644
--- a/tests/HwAccelerationTest/src/com/android/test/hwui/StretchShaderActivity.java
+++ b/tests/HwAccelerationTest/src/com/android/test/hwui/StretchShaderActivity.java
@@ -409,10 +409,6 @@
             if (mStretchDistance > 0 && canvas instanceof RecordingCanvas) {
                 Rect bounds = getBounds();
                 ((RecordingCanvas) canvas).mNode.stretch(
-                        0,
-                        0,
-                        bounds.width(),
-                        bounds.height(),
                         mOverScrollX,
                         mOverScrollY,
                         mStretchDistance,
diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/StretchySurfaceViewActivity.java b/tests/HwAccelerationTest/src/com/android/test/hwui/StretchySurfaceViewActivity.java
index 67b9be5..acb872c 100644
--- a/tests/HwAccelerationTest/src/com/android/test/hwui/StretchySurfaceViewActivity.java
+++ b/tests/HwAccelerationTest/src/com/android/test/hwui/StretchySurfaceViewActivity.java
@@ -99,7 +99,7 @@
                 super.onDraw(canvas);
 
                 RenderNode node = ((RecordingCanvas) canvas).mNode;
-                node.stretch(0f, 0f, getWidth(), getHeight() / 2f, 0f,
+                node.stretch(0f,
                         1f, 400f, 400f);
             }
         };