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