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