diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index dd1caa571..8fb85d22c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -44,7 +44,7 @@ diff --git a/app/src/main/assets/fonts/GloriaHallelujah.ttf b/app/src/main/assets/fonts/GloriaHallelujah.ttf deleted file mode 100644 index 3b42f5b38..000000000 Binary files a/app/src/main/assets/fonts/GloriaHallelujah.ttf and /dev/null differ diff --git a/app/src/main/assets/fonts/Slabo.ttf b/app/src/main/assets/fonts/Slabo.ttf deleted file mode 100644 index 4d73e0984..000000000 Binary files a/app/src/main/assets/fonts/Slabo.ttf and /dev/null differ diff --git a/app/src/main/assets/fonts/number.ttf b/app/src/main/assets/fonts/number.ttf new file mode 100644 index 000000000..c6bb4c206 Binary files /dev/null and b/app/src/main/assets/fonts/number.ttf differ diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/ImageHandler.java b/app/src/main/java/com/hippo/ehviewer/gallery/ImageHandler.java index 129598172..db6d24957 100644 --- a/app/src/main/java/com/hippo/ehviewer/gallery/ImageHandler.java +++ b/app/src/main/java/com/hippo/ehviewer/gallery/ImageHandler.java @@ -66,7 +66,7 @@ public class ImageHandler { sDiskCache = diskCache; } - public ImageHandler(GalleryBase galleryBase, int pages, Mode mode, UniFile downloadDir) { + ImageHandler(GalleryBase galleryBase, int pages, Mode mode, UniFile downloadDir) { mGalleryBase = galleryBase; mMode = mode; mFilenames = new AtomicReferenceArray<>(pages); diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/SpiderQueen.java b/app/src/main/java/com/hippo/ehviewer/gallery/SpiderQueen.java index d5b8152ec..4c02a8bc4 100644 --- a/app/src/main/java/com/hippo/ehviewer/gallery/SpiderQueen.java +++ b/app/src/main/java/com/hippo/ehviewer/gallery/SpiderQueen.java @@ -17,7 +17,6 @@ package com.hippo.ehviewer.gallery; import android.graphics.Bitmap; -import android.os.Looper; import android.os.Process; import com.hippo.ehviewer.client.EhConfig; @@ -101,6 +100,9 @@ public class SpiderQueen implements Runnable { private final ReentrantLock mWorkerArrayLock = new ReentrantLock(); // Lock for mRequestTokens and mSpiderInfo.tokens private final Object mTokenLock = new Object(); + // The lock to make sure onGetSize call first + public boolean mGetSizeFinished; + private final Object mGetSizeLock = new Object(); public SpiderQueen(EhHttpClient httpClient, File spiderInfoDir, GalleryBase galleryBase, ImageHandler.Mode mode, UniFile downloadDir, @@ -476,6 +478,29 @@ public class SpiderQueen implements Runnable { } } + public void setGetSizeFinished(final boolean getSizeFinished) { + synchronized (mGetSizeLock) { + if (mGetSizeFinished != getSizeFinished) { + mGetSizeFinished = getSizeFinished; + if (getSizeFinished) { + mGetSizeLock.notifyAll(); + } + } + } + } + + private void waitUntilFinishGettingSize() { + synchronized (mGetSizeLock) { + while (!mGetSizeFinished) { + try { + mGetSizeLock.wait(); + } catch (InterruptedException e) { + // ignored, we'll start waiting again + } + } + } + } + public void doIt() throws Exception { Say.d(TAG, "Spider Queen do it"); @@ -538,18 +563,19 @@ public class SpiderQueen implements Runnable { return; } - - // TODO make sure onGetPages first then other listener - // 7. tell listener pages - mSpiderListener.onGetPages(spiderInfo.pages); - // 6. Make workers and render to work + setGetSizeFinished(false); ensureWorker(); mRender = new Render(); Thread thread = new Thread(mRender, "Render"); thread.setPriority(Process.THREAD_PRIORITY_BACKGROUND); thread.start(); + // 7. tell listener pages + mSpiderListener.onGetPages(spiderInfo.pages); + setGetSizeFinished(true); + + // 8. Get tokens handleToken(spiderInfo, ehConfig); @@ -688,6 +714,9 @@ public class SpiderQueen implements Runnable { GalleryBase gb = mGalleryBase; ImageHandler handler = mImageHandler; + // Wait for post send onGetSize over + waitUntilFinishGettingSize(); + while (!mStop) { // Get index final int index = getRequestIndex(); @@ -812,13 +841,11 @@ public class SpiderQueen implements Runnable { private void waitUntilUnpaused() { synchronized (mPauseLock) { - if (Looper.myLooper() != Looper.getMainLooper()) { - while (mPauseAccess) { - try { - mPauseLock.wait(); - } catch (InterruptedException e) { - // ignored, we'll start waiting again - } + while (mPauseAccess) { + try { + mPauseLock.wait(); + } catch (InterruptedException e) { + // ignored, we'll start waiting again } } } @@ -837,6 +864,9 @@ public class SpiderQueen implements Runnable { public void run() { Say.d(TAG, "Render starts"); + // Wait for post send onGetSize over + waitUntilFinishGettingSize(); + while (!mStop) { waitUntilUnpaused(); diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/anim/Animation.java b/app/src/main/java/com/hippo/ehviewer/gallery/anim/Animation.java index bb01c9ab8..f7dcebe02 100644 --- a/app/src/main/java/com/hippo/ehviewer/gallery/anim/Animation.java +++ b/app/src/main/java/com/hippo/ehviewer/gallery/anim/Animation.java @@ -49,20 +49,49 @@ abstract public class Animation { private static final long ANIMATION_START = -1; private static final long NO_ANIMATION = -2; + /** + * When the animation reaches the end and 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 int mDuration; + private long mDuration; private Interpolator mInterpolator; + private int mRepeatCount; + private int mRepeatMode; + + private int mRunnedCount; public void setInterpolator(Interpolator interpolator) { mInterpolator = interpolator; } - public void setDuration(int duration) { + public void setDuration(long duration) { mDuration = duration; } + public void setRepeatCount(int repeatCount) { + mRepeatCount = repeatCount; + } + + public void setRepeatMode(int repeatMode) { + mRepeatMode = repeatMode; + } + public void start() { mStartTime = ANIMATION_START; + mRunnedCount = 0; } public void setStartTime(long time) { @@ -77,14 +106,29 @@ abstract public class Animation { mStartTime = NO_ANIMATION; } + // TODO mRepeatMode public boolean calculate(long currentTimeMillis) { - if (mStartTime == NO_ANIMATION) return false; - if (mStartTime == ANIMATION_START) mStartTime = currentTimeMillis; - int elapse = (int) (currentTimeMillis - mStartTime); + if (mStartTime == NO_ANIMATION) { + return false; + } + if (mStartTime == ANIMATION_START) { + mStartTime = currentTimeMillis; + } + + long elapse = currentTimeMillis - mStartTime; float x = MathUtils.clamp((float) elapse / mDuration, 0f, 1f); Interpolator i = mInterpolator; onCalculate(i != null ? i.getInterpolation(x) : x); - if (elapse >= mDuration) mStartTime = NO_ANIMATION; + + if (elapse >= mDuration) { + mRunnedCount++; + if (mRunnedCount >= mRepeatCount && mRepeatCount != INFINITE) { + mStartTime = NO_ANIMATION; + } else { + mStartTime += elapse; + } + } + return mStartTime != NO_ANIMATION; } diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/anim/FloatAnimation.java b/app/src/main/java/com/hippo/ehviewer/gallery/anim/FloatAnimation.java index baeafafc1..73837f02b 100644 --- a/app/src/main/java/com/hippo/ehviewer/gallery/anim/FloatAnimation.java +++ b/app/src/main/java/com/hippo/ehviewer/gallery/anim/FloatAnimation.java @@ -22,11 +22,10 @@ public class FloatAnimation extends Animation { private final float mTo; private float mCurrent; - public FloatAnimation(float from, float to, int duration) { + public FloatAnimation(float from, float to) { mFrom = from; mTo = to; mCurrent = from; - setDuration(duration); } @Override diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/BasicTexture.java b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/BasicTexture.java index 45b1df7ac..ef691364a 100644 --- a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/BasicTexture.java +++ b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/BasicTexture.java @@ -16,6 +16,7 @@ package com.hippo.ehviewer.gallery.glrenderer; +import android.graphics.RectF; import android.util.Log; import com.hippo.yorozuya.MathUtils; @@ -143,6 +144,10 @@ public abstract class BasicTexture implements Texture { canvas.drawTexture(this, x, y, w, h); } + public void draw(GLCanvas canvas, RectF source, RectF target) { + canvas.drawTexture(this, source, target); + } + // onBind is called before GLCanvas binds this texture. // It should make sure the data is uploaded to GL memory. abstract protected boolean onBind(GLCanvas canvas); diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/GLES11Canvas.java b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/GLES11Canvas.java index 6ca11c7c9..9ebd3ef0b 100644 --- a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/GLES11Canvas.java +++ b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/GLES11Canvas.java @@ -24,7 +24,7 @@ import android.opengl.GLUtils; import android.opengl.Matrix; import android.util.Log; -import com.hippo.ehviewer.gallery.util.IntArray; +import com.hippo.yorozuya.IntArray; import com.hippo.yorozuya.ColorUtils; import junit.framework.Assert; diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/GLES20Canvas.java b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/GLES20Canvas.java index 33abebbbe..d30053903 100644 --- a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/GLES20Canvas.java +++ b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/GLES20Canvas.java @@ -23,7 +23,7 @@ import android.opengl.GLUtils; import android.opengl.Matrix; import android.util.Log; -import com.hippo.ehviewer.gallery.util.IntArray; +import com.hippo.yorozuya.IntArray; import com.hippo.yorozuya.MathUtils; import java.nio.Buffer; diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/StringTexture.java b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/StringTexture.java index 7bcdd17b5..2dc8aa3cf 100644 --- a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/StringTexture.java +++ b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/StringTexture.java @@ -23,7 +23,6 @@ import android.graphics.Paint.FontMetricsInt; import android.graphics.Typeface; import android.text.TextPaint; import android.text.TextUtils; -import android.util.FloatMath; // StringTexture is a texture shows the content of a specified String. // @@ -72,7 +71,7 @@ public class StringTexture extends CanvasTexture { private static StringTexture newInstance(String text, TextPaint paint) { FontMetricsInt metrics = paint.getFontMetricsInt(); - int width = (int) FloatMath.ceil(paint.measureText(text)); + 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; diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/TiledTexture.java b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/TiledTexture.java index 779da0d02..8ba834c40 100644 --- a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/TiledTexture.java +++ b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/TiledTexture.java @@ -25,6 +25,7 @@ import android.graphics.PorterDuff.Mode; import android.graphics.PorterDuffXfermode; import android.graphics.RectF; import android.os.SystemClock; +import android.support.annotation.NonNull; import com.hippo.ehviewer.gallery.ui.GLRoot; @@ -36,6 +37,9 @@ import java.util.ArrayList; // upload the whole bitmap but we reduce the time of uploading each tile // so it make the animation more smooth and prevents jank. public class TiledTexture implements Texture { + + private static final String TAG = TiledTexture.class.getSimpleName(); + private static final int CONTENT_SIZE = 254; private static final int BORDER_SIZE = 1; private static final int TILE_SIZE = CONTENT_SIZE + 2 * BORDER_SIZE; @@ -52,6 +56,9 @@ public class TiledTexture implements Texture { private static Canvas sCanvas; private static Paint sBitmapPaint; private static Paint sPaint; + private static OnFreeBitmapListener sOnFreeBitmapListener; + + private Bitmap mBitmap; private int mUploadIndex = 0; @@ -62,6 +69,15 @@ public class TiledTexture implements Texture { private final RectF mSrcRect = new RectF(); private final RectF mDestRect = new RectF(); + public interface OnFreeBitmapListener { + + void onFreeBitmapListener(Bitmap bitmap); + } + + public static void setOnFreeBitmapListener(OnFreeBitmapListener listener) { + sOnFreeBitmapListener = listener; + } + public static class Uploader implements GLRoot.OnGLIdleListener { private final ArrayDeque mTextures = new ArrayDeque<>(INIT_CAPACITY); @@ -95,6 +111,7 @@ public class TiledTexture implements Texture { while (now < dueTime && !deque.isEmpty()) { TiledTexture t = deque.peekFirst(); if (t.uploadNextTile(canvas)) { + t.freeBitmap(); deque.removeFirst(); mGlRoot.requestRender(); } @@ -202,7 +219,8 @@ public class TiledTexture implements Texture { return mUploadIndex == mTiles.length; } - public TiledTexture(Bitmap bitmap) { + public TiledTexture(@NonNull Bitmap bitmap) { + mBitmap = bitmap; mWidth = bitmap.getWidth(); mHeight = bitmap.getHeight(); ArrayList list = new ArrayList(); @@ -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