1/*
2 * Copyright (C) 2013 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
17package com.android.photos.views;
18
19import android.annotation.SuppressLint;
20import android.annotation.TargetApi;
21import android.content.Context;
22import android.graphics.Bitmap;
23import android.graphics.Canvas;
24import android.graphics.Color;
25import android.graphics.Matrix;
26import android.graphics.Paint;
27import android.graphics.Paint.Align;
28import android.graphics.RectF;
29import android.opengl.GLSurfaceView;
30import android.opengl.GLSurfaceView.Renderer;
31import android.os.Build;
32import android.util.AttributeSet;
33import android.view.Choreographer;
34import android.view.Choreographer.FrameCallback;
35import android.view.View;
36import android.widget.FrameLayout;
37
38import com.android.gallery3d.glrenderer.BasicTexture;
39import com.android.gallery3d.glrenderer.GLES20Canvas;
40import com.android.photos.views.TiledImageRenderer.TileSource;
41
42import javax.microedition.khronos.egl.EGLConfig;
43import javax.microedition.khronos.opengles.GL10;
44
45/**
46 * Shows an image using {@link TiledImageRenderer} using either {@link GLSurfaceView}
47 * or {@link BlockingGLTextureView}.
48 */
49public class TiledImageView extends FrameLayout {
50
51    private static final boolean USE_TEXTURE_VIEW = false;
52    private static final boolean IS_SUPPORTED =
53            Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
54    private static final boolean USE_CHOREOGRAPHER =
55            Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
56
57    private BlockingGLTextureView mTextureView;
58    private GLSurfaceView mGLSurfaceView;
59    private boolean mInvalPending = false;
60    private FrameCallback mFrameCallback;
61
62    protected static class ImageRendererWrapper {
63        // Guarded by locks
64        public float scale;
65        public int centerX, centerY;
66        public int rotation;
67        public TileSource source;
68        Runnable isReadyCallback;
69
70        // GL thread only
71        TiledImageRenderer image;
72    }
73
74    private float[] mValues = new float[9];
75
76    // -------------------------
77    // Guarded by mLock
78    // -------------------------
79    protected Object mLock = new Object();
80    protected ImageRendererWrapper mRenderer;
81
82    public static boolean isTilingSupported() {
83        return IS_SUPPORTED;
84    }
85
86    public TiledImageView(Context context) {
87        this(context, null);
88    }
89
90    public TiledImageView(Context context, AttributeSet attrs) {
91        super(context, attrs);
92        if (!IS_SUPPORTED) {
93            return;
94        }
95
96        mRenderer = new ImageRendererWrapper();
97        mRenderer.image = new TiledImageRenderer(this);
98        View view;
99        if (USE_TEXTURE_VIEW) {
100            mTextureView = new BlockingGLTextureView(context);
101            mTextureView.setRenderer(new TileRenderer());
102            view = mTextureView;
103        } else {
104            mGLSurfaceView = new GLSurfaceView(context);
105            mGLSurfaceView.setEGLContextClientVersion(2);
106            mGLSurfaceView.setRenderer(new TileRenderer());
107            mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
108            view = mGLSurfaceView;
109        }
110        addView(view, new LayoutParams(
111                LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
112        //setTileSource(new ColoredTiles());
113    }
114
115    @Override
116    public void setVisibility(int visibility) {
117        super.setVisibility(visibility);
118        // need to update inner view's visibility because it seems like we're causing it to draw
119        // from {@link #dispatchDraw} or {@link #invalidate} even if we are invisible.
120        if (USE_TEXTURE_VIEW) {
121            mTextureView.setVisibility(visibility);
122        } else {
123            mGLSurfaceView.setVisibility(visibility);
124        }
125    }
126
127    public void destroy() {
128        if (!IS_SUPPORTED) {
129            return;
130        }
131        if (USE_TEXTURE_VIEW) {
132            mTextureView.destroy();
133        } else {
134            mGLSurfaceView.queueEvent(mFreeTextures);
135        }
136    }
137
138    private Runnable mFreeTextures = new Runnable() {
139
140        @Override
141        public void run() {
142            mRenderer.image.freeTextures();
143        }
144    };
145
146    public void onPause() {
147        if (!IS_SUPPORTED) {
148            return;
149        }
150        if (!USE_TEXTURE_VIEW) {
151            mGLSurfaceView.onPause();
152        }
153    }
154
155    public void onResume() {
156        if (!IS_SUPPORTED) {
157            return;
158        }
159        if (!USE_TEXTURE_VIEW) {
160            mGLSurfaceView.onResume();
161        }
162    }
163
164    public void setTileSource(TileSource source, Runnable isReadyCallback) {
165        if (!IS_SUPPORTED) {
166            return;
167        }
168        synchronized (mLock) {
169            mRenderer.source = source;
170            mRenderer.isReadyCallback = isReadyCallback;
171            mRenderer.centerX = source != null ? source.getImageWidth() / 2 : 0;
172            mRenderer.centerY = source != null ? source.getImageHeight() / 2 : 0;
173            mRenderer.rotation = source != null ? source.getRotation() : 0;
174            mRenderer.scale = 0;
175            updateScaleIfNecessaryLocked(mRenderer);
176        }
177        invalidate();
178    }
179
180    @Override
181    protected void onLayout(boolean changed, int left, int top, int right,
182            int bottom) {
183        super.onLayout(changed, left, top, right, bottom);
184        if (!IS_SUPPORTED) {
185            return;
186        }
187        synchronized (mLock) {
188            updateScaleIfNecessaryLocked(mRenderer);
189        }
190    }
191
192    private void updateScaleIfNecessaryLocked(ImageRendererWrapper renderer) {
193        if (renderer == null || renderer.source == null
194                || renderer.scale > 0 || getWidth() == 0) {
195            return;
196        }
197        renderer.scale = Math.min(
198                (float) getWidth() / (float) renderer.source.getImageWidth(),
199                (float) getHeight() / (float) renderer.source.getImageHeight());
200    }
201
202    @Override
203    protected void dispatchDraw(Canvas canvas) {
204        if (!IS_SUPPORTED) {
205            return;
206        }
207        if (USE_TEXTURE_VIEW) {
208            mTextureView.render();
209        }
210        super.dispatchDraw(canvas);
211    }
212
213    @SuppressLint("NewApi")
214    @Override
215    public void setTranslationX(float translationX) {
216        if (!IS_SUPPORTED) {
217            return;
218        }
219        super.setTranslationX(translationX);
220    }
221
222    @Override
223    public void invalidate() {
224        if (!IS_SUPPORTED) {
225            return;
226        }
227        if (USE_TEXTURE_VIEW) {
228            super.invalidate();
229            mTextureView.invalidate();
230        } else {
231            if (USE_CHOREOGRAPHER) {
232                invalOnVsync();
233            } else {
234                mGLSurfaceView.requestRender();
235            }
236        }
237    }
238
239    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
240    private void invalOnVsync() {
241        if (!mInvalPending) {
242            mInvalPending = true;
243            if (mFrameCallback == null) {
244                mFrameCallback = new FrameCallback() {
245                    @Override
246                    public void doFrame(long frameTimeNanos) {
247                        mInvalPending = false;
248                        mGLSurfaceView.requestRender();
249                    }
250                };
251            }
252            Choreographer.getInstance().postFrameCallback(mFrameCallback);
253        }
254    }
255
256    private RectF mTempRectF = new RectF();
257    public void positionFromMatrix(Matrix matrix) {
258        if (!IS_SUPPORTED) {
259            return;
260        }
261        if (mRenderer.source != null) {
262            final int rotation = mRenderer.source.getRotation();
263            final boolean swap = !(rotation % 180 == 0);
264            final int width = swap ? mRenderer.source.getImageHeight()
265                    : mRenderer.source.getImageWidth();
266            final int height = swap ? mRenderer.source.getImageWidth()
267                    : mRenderer.source.getImageHeight();
268            mTempRectF.set(0, 0, width, height);
269            matrix.mapRect(mTempRectF);
270            matrix.getValues(mValues);
271            int cx = width / 2;
272            int cy = height / 2;
273            float scale = mValues[Matrix.MSCALE_X];
274            int xoffset = Math.round((getWidth() - mTempRectF.width()) / 2 / scale);
275            int yoffset = Math.round((getHeight() - mTempRectF.height()) / 2 / scale);
276            if (rotation == 90 || rotation == 180) {
277                cx += (mTempRectF.left / scale) - xoffset;
278            } else {
279                cx -= (mTempRectF.left / scale) - xoffset;
280            }
281            if (rotation == 180 || rotation == 270) {
282                cy += (mTempRectF.top / scale) - yoffset;
283            } else {
284                cy -= (mTempRectF.top / scale) - yoffset;
285            }
286            mRenderer.scale = scale;
287            mRenderer.centerX = swap ? cy : cx;
288            mRenderer.centerY = swap ? cx : cy;
289            invalidate();
290        }
291    }
292
293    private class TileRenderer implements Renderer {
294
295        private GLES20Canvas mCanvas;
296
297        @Override
298        public void onSurfaceCreated(GL10 gl, EGLConfig config) {
299            mCanvas = new GLES20Canvas();
300            BasicTexture.invalidateAllTextures();
301            mRenderer.image.setModel(mRenderer.source, mRenderer.rotation);
302        }
303
304        @Override
305        public void onSurfaceChanged(GL10 gl, int width, int height) {
306            mCanvas.setSize(width, height);
307            mRenderer.image.setViewSize(width, height);
308        }
309
310        @Override
311        public void onDrawFrame(GL10 gl) {
312            mCanvas.clearBuffer();
313            Runnable readyCallback;
314            synchronized (mLock) {
315                readyCallback = mRenderer.isReadyCallback;
316                mRenderer.image.setModel(mRenderer.source, mRenderer.rotation);
317                mRenderer.image.setPosition(mRenderer.centerX, mRenderer.centerY,
318                        mRenderer.scale);
319            }
320            boolean complete = mRenderer.image.draw(mCanvas);
321            if (complete && readyCallback != null) {
322                synchronized (mLock) {
323                    // Make sure we don't trample on a newly set callback/source
324                    // if it changed while we were rendering
325                    if (mRenderer.isReadyCallback == readyCallback) {
326                        mRenderer.isReadyCallback = null;
327                    }
328                }
329                if (readyCallback != null) {
330                    post(readyCallback);
331                }
332            }
333        }
334
335    }
336
337    @SuppressWarnings("unused")
338    private static class ColoredTiles implements TileSource {
339        private static final int[] COLORS = new int[] {
340            Color.RED,
341            Color.BLUE,
342            Color.YELLOW,
343            Color.GREEN,
344            Color.CYAN,
345            Color.MAGENTA,
346            Color.WHITE,
347        };
348
349        private Paint mPaint = new Paint();
350        private Canvas mCanvas = new Canvas();
351
352        @Override
353        public int getTileSize() {
354            return 256;
355        }
356
357        @Override
358        public int getImageWidth() {
359            return 16384;
360        }
361
362        @Override
363        public int getImageHeight() {
364            return 8192;
365        }
366
367        @Override
368        public int getRotation() {
369            return 0;
370        }
371
372        @Override
373        public Bitmap getTile(int level, int x, int y, Bitmap bitmap) {
374            int tileSize = getTileSize();
375            if (bitmap == null) {
376                bitmap = Bitmap.createBitmap(tileSize, tileSize,
377                        Bitmap.Config.ARGB_8888);
378            }
379            mCanvas.setBitmap(bitmap);
380            mCanvas.drawColor(COLORS[level]);
381            mPaint.setColor(Color.BLACK);
382            mPaint.setTextSize(20);
383            mPaint.setTextAlign(Align.CENTER);
384            mCanvas.drawText(x + "x" + y, 128, 128, mPaint);
385            tileSize <<= level;
386            x /= tileSize;
387            y /= tileSize;
388            mCanvas.drawText(x + "x" + y + " @ " + level, 128, 30, mPaint);
389            mCanvas.setBitmap(null);
390            return bitmap;
391        }
392
393        @Override
394        public BasicTexture getPreview() {
395            return null;
396        }
397    }
398}
399