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"
+ + "}";
+}