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