();
@@ -233,6 +251,20 @@ public class TiledTexture implements Texture {
freeTile(mTiles[i]);
}
}
+ freeBitmap();
+ }
+
+ private void freeBitmap() {
+ if (mBitmap != null) {
+ onFreeBitmap(mBitmap);
+ mBitmap = null;
+ }
+ }
+
+ private void onFreeBitmap(Bitmap bitmap) {
+ if (sOnFreeBitmapListener != null) {
+ sOnFreeBitmapListener.onFreeBitmapListener(bitmap);
+ }
}
public static void freeResources() {
diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/ui/DownUpDetector.java b/app/src/main/java/com/hippo/ehviewer/gallery/ui/DownUpDetector.java
new file mode 100644
index 000000000..55be6ff6c
--- /dev/null
+++ b/app/src/main/java/com/hippo/ehviewer/gallery/ui/DownUpDetector.java
@@ -0,0 +1,67 @@
+/*
+ * 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);
+ void onPointerDown(MotionEvent e);
+ void onPointerUp(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.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ setState(true, ev);
+ break;
+ case MotionEvent.ACTION_POINTER_DOWN:
+ mListener.onPointerDown(ev);
+ break;
+ case MotionEvent.ACTION_UP:
+ mListener.onPointerUp(ev);
+ setState(false, ev);
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ setState(false, ev);
+ break;
+ }
+ }
+
+ public boolean isDown() {
+ return mStillDown;
+ }
+}
diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/ui/EdgeEffect.java b/app/src/main/java/com/hippo/ehviewer/gallery/ui/EdgeEffect.java
new file mode 100644
index 000000000..e7c65e8a7
--- /dev/null
+++ b/app/src/main/java/com/hippo/ehviewer/gallery/ui/EdgeEffect.java
@@ -0,0 +1,443 @@
+/*
+ * Copyright (C) 2011 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.Canvas;
+import android.graphics.Rect;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import com.hippo.ehviewer.R;
+import com.hippo.ehviewer.gallery.glrenderer.GLCanvas;
+import com.hippo.ehviewer.gallery.glrenderer.ResourceTexture;
+
+// This is copied from android.widget.EdgeEffect with some small modifications:
+// (1) Copy the images (overscroll_{edge|glow}.png) to local resources.
+// (2) Use "GLCanvas" instead of "Canvas" for draw()'s parameter.
+// (3) Use a private Drawable class (which inherits from ResourceTexture)
+// instead of android.graphics.drawable.Drawable to hold the images.
+// The private Drawable class is used to translate original Canvas calls to
+// corresponding GLCanvas calls.
+
+/**
+ * This class performs the graphical effect used at the edges of scrollable widgets
+ * when the user scrolls beyond the content bounds in 2D space.
+ *
+ * EdgeEffect is stateful. Custom widgets using EdgeEffect should create an
+ * instance for each edge that should show the effect, feed it input data using
+ * the methods {@link #onAbsorb(int)}, {@link #onPull(float)}, and {@link #onRelease()},
+ * and draw the effect using {@link #draw(GLCanvas)} in the widget's overridden
+ * {@link android.view.View#draw(Canvas)} method. If {@link #isFinished()} returns
+ * false after drawing, the edge effect's animation is not yet complete and the widget
+ * should schedule another drawing pass to continue the animation.
+ *
+ * When drawing, widgets should draw their main content and child views first,
+ * usually by invoking super.draw(canvas) from an overridden draw
+ * method. (This will invoke onDraw and dispatch drawing to child views as needed.)
+ * The edge effect may then be drawn on top of the view's content using the
+ * {@link #draw(GLCanvas)} method.
+ */
+public class EdgeEffect {
+ @SuppressWarnings("unused")
+ private static final String TAG = "EdgeEffect";
+
+ // Time it will take the effect to fully recede in ms
+ private static final int RECEDE_TIME = 1000;
+
+ // 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 = 1000;
+
+ private static final float MAX_ALPHA = 0.8f;
+ private static final float HELD_EDGE_ALPHA = 0.7f;
+ private static final float HELD_EDGE_SCALE_Y = 0.5f;
+ private static final float HELD_GLOW_ALPHA = 0.5f;
+ private static final float HELD_GLOW_SCALE_Y = 0.5f;
+
+ private static final float MAX_GLOW_HEIGHT = 4.f;
+
+ private static final float PULL_GLOW_BEGIN = 1.f;
+ private static final float PULL_EDGE_BEGIN = 0.6f;
+
+ // Minimum velocity that will be absorbed
+ private static final int MIN_VELOCITY = 100;
+
+ private static final float EPSILON = 0.001f;
+
+ private final Drawable mEdge;
+ private final Drawable mGlow;
+ private int mWidth;
+ private int mHeight;
+ private final int MIN_WIDTH = 300;
+ private final int mMinWidth;
+
+ private float mEdgeAlpha;
+ private float mEdgeScaleY;
+ private float mGlowAlpha;
+ private float mGlowScaleY;
+
+ private float mEdgeAlphaStart;
+ private float mEdgeAlphaFinish;
+ private float mEdgeScaleYStart;
+ private float mEdgeScaleYFinish;
+ 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;
+
+ // How much dragging should effect the height of the edge image.
+ // Number determined by user testing.
+ private static final int PULL_DISTANCE_EDGE_FACTOR = 7;
+
+ // How much dragging should effect the height of the glow image.
+ // Number determined by user testing.
+ private static final int PULL_DISTANCE_GLOW_FACTOR = 7;
+ private static final float PULL_DISTANCE_ALPHA_GLOW_FACTOR = 1.1f;
+
+ private static final int VELOCITY_EDGE_FACTOR = 8;
+ private static final int VELOCITY_GLOW_FACTOR = 16;
+
+ private int mState = STATE_IDLE;
+
+ private float mPullDistance;
+
+ /**
+ * Construct a new EdgeEffect with a theme appropriate for the provided context.
+ * @param context Context used to provide theming and resource information for the EdgeEffect
+ */
+ public EdgeEffect(Context context) {
+ mEdge = new Drawable(context, R.drawable.overscroll_edge);
+ mGlow = new Drawable(context, R.drawable.overscroll_glow);
+
+ mMinWidth = (int) (context.getResources().getDisplayMetrics().density * MIN_WIDTH + 0.5f);
+ 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) {
+ mWidth = width;
+ mHeight = height;
+ }
+
+ /**
+ * 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.
+ *
+ * @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) {
+ final long now = AnimationTime.get();
+ if (mState == STATE_PULL_DECAY && now - mStartTime < mDuration) {
+ return;
+ }
+ if (mState != STATE_PULL) {
+ mGlowScaleY = PULL_GLOW_BEGIN;
+ }
+ mState = STATE_PULL;
+
+ mStartTime = now;
+ mDuration = PULL_TIME;
+
+ mPullDistance += deltaDistance;
+ float distance = Math.abs(mPullDistance);
+
+ mEdgeAlpha = mEdgeAlphaStart = Math.max(PULL_EDGE_BEGIN, Math.min(distance, MAX_ALPHA));
+ mEdgeScaleY = mEdgeScaleYStart = Math.max(
+ HELD_EDGE_SCALE_Y, Math.min(distance * PULL_DISTANCE_EDGE_FACTOR, 1.f));
+
+ mGlowAlpha = mGlowAlphaStart = Math.min(MAX_ALPHA,
+ mGlowAlpha +
+ (Math.abs(deltaDistance) * PULL_DISTANCE_ALPHA_GLOW_FACTOR));
+
+ float glowChange = Math.abs(deltaDistance);
+ if (deltaDistance > 0 && mPullDistance < 0) {
+ glowChange = -glowChange;
+ }
+ if (mPullDistance == 0) {
+ mGlowScaleY = 0;
+ }
+
+ // Do not allow glow to get larger than MAX_GLOW_HEIGHT.
+ mGlowScaleY = mGlowScaleYStart = Math.min(MAX_GLOW_HEIGHT, Math.max(
+ 0, mGlowScaleY + glowChange * PULL_DISTANCE_GLOW_FACTOR));
+
+ mEdgeAlphaFinish = mEdgeAlpha;
+ mEdgeScaleYFinish = mEdgeScaleY;
+ 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;
+ mEdgeAlphaStart = mEdgeAlpha;
+ mEdgeScaleYStart = mEdgeScaleY;
+ mGlowAlphaStart = mGlowAlpha;
+ mGlowScaleYStart = mGlowScaleY;
+
+ mEdgeAlphaFinish = 0.f;
+ mEdgeScaleYFinish = 0.f;
+ mGlowAlphaFinish = 0.f;
+ mGlowScaleYFinish = 0.f;
+
+ mStartTime = AnimationTime.get();
+ 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.
+ *
+ * @param velocity Velocity at impact in pixels per second.
+ */
+ public void onAbsorb(int velocity) {
+ mState = STATE_ABSORB;
+ velocity = Math.max(MIN_VELOCITY, Math.abs(velocity));
+
+ mStartTime = AnimationTime.get();
+ mDuration = 0.1f + (velocity * 0.03f);
+
+ // The edge should always be at least partially visible, regardless
+ // of velocity.
+ mEdgeAlphaStart = 0.f;
+ mEdgeScaleY = mEdgeScaleYStart = 0.f;
+ // The glow depends more on the velocity, and therefore starts out
+ // nearly invisible.
+ mGlowAlphaStart = 0.5f;
+ mGlowScaleYStart = 0.f;
+
+ // Factor the velocity by 8. Testing on device shows this works best to
+ // reflect the strength of the user's scrolling.
+ mEdgeAlphaFinish = Math.max(0, Math.min(velocity * VELOCITY_EDGE_FACTOR, 1));
+ // Edge should never get larger than the size of its asset.
+ mEdgeScaleYFinish = Math.max(
+ HELD_EDGE_SCALE_Y, Math.min(velocity * VELOCITY_EDGE_FACTOR, 1.f));
+
+ // Growth for the size of the glow should be quadratic to properly
+ // respond
+ // to a user's scrolling speed. The faster the scrolling speed, the more
+ // intense the effect should be for both the size and the saturation.
+ mGlowScaleYFinish = Math.min(0.025f + (velocity * (velocity / 100) * 0.00015f), 1.75f);
+ // Alpha should change for the glow as well as size.
+ mGlowAlphaFinish = Math.max(
+ mGlowAlphaStart, Math.min(velocity * VELOCITY_GLOW_FACTOR * .00001f, MAX_ALPHA));
+ }
+
+
+ /**
+ * Draw into the provided canvas. Assumes that the canvas has been rotated
+ * accordingly and the size has been set. The effect will be drawn the full
+ * width of X=0 to X=width, beginning from Y=0 and extending to some factor <
+ * 1.f of height.
+ *
+ * @param canvas Canvas to draw into
+ * @return true if drawing should continue beyond this frame to continue the
+ * animation
+ */
+ public boolean draw(GLCanvas canvas) {
+ update();
+
+ final int edgeHeight = mEdge.getIntrinsicHeight();
+ final int edgeWidth = mEdge.getIntrinsicWidth();
+ final int glowHeight = mGlow.getIntrinsicHeight();
+ final int glowWidth = mGlow.getIntrinsicWidth();
+
+ mGlow.setAlpha((int) (Math.max(0, Math.min(mGlowAlpha, 1)) * 255));
+
+ int glowBottom = (int) Math.min(
+ glowHeight * mGlowScaleY * glowHeight/ glowWidth * 0.6f,
+ glowHeight * MAX_GLOW_HEIGHT);
+ if (mWidth < mMinWidth) {
+ // Center the glow and clip it.
+ int glowLeft = (mWidth - mMinWidth)/2;
+ mGlow.setBounds(glowLeft, 0, mWidth - glowLeft, glowBottom);
+ } else {
+ // Stretch the glow to fit.
+ mGlow.setBounds(0, 0, mWidth, glowBottom);
+ }
+
+ mGlow.draw(canvas);
+
+ mEdge.setAlpha((int) (Math.max(0, Math.min(mEdgeAlpha, 1)) * 255));
+
+ int edgeBottom = (int) (edgeHeight * mEdgeScaleY);
+ if (mWidth < mMinWidth) {
+ // Center the edge and clip it.
+ int edgeLeft = (mWidth - mMinWidth)/2;
+ mEdge.setBounds(edgeLeft, 0, mWidth - edgeLeft, edgeBottom);
+ } else {
+ // Stretch the edge to fit.
+ mEdge.setBounds(0, 0, mWidth, edgeBottom);
+ }
+ mEdge.draw(canvas);
+
+ return mState != STATE_IDLE;
+ }
+
+ private void update() {
+ final long time = AnimationTime.get();
+ final float t = Math.min((time - mStartTime) / mDuration, 1.f);
+
+ final float interp = mInterpolator.getInterpolation(t);
+
+ mEdgeAlpha = mEdgeAlphaStart + (mEdgeAlphaFinish - mEdgeAlphaStart) * interp;
+ mEdgeScaleY = mEdgeScaleYStart + (mEdgeScaleYFinish - mEdgeScaleYStart) * interp;
+ mGlowAlpha = mGlowAlphaStart + (mGlowAlphaFinish - mGlowAlphaStart) * interp;
+ mGlowScaleY = mGlowScaleYStart + (mGlowScaleYFinish - mGlowScaleYStart) * interp;
+
+ if (t >= 1.f - EPSILON) {
+ switch (mState) {
+ case STATE_ABSORB:
+ mState = STATE_RECEDE;
+ mStartTime = AnimationTime.get();
+ mDuration = RECEDE_TIME;
+
+ mEdgeAlphaStart = mEdgeAlpha;
+ mEdgeScaleYStart = mEdgeScaleY;
+ mGlowAlphaStart = mGlowAlpha;
+ mGlowScaleYStart = mGlowScaleY;
+
+ // After absorb, the glow and edge should fade to nothing.
+ mEdgeAlphaFinish = 0.f;
+ mEdgeScaleYFinish = 0.f;
+ mGlowAlphaFinish = 0.f;
+ mGlowScaleYFinish = 0.f;
+ break;
+ case STATE_PULL:
+ mState = STATE_PULL_DECAY;
+ mStartTime = AnimationTime.get();
+ mDuration = PULL_DECAY_TIME;
+
+ mEdgeAlphaStart = mEdgeAlpha;
+ mEdgeScaleYStart = mEdgeScaleY;
+ mGlowAlphaStart = mGlowAlpha;
+ mGlowScaleYStart = mGlowScaleY;
+
+ // After pull, the glow and edge should fade to nothing.
+ mEdgeAlphaFinish = 0.f;
+ mEdgeScaleYFinish = 0.f;
+ mGlowAlphaFinish = 0.f;
+ mGlowScaleYFinish = 0.f;
+ break;
+ case STATE_PULL_DECAY:
+ // When receding, we want edge to decrease more slowly
+ // than the glow.
+ float factor = mGlowScaleYFinish != 0 ? 1
+ / (mGlowScaleYFinish * mGlowScaleYFinish)
+ : Float.MAX_VALUE;
+ mEdgeScaleY = mEdgeScaleYStart +
+ (mEdgeScaleYFinish - mEdgeScaleYStart) *
+ interp * factor;
+ mState = STATE_RECEDE;
+ break;
+ case STATE_RECEDE:
+ mState = STATE_IDLE;
+ break;
+ }
+ }
+ }
+
+ private static class Drawable extends ResourceTexture {
+ private Rect mBounds = new Rect();
+ private int mAlpha = 255;
+
+ public Drawable(Context context, int resId) {
+ super(context, resId);
+ }
+
+ public int getIntrinsicWidth() {
+ return getWidth();
+ }
+
+ public int getIntrinsicHeight() {
+ return getHeight();
+ }
+
+ public void setBounds(int left, int top, int right, int bottom) {
+ mBounds.set(left, top, right, bottom);
+ }
+
+ public void setAlpha(int alpha) {
+ mAlpha = alpha;
+ }
+
+ public void draw(GLCanvas canvas) {
+ canvas.save(GLCanvas.SAVE_FLAG_ALPHA);
+ canvas.multiplyAlpha(mAlpha / 255.0f);
+ Rect b = mBounds;
+ draw(canvas, b.left, b.top, b.width(), b.height());
+ canvas.restore();
+ }
+ }
+}
diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/ui/EdgeView.java b/app/src/main/java/com/hippo/ehviewer/gallery/ui/EdgeView.java
new file mode 100644
index 000000000..4676db9d6
--- /dev/null
+++ b/app/src/main/java/com/hippo/ehviewer/gallery/ui/EdgeView.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2011 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.opengl.Matrix;
+
+import com.hippo.ehviewer.gallery.glrenderer.GLCanvas;
+
+// EdgeView draws EdgeEffect (blue glow) at four sides of the view.
+public class EdgeView extends GLView {
+ @SuppressWarnings("unused")
+ private static final String TAG = "EdgeView";
+
+ public static final int INVALID_DIRECTION = -1;
+ public static final int TOP = 0;
+ public static final int LEFT = 1;
+ public static final int BOTTOM = 2;
+ public static final int RIGHT = 3;
+
+ // Each edge effect has a transform matrix, and each matrix has 16 elements.
+ // We put all the elements in one array. These constants specify the
+ // starting index of each matrix.
+ private static final int TOP_M = TOP * 16;
+ private static final int LEFT_M = LEFT * 16;
+ private static final int BOTTOM_M = BOTTOM * 16;
+ private static final int RIGHT_M = RIGHT * 16;
+
+ private EdgeEffect[] mEffect = new EdgeEffect[4];
+ private float[] mMatrix = new float[4 * 16];
+
+ public EdgeView(Context context) {
+ for (int i = 0; i < 4; i++) {
+ mEffect[i] = new EdgeEffect(context);
+ }
+ }
+
+ @Override
+ protected void onLayout(
+ boolean changeSize, int left, int top, int right, int bottom) {
+ if (!changeSize) return;
+
+ int w = right - left;
+ int h = bottom - top;
+ for (int i = 0; i < 4; i++) {
+ if ((i & 1) == 0) { // top or bottom
+ mEffect[i].setSize(w, h);
+ } else { // left or right
+ mEffect[i].setSize(h, w);
+ }
+ }
+
+ // Set up transforms for the four edges. Without transforms an
+ // EdgeEffect draws the TOP edge from (0, 0) to (w, Y * h) where Y
+ // is some factor < 1. For other edges we need to move, rotate, and
+ // flip the effects into proper places.
+ Matrix.setIdentityM(mMatrix, TOP_M);
+ Matrix.setIdentityM(mMatrix, LEFT_M);
+ Matrix.setIdentityM(mMatrix, BOTTOM_M);
+ Matrix.setIdentityM(mMatrix, RIGHT_M);
+
+ Matrix.rotateM(mMatrix, LEFT_M, 90, 0, 0, 1);
+ Matrix.scaleM(mMatrix, LEFT_M, 1, -1, 1);
+
+ Matrix.translateM(mMatrix, BOTTOM_M, 0, h, 0);
+ Matrix.scaleM(mMatrix, BOTTOM_M, 1, -1, 1);
+
+ Matrix.translateM(mMatrix, RIGHT_M, w, 0, 0);
+ Matrix.rotateM(mMatrix, RIGHT_M, 90, 0, 0, 1);
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ super.render(canvas);
+ boolean more = false;
+ for (int i = 0; i < 4; i++) {
+ canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+ canvas.multiplyMatrix(mMatrix, i * 16);
+ more |= mEffect[i].draw(canvas);
+ canvas.restore();
+ }
+ if (more) {
+ invalidate();
+ }
+ }
+
+ // Called when the content is pulled away from the edge.
+ // offset is in pixels. direction is one of {TOP, LEFT, BOTTOM, RIGHT}.
+ public void onPull(int offset, int direction) {
+ int fullLength = ((direction & 1) == 0) ? getWidth() : getHeight();
+ mEffect[direction].onPull((float)offset / fullLength);
+ if (!mEffect[direction].isFinished()) {
+ invalidate();
+ }
+ }
+
+ // Call when the object is released after being pulled.
+ public void onRelease() {
+ boolean more = false;
+ for (int i = 0; i < 4; i++) {
+ mEffect[i].onRelease();
+ more |= !mEffect[i].isFinished();
+ }
+ if (more) {
+ invalidate();
+ }
+ }
+
+ // Call when the effect absorbs an impact at the given velocity.
+ // Used when a fling reaches the scroll boundary. velocity is in pixels
+ // per second. direction is one of {TOP, LEFT, BOTTOM, RIGHT}.
+ public void onAbsorb(int velocity, int direction) {
+ mEffect[direction].onAbsorb(velocity);
+ if (!mEffect[direction].isFinished()) {
+ invalidate();
+ }
+ }
+}
diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/ui/FrameView.java b/app/src/main/java/com/hippo/ehviewer/gallery/ui/FrameView.java
index 95317cf99..db8d07740 100644
--- a/app/src/main/java/com/hippo/ehviewer/gallery/ui/FrameView.java
+++ b/app/src/main/java/com/hippo/ehviewer/gallery/ui/FrameView.java
@@ -14,8 +14,8 @@ public class FrameView extends GLView {
}
// Consider min
- maxWidth = Math.max(maxWidth, getMinimumWidth());
- maxHeight = Math.max(maxHeight, getMinimumHeight());
+ maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
+ maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
// The final
maxWidth = getDefaultSize(maxWidth, widthSpec);
diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/ui/GLRootView.java b/app/src/main/java/com/hippo/ehviewer/gallery/ui/GLRootView.java
index 3fb22c7f6..819da2e5b 100644
--- a/app/src/main/java/com/hippo/ehviewer/gallery/ui/GLRootView.java
+++ b/app/src/main/java/com/hippo/ehviewer/gallery/ui/GLRootView.java
@@ -35,11 +35,11 @@ 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.TextTexture;
import com.hippo.ehviewer.gallery.glrenderer.UploadedTexture;
import com.hippo.ehviewer.gallery.util.ApiHelper;
import com.hippo.ehviewer.gallery.util.GalleryUtils;
import com.hippo.ehviewer.gallery.util.MotionEventHelper;
-import com.hippo.ehviewer.gallery.util.Profile;
import com.hippo.yorozuya.AssertUtils;
import java.util.ArrayDeque;
@@ -72,9 +72,6 @@ public class GLRootView extends GLSurfaceView
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;
@@ -144,9 +141,6 @@ public class GLRootView extends GLSurfaceView
// Uncomment this to enable gl error check.
// setDebugFlags(DEBUG_CHECK_GL_ERROR);
-
- // Uncomment this to enable gl error check.
- // setDebugFlags(DEBUG_CHECK_GL_ERROR);
}
@Override
@@ -313,7 +307,7 @@ public class GLRootView extends GLSurfaceView
mRenderLock.unlock();
}
- if (DEBUG_FPS || DEBUG_PROFILE) {
+ if (DEBUG_FPS) {
setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
} else {
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
@@ -330,10 +324,6 @@ public class GLRootView extends GLSurfaceView
Log.i(TAG, "onSurfaceChanged: " + width + "x" + height + ", gl10: " + gl1.toString());
Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY);
GalleryUtils.setRenderThread();
- if (DEBUG_PROFILE) {
- Log.d(TAG, "Start profiling");
- Profile.enable(20); // take a sample every 20ms
- }
GL11 gl = (GL11) gl1;
AssertUtils.assertTrue(mGL == gl);
@@ -356,11 +346,9 @@ public class GLRootView extends GLSurfaceView
@Override
public void onDrawFrame(GL10 gl) {
AnimationTime.update();
- long t0;
- if (DEBUG_PROFILE_SLOW_ONLY) {
- Profile.hold();
- t0 = System.nanoTime();
- }
+
+ long t0 = System.nanoTime();
+
mRenderLock.lock();
while (mFreeze) {
@@ -388,25 +376,16 @@ public class GLRootView extends GLSurfaceView
// });
//}
- if (DEBUG_PROFILE_SLOW_ONLY) {
- long t = System.nanoTime();
- long durationInMs = (t - mLastDrawFinishTime) / 1000000;
- long durationDrawInMs = (t - t0) / 1000000;
- mLastDrawFinishTime = t;
+ long t = System.nanoTime();
+ long duration = (t - t0) / 1000000;
- if (durationInMs > 34) { // 34ms -> we skipped at least 2 frames
- Log.v(TAG, "----- SLOW (" + durationDrawInMs + "/" +
- durationInMs + ") -----");
- Profile.commit();
- } else {
- Profile.drop();
- }
+ if (duration > 34) {
+ Log.v(TAG, "--- " + duration + " ---");
}
}
private void onDrawFrameLocked(GL10 gl) {
if (DEBUG_FPS) outputFps();
-
// release the unbound textures and deleted buffers.
mCanvas.deleteRecycledResources();
@@ -549,12 +528,6 @@ public class GLRootView extends GLSurfaceView
public void onPause() {
unfreeze();
super.onPause();
- if (DEBUG_PROFILE) {
- Log.d(TAG, "Stop profiling");
- Profile.disableAll();
- Profile.dumpToFile("/sdcard/gallery.prof");
- Profile.reset();
- }
}
@Override
diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/ui/GLView.java b/app/src/main/java/com/hippo/ehviewer/gallery/ui/GLView.java
index 999b5c76b..f59a09059 100644
--- a/app/src/main/java/com/hippo/ehviewer/gallery/ui/GLView.java
+++ b/app/src/main/java/com/hippo/ehviewer/gallery/ui/GLView.java
@@ -493,8 +493,16 @@ public class GLView {
}
protected void onMeasure(int widthSpec, int heightSpec) {
- setMeasuredSize(getDefaultSize(getMinimumWidth(), widthSpec),
- getDefaultSize(getMinimumHeight(), heightSpec));
+ setMeasuredSize(getDefaultSize(getSuggestedMinimumWidth(), widthSpec),
+ getDefaultSize(getSuggestedMinimumHeight(), heightSpec));
+ }
+
+ protected int getSuggestedMinimumHeight() {
+ return getMinimumHeight();
+ }
+
+ protected int getSuggestedMinimumWidth() {
+ return getMinimumWidth();
}
/**
diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/ui/GalleryPageView.java b/app/src/main/java/com/hippo/ehviewer/gallery/ui/GalleryPageView.java
new file mode 100644
index 000000000..60de75783
--- /dev/null
+++ b/app/src/main/java/com/hippo/ehviewer/gallery/ui/GalleryPageView.java
@@ -0,0 +1,95 @@
+package com.hippo.ehviewer.gallery.ui;
+
+import android.content.Context;
+import android.graphics.Rect;
+
+import com.hippo.ehviewer.gallery.glrenderer.TextTexture;
+import com.hippo.yorozuya.LayoutUtils;
+
+public class GalleryPageView extends FrameView {
+
+ public ScaledImageView mImageView;
+ public LinearView mLinearView;
+ public TextView mIndexView;
+ public ProgressView mProgressView;
+
+ private Rect mSeen = new Rect();
+
+ private static TextTexture sTextTexture;
+
+ public static void setTextTexture(TextTexture textTexture) {
+ sTextTexture = textTexture;
+ }
+
+ public GalleryPageView(Context context) {
+ mImageView = new ScaledImageView();
+ mLinearView = new LinearView();
+ mLinearView.setInterval(LayoutUtils.dp2pix(context, 24)); // 12dp
+ mIndexView = new TextView(sTextTexture);
+ mProgressView = new ProgressView();
+ mProgressView.setMinimumWidth(LayoutUtils.dp2pix(context, 56)); // 56dp
+ mProgressView.setMinimumHeight(LayoutUtils.dp2pix(context, 56)); // 56dp
+
+ GravityLayoutParams lp = new GravityLayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT);
+ lp.gravity = Gravity.CENTER_HORIZONTAL;
+ mLinearView.addComponent(mIndexView, lp);
+ lp = new GravityLayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT);
+ lp.gravity = Gravity.CENTER_HORIZONTAL;
+ mLinearView.addComponent(mProgressView, lp);
+
+ lp = new GravityLayoutParams(LayoutParams.MATCH_PARENT,
+ LayoutParams.MATCH_PARENT);
+ addComponent(mImageView, lp);
+
+ lp = new GravityLayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT);
+ lp.gravity = Gravity.CENTER;
+ addComponent(mLinearView, lp);
+ }
+
+ @SuppressWarnings("ResourceType")
+ private void updateSeen() {
+ Rect bounds = bounds();
+ Rect parentBounds = mParent.bounds();
+ mSeen.set(bounds);
+ if (!mSeen.intersect(0, 0, parentBounds.width(), parentBounds.height())) {
+ mSeen.set(0, 0, 0, 0);
+ } else {
+ mSeen.offset(-bounds.left, -bounds.top);
+ }
+ mImageView.setSeen(mSeen);
+ }
+
+ @Override
+ protected void onLayout(boolean changeSize, int left, int top, int right, int bottom) {
+ super.onLayout(changeSize, left, top, right, bottom);
+ updateSeen();
+ }
+
+ @Override
+ public void offsetLeftAndRight(int offset) {
+ super.offsetLeftAndRight(offset);
+ updateSeen();
+ }
+
+ @Override
+ public void offsetTopAndBottom(int offset) {
+ super.offsetTopAndBottom(offset);
+ updateSeen();
+ }
+
+ public void setScaleOffset(GalleryView.Scale scale,
+ GalleryView.StartPosition startPosition, float lastScale) {
+ mImageView.setScaleOffset(scale, startPosition, lastScale);
+ }
+
+ public void scroll(int dx, int dy, int[] remain) {
+ mImageView.scroll(dx, dy, remain);
+ }
+
+ public void scale(float focusX, float focusY, float scale) {
+ mImageView.scale(focusX, focusY, scale);
+ }
+}
diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/ui/GalleryView.java b/app/src/main/java/com/hippo/ehviewer/gallery/ui/GalleryView.java
new file mode 100644
index 000000000..da003b53d
--- /dev/null
+++ b/app/src/main/java/com/hippo/ehviewer/gallery/ui/GalleryView.java
@@ -0,0 +1,760 @@
+package com.hippo.ehviewer.gallery.ui;
+
+import android.content.Context;
+import android.util.SparseArray;
+import android.view.MotionEvent;
+import android.view.animation.Interpolator;
+
+import com.hippo.ehviewer.R;
+import com.hippo.ehviewer.gallery.anim.Animation;
+import com.hippo.ehviewer.gallery.glrenderer.GLCanvas;
+import com.hippo.ehviewer.gallery.util.GalleryUtils;
+import com.hippo.yorozuya.IntArray;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class GalleryView extends GLView implements GestureRecognizer.Listener {
+
+ private static final String TAG = GalleryView.class.getSimpleName();
+
+ private static final int NO_POSITION = -1;
+
+ private static final int MAX_SETTLE_DURATION = 600; // ms
+
+ private static final int UNKNOWN_SEEN = 0;
+ private static final int SEEN = 1;
+ private static final int UNSEEN = 2;
+
+ private boolean mEnableRequestLayout = true;
+
+ private GestureRecognizer mGestureRecognizer;
+
+ private Adapter mAdapter;
+
+ private int mFirstShownIndex = 0;
+ private int mLayoutOffset = 0;
+
+ private SparseArray mAttachedPage = new SparseArray<>();
+ private List mInvalidAttachedComponent = new ArrayList<>();
+ private IntArray mMissedPageInfo = new IntArray(5 * 3);
+ private List mUsedPage = new ArrayList<>();
+ private SparseArray mPageMap = new SparseArray<>();
+
+ private EdgeView mEdgeView;
+
+ private boolean mScale = false;
+ private boolean mScroll = false;
+
+ private int mDeltaX;
+ private int[] mScrollRemain = new int[2];
+
+ private boolean mHaveChangePage = false;
+
+ private Mode mMode = Mode.NONE;
+ private Mode mLastLayoutMode = Mode.NONE;
+
+ private Scale mScaleMode = Scale.FIT_HEIGHT; // TODO
+ private StartPosition mStartPosition = StartPosition.TOP_RIGHT; // TODO
+ private float mLastScale = 2f;
+
+ private int mProgressSpec = MeasureSpec.makeMeasureSpec(48, MeasureSpec.EXACTLY);
+ public int mInterval = 24;
+
+ private SmoothScroller mSmoothScroller;
+
+ private static final Interpolator sInterpolator = new Interpolator() {
+
+ @Override
+ public float getInterpolation(float t) {
+ t -= 1.0f;
+ return t * t * t * t * t + 1.0f;
+ }
+ };
+
+ public enum Mode {
+ NONE, // Just a progress view
+ LEFT_TO_RIGHT,
+ RIGHT_TO_LEFT,
+ TOP_TO_BOTTOM
+ }
+
+ public enum Scale {
+ ORIGIN (0),
+ FIT_WIDTH (1),
+ FIT_HEIGHT (2),
+ FIT (3),
+ FIXED (4);
+
+ Scale(int ni) {
+ nativeInt = ni;
+ }
+ final int nativeInt;
+ }
+
+ public enum StartPosition {
+ TOP_LEFT,
+ TOP_RIGHT,
+ BOTTOM_LEFT,
+ BOTTOM_RIGHT,
+ CENTER
+ }
+
+ public GalleryView(Context context) {
+ mEdgeView = new EdgeView(context);
+ mGestureRecognizer = new GestureRecognizer(context, this);
+ mSmoothScroller = new SmoothScroller();
+ mSmoothScroller.setInterpolator(sInterpolator);
+
+ setBackgroundColor(GalleryUtils.intColorToFloatARGBArray(context.getResources().getColor(R.color.gallery_background)));
+ }
+
+ @Override
+ protected void onAttachToRoot(GLRoot root) {
+ super.onAttachToRoot(root);
+
+ mEdgeView.onAttachToRoot(root);
+ }
+
+ @Override
+ protected void onDetachFromRoot() {
+ super.onDetachFromRoot();
+
+ mEdgeView.onDetachFromRoot();
+ }
+
+ public void setMode(Mode mode) {
+ if (mMode != mode) {
+ mMode = mode;
+ superRequestLayout();
+ }
+ }
+
+ public void setAdapter(Adapter adapter) {
+ mAdapter = adapter;
+ }
+
+ public void setProgressSize(int size) {
+ mProgressSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
+ superRequestLayout();
+ }
+
+ public void setInterval(int interval) {
+ mInterval = interval;
+ superRequestLayout();
+ }
+
+ public void superRequestLayout() {
+ super.requestLayout();
+ }
+
+ public void scrollToPage(int page) {
+ if (page < 0 || page >= mAdapter.getPages()) {
+ return;
+ }
+
+ mFirstShownIndex = page;
+ mLayoutOffset = 0;
+ fill();
+ }
+
+ public void smoothScrollToPage(int page) {
+
+ }
+
+ private void scrollX(int offset) {
+ if (offset == 0 || (mMode != Mode.LEFT_TO_RIGHT && mMode != Mode.RIGHT_TO_LEFT)) {
+ return;
+ }
+ offChildsetLeftAndRight(offset); // TODO Really need it ?
+
+ mLayoutOffset += offset;
+ boolean reverse = mMode == Mode.RIGHT_TO_LEFT;
+
+ // Fix TODO What if offset is very large
+ int width = getWidth();
+ if (!mHaveChangePage) {
+ if (mLayoutOffset > mInterval) {
+ mHaveChangePage = true;
+ mFirstShownIndex += reverse ? 1 : -1;
+ mLayoutOffset = -width + mLayoutOffset - mInterval;
+ } else if (mLayoutOffset < -mInterval) {
+ mHaveChangePage = true;
+ mFirstShownIndex += reverse ? -1 : 1;
+ mLayoutOffset = width + mLayoutOffset + mInterval;
+ }
+ } else {
+ if (mLayoutOffset > width) {
+ mHaveChangePage = false;
+ mFirstShownIndex += reverse ? 1 : -1;
+ mLayoutOffset = mLayoutOffset - width - mInterval;
+ } else if (mLayoutOffset < -width) {
+ mHaveChangePage = false;
+ mFirstShownIndex += reverse ? -1 : 1;
+ mLayoutOffset = mLayoutOffset + width + mInterval;
+ } else if (mLayoutOffset < mInterval && mLayoutOffset > -mInterval) {
+ mHaveChangePage = false;
+ }
+ }
+
+ fill();
+ }
+
+ @Override
+ protected boolean dispatchTouchEvent(MotionEvent event) {
+ // Do not pass event to component, so handle event here
+ mGestureRecognizer.onTouchEvent(event);
+ return true;
+ }
+
+ @Override
+ public boolean onSingleTapUp(float x, float y) {
+ return false;
+ }
+
+ @Override
+ public boolean onDoubleTap(float x, float y) {
+ return false;
+ }
+
+ // Try to scroll to make mLayoutOffset == 0
+ private int onScrollX1(int dx, int totalX) {
+ int remain;
+ int actualDx;
+ if (mLayoutOffset > 0 && dx > 0) {
+ remain = dx - mLayoutOffset;
+ if (remain > 0) {
+ // Get remain
+ actualDx = dx - remain;
+ } else {
+ // No remain
+ remain = 0;
+ actualDx = dx;
+ }
+ } else if (mLayoutOffset < 0 && dx < 0) {
+ remain = dx - mLayoutOffset;
+ if (remain < 0) {
+ // Get remain
+ actualDx = dx - remain;
+ } else {
+ // No remain
+ remain = 0;
+ actualDx = dx;
+ }
+ } else {
+ remain = 0;
+ actualDx = dx;
+ }
+
+ onScrollX2(actualDx, totalX);
+
+ return remain;
+ }
+
+ private int onScrollX2(int dx, int totalX) {
+ int remain = 0;
+ if (mMode == Mode.LEFT_TO_RIGHT) {
+ if (mFirstShownIndex == 0 && mLayoutOffset - dx > 0) { // Start
+ scrollX(-mLayoutOffset);
+ remain = dx - mLayoutOffset;
+ } else if (mFirstShownIndex == mAdapter.getPages() - 1 && mLayoutOffset - dx < 0) { // end
+ scrollX(-mLayoutOffset);
+ remain = dx - mLayoutOffset;
+ } else {
+ scrollX(-dx);
+ }
+ } else if (mMode == Mode.RIGHT_TO_LEFT) {
+ if (mFirstShownIndex == 0 && mLayoutOffset - dx < 0) { // Start
+ scrollX(-mLayoutOffset);
+ remain = dx - mLayoutOffset;
+ } else if (mFirstShownIndex == mAdapter.getPages() - 1 && mLayoutOffset - dx > 0) { // end
+ scrollX(-mLayoutOffset);
+ remain = dx - mLayoutOffset;
+ } else {
+ scrollX(-dx);
+ }
+ }
+ return remain;
+ }
+
+ @Override
+ public boolean onScroll(float dx, float dy, float totalX, float totalY) {
+ if (mScale) {
+ return false;
+ }
+ mScroll = true;
+
+ int dxI = (int) dx;
+ int dyI = (int) dy;
+ int totalXI = (int) totalX;
+ if (mMode == Mode.LEFT_TO_RIGHT || mMode == Mode.RIGHT_TO_LEFT) {
+ int remain = dxI;
+ if (remain != 0 && mLayoutOffset != 0) {
+ remain = onScrollX1(dxI, totalXI);
+ }
+ if (remain != 0 && mLayoutOffset == 0) {
+ GalleryPageView page = mPageMap.get(mFirstShownIndex);
+ if (page != null) {
+ page.scroll(remain, dyI, mScrollRemain);
+ remain = mScrollRemain[0];
+ }
+ }
+ if (remain != 0) {
+ remain = onScrollX2(dxI, totalXI);
+ }
+
+ if (remain == 0) {
+ if (mDeltaX != 0) {
+ mDeltaX = 0;
+ mEdgeView.onRelease();
+ }
+ } else if (remain < 0) {
+ if (mDeltaX > 0) {
+ mDeltaX = 0;
+ mEdgeView.onRelease();
+ }
+ mDeltaX += remain;
+ mEdgeView.onPull(-mDeltaX, EdgeView.LEFT);
+ } else {
+ if (mDeltaX < 0) {
+ mDeltaX = 0;
+ mEdgeView.onRelease();
+ }
+ mDeltaX += remain;
+ mEdgeView.onPull(mDeltaX, EdgeView.RIGHT);
+ }
+
+ } else if (mMode == Mode.TOP_TO_BOTTOM) {
+ // TODO
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ return false;
+ }
+
+ @Override
+ public boolean onScaleBegin(float focusX, float focusY) {
+ return onScale(focusX, focusY, 1.0f);
+ }
+
+ private boolean canScale() {
+ if (mMode != Mode.LEFT_TO_RIGHT && mMode != Mode.RIGHT_TO_LEFT) {
+ return false;
+ }
+ if (mLayoutOffset != 0) {
+ return false;
+ }
+ if (mScroll) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onScale(float focusX, float focusY, float scale) {
+ if (!canScale()) {
+ return false;
+ }
+ mScale = true;
+
+ GalleryPageView page = mPageMap.get(mFirstShownIndex);
+ if (page != null) {
+ page.scale(focusX, focusY, scale);
+ }
+
+ return true;
+ }
+
+ @Override
+ public void onScaleEnd() {
+
+ }
+
+ @Override
+ public void onDown(float x, float y) {
+ mScale = false;
+ mScroll = false;
+ mSmoothScroller.forceStop();
+ }
+
+ @Override
+ public void onUp() {
+ mDeltaX = 0;
+ mEdgeView.onRelease();
+
+ if ((mMode == Mode.LEFT_TO_RIGHT || mMode == Mode.RIGHT_TO_LEFT) && mLayoutOffset != 0) {
+ // Scroll
+ final float pageDelta = 7 * (float) Math.abs(mLayoutOffset) / (getWidth() + mInterval);
+ int duration = (int) ((pageDelta + 1) * 100);
+ duration = Math.min(duration, MAX_SETTLE_DURATION);
+ mSmoothScroller.startSmoothScroll(-mLayoutOffset, 0, duration);
+ } else if (mMode == Mode.TOP_TO_BOTTOM) {
+ // TODO
+ }
+
+
+
+
+ }
+
+ @Override
+ public void onPointerDown(float x, float y) {
+ if (canScale()) {
+ mScale = true;
+ }
+ }
+
+ @Override
+ public void onPointerUp() {
+
+ }
+
+ private void offChildsetLeftAndRight(int offset) {
+ for (int i = 0, n = getComponentCount(); i < n; i++) {
+ getComponent(i).offsetLeftAndRight(offset);
+ }
+ }
+
+ private void offChildsetTopAndBottom(int offset) {
+ for (int i = 0, n = getComponentCount(); i < n; i++) {
+ getComponent(i).offsetTopAndBottom(offset);
+ }
+ }
+
+ @Override
+ public void requestLayout() {
+ if (mEnableRequestLayout) {
+ super.requestLayout();
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changeSize, int left, int top, int right, int bottom) {
+ mSmoothScroller.forceStop();
+ mEdgeView.layout(left, top, right, bottom);
+
+ /* TODO set all seen UNKNOWN_SEEN to setScaleAndOffset
+ if (mLastLayoutMode != mMode) {
+ mLastLayoutMode = mMode;
+ for () {
+
+ }
+ }
+ */
+
+
+ fill();
+ }
+
+ public GalleryPageView getPage(int index) {
+ return mPageMap.get(index);
+ }
+
+ private void unbindGalleryPage(GalleryPageView view) {
+ LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ int index = lp.index;
+ lp.index = NO_POSITION;
+ if (index != NO_POSITION) {
+ mAdapter.unbindPage(view, index);
+ }
+ }
+
+ private void attachePage(GalleryPageView page, int index) {
+ mPageMap.put(index, page);
+ }
+
+ private void detachPage(int index) {
+ mPageMap.remove(index);
+ }
+
+ private void detachPage(GalleryPageView page) {
+ LayoutParams lp = (LayoutParams) page.getLayoutParams();
+ int index = lp.index;
+ lp.index = NO_POSITION;
+ if (index != NO_POSITION) {
+ mPageMap.remove(index);
+ }
+ }
+
+
+ private void fillX(int start, int end, int startIndex, boolean reverse) {
+ for (int i = 0, n = getComponentCount(); i < n; i++) {
+ GLView component = getComponent(i);
+ if (component instanceof GalleryPageView) {
+ mAttachedPage.put(((LayoutParams) component.getLayoutParams()).index,
+ (GalleryPageView) component);
+ } else {
+ mInvalidAttachedComponent.add(component);
+ }
+ }
+
+ // Remove invalid view
+ int n = mInvalidAttachedComponent.size();
+ while (n-- > 0) {
+ removeComponentAt(n);
+ }
+ mInvalidAttachedComponent.clear();
+
+ // Fill
+ int width = getWidth();
+ int height = getHeight();
+ int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
+ int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
+ int offset = width + mInterval;
+ if (reverse) {
+ offset = -offset;
+ }
+ int pages = mAdapter.getPages();
+ for (int spec = start, i = startIndex; reverse ? spec > end : spec < end && i < pages; i++) {
+ GalleryPageView page = mAttachedPage.get(i);
+ if (page != null) {
+ mAttachedPage.remove(i);
+ page.measure(widthSpec, heightSpec);
+ if (reverse) {
+ page.layout(spec - width, 0, spec, height);
+ } else {
+ page.layout(spec, 0, spec + width, height);
+ }
+ } else {
+ // Add index, left, top, right, bottom
+ mMissedPageInfo.add(i);
+ if (reverse) {
+ mMissedPageInfo.add(spec - width);
+ mMissedPageInfo.add(0);
+ mMissedPageInfo.add(spec);
+ mMissedPageInfo.add(height);
+ } else {
+ mMissedPageInfo.add(spec);
+ mMissedPageInfo.add(0);
+ mMissedPageInfo.add(spec + width);
+ mMissedPageInfo.add(height);
+ }
+ }
+ spec += offset;
+ }
+
+ // Fill missed page
+ int count = mAttachedPage.size();
+ n = mMissedPageInfo.size();
+ for (int i = 0; i < n; i += 5) {
+ int index = mMissedPageInfo.get(i);
+ GalleryPageView page;
+ if (i < count) {
+ page = mAttachedPage.valueAt(i);
+ // Unbind
+ detachPage(page);
+ unbindGalleryPage(page);
+ mUsedPage.add(page);
+ } else {
+ page = mAdapter.createPage();
+ addComponent(page);
+ }
+ // Bind
+ ((LayoutParams) page.getLayoutParams()).index = index;
+ ((LayoutParams) page.getLayoutParams()).seen = UNKNOWN_SEEN;
+ attachePage(page, index);
+ // Update index in LayoutParams
+ mAdapter.bindPage(page, index);
+ page.measure(widthSpec, heightSpec);
+ page.layout(mMissedPageInfo.get(i + 1), mMissedPageInfo.get(i + 2),
+ mMissedPageInfo.get(i + 3), mMissedPageInfo.get(i + 4));
+
+ }
+ mMissedPageInfo.clear();
+
+ // Clear unuse page
+ for (int i = 0; i < count; i++) {
+ GalleryPageView page = mAttachedPage.valueAt(i);
+ if (mUsedPage.contains(page)) {
+ continue;
+ }
+ detachPage(page);
+ unbindGalleryPage(page);
+ removeComponent(page);
+ }
+ mAttachedPage.clear();
+ mUsedPage.clear();
+
+ // Update seen
+ for (int i = 0; i < getComponentCount(); i++) {
+ GalleryPageView page = (GalleryPageView) getComponent(i);
+ LayoutParams lp = ((LayoutParams) page.getLayoutParams());
+ int oldSeen = lp.seen;
+ int seen = page.bounds().intersects(0, 0, width, height) ? SEEN : UNSEEN;
+ lp.seen = seen;
+ if (oldSeen == UNKNOWN_SEEN || (oldSeen == SEEN && seen == UNSEEN)) {
+ page.setScaleOffset(mScaleMode, mStartPosition, mLastScale);
+ }
+ }
+ }
+
+ private void fillY(int start, int end, int startIndex, boolean reverse) {
+
+ }
+
+ private void fill() {
+ // Disable request layout
+ mEnableRequestLayout = false;
+
+ if (mMode == Mode.NONE) {
+ // Try to get old ProgressView
+ ProgressView progressView = null;
+ if (getComponentCount() == 1) {
+ GLView component = getComponent(0);
+ if (component instanceof ProgressView) {
+ progressView = (ProgressView) component;
+ }
+ }
+ if (progressView == null) {
+ progressView = new ProgressView();
+ }
+
+ // Remove all component and unbind
+ int i = getComponentCount();
+ while (i-- > 0) {
+ GLView component = getComponent(i);
+ removeComponentAt(i);
+
+ // Remove from map, unbind gallery page
+ if (component instanceof GalleryPageView) {
+ detachPage((GalleryPageView) component);
+ unbindGalleryPage((GalleryPageView) component);
+ }
+ }
+
+ addComponent(progressView);
+
+ // Measure and layout progress
+ progressView.measure(mProgressSpec, mProgressSpec);
+ int progressWidth = progressView.getMeasuredWidth();
+ int progressHeight = progressView.getMeasuredHeight();
+ int progressLeft = getWidth() / 2 - progressWidth / 2;
+ int progressTop = getHeight() / 2 - progressHeight / 2;
+ progressView.layout(progressLeft, progressTop, progressLeft + progressWidth, progressTop + progressHeight);
+
+ // Bind progress view, just setIndeterminate(true);
+ progressView.setIndeterminate(true);
+ } else if (mMode == Mode.LEFT_TO_RIGHT || mMode == Mode.RIGHT_TO_LEFT) {
+ int width = getWidth();
+ int start;
+ int end;
+ int startIndex;
+ boolean reverse = mMode == Mode.RIGHT_TO_LEFT;
+
+ if (reverse) {
+ if (mFirstShownIndex == 0) {
+ start = width;
+ startIndex = 0;
+ } else {
+ startIndex = mFirstShownIndex - 1;
+ start = width * 2 + mInterval;
+ }
+ if (mFirstShownIndex == mAdapter.getPages() - 1) {
+ end = 0;
+ } else {
+ end = -width - mInterval * 2 - 1;
+ }
+ } else {
+ if (mFirstShownIndex == 0) {
+ start = 0;
+ startIndex = 0;
+ } else {
+ startIndex = mFirstShownIndex - 1;
+ start = -width - mInterval;
+ }
+ if (mFirstShownIndex == mAdapter.getPages() - 1) {
+ end = width;
+ } else {
+ end = width * 2 + mInterval * 2 + 1;
+ }
+ }
+
+ // Set offset
+ start += mLayoutOffset;
+
+ fillX(start, end, startIndex, reverse);
+ }
+
+ mEnableRequestLayout = true;
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ if (mSmoothScroller.calculate(AnimationTime.get())) {
+ invalidate();
+ }
+
+ super.render(canvas);
+ mEdgeView.render(canvas);
+ }
+
+ @Override
+ protected boolean checkLayoutParams(GLView.LayoutParams p) {
+ return p instanceof LayoutParams;
+ }
+
+ @Override
+ protected GLView.LayoutParams generateDefaultLayoutParams() {
+ return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ }
+
+ @Override
+ protected GLView.LayoutParams generateLayoutParams(GLView.LayoutParams p) {
+ return p == null ? generateDefaultLayoutParams() : new LayoutParams(p);
+ }
+
+ public static class LayoutParams extends GLView.LayoutParams {
+
+ private int index = NO_POSITION;
+ private int seen = UNKNOWN_SEEN;
+
+ public LayoutParams(GLView.LayoutParams source) {
+ super(source);
+ }
+
+ public LayoutParams(int width, int height) {
+ super(width, height);
+ }
+ }
+
+ public static abstract class Adapter {
+
+ public abstract int getPages();
+
+ public abstract GalleryPageView createPage();
+
+ public abstract void bindPage(GalleryPageView view, int index);
+
+ public abstract void unbindPage(GalleryPageView view, int index);
+ }
+
+ public class SmoothScroller extends Animation {
+
+ private int mDx;
+ private int mDy;
+ private int mLastX;
+ private int mLastY;
+
+ public void startSmoothScroll(int dx, int dy, int duration) {
+ mDx = dx;
+ mDy = dy;
+ mLastX = 0;
+ mLastY = 0;
+ setDuration(duration);
+ start();
+ invalidate();
+ }
+
+ @Override
+ protected void onCalculate(float progress) {
+ int x = (int) (mDx * progress);
+ int y = (int) (mDy * progress);
+ scrollX(x - mLastX);
+ mLastX = x;
+ }
+ }
+}
diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/ui/GestureRecognizer.java b/app/src/main/java/com/hippo/ehviewer/gallery/ui/GestureRecognizer.java
new file mode 100644
index 000000000..338f145b6
--- /dev/null
+++ b/app/src/main/java/com/hippo/ehviewer/gallery/ui/GestureRecognizer.java
@@ -0,0 +1,145 @@
+/*
+ * 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.support.v4.view.GestureDetectorCompat;
+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 onScroll(float dx, float dy, float totalX, float totalY);
+ 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();
+ void onPointerDown(float x, float y);
+ void onPointerUp();
+ }
+
+ private final GestureDetectorCompat mGestureDetector;
+ private final ScaleGestureDetector mScaleDetector;
+ private final DownUpDetector mDownUpDetector;
+ private final Listener mListener;
+
+ public GestureRecognizer(Context context, Listener listener) {
+ mListener = listener;
+ mGestureDetector = new GestureDetectorCompat(context, new MyGestureListener(),
+ null /* ignoreMultitouch */);
+ mScaleDetector = new ScaleGestureDetector(
+ context, new MyScaleListener());
+ mDownUpDetector = new DownUpDetector(new MyDownUpListener());
+ }
+
+ public void onTouchEvent(MotionEvent event) {
+ mScaleDetector.onTouchEvent(event);
+ mGestureDetector.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 {
+ @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) {
+ 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();
+ }
+
+ @Override
+ public void onPointerDown(MotionEvent e) {
+ mListener.onPointerDown(e.getX(), e.getY());
+ }
+
+ @Override
+ public void onPointerUp(MotionEvent e) {
+ mListener.onPointerUp();
+ }
+ }
+}
diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/ui/ScaledImageView.java b/app/src/main/java/com/hippo/ehviewer/gallery/ui/ScaledImageView.java
new file mode 100644
index 000000000..ff4db1f4d
--- /dev/null
+++ b/app/src/main/java/com/hippo/ehviewer/gallery/ui/ScaledImageView.java
@@ -0,0 +1,325 @@
+package com.hippo.ehviewer.gallery.ui;
+
+import android.graphics.Rect;
+import android.graphics.RectF;
+
+import com.hippo.ehviewer.gallery.glrenderer.GLCanvas;
+import com.hippo.ehviewer.gallery.glrenderer.TiledTexture;
+import com.hippo.yorozuya.MathUtils;
+
+public class ScaledImageView extends GLView {
+
+ // TODO adjust scale max and min accroding to image size and screen size
+ private static final float SCALE_MIN = 1 / 10.0f;
+ private static final float SCALE_MAX = 10.0f;
+
+ private TiledTexture mTiledTexture;
+
+ // Corresponding source is [0, 0, mTiledTexture.getWidth(), mTiledTexture.getHeight()]
+ private RectF mTarget = new RectF();
+ private RectF mSeen = new RectF();
+ private RectF mSourceAfterSeen = new RectF();
+ private RectF mTargetAfterSeen = new RectF();
+ private boolean mScaleOffsetDirty = true;
+ private boolean mSeenDirty = true;
+
+ private GalleryView.Scale mScaleMode;
+ private GalleryView.StartPosition mStartPosition;
+ private float mLastScale;
+ private float mScale;
+
+ public void setTiledTexture(TiledTexture tiledTexture) {
+ mTiledTexture = tiledTexture;
+ setScaleOffset(mScaleMode, mStartPosition, mLastScale);
+ invalidate();
+ }
+
+ public TiledTexture getTiledTexture() {
+ return mTiledTexture;
+ }
+
+ @Override
+ protected int getSuggestedMinimumWidth() {
+ return Math.max(super.getSuggestedMinimumWidth(),
+ mTiledTexture == null ? 0 : mTiledTexture.getWidth());
+ }
+
+ @Override
+ protected int getSuggestedMinimumHeight() {
+ return Math.max(super.getSuggestedMinimumHeight(),
+ mTiledTexture == null ? 0 : mTiledTexture.getHeight());
+ }
+
+ /**
+ *
+ * If target is shorter then screen, make it in screen center. If target is
+ * longer then parent, make sure target fill parent over
+ */
+ private void adjustPosition() {
+ RectF target = mTarget;
+ int screenWidth = getWidth();
+ int screenHeight = getHeight();
+ float targetWidth = target.width();
+ float targetHeight = target.height();
+
+ if (targetWidth > screenWidth) {
+ float fixXOffset = target.left;
+ if (fixXOffset > 0) {
+ target.left -= fixXOffset;
+ target.right -= fixXOffset;
+ } else if ((fixXOffset = screenWidth - target.right) > 0) {
+ target.left += fixXOffset;
+ target.right += fixXOffset;
+ }
+ } else {
+ float left = (screenWidth - targetWidth) / 2;
+ target.offsetTo(left, target.top);
+ }
+ if (targetHeight > screenHeight) {
+ float fixYOffset = target.top;
+ if (fixYOffset > 0) {
+ target.top -= fixYOffset;
+ target.bottom -= fixYOffset;
+ } else if ((fixYOffset = screenHeight - target.bottom) > 0) {
+ target.top += fixYOffset;
+ target.bottom += fixYOffset;
+ }
+ } else {
+ float top = (screenHeight - targetHeight) / 2;
+ target.offsetTo(target.left, top);
+ }
+ }
+
+ public void setScaleOffset(GalleryView.Scale scale,
+ GalleryView.StartPosition startPosition, float lastScale) {
+ mScaleMode = scale;
+ mStartPosition = startPosition;
+ mLastScale = lastScale;
+
+ int screenWidth = getWidth();
+ int screenHeight = getHeight();
+
+ if (mTiledTexture == null || screenWidth == 0 || screenHeight == 0) {
+ mScaleOffsetDirty = true;
+ return;
+ }
+
+ int imageWidth = mTiledTexture.getWidth();
+ int imageHeight = mTiledTexture.getHeight();
+
+ // Set scale
+ float targetWidth;
+ float targetHeight;
+ switch (scale) {
+ case ORIGIN:
+ mScale = 1.0f;
+ targetWidth = imageWidth;
+ targetHeight = imageHeight;
+ break;
+ case FIT_WIDTH:
+ mScale = (float) screenWidth / imageWidth;
+ targetWidth = screenWidth;
+ targetHeight = imageHeight * mScale;
+ break;
+ case FIT_HEIGHT:
+ mScale = (float) screenHeight / imageHeight;
+ targetWidth = imageWidth * mScale;
+ targetHeight = screenHeight;
+ break;
+ case FIT:
+ float scaleX = (float) screenWidth / imageWidth;
+ float scaleY = (float) screenHeight / imageHeight;
+ if (scaleX < scaleY) {
+ mScale = scaleX;
+ targetWidth = screenWidth;
+ targetHeight = imageHeight * scaleX;
+ } else {
+ mScale = scaleY;
+ targetWidth = imageWidth * scaleY;
+ targetHeight = screenHeight;
+ break;
+ }
+ break;
+ case FIXED:
+ default:
+ mScale = lastScale;
+ targetWidth = imageWidth * lastScale;
+ targetHeight = imageHeight * lastScale;
+ break;
+ }
+
+ // adjust scale, not too big, not too small
+ if (mScale < SCALE_MIN) {
+ mScale = SCALE_MIN;
+ targetWidth = imageWidth * SCALE_MIN;
+ targetHeight = imageHeight * SCALE_MIN;
+ } else if (mScale > SCALE_MAX) {
+ mScale = SCALE_MAX;
+ targetWidth = imageWidth * SCALE_MAX;
+ targetHeight = imageHeight * SCALE_MAX;
+ }
+
+ RectF target = mTarget;
+ // set start position
+ switch (startPosition) {
+ case TOP_LEFT:
+ target.left = 0;
+ target.top = 0;
+ break;
+ case TOP_RIGHT:
+ target.left = screenWidth - targetWidth;
+ target.top = 0;
+ break;
+ case BOTTOM_LEFT:
+ target.left = 0;
+ target.top = screenHeight - targetHeight;
+ break;
+ case BOTTOM_RIGHT:
+ target.left = screenWidth - targetWidth;
+ target.top = screenHeight - targetHeight;
+ break;
+ case CENTER:
+ default:
+ target.left = (screenWidth - targetWidth) / 2;
+ target.top = (screenHeight - targetHeight) / 2;
+ break;
+ }
+
+ target.right = target.left + targetWidth;
+ target.bottom = target.top + targetHeight;
+
+ // adjust position
+ adjustPosition();
+
+ mScaleOffsetDirty = false;
+ mSeenDirty = true;
+ }
+
+ public void scroll(int dx, int dy, int[] remain) {
+ if (mTiledTexture == null) {
+ remain[0] = dx;
+ remain[1] = dy;
+ return;
+ }
+
+ RectF target = mTarget;
+ int screenWidth = getWidth();
+ int screenHeight = getHeight();
+ float targetWidth = target.width();
+ float targetHeight = target.height();
+
+ if (targetWidth > screenWidth) {
+ target.offset(-dx, 0);
+
+ float fixXOffset = target.left;
+ if (fixXOffset > 0) {
+ target.left -= fixXOffset;
+ target.right -= fixXOffset;
+ remain[0] = -(int) fixXOffset;
+ } else if ((fixXOffset = screenWidth - target.right) > 0) {
+ target.left += fixXOffset;
+ target.right += fixXOffset;
+ remain[0] = (int) fixXOffset;
+ } else {
+ remain[0] = 0;
+ }
+ } else {
+ remain[0] = dx;
+ }
+ if (targetHeight > screenHeight) {
+ target.offset(0, -dy);
+
+ float fixYOffset = target.top;
+ if (fixYOffset > 0) {
+ target.top -= fixYOffset;
+ target.bottom -= fixYOffset;
+ remain[1] = -(int) fixYOffset;
+ } else if ((fixYOffset = screenHeight - target.bottom) > 0) {
+ target.top += fixYOffset;
+ target.bottom += fixYOffset;
+ remain[1] = (int) fixYOffset;
+ } else {
+ remain[1] = 0;
+ }
+ } else {
+ remain[1] = dy;
+ }
+
+ if (dx != remain[0] || dy != remain[1]) {
+ mSeenDirty = true;
+ invalidate();
+ }
+ }
+
+ public void scale(float focusX, float focusY, float scale) {
+ if (mTiledTexture == null) {
+ return;
+ }
+
+ if ((mScale == SCALE_MAX && scale > 1f) || (mScale == SCALE_MIN && scale < 1f)) {
+ return;
+ }
+
+ float newScale = mScale * scale;
+ newScale = MathUtils.clamp(newScale, SCALE_MIN, SCALE_MAX);
+ mScale = newScale;
+ RectF target = mTarget;
+ float left = (focusX - ((focusX - target.left) * scale));
+ float top = (focusY - ((focusY - target.top) * scale));
+ target.set(left, top,
+ (left + (mTiledTexture.getWidth() * newScale)),
+ (top + (mTiledTexture.getHeight() * newScale)));
+
+ // adjust position
+ adjustPosition();
+
+ mSeenDirty = true;
+ invalidate();
+ }
+
+ public void setSeen(Rect seen) {
+ mSeen.set(seen);
+ mSeenDirty = true;
+ }
+
+ public void applySeen() {
+ int width = mTiledTexture.getWidth();
+ int height = mTiledTexture.getHeight();
+ RectF target = mTarget;
+ RectF targetAfterSeen = mTargetAfterSeen;
+ RectF sourceAfterSeen = mSourceAfterSeen;
+
+ targetAfterSeen.set(target);
+ if (targetAfterSeen.intersect(mSeen)) {
+ sourceAfterSeen.left = MathUtils.lerp(0, width,
+ MathUtils.delerp(target.left, target.right, targetAfterSeen.left));
+ sourceAfterSeen.right = MathUtils.lerp(0, width,
+ MathUtils.delerp(target.left, target.right, targetAfterSeen.right));
+ sourceAfterSeen.top = MathUtils.lerp(0, height,
+ MathUtils.delerp(target.top, target.bottom, targetAfterSeen.top));
+ sourceAfterSeen.bottom = MathUtils.lerp(0, height,
+ MathUtils.delerp(target.top, target.bottom, targetAfterSeen.bottom));
+ } else {
+ // Can't be seen
+ sourceAfterSeen.set(0f, 0f, 0f, 0f);
+ }
+ }
+
+ @Override
+ protected void onRender(GLCanvas canvas) {
+ if (mTiledTexture != null) {
+
+ if (mScaleOffsetDirty) {
+ setScaleOffset(mScaleMode, mStartPosition, mLastScale);
+ }
+ if (mSeenDirty) {
+ applySeen();
+ mSeenDirty = false;
+ }
+ // Draw
+ if (!mSourceAfterSeen.isEmpty()) {
+ mTiledTexture.draw(canvas, mSourceAfterSeen, mTargetAfterSeen);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/ui/TextView.java b/app/src/main/java/com/hippo/ehviewer/gallery/ui/TextView.java
index 735b3ad70..b579240b0 100644
--- a/app/src/main/java/com/hippo/ehviewer/gallery/ui/TextView.java
+++ b/app/src/main/java/com/hippo/ehviewer/gallery/ui/TextView.java
@@ -33,8 +33,8 @@ public class TextView extends GLView {
textHight = (int) mTextTexture.getTextHeight(mText);
}
- setMeasuredSize(getDefaultSize(Math.max(getMinimumWidth(), textWidth), widthSpec),
- getDefaultSize(Math.max(getMinimumHeight(), textHight), heightSpec));
+ setMeasuredSize(getDefaultSize(Math.max(getSuggestedMinimumWidth(), textWidth), widthSpec),
+ getDefaultSize(Math.max(getSuggestedMinimumHeight(), textHight), heightSpec));
}
@Override
diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/util/IntArray.java b/app/src/main/java/com/hippo/ehviewer/gallery/util/IntArray.java
deleted file mode 100644
index 0b18003be..000000000
--- a/app/src/main/java/com/hippo/ehviewer/gallery/util/IntArray.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * 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;
-
-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/app/src/main/java/com/hippo/ehviewer/gallery/util/Profile.java b/app/src/main/java/com/hippo/ehviewer/gallery/util/Profile.java
deleted file mode 100644
index bcdd4b96f..000000000
--- a/app/src/main/java/com/hippo/ehviewer/gallery/util/Profile.java
+++ /dev/null
@@ -1,225 +0,0 @@
-/*
- * 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.os.Handler;
-import android.os.HandlerThread;
-import android.os.Process;
-
-import java.util.ArrayList;
-import java.util.Random;
-
-// The Profile class is used to collect profiling information for a thread. It
-// samples stack traces for a thread periodically. enable() and disable() is
-// used to enable and disable profiling for the calling thread. The profiling
-// information can then be dumped to a file using the dumpToFile() method.
-//
-// The disableAll() method can be used to disable profiling for all threads and
-// can be called in onPause() to ensure all profiling is disabled when an
-// activity is paused.
-public class Profile {
- @SuppressWarnings("unused")
- private static final String TAG = "Profile";
- private static final int NS_PER_MS = 1000000;
-
- // This is a watchdog entry for one thread.
- // For every cycleTime period, we dump the stack of the thread.
- private static class WatchEntry {
- Thread thread;
-
- // Both are in milliseconds
- int cycleTime;
- int wakeTime;
-
- boolean isHolding;
- ArrayList holdingStacks = new ArrayList<>();
- }
-
- // This is a watchdog thread which dumps stacks of other threads periodically.
- private static Watchdog sWatchdog = new Watchdog();
-
- private static class Watchdog {
- private ArrayList mList = new ArrayList<>();
- private HandlerThread mHandlerThread;
- private Handler mHandler;
- private Runnable mProcessRunnable = new Runnable() {
- @Override
- public void run() {
- synchronized (Watchdog.this) {
- processList();
- }
- }
- };
- private Random mRandom = new Random();
- private ProfileData mProfileData = new ProfileData();
-
- public Watchdog() {
- mHandlerThread = new HandlerThread("Watchdog Handler",
- Process.THREAD_PRIORITY_FOREGROUND);
- mHandlerThread.start();
- mHandler = new Handler(mHandlerThread.getLooper());
- }
-
- public synchronized void addWatchEntry(Thread thread, int cycleTime) {
- WatchEntry e = new WatchEntry();
- e.thread = thread;
- e.cycleTime = cycleTime;
- int firstDelay = 1 + mRandom.nextInt(cycleTime);
- e.wakeTime = (int) (System.nanoTime() / NS_PER_MS) + firstDelay;
- mList.add(e);
- processList();
- }
-
- public synchronized void removeWatchEntry(Thread thread) {
- for (int i = 0; i < mList.size(); i++) {
- if (mList.get(i).thread == thread) {
- mList.remove(i);
- break;
- }
- }
- processList();
- }
-
- public synchronized void removeAllWatchEntries() {
- mList.clear();
- processList();
- }
-
- private void processList() {
- mHandler.removeCallbacks(mProcessRunnable);
- if (mList.size() == 0) return;
-
- int currentTime = (int) (System.nanoTime() / NS_PER_MS);
- int nextWakeTime = 0;
-
- for (WatchEntry entry : mList) {
- if (currentTime > entry.wakeTime) {
- entry.wakeTime += entry.cycleTime;
- sampleStack(entry);
- }
-
- if (entry.wakeTime > nextWakeTime) {
- nextWakeTime = entry.wakeTime;
- }
- }
-
- long delay = nextWakeTime - currentTime;
- mHandler.postDelayed(mProcessRunnable, delay);
- }
-
- private void sampleStack(WatchEntry entry) {
- Thread thread = entry.thread;
- StackTraceElement[] stack = thread.getStackTrace();
- String[] lines = new String[stack.length];
- for (int i = 0; i < stack.length; i++) {
- lines[i] = stack[i].toString();
- }
- if (entry.isHolding) {
- entry.holdingStacks.add(lines);
- } else {
- mProfileData.addSample(lines);
- }
- }
-
- private WatchEntry findEntry(Thread thread) {
- for (int i = 0; i < mList.size(); i++) {
- WatchEntry entry = mList.get(i);
- if (entry.thread == thread) return entry;
- }
- return null;
- }
-
- public synchronized void dumpToFile(String filename) {
- mProfileData.dumpToFile(filename);
- }
-
- public synchronized void reset() {
- mProfileData.reset();
- }
-
- public synchronized void hold(Thread t) {
- WatchEntry entry = findEntry(t);
-
- // This can happen if the profiling is disabled (probably from
- // another thread). Same check is applied in commit() and drop()
- // below.
- if (entry == null) return;
-
- entry.isHolding = true;
- }
-
- public synchronized void commit(Thread t) {
- WatchEntry entry = findEntry(t);
- if (entry == null) return;
- ArrayList stacks = entry.holdingStacks;
- for (int i = 0; i < stacks.size(); i++) {
- mProfileData.addSample(stacks.get(i));
- }
- entry.isHolding = false;
- entry.holdingStacks.clear();
- }
-
- public synchronized void drop(Thread t) {
- WatchEntry entry = findEntry(t);
- if (entry == null) return;
- entry.isHolding = false;
- entry.holdingStacks.clear();
- }
- }
-
- // Enable profiling for the calling thread. Periodically (every cycleTimeInMs
- // milliseconds) sample the stack trace of the calling thread.
- public static void enable(int cycleTimeInMs) {
- Thread t = Thread.currentThread();
- sWatchdog.addWatchEntry(t, cycleTimeInMs);
- }
-
- // Disable profiling for the calling thread.
- public static void disable() {
- sWatchdog.removeWatchEntry(Thread.currentThread());
- }
-
- // Disable profiling for all threads.
- public static void disableAll() {
- sWatchdog.removeAllWatchEntries();
- }
-
- // Dump the profiling data to a file.
- public static void dumpToFile(String filename) {
- sWatchdog.dumpToFile(filename);
- }
-
- // Reset the collected profiling data.
- public static void reset() {
- sWatchdog.reset();
- }
-
- // Hold the future samples coming from current thread until commit() or
- // drop() is called, and those samples are recorded or ignored as a result.
- // This must called after enable() to be effective.
- public static void hold() {
- sWatchdog.hold(Thread.currentThread());
- }
-
- public static void commit() {
- sWatchdog.commit(Thread.currentThread());
- }
-
- public static void drop() {
- sWatchdog.drop(Thread.currentThread());
- }
-}
diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/util/ProfileData.java b/app/src/main/java/com/hippo/ehviewer/gallery/util/ProfileData.java
deleted file mode 100644
index c4288506b..000000000
--- a/app/src/main/java/com/hippo/ehviewer/gallery/util/ProfileData.java
+++ /dev/null
@@ -1,168 +0,0 @@
-/*
- * 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.util.Log;
-
-import com.hippo.yorozuya.IOUtils;
-
-import java.io.DataOutputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Map.Entry;
-
-// ProfileData keeps profiling samples in a tree structure.
-// The addSample() method adds a sample. The dumpToFile() method saves the data
-// to a file. The reset() method clears all samples.
-public class ProfileData {
- @SuppressWarnings("unused")
- private static final String TAG = "ProfileData";
-
- private static class Node {
- public int id; // this is the name of this node, mapped from mNameToId
- public Node parent;
- public int sampleCount;
- public ArrayList children;
- public Node(Node parent, int id) {
- this.parent = parent;
- this.id = id;
- }
- }
-
- private Node mRoot;
- private int mNextId;
- private HashMap mNameToId;
- private DataOutputStream mOut;
- private byte mScratch[] = new byte[4]; // scratch space for writeInt()
-
- public ProfileData() {
- mRoot = new Node(null, -1); // The id of the root node is unused.
- mNameToId = new HashMap<>();
- }
-
- public void reset() {
- mRoot = new Node(null, -1);
- mNameToId.clear();
- mNextId = 0;
- }
-
- private int nameToId(String name) {
- Integer id = mNameToId.get(name);
- if (id == null) {
- id = ++mNextId; // The tool doesn't want id=0, so we start from 1.
- mNameToId.put(name, id);
- }
- return id;
- }
-
- public void addSample(String[] stack) {
- int[] ids = new int[stack.length];
- for (int i = 0; i < stack.length; i++) {
- ids[i] = nameToId(stack[i]);
- }
-
- Node node = mRoot;
- for (int i = stack.length - 1; i >= 0; i--) {
- if (node.children == null) {
- node.children = new ArrayList<>();
- }
-
- int id = ids[i];
- ArrayList children = node.children;
- int j;
- for (j = 0; j < children.size(); j++) {
- if (children.get(j).id == id) break;
- }
- if (j == children.size()) {
- children.add(new Node(node, id));
- }
-
- node = children.get(j);
- }
-
- node.sampleCount++;
- }
-
- public void dumpToFile(String filename) {
- try {
- mOut = new DataOutputStream(new FileOutputStream(filename));
- // Start record
- writeInt(0);
- writeInt(3);
- writeInt(1);
- writeInt(20000); // Sampling period: 20ms
- writeInt(0);
-
- // Samples
- writeAllStacks(mRoot, 0);
-
- // End record
- writeInt(0);
- writeInt(1);
- writeInt(0);
- writeAllSymbols();
- } catch (IOException ex) {
- Log.w("Failed to dump to file", ex);
- } finally {
- IOUtils.closeQuietly(mOut);
- }
- }
-
- // Writes out one stack, consisting of N+2 words:
- // first word: sample count
- // second word: depth of the stack (N)
- // N words: each word is the id of one address in the stack
- private void writeOneStack(Node node, int depth) throws IOException {
- writeInt(node.sampleCount);
- writeInt(depth);
- while (depth-- > 0) {
- writeInt(node.id);
- node = node.parent;
- }
- }
-
- private void writeAllStacks(Node node, int depth) throws IOException {
- if (node.sampleCount > 0) {
- writeOneStack(node, depth);
- }
-
- ArrayList children = node.children;
- if (children != null) {
- for (int i = 0; i < children.size(); i++) {
- writeAllStacks(children.get(i), depth + 1);
- }
- }
- }
-
- // Writes out the symbol table. Each line is like:
- // 0x17e java.util.ArrayList.isEmpty(ArrayList.java:319)
- private void writeAllSymbols() throws IOException {
- for (Entry entry : mNameToId.entrySet()) {
- mOut.writeBytes(String.format("0x%x %s\n", entry.getValue(), entry.getKey()));
- }
- }
-
- private void writeInt(int v) throws IOException {
- mScratch[0] = (byte) v;
- mScratch[1] = (byte) (v >> 8);
- mScratch[2] = (byte) (v >> 16);
- mScratch[3] = (byte) (v >> 24);
- mOut.write(mScratch);
- }
-}
diff --git a/app/src/main/java/com/hippo/ehviewer/ui/GalleryActivity.java b/app/src/main/java/com/hippo/ehviewer/ui/GalleryActivity.java
index 18281bfc0..2ed74fe2e 100644
--- a/app/src/main/java/com/hippo/ehviewer/ui/GalleryActivity.java
+++ b/app/src/main/java/com/hippo/ehviewer/ui/GalleryActivity.java
@@ -16,7 +16,308 @@
package com.hippo.ehviewer.ui;
-import com.hippo.app.StatsActivity;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Typeface;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
-public class GalleryActivity extends StatsActivity {
+import com.hippo.app.StatsActivity;
+import com.hippo.ehviewer.R;
+import com.hippo.ehviewer.client.data.GalleryBase;
+import com.hippo.ehviewer.gallery.GalleryProvider;
+import com.hippo.ehviewer.gallery.GalleryProviderListener;
+import com.hippo.ehviewer.gallery.GallerySpider;
+import com.hippo.ehviewer.gallery.ImageHandler;
+import com.hippo.ehviewer.gallery.glrenderer.TextTexture;
+import com.hippo.ehviewer.gallery.glrenderer.TiledTexture;
+import com.hippo.ehviewer.gallery.ui.GLRootView;
+import com.hippo.ehviewer.gallery.ui.GLView;
+import com.hippo.ehviewer.gallery.ui.GalleryPageView;
+import com.hippo.ehviewer.gallery.ui.GalleryView;
+import com.hippo.util.SystemUiHelper;
+import com.hippo.yorozuya.LayoutUtils;
+
+import java.io.IOException;
+
+public class GalleryActivity extends StatsActivity implements TiledTexture.OnFreeBitmapListener, GalleryProviderListener {
+
+ public static final String KEY_GALLERY_BASE = "gallery_base";
+
+ private SystemUiHelper mSystemUiHelper;
+
+ private Resources mResources;
+
+ private TiledTexture.Uploader mUploader;
+
+ private GalleryView mGalleryView;
+
+ private GalleryAdapter mAdapter;
+
+ private TextTexture mTextTexture;
+
+ private GallerySpider mGallerySpider;
+
+ private GalleryBase mGalleryBase;
+
+ private boolean mDieYoung;
+
+ private GalleryView.Mode mMode;
+
+ private int mSize;
+
+ private boolean handleIntent(Intent intent) {
+ if (intent != null) {
+ mGalleryBase = intent.getParcelableExtra(KEY_GALLERY_BASE);
+ return mGalleryBase != null;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (!handleIntent(getIntent())) {
+ mDieYoung = true;
+ finish();
+ return;
+ }
+
+ try {
+ mGallerySpider = GallerySpider.obtain(mGalleryBase, ImageHandler.Mode.DOWNLOAD);
+ } catch (IOException e) {
+ e.printStackTrace();
+ mDieYoung = true;
+ finish();
+ return;
+ }
+ mGallerySpider.addGalleryProviderListener(this);
+
+ mSystemUiHelper = new SystemUiHelper(this, SystemUiHelper.LEVEL_IMMERSIVE,
+ SystemUiHelper.FLAG_LAYOUT_IN_SCREEN_OLDER_DEVICES | SystemUiHelper.FLAG_IMMERSIVE_STICKY);
+ mSystemUiHelper.hide();
+
+ setContentView(R.layout.activity_gallery);
+ GLRootView glRootView = (GLRootView) findViewById(R.id.gl_root_view);
+
+ mResources = getResources();
+ // Prepare
+ TiledTexture.prepareResources();
+ TiledTexture.setOnFreeBitmapListener(this);
+ mUploader = new TiledTexture.Uploader(glRootView);
+ Typeface tf = Typeface.createFromAsset(mResources.getAssets(), "fonts/number.ttf");
+ mTextTexture = TextTexture.create(tf,
+ mResources.getDimensionPixelSize(R.dimen.gallery_index_text),
+ mResources.getColor(R.color.secondary_text_dark),
+ new char[]{'.', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'});
+ mUploader.addTexture(mTextTexture);
+ GalleryPageView.setTextTexture(mTextTexture);
+
+ mGalleryView = new GalleryView(this);
+ mAdapter = new GalleryAdapter();
+ mGalleryView.setAdapter(mAdapter);
+ mGalleryView.setProgressSize(LayoutUtils.dp2pix(this, 56));
+ mGalleryView.setInterval(LayoutUtils.dp2pix(this, 48));
+
+ mSize = mGallerySpider.size();
+ if (mSize <= 0) {
+ setMode(GalleryView.Mode.NONE);
+ } else {
+ setMode(GalleryView.Mode.LEFT_TO_RIGHT);
+ }
+
+ glRootView.setContentPane(mGalleryView);
+ }
+
+ private void setMode(GalleryView.Mode mode) {
+ mMode = mode;
+ mGalleryView.setMode(mode);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+
+ if (!mDieYoung) {
+ GalleryPageView.setTextTexture(null);
+ mTextTexture.recycle();
+ TiledTexture.setOnFreeBitmapListener(null);
+ TiledTexture.freeResources();
+ mGallerySpider.removeGalleryProviderListener(this);
+ GallerySpider.release(mGallerySpider);
+ mGallerySpider = null;
+ }
+ // TODO free all TileTexture in GalleryView
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ mSystemUiHelper.hide();
+ }
+
+ @Override
+ public void onFreeBitmapListener(Bitmap bitmap) {
+ if (mGallerySpider != null) {
+ mGallerySpider.releaseBitmap(bitmap);
+ }
+ }
+
+ @Override
+ public void onTotallyFailed(Exception e) {
+ //e.printStackTrace(); // TODO
+ }
+
+ @Override
+ public void onPartlyFailed(Exception e) {
+ //e.printStackTrace(); // TODO
+ }
+
+ @Override
+ public void onGetSize(int size) {
+ if (mSize != size) {
+ mSize = size;
+ setMode(GalleryView.Mode.LEFT_TO_RIGHT);
+ }
+ }
+
+ @Override
+ public void onGetBitmap(int index, @Nullable Bitmap bitmap) {
+ GalleryPageView page = mGalleryView.getPage(index);
+ if (page != null) {
+ if (bitmap == null) {
+ bindBadBitmap(page);
+ } else {
+ bindBitmap(page, bitmap);
+ }
+ } else {
+ if (mGallerySpider != null) {
+ mGallerySpider.releaseBitmap(bitmap);
+ }
+ }
+ }
+
+ @Override
+ public void onPagePercent(int index, float percent) {
+ GalleryPageView page = mGalleryView.getPage(index);
+ if (page != null) {
+ bindPercent(page, percent);
+ }
+ }
+
+ @Override
+ public void onPageSucceed(int index) {
+ GalleryPageView page = mGalleryView.getPage(index);
+ if (page != null) {
+ bind(page, index);
+ }
+ }
+
+ @Override
+ public void onPageFailed(int index, Exception e) {
+ //e.printStackTrace(); // TODO
+ GalleryPageView page = mGalleryView.getPage(index);
+ if (page != null) {
+ bindFailed(page);
+ }
+ }
+
+ private void clearTiledTextureInPage(GalleryPageView view) {
+ TiledTexture tiledTexture = view.mImageView.getTiledTexture();
+ if (tiledTexture != null) {
+ view.mImageView.setTiledTexture(null);
+ tiledTexture.recycle();
+ }
+ }
+
+ private void bindBadBitmap(GalleryPageView view) {
+ clearTiledTextureInPage(view);
+ view.mProgressView.setVisibility(GLView.INVISIBLE);
+ view.mIndexView.setVisibility(GLView.VISIBLE);
+ view.mIndexView.setText(".3");
+ }
+
+ private void bindBitmap(GalleryPageView view, Bitmap bitmap) {
+ TiledTexture tiledTexture = new TiledTexture(bitmap);
+ mUploader.addTexture(tiledTexture);
+
+ clearTiledTextureInPage(view);
+ view.mProgressView.setVisibility(GLView.INVISIBLE);
+ view.mIndexView.setVisibility(GLView.INVISIBLE);
+ view.mImageView.setTiledTexture(tiledTexture);
+ }
+
+ private void bindPercent(GalleryPageView view, float precent) {
+ clearTiledTextureInPage(view);
+ view.mProgressView.setVisibility(GLView.VISIBLE);
+ view.mProgressView.setIndeterminate(false);
+ view.mProgressView.setProgress(precent);
+ view.mIndexView.setVisibility(GLView.VISIBLE);
+ }
+
+ private void bindNone(GalleryPageView view) {
+ clearTiledTextureInPage(view);
+ view.mProgressView.setVisibility(GLView.VISIBLE);
+ view.mProgressView.setIndeterminate(true);
+ view.mIndexView.setVisibility(GLView.VISIBLE);
+ }
+
+ private void bindFailed(GalleryPageView view) {
+ clearTiledTextureInPage(view);
+ view.mProgressView.setVisibility(GLView.INVISIBLE);
+ view.mIndexView.setVisibility(GLView.VISIBLE);
+ view.mIndexView.setText(".1");
+ }
+
+ private void bindUnknown(GalleryPageView view) {
+ clearTiledTextureInPage(view);
+ view.mProgressView.setVisibility(GLView.INVISIBLE);
+ view.mIndexView.setVisibility(GLView.VISIBLE);
+ view.mIndexView.setText(".2");
+ }
+
+ private void bind(GalleryPageView view, int index) {
+ Object result = mGallerySpider.request(index);
+ if (result instanceof Float) {
+ bindPercent(view, (Float) result);
+ } else if (result == GalleryProvider.RESULT_WAIT) {
+ // Just wait
+ } else if (result == GalleryProvider.RESULT_NONE) {
+ bindNone(view);
+ } else if (result == GalleryProvider.RESULT_FAILED) {
+ bindFailed(view);
+ } else {
+ bindUnknown(view);
+ }
+ }
+
+ class GalleryAdapter extends GalleryView.Adapter {
+
+ @Override
+ public int getPages() {
+ return mMode == GalleryView.Mode.NONE ? 1 : mSize;
+ }
+
+ @Override
+ public GalleryPageView createPage() {
+ return new GalleryPageView(GalleryActivity.this);
+ }
+
+ @Override
+ public void bindPage(GalleryPageView view, int index) {
+ view.mProgressView.setColor(mResources.getColor(R.color.theme_accent));
+ view.mIndexView.setText(Integer.toString(index + 1));
+ bind(view, index);
+ }
+
+ @Override
+ public void unbindPage(GalleryPageView view, int index) {
+ view.mProgressView.setIndeterminate(false);
+ clearTiledTextureInPage(view);
+ }
+ }
}
diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryDetailScene.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryDetailScene.java
index 38f3c29d7..e49e446d2 100644
--- a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryDetailScene.java
+++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryDetailScene.java
@@ -281,7 +281,7 @@ public class GalleryDetailScene extends Scene implements View.OnClickListener,
if (v == mRead) {
Intent intent = new Intent(getStageActivity(), GalleryActivity.class);
- //intent.putExtra(GalleryActivity.KEY_GALLERY_BASE, mGalleryDetail);
+ intent.putExtra(GalleryActivity.KEY_GALLERY_BASE, mGalleryDetail);
getStageActivity().startActivity(intent);
} else if (v == mDownload) {
diff --git a/app/src/main/res/drawable-hdpi/overscroll_edge.png b/app/src/main/res/drawable-hdpi/overscroll_edge.png
new file mode 100644
index 000000000..1952e0ed3
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/overscroll_edge.png differ
diff --git a/app/src/main/res/drawable-hdpi/overscroll_glow.png b/app/src/main/res/drawable-hdpi/overscroll_glow.png
new file mode 100644
index 000000000..45c013532
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/overscroll_glow.png differ
diff --git a/app/src/main/res/drawable-xhdpi/overscroll_edge.png b/app/src/main/res/drawable-xhdpi/overscroll_edge.png
new file mode 100644
index 000000000..b5e6e61cd
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/overscroll_edge.png differ
diff --git a/app/src/main/res/drawable-xhdpi/overscroll_glow.png b/app/src/main/res/drawable-xhdpi/overscroll_glow.png
new file mode 100644
index 000000000..779c3a590
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/overscroll_glow.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/overscroll_edge.png b/app/src/main/res/drawable-xxhdpi/overscroll_edge.png
new file mode 100644
index 000000000..734bfa762
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/overscroll_edge.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/overscroll_glow.png b/app/src/main/res/drawable-xxhdpi/overscroll_glow.png
new file mode 100644
index 000000000..dc1f2795f
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/overscroll_glow.png differ
diff --git a/app/src/main/res/layout/activity_gallery.xml b/app/src/main/res/layout/activity_gallery.xml
index 656555576..b4cc500c7 100644
--- a/app/src/main/res/layout/activity_gallery.xml
+++ b/app/src/main/res/layout/activity_gallery.xml
@@ -19,8 +19,8 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
-
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 8066bcb69..2174e4f48 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -109,4 +109,6 @@
@color/material_yellow_800
@color/material_deep_Orange_400
+ #ff212121
+
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 7e1593cf1..21ebd8f78 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -147,8 +147,9 @@
224dp
248dp
48dp
-
96dp
128dp
+ 56sp
+
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 723314ca6..b81815ab3 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -49,4 +49,8 @@
- @null
+
+
diff --git a/yorozuya b/yorozuya
index 765b1e7d0..482a8c483 160000
--- a/yorozuya
+++ b/yorozuya
@@ -1 +1 @@
-Subproject commit 765b1e7d0c013a51a6add12f36633ad764990529
+Subproject commit 482a8c4838e163b2ce2aa76df7a47691bd448330