/* * 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); 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.setFloatUniform("uMaxStretchIntensity", mMaxStretchIntensity); mRuntimeShader.setFloatUniform("uStretchAffectedDist", mStretchAffectedDistance); mRuntimeShader.setFloatUniform("uDistanceStretchedX", distanceStretchedX); mRuntimeShader.setFloatUniform("uDistanceStretchedY", distanceStretchedY); mRuntimeShader.setFloatUniform("uDistDiffX", diffX); mRuntimeShader.setFloatUniform("uDistDiffY", diffY); mRuntimeShader.setFloatUniform("uOverscrollX", normOverScrollDistX); mRuntimeShader.setFloatUniform("uOverscrollY", normOverScrollDistY); mRuntimeShader.setFloatUniform("uScrollX", uScrollX); mRuntimeShader.setFloatUniform("uScrollY", uScrollY); mRuntimeShader.setFloatUniform("viewportWidth", width); mRuntimeShader.setFloatUniform("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( mOverScrollX, mOverScrollY, mStretchDistance, 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 = "uniform 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 uContentTexture.eval(coord);\n" + "}"; }