1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.test.hwui;
18 
19 import android.annotation.Nullable;
20 import android.app.Activity;
21 import android.graphics.Bitmap;
22 import android.graphics.BitmapShader;
23 import android.graphics.Canvas;
24 import android.graphics.Color;
25 import android.graphics.ColorFilter;
26 import android.graphics.RecordingCanvas;
27 import android.graphics.Rect;
28 import android.graphics.RenderEffect;
29 import android.graphics.RuntimeShader;
30 import android.graphics.Shader;
31 import android.graphics.drawable.BitmapDrawable;
32 import android.graphics.drawable.ColorDrawable;
33 import android.graphics.drawable.Drawable;
34 import android.os.Bundle;
35 import android.view.ViewGroup;
36 import android.view.ViewTreeObserver;
37 import android.widget.ImageView;
38 import android.widget.LinearLayout;
39 import android.widget.SeekBar;
40 import android.widget.TextView;
41 
42 public class StretchShaderActivity extends Activity {
43 
44     private static final float MAX_STRETCH_INTENSITY = 1.5f;
45     private static final float STRETCH_AFFECTED_DISTANCE = 1.0f;
46 
47     private float mScrollX = 0f;
48     private float mScrollY = 0f;
49 
50     private float mMaxStretchIntensity = MAX_STRETCH_INTENSITY;
51     private float mStretchAffectedDistance = STRETCH_AFFECTED_DISTANCE;
52 
53     private float mOverscrollX = 25f;
54     private float mOverscrollY = 25f;
55 
56     private RuntimeShader mRuntimeShader;
57     private ImageView mImageView;
58     private ImageView mTestImageView;
59 
60     private Bitmap mBitmap;
61 
62     private StretchDrawable mStretchDrawable = new StretchDrawable();
63 
64     @Override
onCreate(@ullable Bundle savedInstanceState)65     protected void onCreate(@Nullable Bundle savedInstanceState) {
66         super.onCreate(savedInstanceState);
67 
68         LinearLayout linearLayout = new LinearLayout(this);
69         linearLayout.setOrientation(LinearLayout.VERTICAL);
70 
71         mBitmap = ((BitmapDrawable) getDrawable(R.drawable.sunset1)).getBitmap();
72         mRuntimeShader = new RuntimeShader(SKSL);
73 
74         BitmapShader bitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP,
75                 Shader.TileMode.CLAMP);
76         mRuntimeShader.setInputShader("uContentTexture", bitmapShader);
77 
78         mImageView = new ImageView(this);
79 
80         mImageView.setRenderEffect(RenderEffect.createShaderEffect(mRuntimeShader));
81         mImageView.setImageDrawable(new ColorDrawable(Color.CYAN));
82 
83         TextView overscrollXText = new TextView(this);
84         overscrollXText.setText("Overscroll X");
85 
86         SeekBar overscrollXBar = new SeekBar(this);
87         overscrollXBar.setProgress(0);
88         overscrollXBar.setMin(-50);
89         overscrollXBar.setMax(50);
90         overscrollXBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
91             @Override
92             public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
93                 mOverscrollX = progress;
94                 overscrollXText.setText("Overscroll X: " + mOverscrollX);
95                 updateShader();
96             }
97 
98             @Override
99             public void onStartTrackingTouch(SeekBar seekBar) {
100 
101             }
102 
103             @Override
104             public void onStopTrackingTouch(SeekBar seekBar) {
105 
106             }
107         });
108 
109         TextView overscrollYText = new TextView(this);
110         overscrollYText.setText("Overscroll Y");
111 
112         SeekBar overscrollYBar = new SeekBar(this);
113         overscrollYBar.setProgress(0);
114         overscrollYBar.setMin(-50);
115         overscrollYBar.setMax(50);
116         overscrollYBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
117             @Override
118             public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
119                 mOverscrollY = progress;
120                 overscrollYText.setText("Overscroll Y: " + mOverscrollY);
121                 updateShader();
122             }
123 
124             @Override
125             public void onStartTrackingTouch(SeekBar seekBar) {
126 
127             }
128 
129             @Override
130             public void onStopTrackingTouch(SeekBar seekBar) {
131 
132             }
133         });
134 
135         TextView scrollXText = new TextView(this);
136         scrollXText.setText("Scroll X");
137         SeekBar scrollXSeekBar = new SeekBar(this);
138         scrollXSeekBar.setMin(0);
139         scrollXSeekBar.setMax(100);
140         scrollXSeekBar.setProgress(0);
141         scrollXSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
142             @Override
143             public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
144                 mScrollX = (progress / 100f);
145                 scrollXText.setText("Scroll X: " + mScrollY);
146                 updateShader();
147             }
148 
149             @Override
150             public void onStartTrackingTouch(SeekBar seekBar) {
151 
152             }
153 
154             @Override
155             public void onStopTrackingTouch(SeekBar seekBar) {
156 
157             }
158         });
159 
160         TextView scrollYText = new TextView(this);
161         scrollYText.setText("Scroll Y");
162         SeekBar scrollYSeekBar = new SeekBar(this);
163         scrollYSeekBar.setMin(0);
164         scrollYSeekBar.setMax(100);
165         scrollYSeekBar.setProgress(0);
166         scrollYSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
167             @Override
168             public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
169                 mScrollY = (progress / 100f);
170                 scrollYText.setText("Scroll Y: " + mScrollY);
171                 updateShader();
172             }
173 
174             @Override
175             public void onStartTrackingTouch(SeekBar seekBar) {
176 
177             }
178 
179             @Override
180             public void onStopTrackingTouch(SeekBar seekBar) {
181 
182             }
183         });
184 
185         TextView stretchIntensityText = new TextView(this);
186         int stretchProgress = (int) (mMaxStretchIntensity * 100);
187         stretchIntensityText.setText("StretchIntensity: " + mMaxStretchIntensity);
188         SeekBar stretchIntensitySeekbar = new SeekBar(this);
189         stretchIntensitySeekbar.setProgress(stretchProgress);
190         stretchIntensitySeekbar.setMin(1);
191         stretchIntensitySeekbar.setMax((int) (MAX_STRETCH_INTENSITY * 100));
192         stretchIntensitySeekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
193             @Override
194             public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
195                 mMaxStretchIntensity = progress / 100f;
196                 stretchIntensityText.setText("StretchIntensity: " + mMaxStretchIntensity);
197                 updateShader();
198             }
199 
200             @Override
201             public void onStartTrackingTouch(SeekBar seekBar) {
202 
203             }
204 
205             @Override
206             public void onStopTrackingTouch(SeekBar seekBar) {
207 
208             }
209         });
210 
211         TextView stretchDistanceText = new TextView(this);
212         stretchDistanceText.setText("StretchDistance");
213         SeekBar stretchDistanceSeekbar = new SeekBar(this);
214         stretchDistanceSeekbar.setMin(0);
215         stretchDistanceSeekbar.setProgress((int) (mStretchAffectedDistance * 100));
216         stretchDistanceSeekbar.setMax(100);
217         stretchDistanceSeekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
218             @Override
219             public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
220                 mStretchAffectedDistance = progress / 100f;
221                 stretchDistanceText.setText("StretchDistance: " + mStretchAffectedDistance);
222                 updateShader();
223             }
224 
225             @Override
226             public void onStartTrackingTouch(SeekBar seekBar) {
227 
228             }
229 
230             @Override
231             public void onStopTrackingTouch(SeekBar seekBar) {
232 
233             }
234         });
235 
236 
237         linearLayout.addView(mImageView,
238                 new LinearLayout.LayoutParams(
239                         mBitmap.getWidth(),
240                         mBitmap.getHeight())
241         );
242 
243         linearLayout.addView(overscrollXText,
244                 new LinearLayout.LayoutParams(
245                         LinearLayout.LayoutParams.WRAP_CONTENT,
246                         LinearLayout.LayoutParams.WRAP_CONTENT
247                 ));
248         linearLayout.addView(overscrollXBar,
249                 new LinearLayout.LayoutParams(
250                         ViewGroup.LayoutParams.MATCH_PARENT,
251                         ViewGroup.LayoutParams.WRAP_CONTENT
252                 )
253         );
254 
255         linearLayout.addView(overscrollYText,
256                 new LinearLayout.LayoutParams(
257                         LinearLayout.LayoutParams.WRAP_CONTENT,
258                         LinearLayout.LayoutParams.WRAP_CONTENT
259                 ));
260         linearLayout.addView(overscrollYBar,
261                 new LinearLayout.LayoutParams(
262                         ViewGroup.LayoutParams.MATCH_PARENT,
263                         ViewGroup.LayoutParams.WRAP_CONTENT
264                 )
265         );
266 
267         linearLayout.addView(scrollXText,
268                 new LinearLayout.LayoutParams(
269                         LinearLayout.LayoutParams.WRAP_CONTENT,
270                         LinearLayout.LayoutParams.WRAP_CONTENT
271                 ));
272 
273         linearLayout.addView(scrollXSeekBar,
274                 new LinearLayout.LayoutParams(
275                         ViewGroup.LayoutParams.MATCH_PARENT,
276                         ViewGroup.LayoutParams.WRAP_CONTENT
277                 ));
278 
279         linearLayout.addView(scrollYText,
280                 new LinearLayout.LayoutParams(
281                         LinearLayout.LayoutParams.WRAP_CONTENT,
282                         LinearLayout.LayoutParams.WRAP_CONTENT
283                 ));
284 
285         linearLayout.addView(scrollYSeekBar,
286                 new LinearLayout.LayoutParams(
287                         ViewGroup.LayoutParams.MATCH_PARENT,
288                         ViewGroup.LayoutParams.WRAP_CONTENT
289                 ));
290 
291         linearLayout.addView(stretchIntensityText,
292                 new LinearLayout.LayoutParams(
293                         LinearLayout.LayoutParams.WRAP_CONTENT,
294                         LinearLayout.LayoutParams.WRAP_CONTENT
295                 )
296         );
297 
298         linearLayout.addView(stretchIntensitySeekbar,
299                 new LinearLayout.LayoutParams(
300                         LinearLayout.LayoutParams.MATCH_PARENT,
301                         LinearLayout.LayoutParams.WRAP_CONTENT
302                 )
303         );
304 
305         linearLayout.addView(stretchDistanceText,
306                 new LinearLayout.LayoutParams(
307                         LinearLayout.LayoutParams.WRAP_CONTENT,
308                         LinearLayout.LayoutParams.WRAP_CONTENT
309                 ));
310 
311         linearLayout.addView(stretchDistanceSeekbar,
312                 new LinearLayout.LayoutParams(
313                         LinearLayout.LayoutParams.MATCH_PARENT,
314                         LinearLayout.LayoutParams.WRAP_CONTENT
315                 ));
316 
317         ImageView test = new ImageView(this);
318         mStretchDrawable.setBitmap(mBitmap);
319         test.setImageDrawable(mStretchDrawable);
320 
321         mTestImageView = test;
322         linearLayout.addView(test,
323                 new LinearLayout.LayoutParams(mBitmap.getWidth(), mBitmap.getHeight()));
324 
325         setContentView(linearLayout);
326 
327     }
328 
329     @Override
onResume()330     protected void onResume() {
331         super.onResume();
332         mImageView.getViewTreeObserver().addOnPreDrawListener(
333                 new ViewTreeObserver.OnPreDrawListener() {
334                     @Override
335                     public boolean onPreDraw() {
336                         updateShader();
337                         mImageView.getViewTreeObserver().removeOnPreDrawListener(this);
338                         return false;
339                     }
340                 });
341     }
342 
updateShader()343     private void updateShader() {
344         final float width = mImageView.getWidth();
345         final float height = mImageView.getHeight();
346         final float distanceNotStretched = mStretchAffectedDistance;
347         final float normOverScrollDistX = mOverscrollX / width;
348         final float normOverScrollDistY = mOverscrollY / height;
349         final float distanceStretchedX =
350                 mStretchAffectedDistance
351                         / (1 + Math.abs(normOverScrollDistX) * mMaxStretchIntensity);
352         final float distanceStretchedY =
353                 mStretchAffectedDistance
354                         / (1 + Math.abs(normOverScrollDistY) * mMaxStretchIntensity);
355         final float diffX = distanceStretchedX - distanceNotStretched;
356         final float diffY = distanceStretchedY - distanceNotStretched;
357         float uScrollX = mScrollX;
358         float uScrollY = mScrollY;
359 
360         mRuntimeShader.setFloatUniform("uMaxStretchIntensity", mMaxStretchIntensity);
361         mRuntimeShader.setFloatUniform("uStretchAffectedDist", mStretchAffectedDistance);
362         mRuntimeShader.setFloatUniform("uDistanceStretchedX", distanceStretchedX);
363         mRuntimeShader.setFloatUniform("uDistanceStretchedY", distanceStretchedY);
364         mRuntimeShader.setFloatUniform("uDistDiffX", diffX);
365         mRuntimeShader.setFloatUniform("uDistDiffY", diffY);
366         mRuntimeShader.setFloatUniform("uOverscrollX", normOverScrollDistX);
367         mRuntimeShader.setFloatUniform("uOverscrollY", normOverScrollDistY);
368         mRuntimeShader.setFloatUniform("uScrollX", uScrollX);
369         mRuntimeShader.setFloatUniform("uScrollY", uScrollY);
370         mRuntimeShader.setFloatUniform("viewportWidth", width);
371         mRuntimeShader.setFloatUniform("viewportHeight", height);
372 
373         mImageView.setRenderEffect(RenderEffect.createShaderEffect(mRuntimeShader));
374 
375         mStretchDrawable.setStretchDistance(mStretchAffectedDistance);
376         mStretchDrawable.setOverscrollX(normOverScrollDistX);
377         mStretchDrawable.setOverscrollY(normOverScrollDistY);
378     }
379 
380     private static class StretchDrawable extends Drawable {
381 
382         private float mStretchDistance = 0;
383         private float mOverScrollX = 0f;
384         private float mOverScrollY = 0f;
385         private Bitmap mBitmap = null;
386 
setStretchDistance(float stretchDistance)387         public void setStretchDistance(float stretchDistance) {
388             mStretchDistance = stretchDistance;
389             invalidateSelf();
390         }
391 
setOverscrollX(float overscrollX)392         public void setOverscrollX(float overscrollX) {
393             mOverScrollX = overscrollX;
394             invalidateSelf();
395         }
396 
setOverscrollY(float overscrollY)397         public void setOverscrollY(float overscrollY) {
398             mOverScrollY = overscrollY;
399             invalidateSelf();
400         }
401 
setBitmap(Bitmap bitmap)402         public void setBitmap(Bitmap bitmap) {
403             mBitmap = bitmap;
404             invalidateSelf();
405         }
406 
407         @Override
draw(Canvas canvas)408         public void draw(Canvas canvas) {
409             if (mStretchDistance > 0 && canvas instanceof RecordingCanvas) {
410                 Rect bounds = getBounds();
411                 ((RecordingCanvas) canvas).mNode.stretch(
412                         mOverScrollX,
413                         mOverScrollY,
414                         mStretchDistance,
415                         mStretchDistance
416                 );
417             }
418             if (mBitmap != null) {
419                 canvas.drawBitmap(mBitmap, 0f, 0f, null);
420             }
421         }
422 
423         @Override
setAlpha(int alpha)424         public void setAlpha(int alpha) {
425 
426         }
427 
428         @Override
setColorFilter(ColorFilter colorFilter)429         public void setColorFilter(ColorFilter colorFilter) {
430 
431         }
432 
433         @Override
getOpacity()434         public int getOpacity() {
435             return 0;
436         }
437     }
438 
439     private static final String SKSL = "uniform shader uContentTexture;\n"
440             + "uniform float uMaxStretchIntensity; // multiplier to apply to scale effect\n"
441             + "uniform float uStretchAffectedDist; // Maximum percentage to stretch beyond bounds"
442             + " of target\n"
443             + "\n"
444             + "// Distance stretched as a function of the normalized overscroll times scale "
445             + "intensity\n"
446             + "uniform float uDistanceStretchedX;\n"
447             + "uniform float uDistanceStretchedY;\n"
448             + "uniform float uDistDiffX;\n"
449             + "uniform float uDistDiffY; // Difference between the peak stretch amount and "
450             + "overscroll amount normalized\n"
451             + "uniform float uScrollX; // Horizontal offset represented as a ratio of pixels "
452             + "divided by the target width\n"
453             + "uniform float uScrollY; // Vertical offset represented as a ratio of pixels "
454             + "divided by the target height\n"
455             + "uniform float uOverscrollX; // Normalized overscroll amount in the horizontal "
456             + "direction\n"
457             + "uniform float uOverscrollY; // Normalized overscroll amount in the vertical "
458             + "direction\n"
459             + "\n"
460             + "uniform float viewportWidth; // target height in pixels\n"
461             + "uniform float viewportHeight; // target width in pixels\n"
462             + "\n"
463             + "vec4 main(vec2 coord) {\n"
464             + "\n"
465             + "    // Normalize SKSL pixel coordinate into a unit vector\n"
466             + "    vec2 uv = vec2(coord.x / viewportWidth, coord.y / viewportHeight);\n"
467             + "    float inU = uv.x;\n"
468             + "    float inV = uv.y;\n"
469             + "    float outU;\n"
470             + "    float outV;\n"
471             + "    float stretchIntensity;\n"
472             + "\n"
473             + "    // Add the normalized scroll position within scrolling list\n"
474             + "    inU += uScrollX;\n"
475             + "    inV += uScrollY;\n"
476             + "\n"
477             + "    outU = inU;\n"
478             + "    outV = inV;\n"
479             + "    if (uOverscrollX > 0) {\n"
480             + "        if (inU <= uStretchAffectedDist) {\n"
481             + "            inU = uStretchAffectedDist - inU;\n"
482             + "            float posBasedVariation = smoothstep(0., uStretchAffectedDist, inU);\n"
483             + "            stretchIntensity = uMaxStretchIntensity * uOverscrollX * "
484             + "posBasedVariation;\n"
485             + "            outU = uDistanceStretchedX - (inU / (1. + stretchIntensity));\n"
486             + "        } else {\n"
487             + "            outU = uDistDiffX + inU;\n"
488             + "        }\n"
489             + "    }\n"
490             + "\n"
491             + "     if (uOverscrollX < 0) {\n"
492             + "            float stretchAffectedDist = 1. - uStretchAffectedDist;\n"
493             + "            if (inU >= stretchAffectedDist) {\n"
494             + "                inU = inU - stretchAffectedDist;\n"
495             + "                float posBasedVariation = (smoothstep(0., uStretchAffectedDist, "
496             + "inU));\n"
497             + "                stretchIntensity = uMaxStretchIntensity * (-uOverscrollX) * "
498             + "posBasedVariation;\n"
499             + "                outU = 1 - (uDistanceStretchedX - (inU / (1. + stretchIntensity)))"
500             + ";\n"
501             + "            } else if (inU < stretchAffectedDist) {\n"
502             + "                outU = -uDistDiffX + inU;\n"
503             + "            }\n"
504             + "        }\n"
505             + "\n"
506             + "    if (uOverscrollY > 0) {\n"
507             + "        if (inV <= uStretchAffectedDist) {\n"
508             + "            inV = uStretchAffectedDist - inV;\n"
509             + "            float posBasedVariation = smoothstep(0., uStretchAffectedDist, inV);\n"
510             + "            stretchIntensity = uMaxStretchIntensity * uOverscrollY * "
511             + "posBasedVariation;\n"
512             + "            outV = uDistanceStretchedY - (inV / (1. + stretchIntensity));\n"
513             + "        } else if (inV >= uStretchAffectedDist) {\n"
514             + "            outV = uDistDiffY + inV;\n"
515             + "        }\n"
516             + "    }\n"
517             + "\n"
518             + "    if (uOverscrollY < 0) {\n"
519             + "        float stretchAffectedDist = 1. - uStretchAffectedDist;\n"
520             + "        if (inV >= stretchAffectedDist) {\n"
521             + "            inV = inV - stretchAffectedDist;\n"
522             + "            float posBasedVariation = (smoothstep(0., uStretchAffectedDist, inV));\n"
523             + "            stretchIntensity = uMaxStretchIntensity * (-uOverscrollY) * "
524             + "posBasedVariation;\n"
525             + "            outV = 1 - (uDistanceStretchedY - (inV / (1. + stretchIntensity)));\n"
526             + "        } else if (inV < stretchAffectedDist) {\n"
527             + "            outV = -uDistDiffY + inV;\n"
528             + "        }\n"
529             + "    }\n"
530             + "\n"
531             + "    uv.x = outU;\n"
532             + "    uv.y = outV;\n"
533             + "    coord.x = uv.x * viewportWidth;\n"
534             + "    coord.y = uv.y * viewportHeight;\n"
535             + "    return uContentTexture.eval(coord);\n"
536             + "}";
537 }
538