repeatCount is INFINITE
+ * or a positive value, the animation restarts from the beginning.
+ */
+ public static final int RESTART = 1;
+ /**
+ * When the animation reaches the end and repeatCount is INFINITE
+ * or a positive value, the animation reverses direction on every iteration.
+ */
+ public static final int REVERSE = 2;
+ /**
+ * This value used used with the {@link #setRepeatCount(int)} property to repeat
+ * the animation indefinitely.
+ */
+ public static final int INFINITE = -1;
+
+ private long mStartTime = NO_ANIMATION;
+ private long mDuration;
+ private Interpolator mInterpolator;
+ private int mRepeatCount;
+ private int mRepeatMode;
+
+ private int mRunnedCount;
+ private long mLastFrameTime;
+
+ public void setInterpolator(Interpolator interpolator) {
+ mInterpolator = interpolator;
+ }
+
+ public void setDuration(long duration) {
+ mDuration = duration;
+ }
+
+ public void setRepeatCount(int repeatCount) {
+ mRepeatCount = repeatCount;
+ }
+
+ public void setRepeatMode(int repeatMode) {
+ mRepeatMode = repeatMode;
+ }
+
+ public boolean isRunning() {
+ return mStartTime > 0;
+ }
+
+ public long getLastFrameTime() {
+ return mLastFrameTime;
+ }
+
+ public void start() {
+ if (mStartTime == NO_ANIMATION) {
+ mStartTime = ANIMATION_START;
+ mRunnedCount = 0;
+ mLastFrameTime = 0;
+ }
+ }
+
+ public void startAt(long time) {
+ start();
+ setStartTime(time);
+ }
+
+ public void startNow() {
+ start();
+ setStartTime(SystemClock.uptimeMillis());
+ }
+
+ public void setStartTime(long time) {
+ mStartTime = time;
+ }
+
+ public boolean isActive() {
+ return mStartTime != NO_ANIMATION;
+ }
+
+ public void forceStop() {
+ mStartTime = NO_ANIMATION;
+ }
+
+ public void forceReset() {
+ mStartTime = ANIMATION_START;
+ mRunnedCount = 0;
+ mLastFrameTime = 0;
+ }
+
+ // TODO mRepeatMode
+ public boolean calculate(long currentTimeMillis) {
+ if (mStartTime == NO_ANIMATION) {
+ return false;
+ }
+ if (mStartTime == ANIMATION_START) {
+ mStartTime = currentTimeMillis;
+ }
+ mLastFrameTime = currentTimeMillis;
+
+ long elapse = currentTimeMillis - mStartTime;
+ float x = MathUtils.clamp(mDuration == 0.0f ? 1.0f : (float) elapse / mDuration, 0.0f, 1.0f); // Avoid NaN
+ Interpolator i = mInterpolator;
+ onCalculate(i != null ? i.getInterpolation(x) : x);
+
+ if (elapse >= mDuration) {
+ mRunnedCount++;
+ if (mRunnedCount >= mRepeatCount && mRepeatCount != INFINITE) {
+ mStartTime = NO_ANIMATION;
+ } else {
+ mStartTime += elapse;
+ }
+ }
+
+ return mStartTime != NO_ANIMATION;
+ }
+
+ abstract protected void onCalculate(float progress);
+}
diff --git a/app/src/main/java/com/hippo/gl/anim/CanvasAnimation.java b/app/src/main/java/com/hippo/gl/anim/CanvasAnimation.java
new file mode 100644
index 000000000..74f1b4570
--- /dev/null
+++ b/app/src/main/java/com/hippo/gl/anim/CanvasAnimation.java
@@ -0,0 +1,25 @@
+/*
+ * 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.gl.anim;
+
+import com.hippo.gl.glrenderer.GLCanvas;
+
+public abstract class CanvasAnimation extends Animation {
+
+ public abstract int getCanvasSaveFlags();
+ public abstract void apply(GLCanvas canvas);
+}
diff --git a/app/src/main/java/com/hippo/gl/anim/FloatAnimation.java b/app/src/main/java/com/hippo/gl/anim/FloatAnimation.java
new file mode 100644
index 000000000..c2908e09b
--- /dev/null
+++ b/app/src/main/java/com/hippo/gl/anim/FloatAnimation.java
@@ -0,0 +1,40 @@
+/*
+ * 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.gl.anim;
+
+import com.hippo.yorozuya.MathUtils;
+
+public class FloatAnimation extends Animation {
+
+ private float mFrom;
+ private float mTo;
+ private float mCurrent;
+
+ public void setRange(float from, float to) {
+ mFrom = from;
+ mTo = to;
+ }
+
+ @Override
+ protected void onCalculate(float progress) {
+ mCurrent = MathUtils.lerp(mFrom, mTo, progress);
+ }
+
+ public float get() {
+ return mCurrent;
+ }
+}
diff --git a/app/src/main/java/com/hippo/gl/glrenderer/BasicTexture.java b/app/src/main/java/com/hippo/gl/glrenderer/BasicTexture.java
new file mode 100644
index 000000000..9cd409df8
--- /dev/null
+++ b/app/src/main/java/com/hippo/gl/glrenderer/BasicTexture.java
@@ -0,0 +1,222 @@
+/*
+ * 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.gl.glrenderer;
+
+import android.graphics.RectF;
+import android.util.Log;
+
+import com.hippo.yorozuya.MathUtils;
+
+import java.util.WeakHashMap;
+
+// BasicTexture is a Texture corresponds to a real GL texture.
+// The state of a BasicTexture indicates whether its data is loaded to GL memory.
+// If a BasicTexture is loaded into GL memory, it has a GL texture id.
+public abstract class BasicTexture implements Texture {
+
+ @SuppressWarnings("unused")
+ private static final String TAG = "BasicTexture";
+ protected static final int UNSPECIFIED = -1;
+
+ protected static final int STATE_UNLOADED = 0;
+ protected static final int STATE_LOADED = 1;
+ protected static final int STATE_ERROR = -1;
+
+ // Log a warning if a texture is larger along a dimension
+ private static final int MAX_TEXTURE_SIZE = 4096;
+
+ protected int mId = -1;
+ protected int mState;
+
+ protected int mWidth = UNSPECIFIED;
+ protected int mHeight = UNSPECIFIED;
+
+ protected int mTextureWidth;
+ protected int mTextureHeight;
+
+ private boolean mHasBorder;
+
+ protected GLCanvas mCanvasRef = null;
+ private final static WeakHashMap+ * 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 = MathUtils.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/app/src/main/java/com/hippo/gl/glrenderer/RawTexture.java b/app/src/main/java/com/hippo/gl/glrenderer/RawTexture.java new file mode 100644 index 000000000..a58c6cc6d --- /dev/null +++ b/app/src/main/java/com/hippo/gl/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.gl.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/app/src/main/java/com/hippo/gl/glrenderer/ResourceTexture.java b/app/src/main/java/com/hippo/gl/glrenderer/ResourceTexture.java new file mode 100644 index 000000000..3aaf1e317 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/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.gl.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/app/src/main/java/com/hippo/gl/glrenderer/SpriteTexture.java b/app/src/main/java/com/hippo/gl/glrenderer/SpriteTexture.java new file mode 100644 index 000000000..fcfeac8ec --- /dev/null +++ b/app/src/main/java/com/hippo/gl/glrenderer/SpriteTexture.java @@ -0,0 +1,73 @@ +package com.hippo.gl.glrenderer; + +import android.graphics.Bitmap; +import android.graphics.RectF; + +import com.hippo.yorozuya.AssertUtils; + +public class SpriteTexture extends TiledTexture { + + private int mCount; + private int[] mRects; + + private RectF mTempSource = new RectF(); + private RectF mTempTarget = new RectF(); + + public SpriteTexture(Bitmap bitmap, int count, int[] rects) { + super(bitmap); + + AssertUtils.assertEquals("rects.length must be count * 4", count * 4, rects.length); + mCount = count; + mRects = rects; + } + + public int getCount() { + return mCount; + } + + public void drawSprite(GLCanvas canvas, int index, int x, int y) { + int[] rects = mRects; + int offset = index * 4; + int sourceX = rects[offset]; + int sourceY = rects[offset + 1]; + int sourceWidth = rects[offset + 2]; + int sourceHeight = rects[offset + 3]; + mTempSource.set(sourceX, sourceY, sourceX + sourceWidth, sourceY + sourceHeight); + mTempTarget.set(x, y, x + sourceWidth, y + sourceHeight); + draw(canvas, mTempSource, mTempTarget); + } + + public void drawSprite(GLCanvas canvas, int index, int x, int y, int width, int height) { + int[] rects = mRects; + int offset = index * 4; + int sourceX = rects[offset]; + int sourceY = rects[offset + 1]; + mTempSource.set(sourceX, sourceY, sourceX + rects[offset + 2], sourceY + rects[offset + 3]); + mTempTarget.set(x, y, x + width, y + height); + draw(canvas, mTempSource, mTempTarget); + } + + public void drawSpriteMixed(GLCanvas canvas, int index, int color, float ratio, int x, int y) { + int[] rects = mRects; + int offset = index * 4; + int sourceX = rects[offset]; + int sourceY = rects[offset + 1]; + int sourceWidth = rects[offset + 2]; + int sourceHeight = rects[offset + 3]; + mTempSource.set(sourceX, sourceY, sourceX + sourceWidth, sourceY + sourceHeight); + mTempTarget.set(x, y, x + sourceWidth, y + sourceHeight); + drawMixed(canvas, color, ratio, mTempSource, mTempTarget); + } + + public void drawSpriteMixed(GLCanvas canvas, int index, int color, float ratio, + int x, int y, int width, int height) { + int[] rects = mRects; + int offset = index * 4; + int sourceX = rects[offset]; + int sourceY = rects[offset + 1]; + mTempSource.set(sourceX, sourceY, sourceX + rects[offset + 2], + sourceY + rects[offset + 3]); + mTempTarget.set(x, y, x + width, y + height); + drawMixed(canvas, color, ratio, mTempSource, mTempTarget); + } +} diff --git a/app/src/main/java/com/hippo/gl/glrenderer/StringTexture.java b/app/src/main/java/com/hippo/gl/glrenderer/StringTexture.java new file mode 100644 index 000000000..609e79509 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/glrenderer/StringTexture.java @@ -0,0 +1,87 @@ +/* + * 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.gl.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; + +// 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) Math.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/app/src/main/java/com/hippo/gl/glrenderer/TextTexture.java b/app/src/main/java/com/hippo/gl/glrenderer/TextTexture.java new file mode 100644 index 000000000..22b6848ad --- /dev/null +++ b/app/src/main/java/com/hippo/gl/glrenderer/TextTexture.java @@ -0,0 +1,181 @@ +package com.hippo.gl.glrenderer; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Typeface; + +import java.util.Arrays; + +// TODO support multiline +/** + * Only support single line + */ +public class TextTexture extends SpriteTexture { + + private char[] mCharacters; + private float[] mWidths; + private float mHeight; + private float mMaxWidth; + + private TextTexture(Bitmap bitmap, int count, int[] rects, char[] characters, + float[] widths, float height, float maxWidth) { + super(bitmap, count, rects); + mCharacters = characters; + mWidths = widths; + mHeight = height; + mMaxWidth = maxWidth; + } + + public int[] getTextIndexes(String text) { + char[] characters = mCharacters; + + int length = text.length(); + int[] indexes = new int[length]; + for (int i = 0; i < length; i++) { + char ch = text.charAt(i); + indexes[i] = Arrays.binarySearch(characters, ch); + } + + return indexes; + } + + public float getTextWidth(String text) { + char[] characters = mCharacters; + float[] widths = mWidths; + float width = 0.0f; + + for (int i = 0, n = text.length(); i < n; i++) { + char ch = text.charAt(i); + int index = Arrays.binarySearch(characters, ch); + if (index >= 0) { + width += widths[index]; + } else { + width += mMaxWidth; + } + } + + return width; + } + + public float getTextWidth(int[] indexes) { + float[] widths = mWidths; + float width = 0.0f; + int length = indexes.length; + for (int i = 0; i < length; i++) { + int index = indexes[i]; + if (index >= 0) { + width += widths[index]; + } else { + width += mMaxWidth; + } + } + + return width; + } + + public float getMaxWidth() { + return mMaxWidth; + } + + public float getTextHeight() { + return mHeight; + } + + public void drawText(GLCanvas canvas, String text, int x, int y) { + char[] characters = mCharacters; + float[] widths = mWidths; + + for (int i = 0, n = text.length(); i < n; i++) { + char ch = text.charAt(i); + int index = Arrays.binarySearch(characters, ch); + if (index >= 0) { + drawSprite(canvas, index, x, y); + x += widths[index]; + } else { + x += mMaxWidth; + } + } + } + + public void drawText(GLCanvas canvas, int[] indexes, int x, int y) { + float[] widths = mWidths; + + for (int i = 0, n = indexes.length; i < n; i++) { + int index = indexes[i]; + if (index >= 0) { + drawSprite(canvas, index, x, y); + x += widths[index]; + } else { + x += mMaxWidth; + } + } + } + + /** + * Create a TextTexture to draw text + * + * @param typeface the typeface + * @param size text size + * @param characters all Characters + * @return the TextTexture + */ + public static TextTexture create(Typeface typeface, int size, int color, char[] characters) { + Paint paint = new Paint(); + paint.setAntiAlias(true); + paint.setTextSize(size); + paint.setColor(color); + paint.setTypeface(typeface); + + Paint.FontMetricsInt fmi = paint.getFontMetricsInt(); + int fixed = fmi.bottom; + int height = fmi.bottom - fmi.top; + + int length = characters.length; + float[] widths = new float[length]; + paint.getTextWidths(characters, 0, length, widths); + + // Caculate bitmap size + float maxWidth = 0.0f; + for (float f : widths) { + maxWidth = Math.max(maxWidth, f); + } + int hCount = (int) Math.ceil(Math.sqrt(height / maxWidth * length)); + int vCount = (int) Math.ceil(Math.sqrt(maxWidth / height * length)); + if (hCount * (vCount - 1) > length) { + vCount--; + } + if ((hCount - 1) * vCount > length) { + hCount--; + } + + Bitmap bitmap = Bitmap.createBitmap((int) Math.ceil(hCount * maxWidth), + (int) Math.ceil(vCount * height), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + canvas.translate(0, height - fixed); + + // Draw + int[] rects = new int[length * 4]; + int x = 0; + int y = 0; + for (int i = 0; i < length; i++) { + int offset = i * 4; + rects[offset + 0] = x; + rects[offset + 1] = y; + rects[offset + 2] = (int) widths[i]; + rects[offset + 3] = height; + + canvas.drawText(characters, i, 1, x, y, paint); + + if (i % hCount == hCount - 1) { + // The end of row + x = 0; + y += height; + } else { + x += maxWidth; + } + } + + return new TextTexture(bitmap, length, rects, characters, widths, height, maxWidth); + } +} diff --git a/app/src/main/java/com/hippo/gl/glrenderer/Texture.java b/app/src/main/java/com/hippo/gl/glrenderer/Texture.java new file mode 100644 index 000000000..23a5ddf8f --- /dev/null +++ b/app/src/main/java/com/hippo/gl/glrenderer/Texture.java @@ -0,0 +1,47 @@ +/* + * 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.gl.glrenderer; + + +import android.graphics.RectF; + +// 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 { + int getWidth(); + int getHeight(); + void draw(GLCanvas canvas, int x, int y); + void draw(GLCanvas canvas, int x, int y, int w, int h); + void draw(GLCanvas canvas, RectF source, RectF target); + boolean isOpaque(); +} diff --git a/app/src/main/java/com/hippo/gl/glrenderer/TextureUploader.java b/app/src/main/java/com/hippo/gl/glrenderer/TextureUploader.java new file mode 100644 index 000000000..34371585d --- /dev/null +++ b/app/src/main/java/com/hippo/gl/glrenderer/TextureUploader.java @@ -0,0 +1,104 @@ +/* + * 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.gl.glrenderer; + +import com.hippo.gl.view.GLRoot; + +import java.util.ArrayDeque; + +public class TextureUploader implements GLRoot.OnGLIdleListener { + private static final int INIT_CAPACITY = 64; + private static final int QUOTA_PER_FRAME = 1; + + private final ArrayDeque
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 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 ArrayListNote: On API level 17 and lower, makeMeasureSpec's + * implementation was such that the order of arguments did not matter + * and overflow in either value could impact the resulting MeasureSpec. + * {@link android.widget.RelativeLayout} was affected by this bug. + * Apps targeting API levels greater than 17 will get the fixed, more strict + * behavior.
+ * + * @param size the size of the measure specification + * @param mode the mode of the measure specification + * @return the measure specification based on size and mode + */ + public static int makeMeasureSpec(int size, int mode) { + return (size & ~MODE_MASK) | (mode & MODE_MASK); + } + + /** + * Extracts the mode from the supplied measure specification. + * + * @param measureSpec the measure specification to extract the mode from + * @return {@link #UNSPECIFIED}, + * {@link #AT_MOST} or + * {@link #EXACTLY} + */ + public static int getMode(int measureSpec) { + return (measureSpec & MODE_MASK); + } + + /** + * Extracts the size from the supplied measure specification. + * + * @param measureSpec the measure specification to extract the size from + * @return the size in pixels defined in the supplied measure specification + */ + public static int getSize(int measureSpec) { + return (measureSpec & ~MODE_MASK); + } + + /** + * Returns a String representation of the specified measure + * specification. + * + * @param measureSpec the measure specification to convert to a String + * @return a String with the following format: "MeasureSpec: MODE SIZE" + */ + public static String toString(int measureSpec) { + int mode = getMode(measureSpec); + int size = getSize(measureSpec); + + StringBuilder sb = new StringBuilder("MeasureSpec: "); + + if (mode == UNSPECIFIED) + sb.append("UNSPECIFIED "); + else if (mode == EXACTLY) + sb.append("EXACTLY "); + else if (mode == AT_MOST) + sb.append("AT_MOST "); + else + sb.append(mode).append(" "); + + sb.append(size); + return sb.toString(); + } + } + + /** + * The LayoutParams look like {@link android.view.ViewGroup.LayoutParams}, + * and work like it. + */ + public static class LayoutParams { + + /** + * Special value for the height or width requested by a View. + * MATCH_PARENT means that the view wants to be as big as its parent, + * minus the parent's padding, if any. Introduced in API Level 8. + */ + public static final int MATCH_PARENT = -1; + + /** + * Special value for the height or width requested by a View. + * WRAP_CONTENT means that the view wants to be just large enough to fit + * its own internal content, taking its own padding into account. + */ + public static final int WRAP_CONTENT = -2; + + /** + * Information about how wide the view wants to be. Can be one of the + * constants {@link #MATCH_PARENT} or {@link #WRAP_CONTENT} or an exact size. + */ + public int width; + + /** + * Information about how tall the view wants to be. Can be one of the + * constants {@link #MATCH_PARENT} or {@link #WRAP_CONTENT} or an exact size. + */ + public int height; + + /** + * Creates a new set of layout parameters with the specified width + * and height. + * + * @param width the width, either {@link #WRAP_CONTENT}, + * {@link #MATCH_PARENT}, or a fixed size in pixels + * @param height the height, either {@link #WRAP_CONTENT}, + * {@link #MATCH_PARENT}, or a fixed size in pixels + */ + public LayoutParams(int width, int height) { + this.width = width; + this.height = height; + } + + /** + * Copy constructor. Clones the width and height values of the source. + * + * @param source The layout params to copy from. + */ + public LayoutParams(LayoutParams source) { + this.width = source.width; + this.height = source.height; + } + } + + /** + * LayoutParams with gravity inside + */ + public static class GravityLayoutParams extends LayoutParams { + + public int gravity = Gravity.NO_GRAVITY; + + public GravityLayoutParams(GLView.LayoutParams source) { + super(source); + + if (source instanceof GravityLayoutParams) { + gravity = ((GravityLayoutParams) source).gravity; + } + } + + public GravityLayoutParams(int width, int height) { + super(width, height); + } + } +} diff --git a/app/src/main/java/com/hippo/gl/view/Gravity.java b/app/src/main/java/com/hippo/gl/view/Gravity.java new file mode 100644 index 000000000..3fd670338 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/view/Gravity.java @@ -0,0 +1,82 @@ +package com.hippo.gl.view; + +import android.support.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +public class Gravity { + + @IntDef({POSITION_BEGIN, POSITION_CENTER, POSITION_FINISH}) + @Retention(RetentionPolicy.SOURCE) + public @interface PositionMode {} + + @IntDef({HORIZONTAL, VERTICAL}) + @Retention(RetentionPolicy.SOURCE) + public @interface DirectionMode {} + + public static final int NO_GRAVITY = 0x0000; + + public static final int LEFT = 0x0001; + public static final int TOP = 0x0002; + public static final int RIGHT = 0x0004; + public static final int BOTTOM = 0x0008; + + public static final int CENTER_HORIZONTAL = LEFT | RIGHT; + public static final int CENTER_VERTICAL = TOP | BOTTOM; + public static final int CENTER = CENTER_VERTICAL|CENTER_HORIZONTAL; + + public static final int HORIZONTAL_MASK = CENTER_HORIZONTAL; + public static final int VERTICAL_MASK = CENTER_VERTICAL; + + public static final int POSITION_BEGIN = 0; + public static final int POSITION_CENTER = 1; + public static final int POSITION_FINISH = 2; + + public static final int HORIZONTAL = 0; + public static final int VERTICAL = 1; + + public static boolean centerHorizontal(int gravity) { + return (gravity & HORIZONTAL_MASK) == CENTER_HORIZONTAL; + } + + public static boolean right(int gravity) { + return (gravity & HORIZONTAL_MASK) == RIGHT; + } + + public static boolean left(int gravity) { + return (gravity & HORIZONTAL_MASK) == LEFT; + } + + public static boolean centerVertical(int gravity) { + return (gravity & VERTICAL_MASK) == CENTER_VERTICAL; + } + + public static boolean top(int gravity) { + return (gravity & VERTICAL_MASK) == TOP; + } + + public static boolean bottom(int gravity) { + return (gravity & VERTICAL_MASK) == BOTTOM; + } + + public static @PositionMode int getPosition(int gravity, @DirectionMode int direction) { + if (direction == HORIZONTAL) { + if (right(gravity)) { + return POSITION_FINISH; + } else if (centerHorizontal(gravity)) { + return POSITION_CENTER; + } else { + return POSITION_BEGIN; + } + } else { + if (bottom(gravity)) { + return POSITION_FINISH; + } else if (centerVertical(gravity)) { + return POSITION_CENTER; + } else { + return POSITION_BEGIN; + } + } + } +} diff --git a/app/src/main/java/com/hippo/gl/view/OrientationSource.java b/app/src/main/java/com/hippo/gl/view/OrientationSource.java new file mode 100644 index 000000000..a8b6f8967 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/view/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.gl.view; + +public interface OrientationSource { + int getDisplayRotation(); + int getCompensation(); +} diff --git a/app/src/main/java/com/hippo/gl/view/TouchHelper.java b/app/src/main/java/com/hippo/gl/view/TouchHelper.java new file mode 100644 index 000000000..42e307e35 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/view/TouchHelper.java @@ -0,0 +1,242 @@ +/* + * Copyright 2015 Hippo Seven + * + * 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.gl.view; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +import com.hippo.yorozuya.LayoutUtils; +import com.hippo.yorozuya.SimpleHandler; + +public class TouchHelper { + + private TouchOwner mOwner; + + private boolean mPrePressed = false; + private boolean mHasPerformedLongPress = false; + + private CheckForLongPress mPendingCheckForLongPress = null; + private CheckForTap mPendingCheckForTap = null; + private PerformClick mPerformClick = null; + private UnsetPressedState mUnsetPressedState = null; + + /** + * Cache the touch slop from the context that created the view. + */ + private static int sTouchSlop = 8; + + public static void initialize(Context context) { + sTouchSlop = LayoutUtils.dp2pix(context, 8); // 8dp + } + + public TouchHelper(@NonNull TouchOwner owner) { + mOwner = owner; + } + + public boolean onTouch(MotionEvent event) { + TouchOwner owner = mOwner; + final float x = event.getX(); + final float y = event.getY(); + + if (event.getAction() == MotionEvent.ACTION_DOWN || + event.getAction() == MotionEvent.ACTION_MOVE) + owner.setHotspot(x, y); + + if (!owner.isEnabled()) { + if (event.getAction() == MotionEvent.ACTION_UP && owner.isPressed()) { + owner.setPressed(false); + } + // A disabled view that is clickable still consumes the touch + // events, it just doesn't respond to them. + return (owner.isClickable() || owner.isLongClickable()); + } + + if (owner.isClickable() || owner.isLongClickable()) { + switch (event.getAction()) { + case MotionEvent.ACTION_UP: + if (owner.isPressed() || mPrePressed) { + if (mPrePressed) { + // The button is being released before we actually + // showed it as pressed. Make it show the pressed + // state now (before scheduling the click) to ensure + // the user sees it. + setPressed(true, x, y); + } + + if (!mHasPerformedLongPress) { + // This is a tap, so remove the longpress check + removeLongPressCallback(); + + // Use a Runnable and post this rather than calling + // performClick directly. This lets other visual state + // of the view update before click actions start. + if (mPerformClick == null) { + mPerformClick = new PerformClick(); + } + if (!SimpleHandler.getInstance().post(mPerformClick)) { + owner.performClick(); + } + } + + if (mUnsetPressedState == null) { + mUnsetPressedState = new UnsetPressedState(); + } + + if (mPrePressed) { + SimpleHandler.getInstance().postDelayed(mUnsetPressedState, + ViewConfiguration.getPressedStateDuration()); + } else if (!SimpleHandler.getInstance().post(mUnsetPressedState)) { + // If the post failed, unpress right now + mUnsetPressedState.run(); + } + + removeTapCallback(); + } + break; + + case MotionEvent.ACTION_DOWN: + mHasPerformedLongPress = false; + + mPrePressed = true; + if (mPendingCheckForTap == null) { + mPendingCheckForTap = new CheckForTap(); + } + mPendingCheckForTap.x = event.getX(); + mPendingCheckForTap.y = event.getY(); + SimpleHandler.getInstance().postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); + break; + + case MotionEvent.ACTION_CANCEL: + owner.setPressed(false); + removeTapCallback(); + removeLongPressCallback(); + break; + + case MotionEvent.ACTION_MOVE: + owner.setHotspot(x, y); + + // Be lenient about moving outside of buttons + if (!pointInView(x, y, sTouchSlop)) { + // Outside button + removeTapCallback(); + if (owner.isPressed()) { + // Remove any future long press/tap checks + removeLongPressCallback(); + + owner.setPressed(false); + } + } + break; + } + + return true; + } + + return false; + } + + private void setPressed(boolean pressed, float x, float y){ + mOwner.setPressed(pressed); + if (pressed) { + mOwner.setHotspot(x, y); + } + } + + /** + * Utility method to determine whether the given point, in local coordinates, + * is inside the view, where the area of the view is expanded by the slop factor. + * This method is called while processing touch-move events to determine if the event + * is still within the view. + */ + public boolean pointInView(float localX, float localY, float slop) { + return localX >= -slop && localY >= -slop && localX < (mOwner.getWidth() + slop) && + localY < (mOwner.getHeight() + slop); + } + + /** + * Remove the longpress detection timer. + */ + private void removeLongPressCallback() { + if (mPendingCheckForLongPress != null) { + SimpleHandler.getInstance().removeCallbacks(mPendingCheckForLongPress); + } + } + + /** + * Remove the tap detection timer. + */ + private void removeTapCallback() { + if (mPendingCheckForTap != null) { + mPrePressed = false; + SimpleHandler.getInstance().removeCallbacks(mPendingCheckForTap); + } + } + + private void checkForLongClick(int delayOffset) { + if (mOwner.isLongClickable()) { + mHasPerformedLongPress = false; + + if (mPendingCheckForLongPress == null) { + mPendingCheckForLongPress = new CheckForLongPress(); + } + SimpleHandler.getInstance().postDelayed(mPendingCheckForLongPress, + ViewConfiguration.getLongPressTimeout() - delayOffset); + } + } + + private final class CheckForLongPress implements Runnable { + + @Override + public void run() { + if (mOwner.isPressed()) { + if (mOwner.performLongClick()) { + mHasPerformedLongPress = true; + } + } + } + } + + private final class CheckForTap implements Runnable { + public float x; + public float y; + + @Override + public void run() { + mPrePressed = false; + setPressed(true, x, y); + checkForLongClick(ViewConfiguration.getTapTimeout()); + } + } + + private final class PerformClick implements Runnable { + + @Override + public void run() { + mOwner.performClick(); + } + } + + private final class UnsetPressedState implements Runnable { + + @Override + public void run() { + mOwner.setPressed(false); + } + } +} diff --git a/app/src/main/java/com/hippo/gl/view/TouchOwner.java b/app/src/main/java/com/hippo/gl/view/TouchOwner.java new file mode 100644 index 000000000..338652a64 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/view/TouchOwner.java @@ -0,0 +1,40 @@ +/* + * Copyright 2015 Hippo Seven + * + * 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.gl.view; + +public interface TouchOwner { + + void setHotspot(float x, float y); + + boolean isEnabled(); + + boolean isPressed(); + + void setPressed(boolean pressed); + + boolean isClickable(); + + boolean isLongClickable(); + + boolean performClick(); + + boolean performLongClick(); + + int getWidth(); + + int getHeight(); +} diff --git a/app/src/main/java/com/hippo/gl/widget/EdgeEffect.java b/app/src/main/java/com/hippo/gl/widget/EdgeEffect.java new file mode 100644 index 000000000..f9559d616 --- /dev/null +++ b/app/src/main/java/com/hippo/gl/widget/EdgeEffect.java @@ -0,0 +1,368 @@ +/* + * Copyright 2016 Hippo Seven + * + * 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.gl.widget; + +import android.graphics.Rect; +import android.view.animation.AnimationUtils; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +import com.hippo.gl.glrenderer.GLCanvas; + +/** + * Material design edge effect + */ +public class EdgeEffect { + + // Time it will take the effect to fully recede in ms + private static final int RECEDE_TIME = 600; + + // Time it will take before a pulled glow begins receding in ms + private static final int PULL_TIME = 167; + + // Time it will take in ms for a pulled glow to decay to partial strength before release + private static final int PULL_DECAY_TIME = 2000; + + private static final float MAX_ALPHA = 0.5f; + + private static final float MAX_GLOW_SCALE = 2.f; + + private static final float PULL_GLOW_BEGIN = 0.f; + + // Minimum velocity that will be absorbed + private static final int MIN_VELOCITY = 100; + // Maximum velocity, clamps at this value + private static final int MAX_VELOCITY = 10000; + + private static final float EPSILON = 0.001f; + + private static final double ANGLE = Math.PI / 6; + private static final float SIN = (float) Math.sin(ANGLE); + private static final float COS = (float) Math.cos(ANGLE); + + private float mGlowAlpha; + private float mGlowScaleY; + + private float mGlowAlphaStart; + private float mGlowAlphaFinish; + private float mGlowScaleYStart; + private float mGlowScaleYFinish; + + private long mStartTime; + private float mDuration; + + private final Interpolator mInterpolator; + + private static final int STATE_IDLE = 0; + private static final int STATE_PULL = 1; + private static final int STATE_ABSORB = 2; + private static final int STATE_RECEDE = 3; + private static final int STATE_PULL_DECAY = 4; + + private static final float PULL_DISTANCE_ALPHA_GLOW_FACTOR = 0.8f; + + private static final int VELOCITY_GLOW_FACTOR = 6; + + private int mState = STATE_IDLE; + + private float mPullDistance; + + private final Rect mBounds = new Rect(); + private int mColor; + private float mRadius; + private float mBaseGlowScale; + private float mDisplacement = 0.5f; + private float mTargetDisplacement = 0.5f; + + /** + * Construct a new EdgeEffect with a theme appropriate for the provided context. + */ + public EdgeEffect(int color) { + mColor = color; + mInterpolator = new DecelerateInterpolator(); + } + + /** + * Set the size of this edge effect in pixels. + * + * @param width Effect width in pixels + * @param height Effect height in pixels + */ + public void setSize(int width, int height) { + final float r = width * 0.75f / SIN; + final float y = COS * r; + final float h = r - y; + final float or = height * 0.75f / SIN; + final float oy = COS * or; + final float oh = or - oy; + + mRadius = r; + mBaseGlowScale = h > 0 ? Math.min(oh / h, 1.f) : 1.f; + + mBounds.set(mBounds.left, mBounds.top, width, (int) Math.min(height, h)); + } + + /** + * Reports if this EdgeEffect's animation is finished. If this method returns false + * after a call to {@link #draw(GLCanvas)} the host widget should schedule another + * drawing pass to continue the animation. + * + * @return true if animation is finished, false if drawing should continue on the next frame. + */ + public boolean isFinished() { + return mState == STATE_IDLE; + } + + /** + * Immediately finish the current animation. + * After this call {@link #isFinished()} will return true. + */ + public void finish() { + mState = STATE_IDLE; + } + + /** + * A view should call this when content is pulled away from an edge by the user. + * This will update the state of the current visual effect and its associated animation. + * The host view should always {@link android.view.View#invalidate()} after this + * and draw the results accordingly. + * + *Views using EdgeEffect should favor {@link #onPull(float, float)} when the displacement + * of the pull point is known.
+ * + * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to + * 1.f (full length of the view) or negative values to express change + * back toward the edge reached to initiate the effect. + */ + public void onPull(float deltaDistance) { + onPull(deltaDistance, 0.5f); + } + + /** + * A view should call this when content is pulled away from an edge by the user. + * This will update the state of the current visual effect and its associated animation. + * The host view should always {@link android.view.View#invalidate()} after this + * and draw the results accordingly. + * + * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to + * 1.f (full length of the view) or negative values to express change + * back toward the edge reached to initiate the effect. + * @param displacement The displacement from the starting side of the effect of the point + * initiating the pull. In the case of touch this is the finger position. + * Values may be from 0-1. + */ + public void onPull(float deltaDistance, float displacement) { + final long now = AnimationUtils.currentAnimationTimeMillis(); + mTargetDisplacement = displacement; + if (mState == STATE_PULL_DECAY && now - mStartTime < mDuration) { + return; + } + if (mState != STATE_PULL) { + mGlowScaleY = Math.max(PULL_GLOW_BEGIN, mGlowScaleY); + } + mState = STATE_PULL; + + mStartTime = now; + mDuration = PULL_TIME; + + mPullDistance += deltaDistance; + + final float absdd = Math.abs(deltaDistance); + mGlowAlpha = mGlowAlphaStart = Math.min(MAX_ALPHA, + mGlowAlpha + (absdd * PULL_DISTANCE_ALPHA_GLOW_FACTOR)); + + if (mPullDistance == 0) { + mGlowScaleY = mGlowScaleYStart = 0; + } else { + final float scale = Math.max(0, 1 - 1 / + (float) Math.sqrt(Math.abs(mPullDistance) * mBounds.height()) - 0.3f) / 0.7f; + + mGlowScaleY = mGlowScaleYStart = scale; + } + + mGlowAlphaFinish = mGlowAlpha; + mGlowScaleYFinish = mGlowScaleY; + } + + /** + * Call when the object is released after being pulled. + * This will begin the "decay" phase of the effect. After calling this method + * the host view should {@link android.view.View#invalidate()} and thereby + * draw the results accordingly. + */ + public void onRelease() { + mPullDistance = 0; + + if (mState != STATE_PULL && mState != STATE_PULL_DECAY) { + return; + } + + mState = STATE_RECEDE; + mGlowAlphaStart = mGlowAlpha; + mGlowScaleYStart = mGlowScaleY; + + mGlowAlphaFinish = 0.f; + mGlowScaleYFinish = 0.f; + + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mDuration = RECEDE_TIME; + } + + /** + * Call when the effect absorbs an impact at the given velocity. + * Used when a fling reaches the scroll boundary. + * + *When using a {@link android.widget.Scroller} or {@link android.widget.OverScroller},
+ * the method getCurrVelocity will provide a reasonable approximation
+ * to use here.