1/*
2 * Copyright (C) 2010 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.content.Context;
20import android.graphics.Bitmap;
21import android.graphics.BitmapFactory;
22import android.graphics.Rect;
23
24import com.android.gallery3d.common.Utils;
25
26import java.nio.ByteBuffer;
27import java.nio.ByteOrder;
28import java.nio.FloatBuffer;
29
30// NinePatchTexture is a texture backed by a NinePatch resource.
31//
32// getPaddings() returns paddings specified in the NinePatch.
33// getNinePatchChunk() returns the layout data specified in the NinePatch.
34//
35public class NinePatchTexture extends ResourceTexture {
36    @SuppressWarnings("unused")
37    private static final String TAG = "NinePatchTexture";
38    private NinePatchChunk mChunk;
39    private SmallCache<NinePatchInstance> mInstanceCache
40            = new SmallCache<NinePatchInstance>();
41
42    public NinePatchTexture(Context context, int resId) {
43        super(context, resId);
44    }
45
46    @Override
47    protected Bitmap onGetBitmap() {
48        if (mBitmap != null) return mBitmap;
49
50        BitmapFactory.Options options = new BitmapFactory.Options();
51        options.inPreferredConfig = Bitmap.Config.ARGB_8888;
52        Bitmap bitmap = BitmapFactory.decodeResource(
53                mContext.getResources(), mResId, options);
54        mBitmap = bitmap;
55        setSize(bitmap.getWidth(), bitmap.getHeight());
56        byte[] chunkData = bitmap.getNinePatchChunk();
57        mChunk = chunkData == null
58                ? null
59                : NinePatchChunk.deserialize(bitmap.getNinePatchChunk());
60        if (mChunk == null) {
61            throw new RuntimeException("invalid nine-patch image: " + mResId);
62        }
63        return bitmap;
64    }
65
66    public Rect getPaddings() {
67        // get the paddings from nine patch
68        if (mChunk == null) onGetBitmap();
69        return mChunk.mPaddings;
70    }
71
72    public NinePatchChunk getNinePatchChunk() {
73        if (mChunk == null) onGetBitmap();
74        return mChunk;
75    }
76
77    // This is a simple cache for a small number of things. Linear search
78    // is used because the cache is small. It also tries to remove less used
79    // item when the cache is full by moving the often-used items to the front.
80    private static class SmallCache<V> {
81        private static final int CACHE_SIZE = 16;
82        private static final int CACHE_SIZE_START_MOVE = CACHE_SIZE / 2;
83        private int[] mKey = new int[CACHE_SIZE];
84        private V[] mValue = (V[]) new Object[CACHE_SIZE];
85        private int mCount;  // number of items in this cache
86
87        // Puts a value into the cache. If the cache is full, also returns
88        // a less used item, otherwise returns null.
89        public V put(int key, V value) {
90            if (mCount == CACHE_SIZE) {
91                V old = mValue[CACHE_SIZE - 1];  // remove the last item
92                mKey[CACHE_SIZE - 1] = key;
93                mValue[CACHE_SIZE - 1] = value;
94                return old;
95            } else {
96                mKey[mCount] = key;
97                mValue[mCount] = value;
98                mCount++;
99                return null;
100            }
101        }
102
103        public V get(int key) {
104            for (int i = 0; i < mCount; i++) {
105                if (mKey[i] == key) {
106                    // Move the accessed item one position to the front, so it
107                    // will less likely to be removed when cache is full. Only
108                    // do this if the cache is starting to get full.
109                    if (mCount > CACHE_SIZE_START_MOVE && i > 0) {
110                        int tmpKey = mKey[i];
111                        mKey[i] = mKey[i - 1];
112                        mKey[i - 1] = tmpKey;
113
114                        V tmpValue = mValue[i];
115                        mValue[i] = mValue[i - 1];
116                        mValue[i - 1] = tmpValue;
117                    }
118                    return mValue[i];
119                }
120            }
121            return null;
122        }
123
124        public void clear() {
125            for (int i = 0; i < mCount; i++) {
126                mValue[i] = null;  // make sure it's can be garbage-collected.
127            }
128            mCount = 0;
129        }
130
131        public int size() {
132            return mCount;
133        }
134
135        public V valueAt(int i) {
136            return mValue[i];
137        }
138    }
139
140    private NinePatchInstance findInstance(GLCanvas canvas, int w, int h) {
141        int key = w;
142        key = (key << 16) | h;
143        NinePatchInstance instance = mInstanceCache.get(key);
144
145        if (instance == null) {
146            instance = new NinePatchInstance(this, w, h);
147            NinePatchInstance removed = mInstanceCache.put(key, instance);
148            if (removed != null) {
149                removed.recycle(canvas);
150            }
151        }
152
153        return instance;
154    }
155
156    @Override
157    public void draw(GLCanvas canvas, int x, int y, int w, int h) {
158        if (!isLoaded()) {
159            mInstanceCache.clear();
160        }
161
162        if (w != 0 && h != 0) {
163            findInstance(canvas, w, h).draw(canvas, this, x, y);
164        }
165    }
166
167    @Override
168    public void recycle() {
169        super.recycle();
170        GLCanvas canvas = mCanvasRef;
171        if (canvas == null) return;
172        int n = mInstanceCache.size();
173        for (int i = 0; i < n; i++) {
174            NinePatchInstance instance = mInstanceCache.valueAt(i);
175            instance.recycle(canvas);
176        }
177        mInstanceCache.clear();
178    }
179}
180
181// This keeps data for a specialization of NinePatchTexture with the size
182// (width, height). We pre-compute the coordinates for efficiency.
183class NinePatchInstance {
184
185    @SuppressWarnings("unused")
186    private static final String TAG = "NinePatchInstance";
187
188    // We need 16 vertices for a normal nine-patch image (the 4x4 vertices)
189    private static final int VERTEX_BUFFER_SIZE = 16 * 2;
190
191    // We need 22 indices for a normal nine-patch image, plus 2 for each
192    // transparent region. Current there are at most 1 transparent region.
193    private static final int INDEX_BUFFER_SIZE = 22 + 2;
194
195    private FloatBuffer mXyBuffer;
196    private FloatBuffer mUvBuffer;
197    private ByteBuffer mIndexBuffer;
198
199    // Names for buffer names: xy, uv, index.
200    private int mXyBufferName = -1;
201    private int mUvBufferName;
202    private int mIndexBufferName;
203
204    private int mIdxCount;
205
206    public NinePatchInstance(NinePatchTexture tex, int width, int height) {
207        NinePatchChunk chunk = tex.getNinePatchChunk();
208
209        if (width <= 0 || height <= 0) {
210            throw new RuntimeException("invalid dimension");
211        }
212
213        // The code should be easily extended to handle the general cases by
214        // allocating more space for buffers. But let's just handle the only
215        // use case.
216        if (chunk.mDivX.length != 2 || chunk.mDivY.length != 2) {
217            throw new RuntimeException("unsupported nine patch");
218        }
219
220        float divX[] = new float[4];
221        float divY[] = new float[4];
222        float divU[] = new float[4];
223        float divV[] = new float[4];
224
225        int nx = stretch(divX, divU, chunk.mDivX, tex.getWidth(), width);
226        int ny = stretch(divY, divV, chunk.mDivY, tex.getHeight(), height);
227
228        prepareVertexData(divX, divY, divU, divV, nx, ny, chunk.mColor);
229    }
230
231    /**
232     * Stretches the texture according to the nine-patch rules. It will
233     * linearly distribute the strechy parts defined in the nine-patch chunk to
234     * the target area.
235     *
236     * <pre>
237     *                      source
238     *          /--------------^---------------\
239     *         u0    u1       u2  u3     u4   u5
240     * div ---> |fffff|ssssssss|fff|ssssss|ffff| ---> u
241     *          |    div0    div1 div2   div3  |
242     *          |     |       /   /      /    /
243     *          |     |      /   /     /    /
244     *          |     |     /   /    /    /
245     *          |fffff|ssss|fff|sss|ffff| ---> x
246     *         x0    x1   x2  x3  x4   x5
247     *          \----------v------------/
248     *                  target
249     *
250     * f: fixed segment
251     * s: stretchy segment
252     * </pre>
253     *
254     * @param div the stretch parts defined in nine-patch chunk
255     * @param source the length of the texture
256     * @param target the length on the drawing plan
257     * @param u output, the positions of these dividers in the texture
258     *        coordinate
259     * @param x output, the corresponding position of these dividers on the
260     *        drawing plan
261     * @return the number of these dividers.
262     */
263    private static int stretch(
264            float x[], float u[], int div[], int source, int target) {
265        int textureSize = Utils.nextPowerOf2(source);
266        float textureBound = (float) source / textureSize;
267
268        float stretch = 0;
269        for (int i = 0, n = div.length; i < n; i += 2) {
270            stretch += div[i + 1] - div[i];
271        }
272
273        float remaining = target - source + stretch;
274
275        float lastX = 0;
276        float lastU = 0;
277
278        x[0] = 0;
279        u[0] = 0;
280        for (int i = 0, n = div.length; i < n; i += 2) {
281            // Make the stretchy segment a little smaller to prevent sampling
282            // on neighboring fixed segments.
283            // fixed segment
284            x[i + 1] = lastX + (div[i] - lastU) + 0.5f;
285            u[i + 1] = Math.min((div[i] + 0.5f) / textureSize, textureBound);
286
287            // stretchy segment
288            float partU = div[i + 1] - div[i];
289            float partX = remaining * partU / stretch;
290            remaining -= partX;
291            stretch -= partU;
292
293            lastX = x[i + 1] + partX;
294            lastU = div[i + 1];
295            x[i + 2] = lastX - 0.5f;
296            u[i + 2] = Math.min((lastU - 0.5f)/ textureSize, textureBound);
297        }
298        // the last fixed segment
299        x[div.length + 1] = target;
300        u[div.length + 1] = textureBound;
301
302        // remove segments with length 0.
303        int last = 0;
304        for (int i = 1, n = div.length + 2; i < n; ++i) {
305            if ((x[i] - x[last]) < 1f) continue;
306            x[++last] = x[i];
307            u[last] = u[i];
308        }
309        return last + 1;
310    }
311
312    private void prepareVertexData(float x[], float y[], float u[], float v[],
313            int nx, int ny, int[] color) {
314        /*
315         * Given a 3x3 nine-patch image, the vertex order is defined as the
316         * following graph:
317         *
318         * (0) (1) (2) (3)
319         *  |  /|  /|  /|
320         *  | / | / | / |
321         * (4) (5) (6) (7)
322         *  | \ | \ | \ |
323         *  |  \|  \|  \|
324         * (8) (9) (A) (B)
325         *  |  /|  /|  /|
326         *  | / | / | / |
327         * (C) (D) (E) (F)
328         *
329         * And we draw the triangle strip in the following index order:
330         *
331         * index: 04152637B6A5948C9DAEBF
332         */
333        int pntCount = 0;
334        float xy[] = new float[VERTEX_BUFFER_SIZE];
335        float uv[] = new float[VERTEX_BUFFER_SIZE];
336        for (int j = 0; j < ny; ++j) {
337            for (int i = 0; i < nx; ++i) {
338                int xIndex = (pntCount++) << 1;
339                int yIndex = xIndex + 1;
340                xy[xIndex] = x[i];
341                xy[yIndex] = y[j];
342                uv[xIndex] = u[i];
343                uv[yIndex] = v[j];
344            }
345        }
346
347        int idxCount = 1;
348        boolean isForward = false;
349        byte index[] = new byte[INDEX_BUFFER_SIZE];
350        for (int row = 0; row < ny - 1; row++) {
351            --idxCount;
352            isForward = !isForward;
353
354            int start, end, inc;
355            if (isForward) {
356                start = 0;
357                end = nx;
358                inc = 1;
359            } else {
360                start = nx - 1;
361                end = -1;
362                inc = -1;
363            }
364
365            for (int col = start; col != end; col += inc) {
366                int k = row * nx + col;
367                if (col != start) {
368                    int colorIdx = row * (nx - 1) + col;
369                    if (isForward) colorIdx--;
370                    if (color[colorIdx] == NinePatchChunk.TRANSPARENT_COLOR) {
371                        index[idxCount] = index[idxCount - 1];
372                        ++idxCount;
373                        index[idxCount++] = (byte) k;
374                    }
375                }
376
377                index[idxCount++] = (byte) k;
378                index[idxCount++] = (byte) (k + nx);
379            }
380        }
381
382        mIdxCount = idxCount;
383
384        int size = (pntCount * 2) * (Float.SIZE / Byte.SIZE);
385        mXyBuffer = allocateDirectNativeOrderBuffer(size).asFloatBuffer();
386        mUvBuffer = allocateDirectNativeOrderBuffer(size).asFloatBuffer();
387        mIndexBuffer = allocateDirectNativeOrderBuffer(mIdxCount);
388
389        mXyBuffer.put(xy, 0, pntCount * 2).position(0);
390        mUvBuffer.put(uv, 0, pntCount * 2).position(0);
391        mIndexBuffer.put(index, 0, idxCount).position(0);
392    }
393
394    private static ByteBuffer allocateDirectNativeOrderBuffer(int size) {
395        return ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder());
396    }
397
398    private void prepareBuffers(GLCanvas canvas) {
399        mXyBufferName = canvas.uploadBuffer(mXyBuffer);
400        mUvBufferName = canvas.uploadBuffer(mUvBuffer);
401        mIndexBufferName = canvas.uploadBuffer(mIndexBuffer);
402
403        // These buffers are never used again.
404        mXyBuffer = null;
405        mUvBuffer = null;
406        mIndexBuffer = null;
407    }
408
409    public void draw(GLCanvas canvas, NinePatchTexture tex, int x, int y) {
410        if (mXyBufferName == -1) {
411            prepareBuffers(canvas);
412        }
413        canvas.drawMesh(tex, x, y, mXyBufferName, mUvBufferName, mIndexBufferName, mIdxCount);
414    }
415
416    public void recycle(GLCanvas canvas) {
417        if (mXyBuffer == null) {
418            canvas.deleteBuffer(mXyBufferName);
419            canvas.deleteBuffer(mUvBufferName);
420            canvas.deleteBuffer(mIndexBufferName);
421            mXyBufferName = -1;
422        }
423    }
424}
425