Adjust cache

This commit is contained in:
seven332 committed 2015-03-13 22:26:09 +08:00
1 parent b407e38733
commit 497438c0a1
24 files changed
+4033 -2156

No files matched your search

@@ -23,12 +23,12 @@ import android.content.pm.PackageManager.NameNotFoundException;
import android.graphics.Typeface;
import android.view.ContextThemeWrapper;
import com.hippo.ehviewer.cache.ImageCache;
import com.hippo.ehviewer.data.Data;
import com.hippo.ehviewer.ehclient.EhClient;
import com.hippo.ehviewer.ehclient.EhInfo;
import com.hippo.ehviewer.ehclient.ExDownloaderManager;
import com.hippo.ehviewer.network.HttpHelper;
import com.hippo.ehviewer.cache.ImageCache;
import com.hippo.ehviewer.util.Config;
import com.hippo.ehviewer.util.Crash;
import com.hippo.ehviewer.util.EhUtils;
@@ -148,7 +148,7 @@ public class AppContext extends Application implements UncaughtExceptionHandler
*/
@Override
public void onLowMemory() {
ImageCache.getInstance(this).evictAll();
ImageCache.getImageCache(this).evictAll();
super.onLowMemory();
}
@@ -19,8 +19,9 @@ package com.hippo.ehviewer;
import android.content.Context;
import android.graphics.Bitmap;
import com.hippo.ehviewer.cache.ImageCache;
import com.hippo.ehviewer.network.HttpHelper;
import com.hippo.ehviewer.cache.AnyCache;
import com.hippo.ehviewer.cache.ImageCache;
import com.hippo.ehviewer.util.BgThread;
import java.util.Stack;
@@ -52,7 +53,7 @@ public class ImageLoader {
private final Context mContext;
private final Stack<LoadTask> mLoadTasks;
private final ImageCache mImageCache;
private final AnyCache<Bitmap> mImageCache;
private final ImageDownloader mImageDownloader;
private ImageLoader(Context context) {
@@ -60,7 +61,7 @@ public class ImageLoader {
mImageDownloader = new ImageDownloader();
mContext = context;
mImageCache = ImageCache.getInstance(mContext);
mImageCache = ImageCache.getImageCache(mContext);
new BgThread(new LoadFromCacheTask()).start();
}
@@ -110,7 +111,7 @@ public class ImageLoader {
int[] state = new int[1];
String key = loadTask.key;
loadTask.bitmap = mImageCache.getCachedBitmap(key, state);
loadTask.bitmap = mImageCache.get(key);
loadTask.state = state[0];
if (loadTask.bitmap != null)
@@ -158,7 +159,7 @@ public class ImageLoader {
// TODO use proxy to get image
loadTask.bitmap = httpHelper.getImage(loadTask.url);
if (loadTask.bitmap != null) {
mImageCache.addBitmapToCache(loadTask.key, loadTask.bitmap);
mImageCache.put(loadTask.key, loadTask.bitmap);
loadTask.state = STATE_FROM_NETWORK;
} else {
loadTask.state = STATE_NONE;
+405
View File
@@ -0,0 +1,405 @@
/*
* 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.cache;
import android.os.Looper;
import android.support.v4.util.LruCache;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class AnyCache<V> {
private LruCache<String, V> mMemoryCache;
private DiskCache<V> mDiskCache;
/**
* Used to temporarily pause the disk cache while scrolling
*/
public boolean mPauseDiskAccess = false;
private final Object mPauseLock = new Object();
private AnyCache(Builder builder) {
if (builder.mMemoryCacheMaxSize > 0 && builder.mMemoryCacheHelper != null) {
mMemoryCache = new MemoryCahce<V>(
builder.mMemoryCacheMaxSize, builder.mMemoryCacheHelper);
}
if (builder.mDiskCacheDir != null && builder.mDiskCacheMaxSize > 0 &&
builder.mDiskCacheHelper != null) {
try {
mDiskCache = new DiskCache<V>(
builder.mDiskCacheDir, builder.mDiskCacheMaxSize, builder.mDiskCacheHelper);
} catch (IOException e) {
// Can't create disk cache
}
}
}
/**
* Check if have memory cache
*
* @return true if have memory cache
*/
public boolean hasMemoryCache() {
return mMemoryCache != null;
}
/**
* Check if have disk cache
*
* @return true if have disk cache
*/
public boolean hasDiskCache() {
return mDiskCache != null;
}
/**
* Get value from memory cache
*
* @param key the key to get value
* @return the value you get, null for miss or no memory cache
*/
public V getFromMemory(String key) {
if (mMemoryCache != null) {
return mMemoryCache.get(key);
} else {
return null;
}
}
/**
* Get value from memory cache
*
* @param key the key to get value
* @return the value you get, null for miss or no memory cache or get error
*/
public V getFromDisk(String key) {
if (mDiskCache != null) {
// Wait for pause
waitUntilUnpaused();
String diskKey = hashKeyForDisk(key);
return mDiskCache.get(diskKey);
} else {
return null;
}
}
/**
* Get value from memory cache and disk cache. If miss in memory cache and
* get in disk cache, it will put value from disk cache to memory cache.
*
* @param key the key to get value
* @return the value you get
*/
public V get(String key) {
V value;
if (mMemoryCache != null) {
value = mMemoryCache.get(key);
if (value != null) {
return value;
}
}
if (mDiskCache != null) {
// Wait for pause
waitUntilUnpaused();
String diskKey = hashKeyForDisk(key);
value = mDiskCache.get(diskKey);
// Put value to memory cach
if (value != null && mMemoryCache != null) {
mMemoryCache.put(key, value);
}
return value;
} else {
return null;
}
}
/**
* Put value to memory cache
*
* @param key the key
* @param value the value
* @return false if no memory cache
*/
public boolean putToMemory(String key, V value) {
if (mMemoryCache != null) {
mMemoryCache.put(key, value);
return true;
} else {
return false;
}
}
/**
* Put value to disk cache
*
* @param key the key
* @param value the value
* @return false if no disk cache or get error
*/
public boolean putToDisk(String key, V value) {
if (mDiskCache != null) {
// Wait for pause
waitUntilUnpaused();
String diskKey = hashKeyForDisk(key);
return mDiskCache.put(diskKey, value);
} else {
return false;
}
}
/**
* Put value to memory cache and disk cache
*
* @param key the key
* @param value the value
*/
public void put(String key, V value) {
if (mMemoryCache != null) {
mMemoryCache.put(key, value);
}
if (mDiskCache != null) {
// Wait for pause
waitUntilUnpaused();
String diskKey = hashKeyForDisk(key);
mDiskCache.put(diskKey, value);
}
}
/**
* Used to temporarily pause the disk cache while the user is scrolling to
* improve scrolling.
*
* @param pause True to temporarily pause the disk cache, false otherwise.
*/
public void setPauseDiskCache(final boolean pause) {
synchronized (mPauseLock) {
if (mPauseDiskAccess != pause) {
mPauseDiskAccess = pause;
if (!pause) {
mPauseLock.notify();
}
}
}
}
private void waitUntilUnpaused() {
synchronized (mPauseLock) {
if (Looper.myLooper() != Looper.getMainLooper()) {
while (mPauseDiskAccess) {
try {
mPauseLock.wait();
} catch (InterruptedException e) {
// ignored, we'll start waiting again
}
}
}
}
}
/**
* Evicts all of the items from the memory cache and lets the system know
* now would be a good time to garbage collect
*/
public void evictAll() {
if (mMemoryCache != null) {
mMemoryCache.evictAll();
}
System.gc();
}
/**
* A hashing method that changes a string (like a URL) into a hash suitable
* for using as a disk filename.
*
* @param key The key used to store the file
*/
private static String hashKeyForDisk(final String key) {
String cacheKey;
try {
final MessageDigest digest = MessageDigest.getInstance("MD5");
digest.update(key.getBytes());
cacheKey = bytesToHexString(digest.digest());
} catch (final NoSuchAlgorithmException e) {
cacheKey = String.valueOf(key.hashCode());
}
return cacheKey;
}
/**
* http://stackoverflow.com/questions/332079
*
* @param bytes The bytes to convert.
* @return A {@link String} converted from the bytes of a hashable key used
* to store a filename on the disk, to hex digits.
*/
private static String bytesToHexString(final byte[] bytes) {
final StringBuilder builder = new StringBuilder();
for (final byte b : bytes) {
final String hex = Integer.toHexString(0xFF & b);
if (hex.length() == 1) {
builder.append('0');
}
builder.append(hex);
}
return builder.toString();
}
public static class Builder {
private int mMemoryCacheMaxSize;
private MemoryCacheHelper mMemoryCacheHelper;
private File mDiskCacheDir;
private int mDiskCacheMaxSize;
private DiskCacheHelper mDiskCacheHelper;
public Builder setMemoryCache(int maxSize, MemoryCacheHelper helper) {
mMemoryCacheMaxSize = maxSize;
mMemoryCacheHelper = helper;
return this;
}
/**
* Set this dir to store disk cache.
*
* @param cacheDir this dir to store disk cache, null for no disk cache
* @return the Builder
*/
public Builder setDiskCache(File cacheDir, int maxSize, DiskCacheHelper helper) {
mDiskCacheDir = cacheDir;
mDiskCacheMaxSize = maxSize;
mDiskCacheHelper = helper;
return this;
}
public AnyCache build() {
return new AnyCache(this);
}
}
public class MemoryCahce<E> extends LruCache<String, E> {
private MemoryCacheHelper<E> mHelper;
public MemoryCahce(int maxSize, MemoryCacheHelper<E> helper) {
super(maxSize);
mHelper = helper;
}
@Override
protected int sizeOf(String key, E value) {
return mHelper.sizeOf(key, value);
}
}
public abstract static class MemoryCacheHelper<E> {
public abstract int sizeOf(String key, E value);
}
public static class DiskCache<E> {
private static final int IO_BUFFER_SIZE = 8 * 1024;
public DiskLruCache mDiskLruCache;
public DiskCacheHelper<E> mHelper;
public DiskCache(File cacheDir, int size, DiskCacheHelper<E> helper) throws IOException {
mDiskLruCache = DiskLruCache.open(cacheDir, 1, 1, size);
mHelper = helper;
}
public E get(String key) {
DiskLruCache.Snapshot snapshot = null;
try {
snapshot = mDiskLruCache.get(key);
if ( snapshot == null ) {
// Miss
return null;
}
final InputStream in = snapshot.getInputStream(0);
if (in != null) {
final BufferedInputStream buffIn =
new BufferedInputStream(in, IO_BUFFER_SIZE);
return mHelper.get(buffIn);
} else {
// Can't get InputStream
return null;
}
} catch (IOException e) {
// e.printStackTrace();
return null;
} finally {
Util.closeQuietly(snapshot);
}
}
public boolean put(String key, E value) {
DiskLruCache.Editor editor = null;
OutputStream os = null;
try {
editor = mDiskLruCache.edit(key);
if (editor == null) {
// The editor is in progress
return false;
}
os = editor.newOutputStream(0);
if (os != null) {
final BufferedOutputStream buffOut =
new BufferedOutputStream(os, IO_BUFFER_SIZE);
boolean result = mHelper.put(buffOut, value);
mDiskLruCache.flush();
editor.commit();
return result;
} else {
// Can't get OutputStream
editor.abort();
return false;
}
} catch (IOException e) {
Util.closeQuietly(os);
try {
if (editor != null) {
editor.abort();
}
} catch (IOException ignored) {
}
return false;
}
}
}
public abstract static class DiskCacheHelper<E> {
public abstract E get(InputStream is);
public abstract boolean put(OutputStream os, E value);
}
}
File diff suppressed because it is too large. Load diff
+55 -654
View File
@@ -1,560 +1,83 @@
/*
* Copyright (C) 2012 Andrew Neal 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.
* 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.cache;
import android.app.ActivityManager;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Build;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.Fragment;
import android.app.FragmentManager;
import android.content.ComponentCallbacks2;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.os.Looper;
import android.support.v4.app.FragmentActivity;
import android.util.Log;
public class ImageCache {
import com.hippo.ehviewer.util.Utils;
/**
* This class holds the memory and disk bitmap caches.
*/
public final class ImageCache {
private static final String TAG = ImageCache.class.getSimpleName();
private static final String TAG = "ImageCache";
/**
* Default memory cache size as a percent of device memory class
*/
private static final float MEM_CACHE_DIVIDER = 0.25f;
/**
* Default disk cache size 10MB
*/
private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10;
/**
* Compression settings when writing images to disk cache
*/
private static final CompressFormat COMPRESS_FORMAT = CompressFormat.JPEG;
/**
* Disk cache index to read from
*/
private static final int DISK_CACHE_INDEX = 0;
private static final Bitmap.CompressFormat COMPRESS_FORMAT = Bitmap.CompressFormat.JPEG;
/**
* Image compression quality
*/
private static final int COMPRESS_QUALITY = 98;
/**
* LRU cache
*/
private MemoryCache mLruCache;
private static AnyCache<Bitmap> sImageCache;
/**
* Disk LRU cache
*/
private DiskLruCache mDiskCache;
public static AnyCache<Bitmap> getImageCache(Context context) {
if (sImageCache == null) {
AnyCache.Builder builder = new AnyCache.Builder();
private static ImageCache sInstance;
final ActivityManager activityManager = (ActivityManager)context
.getSystemService(Context.ACTIVITY_SERVICE);
final int memoryCacheMax = Math.round(MEM_CACHE_DIVIDER * activityManager.getMemoryClass()
* 1024 * 1024);
builder.setMemoryCache(memoryCacheMax, new BitmapMemoryCacheHelper());
/**
* Used to temporarily pause the disk cache while scrolling
*/
public boolean mPauseDiskAccess = false;
private final Object mPauseLock = new Object();
/**
* Constructor of <code>ImageCache</code>
*
* @param context The {@link Context} to use
*/
public ImageCache(final Context context) {
init(context);
}
/**
* Used to create a singleton of {@link ImageCache}
*
* @param context The {@link Context} to use
* @return A new instance of this class.
*/
public final static ImageCache getInstance(final Context context) {
if (sInstance == null) {
sInstance = new ImageCache(context.getApplicationContext());
}
return sInstance;
}
/**
* Initialize the cache, providing all parameters.
*
* @param context The {@link Context} to use
* @param cacheParams The cache parameters to initialize the cache
*/
private void init(final Context context) {
Utils.execute(false, new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(final Void... unused) {
// Initialize the disk cahe in a background thread
initDiskCache(context);
return null;
}
}, (Void[])null);
// Set up the memory cache
initLruCache(context);
}
/**
* Initializes the disk cache. Note that this includes disk access so this
* should not be executed on the main/UI thread. By default an ImageCache
* does not initialize the disk cache when it is created, instead you should
* call initDiskCache() to initialize it on a background thread.
*
* @param context The {@link Context} to use
*/
private synchronized void initDiskCache(final Context context) {
// Set up disk cache
if (mDiskCache == null || mDiskCache.isClosed()) {
File diskCacheDir = getDiskCacheDir(context, TAG);
if (diskCacheDir != null) {
if (!diskCacheDir.exists()) {
diskCacheDir.mkdirs();
}
if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
try {
mDiskCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
} catch (final IOException e) {
diskCacheDir = null;
}
}
builder.setDiskCache(diskCacheDir, 20 * 1024 * 1024, new BitmapDiskCacheHelper());
}
sImageCache = builder.build();
}
return sImageCache;
}
/**
* Sets up the Lru cache
* Get the external app cache directory
*
* @param context The {@link Context} to use
* @return The external cache directory
*/
@SuppressLint("NewApi")
public void initLruCache(final Context context) {
final ActivityManager activityManager = (ActivityManager)context
.getSystemService(Context.ACTIVITY_SERVICE);
final int lruCacheSize = Math.round(MEM_CACHE_DIVIDER * activityManager.getMemoryClass()
* 1024 * 1024);
mLruCache = new MemoryCache(lruCacheSize);
// Release some memory as needed
context.registerComponentCallbacks(new ComponentCallbacks2() {
/**
* {@inheritDoc}
*/
@Override
public void onTrimMemory(final int level) {
if (level >= TRIM_MEMORY_MODERATE) {
evictAll();
} else if (level >= TRIM_MEMORY_BACKGROUND) {
mLruCache.trimToSize(mLruCache.size() / 2);
}
}
/**
* {@inheritDoc}
*/
@Override
public void onLowMemory() {
// Nothing to do
}
/**
* {@inheritDoc}
*/
@Override
public void onConfigurationChanged(final Configuration newConfig) {
// Nothing to do
}
});
}
/**
* Find and return an existing ImageCache stored in a {@link RetainFragment}
* , if not found a new one is created using the supplied params and saved
* to a {@link RetainFragment}
*
* @param activity The calling {@link FragmentActivity}
* @return An existing retained ImageCache object or a new one if one did
* not exist
*/
public static final ImageCache findOrCreateCache(final Activity activity) {
// Search for, or create an instance of the non-UI RetainFragment
final RetainFragment retainFragment = findOrCreateRetainFragment(
activity.getFragmentManager());
// See if we already have an ImageCache stored in RetainFragment
ImageCache cache = (ImageCache)retainFragment.getObject();
// No existing ImageCache, create one and store it in RetainFragment
if (cache == null) {
cache = getInstance(activity);
retainFragment.setObject(cache);
}
return cache;
}
/**
* Locate an existing instance of this {@link Fragment} or if not found,
* create and add it using {@link FragmentManager}
*
* @param fm The {@link FragmentManager} to use
* @return The existing instance of the {@link Fragment} or the new instance
* if just created
*/
public static final RetainFragment findOrCreateRetainFragment(final FragmentManager fm) {
// Check to see if we have retained the worker fragment
RetainFragment retainFragment = (RetainFragment)fm.findFragmentByTag(TAG);
// If not retained, we need to create and add it
if (retainFragment == null) {
retainFragment = new RetainFragment();
fm.beginTransaction().add(retainFragment, TAG).commit();
}
return retainFragment;
}
/**
* Adds a new image to the memory and disk caches
*
* @param data The key used to store the image
* @param bitmap The {@link Bitmap} to cache
*/
public void addBitmapToCache(final String data, final Bitmap bitmap) {
if (data == null || bitmap == null) {
return;
}
// Add to memory cache
addBitmapToMemCache(data, bitmap);
// Add to disk cache
if (mDiskCache != null) {
final String key = hashKeyForDisk(data);
OutputStream out = null;
try {
final DiskLruCache.Snapshot snapshot = mDiskCache.get(key);
if (snapshot == null) {
final DiskLruCache.Editor editor = mDiskCache.edit(key);
if (editor != null) {
out = editor.newOutputStream(DISK_CACHE_INDEX);
bitmap.compress(COMPRESS_FORMAT, COMPRESS_QUALITY, out);
editor.commit();
out.close();
flush();
}
} else {
snapshot.getInputStream(DISK_CACHE_INDEX).close();
}
} catch (final IOException e) {
Log.e(TAG, "addBitmapToCache - " + e);
} finally {
try {
if (out != null) {
out.close();
out = null;
}
} catch (final IOException e) {
Log.e(TAG, "addBitmapToCache - " + e);
} catch (final IllegalStateException e) {
Log.e(TAG, "addBitmapToCache - " + e);
}
}
}
}
/**
* Called to add a new image to the memory cache
*
* @param data The key identifier
* @param bitmap The {@link Bitmap} to cache
*/
public void addBitmapToMemCache(final String data, final Bitmap bitmap) {
if (data == null || bitmap == null) {
return;
}
// Add to memory cache
if (getBitmapFromMemCache(data) == null) {
mLruCache.put(data, bitmap);
}
}
/**
* Fetches a cached image from the memory cache
*
* @param data Unique identifier for which item to get
* @return The {@link Bitmap} if found in cache, null otherwise
*/
public final Bitmap getBitmapFromMemCache(final String data) {
if (data == null) {
return null;
}
if (mLruCache != null) {
final Bitmap lruBitmap = mLruCache.get(data);
if (lruBitmap != null) {
return lruBitmap;
}
}
return null;
}
/**
* Fetches a cached image from the disk cache
*
* @param data Unique identifier for which item to get
* @return The {@link Bitmap} if found in cache, null otherwise
*/
public final Bitmap getBitmapFromDiskCache(final String data) {
if (data == null) {
return null;
}
// Check in the memory cache here to avoid going to the disk cache less
// often
if (getBitmapFromMemCache(data) != null) {
return getBitmapFromMemCache(data);
}
waitUntilUnpaused();
final String key = hashKeyForDisk(data);
if (mDiskCache != null) {
InputStream inputStream = null;
try {
final DiskLruCache.Snapshot snapshot = mDiskCache.get(key);
if (snapshot != null) {
inputStream = snapshot.getInputStream(DISK_CACHE_INDEX);
if (inputStream != null) {
final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
if (bitmap != null) {
return bitmap;
}
}
}
} catch (final IOException e) {
Log.e(TAG, "getBitmapFromDiskCache - " + e);
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (final IOException e) {
}
}
}
return null;
}
public static final int STATE_NONE = 0;
public static final int STATE_FROM_MEMORY = 1;
public static final int STATE_FROM_DISK = 2;
/**
* Tries to return a cached image from memory cache before fetching from the
* disk cache
*
* @param data Unique identifier for which item to get
* @return The {@link Bitmap} if found in cache, null otherwise
*/
public final Bitmap getCachedBitmap(final String data, int[] state) {
if (data == null) {
return null;
}
Bitmap cachedImage = getBitmapFromMemCache(data);
if (cachedImage == null) {
cachedImage = getBitmapFromDiskCache(data);
if (cachedImage != null) {
addBitmapToMemCache(data, cachedImage);
if (state != null)
state[0] = STATE_FROM_DISK;
} else {
if (state != null)
state[0] = STATE_NONE;
}
} else {
if (state != null)
state[0] = STATE_FROM_MEMORY;
}
return cachedImage;
}
/**
* flush() is called to synchronize up other methods that are accessing the
* cache first
*/
public void flush() {
Utils.execute(false, new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(final Void... unused) {
if (mDiskCache != null) {
try {
if (!mDiskCache.isClosed()) {
mDiskCache.flush();
}
} catch (final IOException e) {
Log.e(TAG, "flush - " + e);
}
}
return null;
}
}, (Void[])null);
}
/**
* Clears the disk and memory caches
*/
public void clearCaches() {
Utils.execute(false, new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(final Void... unused) {
// Clear the disk cache
try {
if (mDiskCache != null) {
mDiskCache.delete();
mDiskCache = null;
}
} catch (final IOException e) {
Log.e(TAG, "clearCaches - " + e);
}
// Clear the memory cache
evictAll();
return null;
}
}, (Void[])null);
}
/**
* Closes the disk cache associated with this ImageCache object. Note that
* this includes disk access so this should not be executed on the main/UI
* thread.
*/
public void close() {
Utils.execute(false, new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(final Void... unused) {
if (mDiskCache != null) {
try {
if (!mDiskCache.isClosed()) {
mDiskCache.close();
mDiskCache = null;
}
} catch (final IOException e) {
Log.e(TAG, "close - " + e);
}
}
return null;
}
}, (Void[])null);
}
/**
* Evicts all of the items from the memory cache and lets the system know
* now would be a good time to garbage collect
*/
public void evictAll() {
if (mLruCache != null) {
mLruCache.evictAll();
}
System.gc();
}
/**
* @param key The key used to identify which cache entries to delete.
*/
public void removeFromCache(final String key) {
if (key == null) {
return;
}
// Remove the Lru entry
if (mLruCache != null) {
mLruCache.remove(key);
}
try {
// Remove the disk entry
if (mDiskCache != null) {
mDiskCache.remove(hashKeyForDisk(key));
}
} catch (final IOException e) {
Log.e(TAG, "remove - " + e);
}
flush();
}
/**
* Used to temporarily pause the disk cache while the user is scrolling to
* improve scrolling.
*
* @param pause True to temporarily pause the disk cache, false otherwise.
*/
public void setPauseDiskCache(final boolean pause) {
synchronized (mPauseLock) {
if (mPauseDiskAccess != pause) {
mPauseDiskAccess = pause;
if (!pause) {
mPauseLock.notify();
}
}
}
}
private void waitUntilUnpaused() {
synchronized (mPauseLock) {
if (Looper.myLooper() != Looper.getMainLooper()) {
while (mPauseDiskAccess) {
try {
mPauseLock.wait();
} catch (InterruptedException e) {
// ignored, we'll start waiting again
}
}
}
}
}
/**
* @return True if the user is scrolling, false otherwise.
*/
public boolean isDiskCachePaused() {
return mPauseDiskAccess;
public static File getExternalCacheDir(final Context context) {
return context.getExternalCacheDir();
}
/**
@@ -565,157 +88,35 @@ public final class ImageCache {
* directory
* @return The cache directory
*/
public static final File getDiskCacheDir(final Context context, final String uniqueName) {
public static File getDiskCacheDir(final Context context, final String uniqueName) {
// getExternalCacheDir(context) returns null if external storage is not ready
final String cachePath = getExternalCacheDir(context) != null
? getExternalCacheDir(context).getPath()
: context.getCacheDir().getPath();
? getExternalCacheDir(context).getPath()
: context.getCacheDir().getPath();
return new File(cachePath, uniqueName);
}
/**
* Check if external storage is built-in or removable
*
* @return True if external storage is removable (like an SD card), false
* otherwise
*/
public static final boolean isExternalStorageRemovable() {
return Environment.isExternalStorageRemovable();
}
/**
* Get the external app cache directory
*
* @param context The {@link Context} to use
* @return The external cache directory
*/
public static final File getExternalCacheDir(final Context context) {
return context.getExternalCacheDir();
}
/**
* Check how much usable space is available at a given path.
*
* @param path The path to check
* @return The space available in bytes
*/
public static final long getUsableSpace(final File path) {
return path.getUsableSpace();
}
/**
* A hashing method that changes a string (like a URL) into a hash suitable
* for using as a disk filename.
*
* @param key The key used to store the file
*/
public static final String hashKeyForDisk(final String key) {
String cacheKey;
try {
final MessageDigest digest = MessageDigest.getInstance("MD5");
digest.update(key.getBytes());
cacheKey = bytesToHexString(digest.digest());
} catch (final NoSuchAlgorithmException e) {
cacheKey = String.valueOf(key.hashCode());
}
return cacheKey;
}
/**
* http://stackoverflow.com/questions/332079
*
* @param bytes The bytes to convert.
* @return A {@link String} converted from the bytes of a hashable key used
* to store a filename on the disk, to hex digits.
*/
private static final String bytesToHexString(final byte[] bytes) {
final StringBuilder builder = new StringBuilder();
for (final byte b : bytes) {
final String hex = Integer.toHexString(0xFF & b);
if (hex.length() == 1) {
builder.append('0');
private static class BitmapMemoryCacheHelper extends AnyCache.MemoryCacheHelper<Bitmap> {
@Override
public int sizeOf(String key, android.graphics.Bitmap bitmap) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return bitmap.getAllocationByteCount();
} else {
return bitmap.getByteCount();
}
builder.append(hex);
}
return builder.toString();
}
/**
* A simple non-UI Fragment that stores a single Object and is retained over
* configuration changes. In this sample it will be used to retain an
* {@link ImageCache} object.
*/
public static final class RetainFragment extends Fragment {
/**
* The object to be stored
*/
private Object mObject;
/**
* Empty constructor as per the {@link Fragment} documentation
*/
public RetainFragment() {
}
/**
* {@inheritDoc}
*/
private static class BitmapDiskCacheHelper extends AnyCache.DiskCacheHelper<Bitmap> {
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Make sure this Fragment is retained over a configuration change
setRetainInstance(true);
public Bitmap get(InputStream is) {
return BitmapFactory.decodeStream(is);
}
/**
* Store a single object in this {@link Fragment}
*
* @param object The object to store
*/
public void setObject(final Object object) {
mObject = object;
}
/**
* Get the stored object
*
* @return The stored object
*/
public Object getObject() {
return mObject;
}
}
/**
* Used to cache images via {@link LruCache}.
*/
public static final class MemoryCache extends LruCache<String, Bitmap> {
/**
* Constructor of <code>MemoryCache</code>
*
* @param maxSize The allowed size of the {@link LruCache}
*/
public MemoryCache(final int maxSize) {
super(maxSize);
}
/**
* Get the size in bytes of a bitmap.
*/
public static final int getBitmapSize(final Bitmap bitmap) {
return bitmap.getByteCount();
}
/**
* {@inheritDoc}
*/
@Override
protected int sizeOf(final String paramString, final Bitmap paramBitmap) {
return getBitmapSize(paramBitmap);
public boolean put(OutputStream os, Bitmap value) {
value.compress(COMPRESS_FORMAT, COMPRESS_QUALITY, os);
return true;
}
}
}
-333
View File
@@ -1,333 +0,0 @@
/*
* Copyright (C) 2011 The Android Open Source Project Licensed under the Apache
* License, Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
* or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
package com.hippo.ehviewer.cache;
// NOTE: upstream of this class is android.util.LruCache, changes below
// expose trimToSize() to be called externally.
import android.annotation.SuppressLint;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Static library version of {@link android.util.LruCache}. Used to write apps
* that run on API levels prior to 12. When running on API level 12 or above,
* this implementation is still used; it does not try to switch to the
* framework's implementation. See the framework SDK documentation for a class
* overview.
*/
public class LruCache<K, V> {
private final LinkedHashMap<K, V> map;
private final int maxSize;
/** Size of this cache in units. Not necessarily the number of elements. */
private int size;
private int putCount;
private int createCount;
private int evictionCount;
private int hitCount;
private int missCount;
/**
* @param maxSize for caches that do not override {@link #sizeOf}, this is
* the maximum number of entries in the cache. For all other
* caches, this is the maximum sum of the sizes of the entries in
* this cache.
*/
public LruCache(final int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
/**
* Returns the value for {@code key} if it exists in the cache or can be
* created by {@code #create}. If a value was returned, it is moved to the
* head of the queue. This returns null if a value is not cached and cannot
* be created.
*/
public final V get(final K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
mapValue = map.get(key);
if (mapValue != null) {
this.hitCount++;
return mapValue;
}
this.missCount++;
}
/*
* Attempt to create a value. This may take a long time, and the map may
* be different when create() returns. If a conflicting value was added
* to the map while create() was working, we leave that value in the map
* and release the created value.
*/
final V createdValue = create(key);
if (createdValue == null) {
return null;
}
synchronized (this) {
this.createCount++;
mapValue = map.put(key, createdValue);
if (mapValue != null) {
/* There was a conflict so undo that last put */
this.map.put(key, mapValue);
} else {
this.size += safeSizeOf(key, createdValue);
}
}
if (mapValue != null) {
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
trimToSize(maxSize);
return createdValue;
}
}
/**
* Caches {@code value} for {@code key}. The value is moved to the head of
* the queue.
*
* @return the previous value mapped by {@code key}.
*/
public final V put(final K key, final V value) {
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}
V previous;
synchronized (this) {
this.putCount++;
this.size += safeSizeOf(key, value);
previous = this.map.put(key, value);
if (previous != null) {
this.size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, value);
}
trimToSize(maxSize);
return previous;
}
/**
* @param maxSize the maximum size of the cache before returning. May be -1
* to evict even 0-sized elements.
*/
public void trimToSize(final int maxSize) {
while (true) {
K key;
V value;
synchronized (this) {
if (this.size < 0 || this.map.isEmpty() && size != 0) {
throw new IllegalStateException(getClass().getName()
+ ".sizeOf() is reporting inconsistent results!");
}
if (this.size <= maxSize || this.map.isEmpty()) {
break;
}
final Map.Entry<K, V> toEvict = this.map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
this.map.remove(key);
this.size -= safeSizeOf(key, value);
this.evictionCount++;
}
entryRemoved(true, key, value, null);
}
}
/**
* Removes the entry for {@code key} if it exists.
*
* @return the previous value mapped by {@code key}.
*/
public final V remove(final K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V previous;
synchronized (this) {
previous = this.map.remove(key);
if (previous != null) {
this.size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, null);
}
return previous;
}
/**
* Called for entries that have been evicted or removed. This method is
* invoked when a value is evicted to make space, removed by a call to
* {@link #remove}, or replaced by a call to {@link #put}. The default
* implementation does nothing.
* <p>
* The method is called without synchronization: other threads may access
* the cache while this method is executing.
*
* @param evicted true if the entry is being removed to make space, false if
* the removal was caused by a {@link #put} or {@link #remove}.
* @param newValue the new value for {@code key}, if it exists. If non-null,
* this removal was caused by a {@link #put}. Otherwise it was
* caused by an eviction or a {@link #remove}.
*/
protected void entryRemoved(final boolean evicted, final K key, final V oldValue,
final V newValue) {
}
/**
* Called after a cache miss to compute a value for the corresponding key.
* Returns the computed value or null if no value can be computed. The
* default implementation returns null.
* <p>
* The method is called without synchronization: other threads may access
* the cache while this method is executing.
* <p>
* If a value for {@code key} exists in the cache when this method returns,
* the created value will be released with {@link #entryRemoved} and
* discarded. This can occur when multiple threads request the same key at
* the same time (causing multiple values to be created), or when one thread
* calls {@link #put} while another is creating a value for the same key.
*/
protected V create(final K key) {
return null;
}
private int safeSizeOf(final K key, final V value) {
final int result = sizeOf(key, value);
if (result < 0) {
throw new IllegalStateException("Negative size: " + key + "=" + value);
}
return result;
}
/**
* Returns the size of the entry for {@code key} and {@code value} in
* user-defined units. The default implementation returns 1 so that size is
* the number of entries and max size is the maximum number of entries.
* <p>
* An entry's size must not change while it is in the cache.
*/
protected int sizeOf(final K key, final V value) {
return 1;
}
/**
* Clear the cache, calling {@link #entryRemoved} on each removed entry.
*/
public final void evictAll() {
trimToSize(-1); // -1 will evict 0-sized elements
}
/**
* For caches that do not override {@link #sizeOf}, this returns the number
* of entries in the cache. For all other caches, this returns the sum of
* the sizes of the entries in this cache.
*/
public synchronized final int size() {
return this.size;
}
/**
* For caches that do not override {@link #sizeOf}, this returns the maximum
* number of entries in the cache. For all other caches, this returns the
* maximum sum of the sizes of the entries in this cache.
*/
public synchronized final int maxSize() {
return this.maxSize;
}
/**
* Returns the number of times {@link #get} returned a value.
*/
public synchronized final int hitCount() {
return this.hitCount;
}
/**
* Returns the number of times {@link #get} returned null or required a new
* value to be created.
*/
public synchronized final int missCount() {
return this.missCount;
}
/**
* Returns the number of times {@link #create(Object)} returned a value.
*/
public synchronized final int createCount() {
return this.createCount;
}
/**
* Returns the number of times {@link #put} was called.
*/
public synchronized final int putCount() {
return this.putCount;
}
/**
* Returns the number of values that have been evicted.
*/
public synchronized final int evictionCount() {
return this.evictionCount;
}
/**
* Returns a copy of the current contents of the cache, ordered from least
* recently accessed to most recently accessed.
*/
public synchronized final Map<K, V> snapshot() {
return new LinkedHashMap<K, V>(this.map);
}
@SuppressLint("DefaultLocale")
@Override
public synchronized final String toString() {
final int accesses = this.hitCount + this.missCount;
final int hitPercent = accesses != 0 ? 100 * this.hitCount / accesses : 0;
return String.format("LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]", this.maxSize,
this.hitCount, this.missCount, hitPercent);
}
}
@@ -0,0 +1,196 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.cache;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
/**
* Buffers input from an {@link InputStream} for reading lines.
*
* <p>This class is used for buffered reading of lines. For purposes of this class, a line ends
* with "\n" or "\r\n". End of input is reported by throwing {@code EOFException}. Unterminated
* line at end of input is invalid and will be ignored, the caller may use {@code
* hasUnterminatedLine()} to detect it after catching the {@code EOFException}.
*
* <p>This class is intended for reading input that strictly consists of lines, such as line-based
* cache entries or cache journal. Unlike the {@link java.io.BufferedReader} which in conjunction
* with {@link java.io.InputStreamReader} provides similar functionality, this class uses different
* end-of-input reporting and a more restrictive definition of a line.
*
* <p>This class supports only charsets that encode '\r' and '\n' as a single byte with value 13
* and 10, respectively, and the representation of no other character contains these values.
* We currently check in constructor that the charset is one of US-ASCII, UTF-8 and ISO-8859-1.
* The default charset is US_ASCII.
*/
class StrictLineReader implements Closeable {
private static final byte CR = (byte) '\r';
private static final byte LF = (byte) '\n';
private final InputStream in;
private final Charset charset;
/*
* Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end
* and the data in the range [pos, end) is buffered for reading. At end of input, if there is
* an unterminated line, we set end == -1, otherwise end == pos. If the underlying
* {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1.
*/
private byte[] buf;
private int pos;
private int end;
/**
* Constructs a new {@code LineReader} with the specified charset and the default capacity.
*
* @param in the {@code InputStream} to read data from.
* @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
* supported.
* @throws NullPointerException if {@code in} or {@code charset} is null.
* @throws IllegalArgumentException if the specified charset is not supported.
*/
public StrictLineReader(InputStream in, Charset charset) {
this(in, 8192, charset);
}
/**
* Constructs a new {@code LineReader} with the specified capacity and charset.
*
* @param in the {@code InputStream} to read data from.
* @param capacity the capacity of the buffer.
* @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
* supported.
* @throws NullPointerException if {@code in} or {@code charset} is null.
* @throws IllegalArgumentException if {@code capacity} is negative or zero
* or the specified charset is not supported.
*/
public StrictLineReader(InputStream in, int capacity, Charset charset) {
if (in == null || charset == null) {
throw new NullPointerException();
}
if (capacity < 0) {
throw new IllegalArgumentException("capacity <= 0");
}
if (!(charset.equals(Util.US_ASCII))) {
throw new IllegalArgumentException("Unsupported encoding");
}
this.in = in;
this.charset = charset;
buf = new byte[capacity];
}
/**
* Closes the reader by closing the underlying {@code InputStream} and
* marking this reader as closed.
*
* @throws IOException for errors when closing the underlying {@code InputStream}.
*/
public void close() throws IOException {
synchronized (in) {
if (buf != null) {
buf = null;
in.close();
}
}
}
/**
* Reads the next line. A line ends with {@code "\n"} or {@code "\r\n"},
* this end of line marker is not included in the result.
*
* @return the next line from the input.
* @throws IOException for underlying {@code InputStream} errors.
* @throws EOFException for the end of source stream.
*/
public String readLine() throws IOException {
synchronized (in) {
if (buf == null) {
throw new IOException("LineReader is closed");
}
// Read more data if we are at the end of the buffered data.
// Though it's an error to read after an exception, we will let {@code fillBuf()}
// throw again if that happens; thus we need to handle end == -1 as well as end == pos.
if (pos >= end) {
fillBuf();
}
// Try to find LF in the buffered data and return the line if successful.
for (int i = pos; i != end; ++i) {
if (buf[i] == LF) {
int lineEnd = (i != pos && buf[i - 1] == CR) ? i - 1 : i;
String res = new String(buf, pos, lineEnd - pos, charset.name());
pos = i + 1;
return res;
}
}
// Let's anticipate up to 80 characters on top of those already read.
ByteArrayOutputStream out = new ByteArrayOutputStream(end - pos + 80) {
@Override
public String toString() {
int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count;
try {
return new String(buf, 0, length, charset.name());
} catch (UnsupportedEncodingException e) {
throw new AssertionError(e); // Since we control the charset this will never happen.
}
}
};
while (true) {
out.write(buf, pos, end - pos);
// Mark unterminated line in case fillBuf throws EOFException or IOException.
end = -1;
fillBuf();
// Try to find LF in the buffered data and return the line if successful.
for (int i = pos; i != end; ++i) {
if (buf[i] == LF) {
if (i != pos) {
out.write(buf, pos, i - pos);
}
pos = i + 1;
return out.toString();
}
}
}
}
}
public boolean hasUnterminatedLine() {
return end == -1;
}
/**
* Reads new input data into the buffer. Call only with pos == end or end == -1,
* depending on the desired outcome if the function throws.
*/
private void fillBuf() throws IOException {
int result = in.read(buf, 0, buf.length);
if (result == -1) {
throw new EOFException();
}
pos = 0;
end = result;
}
}
+77
View File
@@ -0,0 +1,77 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.cache;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.io.StringWriter;
import java.nio.charset.Charset;
/** Junk drawer of utility methods. */
final class Util {
static final Charset US_ASCII = Charset.forName("US-ASCII");
static final Charset UTF_8 = Charset.forName("UTF-8");
private Util() {
}
static String readFully(Reader reader) throws IOException {
try {
StringWriter writer = new StringWriter();
char[] buffer = new char[1024];
int count;
while ((count = reader.read(buffer)) != -1) {
writer.write(buffer, 0, count);
}
return writer.toString();
} finally {
reader.close();
}
}
/**
* Deletes the contents of {@code dir}. Throws an IOException if any file
* could not be deleted, or if {@code dir} is not a readable directory.
*/
static void deleteContents(File dir) throws IOException {
File[] files = dir.listFiles();
if (files == null) {
throw new IOException("not a readable directory: " + dir);
}
for (File file : files) {
if (file.isDirectory()) {
deleteContents(file);
}
if (!file.delete()) {
throw new IOException("failed to delete file: " + file);
}
}
}
static void closeQuietly(/*Auto*/Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (RuntimeException rethrown) {
throw rethrown;
} catch (Exception ignored) {
}
}
}
}
@@ -19,7 +19,6 @@ package com.hippo.ehviewer.ui;
import android.app.Activity;
import com.google.analytics.tracking.android.EasyTracker;
import com.hippo.ehviewer.cache.ImageCache;
import com.hippo.ehviewer.util.Config;
import com.hippo.ehviewer.util.Ui;
@@ -46,7 +45,5 @@ public abstract class AbsActivity extends Activity {
if (Config.getAllowAnalyics())
EasyTracker.getInstance(this).activityStop(this);
ImageCache.getInstance(this).flush();
}
}
@@ -27,6 +27,7 @@ import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
@@ -56,7 +57,6 @@ import com.hippo.ehviewer.AppContext;
import com.hippo.ehviewer.ImageLoader;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.app.MaterialAlertDialog;
import com.hippo.ehviewer.cache.ImageCache;
import com.hippo.ehviewer.data.ApiGalleryDetail;
import com.hippo.ehviewer.data.Comment;
import com.hippo.ehviewer.data.Data;
@@ -75,6 +75,8 @@ import com.hippo.ehviewer.drawable.OvalDrawable;
import com.hippo.ehviewer.effect.ripple.RippleSalon;
import com.hippo.ehviewer.ehclient.DetailUrlParser;
import com.hippo.ehviewer.ehclient.EhClient;
import com.hippo.ehviewer.cache.AnyCache;
import com.hippo.ehviewer.cache.ImageCache;
import com.hippo.ehviewer.service.DownloadService;
import com.hippo.ehviewer.util.Config;
import com.hippo.ehviewer.util.Constants;
@@ -1159,7 +1161,7 @@ public class GalleryDetailActivity extends AbsTranslucentActivity
@Override
public void onScrollStateChanged(ResponedScrollView view, int state) {
if (view == mDetailScroll) {
ImageCache imageCache = ImageCache.getInstance(this);
AnyCache<Bitmap> imageCache = ImageCache.getImageCache(this);
if (state == ResponedScrollView.SCROLL_START)
imageCache.setPauseDiskCache(true);
else
@@ -16,12 +16,10 @@
package com.hippo.ehviewer.widget;
import java.util.ArrayList;
import java.util.List;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.os.Build;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.RecyclerView;
@@ -36,19 +34,23 @@ import android.widget.TextView;
import com.hippo.ehviewer.AppHandler;
import com.hippo.ehviewer.ImageLoader;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.cache.ImageCache;
import com.hippo.ehviewer.cardview.CardViewSalon;
import com.hippo.ehviewer.data.GalleryInfo;
import com.hippo.ehviewer.ehclient.ListParser;
import com.hippo.ehviewer.cache.ImageCache;
import com.hippo.ehviewer.util.Config;
import com.hippo.ehviewer.util.Ui;
import com.hippo.ehviewer.util.ViewUtils;
import com.hippo.ehviewer.widget.recyclerview.EasyRecyclerView;
import com.hippo.ehviewer.widget.recyclerview.MarginItemDecoration;
import com.hippo.ehviewer.widget.recyclerview.EasyRecyclerView.FooterAdapter;
import com.hippo.ehviewer.widget.recyclerview.MarginItemDecoration;
import com.hippo.ehviewer.widget.refreshlayout.RefreshLayout;
public class GalleryListView extends FrameLayout implements RefreshLayout.OnFooterRefreshListener,
RefreshLayout.OnRefreshListener {
import java.util.ArrayList;
import java.util.List;
public class GalleryListView extends FrameLayout implements RefreshLayout.OnHeaderRefreshListener,
RefreshLayout.OnFooterRefreshListener {
private static final String TAG = GalleryListView.class.getSimpleName();
@@ -112,6 +114,8 @@ public class GalleryListView extends FrameLayout implements RefreshLayout.OnFoot
private int mListDetailThumbWidth;
private int mListDetailThumbHeight;
private int mOldPaddingBottom;
public GalleryListView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs, 0, 0);
@@ -130,13 +134,13 @@ public class GalleryListView extends FrameLayout implements RefreshLayout.OnFoot
private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
mContext = context;
mGiList = new ArrayList<GalleryInfo>();
mGiList = new ArrayList<>();
mListener = new OnGetGalleryListListener();
LayoutInflater.from(mContext).inflate(R.layout.gallery_list_view, this);
mRefreshLayout = (RefreshLayout) findViewById(R.id.refresh_layout);
mEasyRecyclerView = mRefreshLayout.getEasyRecyclerView();
mEasyRecyclerView = (EasyRecyclerView) mRefreshLayout.findViewById(R.id.easy_recycler_view);
mRefreshTextView = (RefreshTextView) findViewById(R.id.refresh_text);
mAdapter = new GalleryAdapter(mContext, mGiList);
@@ -158,18 +162,24 @@ public class GalleryListView extends FrameLayout implements RefreshLayout.OnFoot
}
});
mRefreshLayout.setColorSchemeResources(
mRefreshLayout.setHeaderEnable(true);
mRefreshLayout.setFooterEnable(true);
mRefreshLayout.setHeaderColorSchemeResources(
R.color.refresh_color_1,
R.color.refresh_color_2,
R.color.refresh_color_3,
R.color.refresh_color_4);
R.color.refresh_color_4
);
mRefreshLayout.setFooterColorSchemeResources(
R.color.refresh_color_1,
R.color.refresh_color_2,
R.color.refresh_color_3,
R.color.refresh_color_4
);
mRefreshLayout.setOnHeaderRefreshListener(this);
mRefreshLayout.setOnFooterRefreshListener(this);
mRefreshLayout.setFooterString(
mContext.getString(R.string.footer_loading),
mContext.getString(R.string.footer_loaded),
mContext.getString(R.string.footer_fail));
mRefreshLayout.addFooterView();
mOldPaddingBottom = mEasyRecyclerView.getPaddingBottom();
// Caculate gallery detail height
mInflater = LayoutInflater.from(mContext);
@@ -226,7 +236,7 @@ public class GalleryListView extends FrameLayout implements RefreshLayout.OnFoot
public void setPaddingTopAndBottom(int top, int bottom) {
setPadding(getPaddingLeft(), top, getPaddingRight(), getPaddingBottom());
mEasyRecyclerView.setPadding(mEasyRecyclerView.getPaddingLeft(), mEasyRecyclerView.getPaddingTop(),
mEasyRecyclerView.getPaddingRight(), mEasyRecyclerView.getPaddingBottom() + bottom);
mEasyRecyclerView.getPaddingRight(), mOldPaddingBottom + bottom);
}
private void setItemMargin(int margin) {
@@ -237,15 +247,15 @@ public class GalleryListView extends FrameLayout implements RefreshLayout.OnFoot
}
public void setEnabledHeader(boolean enabled) {
mRefreshLayout.setEnabledHeader(enabled);
mRefreshLayout.setHeaderEnable(enabled);
}
public void setEnabledFooter(boolean enabled) {
mRefreshLayout.setEnabledFooter(enabled);
mRefreshLayout.setFooterEnable(enabled);
}
public void setHeaderRefreshing(boolean refreshing) {
mRefreshLayout.setRefreshing(refreshing);
mRefreshLayout.setHeaderRefreshing(refreshing);
}
public void setOnItemClickListener(EasyRecyclerView.OnItemClickListener l) {
@@ -294,7 +304,7 @@ public class GalleryListView extends FrameLayout implements RefreshLayout.OnFoot
}
@Override
public void onRefresh() {
public void onHeaderRefresh() {
// It is invokened by user pull, so no need to
if (mFirstPage > 0) {
getPrePage(true);
@@ -327,7 +337,7 @@ public class GalleryListView extends FrameLayout implements RefreshLayout.OnFoot
private void setGallerysLayout() {
if (mRefreshLayout.getVisibility() == View.VISIBLE) {
if (!isFootRefresh)
mRefreshLayout.setRefreshing(true);
mRefreshLayout.setHeaderRefreshing(true);
mRefreshTextView.setVisibility(View.GONE);
} else {
mRefreshLayout.setVisibility(View.GONE);
@@ -541,7 +551,8 @@ public class GalleryListView extends FrameLayout implements RefreshLayout.OnFoot
}
}
mRefreshLayout.setAnyRefreshComplete(true);
mRefreshLayout.setHeaderRefreshing(false);
mRefreshLayout.setFooterRefreshing(false);
}
@Override
@@ -572,7 +583,8 @@ public class GalleryListView extends FrameLayout implements RefreshLayout.OnFoot
mRefreshTextView.setVisibility(View.GONE);
MaterialToast.showToast(eMsg);
}
mRefreshLayout.setAnyRefreshComplete(false);
mRefreshLayout.setHeaderRefreshing(false);
mRefreshLayout.setFooterRefreshing(false);
}
}
@@ -602,17 +614,17 @@ public class GalleryListView extends FrameLayout implements RefreshLayout.OnFoot
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
switch (newState) {
case RecyclerView.SCROLL_STATE_DRAGGING:
ImageCache.getInstance(mContext).setPauseDiskCache(true);
ImageCache.getImageCache(mContext).setPauseDiskCache(true);
break;
case RecyclerView.SCROLL_STATE_IDLE:
ImageCache.getInstance(mContext).setPauseDiskCache(false);
ImageCache.getImageCache(mContext).setPauseDiskCache(false);
break;
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
mRefreshLayout.onScrolled(recyclerView, dx, dy);
// mRefreshLayout.onScrolled(recyclerView, dx, dy);
int itemCount = mLayoutManager.getChildCount();
int firstVisibleItem = mLayoutManager.findFirstVisibleItemPositions(mFirstPositionTemp)[0];
@@ -733,12 +745,18 @@ public class GalleryListView extends FrameLayout implements RefreshLayout.OnFoot
int position) {
GalleryInfo gi = mGiList.get(position);
final LoadImageView thumb = holder.thumb;
if (!String.valueOf(gi.gid).equals(thumb.getKey())) {
final String key = String.valueOf(gi.gid);
if (!key.equals(thumb.getKey())) {
// Set new thumb
thumb.setImageDrawable(null);
thumb.setLoadInfo(gi.thumb, String.valueOf(gi.gid));
mImageLoader.add(gi.thumb, String.valueOf(gi.gid),
new LoadImageView.SimpleImageGetListener(thumb).setFixScaleType(true));
thumb.setLoadInfo(gi.thumb, key);
Bitmap b = ImageCache.getImageCache(getContext()).getFromMemory(key);
if (b != null) {
thumb.setContextImage(b, true, false);
} else {
mImageLoader.add(gi.thumb, String.valueOf(gi.gid),
new LoadImageView.SimpleImageGetListener(thumb).setFixScaleType(true));
}
}
// Set simple language
TextView simpleLanguage = holder.simpleLanguage;
@@ -96,9 +96,19 @@ public class LoadImageView extends FixedAspectImageView {
* Load target bmp, set progressive animation
* @param bmp
*/
public void setContextImage(Bitmap bmp, int state) {
public void setContextImage(Bitmap bmp, boolean fixScaleType, boolean transit) {
if (fixScaleType) {
float aspectRatio = (float)bmp.getHeight() / bmp.getWidth();
if (aspectRatio < 1.7 && aspectRatio > 1.3)
setScaleType(ImageView.ScaleType.CENTER_CROP);
else
setScaleType(ImageView.ScaleType.FIT_CENTER);
} else {
setScaleType(ImageView.ScaleType.FIT_CENTER);
}
setImageBitmap(bmp);
if (state != ImageLoader.STATE_FROM_MEMORY) {
if (transit) {
final Drawable drawable = getDrawable();
DrawableTransition.transit(drawable, false, 1000);
}
@@ -134,16 +144,8 @@ public class LoadImageView extends FixedAspectImageView {
public void onGetImage(String key, Bitmap bmp, int state) {
if (isVaild(key) && mLiv.getState() == LoadImageView.LOADING) {
if (bmp != null) {
if (mFixScaleType) {
float aspectRatio = (float)bmp.getHeight() / bmp.getWidth();
if (aspectRatio < 1.7 && aspectRatio > 1.3)
mLiv.setScaleType(ImageView.ScaleType.CENTER_CROP);
else
mLiv.setScaleType(ImageView.ScaleType.FIT_CENTER);
}
if (mTransitabled)
mLiv.setContextImage(bmp, state);
mLiv.setContextImage(bmp, mFixScaleType, true);
else
mLiv.setImageBitmap(bmp);
mLiv.setState(LoadImageView.LOADED);
@@ -1,238 +0,0 @@
/*
* Copyright (C) 2014 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.v4.widget.SwipeRefreshLayout;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.widget.recyclerview.EasyRecyclerView;
public class RefreshLayout extends SwipeRefreshLayout {
private final static int FOOTER_REFRESHING = 0;
private final static int FOOTER_SUCCESS = 1;
private final static int FOOTER_FAIL = 2;
private EasyRecyclerView mEasyRecyclerView;
private View mFooterView;
private TextView mFooterTipTextView;
private View mFooterProgressBar;
private int mFooterState = FOOTER_SUCCESS;
private OnFooterRefreshListener mFooterRefreshListener;
// Footer String to show
private String mFooterRefreshStr;
private String mFooterSuccessStr;
private String mFooterFailStr;
private boolean mIsEnabledHeader = true;
private boolean mIsEnabledFooter = true;
public RefreshLayout(Context context) {
super(context);
init(context, null);
}
public RefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
if (attrs == null)
mEasyRecyclerView = new EasyRecyclerView(context);
else
mEasyRecyclerView = new EasyRecyclerView(context, attrs);
mEasyRecyclerView.setId(R.id.easy_recycler_view);
addView(mEasyRecyclerView);
// Footer view
mFooterView = LayoutInflater.from(context).inflate(R.layout.pull_list_view_footer, null);
mFooterTipTextView = (TextView) mFooterView.findViewById(R.id.footer_tip_text);
mFooterProgressBar = mFooterView.findViewById(R.id.footer_progressBar);
mFooterView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (!isRefreshing() && mFooterState == FOOTER_FAIL)
footerRefresh();
}
});
}
public EasyRecyclerView getEasyRecyclerView() {
return mEasyRecyclerView;
}
public void addFooterView() {
((EasyRecyclerView.FooterAdapter<?>) mEasyRecyclerView.getAdapter()).setFooterView(mFooterView);
}
public void removeFooterView() {
((EasyRecyclerView.FooterAdapter<?>) mEasyRecyclerView.getAdapter()).setFooterView(null);
}
public void setOnHeaderRefreshListener(OnRefreshListener l) {
super.setOnRefreshListener(l);
}
public void setOnFooterRefreshListener(OnFooterRefreshListener l) {
mFooterRefreshListener = l;
}
private void footerRefresh() {
if (mFooterRefreshListener.onFooterRefresh()) {
mFooterState = FOOTER_REFRESHING;
changeFooterViewByState();
// Disable header refresh
if (mIsEnabledHeader)
setEnabled(false);
}
}
/**
* Set footer UI String
*
* @param refreshStr
* @param successStr
* @param failStr
*/
public void setFooterString(String refreshStr, String successStr, String failStr) {
mFooterRefreshStr = refreshStr;
mFooterSuccessStr = successStr;
mFooterFailStr = failStr;
switch (mFooterState) {
case FOOTER_REFRESHING:
mFooterTipTextView.setText(mFooterRefreshStr);
break;
case FOOTER_SUCCESS:
mFooterTipTextView.setText(mFooterSuccessStr);
break;
case FOOTER_FAIL:
mFooterTipTextView.setText(mFooterFailStr);
break;
}
}
/**
* Refresh footer UI
*/
private void changeFooterViewByState() {
switch (mFooterState) {
case FOOTER_REFRESHING:
mFooterTipTextView.setText(mFooterRefreshStr);
mFooterProgressBar.setVisibility(View.VISIBLE);
break;
case FOOTER_SUCCESS:
mFooterTipTextView.setText(mFooterSuccessStr);
mFooterProgressBar.setVisibility(View.GONE);
break;
case FOOTER_FAIL:
mFooterTipTextView.setText(mFooterFailStr);
mFooterProgressBar.setVisibility(View.GONE);
break;
}
}
public void setHeaderRefreshComplete() {
super.setRefreshing(false);
}
public void setFooterRefreshComplete(boolean isSuccess) {
if (isSuccess)
mFooterState = FOOTER_SUCCESS;
else
mFooterState = FOOTER_FAIL;
changeFooterViewByState();
// enable header refresh
if (mIsEnabledHeader)
setEnabled(true);
}
public void setAnyRefreshComplete() {
setAnyRefreshComplete(true);
}
public void setAnyRefreshComplete(boolean isSuccess) {
setHeaderRefreshComplete();
setFooterRefreshComplete(isSuccess);
}
/**
* @return True if actionbar is refreshing
*/
public boolean isHeaderRefreshing() {
return super.isRefreshing();
}
/**
* @return True if footer is refreshing
*/
public boolean isFooterRefreshing() {
return mFooterState == FOOTER_REFRESHING;
}
/**
* @return True if actionbar or footer is refreshing
*/
@Override
public boolean isRefreshing() {
return (mIsEnabledHeader ? isHeaderRefreshing() : false)
| (mIsEnabledFooter ? isFooterRefreshing() : false);
}
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (mIsEnabledFooter && mFooterView.isShown() && mFooterState != FOOTER_FAIL)
footerRefresh();
}
public void setEnabledHeader(boolean enabled) {
if (mIsEnabledHeader == enabled)
return;
mIsEnabledHeader = enabled;
setEnabled(enabled);
}
public void setEnabledFooter(boolean enabled) {
if (mIsEnabledFooter == enabled)
return;
if (mIsEnabledFooter = enabled)
addFooterView();
else
removeFooterView();
}
public interface OnFooterRefreshListener {
/**
* @return True if this refresh action is vaild
*/
boolean onFooterRefresh();
}
}
@@ -0,0 +1,84 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.widget.refreshlayout;
import android.view.animation.Interpolator;
/**
* A pre-baked bezier-curved interpolator for indeterminate progress animations.
*/
final class BakedBezierInterpolator implements Interpolator {
private static final BakedBezierInterpolator INSTANCE = new BakedBezierInterpolator();
public final static BakedBezierInterpolator getInstance() {
return INSTANCE;
}
/**
* Use getInstance instead of instantiating.
*/
private BakedBezierInterpolator() {
super();
}
/**
* Lookup table values.
* Generated using a Bezier curve from (0,0) to (1,1) with control points:
* P0 (0,0)
* P1 (0.4, 0)
* P2 (0.2, 1.0)
* P3 (1.0, 1.0)
*
* Values sampled with x at regular intervals between 0 and 1.
*/
private static final float[] VALUES = new float[] {
0.0f, 0.0002f, 0.0009f, 0.0019f, 0.0036f, 0.0059f, 0.0086f, 0.0119f, 0.0157f, 0.0209f,
0.0257f, 0.0321f, 0.0392f, 0.0469f, 0.0566f, 0.0656f, 0.0768f, 0.0887f, 0.1033f, 0.1186f,
0.1349f, 0.1519f, 0.1696f, 0.1928f, 0.2121f, 0.237f, 0.2627f, 0.2892f, 0.3109f, 0.3386f,
0.3667f, 0.3952f, 0.4241f, 0.4474f, 0.4766f, 0.5f, 0.5234f, 0.5468f, 0.5701f, 0.5933f,
0.6134f, 0.6333f, 0.6531f, 0.6698f, 0.6891f, 0.7054f, 0.7214f, 0.7346f, 0.7502f, 0.763f,
0.7756f, 0.7879f, 0.8f, 0.8107f, 0.8212f, 0.8326f, 0.8415f, 0.8503f, 0.8588f, 0.8672f,
0.8754f, 0.8833f, 0.8911f, 0.8977f, 0.9041f, 0.9113f, 0.9165f, 0.9232f, 0.9281f, 0.9328f,
0.9382f, 0.9434f, 0.9476f, 0.9518f, 0.9557f, 0.9596f, 0.9632f, 0.9662f, 0.9695f, 0.9722f,
0.9753f, 0.9777f, 0.9805f, 0.9826f, 0.9847f, 0.9866f, 0.9884f, 0.9901f, 0.9917f, 0.9931f,
0.9944f, 0.9955f, 0.9964f, 0.9973f, 0.9981f, 0.9986f, 0.9992f, 0.9995f, 0.9998f, 1.0f, 1.0f
};
private static final float STEP_SIZE = 1.0f / (VALUES.length - 1);
@Override
public float getInterpolation(float input) {
if (input >= 1.0f) {
return 1.0f;
}
if (input <= 0f) {
return 0f;
}
int position = Math.min(
(int)(input * (VALUES.length - 1)),
VALUES.length - 2);
float quantized = position * STEP_SIZE;
float difference = input - quantized;
float weight = difference / STEP_SIZE;
return VALUES[position] + weight * (VALUES[position + 1] - VALUES[position]);
}
}
@@ -0,0 +1,154 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.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 int mShadowRadius;
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,714 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.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.util.DisplayMetrics;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
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 END_CURVE_INTERPOLATOR = new EndCurveInterpolator();
private static final Interpolator START_CURVE_INTERPOLATOR = new StartCurveInterpolator();
private static final Interpolator EASE_INTERPOLATOR = new AccelerateDecelerateInterpolator();
@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 duration of a single progress spin in milliseconds. */
private static final int ANIMATION_DURATION = 1000 * 80 / 60;
/** 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 void applyFinishTranslation(float interpolatedTime, Ring ring) {
// shrink back down and complete a full rotation before
// starting other circles
// Rotation goes between [0..1].
float targetRotation = (float) (Math.floor(ring.getStartingRotation() / MAX_PROGRESS_ARC)
+ 1f);
final float startTrim = ring.getStartingStartTrim()
+ (ring.getStartingEndTrim() - ring.getStartingStartTrim()) * interpolatedTime;
ring.setStartTrim(startTrim);
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 = (float) Math.toRadians(
ring.getStrokeWidth() / (2 * Math.PI * ring.getCenterRadius()));
final float startingEndTrim = ring.getStartingEndTrim();
final float startingTrim = ring.getStartingStartTrim();
final float startingRotation = ring.getStartingRotation();
// Offset the minProgressArc to where the endTrim is
// located.
final float minArc = MAX_PROGRESS_ARC - minProgressArc;
final float endTrim = startingEndTrim + (minArc
* START_CURVE_INTERPOLATOR.getInterpolation(interpolatedTime));
ring.setEndTrim(endTrim);
final float startTrim = startingTrim + (MAX_PROGRESS_ARC
* END_CURVE_INTERPOLATOR.getInterpolation(interpolatedTime));
ring.setStartTrim(startTrim);
final float rotation = startingRotation + (0.25f * interpolatedTime);
ring.setRotation(rotation);
float groupRotation = ((720.0f / NUM_POINTS) * interpolatedTime)
+ (720.0f * (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();
private int mBackgroundColor;
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(mColors[mColorIndex]);
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(mColors[mColorIndex]);
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);
}
/**
* @param index Index into the color array of the color to display in
* the progress spinner.
*/
public void setColorIndex(int index) {
mColorIndex = index;
}
/**
* Proceed to the next available ring color. This will automatically
* wrap back to the beginning of colors.
*/
public void goToNextColor() {
mColorIndex = (mColorIndex + 1) % (mColors.length);
}
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;
}
@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);
}
}
/**
* Squishes the interpolation curve into the second half of the animation.
*/
private static class EndCurveInterpolator extends AccelerateDecelerateInterpolator {
@Override
public float getInterpolation(float input) {
return super.getInterpolation(Math.max(0, (input - 0.5f) * 2.0f));
}
}
/**
* Squishes the interpolation curve into the first half of the animation.
*/
private static class StartCurveInterpolator extends AccelerateDecelerateInterpolator {
@Override
public float getInterpolation(float input) {
return super.getInterpolation(Math.min(1, input * 2.0f));
}
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,276 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.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.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 = BakedBezierInterpolator.getInstance();
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) {
final int width = mBounds.width();
final int height = mBounds.height();
final int cx = width / 2;
final int cy = height / 2;
boolean drawTriggerWhileFinishing = false;
int restoreCount = canvas.save();
canvas.clipRect(mBounds);
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(mBounds);
drawTrigger(canvas, cx, cy);
}
// Keep running until we finish out the last cycle.
ViewCompat.postInvalidateOnAnimation(
mParent, mBounds.left, mBounds.top, mBounds.right, mBounds.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();
// Go to bottom
canvas.translate(cx, mParent.getHeight() - 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;
}
}