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 
17 package com.android.gallery3d.glrenderer;
18 
19 import android.content.Context;
20 import android.graphics.Bitmap;
21 import android.graphics.BitmapFactory;
22 import android.graphics.Rect;
23 
24 import com.android.gallery3d.common.Utils;
25 
26 import java.nio.ByteBuffer;
27 import java.nio.ByteOrder;
28 import 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 //
35 public 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 
NinePatchTexture(Context context, int resId)42     public NinePatchTexture(Context context, int resId) {
43         super(context, resId);
44     }
45 
46     @Override
onGetBitmap()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 
getPaddings()66     public Rect getPaddings() {
67         // get the paddings from nine patch
68         if (mChunk == null) onGetBitmap();
69         return mChunk.mPaddings;
70     }
71 
getNinePatchChunk()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.
put(int key, V value)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 
get(int key)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 
clear()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 
size()131         public int size() {
132             return mCount;
133         }
134 
valueAt(int i)135         public V valueAt(int i) {
136             return mValue[i];
137         }
138     }
139 
findInstance(GLCanvas canvas, int w, int h)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
draw(GLCanvas canvas, int x, int y, int w, int h)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
recycle()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.
183 class 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 
NinePatchInstance(NinePatchTexture tex, int width, int height)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      */
stretch( float x[], float u[], int div[], int source, int target)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 
prepareVertexData(float x[], float y[], float u[], float v[], int nx, int ny, int[] color)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 
allocateDirectNativeOrderBuffer(int size)394     private static ByteBuffer allocateDirectNativeOrderBuffer(int size) {
395         return ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder());
396     }
397 
prepareBuffers(GLCanvas canvas)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 
draw(GLCanvas canvas, NinePatchTexture tex, int x, int y)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 
recycle(GLCanvas canvas)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