1/*
2 * Copyright (C) 2012 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.gallery3d.glrenderer;
18
19import android.graphics.Bitmap;
20import android.graphics.Bitmap.Config;
21import android.graphics.Canvas;
22import android.graphics.Color;
23import android.graphics.Paint;
24import android.graphics.PorterDuff.Mode;
25import android.graphics.PorterDuffXfermode;
26import android.graphics.RectF;
27import android.os.SystemClock;
28
29import com.android.gallery3d.ui.GLRoot;
30import com.android.gallery3d.ui.GLRoot.OnGLIdleListener;
31
32import java.util.ArrayDeque;
33import java.util.ArrayList;
34
35// This class is similar to BitmapTexture, except the bitmap is
36// split into tiles. By doing so, we may increase the time required to
37// upload the whole bitmap but we reduce the time of uploading each tile
38// so it make the animation more smooth and prevents jank.
39public class TiledTexture implements Texture {
40    private static final int CONTENT_SIZE = 254;
41    private static final int BORDER_SIZE = 1;
42    private static final int TILE_SIZE = CONTENT_SIZE + 2 * BORDER_SIZE;
43    private static final int INIT_CAPACITY = 8;
44
45    // We are targeting at 60fps, so we have 16ms for each frame.
46    // In this 16ms, we use about 4~8 ms to upload tiles.
47    private static final long UPLOAD_TILE_LIMIT = 4; // ms
48
49    private static Tile sFreeTileHead = null;
50    private static final Object sFreeTileLock = new Object();
51
52    private static Bitmap sUploadBitmap;
53    private static Canvas sCanvas;
54    private static Paint sBitmapPaint;
55    private static Paint sPaint;
56
57    private int mUploadIndex = 0;
58
59    private final Tile[] mTiles;  // Can be modified in different threads.
60                                  // Should be protected by "synchronized."
61    private final int mWidth;
62    private final int mHeight;
63    private final RectF mSrcRect = new RectF();
64    private final RectF mDestRect = new RectF();
65
66    public static class Uploader implements OnGLIdleListener {
67        private final ArrayDeque<TiledTexture> mTextures =
68                new ArrayDeque<TiledTexture>(INIT_CAPACITY);
69
70        private final GLRoot mGlRoot;
71        private boolean mIsQueued = false;
72
73        public Uploader(GLRoot glRoot) {
74            mGlRoot = glRoot;
75        }
76
77        public synchronized void clear() {
78            mTextures.clear();
79        }
80
81        public synchronized void addTexture(TiledTexture t) {
82            if (t.isReady()) return;
83            mTextures.addLast(t);
84
85            if (mIsQueued) return;
86            mIsQueued = true;
87            mGlRoot.addOnGLIdleListener(this);
88        }
89
90        @Override
91        public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) {
92            ArrayDeque<TiledTexture> deque = mTextures;
93            synchronized (this) {
94                long now = SystemClock.uptimeMillis();
95                long dueTime = now + UPLOAD_TILE_LIMIT;
96                while (now < dueTime && !deque.isEmpty()) {
97                    TiledTexture t = deque.peekFirst();
98                    if (t.uploadNextTile(canvas)) {
99                        deque.removeFirst();
100                        mGlRoot.requestRender();
101                    }
102                    now = SystemClock.uptimeMillis();
103                }
104                mIsQueued = !mTextures.isEmpty();
105
106                // return true to keep this listener in the queue
107                return mIsQueued;
108            }
109        }
110    }
111
112    private static class Tile extends UploadedTexture {
113        public int offsetX;
114        public int offsetY;
115        public Bitmap bitmap;
116        public Tile nextFreeTile;
117        public int contentWidth;
118        public int contentHeight;
119
120        @Override
121        public void setSize(int width, int height) {
122            contentWidth = width;
123            contentHeight = height;
124            mWidth = width + 2 * BORDER_SIZE;
125            mHeight = height + 2 * BORDER_SIZE;
126            mTextureWidth = TILE_SIZE;
127            mTextureHeight = TILE_SIZE;
128        }
129
130        @Override
131        protected Bitmap onGetBitmap() {
132            // make a local copy of the reference to the bitmap,
133            // since it might be null'd in a different thread. b/8694871
134            Bitmap localBitmapRef = bitmap;
135            bitmap = null;
136
137            if (localBitmapRef != null) {
138                int x = BORDER_SIZE - offsetX;
139                int y = BORDER_SIZE - offsetY;
140                int r = localBitmapRef.getWidth() + x;
141                int b = localBitmapRef.getHeight() + y;
142                sCanvas.drawBitmap(localBitmapRef, x, y, sBitmapPaint);
143                localBitmapRef = null;
144
145                // draw borders if need
146                if (x > 0) sCanvas.drawLine(x - 1, 0, x - 1, TILE_SIZE, sPaint);
147                if (y > 0) sCanvas.drawLine(0, y - 1, TILE_SIZE, y - 1, sPaint);
148                if (r < CONTENT_SIZE) sCanvas.drawLine(r, 0, r, TILE_SIZE, sPaint);
149                if (b < CONTENT_SIZE) sCanvas.drawLine(0, b, TILE_SIZE, b, sPaint);
150            }
151
152            return sUploadBitmap;
153        }
154
155        @Override
156        protected void onFreeBitmap(Bitmap bitmap) {
157            // do nothing
158        }
159    }
160
161    private static void freeTile(Tile tile) {
162        tile.invalidateContent();
163        tile.bitmap = null;
164        synchronized (sFreeTileLock) {
165            tile.nextFreeTile = sFreeTileHead;
166            sFreeTileHead = tile;
167        }
168    }
169
170    private static Tile obtainTile() {
171        synchronized (sFreeTileLock) {
172            Tile result = sFreeTileHead;
173            if (result == null) return new Tile();
174            sFreeTileHead = result.nextFreeTile;
175            result.nextFreeTile = null;
176            return result;
177        }
178    }
179
180    private boolean uploadNextTile(GLCanvas canvas) {
181        if (mUploadIndex == mTiles.length) return true;
182
183        synchronized (mTiles) {
184            Tile next = mTiles[mUploadIndex++];
185
186            // Make sure tile has not already been recycled by the time
187            // this is called (race condition in onGLIdle)
188            if (next.bitmap != null) {
189                boolean hasBeenLoad = next.isLoaded();
190                next.updateContent(canvas);
191
192                // It will take some time for a texture to be drawn for the first
193                // time. When scrolling, we need to draw several tiles on the screen
194                // at the same time. It may cause a UI jank even these textures has
195                // been uploaded.
196                if (!hasBeenLoad) next.draw(canvas, 0, 0);
197            }
198        }
199        return mUploadIndex == mTiles.length;
200    }
201
202    public TiledTexture(Bitmap bitmap) {
203        mWidth = bitmap.getWidth();
204        mHeight = bitmap.getHeight();
205        ArrayList<Tile> list = new ArrayList<Tile>();
206
207        for (int x = 0, w = mWidth; x < w; x += CONTENT_SIZE) {
208            for (int y = 0, h = mHeight; y < h; y += CONTENT_SIZE) {
209                Tile tile = obtainTile();
210                tile.offsetX = x;
211                tile.offsetY = y;
212                tile.bitmap = bitmap;
213                tile.setSize(
214                        Math.min(CONTENT_SIZE, mWidth - x),
215                        Math.min(CONTENT_SIZE, mHeight - y));
216                list.add(tile);
217            }
218        }
219        mTiles = list.toArray(new Tile[list.size()]);
220    }
221
222    public boolean isReady() {
223        return mUploadIndex == mTiles.length;
224    }
225
226    // Can be called in UI thread.
227    public void recycle() {
228        synchronized (mTiles) {
229            for (int i = 0, n = mTiles.length; i < n; ++i) {
230                freeTile(mTiles[i]);
231            }
232        }
233    }
234
235    public static void freeResources() {
236        sUploadBitmap = null;
237        sCanvas = null;
238        sBitmapPaint = null;
239        sPaint = null;
240    }
241
242    public static void prepareResources() {
243        sUploadBitmap = Bitmap.createBitmap(TILE_SIZE, TILE_SIZE, Config.ARGB_8888);
244        sCanvas = new Canvas(sUploadBitmap);
245        sBitmapPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
246        sBitmapPaint.setXfermode(new PorterDuffXfermode(Mode.SRC));
247        sPaint = new Paint();
248        sPaint.setXfermode(new PorterDuffXfermode(Mode.SRC));
249        sPaint.setColor(Color.TRANSPARENT);
250    }
251
252    // We want to draw the "source" on the "target".
253    // This method is to find the "output" rectangle which is
254    // the corresponding area of the "src".
255    //                                   (x,y)  target
256    // (x0,y0)  source                     +---------------+
257    //    +----------+                     |               |
258    //    | src      |                     | output        |
259    //    | +--+     |    linear map       | +----+        |
260    //    | +--+     |    ---------->      | |    |        |
261    //    |          | by (scaleX, scaleY) | +----+        |
262    //    +----------+                     |               |
263    //      Texture                        +---------------+
264    //                                          Canvas
265    private static void mapRect(RectF output,
266            RectF src, float x0, float y0, float x, float y, float scaleX,
267            float scaleY) {
268        output.set(x + (src.left - x0) * scaleX,
269                y + (src.top - y0) * scaleY,
270                x + (src.right - x0) * scaleX,
271                y + (src.bottom - y0) * scaleY);
272    }
273
274    // Draws a mixed color of this texture and a specified color onto the
275    // a rectangle. The used color is: from * (1 - ratio) + to * ratio.
276    public void drawMixed(GLCanvas canvas, int color, float ratio,
277            int x, int y, int width, int height) {
278        RectF src = mSrcRect;
279        RectF dest = mDestRect;
280        float scaleX = (float) width / mWidth;
281        float scaleY = (float) height / mHeight;
282        synchronized (mTiles) {
283            for (int i = 0, n = mTiles.length; i < n; ++i) {
284                Tile t = mTiles[i];
285                src.set(0, 0, t.contentWidth, t.contentHeight);
286                src.offset(t.offsetX, t.offsetY);
287                mapRect(dest, src, 0, 0, x, y, scaleX, scaleY);
288                src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY);
289                canvas.drawMixed(t, color, ratio, mSrcRect, mDestRect);
290            }
291        }
292    }
293
294    // Draws the texture on to the specified rectangle.
295    @Override
296    public void draw(GLCanvas canvas, int x, int y, int width, int height) {
297        RectF src = mSrcRect;
298        RectF dest = mDestRect;
299        float scaleX = (float) width / mWidth;
300        float scaleY = (float) height / mHeight;
301        synchronized (mTiles) {
302            for (int i = 0, n = mTiles.length; i < n; ++i) {
303                Tile t = mTiles[i];
304                src.set(0, 0, t.contentWidth, t.contentHeight);
305                src.offset(t.offsetX, t.offsetY);
306                mapRect(dest, src, 0, 0, x, y, scaleX, scaleY);
307                src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY);
308                canvas.drawTexture(t, mSrcRect, mDestRect);
309            }
310        }
311    }
312
313    // Draws a sub region of this texture on to the specified rectangle.
314    public void draw(GLCanvas canvas, RectF source, RectF target) {
315        RectF src = mSrcRect;
316        RectF dest = mDestRect;
317        float x0 = source.left;
318        float y0 = source.top;
319        float x = target.left;
320        float y = target.top;
321        float scaleX = target.width() / source.width();
322        float scaleY = target.height() / source.height();
323
324        synchronized (mTiles) {
325            for (int i = 0, n = mTiles.length; i < n; ++i) {
326                Tile t = mTiles[i];
327                src.set(0, 0, t.contentWidth, t.contentHeight);
328                src.offset(t.offsetX, t.offsetY);
329                if (!src.intersect(source)) continue;
330                mapRect(dest, src, x0, y0, x, y, scaleX, scaleY);
331                src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY);
332                canvas.drawTexture(t, src, dest);
333            }
334        }
335    }
336
337    @Override
338    public int getWidth() {
339        return mWidth;
340    }
341
342    @Override
343    public int getHeight() {
344        return mHeight;
345    }
346
347    @Override
348    public void draw(GLCanvas canvas, int x, int y) {
349        draw(canvas, x, y, mWidth, mHeight);
350    }
351
352    @Override
353    public boolean isOpaque() {
354        return false;
355    }
356}
357