{
+ private static final int CACHE_SIZE = 16;
+ private static final int CACHE_SIZE_START_MOVE = CACHE_SIZE / 2;
+ private int[] mKey = new int[CACHE_SIZE];
+ private V[] mValue = (V[]) new Object[CACHE_SIZE];
+ private int mCount; // number of items in this cache
+
+ // Puts a value into the cache. If the cache is full, also returns
+ // a less used item, otherwise returns null.
+ public V put(int key, V value) {
+ if (mCount == CACHE_SIZE) {
+ V old = mValue[CACHE_SIZE - 1]; // remove the last item
+ mKey[CACHE_SIZE - 1] = key;
+ mValue[CACHE_SIZE - 1] = value;
+ return old;
+ } else {
+ mKey[mCount] = key;
+ mValue[mCount] = value;
+ mCount++;
+ return null;
+ }
+ }
+
+ public V get(int key) {
+ for (int i = 0; i < mCount; i++) {
+ if (mKey[i] == key) {
+ // Move the accessed item one position to the front, so it
+ // will less likely to be removed when cache is full. Only
+ // do this if the cache is starting to get full.
+ if (mCount > CACHE_SIZE_START_MOVE && i > 0) {
+ int tmpKey = mKey[i];
+ mKey[i] = mKey[i - 1];
+ mKey[i - 1] = tmpKey;
+
+ V tmpValue = mValue[i];
+ mValue[i] = mValue[i - 1];
+ mValue[i - 1] = tmpValue;
+ }
+ return mValue[i];
+ }
+ }
+ return null;
+ }
+
+ public void clear() {
+ for (int i = 0; i < mCount; i++) {
+ mValue[i] = null; // make sure it's can be garbage-collected.
+ }
+ mCount = 0;
+ }
+
+ public int size() {
+ return mCount;
+ }
+
+ public V valueAt(int i) {
+ return mValue[i];
+ }
+ }
+
+ private NinePatchInstance findInstance(GLCanvas canvas, int w, int h) {
+ int key = w;
+ key = (key << 16) | h;
+ NinePatchInstance instance = mInstanceCache.get(key);
+
+ if (instance == null) {
+ instance = new NinePatchInstance(this, w, h);
+ NinePatchInstance removed = mInstanceCache.put(key, instance);
+ if (removed != null) {
+ removed.recycle(canvas);
+ }
+ }
+
+ return instance;
+ }
+
+ @Override
+ public void draw(GLCanvas canvas, int x, int y, int w, int h) {
+ if (!isLoaded()) {
+ mInstanceCache.clear();
+ }
+
+ if (w != 0 && h != 0) {
+ findInstance(canvas, w, h).draw(canvas, this, x, y);
+ }
+ }
+
+ @Override
+ public void recycle() {
+ super.recycle();
+ GLCanvas canvas = mCanvasRef;
+ if (canvas == null) return;
+ int n = mInstanceCache.size();
+ for (int i = 0; i < n; i++) {
+ NinePatchInstance instance = mInstanceCache.valueAt(i);
+ instance.recycle(canvas);
+ }
+ mInstanceCache.clear();
+ }
+}
+
+// This keeps data for a specialization of NinePatchTexture with the size
+// (width, height). We pre-compute the coordinates for efficiency.
+class NinePatchInstance {
+
+ @SuppressWarnings("unused")
+ private static final String TAG = "NinePatchInstance";
+
+ // We need 16 vertices for a normal nine-patch image (the 4x4 vertices)
+ private static final int VERTEX_BUFFER_SIZE = 16 * 2;
+
+ // We need 22 indices for a normal nine-patch image, plus 2 for each
+ // transparent region. Current there are at most 1 transparent region.
+ private static final int INDEX_BUFFER_SIZE = 22 + 2;
+
+ private FloatBuffer mXyBuffer;
+ private FloatBuffer mUvBuffer;
+ private ByteBuffer mIndexBuffer;
+
+ // Names for buffer names: xy, uv, index.
+ private int mXyBufferName = -1;
+ private int mUvBufferName;
+ private int mIndexBufferName;
+
+ private int mIdxCount;
+
+ public NinePatchInstance(NinePatchTexture tex, int width, int height) {
+ NinePatchChunk chunk = tex.getNinePatchChunk();
+
+ if (width <= 0 || height <= 0) {
+ throw new RuntimeException("invalid dimension");
+ }
+
+ // The code should be easily extended to handle the general cases by
+ // allocating more space for buffers. But let's just handle the only
+ // use case.
+ if (chunk.mDivX.length != 2 || chunk.mDivY.length != 2) {
+ throw new RuntimeException("unsupported nine patch");
+ }
+
+ float divX[] = new float[4];
+ float divY[] = new float[4];
+ float divU[] = new float[4];
+ float divV[] = new float[4];
+
+ int nx = stretch(divX, divU, chunk.mDivX, tex.getWidth(), width);
+ int ny = stretch(divY, divV, chunk.mDivY, tex.getHeight(), height);
+
+ prepareVertexData(divX, divY, divU, divV, nx, ny, chunk.mColor);
+ }
+
+ /**
+ * Stretches the texture according to the nine-patch rules. It will
+ * linearly distribute the strechy parts defined in the nine-patch chunk to
+ * the target area.
+ *
+ *
+ * source
+ * /--------------^---------------\
+ * u0 u1 u2 u3 u4 u5
+ * div ---> |fffff|ssssssss|fff|ssssss|ffff| ---> u
+ * | div0 div1 div2 div3 |
+ * | | / / / /
+ * | | / / / /
+ * | | / / / /
+ * |fffff|ssss|fff|sss|ffff| ---> x
+ * x0 x1 x2 x3 x4 x5
+ * \----------v------------/
+ * target
+ *
+ * f: fixed segment
+ * s: stretchy segment
+ *
+ *
+ * @param div the stretch parts defined in nine-patch chunk
+ * @param source the length of the texture
+ * @param target the length on the drawing plan
+ * @param u output, the positions of these dividers in the texture
+ * coordinate
+ * @param x output, the corresponding position of these dividers on the
+ * drawing plan
+ * @return the number of these dividers.
+ */
+ private static int stretch(
+ float x[], float u[], int div[], int source, int target) {
+ int textureSize = Utils.nextPowerOf2(source);
+ float textureBound = (float) source / textureSize;
+
+ float stretch = 0;
+ for (int i = 0, n = div.length; i < n; i += 2) {
+ stretch += div[i + 1] - div[i];
+ }
+
+ float remaining = target - source + stretch;
+
+ float lastX = 0;
+ float lastU = 0;
+
+ x[0] = 0;
+ u[0] = 0;
+ for (int i = 0, n = div.length; i < n; i += 2) {
+ // Make the stretchy segment a little smaller to prevent sampling
+ // on neighboring fixed segments.
+ // fixed segment
+ x[i + 1] = lastX + (div[i] - lastU) + 0.5f;
+ u[i + 1] = Math.min((div[i] + 0.5f) / textureSize, textureBound);
+
+ // stretchy segment
+ float partU = div[i + 1] - div[i];
+ float partX = remaining * partU / stretch;
+ remaining -= partX;
+ stretch -= partU;
+
+ lastX = x[i + 1] + partX;
+ lastU = div[i + 1];
+ x[i + 2] = lastX - 0.5f;
+ u[i + 2] = Math.min((lastU - 0.5f)/ textureSize, textureBound);
+ }
+ // the last fixed segment
+ x[div.length + 1] = target;
+ u[div.length + 1] = textureBound;
+
+ // remove segments with length 0.
+ int last = 0;
+ for (int i = 1, n = div.length + 2; i < n; ++i) {
+ if ((x[i] - x[last]) < 1f) continue;
+ x[++last] = x[i];
+ u[last] = u[i];
+ }
+ return last + 1;
+ }
+
+ private void prepareVertexData(float x[], float y[], float u[], float v[],
+ int nx, int ny, int[] color) {
+ /*
+ * Given a 3x3 nine-patch image, the vertex order is defined as the
+ * following graph:
+ *
+ * (0) (1) (2) (3)
+ * | /| /| /|
+ * | / | / | / |
+ * (4) (5) (6) (7)
+ * | \ | \ | \ |
+ * | \| \| \|
+ * (8) (9) (A) (B)
+ * | /| /| /|
+ * | / | / | / |
+ * (C) (D) (E) (F)
+ *
+ * And we draw the triangle strip in the following index order:
+ *
+ * index: 04152637B6A5948C9DAEBF
+ */
+ int pntCount = 0;
+ float xy[] = new float[VERTEX_BUFFER_SIZE];
+ float uv[] = new float[VERTEX_BUFFER_SIZE];
+ for (int j = 0; j < ny; ++j) {
+ for (int i = 0; i < nx; ++i) {
+ int xIndex = (pntCount++) << 1;
+ int yIndex = xIndex + 1;
+ xy[xIndex] = x[i];
+ xy[yIndex] = y[j];
+ uv[xIndex] = u[i];
+ uv[yIndex] = v[j];
+ }
+ }
+
+ int idxCount = 1;
+ boolean isForward = false;
+ byte index[] = new byte[INDEX_BUFFER_SIZE];
+ for (int row = 0; row < ny - 1; row++) {
+ --idxCount;
+ isForward = !isForward;
+
+ int start, end, inc;
+ if (isForward) {
+ start = 0;
+ end = nx;
+ inc = 1;
+ } else {
+ start = nx - 1;
+ end = -1;
+ inc = -1;
+ }
+
+ for (int col = start; col != end; col += inc) {
+ int k = row * nx + col;
+ if (col != start) {
+ int colorIdx = row * (nx - 1) + col;
+ if (isForward) colorIdx--;
+ if (color[colorIdx] == NinePatchChunk.TRANSPARENT_COLOR) {
+ index[idxCount] = index[idxCount - 1];
+ ++idxCount;
+ index[idxCount++] = (byte) k;
+ }
+ }
+
+ index[idxCount++] = (byte) k;
+ index[idxCount++] = (byte) (k + nx);
+ }
+ }
+
+ mIdxCount = idxCount;
+
+ int size = (pntCount * 2) * (Float.SIZE / Byte.SIZE);
+ mXyBuffer = allocateDirectNativeOrderBuffer(size).asFloatBuffer();
+ mUvBuffer = allocateDirectNativeOrderBuffer(size).asFloatBuffer();
+ mIndexBuffer = allocateDirectNativeOrderBuffer(mIdxCount);
+
+ mXyBuffer.put(xy, 0, pntCount * 2).position(0);
+ mUvBuffer.put(uv, 0, pntCount * 2).position(0);
+ mIndexBuffer.put(index, 0, idxCount).position(0);
+ }
+
+ private static ByteBuffer allocateDirectNativeOrderBuffer(int size) {
+ return ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder());
+ }
+
+ private void prepareBuffers(GLCanvas canvas) {
+ mXyBufferName = canvas.uploadBuffer(mXyBuffer);
+ mUvBufferName = canvas.uploadBuffer(mUvBuffer);
+ mIndexBufferName = canvas.uploadBuffer(mIndexBuffer);
+
+ // These buffers are never used again.
+ mXyBuffer = null;
+ mUvBuffer = null;
+ mIndexBuffer = null;
+ }
+
+ public void draw(GLCanvas canvas, NinePatchTexture tex, int x, int y) {
+ if (mXyBufferName == -1) {
+ prepareBuffers(canvas);
+ }
+ canvas.drawMesh(tex, x, y, mXyBufferName, mUvBufferName, mIndexBufferName, mIdxCount);
+ }
+
+ public void recycle(GLCanvas canvas) {
+ if (mXyBuffer == null) {
+ canvas.deleteBuffer(mXyBufferName);
+ canvas.deleteBuffer(mUvBufferName);
+ canvas.deleteBuffer(mIndexBufferName);
+ mXyBufferName = -1;
+ }
+ }
+}
diff --git a/src/com/hippo/ehviewer/gallery/glrenderer/RawTexture.java b/src/com/hippo/ehviewer/gallery/glrenderer/RawTexture.java
new file mode 100644
index 000000000..7a6495ef3
--- /dev/null
+++ b/src/com/hippo/ehviewer/gallery/glrenderer/RawTexture.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.hippo.ehviewer.gallery.glrenderer;
+
+import android.util.Log;
+
+import javax.microedition.khronos.opengles.GL11;
+
+public class RawTexture extends BasicTexture {
+ private static final String TAG = "RawTexture";
+
+ private final boolean mOpaque;
+ private boolean mIsFlipped;
+
+ public RawTexture(int width, int height, boolean opaque) {
+ mOpaque = opaque;
+ setSize(width, height);
+ }
+
+ @Override
+ public boolean isOpaque() {
+ return mOpaque;
+ }
+
+ @Override
+ public boolean isFlippedVertically() {
+ return mIsFlipped;
+ }
+
+ public void setIsFlippedVertically(boolean isFlipped) {
+ mIsFlipped = isFlipped;
+ }
+
+ protected void prepare(GLCanvas canvas) {
+ GLId glId = canvas.getGLId();
+ mId = glId.generateTexture();
+ canvas.initializeTextureSize(this, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE);
+ canvas.setTextureParameters(this);
+ mState = STATE_LOADED;
+ setAssociatedCanvas(canvas);
+ }
+
+ @Override
+ protected boolean onBind(GLCanvas canvas) {
+ if (isLoaded()) return true;
+ Log.w(TAG, "lost the content due to context change");
+ return false;
+ }
+
+ @Override
+ public void yield() {
+ // we cannot free the texture because we have no backup.
+ }
+
+ @Override
+ protected int getTarget() {
+ return GL11.GL_TEXTURE_2D;
+ }
+}
diff --git a/src/com/hippo/ehviewer/gallery/glrenderer/ResourceTexture.java b/src/com/hippo/ehviewer/gallery/glrenderer/ResourceTexture.java
new file mode 100644
index 000000000..ae6df2761
--- /dev/null
+++ b/src/com/hippo/ehviewer/gallery/glrenderer/ResourceTexture.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.hippo.ehviewer.gallery.glrenderer;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+
+import junit.framework.Assert;
+
+// ResourceTexture is a texture whose Bitmap is decoded from a resource.
+// By default ResourceTexture is not opaque.
+public class ResourceTexture extends UploadedTexture {
+
+ protected final Context mContext;
+ protected final int mResId;
+
+ public ResourceTexture(Context context, int resId) {
+ Assert.assertNotNull(context);
+ mContext = context;
+ mResId = resId;
+ setOpaque(false);
+ }
+
+ @Override
+ protected Bitmap onGetBitmap() {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ return BitmapFactory.decodeResource(
+ mContext.getResources(), mResId, options);
+ }
+
+ @Override
+ protected void onFreeBitmap(Bitmap bitmap) {
+ if (!inFinalizer()) {
+ bitmap.recycle();
+ }
+ }
+}
diff --git a/src/com/hippo/ehviewer/gallery/glrenderer/StringTexture.java b/src/com/hippo/ehviewer/gallery/glrenderer/StringTexture.java
new file mode 100644
index 000000000..263f982b0
--- /dev/null
+++ b/src/com/hippo/ehviewer/gallery/glrenderer/StringTexture.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.hippo.ehviewer.gallery.glrenderer;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint.FontMetricsInt;
+import android.graphics.Typeface;
+import android.text.TextPaint;
+import android.text.TextUtils;
+import android.util.FloatMath;
+
+// StringTexture is a texture shows the content of a specified String.
+//
+// To create a StringTexture, use the newInstance() method and specify
+// the String, the font size, and the color.
+public class StringTexture extends CanvasTexture {
+ private final String mText;
+ private final TextPaint mPaint;
+ private final FontMetricsInt mMetrics;
+
+ private StringTexture(String text, TextPaint paint,
+ FontMetricsInt metrics, int width, int height) {
+ super(width, height);
+ mText = text;
+ mPaint = paint;
+ mMetrics = metrics;
+ }
+
+ public static TextPaint getDefaultPaint(float textSize, int color) {
+ TextPaint paint = new TextPaint();
+ paint.setTextSize(textSize);
+ paint.setAntiAlias(true);
+ paint.setColor(color);
+ paint.setShadowLayer(2f, 0f, 0f, Color.BLACK);
+ return paint;
+ }
+
+ public static StringTexture newInstance(
+ String text, float textSize, int color) {
+ return newInstance(text, getDefaultPaint(textSize, color));
+ }
+
+ public static StringTexture newInstance(
+ String text, float textSize, int color,
+ float lengthLimit, boolean isBold) {
+ TextPaint paint = getDefaultPaint(textSize, color);
+ if (isBold) {
+ paint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
+ }
+ if (lengthLimit > 0) {
+ text = TextUtils.ellipsize(
+ text, paint, lengthLimit, TextUtils.TruncateAt.END).toString();
+ }
+ return newInstance(text, paint);
+ }
+
+ private static StringTexture newInstance(String text, TextPaint paint) {
+ FontMetricsInt metrics = paint.getFontMetricsInt();
+ int width = (int) FloatMath.ceil(paint.measureText(text));
+ int height = metrics.bottom - metrics.top;
+ // The texture size needs to be at least 1x1.
+ if (width <= 0) width = 1;
+ if (height <= 0) height = 1;
+ return new StringTexture(text, paint, metrics, width, height);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas, Bitmap backing) {
+ canvas.translate(0, -mMetrics.ascent);
+ canvas.drawText(mText, 0, 0, mPaint);
+ }
+}
diff --git a/src/com/hippo/ehviewer/gallery/glrenderer/Texture.java b/src/com/hippo/ehviewer/gallery/glrenderer/Texture.java
new file mode 100644
index 000000000..12982cf55
--- /dev/null
+++ b/src/com/hippo/ehviewer/gallery/glrenderer/Texture.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.hippo.ehviewer.gallery.glrenderer;
+
+
+// Texture is a rectangular image which can be drawn on GLCanvas.
+// The isOpaque() function gives a hint about whether the texture is opaque,
+// so the drawing can be done faster.
+//
+// This is the current texture hierarchy:
+//
+// Texture
+// -- ColorTexture
+// -- FadeInTexture
+// -- BasicTexture
+// -- UploadedTexture
+// -- BitmapTexture
+// -- Tile
+// -- ResourceTexture
+// -- NinePatchTexture
+// -- CanvasTexture
+// -- StringTexture
+//
+public interface Texture {
+ public int getWidth();
+ public int getHeight();
+ public void draw(GLCanvas canvas, int x, int y);
+ public void draw(GLCanvas canvas, int x, int y, int w, int h);
+ public boolean isOpaque();
+}
diff --git a/src/com/hippo/ehviewer/gallery/glrenderer/TextureUploader.java b/src/com/hippo/ehviewer/gallery/glrenderer/TextureUploader.java
new file mode 100644
index 000000000..fea10db37
--- /dev/null
+++ b/src/com/hippo/ehviewer/gallery/glrenderer/TextureUploader.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.hippo.ehviewer.gallery.glrenderer;
+
+import com.hippo.ehviewer.gallery.ui.GLRoot;
+import com.hippo.ehviewer.gallery.ui.GLRoot.OnGLIdleListener;
+
+import java.util.ArrayDeque;
+
+public class TextureUploader implements OnGLIdleListener {
+ private static final int INIT_CAPACITY = 64;
+ private static final int QUOTA_PER_FRAME = 1;
+
+ private final ArrayDeque mFgTextures =
+ new ArrayDeque(INIT_CAPACITY);
+ private final ArrayDeque mBgTextures =
+ new ArrayDeque(INIT_CAPACITY);
+ private final GLRoot mGLRoot;
+ private volatile boolean mIsQueued = false;
+
+ public TextureUploader(GLRoot root) {
+ mGLRoot = root;
+ }
+
+ public synchronized void clear() {
+ while (!mFgTextures.isEmpty()) {
+ mFgTextures.pop().setIsUploading(false);
+ }
+ while (!mBgTextures.isEmpty()) {
+ mBgTextures.pop().setIsUploading(false);
+ }
+ }
+
+ // caller should hold synchronized on "this"
+ private void queueSelfIfNeed() {
+ if (mIsQueued) return;
+ mIsQueued = true;
+ mGLRoot.addOnGLIdleListener(this);
+ }
+
+ public synchronized void addBgTexture(UploadedTexture t) {
+ if (t.isContentValid()) return;
+ mBgTextures.addLast(t);
+ t.setIsUploading(true);
+ queueSelfIfNeed();
+ }
+
+ public synchronized void addFgTexture(UploadedTexture t) {
+ if (t.isContentValid()) return;
+ mFgTextures.addLast(t);
+ t.setIsUploading(true);
+ queueSelfIfNeed();
+ }
+
+ private int upload(GLCanvas canvas, ArrayDeque deque,
+ int uploadQuota, boolean isBackground) {
+ while (uploadQuota > 0) {
+ UploadedTexture t;
+ synchronized (this) {
+ if (deque.isEmpty()) break;
+ t = deque.removeFirst();
+ t.setIsUploading(false);
+ if (t.isContentValid()) continue;
+
+ // this has to be protected by the synchronized block
+ // to prevent the inner bitmap get recycled
+ t.updateContent(canvas);
+ }
+
+ // It will took some more time for a texture to be drawn for
+ // the first time.
+ // Thus, when scrolling, if a new column appears on screen,
+ // it may cause a UI jank even these textures are uploaded.
+ if (isBackground) t.draw(canvas, 0, 0);
+ --uploadQuota;
+ }
+ return uploadQuota;
+ }
+
+ @Override
+ public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) {
+ int uploadQuota = QUOTA_PER_FRAME;
+ uploadQuota = upload(canvas, mFgTextures, uploadQuota, false);
+ if (uploadQuota < QUOTA_PER_FRAME) mGLRoot.requestRender();
+ upload(canvas, mBgTextures, uploadQuota, true);
+ synchronized (this) {
+ mIsQueued = !mFgTextures.isEmpty() || !mBgTextures.isEmpty();
+ return mIsQueued;
+ }
+ }
+}
diff --git a/src/com/hippo/ehviewer/gallery/glrenderer/TiledTexture.java b/src/com/hippo/ehviewer/gallery/glrenderer/TiledTexture.java
new file mode 100644
index 000000000..b551128f9
--- /dev/null
+++ b/src/com/hippo/ehviewer/gallery/glrenderer/TiledTexture.java
@@ -0,0 +1,349 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.hippo.ehviewer.gallery.glrenderer;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.RectF;
+import android.os.SystemClock;
+
+import com.hippo.ehviewer.gallery.ui.GLRoot;
+import com.hippo.ehviewer.gallery.ui.GLRoot.OnGLIdleListener;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+
+// This class is similar to BitmapTexture, except the bitmap is
+// split into tiles. By doing so, we may increase the time required to
+// upload the whole bitmap but we reduce the time of uploading each tile
+// so it make the animation more smooth and prevents jank.
+public class TiledTexture implements Texture {
+ private static final int CONTENT_SIZE = 254;
+ private static final int BORDER_SIZE = 1;
+ private static final int TILE_SIZE = CONTENT_SIZE + 2 * BORDER_SIZE;
+ private static final int INIT_CAPACITY = 8;
+
+ // We are targeting at 60fps, so we have 16ms for each frame.
+ // In this 16ms, we use about 4~8 ms to upload tiles.
+ private static final long UPLOAD_TILE_LIMIT = 4; // ms
+
+ private static Tile sFreeTileHead = null;
+ private static final Object sFreeTileLock = new Object();
+
+ private static Bitmap sUploadBitmap;
+ private static Canvas sCanvas;
+ private static Paint sBitmapPaint;
+ private static Paint sPaint;
+
+ private int mUploadIndex = 0;
+
+ private final Tile[] mTiles; // Can be modified in different threads.
+ // Should be protected by "synchronized."
+ private final int mWidth;
+ private final int mHeight;
+ private final RectF mSrcRect = new RectF();
+ private final RectF mDestRect = new RectF();
+
+ public static class Uploader implements OnGLIdleListener {
+ private final ArrayDeque mTextures =
+ new ArrayDeque(INIT_CAPACITY);
+
+ private final GLRoot mGlRoot;
+ private boolean mIsQueued = false;
+
+ public Uploader(GLRoot glRoot) {
+ mGlRoot = glRoot;
+ }
+
+ public synchronized void clear() {
+ mTextures.clear();
+ }
+
+ public synchronized void addTexture(TiledTexture t) {
+ if (t.isReady()) return;
+ mTextures.addLast(t);
+
+ if (mIsQueued) return;
+ mIsQueued = true;
+ mGlRoot.addOnGLIdleListener(this);
+ }
+
+ @Override
+ public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) {
+ ArrayDeque deque = mTextures;
+ synchronized (this) {
+ long now = SystemClock.uptimeMillis();
+ long dueTime = now + UPLOAD_TILE_LIMIT;
+ while (now < dueTime && !deque.isEmpty()) {
+ TiledTexture t = deque.peekFirst();
+ if (t.uploadNextTile(canvas)) {
+ deque.removeFirst();
+ mGlRoot.requestRender();
+ }
+ now = SystemClock.uptimeMillis();
+ }
+ mIsQueued = !mTextures.isEmpty();
+
+ // return true to keep this listener in the queue
+ return mIsQueued;
+ }
+ }
+ }
+
+ private static class Tile extends UploadedTexture {
+ public int offsetX;
+ public int offsetY;
+ public Bitmap bitmap;
+ public Tile nextFreeTile;
+ public int contentWidth;
+ public int contentHeight;
+
+ @Override
+ public void setSize(int width, int height) {
+ contentWidth = width;
+ contentHeight = height;
+ mWidth = width + 2 * BORDER_SIZE;
+ mHeight = height + 2 * BORDER_SIZE;
+ mTextureWidth = TILE_SIZE;
+ mTextureHeight = TILE_SIZE;
+ }
+
+ @Override
+ protected Bitmap onGetBitmap() {
+ int x = BORDER_SIZE - offsetX;
+ int y = BORDER_SIZE - offsetY;
+ int r = bitmap.getWidth() + x;
+ int b = bitmap.getHeight() + y;
+ sCanvas.drawBitmap(bitmap, x, y, sBitmapPaint);
+ bitmap = null;
+
+ // draw borders if need
+ if (x > 0) sCanvas.drawLine(x - 1, 0, x - 1, TILE_SIZE, sPaint);
+ if (y > 0) sCanvas.drawLine(0, y - 1, TILE_SIZE, y - 1, sPaint);
+ if (r < CONTENT_SIZE) sCanvas.drawLine(r, 0, r, TILE_SIZE, sPaint);
+ if (b < CONTENT_SIZE) sCanvas.drawLine(0, b, TILE_SIZE, b, sPaint);
+
+ return sUploadBitmap;
+ }
+
+ @Override
+ protected void onFreeBitmap(Bitmap bitmap) {
+ // do nothing
+ }
+ }
+
+ private static void freeTile(Tile tile) {
+ tile.invalidateContent();
+ tile.bitmap = null;
+ synchronized (sFreeTileLock) {
+ tile.nextFreeTile = sFreeTileHead;
+ sFreeTileHead = tile;
+ }
+ }
+
+ private static Tile obtainTile() {
+ synchronized (sFreeTileLock) {
+ Tile result = sFreeTileHead;
+ if (result == null) return new Tile();
+ sFreeTileHead = result.nextFreeTile;
+ result.nextFreeTile = null;
+ return result;
+ }
+ }
+
+ private boolean uploadNextTile(GLCanvas canvas) {
+ if (mUploadIndex == mTiles.length) return true;
+
+ synchronized (mTiles) {
+ Tile next = mTiles[mUploadIndex++];
+
+ // Make sure tile has not already been recycled by the time
+ // this is called (race condition in onGLIdle)
+ if (next.bitmap != null) {
+ boolean hasBeenLoad = next.isLoaded();
+ next.updateContent(canvas);
+
+ // It will take some time for a texture to be drawn for the first
+ // time. When scrolling, we need to draw several tiles on the screen
+ // at the same time. It may cause a UI jank even these textures has
+ // been uploaded.
+ if (!hasBeenLoad) next.draw(canvas, 0, 0);
+ }
+ }
+ return mUploadIndex == mTiles.length;
+ }
+
+ public TiledTexture(Bitmap bitmap) {
+ mWidth = bitmap.getWidth();
+ mHeight = bitmap.getHeight();
+ ArrayList list = new ArrayList();
+
+ for (int x = 0, w = mWidth; x < w; x += CONTENT_SIZE) {
+ for (int y = 0, h = mHeight; y < h; y += CONTENT_SIZE) {
+ Tile tile = obtainTile();
+ tile.offsetX = x;
+ tile.offsetY = y;
+ tile.bitmap = bitmap;
+ tile.setSize(
+ Math.min(CONTENT_SIZE, mWidth - x),
+ Math.min(CONTENT_SIZE, mHeight - y));
+ list.add(tile);
+ }
+ }
+ mTiles = list.toArray(new Tile[list.size()]);
+ }
+
+ public boolean isReady() {
+ return mUploadIndex == mTiles.length;
+ }
+
+ // Can be called in UI thread.
+ public void recycle() {
+ synchronized (mTiles) {
+ for (int i = 0, n = mTiles.length; i < n; ++i) {
+ freeTile(mTiles[i]);
+ }
+ }
+ }
+
+ public static void freeResources() {
+ sUploadBitmap = null;
+ sCanvas = null;
+ sBitmapPaint = null;
+ sPaint = null;
+ }
+
+ public static void prepareResources() {
+ sUploadBitmap = Bitmap.createBitmap(TILE_SIZE, TILE_SIZE, Config.ARGB_8888);
+ sCanvas = new Canvas(sUploadBitmap);
+ sBitmapPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
+ sBitmapPaint.setXfermode(new PorterDuffXfermode(Mode.SRC));
+ sPaint = new Paint();
+ sPaint.setXfermode(new PorterDuffXfermode(Mode.SRC));
+ sPaint.setColor(Color.TRANSPARENT);
+ }
+
+ // We want to draw the "source" on the "target".
+ // This method is to find the "output" rectangle which is
+ // the corresponding area of the "src".
+ // (x,y) target
+ // (x0,y0) source +---------------+
+ // +----------+ | |
+ // | src | | output |
+ // | +--+ | linear map | +----+ |
+ // | +--+ | ----------> | | | |
+ // | | by (scaleX, scaleY) | +----+ |
+ // +----------+ | |
+ // Texture +---------------+
+ // Canvas
+ private static void mapRect(RectF output,
+ RectF src, float x0, float y0, float x, float y, float scaleX,
+ float scaleY) {
+ output.set(x + (src.left - x0) * scaleX,
+ y + (src.top - y0) * scaleY,
+ x + (src.right - x0) * scaleX,
+ y + (src.bottom - y0) * scaleY);
+ }
+
+ // Draws a mixed color of this texture and a specified color onto the
+ // a rectangle. The used color is: from * (1 - ratio) + to * ratio.
+ public void drawMixed(GLCanvas canvas, int color, float ratio,
+ int x, int y, int width, int height) {
+ RectF src = mSrcRect;
+ RectF dest = mDestRect;
+ float scaleX = (float) width / mWidth;
+ float scaleY = (float) height / mHeight;
+ synchronized (mTiles) {
+ for (int i = 0, n = mTiles.length; i < n; ++i) {
+ Tile t = mTiles[i];
+ src.set(0, 0, t.contentWidth, t.contentHeight);
+ src.offset(t.offsetX, t.offsetY);
+ mapRect(dest, src, 0, 0, x, y, scaleX, scaleY);
+ src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY);
+ canvas.drawMixed(t, color, ratio, mSrcRect, mDestRect);
+ }
+ }
+ }
+
+ // Draws the texture on to the specified rectangle.
+ @Override
+ public void draw(GLCanvas canvas, int x, int y, int width, int height) {
+ RectF src = mSrcRect;
+ RectF dest = mDestRect;
+ float scaleX = (float) width / mWidth;
+ float scaleY = (float) height / mHeight;
+ synchronized (mTiles) {
+ for (int i = 0, n = mTiles.length; i < n; ++i) {
+ Tile t = mTiles[i];
+ src.set(0, 0, t.contentWidth, t.contentHeight);
+ src.offset(t.offsetX, t.offsetY);
+ mapRect(dest, src, 0, 0, x, y, scaleX, scaleY);
+ src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY);
+ canvas.drawTexture(t, mSrcRect, mDestRect);
+ }
+ }
+ }
+
+ // Draws a sub region of this texture on to the specified rectangle.
+ public void draw(GLCanvas canvas, RectF source, RectF target) {
+ RectF src = mSrcRect;
+ RectF dest = mDestRect;
+ float x0 = source.left;
+ float y0 = source.top;
+ float x = target.left;
+ float y = target.top;
+ float scaleX = target.width() / source.width();
+ float scaleY = target.height() / source.height();
+
+ synchronized (mTiles) {
+ for (int i = 0, n = mTiles.length; i < n; ++i) {
+ Tile t = mTiles[i];
+ src.set(0, 0, t.contentWidth, t.contentHeight);
+ src.offset(t.offsetX, t.offsetY);
+ if (!src.intersect(source)) continue;
+ mapRect(dest, src, x0, y0, x, y, scaleX, scaleY);
+ src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY);
+ canvas.drawTexture(t, src, dest);
+ }
+ }
+ }
+
+ @Override
+ public int getWidth() {
+ return mWidth;
+ }
+
+ @Override
+ public int getHeight() {
+ return mHeight;
+ }
+
+ @Override
+ public void draw(GLCanvas canvas, int x, int y) {
+ draw(canvas, x, y, mWidth, mHeight);
+ }
+
+ @Override
+ public boolean isOpaque() {
+ return false;
+ }
+}
diff --git a/src/com/hippo/ehviewer/gallery/glrenderer/UploadedTexture.java b/src/com/hippo/ehviewer/gallery/glrenderer/UploadedTexture.java
new file mode 100644
index 000000000..760eab504
--- /dev/null
+++ b/src/com/hippo/ehviewer/gallery/glrenderer/UploadedTexture.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.hippo.ehviewer.gallery.glrenderer;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.opengl.GLUtils;
+
+import junit.framework.Assert;
+
+import java.util.HashMap;
+
+import javax.microedition.khronos.opengles.GL11;
+
+// UploadedTextures use a Bitmap for the content of the texture.
+//
+// Subclasses should implement onGetBitmap() to provide the Bitmap and
+// implement onFreeBitmap(mBitmap) which will be called when the Bitmap
+// is not needed anymore.
+//
+// isContentValid() is meaningful only when the isLoaded() returns true.
+// It means whether the content needs to be updated.
+//
+// The user of this class should call recycle() when the texture is not
+// needed anymore.
+//
+// By default an UploadedTexture is opaque (so it can be drawn faster without
+// blending). The user or subclass can override it using setOpaque().
+public abstract class UploadedTexture extends BasicTexture {
+
+ // To prevent keeping allocation the borders, we store those used borders here.
+ // Since the length will be power of two, it won't use too much memory.
+ private static HashMap sBorderLines =
+ new HashMap();
+ private static BorderKey sBorderKey = new BorderKey();
+
+ @SuppressWarnings("unused")
+ private static final String TAG = "Texture";
+ private boolean mContentValid = true;
+
+ // indicate this textures is being uploaded in background
+ private boolean mIsUploading = false;
+ private boolean mOpaque = true;
+ private boolean mThrottled = false;
+ private static int sUploadedCount;
+ private static final int UPLOAD_LIMIT = 100;
+
+ protected Bitmap mBitmap;
+ private int mBorder;
+
+ protected UploadedTexture() {
+ this(false);
+ }
+
+ protected UploadedTexture(boolean hasBorder) {
+ super(null, 0, STATE_UNLOADED);
+ if (hasBorder) {
+ setBorder(true);
+ mBorder = 1;
+ }
+ }
+
+ protected void setIsUploading(boolean uploading) {
+ mIsUploading = uploading;
+ }
+
+ public boolean isUploading() {
+ return mIsUploading;
+ }
+
+ private static class BorderKey implements Cloneable {
+ public boolean vertical;
+ public Config config;
+ public int length;
+
+ @Override
+ public int hashCode() {
+ int x = config.hashCode() ^ length;
+ return vertical ? x : -x;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (!(object instanceof BorderKey)) return false;
+ BorderKey o = (BorderKey) object;
+ return vertical == o.vertical
+ && config == o.config && length == o.length;
+ }
+
+ @Override
+ public BorderKey clone() {
+ try {
+ return (BorderKey) super.clone();
+ } catch (CloneNotSupportedException e) {
+ throw new AssertionError(e);
+ }
+ }
+ }
+
+ protected void setThrottled(boolean throttled) {
+ mThrottled = throttled;
+ }
+
+ private static Bitmap getBorderLine(
+ boolean vertical, Config config, int length) {
+ BorderKey key = sBorderKey;
+ key.vertical = vertical;
+ key.config = config;
+ key.length = length;
+ Bitmap bitmap = sBorderLines.get(key);
+ if (bitmap == null) {
+ bitmap = vertical
+ ? Bitmap.createBitmap(1, length, config)
+ : Bitmap.createBitmap(length, 1, config);
+ sBorderLines.put(key.clone(), bitmap);
+ }
+ return bitmap;
+ }
+
+ private Bitmap getBitmap() {
+ if (mBitmap == null) {
+ mBitmap = onGetBitmap();
+ int w = mBitmap.getWidth() + mBorder * 2;
+ int h = mBitmap.getHeight() + mBorder * 2;
+ if (mWidth == UNSPECIFIED) {
+ setSize(w, h);
+ }
+ }
+ return mBitmap;
+ }
+
+ private void freeBitmap() {
+ Assert.assertTrue(mBitmap != null);
+ onFreeBitmap(mBitmap);
+ mBitmap = null;
+ }
+
+ @Override
+ public int getWidth() {
+ if (mWidth == UNSPECIFIED) getBitmap();
+ return mWidth;
+ }
+
+ @Override
+ public int getHeight() {
+ if (mWidth == UNSPECIFIED) getBitmap();
+ return mHeight;
+ }
+
+ protected abstract Bitmap onGetBitmap();
+
+ protected abstract void onFreeBitmap(Bitmap bitmap);
+
+ protected void invalidateContent() {
+ if (mBitmap != null) freeBitmap();
+ mContentValid = false;
+ mWidth = UNSPECIFIED;
+ mHeight = UNSPECIFIED;
+ }
+
+ /**
+ * Whether the content on GPU is valid.
+ */
+ public boolean isContentValid() {
+ return isLoaded() && mContentValid;
+ }
+
+ /**
+ * Updates the content on GPU's memory.
+ * @param canvas
+ */
+ public void updateContent(GLCanvas canvas) {
+ if (!isLoaded()) {
+ if (mThrottled && ++sUploadedCount > UPLOAD_LIMIT) {
+ return;
+ }
+ uploadToCanvas(canvas);
+ } else if (!mContentValid) {
+ Bitmap bitmap = getBitmap();
+ int format = GLUtils.getInternalFormat(bitmap);
+ int type = GLUtils.getType(bitmap);
+ canvas.texSubImage2D(this, mBorder, mBorder, bitmap, format, type);
+ freeBitmap();
+ mContentValid = true;
+ }
+ }
+
+ public static void resetUploadLimit() {
+ sUploadedCount = 0;
+ }
+
+ public static boolean uploadLimitReached() {
+ return sUploadedCount > UPLOAD_LIMIT;
+ }
+
+ private void uploadToCanvas(GLCanvas canvas) {
+
+ Bitmap bitmap = getBitmap();
+ if (bitmap != null) {
+ try {
+ int bWidth = bitmap.getWidth();
+ int bHeight = bitmap.getHeight();
+ int width = bWidth + mBorder * 2;
+ int height = bHeight + mBorder * 2;
+ int texWidth = getTextureWidth();
+ int texHeight = getTextureHeight();
+
+ Assert.assertTrue(bWidth <= texWidth && bHeight <= texHeight);
+
+ // Upload the bitmap to a new texture.
+ mId = canvas.getGLId().generateTexture();
+ canvas.setTextureParameters(this);
+
+ if (bWidth == texWidth && bHeight == texHeight) {
+ canvas.initializeTexture(this, bitmap);
+ } else {
+ int format = GLUtils.getInternalFormat(bitmap);
+ int type = GLUtils.getType(bitmap);
+ Config config = bitmap.getConfig();
+
+ canvas.initializeTextureSize(this, format, type);
+ canvas.texSubImage2D(this, mBorder, mBorder, bitmap, format, type);
+
+ if (mBorder > 0) {
+ // Left border
+ Bitmap line = getBorderLine(true, config, texHeight);
+ canvas.texSubImage2D(this, 0, 0, line, format, type);
+
+ // Top border
+ line = getBorderLine(false, config, texWidth);
+ canvas.texSubImage2D(this, 0, 0, line, format, type);
+ }
+
+ // Right border
+ if (mBorder + bWidth < texWidth) {
+ Bitmap line = getBorderLine(true, config, texHeight);
+ canvas.texSubImage2D(this, mBorder + bWidth, 0, line, format, type);
+ }
+
+ // Bottom border
+ if (mBorder + bHeight < texHeight) {
+ Bitmap line = getBorderLine(false, config, texWidth);
+ canvas.texSubImage2D(this, 0, mBorder + bHeight, line, format, type);
+ }
+ }
+ } finally {
+ freeBitmap();
+ }
+ // Update texture state.
+ setAssociatedCanvas(canvas);
+ mState = STATE_LOADED;
+ mContentValid = true;
+ } else {
+ mState = STATE_ERROR;
+ throw new RuntimeException("Texture load fail, no bitmap");
+ }
+ }
+
+ @Override
+ protected boolean onBind(GLCanvas canvas) {
+ updateContent(canvas);
+ return isContentValid();
+ }
+
+ @Override
+ protected int getTarget() {
+ return GL11.GL_TEXTURE_2D;
+ }
+
+ public void setOpaque(boolean isOpaque) {
+ mOpaque = isOpaque;
+ }
+
+ @Override
+ public boolean isOpaque() {
+ return mOpaque;
+ }
+
+ @Override
+ public void recycle() {
+ super.recycle();
+ if (mBitmap != null) freeBitmap();
+ }
+}
diff --git a/src/com/hippo/ehviewer/gallery/ui/AnimationTime.java b/src/com/hippo/ehviewer/gallery/ui/AnimationTime.java
new file mode 100644
index 000000000..921787eac
--- /dev/null
+++ b/src/com/hippo/ehviewer/gallery/ui/AnimationTime.java
@@ -0,0 +1,45 @@
+
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.hippo.ehviewer.gallery.ui;
+
+import android.os.SystemClock;
+
+//
+// The animation time should ideally be the vsync time the frame will be
+// displayed, but that is an unknown time in the future. So we use the system
+// time just after eglSwapBuffers (when GLSurfaceView.onDrawFrame is called)
+// as a approximation.
+//
+public class AnimationTime {
+ private static volatile long sTime;
+
+ // Sets current time as the animation time.
+ public static void update() {
+ sTime = SystemClock.uptimeMillis();
+ }
+
+ // Returns the animation time.
+ public static long get() {
+ return sTime;
+ }
+
+ public static long startTime() {
+ sTime = SystemClock.uptimeMillis();
+ return sTime;
+ }
+}
diff --git a/src/com/hippo/ehviewer/gallery/ui/DownUpDetector.java b/src/com/hippo/ehviewer/gallery/ui/DownUpDetector.java
new file mode 100644
index 000000000..05f496637
--- /dev/null
+++ b/src/com/hippo/ehviewer/gallery/ui/DownUpDetector.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.hippo.ehviewer.gallery.ui;
+
+import android.view.MotionEvent;
+
+public class DownUpDetector {
+ public interface DownUpListener {
+ void onDown(MotionEvent e);
+ void onUp(MotionEvent e);
+ }
+
+ private boolean mStillDown;
+ private DownUpListener mListener;
+
+ public DownUpDetector(DownUpListener listener) {
+ mListener = listener;
+ }
+
+ private void setState(boolean down, MotionEvent e) {
+ if (down == mStillDown) return;
+ mStillDown = down;
+ if (down) {
+ mListener.onDown(e);
+ } else {
+ mListener.onUp(e);
+ }
+ }
+
+ public void onTouchEvent(MotionEvent ev) {
+ switch (ev.getAction() & MotionEvent.ACTION_MASK) {
+ case MotionEvent.ACTION_DOWN:
+ setState(true, ev);
+ break;
+
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_POINTER_DOWN: // Multitouch event - abort.
+ setState(false, ev);
+ break;
+ }
+ }
+
+ public boolean isDown() {
+ return mStillDown;
+ }
+}
diff --git a/src/com/hippo/ehviewer/gallery/ui/GLRoot.java b/src/com/hippo/ehviewer/gallery/ui/GLRoot.java
new file mode 100644
index 000000000..565e7fd27
--- /dev/null
+++ b/src/com/hippo/ehviewer/gallery/ui/GLRoot.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.hippo.ehviewer.gallery.ui;
+
+import android.content.Context;
+import android.graphics.Matrix;
+
+import com.hippo.ehviewer.gallery.anim.CanvasAnimation;
+import com.hippo.ehviewer.gallery.glrenderer.GLCanvas;
+
+public interface GLRoot {
+
+ // Listener will be called when GL is idle AND before each frame.
+ // Mainly used for uploading textures.
+ public static interface OnGLIdleListener {
+ public boolean onGLIdle(
+ GLCanvas canvas, boolean renderRequested);
+ }
+
+ public void addOnGLIdleListener(OnGLIdleListener listener);
+ public void registerLaunchedAnimation(CanvasAnimation animation);
+ public void requestRenderForced();
+ public void requestRender();
+ public void requestLayoutContentPane();
+
+ public void lockRenderThread();
+ public void unlockRenderThread();
+
+ public void setContentPane(GLView content);
+ public void setOrientationSource(OrientationSource source);
+ public int getDisplayRotation();
+ public int getCompensation();
+ public Matrix getCompensationMatrix();
+ public void freeze();
+ public void unfreeze();
+ public void setLightsOutMode(boolean enabled);
+
+ public Context getContext();
+}
diff --git a/src/com/hippo/ehviewer/gallery/ui/GLRootView.java b/src/com/hippo/ehviewer/gallery/ui/GLRootView.java
new file mode 100644
index 000000000..73d7fc02b
--- /dev/null
+++ b/src/com/hippo/ehviewer/gallery/ui/GLRootView.java
@@ -0,0 +1,599 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.hippo.ehviewer.gallery.ui;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.PixelFormat;
+import android.opengl.GLSurfaceView;
+import android.os.Build;
+import android.os.Process;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.SurfaceHolder;
+import android.view.View;
+
+import com.hippo.ehviewer.R;
+import com.hippo.ehviewer.gallery.anim.CanvasAnimation;
+import com.hippo.ehviewer.gallery.common.ApiHelper;
+import com.hippo.ehviewer.gallery.common.Utils;
+import com.hippo.ehviewer.gallery.glrenderer.BasicTexture;
+import com.hippo.ehviewer.gallery.glrenderer.GLCanvas;
+import com.hippo.ehviewer.gallery.glrenderer.GLES11Canvas;
+import com.hippo.ehviewer.gallery.glrenderer.GLES20Canvas;
+import com.hippo.ehviewer.gallery.glrenderer.UploadedTexture;
+import com.hippo.ehviewer.gallery.util.GalleryUtils;
+import com.hippo.ehviewer.gallery.util.MotionEventHelper;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.ReentrantLock;
+
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+import javax.microedition.khronos.opengles.GL11;
+
+// The root component of all GLViews. The rendering is done in GL
+// thread while the event handling is done in the main thread. To synchronize
+// the two threads, the entry points of this package need to synchronize on the
+// GLRootView instance unless it can be proved that the rendering
+// thread won't access the same thing as the method. The entry points include:
+// (1) The public methods of HeadUpDisplay
+// (2) The public methods of CameraHeadUpDisplay
+// (3) The overridden methods in GLRootView.
+public class GLRootView extends GLSurfaceView
+ implements GLSurfaceView.Renderer, GLRoot {
+ private static final String TAG = "GLRootView";
+
+ private static final boolean DEBUG_FPS = false;
+ private int mFrameCount = 0;
+ private long mFrameCountingStart = 0;
+
+ private static final boolean DEBUG_INVALIDATE = false;
+ private int mInvalidateColor = 0;
+
+ private static final boolean DEBUG_DRAWING_STAT = false;
+
+ private static final boolean DEBUG_PROFILE = false;
+ private static final boolean DEBUG_PROFILE_SLOW_ONLY = false;
+
+ private static final int FLAG_INITIALIZED = 1;
+ private static final int FLAG_NEED_LAYOUT = 2;
+
+ private GL11 mGL;
+ private GLCanvas mCanvas;
+ private GLView mContentView;
+
+ private OrientationSource mOrientationSource;
+ // mCompensation is the difference between the UI orientation on GLCanvas
+ // and the framework orientation. See OrientationManager for details.
+ private int mCompensation;
+ // mCompensationMatrix maps the coordinates of touch events. It is kept sync
+ // with mCompensation.
+ private Matrix mCompensationMatrix = new Matrix();
+ private int mDisplayRotation;
+
+ private int mFlags = FLAG_NEED_LAYOUT;
+ private volatile boolean mRenderRequested = false;
+
+ private final ArrayList mAnimations =
+ new ArrayList();
+
+ private final ArrayDeque mIdleListeners =
+ new ArrayDeque();
+
+ private final IdleRunner mIdleRunner = new IdleRunner();
+
+ private final ReentrantLock mRenderLock = new ReentrantLock();
+ private final Condition mFreezeCondition =
+ mRenderLock.newCondition();
+ private boolean mFreeze;
+
+ private long mLastDrawFinishTime;
+ private boolean mInDownState = false;
+ private boolean mFirstDraw = true;
+
+ public GLRootView(Context context) {
+ this(context, null);
+ }
+
+ public GLRootView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mFlags |= FLAG_INITIALIZED;
+ setBackgroundDrawable(null);
+ setEGLContextClientVersion(ApiHelper.HAS_GLES20_REQUIRED ? 2 : 1);
+ if (ApiHelper.USE_888_PIXEL_FORMAT) {
+ setEGLConfigChooser(8, 8, 8, 0, 0, 0);
+ } else {
+ setEGLConfigChooser(5, 6, 5, 0, 0, 0);
+ }
+ setRenderer(this);
+ if (ApiHelper.USE_888_PIXEL_FORMAT) {
+ getHolder().setFormat(PixelFormat.RGB_888);
+ } else {
+ getHolder().setFormat(PixelFormat.RGB_565);
+ }
+
+ // Uncomment this to enable gl error check.
+ // setDebugFlags(DEBUG_CHECK_GL_ERROR);
+ }
+
+ @Override
+ public void registerLaunchedAnimation(CanvasAnimation animation) {
+ // Register the newly launched animation so that we can set the start
+ // time more precisely. (Usually, it takes much longer for first
+ // rendering, so we set the animation start time as the time we
+ // complete rendering)
+ mAnimations.add(animation);
+ }
+
+ @Override
+ public void addOnGLIdleListener(OnGLIdleListener listener) {
+ synchronized (mIdleListeners) {
+ mIdleListeners.addLast(listener);
+ mIdleRunner.enable();
+ }
+ }
+
+ @Override
+ public void setContentPane(GLView content) {
+ if (mContentView == content) return;
+ if (mContentView != null) {
+ if (mInDownState) {
+ long now = SystemClock.uptimeMillis();
+ MotionEvent cancelEvent = MotionEvent.obtain(
+ now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);
+ mContentView.dispatchTouchEvent(cancelEvent);
+ cancelEvent.recycle();
+ mInDownState = false;
+ }
+ mContentView.detachFromRoot();
+ BasicTexture.yieldAllTextures();
+ }
+ mContentView = content;
+ if (content != null) {
+ content.attachToRoot(this);
+ requestLayoutContentPane();
+ }
+ }
+
+ @Override
+ public void requestRenderForced() {
+ superRequestRender();
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public void requestRender() {
+ if (DEBUG_INVALIDATE) {
+ StackTraceElement e = Thread.currentThread().getStackTrace()[4];
+ String caller = e.getFileName() + ":" + e.getLineNumber() + " ";
+ Log.d(TAG, "invalidate: " + caller);
+ }
+ if (mRenderRequested) return;
+ mRenderRequested = true;
+ if (ApiHelper.HAS_POST_ON_ANIMATION) {
+ postOnAnimation(mRequestRenderOnAnimationFrame);
+ } else {
+ super.requestRender();
+ }
+ }
+
+ private Runnable mRequestRenderOnAnimationFrame = new Runnable() {
+ @Override
+ public void run() {
+ superRequestRender();
+ }
+ };
+
+ private void superRequestRender() {
+ super.requestRender();
+ }
+
+ @Override
+ public void requestLayoutContentPane() {
+ mRenderLock.lock();
+ try {
+ if (mContentView == null || (mFlags & FLAG_NEED_LAYOUT) != 0) return;
+
+ // "View" system will invoke onLayout() for initialization(bug ?), we
+ // have to ignore it since the GLThread is not ready yet.
+ if ((mFlags & FLAG_INITIALIZED) == 0) return;
+
+ mFlags |= FLAG_NEED_LAYOUT;
+ requestRender();
+ } finally {
+ mRenderLock.unlock();
+ }
+ }
+
+ private void layoutContentPane() {
+ mFlags &= ~FLAG_NEED_LAYOUT;
+
+ int w = getWidth();
+ int h = getHeight();
+ int displayRotation = 0;
+ int compensation = 0;
+
+ // Get the new orientation values
+ if (mOrientationSource != null) {
+ displayRotation = mOrientationSource.getDisplayRotation();
+ compensation = mOrientationSource.getCompensation();
+ } else {
+ displayRotation = 0;
+ compensation = 0;
+ }
+
+ if (mCompensation != compensation) {
+ mCompensation = compensation;
+ if (mCompensation % 180 != 0) {
+ mCompensationMatrix.setRotate(mCompensation);
+ // move center to origin before rotation
+ mCompensationMatrix.preTranslate(-w / 2, -h / 2);
+ // align with the new origin after rotation
+ mCompensationMatrix.postTranslate(h / 2, w / 2);
+ } else {
+ mCompensationMatrix.setRotate(mCompensation, w / 2, h / 2);
+ }
+ }
+ mDisplayRotation = displayRotation;
+
+ // Do the actual layout.
+ if (mCompensation % 180 != 0) {
+ int tmp = w;
+ w = h;
+ h = tmp;
+ }
+ Log.i(TAG, "layout content pane " + w + "x" + h
+ + " (compensation " + mCompensation + ")");
+ if (mContentView != null && w != 0 && h != 0) {
+ mContentView.layout(0, 0, w, h);
+ }
+ // Uncomment this to dump the view hierarchy.
+ //mContentView.dumpTree("");
+ }
+
+ @Override
+ protected void onLayout(
+ boolean changed, int left, int top, int right, int bottom) {
+ if (changed) requestLayoutContentPane();
+ }
+
+ /**
+ * Called when the context is created, possibly after automatic destruction.
+ */
+ // This is a GLSurfaceView.Renderer callback
+ @Override
+ public void onSurfaceCreated(GL10 gl1, EGLConfig config) {
+ GL11 gl = (GL11) gl1;
+ if (mGL != null) {
+ // The GL Object has changed
+ Log.i(TAG, "GLObject has changed from " + mGL + " to " + gl);
+ }
+ mRenderLock.lock();
+ try {
+ mGL = gl;
+ mCanvas = ApiHelper.HAS_GLES20_REQUIRED ? new GLES20Canvas() : new GLES11Canvas(gl);
+ BasicTexture.invalidateAllTextures();
+ } finally {
+ mRenderLock.unlock();
+ }
+
+ if (DEBUG_FPS || DEBUG_PROFILE) {
+ setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
+ } else {
+ setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
+ }
+ }
+
+ /**
+ * Called when the OpenGL surface is recreated without destroying the
+ * context.
+ */
+ // This is a GLSurfaceView.Renderer callback
+ @Override
+ public void onSurfaceChanged(GL10 gl1, int width, int height) {
+ Log.i(TAG, "onSurfaceChanged: " + width + "x" + height
+ + ", gl10: " + gl1.toString());
+ Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY);
+ GalleryUtils.setRenderThread();
+ GL11 gl = (GL11) gl1;
+ Utils.assertTrue(mGL == gl);
+
+ mCanvas.setSize(width, height);
+ }
+
+ private void outputFps() {
+ long now = System.nanoTime();
+ if (mFrameCountingStart == 0) {
+ mFrameCountingStart = now;
+ } else if ((now - mFrameCountingStart) > 1000000000) {
+ Log.d(TAG, "fps: " + (double) mFrameCount
+ * 1000000000 / (now - mFrameCountingStart));
+ mFrameCountingStart = now;
+ mFrameCount = 0;
+ }
+ ++mFrameCount;
+ }
+
+ @Override
+ public void onDrawFrame(GL10 gl) {
+ AnimationTime.update();
+ mRenderLock.lock();
+
+ while (mFreeze) {
+ mFreezeCondition.awaitUninterruptibly();
+ }
+
+ try {
+ onDrawFrameLocked(gl);
+ } finally {
+ mRenderLock.unlock();
+ }
+
+ // We put a black cover View in front of the SurfaceView and hide it
+ // after the first draw. This prevents the SurfaceView being transparent
+ // before the first draw.
+ if (mFirstDraw) {
+ mFirstDraw = false;
+ post(new Runnable() {
+ @Override
+ public void run() {
+ View root = getRootView();
+ View cover = root.findViewById(R.id.gl_root_cover);
+ cover.setVisibility(GONE);
+ }
+ });
+ }
+ }
+
+ private void onDrawFrameLocked(GL10 gl) {
+ if (DEBUG_FPS) outputFps();
+
+ // release the unbound textures and deleted buffers.
+ mCanvas.deleteRecycledResources();
+
+ // reset texture upload limit
+ UploadedTexture.resetUploadLimit();
+
+ mRenderRequested = false;
+
+ if ((mOrientationSource != null
+ && mDisplayRotation != mOrientationSource.getDisplayRotation())
+ || (mFlags & FLAG_NEED_LAYOUT) != 0) {
+ layoutContentPane();
+ }
+
+ mCanvas.save(GLCanvas.SAVE_FLAG_ALL);
+ rotateCanvas(-mCompensation);
+ if (mContentView != null) {
+ mContentView.render(mCanvas);
+ }
+ mCanvas.restore();
+
+ if (!mAnimations.isEmpty()) {
+ long now = AnimationTime.get();
+ for (int i = 0, n = mAnimations.size(); i < n; i++) {
+ mAnimations.get(i).setStartTime(now);
+ }
+ mAnimations.clear();
+ }
+
+ if (UploadedTexture.uploadLimitReached()) {
+ requestRender();
+ }
+
+ synchronized (mIdleListeners) {
+ if (!mIdleListeners.isEmpty()) mIdleRunner.enable();
+ }
+
+ if (DEBUG_INVALIDATE) {
+ mCanvas.fillRect(10, 10, 5, 5, mInvalidateColor);
+ mInvalidateColor = ~mInvalidateColor;
+ }
+
+ if (DEBUG_DRAWING_STAT) {
+ mCanvas.dumpStatisticsAndClear();
+ }
+ }
+
+ private void rotateCanvas(int degrees) {
+ if (degrees == 0) return;
+ int w = getWidth();
+ int h = getHeight();
+ int cx = w / 2;
+ int cy = h / 2;
+ mCanvas.translate(cx, cy);
+ mCanvas.rotate(degrees, 0, 0, 1);
+ if (degrees % 180 != 0) {
+ mCanvas.translate(-cy, -cx);
+ } else {
+ mCanvas.translate(-cx, -cy);
+ }
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent event) {
+ if (!isEnabled()) return false;
+
+ int action = event.getAction();
+ if (action == MotionEvent.ACTION_CANCEL
+ || action == MotionEvent.ACTION_UP) {
+ mInDownState = false;
+ } else if (!mInDownState && action != MotionEvent.ACTION_DOWN) {
+ return false;
+ }
+
+ if (mCompensation != 0) {
+ event = MotionEventHelper.transformEvent(event, mCompensationMatrix);
+ }
+
+ mRenderLock.lock();
+ try {
+ // If this has been detached from root, we don't need to handle event
+ boolean handled = mContentView != null
+ && mContentView.dispatchTouchEvent(event);
+ if (action == MotionEvent.ACTION_DOWN && handled) {
+ mInDownState = true;
+ }
+ return handled;
+ } finally {
+ mRenderLock.unlock();
+ }
+ }
+
+ private class IdleRunner implements Runnable {
+ // true if the idle runner is in the queue
+ private boolean mActive = false;
+
+ @Override
+ public void run() {
+ OnGLIdleListener listener;
+ synchronized (mIdleListeners) {
+ mActive = false;
+ if (mIdleListeners.isEmpty()) return;
+ listener = mIdleListeners.removeFirst();
+ }
+ mRenderLock.lock();
+ boolean keepInQueue;
+ try {
+ keepInQueue = listener.onGLIdle(mCanvas, mRenderRequested);
+ } finally {
+ mRenderLock.unlock();
+ }
+ synchronized (mIdleListeners) {
+ if (keepInQueue) mIdleListeners.addLast(listener);
+ if (!mRenderRequested && !mIdleListeners.isEmpty()) enable();
+ }
+ }
+
+ public void enable() {
+ // Who gets the flag can add it to the queue
+ if (mActive) return;
+ mActive = true;
+ queueEvent(this);
+ }
+ }
+
+ @Override
+ public void lockRenderThread() {
+ mRenderLock.lock();
+ }
+
+ @Override
+ public void unlockRenderThread() {
+ mRenderLock.unlock();
+ }
+
+ @Override
+ public void onPause() {
+ unfreeze();
+ super.onPause();
+ }
+
+ @Override
+ public void setOrientationSource(OrientationSource source) {
+ mOrientationSource = source;
+ }
+
+ @Override
+ public int getDisplayRotation() {
+ return mDisplayRotation;
+ }
+
+ @Override
+ public int getCompensation() {
+ return mCompensation;
+ }
+
+ @Override
+ public Matrix getCompensationMatrix() {
+ return mCompensationMatrix;
+ }
+
+ @Override
+ public void freeze() {
+ mRenderLock.lock();
+ mFreeze = true;
+ mRenderLock.unlock();
+ }
+
+ @Override
+ public void unfreeze() {
+ mRenderLock.lock();
+ mFreeze = false;
+ mFreezeCondition.signalAll();
+ mRenderLock.unlock();
+ }
+
+ @Override
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ public void setLightsOutMode(boolean enabled) {
+ if (!ApiHelper.HAS_SET_SYSTEM_UI_VISIBILITY) return;
+
+ int flags = 0;
+ if (enabled) {
+ flags = STATUS_BAR_HIDDEN;
+ if (ApiHelper.HAS_VIEW_SYSTEM_UI_FLAG_LAYOUT_STABLE) {
+ flags |= (SYSTEM_UI_FLAG_FULLSCREEN | SYSTEM_UI_FLAG_LAYOUT_STABLE);
+ }
+ }
+ setSystemUiVisibility(flags);
+ }
+
+ // We need to unfreeze in the following methods and in onPause().
+ // These methods will wait on GLThread. If we have freezed the GLRootView,
+ // the GLThread will wait on main thread to call unfreeze and cause dead
+ // lock.
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
+ unfreeze();
+ super.surfaceChanged(holder, format, w, h);
+ }
+
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ unfreeze();
+ super.surfaceCreated(holder);
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ unfreeze();
+ super.surfaceDestroyed(holder);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ unfreeze();
+ super.onDetachedFromWindow();
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ unfreeze();
+ } finally {
+ super.finalize();
+ }
+ }
+}
diff --git a/src/com/hippo/ehviewer/gallery/ui/GLView.java b/src/com/hippo/ehviewer/gallery/ui/GLView.java
new file mode 100644
index 000000000..8c23bc95d
--- /dev/null
+++ b/src/com/hippo/ehviewer/gallery/ui/GLView.java
@@ -0,0 +1,447 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.hippo.ehviewer.gallery.ui;
+
+import android.annotation.SuppressLint;
+import android.graphics.Rect;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import com.hippo.ehviewer.gallery.anim.CanvasAnimation;
+import com.hippo.ehviewer.gallery.common.Utils;
+import com.hippo.ehviewer.gallery.glrenderer.GLCanvas;
+
+import java.util.ArrayList;
+
+// GLView is a UI component. It can render to a GLCanvas and accept touch
+// events. A GLView may have zero or more child GLView and they form a tree
+// structure. The rendering and event handling will pass through the tree
+// structure.
+//
+// A GLView tree should be attached to a GLRoot before event dispatching and
+// rendering happens. GLView asks GLRoot to re-render or re-layout the
+// GLView hierarchy using requestRender() and requestLayoutContentPane().
+//
+// The render() method is called in a separate thread. Before calling
+// dispatchTouchEvent() and layout(), GLRoot acquires a lock to avoid the
+// rendering thread running at the same time. If there are other entry points
+// from main thread (like a Handler) in your GLView, you need to call
+// lockRendering() if the rendering thread should not run at the same time.
+//
+public class GLView {
+ private static final String TAG = "GLView";
+
+ public static final int VISIBLE = 0;
+ public static final int INVISIBLE = 1;
+
+ private static final int FLAG_INVISIBLE = 1;
+ private static final int FLAG_SET_MEASURED_SIZE = 2;
+ private static final int FLAG_LAYOUT_REQUESTED = 4;
+
+ public interface OnClickListener {
+ void onClick(GLView v);
+ }
+
+ protected final Rect mBounds = new Rect();
+ protected final Rect mPaddings = new Rect();
+
+ private GLRoot mRoot;
+ protected GLView mParent;
+ private ArrayList mComponents;
+ private GLView mMotionTarget;
+
+ private CanvasAnimation mAnimation;
+
+ private int mViewFlags = 0;
+
+ protected int mMeasuredWidth = 0;
+ protected int mMeasuredHeight = 0;
+
+ private int mLastWidthSpec = -1;
+ private int mLastHeightSpec = -1;
+
+ protected int mScrollY = 0;
+ protected int mScrollX = 0;
+ protected int mScrollHeight = 0;
+ protected int mScrollWidth = 0;
+
+ private float [] mBackgroundColor;
+
+ public void startAnimation(CanvasAnimation animation) {
+ GLRoot root = getGLRoot();
+ if (root == null) throw new IllegalStateException();
+ mAnimation = animation;
+ if (mAnimation != null) {
+ mAnimation.start();
+ root.registerLaunchedAnimation(mAnimation);
+ }
+ invalidate();
+ }
+
+ // Sets the visiblity of this GLView (either GLView.VISIBLE or
+ // GLView.INVISIBLE).
+ public void setVisibility(int visibility) {
+ if (visibility == getVisibility()) return;
+ if (visibility == VISIBLE) {
+ mViewFlags &= ~FLAG_INVISIBLE;
+ } else {
+ mViewFlags |= FLAG_INVISIBLE;
+ }
+ onVisibilityChanged(visibility);
+ invalidate();
+ }
+
+ // Returns GLView.VISIBLE or GLView.INVISIBLE
+ public int getVisibility() {
+ return (mViewFlags & FLAG_INVISIBLE) == 0 ? VISIBLE : INVISIBLE;
+ }
+
+ // This should only be called on the content pane (the topmost GLView).
+ public void attachToRoot(GLRoot root) {
+ Utils.assertTrue(mParent == null && mRoot == null);
+ onAttachToRoot(root);
+ }
+
+ // This should only be called on the content pane (the topmost GLView).
+ public void detachFromRoot() {
+ Utils.assertTrue(mParent == null && mRoot != null);
+ onDetachFromRoot();
+ }
+
+ // Returns the number of children of the GLView.
+ public int getComponentCount() {
+ return mComponents == null ? 0 : mComponents.size();
+ }
+
+ // Returns the children for the given index.
+ public GLView getComponent(int index) {
+ if (mComponents == null) {
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ return mComponents.get(index);
+ }
+
+ // Adds a child to this GLView.
+ public void addComponent(GLView component) {
+ // Make sure the component doesn't have a parent currently.
+ if (component.mParent != null) throw new IllegalStateException();
+
+ // Build parent-child links
+ if (mComponents == null) {
+ mComponents = new ArrayList();
+ }
+ mComponents.add(component);
+ component.mParent = this;
+
+ // If this is added after we have a root, tell the component.
+ if (mRoot != null) {
+ component.onAttachToRoot(mRoot);
+ }
+ }
+
+ // Removes a child from this GLView.
+ public boolean removeComponent(GLView component) {
+ if (mComponents == null) return false;
+ if (mComponents.remove(component)) {
+ removeOneComponent(component);
+ return true;
+ }
+ return false;
+ }
+
+ // Removes all children of this GLView.
+ public void removeAllComponents() {
+ for (int i = 0, n = mComponents.size(); i < n; ++i) {
+ removeOneComponent(mComponents.get(i));
+ }
+ mComponents.clear();
+ }
+
+ private void removeOneComponent(GLView component) {
+ if (mMotionTarget == component) {
+ long now = SystemClock.uptimeMillis();
+ MotionEvent cancelEvent = MotionEvent.obtain(
+ now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);
+ dispatchTouchEvent(cancelEvent);
+ cancelEvent.recycle();
+ }
+ component.onDetachFromRoot();
+ component.mParent = null;
+ }
+
+ public Rect bounds() {
+ return mBounds;
+ }
+
+ public int getWidth() {
+ return mBounds.right - mBounds.left;
+ }
+
+ public int getHeight() {
+ return mBounds.bottom - mBounds.top;
+ }
+
+ public GLRoot getGLRoot() {
+ return mRoot;
+ }
+
+ // Request re-rendering of the view hierarchy.
+ // This is used for animation or when the contents changed.
+ public void invalidate() {
+ GLRoot root = getGLRoot();
+ if (root != null) root.requestRender();
+ }
+
+ // Request re-layout of the view hierarchy.
+ public void requestLayout() {
+ mViewFlags |= FLAG_LAYOUT_REQUESTED;
+ mLastHeightSpec = -1;
+ mLastWidthSpec = -1;
+ if (mParent != null) {
+ mParent.requestLayout();
+ } else {
+ // Is this a content pane ?
+ GLRoot root = getGLRoot();
+ if (root != null) root.requestLayoutContentPane();
+ }
+ }
+
+ protected void render(GLCanvas canvas) {
+ renderBackground(canvas);
+ canvas.save();
+ for (int i = 0, n = getComponentCount(); i < n; ++i) {
+ renderChild(canvas, getComponent(i));
+ }
+ canvas.restore();
+ }
+
+ public float [] getBackgroundColor() {
+ return mBackgroundColor;
+ }
+
+ public void setBackgroundColor(float [] color) {
+ mBackgroundColor = color;
+ }
+
+ protected void renderBackground(GLCanvas view) {
+ if (mBackgroundColor != null) {
+ view.clearBuffer(mBackgroundColor);
+ }
+ }
+
+ protected void renderChild(GLCanvas canvas, GLView component) {
+ if (component.getVisibility() != GLView.VISIBLE
+ && component.mAnimation == null) return;
+
+ int xoffset = component.mBounds.left - mScrollX;
+ int yoffset = component.mBounds.top - mScrollY;
+
+ canvas.translate(xoffset, yoffset);
+
+ CanvasAnimation anim = component.mAnimation;
+ if (anim != null) {
+ canvas.save(anim.getCanvasSaveFlags());
+ if (anim.calculate(AnimationTime.get())) {
+ invalidate();
+ } else {
+ component.mAnimation = null;
+ }
+ anim.apply(canvas);
+ }
+ component.render(canvas);
+ if (anim != null) canvas.restore();
+ canvas.translate(-xoffset, -yoffset);
+ }
+
+ protected boolean onTouch(MotionEvent event) {
+ return false;
+ }
+
+ protected boolean dispatchTouchEvent(MotionEvent event,
+ int x, int y, GLView component, boolean checkBounds) {
+ Rect rect = component.mBounds;
+ int left = rect.left;
+ int top = rect.top;
+ if (!checkBounds || rect.contains(x, y)) {
+ event.offsetLocation(-left, -top);
+ if (component.dispatchTouchEvent(event)) {
+ event.offsetLocation(left, top);
+ return true;
+ }
+ event.offsetLocation(left, top);
+ }
+ return false;
+ }
+
+ protected boolean dispatchTouchEvent(MotionEvent event) {
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+ int action = event.getAction();
+ if (mMotionTarget != null) {
+ if (action == MotionEvent.ACTION_DOWN) {
+ MotionEvent cancel = MotionEvent.obtain(event);
+ cancel.setAction(MotionEvent.ACTION_CANCEL);
+ dispatchTouchEvent(cancel, x, y, mMotionTarget, false);
+ mMotionTarget = null;
+ } else {
+ dispatchTouchEvent(event, x, y, mMotionTarget, false);
+ if (action == MotionEvent.ACTION_CANCEL
+ || action == MotionEvent.ACTION_UP) {
+ mMotionTarget = null;
+ }
+ return true;
+ }
+ }
+ if (action == MotionEvent.ACTION_DOWN) {
+ // in the reverse rendering order
+ for (int i = getComponentCount() - 1; i >= 0; --i) {
+ GLView component = getComponent(i);
+ if (component.getVisibility() != GLView.VISIBLE) continue;
+ if (dispatchTouchEvent(event, x, y, component, true)) {
+ mMotionTarget = component;
+ return true;
+ }
+ }
+ }
+ return onTouch(event);
+ }
+
+ public Rect getPaddings() {
+ return mPaddings;
+ }
+
+ @SuppressLint("WrongCall")
+ public void layout(int left, int top, int right, int bottom) {
+ boolean sizeChanged = setBounds(left, top, right, bottom);
+ mViewFlags &= ~FLAG_LAYOUT_REQUESTED;
+ // We call onLayout no matter sizeChanged is true or not because the
+ // orientation may change without changing the size of the View (for
+ // example, rotate the device by 180 degrees), and we want to handle
+ // orientation change in onLayout.
+ onLayout(sizeChanged, left, top, right, bottom);
+ }
+
+ private boolean setBounds(int left, int top, int right, int bottom) {
+ boolean sizeChanged = (right - left) != (mBounds.right - mBounds.left)
+ || (bottom - top) != (mBounds.bottom - mBounds.top);
+ mBounds.set(left, top, right, bottom);
+ return sizeChanged;
+ }
+
+ @SuppressLint("WrongCall")
+ public void measure(int widthSpec, int heightSpec) {
+ if (widthSpec == mLastWidthSpec && heightSpec == mLastHeightSpec
+ && (mViewFlags & FLAG_LAYOUT_REQUESTED) == 0) {
+ return;
+ }
+
+ mLastWidthSpec = widthSpec;
+ mLastHeightSpec = heightSpec;
+
+ mViewFlags &= ~FLAG_SET_MEASURED_SIZE;
+ onMeasure(widthSpec, heightSpec);
+ if ((mViewFlags & FLAG_SET_MEASURED_SIZE) == 0) {
+ throw new IllegalStateException(getClass().getName()
+ + " should call setMeasuredSize() in onMeasure()");
+ }
+ }
+
+ protected void onMeasure(int widthSpec, int heightSpec) {
+ }
+
+ protected void setMeasuredSize(int width, int height) {
+ mViewFlags |= FLAG_SET_MEASURED_SIZE;
+ mMeasuredWidth = width;
+ mMeasuredHeight = height;
+ }
+
+ public int getMeasuredWidth() {
+ return mMeasuredWidth;
+ }
+
+ public int getMeasuredHeight() {
+ return mMeasuredHeight;
+ }
+
+ protected void onLayout(
+ boolean changeSize, int left, int top, int right, int bottom) {
+ }
+
+ /**
+ * Gets the bounds of the given descendant that relative to this view.
+ */
+ public boolean getBoundsOf(GLView descendant, Rect out) {
+ int xoffset = 0;
+ int yoffset = 0;
+ GLView view = descendant;
+ while (view != this) {
+ if (view == null) return false;
+ Rect bounds = view.mBounds;
+ xoffset += bounds.left;
+ yoffset += bounds.top;
+ view = view.mParent;
+ }
+ out.set(xoffset, yoffset, xoffset + descendant.getWidth(),
+ yoffset + descendant.getHeight());
+ return true;
+ }
+
+ protected void onVisibilityChanged(int visibility) {
+ for (int i = 0, n = getComponentCount(); i < n; ++i) {
+ GLView child = getComponent(i);
+ if (child.getVisibility() == GLView.VISIBLE) {
+ child.onVisibilityChanged(visibility);
+ }
+ }
+ }
+
+ protected void onAttachToRoot(GLRoot root) {
+ mRoot = root;
+ for (int i = 0, n = getComponentCount(); i < n; ++i) {
+ getComponent(i).onAttachToRoot(root);
+ }
+ }
+
+ protected void onDetachFromRoot() {
+ for (int i = 0, n = getComponentCount(); i < n; ++i) {
+ getComponent(i).onDetachFromRoot();
+ }
+ mRoot = null;
+ }
+
+ public void lockRendering() {
+ if (mRoot != null) {
+ mRoot.lockRenderThread();
+ }
+ }
+
+ public void unlockRendering() {
+ if (mRoot != null) {
+ mRoot.unlockRenderThread();
+ }
+ }
+
+ // This is for debugging only.
+ // Dump the view hierarchy into log.
+ void dumpTree(String prefix) {
+ Log.d(TAG, prefix + getClass().getSimpleName());
+ for (int i = 0, n = getComponentCount(); i < n; ++i) {
+ getComponent(i).dumpTree(prefix + "....");
+ }
+ }
+}
diff --git a/src/com/hippo/ehviewer/gallery/ui/GestureRecognizer.java b/src/com/hippo/ehviewer/gallery/ui/GestureRecognizer.java
new file mode 100644
index 000000000..161bd097d
--- /dev/null
+++ b/src/com/hippo/ehviewer/gallery/ui/GestureRecognizer.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.hippo.ehviewer.gallery.ui;
+
+import android.content.Context;
+import android.os.SystemClock;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+
+// This class aggregates three gesture detectors: GestureDetector,
+// ScaleGestureDetector, and DownUpDetector.
+public class GestureRecognizer {
+ @SuppressWarnings("unused")
+ private static final String TAG = "GestureRecognizer";
+
+ public interface Listener {
+ boolean onSingleTapUp(float x, float y);
+ boolean onDoubleTap(float x, float y);
+ boolean onScrollBegin(float dx, float dy, float totalX, float totalY);
+ boolean onScroll(float dx, float dy, float totalX, float totalY);
+ boolean onScrollEnd();
+ boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
+ boolean onScaleBegin(float focusX, float focusY);
+ boolean onScale(float focusX, float focusY, float scale);
+ void onScaleEnd();
+ void onDown(float x, float y);
+ void onUp();
+ }
+
+ private final GestureDetector mGestureDetector;
+ private final ScaleGestureDetector mScaleDetector;
+ private final DownUpDetector mDownUpDetector;
+ private final Listener mListener;
+
+ private boolean mStillScroll = false;
+
+ public GestureRecognizer(Context context, Listener listener) {
+ mListener = listener;
+ mGestureDetector = new GestureDetector(context, new MyGestureListener(),
+ null, true /* ignoreMultitouch */);
+ mScaleDetector = new ScaleGestureDetector(
+ context, new MyScaleListener());
+ mDownUpDetector = new DownUpDetector(new MyDownUpListener());
+ }
+
+ public void onTouchEvent(MotionEvent event) {
+ mGestureDetector.onTouchEvent(event);
+ mScaleDetector.onTouchEvent(event);
+ mDownUpDetector.onTouchEvent(event);
+ }
+
+ public boolean isDown() {
+ return mDownUpDetector.isDown();
+ }
+
+ public void cancelScale() {
+ long now = SystemClock.uptimeMillis();
+ MotionEvent cancelEvent = MotionEvent.obtain(
+ now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);
+ mScaleDetector.onTouchEvent(cancelEvent);
+ cancelEvent.recycle();
+ }
+
+ private class MyGestureListener
+ extends GestureDetector.SimpleOnGestureListener {
+ float firstX = 0;
+ float firstY = 0;
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ return mListener.onSingleTapUp(e.getX(), e.getY());
+ }
+
+ @Override
+ public boolean onDoubleTap(MotionEvent e) {
+ return mListener.onDoubleTap(e.getX(), e.getY());
+ }
+
+ @Override
+ public boolean onScroll(
+ MotionEvent e1, MotionEvent e2, float dx, float dy) {
+ if (e1.getX() != firstX || e1.getY() != firstY) {
+ mStillScroll = true;
+ firstX = e1.getX();
+ firstY = e1.getY();
+ return mListener.onScrollBegin(
+ dx, dy, e2.getX() - e1.getX(), e2.getY() - e1.getY());
+ }
+ else
+ return mListener.onScroll(
+ dx, dy, e2.getX() - e1.getX(), e2.getY() - e1.getY());
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
+ float velocityY) {
+ return mListener.onFling(e1, e2, velocityX, velocityY);
+ }
+ }
+
+ private class MyScaleListener
+ extends ScaleGestureDetector.SimpleOnScaleGestureListener {
+ @Override
+ public boolean onScaleBegin(ScaleGestureDetector detector) {
+ return mListener.onScaleBegin(
+ detector.getFocusX(), detector.getFocusY());
+ }
+
+ @Override
+ public boolean onScale(ScaleGestureDetector detector) {
+ return mListener.onScale(detector.getFocusX(),
+ detector.getFocusY(), detector.getScaleFactor());
+ }
+
+ @Override
+ public void onScaleEnd(ScaleGestureDetector detector) {
+ mListener.onScaleEnd();
+ }
+ }
+
+ private class MyDownUpListener implements DownUpDetector.DownUpListener {
+ @Override
+ public void onDown(MotionEvent e) {
+ mListener.onDown(e.getX(), e.getY());
+ }
+
+ @Override
+ public void onUp(MotionEvent e) {
+ mListener.onUp();
+ if (mStillScroll) {
+ mStillScroll = false;
+ mListener.onScrollEnd();
+ }
+ }
+ }
+}
diff --git a/src/com/hippo/ehviewer/gallery/ui/OrientationSource.java b/src/com/hippo/ehviewer/gallery/ui/OrientationSource.java
new file mode 100644
index 000000000..b4825a7bf
--- /dev/null
+++ b/src/com/hippo/ehviewer/gallery/ui/OrientationSource.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.hippo.ehviewer.gallery.ui;
+
+public interface OrientationSource {
+ public int getDisplayRotation();
+ public int getCompensation();
+}
diff --git a/src/com/hippo/ehviewer/gallery/util/GalleryUtils.java b/src/com/hippo/ehviewer/gallery/util/GalleryUtils.java
new file mode 100644
index 000000000..6e580a42a
--- /dev/null
+++ b/src/com/hippo/ehviewer/gallery/util/GalleryUtils.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.hippo.ehviewer.gallery.util;
+
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Color;
+import android.net.Uri;
+import android.os.ConditionVariable;
+import android.os.Environment;
+import android.os.StatFs;
+import android.provider.MediaStore;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.WindowManager;
+
+import com.hippo.ehviewer.util.ThreadPool.CancelListener;
+import com.hippo.ehviewer.util.ThreadPool.JobContext;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.Locale;
+
+public class GalleryUtils {
+ private static final String TAG = "GalleryUtils";
+ private static final String MAPS_PACKAGE_NAME = "com.google.android.apps.maps";
+ private static final String MAPS_CLASS_NAME = "com.google.android.maps.MapsActivity";
+
+ public static final String MIME_TYPE_IMAGE = "image/*";
+ public static final String MIME_TYPE_VIDEO = "video/*";
+ public static final String MIME_TYPE_PANORAMA360 = "application/vnd.google.panorama360+jpg";
+ public static final String MIME_TYPE_ALL = "*/*";
+
+ private static float sPixelDensity = -1f;
+
+ public static void initialize(Context context) {
+ DisplayMetrics metrics = new DisplayMetrics();
+ WindowManager wm = (WindowManager)
+ context.getSystemService(Context.WINDOW_SERVICE);
+ wm.getDefaultDisplay().getMetrics(metrics);
+ sPixelDensity = metrics.density;
+ }
+
+ public static float[] intColorToFloatARGBArray(int from) {
+ return new float[] {
+ Color.alpha(from) / 255f,
+ Color.red(from) / 255f,
+ Color.green(from) / 255f,
+ Color.blue(from) / 255f
+ };
+ }
+
+ public static float dpToPixel(float dp) {
+ return sPixelDensity * dp;
+ }
+
+ public static int dpToPixel(int dp) {
+ return Math.round(dpToPixel((float) dp));
+ }
+
+ public static int meterToPixel(float meter) {
+ // 1 meter = 39.37 inches, 1 inch = 160 dp.
+ return Math.round(dpToPixel(meter * 39.37f * 160));
+ }
+
+ public static byte[] getBytes(String in) {
+ byte[] result = new byte[in.length() * 2];
+ int output = 0;
+ for (char ch : in.toCharArray()) {
+ result[output++] = (byte) (ch & 0xFF);
+ result[output++] = (byte) (ch >> 8);
+ }
+ return result;
+ }
+
+ // Below are used the detect using database in the render thread. It only
+ // works most of the time, but that's ok because it's for debugging only.
+
+ private static volatile Thread sCurrentThread;
+ private static volatile boolean sWarned;
+
+ public static void setRenderThread() {
+ sCurrentThread = Thread.currentThread();
+ }
+
+ public static void assertNotInRenderThread() {
+ if (!sWarned) {
+ if (Thread.currentThread() == sCurrentThread) {
+ sWarned = true;
+ Log.w(TAG, new Throwable("Should not do this in render thread"));
+ }
+ }
+ }
+
+ private static final double RAD_PER_DEG = Math.PI / 180.0;
+ private static final double EARTH_RADIUS_METERS = 6367000.0;
+
+ public static double fastDistanceMeters(double latRad1, double lngRad1,
+ double latRad2, double lngRad2) {
+ if ((Math.abs(latRad1 - latRad2) > RAD_PER_DEG)
+ || (Math.abs(lngRad1 - lngRad2) > RAD_PER_DEG)) {
+ return accurateDistanceMeters(latRad1, lngRad1, latRad2, lngRad2);
+ }
+ // Approximate sin(x) = x.
+ double sineLat = (latRad1 - latRad2);
+
+ // Approximate sin(x) = x.
+ double sineLng = (lngRad1 - lngRad2);
+
+ // Approximate cos(lat1) * cos(lat2) using
+ // cos((lat1 + lat2)/2) ^ 2
+ double cosTerms = Math.cos((latRad1 + latRad2) / 2.0);
+ cosTerms = cosTerms * cosTerms;
+ double trigTerm = sineLat * sineLat + cosTerms * sineLng * sineLng;
+ trigTerm = Math.sqrt(trigTerm);
+
+ // Approximate arcsin(x) = x
+ return EARTH_RADIUS_METERS * trigTerm;
+ }
+
+ public static double accurateDistanceMeters(double lat1, double lng1,
+ double lat2, double lng2) {
+ double dlat = Math.sin(0.5 * (lat2 - lat1));
+ double dlng = Math.sin(0.5 * (lng2 - lng1));
+ double x = dlat * dlat + dlng * dlng * Math.cos(lat1) * Math.cos(lat2);
+ return (2 * Math.atan2(Math.sqrt(x), Math.sqrt(Math.max(0.0,
+ 1.0 - x)))) * EARTH_RADIUS_METERS;
+ }
+
+
+ public static final double toMile(double meter) {
+ return meter / 1609;
+ }
+
+ // For debugging, it will block the caller for timeout millis.
+ public static void fakeBusy(JobContext jc, int timeout) {
+ final ConditionVariable cv = new ConditionVariable();
+ jc.setCancelListener(new CancelListener() {
+ @Override
+ public void onCancel() {
+ cv.open();
+ }
+ });
+ cv.block(timeout);
+ jc.setCancelListener(null);
+ }
+
+ public static void startCameraActivity(Context context) {
+ Intent intent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA)
+ .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP
+ | Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(intent);
+ }
+
+ public static String formatLatitudeLongitude(String format, double latitude,
+ double longitude) {
+ // We need to specify the locale otherwise it may go wrong in some language
+ // (e.g. Locale.FRENCH)
+ return String.format(Locale.ENGLISH, format, latitude, longitude);
+ }
+
+ public static void showOnMap(Context context, double latitude, double longitude) {
+ try {
+ // We don't use "geo:latitude,longitude" because it only centers
+ // the MapView to the specified location, but we need a marker
+ // for further operations (routing to/from).
+ // The q=(lat, lng) syntax is suggested by geo-team.
+ String uri = formatLatitudeLongitude("http://maps.google.com/maps?f=q&q=(%f,%f)",
+ latitude, longitude);
+ ComponentName compName = new ComponentName(MAPS_PACKAGE_NAME,
+ MAPS_CLASS_NAME);
+ Intent mapsIntent = new Intent(Intent.ACTION_VIEW,
+ Uri.parse(uri)).setComponent(compName);
+ context.startActivity(mapsIntent);
+ } catch (ActivityNotFoundException e) {
+ // Use the "geo intent" if no GMM is installed
+ Log.e(TAG, "GMM activity not found!", e);
+ String url = formatLatitudeLongitude("geo:%f,%f", latitude, longitude);
+ Intent mapsIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+ context.startActivity(mapsIntent);
+ }
+ }
+
+ public static void setViewPointMatrix(
+ float matrix[], float x, float y, float z) {
+ // The matrix is
+ // -z, 0, x, 0
+ // 0, -z, y, 0
+ // 0, 0, 1, 0
+ // 0, 0, 1, -z
+ Arrays.fill(matrix, 0, 16, 0);
+ matrix[0] = matrix[5] = matrix[15] = -z;
+ matrix[8] = x;
+ matrix[9] = y;
+ matrix[10] = matrix[11] = 1;
+ }
+
+ public static int getBucketId(String path) {
+ return path.toLowerCase().hashCode();
+ }
+
+ // Return the local path that matches the given bucketId. If no match is
+ // found, return null
+ public static String searchDirForPath(File dir, int bucketId) {
+ File[] files = dir.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ if (file.isDirectory()) {
+ String path = file.getAbsolutePath();
+ if (GalleryUtils.getBucketId(path) == bucketId) {
+ return path;
+ } else {
+ path = searchDirForPath(file, bucketId);
+ if (path != null) return path;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ public static boolean hasSpaceForSize(long size) {
+ String state = Environment.getExternalStorageState();
+ if (!Environment.MEDIA_MOUNTED.equals(state)) {
+ return false;
+ }
+
+ String path = Environment.getExternalStorageDirectory().getPath();
+ try {
+ StatFs stat = new StatFs(path);
+ return stat.getAvailableBlocks() * (long) stat.getBlockSize() > size;
+ } catch (Exception e) {
+ Log.i(TAG, "Fail to access external storage", e);
+ }
+ return false;
+ }
+}
diff --git a/src/com/hippo/ehviewer/gallery/util/MotionEventHelper.java b/src/com/hippo/ehviewer/gallery/util/MotionEventHelper.java
new file mode 100644
index 000000000..84d4afd6e
--- /dev/null
+++ b/src/com/hippo/ehviewer/gallery/util/MotionEventHelper.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.hippo.ehviewer.gallery.util;
+
+import android.annotation.TargetApi;
+import android.graphics.Matrix;
+import android.util.FloatMath;
+import android.view.MotionEvent;
+import android.view.MotionEvent.PointerCoords;
+
+import com.hippo.ehviewer.gallery.common.ApiHelper;
+
+public final class MotionEventHelper {
+ private MotionEventHelper() {}
+
+ public static MotionEvent transformEvent(MotionEvent e, Matrix m) {
+ // We try to use the new transform method if possible because it uses
+ // less memory.
+ if (ApiHelper.HAS_MOTION_EVENT_TRANSFORM) {
+ return transformEventNew(e, m);
+ } else {
+ return transformEventOld(e, m);
+ }
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+ private static MotionEvent transformEventNew(MotionEvent e, Matrix m) {
+ MotionEvent newEvent = MotionEvent.obtain(e);
+ newEvent.transform(m);
+ return newEvent;
+ }
+
+ // This is copied from Input.cpp in the android framework.
+ private static MotionEvent transformEventOld(MotionEvent e, Matrix m) {
+ long downTime = e.getDownTime();
+ long eventTime = e.getEventTime();
+ int action = e.getAction();
+ int pointerCount = e.getPointerCount();
+ int[] pointerIds = getPointerIds(e);
+ PointerCoords[] pointerCoords = getPointerCoords(e);
+ int metaState = e.getMetaState();
+ float xPrecision = e.getXPrecision();
+ float yPrecision = e.getYPrecision();
+ int deviceId = e.getDeviceId();
+ int edgeFlags = e.getEdgeFlags();
+ int source = e.getSource();
+ int flags = e.getFlags();
+
+ // Copy the x and y coordinates into an array, map them, and copy back.
+ float[] xy = new float[pointerCoords.length * 2];
+ for (int i = 0; i < pointerCount;i++) {
+ xy[2 * i] = pointerCoords[i].x;
+ xy[2 * i + 1] = pointerCoords[i].y;
+ }
+ m.mapPoints(xy);
+ for (int i = 0; i < pointerCount;i++) {
+ pointerCoords[i].x = xy[2 * i];
+ pointerCoords[i].y = xy[2 * i + 1];
+ pointerCoords[i].orientation = transformAngle(
+ m, pointerCoords[i].orientation);
+ }
+
+ MotionEvent n = MotionEvent.obtain(downTime, eventTime, action,
+ pointerCount, pointerIds, pointerCoords, metaState, xPrecision,
+ yPrecision, deviceId, edgeFlags, source, flags);
+
+ return n;
+ }
+
+ private static int[] getPointerIds(MotionEvent e) {
+ int n = e.getPointerCount();
+ int[] r = new int[n];
+ for (int i = 0; i < n; i++) {
+ r[i] = e.getPointerId(i);
+ }
+ return r;
+ }
+
+ private static PointerCoords[] getPointerCoords(MotionEvent e) {
+ int n = e.getPointerCount();
+ PointerCoords[] r = new PointerCoords[n];
+ for (int i = 0; i < n; i++) {
+ r[i] = new PointerCoords();
+ e.getPointerCoords(i, r[i]);
+ }
+ return r;
+ }
+
+ private static float transformAngle(Matrix m, float angleRadians) {
+ // Construct and transform a vector oriented at the specified clockwise
+ // angle from vertical. Coordinate system: down is increasing Y, right is
+ // increasing X.
+ float[] v = new float[2];
+ v[0] = FloatMath.sin(angleRadians);
+ v[1] = -FloatMath.cos(angleRadians);
+ m.mapVectors(v);
+
+ // Derive the transformed vector's clockwise angle from vertical.
+ float result = (float) Math.atan2(v[0], -v[1]);
+ if (result < -Math.PI / 2) {
+ result += Math.PI;
+ } else if (result > Math.PI / 2) {
+ result -= Math.PI;
+ }
+ return result;
+ }
+}
diff --git a/src/com/hippo/ehviewer/util/Future.java b/src/com/hippo/ehviewer/util/Future.java
new file mode 100644
index 000000000..c0d565634
--- /dev/null
+++ b/src/com/hippo/ehviewer/util/Future.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.hippo.ehviewer.util;
+
+// This Future differs from the java.util.concurrent.Future in these aspects:
+//
+// - Once cancel() is called, isCancelled() always returns true. It is a sticky
+// flag used to communicate to the implementation. The implmentation may
+// ignore that flag. Regardless whether the Future is cancelled, a return
+// value will be provided to get(). The implementation may choose to return
+// null if it finds the Future is cancelled.
+//
+// - get() does not throw exceptions.
+//
+public interface Future {
+ public void cancel();
+ public boolean isCancelled();
+ public boolean isDone();
+ public T get();
+ public void waitDone();
+}
diff --git a/src/com/hippo/ehviewer/util/FutureListener.java b/src/com/hippo/ehviewer/util/FutureListener.java
new file mode 100644
index 000000000..280f64451
--- /dev/null
+++ b/src/com/hippo/ehviewer/util/FutureListener.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.hippo.ehviewer.util;
+
+public interface FutureListener {
+ public void onFutureDone(Future future);
+}
diff --git a/src/com/hippo/ehviewer/util/IntArray.java b/src/com/hippo/ehviewer/util/IntArray.java
new file mode 100644
index 000000000..ef32a7cad
--- /dev/null
+++ b/src/com/hippo/ehviewer/util/IntArray.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.hippo.ehviewer.util;
+
+public class IntArray {
+ private static final int INIT_CAPACITY = 8;
+
+ private int mData[] = new int[INIT_CAPACITY];
+ private int mSize = 0;
+
+ public void add(int value) {
+ if (mData.length == mSize) {
+ int temp[] = new int[mSize + mSize];
+ System.arraycopy(mData, 0, temp, 0, mSize);
+ mData = temp;
+ }
+ mData[mSize++] = value;
+ }
+
+ public int removeLast() {
+ mSize--;
+ return mData[mSize];
+ }
+
+ public int size() {
+ return mSize;
+ }
+
+ // For testing only
+ public int[] toArray(int[] result) {
+ if (result == null || result.length < mSize) {
+ result = new int[mSize];
+ }
+ System.arraycopy(mData, 0, result, 0, mSize);
+ return result;
+ }
+
+ public int[] getInternalArray() {
+ return mData;
+ }
+
+ public void clear() {
+ mSize = 0;
+ if (mData.length != INIT_CAPACITY) mData = new int[INIT_CAPACITY];
+ }
+}
diff --git a/src/com/hippo/ehviewer/util/PriorityThreadFactory.java b/src/com/hippo/ehviewer/util/PriorityThreadFactory.java
new file mode 100644
index 000000000..877e8f1bf
--- /dev/null
+++ b/src/com/hippo/ehviewer/util/PriorityThreadFactory.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.hippo.ehviewer.util;
+
+
+import android.os.Process;
+
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * A thread factory that creates threads with a given thread priority.
+ */
+public class PriorityThreadFactory implements ThreadFactory {
+
+ private final int mPriority;
+ private final AtomicInteger mNumber = new AtomicInteger();
+ private final String mName;
+
+ public PriorityThreadFactory(String name, int priority) {
+ mName = name;
+ mPriority = priority;
+ }
+
+ @Override
+ public Thread newThread(Runnable r) {
+ return new Thread(r, mName + '-' + mNumber.getAndIncrement()) {
+ @Override
+ public void run() {
+ Process.setThreadPriority(mPriority);
+ super.run();
+ }
+ };
+ }
+
+}
diff --git a/src/com/hippo/ehviewer/util/ThreadPool.java b/src/com/hippo/ehviewer/util/ThreadPool.java
new file mode 100644
index 000000000..cd18591e5
--- /dev/null
+++ b/src/com/hippo/ehviewer/util/ThreadPool.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.hippo.ehviewer.util;
+
+import android.util.Log;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+public class ThreadPool {
+ @SuppressWarnings("unused")
+ private static final String TAG = "ThreadPool";
+ private static final int CORE_POOL_SIZE = 4;
+ private static final int MAX_POOL_SIZE = 8;
+ private static final int KEEP_ALIVE_TIME = 10; // 10 seconds
+
+ // Resource type
+ public static final int MODE_NONE = 0;
+ public static final int MODE_CPU = 1;
+ public static final int MODE_NETWORK = 2;
+
+ public static final JobContext JOB_CONTEXT_STUB = new JobContextStub();
+
+ ResourceCounter mCpuCounter = new ResourceCounter(2);
+ ResourceCounter mNetworkCounter = new ResourceCounter(2);
+
+ // A Job is like a Callable, but it has an addition JobContext parameter.
+ public interface Job {
+ public T run(JobContext jc);
+ }
+
+ public interface JobContext {
+ boolean isCancelled();
+ void setCancelListener(CancelListener listener);
+ boolean setMode(int mode);
+ }
+
+ private static class JobContextStub implements JobContext {
+ @Override
+ public boolean isCancelled() {
+ return false;
+ }
+
+ @Override
+ public void setCancelListener(CancelListener listener) {
+ }
+
+ @Override
+ public boolean setMode(int mode) {
+ return true;
+ }
+ }
+
+ public interface CancelListener {
+ public void onCancel();
+ }
+
+ private static class ResourceCounter {
+ public int value;
+ public ResourceCounter(int v) {
+ value = v;
+ }
+ }
+
+ private final Executor mExecutor;
+
+ public ThreadPool() {
+ this(CORE_POOL_SIZE, MAX_POOL_SIZE);
+ }
+
+ public ThreadPool(int initPoolSize, int maxPoolSize) {
+ mExecutor = new ThreadPoolExecutor(
+ initPoolSize, maxPoolSize, KEEP_ALIVE_TIME,
+ TimeUnit.SECONDS, new LinkedBlockingQueue(),
+ new PriorityThreadFactory("thread-pool",
+ android.os.Process.THREAD_PRIORITY_BACKGROUND));
+ }
+
+ // Submit a job to the thread pool. The listener will be called when the
+ // job is finished (or cancelled).
+ public Future submit(Job job, FutureListener listener) {
+ Worker w = new Worker(job, listener);
+ mExecutor.execute(w);
+ return w;
+ }
+
+ public Future submit(Job job) {
+ return submit(job, null);
+ }
+
+ private class Worker implements Runnable, Future, JobContext {
+ @SuppressWarnings("hiding")
+ private static final String TAG = "Worker";
+ private Job mJob;
+ private FutureListener mListener;
+ private CancelListener mCancelListener;
+ private ResourceCounter mWaitOnResource;
+ private volatile boolean mIsCancelled;
+ private boolean mIsDone;
+ private T mResult;
+ private int mMode;
+
+ public Worker(Job job, FutureListener listener) {
+ mJob = job;
+ mListener = listener;
+ }
+
+ // This is called by a thread in the thread pool.
+ @Override
+ public void run() {
+ T result = null;
+
+ // A job is in CPU mode by default. setMode returns false
+ // if the job is cancelled.
+ if (setMode(MODE_CPU)) {
+ try {
+ result = mJob.run(this);
+ } catch (Throwable ex) {
+ Log.w(TAG, "Exception in running a job", ex);
+ }
+ }
+
+ synchronized(this) {
+ setMode(MODE_NONE);
+ mResult = result;
+ mIsDone = true;
+ notifyAll();
+ }
+ if (mListener != null) mListener.onFutureDone(this);
+ }
+
+ // Below are the methods for Future.
+ @Override
+ public synchronized void cancel() {
+ if (mIsCancelled) return;
+ mIsCancelled = true;
+ if (mWaitOnResource != null) {
+ synchronized (mWaitOnResource) {
+ mWaitOnResource.notifyAll();
+ }
+ }
+ if (mCancelListener != null) {
+ mCancelListener.onCancel();
+ }
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return mIsCancelled;
+ }
+
+ @Override
+ public synchronized boolean isDone() {
+ return mIsDone;
+ }
+
+ @Override
+ public synchronized T get() {
+ while (!mIsDone) {
+ try {
+ wait();
+ } catch (Exception ex) {
+ Log.w(TAG, "ingore exception", ex);
+ // ignore.
+ }
+ }
+ return mResult;
+ }
+
+ @Override
+ public void waitDone() {
+ get();
+ }
+
+ // Below are the methods for JobContext (only called from the
+ // thread running the job)
+ @Override
+ public synchronized void setCancelListener(CancelListener listener) {
+ mCancelListener = listener;
+ if (mIsCancelled && mCancelListener != null) {
+ mCancelListener.onCancel();
+ }
+ }
+
+ @Override
+ public boolean setMode(int mode) {
+ // Release old resource
+ ResourceCounter rc = modeToCounter(mMode);
+ if (rc != null) releaseResource(rc);
+ mMode = MODE_NONE;
+
+ // Acquire new resource
+ rc = modeToCounter(mode);
+ if (rc != null) {
+ if (!acquireResource(rc)) {
+ return false;
+ }
+ mMode = mode;
+ }
+
+ return true;
+ }
+
+ private ResourceCounter modeToCounter(int mode) {
+ if (mode == MODE_CPU) {
+ return mCpuCounter;
+ } else if (mode == MODE_NETWORK) {
+ return mNetworkCounter;
+ } else {
+ return null;
+ }
+ }
+
+ private boolean acquireResource(ResourceCounter counter) {
+ while (true) {
+ synchronized (this) {
+ if (mIsCancelled) {
+ mWaitOnResource = null;
+ return false;
+ }
+ mWaitOnResource = counter;
+ }
+
+ synchronized (counter) {
+ if (counter.value > 0) {
+ counter.value--;
+ break;
+ } else {
+ try {
+ counter.wait();
+ } catch (InterruptedException ex) {
+ // ignore.
+ }
+ }
+ }
+ }
+
+ synchronized (this) {
+ mWaitOnResource = null;
+ }
+
+ return true;
+ }
+
+ private void releaseResource(ResourceCounter counter) {
+ synchronized (counter) {
+ counter.value++;
+ counter.notifyAll();
+ }
+ }
+ }
+}
diff --git a/src/com/hippo/ehviewer/util/TimeRunner.java b/src/com/hippo/ehviewer/util/TimeRunner.java
new file mode 100644
index 000000000..1535d2a3f
--- /dev/null
+++ b/src/com/hippo/ehviewer/util/TimeRunner.java
@@ -0,0 +1,119 @@
+package com.hippo.ehviewer.util;
+
+import android.annotation.SuppressLint;
+import android.os.Handler;
+import android.os.Message;
+
+public abstract class TimeRunner {
+
+ private static final int INTERVAL = 1;
+
+ private static final int RUNNING = 0;
+ private static final int START = 1;
+ private static final int END = 2;
+
+ private boolean mRunInThread = true;
+ private boolean mListenerInThread = true;
+
+ private OnTimeListener mListener;
+
+ public interface OnTimeListener {
+ public void onStart();
+ public void onEnd();
+ }
+
+ // TODO
+ @SuppressLint("HandlerLeak")
+ private Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case RUNNING:
+ run((Float)msg.obj, msg.arg1);
+ break;
+ case START:
+ mListener.onStart();
+ break;
+ case END:
+ mListener.onEnd();
+ break;
+ }
+ }
+ };
+
+ private int mDuration = 0;
+ private int mDelay = 0;
+
+ public void setOnTimerListener(OnTimeListener listener) {
+ mListener = listener;
+ }
+
+ public void setDuration(int duration) {
+ mDuration = duration;
+ }
+
+ public void setRunInThread(boolean runInThread) {
+ mRunInThread = runInThread;
+ }
+
+ public void setListenerInThread(boolean listenerInThread) {
+ mListenerInThread = listenerInThread;
+ }
+
+ public void start() {
+ start(0);
+ }
+
+ public void start(int delay) {
+ mDelay = delay;
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ Thread.sleep(mDelay);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ if (mListener != null) {
+ if (mListenerInThread) {
+ mListener.onStart();
+ } else {
+ Message msg = new Message();
+ msg.what = START;
+ mHandler.sendMessage(msg);
+ }
+ }
+
+ long startTime = System.currentTimeMillis();
+ for (int runningTime = 0; runningTime < mDuration; runningTime = (int)(System.currentTimeMillis() - startTime)) {
+ if (mRunInThread) {
+ TimeRunner.this.run(runningTime/(float)mDuration, runningTime);
+ } else {
+ Message msg = new Message();
+ msg.what = RUNNING;
+ msg.obj = runningTime/(float)mDuration;
+ msg.arg1 = runningTime;
+ mHandler.sendMessage(msg);
+ }
+ try {
+ Thread.sleep(INTERVAL);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ if (mListener != null) {
+ if (mListenerInThread) {
+ mListener.onEnd();
+ } else {
+ Message msg = new Message();
+ msg.what = END;
+ mHandler.sendMessage(msg);
+ }
+ }
+ }
+ }).start();
+ }
+
+ protected abstract void run(float interpolatedTime, int runningTime);
+}