Add files

This commit is contained in:
seven332 committed 2016-01-26 01:03:31 +08:00
1 parent 217c81a6d1
commit d1650c4ec5
297 files changed
+105101 -116

No files matched your search

@@ -0,0 +1,167 @@
/*
* Copyright (C) 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.drawable;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import com.hippo.ehviewer.R;
import com.hippo.yorozuya.MathUtils;
public class AddDeleteDrawable extends Drawable {
private final Paint mPaint = new Paint();
private final Path mPath = new Path();
private final int mSize;
private float mProgress;
private boolean mAutoUpdateMirror = false;
private boolean mVerticalMirror = false;
/**
* @param context used to get the configuration for the drawable from
*/
public AddDeleteDrawable(Context context) {
Resources resources = context.getResources();
mSize = resources.getDimensionPixelSize(R.dimen.add_size);
float barThickness = Math.round(resources.getDimension(R.dimen.add_thickness));
mPaint.setColor(resources.getColor(R.color.primary_drawable_light));
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeJoin(Paint.Join.MITER);
mPaint.setStrokeCap(Paint.Cap.BUTT);
mPaint.setStrokeWidth(barThickness);
float halfSize = mSize / 2;
mPath.moveTo(0f, -halfSize);
mPath.lineTo(0, halfSize);
mPath.moveTo(-halfSize, 0);
mPath.lineTo(halfSize, 0);
}
@Override
public void draw(Canvas canvas) {
Rect bounds = getBounds();
float canvasRotate;
if (mVerticalMirror) {
canvasRotate = MathUtils.lerp(270, 135f, mProgress);
} else {
canvasRotate = MathUtils.lerp(0f, 135f, mProgress);
}
canvas.save();
canvas.translate(bounds.centerX(), bounds.centerY());
canvas.rotate(canvasRotate);
canvas.drawPath(mPath, mPaint);
canvas.restore();
}
public void setColor(int color) {
mPaint.setColor(color);
invalidateSelf();
}
@Override
public void setAlpha(int alpha) {
mPaint.setAlpha(alpha);
}
@Override
public void setColorFilter(ColorFilter cf) {
mPaint.setColorFilter(cf);
}
@Override
public int getIntrinsicHeight() {
return mSize;
}
@Override
public int getIntrinsicWidth() {
return mSize;
}
/**
* If set, canvas is flipped when progress reached to end and going back to start.
*/
protected void setVerticalMirror(boolean verticalMirror) {
mVerticalMirror = verticalMirror;
}
public void setAutoUpdateMirror(boolean autoUpdateMirror) {
mAutoUpdateMirror = autoUpdateMirror;
}
@SuppressWarnings("unused")
public float getProgress() {
return mProgress;
}
@SuppressWarnings("unused")
public void setProgress(float progress) {
if (mAutoUpdateMirror) {
if (progress == 1f) {
setVerticalMirror(true);
} else if (progress == 0f) {
setVerticalMirror(false);
}
}
mProgress = progress;
invalidateSelf();
}
public void setAdd(long duration) {
setShape(false, duration);
}
public void setDelete(long duration) {
setShape(true, duration);
}
public void setShape(boolean delete, long duration) {
if (!((!delete && mProgress == 0f) || (delete && mProgress == 1f))) {
float endProgress = delete ? 1f : 0f;
if (duration <= 0) {
setProgress(endProgress);
} else {
ObjectAnimator oa = ObjectAnimator.ofFloat(this, "progress", endProgress);
oa.setDuration(duration);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
oa.setAutoCancel(true);
}
oa.start();
}
}
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
}
@@ -0,0 +1,67 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.drawable;
import android.graphics.Bitmap;
import android.support.annotation.Nullable;
import android.util.Log;
import java.lang.ref.WeakReference;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;
class BitmapPool {
private static final String TAG = BitmapPool.class.getSimpleName();
private final Set<WeakReference<Bitmap>> mReusableBitmapSet = new LinkedHashSet<>();
public synchronized void put(@Nullable Bitmap bitmap) {
if (bitmap != null) {
mReusableBitmapSet.add(new WeakReference<>(bitmap));
}
}
@Nullable
public synchronized Bitmap get(int width, int height) {
final Iterator<WeakReference<Bitmap>> iterator = mReusableBitmapSet.iterator();
Bitmap item;
while (iterator.hasNext()) {
item = iterator.next().get();
if (item != null) {
if (item.getWidth() == width && item.getHeight() == height) {
// Remove from reusable set so it can't be used again.
iterator.remove();
return item;
}
} else {
// Remove from the set if the reference has been cleared or
// it can't be used.
iterator.remove();
}
}
// Can not find reusable bitmap
try {
return Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
} catch (OutOfMemoryError e) {
Log.e(TAG, "Out of memory");
return null;
}
}
}
@@ -0,0 +1,211 @@
/*
* Copyright (C) 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.drawable;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import com.hippo.ehviewer.R;
import com.hippo.yorozuya.MathUtils;
/**
* A drawable that can draw a "Drawer hamburger" menu or an Arrow and animate between them.
*/
public class DrawerArrowDrawable extends Drawable {
private final Paint mPaint = new Paint();
// The angle in degress that the arrow head is inclined at.
private static final float ARROW_HEAD_ANGLE = (float) Math.toRadians(45);
private final float mBarThickness;
// The length of top and bottom bars when they merge into an arrow
private final float mTopBottomArrowSize;
// The length of middle bar
private final float mBarSize;
// The length of the middle bar when arrow is shaped
private final float mMiddleArrowSize;
// The space between bars when they are parallel
private final float mBarGap;
// Whether bars should spin or not during progress
private final boolean mSpin;
// Use Path instead of canvas operations so that if color has transparency, overlapping sections
// wont look different
private final Path mPath = new Path();
// The reported intrinsic size of the drawable.
private final int mSize;
// Whether we should mirror animation when animation is reversed.
private boolean mVerticalMirror = false;
// The interpolated version of the original progress
private float mProgress;
// the amount that overlaps w/ bar size when rotation is max
private float mMaxCutForBarSize;
/**
* @param context used to get the configuration for the drawable from
*/
public DrawerArrowDrawable(Context context) {
Resources resources = context.getResources();
mPaint.setAntiAlias(true);
mPaint.setColor(resources.getColor(R.color.primary_drawable_light));
mSize = resources.getDimensionPixelSize(R.dimen.dad_drawable_size);
// round this because having this floating may cause bad measurements
mBarSize = Math.round(resources.getDimension(R.dimen.dad_bar_size));
// round this because having this floating may cause bad measurements
mTopBottomArrowSize = Math.round(resources.getDimension(R.dimen.dad_top_bottom_bar_arrow_size));
mBarThickness = resources.getDimension(R.dimen.dad_thickness);
// round this because having this floating may cause bad measurements
mBarGap = Math.round(resources.getDimension(R.dimen.dad_gap_between_bars));
mSpin = resources.getBoolean(R.bool.dad_spin_bars);
mMiddleArrowSize = resources.getDimension(R.dimen.dad_middle_bar_arrow_size);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeJoin(Paint.Join.MITER);
mPaint.setStrokeCap(Paint.Cap.BUTT);
mPaint.setStrokeWidth(mBarThickness);
mMaxCutForBarSize = (float) (mBarThickness / 2 * Math.cos(ARROW_HEAD_ANGLE));
}
/**
* If set, canvas is flipped when progress reached to end and going back to start.
*/
protected void setVerticalMirror(boolean verticalMirror) {
mVerticalMirror = verticalMirror;
}
@Override
public void draw(Canvas canvas) {
Rect bounds = getBounds();
// Interpolated widths of arrow bars
final float arrowSize = MathUtils.lerp(mBarSize, mTopBottomArrowSize, mProgress);
final float middleBarSize = MathUtils.lerp(mBarSize, mMiddleArrowSize, mProgress);
// Interpolated size of middle bar
final float middleBarCut = Math.round(MathUtils.lerp(0, mMaxCutForBarSize, mProgress));
// The rotation of the top and bottom bars (that make the arrow head)
final float rotation = MathUtils.lerp(0, ARROW_HEAD_ANGLE, mProgress);
// The whole canvas rotates as the transition happens
final float canvasRotate = MathUtils.lerp(-180, 0, mProgress);
final float arrowWidth = Math.round(arrowSize * Math.cos(rotation));
final float arrowHeight = Math.round(arrowSize * Math.sin(rotation));
mPath.rewind();
final float topBottomBarOffset = MathUtils.lerp(mBarGap + mBarThickness, -mMaxCutForBarSize,
mProgress);
final float arrowEdge = -middleBarSize / 2;
// draw middle bar
mPath.moveTo(arrowEdge + middleBarCut, 0);
mPath.rLineTo(middleBarSize - middleBarCut * 2, 0);
// bottom bar
mPath.moveTo(arrowEdge, topBottomBarOffset);
mPath.rLineTo(arrowWidth, arrowHeight);
// top bar
mPath.moveTo(arrowEdge, -topBottomBarOffset);
mPath.rLineTo(arrowWidth, -arrowHeight);
mPath.close();
canvas.save();
// Rotate the whole canvas if spinning, if not, rotate it 180 to get
// the arrow pointing the other way for RTL.
canvas.translate(bounds.centerX(), bounds.centerY());
if (mSpin) {
canvas.rotate(canvasRotate * (mVerticalMirror ? -1 : 1));
}
canvas.drawPath(mPath, mPaint);
canvas.restore();
}
@Override
public void setAlpha(int i) {
mPaint.setAlpha(i);
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
mPaint.setColorFilter(colorFilter);
}
@Override
public int getIntrinsicHeight() {
return mSize;
}
@Override
public int getIntrinsicWidth() {
return mSize;
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
@SuppressWarnings("unused")
public float getProgress() {
return mProgress;
}
@SuppressWarnings("unused")
public void setProgress(float progress) {
if (progress == 1f) {
setVerticalMirror(true);
} else if (progress == 0f) {
setVerticalMirror(false);
}
mProgress = progress;
invalidateSelf();
}
public void setMenu(long duration) {
setShape(false, duration);
}
public void setArrow(long duration) {
setShape(true, duration);
}
public void setShape(boolean arrow, long duration) {
if (!((!arrow && mProgress == 0f) || (arrow && mProgress == 1f))) {
float endProgress = arrow ? 1f : 0f;
if (duration <= 0) {
setProgress(endProgress);
} else {
ObjectAnimator oa = ObjectAnimator.ofFloat(this, "progress", endProgress);
oa.setDuration(duration);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
oa.setAutoCancel(true);
}
oa.start();
}
}
}
}
@@ -0,0 +1,175 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.drawable;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import com.hippo.yorozuya.MathUtils;
import java.util.ArrayList;
import java.util.List;
public class ImageDrawable extends Drawable implements ImageWrapper.Callback, Animatable {
private static final int TILE_SIZE = 512;
private ImageWrapper mImageWrapper;
private Paint mPaint;
private List<Tile> mTileList;
private static class Tile {
Bitmap bitmap;
int w;
int h;
int x;
int y;
}
private static final BitmapPool sBitmapPool = new BitmapPool();
public ImageDrawable(@NonNull ImageWrapper imageWrapper) {
mImageWrapper = imageWrapper;
mPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
mTileList = createTileArray();
// Render first frame
render();
// Add callback
imageWrapper.addCallback(this);
}
private List<Tile> createTileArray() {
int width = mImageWrapper.getWidth();
int height = mImageWrapper.getHeight();
int capacity = MathUtils.clamp(MathUtils.ceilDivide(width, TILE_SIZE) *
MathUtils.ceilDivide(height, TILE_SIZE), 0, 100);
List<Tile> tiles = new ArrayList<>(capacity);
for (int x = 0; x < width; x += TILE_SIZE) {
int w = Math.min(TILE_SIZE, width - x);
for (int y = 0; y < height; y += TILE_SIZE) {
int h = Math.min(TILE_SIZE, height - y);
Tile tile = new Tile();
tile.x = x;
tile.y = y;
tile.w = w;
tile.h = h;
tile.bitmap = sBitmapPool.get(w, h);
tiles.add(tile);
}
}
return tiles;
}
private void render() {
ImageWrapper imageWrapper = mImageWrapper;
if (imageWrapper.isRecycled()) {
return;
}
List<Tile> tiles = mTileList;
for (int i = 0, length = tiles.size(); i < length; i++) {
Tile tile = tiles.get(i);
if (tile.bitmap != null) {
imageWrapper.render(tile.x, tile.y, tile.bitmap, 0, 0, tile.w, tile.h, false, 0);
}
}
}
@Override
public int getIntrinsicWidth() {
return mImageWrapper.getWidth();
}
@Override
public int getIntrinsicHeight() {
return mImageWrapper.getHeight();
}
@Override
public void draw(Canvas canvas) {
List<Tile> tiles = mTileList;
for (int i = 0, length = tiles.size(); i < length; i++) {
Tile tile = tiles.get(i);
if (tile.bitmap != null) {
canvas.drawBitmap(tile.bitmap, tile.x, tile.y, mPaint);
}
}
}
@Override
public void setAlpha(int alpha) {
mPaint.setAlpha(alpha);
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
mPaint.setColorFilter(colorFilter);
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
public void recycle() {
mImageWrapper.removeCallback(this);
// Free tile's bitmap
List<Tile> tiles = mTileList;
for (int i = 0, length = tiles.size(); i < length; i++) {
Tile tile = tiles.get(i);
sBitmapPool.put(tile.bitmap);
tile.bitmap = null;
}
}
@Override
public void renderImage(ImageWrapper who) {
render();
}
@Override
public void invalidateImage(ImageWrapper who) {
invalidateSelf();
}
@Override
public void start() {
mImageWrapper.start();
}
@Override
public void stop() {
mImageWrapper.stop();
}
@Override
public boolean isRunning() {
return mImageWrapper.isRunning();
}
}
@@ -0,0 +1,180 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.drawable;
import android.graphics.Bitmap;
import android.graphics.drawable.Animatable;
import android.support.annotation.NonNull;
import android.util.Log;
import com.hippo.image.Image;
import com.hippo.yorozuya.SimpleHandler;
import java.lang.ref.WeakReference;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;
public class ImageWrapper implements Animatable, Runnable {
private static final String TAG = ImageWrapper.class.getSimpleName();
private Image mImage;
private boolean mRunning = false;
private final Set<WeakReference<Callback>> mCallbackSet = new LinkedHashSet<>();
public ImageWrapper(@NonNull Image image) {
mImage = image;
}
public int getWidth() {
return mImage.getWidth();
}
public int getHeight() {
return mImage.getHeight();
}
public boolean render(int srcX, int srcY, Bitmap dst, int dstX, int dstY,
int width, int height, boolean fillBlank, int defaultColor) {
return mImage.render(srcX, srcY, dst, dstX, dstY,
width, height, fillBlank, defaultColor);
}
public int getFrameCount() {
return mImage.getFrameCount();
}
public void recycle() {
stop();
mImage.recycle();
}
public boolean isRecycled() {
return mImage.isRecycled();
}
public boolean isLarge() {
return mImage.getWidth() * mImage.getHeight() > 512 * 512;
}
public void addCallback(@NonNull Callback callback) {
final Iterator<WeakReference<Callback>> iterator = mCallbackSet.iterator();
Callback c;
while (iterator.hasNext()) {
c = iterator.next().get();
if (c == null) {
// Remove from the set if the reference has been cleared or
// it can't be used.
iterator.remove();
} else if (c == callback) {
return;
}
}
mCallbackSet.add(new WeakReference<>(callback));
}
public void removeCallback(@NonNull Callback callback) {
final Iterator<WeakReference<Callback>> iterator = mCallbackSet.iterator();
Callback c;
while (iterator.hasNext()) {
c = iterator.next().get();
if (c == null) {
// Remove from the set if the reference has been cleared or
// it can't be used.
iterator.remove();
} else if (c == callback) {
iterator.remove();
return;
}
}
}
@Override
public void start() {
if (mImage.isRecycled() || mImage.getFrameCount() <= 1 || mRunning) {
return;
}
mRunning = true;
SimpleHandler.getInstance().postDelayed(this, Math.max(0, mImage.getDelay()));
}
@Override
public void stop() {
mRunning = false;
SimpleHandler.getInstance().removeCallbacks(this);
}
@Override
public boolean isRunning() {
return mRunning;
}
private boolean notifyUpdate() {
boolean hasCallback = false;
final Iterator<WeakReference<Callback>> iterator = mCallbackSet.iterator();
Callback callback;
while (iterator.hasNext()) {
callback = iterator.next().get();
if (callback != null) {
hasCallback = true;
callback.renderImage(this);
callback.invalidateImage(this);
} else {
// Remove from the set if the reference has been cleared or
// it can't be used.
iterator.remove();
}
}
return hasCallback;
}
@Override
public void run() {
//Log.i(TAG, this + " run");
// Check recycled
if (mImage.isRecycled()) {
mRunning = false;
return;
}
mImage.advance();
if (notifyUpdate()) {
if (mRunning) {
SimpleHandler.getInstance().postDelayed(this, Math.max(0, mImage.getDelay()));
}
} else {
// No callback ? Stop now
Log.w(TAG, "No callback");
mRunning = false;
}
}
public interface Callback {
void renderImage(ImageWrapper who);
void invalidateImage(ImageWrapper who);
}
}
@@ -16,15 +16,19 @@
package com.hippo.ehviewer;
import android.app.ActivityManager;
import android.app.Application;
import android.content.Context;
import android.support.annotation.NonNull;
import com.hippo.conaco.Conaco;
import com.hippo.drawable.ImageWrapper;
import com.hippo.ehviewer.client.EhClient;
import com.hippo.ehviewer.client.EhCookieStore;
import com.hippo.network.StatusCodeException;
import com.hippo.okhttp.CookieDB;
import java.io.File;
import java.util.concurrent.TimeUnit;
import okhttp3.OkHttpClient;
@@ -34,6 +38,8 @@ public class EhApplication extends Application {
private EhCookieStore mEhCookieStore;
private EhClient mEhClient;
private OkHttpClient mOkHttpClient;
private ImageWrapperHelper mImageWrapperHelper;
private Conaco<ImageWrapper> mConaco;
@Override
public void onCreate() {
@@ -62,6 +68,7 @@ public class EhApplication extends Application {
return application.mEhClient;
}
@NonNull
public static OkHttpClient getOkHttpClient(@NonNull Context context) {
EhApplication application = ((EhApplication) context.getApplicationContext());
if (application.mOkHttpClient == null) {
@@ -74,4 +81,37 @@ public class EhApplication extends Application {
}
return application.mOkHttpClient;
}
@NonNull
public static ImageWrapperHelper getImageWrapperHelper(@NonNull Context context) {
EhApplication application = ((EhApplication) context.getApplicationContext());
if (application.mImageWrapperHelper == null) {
application.mImageWrapperHelper = new ImageWrapperHelper();
}
return application.mImageWrapperHelper;
}
private static int getMemoryCacheMaxSize(Context context) {
final ActivityManager activityManager = (ActivityManager) context.
getSystemService(Context.ACTIVITY_SERVICE);
return Math.min(20 * 1024 * 1024,
Math.round(0.2f * activityManager.getMemoryClass() * 1024 * 1024));
}
@NonNull
public static Conaco<ImageWrapper> getConaco(@NonNull Context context) {
EhApplication application = ((EhApplication) context.getApplicationContext());
if (application.mConaco == null) {
Conaco.Builder<ImageWrapper> builder = new Conaco.Builder<>();
builder.hasMemoryCache = true;
builder.memoryCacheMaxSize = getMemoryCacheMaxSize(context);
builder.hasDiskCache = true;
builder.diskCacheDir = new File(context.getCacheDir(), "thumb");
builder.diskCacheMaxSize = 80 * 1024 * 1024; // 80MB
builder.okHttpClient = getOkHttpClient(context);
builder.objectHelper = getImageWrapperHelper(context);
application.mConaco = builder.build();
}
return application.mConaco;
}
}
@@ -0,0 +1,26 @@
package com.hippo.ehviewer;
public class EhCacheKeyFactory {
public static String getThumbKey(int gid) {
return "thumb:" + gid;
}
/**
* @param index start from 0
*/
public static String getNormalPreviewKey(int gid, int index) {
return "preview:normal:" + gid + ":" + index;
}
/**
* @param index start from 0
*/
public static String getLargePreviewKey(int gid, int index) {
return "preview:large:" + gid + ":" + index;
}
public static String getImageKey(int gid, int index) {
return "image:" + gid + ":" + index;
}
}
@@ -0,0 +1,68 @@
/*
* Copyright 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.hippo.conaco.ValueHelper;
import com.hippo.conaco.ValueHolder;
import com.hippo.drawable.ImageWrapper;
import com.hippo.image.Image;
import com.hippo.yorozuya.io.InputStreamPipe;
public class ImageWrapperHelper implements ValueHelper<ImageWrapper> {
private static final String TAG = ImageWrapperHelper.class.getSimpleName();
@Nullable
@Override
public ImageWrapper decode(@NonNull InputStreamPipe isPipe) {
try {
isPipe.obtain();
Image image = Image.decode(isPipe.open(), false);
if (image != null) {
return new ImageWrapper(image);
} else {
return null;
}
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
isPipe.close();
isPipe.release();
}
}
@Override
public int sizeOf(@NonNull String key, @NonNull ImageWrapper value) {
return value.getWidth() * value.getHeight() * 4;
}
@Override
public void onRemove(@NonNull String key, @NonNull ValueHolder<ImageWrapper> oldValue) {
if (oldValue.isFree()) {
oldValue.getValue().recycle();
}
}
@Override
public boolean useMemoryCache(@NonNull String key, ValueHolder<ImageWrapper> holder) {
return holder == null || !holder.getValue().isLarge();
}
}
@@ -37,6 +37,7 @@ public class EhClient {
public static final String TAG = EhClient.class.getSimpleName();
public static final int METHOD_SIGN_IN = 0;
public static final int METHOD_GET_GALLERY_LIST = 1;
private final ThreadPoolExecutor mRequestThreadPool;
private final OkHttpClient mOkHttpClient;
@@ -113,12 +114,24 @@ public class EhClient {
}
}
private Object getGalleryList(Object... params) throws Exception {
Call call = EhEngine.prepareGetGalleryList(mOkHttpClient, (String) params[0]);
if (!mStop) {
mCall = call;
return EhEngine.doGetGalleryList(call);
} else {
throw new CancelledException();
}
}
@Override
protected Object doInBackground(Object... params) {
try {
switch (mMethod) {
case METHOD_SIGN_IN:
return signIn(params);
case METHOD_GET_GALLERY_LIST:
return getGalleryList(params);
default:
return new IllegalStateException("Can't detect method " + mMethod);
}
@@ -29,19 +29,15 @@ public class EhCookieStore extends CookieDBStore {
private static final String KEY_IPD_MEMBER_ID = "ipb_member_id";
private static final String KEY_IPD_PASS_HASH = "ipb_pass_hash";
private static final String KEY_YAY = "yay";
public void cleanUpForSignIn() {
remove(EhUrl.HOST_E, KEY_IPD_MEMBER_ID);
remove(EhUrl.HOST_E, KEY_IPD_PASS_HASH);
remove(EhUrl.HOST_EX, KEY_IPD_MEMBER_ID);
remove(EhUrl.HOST_EX, KEY_IPD_PASS_HASH);
remove(EhUrl.HOST_EX, KEY_YAY);
remove(EhUrl.DOMAIN_E);
remove(EhUrl.DOMAIN_EX);
}
public boolean hasSignedIn() {
return contain(EhUrl.HOST_EX, KEY_IPD_MEMBER_ID) &&
contain(EhUrl.HOST_EX, KEY_IPD_PASS_HASH);
return contain(EhUrl.DOMAIN_EX, KEY_IPD_MEMBER_ID) &&
contain(EhUrl.DOMAIN_EX, KEY_IPD_PASS_HASH);
}
private Cookie newCookie(Cookie cookie, String newDomain) {
@@ -72,10 +68,10 @@ public class EhCookieStore extends CookieDBStore {
for (Cookie cookie: cookies) {
cs.add(cookie);
if (EhUrl.HOST_E.equals(cookie.domain()) &&
if (EhUrl.DOMAIN_E.equals(cookie.domain()) &&
(KEY_IPD_MEMBER_ID.equals(cookie.name()) ||
KEY_IPD_PASS_HASH.equals(cookie.name()))) {
cs.add(newCookie(cookie, CookieDB.cookiesDomain(EhUrl.HOST_EX)));
cs.add(newCookie(cookie, CookieDB.cookiesDomain(EhUrl.DOMAIN_EX)));
}
}
@@ -16,13 +16,16 @@
package com.hippo.ehviewer.client;
import android.support.annotation.Nullable;
import android.util.Log;
import com.hippo.ehviewer.client.parser.GalleryListParser;
import com.hippo.ehviewer.client.parser.SignInParser;
import com.hippo.network.StatusCodeException;
import okhttp3.Call;
import okhttp3.FormBody;
import okhttp3.Headers;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
@@ -31,20 +34,24 @@ public class EhEngine {
private static final String TAG = EhEngine.class.getSimpleName();
public static final String UNKNOWN = "Unknown";
private static final String SAD_PANDA_DISPOSITION = "inline; filename=\"sadpanda.jpg\"";
private static final String SAD_PANDA_TYPE = "image/gif";
private static final String SAD_PANDA_LENGTH = "9615";
private static void throwException(Call call, int code, String body, Exception e) throws Exception {
private static void throwException(Call call, int code, @Nullable Headers headers,
@Nullable String body, Exception e) throws Exception {
if (call.isCanceled()) {
throw new CancelledException();
}
if (e instanceof EhException) {
if (!UNKNOWN.equals(e.getMessage())) {
throw e;
}
// Check sad panda
if (headers != null && SAD_PANDA_DISPOSITION.equals(headers.get("Content-Disposition")) &&
SAD_PANDA_TYPE.equals(headers.get("Content-Type")) &&
SAD_PANDA_LENGTH.equals(headers.get("Content-Length"))) {
throw new EhException("Sad Panda");
}
if (!body.contains("<")) {
if (body != null && !body.contains("<")) {
throw new EhException(body);
}
@@ -71,15 +78,40 @@ public class EhEngine {
public static String doSignIn(Call call) throws Exception {
String body = null;
Headers headers = null;
int code = -1;
try {
Response response = call.execute();
code = response.code();
headers = response.headers();
body = response.body().string();
return SignInParser.parse(body);
} catch (Exception e) {
throwException(call, code, body, e);
throwException(call, code, headers, body, e);
throw e;
}
}
public static Call prepareGetGalleryList(OkHttpClient okHttpClient, String url) {
Log.d(TAG, url);
Request request = new EhRequestBuilder(url).build();
return okHttpClient.newCall(request);
}
public static GalleryListParser.Result doGetGalleryList(Call call) throws Exception {
String body = null;
Headers headers = null;
int code = -1;
try {
Response response = call.execute();
code = response.code();
headers = response.headers();
body = response.body().string();
return GalleryListParser.parse(body);
} catch (Exception e) {
throwException(call, code, headers, body, e);
throw e;
}
}
}
@@ -18,8 +18,10 @@ package com.hippo.ehviewer.client;
public class EhUrl {
public static final String HOST_EX = "exhentai.org";
public static final String HOST_E = "e-hentai.org";
public static final String DOMAIN_EX = "exhentai.org";
public static final String DOMAIN_E = "e-hentai.org";
public static final String HOST_EX = "http://" + DOMAIN_EX + "/";
public static final String API_SIGN_IN = "http://forums.e-hentai.org/index.php?act=Login&CODE=01";
@@ -17,14 +17,114 @@
package com.hippo.ehviewer.client;
import android.content.Context;
import android.graphics.Color;
import android.text.TextUtils;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.Settings;
import com.hippo.ehviewer.client.data.GalleryInfo;
public class EhUtils {
public static final int NONE = -1; // Use it for homepage
public static final int UNKNOWN = 0x400;
public static final int BG_COLOR_DOUJINSHI = 0xfff44336;
public static final int BG_COLOR_MANGA = 0xffff9800;
public static final int BG_COLOR_ARTIST_CG = 0xfffbc02d;
public static final int BG_COLOR_GAME_CG = 0xff4caf50;
public static final int BG_COLOR_WESTERN = 0xff8bc34a;
public static final int BG_COLOR_NON_H = 0xff2196f3;
public static final int BG_COLOR_IMAGE_SET = 0xff3f51b5;
public static final int BG_COLOR_COSPLAY = 0xff9c27b0;
public static final int BG_COLOR_ASIAN_PORN = 0xff9575cd;
public static final int BG_COLOR_MISC = 0xfff06292;
public static final int BG_COLOR_UNKNOWN = Color.BLACK;
private static final int[] CATEGORY_VALUES = {
EhConfig.MISC,
EhConfig.DOUJINSHI,
EhConfig.MANGA,
EhConfig.ARTIST_CG,
EhConfig.GAME_CG,
EhConfig.IMAGE_SET,
EhConfig.COSPLAY,
EhConfig.ASIAN_PORN,
EhConfig.NON_H,
EhConfig.WESTERN,
UNKNOWN };
private static final String[][] CATEGORY_STRINGS = {
new String[] { "misc" },
new String[] { "doujinshi" },
new String[] { "manga" },
new String[] { "artistcg", "Artist CG Sets" },
new String[] { "gamecg", "Game CG Sets" },
new String[] { "imageset", "Image Sets" },
new String[] { "cosplay" },
new String[] { "asianporn", "Asian Porn" },
new String[] { "non-h" },
new String[] { "western" },
new String[] { "unknown" }
};
public static int getCategory(String type) {
int i;
for (i = 0; i < CATEGORY_STRINGS.length - 1; i++) {
for (String str : CATEGORY_STRINGS[i])
if (str.equalsIgnoreCase(type))
return CATEGORY_VALUES[i];
}
return CATEGORY_VALUES[i];
}
public static String getCategory(int type) {
int i;
for (i = 0; i < CATEGORY_VALUES.length - 1; i++) {
if (CATEGORY_VALUES[i] == type)
break;
}
return CATEGORY_STRINGS[i][0];
}
public static int getCategoryColor(int category) {
switch (category) {
case EhConfig.DOUJINSHI:
return BG_COLOR_DOUJINSHI;
case EhConfig.MANGA:
return BG_COLOR_MANGA;
case EhConfig.ARTIST_CG:
return BG_COLOR_ARTIST_CG;
case EhConfig.GAME_CG:
return BG_COLOR_GAME_CG;
case EhConfig.WESTERN:
return BG_COLOR_WESTERN;
case EhConfig.NON_H:
return BG_COLOR_NON_H;
case EhConfig.IMAGE_SET:
return BG_COLOR_IMAGE_SET;
case EhConfig.COSPLAY:
return BG_COLOR_COSPLAY;
case EhConfig.ASIAN_PORN:
return BG_COLOR_ASIAN_PORN;
case EhConfig.MISC:
return BG_COLOR_MISC;
default:
return BG_COLOR_UNKNOWN;
}
}
public static boolean hasSignedIn(Context context) {
return EhApplication.getEhCookieStore(context).hasSignedIn() &&
Settings.getDisplayName() != null;
}
public static String getSuitableTitle(GalleryInfo gi) {
if (false) {//;Settings.getJpnTitle()) {
return TextUtils.isEmpty(gi.titleJpn) ? gi.title : gi.titleJpn;
} else {
return gi.title;
}
}
}
@@ -0,0 +1,151 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.data;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.regex.Pattern;
public class GalleryInfo implements Parcelable {
/**
* ISO 639-1
*/
@SuppressWarnings("unused")
public static final String S_LANG_JA = "JA";
public static final String S_LANG_EN = "EN";
public static final String S_LANG_ZH = "ZH";
public static final String S_LANG_NL = "NL";
public static final String S_LANG_FR = "FR";
public static final String S_LANG_DE = "DE";
public static final String S_LANG_HU = "HU";
public static final String S_LANG_IT = "IT";
public static final String S_LANG_KO = "KO";
public static final String S_LANG_PL = "PL";
public static final String S_LANG_PT = "PT";
public static final String S_LANG_RU = "RU";
public static final String S_LANG_ES = "ES";
public static final String S_LANG_TH = "TH";
public static final String S_LANG_VI = "VI";
public static final String[] S_LANGS = {
S_LANG_EN,
S_LANG_ZH,
S_LANG_ES,
S_LANG_KO,
S_LANG_RU,
S_LANG_FR,
S_LANG_PT,
S_LANG_TH,
S_LANG_DE,
S_LANG_IT,
S_LANG_VI,
S_LANG_PL,
S_LANG_HU,
S_LANG_NL,
};
public static final Pattern[] S_LANG_PATTERNS = {
Pattern.compile("[(\\[]eng(?:lish)?[)\\]]", Pattern.CASE_INSENSITIVE),
Pattern.compile("[((\\[]chinese[))\\]]|[汉漢]化|中[国國][语語]|中文|[(\\[]CN[)\\]]", Pattern.CASE_INSENSITIVE),
Pattern.compile("[(\\[]spanish[)\\]]|[(\\[]Español[)\\]]", Pattern.CASE_INSENSITIVE),
Pattern.compile("[(\\[]korean?[)\\]]", Pattern.CASE_INSENSITIVE),
Pattern.compile("[(\\[]rus(?:sian)?[)\\]]", Pattern.CASE_INSENSITIVE),
Pattern.compile("[(\\[]fr(?:ench)?[)\\]]", Pattern.CASE_INSENSITIVE),
Pattern.compile("[(\\[]portuguese", Pattern.CASE_INSENSITIVE),
Pattern.compile("[(\\[]thai(?: ภาษาไทย)?[)\\]]", Pattern.CASE_INSENSITIVE),
Pattern.compile("[(\\[]german[)\\]]", Pattern.CASE_INSENSITIVE),
Pattern.compile("[(\\[]italiano?[)\\]]", Pattern.CASE_INSENSITIVE),
Pattern.compile("[(\\[]vietnamese(?: Tiếng Việt)?[)\\]]", Pattern.CASE_INSENSITIVE),
Pattern.compile("[(\\[]polish[)\\]]", Pattern.CASE_INSENSITIVE),
Pattern.compile("[(\\[]hun(?:garian)?[)\\]]", Pattern.CASE_INSENSITIVE),
Pattern.compile("[(\\[]dutch[)\\]]", Pattern.CASE_INSENSITIVE),
};
public int gid;
public String token;
public String title;
public String titleJpn;
public String thumb;
public int category;
public String posted;
public String uploader;
public float rating;
/**
* language from title
*/
public String simpleLanguage;
public final void generateSLang() {
for (int i = 0; i < S_LANGS.length; i++) {
if (S_LANG_PATTERNS[i].matcher(title).find()) {
simpleLanguage = S_LANGS[i];
return;
}
}
simpleLanguage = null;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(this.gid);
dest.writeString(this.token);
dest.writeString(this.title);
dest.writeString(this.titleJpn);
dest.writeString(this.thumb);
dest.writeInt(this.category);
dest.writeString(this.posted);
dest.writeString(this.uploader);
dest.writeFloat(this.rating);
dest.writeString(this.simpleLanguage);
}
public GalleryInfo() {}
protected GalleryInfo(Parcel in) {
this.gid = in.readInt();
this.token = in.readString();
this.title = in.readString();
this.titleJpn = in.readString();
this.thumb = in.readString();
this.category = in.readInt();
this.posted = in.readString();
this.uploader = in.readString();
this.rating = in.readFloat();
this.simpleLanguage = in.readString();
}
public static final Parcelable.Creator<GalleryInfo> CREATOR = new Parcelable.Creator<GalleryInfo>() {
@Override
public GalleryInfo createFromParcel(Parcel source) {
return new GalleryInfo(source);
}
@Override
public GalleryInfo[] newArray(int size) {
return new GalleryInfo[size];
}
};
}
@@ -0,0 +1,266 @@
/*
* Copyright (C) 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.data;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.IntDef;
import com.hippo.ehviewer.client.EhConfig;
import com.hippo.ehviewer.client.EhUrl;
import com.hippo.ehviewer.client.EhUtils;
import com.hippo.ehviewer.widget.AdvanceSearchTable;
import com.hippo.network.UrlBuilder;
import java.io.UnsupportedEncodingException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.net.URLEncoder;
public class ListUrlBuilder implements Cloneable, Parcelable {
@IntDef({MODE_NORMAL, MODE_UPLOADER, MODE_TAG, MODE_WHATS_HOT, MODE_IMAGE_SEARCH})
@Retention(RetentionPolicy.SOURCE)
private @interface Mode {}
// Mode
public static final int MODE_NORMAL = 0x0;
public static final int MODE_UPLOADER = 0x1;
public static final int MODE_TAG = 0x2;
public static final int MODE_WHATS_HOT = 0x3;
public static final int MODE_IMAGE_SEARCH = 0x4;
public static final int ALL_CATEGORT = EhUtils.UNKNOWN - 1;
//DOUJINSHI|MANGA|ARTIST_CG|GAME_CG|WESTERN|NON_H|IMAGE_SET|COSPLAY|ASIAN_PORN|MISC;
public static final int DEFAULT_ADVANCE = AdvanceSearchTable.SNAME | AdvanceSearchTable.STAGS;
public static final int DEFAULT_MIN_RATING = 2;
@Mode
private int mMode = MODE_NORMAL;
private int mPageIndex = 0;
private int mCategory = EhUtils.NONE;
private String mKeyword = null;
private int mAdvanceSearch = -1;
private int mMinRating = -1;
/**
* Make this ListUrlBuilder point to homepage
*/
public void reset() {
mMode = MODE_NORMAL;
mPageIndex = 0;
mCategory = EhUtils.NONE;
mKeyword = null;
mAdvanceSearch = -1;
mMinRating = -1;
}
@Override
public ListUrlBuilder clone() {
try {
return (ListUrlBuilder) super.clone();
} catch (CloneNotSupportedException e) {
throw new IllegalStateException(e);
}
}
@Mode
public int getMode() {
return mMode;
}
public void setMode(@Mode int mode) {
mMode = mode;
}
public int getPageIndex() {
return mPageIndex;
}
public void setPageIndex(int pageIndex) {
mPageIndex = pageIndex;
}
public int getCategory() {
return mCategory;
}
public void setCategory(int category) {
mCategory = category;
}
public String getKeyword() {
return mKeyword;
}
public void setKeyword(String keyword) {
mKeyword = keyword;
}
public int getAdvanceSearch() {
return mAdvanceSearch;
}
public void setAdvanceSearch(int advanceSearch) {
mAdvanceSearch = advanceSearch;
}
public int getMinRating() {
return mMinRating;
}
public void setMinRating(int minRating) {
mMinRating = minRating;
}
/**
* Make them the same
* @param glub The template
*/
public void set(ListUrlBuilder glub) {
mMode = glub.mMode;
mPageIndex = glub.mPageIndex;
mCategory = glub.mCategory;
mKeyword = glub.mKeyword;
mAdvanceSearch = glub.mAdvanceSearch;
mMinRating = glub.mMinRating;
}
/*
public void set(QuickSearch quickSearch) {
mMode = quickSearch.mode;
mPageIndex = 0;
mCategory = quickSearch.category;
mKeyword = quickSearch.keyword;
mAdvanceSearch = quickSearch.advancedSearch;
mMinRating = quickSearch.minRating;
}
*/
public String build() {
switch (mMode) {
default:
case MODE_NORMAL:
case MODE_UPLOADER: {
boolean filter = false;
UrlBuilder ub = new UrlBuilder(EhUrl.HOST_EX);
if (mCategory != EhUtils.NONE) {
ub.addQuery("f_doujinshi", ((mCategory & EhConfig.DOUJINSHI) == 0) ? "0" : "1");
ub.addQuery("f_manga", ((mCategory & EhConfig.MANGA) == 0) ? "0" : "1");
ub.addQuery("f_artistcg", ((mCategory & EhConfig.ARTIST_CG) == 0) ? "0" : "1");
ub.addQuery("f_gamecg", ((mCategory & EhConfig.GAME_CG) == 0) ? "0" : "1");
ub.addQuery("f_western", ((mCategory & EhConfig.WESTERN) == 0) ? "0" : "1");
ub.addQuery("f_non-h", ((mCategory & EhConfig.NON_H) == 0) ? "0" : "1");
ub.addQuery("f_imageset", ((mCategory & EhConfig.IMAGE_SET) == 0) ? "0" : "1");
ub.addQuery("f_cosplay", ((mCategory & EhConfig.COSPLAY) == 0) ? "0" : "1");
ub.addQuery("f_asianporn", ((mCategory & EhConfig.ASIAN_PORN) == 0) ? "0" : "1");
ub.addQuery("f_misc", ((mCategory & EhConfig.MISC) == 0) ? "0" : "1");
filter = true;
}
// Search key
if (mKeyword != null) {
try {
ub.addQuery("f_search", URLEncoder.encode(mKeyword, "UTF-8"));
filter = true;
} catch (UnsupportedEncodingException e) {
// Empty
}
}
// Page index
if (mPageIndex != 0) {
ub.addQuery("page", mPageIndex);
}
// Advance search
if (mAdvanceSearch != -1) {
ub.addQuery("advsearch", "1");
if((mAdvanceSearch & AdvanceSearchTable.SNAME) != 0) ub.addQuery("f_sname", "on");
if((mAdvanceSearch & AdvanceSearchTable.STAGS) != 0) ub.addQuery("f_stags", "on");
if((mAdvanceSearch & AdvanceSearchTable.SDESC) != 0) ub.addQuery("f_sdesc", "on");
if((mAdvanceSearch & AdvanceSearchTable.STORR) != 0) ub.addQuery("f_storr", "on");
if((mAdvanceSearch & AdvanceSearchTable.STO) != 0) ub.addQuery("f_sto", "on");
if((mAdvanceSearch & AdvanceSearchTable.SDT1) != 0) ub.addQuery("f_sdt1", "on");
if((mAdvanceSearch & AdvanceSearchTable.SDT2) != 0) ub.addQuery("f_sdt2", "on");
if((mAdvanceSearch & AdvanceSearchTable.SH) != 0) ub.addQuery("f_sh", "on");
// Set min star
if (mMinRating != -1) {
ub.addQuery("f_sr", "on");
ub.addQuery("f_srdd", mMinRating);
}
filter = true;
}
// Add filter foot
if (filter) {
ub.addQuery("f_apply", "Apply+Filter");
}
return ub.build();
}
case MODE_TAG: {
StringBuilder sb = new StringBuilder(EhUrl.HOST_EX);
sb.append("tag/");
try {
sb.append(URLEncoder.encode(mKeyword, "UTF-8"));
} catch (UnsupportedEncodingException e) {
// Empty
}
return sb.toString();
}
}
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(this.mMode);
dest.writeInt(this.mPageIndex);
dest.writeInt(this.mCategory);
dest.writeString(this.mKeyword);
dest.writeInt(this.mAdvanceSearch);
dest.writeInt(this.mMinRating);
}
public ListUrlBuilder() {}
protected ListUrlBuilder(Parcel in) {
mMode = in.readInt();
mPageIndex = in.readInt();
mCategory = in.readInt();
mKeyword = in.readString();
mAdvanceSearch = in.readInt();
mMinRating = in.readInt();
}
public static final Parcelable.Creator<ListUrlBuilder> CREATOR = new Parcelable.Creator<ListUrlBuilder>() {
@Override
public ListUrlBuilder createFromParcel(Parcel source) {
return new ListUrlBuilder(source);
}
@Override
public ListUrlBuilder[] newArray(int size) {
return new ListUrlBuilder[size];
}
};
}
@@ -0,0 +1,28 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.exception;
import com.hippo.ehviewer.client.EhException;
public class NotFoundException extends EhException {
public static final String MESSAGE = "Not found";
public NotFoundException() {
super(MESSAGE);
}
}
@@ -0,0 +1,33 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.exception;
import com.hippo.ehviewer.client.EhException;
public class ParseException extends EhException {
private String mBody;
public ParseException(String detailMessage, String body) {
super(detailMessage);
mBody = body;
}
public String getBody() {
return mBody;
}
}
@@ -0,0 +1,46 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.parser;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class GalleryDetailUrlParser {
public static final Pattern URL_PATTERN = Pattern.compile("/(\\d+)/(\\w+)");
public static Result parser(String url) {
if (url == null) {
return null;
}
Matcher m = URL_PATTERN.matcher(url);
if (m.find()) {
Result result = new Result();
result.gid = Integer.parseInt(m.group(1));
result.token = m.group(2);
return result;
} else {
return null;
}
}
public static class Result {
public int gid;
public String token;
}
}
@@ -0,0 +1,139 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.parser;
import android.support.annotation.NonNull;
import com.hippo.ehviewer.client.EhUtils;
import com.hippo.ehviewer.client.data.GalleryInfo;
import com.hippo.ehviewer.client.exception.NotFoundException;
import com.hippo.ehviewer.client.exception.ParseException;
import com.hippo.yorozuya.NumberUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class GalleryListParser {
static final Pattern PAGES_PATTERN = Pattern.compile("<a[^<>]+>([\\d]+)</a></td><td[^<>]+>(?:<a[^<>]+>)?&");
static final Pattern PATTERN = Pattern.compile(
"<td class=\"itdc\">(?:<a.+?>)?<img.+?alt=\"(.+?)\".+?/>(?:</a>)?</td>" // category
+ "<td.+?>(.+?)</td>" // posted
+ "<td.+?><div.+?><div.+?height:(\\d+)px; width:(\\d+)px\">"
+ "(?:<img.+?src=\"(.+?)\".+?alt=\"(.+?)\" style.+?/>"
+ "|init~([^<>\"~]+~[^<>\"~]+)~([^<>]+))" // thumb and title
+ "</div>"
+ ".+?"
+ "<div class=\"it5\"><a href=\"([^<>\"]+)\"[^<>]+>(.+?)</a></div>" // url and title
+ ".+?"
+ "<div class=\"ir it4r\" style=\"([^<>\"]+)\">" // rating
+ ".+?"
+ "<td class=\"itu\"><div><a.+?>(.+?)</a>"); // uploader
static final Pattern RATING_PATTERN = Pattern.compile("\\d+px");
public static class Result {
public int pages;
public List<GalleryInfo> galleryInfos;
}
public static Result parse(@NonNull String body) throws Exception {
Result result = new Result();
Matcher m;
// pages
m = PAGES_PATTERN.matcher(body);
if (m.find()) {
result.pages = ParserUtils.parseInt(m.group(1));
} else if (body.contains("No hits found</p>")) {
throw new NotFoundException();
} else {
// Can not get page number
throw new ParseException("Can't parse gallery list", body);
}
if (result.pages > 0) {
List<GalleryInfo> list = new ArrayList<>(25);
result.galleryInfos = list;
m = PATTERN.matcher(body);
while (m.find()) {
GalleryInfo gi = new GalleryInfo();
gi.category = EhUtils.getCategory(ParserUtils.trim(m.group(1)));
gi.posted = ParserUtils.trim(m.group(2));
//gi.thumbHeight = ParserUtils.parseInt(m.group(3));
//gi.thumbWidth = ParserUtils.parseInt(m.group(4));
if (m.group(5) == null) {
gi.thumb = ParserUtils.trim("http://" + m.group(7).replace('~', '/'));
gi.title = ParserUtils.trim(m.group(8));
} else {
gi.thumb = ParserUtils.trim(m.group(5));
gi.title = ParserUtils.trim(m.group(6));
}
Matcher matcher = GalleryDetailUrlParser.URL_PATTERN.matcher(m.group(9));
if (matcher.find()) {
gi.gid = ParserUtils.parseInt(matcher.group(1));
gi.token = ParserUtils.trim(matcher.group(2));
} else {
continue;
}
gi.rating = NumberUtils.parseFloatSafely(getRating(m.group(11)), Float.NaN);
gi.uploader = ParserUtils.trim(m.group(12));
gi.generateSLang();
list.add(gi);
}
if (list.size() == 0) {
throw new ParseException("Can't parse gallery list", body);
}
}
return result;
}
private static String getRating(String rawRate) {
Matcher m = RATING_PATTERN.matcher(rawRate);
int num1;
int num2;
int rate = 5;
String re;
if (m.find()) {
num1 = ParserUtils.parseInt(m.group().replace("px", ""));
} else {
return null;
}
if (m.find()) {
num2 = ParserUtils.parseInt(m.group().replace("px", ""));
} else {
return null;
}
rate = rate - num1 / 16;
if (num2 == 21) {
rate--;
re = Integer.toString(rate);
re = re + ".5";
} else
re = Integer.toString(rate);
return re;
}
}
@@ -0,0 +1,34 @@
/*
* Copyright 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.parser;
import com.hippo.yorozuya.StringUtils;
public class ParserUtils {
public static String trim(String str) {
// Avoid null
if (str == null) {
str = "";
}
return StringUtils.unescapeXml(str).trim();
}
public static int parseInt(String str) {
return Integer.parseInt(trim(str).replace(",", ""));
}
}
@@ -16,8 +16,8 @@
package com.hippo.ehviewer.client.parser;
import com.hippo.ehviewer.client.EhEngine;
import com.hippo.ehviewer.client.EhException;
import com.hippo.ehviewer.client.exception.ParseException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -38,7 +38,7 @@ public class SignInParser {
if (m.find()) {
throw new EhException(m.group(1) == null ? m.group(2) : m.group(1));
} else {
throw new EhException(EhEngine.UNKNOWN);
throw new ParseException("Can't parse sign in", body);
}
}
}
@@ -17,9 +17,11 @@
package com.hippo.ehviewer.ui;
import android.os.Bundle;
import android.support.annotation.IdRes;
import android.support.annotation.Nullable;
import android.support.design.widget.NavigationView;
import android.support.v4.widget.DrawerLayout;
import android.view.Gravity;
import android.view.MenuItem;
import com.hippo.ehviewer.R;
@@ -34,6 +36,7 @@ public final class MainActivity extends StageActivity
implements NavigationView.OnNavigationItemSelectedListener {
private DrawerLayout mDrawerLayout;
private NavigationView mNavView;
@Override
public int getContainerViewId() {
@@ -46,9 +49,9 @@ public final class MainActivity extends StageActivity
setContentView(R.layout.activity_main);
mDrawerLayout = (DrawerLayout) findViewById(R.id.draw_view);
mNavView = (NavigationView) findViewById(R.id.nav_view);
NavigationView navView = (NavigationView) findViewById(R.id.nav_view);
navView.setNavigationItemSelectedListener(this);
mNavView.setNavigationItemSelectedListener(this);
if (savedInstanceState == null) {
onInit();
@@ -63,6 +66,18 @@ public final class MainActivity extends StageActivity
}
}
public void toggleDrawer(int drawerGravity) {
if (mDrawerLayout.isDrawerOpen(drawerGravity)) {
mDrawerLayout.closeDrawer(drawerGravity);
} else {
mDrawerLayout.openDrawer(drawerGravity);
}
}
public void setNavCheckedItem(@IdRes int resId) {
mNavView.setCheckedItem(resId);
}
private void onInit() {
if (Settings.getShowWarning()) {
setDrawerLayoutEnable(false);
@@ -72,7 +87,18 @@ public final class MainActivity extends StageActivity
startScene(LoginScene.class);
} else {
setDrawerLayoutEnable(true);
startScene(GalleryListScene.class);
Bundle args = new Bundle();
args.putString(GalleryListScene.KEY_ACTION, GalleryListScene.ACTION_HOMEPAGE);
startScene(GalleryListScene.class, args);
}
}
@Override
public void onBackPressed() {
if (mDrawerLayout.isDrawerOpen(Gravity.LEFT) || mDrawerLayout.isDrawerOpen(Gravity.RIGHT)) {
mDrawerLayout.closeDrawers();
} else {
super.onBackPressed();
}
}
@@ -16,6 +16,8 @@
package com.hippo.ehviewer.ui.scene;
import android.os.Bundle;
import android.support.annotation.IdRes;
import android.support.v4.app.FragmentActivity;
import com.hippo.ehviewer.ui.MainActivity;
@@ -29,4 +31,29 @@ public class BaseScene extends SceneFragment {
((MainActivity) activity).setDrawerLayoutEnable(enable);
}
}
public void toggleDrawer(int drawerGravity) {
FragmentActivity activity = getActivity();
if (activity instanceof MainActivity) {
((MainActivity) activity).toggleDrawer(drawerGravity);
}
}
public void setNavCheckedItem(@IdRes int resId) {
FragmentActivity activity = getActivity();
if (activity instanceof MainActivity) {
((MainActivity) activity).setNavCheckedItem(resId);
}
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null) {
onRestoreInstanceState(savedInstanceState);
}
}
protected void onRestoreInstanceState(Bundle savedInstanceState) {
}
}
@@ -16,27 +16,677 @@
package com.hippo.ehviewer.ui.scene;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.style.ImageSpan;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.hippo.anani.SimpleAnimatorListener;
import com.hippo.drawable.AddDeleteDrawable;
import com.hippo.drawable.DrawerArrowDrawable;
import com.hippo.easyrecyclerview.EasyRecyclerView;
import com.hippo.easyrecyclerview.FastScroller;
import com.hippo.easyrecyclerview.MarginItemDecoration;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.EhCacheKeyFactory;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.client.EhClient;
import com.hippo.ehviewer.client.EhRequest;
import com.hippo.ehviewer.client.EhUtils;
import com.hippo.ehviewer.client.data.GalleryInfo;
import com.hippo.ehviewer.client.data.ListUrlBuilder;
import com.hippo.ehviewer.client.parser.GalleryListParser;
import com.hippo.ehviewer.widget.SearchBar;
import com.hippo.ehviewer.widget.SearchBarLayout;
import com.hippo.ehviewer.widget.SearchLayout;
import com.hippo.ehviewer.widget.SimpleRatingView;
import com.hippo.rippleold.RippleSalon;
import com.hippo.vector.VectorDrawable;
import com.hippo.view.ViewTransition;
import com.hippo.widget.ContentLayout;
import com.hippo.widget.LoadImageView;
import com.hippo.widget.OffsetLayout;
import com.hippo.yorozuya.MathUtils;
public class GalleryListScene extends BaseScene {
public final class GalleryListScene extends BaseScene
implements EasyRecyclerView.OnItemClickListener, SearchBar.Helper,
SearchBar.OnStateChangeListener, FastScroller.OnDragHandlerListener,
SearchLayout.Helper {
public final static String KEY_ACTION = "action";
public final static String ACTION_HOMEPAGE = "action_homepage";
public final static String ACTION_WHATS_HOT = "action_whats_hot";
public final static String KEY_LIST_URL_BUILDER = "list_url_builder";
private final static int STATE_NORMAL = 0;
private final static int STATE_SIMPLE_SEARCH = 1;
private final static int STATE_SEARCH = 2;
private final static int STATE_SEARCH_SHOW_LIST = 3;
private static final long ANIMATE_TIME = 300L;
private EhClient mClient;
private SearchBarLayout mSearchBarLayout;
private ContentLayout mContentLayout;
private EasyRecyclerView mRecyclerView;
private SearchLayout mSearchLayout;
private SearchBar mSearchBar;
private ViewTransition mViewTransition;
private GalleryListAdapter mAdapter;
private GalleryListHelper mHelper;
private DrawerArrowDrawable mLeftDrawable;
private AddDeleteDrawable mRightDrawable;
private SearchBarMoveHelper mSearchBarMoveHelper;
private ListUrlBuilder mUrlBuilder;
private int mState = STATE_NORMAL;
@Override
public int getLaunchMode() {
return LAUNCH_MODE_SINGLE_TOP;
}
@Override
public void onNewArguments(@NonNull Bundle args) {
// TODO
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mClient = EhApplication.getEhClient(getContext());
if (savedInstanceState == null) {
onInit();
}
}
public void onInit() {
mUrlBuilder = new ListUrlBuilder();
Bundle args = getArguments();
if (args != null) {
String action = args.getString(KEY_ACTION);
if (ACTION_HOMEPAGE.equals(action)) {
setNavCheckedItem(R.id.nav_homepage);
} else if (ACTION_WHATS_HOT.equals(action)) {
setNavCheckedItem(R.id.nav_whats_hot);
mUrlBuilder.setMode(ListUrlBuilder.MODE_WHATS_HOT);
}
}
}
private void setSearchBarHint(Context context, SearchBar searchBar) {
Resources resources = context.getResources();
Drawable searchImage = VectorDrawable.create(context, R.drawable.ic_magnify);
SpannableStringBuilder ssb = new SpannableStringBuilder(" ");
ssb.append(resources.getString(R.string.gallery_list_search_bar_hint));
int textSize = (int) (searchBar.getEditTextTextSize() * 1.25);
if (searchImage != null) {
searchImage.setBounds(0, 0, textSize, textSize);
ssb.setSpan(new ImageSpan(searchImage), 1, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
searchBar.setEditTextHint(ssb);
}
private void updateSearchBar() {
ListUrlBuilder builder = mUrlBuilder;
SearchBar sb = mSearchBar;
Resources resources = getResources();
switch (builder.getMode()) {
case ListUrlBuilder.MODE_NORMAL:
if (EhUtils.NONE ==builder.getCategory()) {
sb.setTitle(resources.getString(R.string.app_name));
}
break;
// TODO
}
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.scene_gallery_list, container, false);
Resources resources = getContext().getResources();
mSearchBarLayout = (SearchBarLayout) view.findViewById(R.id.search_bar_layout);
mContentLayout = (ContentLayout) mSearchBarLayout.findViewById(R.id.content_layout);
mRecyclerView = mContentLayout.getRecyclerView();
mSearchLayout = (SearchLayout) mSearchBarLayout.findViewById(R.id.search_layout);
mSearchBar = (SearchBar) mSearchBarLayout.findViewById(R.id.search_bar);
mViewTransition = new ViewTransition(mContentLayout, mSearchLayout);
mHelper = new GalleryListHelper();
mHelper.setEmptyString(resources.getString(R.string.gallery_list_empty_hit));
mContentLayout.setHelper(mHelper);
mContentLayout.getFastScroller().setOnDragHandlerListener(this);
mAdapter = new GalleryListAdapter();
mRecyclerView.setAdapter(mAdapter);
mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
mRecyclerView.setSelector(RippleSalon.generateRippleDrawable(false));
mRecyclerView.setDrawSelectorOnTop(true);
mRecyclerView.hasFixedSize();
mRecyclerView.setClipToPadding(false);
mRecyclerView.setOnItemClickListener(this);
mRecyclerView.addItemDecoration(new MarginItemDecoration(
getContext().getResources().getDimensionPixelOffset(R.dimen.list_item_margin)));
int paddingH = resources.getDimensionPixelOffset(R.dimen.list_content_margin_h);
int paddingV = resources.getDimensionPixelOffset(R.dimen.list_content_margin_v);
mRecyclerView.setPadding(paddingV, paddingH, paddingV, paddingH);
mLeftDrawable = new DrawerArrowDrawable(getContext());
mRightDrawable = new AddDeleteDrawable(getContext());
mSearchBar.setLeftDrawable(mLeftDrawable);
mSearchBar.setRightDrawable(mRightDrawable);
mSearchBar.setHelper(this);
mSearchBar.setOnStateChangeListener(this);
setSearchBarHint(getContext(), mSearchBar);
updateSearchBar();
mSearchLayout.setHelper(this);
mSearchBarMoveHelper = new SearchBarMoveHelper();
mRecyclerView.addOnScrollListener(mSearchBarMoveHelper);
mSearchLayout.addOnScrollListener(mSearchBarMoveHelper);
// Refresh
mHelper.firstRefresh();
return view;
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(KEY_LIST_URL_BUILDER, mUrlBuilder);
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
mUrlBuilder = savedInstanceState.getParcelable(KEY_LIST_URL_BUILDER);
}
@Override
public boolean onItemClick(EasyRecyclerView parent, View view, int position, long id) {
return false;
}
private void setState(int state) {
setState(state, true);
}
private void setState(int state, boolean animation) {
if (mState != state) {
int oldState = mState;
mState = state;
switch (oldState) {
case STATE_NORMAL:
switch (state) {
case STATE_SIMPLE_SEARCH:
mSearchBar.setState(SearchBar.STATE_SEARCH_LIST, animation);
mSearchBarMoveHelper.returnSearchBarPosition();
break;
case STATE_SEARCH:
mViewTransition.showView(1, animation);
mSearchLayout.scrollSearchContainerToTop();
mSearchBar.setState(SearchBar.STATE_SEARCH, animation);
mSearchBarMoveHelper.returnSearchBarPosition();
break;
case STATE_SEARCH_SHOW_LIST:
mViewTransition.showView(1, animation);
mSearchLayout.scrollSearchContainerToTop();
mSearchBar.setState(SearchBar.STATE_SEARCH_LIST, animation);
mSearchBarMoveHelper.returnSearchBarPosition();
break;
}
break;
case STATE_SIMPLE_SEARCH:
switch (state) {
case STATE_NORMAL:
mSearchBar.setState(SearchBar.STATE_NORMAL, animation);
mSearchBarMoveHelper.returnSearchBarPosition();
break;
case STATE_SEARCH:
mViewTransition.showView(1, animation);
mSearchLayout.scrollSearchContainerToTop();
mSearchBar.setState(SearchBar.STATE_SEARCH, animation);
mSearchBarMoveHelper.returnSearchBarPosition();
break;
case STATE_SEARCH_SHOW_LIST:
mViewTransition.showView(1, animation);
mSearchLayout.scrollSearchContainerToTop();
mSearchBar.setState(SearchBar.STATE_SEARCH_LIST, animation);
mSearchBarMoveHelper.returnSearchBarPosition();
break;
}
break;
case STATE_SEARCH:
switch (state) {
case STATE_NORMAL:
mViewTransition.showView(0, animation);
mSearchBar.setState(SearchBar.STATE_NORMAL, animation);
mSearchBarMoveHelper.returnSearchBarPosition();
break;
case STATE_SIMPLE_SEARCH:
mViewTransition.showView(0, animation);
mSearchBar.setState(SearchBar.STATE_SEARCH_LIST, animation);
mSearchBarMoveHelper.returnSearchBarPosition();
break;
case STATE_SEARCH_SHOW_LIST:
mSearchBar.setState(SearchBar.STATE_SEARCH_LIST, animation);
mSearchBarMoveHelper.returnSearchBarPosition();
break;
}
break;
case STATE_SEARCH_SHOW_LIST:
switch (state) {
case STATE_NORMAL:
mViewTransition.showView(0, animation);
mSearchBar.setState(SearchBar.STATE_NORMAL, animation);
mSearchBarMoveHelper.returnSearchBarPosition();
break;
case STATE_SIMPLE_SEARCH:
mViewTransition.showView(0, animation);
mSearchBar.setState(SearchBar.STATE_SEARCH_LIST, animation);
mSearchBarMoveHelper.returnSearchBarPosition();
break;
case STATE_SEARCH:
mSearchBar.setState(SearchBar.STATE_SEARCH, animation);
mSearchBarMoveHelper.returnSearchBarPosition();
break;
}
break;
}
}
}
@Override
public void onClickTitle() {
if (mState == STATE_NORMAL) {
setState(STATE_SIMPLE_SEARCH);
}
}
@Override
public void onClickLeftIcon() {
if (mSearchBar.getState() == SearchBar.STATE_NORMAL) {
toggleDrawer(Gravity.LEFT);
} else {
setState(STATE_NORMAL);
}
}
@Override
public void onClickRightIcon() {
if (mSearchBar.getState() == SearchBar.STATE_NORMAL) {
setState(STATE_SEARCH);
} else {
// Clear
mSearchBar.setText("");
}
}
@Override
public void onSearchEditTextClick() {
if (mState == STATE_SEARCH) {
setState(STATE_SEARCH_SHOW_LIST);
}
}
@Override
public void onApplySearch(String query) {
}
@Override
public void onSearchEditTextBackPressed() {
setState(STATE_NORMAL);
}
@Override
public void onStartDragHandler() {}
@Override
public void onEndDragHandler() {
mSearchBarMoveHelper.returnSearchBarPosition();
}
@Override
public void onStateChange(SearchBar searchBar, int newState, int oldState, boolean animation) {
if (newState == SearchBar.STATE_NORMAL) {
mSearchBarLayout.setEnableUpdatePaddingTop(true);
} else {
mSearchBarLayout.setEnableUpdatePaddingTop(false);
}
switch (oldState) {
default:
case SearchBar.STATE_NORMAL:
mLeftDrawable.setArrow(animation ? ANIMATE_TIME : 0);
mRightDrawable.setDelete(animation ? ANIMATE_TIME : 0);
break;
case SearchBar.STATE_SEARCH:
if (newState == SearchBar.STATE_NORMAL) {
mLeftDrawable.setMenu(animation ? ANIMATE_TIME : 0);
mRightDrawable.setAdd(animation ? ANIMATE_TIME : 0);
}
break;
case SearchBar.STATE_SEARCH_LIST:
if (newState == STATE_NORMAL) {
mLeftDrawable.setMenu(animation ? ANIMATE_TIME : 0);
mRightDrawable.setAdd(animation ? ANIMATE_TIME : 0);
}
break;
}
}
@Override
public void onChangeSearchMode() {
mSearchBarMoveHelper.showSearchBar();
}
private class SearchBarMoveHelper extends RecyclerView.OnScrollListener {
private ValueAnimator mSearchBarMoveAnimator;
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState){
if (newState == RecyclerView.SCROLL_STATE_IDLE && isVaildView(recyclerView)) {
returnSearchBarPosition();
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (isVaildView(recyclerView)) {
int oldBottom = mSearchBar.getBottom();
OffsetLayout.LayoutParams sbLayoutParams = (OffsetLayout.LayoutParams) mSearchBar.getLayoutParams();
int offsetYStep = MathUtils.clamp(-dy, -mSearchBar.getBottom(), -sbLayoutParams.offsetY);
mSearchBar.offsetTopAndBottom(offsetYStep);
sbLayoutParams.offsetY += offsetYStep;
int newBottom = mSearchBar.getBottom();
// Sometimes if it is out of screen than go into again, it do not show,
// so I need to requestLayout
if (oldBottom <= 0 && newBottom > 0) {
mSearchBar.requestLayout();
}
}
}
private boolean isVaildView(RecyclerView view) {
return (mState == STATE_NORMAL && view == mRecyclerView) ||
(mState == STATE_SEARCH && view == mSearchLayout);
}
private RecyclerView getVaildRecyclerView() {
if (mState == STATE_NORMAL || mState == STATE_SIMPLE_SEARCH) {
return mRecyclerView;
} else {
return mSearchLayout;
}
}
private int getSearchBarOriginalBottom() {
return mSearchBar.getBottom() - ((OffsetLayout.LayoutParams) mSearchBar.getLayoutParams()).offsetY;
}
private void returnSearchBarPosition() {
boolean show;
if (mState == STATE_SIMPLE_SEARCH || mState == STATE_SEARCH_SHOW_LIST) {
show = true;
} else {
RecyclerView recyclerView = getVaildRecyclerView();
if (!recyclerView.isShown()) {
show = true;
} else if (recyclerView.computeVerticalScrollOffset() < getSearchBarOriginalBottom()){
show = true;
} else {
show = mSearchBar.getBottom() > (mSearchBar.getHeight()) / 2;
}
}
int offset;
if (show) {
offset = -((OffsetLayout.LayoutParams) mSearchBar.getLayoutParams()).offsetY;
} else {
offset = -mSearchBar.getBottom();
}
if (offset != 0) {
final boolean needRequestLayout = mSearchBar.getBottom() <= 0 && offset > 0;
final ValueAnimator va = ValueAnimator.ofInt(0, offset);
va.setDuration(ANIMATE_TIME);
va.addListener(new SimpleAnimatorListener() {
@Override
public void onAnimationEnd(Animator animation) {
mSearchBarMoveAnimator = null;
}
});
va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
int lastValue;
boolean hasRequestLayout;
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value = (Integer) animation.getAnimatedValue();
int offsetStep = value - lastValue;
lastValue = value;
mSearchBar.offsetTopAndBottom(offsetStep);
((OffsetLayout.LayoutParams) mSearchBar.getLayoutParams()).offsetY += offsetStep;
if (!hasRequestLayout && needRequestLayout && mSearchBar.getBottom() > 0) {
hasRequestLayout = true;
mSearchBar.requestLayout();
}
}
});
if (mSearchBarMoveAnimator != null) {
mSearchBarMoveAnimator.cancel();
}
mSearchBarMoveAnimator = va;
va.start();
}
}
private void showSearchBar() {
showSearchBar(true);
}
private void showSearchBar(boolean animation) {
if (mSearchBar.isLayoutRequested()) {
return;
}
// Cancel old animator
if (mSearchBarMoveAnimator != null) {
mSearchBarMoveAnimator.cancel();
}
int offset = -((OffsetLayout.LayoutParams) mSearchBar.getLayoutParams()).offsetY;
if (offset != 0) {
if (animation) {
final boolean needRequestLayout = mSearchBar.getBottom() <= 0 && offset > 0;
final ValueAnimator va = ValueAnimator.ofInt(0, offset);
va.setDuration(ANIMATE_TIME);
va.addListener(new SimpleAnimatorListener() {
@Override
public void onAnimationEnd(Animator animation) {
mSearchBarMoveAnimator = null;
}
});
va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
int lastValue;
boolean hasRequestLayout;
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value = (Integer) animation.getAnimatedValue();
int offsetStep = value - lastValue;
lastValue = value;
mSearchBar.offsetTopAndBottom(offsetStep);
((OffsetLayout.LayoutParams) mSearchBar.getLayoutParams()).offsetY += offsetStep;
if (!hasRequestLayout && needRequestLayout && mSearchBar.getBottom() > 0) {
hasRequestLayout = true;
mSearchBar.requestLayout();
}
}
});
if (mSearchBarMoveAnimator != null) {
mSearchBarMoveAnimator.cancel();
}
mSearchBarMoveAnimator = va;
va.start();
} else {
mSearchBar.offsetTopAndBottom(offset);
((OffsetLayout.LayoutParams) mSearchBar.getLayoutParams()).offsetY += offset;
}
}
}
}
private class GalleryListHolder extends RecyclerView.ViewHolder {
private LoadImageView thumb;
private TextView title;
private TextView uploader;
private SimpleRatingView rating;
private TextView category;
private TextView posted;
private TextView simpleLanguage;
public GalleryListHolder(View itemView) {
super(itemView);
thumb = (LoadImageView) itemView.findViewById(R.id.thumb);
title = (TextView) itemView.findViewById(R.id.title);
uploader = (TextView) itemView.findViewById(R.id.uploader);
rating = (SimpleRatingView) itemView.findViewById(R.id.rating);
category = (TextView) itemView.findViewById(R.id.category);
posted = (TextView) itemView.findViewById(R.id.posted);
simpleLanguage = (TextView) itemView.findViewById(R.id.simple_language);
}
}
private class GalleryListAdapter extends RecyclerView.Adapter<GalleryListHolder> {
private LayoutInflater mInflater;
public GalleryListAdapter() {
mInflater = getActivity().getLayoutInflater();
}
@Override
public GalleryListHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new GalleryListHolder(mInflater.inflate(R.layout.item_gallery_list, parent, false));
}
@Override
public void onBindViewHolder(GalleryListHolder holder, int position) {
GalleryInfo gi = mHelper.getDataAt(position);
holder.thumb.load(EhCacheKeyFactory.getThumbKey(gi.gid), gi.thumb, true);
holder.title.setText(gi.title);
holder.uploader.setText(gi.uploader);
holder.rating.setRating(gi.rating);
TextView category = holder.category;
String newCategoryText = EhUtils.getCategory(gi.category);
if (!newCategoryText.equals(category.getText())) {
category.setText(newCategoryText);
category.setBackgroundColor(EhUtils.getCategoryColor(gi.category));
}
holder.posted.setText(gi.posted);
holder.simpleLanguage.setText(gi.simpleLanguage);
}
@Override
public int getItemCount() {
return mHelper.size();
}
}
private class GalleryListHelper extends ContentLayout.ContentHelper<GalleryInfo> {
@Override
protected void getPageData(int taskId, int type, int page) {
mUrlBuilder.setPageIndex(page);
String url = mUrlBuilder.build();
EhRequest request = new EhRequest();
request.setMethod(EhClient.METHOD_GET_GALLERY_LIST);
request.setCallback(new GetGalleryListListener(taskId));
request.setArgs(url);
mClient.execute(request);
}
@Override
protected Context getContext() {
return GalleryListScene.this.getContext();
}
@Override
protected void notifyDataSetChanged() {
mAdapter.notifyDataSetChanged();
}
@Override
protected void notifyItemRangeRemoved(int positionStart, int itemCount) {
mAdapter.notifyItemRangeRemoved(positionStart, itemCount);
}
@Override
protected void notifyItemRangeInserted(int positionStart, int itemCount) {
mAdapter.notifyItemRangeInserted(positionStart, itemCount);
}
}
private class GetGalleryListListener implements EhClient.Callback<GalleryListParser.Result> {
private int mTaskId;
public GetGalleryListListener(int taskId) {
mTaskId = taskId;
}
@Override
public void onSuccess(GalleryListParser.Result result) {
mHelper.setPages(mTaskId, result.pages);
mHelper.onGetPageData(mTaskId, result.galleryInfos);
}
@Override
public void onFailure(Exception e) {
mHelper.onGetExpection(mTaskId, e);
}
@Override
public void onCancel() {
}
}
}
@@ -39,7 +39,9 @@ import com.hippo.ehviewer.Settings;
import com.hippo.ehviewer.client.EhClient;
import com.hippo.ehviewer.client.EhCookieStore;
import com.hippo.ehviewer.client.EhRequest;
import com.hippo.rippleold.RippleSalon;
import com.hippo.utils.ExceptionUtils;
import com.hippo.widget.CardButton;
public final class LoginScene extends BaseScene implements EditText.OnEditorActionListener,
View.OnClickListener {
@@ -49,10 +51,11 @@ public final class LoginScene extends BaseScene implements EditText.OnEditorActi
private TextInputLayout mPasswordLayout;
private EditText mUsername;
private EditText mPassword;
private View mRegister;
private View mSignIn;
private CardButton mRegister;
private CardButton mSignIn;
private TextView mSignInViaWebview;
private TextView mSignInViaCookies;
private TextView mSkipSigningIn;
private EhClient mClient;
private EhCookieStore mCookieStore;
@@ -78,13 +81,15 @@ public final class LoginScene extends BaseScene implements EditText.OnEditorActi
mUsername = mUsernameLayout.getEditText();
mPasswordLayout = (TextInputLayout) loginForm.findViewById(R.id.password_layout);
mPassword = mPasswordLayout.getEditText();
mRegister = loginForm.findViewById(R.id.register);
mSignIn = loginForm.findViewById(R.id.sign_in);
mRegister = (CardButton) loginForm.findViewById(R.id.register);
mSignIn = (CardButton) loginForm.findViewById(R.id.sign_in);
mSignInViaWebview = (TextView) loginForm.findViewById(R.id.sign_in_via_webview);
mSignInViaCookies = (TextView) loginForm.findViewById(R.id.sign_in_via_cookies);
mSkipSigningIn = (TextView) loginForm.findViewById(R.id.skip_signing_in);
mSignInViaWebview.setPaintFlags(mSignInViaWebview.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
mSignInViaCookies.setPaintFlags(mSignInViaCookies.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
mSkipSigningIn.setPaintFlags(mSignInViaCookies.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
mPassword.setOnEditorActionListener(this);
@@ -92,6 +97,12 @@ public final class LoginScene extends BaseScene implements EditText.OnEditorActi
mSignIn.setOnClickListener(this);
mSignInViaWebview.setOnClickListener(this);
mSignInViaCookies.setOnClickListener(this);
mSkipSigningIn.setOnClickListener(this);
mRegister.setRawBackgroundDrawable(
RippleSalon.generateRippleDrawable(true, mRegister.getBackground()));
mSignIn.setRawBackgroundDrawable(
RippleSalon.generateRippleDrawable(true, mSignIn.getBackground()));
return view;
}
@@ -116,6 +127,8 @@ public final class LoginScene extends BaseScene implements EditText.OnEditorActi
} else if (mSignInViaCookies == v) {
} else if (mSkipSigningIn == v) {
redirectTo();
}
}
@@ -187,6 +200,7 @@ public final class LoginScene extends BaseScene implements EditText.OnEditorActi
@Override
public void onFailure(Exception e) {
e.printStackTrace();
mSignInRequest = null;
mProgress.setVisibility(View.GONE);
Toast.makeText(getContext(), ExceptionUtils.getReadableString(getContext(), e),
@@ -204,7 +218,9 @@ public final class LoginScene extends BaseScene implements EditText.OnEditorActi
}
private void redirectTo() {
startScene(GalleryListScene.class);
Bundle args = new Bundle();
args.putString(GalleryListScene.KEY_ACTION, GalleryListScene.ACTION_HOMEPAGE);
startScene(GalleryListScene.class, args);
finish();
// Enable drawer
setDrawerLayoutEnable(true);
@@ -26,13 +26,15 @@ import android.view.ViewGroup;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.Settings;
import com.hippo.ehviewer.client.EhUtils;
import com.hippo.rippleold.RippleSalon;
import com.hippo.vector.VectorDrawable;
import com.hippo.widget.CardButton;
import com.hippo.widget.SimpleImageView;
public final class WarningScene extends BaseScene implements View.OnClickListener {
private View mCancel;
private View mOk;
private CardButton mCancel;
private CardButton mOk;
@Nullable
@Override
@@ -41,8 +43,8 @@ public final class WarningScene extends BaseScene implements View.OnClickListene
View view = inflater.inflate(R.layout.scene_warning, container, false);
SimpleImageView alert = (SimpleImageView) view.findViewById(R.id.icon_alert);
mCancel = view.findViewById(R.id.cancel);
mOk = view.findViewById(R.id.ok);
mCancel = (CardButton) view.findViewById(R.id.cancel);
mOk = (CardButton) view.findViewById(R.id.ok);
Drawable drawable = VectorDrawable.create(getContext(), R.drawable.ic_alert);
alert.setDrawable(drawable);
@@ -50,6 +52,11 @@ public final class WarningScene extends BaseScene implements View.OnClickListene
mCancel.setOnClickListener(this);
mOk.setOnClickListener(this);
mCancel.setRawBackgroundDrawable(
RippleSalon.generateRippleDrawable(true, mCancel.getBackground()));
mOk.setRawBackgroundDrawable(
RippleSalon.generateRippleDrawable(true, mOk.getBackground()));
return view;
}
@@ -62,7 +69,9 @@ public final class WarningScene extends BaseScene implements View.OnClickListene
Settings.putShowWarning(false);
if (EhUtils.hasSignedIn(getContext())) {
startScene(GalleryListScene.class);
Bundle args = new Bundle();
args.putString(GalleryListScene.KEY_ACTION, GalleryListScene.ACTION_HOMEPAGE);
startScene(GalleryListScene.class, args);
finish();
// Enable drawer
setDrawerLayoutEnable(true);
@@ -0,0 +1,153 @@
/*
* Copyright (C) 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.widget;
import android.content.Context;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.Spinner;
import android.widget.TableLayout;
import com.hippo.ehviewer.R;
import com.hippo.yorozuya.NumberUtils;
public class AdvanceSearchTable extends TableLayout {
private static final String STATE_KEY_SUPER = "super";
private static final String STATE_KEY_ADVANCE_SEARCH = "advance_search";
private static final String STATE_KEY_MIN_RATING = "min_rating";
public static final int SNAME = 0x1;
public static final int STAGS = 0x2;
public static final int SDESC = 0x4;
public static final int STORR = 0x8;
public static final int STO = 0x10;
public static final int SDT1 = 0x20;
public static final int SDT2 = 0x40;
public static final int SH = 0x80;
private CheckBox mSname;
private CheckBox mStags;
private CheckBox mSdesc;
private CheckBox mStorr;
private CheckBox mSto;
private CheckBox mSdt1;
private CheckBox mSdt2;
private CheckBox mSh;
private CheckBox mSr;
private Spinner mMinRating;
public AdvanceSearchTable(Context context) {
super(context);
init(context);
}
public AdvanceSearchTable(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public void init(Context context) {
LayoutInflater inflater = LayoutInflater.from(getContext());
inflater.inflate(R.layout.widget_advance_search_table, this);
ViewGroup row0 = (ViewGroup) getChildAt(0);
mSname = (CheckBox) row0.getChildAt(0);
mStags = (CheckBox) row0.getChildAt(1);
ViewGroup row1 = (ViewGroup) getChildAt(1);
mSdesc = (CheckBox) row1.getChildAt(0);
mStorr = (CheckBox) row1.getChildAt(1);
ViewGroup row2 = (ViewGroup) getChildAt(2);
mSto = (CheckBox) row2.getChildAt(0);
mSdt1 = (CheckBox) row2.getChildAt(1);
ViewGroup row3 = (ViewGroup) getChildAt(3);
mSdt2 = (CheckBox) row3.getChildAt(0);
mSh = (CheckBox) row3.getChildAt(1);
ViewGroup row4 = (ViewGroup) getChildAt(4);
mSr = (CheckBox) row4.getChildAt(0);
mMinRating = (Spinner) row4.getChildAt(1);
}
public int getAdvanceSearch() {
int advanceSearch = 0;
if (mSname.isChecked()) advanceSearch |= SNAME;
if (mStags.isChecked()) advanceSearch |= STAGS;
if (mSdesc.isChecked()) advanceSearch |= SDESC;
if (mStorr.isChecked()) advanceSearch |= STORR;
if (mSto.isChecked()) advanceSearch |= STO;
if (mSdt1.isChecked()) advanceSearch |= SDT1;
if (mSdt2.isChecked()) advanceSearch |= SDT2;
if (mSh.isChecked()) advanceSearch |= SH;
return advanceSearch;
}
public int getMinRating() {
int position = mMinRating.getSelectedItemPosition();
if (mSr.isChecked() && position >= 0) {
return position + 2;
} else {
return -1;
}
}
public void setAdvanceSearch(int advanceSearch) {
mSname.setChecked(NumberUtils.int2boolean(advanceSearch & SNAME));
mStags.setChecked(NumberUtils.int2boolean(advanceSearch & STAGS));
mSdesc.setChecked(NumberUtils.int2boolean(advanceSearch & SDESC));
mStorr.setChecked(NumberUtils.int2boolean(advanceSearch & STORR));
mSto.setChecked(NumberUtils.int2boolean(advanceSearch & STO));
mSdt1.setChecked(NumberUtils.int2boolean(advanceSearch & SDT1));
mSdt2.setChecked(NumberUtils.int2boolean(advanceSearch & SDT2));
mSh.setChecked(NumberUtils.int2boolean(advanceSearch & SH));
}
public void setMinRating(int minRating) {
if (minRating >= 2 && minRating <= 5) {
mSr.setChecked(true);
mMinRating.setSelection(minRating - 2);
} else {
mSr.setChecked(false);
}
}
@Override
public Parcelable onSaveInstanceState() {
final Bundle state = new Bundle();
state.putParcelable(STATE_KEY_SUPER, super.onSaveInstanceState());
state.putInt(STATE_KEY_ADVANCE_SEARCH, getAdvanceSearch());
state.putInt(STATE_KEY_MIN_RATING, getMinRating());
return state;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
final Bundle savedState = (Bundle) state;
super.onRestoreInstanceState(savedState.getParcelable(STATE_KEY_SUPER));
setAdvanceSearch(savedState.getInt(STATE_KEY_ADVANCE_SEARCH));
setMinRating(savedState.getInt(STATE_KEY_MIN_RATING));
}
}
}
@@ -0,0 +1,136 @@
/*
* Copyright (C) 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.widget;
import android.content.Context;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.widget.TableLayout;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.client.EhConfig;
import com.hippo.widget.CheckTextView;
import com.hippo.yorozuya.NumberUtils;
public class CategoryTable extends TableLayout {
private static final String STATE_KEY_SUPER = "super";
private static final String STATE_KEY_CATEGORY = "category";
private CheckTextView mDoujinshi;
private CheckTextView mManga;
private CheckTextView mArtistCG;
private CheckTextView mGameCG;
private CheckTextView mWestern;
private CheckTextView mNonH;
private CheckTextView mImageSets;
private CheckTextView mCosplay;
private CheckTextView mAsianPorn;
private CheckTextView mMisc;
public CategoryTable(Context context) {
super(context);
init();
}
public CategoryTable(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public void init() {
LayoutInflater inflater = LayoutInflater.from(getContext());
inflater.inflate(R.layout.widget_category_table, this);
ViewGroup row0 = (ViewGroup) getChildAt(0);
mDoujinshi = (CheckTextView) row0.getChildAt(0);
mManga = (CheckTextView) row0.getChildAt(1);
ViewGroup row1 = (ViewGroup) getChildAt(1);
mArtistCG = (CheckTextView) row1.getChildAt(0);
mGameCG = (CheckTextView) row1.getChildAt(1);
ViewGroup row2 = (ViewGroup) getChildAt(2);
mWestern = (CheckTextView) row2.getChildAt(0);
mNonH = (CheckTextView) row2.getChildAt(1);
ViewGroup row3 = (ViewGroup) getChildAt(3);
mImageSets = (CheckTextView) row3.getChildAt(0);
mCosplay = (CheckTextView) row3.getChildAt(1);
ViewGroup row4 = (ViewGroup) getChildAt(4);
mAsianPorn = (CheckTextView) row4.getChildAt(0);
mMisc = (CheckTextView) row4.getChildAt(1);
}
/**
* Set each button checked or not according to category.
*
* @param category target category
*/
public void setCategory(int category) {
mDoujinshi.setChecked(!NumberUtils.int2boolean(category & EhConfig.DOUJINSHI), false);
mManga.setChecked(!NumberUtils.int2boolean(category & EhConfig.MANGA), false);
mArtistCG.setChecked(!NumberUtils.int2boolean(category & EhConfig.ARTIST_CG), false);
mGameCG.setChecked(!NumberUtils.int2boolean(category & EhConfig.GAME_CG), false);
mWestern.setChecked(!NumberUtils.int2boolean(category & EhConfig.WESTERN), false);
mNonH.setChecked(!NumberUtils.int2boolean(category & EhConfig.NON_H), false);
mImageSets.setChecked(!NumberUtils.int2boolean(category & EhConfig.IMAGE_SET), false);
mCosplay.setChecked(!NumberUtils.int2boolean(category & EhConfig.COSPLAY), false);
mAsianPorn.setChecked(!NumberUtils.int2boolean(category & EhConfig.ASIAN_PORN), false);
mMisc.setChecked(!NumberUtils.int2boolean(category & EhConfig.MISC), false);
}
/**
* Get category according to button.
* @return the category of this view
*/
public int getCategory() {
int category = 0;
if (!mDoujinshi.isChecked()) category |= EhConfig.DOUJINSHI;
if (!mManga.isChecked()) category |= EhConfig.MANGA;
if (!mArtistCG.isChecked()) category |= EhConfig.ARTIST_CG;
if (!mGameCG.isChecked()) category |= EhConfig.GAME_CG;
if (!mWestern.isChecked()) category |= EhConfig.WESTERN;
if (!mNonH.isChecked()) category |= EhConfig.NON_H;
if (!mImageSets.isChecked()) category |= EhConfig.IMAGE_SET;
if (!mCosplay.isChecked()) category |= EhConfig.COSPLAY;
if (!mAsianPorn.isChecked()) category |= EhConfig.ASIAN_PORN;
if (!mMisc.isChecked()) category |= EhConfig.MISC;
return category;
}
@Override
public Parcelable onSaveInstanceState() {
final Bundle state = new Bundle();
state.putParcelable(STATE_KEY_SUPER, super.onSaveInstanceState());
state.putInt(STATE_KEY_CATEGORY, getCategory());
return state;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
final Bundle savedState = (Bundle) state;
super.onRestoreInstanceState(savedState.getParcelable(STATE_KEY_SUPER));
setCategory(savedState.getInt(STATE_KEY_CATEGORY));
}
}
}
@@ -0,0 +1,442 @@
/*
* Copyright 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.widget;
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Path;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import com.hippo.anani.SimpleAnimatorListener;
import com.hippo.cardsalon.CardView;
import com.hippo.ehviewer.R;
import com.hippo.view.ViewTransition;
import com.hippo.widget.SimpleImageView;
import com.hippo.yorozuya.MathUtils;
import com.hippo.yorozuya.ViewUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class SearchBar extends CardView implements View.OnClickListener,
TextView.OnEditorActionListener, TextWatcher,
SearchEditText.SearchEditTextListener {
private static final String STATE_KEY_SUPER = "super";
private static final String STATE_KEY_STATE = "state";
private static final long ANIMATE_TIME = 300L;
public static final int STATE_NORMAL = 0;
public static final int STATE_SEARCH = 1;
public static final int STATE_SEARCH_LIST = 2;
private int mState = STATE_NORMAL;
private Path mPath = new Path();
private int mWidth;
private int mHeight;
private int mBaseHeight;
private float mProgress;
private SimpleImageView mMenuButton;
private TextView mTitleTextView;
private SimpleImageView mActionButton;
private SearchEditText mEditText;
private ListView mList;
private ViewTransition mViewTransition;
private SearchDatabase mSearchDatabase;
private List<String> mSuggestionList;
private ArrayAdapter mSuggestionAdapter;
private Helper mHelper;
private OnStateChangeListener mOnStateChangeListener;
private boolean mInAnimation = false;
public SearchBar(Context context) {
super(context);
init(context);
}
public SearchBar(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public SearchBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
mSearchDatabase = SearchDatabase.getInstance(getContext());
LayoutInflater.from(context).inflate(R.layout.widget_search_bar, this);
mMenuButton = (SimpleImageView) findViewById(R.id.search_menu);
mTitleTextView = (TextView) findViewById(R.id.search_title);
mActionButton = (SimpleImageView) findViewById(R.id.search_action);
mEditText = (SearchEditText) findViewById(R.id.search_edit_text);
mList = (ListView) findViewById(R.id.search_bar_list);
mViewTransition = new ViewTransition(mTitleTextView, mEditText);
mTitleTextView.setOnClickListener(this);
mMenuButton.setOnClickListener(this);
mActionButton.setOnClickListener(this);
mEditText.setSearchEditTextListener(this);
mEditText.setOnEditorActionListener(this);
mEditText.addTextChangedListener(this);
// Get base height
ViewUtils.measureView(this, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
mBaseHeight = getMeasuredHeight();
mSuggestionList = new ArrayList<>();
// TODO Use custom view
mSuggestionAdapter = new ArrayAdapter<>(getContext(),
android.R.layout.simple_list_item_1, mSuggestionList);
mList.setAdapter(mSuggestionAdapter);
mList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
String suggestion = mSuggestionList.get(position);
mEditText.setText(suggestion);
mEditText.setSelection(suggestion.length());
}
});
}
private void updateSuggestions() {
String prefix = mEditText.getText().toString();
String[] suggestions = mSearchDatabase.getSuggestions(prefix);
mSuggestionList.clear();
Collections.addAll(mSuggestionList, suggestions);
mSuggestionAdapter.notifyDataSetChanged();
}
public float getEditTextTextSize() {
return mEditText.getTextSize();
}
public void setEditTextHint(CharSequence hint) {
mEditText.setHint(hint);
}
public void setHelper(Helper helper) {
mHelper = helper;
}
public void setOnStateChangeListener(OnStateChangeListener listener) {
mOnStateChangeListener = listener;
}
public void setText(String text) {
mEditText.setText(text);
}
public String getText() {
return mEditText.getText().toString();
}
public void setTitle(String title) {
mTitleTextView.setText(title);
}
public void setSearch(String search) {
mTitleTextView.setText(search);
mEditText.setText(search);
}
public void setLeftDrawable(Drawable drawable) {
mMenuButton.setDrawable(drawable);
}
public void setRightDrawable(Drawable drawable) {
mActionButton.setDrawable(drawable);
}
public void setLeftIconVisibility(int visibility) {
mMenuButton.setVisibility(visibility);
}
public void setRightIconVisibility(int visibility) {
mActionButton.setVisibility(visibility);
}
public void setEditTextMargin(int left, int right) {
MarginLayoutParams lp = (MarginLayoutParams) mEditText.getLayoutParams();
lp.leftMargin = left;
lp.rightMargin = right;
mEditText.setLayoutParams(lp);
}
@Override
public void onClick(View v) {
if (v == mTitleTextView) {
mHelper.onClickTitle();
} else if (v == mMenuButton) {
mHelper.onClickLeftIcon();
} else if (v == mActionButton) {
mHelper.onClickRightIcon();
}
}
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (v == mEditText) {
if (actionId == EditorInfo.IME_ACTION_SEARCH || actionId == EditorInfo.IME_NULL) {
String query = mEditText.getText().toString();
mHelper.onApplySearch(query);
return true;
}
}
return false;
}
public int getState() {
return mState;
}
public void setState(int state) {
setState(state, true);
}
public void setState(int state, boolean animation) {
if (mState != state) {
int oldState = mState;
mState = state;
switch (oldState) {
default:
case STATE_NORMAL:
mViewTransition.showView(1, animation);
mEditText.requestFocus();
if (state == STATE_SEARCH_LIST) {
showImeAndSuggestionsList(animation);
}
if (mOnStateChangeListener != null) {
mOnStateChangeListener.onStateChange(this, state, oldState, animation);
}
break;
case STATE_SEARCH:
if (state == STATE_NORMAL) {
mViewTransition.showView(0, animation);
} else if (state == STATE_SEARCH_LIST) {
showImeAndSuggestionsList(animation);
}
if (mOnStateChangeListener != null) {
mOnStateChangeListener.onStateChange(this, state, oldState, animation);
}
break;
case STATE_SEARCH_LIST:
hideImeAndSuggestionsList(animation);
if (state == STATE_NORMAL) {
mViewTransition.showView(0, animation);
}
if (mOnStateChangeListener != null) {
mOnStateChangeListener.onStateChange(this, state, oldState, animation);
}
break;
}
}
}
public void showImeAndSuggestionsList() {
showImeAndSuggestionsList(true);
}
public void showImeAndSuggestionsList(boolean animation) {
// Show ime
InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(mEditText, 0);
// update suggestion for show suggestions list
updateSuggestions();
// Show suggestions list
if (animation) {
ObjectAnimator oa = ObjectAnimator.ofFloat(this, "progress", 1f);
oa.setDuration(ANIMATE_TIME);
oa.addListener(new SimpleAnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
mList.setVisibility(View.VISIBLE);
mInAnimation = true;
}
@Override
public void onAnimationEnd(Animator animation) {
mInAnimation = false;
}
});
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
oa.setAutoCancel(true);
}
oa.start();
} else {
mList.setVisibility(View.VISIBLE);
setProgress(1f);
}
}
private void hideImeAndSuggestionsList() {
hideImeAndSuggestionsList(true);
}
private void hideImeAndSuggestionsList(boolean animation) {
// Hide ime
InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(this.getWindowToken(), 0);
// Hide suggestions list
if (animation) {
ObjectAnimator oa = ObjectAnimator.ofFloat(this, "progress", 0f);
oa.setDuration(ANIMATE_TIME);
oa.addListener(new SimpleAnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
mInAnimation = true;
}
@Override
public void onAnimationEnd(Animator animation) {
mList.setVisibility(View.GONE);
mInAnimation = false;
}
});
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
oa.setAutoCancel(true);
}
oa.start();
} else {
setProgress(0f);
mList.setVisibility(View.GONE);
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (mList.getVisibility() == View.VISIBLE) {
mWidth = right - left;
mHeight = bottom - top;
}
}
@SuppressWarnings("unused")
public void setProgress(float progress) {
mProgress = progress;
invalidate();
}
@SuppressWarnings("unused")
public float getProgress() {
return mProgress;
}
@Override
public void draw(@NonNull Canvas canvas) {
if (mInAnimation) {
final int state = canvas.save();
float bottom = MathUtils.lerp(mBaseHeight, mHeight, mProgress);
mPath.rewind();
mPath.addRect(0f, 0f, mWidth, bottom, Path.Direction.CW);
canvas.clipPath(mPath);
super.draw(canvas);
canvas.restoreToCount(state);
} else {
super.draw(canvas);
}
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
// Empty
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
// Empty
}
@Override
public void afterTextChanged(Editable s) {
updateSuggestions();
}
@Override
public void onClick() {
mHelper.onSearchEditTextClick();
}
@Override
public void onBackPressed() {
mHelper.onSearchEditTextBackPressed();
}
@Override
public Parcelable onSaveInstanceState() {
final Bundle state = new Bundle();
state.putParcelable(STATE_KEY_SUPER, super.onSaveInstanceState());
state.putInt(STATE_KEY_STATE, mState);
return state;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
final Bundle savedState = (Bundle) state;
super.onRestoreInstanceState(savedState.getParcelable(STATE_KEY_SUPER));
setState(savedState.getInt(STATE_KEY_STATE), false);
}
}
public interface Helper {
void onClickTitle();
void onClickLeftIcon();
void onClickRightIcon();
void onSearchEditTextClick();
void onApplySearch(String query);
void onSearchEditTextBackPressed();
}
public interface OnStateChangeListener {
void onStateChange(SearchBar searchBar, int newState, int oldState, boolean animation);
}
}
@@ -0,0 +1,103 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import com.hippo.ehviewer.R;
import com.hippo.widget.OffsetLayout;
public class SearchBarLayout extends OffsetLayout {
private int mSearchBarId;
private int mContentId1;
private int mContentId2;
private int mContentId3;
private int mContentOriginalPaddingTop1 = -1;
private int mContentOriginalPaddingTop2 = -1;
private int mContentOriginalPaddingTop3 = -1;
private boolean mEnableUpdatePaddingTop = true;
public SearchBarLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public SearchBarLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SearchBarLayout);
mSearchBarId = a.getResourceId(R.styleable.SearchBarLayout_search_bar, 0);
mContentId1 = a.getResourceId(R.styleable.SearchBarLayout_content1, 0);
mContentId2 = a.getResourceId(R.styleable.SearchBarLayout_content2, 0);
mContentId3 = a.getResourceId(R.styleable.SearchBarLayout_content3, 0);
a.recycle();
}
public void setEnableUpdatePaddingTop(boolean enableUpdatePaddingTop) {
mEnableUpdatePaddingTop = enableUpdatePaddingTop;
}
private void updateContentPadding(@Nullable View content, int newPaddingTop) {
if (content != null && newPaddingTop != content.getPaddingTop()) {
content.setPadding(content.getPaddingLeft(), newPaddingTop,
content.getPaddingRight(), content.getPaddingBottom());
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (!mEnableUpdatePaddingTop) {
return;
}
if (mSearchBarId != 0 && (mContentId1 != 0 || mContentId2 != 0 || mContentId3 != 0)) {
View searchBar = findViewById(mSearchBarId);
View content1 = findViewById(mContentId1);
View content2 = findViewById(mContentId2);
View content3 = findViewById(mContentId3);
if (searchBar != null && (content1 != null || content2 != null || content3 != null)) {
// Get content original padding top
if (mContentOriginalPaddingTop1 == -1 && content1 != null) {
mContentOriginalPaddingTop1 = content1.getPaddingTop();
}
if (mContentOriginalPaddingTop2 == -1 && content2 != null) {
mContentOriginalPaddingTop2 = content2.getPaddingTop();
}
if (mContentOriginalPaddingTop3 == -1 && content3 != null) {
mContentOriginalPaddingTop3 = content3.getPaddingTop();
}
int paddingTopOffset = searchBar.getBottom() -
((LayoutParams) searchBar.getLayoutParams()).offsetY;
updateContentPadding(content1, paddingTopOffset + mContentOriginalPaddingTop1);
updateContentPadding(content2, paddingTopOffset + mContentOriginalPaddingTop2);
updateContentPadding(content3, paddingTopOffset + mContentOriginalPaddingTop3);
}
}
}
}
@@ -0,0 +1,160 @@
/*
* Copyright (C) 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.widget;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.text.TextUtils;
import com.hippo.yorozuya.Say;
import java.util.LinkedList;
import java.util.List;
public class SearchDatabase {
private static final String TAG = SearchDatabase.class.getSimpleName();
public static final String COLUMN_QUERY = "query";
public static final String COLUMN_DATE = "date";
private static final String DATABASE_NAME = "search_database.db";
private static final String TABLE_SUGGESTIONS = "suggestions";
private static final int MAX_HISTORY = 100;
private SQLiteDatabase mDatabase;
private static SearchDatabase sInstance;
public static SearchDatabase getInstance(Context context) {
if (sInstance == null) {
sInstance = new SearchDatabase(context.getApplicationContext());
}
return sInstance;
}
private SearchDatabase(Context context) {
DatabaseHelper databaseHelper = new DatabaseHelper(context);
mDatabase = databaseHelper.getWritableDatabase();
}
public String[] getSuggestions(String prefix) {
List<String> queryList = new LinkedList<>();
// TODO add limit
StringBuilder sb = new StringBuilder();
sb.append("SELECT * FROM ").append(TABLE_SUGGESTIONS);
if (!TextUtils.isEmpty(prefix)) {
sb.append(" WHERE ").append(COLUMN_QUERY).append(" LIKE '")
.append(prefix).append("%'");
}
sb.append(" ORDER BY ").append(COLUMN_DATE).append(" DESC")
.append(" LIMIT 5");
Cursor cursor = mDatabase.rawQuery(sb.toString(), null);
int queryIndex = cursor.getColumnIndex(COLUMN_QUERY);
if (cursor.moveToFirst()) {
while (!cursor.isAfterLast()) {
String suggestion = cursor.getString(queryIndex);
if (!prefix.equals(suggestion)) {
queryList.add(suggestion);
}
cursor.moveToNext();
}
}
cursor.close();
return queryList.toArray(new String[queryList.size()]);
}
public void addQuery(final String query) {
if (!TextUtils.isEmpty(query)) {
// Delete old first
deleteQuery(query);
// Add it to database
ContentValues values = new ContentValues();
values.put(COLUMN_QUERY, query);
values.put(COLUMN_DATE, System.currentTimeMillis());
mDatabase.insert(TABLE_SUGGESTIONS, null, values);
// Remove history if more than max
truncateHistory(MAX_HISTORY);
}
}
public void deleteQuery(final String query) {
mDatabase.delete(TABLE_SUGGESTIONS, COLUMN_QUERY + "=?", new String[]{query});
}
public void clearQuery() {
truncateHistory(0);
}
/**
* Reduces the length of the history table, to prevent it from growing too large.
*
* @param maxEntries Max entries to leave in the table. 0 means remove all entries.
*/
protected void truncateHistory(int maxEntries) {
if (maxEntries < 0) {
throw new IllegalArgumentException();
}
try {
// null means "delete all". otherwise "delete but leave n newest"
String selection = null;
if (maxEntries > 0) {
selection = "_id IN " +
"(SELECT _id FROM " + TABLE_SUGGESTIONS +
" ORDER BY " + COLUMN_DATE + " DESC" +
" LIMIT -1 OFFSET " + String.valueOf(maxEntries) + ")";
}
mDatabase.delete(TABLE_SUGGESTIONS, selection, null);
} catch (RuntimeException e) {
Say.e(TAG, "truncateHistory", e);
}
}
/**
* Builds the database. This version has extra support for using the version field
* as a mode flags field, and configures the database columns depending on the mode bits
* (features) requested by the extending class.
*/
private static class DatabaseHelper extends SQLiteOpenHelper {
public DatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, 1);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + TABLE_SUGGESTIONS + " (" +
"_id INTEGER PRIMARY KEY" +
"," + COLUMN_QUERY + " TEXT" +
"," + COLUMN_DATE + " LONG" +
");");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE_SUGGESTIONS);
onCreate(db);
}
}
}
@@ -0,0 +1,86 @@
/*
* Copyright (C) 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.widget;
import android.content.Context;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.widget.EditText;
public class SearchEditText extends EditText {
private SearchEditTextListener mListener;
public SearchEditText(Context context) {
super(context);
}
public SearchEditText(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SearchEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setSearchEditTextListener(SearchEditTextListener listener) {
mListener = listener;
}
@Override
public boolean onKeyPreIme(int keyCode, @NonNull KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
// special case for the back key, we do not even try to send it
// to the drop down list but instead, consume it immediately
if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
KeyEvent.DispatcherState state = getKeyDispatcherState();
if (state != null) {
state.startTracking(event, this);
}
return true;
} else if (event.getAction() == KeyEvent.ACTION_UP) {
KeyEvent.DispatcherState state = getKeyDispatcherState();
if (state != null) {
state.handleUpEvent(event);
}
if (event.isTracking() && !event.isCanceled()) {
// TODO stopSelectionActionMode
if (mListener != null) {
mListener.onBackPressed();
return true;
}
}
}
}
return super.onKeyPreIme(keyCode, event);
}
@Override
public boolean onTouchEvent(@NonNull MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP && mListener != null) {
mListener.onClick();
}
return super.onTouchEvent(event);
}
public interface SearchEditTextListener {
void onClick();
void onBackPressed();
}
}
@@ -0,0 +1,334 @@
/*
* Copyright (C) 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.widget;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.SwitchCompat;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CompoundButton;
import android.widget.FrameLayout;
import android.widget.RadioGroup;
import android.widget.TextView;
import com.hippo.easyrecyclerview.EasyRecyclerView;
import com.hippo.easyrecyclerview.MarginItemDecoration;
import com.hippo.ehviewer.R;
import com.hippo.rippleold.RippleSalon;
import com.hippo.widget.SimpleImageView;
import com.hippo.yorozuya.ViewUtils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
public class SearchLayout extends EasyRecyclerView implements CompoundButton.OnCheckedChangeListener,
View.OnClickListener {
@IntDef({SEARCH_MODE_NORMAL, SEARCH_MODE_IMAGE, SEARCH_MODE_SPECIFY})
@Retention(RetentionPolicy.SOURCE)
private @interface SearchMode {}
private static final String STATE_KEY_SUPER = "super";
private static final String STATE_KEY_SEARCH_MODE = "search_mode";
private static final String STATE_KEY_ENABLE_ADVANCE = "enable_advance";
public static final int SEARCH_MODE_NORMAL = 0;
public static final int SEARCH_MODE_IMAGE = 1;
public static final int SEARCH_MODE_SPECIFY = 2;
private static final int ITEM_TYPE_NORMAL = 0;
private static final int ITEM_TYPE_NORMAL_ADVANCE = 1;
private static final int ITEM_TYPE_IMAGE = 2;
private static final int ITEM_TYPE_SPECIFY = 3;
private static final int ITEM_TYPE_ACTION = 4;
private static final int[] SEARCH_ITEM_COUNT_ARRAY = {
3, 2, 2
};
private static final int[][] SEARCH_ITEM_TYPE = {
{ITEM_TYPE_NORMAL, ITEM_TYPE_NORMAL_ADVANCE, ITEM_TYPE_ACTION}, // SEARCH_MODE_NORMAL
{ITEM_TYPE_IMAGE, ITEM_TYPE_ACTION}, // SEARCH_MODE_IMAGE
{ITEM_TYPE_SPECIFY, ITEM_TYPE_ACTION} // SEARCH_MODE_SPECIFY
};
private LayoutInflater mInflater;
private int mSearchMode = SEARCH_MODE_NORMAL;
private boolean mEnableAdvance = false;
private View mNormalView;
private CategoryTable mCategoryTable;
private RadioGroup mNormalSearchMode;
private SimpleImageView mNormalSearchModeHelp;
private View mSearchTagHelp;
private SwitchCompat mEnableAdvanceSwitch;
private View mAdvanceView;
private AdvanceSearchTable mTableAdvanceSearch;
private View mActionView;
private TextView mAction1;
private TextView mAction2;
private LinearLayoutManager mLayoutManager;
private SearchAdapter mAdapter;
private Helper mHelper;
public SearchLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public SearchLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
@SuppressLint("InflateParams")
private void init(Context context) {
Resources resources = context.getResources();
mInflater = LayoutInflater.from(context);
mLayoutManager = new LinearLayoutManager(context);
mAdapter = new SearchAdapter();
//mAnimator = new SearchItemAnimator(this);
setLayoutManager(mLayoutManager);
setAdapter(mAdapter);
setHasFixedSize(true);
setClipToPadding(false);
//setItemAnimator(mAnimator);
addItemDecoration(new MarginItemDecoration(
getContext().getResources().getDimensionPixelOffset(R.dimen.list_item_margin)));
int paddingH = resources.getDimensionPixelOffset(R.dimen.list_content_margin_h);
int paddingV = resources.getDimensionPixelOffset(R.dimen.list_content_margin_v);
setPadding(paddingV, paddingH, paddingV, paddingH);
FrameLayout frameLayout = new FrameLayout(context);
// Create normal view
View normalView = mInflater.inflate(R.layout.search_normal, frameLayout, false);
mNormalView = normalView;
mCategoryTable = (CategoryTable) normalView.findViewById(R.id.search_category_table);
mNormalSearchMode = (RadioGroup) normalView.findViewById(R.id.normal_search_mode);
mNormalSearchModeHelp = (SimpleImageView) normalView.findViewById(R.id.normal_search_mode_help);
mEnableAdvanceSwitch = (SwitchCompat) normalView.findViewById(R.id.search_enable_advance);
Drawable helpDrawable = com.hippo.vector.VectorDrawable.create(context, R.drawable.ic_help_circle);
mNormalSearchModeHelp.setDrawable(helpDrawable);
mNormalSearchModeHelp.setOnClickListener(this);
RippleSalon.addRipple(mNormalSearchModeHelp, false);
mEnableAdvanceSwitch.setOnCheckedChangeListener(SearchLayout.this);
mEnableAdvanceSwitch.setSwitchPadding(resources.getDimensionPixelSize(R.dimen.switch_padding));
// Create advance view
mAdvanceView = mInflater.inflate(R.layout.search_advance, null);
mTableAdvanceSearch = (AdvanceSearchTable) mAdvanceView.findViewById(R.id.search_advance_search_table);
// Create action view
mActionView = mInflater.inflate(R.layout.search_action, null);
mAction1 = (TextView) mActionView.findViewById(R.id.search_action_1);
mAction2 = (TextView) mActionView.findViewById(R.id.search_action_2);
}
public void setHelper(Helper helper) {
mHelper = helper;
}
public void scrollSearchContainerToTop() {
mLayoutManager.scrollToPositionWithOffset(0, 0);
}
@Override
protected void dispatchSaveInstanceState(@NonNull SparseArray<Parcelable> container) {
super.dispatchSaveInstanceState(container);
mNormalView.saveHierarchyState(container);
mAdvanceView.saveHierarchyState(container);
mActionView.saveHierarchyState(container);
}
@Override
protected void dispatchRestoreInstanceState(@NonNull SparseArray<Parcelable> container) {
super.dispatchRestoreInstanceState(container);
mNormalView.restoreHierarchyState(container);
mAdvanceView.restoreHierarchyState(container);
mActionView.restoreHierarchyState(container);
}
@Override
public Parcelable onSaveInstanceState() {
final Bundle state = new Bundle();
state.putParcelable(STATE_KEY_SUPER, super.onSaveInstanceState());
state.putInt(STATE_KEY_SEARCH_MODE, mSearchMode);
state.putBoolean(STATE_KEY_ENABLE_ADVANCE, mEnableAdvance);
return state;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
final Bundle savedState = (Bundle) state;
super.onRestoreInstanceState(savedState.getParcelable(STATE_KEY_SUPER));
mSearchMode = savedState.getInt(STATE_KEY_SEARCH_MODE);
mEnableAdvance = savedState.getBoolean(STATE_KEY_ENABLE_ADVANCE);
}
}
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (buttonView == mEnableAdvanceSwitch) {
mEnableAdvance = isChecked;
if (mSearchMode == SEARCH_MODE_NORMAL) {
if (isChecked) {
mAdapter.notifyItemInserted(1);
} else {
mAdapter.notifyItemRemoved(1);
}
if (mHelper != null) {
mHelper.onChangeSearchMode();
}
}
}
}
public void setSearchMode(@SearchMode int searchMode, boolean animation) {
if (mSearchMode != searchMode) {
int oldItemCount = mAdapter.getItemCount();
mSearchMode = searchMode;
int newItemCount = mAdapter.getItemCount();
if (animation) {
mAdapter.notifyItemRangeRemoved(0, oldItemCount - 1);
mAdapter.notifyItemRangeInserted(0, newItemCount - 1);
} else {
mAdapter.notifyDataSetChanged();
}
if (mHelper != null) {
mHelper.onChangeSearchMode();
}
}
}
public void toggleSearchMode() {
int oldItemCount = mAdapter.getItemCount();
mSearchMode++;
if (mSearchMode > SEARCH_MODE_SPECIFY) {
mSearchMode = SEARCH_MODE_NORMAL;
}
int newItemCount = mAdapter.getItemCount();
mAdapter.notifyItemRangeRemoved(0, oldItemCount - 1);
mAdapter.notifyItemRangeInserted(0, newItemCount - 1);
if (mHelper != null) {
mHelper.onChangeSearchMode();
}
}
@Override
public void onClick(View v) {
if (mNormalSearchModeHelp == v) {
new AlertDialog.Builder(getContext()).setTitle("TODO").setMessage("TODO").show();
}
}
private class SimpleHolder extends RecyclerView.ViewHolder {
public SimpleHolder(View itemView) {
super(itemView);
}
}
private class SearchAdapter extends EasyRecyclerView.Adapter<RecyclerView.ViewHolder> {
@Override
public int getItemCount() {
int count = SEARCH_ITEM_COUNT_ARRAY[mSearchMode];
if (mSearchMode == SEARCH_MODE_NORMAL && !mEnableAdvance) {
count--;
}
return count;
}
@Override
public int getItemViewType(int position) {
int type = SEARCH_ITEM_TYPE[mSearchMode][position];
if (mSearchMode == SEARCH_MODE_NORMAL && position == 1 && !mEnableAdvance) {
type = ITEM_TYPE_ACTION;
}
return type;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view;
if (viewType == ITEM_TYPE_ACTION) {
ViewUtils.removeFromParent(mActionView);
mActionView.setLayoutParams(new RecyclerView.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
view = mActionView;
} else {
view = mInflater.inflate(R.layout.search_category, parent, false);
TextView title = (TextView) view.findViewById(R.id.category_title);
FrameLayout content = (FrameLayout) view.findViewById(R.id.category_content);
switch (viewType) {
case ITEM_TYPE_NORMAL:
title.setText(R.string.search_normal);
ViewUtils.removeFromParent(mNormalView);
content.addView(mNormalView);
break;
case ITEM_TYPE_NORMAL_ADVANCE:
title.setText(R.string.search_advance);
ViewUtils.removeFromParent(mAdvanceView);
content.addView(mAdvanceView);
break;
}
}
return new SimpleHolder(view);
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
// Empty, bind view in create view
}
}
public interface Helper {
void onChangeSearchMode();
}
}
@@ -0,0 +1,109 @@
/*
* Copyright (C) 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.widget;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
import com.hippo.ehviewer.R;
public class SimpleRatingView extends View {
private Drawable mStarDrawable;
private Drawable mStarHalfDrawable;
private Drawable mStarOutlineDrawable;
private int mRatingSize;
private int mRatingInterval;
private float mRating;
private int mRatingInt;
public SimpleRatingView(Context context) {
super(context);
init(context);
}
public SimpleRatingView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public SimpleRatingView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
Resources resources = context.getResources();
mStarDrawable = resources.getDrawable(R.drawable.ic_star_x16);
mStarHalfDrawable = resources.getDrawable(R.drawable.ic_star_half_x16);
mStarOutlineDrawable = resources.getDrawable(R.drawable.ic_star_outline_x16);
mRatingSize = resources.getDimensionPixelOffset(R.dimen.rating_size);
mRatingInterval = resources.getDimensionPixelOffset(R.dimen.rating_interval);
mStarDrawable.setBounds(0, 0, mRatingSize, mRatingSize);
mStarHalfDrawable.setBounds(0, 0, mRatingSize, mRatingSize);
mStarOutlineDrawable.setBounds(0, 0, mRatingSize, mRatingSize);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(mRatingSize * 5 + mRatingInterval * 4, mRatingSize);
}
@Override
protected void onDraw(Canvas canvas) {
int ratingInt = mRatingInt;
int step = mRatingSize + mRatingInterval;
int numStar = ratingInt / 2;
int numStarHalf = ratingInt % 2;
int saved = canvas.save();
while (numStar-- > 0) {
mStarDrawable.draw(canvas);
canvas.translate(step, 0);
}
if (numStarHalf == 1) {
mStarHalfDrawable.draw(canvas);
canvas.translate(step, 0);
}
int numOutline = 5 - numStar - numStarHalf;
while (numOutline-- > 0) {
mStarOutlineDrawable.draw(canvas);
canvas.translate(step, 0);
}
canvas.restoreToCount(saved);
}
public void setRating(float rating) {
if (mRating != rating) {
mRating = rating;
int ratingInt = (int) Math.ceil(rating * 2);
if (mRatingInt != ratingInt) {
mRatingInt = ratingInt;
invalidate();
}
}
}
public float getRating() {
return mRating;
}
}
@@ -0,0 +1,130 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.image;
import android.graphics.Bitmap;
import java.io.InputStream;
public class Image {
public static final int FORMAT_JPEG = 0x00;
public static final int FORMAT_PNG = 0x01;
public static final int FORMAT_GIF = 0x02;
private long mNativePtr;
private int mFormat;
private int mWidth;
private int mHeight;
private Image(long nativePtr, int format, int width, int height) {
mNativePtr = nativePtr;
mFormat = format;
mWidth = width;
mHeight = height;
}
public int getFormat() {
return mFormat;
}
public int getWidth() {
return mWidth;
}
public int getHeight() {
return mHeight;
}
private void checkRecycled() {
if (mNativePtr == 0) {
throw new IllegalStateException("The image is recycled.");
}
}
public boolean complete() {
checkRecycled();
return nativeComplete(mNativePtr, mFormat);
}
public boolean isCompleted() {
checkRecycled();
return nativeIsCompleted(mNativePtr, mFormat);
}
public boolean render(int srcX, int srcY, Bitmap dst, int dstX, int dstY,
int width, int height, boolean fillBlank, int defaultColor) {
checkRecycled();
return nativeRender(mNativePtr, mFormat, srcX, srcY, dst, dstX, dstY,
width, height, fillBlank, defaultColor);
}
public void advance() {
checkRecycled();
nativeAdvance(mNativePtr, mFormat);
}
public int getDelay() {
checkRecycled();
return nativeGetDelay(mNativePtr, mFormat);
}
public int getFrameCount() {
checkRecycled();
return nativeFrameCount(mNativePtr, mFormat);
}
public void recycle() {
if (mNativePtr != 0) {
nativeRecycle(mNativePtr, mFormat);
mNativePtr = 0;
}
}
public boolean isRecycled() {
return mNativePtr == 0;
}
public static Image decode(InputStream is, boolean partially) {
return nativeDecode(is, partially);
}
static {
System.loadLibrary("stream");
System.loadLibrary("jpeg-turbo");
System.loadLibrary("gif");
System.loadLibrary("image");
}
private static native Image nativeDecode(InputStream is, boolean partially);
private static native boolean nativeComplete(long nativePtr, int format);
private static native boolean nativeIsCompleted(long nativePtr, int format);
private static native boolean nativeRender(long nativePtr, int format,
int srcX, int srcY, Bitmap dst, int dstX, int dstY,
int width, int height, boolean fillBlank, int defaultColor);
private static native void nativeAdvance(long nativePtr, int format);
private static native int nativeGetDelay(long nativePtr, int format);
private static native int nativeFrameCount(long nativePtr, int format);
private static native void nativeRecycle(long nativePtr, int format);
}
@@ -0,0 +1,57 @@
/*
* Copyright (C) 2014-2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.network;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class UrlBuilder {
public String mRootUrl;
public Map<String, Object> mQueryMap = new HashMap<>();
public UrlBuilder(String rootUrl) {
mRootUrl = rootUrl;
}
public void addQuery(String key, Object value) {
mQueryMap.put(key, value);
}
public String build() {
if (mQueryMap.size() == 0) {
return mRootUrl;
} else {
StringBuilder sb = new StringBuilder(mRootUrl);
sb.append("?");
Iterator<String> iter = mQueryMap.keySet().iterator();
if (iter.hasNext()) {
String key = iter.next();
Object value = mQueryMap.get(key);
sb.append(key).append("=").append(value);
}
while (iter.hasNext()) {
String key = iter.next();
Object value = mQueryMap.get(key);
sb.append("&").append(key).append("=").append(value);
}
return sb.toString();
}
}
}
@@ -62,6 +62,7 @@ public class CookieDB {
return cookiesDomain(url.getHost());
}
// FIXME It is not right to store like this
public static String cookiesDomain(String domain) {
if (TextUtils.isEmpty(domain)) {
return "";
@@ -100,6 +100,18 @@ public class CookieDBStore implements CookieJar {
}
}
private synchronized void removeInternal(String domain) {
// Remove all cookies of the domain from map
List<CookieWithID> cookies = map.remove(domain);
if (cookies != null) {
for (CookieWithID cwi : cookies) {
// Remove from DB
CookieDB.removeCookie(cwi.id);
}
cookies.clear();
}
}
private synchronized void addInternal(String domain, Cookie cookie) {
// Check expired
if (hasExpired(cookie)) {
@@ -134,6 +146,10 @@ public class CookieDBStore implements CookieJar {
removeInternal(CookieDB.cookiesDomain(url), name);
}
public void remove(String url) {
removeInternal(CookieDB.cookiesDomain(url));
}
public void add(Cookie cookie) {
addInternal(CookieDB.cookiesDomain(cookie.domain()), cookie);
}
@@ -16,15 +16,39 @@
package com.hippo.scene;
import android.os.Bundle;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
public class SceneFragment extends Fragment {
@IntDef({LAUNCH_MODE_STANDARD, LAUNCH_MODE_SINGLE_TOP})
@Retention(RetentionPolicy.SOURCE)
private @interface LaunchMode {}
public static final int LAUNCH_MODE_STANDARD = 0;
public static final int LAUNCH_MODE_SINGLE_TOP = 1;
@LaunchMode
public int getLaunchMode() {
return LAUNCH_MODE_STANDARD;
}
public void onNewArguments(@NonNull Bundle args) {}
public <T extends SceneFragment> void startScene(Class<T> clazz) {
startScene(clazz, null);
}
public <T extends SceneFragment> void startScene(Class<T> clazz, Bundle args) {
FragmentActivity activity = getActivity();
if (activity instanceof StageActivity) {
((StageActivity) activity).startScene(clazz);
((StageActivity) activity).startScene(clazz, args);
}
}
@@ -41,4 +65,11 @@ public class SceneFragment extends Fragment {
activity.finish();
}
}
/**
* @return true for handle this back pressed action
*/
public boolean onBackPressed() {
return false;
}
}
@@ -75,6 +75,17 @@ public abstract class StageActivity extends AppCompatActivity {
old = fragmentManager.findFragmentByTag(tag);
}
// Launch mode single top
if (old instanceof SceneFragment && clazz.isInstance(old)) {
SceneFragment oldScene = (SceneFragment) old;
if (oldScene.getLaunchMode() == SceneFragment.LAUNCH_MODE_SINGLE_TOP) {
if (args != null) {
oldScene.onNewArguments(args);
}
return;
}
}
// New scene
SceneFragment scene = newSceneInstance(clazz);
scene.setArguments(args);
@@ -142,10 +153,25 @@ public abstract class StageActivity extends AppCompatActivity {
@Override
public void onBackPressed() {
int size = mSceneTagList.size();
if (size > 0) {
finishScene(mSceneTagList.get(size - 1));
} else {
finish();
String tag = mSceneTagList.get(size - 1);
SceneFragment scene;
Fragment fragment = getSupportFragmentManager().findFragmentByTag(tag);
if (fragment == null) {
Log.e(TAG, "onBackPressed: Can't find scene by tag: " + tag);
return;
}
if (!(fragment instanceof SceneFragment)) {
Log.e(TAG, "onBackPressed: The fragment is not SceneFragment");
return;
}
scene = (SceneFragment) fragment;
if (!scene.onBackPressed()) {
if (size > 0) {
finishScene(tag);
} else {
finish();
}
}
}
@@ -0,0 +1,131 @@
/*
* Copyright (C) 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.view;
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.view.View;
import com.hippo.anani.SimpleAnimatorListener;
public class ViewTransition {
private static long ANIMATE_TIME = 300L;
private final View[] mViews;
private int mShownView = -1;
private Animator mAnimator1;
private Animator mAnimator2;
public ViewTransition(View... views) {
if (views.length < 2) {
throw new IllegalStateException("You must pass view to ViewTransition");
}
for (View v : views) {
if (v == null) {
throw new IllegalStateException("Any View pass to ViewTransition must not be null");
}
}
mViews = views;
showView(0, false);
}
public int getShownViewIndex() {
return mShownView;
}
public boolean showView(int shownView) {
return showView(shownView, true);
}
public boolean showView(int shownView, boolean animation) {
View[] views = mViews;
int length = views.length;
if (shownView >= length || shownView < 0) {
throw new IndexOutOfBoundsException("Only " + length + " view(s) in " +
"the ViewTransition, but attempt to show " + shownView);
}
if (mShownView != shownView) {
int oldShownView = mShownView;
mShownView = shownView;
// Cancel animation
if (mAnimator1 != null) {
mAnimator1.cancel();
}
if (mAnimator2 != null) {
mAnimator2.cancel();
}
if (animation) {
for (int i = 0; i < length; i++) {
if (i != oldShownView && i != shownView) {
View v = views[i];
v.setAlpha(0f);
v.setVisibility(View.GONE);
}
}
startAnimations(views[oldShownView], views[shownView]);
} else {
for (int i = 0; i < length; i++) {
View v = views[i];
if (i == shownView) {
v.setAlpha(1f);
v.setVisibility(View.VISIBLE);
} else {
v.setAlpha(0f);
v.setVisibility(View.GONE);
}
}
}
return true;
} else {
return false;
}
}
private void startAnimations(final View hiddenView, final View shownView) {
ObjectAnimator oa1 = ObjectAnimator.ofFloat(hiddenView, "alpha", 0f);
oa1.setDuration(ANIMATE_TIME);
oa1.addListener(new SimpleAnimatorListener() {
@Override
public void onAnimationEnd(Animator animation) {
hiddenView.setVisibility(View.GONE);
mAnimator1 = null;
}
});
oa1.start();
mAnimator1 = oa1;
shownView.setVisibility(View.VISIBLE);
ObjectAnimator oa2 = ObjectAnimator.ofFloat(shownView, "alpha", 1f);
oa2.setDuration(ANIMATE_TIME);
oa2.addListener(new SimpleAnimatorListener() {
@Override
public void onAnimationEnd(Animator animation) {
mAnimator2 = null;
}
});
oa2.start();
mAnimator2 = oa2;
}
}
@@ -0,0 +1,109 @@
package com.hippo.widget;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.widget.Button;
import com.hippo.cardsalon.CardHelper;
import com.hippo.cardsalon.CardViewDelegate;
public class CardButton extends Button implements CardViewDelegate {
private CardHelper mCardHelper;
public CardButton(Context context) {
super(context);
init(context, null);
}
public CardButton(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public CardButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
mCardHelper = new CardHelper(this);
mCardHelper.initialize(context, attrs);
}
@Override
public void setBackgroundResource(int resid) {
// Empty
}
@SuppressWarnings("deprecation")
@Override
public void setBackgroundDrawable(Drawable drawable) {
// Empty
}
@Override
public void setCardPadding(int left, int top, int right, int bottom) {
if (mCardHelper != null) {
mCardHelper.setPadding(left, top, right, bottom);
}
}
@SuppressWarnings("deprecation")
@Override
public void setRawBackgroundDrawable(Drawable drawable) {
super.setBackgroundDrawable(drawable);
}
@Override
public void setCardRadius(float radius) {
mCardHelper.setCardRadius(radius);
}
@Override
public float getCardRadius() {
return mCardHelper.getCardRadius();
}
@Override
public void setCardBackgroundColor(ColorStateList color) {
mCardHelper.setCardBackgroundColor(color);
}
@Override
public ColorStateList getCardBackgroundColor() {
return mCardHelper.getCardBackgroundColor();
}
@Override
public void setCardBoundSize(float size) {
mCardHelper.setCardBoundSize(size);
}
@Override
public float getCardBoundSize() {
return mCardHelper.getCardBoundSize();
}
@Override
public void setCardBoundColor(ColorStateList color) {
mCardHelper.setCardBoundColor(color);
}
@Override
public ColorStateList getCardBoundColor() {
return mCardHelper.getCardBoundColor();
}
@Override
public void setCardElevation(float elevation) {
mCardHelper.setCardElevation(elevation);
}
@Override
public float getCardElevation() {
return mCardHelper.getCardElevation();
}
}
@@ -0,0 +1,109 @@
package com.hippo.widget;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.widget.LinearLayout;
import com.hippo.cardsalon.CardHelper;
import com.hippo.cardsalon.CardViewDelegate;
public class CardLinearLayout extends LinearLayout implements CardViewDelegate {
private CardHelper mCardHelper;
public CardLinearLayout(Context context) {
super(context);
init(context, null);
}
public CardLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public CardLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
mCardHelper = new CardHelper(this);
mCardHelper.initialize(context, attrs);
}
@Override
public void setBackgroundResource(int resid) {
// Empty
}
@SuppressWarnings("deprecation")
@Override
public void setBackgroundDrawable(Drawable drawable) {
// Empty
}
@Override
public void setCardPadding(int left, int top, int right, int bottom) {
if (mCardHelper != null) {
mCardHelper.setPadding(left, top, right, bottom);
}
}
@SuppressWarnings("deprecation")
@Override
public void setRawBackgroundDrawable(Drawable drawable) {
super.setBackgroundDrawable(drawable);
}
@Override
public void setCardRadius(float radius) {
mCardHelper.setCardRadius(radius);
}
@Override
public float getCardRadius() {
return mCardHelper.getCardRadius();
}
@Override
public void setCardBackgroundColor(ColorStateList color) {
mCardHelper.setCardBackgroundColor(color);
}
@Override
public ColorStateList getCardBackgroundColor() {
return mCardHelper.getCardBackgroundColor();
}
@Override
public void setCardBoundSize(float size) {
mCardHelper.setCardBoundSize(size);
}
@Override
public float getCardBoundSize() {
return mCardHelper.getCardBoundSize();
}
@Override
public void setCardBoundColor(ColorStateList color) {
mCardHelper.setCardBoundColor(color);
}
@Override
public ColorStateList getCardBoundColor() {
return mCardHelper.getCardBoundColor();
}
@Override
public void setCardElevation(float elevation) {
mCardHelper.setCardElevation(elevation);
}
@Override
public float getCardElevation() {
return mCardHelper.getCardElevation();
}
}
@@ -0,0 +1,125 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.widget;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.widget.RelativeLayout;
import com.hippo.cardsalon.CardHelper;
import com.hippo.cardsalon.CardViewDelegate;
public class CardRelativeLayout extends RelativeLayout implements CardViewDelegate {
private CardHelper mCardHelper;
public CardRelativeLayout(Context context) {
super(context);
init(context, null);
}
public CardRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public CardRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
mCardHelper = new CardHelper(this);
mCardHelper.initialize(context, attrs);
}
@Override
public void setBackgroundResource(int resid) {
// Empty
}
@SuppressWarnings("deprecation")
@Override
public void setBackgroundDrawable(Drawable drawable) {
// Empty
}
@Override
public void setCardPadding(int left, int top, int right, int bottom) {
if (mCardHelper != null) {
mCardHelper.setPadding(left, top, right, bottom);
}
}
@SuppressWarnings("deprecation")
@Override
public void setRawBackgroundDrawable(Drawable drawable) {
super.setBackgroundDrawable(drawable);
}
@Override
public void setCardRadius(float radius) {
mCardHelper.setCardRadius(radius);
}
@Override
public float getCardRadius() {
return mCardHelper.getCardRadius();
}
@Override
public void setCardBackgroundColor(ColorStateList color) {
mCardHelper.setCardBackgroundColor(color);
}
@Override
public ColorStateList getCardBackgroundColor() {
return mCardHelper.getCardBackgroundColor();
}
@Override
public void setCardBoundSize(float size) {
mCardHelper.setCardBoundSize(size);
}
@Override
public float getCardBoundSize() {
return mCardHelper.getCardBoundSize();
}
@Override
public void setCardBoundColor(ColorStateList color) {
mCardHelper.setCardBoundColor(color);
}
@Override
public ColorStateList getCardBoundColor() {
return mCardHelper.getCardBoundColor();
}
@Override
public void setCardElevation(float elevation) {
mCardHelper.setCardElevation(elevation);
}
@Override
public float getCardElevation() {
return mCardHelper.getCardElevation();
}
}
@@ -0,0 +1,224 @@
/*
* Copyright (C) 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.widget;
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.animation.Interpolator;
import android.widget.TextView;
import com.hippo.anani.AnimationUtils;
import com.hippo.anani.SimpleAnimatorListener;
import com.hippo.ehviewer.R;
import com.hippo.hotspot.HotspotTouchHelper;
import com.hippo.hotspot.Hotspotable;
import com.hippo.yorozuya.MathUtils;
public class CheckTextView extends TextView implements OnClickListener, Hotspotable {
private static final String STATE_KEY_SUPER = "super";
private static final String STATE_KEY_CHECKED = "checked";
private static final long ANIMATION_DURATION = 200;
private int mMaskColor;
private boolean mChecked = false;
private boolean mPrepareAnimator = false;
private Paint mPaint;
private float mRadius = 0f;
private float mX;
private float mY;
Animator mAnimator;
private float mMaxRadius;
public CheckTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public CheckTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CheckTextView);
mMaskColor = a.getColor(R.styleable.CheckTextView_maskColor, Color.WHITE);
a.recycle();
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mPaint.setColor(mMaskColor);
setOnClickListener(this);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
HotspotTouchHelper.setHotspotTouchHelper(this, new HotspotTouchHelper(this));
}
}
@Override
public void setHotspot(float x, float y) {
mX = x;
mY = y;
mMaxRadius = MathUtils.coverageRadius(getWidth(), getHeight(), x, y);
}
@Override
public void drawableHotspotChanged(float x, float y) {
super.drawableHotspotChanged(x, y);
mX = x;
mY = y;
mMaxRadius = MathUtils.coverageRadius(getWidth(), getHeight(), x, y);
}
public void setRadius(float radius) {
float bigger = Math.max(mRadius, radius);
mRadius = radius;
invalidate((int) (mX - bigger), (int) (mY - bigger), (int) (mX + bigger), (int) (mY + bigger));
}
public float getRadius() {
return mRadius;
}
private Animator.AnimatorListener mAnimatorListener = new SimpleAnimatorListener() {
@Override
public void onAnimationEnd(Animator animation) {
mAnimator = null;
}
};
public void prepareAnimations() {
mPrepareAnimator = true;
}
private void createAnimations() {
float startRadius;
float endRadius;
Interpolator interpolator;
if (mChecked) {
startRadius = 0;
endRadius = mMaxRadius;
interpolator = AnimationUtils.FAST_SLOW_INTERPOLATOR;
} else {
startRadius = mMaxRadius;
endRadius = 0;
interpolator = AnimationUtils.SLOW_FAST_INTERPOLATOR;
}
mRadius = startRadius;
final ObjectAnimator radiusAnim = ObjectAnimator.ofFloat(this, "radius", startRadius, endRadius);
radiusAnim.setDuration(ANIMATION_DURATION);
radiusAnim.setInterpolator(interpolator);
radiusAnim.addListener(mAnimatorListener);
radiusAnim.start();
mAnimator = radiusAnim;
}
private void cancelAnimations() {
if (mAnimator != null) {
mAnimator.cancel();
}
}
@Override
public void onClick(View v) {
setChecked(!mChecked);
}
@Override
protected void onDraw(@NonNull Canvas canvas) {
super.onDraw(canvas);
if (mPrepareAnimator) {
mPrepareAnimator = false;
cancelAnimations();
createAnimations();
}
if (mAnimator != null) {
canvas.drawCircle(mX, mY, mRadius, mPaint);
} else {
if (mChecked) {
canvas.drawColor(mMaskColor);
}
}
}
public void setChecked(boolean checked) {
setChecked(checked, true);
}
/**
* Changes the checked state of this CheckTextView.
*
* @param checked checked true to check the CheckTextView, false to uncheck it
* @param animation true for show animation
*/
public void setChecked(boolean checked, boolean animation) {
if (mChecked != checked) {
mChecked = checked;
if (animation) {
prepareAnimations();
}
invalidate();
}
}
/**
* Get the checked state of it.
*
* @return true is it is checked
*/
public boolean isChecked() {
return mChecked;
}
@Override
public Parcelable onSaveInstanceState() {
final Bundle state = new Bundle();
state.putParcelable(STATE_KEY_SUPER, super.onSaveInstanceState());
state.putBoolean(STATE_KEY_CHECKED, mChecked);
return state;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
final Bundle savedState = (Bundle) state;
super.onRestoreInstanceState(savedState.getParcelable(STATE_KEY_SUPER));
setChecked(savedState.getBoolean(STATE_KEY_CHECKED), false);
}
}
}
@@ -0,0 +1,711 @@
/*
* Copyright 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.widget;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.hippo.easyrecyclerview.EasyRecyclerView;
import com.hippo.easyrecyclerview.FastScroller;
import com.hippo.easyrecyclerview.HandlerDrawable;
import com.hippo.easyrecyclerview.LayoutManagerUtils;
import com.hippo.ehviewer.R;
import com.hippo.utils.ExceptionUtils;
import com.hippo.vector.VectorDrawable;
import com.hippo.view.ViewTransition;
import com.hippo.widget.refreshlayout.RefreshLayout;
import com.hippo.yorozuya.IdIntGenerator;
import com.hippo.yorozuya.IntList;
import com.hippo.yorozuya.LayoutUtils;
import com.hippo.yorozuya.ResourcesUtils;
import java.util.ArrayList;
import java.util.List;
public class ContentLayout extends FrameLayout {
private static final String TAG = ContentLayout.class.getSimpleName();
private ProgressView mProgressView;
private ViewGroup mTipView;
private ViewGroup mContentView;
private RefreshLayout mRefreshLayout;
private EasyRecyclerView mRecyclerView;
private FastScroller mFastScroller;
private View mImageView;
private TextView mTextView;
private ContentHelper mContentHelper;
private int mRecyclerViewOriginTop;
private int mRecyclerViewOriginBottom;
public ContentLayout(Context context) {
super(context);
init(context);
}
public ContentLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public ContentLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
LayoutInflater.from(context).inflate(R.layout.widget_content_layout, this);
mProgressView = (ProgressView) findViewById(R.id.progress);
mTipView = (ViewGroup) findViewById(R.id.tip);
mContentView = (ViewGroup) findViewById(R.id.content_view);
mRefreshLayout = (RefreshLayout) mContentView.findViewById(R.id.refresh_layout);
mFastScroller = (FastScroller) mContentView.findViewById(R.id.fast_scroller);
mRecyclerView = (EasyRecyclerView) mRefreshLayout.findViewById(R.id.recycler_view);
mImageView = mTipView.getChildAt(0);
mTextView = (TextView) mTipView.getChildAt(1);
SimpleImageView imageView = (SimpleImageView) findViewById(R.id.empty_image);
mFastScroller.attachToRecyclerView(mRecyclerView);
HandlerDrawable drawable = new HandlerDrawable();
drawable.setColor(ResourcesUtils.getAttrColor(context, R.attr.colorAccent));
mFastScroller.setHandlerDrawable(drawable);
imageView.setDrawable(VectorDrawable.create(context, R.drawable.ic_emoticon_sad));
mRefreshLayout.setHeaderColorSchemeResources(
R.color.loading_indicator_red,
R.color.loading_indicator_purple,
R.color.loading_indicator_blue,
R.color.loading_indicator_cyan,
R.color.loading_indicator_green,
R.color.loading_indicator_yellow);
mRefreshLayout.setFooterColorSchemeResources(
R.color.loading_indicator_red,
R.color.loading_indicator_blue,
R.color.loading_indicator_green,
R.color.loading_indicator_orange);
mRecyclerViewOriginTop = mRecyclerView.getPaddingTop();
mRecyclerViewOriginBottom = mRecyclerView.getPaddingBottom();
}
public EasyRecyclerView getRecyclerView() {
return mRecyclerView;
}
public FastScroller getFastScroller() {
return mFastScroller;
}
public void setHelper(ContentHelper helper) {
mContentHelper = helper;
helper.init(this);
}
public void showFastScroll() {
if (!mFastScroller.isAttached()) {
mFastScroller.attachToRecyclerView(mRecyclerView);
}
}
public void hideFastScroll() {
mFastScroller.detachedFromRecyclerView();
}
public void setFitPaddingTop(int fitPaddingTop) {
// RecyclerView
mRecyclerView.setPadding(mRecyclerView.getPaddingLeft(), mRecyclerViewOriginTop + fitPaddingTop, mRecyclerView.getPaddingRight(), mRecyclerView.getPaddingBottom());
// RefreshLayout
mRefreshLayout.setProgressViewOffset(false, fitPaddingTop, fitPaddingTop + LayoutUtils.dp2pix(getContext(), 32)); // TODO
}
public void setFitPaddingBottom(int fitPaddingBottom) {
// RecyclerView
mRecyclerView.setPadding(mRecyclerView.getPaddingLeft(),
mRecyclerView.getPaddingTop(), mRecyclerView.getPaddingRight(),
mRecyclerViewOriginBottom + fitPaddingBottom);
}
public abstract static class ContentHelper<E> {
private static final String TAG = ContentHelper.class.getSimpleName();
public static final int TYPE_REFRESH = 0;
public static final int TYPE_PRE_PAGE = 1;
public static final int TYPE_PRE_PAGE_KEEP_POS = 2;
public static final int TYPE_NEXT_PAGE = 3;
public static final int TYPE_NEXT_PAGE_KEEP_POS = 4;
public static final int TYPE_SOMEWHERE = 5;
public static final int TYPE_REFRESH_PAGE = 6;
public static final int REFRESH_TYPE_HEADER = 0;
public static final int REFRESH_TYPE_FOOTER = 1;
public static final int REFRESH_TYPE_PROGRESS_VIEW = 2;
private ProgressView mProgressView;
private ViewGroup mTipView;
private ViewGroup mContentView;
private RefreshLayout mRefreshLayout;
private EasyRecyclerView mRecyclerView;
private View mImageView;
private TextView mTextView;
private ViewTransition mViewTransition;
/**
* Store data
*/
private List<E> mData = new ArrayList<>();
/**
* Generate task id
*/
private IdIntGenerator mIdGenerator = new IdIntGenerator();
/**
* Store the page divider index
*
* For example, the data contain page 3, page 4, page 5,
* page 3 size is 7, page 4 size is 8, page 5 size is 9,
* so <code>mPageDivider</code> contain 7, 15, 24.
*/
private IntList mPageDivider = new IntList();
/**
* The first page in <code>mData</code>
*/
private int mStartPage;
/**
* The last page + 1 in <code>mData</code>
*/
private int mEndPage;
/**
* The available page count.
*/
private int mPages;
private int mCurrentTaskId;
private int mCurrentTaskType;
private int mCurrentTaskPage;
private int mNextPageScrollSize;
private String mEmptyString = "No hint";
private RecyclerView.OnScrollListener mOnScrollListener = new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (!mRefreshLayout.isRefreshing() && mRefreshLayout.isAlmostBottom() && mEndPage < mPages) {
// Get next page
mRefreshLayout.setFooterRefreshing(true);
mOnRefreshListener.onFooterRefresh();
}
}
};
private RefreshLayout.OnRefreshListener mOnRefreshListener = new RefreshLayout.OnRefreshListener() {
@Override
public void onHeaderRefresh() {
if (mStartPage > 0) {
mCurrentTaskId = mIdGenerator.nextId();
mCurrentTaskType = TYPE_PRE_PAGE_KEEP_POS;
mCurrentTaskPage = mStartPage - 1;
getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage);
} else {
doRefresh();
}
}
@Override
public void onFooterRefresh() {
if (mEndPage < mPages) {
// Get next page
mCurrentTaskId = mIdGenerator.nextId();
mCurrentTaskType = TYPE_NEXT_PAGE_KEEP_POS;
mCurrentTaskPage = mEndPage;
getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage);
} else if (mEndPage == mPages) {
// Refresh last page
mCurrentTaskId = mIdGenerator.nextId();
mCurrentTaskType = TYPE_REFRESH_PAGE;
mCurrentTaskPage = mEndPage - 1;
getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage);
} else {
Log.e(TAG, "Try to footer refresh, but mEndPage = " + mEndPage + ", mPages = " + mPages);
}
}
};
private LayoutManagerUtils.OnScrollToPositionListener mOnScrollToPositionListener =
new LayoutManagerUtils.OnScrollToPositionListener() {
@Override
public void onScrollToPosition() {
ContentHelper.this.onScrollToPosition();
}
};
private void init(ContentLayout contentLayout) {
mNextPageScrollSize = LayoutUtils.dp2pix(contentLayout.getContext(), 48);
mProgressView = contentLayout.mProgressView;
mTipView = contentLayout.mTipView;
mContentView = contentLayout.mContentView;
mRefreshLayout = contentLayout.mRefreshLayout;
mRecyclerView = contentLayout.mRecyclerView;
mImageView = contentLayout.mImageView;
mTextView = contentLayout.mTextView;
mViewTransition = new ViewTransition(mContentView, mProgressView, mTipView);
mViewTransition.showView(2, false);
mRecyclerView.addOnScrollListener(mOnScrollListener);
mRefreshLayout.setOnRefreshListener(mOnRefreshListener);
mTipView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
refresh();
}
});
}
/**
* Call {@link #onGetPageData(int, List)} when get data
*
* @param taskId task id
* @param page the page to get
*/
protected abstract void getPageData(int taskId, int type, int page);
protected abstract Context getContext();
protected abstract void notifyDataSetChanged();
protected abstract void notifyItemRangeRemoved(int positionStart, int itemCount);
protected abstract void notifyItemRangeInserted(int positionStart, int itemCount);
protected void onScrollToPosition() {
}
protected void onShowProgress() {
}
protected void onShowText() {
}
public void setEnable(boolean enable) {
mRefreshLayout.setEnabled(enable);
}
public void setEmptyString(String str) {
mEmptyString = str;
}
/**
* @throws IndexOutOfBoundsException
* if {@code location < 0 || location >= size()}
*/
public E getDataAt(int location) {
return mData.get(location);
}
public int size() {
return mData.size();
}
public void setPages(int taskId, int pages) {
// TODO what it pages > mEndPage
if (mCurrentTaskId == taskId) {
mPages = pages;
}
}
public int getPages() {
return mPages;
}
public void addAt(int index, E data) {
mData.add(index, data);
for (int i = 0, n = mPageDivider.size(); i < n; i++) {
int divider = mPageDivider.get(i);
if (index < divider) {
mPageDivider.set(i, divider + 1);
}
}
notifyItemRangeInserted(index, 1);
}
public void removeAt(int index) {
mData.remove(index);
for (int i = 0, n = mPageDivider.size(); i < n; i++) {
int divider = mPageDivider.get(i);
if (index < divider) {
mPageDivider.set(i, divider - 1);
}
}
notifyItemRangeRemoved(index, 1);
}
public void onGetEmptyData(int taskId) {
if (mCurrentTaskId != taskId) {
return;
}
switch (mCurrentTaskType) {
case TYPE_REFRESH:
case TYPE_SOMEWHERE:
showText(mEmptyString);
break;
case TYPE_NEXT_PAGE:
case TYPE_NEXT_PAGE_KEEP_POS:
// Last page ?
break;
case TYPE_PRE_PAGE:
case TYPE_PRE_PAGE_KEEP_POS:
case TYPE_REFRESH_PAGE:
// TODO
break;
}
mRefreshLayout.setHeaderRefreshing(false);
mRefreshLayout.setFooterRefreshing(false);
}
public void onGetPageData(int taskId, List<E> data) {
if (mCurrentTaskId == taskId) {
showContent();
int dataSize;
switch (mCurrentTaskType) {
case TYPE_REFRESH:
mStartPage = 0;
mEndPage = 1;
mPageDivider.clear();
mPageDivider.add(data.size());
mData.clear();
mData.addAll(data);
notifyDataSetChanged();
mRecyclerView.stopScroll();
LayoutManagerUtils.scrollToPositionWithOffset(mRecyclerView.getLayoutManager(), 0, 0);
onScrollToPosition();
break;
case TYPE_PRE_PAGE:
case TYPE_PRE_PAGE_KEEP_POS:
mData.addAll(0, data);
notifyItemRangeInserted(0, data.size());
dataSize = data.size();
for (int i = 0, n = mPageDivider.size(); i < n; i++) {
mPageDivider.set(i, mPageDivider.get(i) + dataSize);
}
mPageDivider.add(0, dataSize);
mStartPage--;
// assert mStartPage >= 0
if (mCurrentTaskType == TYPE_PRE_PAGE_KEEP_POS) {
mRecyclerView.stopScroll();
LayoutManagerUtils.scrollToPositionProperly(mRecyclerView.getLayoutManager(), getContext(),
dataSize - 1, mOnScrollToPositionListener);
} else {
mRecyclerView.stopScroll();
LayoutManagerUtils.scrollToPositionWithOffset(mRecyclerView.getLayoutManager(), 0, 0);
onScrollToPosition();
}
break;
case TYPE_NEXT_PAGE:
case TYPE_NEXT_PAGE_KEEP_POS:
dataSize = data.size();
int oldDataSize = mData.size();
mData.addAll(data);
notifyItemRangeInserted(oldDataSize, dataSize);
mPageDivider.add(oldDataSize + dataSize);
mEndPage++;
if (mCurrentTaskType == TYPE_NEXT_PAGE_KEEP_POS) {
mRecyclerView.stopScroll();
mRecyclerView.smoothScrollBy(0, mNextPageScrollSize);
onScrollToPosition();
} else {
mRecyclerView.stopScroll();
LayoutManagerUtils.scrollToPositionWithOffset(mRecyclerView.getLayoutManager(), oldDataSize, 0);
onScrollToPosition();
}
break;
case TYPE_SOMEWHERE:
mData.clear();
mData.addAll(data);
notifyDataSetChanged();
mStartPage = mCurrentTaskPage;
mEndPage = mCurrentTaskPage + 1;
mPageDivider.clear();
mPageDivider.add(data.size());
mRecyclerView.stopScroll();
LayoutManagerUtils.scrollToPositionWithOffset(mRecyclerView.getLayoutManager(), 0, 0);
onScrollToPosition();
break;
case TYPE_REFRESH_PAGE:
if (mCurrentTaskPage < mStartPage || mCurrentTaskPage >= mEndPage) {
Log.e(TAG, "TYPE_REFRESH_PAGE, but mCurrentTaskPage = " + mCurrentTaskPage +
", mStartPage = " + mStartPage + ", mEndPage = " + mEndPage);
break;
}
int oldIndexStart = mCurrentTaskPage == mStartPage ? 0 : mPageDivider.get(mCurrentTaskPage - mStartPage - 1);
int oldIndexEnd = mPageDivider.get(mCurrentTaskPage - mStartPage);
mData.subList(oldIndexStart, oldIndexEnd).clear();
int newIndexStart = oldIndexStart;
int newIndexEnd = newIndexStart + data.size();
mData.addAll(oldIndexStart, data);
notifyDataSetChanged();
for (int i = mCurrentTaskPage - mStartPage, n = mPageDivider.size(); i < n; i++) {
mPageDivider.set(i, mPageDivider.get(i) - oldIndexEnd + newIndexEnd);
}
if (newIndexEnd > oldIndexEnd && newIndexEnd > 0) {
mRecyclerView.stopScroll();
LayoutManagerUtils.scrollToPositionWithOffset(mRecyclerView.getLayoutManager(), newIndexEnd - 1, 0);
onScrollToPosition();
}
break;
}
}
mRefreshLayout.setHeaderRefreshing(false);
mRefreshLayout.setFooterRefreshing(false);
}
public void onGetExpection(int taskId, Exception e) {
if (mCurrentTaskId == taskId) {
mRefreshLayout.setHeaderRefreshing(false);
mRefreshLayout.setFooterRefreshing(false);
String readableError;
if (e != null) {
e.printStackTrace();
readableError = ExceptionUtils.getReadableString(getContext(), e);
} else {
readableError = getContext().getString(R.string.error_unknown);
}
if (mViewTransition.getShownViewIndex() == 0) {
Toast.makeText(getContext(), readableError, Toast.LENGTH_SHORT).show();
} else {
showText(readableError);
}
}
}
public void showContent() {
mViewTransition.showView(0);
}
private boolean isContentShowning() {
return mViewTransition.getShownViewIndex() == 0;
}
public void showProgressBar() {
mViewTransition.showView(1, false);
}
public void showProgressBar(boolean animation) {
if (mViewTransition.showView(1, animation)) {
onShowProgress();
}
}
public void showText(CharSequence text) {
mTextView.setText(text);
if (mViewTransition.showView(2)) {
onShowText();
}
}
public void showEmptyString() {
showText(mEmptyString);
}
/**
* Be carefull
*/
public void doGetData(int type, int page, int refreshType) {
switch (refreshType) {
default:
case REFRESH_TYPE_HEADER:
showContent();
mRefreshLayout.setFooterRefreshing(false);
mRefreshLayout.setHeaderRefreshing(true);
break;
case REFRESH_TYPE_FOOTER:
showContent();
mRefreshLayout.setHeaderRefreshing(false);
mRefreshLayout.setFooterRefreshing(true);
break;
case REFRESH_TYPE_PROGRESS_VIEW:
showProgressBar();
mRefreshLayout.setHeaderRefreshing(false);
mRefreshLayout.setFooterRefreshing(false);
break;
}
mCurrentTaskId = mIdGenerator.nextId();
mCurrentTaskType = type;
mCurrentTaskPage = page;
getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage);
}
private void doRefresh() {
mCurrentTaskId = mIdGenerator.nextId();
mCurrentTaskType = TYPE_REFRESH;
mCurrentTaskPage = 0;
getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage);
}
/**
* Lisk {@link #refresh()}, but no animation when show progress bar
*/
public void firstRefresh() {
showProgressBar(false);
doRefresh();
}
/**
* Show progress bar first, than do refresh
*/
public void refresh() {
showProgressBar();
doRefresh();
}
private void cancelCurrentTask() {
mCurrentTaskId = mIdGenerator.nextId();
mRefreshLayout.setHeaderRefreshing(false);
mRefreshLayout.setFooterRefreshing(false);
}
private int getPageStart(int page) {
if (mStartPage == page) {
return 0;
} else {
return mPageDivider.get(page - mStartPage - 1);
}
}
private int getPageEnd(int page) {
return mPageDivider.get(page - mStartPage);
}
private int getPageForPosition(int position) {
if (position < 0) {
return -1;
}
IntList pageDivider = mPageDivider;
for (int i = 0, n = pageDivider.size(); i < n; i++) {
if (position < pageDivider.get(i)) {
return i + mStartPage;
}
}
return -1;
}
public int getPageForTop() {
return getPageForPosition(LayoutManagerUtils.getFirstVisibleItemPostion(mRecyclerView.getLayoutManager()));
}
public int getPageForBottom() {
return getPageForPosition(LayoutManagerUtils.getLastVisibleItemPostion(mRecyclerView.getLayoutManager()));
}
public boolean canGoTo() {
return isContentShowning();
}
/**
* Check range first!
*
* @param page the targe page
* @throws IndexOutOfBoundsException
*/
public void goTo(int page) throws IndexOutOfBoundsException {
if (page < 0 || page >= mPages) {
throw new IndexOutOfBoundsException("Page count is " + mPages + ", page is " + page);
} else if (page >= mStartPage && page < mEndPage) {
cancelCurrentTask();
int position = getPageStart(page);
mRecyclerView.stopScroll();
LayoutManagerUtils.scrollToPositionWithOffset(mRecyclerView.getLayoutManager(), position, 0);
onScrollToPosition();
} else if (page == mStartPage - 1) {
mRefreshLayout.setFooterRefreshing(false);
mRefreshLayout.setHeaderRefreshing(true);
mCurrentTaskId = mIdGenerator.nextId();
mCurrentTaskType = TYPE_PRE_PAGE;
mCurrentTaskPage = page;
getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage);
} else if (page == mEndPage) {
mRefreshLayout.setHeaderRefreshing(false);
mRefreshLayout.setFooterRefreshing(true);
mCurrentTaskId = mIdGenerator.nextId();
mCurrentTaskType = TYPE_NEXT_PAGE;
mCurrentTaskPage = page;
getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage);
} else {
mRefreshLayout.setFooterRefreshing(false);
mRefreshLayout.setHeaderRefreshing(true);
mCurrentTaskId = mIdGenerator.nextId();
mCurrentTaskType = TYPE_SOMEWHERE;
mCurrentTaskPage = page;
getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage);
}
}
}
}
@@ -0,0 +1,299 @@
/*
* Copyright 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
import com.hippo.ehviewer.R;
import com.hippo.yorozuya.MathUtils;
/**
* aspect is width / height
*/
public class FixedAspectImageView extends ImageView {
private static final String TAG = FixedAspectImageView.class.getSimpleName();
private static final int[] MIN_ATTRS = {
android.R.attr.minWidth,
android.R.attr.minHeight
};
private static final int[] ATTRS = {
android.R.attr.adjustViewBounds,
android.R.attr.maxWidth,
android.R.attr.maxHeight
};
private int mMinWidth = 0;
private int mMinHeight = 0;
private int mMaxWidth = Integer.MAX_VALUE;
private int mMaxHeight = Integer.MAX_VALUE;
private boolean mAdjustViewBounds = false;
private float mAspect = -1f;
public FixedAspectImageView(Context context) {
super(context);
}
public FixedAspectImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public FixedAspectImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
TypedArray a;
// Make sure we get value from xml
a = context.obtainStyledAttributes(attrs, MIN_ATTRS, defStyle, 0);
setMinimumWidth(a.getDimensionPixelSize(0, 0));
setMinimumHeight(a.getDimensionPixelSize(1, 0));
a.recycle();
a = context.obtainStyledAttributes(attrs, ATTRS, defStyle, 0);
setAdjustViewBounds(a.getBoolean(0, false));
setMaxWidth(a.getDimensionPixelSize(1, Integer.MAX_VALUE));
setMaxHeight(a.getDimensionPixelSize(2, Integer.MAX_VALUE));
a.recycle();
a = context.obtainStyledAttributes(
attrs, R.styleable.FixedAspectImageView, defStyle, 0);
setAspect(a.getFloat(R.styleable.FixedAspectImageView_aspect, -1f));
a.recycle();
}
@Override
public void setMinimumWidth(int minWidth) {
super.setMinimumWidth(minWidth);
mMinWidth = minWidth;
}
@Override
public void setMinimumHeight(int minHeight) {
super.setMinimumHeight(minHeight);
mMinHeight = minHeight;
}
@Override
public void setMaxWidth(int maxWidth) {
super.setMaxWidth(maxWidth);
mMaxWidth = maxWidth;
}
@Override
public void setMaxHeight(int maxHeight) {
super.setMaxHeight(maxHeight);
mMaxHeight = maxHeight;
}
@Override
public void setAdjustViewBounds(boolean adjustViewBounds) {
super.setAdjustViewBounds(adjustViewBounds);
mAdjustViewBounds = adjustViewBounds;
}
/**
* Enable aspect will set AdjustViewBounds true.
* Any negative float to disable it,
* disable Aspect will not disable AdjustViewBounds.
*
* @param ratio width/height
*/
public void setAspect(float ratio) {
if (ratio > 0) {
mAspect = ratio;
setAdjustViewBounds(true);
} else {
mAspect = -1f;
}
requestLayout();
}
private int resolveAdjustedSize(int desiredSize, int minSize, int maxSize,
int measureSpec) {
int result = desiredSize;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
// Parent says we can be as big as we want. Just don't be smaller
// than min size, and don't be larger than max size.
result = MathUtils.clamp(desiredSize, minSize, maxSize);
break;
case MeasureSpec.AT_MOST:
// Parent says we can be as big as we want, up to specSize.
// Don't be larger than specSize, and don't be smaller
// than min size, and don't be larger than max size.
result = Math.min(MathUtils.clamp(desiredSize, minSize, maxSize), specSize);
break;
case MeasureSpec.EXACTLY:
// No choice. Do what we are told.
result = specSize;
break;
}
return result;
}
private boolean isSizeAcceptable(int size, int minSize, int maxSize, int measureSpec) {
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
// Parent says we can be as big as we want. Just don't be smaller
// than min size, and don't be larger than max size.
return size >= minSize && size <= maxSize;
case MeasureSpec.AT_MOST:
// Parent says we can be as big as we want, up to specSize.
// Don't be larger than specSize, and don't be smaller
// than min size, and don't be larger than max size.
return size <= specSize && size >= minSize && size <= maxSize;
case MeasureSpec.EXACTLY:
// No choice.
return size == specSize;
default:
// WTF? Return true to make you happy. (´・ω・`)
return true;
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int w;
int h;
// Desired aspect ratio of the view's contents (not including padding)
float desiredAspect = 0.0f;
// We are allowed to change the view's width
boolean resizeWidth = false;
// We are allowed to change the view's height
boolean resizeHeight = false;
final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
Drawable drawable = getDrawable();
if (drawable == null) {
// If no drawable, its intrinsic size is 0.
w = h = 0;
if (mAdjustViewBounds && mAspect > 0.0f) {
resizeWidth = widthSpecMode != MeasureSpec.EXACTLY;
resizeHeight = heightSpecMode != MeasureSpec.EXACTLY;
desiredAspect = mAspect;
}
} else {
w = drawable.getIntrinsicWidth();
h = drawable.getIntrinsicHeight();
if (w <= 0) w = 1;
if (h <= 0) h = 1;
// We are supposed to adjust view bounds to match the aspect
// ratio of our drawable. See if that is possible.
if (mAdjustViewBounds) {
resizeWidth = widthSpecMode != MeasureSpec.EXACTLY;
resizeHeight = heightSpecMode != MeasureSpec.EXACTLY;
desiredAspect = mAspect <= 0.0f ? ((float) w / (float) h) : mAspect;
}
}
int pleft = getPaddingLeft();
int pright = getPaddingRight();
int ptop = getPaddingTop();
int pbottom = getPaddingBottom();
int widthSize;
int heightSize;
if (resizeWidth || resizeHeight) {
// If we get here, it means we want to resize to match the
// drawables aspect ratio, and we have the freedom to change at
// least one dimension.
// Get the max possible width given our constraints
widthSize = resolveAdjustedSize(w + pleft + pright, mMinWidth, mMaxWidth, widthMeasureSpec);
// Get the max possible height given our constraints
heightSize = resolveAdjustedSize(h + ptop + pbottom, mMinHeight, mMaxHeight, heightMeasureSpec);
if (desiredAspect != 0.0f) {
// See what our actual aspect ratio is
float actualAspect = (float)(widthSize - pleft - pright) /
(heightSize - ptop - pbottom);
if (Math.abs(actualAspect - desiredAspect) > 0.0000001) {
boolean done = false;
// Try adjusting width to be proportional to height
if (resizeWidth) {
int newWidth = (int)(desiredAspect * (heightSize - ptop - pbottom)) +
pleft + pright;
// Allow the width to outgrow its original estimate if height is fixed.
//if (!resizeHeight) {
//widthSize = resolveAdjustedSize(newWidth, mMinWidth, mMaxWidth, widthMeasureSpec);
//}
if (isSizeAcceptable(newWidth, mMinWidth, mMaxWidth, widthMeasureSpec)) {
widthSize = newWidth;
done = true;
}
}
// Try adjusting height to be proportional to width
if (!done && resizeHeight) {
int newHeight = (int)((widthSize - pleft - pright) / desiredAspect) +
ptop + pbottom;
// Allow the height to outgrow its original estimate if width is fixed.
if (!resizeWidth) {
heightSize = resolveAdjustedSize(newHeight, mMinHeight, mMaxHeight,
heightMeasureSpec);
}
if (isSizeAcceptable(newHeight, mMinHeight, mMaxHeight, heightMeasureSpec)) {
heightSize = newHeight;
}
}
}
}
} else {
// We are either don't want to preserve the drawables aspect ratio,
// or we are not allowed to change view dimensions. Just measure in
// the normal way.
w += pleft + pright;
h += ptop + pbottom;
w = Math.max(w, getSuggestedMinimumWidth());
h = Math.max(h, getSuggestedMinimumHeight());
widthSize = View.resolveSizeAndState(w, widthMeasureSpec, 0);
heightSize = View.resolveSizeAndState(h, heightMeasureSpec, 0);
}
setMeasuredDimension(widthSize, heightSize);
}
}
@@ -0,0 +1,336 @@
/*
* Copyright 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.TransitionDrawable;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.view.View;
import com.hippo.conaco.Conaco;
import com.hippo.conaco.ConacoTask;
import com.hippo.conaco.DataContainer;
import com.hippo.conaco.Unikery;
import com.hippo.conaco.ValueHolder;
import com.hippo.drawable.ImageDrawable;
import com.hippo.drawable.ImageWrapper;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.R;
public class LoadImageView extends FixedAspectImageView implements Unikery<ImageWrapper>,
View.OnClickListener, View.OnLongClickListener {
private int mTaskId = Unikery.INVAILD_ID;
private Conaco<ImageWrapper> mConaco;
private String mKey;
private String mUrl;
private DataContainer mContainer;
private ValueHolder<ImageWrapper> mHolder;
private boolean mFailed;
private RetryType mRetryType = RetryType.NONE;
private static final RetryType[] sRetryTypeArray = {
RetryType.NONE,
RetryType.CLICK,
RetryType.LONG_CLICK
};
public enum RetryType {
NONE (0),
CLICK (1),
LONG_CLICK (2);
RetryType(int ni) {
nativeInt = ni;
}
final int nativeInt;
}
public LoadImageView(Context context) {
super(context);
}
public LoadImageView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public LoadImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LoadImageView);
final int index = a.getInt(R.styleable.LoadImageView_retryType, -1);
if (index >= 0) {
setRetryType(sRetryTypeArray[index]);
}
a.recycle();
mConaco = EhApplication.getConaco(context);
}
public void setRetryType(RetryType retryType) {
if (mRetryType != retryType) {
RetryType oldRetryType = mRetryType;
mRetryType = retryType;
if (mFailed) {
if (oldRetryType == RetryType.CLICK) {
setOnClickListener(null);
setClickable(false);
} else if (oldRetryType == RetryType.LONG_CLICK) {
setOnLongClickListener(null);
setLongClickable(false);
}
if (retryType == RetryType.CLICK) {
setOnClickListener(this);
} else if (retryType == RetryType.LONG_CLICK) {
setOnLongClickListener(this);
}
}
}
}
private void cancelRetryType() {
if (mRetryType == RetryType.CLICK) {
setOnClickListener(null);
setClickable(false);
} else if (mRetryType == RetryType.LONG_CLICK) {
setOnLongClickListener(null);
setLongClickable(false);
}
}
public void load(String key, String url, boolean useNetwork) {
load(key, url, null, useNetwork);
}
public void load(String key, String url, DataContainer container, boolean useNetwork) {
mFailed = false;
cancelRetryType();
if (url == null) {
return;
}
if (key == null && container == null) {
return;
}
mKey = key;
mUrl = url;
mContainer = container;
ConacoTask.Builder<ImageWrapper> builder = new ConacoTask.Builder<ImageWrapper>()
.setUnikery(this)
.setKey(key)
.setUrl(url)
.setDataContainer(container)
.setUseNetwork(useNetwork);
mConaco.load(builder);
}
public void unload() {
mConaco.cancel(this);
mKey = null;
mUrl = null;
mContainer = null;
removeDrawableAndHolder();
}
public void cancel() {
mConaco.cancel(this);
}
@Override
public void setTaskId(int id) {
mTaskId = id;
}
@Override
public int getTaskId() {
return mTaskId;
}
private void removeDrawableAndHolder() {
// Remove drawable
Drawable drawable = getDrawable();
if (drawable instanceof TransitionDrawable) {
TransitionDrawable tDrawable = (TransitionDrawable) drawable;
int number = tDrawable.getNumberOfLayers();
if (number > 0) {
drawable = tDrawable.getDrawable(number - 1);
}
}
if (drawable instanceof ImageDrawable) {
((ImageDrawable) drawable).recycle();
}
setImageDrawable(null);
// Remove holder
if (mHolder != null) {
mHolder.release(this);
ImageWrapper imageWrapper = mHolder.getValue();
if (mHolder.isFree()) {
// ImageWrapper is free, stop animate
imageWrapper.stop();
if (!mHolder.isInMemoryCache()) {
// ImageWrapper is not needed any more, recycle it
imageWrapper.recycle();
}
}
mHolder = null;
}
}
public void start() {
Drawable drawable = getDrawable();
if (drawable instanceof TransitionDrawable) {
TransitionDrawable tDrawable = (TransitionDrawable) drawable;
int number = tDrawable.getNumberOfLayers();
if (number > 0) {
drawable = tDrawable.getDrawable(number - 1);
}
}
if (drawable instanceof ImageDrawable) {
((ImageDrawable) drawable).start();
}
}
public void stop() {
Drawable drawable = getDrawable();
if (drawable instanceof TransitionDrawable) {
TransitionDrawable tDrawable = (TransitionDrawable) drawable;
int number = tDrawable.getNumberOfLayers();
if (number > 0) {
drawable = tDrawable.getDrawable(number - 1);
}
}
if (drawable instanceof ImageDrawable) {
((ImageDrawable) drawable).stop();
}
}
@Override
public void onMiss(Conaco.Source source) {
if (source == Conaco.Source.MEMORY) {
removeDrawableAndHolder();
}
}
@Override
public void onRequest() {
}
@Override
public void onProgress(long singleReceivedSize, long receivedSize, long totalSize) {
}
@Override
public boolean onGetObject(@NonNull ValueHolder<ImageWrapper> holder, Conaco.Source source) {
// Release
mKey = null;
mUrl = null;
mContainer = null;
holder.obtain(this);
removeDrawableAndHolder();
mHolder = holder;
ImageWrapper imageWrapper = holder.getValue();
Drawable drawable = new ImageDrawable(imageWrapper);
imageWrapper.start();
if ((source == Conaco.Source.DISK || source == Conaco.Source.NETWORK) &&
imageWrapper.getFrameCount() <= 1) {
Drawable[] layers = new Drawable[2];
layers[0] = new ColorDrawable(Color.TRANSPARENT);
layers[1] = drawable;
TransitionDrawable transitionDrawable = new TransitionDrawable(layers);
setImageDrawable(transitionDrawable);
transitionDrawable.startTransition(300);
} else {
setImageDrawable(drawable);
}
return true;
}
@Override
public void onSetDrawable(Drawable drawable) {
removeDrawableAndHolder();
setImageDrawable(drawable);
}
@Override
public void onFailure() {
mFailed = true;
removeDrawableAndHolder();
setImageDrawable(getContext().getResources().getDrawable(R.drawable.image_failed));
if (mRetryType == RetryType.CLICK) {
setOnClickListener(this);
} else if (mRetryType == RetryType.LONG_CLICK) {
setOnLongClickListener(this);
} else {
// Can't retry, so release
mKey = null;
mUrl = null;
mContainer = null;
}
}
@Override
public void onCancel() {
mFailed = false;
cancelRetryType();
// release
mKey = null;
mUrl = null;
mContainer = null;
}
@Override
public void onClick(@NonNull View v) {
load(mKey, mUrl, mContainer, true);
}
@Override
public boolean onLongClick(@NonNull View v) {
load(mKey, mUrl, mContainer, true);
return true;
}
}
@@ -0,0 +1,154 @@
/*
* Copyright (C) 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.widget;
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
public class OffsetLayout extends FrameLayout {
public OffsetLayout(Context context) {
super(context);
}
public OffsetLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public OffsetLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutChildrenEx(left, top, right, bottom, false /* no force left gravity */);
}
// TODO foreground not work fine
@SuppressLint("RtlHardcoded")
void layoutChildrenEx(int left, int top, int right, int bottom,
boolean forceLeftGravity) {
final int count = getChildCount();
final int parentLeft = getPaddingLeft();
final int parentRight = right - left - getPaddingRight();
final int parentTop = getPaddingTop();
final int parentBottom = bottom - top - getPaddingBottom();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
int childLeft;
int childTop;
int gravity = lp.gravity;
if (gravity == -1) {
gravity = Gravity.TOP | Gravity.LEFT;
}
final int absoluteGravity = gravity;
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
if (!forceLeftGravity) {
childLeft = parentRight - width - lp.rightMargin;
break;
}
case Gravity.LEFT:
default:
childLeft = parentLeft + lp.leftMargin;
}
switch (verticalGravity) {
case Gravity.TOP:
childTop = parentTop + lp.topMargin;
break;
case Gravity.CENTER_VERTICAL:
childTop = parentTop + (parentBottom - parentTop - height) / 2 +
lp.topMargin - lp.bottomMargin;
break;
case Gravity.BOTTOM:
childTop = parentBottom - height - lp.bottomMargin;
break;
default:
childTop = parentTop + lp.topMargin;
}
child.layout(childLeft + lp.offsetX, childTop + lp.offsetY,
childLeft + width + lp.offsetX, childTop + height + lp.offsetY);
}
}
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p);
}
public static class LayoutParams extends FrameLayout.LayoutParams {
public int offsetX;
public int offsetY;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
public LayoutParams(MarginLayoutParams source) {
super(source);
}
public LayoutParams(FrameLayout.LayoutParams source) {
super((ViewGroup.MarginLayoutParams) source);
this.gravity = source.gravity;
}
public LayoutParams(int width, int height) {
super(width, height);
}
}
}
@@ -0,0 +1,153 @@
/*
* Copyright 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.widget.refreshlayout;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RadialGradient;
import android.graphics.Shader;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.OvalShape;
import android.support.v4.view.ViewCompat;
import android.view.animation.Animation;
import android.widget.ImageView;
/**
* Private class created to work around issues with AnimationListeners being
* called before the animation is actually complete and support shadows on older
* platforms.
*
* @hide
*/
class CircleImageView extends ImageView {
private static final int KEY_SHADOW_COLOR = 0x1E000000;
private static final int FILL_SHADOW_COLOR = 0x3D000000;
// PX
private static final float X_OFFSET = 0f;
private static final float Y_OFFSET = 1.75f;
private static final float SHADOW_RADIUS = 3.5f;
private static final int SHADOW_ELEVATION = 4;
private Animation.AnimationListener mListener;
private int mShadowRadius;
public CircleImageView(Context context, int color, final float radius) {
super(context);
final float density = getContext().getResources().getDisplayMetrics().density;
final int diameter = (int) (radius * density * 2);
final int shadowYOffset = (int) (density * Y_OFFSET);
final int shadowXOffset = (int) (density * X_OFFSET);
mShadowRadius = (int) (density * SHADOW_RADIUS);
ShapeDrawable circle;
if (elevationSupported()) {
circle = new ShapeDrawable(new OvalShape());
ViewCompat.setElevation(this, SHADOW_ELEVATION * density);
} else {
OvalShape oval = new OvalShadow(mShadowRadius, diameter);
circle = new ShapeDrawable(oval);
ViewCompat.setLayerType(this, ViewCompat.LAYER_TYPE_SOFTWARE, circle.getPaint());
circle.getPaint().setShadowLayer(mShadowRadius, shadowXOffset, shadowYOffset,
KEY_SHADOW_COLOR);
final int padding = mShadowRadius;
// set padding so the inner image sits correctly within the shadow.
setPadding(padding, padding, padding, padding);
}
circle.getPaint().setColor(color);
setBackgroundDrawable(circle);
}
private boolean elevationSupported() {
return android.os.Build.VERSION.SDK_INT >= 21;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (!elevationSupported()) {
setMeasuredDimension(getMeasuredWidth() + mShadowRadius*2, getMeasuredHeight()
+ mShadowRadius*2);
}
}
public void setAnimationListener(Animation.AnimationListener listener) {
mListener = listener;
}
@Override
public void onAnimationStart() {
super.onAnimationStart();
if (mListener != null) {
mListener.onAnimationStart(getAnimation());
}
}
@Override
public void onAnimationEnd() {
super.onAnimationEnd();
if (mListener != null) {
mListener.onAnimationEnd(getAnimation());
}
}
/**
* Update the background color of the circle image view.
*
* @param colorRes Id of a color resource.
*/
public void setBackgroundColorRes(int colorRes) {
setBackgroundColor(getContext().getResources().getColor(colorRes));
}
@Override
public void setBackgroundColor(int color) {
if (getBackground() instanceof ShapeDrawable) {
((ShapeDrawable) getBackground()).getPaint().setColor(color);
}
}
private class OvalShadow extends OvalShape {
private RadialGradient mRadialGradient;
private Paint mShadowPaint;
private int mCircleDiameter;
public OvalShadow(int shadowRadius, int circleDiameter) {
super();
mShadowPaint = new Paint();
mShadowRadius = shadowRadius;
mCircleDiameter = circleDiameter;
mRadialGradient = new RadialGradient(mCircleDiameter / 2, mCircleDiameter / 2,
mShadowRadius, new int[] {
FILL_SHADOW_COLOR, Color.TRANSPARENT
}, null, Shader.TileMode.CLAMP);
mShadowPaint.setShader(mRadialGradient);
}
@Override
public void draw(Canvas canvas, Paint paint) {
final int viewWidth = CircleImageView.this.getWidth();
final int viewHeight = CircleImageView.this.getHeight();
canvas.drawCircle(viewWidth / 2, viewHeight / 2, (mCircleDiameter / 2 + mShadowRadius),
mShadowPaint);
canvas.drawCircle(viewWidth / 2, viewHeight / 2, (mCircleDiameter / 2), paint);
}
}
}
@@ -0,0 +1,791 @@
/*
* Copyright 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.widget.refreshlayout;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
import android.view.animation.Transformation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
/**
* Fancy progress indicator for Material theme.
*
* @hide
*/
class MaterialProgressDrawable extends Drawable implements Animatable {
private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
private static final Interpolator MATERIAL_INTERPOLATOR = new FastOutSlowInInterpolator();
private static final float FULL_ROTATION = 1080.0f;
@Retention(RetentionPolicy.CLASS)
@IntDef({LARGE, DEFAULT})
public @interface ProgressDrawableSize {}
// Maps to ProgressBar.Large style
static final int LARGE = 0;
// Maps to ProgressBar default style
static final int DEFAULT = 1;
// Maps to ProgressBar default style
private static final int CIRCLE_DIAMETER = 40;
private static final float CENTER_RADIUS = 8.75f; //should add up to 10 when + stroke_width
private static final float STROKE_WIDTH = 2.5f;
// Maps to ProgressBar.Large style
private static final int CIRCLE_DIAMETER_LARGE = 56;
private static final float CENTER_RADIUS_LARGE = 12.5f;
private static final float STROKE_WIDTH_LARGE = 3f;
private final int[] COLORS = new int[] {
Color.BLACK
};
/**
* The value in the linear interpolator for animating the drawable at which
* the color transition should start
*/
private static final float COLOR_START_DELAY_OFFSET = 0.75f;
private static final float END_TRIM_START_DELAY_OFFSET = 0.5f;
private static final float START_TRIM_DURATION_OFFSET = 0.5f;
/** The duration of a single progress spin in milliseconds. */
private static final int ANIMATION_DURATION = 1332;
/** The number of points in the progress "star". */
private static final float NUM_POINTS = 5f;
/** The list of animators operating on this drawable. */
private final ArrayList<Animation> mAnimators = new ArrayList<Animation>();
/** The indicator ring, used to manage animation state. */
private final Ring mRing;
/** Canvas rotation in degrees. */
private float mRotation;
/** Layout info for the arrowhead in dp */
private static final int ARROW_WIDTH = 10;
private static final int ARROW_HEIGHT = 5;
private static final float ARROW_OFFSET_ANGLE = 5;
/** Layout info for the arrowhead for the large spinner in dp */
private static final int ARROW_WIDTH_LARGE = 12;
private static final int ARROW_HEIGHT_LARGE = 6;
private static final float MAX_PROGRESS_ARC = .8f;
private Resources mResources;
private View mParent;
private Animation mAnimation;
private float mRotationCount;
private double mWidth;
private double mHeight;
boolean mFinishing;
public MaterialProgressDrawable(Context context, View parent) {
mParent = parent;
mResources = context.getResources();
mRing = new Ring(mCallback);
mRing.setColors(COLORS);
updateSizes(DEFAULT);
setupAnimators();
}
private void setSizeParameters(double progressCircleWidth, double progressCircleHeight,
double centerRadius, double strokeWidth, float arrowWidth, float arrowHeight) {
final Ring ring = mRing;
final DisplayMetrics metrics = mResources.getDisplayMetrics();
final float screenDensity = metrics.density;
mWidth = progressCircleWidth * screenDensity;
mHeight = progressCircleHeight * screenDensity;
ring.setStrokeWidth((float) strokeWidth * screenDensity);
ring.setCenterRadius(centerRadius * screenDensity);
ring.setColorIndex(0);
ring.setArrowDimensions(arrowWidth * screenDensity, arrowHeight * screenDensity);
ring.setInsets((int) mWidth, (int) mHeight);
}
/**
* Set the overall size for the progress spinner. This updates the radius
* and stroke width of the ring.
*
* @param size One of {@link #LARGE} or
* {@link #DEFAULT}
*/
public void updateSizes(@ProgressDrawableSize int size) {
if (size == LARGE) {
setSizeParameters(CIRCLE_DIAMETER_LARGE, CIRCLE_DIAMETER_LARGE, CENTER_RADIUS_LARGE,
STROKE_WIDTH_LARGE, ARROW_WIDTH_LARGE, ARROW_HEIGHT_LARGE);
} else {
setSizeParameters(CIRCLE_DIAMETER, CIRCLE_DIAMETER, CENTER_RADIUS, STROKE_WIDTH,
ARROW_WIDTH, ARROW_HEIGHT);
}
}
/**
* @param show Set to true to display the arrowhead on the progress spinner.
*/
public void showArrow(boolean show) {
mRing.setShowArrow(show);
}
/**
* @param scale Set the scale of the arrowhead for the spinner.
*/
public void setArrowScale(float scale) {
mRing.setArrowScale(scale);
}
/**
* Set the start and end trim for the progress spinner arc.
*
* @param startAngle start angle
* @param endAngle end angle
*/
public void setStartEndTrim(float startAngle, float endAngle) {
mRing.setStartTrim(startAngle);
mRing.setEndTrim(endAngle);
}
/**
* Set the amount of rotation to apply to the progress spinner.
*
* @param rotation Rotation is from [0..1]
*/
public void setProgressRotation(float rotation) {
mRing.setRotation(rotation);
}
/**
* Update the background color of the circle image view.
*/
public void setBackgroundColor(int color) {
mRing.setBackgroundColor(color);
}
/**
* Set the colors used in the progress animation from color resources.
* The first color will also be the color of the bar that grows in response
* to a user swipe gesture.
*
* @param colors
*/
public void setColorSchemeColors(int... colors) {
mRing.setColors(colors);
mRing.setColorIndex(0);
}
@Override
public int getIntrinsicHeight() {
return (int) mHeight;
}
@Override
public int getIntrinsicWidth() {
return (int) mWidth;
}
@Override
public void draw(Canvas c) {
final Rect bounds = getBounds();
final int saveCount = c.save();
c.rotate(mRotation, bounds.exactCenterX(), bounds.exactCenterY());
mRing.draw(c, bounds);
c.restoreToCount(saveCount);
}
@Override
public void setAlpha(int alpha) {
mRing.setAlpha(alpha);
}
public int getAlpha() {
return mRing.getAlpha();
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
mRing.setColorFilter(colorFilter);
}
@SuppressWarnings("unused")
void setRotation(float rotation) {
mRotation = rotation;
invalidateSelf();
}
@SuppressWarnings("unused")
private float getRotation() {
return mRotation;
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
@Override
public boolean isRunning() {
final ArrayList<Animation> animators = mAnimators;
final int N = animators.size();
for (int i = 0; i < N; i++) {
final Animation animator = animators.get(i);
if (animator.hasStarted() && !animator.hasEnded()) {
return true;
}
}
return false;
}
@Override
public void start() {
mAnimation.reset();
mRing.storeOriginals();
// Already showing some part of the ring
if (mRing.getEndTrim() != mRing.getStartTrim()) {
mFinishing = true;
mAnimation.setDuration(ANIMATION_DURATION/2);
mParent.startAnimation(mAnimation);
} else {
mRing.setColorIndex(0);
mRing.resetOriginals();
mAnimation.setDuration(ANIMATION_DURATION);
mParent.startAnimation(mAnimation);
}
}
@Override
public void stop() {
mParent.clearAnimation();
setRotation(0);
mRing.setShowArrow(false);
mRing.setColorIndex(0);
mRing.resetOriginals();
}
private float getMinProgressArc(Ring ring) {
return (float) Math.toRadians(
ring.getStrokeWidth() / (2 * Math.PI * ring.getCenterRadius()));
}
// Adapted from ArgbEvaluator.java
private int evaluateColorChange(float fraction, int startValue, int endValue) {
int startInt = (Integer) startValue;
int startA = (startInt >> 24) & 0xff;
int startR = (startInt >> 16) & 0xff;
int startG = (startInt >> 8) & 0xff;
int startB = startInt & 0xff;
int endInt = (Integer) endValue;
int endA = (endInt >> 24) & 0xff;
int endR = (endInt >> 16) & 0xff;
int endG = (endInt >> 8) & 0xff;
int endB = endInt & 0xff;
return (int)((startA + (int)(fraction * (endA - startA))) << 24) |
(int)((startR + (int)(fraction * (endR - startR))) << 16) |
(int)((startG + (int)(fraction * (endG - startG))) << 8) |
(int)((startB + (int)(fraction * (endB - startB))));
}
/**
* Update the ring color if this is within the last 25% of the animation.
* The new ring color will be a translation from the starting ring color to
* the next color.
*/
private void updateRingColor(float interpolatedTime, Ring ring) {
if (interpolatedTime > COLOR_START_DELAY_OFFSET) {
// scale the interpolatedTime so that the full
// transformation from 0 - 1 takes place in the
// remaining time
ring.setColor(evaluateColorChange((interpolatedTime - COLOR_START_DELAY_OFFSET)
/ (1.0f - COLOR_START_DELAY_OFFSET), ring.getStartingColor(),
ring.getNextColor()));
}
}
private void applyFinishTranslation(float interpolatedTime, Ring ring) {
// shrink back down and complete a full rotation before
// starting other circles
// Rotation goes between [0..1].
updateRingColor(interpolatedTime, ring);
float targetRotation = (float) (Math.floor(ring.getStartingRotation() / MAX_PROGRESS_ARC)
+ 1f);
final float minProgressArc = getMinProgressArc(ring);
final float startTrim = ring.getStartingStartTrim()
+ (ring.getStartingEndTrim() - minProgressArc - ring.getStartingStartTrim())
* interpolatedTime;
ring.setStartTrim(startTrim);
ring.setEndTrim(ring.getStartingEndTrim());
final float rotation = ring.getStartingRotation()
+ ((targetRotation - ring.getStartingRotation()) * interpolatedTime);
ring.setRotation(rotation);
}
private void setupAnimators() {
final Ring ring = mRing;
final Animation animation = new Animation() {
@Override
public void applyTransformation(float interpolatedTime, Transformation t) {
if (mFinishing) {
applyFinishTranslation(interpolatedTime, ring);
} else {
// The minProgressArc is calculated from 0 to create an
// angle that matches the stroke width.
final float minProgressArc = getMinProgressArc(ring);
final float startingEndTrim = ring.getStartingEndTrim();
final float startingTrim = ring.getStartingStartTrim();
final float startingRotation = ring.getStartingRotation();
updateRingColor(interpolatedTime, ring);
// Moving the start trim only occurs in the first 50% of a
// single ring animation
if (interpolatedTime <= START_TRIM_DURATION_OFFSET) {
// scale the interpolatedTime so that the full
// transformation from 0 - 1 takes place in the
// remaining time
final float scaledTime = (interpolatedTime)
/ (1.0f - START_TRIM_DURATION_OFFSET);
final float startTrim = startingTrim
+ ((MAX_PROGRESS_ARC - minProgressArc) * MATERIAL_INTERPOLATOR
.getInterpolation(scaledTime));
ring.setStartTrim(startTrim);
}
// Moving the end trim starts after 50% of a single ring
// animation completes
if (interpolatedTime > END_TRIM_START_DELAY_OFFSET) {
// scale the interpolatedTime so that the full
// transformation from 0 - 1 takes place in the
// remaining time
final float minArc = MAX_PROGRESS_ARC - minProgressArc;
float scaledTime = (interpolatedTime - START_TRIM_DURATION_OFFSET)
/ (1.0f - START_TRIM_DURATION_OFFSET);
final float endTrim = startingEndTrim
+ (minArc * MATERIAL_INTERPOLATOR.getInterpolation(scaledTime));
ring.setEndTrim(endTrim);
}
final float rotation = startingRotation + (0.25f * interpolatedTime);
ring.setRotation(rotation);
float groupRotation = ((FULL_ROTATION / NUM_POINTS) * interpolatedTime)
+ (FULL_ROTATION * (mRotationCount / NUM_POINTS));
setRotation(groupRotation);
}
}
};
animation.setRepeatCount(Animation.INFINITE);
animation.setRepeatMode(Animation.RESTART);
animation.setInterpolator(LINEAR_INTERPOLATOR);
animation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
mRotationCount = 0;
}
@Override
public void onAnimationEnd(Animation animation) {
// do nothing
}
@Override
public void onAnimationRepeat(Animation animation) {
ring.storeOriginals();
ring.goToNextColor();
ring.setStartTrim(ring.getEndTrim());
if (mFinishing) {
// finished closing the last ring from the swipe gesture; go
// into progress mode
mFinishing = false;
animation.setDuration(ANIMATION_DURATION);
ring.setShowArrow(false);
} else {
mRotationCount = (mRotationCount + 1) % (NUM_POINTS);
}
}
});
mAnimation = animation;
}
private final Callback mCallback = new Callback() {
@Override
public void invalidateDrawable(Drawable d) {
invalidateSelf();
}
@Override
public void scheduleDrawable(Drawable d, Runnable what, long when) {
scheduleSelf(what, when);
}
@Override
public void unscheduleDrawable(Drawable d, Runnable what) {
unscheduleSelf(what);
}
};
private static class Ring {
private final RectF mTempBounds = new RectF();
private final Paint mPaint = new Paint();
private final Paint mArrowPaint = new Paint();
private final Callback mCallback;
private float mStartTrim = 0.0f;
private float mEndTrim = 0.0f;
private float mRotation = 0.0f;
private float mStrokeWidth = 5.0f;
private float mStrokeInset = 2.5f;
private int[] mColors;
// mColorIndex represents the offset into the available mColors that the
// progress circle should currently display. As the progress circle is
// animating, the mColorIndex moves by one to the next available color.
private int mColorIndex;
private float mStartingStartTrim;
private float mStartingEndTrim;
private float mStartingRotation;
private boolean mShowArrow;
private Path mArrow;
private float mArrowScale;
private double mRingCenterRadius;
private int mArrowWidth;
private int mArrowHeight;
private int mAlpha;
private final Paint mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private int mBackgroundColor;
private int mCurrentColor;
public Ring(Callback callback) {
mCallback = callback;
mPaint.setStrokeCap(Paint.Cap.SQUARE);
mPaint.setAntiAlias(true);
mPaint.setStyle(Style.STROKE);
mArrowPaint.setStyle(Style.FILL);
mArrowPaint.setAntiAlias(true);
}
public void setBackgroundColor(int color) {
mBackgroundColor = color;
}
/**
* Set the dimensions of the arrowhead.
*
* @param width Width of the hypotenuse of the arrow head
* @param height Height of the arrow point
*/
public void setArrowDimensions(float width, float height) {
mArrowWidth = (int) width;
mArrowHeight = (int) height;
}
/**
* Draw the progress spinner
*/
public void draw(Canvas c, Rect bounds) {
final RectF arcBounds = mTempBounds;
arcBounds.set(bounds);
arcBounds.inset(mStrokeInset, mStrokeInset);
final float startAngle = (mStartTrim + mRotation) * 360;
final float endAngle = (mEndTrim + mRotation) * 360;
float sweepAngle = endAngle - startAngle;
mPaint.setColor(mCurrentColor);
c.drawArc(arcBounds, startAngle, sweepAngle, false, mPaint);
drawTriangle(c, startAngle, sweepAngle, bounds);
if (mAlpha < 255) {
mCirclePaint.setColor(mBackgroundColor);
mCirclePaint.setAlpha(255 - mAlpha);
c.drawCircle(bounds.exactCenterX(), bounds.exactCenterY(), bounds.width() / 2,
mCirclePaint);
}
}
private void drawTriangle(Canvas c, float startAngle, float sweepAngle, Rect bounds) {
if (mShowArrow) {
if (mArrow == null) {
mArrow = new Path();
mArrow.setFillType(Path.FillType.EVEN_ODD);
} else {
mArrow.reset();
}
// Adjust the position of the triangle so that it is inset as
// much as the arc, but also centered on the arc.
float inset = (int) mStrokeInset / 2 * mArrowScale;
float x = (float) (mRingCenterRadius * Math.cos(0) + bounds.exactCenterX());
float y = (float) (mRingCenterRadius * Math.sin(0) + bounds.exactCenterY());
// Update the path each time. This works around an issue in SKIA
// where concatenating a rotation matrix to a scale matrix
// ignored a starting negative rotation. This appears to have
// been fixed as of API 21.
mArrow.moveTo(0, 0);
mArrow.lineTo(mArrowWidth * mArrowScale, 0);
mArrow.lineTo((mArrowWidth * mArrowScale / 2), (mArrowHeight
* mArrowScale));
mArrow.offset(x - inset, y);
mArrow.close();
// draw a triangle
mArrowPaint.setColor(mCurrentColor);
c.rotate(startAngle + sweepAngle - ARROW_OFFSET_ANGLE, bounds.exactCenterX(),
bounds.exactCenterY());
c.drawPath(mArrow, mArrowPaint);
}
}
/**
* Set the colors the progress spinner alternates between.
*
* @param colors Array of integers describing the colors. Must be non-<code>null</code>.
*/
public void setColors(@NonNull int[] colors) {
mColors = colors;
// if colors are reset, make sure to reset the color index as well
setColorIndex(0);
}
/**
* Set the absolute color of the progress spinner. This is should only
* be used when animating between current and next color when the
* spinner is rotating.
*
* @param color int describing the color.
*/
public void setColor(int color) {
mCurrentColor = color;
}
/**
* @param index Index into the color array of the color to display in
* the progress spinner.
*/
public void setColorIndex(int index) {
mColorIndex = index;
mCurrentColor = mColors[mColorIndex];
}
/**
* @return int describing the next color the progress spinner should use when drawing.
*/
public int getNextColor() {
return mColors[getNextColorIndex()];
}
private int getNextColorIndex() {
return (mColorIndex + 1) % (mColors.length);
}
/**
* Proceed to the next available ring color. This will automatically
* wrap back to the beginning of colors.
*/
public void goToNextColor() {
setColorIndex(getNextColorIndex());
}
public void setColorFilter(ColorFilter filter) {
mPaint.setColorFilter(filter);
invalidateSelf();
}
/**
* @param alpha Set the alpha of the progress spinner and associated arrowhead.
*/
public void setAlpha(int alpha) {
mAlpha = alpha;
}
/**
* @return Current alpha of the progress spinner and arrowhead.
*/
public int getAlpha() {
return mAlpha;
}
/**
* @param strokeWidth Set the stroke width of the progress spinner in pixels.
*/
public void setStrokeWidth(float strokeWidth) {
mStrokeWidth = strokeWidth;
mPaint.setStrokeWidth(strokeWidth);
invalidateSelf();
}
@SuppressWarnings("unused")
public float getStrokeWidth() {
return mStrokeWidth;
}
@SuppressWarnings("unused")
public void setStartTrim(float startTrim) {
mStartTrim = startTrim;
invalidateSelf();
}
@SuppressWarnings("unused")
public float getStartTrim() {
return mStartTrim;
}
public float getStartingStartTrim() {
return mStartingStartTrim;
}
public float getStartingEndTrim() {
return mStartingEndTrim;
}
public int getStartingColor() {
return mColors[mColorIndex];
}
@SuppressWarnings("unused")
public void setEndTrim(float endTrim) {
mEndTrim = endTrim;
invalidateSelf();
}
@SuppressWarnings("unused")
public float getEndTrim() {
return mEndTrim;
}
@SuppressWarnings("unused")
public void setRotation(float rotation) {
mRotation = rotation;
invalidateSelf();
}
@SuppressWarnings("unused")
public float getRotation() {
return mRotation;
}
public void setInsets(int width, int height) {
final float minEdge = (float) Math.min(width, height);
float insets;
if (mRingCenterRadius <= 0 || minEdge < 0) {
insets = (float) Math.ceil(mStrokeWidth / 2.0f);
} else {
insets = (float) (minEdge / 2.0f - mRingCenterRadius);
}
mStrokeInset = insets;
}
@SuppressWarnings("unused")
public float getInsets() {
return mStrokeInset;
}
/**
* @param centerRadius Inner radius in px of the circle the progress
* spinner arc traces.
*/
public void setCenterRadius(double centerRadius) {
mRingCenterRadius = centerRadius;
}
public double getCenterRadius() {
return mRingCenterRadius;
}
/**
* @param show Set to true to show the arrow head on the progress spinner.
*/
public void setShowArrow(boolean show) {
if (mShowArrow != show) {
mShowArrow = show;
invalidateSelf();
}
}
/**
* @param scale Set the scale of the arrowhead for the spinner.
*/
public void setArrowScale(float scale) {
if (scale != mArrowScale) {
mArrowScale = scale;
invalidateSelf();
}
}
/**
* @return The amount the progress spinner is currently rotated, between [0..1].
*/
public float getStartingRotation() {
return mStartingRotation;
}
/**
* If the start / end trim are offset to begin with, store them so that
* animation starts from that offset.
*/
public void storeOriginals() {
mStartingStartTrim = mStartTrim;
mStartingEndTrim = mEndTrim;
mStartingRotation = mRotation;
}
/**
* Reset the progress spinner to default rotation, start and end angles.
*/
public void resetOriginals() {
mStartingStartTrim = 0;
mStartingEndTrim = 0;
mStartingRotation = 0;
setStartTrim(0);
setEndTrim(0);
setRotation(0);
}
private void invalidateSelf() {
mCallback.invalidateDrawable(null);
}
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,277 @@
/*
* Copyright 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.widget.refreshlayout;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.view.View;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
/**
* Custom progress bar that shows a cycle of colors as widening circles that
* overdraw each other. When finished, the bar is cleared from the inside out as
* the main cycle continues. Before running, this can also indicate how close
* the user is to triggering something (e.g. how far they need to pull down to
* trigger a refresh).
*/
final class SwipeProgressBar {
// Default progress animation colors are grays.
private final static int COLOR1 = 0xB3000000;
private final static int COLOR2 = 0x80000000;
private final static int COLOR3 = 0x4d000000;
private final static int COLOR4 = 0x1a000000;
// The duration of the animation cycle.
private static final int ANIMATION_DURATION_MS = 2000;
// The duration of the animation to clear the bar.
private static final int FINISH_ANIMATION_DURATION_MS = 1000;
// Interpolator for varying the speed of the animation.
private static final Interpolator INTERPOLATOR = new FastOutSlowInInterpolator();
private final Paint mPaint = new Paint();
private final RectF mClipRect = new RectF();
private float mTriggerPercentage;
private long mStartTime;
private long mFinishTime;
private boolean mRunning;
// Colors used when rendering the animation,
private int mColor1;
private int mColor2;
private int mColor3;
private int mColor4;
private View mParent;
private Rect mBounds = new Rect();
public SwipeProgressBar(View parent) {
mParent = parent;
mColor1 = COLOR1;
mColor2 = COLOR2;
mColor3 = COLOR3;
mColor4 = COLOR4;
}
/**
* Set the four colors used in the progress animation. The first color will
* also be the color of the bar that grows in response to a user swipe
* gesture.
*
* @param color1 Integer representation of a color.
* @param color2 Integer representation of a color.
* @param color3 Integer representation of a color.
* @param color4 Integer representation of a color.
*/
void setColorScheme(int color1, int color2, int color3, int color4) {
mColor1 = color1;
mColor2 = color2;
mColor3 = color3;
mColor4 = color4;
}
/**
* Update the progress the user has made toward triggering the swipe
* gesture. and use this value to update the percentage of the trigger that
* is shown.
*/
void setTriggerPercentage(float triggerPercentage) {
mTriggerPercentage = triggerPercentage;
mStartTime = 0;
ViewCompat.postInvalidateOnAnimation(
mParent, mBounds.left, mBounds.top, mBounds.right, mBounds.bottom);
}
/**
* Start showing the progress animation.
*/
void start() {
if (!mRunning) {
mTriggerPercentage = 0;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mRunning = true;
mParent.postInvalidate();
}
}
/**
* Stop showing the progress animation.
*/
void stop() {
if (mRunning) {
mTriggerPercentage = 0;
mFinishTime = AnimationUtils.currentAnimationTimeMillis();
mRunning = false;
mParent.postInvalidate();
}
}
/**
* @return Return whether the progress animation is currently running.
*/
boolean isRunning() {
return mRunning || mFinishTime > 0;
}
void draw(Canvas canvas) {
Rect bounds = mBounds;
final int width = bounds.width();
final int height = bounds.height();
final int cx = bounds.centerX();
final int cy = bounds.centerY();
boolean drawTriggerWhileFinishing = false;
int restoreCount = canvas.save();
canvas.clipRect(bounds);
if (mRunning || (mFinishTime > 0)) {
long now = AnimationUtils.currentAnimationTimeMillis();
long elapsed = (now - mStartTime) % ANIMATION_DURATION_MS;
long iterations = (now - mStartTime) / ANIMATION_DURATION_MS;
float rawProgress = (elapsed / (ANIMATION_DURATION_MS / 100f));
// If we're not running anymore, that means we're running through
// the finish animation.
if (!mRunning) {
// If the finish animation is done, don't draw anything, and
// don't repost.
if ((now - mFinishTime) >= FINISH_ANIMATION_DURATION_MS) {
mFinishTime = 0;
return;
}
// Otherwise, use a 0 opacity alpha layer to clear the animation
// from the inside out. This layer will prevent the circles from
// drawing within its bounds.
long finishElapsed = (now - mFinishTime) % FINISH_ANIMATION_DURATION_MS;
float finishProgress = (finishElapsed / (FINISH_ANIMATION_DURATION_MS / 100f));
float pct = (finishProgress / 100f);
// Radius of the circle is half of the screen.
float clearRadius = width / 2 * INTERPOLATOR.getInterpolation(pct);
mClipRect.set(cx - clearRadius, 0, cx + clearRadius, height);
canvas.saveLayerAlpha(mClipRect, 0, 0);
// Only draw the trigger if there is a space in the center of
// this refreshing view that needs to be filled in by the
// trigger. If the progress view is just still animating, let it
// continue animating.
drawTriggerWhileFinishing = true;
}
// First fill in with the last color that would have finished drawing.
if (iterations == 0) {
canvas.drawColor(mColor1);
} else {
if (rawProgress >= 0 && rawProgress < 25) {
canvas.drawColor(mColor4);
} else if (rawProgress >= 25 && rawProgress < 50) {
canvas.drawColor(mColor1);
} else if (rawProgress >= 50 && rawProgress < 75) {
canvas.drawColor(mColor2);
} else {
canvas.drawColor(mColor3);
}
}
// Then draw up to 4 overlapping concentric circles of varying radii, based on how far
// along we are in the cycle.
// progress 0-50 draw mColor2
// progress 25-75 draw mColor3
// progress 50-100 draw mColor4
// progress 75 (wrap to 25) draw mColor1
if ((rawProgress >= 0 && rawProgress <= 25)) {
float pct = (((rawProgress + 25) * 2) / 100f);
drawCircle(canvas, cx, cy, mColor1, pct);
}
if (rawProgress >= 0 && rawProgress <= 50) {
float pct = ((rawProgress * 2) / 100f);
drawCircle(canvas, cx, cy, mColor2, pct);
}
if (rawProgress >= 25 && rawProgress <= 75) {
float pct = (((rawProgress - 25) * 2) / 100f);
drawCircle(canvas, cx, cy, mColor3, pct);
}
if (rawProgress >= 50 && rawProgress <= 100) {
float pct = (((rawProgress - 50) * 2) / 100f);
drawCircle(canvas, cx, cy, mColor4, pct);
}
if ((rawProgress >= 75 && rawProgress <= 100)) {
float pct = (((rawProgress - 75) * 2) / 100f);
drawCircle(canvas, cx, cy, mColor1, pct);
}
if (mTriggerPercentage > 0 && drawTriggerWhileFinishing) {
// There is some portion of trigger to draw. Restore the canvas,
// then draw the trigger. Otherwise, the trigger does not appear
// until after the bar has finished animating and appears to
// just jump in at a larger width than expected.
canvas.restoreToCount(restoreCount);
restoreCount = canvas.save();
canvas.clipRect(bounds);
drawTrigger(canvas, cx, cy);
}
// Keep running until we finish out the last cycle.
ViewCompat.postInvalidateOnAnimation(
mParent, bounds.left, bounds.top, bounds.right, bounds.bottom);
} else {
// Otherwise if we're in the middle of a trigger, draw that.
if (mTriggerPercentage > 0 && mTriggerPercentage <= 1.0) {
drawTrigger(canvas, cx, cy);
}
}
canvas.restoreToCount(restoreCount);
}
private void drawTrigger(Canvas canvas, int cx, int cy) {
mPaint.setColor(mColor1);
canvas.drawCircle(cx, cy, cx * mTriggerPercentage, mPaint);
}
/**
* Draws a circle centered in the view.
*
* @param canvas the canvas to draw on
* @param cx the center x coordinate
* @param cy the center y coordinate
* @param color the color to draw
* @param pct the percentage of the view that the circle should cover
*/
private void drawCircle(Canvas canvas, float cx, float cy, int color, float pct) {
mPaint.setColor(color);
canvas.save();
canvas.translate(cx, cy);
float radiusScale = INTERPOLATOR.getInterpolation(pct);
canvas.scale(radiusScale, radiusScale);
canvas.drawCircle(0, 0, cx, mPaint);
canvas.restore();
}
/**
* Set the drawing bounds of this SwipeProgressBar.
*/
void setBounds(int left, int top, int right, int bottom) {
mBounds.left = left;
mBounds.top = top;
mBounds.right = right;
mBounds.bottom = bottom;
}
}