Improve GalleryActivity

This commit is contained in:
seven332 committed 2015-07-27 16:01:23 +08:00
1 parent 9ee91aae19
commit bc29540a48
40 files changed
+2442 -530

No files matched your search

@@ -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);
@@ -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();
@@ -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 <code>repeatCount</code> 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 <code>repeatCount</code> 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;
}
@@ -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
@@ -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);
@@ -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;
@@ -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;
@@ -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;
@@ -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<TiledTexture> 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<Tile> list = new ArrayList<Tile>();
@@ -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() {
@@ -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;
}
}
@@ -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.
*
* <p>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.</p>
*
* <p>When drawing, widgets should draw their main content and child views first,
* usually by invoking <code>super.draw(canvas)</code> from an overridden <code>draw</code>
* 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.</p>
*/
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.
*
* <p>When using a {@link android.widget.Scroller} or {@link android.widget.OverScroller},
* the method <code>getCurrVelocity</code> will provide a reasonable approximation
* to use here.</p>
*
* @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();
}
}
}
@@ -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();
}
}
}
@@ -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);
@@ -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
@@ -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();
}
/**
@@ -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);
}
}
@@ -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<GalleryPageView> mAttachedPage = new SparseArray<>();
private List<GLView> mInvalidAttachedComponent = new ArrayList<>();
private IntArray mMissedPageInfo = new IntArray(5 * 3);
private List<GalleryPageView> mUsedPage = new ArrayList<>();
private SparseArray<GalleryPageView> 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;
}
}
}
@@ -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();
}
}
}
@@ -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);
}
}
}
}
@@ -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
@@ -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];
}
}
@@ -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<String[]> 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<WatchEntry> 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<String[]> 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());
}
}
@@ -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<Node> children;
public Node(Node parent, int id) {
this.parent = parent;
this.id = id;
}
}
private Node mRoot;
private int mNextId;
private HashMap<String, Integer> 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<Node> 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<Node> 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<String, Integer> 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);
}
}
@@ -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);
}
}
}
@@ -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) {