Wire SKSL based stretch shader to HWUI

--Ported SKSL based stretch shader from OpenGL prototype
--Hooked up the stretch APIs in RenderNode to the stretch
shader.
--Updated RenderNode layer logic to promote the RenderNode to
a layer if there is a stretch to be applied to it in order
to feed the layer as input to the stretch shader

Bug: 179047472
Test: builds + sample overscroll stretches + updated CTS test
Change-Id: I744ff70099fe251ce07f23d067bf13444a468c08
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index ebef464..ab7732b 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -22188,9 +22188,6 @@
      * and hardware acceleration.
      */
     boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
-        // Clear the overscroll effect:
-        // TODO: Use internal API instead of overriding the existing RenderEffect
-        setRenderEffect(null);
 
         final boolean hardwareAcceleratedCanvas = canvas.isHardwareAccelerated();
         /* If an attached view draws to a HW canvas, it may use its RenderNode + DisplayList.
diff --git a/core/java/android/widget/EdgeEffect.java b/core/java/android/widget/EdgeEffect.java
index 1b62266..dc42ad5 100644
--- a/core/java/android/widget/EdgeEffect.java
+++ b/core/java/android/widget/EdgeEffect.java
@@ -29,7 +29,6 @@
 import android.graphics.Paint;
 import android.graphics.RecordingCanvas;
 import android.graphics.Rect;
-import android.graphics.RenderEffect;
 import android.graphics.RenderNode;
 import android.os.Build;
 import android.util.AttributeSet;
@@ -83,6 +82,8 @@
     public @interface EdgeEffectType {
     }
 
+    private static final float DEFAULT_MAX_STRETCH_INTENSITY = 1.5f;
+
     @SuppressWarnings("UnusedDeclaration")
     private static final String TAG = "EdgeEffect";
 
@@ -128,6 +129,8 @@
 
     private long mStartTime;
     private float mDuration;
+    private float mStretchIntensity = DEFAULT_MAX_STRETCH_INTENSITY;
+    private float mStretchDistance = -1f;
 
     private final Interpolator mInterpolator;
 
@@ -146,6 +149,8 @@
     private float mPullDistance;
 
     private final Rect mBounds = new Rect();
+    private float mWidth;
+    private float mHeight;
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123769450)
     private final Paint mPaint = new Paint();
     private float mRadius;
@@ -202,6 +207,19 @@
         mBaseGlowScale = h > 0 ? Math.min(oh / h, 1.f) : 1.f;
 
         mBounds.set(mBounds.left, mBounds.top, width, (int) Math.min(height, h));
+
+        mWidth = width;
+        mHeight = height;
+    }
+
+    /**
+     * Configure the distance in pixels to stretch the content. This is only consumed as part
+     * if {@link #setType(int)} is set to {@link #TYPE_STRETCH}
+     * @param stretchDistance Stretch distance in pixels when the target View is overscrolled
+     * @hide
+     */
+    public void setStretchDistance(float stretchDistance) {
+        mStretchDistance = stretchDistance;
     }
 
     /**
@@ -437,6 +455,13 @@
     }
 
     /**
+     * @hide
+     */
+    public void setMaxStretchIntensity(float stretchIntensity) {
+        mStretchIntensity = stretchIntensity;
+    }
+
+    /**
      * Set or clear the blend mode. A blend mode defines how source pixels
      * (generated by a drawing command) are composited with the destination pixels
      * (content of the render target).
@@ -520,23 +545,55 @@
             RecordingCanvas recordingCanvas = (RecordingCanvas) canvas;
             if (mTmpMatrix == null) {
                 mTmpMatrix = new Matrix();
-                mTmpPoints = new float[4];
+                mTmpPoints = new float[12];
             }
             //noinspection deprecation
             recordingCanvas.getMatrix(mTmpMatrix);
-            mTmpPoints[0] = mBounds.width() * mDisplacement;
-            mTmpPoints[1] = mDistance * mBounds.height();
-            mTmpPoints[2] = mTmpPoints[0];
-            mTmpPoints[3] = 0;
+
+            mTmpPoints[0] = 0;
+            mTmpPoints[1] = 0; // top-left
+            mTmpPoints[2] = mWidth;
+            mTmpPoints[3] = 0; // top-right
+            mTmpPoints[4] = mWidth;
+            mTmpPoints[5] = mHeight; // bottom-right
+            mTmpPoints[6] = 0;
+            mTmpPoints[7] = mHeight; // bottom-left
+            mTmpPoints[8] = mWidth * mDisplacement;
+            mTmpPoints[9] = 0; // drag start point
+            mTmpPoints[10] = mWidth * mDisplacement;
+            mTmpPoints[11] = mHeight * mDistance; // drag point
             mTmpMatrix.mapPoints(mTmpPoints);
-            float x = mTmpPoints[0] - mTmpPoints[2];
-            float y = mTmpPoints[1] - mTmpPoints[3];
 
             RenderNode renderNode = recordingCanvas.mNode;
 
-            // TODO: use stretchy RenderEffect and use internal API when it is ready
-            // TODO: wrap existing RenderEffect
-            renderNode.setRenderEffect(RenderEffect.createOffsetEffect(x, y));
+            float left = renderNode.getLeft()
+                    + min(mTmpPoints[0], mTmpPoints[2], mTmpPoints[4], mTmpPoints[6]);
+            float top = renderNode.getTop()
+                    + min(mTmpPoints[1], mTmpPoints[3], mTmpPoints[5], mTmpPoints[7]);
+            float right = renderNode.getLeft()
+                    + max(mTmpPoints[0], mTmpPoints[2], mTmpPoints[4], mTmpPoints[6]);
+            float bottom = renderNode.getTop()
+                    + max(mTmpPoints[1], mTmpPoints[3], mTmpPoints[5], mTmpPoints[7]);
+            // assume rotations of increments of 90 degrees
+            float x = mTmpPoints[10] - mTmpPoints[8];
+            float width = right - left;
+            float vecX = Math.max(-1f, Math.min(1f, x / width));
+            float y = mTmpPoints[11] - mTmpPoints[9];
+            float height = bottom - top;
+            float vecY = Math.max(-1f, Math.min(1f, y / height));
+            renderNode.stretch(
+                    left,
+                    top,
+                    right,
+                    bottom,
+                    vecX * mStretchIntensity,
+                    vecY * mStretchIntensity,
+                    // TODO (njawad/mount) figure out proper stretch distance from UX
+                    //  for now leverage placeholder logic if no stretch distance is provided to
+                    //  consume the displacement ratio times the minimum of the width or height
+                    mStretchDistance > 0 ? mStretchDistance :
+                            (mDisplacement * Math.min(mWidth, mHeight))
+            );
         }
 
         boolean oneLastFrame = false;
@@ -548,6 +605,18 @@
         return mState != STATE_IDLE || oneLastFrame;
     }
 
+    private float min(float f1, float f2, float f3, float f4) {
+        float min = Math.min(f1, f2);
+        min = Math.min(min, f3);
+        return Math.min(min, f4);
+    }
+
+    private float max(float f1, float f2, float f3, float f4) {
+        float max = Math.max(f1, f2);
+        max = Math.max(max, f3);
+        return Math.max(max, f4);
+    }
+
     /**
      * Return the maximum height that the edge effect will be drawn at given the original
      * {@link #setSize(int, int) input size}.
diff --git a/core/java/android/widget/HorizontalScrollView.java b/core/java/android/widget/HorizontalScrollView.java
index 23915e0..bf552e2 100644
--- a/core/java/android/widget/HorizontalScrollView.java
+++ b/core/java/android/widget/HorizontalScrollView.java
@@ -249,6 +249,26 @@
     }
 
     /**
+     * API used for prototyping stretch effect parameters in framework sample apps
+     * @hide
+     */
+    public void setEdgeEffectIntensity(float intensity) {
+        mEdgeGlowLeft.setMaxStretchIntensity(intensity);
+        mEdgeGlowRight.setMaxStretchIntensity(intensity);
+        invalidate();
+    }
+
+    /**
+     * API used for prototyping stretch effect parameters in the framework sample apps
+     * @hide
+     */
+    public void setStretchDistance(float distance) {
+        mEdgeGlowLeft.setStretchDistance(distance);
+        mEdgeGlowRight.setStretchDistance(distance);
+        invalidate();
+    }
+
+    /**
      * Sets the right edge effect color.
      *
      * @param color The color for the right edge effect.
diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java
index 65f3da7..3006729 100644
--- a/core/java/android/widget/ScrollView.java
+++ b/core/java/android/widget/ScrollView.java
@@ -281,6 +281,26 @@
     }
 
     /**
+     * API used for prototyping stretch effect parameters in framework sample apps
+     * @hide
+     */
+    public void setEdgeEffectIntensity(float intensity) {
+        mEdgeGlowTop.setMaxStretchIntensity(intensity);
+        mEdgeGlowBottom.setMaxStretchIntensity(intensity);
+        invalidate();
+    }
+
+    /**
+     * API used for prototyping stretch effect parameters in the framework sample apps
+     * @hide
+     */
+    public void setStretchDistance(float distance) {
+        mEdgeGlowTop.setStretchDistance(distance);
+        mEdgeGlowBottom.setStretchDistance(distance);
+        invalidate();
+    }
+
+    /**
      * Sets the bottom edge effect color.
      *
      * @param color The color for the bottom edge effect.
diff --git a/graphics/java/android/graphics/RenderNode.java b/graphics/java/android/graphics/RenderNode.java
index f6f770b..da5162b 100644
--- a/graphics/java/android/graphics/RenderNode.java
+++ b/graphics/java/android/graphics/RenderNode.java
@@ -719,11 +719,11 @@
     /** @hide */
     public boolean stretch(float left, float top, float right, float bottom,
             float vecX, float vecY, float maxStretchAmount) {
-        if (1.0 < vecX || vecX < -1.0) {
-            throw new IllegalArgumentException("vecX must be in the range [-1, 1], was " + vecX);
+        if (Float.isInfinite(vecX) || Float.isNaN(vecX)) {
+            throw new IllegalArgumentException("vecX must be a finite, non-NaN value " + vecX);
         }
-        if (1.0 < vecY || vecY < -1.0) {
-            throw new IllegalArgumentException("vecY must be in the range [-1, 1], was " + vecY);
+        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(
@@ -734,7 +734,16 @@
             throw new IllegalArgumentException(
                     "The max stretch amount must be >0, got " + maxStretchAmount);
         }
-        return nStretch(mNativeRenderNode, left, top, right, bottom, vecX, vecY, maxStretchAmount);
+        return nStretch(
+                mNativeRenderNode,
+                left,
+                top,
+                right,
+                bottom,
+                vecX,
+                vecY,
+                maxStretchAmount
+        );
     }
 
     /**
diff --git a/libs/hwui/RenderProperties.h b/libs/hwui/RenderProperties.h
index 609706e..5540e2d 100644
--- a/libs/hwui/RenderProperties.h
+++ b/libs/hwui/RenderProperties.h
@@ -552,8 +552,8 @@
 
     bool promotedToLayer() const {
         return mLayerProperties.mType == LayerType::None && fitsOnLayer() &&
-               (mComputedFields.mNeedLayerForFunctors ||
-                mLayerProperties.mImageFilter != nullptr ||
+               (mComputedFields.mNeedLayerForFunctors || mLayerProperties.mImageFilter != nullptr ||
+                !mLayerProperties.getStretchEffect().isEmpty() ||
                 (!MathUtils::isZero(mPrimitiveFields.mAlpha) && mPrimitiveFields.mAlpha < 1 &&
                  mPrimitiveFields.mHasOverlappingRendering));
     }
diff --git a/libs/hwui/effects/StretchEffect.cpp b/libs/hwui/effects/StretchEffect.cpp
index 51cbc75..d4fd105 100644
--- a/libs/hwui/effects/StretchEffect.cpp
+++ b/libs/hwui/effects/StretchEffect.cpp
@@ -15,13 +15,195 @@
  */
 
 #include "StretchEffect.h"
+#include <SkImageFilter.h>
+#include <SkRefCnt.h>
+#include <SkRuntimeEffect.h>
+#include <SkString.h>
+#include <SkSurface.h>
+#include <include/effects/SkImageFilters.h>
+
+#include <memory>
 
 namespace android::uirenderer {
 
-sk_sp<SkImageFilter> StretchEffect::getImageFilter() const {
-    // TODO: Implement & Cache
-    // Probably need to use mutable to achieve caching
-    return nullptr;
+static const SkString stretchShader = SkString(R"(
+    uniform shader uContentTexture;
+
+    // multiplier to apply to scale effect
+    uniform float uMaxStretchIntensity;
+
+    // Maximum percentage to stretch beyond bounds  of target
+    uniform float uStretchAffectedDist;
+
+    // Distance stretched as a function of the normalized overscroll times
+    // scale intensity
+    uniform float uDistanceStretchedX;
+    uniform float uDistanceStretchedY;
+    uniform float uDistDiffX;
+
+    // Difference between the peak stretch amount and overscroll amount normalized
+    uniform float uDistDiffY;
+
+    // Horizontal offset represented as a ratio of pixels divided by the target width
+    uniform float uScrollX;
+    // Vertical offset represented as a ratio of pixels divided by the target height
+    uniform float uScrollY;
+
+    // Normalized overscroll amount in the horizontal direction
+    uniform float uOverscrollX;
+
+    // Normalized overscroll amount in the vertical direction
+    uniform float uOverscrollY;
+    uniform float viewportWidth; // target height in pixels
+    uniform float viewportHeight; // target width in pixels
+
+    void computeOverscrollStart(
+        out float outPos,
+        float inPos,
+        float overscroll,
+        float uStretchAffectedDist,
+        float distanceStretched
+    ) {
+        float offsetPos = uStretchAffectedDist - inPos;
+        float posBasedVariation = smoothstep(0., uStretchAffectedDist, offsetPos);
+        float stretchIntensity = overscroll * posBasedVariation;
+        outPos = distanceStretched - (offsetPos / (1. + stretchIntensity));
+    }
+
+    void computeOverscrollEnd(
+        out float outPos,
+        float inPos,
+        float overscroll,
+        float reverseStretchDist,
+        float uStretchAffectedDist,
+        float distanceStretched
+    ) {
+        float offsetPos = inPos - reverseStretchDist;
+        float posBasedVariation = (smoothstep(0., uStretchAffectedDist, offsetPos));
+        float stretchIntensity = (-overscroll) * posBasedVariation;
+        outPos = 1 - (distanceStretched - (offsetPos / (1. + stretchIntensity)));
+    }
+
+    void computeOverscroll(
+        out float outPos,
+        float inPos,
+        float overscroll,
+        float uStretchAffectedDist,
+        float distanceStretched,
+        float distanceDiff
+    ) {
+        if (overscroll > 0) {
+            if (inPos <= uStretchAffectedDist) {
+                computeOverscrollStart(
+                  outPos,
+                  inPos,
+                  overscroll,
+                  uStretchAffectedDist,
+                  distanceStretched
+                );
+            } else if (inPos >= distanceStretched) {
+                outPos = distanceDiff + inPos;
+            }
+        }
+        if (overscroll < 0) {
+            float stretchAffectedDist = 1. - uStretchAffectedDist;
+            if (inPos >= stretchAffectedDist) {
+                computeOverscrollEnd(
+                  outPos,
+                  inPos,
+                  overscroll,
+                  stretchAffectedDist,
+                  uStretchAffectedDist,
+                  distanceStretched
+                );
+            } else if (inPos < stretchAffectedDist) {
+                outPos = -distanceDiff + inPos;
+            }
+        }
+    }
+
+    vec4 main(vec2 coord) {
+        // Normalize SKSL pixel coordinate into a unit vector
+        float inU = coord.x / viewportWidth;
+        float inV = coord.y / viewportHeight;
+        float outU;
+        float outV;
+        float stretchIntensity;
+        // Add the normalized scroll position within scrolling list
+        inU += uScrollX;
+        inV += uScrollY;
+        outU = inU;
+        outV = inV;
+        computeOverscroll(
+            outU,
+            inU,
+            uOverscrollX,
+            uStretchAffectedDist,
+            uDistanceStretchedX,
+            uDistDiffX
+        );
+        computeOverscroll(
+            outV,
+            inV,
+            uOverscrollY,
+            uStretchAffectedDist,
+            uDistanceStretchedY,
+            uDistDiffY
+        );
+        coord.x = outU * viewportWidth;
+        coord.y = outV * viewportHeight;
+        return sample(uContentTexture, coord);
+    })");
+
+static const float ZERO = 0.f;
+
+sk_sp<SkImageFilter> StretchEffect::getImageFilter(const sk_sp<SkImage>& snapshotImage) const {
+    if (isEmpty()) {
+        return nullptr;
+    }
+
+    if (mStretchFilter != nullptr) {
+        return mStretchFilter;
+    }
+
+    float distanceNotStretchedX = maxStretchAmount / stretchArea.width();
+    float distanceNotStretchedY = maxStretchAmount / stretchArea.height();
+    float normOverScrollDistX = mStretchDirection.x();
+    float normOverScrollDistY = mStretchDirection.y();
+    float distanceStretchedX = maxStretchAmount / (1 + abs(normOverScrollDistX));
+    float distanceStretchedY = maxStretchAmount / (1 + abs(normOverScrollDistY));
+    float diffX = distanceStretchedX - distanceNotStretchedX;
+    float diffY = distanceStretchedY - distanceNotStretchedY;
+    float viewportWidth = stretchArea.width();
+    float viewportHeight = stretchArea.height();
+
+    if (mBuilder == nullptr) {
+        mBuilder = std::make_unique<SkRuntimeShaderBuilder>(getStretchEffect());
+    }
+
+    mBuilder->child("uContentTexture") = snapshotImage->makeShader(
+            SkTileMode::kClamp, SkTileMode::kClamp, SkSamplingOptions(SkFilterMode::kLinear));
+    mBuilder->uniform("uStretchAffectedDist").set(&maxStretchAmount, 1);
+    mBuilder->uniform("uDistanceStretchedX").set(&distanceStretchedX, 1);
+    mBuilder->uniform("uDistanceStretchedY").set(&distanceStretchedY, 1);
+    mBuilder->uniform("uDistDiffX").set(&diffX, 1);
+    mBuilder->uniform("uDistDiffY").set(&diffY, 1);
+    mBuilder->uniform("uOverscrollX").set(&normOverScrollDistX, 1);
+    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);
+
+    mStretchFilter = SkImageFilters::Shader(mBuilder->makeShader(nullptr, false),
+                                            SkRect{0, 0, viewportWidth, viewportHeight});
+
+    return mStretchFilter;
+}
+
+sk_sp<SkRuntimeEffect> StretchEffect::getStretchEffect() {
+    const static SkRuntimeEffect::Result instance = SkRuntimeEffect::Make(stretchShader);
+    return instance.effect;
 }
 
 } // namespace android::uirenderer
\ No newline at end of file
diff --git a/libs/hwui/effects/StretchEffect.h b/libs/hwui/effects/StretchEffect.h
index 7dfd639..d2da06b 100644
--- a/libs/hwui/effects/StretchEffect.h
+++ b/libs/hwui/effects/StretchEffect.h
@@ -18,9 +18,11 @@
 
 #include "utils/MathUtils.h"
 
+#include <SkImage.h>
+#include <SkImageFilter.h>
 #include <SkPoint.h>
 #include <SkRect.h>
-#include <SkImageFilter.h>
+#include <SkRuntimeEffect.h>
 
 namespace android::uirenderer {
 
@@ -31,15 +33,27 @@
         SmoothStep,
     };
 
+    StretchEffect(const SkRect& area, const SkVector& direction, float maxStretchAmount)
+            : stretchArea(area), maxStretchAmount(maxStretchAmount), mStretchDirection(direction) {}
+
+    StretchEffect() {}
+
     bool isEmpty() const {
-        return MathUtils::isZero(stretchDirection.x())
-                && MathUtils::isZero(stretchDirection.y());
+        return MathUtils::isZero(mStretchDirection.x()) && MathUtils::isZero(mStretchDirection.y());
     }
 
     void setEmpty() {
         *this = StretchEffect{};
     }
 
+    StretchEffect& operator=(const StretchEffect& other) {
+        this->stretchArea = other.stretchArea;
+        this->mStretchDirection = other.mStretchDirection;
+        this->mStretchFilter = nullptr;
+        this->maxStretchAmount = other.maxStretchAmount;
+        return *this;
+    }
+
     void mergeWith(const StretchEffect& other) {
         if (other.isEmpty()) {
             return;
@@ -48,7 +62,7 @@
             *this = other;
             return;
         }
-        stretchDirection += other.stretchDirection;
+        setStretchDirection(mStretchDirection + other.mStretchDirection);
         if (isEmpty()) {
             return setEmpty();
         }
@@ -56,11 +70,23 @@
         maxStretchAmount = std::max(maxStretchAmount, other.maxStretchAmount);
     }
 
-    sk_sp<SkImageFilter> getImageFilter() const;
+    sk_sp<SkImageFilter> getImageFilter(const sk_sp<SkImage>& snapshotImage) const;
 
     SkRect stretchArea {0, 0, 0, 0};
-    SkVector stretchDirection {0, 0};
     float maxStretchAmount = 0;
+
+    void setStretchDirection(const SkVector& direction) {
+        mStretchFilter = 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<SkImageFilter> mStretchFilter;
 };
 
 } // namespace android::uirenderer
diff --git a/libs/hwui/jni/android_graphics_RenderNode.cpp b/libs/hwui/jni/android_graphics_RenderNode.cpp
index 5f60437..fc7d0d1 100644
--- a/libs/hwui/jni/android_graphics_RenderNode.cpp
+++ b/libs/hwui/jni/android_graphics_RenderNode.cpp
@@ -180,14 +180,13 @@
 }
 
 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 max) {
+                                                jfloat left, jfloat top, jfloat right,
+                                                jfloat bottom, jfloat vX, jfloat vY, jfloat max) {
+    StretchEffect effect =
+            StretchEffect(SkRect::MakeLTRB(left, top, right, bottom), {.fX = vX, .fY = vY}, max);
     RenderNode* renderNode = reinterpret_cast<RenderNode*>(renderNodePtr);
     renderNode->mutateStagingProperties().mutateLayerProperties().mutableStretchEffect().mergeWith(
-            StretchEffect{
-        .stretchArea = SkRect::MakeLTRB(left, top, right, bottom),
-        .stretchDirection = {.fX = vX, .fY = vY},
-        .maxStretchAmount = max
-    });
+            effect);
     renderNode->setPropertyFieldsDirty(RenderNode::GENERIC);
     return true;
 }
@@ -659,10 +658,11 @@
                 return;
             }
 #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, effect->stretchDirection.fX,
-                                effect->stretchDirection.fY, effect->maxStretchAmount);
+                                area.right, area.bottom, stretchDirection.fX, stretchDirection.fY,
+                                effect->maxStretchAmount);
 #endif
             env->DeleteLocalRef(localref);
         }
@@ -702,106 +702,110 @@
 const char* const kClassPathName = "android/graphics/RenderNode";
 
 static const JNINativeMethod gMethods[] = {
-// ----------------------------------------------------------------------------
-// Regular JNI
-// ----------------------------------------------------------------------------
-    { "nCreate",               "(Ljava/lang/String;)J", (void*) android_view_RenderNode_create },
-    { "nGetNativeFinalizer",   "()J",    (void*) android_view_RenderNode_getNativeFinalizer },
-    { "nOutput",               "(J)V",    (void*) android_view_RenderNode_output },
-    { "nGetUsageSize",         "(J)I",    (void*) android_view_RenderNode_getUsageSize },
-    { "nGetAllocatedSize",         "(J)I",    (void*) android_view_RenderNode_getAllocatedSize },
-    { "nAddAnimator",              "(JJ)V", (void*) android_view_RenderNode_addAnimator },
-    { "nEndAllAnimators",          "(J)V", (void*) android_view_RenderNode_endAllAnimators },
-    { "nRequestPositionUpdates",   "(JLandroid/graphics/RenderNode$PositionUpdateListener;)V", (void*) android_view_RenderNode_requestPositionUpdates },
+        // ----------------------------------------------------------------------------
+        // Regular JNI
+        // ----------------------------------------------------------------------------
+        {"nCreate", "(Ljava/lang/String;)J", (void*)android_view_RenderNode_create},
+        {"nGetNativeFinalizer", "()J", (void*)android_view_RenderNode_getNativeFinalizer},
+        {"nOutput", "(J)V", (void*)android_view_RenderNode_output},
+        {"nGetUsageSize", "(J)I", (void*)android_view_RenderNode_getUsageSize},
+        {"nGetAllocatedSize", "(J)I", (void*)android_view_RenderNode_getAllocatedSize},
+        {"nAddAnimator", "(JJ)V", (void*)android_view_RenderNode_addAnimator},
+        {"nEndAllAnimators", "(J)V", (void*)android_view_RenderNode_endAllAnimators},
+        {"nRequestPositionUpdates", "(JLandroid/graphics/RenderNode$PositionUpdateListener;)V",
+         (void*)android_view_RenderNode_requestPositionUpdates},
 
-// ----------------------------------------------------------------------------
-// Critical JNI via @CriticalNative annotation in RenderNode.java
-// ----------------------------------------------------------------------------
-    { "nDiscardDisplayList",   "(J)V",   (void*) android_view_RenderNode_discardDisplayList },
-    { "nIsValid",              "(J)Z",   (void*) android_view_RenderNode_isValid },
-    { "nSetLayerType",         "(JI)Z",  (void*) android_view_RenderNode_setLayerType },
-    { "nGetLayerType",         "(J)I",   (void*) android_view_RenderNode_getLayerType },
-    { "nSetLayerPaint",        "(JJ)Z",  (void*) android_view_RenderNode_setLayerPaint },
-    { "nSetStaticMatrix",      "(JJ)Z",  (void*) android_view_RenderNode_setStaticMatrix },
-    { "nSetAnimationMatrix",   "(JJ)Z",  (void*) android_view_RenderNode_setAnimationMatrix },
-    { "nGetAnimationMatrix",   "(JJ)Z",  (void*) android_view_RenderNode_getAnimationMatrix },
-    { "nSetClipToBounds",      "(JZ)Z",  (void*) android_view_RenderNode_setClipToBounds },
-    { "nGetClipToBounds",      "(J)Z",   (void*) android_view_RenderNode_getClipToBounds },
-    { "nSetClipBounds",        "(JIIII)Z", (void*) android_view_RenderNode_setClipBounds },
-    { "nSetClipBoundsEmpty",   "(J)Z",   (void*) android_view_RenderNode_setClipBoundsEmpty },
-    { "nSetProjectBackwards",  "(JZ)Z",  (void*) android_view_RenderNode_setProjectBackwards },
-    { "nSetProjectionReceiver","(JZ)Z",  (void*) android_view_RenderNode_setProjectionReceiver },
+        // ----------------------------------------------------------------------------
+        // Critical JNI via @CriticalNative annotation in RenderNode.java
+        // ----------------------------------------------------------------------------
+        {"nDiscardDisplayList", "(J)V", (void*)android_view_RenderNode_discardDisplayList},
+        {"nIsValid", "(J)Z", (void*)android_view_RenderNode_isValid},
+        {"nSetLayerType", "(JI)Z", (void*)android_view_RenderNode_setLayerType},
+        {"nGetLayerType", "(J)I", (void*)android_view_RenderNode_getLayerType},
+        {"nSetLayerPaint", "(JJ)Z", (void*)android_view_RenderNode_setLayerPaint},
+        {"nSetStaticMatrix", "(JJ)Z", (void*)android_view_RenderNode_setStaticMatrix},
+        {"nSetAnimationMatrix", "(JJ)Z", (void*)android_view_RenderNode_setAnimationMatrix},
+        {"nGetAnimationMatrix", "(JJ)Z", (void*)android_view_RenderNode_getAnimationMatrix},
+        {"nSetClipToBounds", "(JZ)Z", (void*)android_view_RenderNode_setClipToBounds},
+        {"nGetClipToBounds", "(J)Z", (void*)android_view_RenderNode_getClipToBounds},
+        {"nSetClipBounds", "(JIIII)Z", (void*)android_view_RenderNode_setClipBounds},
+        {"nSetClipBoundsEmpty", "(J)Z", (void*)android_view_RenderNode_setClipBoundsEmpty},
+        {"nSetProjectBackwards", "(JZ)Z", (void*)android_view_RenderNode_setProjectBackwards},
+        {"nSetProjectionReceiver", "(JZ)Z", (void*)android_view_RenderNode_setProjectionReceiver},
 
-    { "nSetOutlineRoundRect",  "(JIIIIFF)Z", (void*) android_view_RenderNode_setOutlineRoundRect },
-    { "nSetOutlinePath",       "(JJF)Z", (void*) android_view_RenderNode_setOutlinePath },
-    { "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",              "(JFFFFFFF)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 },
-    { "nSetAmbientShadowColor","(JI)Z",  (void*) android_view_RenderNode_setAmbientShadowColor },
-    { "nGetAmbientShadowColor","(J)I",   (void*) android_view_RenderNode_getAmbientShadowColor },
-    { "nSetClipToOutline",     "(JZ)Z",  (void*) android_view_RenderNode_setClipToOutline },
-    { "nSetRevealClip",        "(JZFFF)Z", (void*) android_view_RenderNode_setRevealClip },
+        {"nSetOutlineRoundRect", "(JIIIIFF)Z", (void*)android_view_RenderNode_setOutlineRoundRect},
+        {"nSetOutlinePath", "(JJF)Z", (void*)android_view_RenderNode_setOutlinePath},
+        {"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", "(JFFFFFFF)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},
+        {"nSetAmbientShadowColor", "(JI)Z", (void*)android_view_RenderNode_setAmbientShadowColor},
+        {"nGetAmbientShadowColor", "(J)I", (void*)android_view_RenderNode_getAmbientShadowColor},
+        {"nSetClipToOutline", "(JZ)Z", (void*)android_view_RenderNode_setClipToOutline},
+        {"nSetRevealClip", "(JZFFF)Z", (void*)android_view_RenderNode_setRevealClip},
 
-    { "nSetAlpha",             "(JF)Z",  (void*) android_view_RenderNode_setAlpha },
-    { "nSetRenderEffect",      "(JJ)Z",  (void*) android_view_RenderNode_setRenderEffect },
-    { "nSetHasOverlappingRendering", "(JZ)Z",
-            (void*) android_view_RenderNode_setHasOverlappingRendering },
-    { "nSetUsageHint",    "(JI)V", (void*) android_view_RenderNode_setUsageHint },
-    { "nSetElevation",         "(JF)Z",  (void*) android_view_RenderNode_setElevation },
-    { "nSetTranslationX",      "(JF)Z",  (void*) android_view_RenderNode_setTranslationX },
-    { "nSetTranslationY",      "(JF)Z",  (void*) android_view_RenderNode_setTranslationY },
-    { "nSetTranslationZ",      "(JF)Z",  (void*) android_view_RenderNode_setTranslationZ },
-    { "nSetRotation",          "(JF)Z",  (void*) android_view_RenderNode_setRotation },
-    { "nSetRotationX",         "(JF)Z",  (void*) android_view_RenderNode_setRotationX },
-    { "nSetRotationY",         "(JF)Z",  (void*) android_view_RenderNode_setRotationY },
-    { "nSetScaleX",            "(JF)Z",  (void*) android_view_RenderNode_setScaleX },
-    { "nSetScaleY",            "(JF)Z",  (void*) android_view_RenderNode_setScaleY },
-    { "nSetPivotX",            "(JF)Z",  (void*) android_view_RenderNode_setPivotX },
-    { "nSetPivotY",            "(JF)Z",  (void*) android_view_RenderNode_setPivotY },
-    { "nResetPivot",           "(J)Z",   (void*) android_view_RenderNode_resetPivot },
-    { "nSetCameraDistance",    "(JF)Z",  (void*) android_view_RenderNode_setCameraDistance },
-    { "nSetLeft",              "(JI)Z",  (void*) android_view_RenderNode_setLeft },
-    { "nSetTop",               "(JI)Z",  (void*) android_view_RenderNode_setTop },
-    { "nSetRight",             "(JI)Z",  (void*) android_view_RenderNode_setRight },
-    { "nSetBottom",            "(JI)Z",  (void*) android_view_RenderNode_setBottom },
-    { "nGetLeft",              "(J)I",  (void*) android_view_RenderNode_getLeft },
-    { "nGetTop",               "(J)I",  (void*) android_view_RenderNode_getTop },
-    { "nGetRight",             "(J)I",  (void*) android_view_RenderNode_getRight },
-    { "nGetBottom",            "(J)I",  (void*) android_view_RenderNode_getBottom },
-    { "nSetLeftTopRightBottom","(JIIII)Z", (void*) android_view_RenderNode_setLeftTopRightBottom },
-    { "nOffsetLeftAndRight",   "(JI)Z",  (void*) android_view_RenderNode_offsetLeftAndRight },
-    { "nOffsetTopAndBottom",   "(JI)Z",  (void*) android_view_RenderNode_offsetTopAndBottom },
+        {"nSetAlpha", "(JF)Z", (void*)android_view_RenderNode_setAlpha},
+        {"nSetRenderEffect", "(JJ)Z", (void*)android_view_RenderNode_setRenderEffect},
+        {"nSetHasOverlappingRendering", "(JZ)Z",
+         (void*)android_view_RenderNode_setHasOverlappingRendering},
+        {"nSetUsageHint", "(JI)V", (void*)android_view_RenderNode_setUsageHint},
+        {"nSetElevation", "(JF)Z", (void*)android_view_RenderNode_setElevation},
+        {"nSetTranslationX", "(JF)Z", (void*)android_view_RenderNode_setTranslationX},
+        {"nSetTranslationY", "(JF)Z", (void*)android_view_RenderNode_setTranslationY},
+        {"nSetTranslationZ", "(JF)Z", (void*)android_view_RenderNode_setTranslationZ},
+        {"nSetRotation", "(JF)Z", (void*)android_view_RenderNode_setRotation},
+        {"nSetRotationX", "(JF)Z", (void*)android_view_RenderNode_setRotationX},
+        {"nSetRotationY", "(JF)Z", (void*)android_view_RenderNode_setRotationY},
+        {"nSetScaleX", "(JF)Z", (void*)android_view_RenderNode_setScaleX},
+        {"nSetScaleY", "(JF)Z", (void*)android_view_RenderNode_setScaleY},
+        {"nSetPivotX", "(JF)Z", (void*)android_view_RenderNode_setPivotX},
+        {"nSetPivotY", "(JF)Z", (void*)android_view_RenderNode_setPivotY},
+        {"nResetPivot", "(J)Z", (void*)android_view_RenderNode_resetPivot},
+        {"nSetCameraDistance", "(JF)Z", (void*)android_view_RenderNode_setCameraDistance},
+        {"nSetLeft", "(JI)Z", (void*)android_view_RenderNode_setLeft},
+        {"nSetTop", "(JI)Z", (void*)android_view_RenderNode_setTop},
+        {"nSetRight", "(JI)Z", (void*)android_view_RenderNode_setRight},
+        {"nSetBottom", "(JI)Z", (void*)android_view_RenderNode_setBottom},
+        {"nGetLeft", "(J)I", (void*)android_view_RenderNode_getLeft},
+        {"nGetTop", "(J)I", (void*)android_view_RenderNode_getTop},
+        {"nGetRight", "(J)I", (void*)android_view_RenderNode_getRight},
+        {"nGetBottom", "(J)I", (void*)android_view_RenderNode_getBottom},
+        {"nSetLeftTopRightBottom", "(JIIII)Z",
+         (void*)android_view_RenderNode_setLeftTopRightBottom},
+        {"nOffsetLeftAndRight", "(JI)Z", (void*)android_view_RenderNode_offsetLeftAndRight},
+        {"nOffsetTopAndBottom", "(JI)Z", (void*)android_view_RenderNode_offsetTopAndBottom},
 
-    { "nHasOverlappingRendering", "(J)Z",  (void*) android_view_RenderNode_hasOverlappingRendering },
-    { "nGetClipToOutline",        "(J)Z",  (void*) android_view_RenderNode_getClipToOutline },
-    { "nGetAlpha",                "(J)F",  (void*) android_view_RenderNode_getAlpha },
-    { "nGetCameraDistance",       "(J)F",  (void*) android_view_RenderNode_getCameraDistance },
-    { "nGetScaleX",               "(J)F",  (void*) android_view_RenderNode_getScaleX },
-    { "nGetScaleY",               "(J)F",  (void*) android_view_RenderNode_getScaleY },
-    { "nGetElevation",            "(J)F",  (void*) android_view_RenderNode_getElevation },
-    { "nGetTranslationX",         "(J)F",  (void*) android_view_RenderNode_getTranslationX },
-    { "nGetTranslationY",         "(J)F",  (void*) android_view_RenderNode_getTranslationY },
-    { "nGetTranslationZ",         "(J)F",  (void*) android_view_RenderNode_getTranslationZ },
-    { "nGetRotation",             "(J)F",  (void*) android_view_RenderNode_getRotation },
-    { "nGetRotationX",            "(J)F",  (void*) android_view_RenderNode_getRotationX },
-    { "nGetRotationY",            "(J)F",  (void*) android_view_RenderNode_getRotationY },
-    { "nIsPivotExplicitlySet",    "(J)Z",  (void*) android_view_RenderNode_isPivotExplicitlySet },
-    { "nHasIdentityMatrix",       "(J)Z",  (void*) android_view_RenderNode_hasIdentityMatrix },
+        {"nHasOverlappingRendering", "(J)Z",
+         (void*)android_view_RenderNode_hasOverlappingRendering},
+        {"nGetClipToOutline", "(J)Z", (void*)android_view_RenderNode_getClipToOutline},
+        {"nGetAlpha", "(J)F", (void*)android_view_RenderNode_getAlpha},
+        {"nGetCameraDistance", "(J)F", (void*)android_view_RenderNode_getCameraDistance},
+        {"nGetScaleX", "(J)F", (void*)android_view_RenderNode_getScaleX},
+        {"nGetScaleY", "(J)F", (void*)android_view_RenderNode_getScaleY},
+        {"nGetElevation", "(J)F", (void*)android_view_RenderNode_getElevation},
+        {"nGetTranslationX", "(J)F", (void*)android_view_RenderNode_getTranslationX},
+        {"nGetTranslationY", "(J)F", (void*)android_view_RenderNode_getTranslationY},
+        {"nGetTranslationZ", "(J)F", (void*)android_view_RenderNode_getTranslationZ},
+        {"nGetRotation", "(J)F", (void*)android_view_RenderNode_getRotation},
+        {"nGetRotationX", "(J)F", (void*)android_view_RenderNode_getRotationX},
+        {"nGetRotationY", "(J)F", (void*)android_view_RenderNode_getRotationY},
+        {"nIsPivotExplicitlySet", "(J)Z", (void*)android_view_RenderNode_isPivotExplicitlySet},
+        {"nHasIdentityMatrix", "(J)Z", (void*)android_view_RenderNode_hasIdentityMatrix},
 
-    { "nGetTransformMatrix",       "(JJ)V", (void*) android_view_RenderNode_getTransformMatrix },
-    { "nGetInverseTransformMatrix","(JJ)V", (void*) android_view_RenderNode_getInverseTransformMatrix },
+        {"nGetTransformMatrix", "(JJ)V", (void*)android_view_RenderNode_getTransformMatrix},
+        {"nGetInverseTransformMatrix", "(JJ)V",
+         (void*)android_view_RenderNode_getInverseTransformMatrix},
 
-    { "nGetPivotX",                "(J)F",  (void*) android_view_RenderNode_getPivotX },
-    { "nGetPivotY",                "(J)F",  (void*) android_view_RenderNode_getPivotY },
-    { "nGetWidth",                 "(J)I",  (void*) android_view_RenderNode_getWidth },
-    { "nGetHeight",                "(J)I",  (void*) android_view_RenderNode_getHeight },
-    { "nSetAllowForceDark",        "(JZ)Z", (void*) android_view_RenderNode_setAllowForceDark },
-    { "nGetAllowForceDark",        "(J)Z",  (void*) android_view_RenderNode_getAllowForceDark },
-    { "nGetUniqueId",              "(J)J",  (void*) android_view_RenderNode_getUniqueId },
+        {"nGetPivotX", "(J)F", (void*)android_view_RenderNode_getPivotX},
+        {"nGetPivotY", "(J)F", (void*)android_view_RenderNode_getPivotY},
+        {"nGetWidth", "(J)I", (void*)android_view_RenderNode_getWidth},
+        {"nGetHeight", "(J)I", (void*)android_view_RenderNode_getHeight},
+        {"nSetAllowForceDark", "(JZ)Z", (void*)android_view_RenderNode_setAllowForceDark},
+        {"nGetAllowForceDark", "(J)Z", (void*)android_view_RenderNode_getAllowForceDark},
+        {"nGetUniqueId", "(J)J", (void*)android_view_RenderNode_getUniqueId},
 };
 
 int register_android_view_RenderNode(JNIEnv* env) {
diff --git a/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp b/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp
index c010212..cb0ff8d 100644
--- a/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp
+++ b/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp
@@ -169,8 +169,8 @@
     displayList->mProjectedOutline = nullptr;
 }
 
-static bool layerNeedsPaint(const LayerProperties& properties, float alphaMultiplier,
-                            SkPaint* paint) {
+static bool layerNeedsPaint(const sk_sp<SkImage>& snapshotImage, const LayerProperties& properties,
+                            float alphaMultiplier, SkPaint* paint) {
     if (alphaMultiplier < 1.0f || properties.alpha() < 255 ||
         properties.xferMode() != SkBlendMode::kSrcOver || properties.getColorFilter() != nullptr ||
         properties.getImageFilter() != nullptr || !properties.getStretchEffect().isEmpty()) {
@@ -179,7 +179,8 @@
         paint->setColorFilter(sk_ref_sp(properties.getColorFilter()));
 
         sk_sp<SkImageFilter> imageFilter = sk_ref_sp(properties.getImageFilter());
-        sk_sp<SkImageFilter> stretchFilter = properties.getStretchEffect().getImageFilter();
+        sk_sp<SkImageFilter> stretchFilter =
+                properties.getStretchEffect().getImageFilter(snapshotImage);
         sk_sp<SkImageFilter> filter;
         if (imageFilter && stretchFilter) {
             filter = SkImageFilters::Compose(
@@ -240,7 +241,8 @@
         if (renderNode->getLayerSurface() && mComposeLayer) {
             SkASSERT(properties.effectiveLayerType() == LayerType::RenderLayer);
             SkPaint paint;
-            layerNeedsPaint(layerProperties, alphaMultiplier, &paint);
+            sk_sp<SkImage> snapshotImage = renderNode->getLayerSurface()->makeImageSnapshot();
+            layerNeedsPaint(snapshotImage, layerProperties, alphaMultiplier, &paint);
             SkSamplingOptions sampling(SkFilterMode::kLinear);
 
             // surfaces for layers are created on LAYER_SIZE boundaries (which are >= layer size) so
@@ -254,8 +256,8 @@
                 canvas->drawAnnotation(bounds, String8::format(
                     "SurfaceID|%" PRId64, renderNode->uniqueId()).c_str(), nullptr);
             }
-            canvas->drawImageRect(renderNode->getLayerSurface()->makeImageSnapshot(), bounds,
-                                  bounds, sampling, &paint, SkCanvas::kStrict_SrcRectConstraint);
+            canvas->drawImageRect(snapshotImage, bounds, bounds, sampling, &paint,
+                                  SkCanvas::kStrict_SrcRectConstraint);
 
             if (!renderNode->getSkiaLayer()->hasRenderedSinceRepaint) {
                 renderNode->getSkiaLayer()->hasRenderedSinceRepaint = true;
diff --git a/tests/HwAccelerationTest/AndroidManifest.xml b/tests/HwAccelerationTest/AndroidManifest.xml
index 62ccb1a0..6bf4492 100644
--- a/tests/HwAccelerationTest/AndroidManifest.xml
+++ b/tests/HwAccelerationTest/AndroidManifest.xml
@@ -771,6 +771,24 @@
             </intent-filter>
         </activity>
 
+        <activity android:name="StretchShaderActivity"
+                  android:label="RenderEffect/Stretch"
+                  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="EdgeEffectStretchActivity"
+                  android:label="RenderEffect/EdgeEffect stretch"
+                  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="TextActivity"
              android:label="Text/Simple Text"
              android:theme="@android:style/Theme.NoTitleBar"
diff --git a/tests/HwAccelerationTest/res/layout/stretch_layout.xml b/tests/HwAccelerationTest/res/layout/stretch_layout.xml
new file mode 100644
index 0000000..df5f297
--- /dev/null
+++ b/tests/HwAccelerationTest/res/layout/stretch_layout.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 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:id="@+id/scroll_view"
+    android:edgeEffectType="stretch"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent" >
+    <LinearLayout
+        android:orientation="vertical"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+
+        <HorizontalScrollView
+            android:id="@+id/horizontal_scroll_view"
+            android:edgeEffectType="stretch"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent">
+            <LinearLayout
+                android:orientation="horizontal"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent">
+                <ImageView
+                    android:layout_width="match_parent"
+                    android:layout_height="200dp"
+                    android:src="@drawable/sunset1"/>
+                <ImageView
+                    android:layout_width="match_parent"
+                    android:layout_height="200dp"
+                    android:src="@drawable/sunset1"/>
+                <ImageView
+                    android:layout_width="match_parent"
+                    android:layout_height="200dp"
+                    android:src="@drawable/sunset1"/>
+                <ImageView
+                    android:layout_width="match_parent"
+                    android:layout_height="200dp"
+                    android:src="@drawable/sunset1"/>
+                <ImageView
+                    android:layout_width="match_parent"
+                    android:layout_height="200dp"
+                    android:src="@drawable/sunset1"/>
+                <ImageView
+                    android:layout_width="match_parent"
+                    android:layout_height="200dp"
+                    android:src="@drawable/sunset1"/>
+            </LinearLayout>
+        </HorizontalScrollView>
+        <ImageView
+            android:layout_width="match_parent"
+            android:layout_height="200dp"
+            android:src="@drawable/sunset1"/>
+        <ImageView
+            android:layout_width="match_parent"
+            android:layout_height="200dp"
+            android:src="@drawable/sunset1"/>
+        <ImageView
+            android:layout_width="match_parent"
+            android:layout_height="200dp"
+            android:src="@drawable/sunset1"/>
+        <ImageView
+            android:layout_width="match_parent"
+            android:layout_height="200dp"
+            android:src="@drawable/sunset1"/>
+        <ImageView
+            android:layout_width="match_parent"
+            android:layout_height="200dp"
+            android:src="@drawable/sunset1"/>
+        <ImageView
+            android:layout_width="match_parent"
+            android:layout_height="200dp"
+            android:src="@drawable/sunset1"/>
+
+    </LinearLayout>
+</ScrollView>
diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/EdgeEffectStretchActivity.java b/tests/HwAccelerationTest/src/com/android/test/hwui/EdgeEffectStretchActivity.java
new file mode 100644
index 0000000..f0e6299
--- /dev/null
+++ b/tests/HwAccelerationTest/src/com/android/test/hwui/EdgeEffectStretchActivity.java
@@ -0,0 +1,36 @@
+/*
+ * 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.os.Bundle;
+import android.widget.HorizontalScrollView;
+import android.widget.ScrollView;
+
+public class EdgeEffectStretchActivity extends Activity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.stretch_layout);
+        HorizontalScrollView hsv = findViewById(R.id.horizontal_scroll_view);
+        hsv.setStretchDistance(50f);
+
+        ScrollView sv = findViewById(R.id.scroll_view);
+        sv.setStretchDistance(50f);
+    }
+}
diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/StretchShaderActivity.java b/tests/HwAccelerationTest/src/com/android/test/hwui/StretchShaderActivity.java
new file mode 100644
index 0000000..9bd933a
--- /dev/null
+++ b/tests/HwAccelerationTest/src/com/android/test/hwui/StretchShaderActivity.java
@@ -0,0 +1,540 @@
+/*
+ * 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.annotation.Nullable;
+import android.app.Activity;
+import android.graphics.Bitmap;
+import android.graphics.BitmapShader;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.RecordingCanvas;
+import android.graphics.Rect;
+import android.graphics.RenderEffect;
+import android.graphics.RuntimeShader;
+import android.graphics.Shader;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+public class StretchShaderActivity extends Activity {
+
+    private static final float MAX_STRETCH_INTENSITY = 1.5f;
+    private static final float STRETCH_AFFECTED_DISTANCE = 1.0f;
+
+    private float mScrollX = 0f;
+    private float mScrollY = 0f;
+
+    private float mMaxStretchIntensity = MAX_STRETCH_INTENSITY;
+    private float mStretchAffectedDistance = STRETCH_AFFECTED_DISTANCE;
+
+    private float mOverscrollX = 25f;
+    private float mOverscrollY = 25f;
+
+    private RuntimeShader mRuntimeShader;
+    private ImageView mImageView;
+    private ImageView mTestImageView;
+
+    private Bitmap mBitmap;
+
+    private StretchDrawable mStretchDrawable = new StretchDrawable();
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        LinearLayout linearLayout = new LinearLayout(this);
+        linearLayout.setOrientation(LinearLayout.VERTICAL);
+
+        mBitmap = ((BitmapDrawable) getDrawable(R.drawable.sunset1)).getBitmap();
+        mRuntimeShader = new RuntimeShader(SKSL, false);
+
+        BitmapShader bitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP,
+                Shader.TileMode.CLAMP);
+        mRuntimeShader.setInputShader("uContentTexture", bitmapShader);
+
+        mImageView = new ImageView(this);
+
+        mImageView.setRenderEffect(RenderEffect.createShaderEffect(mRuntimeShader));
+        mImageView.setImageDrawable(new ColorDrawable(Color.CYAN));
+
+        TextView overscrollXText = new TextView(this);
+        overscrollXText.setText("Overscroll X");
+
+        SeekBar overscrollXBar = new SeekBar(this);
+        overscrollXBar.setProgress(0);
+        overscrollXBar.setMin(-50);
+        overscrollXBar.setMax(50);
+        overscrollXBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+            @Override
+            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+                mOverscrollX = progress;
+                overscrollXText.setText("Overscroll X: " + mOverscrollX);
+                updateShader();
+            }
+
+            @Override
+            public void onStartTrackingTouch(SeekBar seekBar) {
+
+            }
+
+            @Override
+            public void onStopTrackingTouch(SeekBar seekBar) {
+
+            }
+        });
+
+        TextView overscrollYText = new TextView(this);
+        overscrollYText.setText("Overscroll Y");
+
+        SeekBar overscrollYBar = new SeekBar(this);
+        overscrollYBar.setProgress(0);
+        overscrollYBar.setMin(-50);
+        overscrollYBar.setMax(50);
+        overscrollYBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+            @Override
+            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+                mOverscrollY = progress;
+                overscrollYText.setText("Overscroll Y: " + mOverscrollY);
+                updateShader();
+            }
+
+            @Override
+            public void onStartTrackingTouch(SeekBar seekBar) {
+
+            }
+
+            @Override
+            public void onStopTrackingTouch(SeekBar seekBar) {
+
+            }
+        });
+
+        TextView scrollXText = new TextView(this);
+        scrollXText.setText("Scroll X");
+        SeekBar scrollXSeekBar = new SeekBar(this);
+        scrollXSeekBar.setMin(0);
+        scrollXSeekBar.setMax(100);
+        scrollXSeekBar.setProgress(0);
+        scrollXSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+            @Override
+            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+                mScrollX = (progress / 100f);
+                scrollXText.setText("Scroll X: " + mScrollY);
+                updateShader();
+            }
+
+            @Override
+            public void onStartTrackingTouch(SeekBar seekBar) {
+
+            }
+
+            @Override
+            public void onStopTrackingTouch(SeekBar seekBar) {
+
+            }
+        });
+
+        TextView scrollYText = new TextView(this);
+        scrollYText.setText("Scroll Y");
+        SeekBar scrollYSeekBar = new SeekBar(this);
+        scrollYSeekBar.setMin(0);
+        scrollYSeekBar.setMax(100);
+        scrollYSeekBar.setProgress(0);
+        scrollYSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+            @Override
+            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+                mScrollY = (progress / 100f);
+                scrollYText.setText("Scroll Y: " + mScrollY);
+                updateShader();
+            }
+
+            @Override
+            public void onStartTrackingTouch(SeekBar seekBar) {
+
+            }
+
+            @Override
+            public void onStopTrackingTouch(SeekBar seekBar) {
+
+            }
+        });
+
+        TextView stretchIntensityText = new TextView(this);
+        int stretchProgress = (int) (mMaxStretchIntensity * 100);
+        stretchIntensityText.setText("StretchIntensity: " + mMaxStretchIntensity);
+        SeekBar stretchIntensitySeekbar = new SeekBar(this);
+        stretchIntensitySeekbar.setProgress(stretchProgress);
+        stretchIntensitySeekbar.setMin(1);
+        stretchIntensitySeekbar.setMax((int) (MAX_STRETCH_INTENSITY * 100));
+        stretchIntensitySeekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+            @Override
+            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+                mMaxStretchIntensity = progress / 100f;
+                stretchIntensityText.setText("StretchIntensity: " + mMaxStretchIntensity);
+                updateShader();
+            }
+
+            @Override
+            public void onStartTrackingTouch(SeekBar seekBar) {
+
+            }
+
+            @Override
+            public void onStopTrackingTouch(SeekBar seekBar) {
+
+            }
+        });
+
+        TextView stretchDistanceText = new TextView(this);
+        stretchDistanceText.setText("StretchDistance");
+        SeekBar stretchDistanceSeekbar = new SeekBar(this);
+        stretchDistanceSeekbar.setMin(0);
+        stretchDistanceSeekbar.setProgress((int) (mStretchAffectedDistance * 100));
+        stretchDistanceSeekbar.setMax(100);
+        stretchDistanceSeekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+            @Override
+            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+                mStretchAffectedDistance = progress / 100f;
+                stretchDistanceText.setText("StretchDistance: " + mStretchAffectedDistance);
+                updateShader();
+            }
+
+            @Override
+            public void onStartTrackingTouch(SeekBar seekBar) {
+
+            }
+
+            @Override
+            public void onStopTrackingTouch(SeekBar seekBar) {
+
+            }
+        });
+
+
+        linearLayout.addView(mImageView,
+                new LinearLayout.LayoutParams(
+                        mBitmap.getWidth(),
+                        mBitmap.getHeight())
+        );
+
+        linearLayout.addView(overscrollXText,
+                new LinearLayout.LayoutParams(
+                        LinearLayout.LayoutParams.WRAP_CONTENT,
+                        LinearLayout.LayoutParams.WRAP_CONTENT
+                ));
+        linearLayout.addView(overscrollXBar,
+                new LinearLayout.LayoutParams(
+                        ViewGroup.LayoutParams.MATCH_PARENT,
+                        ViewGroup.LayoutParams.WRAP_CONTENT
+                )
+        );
+
+        linearLayout.addView(overscrollYText,
+                new LinearLayout.LayoutParams(
+                        LinearLayout.LayoutParams.WRAP_CONTENT,
+                        LinearLayout.LayoutParams.WRAP_CONTENT
+                ));
+        linearLayout.addView(overscrollYBar,
+                new LinearLayout.LayoutParams(
+                        ViewGroup.LayoutParams.MATCH_PARENT,
+                        ViewGroup.LayoutParams.WRAP_CONTENT
+                )
+        );
+
+        linearLayout.addView(scrollXText,
+                new LinearLayout.LayoutParams(
+                        LinearLayout.LayoutParams.WRAP_CONTENT,
+                        LinearLayout.LayoutParams.WRAP_CONTENT
+                ));
+
+        linearLayout.addView(scrollXSeekBar,
+                new LinearLayout.LayoutParams(
+                        ViewGroup.LayoutParams.MATCH_PARENT,
+                        ViewGroup.LayoutParams.WRAP_CONTENT
+                ));
+
+        linearLayout.addView(scrollYText,
+                new LinearLayout.LayoutParams(
+                        LinearLayout.LayoutParams.WRAP_CONTENT,
+                        LinearLayout.LayoutParams.WRAP_CONTENT
+                ));
+
+        linearLayout.addView(scrollYSeekBar,
+                new LinearLayout.LayoutParams(
+                        ViewGroup.LayoutParams.MATCH_PARENT,
+                        ViewGroup.LayoutParams.WRAP_CONTENT
+                ));
+
+        linearLayout.addView(stretchIntensityText,
+                new LinearLayout.LayoutParams(
+                        LinearLayout.LayoutParams.WRAP_CONTENT,
+                        LinearLayout.LayoutParams.WRAP_CONTENT
+                )
+        );
+
+        linearLayout.addView(stretchIntensitySeekbar,
+                new LinearLayout.LayoutParams(
+                        LinearLayout.LayoutParams.MATCH_PARENT,
+                        LinearLayout.LayoutParams.WRAP_CONTENT
+                )
+        );
+
+        linearLayout.addView(stretchDistanceText,
+                new LinearLayout.LayoutParams(
+                        LinearLayout.LayoutParams.WRAP_CONTENT,
+                        LinearLayout.LayoutParams.WRAP_CONTENT
+                ));
+
+        linearLayout.addView(stretchDistanceSeekbar,
+                new LinearLayout.LayoutParams(
+                        LinearLayout.LayoutParams.MATCH_PARENT,
+                        LinearLayout.LayoutParams.WRAP_CONTENT
+                ));
+
+        ImageView test = new ImageView(this);
+        mStretchDrawable.setBitmap(mBitmap);
+        test.setImageDrawable(mStretchDrawable);
+
+        mTestImageView = test;
+        linearLayout.addView(test,
+                new LinearLayout.LayoutParams(mBitmap.getWidth(), mBitmap.getHeight()));
+
+        setContentView(linearLayout);
+
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        mImageView.getViewTreeObserver().addOnPreDrawListener(
+                new ViewTreeObserver.OnPreDrawListener() {
+                    @Override
+                    public boolean onPreDraw() {
+                        updateShader();
+                        mImageView.getViewTreeObserver().removeOnPreDrawListener(this);
+                        return false;
+                    }
+                });
+    }
+
+    private void updateShader() {
+        final float width = mImageView.getWidth();
+        final float height = mImageView.getHeight();
+        final float distanceNotStretched = mStretchAffectedDistance;
+        final float normOverScrollDistX = mOverscrollX / width;
+        final float normOverScrollDistY = mOverscrollY / height;
+        final float distanceStretchedX =
+                mStretchAffectedDistance
+                        / (1 + Math.abs(normOverScrollDistX) * mMaxStretchIntensity);
+        final float distanceStretchedY =
+                mStretchAffectedDistance
+                        / (1 + Math.abs(normOverScrollDistY) * mMaxStretchIntensity);
+        final float diffX = distanceStretchedX - distanceNotStretched;
+        final float diffY = distanceStretchedY - distanceNotStretched;
+        float uScrollX = mScrollX;
+        float uScrollY = mScrollY;
+
+        mRuntimeShader.setUniform("uMaxStretchIntensity", mMaxStretchIntensity);
+        mRuntimeShader.setUniform("uStretchAffectedDist", mStretchAffectedDistance);
+        mRuntimeShader.setUniform("uDistanceStretchedX", distanceStretchedX);
+        mRuntimeShader.setUniform("uDistanceStretchedY", distanceStretchedY);
+        mRuntimeShader.setUniform("uDistDiffX", diffX);
+        mRuntimeShader.setUniform("uDistDiffY", diffY);
+        mRuntimeShader.setUniform("uOverscrollX", normOverScrollDistX);
+        mRuntimeShader.setUniform("uOverscrollY", normOverScrollDistY);
+        mRuntimeShader.setUniform("uScrollX", uScrollX);
+        mRuntimeShader.setUniform("uScrollY", uScrollY);
+        mRuntimeShader.setUniform("viewportWidth", width);
+        mRuntimeShader.setUniform("viewportHeight", height);
+
+        mImageView.setRenderEffect(RenderEffect.createShaderEffect(mRuntimeShader));
+
+        mStretchDrawable.setStretchDistance(mStretchAffectedDistance);
+        mStretchDrawable.setOverscrollX(normOverScrollDistX);
+        mStretchDrawable.setOverscrollY(normOverScrollDistY);
+    }
+
+    private static class StretchDrawable extends Drawable {
+
+        private float mStretchDistance = 0;
+        private float mOverScrollX = 0f;
+        private float mOverScrollY = 0f;
+        private Bitmap mBitmap = null;
+
+        public void setStretchDistance(float stretchDistance) {
+            mStretchDistance = stretchDistance;
+            invalidateSelf();
+        }
+
+        public void setOverscrollX(float overscrollX) {
+            mOverScrollX = overscrollX;
+            invalidateSelf();
+        }
+
+        public void setOverscrollY(float overscrollY) {
+            mOverScrollY = overscrollY;
+            invalidateSelf();
+        }
+
+        public void setBitmap(Bitmap bitmap) {
+            mBitmap = bitmap;
+            invalidateSelf();
+        }
+
+        @Override
+        public void draw(Canvas canvas) {
+            if (mStretchDistance > 0 && canvas instanceof RecordingCanvas) {
+                Rect bounds = getBounds();
+                ((RecordingCanvas) canvas).mNode.stretch(
+                        0,
+                        0,
+                        bounds.width(),
+                        bounds.height(),
+                        mOverScrollX,
+                        mOverScrollY,
+                        mStretchDistance
+                );
+            }
+            if (mBitmap != null) {
+                canvas.drawBitmap(mBitmap, 0f, 0f, null);
+            }
+        }
+
+        @Override
+        public void setAlpha(int alpha) {
+
+        }
+
+        @Override
+        public void setColorFilter(ColorFilter colorFilter) {
+
+        }
+
+        @Override
+        public int getOpacity() {
+            return 0;
+        }
+    }
+
+    private static final String SKSL = "in shader uContentTexture;\n"
+            + "uniform float uMaxStretchIntensity; // multiplier to apply to scale effect\n"
+            + "uniform float uStretchAffectedDist; // Maximum percentage to stretch beyond bounds"
+            + " of target\n"
+            + "\n"
+            + "// Distance stretched as a function of the normalized overscroll times scale "
+            + "intensity\n"
+            + "uniform float uDistanceStretchedX;\n"
+            + "uniform float uDistanceStretchedY;\n"
+            + "uniform float uDistDiffX;\n"
+            + "uniform float uDistDiffY; // Difference between the peak stretch amount and "
+            + "overscroll amount normalized\n"
+            + "uniform float uScrollX; // Horizontal offset represented as a ratio of pixels "
+            + "divided by the target width\n"
+            + "uniform float uScrollY; // Vertical offset represented as a ratio of pixels "
+            + "divided by the target height\n"
+            + "uniform float uOverscrollX; // Normalized overscroll amount in the horizontal "
+            + "direction\n"
+            + "uniform float uOverscrollY; // Normalized overscroll amount in the vertical "
+            + "direction\n"
+            + "\n"
+            + "uniform float viewportWidth; // target height in pixels\n"
+            + "uniform float viewportHeight; // target width in pixels\n"
+            + "\n"
+            + "vec4 main(vec2 coord) {\n"
+            + "\n"
+            + "    // Normalize SKSL pixel coordinate into a unit vector\n"
+            + "    vec2 uv = vec2(coord.x / viewportWidth, coord.y / viewportHeight);\n"
+            + "    float inU = uv.x;\n"
+            + "    float inV = uv.y;\n"
+            + "    float outU;\n"
+            + "    float outV;\n"
+            + "    float stretchIntensity;\n"
+            + "\n"
+            + "    // Add the normalized scroll position within scrolling list\n"
+            + "    inU += uScrollX;\n"
+            + "    inV += uScrollY;\n"
+            + "\n"
+            + "    outU = inU;\n"
+            + "    outV = inV;\n"
+            + "    if (uOverscrollX > 0) {\n"
+            + "        if (inU <= uStretchAffectedDist) {\n"
+            + "            inU = uStretchAffectedDist - inU;\n"
+            + "            float posBasedVariation = smoothstep(0., uStretchAffectedDist, inU);\n"
+            + "            stretchIntensity = uMaxStretchIntensity * uOverscrollX * "
+            + "posBasedVariation;\n"
+            + "            outU = uDistanceStretchedX - (inU / (1. + stretchIntensity));\n"
+            + "        } else {\n"
+            + "            outU = uDistDiffX + inU;\n"
+            + "        }\n"
+            + "    }\n"
+            + "\n"
+            + "     if (uOverscrollX < 0) {\n"
+            + "            float stretchAffectedDist = 1. - uStretchAffectedDist;\n"
+            + "            if (inU >= stretchAffectedDist) {\n"
+            + "                inU = inU - stretchAffectedDist;\n"
+            + "                float posBasedVariation = (smoothstep(0., uStretchAffectedDist, "
+            + "inU));\n"
+            + "                stretchIntensity = uMaxStretchIntensity * (-uOverscrollX) * "
+            + "posBasedVariation;\n"
+            + "                outU = 1 - (uDistanceStretchedX - (inU / (1. + stretchIntensity)))"
+            + ";\n"
+            + "            } else if (inU < stretchAffectedDist) {\n"
+            + "                outU = -uDistDiffX + inU;\n"
+            + "            }\n"
+            + "        }\n"
+            + "\n"
+            + "    if (uOverscrollY > 0) {\n"
+            + "        if (inV <= uStretchAffectedDist) {\n"
+            + "            inV = uStretchAffectedDist - inV;\n"
+            + "            float posBasedVariation = smoothstep(0., uStretchAffectedDist, inV);\n"
+            + "            stretchIntensity = uMaxStretchIntensity * uOverscrollY * "
+            + "posBasedVariation;\n"
+            + "            outV = uDistanceStretchedY - (inV / (1. + stretchIntensity));\n"
+            + "        } else if (inV >= uStretchAffectedDist) {\n"
+            + "            outV = uDistDiffY + inV;\n"
+            + "        }\n"
+            + "    }\n"
+            + "\n"
+            + "    if (uOverscrollY < 0) {\n"
+            + "        float stretchAffectedDist = 1. - uStretchAffectedDist;\n"
+            + "        if (inV >= stretchAffectedDist) {\n"
+            + "            inV = inV - stretchAffectedDist;\n"
+            + "            float posBasedVariation = (smoothstep(0., uStretchAffectedDist, inV));\n"
+            + "            stretchIntensity = uMaxStretchIntensity * (-uOverscrollY) * "
+            + "posBasedVariation;\n"
+            + "            outV = 1 - (uDistanceStretchedY - (inV / (1. + stretchIntensity)));\n"
+            + "        } else if (inV < stretchAffectedDist) {\n"
+            + "            outV = -uDistDiffY + inV;\n"
+            + "        }\n"
+            + "    }\n"
+            + "\n"
+            + "    uv.x = outU;\n"
+            + "    uv.y = outV;\n"
+            + "    coord.x = uv.x * viewportWidth;\n"
+            + "    coord.y = uv.y * viewportHeight;\n"
+            + "    return sample(uContentTexture, coord);\n"
+            + "}";
+}