Adjust cache
This commit is contained in:
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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+848
-874
File diff suppressed because it is too large.
Load diff
+55
-654
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
+84
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+714
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user