Add files
This commit is contained in:
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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user