commit fd5d07b2f265c66b039f5d0f8a57336c7b149440 Author: seven332 Date: Sat Mar 7 15:56:40 2015 +0800 Init diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..afbdab33e --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 000000000..0a09a1b9c --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +EhViewer \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 000000000..28bb10af0 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 000000000..c7d1c5a83 --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 000000000..7c62b52a1 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 000000000..68bc9c08d --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 000000000..a1e308618 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,15 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 000000000..6933c1eaf --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 000000000..2f486ebd0 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 000000000..464db56d3 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/.idea/scopes/scope_settings.xml b/.idea/scopes/scope_settings.xml new file mode 100644 index 000000000..0d5175ca0 --- /dev/null +++ b/.idea/scopes/scope_settings.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 000000000..ab55cf163 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/EhViewer.iml b/EhViewer.iml new file mode 100644 index 000000000..8d4928417 --- /dev/null +++ b/EhViewer.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/app.iml b/app/app.iml new file mode 100644 index 000000000..4fc7e1434 --- /dev/null +++ b/app/app.iml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 000000000..c6e7ec635 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,31 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 21 + buildToolsVersion "21.1.2" + + defaultConfig { + applicationId "com.hippo.ehviewer" + minSdkVersion 14 + targetSdkVersion 21 + versionCode 35 + versionName "0.6.11.2" + } + buildTypes { + debug { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.android.support:support-v4:21.0.3' + compile 'com.android.support:recyclerview-v7:21.0.3' + compile 'com.github.japgolly.android:svg-android:2.0.6' +} diff --git a/app/libs/libGoogleAnalyticsServices.jar b/app/libs/libGoogleAnalyticsServices.jar new file mode 100644 index 000000000..082a9180e Binary files /dev/null and b/app/libs/libGoogleAnalyticsServices.jar differ diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 000000000..426599d56 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,49 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in E:\Android\android-sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} +-keepattributes SourceFile,LineNumberTable + +-keep class com.hippo.ehviewer.widget.SearchView {*;} + +-keep class com.hippo.ehviewer.gallery.image.Image {*;} + +-keep class com.hippo.ehviewer.gallery.image.GifImage {*;} + +-keepclassmembers class com.hippo.ehviewer.drawable.MaterialProgressDrawable$Ring { + void set*(***); + *** get*(); +} + +-keepclassmembers class * extends com.hippo.ehviewer.windowsanimate.Sprite { + void set*(***); + *** get*(); +} + +-keepclassmembers class * extends android.graphics.drawable.Drawable { + void set*(***); + *** get*(); +} + +-keepclassmembers class com.hippo.ehviewer.effect.ripple.Ripple { + void set*(***); + *** get*(); +} + +-keepclassmembers class com.hippo.ehviewer.effect.ripple.RippleBackground { + void set*(***); + *** get*(); +} diff --git a/app/src/androidTest/java/com/hippo/ehviewer/ApplicationTest.java b/app/src/androidTest/java/com/hippo/ehviewer/ApplicationTest.java new file mode 100644 index 000000000..db71e0d16 --- /dev/null +++ b/app/src/androidTest/java/com/hippo/ehviewer/ApplicationTest.java @@ -0,0 +1,13 @@ +package com.hippo.ehviewer; + +import android.app.Application; +import android.test.ApplicationTestCase; + +/** + * Testing Fundamentals + */ +public class ApplicationTest extends ApplicationTestCase { + public ApplicationTest() { + super(Application.class); + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..75b0999ea --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/assets/fonts/GloriaHallelujah.ttf b/app/src/main/assets/fonts/GloriaHallelujah.ttf new file mode 100644 index 000000000..3b42f5b38 Binary files /dev/null and b/app/src/main/assets/fonts/GloriaHallelujah.ttf differ diff --git a/app/src/main/assets/fonts/face.ttf b/app/src/main/assets/fonts/face.ttf new file mode 100644 index 000000000..2e61d8c60 Binary files /dev/null and b/app/src/main/assets/fonts/face.ttf differ diff --git a/app/src/main/java/com/hippo/ehviewer/Analytics.java b/app/src/main/java/com/hippo/ehviewer/Analytics.java new file mode 100644 index 000000000..16c82be12 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/Analytics.java @@ -0,0 +1,62 @@ +/* + * 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; + +import android.content.Context; + +import com.google.analytics.tracking.android.EasyTracker; +import com.google.analytics.tracking.android.MapBuilder; +import com.hippo.ehviewer.util.Config; + +import org.json.JSONObject; + +public class Analytics { + + private static final String KEY_GALLERY = "gallery"; + private static final String KEY_OPEN = "open"; + private static final String KEY_READ = "read"; + private static final String KEY_DOWNLOAD = "download"; + private static final String KEY_ADD_TO_FAVORITE = "add_to_favorite"; + + private static void doGallery(Context context, int gid, String token, String method) { + if (!Config.getAllowAnalyics()) + return; + + try { + JSONObject js = new JSONObject(); + js.put("gid", gid).put("token", token); + EasyTracker easyTracker = EasyTracker.getInstance(context); + easyTracker.send(MapBuilder.createEvent(KEY_GALLERY, method, js.toString(), null).build()); + } catch (Exception e) {} + } + + public static void openGallery(Context context, int gid, String token) { + doGallery(context, gid, token, KEY_OPEN); + } + + public static void readGallery(Context context, int gid, String token) { + doGallery(context, gid, token, KEY_READ); + } + + public static void downloadGallery(Context context, int gid, String token) { + doGallery(context, gid, token, KEY_DOWNLOAD); + } + + public static void addToFavoriteGallery(Context context, int gid, String token) { + doGallery(context, gid, token, KEY_ADD_TO_FAVORITE); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/AppContext.java b/app/src/main/java/com/hippo/ehviewer/AppContext.java new file mode 100644 index 000000000..fbdc521bf --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/AppContext.java @@ -0,0 +1,174 @@ +/* + * 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; + +import android.app.Application; +import android.content.Context; +import android.content.pm.PackageInfo; +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.util.Config; +import com.hippo.ehviewer.util.Crash; +import com.hippo.ehviewer.util.EhUtils; +import com.hippo.ehviewer.util.Favorite; +import com.hippo.ehviewer.util.Log; +import com.hippo.ehviewer.util.Ui; +import com.hippo.ehviewer.util.Utils; +import com.hippo.ehviewer.util.ViewUtils; +import com.hippo.ehviewer.widget.MaterialToast; +import com.hippo.ehviewer.widget.SlidingDrawerLayout; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.lang.Thread.UncaughtExceptionHandler; + +public class AppContext extends Application implements UncaughtExceptionHandler { + + @SuppressWarnings("unused") + private static final String TAG = AppContext.class.getSimpleName(); + + private Thread.UncaughtExceptionHandler mDefaultHandler; + + private Typeface mFaceTypeface; + + @Override + public void onCreate() { + super.onCreate(); + + // Do catch error prepare + mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler(); + Thread.setDefaultUncaughtExceptionHandler(this); + + Context mContextThemeWrapper = new ContextThemeWrapper(this, R.style.AppTheme); + ViewUtils.initDialogViewInflater(this); + + // Init everything + Ui.init(mContextThemeWrapper); + Config.init(mContextThemeWrapper); + Log.init(mContextThemeWrapper); + Crash.init(mContextThemeWrapper); + EhClient.createInstance(mContextThemeWrapper); + Favorite.init(mContextThemeWrapper); + Data.createInstance(mContextThemeWrapper); + ExDownloaderManager.createInstance(mContextThemeWrapper); + MaterialToast.setContext(mContextThemeWrapper); + + mFaceTypeface = Typeface.createFromAsset(getAssets(),"fonts/face.ttf"); + + // Set drawer margin + SlidingDrawerLayout.setDefaultMinDrawerMargin(Ui.ACTION_BAR_HEIGHT * 2); + + // Add .nomedia or delete it + File nomedia = new File(Config.getDownloadPath(), ".nomedia"); + if (Config.getMediaScan()) { + nomedia.delete(); + } else { + try { + new FileOutputStream(nomedia).close(); + } catch (IOException e) {} + } + + // Update proxy urls + HttpHelper.updateProxyUrls(mContextThemeWrapper); + + String keyVersionCode = "version_code"; + int oldVersion = Config.getInt(keyVersionCode, Integer.MAX_VALUE); + // Fix <=22 login error + if (oldVersion <= 22) + EhInfo.getInstance(mContextThemeWrapper).logout(); + + // Fix <=25 Update + if (oldVersion <= 25) { + File downloadDir = new File(Config.getDownloadPath()); + String cachePath = getExternalCacheDir() != null + ? getExternalCacheDir().getPath() + : getCacheDir().getPath(); + File oldEdInfoDir = new File(cachePath, "ExDownloaderManager"); + String[] list = downloadDir.list(); + if (list != null && list.length != 0) { + for (String str : list) { + int index = str.indexOf('-'); + if (index == -1) + continue; + String gid = str.substring(0, index); + File gDir = new File(downloadDir, str); + // Move download info file + File oldInfoFile = new File(oldEdInfoDir, gid); + File newInfoFile = new File(gDir, EhUtils.EH_DOWNLOAD_FILENAME); + if (!oldInfoFile.exists()) + continue; + try { + FileOutputStream fos = new FileOutputStream(newInfoFile); + // Add last page + byte[] bs = {'0', '0', '0', '0', '0', '0', '0', '0', '\n'}; + fos.write(bs); + Utils.copy(new FileInputStream(oldInfoFile), fos); + } catch (Throwable e) { + continue; + } + } + } + // Delete old exdownload info dir + Utils.deleteDirInThread(oldEdInfoDir); + } + + // Update version code + try { + PackageInfo pi= getPackageManager().getPackageInfo(getPackageName(), 0); + Config.setInt(keyVersionCode, pi.versionCode); + } catch (NameNotFoundException e) {} + } + + /** + * {@inheritDoc} + */ + @Override + public void onLowMemory() { + ImageCache.getInstance(this).evictAll(); + super.onLowMemory(); + } + + @Override + public void uncaughtException(Thread thread, Throwable ex) { + if (!handleException(ex) && mDefaultHandler != null) { + mDefaultHandler.uncaughtException(thread, ex); + } + android.os.Process.killProcess(android.os.Process.myPid()); + System.exit(1); + } + + private boolean handleException(Throwable ex) { + if (ex == null) + return false; + Crash.saveCrashInfo2File(ex); + return true; + } + + public Typeface getFaceTypeface() { + return mFaceTypeface; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/AppHandler.java b/app/src/main/java/com/hippo/ehviewer/AppHandler.java new file mode 100644 index 000000000..a0fa0ad3f --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/AppHandler.java @@ -0,0 +1,60 @@ +/* + * 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; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + +import com.hippo.ehviewer.network.HttpHelper; +import com.hippo.ehviewer.util.Constants; + +public class AppHandler extends Handler { + + public static final int HTTP_HELPER_TAG = 0x1; + + private static Handler sInstance; + + public AppHandler(Looper mainLooper) { + super(mainLooper); + } + + public final static Handler getInstance() { + if (sInstance == null) + sInstance = new AppHandler(Looper.getMainLooper()); + return sInstance; + } + + @Override + public void handleMessage(Message msg) { + switch(msg.what) { + + case HTTP_HELPER_TAG: + HttpHelper.Package p = (HttpHelper.Package)msg.obj; + HttpHelper.OnRespondListener listener = p.listener; + Object obj = p.obj; + if (msg.arg1 == Constants.TRUE) { + listener.onSuccess(obj); + } else { + listener.onFailure((String)obj); + } + break; + } + + } + +} diff --git a/app/src/main/java/com/hippo/ehviewer/ImageLoader.java b/app/src/main/java/com/hippo/ehviewer/ImageLoader.java new file mode 100644 index 000000000..f5198c0f9 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ImageLoader.java @@ -0,0 +1,181 @@ +/* + * 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; + +import android.content.Context; +import android.graphics.Bitmap; + +import com.hippo.ehviewer.cache.ImageCache; +import com.hippo.ehviewer.network.HttpHelper; +import com.hippo.ehviewer.util.BgThread; + +import java.util.Stack; + +public class ImageLoader { + @SuppressWarnings("unused") + private static final String TAG = ImageLoader.class.getSimpleName(); + + public static final int STATE_NONE = 0; + public static final int STATE_FROM_MEMORY = 1; + public static final int STATE_FROM_DISK = 2; + public static final int STATE_FROM_NETWORK = 3; + + private static ImageLoader sInstance; + + class LoadTask { + public String url; + public String key; + public OnGetImageListener listener; + public Bitmap bitmap; + public int state; + + public LoadTask(String url, String key, OnGetImageListener listener) { + this.url = url; + this.key = key; + this.listener = listener; + } + } + + private final Context mContext; + private final Stack mLoadTasks; + private final ImageCache mImageCache; + private final ImageDownloader mImageDownloader; + + private ImageLoader(Context context) { + mLoadTasks = new Stack(); + mImageDownloader = new ImageDownloader(); + + mContext = context; + mImageCache = ImageCache.getInstance(mContext); + + new BgThread(new LoadFromCacheTask()).start(); + } + + public final static ImageLoader getInstance(final Context context) { + if (sInstance == null) { + sInstance = new ImageLoader(context.getApplicationContext()); + } + return sInstance; + } + + public void add(String url, String key, OnGetImageListener listener) { + synchronized (mLoadTasks) { + mLoadTasks.push(new LoadTask(url, key, listener)); + mLoadTasks.notify(); + } + } + + private class LoadImageCallback implements Runnable { + + private final LoadTask mLoadTask; + + private LoadImageCallback(LoadTask task) { + mLoadTask = task; + } + + @Override + public void run() { + mLoadTask.listener.onGetImage(mLoadTask.key, mLoadTask.bitmap, mLoadTask.state); + } + } + + private class LoadFromCacheTask implements Runnable { + @Override + public void run() { + LoadTask loadTask; + while (true) { + synchronized (mLoadTasks) { + if (mLoadTasks.isEmpty()) { + try { + mLoadTasks.wait(); + } catch (InterruptedException e) {} + continue; + } + loadTask = mLoadTasks.pop(); + } + + int[] state = new int[1]; + String key = loadTask.key; + loadTask.bitmap = mImageCache.getCachedBitmap(key, state); + loadTask.state = state[0]; + + if (loadTask.bitmap != null) + AppHandler.getInstance().post(new LoadImageCallback(loadTask)); + else + mImageDownloader.add(loadTask); + } + } + } + + private class ImageDownloader { + private static final int MAX_DOWNLOAD_THREADS = 5; + private final Stack mDownloadTasks; + private int mWorkingThreadNum = 0; + + public ImageDownloader() { + mDownloadTasks = new Stack(); + } + + public void add(LoadTask loadTask) { + synchronized (mDownloadTasks) { + mDownloadTasks.push(loadTask); + if (mWorkingThreadNum < MAX_DOWNLOAD_THREADS) { + new BgThread(new DownloadImageTask()).start(); + mWorkingThreadNum++; + } + } + } + + private class DownloadImageTask implements Runnable { + @Override + public void run() { + LoadTask loadTask; + HttpHelper httpHelper = new HttpHelper(mContext); + while (true) { + synchronized (mDownloadTasks) { + if (mDownloadTasks.isEmpty()) { + loadTask = null; + mWorkingThreadNum--; + break; + } + loadTask = mDownloadTasks.pop(); + } + + // TODO use proxy to get image + loadTask.bitmap = httpHelper.getImage(loadTask.url); + if (loadTask.bitmap != null) { + mImageCache.addBitmapToCache(loadTask.key, loadTask.bitmap); + loadTask.state = STATE_FROM_NETWORK; + } else { + loadTask.state = STATE_NONE; + } + + AppHandler.getInstance().post(new LoadImageCallback(loadTask)); + } + } + } + } + + public interface OnGetImageListener { + /** + * bmp is null for fail + * @param key + * @param bmp + */ + void onGetImage(String key, Bitmap bmp, int state); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/NotificationId.java b/app/src/main/java/com/hippo/ehviewer/NotificationId.java new file mode 100644 index 000000000..278aa6210 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/NotificationId.java @@ -0,0 +1,22 @@ +/* + * 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; + +public class NotificationId { + public static final int UPDATE_ID = -1; + public static final int DOWNLOAD_ID = -2; +} diff --git a/app/src/main/java/com/hippo/ehviewer/SimpleSuggestionProvider.java b/app/src/main/java/com/hippo/ehviewer/SimpleSuggestionProvider.java new file mode 100644 index 000000000..981ef12dd --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/SimpleSuggestionProvider.java @@ -0,0 +1,28 @@ +/* + * 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; + +import android.content.SearchRecentSuggestionsProvider; + +public class SimpleSuggestionProvider extends SearchRecentSuggestionsProvider { + public final static String AUTHORITY = "com.hippo.ehviewer.SimpleSuggestionProvider"; + public final static int MODE = DATABASE_MODE_QUERIES; + + public SimpleSuggestionProvider() { + setupSuggestions(AUTHORITY, MODE); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/StartActivity.java b/app/src/main/java/com/hippo/ehviewer/StartActivity.java new file mode 100644 index 000000000..1633095ff --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/StartActivity.java @@ -0,0 +1,256 @@ +/* + * 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; + +import android.app.AlertDialog; +import android.content.Intent; +import android.graphics.Typeface; +import android.net.Uri; +import android.os.Bundle; +import android.text.util.Linkify; +import android.view.View; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.TextView; + +import com.hippo.ehviewer.app.MaterialAlertDialog; +import com.hippo.ehviewer.network.Network; +import com.hippo.ehviewer.ui.AbsActivity; +import com.hippo.ehviewer.ui.GalleryListActivity; +import com.hippo.ehviewer.util.Config; +import com.hippo.ehviewer.util.Crash; +import com.hippo.ehviewer.util.Secret; +import com.hippo.ehviewer.util.ViewUtils; +import com.larvalabs.svgandroid.SVGBuilder; + +public class StartActivity extends AbsActivity { + + @SuppressWarnings("unused") + private static final String TAG = StartActivity.class.getSimpleName(); + + private static final int CHECK_WARING = 0; + private static final int CHECK_ANALYTICS = 1; + private static final int CHECK_NETWORK = 2; + private static final int CHECK_CRASH = 3; + + private static final String KEY_ALLOWED = "allowed"; + private static final String KEY_SET_ANALYTICS = "set_analyics"; + + private String lastCrash; + private boolean isAnimationOver = false; + private boolean isCheckOver = false; + + private MaterialAlertDialog createWarningDialog() { + return new MaterialAlertDialog.Builder(this).setCancelable(false) + .setTitle(R.string.dailog_waring_title) + .setMessage(R.string.dailog_waring_plain) + .setPositiveButton(android.R.string.ok) + .setNegativeButton(android.R.string.cancel) + .setButtonListener(new MaterialAlertDialog.OnClickListener() { + @Override + public boolean onClick(MaterialAlertDialog dialog, int which) { + switch (which) { + case MaterialAlertDialog.POSITIVE: + Config.setBoolean(KEY_ALLOWED, true); + check(CHECK_ANALYTICS); + break; + case MaterialAlertDialog.NEGATIVE: + finish(); + break; + } + return true; + } + }).create(); + } + + private AlertDialog createAllowAnalyicsDialog() { + return new MaterialAlertDialog.Builder(this).setCancelable(false) + .setTitle(R.string.dailog_analyics_title) + .setMessageAutoLink(Linkify.WEB_URLS) + .setMessage(R.string.dailog_analyics_plain) + .setPositiveButton(android.R.string.ok) + .setNegativeButton(android.R.string.cancel) + .setButtonListener(new MaterialAlertDialog.OnClickListener() { + @Override + public boolean onClick(MaterialAlertDialog dialog, int which) { + switch (which) { + case MaterialAlertDialog.POSITIVE: + Config.setAllowAnalyics(true); + check(CHECK_NETWORK); + break; + case MaterialAlertDialog.NEGATIVE: + Config.setAllowAnalyics(false); + check(CHECK_NETWORK); + break; + } + return true; + } + }).create(); + } + + private AlertDialog createNetworkDialog(int state) { + int mesgId; + switch (state) { + case Network.NETWORK_STATE_MOBILE: + mesgId = R.string.dailog_network_mobile_title; + break; + case Network.NETWORK_STATE_NONE: + default: + mesgId = R.string.dailog_network_none_title; + break; + } + + return new MaterialAlertDialog.Builder(this).setCancelable(false) + .setTitle(R.string.warning).setMessage(mesgId) + .setPositiveButton(android.R.string.ok) + .setNegativeButton(android.R.string.cancel) + .setButtonListener(new MaterialAlertDialog.OnClickListener() { + @Override + public boolean onClick(MaterialAlertDialog dialog, int which) { + switch (which) { + case MaterialAlertDialog.POSITIVE: + check(CHECK_CRASH); + break; + case MaterialAlertDialog.NEGATIVE: + finish(); + break; + } + return true; + } + }).create(); + } + + private AlertDialog createSendCrashDialog() { + View view = ViewUtils.inflateDialogView(R.layout.send_crash, false); + final EditText et = (EditText) view.findViewById(R.id.description_of_user); + return new MaterialAlertDialog.Builder(this).setCancelable(false) + .setTitle(R.string.dialog_send_crash_title) + .setView(view, true) + .setDefaultButton(MaterialAlertDialog.POSITIVE | MaterialAlertDialog.NEGATIVE) + .setButtonListener(new MaterialAlertDialog.OnClickListener() { + @Override + public boolean onClick(MaterialAlertDialog dialog, int which) { + switch (which) { + case MaterialAlertDialog.POSITIVE: + // A wait dialog + new MaterialAlertDialog.Builder(StartActivity.this) + .setCancelable(false) + .setTitle(R.string.wait) + .setMessage(R.string.dialog_wait_send_crash_msg) + .setPositiveButton(android.R.string.ok) + .setButtonListener( + new MaterialAlertDialog.OnClickListener() { + @Override + public boolean onClick(MaterialAlertDialog dialog, int which) { + checkOver(); + return true; + } + }).show(); + Intent i = new Intent(Intent.ACTION_SENDTO); + i.setData(Uri.parse("mailto:ehviewersu@gmail.com")); + i.putExtra(Intent.EXTRA_SUBJECT, "I found a bug in EhViewer !"); + i.putExtra(Intent.EXTRA_TEXT, lastCrash + "======== Description ========\n" + et.getText().toString()); + startActivity(i); + lastCrash = null; + break; + case MaterialAlertDialog.NEGATIVE: + lastCrash = null; + checkOver(); + break; + } + return true; + } + }).create(); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + // Disable animate + overridePendingTransition(0, 0); + + super.onCreate(savedInstanceState); + setContentView(R.layout.start_activity); + + TextView text = (TextView) findViewById(R.id.text); + text.setTypeface(Typeface.createFromAsset(getAssets(),"fonts/GloriaHallelujah.ttf")); + ImageView image = (ImageView) findViewById(R.id.image); + image.setLayerType(View.LAYER_TYPE_SOFTWARE, null); + image.setImageDrawable(new SVGBuilder().readFromResource(getResources(), R.raw.sad_pandroid).build().getDrawable()); + + // Update secret image here + int state = Network.getNetworkState(this); + if (state == Network.NETWORK_STATE_WIFI) { + Secret.updateSecretImage(this); + } + + AppHandler.getInstance().postDelayed(new Runnable() { + @Override + public void run() { + if (isCheckOver) + redirectTo(); + isAnimationOver = true; + } + }, 3000); + check(CHECK_WARING); + } + + /** + * Order is 1. check waring 2. check analyics 3. check network 4. check + * crash 5. check external storage + * + * @param order + */ + private void check(int order) { + switch (order) { + case CHECK_WARING: + if (!Config.getBoolean(KEY_ALLOWED, false)) { + createWarningDialog().show(); + return; + } + case CHECK_ANALYTICS: + if (!Config.getBoolean(KEY_SET_ANALYTICS, false)) { + Config.setBoolean(KEY_SET_ANALYTICS, true); + createAllowAnalyicsDialog().show(); + return; + } + case CHECK_NETWORK: + int state = Network.getNetworkState(this); + if (state != Network.NETWORK_STATE_WIFI) { + createNetworkDialog(state).show(); + return; + } + case CHECK_CRASH: + if ((lastCrash = Crash.getLastCrash()) != null) { + createSendCrashDialog().show(); + return; + } + } + checkOver(); + } + + private void checkOver() { + if (isAnimationOver) + redirectTo(); + isCheckOver = true; + } + + private void redirectTo() { + Intent intent = new Intent(this, GalleryListActivity.class); + startActivity(intent); + finish(); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/UpdateHelper.java b/app/src/main/java/com/hippo/ehviewer/UpdateHelper.java new file mode 100644 index 000000000..a0fddc623 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/UpdateHelper.java @@ -0,0 +1,258 @@ +/* + * 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; + +import java.io.File; + +import org.json.JSONException; +import org.json.JSONObject; + +import android.app.NotificationManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.net.Uri; +import android.support.v4.app.NotificationCompat; + +import com.hippo.ehviewer.ehclient.EhClient; +import com.hippo.ehviewer.network.HttpHelper; +import com.hippo.ehviewer.util.BgThread; +import com.hippo.ehviewer.util.Config; +import com.hippo.ehviewer.util.Utils; + +// TODO return onFailure when downloading update or check update +// TODO add disable to achieve + +public class UpdateHelper implements Runnable { + @SuppressWarnings("unused") + private static final String TAG = "UpdateHelper"; + + private static final String HEADER = "Ehviewer-"; + private static final String FOOTER = ".apk"; + + + private final AppContext mAppContext; + private OnCheckUpdateListener mListener; + + private static boolean mEnabled = true; + + public interface OnCheckUpdateListener { + public void onSuccess(String version, long size, String url, String fileName, String info); + public void onNoUpdate(); + public void onFailure(String eMsg); + } + + public UpdateHelper(AppContext appContext) { + mAppContext = appContext; + } + + public UpdateHelper SetOnCheckUpdateListener(OnCheckUpdateListener listener) { + this.mListener = listener; + return this; + } + + public void autoCheckUpdate() { + if (Config.getUpdateDate() < Utils.getDate()) + checkUpdate(); + } + + public static void setEnabled(boolean enabled) { + mEnabled = enabled; + } + + + class CheckUpdatePackage { + public OnCheckUpdateListener listener; + public String body; + public String emesg; + } + + private void send(CheckUpdatePackage _package) { + AppHandler.getInstance().post(new Respond(_package)); + } + + @Override + public void run() { + CheckUpdatePackage _package = new CheckUpdatePackage(); + _package.listener = mListener; + final HttpHelper hp = new HttpHelper(mAppContext); + if (!mEnabled) { + _package.body = "working"; + send(_package); + return; + } + + mEnabled = false; + + PackageManager pm = mAppContext.getPackageManager(); + PackageInfo pi = null; + try { + pi = pm.getPackageInfo(mAppContext.getPackageName(), PackageManager.GET_ACTIVITIES); + + JSONObject jo = new JSONObject(); + jo.put("method", "update"); + + JSONObject detailJO = new JSONObject(); + detailJO.put("version", pi.versionName); + detailJO.put("server", Config.getUpdateServer()); + + jo.put("detail", detailJO); + + _package.body = hp.postJson(EhClient.API_EHVIEWER, jo); + _package.emesg = hp.getEMsg(); + send(_package); + } catch (NameNotFoundException e) { + e.printStackTrace(); + _package.body = "NameNotFound"; + send(_package); + } catch (JSONException e) { + e.printStackTrace(); + _package.body = "JSONException"; + send(_package); + } + } + + public void checkUpdate() { + new BgThread(this).start(); + } + + private class Respond implements Runnable { + + CheckUpdatePackage mPackage; + + public Respond(CheckUpdatePackage _package) { + mPackage = _package; + } + + @Override + public void run() { + OnCheckUpdateListener listener = mPackage.listener; + String body = mPackage.body; + if (body != null) { + Config.setUpdateDate(); + + if (listener == null) + return; + + if (body.equals("working")) { + listener.onFailure("正在检查更新或者下载更新"); // TODO + } else if (body.equals("NameNotFound")) { + listener.onFailure("NameNotFound"); // TODO + } else if (body.equals("JSONException")) { + listener.onFailure("JSONException"); // TODO + } else { + try { + JSONObject jo = new JSONObject(body); + JSONObject updateJO = jo.getJSONObject("update"); + + if (updateJO.has("error")) { + listener.onFailure(updateJO.getString("error")); + } else { + String version = updateJO.getString("version"); + if (version.equals("none")) { + listener.onNoUpdate(); + } else { + long size = updateJO.getLong("size"); + String url = updateJO.getString("url"); + String info = updateJO.getString("info"); + + listener.onSuccess(version, size, url, HEADER + version + FOOTER, info); + } + } + } catch (JSONException e) { + e.printStackTrace(); + listener.onFailure("JSONException"); + } + } + } + } + } + + public static class UpdateListener implements HttpHelper.OnDownloadListener { + private final Context mContext; + private final String mFileName; + + private final NotificationManager mNotifyManager; + + public UpdateListener(Context context, String fileName) { + mContext = context; + mFileName = fileName; + mNotifyManager = (NotificationManager) + mContext.getSystemService(Context.NOTIFICATION_SERVICE); + } + + @Override + public void onDownloadStartConnect() { + NotificationCompat.Builder builder = + new NotificationCompat.Builder(mContext) + .setSmallIcon(android.R.drawable.stat_sys_download) + .setContentTitle(mContext.getString(R.string.start_update)) + .setProgress(0, 0, true) + .setOngoing(true) + .setTicker(mContext.getString(R.string.start_update)); + mNotifyManager.notify(NotificationId.UPDATE_ID, builder.build()); + } + + @Override + public void onDownloadStartDownload(int totalSize) { + NotificationCompat.Builder builder = + new NotificationCompat.Builder(mContext) + .setSmallIcon(android.R.drawable.stat_sys_download) + .setContentTitle(mContext.getString(R.string.download_update)) + .setContentText(String.format("%.2f / %.2f KB", 0/1024.0f, totalSize/1024.0f)) + .setProgress(totalSize, 0, false) + .setOngoing(true); + mNotifyManager.notify(NotificationId.UPDATE_ID, builder.build()); + } + + @Override + public void onDownloadStatusUpdate( + int downloadSize, int totalSize) { + NotificationCompat.Builder builder = + new NotificationCompat.Builder(mContext) + .setSmallIcon(android.R.drawable.stat_sys_download) + .setContentTitle(mContext.getString(R.string.download_update)) + .setContentText(String.format("%.2f / %.2f KB", downloadSize/1024.0f, totalSize/1024.0f)) + .setProgress(totalSize, downloadSize, false) + .setOngoing(true); + mNotifyManager.notify(NotificationId.UPDATE_ID, builder.build()); + } + + @Override + public void onDownloadOver(int status, String eMsg) { + if (status == HttpHelper.DOWNLOAD_OK_CODE) { + mNotifyManager.cancel(NotificationId.UPDATE_ID); + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(Uri.fromFile(new File(Config.getDownloadPath(), mFileName)), + "application/vnd.android.package-archive"); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mContext.startActivity(intent); + } else { + NotificationCompat.Builder builder = + new NotificationCompat.Builder(mContext) + .setSmallIcon(android.R.drawable.stat_sys_warning) + .setContentTitle(mContext.getString(R.string.update_failed)) + .setContentText(eMsg) + .setOngoing(false); + mNotifyManager.notify(NotificationId.UPDATE_ID, builder.build()); + } + setEnabled(true); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/app/DirSelectDialog.java b/app/src/main/java/com/hippo/ehviewer/app/DirSelectDialog.java new file mode 100644 index 000000000..57a017b74 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/app/DirSelectDialog.java @@ -0,0 +1,82 @@ +/* + * 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.app; + +import android.view.View; +import android.widget.AdapterView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.util.Ui; +import com.hippo.ehviewer.util.ViewUtils; +import com.hippo.ehviewer.widget.FileExplorerView; + +public class DirSelectDialog extends MaterialAlertDialog { + + private final FileExplorerView mFileExplorerView; + + protected DirSelectDialog(Builder builder, FileExplorerView fileExplorerView) { + super(builder); + + mFileExplorerView = fileExplorerView; + } + + public static DirSelectDialog create(Builder builder, String path) { + View view = ViewUtils.inflateDialogView(R.layout.dir_selection, builder.mDarkTheme); + final FileExplorerView fileExplorerView = (FileExplorerView) view.findViewById(R.id.file_list); + final TextView warningTextView = (TextView) view.findViewById(R.id.warning); + final TextView pathTextView = (TextView) view.findViewById(R.id.path); + + if (path != null) + fileExplorerView.setPath(path); + fileExplorerView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + fileExplorerView.onItemClick(parent, view, position, id); + pathTextView.setText(fileExplorerView.getCurPath()); + if (fileExplorerView.canWrite()) + warningTextView.setVisibility(View.GONE); + else + warningTextView.setVisibility(View.VISIBLE); + } + }); + + pathTextView.setText(fileExplorerView.getCurPath()); + if (fileExplorerView.canWrite()) + warningTextView.setVisibility(View.GONE); + else + warningTextView.setVisibility(View.VISIBLE); + + builder.setView(view, false, + new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, Ui.dp2pix(360))); + + return new DirSelectDialog(builder, fileExplorerView); + } + + public String getCurrentPath() { + return mFileExplorerView.getCurPath(); + } + + public void refresh() { + mFileExplorerView.refresh(); + } + + public boolean canWrite() { + return mFileExplorerView.canWrite(); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/app/MaterialAlertDialog.java b/app/src/main/java/com/hippo/ehviewer/app/MaterialAlertDialog.java new file mode 100644 index 000000000..ae75cf85a --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/app/MaterialAlertDialog.java @@ -0,0 +1,504 @@ +/* + * 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.app; + +import android.annotation.SuppressLint; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.TextView; + +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.util.ViewUtils; + +public class MaterialAlertDialog extends AlertDialog implements + View.OnClickListener, AdapterView.OnItemClickListener { + + public final static int ACTION = 0x1; + public final static int POSITIVE = 0x2; + public final static int NEGATIVE = 0x4; + public final static int NEUTRAL = 0x8; + + private final Context mContext; + + private final Builder mBuilder; + + private View mRootView; + private TextView mTitle; + private Button mActionButton; + private Button mPositiveButton; + private Button mNegativeButton; + private Button mNeutralButton; + + protected MaterialAlertDialog(Builder builder) { + super(builder.mContext); + mContext = builder.mContext; + mBuilder = builder; + + init(); + } + + @SuppressLint("InflateParams") + private void init() { + LayoutInflater inflater = LayoutInflater.from(mContext); + mRootView = inflater.inflate(R.layout.alert_dialog_material, null); + + // Check in scroll view or not + boolean inScrollView; + if (mBuilder.mCustomView != null) + inScrollView = mBuilder.mInScrollView; + else if (mBuilder.mAdapter != null) + inScrollView = false; + else + inScrollView = true; + // Get widget + LinearLayout topPanel; + TextView message = null; + ListView list = null; + if (inScrollView) { + mRootView.findViewById(R.id.top_panel_noscroll).setVisibility( + View.GONE); + mTitle = (TextView) mRootView.findViewById(R.id.title); + topPanel = (LinearLayout) mRootView.findViewById(R.id.top_panel); + message = (TextView) mRootView.findViewById(R.id.message); + } else { + mRootView.findViewById(R.id.scroll_view).setVisibility(View.GONE); + mTitle = (TextView) mRootView.findViewById(R.id.title2); + topPanel = (LinearLayout) mRootView + .findViewById(R.id.top_panel_noscroll); + list = (ListView) mRootView.findViewById(R.id.list); + } + // Title + if (mBuilder.mTitle == null) + mTitle.setVisibility(View.GONE); + else + mTitle.setText(mBuilder.mTitle); + // Content + if (mBuilder.mCustomView != null) { + // CustomView + if (message != null) + message.setVisibility(View.GONE); + if (list != null) + list.setVisibility(View.GONE); + LinearLayout.LayoutParams lp = mBuilder.mCustomLp; + if (lp == null) + lp = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + mBuilder.mCustomView.setId(R.id.custom); + topPanel.addView(mBuilder.mCustomView, lp); + } else if (mBuilder.mAdapter != null) { + // List + if (message != null) + message.setVisibility(View.GONE); + list.setAdapter(mBuilder.mAdapter); + list.setOnItemClickListener(this); + if (mBuilder.mIsSingleChoice) { + list.setChoiceMode(ListView.CHOICE_MODE_SINGLE); + if (mBuilder.mCheckedItem > -1) { + list.setItemChecked(mBuilder.mCheckedItem, true); + list.setSelection(mBuilder.mCheckedItem); + } + } + } else { + // Message + if (list != null) + list.setVisibility(View.GONE); + if (mBuilder.mAutoLinkMask != -1) + message.setAutoLinkMask(mBuilder.mAutoLinkMask); + message.setText(mBuilder.mMessage); + } + + // Buttons + View buttonPanel = mRootView.findViewById(R.id.button_panel); + View nonActionPanel = mRootView.findViewById(R.id.non_action_panel); + mActionButton = (Button) mRootView.findViewById(R.id.button_action); + mPositiveButton = (Button) mRootView.findViewById(R.id.button_positive); + mNegativeButton = (Button) mRootView.findViewById(R.id.button_negative); + mNeutralButton = (Button) mRootView.findViewById(R.id.button_neutral); + boolean hasAction = mBuilder.mActionButtonText != null; + boolean hasNonAction = mBuilder.mPositiveButtonText != null + || mBuilder.mNegativeButtonText != null + || mBuilder.mNeutralButtonText != null; + boolean hasButton = hasAction || hasNonAction; + if (hasButton) { + if (hasAction) { + mActionButton.setText(mBuilder.mActionButtonText); + mActionButton.setOnClickListener(this); + } else { + mActionButton.setVisibility(View.GONE); + } + if (hasNonAction) { + // Positive + if (mBuilder.mPositiveButtonText != null) { + mPositiveButton.setText(mBuilder.mPositiveButtonText); + mPositiveButton.setOnClickListener(this); + } else { + mPositiveButton.setVisibility(View.GONE); + } + // Negative + if (mBuilder.mNegativeButtonText != null) { + mNegativeButton.setText(mBuilder.mNegativeButtonText); + mNegativeButton.setOnClickListener(this); + } else { + mNegativeButton.setVisibility(View.GONE); + } + // Neutral + if (mBuilder.mNeutralButtonText != null) { + mNeutralButton.setText(mBuilder.mNeutralButtonText); + mNeutralButton.setOnClickListener(this); + } else { + mNeutralButton.setVisibility(View.GONE); + } + } else { + nonActionPanel.setVisibility(View.GONE); + } + } else { + buttonPanel.setVisibility(View.GONE); + } + + setView(mRootView); + + setCancelable(mBuilder.mCancelable); + if (mBuilder.mOnCancelListener != null) + setOnCancelListener(mBuilder.mOnCancelListener); + if (mBuilder.mOnDismissListener != null) + setOnDismissListener(mBuilder.mOnDismissListener); + } + + @Override + public void setTitle(CharSequence title) { + mTitle.setText(title); + } + + @Override + public void setTitle(int resId) { + setTitle(getContext().getString(resId)); + } + + public void setView(View view, boolean inScrollView) { + setView(view, inScrollView, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT)); + } + + public void setView(View view, boolean inScrollView, + LinearLayout.LayoutParams lp) { + mRootView.findViewById(inScrollView ? R.id.top_panel_noscroll : R.id.scroll_view).setVisibility(View.GONE); + mRootView.findViewById(inScrollView ? R.id.scroll_view : R.id.top_panel_noscroll).setVisibility(View.VISIBLE); + mRootView.findViewById(R.id.message).setVisibility(View.GONE); + mRootView.findViewById(R.id.list).setVisibility(View.GONE); + LinearLayout topPanel = (LinearLayout) mRootView.findViewById(inScrollView ? R.id.top_panel : R.id.top_panel_noscroll); + topPanel.addView(view, lp); + // Title + TextView oldTitle = mTitle; + mTitle = (TextView) mRootView.findViewById(inScrollView ? R.id.title : R.id.title2); + if (mTitle != oldTitle && oldTitle != null) + mTitle.setText(oldTitle.getText()); + } + + @Override + public void onClick(View v) { + boolean close = true; + + if (mBuilder.mButtonListener != null) { + if (v == mActionButton) + close = mBuilder.mButtonListener.onClick(this, ACTION); + else if (v == mPositiveButton) + close = mBuilder.mButtonListener.onClick(this, POSITIVE); + else if (v == mNegativeButton) + close = mBuilder.mButtonListener.onClick(this, NEGATIVE); + else if (v == mNeutralButton) + close = mBuilder.mButtonListener.onClick(this, NEUTRAL); + } + + if (close) + dismiss(); + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, + long id) { + + boolean close = false; + + if (mBuilder.mOnClickListener != null) + close = mBuilder.mOnClickListener.onClick(this, position); + + if (close) + dismiss(); + } + + public static interface OnClickListener { + public boolean onClick(MaterialAlertDialog dialog, int which); + } + + public static class Builder { + + protected Context mContext; + + protected boolean mDarkTheme = false; + protected CharSequence mTitle; + protected CharSequence mMessage; + protected CharSequence mActionButtonText; + protected CharSequence mPositiveButtonText; + protected CharSequence mNegativeButtonText; + protected CharSequence mNeutralButtonText; + protected OnClickListener mButtonListener; + protected View mCustomView; + protected LinearLayout.LayoutParams mCustomLp; + protected boolean mInScrollView = true; + protected boolean mCancelable = true; + protected int mAutoLinkMask = -1; + + protected OnCancelListener mOnCancelListener; + + protected ListAdapter mAdapter; + protected OnClickListener mOnClickListener; + protected int mCheckedItem; + protected boolean mIsSingleChoice; + + protected DialogInterface.OnDismissListener mOnDismissListener; + + public Builder(Context context) { + mContext = context; + } + + public Builder setDrakTheme(boolean darkTheme) { + mDarkTheme = darkTheme; + return this; + } + + public Builder setTitle(CharSequence title) { + mTitle = title; + return this; + } + + public Builder setTitle(int resId) { + return setTitle(mContext.getString(resId)); + } + + public Builder setMessage(CharSequence message) { + mMessage = message; + return this; + } + + public Builder setMessage(int resId) { + return setMessage(mContext.getString(resId)); + } + + public Builder setActionButton(CharSequence text) { + mActionButtonText = text; + return this; + } + + public Builder setActionButton(int resId) { + return setActionButton(mContext.getString(resId)); + } + + public Builder setPositiveButton(CharSequence text) { + mPositiveButtonText = text; + return this; + } + + public Builder setPositiveButton(int resId) { + return setPositiveButton(mContext.getString(resId)); + } + + public Builder setNegativeButton(CharSequence text) { + mNegativeButtonText = text; + return this; + } + + public Builder setNegativeButton(int resId) { + return setNegativeButton(mContext.getString(resId)); + } + + public Builder setNeutralButton(CharSequence text) { + mNeutralButtonText = text; + return this; + } + + public Builder setNeutralButton(int resId) { + return setNeutralButton(mContext.getString(resId)); + } + + public Builder setDefaultButton(int b) { + if ((b & POSITIVE) == POSITIVE) + mPositiveButtonText = mContext.getString(android.R.string.ok); + if ((b & NEGATIVE) == NEGATIVE) + mNegativeButtonText = mContext.getString(android.R.string.cancel); + return this; + } + + public Builder setButtonListener(OnClickListener l) { + mButtonListener = l; + return this; + } + + /** + * Custom view id will be R.id.custom + * + * @param view + * @param inScrollView + * @return + */ + public Builder setView(View view, boolean inScrollView) { + mCustomView = view; + mInScrollView = inScrollView; + return this; + } + + /** + * Custom view id will be R.id.custom + * + * @param view + * @param inScrollView + * @param lp + * @return + */ + public Builder setView(View view, boolean inScrollView, + LinearLayout.LayoutParams lp) { + mCustomView = view; + mInScrollView = inScrollView; + mCustomLp = lp; + return this; + } + + public Builder setView(int resId, boolean inScrollView, ViewHolder vh) { + mCustomView = ViewUtils.inflateDialogView(resId, mDarkTheme); + mInScrollView = inScrollView; + if (vh != null) + vh.setView(mCustomView); + return this; + } + + public Builder setCancelable(boolean cancelable) { + mCancelable = cancelable; + return this; + } + + public Builder setOnCancelListener(OnCancelListener onCancelListener) { + mOnCancelListener = onCancelListener; + return this; + } + + public Builder setMessageAutoLink(int mask) { + mAutoLinkMask = mask; + return this; + } + + public Builder setItems(int itemsId, final OnClickListener listener) { + mAdapter = new ArrayAdapter( + mDarkTheme ? ViewUtils.sDarkContextThemeWrapper : ViewUtils.sContextThemeWrapper, + R.layout.select_dialog_item, android.R.id.text1, mContext + .getResources().getTextArray(itemsId)); + mOnClickListener = listener; + mIsSingleChoice = false; + return this; + } + + public Builder setItems(CharSequence[] items, + final OnClickListener listener) { + mAdapter = new ArrayAdapter( + mDarkTheme ? ViewUtils.sDarkContextThemeWrapper : ViewUtils.sContextThemeWrapper, + R.layout.select_dialog_item, android.R.id.text1, items); + mOnClickListener = listener; + mIsSingleChoice = false; + return this; + } + + public Builder setAdapter(final ListAdapter adapter, + final OnClickListener listener) { + mAdapter = adapter; + mOnClickListener = listener; + mIsSingleChoice = false; + return this; + } + + public Builder setSingleChoiceItems(int itemsId, int checkedItem, + final OnClickListener listener) { + mAdapter = new ArrayAdapter( + mDarkTheme ? ViewUtils.sDarkContextThemeWrapper : ViewUtils.sContextThemeWrapper, + R.layout.select_dialog_singlechoice, android.R.id.text1, + mContext.getResources().getTextArray(itemsId)); + mOnClickListener = listener; + mCheckedItem = checkedItem; + mIsSingleChoice = true; + return this; + } + + public Builder setSingleChoiceItems(CharSequence[] items, + int checkedItem, final OnClickListener listener) { + mAdapter = new ArrayAdapter( + mDarkTheme ? ViewUtils.sDarkContextThemeWrapper : ViewUtils.sContextThemeWrapper, + R.layout.select_dialog_singlechoice, android.R.id.text1, + items); + mOnClickListener = listener; + mCheckedItem = checkedItem; + mIsSingleChoice = true; + return this; + } + + public Builder setSingleChoiceItems(ListAdapter adapter, + int checkedItem, final OnClickListener listener) { + mAdapter = adapter; + mOnClickListener = listener; + mCheckedItem = checkedItem; + mIsSingleChoice = true; + return this; + } + + public Builder setOnDismissListener(DialogInterface.OnDismissListener l) { + mOnDismissListener = l; + return this; + } + + public MaterialAlertDialog create() { + mContext = new ContextThemeWrapper(mContext, mDarkTheme ? + R.style.AppTheme_Dark_Dialog : R.style.AppTheme_Dialog); + return new MaterialAlertDialog(this); + } + + public MaterialAlertDialog show() { + MaterialAlertDialog dialog = create(); + dialog.show(); + return dialog; + } + } + + public static class ViewHolder { + private View mView; + + private void setView(View v) { + mView = v; + } + + public View getView() { + return mView; + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/app/MaterialProgressDialog.java b/app/src/main/java/com/hippo/ehviewer/app/MaterialProgressDialog.java new file mode 100644 index 000000000..2fb17b543 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/app/MaterialProgressDialog.java @@ -0,0 +1,289 @@ +/* + * 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.app; + +import java.text.NumberFormat; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.Message; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.style.StyleSpan; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ProgressBar; +import android.widget.TextView; + +import com.hippo.ehviewer.R; + +public class MaterialProgressDialog extends MaterialAlertDialog { + + + /** Creates a ProgressDialog with a circular, spinning progress + * bar. This is the default. + */ + public static final int STYLE_SPINNER = 0; + + /** Creates a ProgressDialog with a horizontal progress bar. + */ + public static final int STYLE_HORIZONTAL = 1; + + private ProgressBar mProgress; + private TextView mMessageView; + + private int mProgressStyle = STYLE_HORIZONTAL; + private TextView mProgressNumber; + private String mProgressNumberFormat; + private TextView mProgressPercent; + private NumberFormat mProgressPercentFormat; + + private int mMax; + private int mProgressVal; + private int mSecondaryProgressVal; + private boolean mIndeterminate; + + private boolean mHasStarted; + private Handler mViewUpdateHandler; + + + @SuppressLint({ "InflateParams", "HandlerLeak" }) + protected MaterialProgressDialog(Builder builder) { + super(builder); + initFormats(); + + LayoutInflater inflater = LayoutInflater.from(builder.mContext); + if (mProgressStyle == STYLE_HORIZONTAL) { + + /* Use a separate handler to update the text views as they + * must be updated on the same thread that created them. + */ + mViewUpdateHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + super.handleMessage(msg); + + /* Update the number and percent */ + int progress = mProgress.getProgress(); + int max = mProgress.getMax(); + if (mProgressNumberFormat != null) { + String format = mProgressNumberFormat; + mProgressNumber.setText(String.format(format, progress, max)); + } else { + mProgressNumber.setText(""); + } + if (mProgressPercentFormat != null) { + double percent = (double) progress / (double) max; + SpannableString tmp = new SpannableString(mProgressPercentFormat.format(percent)); + tmp.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), + 0, tmp.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + mProgressPercent.setText(tmp); + } else { + mProgressPercent.setText(""); + } + } + }; + View view = inflater.inflate(R.layout.alert_dialog_progress, null); + mProgress = (ProgressBar) view.findViewById(R.id.progress); + mProgressNumber = (TextView) view.findViewById(R.id.progress_number); + mProgressPercent = (TextView) view.findViewById(R.id.progress_percent); + setView(view, false); + } else { + View view = inflater.inflate(R.layout.progress_dialog, null); + mProgress = (ProgressBar) view.findViewById(R.id.progress); + mMessageView = (TextView) view.findViewById(R.id.message); + setView(view, false); + } + } + + private void initFormats() { + mProgressNumberFormat = "%1d/%2d"; + mProgressPercentFormat = NumberFormat.getPercentInstance(); + mProgressPercentFormat.setMaximumFractionDigits(0); + } + + + public static MaterialProgressDialog create(Context context, CharSequence title) { + return create(context, title, false, null); + } + + public static MaterialProgressDialog create(Context context, CharSequence title, + boolean cancelable) { + return create(context, title, cancelable, null); + } + + public static MaterialProgressDialog create(Context context, CharSequence title, + boolean cancelable, OnCancelListener cancelListener) { + + MaterialProgressDialog.Builder builder = new MaterialProgressDialog.Builder(context); + builder.setTitle(title); + builder.setCancelable(cancelable); + builder.setOnCancelListener(cancelListener); + return new MaterialProgressDialog(builder); + } + + @Override + public void onStart() { + super.onStart(); + mHasStarted = true; + } + + @Override + protected void onStop() { + super.onStop(); + mHasStarted = false; + } + + public void setProgress(int value) { + if (mHasStarted) { + mProgress.setProgress(value); + onProgressChanged(); + } else { + mProgressVal = value; + } + } + + public void setSecondaryProgress(int secondaryProgress) { + if (mProgress != null) { + mProgress.setSecondaryProgress(secondaryProgress); + onProgressChanged(); + } else { + mSecondaryProgressVal = secondaryProgress; + } + } + + public int getProgress() { + if (mProgress != null) { + return mProgress.getProgress(); + } + return mProgressVal; + } + + public int getSecondaryProgress() { + if (mProgress != null) { + return mProgress.getSecondaryProgress(); + } + return mSecondaryProgressVal; + } + + public int getMax() { + if (mProgress != null) { + return mProgress.getMax(); + } + return mMax; + } + + public void setMax(int max) { + if (mProgress != null) { + mProgress.setMax(max); + onProgressChanged(); + } else { + mMax = max; + } + } + + public void incrementProgressBy(int diff) { + if (mProgress != null) { + mProgress.incrementProgressBy(diff); + onProgressChanged(); + } + } + + public void incrementSecondaryProgressBy(int diff) { + if (mProgress != null) { + mProgress.incrementSecondaryProgressBy(diff); + onProgressChanged(); + } + } + + public void setProgressDrawable(Drawable d) { + if (mProgress != null) { + mProgress.setProgressDrawable(d); + } + } + + public void setIndeterminateDrawable(Drawable d) { + if (mProgress != null) { + mProgress.setIndeterminateDrawable(d); + } + } + + public void setIndeterminate(boolean indeterminate) { + if (mProgress != null) { + mProgress.setIndeterminate(indeterminate); + } + } + + public boolean isIndeterminate() { + if (mProgress != null) { + return mProgress.isIndeterminate(); + } + return mIndeterminate; + } + + @Override + public void setMessage(CharSequence message) { + if (mProgress != null) { + if (mProgressStyle == STYLE_HORIZONTAL) { + super.setMessage(message); + } else { + mMessageView.setText(message); + } + } + } + + public void setProgressStyle(int style) { + mProgressStyle = style; + } + + /** + * Change the format of the small text showing current and maximum units + * of progress. The default is "%1d/%2d". + * Should not be called during the number is progressing. + * @param format A string passed to {@link String#format String.format()}; + * use "%1d" for the current number and "%2d" for the maximum. If null, + * nothing will be shown. + */ + public void setProgressNumberFormat(String format) { + mProgressNumberFormat = format; + onProgressChanged(); + } + + /** + * Change the format of the small text showing the percentage of progress. + * The default is + * {@link NumberFormat#getPercentInstance() NumberFormat.getPercentageInstnace().} + * Should not be called during the number is progressing. + * @param format An instance of a {@link NumberFormat} to generate the + * percentage text. If null, nothing will be shown. + */ + public void setProgressPercentFormat(NumberFormat format) { + mProgressPercentFormat = format; + onProgressChanged(); + } + + private void onProgressChanged() { + if (mProgressStyle == STYLE_HORIZONTAL) { + if (mViewUpdateHandler != null && !mViewUpdateHandler.hasMessages(0)) { + mViewUpdateHandler.sendEmptyMessage(0); + } + } + } + +} diff --git a/app/src/main/java/com/hippo/ehviewer/cache/DiskLruCache.java b/app/src/main/java/com/hippo/ehviewer/cache/DiskLruCache.java new file mode 100644 index 000000000..f351fc180 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/cache/DiskLruCache.java @@ -0,0 +1,969 @@ +/* + * 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; + +import java.io.BufferedInputStream; +import java.io.BufferedWriter; +import java.io.Closeable; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.StringWriter; +import java.io.Writer; +import java.lang.reflect.Array; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + ****************************************************************************** Taken from the JB source code, can be found in: + * libcore/luni/src/main/java/libcore/io/DiskLruCache.java or direct link: + * https: + * //android.googlesource.com/platform/libcore/+/android-4.1.1_r1/luni/src/ + * main/java/libcore/io/DiskLruCache.java A cache that uses a bounded amount of + * space on a filesystem. Each cache entry has a string key and a fixed number + * of values. Values are byte sequences, accessible as streams or files. Each + * value must be between {@code 0} and {@code Integer.MAX_VALUE} bytes in + * length. + *

+ * The cache stores its data in a directory on the filesystem. This directory + * must be exclusive to the cache; the cache may delete or overwrite files from + * its directory. It is an error for multiple processes to use the same cache + * directory at the same time. + *

+ * This cache limits the number of bytes that it will store on the filesystem. + * When the number of stored bytes exceeds the limit, the cache will remove + * entries in the background until the limit is satisfied. The limit is not + * strict: the cache may temporarily exceed it while waiting for files to be + * deleted. The limit does not include filesystem overhead or the cache journal + * so space-sensitive applications should set a conservative limit. + *

+ * Clients call {@link #edit} to create or update the values of an entry. An + * entry may have only one editor at one time; if a value is not available to be + * edited then {@link #edit} will return null. + *

    + *
  • When an entry is being created it is necessary to supply + * a full set of values; the empty value should be used as a placeholder if + * necessary. + *
  • When an entry is being edited, it is not necessary to + * supply data for every value; values default to their previous value. + *
+ * Every {@link #edit} call must be matched by a call to {@link Editor#commit} + * or {@link Editor#abort}. Committing is atomic: a read observes the full set + * of values as they were before or after the commit, but never a mix of values. + *

+ * Clients call {@link #get} to read a snapshot of an entry. The read will + * observe the value at the time that {@link #get} was called. Updates and + * removals after the call do not impact ongoing reads. + *

+ * This class is tolerant of some I/O errors. If files are missing from the + * filesystem, the corresponding entries will be dropped from the cache. If an + * error occurs while writing a cache value, the edit will fail silently. + * Callers should handle other problems by catching {@code IOException} and + * responding appropriately. + */ +public final class DiskLruCache implements Closeable { + static final String JOURNAL_FILE = "journal"; + + static final String JOURNAL_FILE_TMP = "journal.tmp"; + + static final String MAGIC = "libcore.io.DiskLruCache"; + + static final String VERSION_1 = "1"; + + static final long ANY_SEQUENCE_NUMBER = -1; + + private static final String CLEAN = "CLEAN"; + + private static final String DIRTY = "DIRTY"; + + private static final String REMOVE = "REMOVE"; + + private static final String READ = "READ"; + + private static final Charset UTF_8 = Charset.forName("UTF-8"); + + private static final int IO_BUFFER_SIZE = 8 * 1024; + + /* + * This cache uses a journal file named "journal". A typical journal file + * looks like this: libcore.io.DiskLruCache 1 100 2 CLEAN + * 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 DIRTY + * 335c4c6028171cfddfbaae1a9c313c52 CLEAN 335c4c6028171cfddfbaae1a9c313c52 + * 3934 2342 REMOVE 335c4c6028171cfddfbaae1a9c313c52 DIRTY + * 1ab96a171faeeee38496d8b330771a7a CLEAN 1ab96a171faeeee38496d8b330771a7a + * 1600 234 READ 335c4c6028171cfddfbaae1a9c313c52 READ + * 3400330d1dfc7f3f7f4b8d4d803dfcf6 The first five lines of the journal form + * its header. They are the constant string "libcore.io.DiskLruCache", the + * disk cache's version, the application's version, the value count, and a + * blank line. Each of the subsequent lines in the file is a record of the + * state of a cache entry. Each line contains space-separated values: a + * state, a key, and optional state-specific values. o DIRTY lines track + * that an entry is actively being created or updated. Every successful + * DIRTY action should be followed by a CLEAN or REMOVE action. DIRTY lines + * without a matching CLEAN or REMOVE indicate that temporary files may need + * to be deleted. o CLEAN lines track a cache entry that has been + * successfully published and may be read. A publish line is followed by the + * lengths of each of its values. o READ lines track accesses for LRU. o + * REMOVE lines track entries that have been deleted. The journal file is + * appended to as cache operations occur. The journal may occasionally be + * compacted by dropping redundant lines. A temporary file named + * "journal.tmp" will be used during compaction; that file should be deleted + * if it exists when the cache is opened. + */ + + private final File directory; + + private final File journalFile; + + private final File journalFileTmp; + + private final int appVersion; + + private final long maxSize; + + private final int valueCount; + + private long size = 0; + + private Writer journalWriter; + + private final LinkedHashMap lruEntries = new LinkedHashMap(0, + 0.75f, true); + + private int redundantOpCount; + + /** + * To differentiate between old and current snapshots, each entry is given a + * sequence number each time an edit is committed. A snapshot is stale if + * its sequence number is not equal to its entry's sequence number. + */ + private long nextSequenceNumber = 0; + + /* From java.util.Arrays */ + @SuppressWarnings("unchecked") + private static T[] copyOfRange(final T[] original, final int start, final int end) { + final int originalLength = original.length; // For exception priority + // compatibility. + if (start > end) { + throw new IllegalArgumentException(); + } + if (start < 0 || start > originalLength) { + throw new ArrayIndexOutOfBoundsException(); + } + final int resultLength = end - start; + final int copyLength = Math.min(resultLength, originalLength - start); + final T[] result = (T[])Array.newInstance(original.getClass().getComponentType(), + resultLength); + System.arraycopy(original, start, result, 0, copyLength); + return result; + } + + /** + * Returns the remainder of 'reader' as a string, closing it when done. + */ + public static String readFully(final Reader reader) throws IOException { + try { + final StringWriter writer = new StringWriter(); + final char[] buffer = new char[1024]; + int count; + while ((count = reader.read(buffer)) != -1) { + writer.write(buffer, 0, count); + } + return writer.toString(); + } finally { + reader.close(); + } + } + + /** + * Returns the ASCII characters up to but not including the next "\r\n", or + * "\n". + * + * @throws java.io.EOFException if the stream is exhausted before the next + * newline character. + */ + public static String readAsciiLine(final InputStream in) throws IOException { + // TODO: support UTF-8 here instead + + final StringBuilder result = new StringBuilder(80); + while (true) { + final int c = in.read(); + if (c == -1) { + throw new EOFException(); + } else if (c == '\n') { + break; + } + + result.append((char)c); + } + final int length = result.length(); + if (length > 0 && result.charAt(length - 1) == '\r') { + result.setLength(length - 1); + } + return result.toString(); + } + + /** + * Closes 'closeable', ignoring any checked exceptions. Does nothing if + * 'closeable' is null. + */ + public static void closeQuietly(final Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (final RuntimeException rethrown) { + throw rethrown; + } catch (final Exception ignored) { + } + } + } + + /** + * Recursively delete everything in {@code dir}. + */ + // TODO: this should specify paths as Strings rather than as Files + public static void deleteContents(final File dir) throws IOException { + final File[] files = dir.listFiles(); + if (files == null) { + throw new IllegalArgumentException("not a directory: " + dir); + } + for (final File file : files) { + if (file.isDirectory()) { + deleteContents(file); + } + if (!file.delete()) { + throw new IOException("failed to delete file: " + file); + } + } + } + + /** This cache uses a single background thread to evict entries. */ + private final ExecutorService executorService = new ThreadPoolExecutor(0, 1, 60L, + TimeUnit.SECONDS, new LinkedBlockingQueue()); + + private final Callable cleanupCallable = new Callable() { + @Override + public Void call() throws Exception { + synchronized (DiskLruCache.this) { + if (journalWriter == null) { + return null; // closed + } + trimToSize(); + if (journalRebuildRequired()) { + rebuildJournal(); + redundantOpCount = 0; + } + } + return null; + } + }; + + private DiskLruCache(final File directory, final int appVersion, final int valueCount, + final long maxSize) { + this.directory = directory; + this.appVersion = appVersion; + journalFile = new File(directory, JOURNAL_FILE); + journalFileTmp = new File(directory, JOURNAL_FILE_TMP); + this.valueCount = valueCount; + this.maxSize = maxSize; + } + + /** + * Opens the cache in {@code directory}, creating a cache if none exists + * there. + * + * @param directory a writable directory + * @param appVersion + * @param valueCount the number of values per cache entry. Must be positive. + * @param maxSize the maximum number of bytes this cache should use to store + * @throws IOException if reading or writing the cache directory fails + */ + public static DiskLruCache open(final File directory, final int appVersion, + final int valueCount, final long maxSize) throws IOException { + if (maxSize <= 0) { + throw new IllegalArgumentException("maxSize <= 0"); + } + if (valueCount <= 0) { + throw new IllegalArgumentException("valueCount <= 0"); + } + + // prefer to pick up where we left off + DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); + if (cache.journalFile.exists()) { + try { + cache.readJournal(); + cache.processJournal(); + cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true), + IO_BUFFER_SIZE); + return cache; + } catch (final IOException journalIsCorrupt) { + // System.logW("DiskLruCache " + directory + " is corrupt: " + // + journalIsCorrupt.getMessage() + ", removing"); + cache.delete(); + } + } + + // create a new empty cache + directory.mkdirs(); + cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); + cache.rebuildJournal(); + return cache; + } + + private void readJournal() throws IOException { + final InputStream in = new BufferedInputStream(new FileInputStream(journalFile), + IO_BUFFER_SIZE); + try { + final String magic = readAsciiLine(in); + final String version = readAsciiLine(in); + final String appVersionString = readAsciiLine(in); + final String valueCountString = readAsciiLine(in); + final String blank = readAsciiLine(in); + if (!MAGIC.equals(magic) || !VERSION_1.equals(version) + || !Integer.toString(appVersion).equals(appVersionString) + || !Integer.toString(valueCount).equals(valueCountString) || !"".equals(blank)) { + throw new IOException("unexpected journal header: [" + magic + ", " + version + + ", " + valueCountString + ", " + blank + "]"); + } + + while (true) { + try { + readJournalLine(readAsciiLine(in)); + } catch (final EOFException endOfJournal) { + break; + } + } + } finally { + closeQuietly(in); + } + } + + private void readJournalLine(final String line) throws IOException { + final String[] parts = line.split(" "); + if (parts.length < 2) { + throw new IOException("unexpected journal line: " + line); + } + + final String key = parts[1]; + if (parts[0].equals(REMOVE) && parts.length == 2) { + lruEntries.remove(key); + return; + } + + Entry entry = lruEntries.get(key); + if (entry == null) { + entry = new Entry(key); + lruEntries.put(key, entry); + } + + if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount) { + entry.readable = true; + entry.currentEditor = null; + entry.setLengths(copyOfRange(parts, 2, parts.length)); + } else if (parts[0].equals(DIRTY) && parts.length == 2) { + entry.currentEditor = new Editor(entry); + } else if (parts[0].equals(READ) && parts.length == 2) { + // this work was already done by calling lruEntries.get() + } else { + throw new IOException("unexpected journal line: " + line); + } + } + + /** + * Computes the initial size and collects garbage as a part of opening the + * cache. Dirty entries are assumed to be inconsistent and will be deleted. + */ + private void processJournal() throws IOException { + deleteIfExists(journalFileTmp); + for (final Iterator i = lruEntries.values().iterator(); i.hasNext();) { + final Entry entry = i.next(); + if (entry.currentEditor == null) { + for (int t = 0; t < valueCount; t++) { + size += entry.lengths[t]; + } + } else { + entry.currentEditor = null; + for (int t = 0; t < valueCount; t++) { + deleteIfExists(entry.getCleanFile(t)); + deleteIfExists(entry.getDirtyFile(t)); + } + i.remove(); + } + } + } + + /** + * Creates a new journal that omits redundant information. This replaces the + * current journal if it exists. + */ + private synchronized void rebuildJournal() throws IOException { + if (journalWriter != null) { + journalWriter.close(); + } + + final Writer writer = new BufferedWriter(new FileWriter(journalFileTmp), IO_BUFFER_SIZE); + writer.write(MAGIC); + writer.write("\n"); + writer.write(VERSION_1); + writer.write("\n"); + writer.write(Integer.toString(appVersion)); + writer.write("\n"); + writer.write(Integer.toString(valueCount)); + writer.write("\n"); + writer.write("\n"); + + for (final Entry entry : lruEntries.values()) { + if (entry.currentEditor != null) { + writer.write(DIRTY + ' ' + entry.key + '\n'); + } else { + writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); + } + } + + writer.close(); + journalFileTmp.renameTo(journalFile); + journalWriter = new BufferedWriter(new FileWriter(journalFile, true), IO_BUFFER_SIZE); + } + + private static void deleteIfExists(final File file) throws IOException { + // try { + // Libcore.os.remove(file.getPath()); + // } catch (ErrnoException errnoException) { + // if (errnoException.errno != OsConstants.ENOENT) { + // throw errnoException.rethrowAsIOException(); + // } + // } + if (file.exists() && !file.delete()) { + throw new IOException(); + } + } + + /** + * Returns a snapshot of the entry named {@code key}, or null if it doesn't + * exist is not currently readable. If a value is returned, it is moved to + * the head of the LRU queue. + */ + public synchronized Snapshot get(final String key) throws IOException { + checkNotClosed(); + validateKey(key); + final Entry entry = lruEntries.get(key); + if (entry == null) { + return null; + } + + if (!entry.readable) { + return null; + } + + /* + * Open all streams eagerly to guarantee that we see a single published + * snapshot. If we opened streams lazily then the streams could come + * from different edits. + */ + final InputStream[] ins = new InputStream[valueCount]; + try { + for (int i = 0; i < valueCount; i++) { + ins[i] = new FileInputStream(entry.getCleanFile(i)); + } + } catch (final FileNotFoundException e) { + // a file must have been deleted manually! + return null; + } + + redundantOpCount++; + journalWriter.append(READ + ' ' + key + '\n'); + if (journalRebuildRequired()) { + executorService.submit(cleanupCallable); + } + + return new Snapshot(key, entry.sequenceNumber, ins); + } + + /** + * Returns an editor for the entry named {@code key}, or null if another + * edit is in progress. + */ + public Editor edit(final String key) throws IOException { + return edit(key, ANY_SEQUENCE_NUMBER); + } + + private synchronized Editor edit(final String key, final long expectedSequenceNumber) + throws IOException { + checkNotClosed(); + validateKey(key); + Entry entry = lruEntries.get(key); + if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER + && (entry == null || entry.sequenceNumber != expectedSequenceNumber)) { + return null; // snapshot is stale + } + if (entry == null) { + entry = new Entry(key); + lruEntries.put(key, entry); + } else if (entry.currentEditor != null) { + return null; // another edit is in progress + } + + final Editor editor = new Editor(entry); + entry.currentEditor = editor; + + // flush the journal before creating files to prevent file leaks + journalWriter.write(DIRTY + ' ' + key + '\n'); + journalWriter.flush(); + return editor; + } + + /** + * Returns the directory where this cache stores its data. + */ + public File getDirectory() { + return directory; + } + + /** + * Returns the maximum number of bytes that this cache should use to store + * its data. + */ + public long maxSize() { + return maxSize; + } + + /** + * Returns the number of bytes currently being used to store the values in + * this cache. This may be greater than the max size if a background + * deletion is pending. + */ + public synchronized long size() { + return size; + } + + private synchronized void completeEdit(final Editor editor, final boolean success) + throws IOException { + final Entry entry = editor.entry; + if (entry.currentEditor != editor) { + throw new IllegalStateException(); + } + + // if this edit is creating the entry for the first time, every index + // must have a value + if (success && !entry.readable) { + for (int i = 0; i < valueCount; i++) { + if (!entry.getDirtyFile(i).exists()) { + editor.abort(); + throw new IllegalStateException("edit didn't create file " + i); + } + } + } + + for (int i = 0; i < valueCount; i++) { + final File dirty = entry.getDirtyFile(i); + if (success) { + if (dirty.exists()) { + final File clean = entry.getCleanFile(i); + dirty.renameTo(clean); + final long oldLength = entry.lengths[i]; + final long newLength = clean.length(); + entry.lengths[i] = newLength; + size = size - oldLength + newLength; + } + } else { + deleteIfExists(dirty); + } + } + + redundantOpCount++; + entry.currentEditor = null; + if (entry.readable | success) { + entry.readable = true; + journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); + if (success) { + entry.sequenceNumber = nextSequenceNumber++; + } + } else { + lruEntries.remove(entry.key); + journalWriter.write(REMOVE + ' ' + entry.key + '\n'); + } + + if (size > maxSize || journalRebuildRequired()) { + executorService.submit(cleanupCallable); + } + } + + /** + * We only rebuild the journal when it will halve the size of the journal + * and eliminate at least 2000 ops. + */ + private boolean journalRebuildRequired() { + final int REDUNDANT_OP_COMPACT_THRESHOLD = 2000; + return redundantOpCount >= REDUNDANT_OP_COMPACT_THRESHOLD + && redundantOpCount >= lruEntries.size(); + } + + /** + * Drops the entry for {@code key} if it exists and can be removed. Entries + * actively being edited cannot be removed. + * + * @return true if an entry was removed. + */ + public synchronized boolean remove(final String key) throws IOException { + checkNotClosed(); + validateKey(key); + final Entry entry = lruEntries.get(key); + if (entry == null || entry.currentEditor != null) { + return false; + } + + for (int i = 0; i < valueCount; i++) { + final File file = entry.getCleanFile(i); + if (!file.delete()) { + throw new IOException("failed to delete " + file); + } + size -= entry.lengths[i]; + entry.lengths[i] = 0; + } + + redundantOpCount++; + journalWriter.append(REMOVE + ' ' + key + '\n'); + lruEntries.remove(key); + + if (journalRebuildRequired()) { + executorService.submit(cleanupCallable); + } + + return true; + } + + /** + * Returns true if this cache has been closed. + */ + public boolean isClosed() { + return journalWriter == null; + } + + private void checkNotClosed() { + if (journalWriter == null) { + throw new IllegalStateException("cache is closed"); + } + } + + /** + * Force buffered operations to the filesystem. + */ + public synchronized void flush() throws IOException { + checkNotClosed(); + trimToSize(); + journalWriter.flush(); + } + + /** + * Closes this cache. Stored values will remain on the filesystem. + */ + @Override + public synchronized void close() throws IOException { + if (journalWriter == null) { + return; // already closed + } + for (final Entry entry : new ArrayList(lruEntries.values())) { + if (entry.currentEditor != null) { + entry.currentEditor.abort(); + } + } + trimToSize(); + journalWriter.close(); + journalWriter = null; + } + + private void trimToSize() throws IOException { + while (size > maxSize) { + // Map.Entry toEvict = lruEntries.eldest(); + final Map.Entry toEvict = lruEntries.entrySet().iterator().next(); + remove(toEvict.getKey()); + } + } + + /** + * Closes the cache and deletes all of its stored values. This will delete + * all files in the cache directory including files that weren't created by + * the cache. + */ + public void delete() throws IOException { + close(); + deleteContents(directory); + } + + private void validateKey(final String key) { + if (key.contains(" ") || key.contains("\n") || key.contains("\r")) { + throw new IllegalArgumentException("keys must not contain spaces or newlines: \"" + key + + "\""); + } + } + + private static String inputStreamToString(final InputStream in) throws IOException { + return readFully(new InputStreamReader(in, UTF_8)); + } + + /** + * A snapshot of the values for an entry. + */ + public final class Snapshot implements Closeable { + private final String key; + + private final long sequenceNumber; + + private final InputStream[] ins; + + private Snapshot(final String key, final long sequenceNumber, final InputStream[] ins) { + this.key = key; + this.sequenceNumber = sequenceNumber; + this.ins = ins; + } + + /** + * Returns an editor for this snapshot's entry, or null if either the + * entry has changed since this snapshot was created or if another edit + * is in progress. + */ + public Editor edit() throws IOException { + return DiskLruCache.this.edit(key, sequenceNumber); + } + + /** + * Returns the unbuffered stream with the value for {@code index}. + */ + public InputStream getInputStream(final int index) { + return ins[index]; + } + + /** + * Returns the string value for {@code index}. + */ + public String getString(final int index) throws IOException { + return inputStreamToString(getInputStream(index)); + } + + @Override + public void close() { + for (final InputStream in : ins) { + closeQuietly(in); + } + } + } + + /** + * Edits the values for an entry. + */ + public final class Editor { + private final Entry entry; + + private boolean hasErrors; + + private Editor(final Entry entry) { + this.entry = entry; + } + + /** + * Returns an unbuffered input stream to read the last committed value, + * or null if no value has been committed. + */ + public InputStream newInputStream(final int index) throws IOException { + synchronized (DiskLruCache.this) { + if (entry.currentEditor != this) { + throw new IllegalStateException(); + } + if (!entry.readable) { + return null; + } + return new FileInputStream(entry.getCleanFile(index)); + } + } + + /** + * Returns the last committed value as a string, or null if no value has + * been committed. + */ + public String getString(final int index) throws IOException { + final InputStream in = newInputStream(index); + return in != null ? inputStreamToString(in) : null; + } + + /** + * Returns a new unbuffered output stream to write the value at + * {@code index}. If the underlying output stream encounters errors when + * writing to the filesystem, this edit will be aborted when + * {@link #commit} is called. The returned output stream does not throw + * IOExceptions. + */ + public OutputStream newOutputStream(final int index) throws IOException { + synchronized (DiskLruCache.this) { + if (entry.currentEditor != this) { + throw new IllegalStateException(); + } + return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index))); + } + } + + /** + * Sets the value at {@code index} to {@code value}. + */ + public void set(final int index, final String value) throws IOException { + Writer writer = null; + try { + writer = new OutputStreamWriter(newOutputStream(index), UTF_8); + writer.write(value); + } finally { + closeQuietly(writer); + } + } + + /** + * Commits this edit so it is visible to readers. This releases the edit + * lock so another edit may be started on the same key. + */ + public void commit() throws IOException { + if (hasErrors) { + completeEdit(this, false); + remove(entry.key); // the previous entry is stale + } else { + completeEdit(this, true); + } + } + + /** + * Aborts this edit. This releases the edit lock so another edit may be + * started on the same key. + */ + public void abort() throws IOException { + completeEdit(this, false); + } + + private class FaultHidingOutputStream extends FilterOutputStream { + private FaultHidingOutputStream(final OutputStream out) { + super(out); + } + + @Override + public void write(final int oneByte) { + try { + out.write(oneByte); + } catch (final IOException e) { + hasErrors = true; + } + } + + @Override + public void write(final byte[] buffer, final int offset, final int length) { + try { + out.write(buffer, offset, length); + } catch (final IOException e) { + hasErrors = true; + } + } + + @Override + public void close() { + try { + out.close(); + } catch (final IOException e) { + hasErrors = true; + } + } + + @Override + public void flush() { + try { + out.flush(); + } catch (final IOException e) { + hasErrors = true; + } + } + } + } + + private final class Entry { + private final String key; + + /** Lengths of this entry's files. */ + private final long[] lengths; + + /** True if this entry has ever been published */ + private boolean readable; + + /** The ongoing edit or null if this entry is not being edited. */ + private Editor currentEditor; + + /** + * The sequence number of the most recently committed edit to this + * entry. + */ + private long sequenceNumber; + + private Entry(final String key) { + this.key = key; + lengths = new long[valueCount]; + } + + public String getLengths() throws IOException { + final StringBuilder result = new StringBuilder(); + for (final long size : lengths) { + result.append(' ').append(size); + } + return result.toString(); + } + + /** + * Set lengths using decimal numbers like "10123". + */ + private void setLengths(final String[] strings) throws IOException { + if (strings.length != valueCount) { + throw invalidLengths(strings); + } + + try { + for (int i = 0; i < strings.length; i++) { + lengths[i] = Long.parseLong(strings[i]); + } + } catch (final NumberFormatException e) { + throw invalidLengths(strings); + } + } + + private IOException invalidLengths(final String[] strings) throws IOException { + throw new IOException("unexpected journal line: " + Arrays.toString(strings)); + } + + public File getCleanFile(final int i) { + return new File(directory, key + "." + i); + } + + public File getDirtyFile(final int i) { + return new File(directory, key + "." + i + ".tmp"); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/cache/ImageCache.java b/app/src/main/java/com/hippo/ehviewer/cache/ImageCache.java new file mode 100644 index 000000000..f7bf74c5e --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/cache/ImageCache.java @@ -0,0 +1,721 @@ +/* + * 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. + */ + +package com.hippo.ehviewer.cache; + +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; + +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(); + + /** + * 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; + + /** + * Image compression quality + */ + private static final int COMPRESS_QUALITY = 98; + + /** + * LRU cache + */ + private MemoryCache mLruCache; + + /** + * Disk LRU cache + */ + private DiskLruCache mDiskCache; + + private static ImageCache sInstance; + + /** + * Used to temporarily pause the disk cache while scrolling + */ + public boolean mPauseDiskAccess = false; + private final Object mPauseLock = new Object(); + + /** + * Constructor of ImageCache + * + * @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() { + + @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; + } + } + } + } + } + + /** + * Sets up the Lru cache + * + * @param context The {@link Context} to use + */ + @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() { + + @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() { + + @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() { + + @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; + } + + /** + * Get a usable cache directory (external if available, internal otherwise) + * + * @param context The {@link Context} to use + * @param uniqueName A unique directory name to append to the cache + * directory + * @return The cache directory + */ + public static final 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(); + 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'); + } + 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} + */ + @Override + public void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // Make sure this Fragment is retained over a configuration change + setRetainInstance(true); + } + + /** + * 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 { + + /** + * Constructor of MemoryCache + * + * @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); + } + + } + +} diff --git a/app/src/main/java/com/hippo/ehviewer/cache/LruCache.java b/app/src/main/java/com/hippo/ehviewer/cache/LruCache.java new file mode 100644 index 000000000..448e8a059 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/cache/LruCache.java @@ -0,0 +1,333 @@ +/* + * 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 { + + private final LinkedHashMap 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(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 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. + *

+ * 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. + *

+ * The method is called without synchronization: other threads may access + * the cache while this method is executing. + *

+ * 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. + *

+ * 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 snapshot() { + return new LinkedHashMap(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); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/cardview/CardViewSalon.java b/app/src/main/java/com/hippo/ehviewer/cardview/CardViewSalon.java new file mode 100644 index 000000000..df423fcda --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/cardview/CardViewSalon.java @@ -0,0 +1,54 @@ +/* + * 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.cardview; + +import android.view.View; + +/** + * @author Hippo + * + * A salon to make views look like card + */ +public final class CardViewSalon { + + @SuppressWarnings("deprecation") + public static void reform(View view, int bgColor, int boundColor) { + view.setBackgroundDrawable(new RoundRectDrawable( + bgColor, boundColor)); + } + + @SuppressWarnings("deprecation") + public static void reform(View view, int[][] stateSets, int[] bgColors, + int[] boundColors) { + view.setBackgroundDrawable(new RoundRectDrawable( + stateSets, bgColors, boundColors)); + } + + @SuppressWarnings("deprecation") + public static void reformWithShadow(View view, int bgColor, int boundColor, + boolean keepPadding) { + view.setBackgroundDrawable(new RoundRectDrawableWithShadow( + bgColor, boundColor, keepPadding)); + } + + @SuppressWarnings("deprecation") + public static void reformWithShadow(View view, int[][] stateSets, int[] bgColors, + int[] boundColors, boolean keepPadding) { + view.setBackgroundDrawable(new RoundRectDrawableWithShadow( + stateSets, bgColors, boundColors, keepPadding)); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/cardview/RoundRectDrawable.java b/app/src/main/java/com/hippo/ehviewer/cardview/RoundRectDrawable.java new file mode 100644 index 000000000..cb910255f --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/cardview/RoundRectDrawable.java @@ -0,0 +1,164 @@ +/* + * 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.cardview; + +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.util.StateSet; + +import com.hippo.ehviewer.util.Ui; + +// Get most code from android.support.v7.widget.CardViewEclairMr1 +public class RoundRectDrawable extends Drawable { + + protected final float mCornerRadius; + private final int[][] mStateSets; + private final int[] mBgColors; + private final int[] mBoundColors; + + private final Paint mBgPaint; + private final Paint mBoundPaint; + + private int mLastIndex = -1; + protected final RectF mTempRectF = new RectF(); + + public RoundRectDrawable(int bgColor, int boundColor) { + mCornerRadius = Ui.dp2pix(2); + mStateSets = null; + mBgColors = null; + mBoundColors = null; + + mBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); + mBgPaint.setColor(bgColor); + if (boundColor == 0) { + mBoundPaint = null; + } else { + mBoundPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); + mBoundPaint.setColor(boundColor); + } + } + + public RoundRectDrawable(int[][] stateSets, int[] bgColors, int[] boundColors) { + mCornerRadius = Ui.dp2pix(2); + mStateSets = stateSets; + mBgColors = bgColors; + mBoundColors = boundColors; + + mBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); + if (mBoundColors == null) { + mBoundPaint = null; + } else { + mBoundPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); + } + } + + @Override + public boolean isStateful() { + return mStateSets != null; + } + + @Override + protected boolean onStateChange(int[] stateSet) { + final int N = mStateSets.length; + for (int i = 0; i < N; i++) { + if (StateSet.stateSetMatches(mStateSets[i], stateSet)) { + if (mLastIndex == i) + return false; + mLastIndex = i; + + mBgPaint.setColor(mBgColors[i]); + if (mBoundColors != null) + mBoundPaint.setColor(mBoundColors[i]); + invalidateSelf(); + return true; + } + } + return false; + } + + @Override + public void draw(Canvas canvas) { + drawBody(canvas, new RectF(getBounds())); + } + + protected void drawBody(Canvas canvas, RectF bounds) { + if (mBoundPaint == null && Build.VERSION.SDK_INT >= 17) { + canvas.drawRoundRect(bounds, mCornerRadius, mCornerRadius, mBgPaint); + + } else { + final float twoRadius = mCornerRadius * 2; + final float innerWidth = bounds.width() - twoRadius; + final float innerHeight = bounds.height() - twoRadius; + Paint drawBoundsPaint = mBoundPaint == null ? mBgPaint : mBoundPaint; + + // Draw four corner + mTempRectF.set(bounds.left, bounds.top, + bounds.left + mCornerRadius * 2, bounds.top + mCornerRadius * 2); + canvas.drawArc(mTempRectF, 180, 90, true, drawBoundsPaint); + mTempRectF.offset(innerWidth, 0); + canvas.drawArc(mTempRectF, 270, 90, true, drawBoundsPaint); + mTempRectF.offset(0, innerHeight); + canvas.drawArc(mTempRectF, 0, 90, true, drawBoundsPaint); + mTempRectF.offset(-innerWidth, 0); + canvas.drawArc(mTempRectF, 90, 90, true, drawBoundsPaint); + + //draw top and bottom pieces + canvas.drawRect(bounds.left + mCornerRadius, bounds.top, + bounds.right - mCornerRadius, bounds.top + mCornerRadius, + drawBoundsPaint); + canvas.drawRect(bounds.left + mCornerRadius, + bounds.bottom - mCornerRadius, bounds.right - mCornerRadius, + bounds.bottom, drawBoundsPaint); + + if (mBoundPaint == null) { + // No bounds color, just draw center and left and right at same time + canvas.drawRect(bounds.left, bounds.top + mCornerRadius, + bounds.right, bounds.bottom - mCornerRadius, mBgPaint); + } else { + // Draw left and right pieces + canvas.drawRect(bounds.left, bounds.top + mCornerRadius, + bounds.left + mCornerRadius, bounds.bottom - mCornerRadius, + drawBoundsPaint); + canvas.drawRect(bounds.right - mCornerRadius, bounds.top + mCornerRadius, + bounds.right, bounds.bottom - mCornerRadius, drawBoundsPaint); + // Draw center + canvas.drawRect(bounds.left + mCornerRadius, bounds.top + mCornerRadius, + bounds.right - mCornerRadius, bounds.bottom - mCornerRadius, mBgPaint); + } + } + } + + @Override + public void setAlpha(int alpha) { + // not supported + } + + @Override + public void setColorFilter(ColorFilter cf) { + // not supported + } + + @Override + public int getOpacity() { + return PixelFormat.OPAQUE; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/cardview/RoundRectDrawableWithShadow.java b/app/src/main/java/com/hippo/ehviewer/cardview/RoundRectDrawableWithShadow.java new file mode 100644 index 000000000..c9137ab13 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/cardview/RoundRectDrawableWithShadow.java @@ -0,0 +1,192 @@ +/* + * 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.cardview; + +import android.graphics.Canvas; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.RadialGradient; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Shader; + +import com.hippo.ehviewer.util.Ui; + +// Get most code from android.support.v7.widget.RoundRectDrawableWithShadow +public class RoundRectDrawableWithShadow extends RoundRectDrawable { + private final static float SHADOW_MULTIPLIER = 1.5f; + + private int mShadowStartColor; + private int mShadowEndColor; + private float mShadowSize; + + private Paint mCornerShadowPaint; + private Paint mEdgeShadowPaint; + + private final RectF mPreShadowBounds = new RectF(); + private final Path mCornerShadowPath = new Path(); + + private boolean mDirty = true; + private final boolean mKeepPadding; + + public RoundRectDrawableWithShadow(int bgColor, int boundColor, + boolean keepPadding) { + super(bgColor, boundColor); + mKeepPadding = keepPadding; + init(); + } + + public RoundRectDrawableWithShadow(int[][] stateSets, + int[] bgColors, int[] boundColors, boolean keepPadding) { + super(stateSets, bgColors, boundColors); + mKeepPadding = keepPadding; + init(); + } + + private void init() { + mShadowStartColor = 0x37000000; + mShadowEndColor = 0x03000000; + mShadowSize = Ui.dp2pix(2) * SHADOW_MULTIPLIER; + + mCornerShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); + mCornerShadowPaint.setStyle(Paint.Style.FILL); + mCornerShadowPaint.setDither(true); + mEdgeShadowPaint = new Paint(mCornerShadowPaint); + } + + @Override + protected void onBoundsChange(Rect bounds) { + super.onBoundsChange(bounds); + mDirty = true; + } + + @Override + public boolean getPadding(Rect padding) { + if (mKeepPadding) { + return false; + } else { + padding.set(getSuggestionPadding()); + return true; + } + } + + public Rect getSuggestionPadding() { + Rect padding = new Rect(); + final int topShadow = (int) Math.ceil(mShadowSize * (1 / (SHADOW_MULTIPLIER * 2))); + final int sideShadow = (int) Math.ceil(mShadowSize - topShadow); + padding.set(sideShadow, topShadow, sideShadow, (int) Math.ceil(mShadowSize)); + return padding; + } + + private void buildShadowCorners() { + RectF innerBounds = new RectF(-mCornerRadius, -mCornerRadius, mCornerRadius, mCornerRadius); + RectF outerBounds = new RectF(innerBounds); + outerBounds.inset(-mShadowSize, -mShadowSize); + + mCornerShadowPath.reset(); + mCornerShadowPath.setFillType(Path.FillType.EVEN_ODD); + mCornerShadowPath.moveTo(-mCornerRadius, 0); + mCornerShadowPath.rLineTo(-mShadowSize, 0); + // outer arc + mCornerShadowPath.arcTo(outerBounds, 180f, 90f, false); + // inner arc + mCornerShadowPath.arcTo(innerBounds, 270f, -90f, false); + mCornerShadowPath.close(); + + float startRatio = mCornerRadius / (mCornerRadius + mShadowSize); + mCornerShadowPaint.setShader(new RadialGradient(0, 0, mCornerRadius + mShadowSize, + new int[]{mShadowStartColor, mShadowStartColor, mShadowEndColor}, + new float[]{0f, startRatio, 1f} + , Shader.TileMode.CLAMP)); + + // we offset the content shadowSize/2 pixels up to make it more realistic. + // this is why edge shadow shader has some extra space + // When drawing bottom edge shadow, we use that extra space. + mEdgeShadowPaint.setShader(new LinearGradient(0, -mCornerRadius + mShadowSize, 0, + -mCornerRadius - mShadowSize, + new int[]{mShadowStartColor, mShadowStartColor, mShadowEndColor}, + new float[]{0f, .5f, 1f}, Shader.TileMode.CLAMP)); + } + + private void buildComponents(Rect bounds) { + mPreShadowBounds.set(bounds.left + mShadowSize, bounds.top + mShadowSize, + bounds.right - mShadowSize, bounds.bottom - mShadowSize); + buildShadowCorners(); + } + + @Override + public void draw(Canvas canvas) { + if (mDirty) { + buildComponents(getBounds()); + mDirty = false; + } + drawShadow(canvas); + final float offset = mShadowSize * (1 - 1 / (SHADOW_MULTIPLIER * 2)); + final float horizontalOffset = mShadowSize - offset; + canvas.translate(0, -offset); + mPreShadowBounds.bottom += offset; + mPreShadowBounds.left -= horizontalOffset; + mPreShadowBounds.right += horizontalOffset; + drawBody(canvas, mPreShadowBounds); + mPreShadowBounds.bottom -= offset; + mPreShadowBounds.left += horizontalOffset; + mPreShadowBounds.right -= horizontalOffset; + canvas.translate(0, offset); + } + + private void drawShadow(Canvas canvas) { + int saved = canvas.save(); + + float cornerPathSize = 2 * (mCornerRadius + mShadowSize); + float edgeShadowTop = -mCornerRadius - mShadowSize; + final Rect bounds = getBounds(); + + // LT + canvas.translate(mPreShadowBounds.left + mCornerRadius, + mPreShadowBounds.top + mCornerRadius); + canvas.drawPath(mCornerShadowPath, mCornerShadowPaint); + canvas.drawRect(0, edgeShadowTop, + bounds.right - cornerPathSize, -mCornerRadius, + mEdgeShadowPaint); + // RB + canvas.rotate(180f); + canvas.translate(-bounds.width() + cornerPathSize, -bounds.height() + cornerPathSize); + canvas.drawPath(mCornerShadowPath, mCornerShadowPaint); + canvas.drawRect(0, edgeShadowTop, + bounds.right - cornerPathSize, -mCornerRadius + mShadowSize, + mEdgeShadowPaint); + + // LB + canvas.rotate(90f); + canvas.translate(0, -bounds.width() + cornerPathSize); + canvas.drawPath(mCornerShadowPath, mCornerShadowPaint); + canvas.drawRect(0, edgeShadowTop, + bounds.bottom - cornerPathSize, -mCornerRadius, + mEdgeShadowPaint); + + // RT + canvas.rotate(180f); + canvas.translate(-bounds.height() + cornerPathSize, -bounds.width() + cornerPathSize); + canvas.drawPath(mCornerShadowPath, mCornerShadowPaint); + canvas.drawRect(0, edgeShadowTop, + bounds.bottom - cornerPathSize, -mCornerRadius, + mEdgeShadowPaint); + + canvas.restoreToCount(saved); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/data/ApiGalleryDetail.java b/app/src/main/java/com/hippo/ehviewer/data/ApiGalleryDetail.java new file mode 100644 index 000000000..78ea9c174 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/data/ApiGalleryDetail.java @@ -0,0 +1,106 @@ +/* + * 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.data; + +import java.util.ArrayList; +import java.util.List; + +public class ApiGalleryDetail extends ApiGalleryInfo + implements LofiDetailImpl { + public int previewPageNum; + public int previewPerPage; + private final List previewLists = new ArrayList(); + + public ApiGalleryDetail() {} + + public ApiGalleryDetail(ApiGalleryInfo apiGalleryInfo) { + gid = apiGalleryInfo.gid; + token = apiGalleryInfo.token; + title = apiGalleryInfo.title; + posted = apiGalleryInfo.posted; + category = apiGalleryInfo.category; + thumb = apiGalleryInfo.thumb; + uploader = apiGalleryInfo.uploader; + rating = apiGalleryInfo.rating; + simpleLanguage = apiGalleryInfo.simpleLanguage; + archiver_key = apiGalleryInfo.archiver_key; + title_jpn = apiGalleryInfo.title_jpn; + filecount = apiGalleryInfo.filecount; + filesize = apiGalleryInfo.filesize; + expunged = apiGalleryInfo.expunged; + torrentcount = apiGalleryInfo.torrentcount; + apiTags = apiGalleryInfo.apiTags; + } + + @Override + public int getGid() { + return gid; + } + + @Override + public String getToken() { + return token; + } + + @Override + public String getTitle() { + return title; + } + + @Override + public int getPreviewPageNum() { + return previewPageNum; + } + + @Override + public void setPreviewPageNum(int pageNum) { + previewPageNum = pageNum; + } + + @Override + public int getPreviewPerPage() { + return previewPerPage; + } + + @Override + public PreviewList getPreview(int page) { + if (page >= 0 && page < previewLists.size()) + return previewLists.get(page); + else + return null; + } + + @Override + public void setPreview(int page, PreviewList previewList) { + if (page >= 0 && page < previewLists.size()) + previewLists.set(page, previewList); + else if (page == previewLists.size()) + previewLists.add(previewList); + else + ; // Do nothing + } + + @Override + public String[] getTags() { + return apiTags; + } + + @Override + public GalleryInfo toGalleryInfo() { + return new GalleryInfo(this); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/data/ApiGalleryInfo.java b/app/src/main/java/com/hippo/ehviewer/data/ApiGalleryInfo.java new file mode 100644 index 000000000..b399a6efe --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/data/ApiGalleryInfo.java @@ -0,0 +1,82 @@ +/* + * 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.data; + +import android.os.Parcel; +import android.os.Parcelable; + +import com.hippo.ehviewer.util.Utils; + + +public class ApiGalleryInfo extends GalleryInfo{ + public String archiver_key; + public String title_jpn; + public String filecount; + public long filesize; + public boolean expunged; + public String torrentcount; + public String[] apiTags; + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public ApiGalleryInfo createFromParcel(Parcel source) { + ApiGalleryInfo p = new ApiGalleryInfo(); + p.gid = source.readInt(); + p.token = source.readString(); + p.title = source.readString(); + p.posted = source.readString(); + p.category = source.readInt(); + p.thumb = source.readString(); + p.uploader = source.readString(); + p.rating = source.readFloat(); + p.simpleLanguage = source.readString(); + p.archiver_key = source.readString(); + p.title_jpn = source.readString(); + p.filecount = source.readString(); + p.filesize = source.readLong(); + p.expunged = Utils.int2boolean(source.readInt()); + p.torrentcount = source.readString(); + int apiTagsLength = source.readInt(); + p.apiTags = new String[apiTagsLength]; + for (int i = 0; i < apiTagsLength; i++) + p.apiTags[i] = source.readString(); + return p; + } + + @Override + public ApiGalleryInfo[] newArray(int size) { + return new ApiGalleryInfo[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeString(archiver_key); + dest.writeString(title_jpn); + dest.writeString(filecount); + dest.writeLong(filesize); + dest.writeInt(Utils.boolean2int(expunged)); + dest.writeString(torrentcount); + dest.writeInt(apiTags.length); + for (String str : apiTags) + dest.writeString(str); + } + +} + diff --git a/app/src/main/java/com/hippo/ehviewer/data/Comment.java b/app/src/main/java/com/hippo/ehviewer/data/Comment.java new file mode 100644 index 000000000..ba872c418 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/data/Comment.java @@ -0,0 +1,29 @@ +/* + * 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.data; + +public class Comment { + public String time; + public String user; + public String comment; + + public Comment (String time, String user, String comment) { + this.time = time; + this.user = user; + this.comment = comment; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/data/Data.java b/app/src/main/java/com/hippo/ehviewer/data/Data.java new file mode 100644 index 000000000..2d3b216b3 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/data/Data.java @@ -0,0 +1,934 @@ +/* + * 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.data; + +import java.util.ArrayList; +import java.util.List; + +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.SparseArray; + +import com.hippo.ehviewer.Analytics; +import com.hippo.ehviewer.service.DownloadService; +import com.hippo.ehviewer.util.Config; +import com.hippo.ehviewer.util.Log; +import com.hippo.ehviewer.util.MathUtils; +import com.hippo.ehviewer.util.SqlUtils; + +// TODO +// what a mess +// refactoring + +/** + * @author Hippo + * + * Ver 2: 移除 read 表,添加 Download 表 + * Ver 3: Download 表添加下载时间项 + * Ver 4: 添历史记录 + */ +public class Data { + private static final String TAG = "Data"; + + private static final int VERSION = 4; + private static final String DB_NAME = "data"; + + private static final String TABLE_GALLERY = "gallery"; + private static final String TABLE_LOCAL_FAVOURITE = "local_favourite"; + private static final String TABLE_TAG = "tag"; + private static final String TABLE_DOWNLOAD = "download"; + private static final String TABLE_HISTORY = "history"; + + private static final String COLUMN_GID = "gid"; + private static final String COLUMN_TOKEN = "token"; + private static final String COLUMN_TITLE = "title"; + private static final String COLUMN_POSTED = "posted"; + private static final String COLUMN_CATEGORY = "category"; + private static final String COLUMN_THUMB = "thumb"; + private static final String COLUMN_UPLOADER = "uploader"; + private static final String COLUMN_RATING = "rating"; + private static final String COLUMN_REFERENCE = "reference"; + + private static final String COLUMN_ID = "id"; + private static final String COLUMN_NAME = "name"; + private static final String COLUMN_MODE = "mode"; + private static final String COLUMN_SEARCH = "search"; + private static final String COLUMN_ADVANCE = "advance"; + private static final String COLUMN_MIN_RATING = "min_rating"; + private static final String COLUMN_TAG = "tag"; + + private static final String COLUMN_STATE = "state"; + private static final String COLUMN_LEGACY = "legacy"; + + private static final String COLUMN_DATE = "date"; + + private final SparseArray mGallerys; + + private long mTagRowNum; + private List mTags; + private List mLocalFavourites; + private List mDownloads; + + private final Context mContext; + + private final DBHelper mDBHelper; + private final SQLiteDatabase mDatabase; + + private static Data sInstance; + + public static void createInstance(Context context) { + sInstance = new Data(context); + } + + public static Data getInstance() { + return sInstance; + } + + public Data(Context context) { + mContext = context; + mGallerys = new SparseArray(); + + mDBHelper = new DBHelper(mContext); + mDatabase = mDBHelper.getWritableDatabase(); + + getTags(); + getLocalFavourites(); + getDownloads(); + } + + @Override + public void finalize() { + mDBHelper.close(); + } + + /****** gallery ******/ + + /** + * Get galleryInfo in table, if do not exits return null + * + * @param gid + * @return + */ + public GalleryInfo getGallery(SQLiteDatabase db, int gid) { + GalleryInfo galleryInfo = mGallerys.get(gid); + if (galleryInfo == null) { + Cursor cursor = db.rawQuery("select * from " + TABLE_GALLERY + " where " + COLUMN_GID + "=?", + new String[]{String.valueOf(gid)}); + if (cursor.moveToFirst()) { + galleryInfo = new GalleryInfo(); + galleryInfo.gid = gid; + galleryInfo.token = cursor.getString(1); + galleryInfo.title = cursor.getString(2); + galleryInfo.posted = cursor.getString(3); + galleryInfo.category = cursor.getInt(4); + galleryInfo.thumb = cursor.getString(5); + galleryInfo.uploader = cursor.getString(6); + galleryInfo.rating = cursor.getFloat(7); + galleryInfo.generateSLang(); + + // add to map + mGallerys.put(gid, galleryInfo); + } + cursor.close(); + } + return galleryInfo; + } + + @SuppressWarnings("unused") + private synchronized boolean containsGallery(int gid) { + boolean re = false; + Cursor cursor = mDatabase.rawQuery("select * from " + + TABLE_GALLERY + " where " + COLUMN_GID + "=?", + new String[]{String.valueOf(gid)}); + if (cursor.moveToFirst()) + re = true; + cursor.close(); + return re; + } + + /** + * If do not contain the gallery return -1; + * @param gid + * @return + */ + private synchronized int getGalleryReference(int gid) { + int reference = -1; + Cursor cursor = mDatabase.rawQuery("select " + COLUMN_REFERENCE + " from " + + TABLE_GALLERY + " where " + COLUMN_GID + "=?", + new String[]{String.valueOf(gid)}); + if (cursor.moveToFirst()) { + reference = cursor.getInt(0); + } + cursor.close(); + return reference; + } + + /** + * + * If exits same gid, add the reference by 1. + * If exits no same gid, add it to table reference is 1 + * + * @param galleryInfo + */ + private void addGallery(GalleryInfo galleryInfo) { + boolean isInsert; + int gid = galleryInfo.gid; + // add to map + mGallerys.put(gid, galleryInfo); + + // add to sql or update and add reference by 1 + int reference; + if ((reference = getGalleryReference(gid)) != -1) { + isInsert = false; + reference++; + } + else { + isInsert = true; + reference = 1; + } + + ContentValues values = new ContentValues(); + values.put(COLUMN_GID, gid); + values.put(COLUMN_TOKEN, galleryInfo.token); + values.put(COLUMN_TITLE, galleryInfo.title); + values.put(COLUMN_POSTED, galleryInfo.posted); + values.put(COLUMN_CATEGORY, galleryInfo.category); + values.put(COLUMN_THUMB, galleryInfo.thumb); + values.put(COLUMN_UPLOADER, galleryInfo.uploader); + values.put(COLUMN_RATING, galleryInfo.rating); + values.put(COLUMN_REFERENCE, reference); + + if (isInsert) { // Set + mDatabase.insert(TABLE_GALLERY, null, values); + } else { // Update + mDatabase.update(TABLE_GALLERY, values, COLUMN_GID + "=?", new String[]{String.valueOf(gid)}); + } + } + + /** + * True if update + * + * @param gid + * @return + */ + private boolean updateGallery(GalleryInfo galleryInfo) { + ContentValues values = new ContentValues(); + values.put(COLUMN_GID, galleryInfo.gid); + values.put(COLUMN_TOKEN, galleryInfo.token); + values.put(COLUMN_TITLE, galleryInfo.title); + values.put(COLUMN_POSTED, galleryInfo.posted); + values.put(COLUMN_CATEGORY, galleryInfo.category); + values.put(COLUMN_THUMB, galleryInfo.thumb); + values.put(COLUMN_UPLOADER, galleryInfo.uploader); + values.put(COLUMN_RATING, galleryInfo.rating); + + return mDatabase.update(TABLE_GALLERY, values, + COLUMN_GID + "=?", new String[]{String.valueOf(galleryInfo.gid)}) != 0; + } + + /** + * decrease reference by 1, only delete when reference is 0 + * @param gid + */ + private boolean deleteGallery(int gid) { + int reference = getGalleryReference(gid); + if (reference == -1) { + Log.w(TAG, "Can't get reference when deleteGallery, gid is " + gid); + return false; + } + reference--; + if (reference == 0) { // delete gallery + // delete in sql + mDatabase.delete(TABLE_GALLERY, COLUMN_GID + "=?", new String[]{String.valueOf(gid)}); + // delete in list + mGallerys.remove(gid); + } else { // update reference + ContentValues values = new ContentValues(); + values.put(COLUMN_REFERENCE, reference); + mDatabase.update(TABLE_GALLERY, values, COLUMN_GID + "=?", new String[]{String.valueOf(gid)}); + } + return true; + } + + /****** download ******/ + private synchronized void getDownloads() { + boolean startService = false; + boolean keepDownloadService = Config.getKeepDownloadService(); + mDownloads = new ArrayList(); + + Cursor cursor = mDatabase.rawQuery("select * from " + TABLE_DOWNLOAD + + " order by " + COLUMN_DATE + " asc", null); + + if (cursor.moveToFirst()) { + while (!cursor.isAfterLast()) { + + int gid = cursor.getInt(0); + GalleryInfo galleryInfo = getGallery(mDatabase, gid); + if (galleryInfo == null) { + cursor.moveToNext(); + continue; + } + int mode = cursor.getInt(1); + int state = cursor.getInt(2); + int legacy = cursor.getInt(3); + if (state == DownloadInfo.STATE_WAIT || state == DownloadInfo.STATE_DOWNLOAD) { + if (keepDownloadService) { + state = DownloadInfo.STATE_WAIT; + startService = true; + } else { + state = DownloadInfo.STATE_NONE; + } + } + + DownloadInfo downloadInfo = new DownloadInfo( + galleryInfo, mode, state, legacy); + mDownloads.add(downloadInfo); + + cursor.moveToNext(); + } + } + cursor.close(); + + if (startService) { + Intent it = new Intent(mContext, DownloadService.class); + mContext.startService(it); + } + } + + /** + * Return null if not found + * @return + */ + public synchronized DownloadInfo getFirstWaitDownloadInfo() { + for (DownloadInfo di : mDownloads) { + if (di.state == DownloadInfo.STATE_WAIT) + return di; + } + return null; + } + + /** + * True if contain this item + * + * @param gid + * @return + */ + public synchronized boolean containsDownload(int gid) { + boolean re = false; + Cursor cursor = mDatabase.rawQuery("select * from " + + TABLE_DOWNLOAD + " where " + COLUMN_GID + "=?", + new String[]{String.valueOf(gid)}); + if (cursor.moveToFirst()) + re = true; + cursor.close(); + return re; + } + + /** + * If exits, just update + * + * @param downloadInfo + * @return + */ + public synchronized boolean addDownload(DownloadInfo downloadInfo) { + GalleryInfo galleryInfo = downloadInfo.galleryInfo; + int gid = galleryInfo.gid; + + // Add to download + boolean update; + if (containsDownload(gid)) { + update = true; + } else { + update = false; + addGallery(galleryInfo); + } + + ContentValues values = new ContentValues(); + values.put(COLUMN_GID, gid); + values.put(COLUMN_MODE, downloadInfo.mode); + values.put(COLUMN_STATE, downloadInfo.state); + values.put(COLUMN_LEGACY, downloadInfo.legacy); + + if (update) { + mDatabase.update(TABLE_DOWNLOAD, values, + COLUMN_GID + "=?", new String[]{String.valueOf(galleryInfo.gid)}); + return false; + } else { + // If not update, set date + values.put(COLUMN_DATE, System.currentTimeMillis()); + + if (mDatabase.insert(TABLE_DOWNLOAD, null, + values) != -1) { // If add ok + mDownloads.add(downloadInfo); + return true; + } else { // If add fail + deleteGallery(gid); + return false; + } + } + } + + public synchronized boolean deleteDownload(int gid) { + int deleteNum = mDatabase.delete(TABLE_DOWNLOAD, COLUMN_GID + "=?", new String[]{String.valueOf(gid)}); + if (deleteNum > 1) + Log.w(TAG, "WTF? more than one gid is " + gid); + + if (deleteNum == 0) + return false; + + // delete from list + for (DownloadInfo di : mDownloads) { + if (gid == di.galleryInfo.gid) { + mDownloads.remove(di); + break; + } + } + + // sub reference + deleteGallery(gid); + + return true; + } + + public synchronized List getAllDownloads() { + return mDownloads; + } + + /** + * Return null if not found + * + * @param gid + * @return + */ + public synchronized DownloadInfo getDownload(int gid) { + for (DownloadInfo di : mDownloads) { + if (gid == di.galleryInfo.gid) + return di; + } + return null; + } + + public synchronized void setDownloadState(int gid, int state) { + setDownloadState(getDownload(gid), state); + } + + public synchronized void setDownloadState(DownloadInfo di, int state) { + if (di == null || di.state == state) + return; + + di.state = state; + // Update in db + ContentValues values = new ContentValues(); + values.put(COLUMN_STATE, state); + mDatabase.update(TABLE_DOWNLOAD, values, + COLUMN_GID + "=?", new String[]{String.valueOf(di.galleryInfo.gid)}); + } + + /** + * If state is STATE_NONE or state is STATE_FINISH and legacy is not 0, + * make state STATE_WAIT + */ + public synchronized void startAllDownload() { + for (DownloadInfo di : mDownloads) { + if (di.state == DownloadInfo.STATE_NONE || + (di.state == DownloadInfo.STATE_FINISH && di.legacy != 0)) { + setDownloadState(di, DownloadInfo.STATE_WAIT); + } + } + } + + /** + * If state is STATE_WAIT or STATE_DOWNLOAD, + * make state STATE_NONE + */ + public synchronized void stopAllDownload() { + for (DownloadInfo di : mDownloads) { + if (di.state == DownloadInfo.STATE_WAIT || + di.state == DownloadInfo.STATE_DOWNLOAD) { + setDownloadState(di, DownloadInfo.STATE_NONE); + } + } + } + + /****** local favourite ******/ + private synchronized void getLocalFavourites() { + mLocalFavourites = new ArrayList(); + Cursor cursor = mDatabase.rawQuery("select * from " + + TABLE_LOCAL_FAVOURITE, null); + + if (cursor.moveToFirst()) { + while (!cursor.isAfterLast()) { + int gid = cursor.getInt(0); + GalleryInfo galleryInfo = getGallery(mDatabase, gid); + if (galleryInfo != null) + mLocalFavourites.add(galleryInfo); + cursor.moveToNext(); + } + } + cursor.close(); + } + + public synchronized boolean containsLocalFavourite(int gid) { + boolean re = false; + Cursor cursor = mDatabase.rawQuery("select * from " + + TABLE_LOCAL_FAVOURITE + " where " + COLUMN_GID + "=?", + new String[]{String.valueOf(gid)}); + if (cursor.moveToFirst()) + re = true; + cursor.close(); + return re; + } + + /** + * galleryInfo can't be null + * @param galleryInfo + */ + public synchronized void addLocalFavourite(GalleryInfo galleryInfo) { + int gid = galleryInfo.gid; + + if (containsLocalFavourite(gid)) { + updateGallery(galleryInfo); + } else { + addGallery(galleryInfo); + // add to sql + ContentValues values = new ContentValues(); + values.put(COLUMN_GID, galleryInfo.gid); + mDatabase.insert(TABLE_LOCAL_FAVOURITE, null, values); + // add to list + mLocalFavourites.add(galleryInfo); + + // Analytics + Analytics.addToFavoriteGallery(mContext, gid, galleryInfo.token); + } + } + + public synchronized void deleteLocalFavourite(int gid) { + // delete from list + for (GalleryInfo galleryInfo : mLocalFavourites) { + if (galleryInfo.gid == gid) { + mLocalFavourites.remove(galleryInfo); + break; + } + } + + // delete from sql + mDatabase.delete(TABLE_LOCAL_FAVOURITE, COLUMN_GID + "=?", new String[]{String.valueOf(gid)}); + deleteGallery(gid); + } + + public synchronized List getAllLocalFavourites() { + return mLocalFavourites; + } + + /****** tag ******/ + private synchronized void getTags() { + mTags = new ArrayList(); + mTagRowNum = 0; + Cursor cursor = mDatabase.rawQuery("select * from " + TABLE_TAG + + " order by " + COLUMN_ID + " asc", null); + if (cursor.moveToFirst()) { + while (!cursor.isAfterLast()) { + + String name = cursor.getString(1); + int mode = cursor.getInt(2); + int category = cursor.getInt(3); + String search = cursor.getString(4); + int advance = cursor.getInt(5); + int min_rating = cursor.getInt(6); + String tagStr = cursor.getString(7); + + Tag tag = new Tag(name, category, search); + tag.setAdvance(advance, min_rating); + tag.setTag(tagStr); + tag.setMode(mode); + + mTags.add(tag); + + cursor.moveToNext(); + mTagRowNum++; + } + } + cursor.close(); + } + + public synchronized boolean addTag(Tag tag) { + ContentValues values = new ContentValues(); + values.put(COLUMN_ID, mTagRowNum); + values.put(COLUMN_NAME, tag.getName()); + values.put(COLUMN_MODE, tag.getMode()); + values.put(COLUMN_CATEGORY, tag.getCategory()); + values.put(COLUMN_SEARCH, tag.getSearch()); + values.put(COLUMN_ADVANCE, tag.getAdvanceType()); + values.put(COLUMN_MIN_RATING, tag.getMinRating()); + values.put(COLUMN_TAG, tag.getTag()); + + boolean ok = mDatabase.insert( + TABLE_TAG, null, values) != -1; + if (ok) { + mTagRowNum++; + mTags.add(tag); + } + return ok; + } + + public synchronized boolean deleteTag(int id) { + int deleteNum = mDatabase.delete(TABLE_TAG, COLUMN_ID + "=?", new String[]{String.valueOf(id)}); + if (deleteNum > 1) + Log.e(TAG, "WTF? more than one id is " + id); + + if (deleteNum == 0) + return false; + if (id < 0 || id >= mTags.size()) { + Log.e(TAG, "id is out of bounds, id is " + id + ", mTags.size() is " + mTags.size()); + return false; + } + + // Update sql + ContentValues values = new ContentValues(); + for (int i = id + 1; i < mTagRowNum; i++) { + values.put(COLUMN_ID, i - 1); + mDatabase.update(TABLE_TAG, values, COLUMN_ID + "=?", new String[]{String.valueOf(i)}); + } + mTagRowNum -= deleteNum; + + // Update list + mTags.remove(id); + + return true; + } + + public synchronized List getAllTags() { + return mTags; + } + + public synchronized Tag getTag(int location) { + return mTags.get(location); + } + + public synchronized List getAllTagNames() { + List names = new ArrayList(); + for (Tag tag : mTags) + names.add(tag.getName()); + return names; + } + + /** + * location >= 0 && location <= tagRowNum please + * @param location + * @param tag + */ + public synchronized void setTag(int location, Tag tag) { + if (location == mTagRowNum) { + addTag(tag); + } else { + // Update list + mTags.set(location, tag); + + // Update sql + ContentValues values = new ContentValues(); + values.put(COLUMN_NAME, tag.getName()); + values.put(COLUMN_CATEGORY, tag.getCategory()); + values.put(COLUMN_SEARCH, tag.getSearch()); + values.put(COLUMN_ADVANCE, tag.getAdvanceType()); + values.put(COLUMN_MIN_RATING, tag.getMinRating()); + mDatabase.update(TABLE_TAG, values, COLUMN_ID + "=?", new String[]{String.valueOf(location)}); + } + } + + public synchronized boolean swapTag(int indexOne, int indexTwo) { + if (indexOne < 0 || indexOne > mTagRowNum + || indexTwo < 0 || indexTwo > mTagRowNum) + return false; + + if (indexOne == indexTwo) + return true; + + // Update list + Tag temp = mTags.get(indexOne); + mTags.set(indexOne, mTags.get(indexTwo)); + mTags.set(indexTwo, temp); + // Update sql + ContentValues values = new ContentValues(); + values.put(COLUMN_ID, -1); + mDatabase.update(TABLE_TAG, values, COLUMN_ID + "=?", new String[]{String.valueOf(indexOne)}); + values.put(COLUMN_ID, indexOne); + mDatabase.update(TABLE_TAG, values, COLUMN_ID + "=?", new String[]{String.valueOf(indexTwo)}); + values.put(COLUMN_ID, indexTwo); + mDatabase.update(TABLE_TAG, values, COLUMN_ID + "=?", new String[]{String.valueOf(-1)}); + return true; + } + + public static final int NONE = 0x0; + public static final int BROWSE = 0x1; + public static final int READ = 0x2; + + public synchronized List getHistory(int mode, boolean desc) { + List history = new ArrayList(); + + StringBuilder sb = new StringBuilder(); + sb.append("select * from ").append(TABLE_HISTORY); + boolean first = true; + if (mode != 0) { + sb.append(" where " + COLUMN_MODE + " in ("); + for (int b = 1; b <= READ; b <<= 1) { + if ((mode & b) != 0) { + sb.append(first ? "" : ", ").append(b); + first = false; + } + } + sb.append(")"); + } + sb.append(" order by ").append(COLUMN_DATE).append(desc ? " desc" : " asc"); + + Log.d(TAG, sb.toString()); + + Cursor cursor = mDatabase.rawQuery(sb.toString(), null); + if (cursor.moveToFirst()) { + while (!cursor.isAfterLast()) { + int gid = cursor.getInt(0); + GalleryInfo gi = getGallery(mDatabase, gid); + if (gi != null) { + history.add(new HistoryItem(gi, cursor.getInt(1), cursor.getLong(2))); + } + cursor.moveToNext(); + } + } + cursor.close(); + + return history; + } + + public synchronized int containsHistory(int gid) { + int re; + Cursor cursor = mDatabase.rawQuery("select * from " + + TABLE_HISTORY + " where " + COLUMN_GID + "=?", + new String[]{String.valueOf(gid)}); + if (cursor.moveToFirst()) + re = cursor.getInt(1); + else + re = NONE; + cursor.close(); + return re; + } + + private int checkMode(int mode) { + return MathUtils.clamp(mode, NONE, READ); + } + + public synchronized void addHistory(GalleryInfo gi, int mode) { + mode = checkMode(mode); + + long curDate = System.currentTimeMillis(); + ContentValues values = new ContentValues(); + values.put(COLUMN_GID, gi.gid); + values.put(COLUMN_DATE, curDate); + + int curMode = checkMode(containsHistory(gi.gid)); + if (curMode == NONE) { + // Add new record + values.put(COLUMN_MODE, mode); + mDatabase.insert(TABLE_HISTORY, null, values); + addGallery(gi); + + truncateHistory(Config.getMaxHistoryCount()); + } else { + // Update record + int newMode = Math.max(curMode, mode); + values.put(COLUMN_MODE, newMode); + mDatabase.update(TABLE_HISTORY, values, + COLUMN_GID + "=?", new String[]{String.valueOf(gi.gid)}); + updateGallery(gi); + } + } + + public void clearHistory() { + truncateHistory(0); + } + + protected synchronized void truncateHistory(int maxEntries) { + if (maxEntries < 0) { + return; + } else if (maxEntries == 0) { + mDatabase.delete(TABLE_HISTORY, null, null); + } else { + try { + mDatabase.execSQL("delete from " + TABLE_HISTORY + " where " + COLUMN_GID + + " in " + "(select " + COLUMN_GID + " from " + TABLE_HISTORY + + " order by " + COLUMN_DATE + " desc limit " + maxEntries + + ", -1);"); + } catch(Throwable e){} + } + } + + public class DBHelper extends SQLiteOpenHelper { + + public DBHelper(Context context) { + super(context, DB_NAME, null, VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + // gallery + String CreateGallery = "create table " + TABLE_GALLERY + "(" + + COLUMN_GID + " integer primary key," + + COLUMN_TOKEN + " text," + + COLUMN_TITLE + " text," + + COLUMN_POSTED + " text," + + COLUMN_CATEGORY + " integer," + + COLUMN_THUMB + " text," + + COLUMN_UPLOADER + " text," + + COLUMN_RATING + " float," + + COLUMN_REFERENCE + " integer);"; + db.execSQL(CreateGallery); + + // local favourite + String CreateLocalFavourite = "create table " + + TABLE_LOCAL_FAVOURITE + "(" + + COLUMN_GID + " integer primary key," + + "foreign key(" + COLUMN_GID + ") references " + TABLE_GALLERY + "(" + COLUMN_GID + "));"; + db.execSQL(CreateLocalFavourite); + + // tag + String CreateTag = "create table " + + TABLE_TAG + "(" + + COLUMN_ID + " integer primary key," + + COLUMN_NAME + " text," + + COLUMN_MODE + " integer," + + COLUMN_CATEGORY + " integer," + + COLUMN_SEARCH + " text," + + COLUMN_ADVANCE + " integer," + + COLUMN_MIN_RATING + " integer," + + COLUMN_TAG + " text);"; + db.execSQL(CreateTag); + + // download + createDownloadTableVer3(db); + + // History + createHistoryTable(db); + } + + private void createDownloadTableVer2(SQLiteDatabase db) { + String createDownload = "create table " + + TABLE_DOWNLOAD + "(" + + COLUMN_GID + " integer primary key," + + COLUMN_MODE + " integer," + + COLUMN_STATE + " integer," + + COLUMN_LEGACY + " integer," + + "foreign key(" + COLUMN_GID + ") references " + TABLE_GALLERY + "(" + COLUMN_GID + "));"; + + SqlUtils.exeSQLSafely(db, createDownload); + } + + private synchronized List getDownloadsVer2(SQLiteDatabase db) { + List list = new ArrayList(); + + Cursor cursor = db.rawQuery("select * from " + TABLE_DOWNLOAD, + null); + + if (cursor.moveToFirst()) { + while (!cursor.isAfterLast()) { + + int gid = cursor.getInt(0); + GalleryInfo galleryInfo = getGallery(db, gid); + if (galleryInfo == null) { + cursor.moveToNext(); + continue; + } + int mode = cursor.getInt(1); + int state = cursor.getInt(2); + int legacy = cursor.getInt(3); + if (state == DownloadInfo.STATE_WAIT || state == DownloadInfo.STATE_DOWNLOAD) + state = DownloadInfo.STATE_NONE; + + DownloadInfo downloadInfo = new DownloadInfo( + galleryInfo, mode, state, legacy); + list.add(downloadInfo); + + cursor.moveToNext(); + } + } + cursor.close(); + + return list; + } + + private void createDownloadTableVer3(SQLiteDatabase db) { + String createDownload = "create table " + + TABLE_DOWNLOAD + "(" + + COLUMN_GID + " integer primary key," + + COLUMN_MODE + " integer," + + COLUMN_STATE + " integer," + + COLUMN_LEGACY + " integer," + + COLUMN_DATE + " long," + + "foreign key(" + COLUMN_GID + ") references " + TABLE_GALLERY + "(" + COLUMN_GID + "));"; + + SqlUtils.exeSQLSafely(db, createDownload); + } + + public void addDownloadInfoVer3(SQLiteDatabase db, List l) { + for (int i = 0; i < l.size(); i++) { + DownloadInfo di = l.get(i); + ContentValues values = new ContentValues(); + values.put(COLUMN_GID, di.galleryInfo.gid); + values.put(COLUMN_MODE, di.mode); + values.put(COLUMN_STATE, di.state); + values.put(COLUMN_LEGACY, di.legacy); + values.put(COLUMN_DATE, (long) i); + db.insert(TABLE_DOWNLOAD, null, values); + } + } + + private void createHistoryTable(SQLiteDatabase db) { + String createDownload = "create table " + + TABLE_HISTORY + "(" + + COLUMN_GID + " integer primary key," + + COLUMN_MODE + " integer," + + COLUMN_DATE + " long," + + "foreign key(" + COLUMN_GID + ") references " + TABLE_GALLERY + "(" + COLUMN_GID + "));"; + + SqlUtils.exeSQLSafely(db, createDownload); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + switch (oldVersion) { + case 1: + SqlUtils.dropTable(db, TABLE_DOWNLOAD); + SqlUtils.dropTable(db, "read"); + createDownloadTableVer2(db); + case 2: + List l = getDownloadsVer2(db); + SqlUtils.dropTable(db, TABLE_DOWNLOAD); + createDownloadTableVer3(db); + addDownloadInfoVer3(db, l); + case 3: + createHistoryTable(db); + case VERSION: + break; + default: + SqlUtils.dropAllTable(db); + onCreate(db); + break; + } + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/data/DownloadInfo.java b/app/src/main/java/com/hippo/ehviewer/data/DownloadInfo.java new file mode 100644 index 000000000..ea4d68469 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/data/DownloadInfo.java @@ -0,0 +1,63 @@ +/* + * 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.data; + +import com.hippo.ehviewer.widget.ExpandableListItem; + + +public class DownloadInfo implements ExpandableListItem { + @SuppressWarnings("unused") + private static final String TAG = DownloadInfo.class.getSimpleName(); + + public static final int STATE_NONE = 0x0; + public static final int STATE_WAIT = 0x1; + public static final int STATE_DOWNLOAD = 0x2; + public static final int STATE_FINISH = 0x3; + + public final GalleryInfo galleryInfo; + public final int mode; + public volatile int state = STATE_NONE; + /** byte/second **/ + public volatile int speed; + public volatile int download = -1; + public volatile int total = -1; + public volatile int legacy; + + private boolean mExpanded = false; + + public DownloadInfo(GalleryInfo gi, int mode) { + this.galleryInfo = gi; + this.mode = mode; + } + + public DownloadInfo(GalleryInfo gi, int mode, int state, int legacy) { + this.galleryInfo = gi; + this.mode = mode; + this.state = state; + this.legacy = legacy; + } + + @Override + public void setExpanded(boolean expanded) { + mExpanded = expanded; + } + + @Override + public boolean getExpanded() { + return mExpanded; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/data/GalleryDetail.java b/app/src/main/java/com/hippo/ehviewer/data/GalleryDetail.java new file mode 100644 index 000000000..aa957edf7 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/data/GalleryDetail.java @@ -0,0 +1,106 @@ +/* + * 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.data; + +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; + +public class GalleryDetail extends GalleryInfo + implements PreviewImpl { + + public String title_jpn; + public int pages; + public String size; + public String resized; + public String parent; + public String visible; + public String language; + public int people; + public String firstPage; + /** + * Can't be null, just can be empty + */ + public LinkedHashMap> tags; + + // For Preview + public int previewSum; + public int previewPerPage; + public PreviewList[] previewLists; + public List comments; + + public GalleryDetail() {} + + public GalleryDetail(GalleryInfo galleryInfo) { + gid = galleryInfo.gid; + token = galleryInfo.token; + title = galleryInfo.title; + posted = galleryInfo.posted; + category = galleryInfo.category; + thumb = galleryInfo.thumb; + uploader = galleryInfo.uploader; + rating = galleryInfo.rating; + simpleLanguage = galleryInfo.simpleLanguage; + } + + @Override + public int getGid() { + return gid; + } + + @Override + public String getToken() { + return token; + } + + @Override + public String getTitle() { + return title; + } + + @Override + public int getPreviewPageNum() { + return previewSum; + } + + @Override + public int getPreviewPerPage() { + return previewPerPage; + } + + @Override + public PreviewList getPreview(int page) { + if (page >= 0 && previewLists != null && page < previewLists.length) + return previewLists[page]; + else + return null; + } + + @Override + public void setPreview(int page, PreviewList previewList) { + if (previewLists == null && previewSum > 0) + previewLists = new PreviewList[previewSum]; + + if (page >= 0 && previewLists != null && page < previewLists.length) + previewLists[page] = previewList; + } + + @Override + public GalleryInfo toGalleryInfo() { + return new GalleryInfo(this); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/data/GalleryInfo.java b/app/src/main/java/com/hippo/ehviewer/data/GalleryInfo.java new file mode 100644 index 000000000..89b15a382 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/data/GalleryInfo.java @@ -0,0 +1,174 @@ +/* + * 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.data; + +import java.util.regex.Pattern; + +import android.os.Parcel; +import android.os.Parcelable; + +public class GalleryInfo implements Parcelable { + + /** + * ISO 639-1 + */ + public static final String S_LANG_JA = "JA"; + public static final String S_LANG_EN = "EN"; + public static final String S_LANG_ZH = "ZH"; + public static final String S_LANG_NL = "NL"; + public static final String S_LANG_FR = "FR"; + public static final String S_LANG_DE = "DE"; + public static final String S_LANG_HU = "HU"; + public static final String S_LANG_IT = "IT"; + public static final String S_LANG_KO = "KO"; + public static final String S_LANG_PL = "PL"; + public static final String S_LANG_PT = "PT"; + public static final String S_LANG_RU = "RU"; + public static final String S_LANG_ES = "ES"; + public static final String S_LANG_TH = "TH"; + public static final String S_LANG_VI = "VI"; + + public static final String[] S_LANGS = { + S_LANG_EN, + S_LANG_ZH, + S_LANG_ES, + S_LANG_KO, + S_LANG_RU, + S_LANG_FR, + S_LANG_PT, + S_LANG_TH, + S_LANG_DE, + S_LANG_IT, + S_LANG_VI, + S_LANG_PL, + S_LANG_HU, + S_LANG_NL, + }; + + public static final String[] S_LANG_PATTERNS = { + "[(\\[]eng(?:lish)?[)\\]]", + "[((\\[]chinese[))\\]]|[汉漢]化|中[国國][语語]|中文|[(\\[]CN[)\\]]", + "[(\\[]spanish[)\\]]|[(\\[]Español[)\\]]", + "[(\\[]korean?[)\\]]", + "[(\\[]rus(?:sian)?[)\\]]", + "[(\\[]fr(?:ench)?[)\\]]", + "[(\\[]portuguese", + "[(\\[]thai(?: ภาษาไทย)?[)\\]]", + "[(\\[]german[)\\]]", + "[(\\[]italiano?[)\\]]", + "[(\\[]vietnamese(?: Tiếng Việt)?[)\\]]", + "[(\\[]polish[)\\]]", + "[(\\[]hun(?:garian)?[)\\]]", + "[(\\[]dutch[)\\]]", + }; + + public int gid; + public String token; + public String title; + public String posted; + public int category; + public String thumb; + public String uploader; + public float rating; + /** + * Will not be recond + */ + public int thumbWidth; + /** + * Will not be recond + */ + public int thumbHeight; + /** + * language get from title + */ + public String simpleLanguage; + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public GalleryInfo createFromParcel(Parcel source) { + GalleryInfo p = new GalleryInfo(); + p.gid = source.readInt(); + p.token = source.readString(); + p.title = source.readString(); + p.posted = source.readString(); + p.category = source.readInt(); + p.thumb = source.readString(); + p.uploader = source.readString(); + p.rating = source.readFloat(); + p.simpleLanguage = source.readString(); + return p; + } + + @Override + public GalleryInfo[] newArray(int size) { + return new GalleryInfo[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(gid); + dest.writeString(token); + dest.writeString(title); + dest.writeString(posted); + dest.writeInt(category); + dest.writeString(thumb); + dest.writeString(uploader); + dest.writeFloat(rating); + dest.writeString(simpleLanguage); + } + + public GalleryInfo() { + // Empty + } + + public GalleryInfo(GalleryInfo gi) { + gid = gi.gid; + token = gi.token; + title = gi.title; + posted = gi.posted; + category = gi.category; + thumb = gi.thumb; + uploader = gi.uploader; + rating = gi.rating; + simpleLanguage = gi.simpleLanguage; + } + + @Override + public boolean equals(Object o) { + if (o instanceof GalleryInfo && ((GalleryInfo) o).gid == gid) + return true; + else + return false; + } + + public final void generateSLang() { + for (int i = 0; i < S_LANGS.length; i++) { + if (Pattern.compile(S_LANG_PATTERNS[i], Pattern.CASE_INSENSITIVE).matcher(title).find()) { + simpleLanguage = S_LANGS[i]; + return; + } + } + simpleLanguage = null; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/data/GalleryPopular.java b/app/src/main/java/com/hippo/ehviewer/data/GalleryPopular.java new file mode 100644 index 000000000..6395bb9ce --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/data/GalleryPopular.java @@ -0,0 +1,54 @@ +/* + * 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.data; + +import android.os.Parcel; +import android.os.Parcelable; + +public class GalleryPopular extends GalleryInfo { + public long count; + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public GalleryPopular createFromParcel(Parcel source) { + GalleryPopular p = new GalleryPopular(); + p.gid = source.readInt(); + p.token = source.readString(); + p.title = source.readString(); + p.posted = source.readString(); + p.category = source.readInt(); + p.thumb = source.readString(); + p.uploader = source.readString(); + p.rating = source.readFloat(); + p.simpleLanguage = source.readString(); + p.count = source.readLong(); + return p; + } + + @Override + public GalleryPopular[] newArray(int size) { + return new GalleryPopular[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeLong(count); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/data/HistoryItem.java b/app/src/main/java/com/hippo/ehviewer/data/HistoryItem.java new file mode 100644 index 000000000..f2a8bb28a --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/data/HistoryItem.java @@ -0,0 +1,73 @@ +/* + * 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.data; + +import android.os.Parcel; +import android.os.Parcelable; + +public class HistoryItem extends GalleryInfo { + public int mode; + public long date; + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public HistoryItem createFromParcel(Parcel source) { + HistoryItem p = new HistoryItem(); + p.gid = source.readInt(); + p.token = source.readString(); + p.title = source.readString(); + p.posted = source.readString(); + p.category = source.readInt(); + p.thumb = source.readString(); + p.uploader = source.readString(); + p.rating = source.readFloat(); + p.simpleLanguage = source.readString(); + p.mode = source.readInt(); + p.date = source.readLong(); + return p; + } + + @Override + public HistoryItem[] newArray(int size) { + return new HistoryItem[size]; + } + }; + + public HistoryItem() { + // Empty + } + + public HistoryItem(GalleryInfo gi, int m, long d) { + super(gi); + mode = m; + date = d; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(mode); + dest.writeLong(date); + } + +} diff --git a/app/src/main/java/com/hippo/ehviewer/data/LargePreviewList.java b/app/src/main/java/com/hippo/ehviewer/data/LargePreviewList.java new file mode 100644 index 000000000..6ca2e55d5 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/data/LargePreviewList.java @@ -0,0 +1,126 @@ +/* + * 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.data; + +import java.util.ArrayList; +import java.util.List; + +import android.annotation.SuppressLint; +import android.content.Intent; +import android.graphics.Bitmap; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import com.hippo.ehviewer.ImageLoader; +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.ui.GalleryActivity; +import com.hippo.ehviewer.widget.SimpleGridLayout; + +public class LargePreviewList extends PreviewList { + + private class Item { + public String mImageUrl; + public String mPageUrl; + + public Item(String imageUrl, String pageUrl) { + mImageUrl = imageUrl; + mPageUrl = pageUrl; + } + } + + public List mItemList = new ArrayList(); + + public void addItem(String imageUrl, String pageUrl) { + mItemList.add(new Item(imageUrl, pageUrl)); + } + + @Override + public String getPageUrl(int index) { + if (index >= 0 && index < mItemList.size()) + return mItemList.get(index).mPageUrl; + return null; + } + + @Override + public int size() { + return mItemList.size(); + } + + @SuppressLint("InflateParams") + @Override + public void addPreview(SimpleGridLayout viewGroup) { + if (mTargetPage != mHolder.getCurPreviewPage()) + return; + + viewGroup.removeAllViews(); + LayoutInflater inflater = LayoutInflater.from(mActivity); + int startIndex = mTargetPage * mPi.getPreviewPerPage(); + int index = startIndex; + for (Item item : mItemList) { + + View view = inflater.inflate(R.layout.preview_item, null); + ((TextView)view.findViewById(R.id.text)).setText(String.valueOf(index + 1)); + final int _index = index; + view.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + // Add to read in Data + Intent intent = new Intent(mActivity, + GalleryActivity.class); + intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, mPi.toGalleryInfo()); + intent.putExtra(GalleryActivity.KEY_START_INDEX, _index); + mActivity.startActivity(intent); + } + }); + viewGroup.addView(view); + // TODO I need a better key + ImageLoader.getInstance(mActivity).add(item.mImageUrl, mPi.getGid() + + "-preview-" + index, + new PreviewImageGetListener(viewGroup, index - startIndex)); + + index++; + } + } + + private class PreviewImageGetListener implements ImageLoader.OnGetImageListener { + private final SimpleGridLayout mViewGroup; + private final int mIndex; + + /** + * index is view index in viewgroup + * @param viewGroup + * @param index + */ + public PreviewImageGetListener(SimpleGridLayout viewGroup, int index) { + mViewGroup = viewGroup; + mIndex = index; + } + + @Override + public void onGetImage(String key, Bitmap bmp, int state) { + if (mTargetPage != mHolder.getCurPreviewPage()) + return; + + if (bmp == null) + mHolder.onGetPreviewImageFailure(); + else + ((ImageView)mViewGroup.getChildAt(mIndex).findViewById(R.id.image)).setImageBitmap(bmp); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/data/ListUrls.java b/app/src/main/java/com/hippo/ehviewer/data/ListUrls.java new file mode 100644 index 000000000..100c1e3c3 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/data/ListUrls.java @@ -0,0 +1,334 @@ +/* + * 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.data; + +import java.io.File; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; + +import com.hippo.ehviewer.ehclient.EhClient; +import com.hippo.ehviewer.network.UrlBuilder; +import com.hippo.ehviewer.util.Config; +import com.hippo.ehviewer.util.Utils; + +public class ListUrls { + + public static final boolean NUM_CATEGORY = false; + + // Mode + public static final int MODE_NORMAL = 0x0; + public static final int MODE_UPLOADER = 0x1; + public static final int MODE_TAG = 0x2; + public static final int MODE_POPULAR = 0x3; + public static final int MODE_IMAGE_SEARCH = 0x4; + + // Category + public static final int NONE = -1; // Use it for homepage + public static final int MISC = 0x1; + public static final int DOUJINSHI = 0x2; + public static final int MANGA = 0x4; + public static final int ARTIST_CG = 0x8; + public static final int GAME_CG = 0x10; + public static final int IMAGE_SET = 0x20; + public static final int COSPLAY = 0x40; + public static final int ASIAN_PORN = 0x80; + public static final int NON_H = 0x100; + public static final int WESTERN = 0x200; + public static final int UNKNOWN = 0x400; + + public static final int ALL_CATEGORT = UNKNOWN - 1; + //DOUJINSHI|MANGA|ARTIST_CG|GAME_CG|WESTERN|NON_H|IMAGE_SET|COSPLAY|ASIAN_PORN|MISC; + + // advance search + public static final int SNAME = 0x1; + public static final int STAGS = 0x2; + public static final int SDESC = 0x4; + public static final int STORR = 0x8; + public static final int STO = 0x10; + public static final int STD1 = 0x20; + public static final int STD2 = 0x40; + public static final int SH = 0x80; + + public static final int DEFAULT_ADVANCE = SNAME | STAGS; + public static final int DEFAULT_MIN_RATING = 2; + + private int mMode = MODE_NORMAL; + + private int page = 0; + private int category = NONE; + + private int advsearchType = -1; + private int minRating = -1; + + private String search = null; + private String mTag = null; + + private int mImageSearchMode; + private String mImageKey; + private String mImageUrl; + private File mSearchFile; + private String mResultUrl = null; + + public ListUrls() {} + + public ListUrls(int category) { + this.category = category; + } + + public ListUrls(int category, int page) { + this.category = category; + this.page = page; + } + + public ListUrls(int category, String search) { + this.category = category; + this.search = search; + } + + public ListUrls(int category, String search, int page) { + this.category = category; + this.search = search; + this.page = page; + } + + public void setCategory(int category) { + this.category = category; + } + + public int getCategory() { + return category; + } + + public String getSearch() { + return search; + } + + public void setAdvance(boolean enabled) { + if (enabled && advsearchType == -1) + advsearchType = DEFAULT_ADVANCE; + else if (!enabled) + advsearchType = -1; + } + + public void setAdvance(int advanceType) { + advsearchType = advanceType; + } + + public void setAdvance(int advanceType, int minRating) { + advsearchType = advanceType; + this.minRating = minRating; + } + + public boolean isAdvance() { + return advsearchType != -1; + } + + public int getAdvanceType() { + return advsearchType; + } + + public boolean isMinRating() { + return minRating != -1; + } + + public int getMinRating() { + return minRating; + } + + public void setPage(int p) { + page = p; + } + + public int getPage() { + return page; + } + + public int getMode() { + return mMode; + } + + public void setMode(int mode) { + mMode = mode; + } + + // For tag mode + public void setTag(String tag) { + mMode = MODE_TAG; + mTag = tag; + } + + public String getTag() { + return mTag; + } + + // For image search mode + public void setSearchImage(String key, String url, int imageSearchMode) { + mMode = MODE_IMAGE_SEARCH; + mImageKey = key; + mImageUrl = url; + mImageSearchMode = imageSearchMode; + } + + public void setSearchFile(File searchFile, int imageSearchMode) { + mMode = MODE_IMAGE_SEARCH; + mSearchFile = searchFile; + mImageSearchMode = imageSearchMode; + } + + public void setSearchResult(String resultUrl) { + mResultUrl = resultUrl; + } + + public File getSearchFile() { + return mSearchFile; + } + + public String getSearchImageKey() { + return mImageKey; + } + + public String getSearchImageUrl() { + return mImageUrl; + } + + public int getImageSearchMode() { + return mImageSearchMode; + } + + + public String getUrl() { + return getUrl(Config.getMode()); + } + + public String getUrl(int mode) { + + switch (mMode) { + case MODE_POPULAR: + return null; + + case MODE_IMAGE_SEARCH: + if (mResultUrl == null) + return null; + else + return mResultUrl + "&page=" + page; + + case MODE_TAG: + // Add tag + StringBuilder sb = new StringBuilder(EhClient.getUrlHeader(mode)); + sb.append("tag/"); + if (mTag != null) { + String[] tag = mTag.split("\\s+"); + try { + for (int i = 0; i < tag.length; i++) + tag[i] = URLEncoder.encode(tag[i], "UTF-8"); + } catch (UnsupportedEncodingException e) {} + sb.append(Utils.join(tag, '+')); + + // Add page + sb.append("/").append(page); + } else { + sb.append(page); + } + return sb.toString(); + + case MODE_NORMAL: + case MODE_UPLOADER: + default: + String header = EhClient.getUrlHeader(mode); + boolean isNeedFooter = false; + UrlBuilder ub; + // Add category + if (category != NONE) { + isNeedFooter = true; + if (NUM_CATEGORY) { + ub = new UrlBuilder(header + category); + } else { + ub = new UrlBuilder(header); + ub.addQuery("f_doujinshi", ((category & DOUJINSHI) == 0) ? "0" : "1"); + ub.addQuery("f_manga", ((category & MANGA) == 0) ? "0" : "1"); + ub.addQuery("f_artistcg", ((category & ARTIST_CG) == 0) ? "0" : "1"); + ub.addQuery("f_gamecg", ((category & GAME_CG) == 0) ? "0" : "1"); + ub.addQuery("f_western", ((category & WESTERN) == 0) ? "0" : "1"); + ub.addQuery("f_non-h", ((category & NON_H) == 0) ? "0" : "1"); + ub.addQuery("f_imageset", ((category & IMAGE_SET) == 0) ? "0" : "1"); + ub.addQuery("f_cosplay", ((category & COSPLAY) == 0) ? "0" : "1"); + ub.addQuery("f_asianporn", ((category & ASIAN_PORN) == 0) ? "0" : "1"); + ub.addQuery("f_misc", ((category & MISC) == 0) ? "0" : "1"); + } + } else { + ub = new UrlBuilder(header); + } + + // Add search item + if (search != null) { + isNeedFooter = true; + + String[] tag = search.split("\\s+"); + try { + for (int i = 0; i < tag.length; i++) + tag[i] = URLEncoder.encode(tag[i], "UTF-8"); + } catch (UnsupportedEncodingException e) {} + + ub.addQuery("f_search", Utils.join(tag, '+')); + } + + // Add page + if (page != 0) { + ub.addQuery("page", page); + } + + // Add foot + if (isNeedFooter) ub.addQuery("f_apply", "Apply+Filter"); + + // Add advance search + if (isAdvance()) { + ub.addQuery("advsearch", "1"); + if((advsearchType & SNAME) != 0) ub.addQuery("f_sname", "on"); + if((advsearchType & STAGS) != 0) ub.addQuery("f_stags", "on"); + if((advsearchType & SDESC) != 0) ub.addQuery("f_sdesc", "on"); + if((advsearchType & STORR) != 0) ub.addQuery("f_storr", "on"); + if((advsearchType & STO) != 0) ub.addQuery("f_sto", "on"); + if((advsearchType & STD1) != 0) ub.addQuery("f_sdt1", "on"); + if((advsearchType & STD2) != 0) ub.addQuery("f_sdt2", "on"); + if((advsearchType & SH) != 0) ub.addQuery("f_sh", "on"); + + // Set min star + if (isMinRating()) { + ub.addQuery("f_sr", "on"); + ub.addQuery("f_srdd", minRating); + } + } + return ub.build(); + } + } + + + @Override + public String toString() { + switch (mMode) { + case MODE_NORMAL: + case MODE_UPLOADER: + return search; + case MODE_TAG: + return mTag; + case MODE_POPULAR: + case MODE_IMAGE_SEARCH: + default: + return null; + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/data/LofiDetailImpl.java b/app/src/main/java/com/hippo/ehviewer/data/LofiDetailImpl.java new file mode 100644 index 000000000..f18780596 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/data/LofiDetailImpl.java @@ -0,0 +1,30 @@ +/* + * 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.data; + +public interface LofiDetailImpl extends PreviewImpl { + + public void setPreviewPageNum(int pageNum); + + /** + * Check null and empty before use + * + * @return + */ + public String[] getTags(); + +} diff --git a/app/src/main/java/com/hippo/ehviewer/data/LofiGalleryDetail.java b/app/src/main/java/com/hippo/ehviewer/data/LofiGalleryDetail.java new file mode 100644 index 000000000..2e24b35a5 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/data/LofiGalleryDetail.java @@ -0,0 +1,104 @@ +/* + * 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.data; + +import java.util.ArrayList; +import java.util.List; + +public class LofiGalleryDetail extends LofiGalleryInfo + implements LofiDetailImpl { + public int previewPageNum; + public int previewPerPage; + private final List previewLists = new ArrayList(); + + public LofiGalleryDetail(GalleryInfo galleryInfo) { + this(new LofiGalleryInfo(galleryInfo)); + } + + public LofiGalleryDetail(LofiGalleryInfo lofiGalleryInfo) { + gid = lofiGalleryInfo.gid; + token = lofiGalleryInfo.token; + title = lofiGalleryInfo.title; + posted = lofiGalleryInfo.posted; + category = lofiGalleryInfo.category; + thumb = lofiGalleryInfo.thumb; + uploader = lofiGalleryInfo.uploader; + rating = lofiGalleryInfo.rating; + simpleLanguage = lofiGalleryInfo.simpleLanguage; + lofiTags = lofiGalleryInfo.lofiTags; + + previewPageNum = Integer.MAX_VALUE; + } + + @Override + public int getGid() { + return gid; + } + + @Override + public String getToken() { + return token; + } + + @Override + public String getTitle() { + return title; + } + + @Override + public int getPreviewPageNum() { + return previewPageNum; + } + + @Override + public void setPreviewPageNum(int pageNum) { + previewPageNum = pageNum; + } + + @Override + public int getPreviewPerPage() { + return previewPerPage; + } + + @Override + public PreviewList getPreview(int page) { + if (page >= 0 && page < previewLists.size()) + return previewLists.get(page); + else + return null; + } + + @Override + public void setPreview(int page, PreviewList previewList) { + if (page >= 0 && page < previewLists.size()) + previewLists.set(page, previewList); + else if (page == previewLists.size()) + previewLists.add(previewList); + else + ; // Do nothing + } + + @Override + public String[] getTags() { + return lofiTags; + } + + @Override + public GalleryInfo toGalleryInfo() { + return new GalleryInfo(this); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/data/LofiGalleryInfo.java b/app/src/main/java/com/hippo/ehviewer/data/LofiGalleryInfo.java new file mode 100644 index 000000000..99b7cc295 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/data/LofiGalleryInfo.java @@ -0,0 +1,86 @@ +/* + * 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.data; + +import android.os.Parcel; +import android.os.Parcelable; + +public class LofiGalleryInfo extends GalleryInfo { + + /** + * It could be null, please check before use. + */ + public String[] lofiTags; + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public LofiGalleryInfo createFromParcel(Parcel source) { + int length; + + LofiGalleryInfo p = new LofiGalleryInfo(); + p.gid = source.readInt(); + p.token = source.readString(); + p.title = source.readString(); + p.posted = source.readString(); + p.category = source.readInt(); + p.thumb = source.readString(); + p.uploader = source.readString(); + p.rating = source.readFloat(); + p.simpleLanguage = source.readString(); + + length = source.readInt(); + if (length > 0) { + p.lofiTags = new String[length]; + for (int i = 0; i < length; i++) + p.lofiTags[i] = source.readString(); + } + + return p; + } + + @Override + public LofiGalleryInfo[] newArray(int size) { + return new LofiGalleryInfo[size]; + } + }; + + public LofiGalleryInfo() {} + + public LofiGalleryInfo(GalleryInfo galleryInfo) { + gid = galleryInfo.gid; + token = galleryInfo.token; + title = galleryInfo.title; + posted = galleryInfo.posted; + category = galleryInfo.category; + thumb = galleryInfo.thumb; + uploader = galleryInfo.uploader; + rating = galleryInfo.rating; + simpleLanguage = galleryInfo.simpleLanguage; + lofiTags = null; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + + int length = lofiTags == null ? 0 : lofiTags.length; + dest.writeInt(length); + for (int i = 0; i < length; i++) + dest.writeString(lofiTags[i]); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/data/NormalPreviewList.java b/app/src/main/java/com/hippo/ehviewer/data/NormalPreviewList.java new file mode 100644 index 000000000..1dd6701dc --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/data/NormalPreviewList.java @@ -0,0 +1,182 @@ +/* + * 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.data; + +import java.util.ArrayList; + +import android.annotation.SuppressLint; +import android.content.Intent; +import android.graphics.Bitmap; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import com.hippo.ehviewer.ImageLoader; +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.ui.GalleryActivity; +import com.hippo.ehviewer.widget.SimpleGridLayout; + +public class NormalPreviewList extends PreviewList{ + + public class Item { + public int xOffset; + public int yOffset; + public int width; + public int height; + public String url; + + public Item(int xOffset, int yOffset, int width, int height, String url) { + this.xOffset = xOffset; + this.yOffset = yOffset; + this.width = width; + this.height = height; + this.url = url; + } + } + + public class Row { + public String imageUrl; + public ArrayList itemArray = new ArrayList(); + public int startIndex; + public Row(String imageUrl) { + this.imageUrl = imageUrl; + } + + public void addItem(int xOffset, int yOffset, int width, int height, + String url) { + itemArray.add(new Item(xOffset, yOffset, width, height, url)); + } + } + + private Row curRow; + public ArrayList rowArray = new ArrayList(); + + public void addItem(String imageUrl, String xOffset, String yOffset, String width, String height, + String url) { + if (curRow == null) { + curRow = new Row(imageUrl); + curRow.startIndex = 0; + rowArray.add(curRow); + } else if (!curRow.imageUrl.equals(imageUrl)) { + Row lastRow = curRow; + curRow = new Row(imageUrl); + curRow.startIndex = lastRow.startIndex + lastRow.itemArray.size(); + rowArray.add(curRow); + } + + curRow.addItem(Integer.parseInt(xOffset), + Integer.parseInt(yOffset), Integer.parseInt(width), + Integer.parseInt(height), url); + } + + @Override + public int size() { + int sum = 0; + for (Row row : rowArray) + sum += row.itemArray.size(); + return sum; + } + + @Override + public String getPageUrl(int index) { + for (Row row : rowArray) { + if (index < row.startIndex + row.itemArray.size() && index >= row.startIndex) + return row.itemArray.get(index - row.startIndex).url; + } + return null; + } + + // TODO reuse the TextView in AutoWrapLayout + @SuppressLint("InflateParams") + @Override + public void addPreview(SimpleGridLayout viewGroup) { + if (mTargetPage != mHolder.getCurPreviewPage()) + return; + + viewGroup.removeAllViews(); + LayoutInflater inflater = LayoutInflater.from(mActivity); + int index = mTargetPage * mPi.getPreviewPerPage(); // it is display index + int rowIndex = 0; + for (NormalPreviewList.Row row : rowArray) { + for (int i = 0; i < row.itemArray.size(); i++) { + View view = inflater.inflate(R.layout.preview_item, null); + + ((TextView)view.findViewById(R.id.text)).setText(String.valueOf(index + 1)); + + final int _index = index; + view.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + // Add to read in Data + Intent intent = new Intent(mActivity, + GalleryActivity.class); + intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, mPi.toGalleryInfo()); + intent.putExtra(GalleryActivity.KEY_START_INDEX, _index); + mActivity.startActivity(intent); + } + }); + + viewGroup.addView(view); + index++; + } + // TODO I need a better key + ImageLoader.getInstance(mActivity).add(row.imageUrl, mPi.getGid() + + "-preview-" + mTargetPage + "-" + rowIndex, + new PreviewImageGetListener(viewGroup, rowIndex)); + rowIndex++; + } + } + + private class PreviewImageGetListener implements ImageLoader.OnGetImageListener { + private final SimpleGridLayout mViewGroup; + private final int mRowIndex; + + public PreviewImageGetListener(SimpleGridLayout viewGroup, int rowIndex) { + mViewGroup = viewGroup; + mRowIndex = rowIndex; + } + + @Override + public void onGetImage(String key, Bitmap bmp, int state) { + if (mTargetPage != mHolder.getCurPreviewPage()) + return; + + if (bmp == null) { + mHolder.onGetPreviewImageFailure(); + } else { + int maxWidth = bmp.getWidth(); + int maxHeight = bmp.getHeight(); + + NormalPreviewList.Row row = rowArray.get(mRowIndex); + int i = row.startIndex; + for(Item item : row.itemArray) { + + if (item.xOffset + item.width > maxWidth) + item.width = maxWidth - item.xOffset; + if (item.yOffset + item.height > maxHeight) + item.height = maxHeight - item.yOffset; + Bitmap bitmap = Bitmap.createBitmap(bmp, item.xOffset, item.yOffset, item.width, item.height); + + ((ImageView)mViewGroup.getChildAt(i).findViewById(R.id.image)).setImageBitmap(bitmap); + + i++; + } + } + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/data/PreviewImpl.java b/app/src/main/java/com/hippo/ehviewer/data/PreviewImpl.java new file mode 100644 index 000000000..1bc513f73 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/data/PreviewImpl.java @@ -0,0 +1,52 @@ +/* + * 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.data; + +public interface PreviewImpl { + + public int getGid(); + + public String getToken(); + + public String getTitle(); + + /** + * If it is unkonwn, return Integer.MAX_VALUE + * + * @return + */ + public int getPreviewPageNum(); + + public int getPreviewPerPage(); + + /** + * If page is error, return null + * + * @param page + * @return + */ + public PreviewList getPreview(int page); + + /** + * + * @param previewList + */ + public void setPreview(int page, PreviewList previewList); + + public GalleryInfo toGalleryInfo(); + +} diff --git a/app/src/main/java/com/hippo/ehviewer/data/PreviewList.java b/app/src/main/java/com/hippo/ehviewer/data/PreviewList.java new file mode 100644 index 000000000..ae33f88d8 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/data/PreviewList.java @@ -0,0 +1,77 @@ +/* + * 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.data; + +import android.app.Activity; +import android.content.Context; +import android.widget.TextView; + +import com.hippo.ehviewer.widget.SimpleGridLayout; + +/** + * This interface describe the previews in one page + * + * @author Hippo + * + */ +public abstract class PreviewList { + + protected PreviewHolder mHolder; + protected Activity mActivity; + protected PreviewImpl mPi; + protected int mTargetPage; + + /** + * Add preview to view group + * @param viewGroup + * @return + */ + public abstract void addPreview(SimpleGridLayout viewGroup); + + public abstract int size(); + + public void setData(PreviewHolder holder, Activity activity, PreviewImpl pi, int targetPage) { + mHolder = holder; + mActivity = activity; + mPi = pi; + mTargetPage = targetPage; + } + + public void setTargetPage(int targetPage) { + mTargetPage = targetPage; + } + + /** + * Return page url at target index, null for invalid index + * @param index + * @return + */ + public abstract String getPageUrl(int index); + + protected class TextViewWithUrl extends TextView { + public String url; + + public TextViewWithUrl(Context context) { + super(context); + } + } + + public interface PreviewHolder { + public int getCurPreviewPage(); + public void onGetPreviewImageFailure(); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/data/Tag.java b/app/src/main/java/com/hippo/ehviewer/data/Tag.java new file mode 100644 index 000000000..5009f233d --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/data/Tag.java @@ -0,0 +1,42 @@ +/* + * 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.data; + + +public class Tag extends ListUrls { + private String mName; + + public Tag(String name, int category, String search) { + super(category, search); + mName = name; + } + + public Tag(String name, ListUrls lus) { + this(name, lus.getCategory(), lus.getSearch()); + setAdvance(lus.getAdvanceType(), lus.getMinRating()); + setTag(lus.getTag()); + setMode(lus.getMode()); + } + + public void setName(String name) { + mName = name; + } + + public String getName() { + return mName; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/drawable/BatteryDrawable.java b/app/src/main/java/com/hippo/ehviewer/drawable/BatteryDrawable.java new file mode 100644 index 000000000..a4d52db1f --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/drawable/BatteryDrawable.java @@ -0,0 +1,172 @@ +/* + * 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.drawable; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; + +import com.hippo.ehviewer.util.MathUtils; + +public class BatteryDrawable extends Drawable { + + @SuppressWarnings("unused") + private static final String TAG = BatteryDrawable.class.getSimpleName(); + + public static final int WARN_LIMIT = 15; + + private int mColor = Color.WHITE; + private int mWarnColor = Color.RED; + private int mElect = -1; + private final Paint mPaint; + + private final Rect mTopRect; + private final Rect mBottomRect; + private final Rect mRightRect; + private final Rect mHeadRect; + private final Rect mElectRect; + private int mStart; + private int mStop; + + public BatteryDrawable() { + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); + mPaint.setStyle(Paint.Style.FILL); + + mTopRect = new Rect(); + mBottomRect = new Rect(); + mRightRect = new Rect(); + mHeadRect = new Rect(); + mElectRect = new Rect(); + updatePaint(); + } + + @Override + protected void onBoundsChange(Rect bounds) { + int width = bounds.width(); + int height = bounds.height(); + int strokeWidth = (int) (Math.sqrt(width * width + height * height) * 0.06f); + int turn1 = width * 6 / 7; + int turn2 = height / 3; + int secBottom = height - strokeWidth; + mStart = strokeWidth; + mStop = turn1 - strokeWidth; + + mTopRect.set(0, 0, turn1, strokeWidth); + mBottomRect.set(0, secBottom, turn1, height); + mRightRect.set(turn1 - strokeWidth, strokeWidth, turn1, secBottom); + mHeadRect.set(turn1, turn2, width, height - turn2); + mElectRect.set(0, strokeWidth, mStop, secBottom); + } + + /** + * How to draw:
+ * |------------------------------|
+ * |\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\|
+ * |------------------------------|---|
+ * |/////////////////| |//|\\\|
+ * |/////////////////| |//|\\\|
+ * |------------------------------|---|
+ * |\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\|
+ * |------------------------------| + */ + @Override + public void draw(Canvas canvas) { + if (mElect == -1) + return; + + mElectRect.right = MathUtils.lerp(mStart, mStop, mElect / 100.0f); + + canvas.drawRect(mTopRect, mPaint); + canvas.drawRect(mBottomRect, mPaint); + canvas.drawRect(mRightRect, mPaint); + canvas.drawRect(mHeadRect, mPaint); + canvas.drawRect(mElectRect, mPaint); + } + + private boolean isWarn() { + return mElect <= WARN_LIMIT; + } + + public void setColor(int color) { + if (mColor == color) + return; + + mColor = color; + if (!isWarn()) { + mPaint.setColor(mColor); + invalidateSelf(); + } + } + + public void setWarnColor(int color) { + if (mWarnColor == color) + return; + + mWarnColor = color; + if (isWarn()) { + mPaint.setColor(mWarnColor); + invalidateSelf(); + } + } + + public void setElect(int elect) { + if (mElect == elect) + return; + + mElect = elect; + updatePaint(); + } + + public void setElect(int elect, boolean warn) { + if (mElect == elect) + return; + + mElect = elect; + updatePaint(warn); + } + + private void updatePaint() { + updatePaint(isWarn()); + } + + private void updatePaint(boolean warn) { + if (warn) + mPaint.setColor(mWarnColor); + else + mPaint.setColor(mColor); + invalidateSelf(); + } + + @Override + public void setAlpha(int alpha) { + // Empty + } + + @Override + public void setColorFilter(ColorFilter cf) { + // Empty + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/drawable/FreeMaterialDrawable.java b/app/src/main/java/com/hippo/ehviewer/drawable/FreeMaterialDrawable.java new file mode 100644 index 000000000..01f81ba95 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/drawable/FreeMaterialDrawable.java @@ -0,0 +1,187 @@ +/* + * 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.drawable; + +import java.util.Arrays; + +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; + +import com.hippo.ehviewer.util.MathUtils; +import com.hippo.ehviewer.util.Ui; + +public class FreeMaterialDrawable extends Drawable { + + private static final String TAG = FreeMaterialDrawable.class.getSimpleName(); + + private static final int MAX_SPRITE_NUM = 6; + + private static final int SPRITE_SHAPE_RECTANGLE = 0; + private static final int SPRITE_SHAPE_ROUND = 1; + private static final int SPRITE_SHAPE_MAX = 2; + + private boolean mKeepTop; + private int mBgIndex; + private int mBgColor; + private final Paint mPaint; + private int mSpriteNum; + private final Sprite[] mSpriteArray = new Sprite[MAX_SPRITE_NUM]; + + private static final int[] sColorArray = new int[] { + 0xff9c27b0, 0xff673ab7, + 0xff3f51b5, 0xff2196f3, 0xff03a9f4, 0xff00bcd4, + 0xff4caf50 + }; + + public FreeMaterialDrawable () { + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mPaint.setStyle(Paint.Style.FILL); + mPaint.setShadowLayer(Ui.dp2pix(3), 0, 0, 0x8a000000); + } + + public FreeMaterialDrawable setBgIndex(int bgIndex) { + mBgIndex = bgIndex; + return this; + } + + public FreeMaterialDrawable setKeepTop(boolean keepTop) { + mKeepTop = keepTop; + return this; + } + + private int getColor(int[] copyOfColorArray) { + int index = MathUtils.random(0, copyOfColorArray.length); + int color = copyOfColorArray[index]; + while (color == -1) + color = copyOfColorArray[++index < copyOfColorArray.length ? index : (index = 0)]; + copyOfColorArray[index] = -1; + return color; + } + + @Override + protected void onBoundsChange(Rect bounds) { + int w = bounds.width(); + int h = bounds.height(); + Sprite s; + int[] copyOfColorArray = Arrays.copyOf(sColorArray, sColorArray.length); + if (mBgIndex >= 0) { + mBgColor = copyOfColorArray[mBgIndex]; + copyOfColorArray[mBgIndex] = -1; + } else { + mBgColor = getColor(copyOfColorArray); + } + mSpriteNum = MathUtils.random(4, MAX_SPRITE_NUM + 1); + for (int i = 0; i < mSpriteNum; i++) { + switch (SPRITE_SHAPE_ROUND) {//MathUtils.random(0, SPRITE_SHAPE_MAX)) { + case SPRITE_SHAPE_RECTANGLE: + s = new Rectangle(w, h, getColor(copyOfColorArray)); + break; + default: + case SPRITE_SHAPE_ROUND: + s = new Round(w, h, getColor(copyOfColorArray)); + break; + } + mSpriteArray[i] = s; + } + } + + @Override + public void draw(Canvas canvas) { + canvas.drawColor(mBgColor); + + for (int i = 0; i < mSpriteNum; i++) + mSpriteArray[i].draw(canvas, mPaint); + } + + @Override + public void setAlpha(int alpha) { + // Empty + } + + @Override + public void setColorFilter(ColorFilter cf) { + // Empty + } + + @Override + public int getOpacity() { + return PixelFormat.OPAQUE; + } + + private abstract class Sprite { + + private final int mColor; + + public Sprite(int w, int h, int color) { + mColor = color; + } + + public void draw(Canvas canvas, Paint paint) { + paint.setColor(mColor); + doDraw(canvas, paint); + } + + public abstract void doDraw(Canvas canvas, Paint paint); + } + + private class Rectangle extends Sprite { + + private final int left; + private final int top; + private final int right; + private final int bottom; + + public Rectangle(int w, int h, int color) { + super(w, h, color); + + int width = MathUtils.random(w / 4, w / 2); + int height = MathUtils.random(h / 4, h / 2); + left = MathUtils.random(w - width / 2); + top = MathUtils.random(h - height / 2); + right = left + width; + bottom = top + height; + } + + @Override + public void doDraw(Canvas canvas, Paint paint) { + canvas.drawRect(left, top, right, bottom, paint); + } + } + + private class Round extends Sprite { + + int mX, mY, mRadius; + + public Round(int w, int h, int color) { + super(w, h, color); + + int t = (w + h) / 2; + mRadius = MathUtils.random(t / 8, t / 4); + mX = MathUtils.random(w); + mY = MathUtils.random(mKeepTop ? mRadius + 10 : 0, h); + } + + @Override + public void doDraw(Canvas canvas, Paint paint) { + canvas.drawCircle(mX, mY, mRadius, paint); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/drawable/MaterialIndicatorDrawable.java b/app/src/main/java/com/hippo/ehviewer/drawable/MaterialIndicatorDrawable.java new file mode 100644 index 000000000..b0cde2dd9 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/drawable/MaterialIndicatorDrawable.java @@ -0,0 +1,695 @@ +/* + * Copyright (C) 2014 Balys Valentukevicius + * + * 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.drawable; + + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +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.PixelFormat; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.util.Property; +import android.util.TypedValue; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + + +public class MaterialIndicatorDrawable extends Drawable implements Animatable { + + private static final String STATE_KEY = "material_menu_icon_state"; + + public enum IconState { + BURGER, ARROW + } + + public enum AnimationState { + BURGER_ARROW; + + public IconState getFirstState() { + switch (this) { + case BURGER_ARROW: + return IconState.BURGER; + default: + return null; + } + } + + public IconState getSecondState() { + switch (this) { + case BURGER_ARROW: + return IconState.ARROW; + default: + return null; + } + } + } + + public enum Stroke { + /** + * 3 dip + */ + REGULAR(3), + /** + * 2 dip + */ + THIN(2), + /** + * 1 dip + */ + EXTRA_THIN(1); + + private final int strokeWidth; + + Stroke(int strokeWidth) { + this.strokeWidth = strokeWidth; + } + + protected static Stroke valueOf(int strokeWidth) { + switch (strokeWidth) { + case 3: + return REGULAR; + case 2: + return THIN; + case 1: + return EXTRA_THIN; + default: + return THIN; + } + } + } + + public static final int DEFAULT_COLOR = Color.WHITE; + public static final int DEFAULT_SCALE = 1; + public static final int DEFAULT_TRANSFORM_DURATION = 800; + public static final int DEFAULT_PRESSED_DURATION = 400; + + private static final int BASE_DRAWABLE_WIDTH = 40; + private static final int BASE_DRAWABLE_HEIGHT = 40; + private static final int BASE_ICON_WIDTH = 20; + private static final int BASE_CIRCLE_RADIUS = 18; + + private static final float ARROW_MID_LINE_ANGLE = 180; + private static final float ARROW_TOP_LINE_ANGLE = 135; + private static final float ARROW_BOT_LINE_ANGLE = 225; + + private static final float TRANSFORMATION_START = 0; + private static final float TRANSFORMATION_MID = 1.0f; + private static final float TRANSFORMATION_END = 2.0f; + + private static final int DEFAULT_CIRCLE_ALPHA = 200; + + private final float diph; + private final float dip1; + private final float dip2; + private final float dip3; + private final float dip4; + + private final int width; + private final int height; + private final float strokeWidth; + private final float iconWidth; + private final float topPadding; + private final float sidePadding; + private final float circleRadius; + + private final Stroke stroke; + + private final Object lock = new Object(); + + private final Paint gridPaint = new Paint(); + private final Paint iconPaint = new Paint(); + private final Paint circlePaint = new Paint(); + + private float transformationValue = 0f; + private float pressedProgressValue = 0f; + private boolean transformationRunning = false; + + private IconState currentIconState = IconState.BURGER; + private AnimationState animationState = AnimationState.BURGER_ARROW; + + private IconState animatingIconState; + private boolean drawTouchCircle; + private boolean neverDrawTouch; + private boolean rtlEnabled; + + private ObjectAnimator transformation; + private ObjectAnimator pressedCircle; + + private MaterialMenuState materialMenuState; + + public MaterialIndicatorDrawable(Context context, int color, Stroke stroke) { + this(context, color, stroke, DEFAULT_SCALE, DEFAULT_TRANSFORM_DURATION, DEFAULT_PRESSED_DURATION); + } + + public MaterialIndicatorDrawable(Context context, int color, Stroke stroke, int transformDuration, int pressedDuration) { + this(context, color, stroke, DEFAULT_SCALE, transformDuration, pressedDuration); + } + + public MaterialIndicatorDrawable(Context context, int color, Stroke stroke, int scale, int transformDuration, int pressedDuration) { + Resources resources = context.getResources(); + // convert each separately due to various densities + this.dip1 = dpToPx(resources, 1) * scale; + this.dip2 = dpToPx(resources, 2) * scale; + this.dip3 = dpToPx(resources, 3) * scale; + this.dip4 = dpToPx(resources, 4) * scale; + this.diph = dip1 / 2; + + this.stroke = stroke; + this.width = (int) (dpToPx(resources, BASE_DRAWABLE_WIDTH) * scale); + this.height = (int) (dpToPx(resources, BASE_DRAWABLE_HEIGHT) * scale); + this.iconWidth = dpToPx(resources, BASE_ICON_WIDTH) * scale; + this.circleRadius = dpToPx(resources, BASE_CIRCLE_RADIUS) * scale; + this.strokeWidth = dpToPx(resources, stroke.strokeWidth) * scale; + + this.sidePadding = (width - iconWidth) / 2; + this.topPadding = (height - 5 * dip3) / 2; + + initPaint(color); + initAnimations(transformDuration, pressedDuration); + + materialMenuState = new MaterialMenuState(); + } + + private MaterialIndicatorDrawable(int color, Stroke stroke, long transformDuration, long pressedDuration, + int width, int height, float iconWidth, float circleRadius, float strokeWidth, float dip1 + ) { + this.dip1 = dip1; + this.dip2 = dip1 * 2; + this.dip3 = dip1 * 3; + this.dip4 = dip1 * 4; + this.diph = dip1 / 2; + this.stroke = stroke; + this.width = width; + this.height = height; + this.iconWidth = iconWidth; + this.circleRadius = circleRadius; + this.strokeWidth = strokeWidth; + this.sidePadding = (width - iconWidth) / 2; + this.topPadding = (height - 5 * dip3) / 2; + + initPaint(color); + initAnimations((int) transformDuration, (int) pressedDuration); + + materialMenuState = new MaterialMenuState(); + } + + private void initPaint(int color) { + gridPaint.setAntiAlias(false); + gridPaint.setColor(Color.GREEN); + gridPaint.setStrokeWidth(1); + + iconPaint.setAntiAlias(true); + iconPaint.setStyle(Style.STROKE); + iconPaint.setStrokeWidth(strokeWidth); + iconPaint.setColor(color); + + circlePaint.setAntiAlias(true); + circlePaint.setStyle(Style.FILL); + circlePaint.setColor(color); + circlePaint.setAlpha(DEFAULT_CIRCLE_ALPHA); + + setBounds(0, 0, width, height); + } + + /* + * Drawing + */ + + @Override public void draw(Canvas canvas) { + final float ratio = transformationValue <= 1 ? transformationValue : 2 - transformationValue; + + if (rtlEnabled) { + canvas.save(); + canvas.scale(-1, 1, 0, 0); + canvas.translate(-getIntrinsicWidth(), 0); + } + + drawTopLine(canvas, ratio); + drawMiddleLine(canvas, ratio); + drawBottomLine(canvas, ratio); + + if (rtlEnabled) { + canvas.restore(); + } + + if (drawTouchCircle) drawTouchCircle(canvas); + } + + private void drawTouchCircle(Canvas canvas) { + canvas.restore(); + canvas.drawCircle(width / 2, height / 2, pressedProgressValue, circlePaint); + } + + private void drawMiddleLine(Canvas canvas, float ratio) { + canvas.restore(); + canvas.save(); + + float rotation = 0; + float pivotX = width / 2; + float pivotY = width / 2; + float startX = sidePadding; + float startY = topPadding + dip3 / 2 * 5; + float stopX = width - sidePadding; + float stopY = topPadding + dip3 / 2 * 5; + int alpha = 255; + + switch (animationState) { + case BURGER_ARROW: + // rotate by 180 + if (isMorphingForward()) { + rotation = ratio * ARROW_MID_LINE_ANGLE; + } else { + rotation = ARROW_MID_LINE_ANGLE + (1 - ratio) * ARROW_MID_LINE_ANGLE; + } + // shorten one end + stopX -= ratio * resolveStrokeModifier(ratio) / 2; + break; + } + + iconPaint.setAlpha(alpha); + canvas.rotate(rotation, pivotX, pivotY); + canvas.drawLine(startX, startY, stopX, stopY, iconPaint); + iconPaint.setAlpha(255); + } + + private void drawTopLine(Canvas canvas, float ratio) { + canvas.save(); + + float rotation = 0, pivotX = 0, pivotY = 0; + float rotation2 = 0; + // pivot at center of line + float pivotX2 = width / 2 + dip3 / 2; + float pivotY2 = topPadding + dip2; + + float startX = sidePadding; + float startY = topPadding + dip2; + float stopX = width - sidePadding; + float stopY = topPadding + dip2; + int alpha = 255; + + switch (animationState) { + case BURGER_ARROW: + if (isMorphingForward()) { + // rotate until required angle + rotation = ratio * ARROW_BOT_LINE_ANGLE; + } else { + // rotate back to start doing a 360 + rotation = ARROW_BOT_LINE_ANGLE + (1 - ratio) * ARROW_TOP_LINE_ANGLE; + } + // rotate by middle + pivotX = width / 2; + pivotY = height / 2; + + // shorten both ends + stopX -= resolveStrokeModifier(ratio); + startX += dip3 * ratio; + + break; + } + + iconPaint.setAlpha(alpha); + canvas.rotate(rotation, pivotX, pivotY); + canvas.rotate(rotation2, pivotX2, pivotY2); + canvas.drawLine(startX, startY, stopX, stopY, iconPaint); + iconPaint.setAlpha(255); + } + + private void drawBottomLine(Canvas canvas, float ratio) { + canvas.restore(); + canvas.save(); + + float rotation = 0, pivotX = 0, pivotY = 0; + float rotation2 = 0; + // pivot at center of line + float pivotX2 = width / 2 + dip3 / 2; + float pivotY2 = height - topPadding - dip2; + + float startX = sidePadding; + float startY = height - topPadding - dip2; + float stopX = width - sidePadding; + float stopY = height - topPadding - dip2; + + switch (animationState) { + case BURGER_ARROW: + if (isMorphingForward()) { + // rotate to required angle + rotation = ARROW_TOP_LINE_ANGLE * ratio; + } else { + // rotate back to start doing a 360 + rotation = ARROW_TOP_LINE_ANGLE + (1 - ratio) * ARROW_BOT_LINE_ANGLE; + } + // pivot center of canvas + pivotX = width / 2; + pivotY = height / 2; + + // shorten both ends + stopX = width - sidePadding - resolveStrokeModifier(ratio); + startX = sidePadding + dip3 * ratio; + break; + } + + canvas.rotate(rotation, pivotX, pivotY); + canvas.rotate(rotation2, pivotX2, pivotY2); + canvas.drawLine(startX, startY, stopX, stopY, iconPaint); + } + + private boolean isMorphingForward() { + return transformationValue <= TRANSFORMATION_MID; + } + + private float resolveStrokeModifier(float ratio) { + switch (stroke) { + case REGULAR: + return ratio * dip3; + case THIN: + return ratio * (dip3 + diph); + case EXTRA_THIN: + return ratio * dip4; + } + return 0; + } + + @Override public void setAlpha(int alpha) { + iconPaint.setAlpha(alpha); + } + + @Override public void setColorFilter(ColorFilter cf) { + iconPaint.setColorFilter(cf); + } + + @Override public int getOpacity() { + return PixelFormat.TRANSPARENT; + } + + /* + * Accessor methods + */ + + public void setColor(int color) { + iconPaint.setColor(color); + circlePaint.setColor(color); + invalidateSelf(); + } + + public void setTransformationDuration(int duration) { + transformation.setDuration(duration); + } + + public void setPressedDuration(int duration) { + pressedCircle.setDuration(duration); + } + + public void setInterpolator(Interpolator interpolator) { + transformation.setInterpolator(interpolator); + } + + public void setNeverDrawTouch(boolean neverDrawTouch) { + this.neverDrawTouch = neverDrawTouch; + } + + public void setIconState(IconState iconState) { + synchronized (lock) { + if (transformationRunning) { + transformation.cancel(); + transformationRunning = false; + } + + if (currentIconState == iconState) return; + + switch (iconState) { + case BURGER: + animationState = AnimationState.BURGER_ARROW; + transformationValue = TRANSFORMATION_START; + break; + case ARROW: + animationState = AnimationState.BURGER_ARROW; + transformationValue = TRANSFORMATION_MID; + break; + } + currentIconState = iconState; + invalidateSelf(); + } + } + + public void animateIconState(IconState state, boolean drawTouch) { + synchronized (lock) { + if (transformationRunning) { + transformation.end(); + pressedCircle.end(); + } + drawTouchCircle = drawTouch; + animatingIconState = state; + start(); + } + } + + public IconState setTransformationOffset(AnimationState animationState, float offset) { + if (offset < TRANSFORMATION_START || offset > TRANSFORMATION_END) { + throw new IllegalArgumentException( + String.format("Value must be between %s and %s", TRANSFORMATION_START, TRANSFORMATION_END) + ); + } + + this.animationState = animationState; + + final boolean isFirstIcon = offset < TRANSFORMATION_MID || offset == TRANSFORMATION_END; + + currentIconState = isFirstIcon ? animationState.getFirstState() : animationState.getSecondState(); + animatingIconState = isFirstIcon ? animationState.getSecondState() : animationState.getFirstState(); + + setTransformationValue(offset); + + return currentIconState; + } + + public void setRTLEnabled(boolean rtlEnabled) { + this.rtlEnabled = rtlEnabled; + invalidateSelf(); + } + + public IconState getIconState() { + return currentIconState; + } + + /* + * Animations + */ + private final Property transformationProperty + = new Property(Float.class, "transformation") { + @Override + public Float get(MaterialIndicatorDrawable object) { + return object.getTransformationValue(); + } + + @Override + public void set(MaterialIndicatorDrawable object, Float value) { + object.setTransformationValue(value); + } + }; + + private final Property pressedProgressProperty + = new Property(Float.class, "pressedProgress") { + @Override + public Float get(MaterialIndicatorDrawable object) { + return object.getPressedProgress(); + } + + @Override + public void set(MaterialIndicatorDrawable object, Float value) { + object.setPressedProgress(value); + } + }; + + public Float getTransformationValue() { + return transformationValue; + } + + public void setTransformationValue(Float value) { + this.transformationValue = value; + invalidateSelf(); + } + + public Float getPressedProgress() { + return pressedProgressValue; + } + + public void setPressedProgress(Float value) { + this.pressedProgressValue = value; + circlePaint.setAlpha((int) (DEFAULT_CIRCLE_ALPHA * (1 - value / (circleRadius * 1.22f)))); + invalidateSelf(); + } + + private void initAnimations(int transformDuration, int pressedDuration) { + transformation = ObjectAnimator.ofFloat(this, transformationProperty, 0); + transformation.setInterpolator(new DecelerateInterpolator(3)); + transformation.setDuration(transformDuration); + transformation.addListener(new AnimatorListenerAdapter() { + @Override public void onAnimationEnd(Animator animation) { + transformationRunning = false; + setIconState(animatingIconState); + } + }); + + + pressedCircle = ObjectAnimator.ofFloat(this, pressedProgressProperty, 0, 0); + pressedCircle.setDuration(pressedDuration); + pressedCircle.setInterpolator(new DecelerateInterpolator()); + pressedCircle.addListener(new AnimatorListenerAdapter() { + @Override public void onAnimationEnd(Animator animation) { + pressedProgressValue = 0; + } + + @Override public void onAnimationCancel(Animator animation) { + pressedProgressValue = 0; + } + }); + } + + private boolean resolveTransformation() { + boolean isCurrentBurger = currentIconState == IconState.BURGER; + boolean isCurrentArrow = currentIconState == IconState.ARROW; + boolean isAnimatingBurger = animatingIconState == IconState.BURGER; + boolean isAnimatingArrow = animatingIconState == IconState.ARROW; + + if ((isCurrentBurger && isAnimatingArrow) || (isCurrentArrow && isAnimatingBurger)) { + animationState = AnimationState.BURGER_ARROW; + return isCurrentBurger; + } + + throw new IllegalStateException( + String.format("Animating from %s to %s is not supported", currentIconState, animatingIconState) + ); + } + + @Override public void start() { + if (transformationRunning || animatingIconState == null || animatingIconState == currentIconState) return; + transformationRunning = true; + + final boolean direction = resolveTransformation(); + transformation.setFloatValues( + direction ? TRANSFORMATION_START : TRANSFORMATION_MID, + direction ? TRANSFORMATION_MID : TRANSFORMATION_END + ); + transformation.start(); + + if (pressedCircle.isRunning()) { + pressedCircle.cancel(); + } + if (drawTouchCircle && !neverDrawTouch) { + pressedCircle.setFloatValues(0, circleRadius * 1.22f); + pressedCircle.start(); + } + + invalidateSelf(); + } + + @Override public void stop() { + if (isRunning() && transformation.isRunning()) { + transformation.end(); + } else { + transformationRunning = false; + invalidateSelf(); + } + } + + @Override public boolean isRunning() { + return transformationRunning; + } + + @Override + public int getIntrinsicWidth() { + return width; + } + + @Override + public int getIntrinsicHeight() { + return height; + } + + + @Override + public ConstantState getConstantState() { + materialMenuState.changingConfigurations = getChangingConfigurations(); + return materialMenuState; + } + + @Override + public Drawable mutate() { + materialMenuState = new MaterialMenuState(); + return this; + } + + /** + * Call from {@link android.app.Activity#onSaveInstanceState(android.os.Bundle)} to store current icon state + * + * @param outState outState + */ + public void onSaveInstanceState(Bundle outState) { + outState.putString(STATE_KEY, currentIconState.name()); + } + + /** + * Call from {@link android.app.Activity#onPostCreate(android.os.Bundle)} to restore icon state + * + * @param state state + */ + public void syncState(Bundle state) { + if (state != null) { + String iconStateName = state.getString(STATE_KEY); + if (iconStateName == null) { + iconStateName = MaterialIndicatorDrawable.IconState.BURGER.name(); + } + setIconState(MaterialIndicatorDrawable.IconState.valueOf(iconStateName)); + } + } + + private final class MaterialMenuState extends ConstantState { + private int changingConfigurations; + + private MaterialMenuState() { + } + + @Override + public Drawable newDrawable() { + MaterialIndicatorDrawable drawable = new MaterialIndicatorDrawable( + circlePaint.getColor(), stroke, transformation.getDuration(), + pressedCircle.getDuration(), width, height, iconWidth, circleRadius, strokeWidth, dip1 + ); + drawable.setIconState(animatingIconState != null ? animatingIconState : currentIconState); + drawable.setRTLEnabled(rtlEnabled); + return drawable; + } + + @Override + public int getChangingConfigurations() { + return changingConfigurations; + } + } + + static float dpToPx(Resources resources, float dp) { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.getDisplayMetrics()); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/drawable/MaterialProgressDrawable.java b/app/src/main/java/com/hippo/ehviewer/drawable/MaterialProgressDrawable.java new file mode 100644 index 000000000..d404d6f7f --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/drawable/MaterialProgressDrawable.java @@ -0,0 +1,446 @@ +package com.hippo.ehviewer.drawable; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.animation.TimeInterpolator; +import android.annotation.TargetApi; +import android.content.res.ColorStateList; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.LinearInterpolator; + +import java.util.ArrayList; + +// Get from android.graphics.drawable.MaterialProgressDrawable +public class MaterialProgressDrawable extends Drawable implements Animatable { + private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); + private static final TimeInterpolator END_CURVE_INTERPOLATOR = new EndCurveInterpolator(); + private static final TimeInterpolator START_CURVE_INTERPOLATOR = new StartCurveInterpolator(); + + private static final long ANIMATION_DURATION = 1333; + + private static final int NUM_POINTS = 5; + + private final ArrayList mAnimators = new ArrayList(); + + private final Ring mRing; + + private MaterialProgressState mState; + + private float mRotation; + + private boolean mMutated; + + public MaterialProgressDrawable() { + this(new MaterialProgressState(null)); + } + + public MaterialProgressDrawable(int width, int height, int color, float strokeWidth, float innerRadius) { + this(new MaterialProgressState(width, height, color, strokeWidth, innerRadius)); + } + + private MaterialProgressDrawable(MaterialProgressState state) { + mState = state; + mRing = new Ring(mCallback); + mMutated = false; + + initializeFromState(); + setupAnimators(); + } + + private void initializeFromState() { + MaterialProgressState state = mState; + + Ring ring = mRing; + ring.setStrokeWidth(state.mStrokeWidth); + + int color = state.mColor.getColorForState(getState(), 0); + ring.setColor(color); + + float minEdge = Math.min(state.mWidth, state.mHeight); + if ((state.mInnerRadius <= 0.0F) || (minEdge < 0.0F)) { + ring.setInsets((int) Math.ceil(state.mStrokeWidth / 2.0F)); + } else { + float insets = minEdge / 2.0F - state.mInnerRadius; + ring.setInsets(insets); + } + } + + @Override + public Drawable mutate() { + if ((!mMutated) && (super.mutate() == this)) { + mState = new MaterialProgressState(mState); + mMutated = true; + } + return this; + } + + @Override + protected boolean onStateChange(int[] state) { + boolean changed = super.onStateChange(state); + + int color = mState.mColor.getColorForState(state, 0); + if (color != mRing.getColor()) { + mRing.setColor(color); + changed = true; + } + + return changed; + } + + @Override + public boolean isStateful() { + return (super.isStateful()) || (mState.mColor.isStateful()); + } + + @Override + public int getIntrinsicHeight() { + return mState.mHeight; + } + + @Override + public int getIntrinsicWidth() { + return mState.mWidth; + } + + @Override + public void draw(Canvas c) { + Rect bounds = getBounds(); + int saveCount = c.save(); + c.rotate(mRotation, bounds.exactCenterX(), bounds.exactCenterY()); + mRing.draw(c, bounds); + c.restoreToCount(saveCount); + } + + public void setColor(int color) { + mState.mColor = ColorStateList.valueOf(color); + mRing.setColor(color); + } + + @Override + public void setAlpha(int alpha) { + mRing.setAlpha(alpha); + } + + @Override + public int getAlpha() { + return mRing.getAlpha(); + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + mRing.setColorFilter(colorFilter); + } + + private void setRotation(float rotation) { + mRotation = rotation; + invalidateSelf(); + } + + private float getRotation() { + return mRotation; + } + + @Override + public int getOpacity() { + return -3; + } + + @Override + public boolean setVisible(boolean visible, boolean restart) { + boolean changed = super.setVisible(visible, restart); + if (visible) { + if ((changed) || (restart)) { + start(); + } + } else { + stop(); + } + return changed; + } + + @Override + public boolean isRunning() { + ArrayList animators = mAnimators; + int N = animators.size(); + for (int i = 0; i < N; i++) { + Animator animator = animators.get(i); + if (animator.isRunning()) { + return true; + } + } + return false; + } + + @Override + @TargetApi(Build.VERSION_CODES.KITKAT) + public void start() { + ArrayList animators = mAnimators; + int N = animators.size(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + for (int i = 0; i < N; i++) { + Animator animator = animators.get(i); + if (animator.isPaused()) + animator.resume(); + else if (!animator.isRunning()) + animator.start(); + } + } else { + for (int i = 0; i < N; i++) { + Animator animator = animators.get(i); + if (!animator.isRunning()) + animator.start(); + } + } + } + + @Override + @TargetApi(Build.VERSION_CODES.KITKAT) + public void stop() { + ArrayList animators = mAnimators; + int N = animators.size(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + for (int i = 0; i < N; i++) { + animators.get(i).pause(); + } + } else { + for (int i = 0; i < N; i++) { + animators.get(i).cancel(); + } + } + } + + private void setupAnimators() { + Ring ring = mRing; + + ObjectAnimator endTrim = ObjectAnimator.ofFloat(ring, "endTrim", + new float[] { 0.0F, 0.75F }); + endTrim.setDuration(ANIMATION_DURATION); + endTrim.setInterpolator(START_CURVE_INTERPOLATOR); + endTrim.setRepeatCount(-1); + endTrim.setRepeatMode(1); + + ObjectAnimator startTrim = ObjectAnimator.ofFloat(ring, "startTrim", + new float[] { 0.0F, 0.75F }); + startTrim.setDuration(ANIMATION_DURATION); + startTrim.setInterpolator(END_CURVE_INTERPOLATOR); + startTrim.setRepeatCount(-1); + startTrim.setRepeatMode(1); + + ObjectAnimator rotation = ObjectAnimator.ofFloat(ring, "rotation", + new float[] { 0.0F, 0.25F }); + rotation.setDuration(ANIMATION_DURATION); + rotation.setInterpolator(LINEAR_INTERPOLATOR); + rotation.setRepeatCount(-1); + rotation.setRepeatMode(1); + + ObjectAnimator groupRotation = ObjectAnimator.ofFloat(this, "rotation", + new float[] { 0.0F, 360.0F }); + groupRotation.setDuration(6665L); + groupRotation.setInterpolator(LINEAR_INTERPOLATOR); + groupRotation.setRepeatCount(-1); + groupRotation.setRepeatMode(1); + + mAnimators.add(endTrim); + mAnimators.add(startTrim); + mAnimators.add(rotation); + mAnimators.add(groupRotation); + } + + private final Drawable.Callback mCallback = new Drawable.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 MaterialProgressState extends Drawable.ConstantState { + private float mStrokeWidth = 5.0F; + private float mInnerRadius = -1.0F; + private int mWidth = -1; + private int mHeight = -1; + private ColorStateList mColor = ColorStateList.valueOf(0); + + public MaterialProgressState(int width, int height, int color, float strokeWidth, float innerRadius) { + mWidth = width; + mHeight = height; + mColor = ColorStateList.valueOf(color); + mStrokeWidth = strokeWidth; + mInnerRadius = innerRadius; + } + + public MaterialProgressState(MaterialProgressState orig) { + if (orig != null) { + mStrokeWidth = orig.mStrokeWidth; + mInnerRadius = orig.mInnerRadius; + mWidth = orig.mWidth; + mHeight = orig.mHeight; + mColor = orig.mColor; + } + } + + @Override + public int getChangingConfigurations() { + return 0; + } + + @Override + public Drawable newDrawable() { + return new MaterialProgressDrawable(this); + } + } + + private static class Ring { + private final RectF mTempBounds = new RectF(); + private final Paint mPaint = new Paint(); + + private final Drawable.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 mAlpha = 255; + private int mColor = -16777216; + + public Ring(Drawable.Callback callback) { + mCallback = callback; + + mPaint.setStrokeCap(Paint.Cap.ROUND); + mPaint.setAntiAlias(true); + mPaint.setStyle(Paint.Style.STROKE); + } + + public void draw(Canvas c, Rect bounds) { + RectF arcBounds = mTempBounds; + arcBounds.set(bounds); + arcBounds.inset(mStrokeInset, mStrokeInset); + + float startAngle = (mStartTrim + mRotation) * 360.0F; + float endAngle = (mEndTrim + mRotation) * 360.0F; + float sweepAngle = endAngle - startAngle; + + float diameter = Math.min(arcBounds.width(), arcBounds.height()); + float minAngle = (float) (360.0D / (diameter * 3.141592653589793D)); + if ((sweepAngle < minAngle) && (sweepAngle > -minAngle)) { + sweepAngle = Math.signum(sweepAngle) * minAngle; + } + + c.drawArc(arcBounds, startAngle, sweepAngle, false, mPaint); + } + + public void setColorFilter(ColorFilter filter) { + mPaint.setColorFilter(filter); + invalidateSelf(); + } + + public ColorFilter getColorFilter() { + return mPaint.getColorFilter(); + } + + public void setAlpha(int alpha) { + mAlpha = alpha; + mPaint.setColor(mColor & 0xFFFFFF | alpha << 24); + invalidateSelf(); + } + + public int getAlpha() { + return mAlpha; + } + + public void setColor(int color) { + mColor = color; + mPaint.setColor(color & 0xFFFFFF | mAlpha << 24); + invalidateSelf(); + } + + public int getColor() { + return mColor; + } + + public void setStrokeWidth(float strokeWidth) { + mStrokeWidth = strokeWidth; + mPaint.setStrokeWidth(strokeWidth); + invalidateSelf(); + } + + public float getStrokeWidth() { + return mStrokeWidth; + } + + public void setStartTrim(float startTrim) { + mStartTrim = startTrim; + invalidateSelf(); + } + + public float getStartTrim() { + return mStartTrim; + } + + public void setEndTrim(float endTrim) { + mEndTrim = endTrim; + invalidateSelf(); + } + + public float getEndTrim() { + return mEndTrim; + } + + public void setRotation(float rotation) { + mRotation = rotation; + invalidateSelf(); + } + + public float getRotation() { + return mRotation; + } + + public void setInsets(float insets) { + mStrokeInset = insets; + } + + public float getInsets() { + return mStrokeInset; + } + + private void invalidateSelf() { + mCallback.invalidateDrawable(null); + } + } + + private static class EndCurveInterpolator extends + AccelerateDecelerateInterpolator { + @Override + public float getInterpolation(float input) { + return super + .getInterpolation(Math.max(0.0F, (input - 0.5F) * 2.0F)); + } + } + + private static class StartCurveInterpolator extends + AccelerateDecelerateInterpolator { + @Override + public float getInterpolation(float input) { + return super.getInterpolation(Math.min(1.0F, input * 2.0F)); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/drawable/OvalDrawable.java b/app/src/main/java/com/hippo/ehviewer/drawable/OvalDrawable.java new file mode 100644 index 000000000..fb610fe11 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/drawable/OvalDrawable.java @@ -0,0 +1,68 @@ +/* + * 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.drawable; + +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; + +public class OvalDrawable extends Drawable { + + private final Paint mPaint; + private final RectF mRectF; + + public OvalDrawable(int color) { + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); + mPaint.setColor(color); + mRectF = new RectF(); + } + + @Override + protected void onBoundsChange(Rect bounds) { + mRectF.set(bounds); + } + + @Override + public void draw(Canvas canvas) { + canvas.drawOval(mRectF, mPaint); + } + + @Override + public void setAlpha(int alpha) { + mPaint.setColor(mPaint.getColor() & 0x00ffffff | (alpha << 24)); + invalidateSelf(); + } + + @Override + public void setColorFilter(ColorFilter cf) { + // Not support + } + + @Override + public int getOpacity() { + return PixelFormat.OPAQUE; + } + + public void setColor(int color) { + mPaint.setColor(color); + invalidateSelf(); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/effect/DrawableTransition.java b/app/src/main/java/com/hippo/ehviewer/effect/DrawableTransition.java new file mode 100644 index 000000000..e42c92de0 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/effect/DrawableTransition.java @@ -0,0 +1,87 @@ +/* + * 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.effect; + +import android.animation.ValueAnimator; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; +import android.graphics.drawable.Drawable; + +public class DrawableTransition { + + public static void transit(final Drawable drawable, boolean fromDark, long duration) { + final AlphaSatColorMatrixEvaluator evaluator = new AlphaSatColorMatrixEvaluator(fromDark); + evaluator.setFraction(0.0f); + drawable.setColorFilter(new ColorMatrixColorFilter(evaluator.getColorMatrix())); + + ValueAnimator animator = ValueAnimator.ofFloat(0.0f, 1.0f); + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + float f = (Float) animation.getAnimatedValue(); + evaluator.setFraction(f); + drawable.setColorFilter(new ColorMatrixColorFilter(evaluator.getColorMatrix())); + } + }); + animator.setDuration(duration); + animator.start(); + } + + // From rnr.tests.imageloadingpattern.AlphaSatColorMatrixEvaluator + // with modification + private static class AlphaSatColorMatrixEvaluator { + private final ColorMatrix colorMatrix; + private final float[] elements = new float[20]; + private final boolean mFromDark; + + public AlphaSatColorMatrixEvaluator(boolean fromDark) { + colorMatrix = new ColorMatrix (); + mFromDark = fromDark; + } + + public ColorMatrix getColorMatrix() { + return colorMatrix; + } + + public void setFraction(float fraction) { + // There are 4 phases so we multiply fraction by that amount + float phase = fraction * 4; + + // Compute the alpha change over period [0, 2] + float alpha = Math.min(phase, 2.0f) / 2.0f; + elements [19] = Math.round(alpha * 255); + + // We substract to make the picture look darker, it will automatically clamp + // This is spread over period [0, 3] + final int MaxBlacker = 50; + float blackening = Math.round((1 - Math.min(phase, 3.0f) / 3.0f) * MaxBlacker); + elements [4] = elements [9] = elements [14] = mFromDark ? -blackening : blackening; + + // Finally we desaturate over [0, 4], taken from ColorMatrix.SetSaturation + float invSat = 1 - Math.max(0.2f, fraction); + float R = 0.213f * invSat; + float G = 0.715f * invSat; + float B = 0.072f * invSat; + + elements[0] = R + fraction; elements[1] = G; elements[2] = B; + elements[5] = R; elements[6] = G + fraction; elements[7] = B; + elements[10] = R; elements[11] = G; elements[12] = B + fraction; + + colorMatrix.set(elements); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/effect/ripple/Ripple.java b/app/src/main/java/com/hippo/ehviewer/effect/ripple/Ripple.java new file mode 100644 index 000000000..035cfed01 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/effect/ripple/Ripple.java @@ -0,0 +1,423 @@ +/* + * 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.effect.ripple; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.Rect; +import android.os.Build; +import android.view.animation.Interpolator; +import android.view.animation.LinearInterpolator; + +import com.hippo.ehviewer.util.MathUtils; + +/** + * Draws a Material ripple. + */ +class Ripple { + @SuppressWarnings("unused") + private static final String TAG = Ripple.class.getSimpleName(); + + private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); + private static final Interpolator DECEL_INTERPOLATOR = new LogInterpolator(); + + private static final float GLOBAL_SPEED = 1.0f; + private static final float WAVE_TOUCH_DOWN_ACCELERATION = 1024.0f * GLOBAL_SPEED; + private static final float WAVE_TOUCH_UP_ACCELERATION = 3400.0f * GLOBAL_SPEED; + private static final float WAVE_OPACITY_DECAY_VELOCITY = 3.0f / GLOBAL_SPEED; + + private static final long RIPPLE_ENTER_DELAY = 80; + + private final RippleOwner mOwner; + + /** Bounds used for computing max radius. */ + private final Rect mBounds; + + /** Full-opacity color for drawing this ripple. */ + private int mColorOpaque; + + /** Maximum ripple radius. */ + private float mOuterRadius; + + /** Screen density used to adjust pixel-based velocities. */ + private float mDensity; + + private float mStartingX; + private float mStartingY; + private float mClampedStartingX; + private float mClampedStartingY; + + // Software animators. + private ObjectAnimator mAnimRadius; + private ObjectAnimator mAnimOpacity; + private ObjectAnimator mAnimX; + private ObjectAnimator mAnimY; + + // Software rendering properties. + private float mOpacity = 1; + private float mOuterX; + private float mOuterY; + + // Values used to tween between the start and end positions. + private float mTweenRadius = 0; + private float mTweenX = 0; + private float mTweenY = 0; + + /** Whether we have an explicit maximum radius. */ + private boolean mHasMaxRadius; + + /** Whether we were canceled externally and should avoid self-removal. */ + private boolean mCanceled; + + /** + * Creates a new ripple. + */ + public Ripple(RippleOwner owner, Rect bounds, float startingX, float startingY) { + mOwner = owner; + mBounds = bounds; + + mStartingX = startingX; + mStartingY = startingY; + } + + public void setup(int maxRadius, int color, float density) { + mColorOpaque = color | 0xFF000000; + mOpacity = Color.alpha(color) / 2 / 255.0f; + + if (maxRadius != RippleOwner.RADIUS_AUTO) { + mHasMaxRadius = true; + mOuterRadius = maxRadius; + } else { + final float halfWidth = mBounds.width() / 2.0f; + final float halfHeight = mBounds.height() / 2.0f; + mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight); + } + + mOuterX = 0; + mOuterY = 0; + mDensity = density; + + clampStartingPosition(); + } + + private void clampStartingPosition() { + final float cX = mBounds.exactCenterX(); + final float cY = mBounds.exactCenterY(); + final float dX = mStartingX - cX; + final float dY = mStartingY - cY; + final float r = mOuterRadius; + if (dX * dX + dY * dY > r * r) { + // Point is outside the circle, clamp to the circumference. + final double angle = Math.atan2(dY, dX); + mClampedStartingX = cX + (float) (Math.cos(angle) * r); + mClampedStartingY = cY + (float) (Math.sin(angle) * r); + } else { + mClampedStartingX = mStartingX; + mClampedStartingY = mStartingY; + } + } + + public void onHotspotBoundsChanged() { + if (!mHasMaxRadius) { + final float halfWidth = mBounds.width() / 2.0f; + final float halfHeight = mBounds.height() / 2.0f; + mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight); + + clampStartingPosition(); + } + } + + public void setOpacity(float a) { + mOpacity = a; + invalidateSelf(); + } + + public float getOpacity() { + return mOpacity; + } + + public void setRadiusGravity(float r) { + mTweenRadius = r; + invalidateSelf(); + } + + public float getRadiusGravity() { + return mTweenRadius; + } + + public void setXGravity(float x) { + mTweenX = x; + invalidateSelf(); + } + + public float getXGravity() { + return mTweenX; + } + + public void setYGravity(float y) { + mTweenY = y; + invalidateSelf(); + } + + public float getYGravity() { + return mTweenY; + } + + /** + * Draws the ripple centered at (0,0) using the specified paint. + */ + public boolean draw(Canvas c, Paint p) { + return drawSoftware(c, p); + } + + private boolean drawSoftware(Canvas c, Paint p) { + boolean hasContent = false; + + p.setColor(mColorOpaque); + final int alpha = MathUtils.clamp((int) (255 * mOpacity + 0.5f), 0, 255); + final float radius = MathUtils.lerp(0, mOuterRadius, mTweenRadius); + if (alpha > 0 && radius > 0) { + final float x = MathUtils.lerp( + mClampedStartingX - mBounds.exactCenterX(), mOuterX, mTweenX); + final float y = MathUtils.lerp( + mClampedStartingY - mBounds.exactCenterY(), mOuterY, mTweenY); + p.setAlpha(alpha); + p.setStyle(Style.FILL); + c.drawCircle(x, y, radius, p); + hasContent = true; + } + + return hasContent; + } + + /** + * Returns the maximum bounds of the ripple relative to the ripple center. + */ + public void getBounds(Rect bounds) { + final int outerX = (int) mOuterX; + final int outerY = (int) mOuterY; + final int r = (int) mOuterRadius + 1; + bounds.set(outerX - r, outerY - r, outerX + r, outerY + r); + } + + /** + * Specifies the starting position relative to the drawable bounds. No-op if + * the ripple has already entered. + */ + public void move(float x, float y) { + mStartingX = x; + mStartingY = y; + + clampStartingPosition(); + } + + /** + * Starts the enter animation. + */ + public void enter() { + cancel(); + + final int radiusDuration = (int) + (1000 * Math.sqrt(mOuterRadius / WAVE_TOUCH_DOWN_ACCELERATION * mDensity) + 0.5); + + final ObjectAnimator radius = ObjectAnimator.ofFloat(this, "radiusGravity", 1); + radius.setDuration(radiusDuration); + radius.setInterpolator(LINEAR_INTERPOLATOR); + radius.setStartDelay(RIPPLE_ENTER_DELAY); + + final ObjectAnimator cX = ObjectAnimator.ofFloat(this, "xGravity", 1); + cX.setDuration(radiusDuration); + cX.setInterpolator(LINEAR_INTERPOLATOR); + cX.setStartDelay(RIPPLE_ENTER_DELAY); + + final ObjectAnimator cY = ObjectAnimator.ofFloat(this, "yGravity", 1); + cY.setDuration(radiusDuration); + cY.setInterpolator(LINEAR_INTERPOLATOR); + cY.setStartDelay(RIPPLE_ENTER_DELAY); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + radius.setAutoCancel(true); + cX.setAutoCancel(true); + cY.setAutoCancel(true); + } + + mAnimRadius = radius; + mAnimX = cX; + mAnimY = cY; + + // Enter animations always run on the UI thread, since it's unlikely + // that anything interesting is happening until the user lifts their + // finger. + radius.start(); + cX.start(); + cY.start(); + } + + /** + * Starts the exit animation. + */ + public void exit() { + cancel(); + + final float radius = MathUtils.lerp(0, mOuterRadius, mTweenRadius); + final float remaining; + if (mAnimRadius != null && mAnimRadius.isRunning()) { + remaining = mOuterRadius - radius; + } else { + remaining = mOuterRadius; + } + + final int radiusDuration = (int) (1000 * Math.sqrt(remaining / (WAVE_TOUCH_UP_ACCELERATION + + WAVE_TOUCH_DOWN_ACCELERATION) * mDensity) + 0.5); + final int opacityDuration = (int) (1000 * 1 / WAVE_OPACITY_DECAY_VELOCITY + 0.5f); + + exitSoftware(radiusDuration, opacityDuration); + } + + /** + * Jump all animations to their end state. The caller is responsible for + * removing the ripple from the list of animating ripples. + */ + public void jump() { + mCanceled = true; + endSoftwareAnimations(); + mCanceled = false; + } + + private void endSoftwareAnimations() { + if (mAnimRadius != null) { + mAnimRadius.end(); + mAnimRadius = null; + } + + if (mAnimOpacity != null) { + mAnimOpacity.end(); + mAnimOpacity = null; + } + + if (mAnimX != null) { + mAnimX.end(); + mAnimX = null; + } + + if (mAnimY != null) { + mAnimY.end(); + mAnimY = null; + } + } + + private void exitSoftware(int radiusDuration, int opacityDuration) { + final ObjectAnimator radiusAnim = ObjectAnimator.ofFloat(this, "radiusGravity", 1); + radiusAnim.setDuration(radiusDuration); + radiusAnim.setInterpolator(DECEL_INTERPOLATOR); + + final ObjectAnimator xAnim = ObjectAnimator.ofFloat(this, "xGravity", 1); + xAnim.setDuration(radiusDuration); + xAnim.setInterpolator(DECEL_INTERPOLATOR); + + final ObjectAnimator yAnim = ObjectAnimator.ofFloat(this, "yGravity", 1); + yAnim.setDuration(radiusDuration); + yAnim.setInterpolator(DECEL_INTERPOLATOR); + + final ObjectAnimator opacityAnim = ObjectAnimator.ofFloat(this, "opacity", 0); + opacityAnim.setDuration(opacityDuration); + opacityAnim.setInterpolator(LINEAR_INTERPOLATOR); + opacityAnim.addListener(mAnimationListener); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + radiusAnim.setAutoCancel(true); + xAnim.setAutoCancel(true); + yAnim.setAutoCancel(true); + opacityAnim.setAutoCancel(true); + } + + mAnimRadius = radiusAnim; + mAnimOpacity = opacityAnim; + mAnimX = xAnim; + mAnimY = yAnim; + + radiusAnim.start(); + opacityAnim.start(); + xAnim.start(); + yAnim.start(); + } + + /** + * Cancels all animations. The caller is responsible for removing + * the ripple from the list of animating ripples. + */ + public void cancel() { + mCanceled = true; + cancelSoftwareAnimations(); + mCanceled = false; + } + + private void cancelSoftwareAnimations() { + if (mAnimRadius != null) { + mAnimRadius.cancel(); + mAnimRadius = null; + } + + if (mAnimOpacity != null) { + mAnimOpacity.cancel(); + mAnimOpacity = null; + } + + if (mAnimX != null) { + mAnimX.cancel(); + mAnimX = null; + } + + if (mAnimY != null) { + mAnimY.cancel(); + mAnimY = null; + } + } + + private void removeSelf() { + // The owner will invalidate itself. + if (!mCanceled) { + mOwner.removeRipple(this); + } + } + + private void invalidateSelf() { + mOwner.invalidateSelf(); + } + + private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + removeSelf(); + } + }; + + /** + * Interpolator with a smooth log deceleration + */ + private static final class LogInterpolator implements Interpolator { + @Override + public float getInterpolation(float input) { + return 1 - (float) Math.pow(400, -input * 1.4); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/effect/ripple/RippleBackground.java b/app/src/main/java/com/hippo/ehviewer/effect/ripple/RippleBackground.java new file mode 100644 index 000000000..0ac0d031b --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/effect/ripple/RippleBackground.java @@ -0,0 +1,285 @@ +/* + * 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 + * 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.effect.ripple; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.Rect; +import android.os.Build; +import android.view.animation.Interpolator; +import android.view.animation.LinearInterpolator; + +import com.hippo.ehviewer.util.MathUtils; + +/** + * Draws a Material ripple. + */ +class RippleBackground { + private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); + + private static final float GLOBAL_SPEED = 1.0f; + private static final float WAVE_OPACITY_DECAY_VELOCITY = 3.0f / GLOBAL_SPEED; + private static final float WAVE_OUTER_OPACITY_EXIT_VELOCITY_MAX = 4.5f * GLOBAL_SPEED; + private static final float WAVE_OUTER_OPACITY_EXIT_VELOCITY_MIN = 1.5f * GLOBAL_SPEED; + private static final float WAVE_OUTER_OPACITY_ENTER_VELOCITY = 10.0f * GLOBAL_SPEED; + private static final float WAVE_OUTER_SIZE_INFLUENCE_MAX = 200f; + private static final float WAVE_OUTER_SIZE_INFLUENCE_MIN = 40f; + + private final RippleOwner mOwner; + + /** Bounds used for computing max radius. */ + private final Rect mBounds; + + /** Full-opacity color for drawing this ripple. */ + private int mColorOpaque; + + /** Maximum alpha value for drawing this ripple. */ + private int mColorAlpha; + + /** Maximum ripple radius. */ + private float mOuterRadius; + + /** Screen density used to adjust pixel-based velocities. */ + private float mDensity; + + // Software animators. + private ObjectAnimator mAnimOuterOpacity; + + // Software rendering properties. + private float mOuterOpacity = 0; + private float mOuterX; + private float mOuterY; + + /** Whether we have an explicit maximum radius. */ + private boolean mHasMaxRadius; + + /** + * Creates a new ripple. + */ + public RippleBackground(RippleOwner owner, Rect bounds) { + mOwner = owner; + mBounds = bounds; + } + + public void setup(int maxRadius, int color, float density) { + mColorOpaque = color | 0xFF000000; + mColorAlpha = Color.alpha(color) / 2; + + if (maxRadius != RippleOwner.RADIUS_AUTO) { + mHasMaxRadius = true; + mOuterRadius = maxRadius; + } else { + final float halfWidth = mBounds.width() / 2.0f; + final float halfHeight = mBounds.height() / 2.0f; + mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight); + } + + mOuterX = 0; + mOuterY = 0; + mDensity = density; + } + + public void onHotspotBoundsChanged() { + if (!mHasMaxRadius) { + final float halfWidth = mBounds.width() / 2.0f; + final float halfHeight = mBounds.height() / 2.0f; + mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight); + } + } + + public void setOuterOpacity(float a) { + mOuterOpacity = a; + invalidateSelf(); + } + + public float getOuterOpacity() { + return mOuterOpacity; + } + + /** + * Draws the ripple centered at (0,0) using the specified paint. + */ + public boolean draw(Canvas c, Paint p) { + return drawSoftware(c, p); + } + + public boolean shouldDraw() { + final int outerAlpha = (int) (mColorAlpha * mOuterOpacity + 0.5f); + return outerAlpha > 0 && mOuterRadius > 0; + } + + private boolean drawSoftware(Canvas c, Paint p) { + boolean hasContent = false; + + p.setColor(mColorOpaque); + final int outerAlpha = (int) (mColorAlpha * mOuterOpacity + 0.5f); + if (outerAlpha > 0 && mOuterRadius > 0) { + p.setAlpha(outerAlpha); + p.setStyle(Style.FILL); + c.drawCircle(mOuterX, mOuterY, mOuterRadius, p); + hasContent = true; + } + + return hasContent; + } + + /** + * Returns the maximum bounds of the ripple relative to the ripple center. + */ + public void getBounds(Rect bounds) { + final int outerX = (int) mOuterX; + final int outerY = (int) mOuterY; + final int r = (int) mOuterRadius + 1; + bounds.set(outerX - r, outerY - r, outerX + r, outerY + r); + } + + /** + * Starts the enter animation. + */ + public void enter() { + cancel(); + + final int outerDuration = (int) (1000 * 1.0f / WAVE_OUTER_OPACITY_ENTER_VELOCITY); + final ObjectAnimator outer = ObjectAnimator.ofFloat(this, "outerOpacity", 0, 1); + outer.setDuration(outerDuration); + outer.setInterpolator(LINEAR_INTERPOLATOR); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + outer.setAutoCancel(true); + } + + mAnimOuterOpacity = outer; + + // Enter animations always run on the UI thread, since it's unlikely + // that anything interesting is happening until the user lifts their + // finger. + outer.start(); + } + + /** + * Starts the exit animation. + */ + public void exit() { + cancel(); + + // Scale the outer max opacity and opacity velocity based + // on the size of the outer radius. + final int opacityDuration = (int) (1000 / WAVE_OPACITY_DECAY_VELOCITY + 0.5f); + final float outerSizeInfluence = MathUtils.clamp( + (mOuterRadius - WAVE_OUTER_SIZE_INFLUENCE_MIN * mDensity) + / (WAVE_OUTER_SIZE_INFLUENCE_MAX * mDensity), 0, 1); + final float outerOpacityVelocity = MathUtils.lerp(WAVE_OUTER_OPACITY_EXIT_VELOCITY_MIN, + WAVE_OUTER_OPACITY_EXIT_VELOCITY_MAX, outerSizeInfluence); + + // Determine at what time the inner and outer opacity intersect. + // inner(t) = mOpacity - t * WAVE_OPACITY_DECAY_VELOCITY / 1000 + // outer(t) = mOuterOpacity + t * WAVE_OUTER_OPACITY_VELOCITY / 1000 + final int inflectionDuration = Math.max(0, (int) (1000 * (1 - mOuterOpacity) + / (WAVE_OPACITY_DECAY_VELOCITY + outerOpacityVelocity) + 0.5f)); + final int inflectionOpacity = (int) (mColorAlpha * (mOuterOpacity + + inflectionDuration * outerOpacityVelocity * outerSizeInfluence / 1000) + 0.5f); + + exitSoftware(opacityDuration, inflectionDuration, inflectionOpacity); + } + + /** + * Jump all animations to their end state. The caller is responsible for + * removing the ripple from the list of animating ripples. + */ + public void jump() { + endSoftwareAnimations(); + } + + private void endSoftwareAnimations() { + if (mAnimOuterOpacity != null) { + mAnimOuterOpacity.end(); + mAnimOuterOpacity = null; + } + } + + private void exitSoftware(int opacityDuration, int inflectionDuration, int inflectionOpacity) { + final ObjectAnimator outerOpacityAnim; + if (inflectionDuration > 0) { + // Outer opacity continues to increase for a bit. + outerOpacityAnim = ObjectAnimator.ofFloat(this, + "outerOpacity", inflectionOpacity / 255.0f); + outerOpacityAnim.setDuration(inflectionDuration); + outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + outerOpacityAnim.setAutoCancel(true); + } + + // Chain the outer opacity exit animation. + final int outerDuration = opacityDuration - inflectionDuration; + if (outerDuration > 0) { + outerOpacityAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + final ObjectAnimator outerFadeOutAnim = ObjectAnimator.ofFloat( + RippleBackground.this, "outerOpacity", 0); + outerFadeOutAnim.setDuration(outerDuration); + outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + outerFadeOutAnim.setAutoCancel(true); + } + + mAnimOuterOpacity = outerFadeOutAnim; + + outerFadeOutAnim.start(); + } + + @Override + public void onAnimationCancel(Animator animation) { + animation.removeListener(this); + } + }); + } + } else { + outerOpacityAnim = ObjectAnimator.ofFloat(this, "outerOpacity", 0); + outerOpacityAnim.setDuration(opacityDuration); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + outerOpacityAnim.setAutoCancel(true); + } + } + + mAnimOuterOpacity = outerOpacityAnim; + + outerOpacityAnim.start(); + } + + /** + * Cancel all animations. The caller is responsible for removing + * the ripple from the list of animating ripples. + */ + public void cancel() { + cancelSoftwareAnimations(); + } + + private void cancelSoftwareAnimations() { + if (mAnimOuterOpacity != null) { + mAnimOuterOpacity.cancel(); + mAnimOuterOpacity = null; + } + } + + private void invalidateSelf() { + mOwner.invalidateSelf(); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/effect/ripple/RippleDrawable.java b/app/src/main/java/com/hippo/ehviewer/effect/ripple/RippleDrawable.java new file mode 100644 index 000000000..80d98723e --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/effect/ripple/RippleDrawable.java @@ -0,0 +1,579 @@ +/* + * 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 + * 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.effect.ripple; + +import android.annotation.TargetApi; +import android.content.res.ColorStateList; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.support.annotation.NonNull; + +import com.hippo.ehviewer.widget.Hotspotable; + +import java.util.Arrays; + +/** + * Drawable that shows a ripple effect in response to state changes. The + * anchoring position of the ripple for a given state may be specified by + * calling {@link #setHotspot(float, float)} with the corresponding state + * attribute identifier. + *

+ * A touch feedback drawable may contain multiple child layers, including a + * special mask layer that is not drawn to the screen. A single layer may be set + * as the mask by specifying its android:id value as {@link android.R.id#mask}. + *

+ * <!-- A red ripple masked against an opaque rectangle. --/>
+ * <ripple android:color="#ffff0000">
+ *   <item android:id="@android:id/mask"
+ *         android:drawable="@android:color/white" />
+ * <ripple />
+ * 
+ *

+ * If a mask layer is set, the ripple effect will be masked against that layer + * before it is drawn over the composite of the remaining child layers. + *

+ * If no mask layer is set, the ripple effect is masked against the composite + * of the child layers. + *

+ * <!-- A blue ripple drawn atop a black rectangle. --/>
+ * <ripple android:color="#ff00ff00">
+ *   <item android:drawable="@android:color/black" />
+ * <ripple />
+ *
+ * <!-- A red ripple drawn atop a drawable resource. --/>
+ * <ripple android:color="#ff00ff00">
+ *   <item android:drawable="@drawable/my_drawable" />
+ * <ripple />
+ * 
+ *

+ * If no child layers or mask is specified and the ripple is set as a View + * background, the ripple will be drawn atop the first available parent + * background within the View's hierarchy. In this case, the drawing region + * may extend outside of the Drawable bounds. + *

+ * <!-- An unbounded green ripple. --/>
+ * <ripple android:color="#ff0000ff" />
+ * 
+ * + * @attr ref android.R.styleable#RippleDrawable_color + */ +public class RippleDrawable extends Drawable implements RippleOwner, Hotspotable { + @SuppressWarnings("unused") + private static final String TAG = RippleDrawable.class.getSimpleName(); + + /** The maximum number of ripples supported. */ + private static final int MAX_RIPPLES = 10; + + /** Current ripple effect bounds, used to constrain ripple effects. */ + private final Rect mHotspotBounds = new Rect(); + + /** The current background. May be actively animating or pending entry. */ + private RippleBackground mBackground; + + /** Whether we expect to draw a background when visible. */ + private boolean mBackgroundActive; + + /** The current ripple. May be actively animating or pending entry. */ + private Ripple mRipple; + + /** Whether we expect to draw a ripple when visible. */ + private boolean mRippleActive; + + // Hotspot coordinates that are awaiting activation. + private float mPendingX; + private float mPendingY; + private boolean mHasPending; + + /** + * Lazily-created array of actively animating ripples. Inactive ripples are + * pruned during draw(). The locations of these will not change. + */ + private Ripple[] mExitingRipples; + private int mExitingRipplesCount = 0; + + /** Paint used to control appearance of ripples. */ + private Paint mRipplePaint; + + /** Target density of the display into which ripples are drawn. */ + private final float mDensity = 1.0f; + + /** Whether bounds are being overridden. */ + private boolean mOverrideBounds; + + /** + * Whether the next draw MUST draw something to canvas. Used to work around + * a bug in hardware invalidation following a render thread-accelerated + * animation. + */ + private boolean mNeedsDraw; + + private ColorStateList mColor = ColorStateList.valueOf(Color.MAGENTA); + + private int mMaxRadius = RADIUS_AUTO; + + private final Drawable mContent; + + RippleDrawable(ColorStateList color, Drawable content) { + setColor(color); + + mContent = content; + } + + @Override + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public void jumpToCurrentState() { + if(Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB) { + super.jumpToCurrentState(); + } + + boolean needsDraw = false; + + if (mRipple != null) { + mRipple.jump(); + } + + if (mBackground != null) { + mBackground.jump(); + } + + needsDraw |= cancelExitingRipples(); + + mNeedsDraw = needsDraw; + invalidateSelf(); + } + + private boolean cancelExitingRipples() { + final int count = mExitingRipplesCount; + final Ripple[] ripples = mExitingRipples; + for (int i = 0; i < count; i++) { + ripples[i].cancel(); + } + + if (ripples != null) { + Arrays.fill(ripples, 0, count, null); + } + mExitingRipplesCount = 0; + + return false; + } + + @Override + public void setAlpha(int alpha) { + mColor.withAlpha(alpha); + } + + @Override + public int getAlpha() { + return Color.alpha(mColor.getDefaultColor()); + } + + @Override + public void setColorFilter(ColorFilter cf) { + //TODO Paint.setColorFilter(filter) ? + } + + @Override + public int getOpacity() { + // Worst-case scenario. + return PixelFormat.TRANSLUCENT; + } + + @Override + protected boolean onStateChange(int[] stateSet) { + boolean changed = false; + if (mContent != null) + changed |= mContent.setState(stateSet); + + boolean enabled = false; + boolean pressed = false; + boolean focused = false; + + for (int state : stateSet) { + if (state == android.R.attr.state_enabled) { + enabled = true; + } + if (state == android.R.attr.state_focused) { + focused = true; + } + if (state == android.R.attr.state_pressed) { + pressed = true; + } + } + + setRippleActive(enabled && pressed); + setBackgroundActive(focused || (enabled && pressed)); + + return changed; + } + + private void setRippleActive(boolean active) { + if (mRippleActive != active) { + mRippleActive = active; + if (active) { + tryRippleEnter(); + } else { + tryRippleExit(); + } + } + } + + private void setBackgroundActive(boolean active) { + if (mBackgroundActive != active) { + mBackgroundActive = active; + if (active) { + tryBackgroundEnter(); + } else { + tryBackgroundExit(); + } + } + } + + @Override + protected void onBoundsChange(Rect bounds) { + super.onBoundsChange(bounds); + + if (!mOverrideBounds) { + mHotspotBounds.set(bounds); + onHotspotBoundsChanged(); + } + + if (mContent != null) + mContent.setBounds(bounds); + + invalidateSelf(); + } + + @Override + public boolean setVisible(boolean visible, boolean restart) { + final boolean changed = super.setVisible(visible, restart); + + if (!visible) { + clearHotspots(); + } else if (changed) { + // If we just became visible, ensure the background and ripple + // visibilities are consistent with their internal states. + if (mRippleActive) { + tryRippleEnter(); + } + + if (mBackgroundActive) { + tryBackgroundEnter(); + } + } + + return changed; + } + + @Override + public boolean isStateful() { + return true; + } + + public void setColor(ColorStateList color) { + int alpha = Color.alpha(color.getDefaultColor()); + mColor = color.withAlpha((int) (alpha * (1 - 0.001f * alpha))); + invalidateSelf(); + } + + @Override + public void setHotspot(float x, float y) { + if (mRipple == null || mBackground == null) { + mPendingX = x; + mPendingY = y; + mHasPending = true; + } + + if (mRipple != null) { + mRipple.move(x, y); + } + } + + /** + * Creates an active hotspot at the specified location. + */ + private void tryBackgroundEnter() { + if (mBackground == null) { + mBackground = new RippleBackground(this, mHotspotBounds); + } + + final int color = mColor.getColorForState(getState(), Color.TRANSPARENT); + mBackground.setup(mMaxRadius, color, mDensity); + mBackground.enter(); + } + + private void tryBackgroundExit() { + if (mBackground != null) { + // Don't null out the background, we need it to draw! + mBackground.exit(); + } + } + + /** + * Attempts to start an enter animation for the active hotspot. Fails if + * there are too many animating ripples. + */ + private void tryRippleEnter() { + if (mExitingRipplesCount >= MAX_RIPPLES) { + // This should never happen unless the user is tapping like a maniac + // or there is a bug that's preventing ripples from being removed. + return; + } + + if (mRipple == null) { + final float x; + final float y; + if (mHasPending) { + mHasPending = false; + x = mPendingX; + y = mPendingY; + } else { + x = mHotspotBounds.exactCenterX(); + y = mHotspotBounds.exactCenterY(); + } + mRipple = new Ripple(this, mHotspotBounds, x, y); + } + + final int color = mColor.getColorForState(getState(), Color.TRANSPARENT); + mRipple.setup(mMaxRadius, color, mDensity); + mRipple.enter(); + } + + /** + * Attempts to start an exit animation for the active hotspot. Fails if + * there is no active hotspot. + */ + private void tryRippleExit() { + if (mRipple != null) { + if (mExitingRipples == null) { + mExitingRipples = new Ripple[MAX_RIPPLES]; + } + mExitingRipples[mExitingRipplesCount++] = mRipple; + mRipple.exit(); + mRipple = null; + } + } + + /** + * Cancels and removes the active ripple, all exiting ripples, and the + * background. Nothing will be drawn after this method is called. + */ + private void clearHotspots() { + boolean needsDraw = false; + + if (mRipple != null) { + mRipple.cancel(); + mRipple = null; + } + + if (mBackground != null) { + mBackground.cancel(); + mBackground = null; + } + + needsDraw |= cancelExitingRipples(); + + mNeedsDraw = needsDraw; + invalidateSelf(); + } + + @Override + public void setHotspotBounds(int left, int top, int right, int bottom) { + mOverrideBounds = true; + mHotspotBounds.set(left, top, right, bottom); + + onHotspotBoundsChanged(); + } + + /** @hide */ + public void getHotspotBounds(Rect outRect) { + outRect.set(mHotspotBounds); + } + + /** + * Notifies all the animating ripples that the hotspot bounds have changed. + */ + private void onHotspotBoundsChanged() { + final int count = mExitingRipplesCount; + final Ripple[] ripples = mExitingRipples; + for (int i = 0; i < count; i++) { + ripples[i].onHotspotBoundsChanged(); + } + + if (mRipple != null) { + mRipple.onHotspotBoundsChanged(); + } + + if (mBackground != null) { + mBackground.onHotspotBoundsChanged(); + } + } + + @Override + public void draw(@NonNull Canvas canvas) { + final boolean drawNonMaskContent = mContent != null; + final Rect bounds = getDirtyBounds(); + final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG); + canvas.clipRect(bounds); + + // If we have content, draw it into a layer first. + if (drawNonMaskContent) { + drawContentLayer(canvas, bounds); + } + + // If we have a background and a non-opaque mask, draw the masking layer. + drawBackgroundLayer(canvas, bounds); + + // If we have ripples and a non-opaque mask, draw the masking layer. + drawRippleLayer(canvas, bounds); + + // If we failed to draw anything and we just canceled animations, at + // least draw a color so that hardware invalidation works correctly. + if (mNeedsDraw) { + canvas.drawColor(Color.TRANSPARENT); + + // Request another draw so we can avoid adding a transparent layer + // during the next display list refresh. + invalidateSelf(); + } + mNeedsDraw = false; + + canvas.restoreToCount(saveCount); + } + + /** + * Removes a ripple from the exiting ripple list. + * + * @param ripple the ripple to remove + */ + @Override + public void removeRipple(Ripple ripple) { + // Ripple ripple ripple ripple. Ripple ripple. + final Ripple[] ripples = mExitingRipples; + final int count = mExitingRipplesCount; + final int index = getRippleIndex(ripple); + if (index >= 0) { + System.arraycopy(ripples, index + 1, ripples, index, count - (index + 1)); + ripples[count - 1] = null; + mExitingRipplesCount--; + + invalidateSelf(); + } + } + + private int getRippleIndex(Ripple ripple) { + final Ripple[] ripples = mExitingRipples; + final int count = mExitingRipplesCount; + for (int i = 0; i < count; i++) { + if (ripples[i] == ripple) { + return i; + } + } + return -1; + } + + private void drawContentLayer(Canvas canvas, Rect bounds) { + mContent.draw(canvas); + } + + private void drawBackgroundLayer( + Canvas canvas, Rect bounds) { + if (mBackground != null && mBackground.shouldDraw()) { + final float x = mHotspotBounds.exactCenterX(); + final float y = mHotspotBounds.exactCenterY(); + canvas.translate(x, y); + mBackground.draw(canvas, getRipplePaint()); + canvas.translate(-x, -y); + } + } + + private void drawRippleLayer(Canvas canvas, Rect bounds) { + int restoreTranslate = -1; + + // Translate the canvas to the current hotspot bounds. + restoreTranslate = canvas.save(); + canvas.translate(mHotspotBounds.exactCenterX(), mHotspotBounds.exactCenterY()); + + // Draw ripples and update the animating ripples array. + final int count = mExitingRipplesCount; + final Ripple[] ripples = mExitingRipples; + for (int i = 0; i <= count; i++) { + final Ripple ripple; + if (i < count) { + ripple = ripples[i]; + } else if (mRipple != null) { + ripple = mRipple; + } else { + continue; + } + + ripple.draw(canvas, getRipplePaint()); + } + + // Always restore the translation. + if (restoreTranslate >= 0) { + canvas.restoreToCount(restoreTranslate); + } + } + + private Paint getRipplePaint() { + if (mRipplePaint == null) { + mRipplePaint = new Paint(); + mRipplePaint.setAntiAlias(true); + } + return mRipplePaint; + } + + @Override + public Rect getDirtyBounds() { + return getBounds(); + } + + /** + * Sets the maximum ripple radius in pixels. The default value of + * {@link #RADIUS_AUTO} defines the radius as the distance from the center + * of the drawable bounds (or hotspot bounds, if specified) to a corner. + * + * @param maxRadius the maximum ripple radius in pixels or + * {@link #RADIUS_AUTO} to automatically determine the maximum + * radius based on the bounds + * @see #getMaxRadius() + * @see #setHotspotBounds(int, int, int, int) + * @hide + */ + public void setMaxRadius(int maxRadius) { + if (maxRadius != RADIUS_AUTO && maxRadius < 0) { + throw new IllegalArgumentException("maxRadius must be RADIUS_AUTO or >= 0"); + } + + mMaxRadius = maxRadius; + } + + /** + * @return the maximum ripple radius in pixels, or {@link #RADIUS_AUTO} if + * the radius is determined automatically + * @see #setMaxRadius(int) + * @hide + */ + public int getMaxRadius() { + return mMaxRadius; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/effect/ripple/RippleOwner.java b/app/src/main/java/com/hippo/ehviewer/effect/ripple/RippleOwner.java new file mode 100644 index 000000000..de9cdb0ee --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/effect/ripple/RippleOwner.java @@ -0,0 +1,29 @@ +/* + * 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 + * 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.effect.ripple; + + +public interface RippleOwner { + /** + * Constant for automatically determining the maximum ripple radius. + */ + public static final int RADIUS_AUTO = -1; + + public void removeRipple(Ripple ripple); + + public void invalidateSelf(); + +} diff --git a/app/src/main/java/com/hippo/ehviewer/effect/ripple/RippleSalon.java b/app/src/main/java/com/hippo/ehviewer/effect/ripple/RippleSalon.java new file mode 100644 index 000000000..a0fc3c649 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/effect/ripple/RippleSalon.java @@ -0,0 +1,55 @@ +/* + * 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.effect.ripple; + +import android.annotation.TargetApi; +import android.content.res.ColorStateList; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.view.View; + +import com.hippo.ehviewer.widget.HotspotTouchHelper; + + +public final class RippleSalon { + + private static final boolean USE_OLD_RIPPLE = + Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP; + + private static final Drawable MASK = new ColorDrawable(Color.BLACK); + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public static void addRipple(View v, ColorStateList color) { + addRipple(v, color, v.getBackground()); + } + + @SuppressWarnings("deprecation") + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public static void addRipple(View v, ColorStateList color, Drawable content) { + if (USE_OLD_RIPPLE) { + RippleDrawable rippleDrawable = new RippleDrawable(color, content); + v.setOnTouchListener(new HotspotTouchHelper(rippleDrawable)); + v.setBackgroundDrawable(rippleDrawable); + } else { + android.graphics.drawable.RippleDrawable rippleDrawable = new android.graphics.drawable.RippleDrawable(color, content, MASK); + v.setBackgroundDrawable(rippleDrawable); + } + } + +} diff --git a/app/src/main/java/com/hippo/ehviewer/ehclient/AddToFavoriteParser.java b/app/src/main/java/com/hippo/ehviewer/ehclient/AddToFavoriteParser.java new file mode 100644 index 000000000..4b81bd403 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ehclient/AddToFavoriteParser.java @@ -0,0 +1,31 @@ +/* + * 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.ehclient; + +public class AddToFavoriteParser { + + public boolean parser(String body) { + if (body == null) + return false; + + if (body.contains("Close Window")) + return true; + else + return false; + } + +} diff --git a/app/src/main/java/com/hippo/ehviewer/ehclient/DetailParser.java b/app/src/main/java/com/hippo/ehviewer/ehclient/DetailParser.java new file mode 100644 index 000000000..a68193049 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ehclient/DetailParser.java @@ -0,0 +1,324 @@ +/* + * 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.ehclient; + +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.hippo.ehviewer.data.Comment; +import com.hippo.ehviewer.data.LargePreviewList; +import com.hippo.ehviewer.data.NormalPreviewList; +import com.hippo.ehviewer.data.PreviewList; +import com.hippo.ehviewer.util.EhUtils; +import com.hippo.ehviewer.util.Utils; + +public class DetailParser { + + private static final String OFFENSIVE_STRING = + "

(And if you choose to ignore this warning, you lose all rights to complain about it in the future.)

"; + private static final String PINING_STRING = + "

This gallery is pining for the fjords.

"; + + public static final int DETAIL = 0x1; + public static final int TAG = 0x2; + public static final int PREVIEW_INFO = 0x4; + public static final int PREVIEW = 0x8; + public static final int COMMENT = 0x10; + public static final int OFFENSIVE = 0x20; + public static final int PINING = 0x40; + public static final int ERROR = 0x80; + + private static final CommentSort cs = new CommentSort(); + + public String eMesg; + public String thumb; + public String title; + public String title_jpn; + public int category; + public String uploader; + public String posted; + public int pages; + public String size; + public String resized; + public String parent; + public String visible; + public String language; + public int people; + public float rating; + public String firstPage; + public int previewPerPage; + public int previewSum; + public LinkedHashMap> tags; + public PreviewList previewList; + public LinkedList comments; + + public void reset() { + eMesg = null; + thumb = null; + title = null; + title_jpn = null; + category = 0; + uploader = null; + posted = null; + pages = 0; + size = null; + resized = null; + parent = null; + visible = null; + language = null; + people = 0; + rating = 0; + firstPage = null; + previewPerPage = 0; + previewSum = 0; + tags = null; + previewList = null; + comments = null; + } + + public int parser(String body, int mode) { + int re = 0; + Pattern p; + Matcher m; + + if (body == null) + return 0; + + if (!body.contains("<")) { + eMesg = body; + return ERROR; + } + + if (body.contains(OFFENSIVE_STRING)) { + return OFFENSIVE; + } + + if (body.contains(PINING_STRING)) { + return PINING; + } + + p = Pattern.compile("
\n

([^<]+)

"); + m = p.matcher(body); + if (m.find()) { + eMesg = m.group(1); + return ERROR; + } + + // Get detail + if ((mode & DETAIL) != 0) { + p = Pattern + .compile("
]+/>
" // thumb + + "
" + + "
" + + "

([^<>]+)

" // title + + "

([^<>]*)

" // title_jpn might be empty string + + "
" + + ".+" + + "
]+><[^<>]*alt=\"([\\w|\\-]+)\"[^<>]*/>
" // category + + "
]+>([^<>]+)" // uploader + + ".+" + + "]*>Posted:]*>([\\w|\\-|\\s|:]+)" // posted + + "]*>Images:]*>([\\d]+) @ ([\\w|\\.|\\s]+)" // pages and size + + "]*>Resized:]*>([^<>]+)" // resized + + "]*>Parent:]*>(?:]*>)?([^<>]+)(?:)?" // parent + + "]*>Visible:]*>([^<>]+)" // visible + + "]*>Language:]*>([^<>]+)" // language + + ".+" + + "\\(([\\d|,]+)\\)" // people + + "" + + "]*>([^<>]+)" // rating + + ".+" + + "
]*>(?:]*>)?]*href=\"([^<>\"]+)\"[^<>]*>"); // get firstPage + m = p.matcher(body); + if (m.find()) { + re |= DETAIL; + + thumb = Utils.unescapeXml(m.group(1)); + title = Utils.unescapeXml(m.group(2)); + title_jpn = Utils.unescapeXml(m.group(3)); + category = EhUtils.getCategory(m.group(4)); + uploader = m.group(5); + posted = m.group(6); + pages = Integer.parseInt(m.group(7)); + size = m.group(8); + resized = m.group(9); + parent = m.group(10); + visible = m.group(11); + language = m.group(12); + people = Integer.parseInt(m.group(13).replace(",", "")); + + Pattern pattern = Pattern.compile("([\\d|\\.]+)"); + Matcher matcher = pattern.matcher(m.group(14)); + if (matcher.find()) + rating = Float.parseFloat(matcher.group(1)); + else + rating = Float.NaN; + + firstPage = m.group(15); + } + } + // Get tag + if ((mode & TAG) != 0) { + tags = new LinkedHashMap>(); + p = Pattern + .compile("]+>([\\w\\s]+):(?:]+>]+>[\\w\\s]+
)+"); + m = p.matcher(body); + while (m.find()) { + re |= TAG; + String groupName = m.group(1); + LinkedList group = getTagGroup(m.group(0)); + if (group != null) { + tags.put(groupName, group); + } + } + } + + // Get preview info + if ((mode & PREVIEW_INFO) != 0) { + p = Pattern.compile("]+>]+>([\\d,]+)]+>(?:]+>)?>(?:)?"); + m = p.matcher(body); + if (m.find()) { + re |= PREVIEW_INFO; + previewSum = Integer.valueOf(m.group(1).replace(",", "")); + } + } + + // Get preview + if ((mode & PREVIEW) != 0) { + boolean isLargePreview = false; + if (body.contains("
]*class=\"gdtm\"[^<>]*>]*width:(\\d+)[^<>]*height:(\\d+)[^<>]*\\((.+?)\\)[^<>]*-(\\d+)px[^<>]*>]*href=\"(.+?)\"[^<>]*>"); + m = p.matcher(body); + while (m.find()) { + re |= PREVIEW; + ((NormalPreviewList)previewList).addItem(m.group(3), m.group(4), "0", m.group(1), + m.group(2), m.group(5)); + } + } + // Set previewPerPage + if (previewList != null) + previewPerPage = previewList.size(); + } + // Get comment + if ((mode & COMMENT) != 0) { + p = Pattern + .compile("
Posted on ([^<>]+) by ]+>([^<>]+).+?
]*>(.+?)
(); + while (m.find()) { + re |= COMMENT; + comments.add(new Comment(m.group(1), m.group(2), m.group(3))); + } + Collections.sort(comments, cs); + } + return re; + } + + private LinkedList getTagGroup(String pageContent) { + LinkedList list = new LinkedList(); + Pattern p = Pattern.compile("]+>]+>([\\w\\s]+)
"); + Matcher m = p.matcher(pageContent); + while (m.find()) + list.add(m.group(1)); + if (list.size() == 0) + return null; + else + return list; + } + + + + static class CommentSort implements Comparator { + private int compareNum(String n1, String n2, int median) { + int re = 0; + for (int i = 0; i < median; i++) { + re *= 10; + re += n1.charAt(i) - n2.charAt(i); + } + return re; + } + private int getMonth(String m) { + if (m.equals("January")) + return 1; + else if (m.equals("February")) + return 2; + else if (m.equals("March")) + return 3; + else if (m.equals("April")) + return 4; + else if (m.equals("May")) + return 5; + else if (m.equals("June")) + return 6; + else if (m.equals("July")) + return 7; + else if (m.equals("August")) + return 8; + else if (m.equals("September")) + return 9; + else if (m.equals("October")) + return 10; + else if (m.equals("November")) + return 11; + else if (m.equals("December")) + return 12; + else + return 0; + } + + @Override + public int compare(Comment c1, Comment c2) { + int re = 0; + Pattern p = Pattern.compile("(\\d{2}) (\\w+) (\\d{4}), (\\d{2}):(\\d{2})"); + Matcher m1 = p.matcher(c1.time); + Matcher m2 = p.matcher(c2.time); + if (!m1.find() || !m2.find()) + return 0; + // year + re += compareNum(m1.group(3), m2.group(3), 4); + re <<= 4; + // Month + re += getMonth(m1.group(2)) - getMonth(m2.group(2)); + re <<= 5; + // Day + re += compareNum(m1.group(1), m2.group(1), 2); + re <<= 5; + // Hour + re += compareNum(m1.group(4), m2.group(4), 2); + re <<= 6; + // Minute + re += compareNum(m1.group(5), m2.group(5), 2); + return re; + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/ehclient/DetailUrlParser.java b/app/src/main/java/com/hippo/ehviewer/ehclient/DetailUrlParser.java new file mode 100644 index 000000000..99393e335 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ehclient/DetailUrlParser.java @@ -0,0 +1,41 @@ +/* + * 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.ehclient; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class DetailUrlParser { + + public int gid; + public String token; + + public boolean parser(String url) { + if (url == null) + return false; + + Pattern p = Pattern.compile("/(\\d+)/(\\w+)"); + Matcher m = p.matcher(url); + if (m.find()) { + gid = Integer.parseInt(m.group(1)); + token = m.group(2); + return true; + } else { + return false; + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/ehclient/EdDetailParser.java b/app/src/main/java/com/hippo/ehviewer/ehclient/EdDetailParser.java new file mode 100644 index 000000000..0b2644aa5 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ehclient/EdDetailParser.java @@ -0,0 +1,131 @@ +/* + * 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.ehclient; + +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.hippo.ehviewer.util.Log; + +public class EdDetailParser { + + int previewPageNum; + int previewStartIndex; + int imageNum; + boolean isLastPage; + List pageTokenArray; + String emsg; + + public boolean parser(String body, int mode, boolean needPreviewInfo) { + Pattern p; + Matcher m; + + if (body == null) + return false; + + if (!body.contains("<")) + emsg = body; + + if (mode == EhClient.MODE_LOFI) { + // Check is last page + p = Pattern.compile("]+>Next Page >"); + m = p.matcher(body); + if (m.find()) + isLastPage = false; + else + isLastPage = true; + + // Parser preview + p = Pattern.compile("
]*>]*>"); + m = p.matcher(body); + boolean first = true; + while(m.find()) { + if (first) { + pageTokenArray = new LinkedList(); + previewStartIndex = Integer.parseInt(m.group(2)) - 1; + first = false; + } + pageTokenArray.add(m.group(1)); + } + if (first) + return false; + else + return true; + + } else { + // Parser preview info + if (needPreviewInfo) { + p = Pattern.compile("]+>]+>([\\d,]+)]+>(?:]+>)?>(?:)?"); + m = p.matcher(body); + if (m.find()) + previewPageNum = Integer.valueOf(m.group(1).replace(",", "")); + else { + Log.e("TAG", "get needPreviewInfo failed"); + return false; + } + + p = Pattern.compile("Images:([\\d,]+) "); + m = p.matcher(body); + if (m.find()) + imageNum = Integer.valueOf(m.group(1).replace(",", "")); + else { + Log.e("TAG", "get needPreviewInfo failed"); + return false; + } + } + + // Parser preview + + boolean first = true; + if (body.contains("
]*>"); + m = p.matcher(body); + while(m.find()) { + if (first) { + pageTokenArray = new LinkedList(); + previewStartIndex = Integer.parseInt(m.group(2)) - 1; + first = false; + } + pageTokenArray.add(m.group(1)); + } + + } else { + p = Pattern.compile("
]*>]*>"); + m = p.matcher(body); + while(m.find()) { + if (first) { + pageTokenArray = new LinkedList(); + previewStartIndex = Integer.parseInt(m.group(2)) - 1; + first = false; + } + pageTokenArray.add(m.group(1)); + } + } + + + + if (first) { + Log.e("TAG", "get gdtm"); + return false; + } else + return true; + } + } + +} diff --git a/app/src/main/java/com/hippo/ehviewer/ehclient/EhClient.java b/app/src/main/java/com/hippo/ehviewer/ehclient/EhClient.java new file mode 100644 index 000000000..b2c5a0a5c --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ehclient/EhClient.java @@ -0,0 +1,1424 @@ +/* + * 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.ehclient; + +import java.io.File; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.Process; + +import com.hippo.ehviewer.Analytics; +import com.hippo.ehviewer.AppHandler; +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.data.ApiGalleryInfo; +import com.hippo.ehviewer.data.Comment; +import com.hippo.ehviewer.data.GalleryDetail; +import com.hippo.ehviewer.data.GalleryInfo; +import com.hippo.ehviewer.data.GalleryPopular; +import com.hippo.ehviewer.data.LofiGalleryDetail; +import com.hippo.ehviewer.data.PreviewList; +import com.hippo.ehviewer.network.HttpHelper; +import com.hippo.ehviewer.util.BgThread; +import com.hippo.ehviewer.util.Config; +import com.hippo.ehviewer.util.EhUtils; +import com.hippo.ehviewer.util.Utils; + +public class EhClient { + @SuppressWarnings("unused") + private static final String TAG = EhClient.class.getSimpleName(); + + public static final int FAVORITE_SLOT_NUM = 10; + + public static final int MODE_G = 0x0; + public static final int MODE_EX = 0x1; + public static final int MODE_LOFI = 0x2; + + public static final String API_G = "http://g.e-hentai.org/api.php"; + public static final String API_EX = "http://exhentai.org/api.php"; + public static final long APIUID = 1363542; + public static final String APIKEY = "f4b5407ab1727b9d08d7"; + + private static final String LOGIN_URL = "http://forums.e-hentai.org/index.php?act=Login&CODE=01"; + private static final String FORUMS_URL = "http://forums.e-hentai.org/index.php"; + + public static final String HEADER_G = "http://g.e-hentai.org/"; + public static final String HEADER_EX = "http://exhentai.org/"; + public static final String HEADER_LOFI = "http://lofi.e-hentai.org/"; + + public static final String API_EHVIEWER = "http://www.ehviewer.com/API"; + + /* Avatar get code */ + public static final int GET_AVATAR_OK = 0x0; + public static final int GET_AVATAR_ERROR = 0x1; + public static final int NO_AVATAR = 0x2; + + private final Context mContext; + private final Handler mHandler; + private final EhInfo mInfo; + + private static EhClient sInstance; + + public static void createInstance(Context context) { + sInstance = new EhClient(context); + } + + public static EhClient getInstance() { + return sInstance; + } + + private EhClient(Context context) { + mContext = context; + mHandler = AppHandler.getInstance(); + mInfo = EhInfo.getInstance(context); + } + + public static String getUrlHeader() { + return getUrlHeader(Config.getMode()); + } + + public static String getUrlHeader(int mode) { + switch (mode) { + case MODE_EX: + return HEADER_EX; + case MODE_LOFI: + return HEADER_LOFI; + case MODE_G: + default: + return HEADER_G; + } + } + + /** + * Get gellary detail url in default mode + * @param gid + * @param token + * @return + */ + public String getDetailUrl(int gid, String token) { + return getDetailUrl(gid, token, 0, Config.getMode()); + } + + /** + * Get gellary detail url in default mode + * @param gid + * @param token + * @return + */ + public String getDetailUrl(int gid, String token, int pageNum) { + return getDetailUrl(gid, token, pageNum, Config.getMode()); + } + + /** + * Get gellary detail url in target mode + * @param gid + * @param token + * @return + */ + public static String getDetailUrl(int gid, String token, int pageNum, int mode) { + StringBuilder sb = new StringBuilder(); + switch (mode) { + case MODE_EX: + sb.append(HEADER_EX); + break; + case MODE_LOFI: + sb.append(HEADER_LOFI); + break; + case MODE_G: + default: + sb.append(HEADER_G); + break; + } + return sb.append("g/").append(gid).append("/").append(token) + .append("/?p=").append(pageNum).toString(); + } + + public String getPageUrl(int gid, String token, int pageNum) { + return getPageUrl(gid, token, pageNum, Config.getMode()); + } + + public static String getPageUrl(int gid, String token, int pageNum, int mode) { + StringBuilder sb = new StringBuilder(); + switch (mode) { + case MODE_EX: + sb.append(HEADER_EX); + break; + case MODE_LOFI: + sb.append(HEADER_LOFI); + break; + case MODE_G: + default: + sb.append(HEADER_G); + break; + } + return sb.append("s/").append(token).append("/").append(gid) + .append("-").append(pageNum).toString(); + } + + public String getApiUrl() { + return getApiUrl(Config.getApiMode()); + } + + public static String getApiUrl(int mode) { + switch (mode) { + case MODE_EX: + return API_EX; + case MODE_G: + default: + return API_G; + } + } + + public String getFavoriteUrl(int page) { + return getFavoriteUrl(Config.getApiMode(), page); + } + + public static String getFavoriteUrl(int mode, int page) { + StringBuilder sb = new StringBuilder(); + switch (mode) { + case MODE_EX: + sb.append(HEADER_EX); + break; + case MODE_G: + default: + sb.append(HEADER_G); + break; + } + return sb.append("favorites.php?page=").append(page).toString(); + } + + public String getFavoriteUrlWithCat(int index, int page) { + return getFavoriteUrlWithCat(Config.getApiMode(), index, page); + } + + public static String getFavoriteUrlWithCat(int mode, int index, int page) { + StringBuilder sb = new StringBuilder(); + switch (mode) { + case MODE_EX: + sb.append(HEADER_EX); + break; + case MODE_G: + default: + sb.append(HEADER_G); + break; + } + return sb.append("favorites.php?favcat=").append(index).append("&page=") + .append(page).toString(); + } + + public interface OnGetMangaUrlListener { + public void onSuccess(Object checkFlag, String[] arg); + public void onFailure(Object checkFlag, String eMsg); + } + + public interface OnGetImageListener { + public void onSuccess(Object checkFlag, Object res); + public void onFailure(int eMsgId); + } + + public interface OnGetGalleryMetadataListener { + public void onSuccess(Map lmds); + public void onFailure(String eMsg); + } + + public interface OnGetGalleryTokensListener { + public void onSuccess(Map tokens); + public void onFailure(String eMsg); + } + + /** + * True if Login + * @return + */ + public boolean isLogin() { + return mInfo.isLogin(); + } + + /** + * Just remove cookie + */ + public void logout() { + mInfo.logout(); + } + + /** + * Return username, if not return default string + */ + public String getUsername() { + return mInfo.getUsername(); + } + + /** + * Return displayname, if not return default string + */ + public String getDisplayname() { + return mInfo.getDisplayname(); + } + + /** + * Return avatar, if not return default avatar + */ + public Drawable getAvatar() { + return mInfo.getAvatar(); + } + + public interface OnLoginListener { + public void onSuccess(); + public void onGetAvatar(int code); + public void onFailure(String eMesg); + } + + private class LoginResponder implements Runnable { + private final boolean isOk; + private final OnLoginListener listener; + private final String eMesg; + + public LoginResponder(OnLoginListener listener) { + this.isOk = true; + this.listener = listener; + this.eMesg = null; + } + + public LoginResponder(OnLoginListener listener, String eMesg) { + this.isOk = false; + this.listener = listener; + this.eMesg = eMesg; + } + + @Override + public void run() { + if (isOk) { + listener.onSuccess(); + } else { + listener.onFailure(eMesg); + } + } + } + + public void login(final String username, final String password, + final OnLoginListener listener) { + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + final HttpHelper hh = new HttpHelper(mContext); + String body = hh.postForm(LOGIN_URL, new String[][] { + new String[] { "UserName", username }, + new String[] { "PassWord", password }, + new String[] { "submit", "Log me in" }, + new String[] { "CookieDate", "1" }, + new String[] { "temporary_https", "on" }}); + LoginResponder responder; + if (body == null) { + responder = new LoginResponder(listener, hh.getEMsg()); + } else if (!body.contains("<")) { + responder = new LoginResponder(listener, body); + } else { + LoginParser parser = new LoginParser(); + if (parser.parser(body)) { + mInfo.login(username, parser.displayname); + getAvatar(listener); + responder = new LoginResponder(listener); + } else { + responder = new LoginResponder(listener, parser.eMesg); + } + } + mHandler.post(responder); + } + }); + thread.setPriority(Process.THREAD_PRIORITY_BACKGROUND); + thread.start(); + } + + private void getAvatar(final OnLoginListener listener) { + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + String body; + Pattern p; + Matcher m; + // Get user profile url + HttpHelper hp = new HttpHelper(mContext); + body = hp.get(FORUMS_URL); + if (body == null) { + listener.onGetAvatar(GET_AVATAR_ERROR); + return; + } + p = Pattern.compile("

.+?"); + m = p.matcher(body); + if (!m.find()) { + listener.onGetAvatar(GET_AVATAR_ERROR); + return; + } + String profileUrl = Utils.unescapeXml(m.group(1)); + // Get avatar url + body = hp.get(profileUrl); + if (body == null) { + listener.onGetAvatar(GET_AVATAR_ERROR); + return; + } + p = Pattern.compile("

"); + m = p.matcher(body); + if (!m.find()) { + listener.onGetAvatar(NO_AVATAR); + return; + } + String avatarUrl = m.group(1); + if (!avatarUrl.startsWith("http")) + avatarUrl = "http://forums.e-hentai.org/" + avatarUrl; + // Get avatar + Bitmap avatar = hp.getImage(avatarUrl); + // Set avatar if not null + if (avatar != null) + mInfo.setAvatar(new BitmapDrawable(EhClient.this.mContext.getResources(), avatar)); + // notification + listener.onGetAvatar(GET_AVATAR_OK); + } + }); + thread.setPriority(Process.THREAD_PRIORITY_BACKGROUND); + thread.start(); + } + + // Get Gallery List + public interface OnGetGListListener { + public void onSuccess(Object checkFlag, List giList, + int maxPage); + public void onFailure(Object checkFlag, String eMsg); + } + + private class GetGListResponder implements Runnable { + private final boolean isOk; + private final OnGetGListListener listener; + private final Object checkFlag; + private final List giList; + private final int maxPage; + private final String eMesg; + + public GetGListResponder(OnGetGListListener listener, Object checkFlag, + List giList, int maxPage) { + this.isOk = true; + this.listener = listener; + this.checkFlag = checkFlag; + this.giList = giList; + this.maxPage = maxPage; + this.eMesg = null; + } + + public GetGListResponder(OnGetGListListener listener, Object checkFlag, String eMesg) { + this.isOk = false; + this.listener = listener; + this.checkFlag = checkFlag; + this.giList = null; + this.maxPage = 0; + this.eMesg = eMesg; + } + + @Override + public void run() { + if (isOk) + listener.onSuccess(checkFlag, giList, maxPage); + else + listener.onFailure(checkFlag, eMesg); + } + } + + public void getGList(String url, Object checkFlag, + OnGetGListListener listener) { + getGList(url, Config.getMode(), checkFlag, listener); + } + + public void getGList(final String url, final int mode, final Object checkFlag, + final OnGetGListListener listener) { + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + HttpHelper hp = new HttpHelper(mContext); + String body = hp.get(url); + GetGListResponder responder; + if (body == null) { + responder = new GetGListResponder(listener, checkFlag, hp.getEMsg()); + } else if (!body.contains("<")) { + responder = new GetGListResponder(listener, checkFlag, body); + } else { + final ListParser parser = new ListParser(); + switch (parser.parser(body, mode)) { + case ListParser.ALL: + responder = new GetGListResponder(listener, checkFlag, + parser.giList, parser.pageNum); + break; + case ListParser.NOT_FOUND: + responder = new GetGListResponder(listener, checkFlag, + parser.giList, 0); + break; + case ListParser.INDEX_ERROR: + responder = new GetGListResponder(listener, checkFlag, + mContext.getString(R.string.em_index_error)); + break; + case ListParser.PARSER_ERROR: + default: + responder = new GetGListResponder(listener, checkFlag, + mContext.getString(R.string.em_parser_error)); + break; + } + } + mHandler.post(responder); + } + }); + thread.setPriority(Process.THREAD_PRIORITY_BACKGROUND); + thread.start(); + } + + // Get Gallery List from file search + + public static final int IMAGE_SEARCH_USE_SIMILARITY_SCAN = 0x1; + public static final int IMAGE_SEARCH_ONLY_SEARCH_COVERS = 0x2; + public static final int IMAGE_SEARCH_SHOW_EXPUNGED = 0x4; + + public interface OnGetGListFromImageSearchListener { + public void onSuccess(Object checkFlag, List giList, + int maxPage, String newUrl); + public void onFailure(Object checkFlag, String eMsg); + } + + private class GetGListFromImageSearchResponder implements Runnable { + + private final boolean isOk; + private final OnGetGListFromImageSearchListener listener; + private final Object checkFlag; + private final List giList; + private final int maxPage; + private final String newUrl; + private final String eMesg; + + public GetGListFromImageSearchResponder(OnGetGListFromImageSearchListener listener, + Object checkFlag, List giList, int maxPage, String newUrl) { + this.isOk = true; + this.listener = listener; + this.checkFlag = checkFlag; + this.giList = giList; + this.maxPage = maxPage; + this.newUrl = newUrl; + this.eMesg = null; + } + + public GetGListFromImageSearchResponder(OnGetGListFromImageSearchListener listener, + Object checkFlag, String eMesg) { + this.isOk = false; + this.listener = listener; + this.checkFlag = checkFlag; + this.giList = null; + this.maxPage = 0; + this.newUrl = null; + this.eMesg = eMesg; + } + + @Override + public void run() { + if (isOk) + listener.onSuccess(checkFlag, giList, maxPage, newUrl); + else + listener.onFailure(checkFlag, eMesg); + } + } + + public static String getFileSearchUrl(int apiMode) { + switch (apiMode) { + case MODE_EX: + return "http://ul.exhentai.org/image_lookup.php"; + case MODE_G: + default: + return "http://ul.e-hentai.org/image_lookup.php"; + } + } + + public void getGListFromImageSearch(File file, int searchMode, + Object checkFlag, OnGetGListFromImageSearchListener listener) { + getGListFromImageSearch(file, null, searchMode, + Config.getApiMode(), checkFlag, listener); + } + + public void getGListFromImageSearch(Bitmap bmp, int searchMode, + Object checkFlag, OnGetGListFromImageSearchListener listener) { + getGListFromImageSearch(null, bmp, searchMode, + Config.getApiMode(), checkFlag, listener); + } + + private void getGListFromImageSearch(final File file, final Bitmap bitmap, + final int searchMode, final int apiMode, final Object checkFlag, + final OnGetGListFromImageSearchListener listener) { + if (file == null && bitmap == null) + listener.onFailure(checkFlag, mContext.getString(R.string.invalid_input)); + + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + HttpHelper hh = new HttpHelper(mContext); + + List dataList = new LinkedList(); + HttpHelper.FormData data; + // Add file or bitmap + if (file != null) { + data = new HttpHelper.FileData(file); + data.setProperty("Content-Disposition", "form-data; name=\"sfile\"; filename=\"" + file.getName() + "\""); + dataList.add(data); + } else { + data = new HttpHelper.BitmapData(bitmap); + data.setProperty("Content-Disposition", "form-data; name=\"sfile\"; filename=\"hehe.jpg\""); + dataList.add(data); + } + // Add + data = new HttpHelper.StringData("File Search"); + data.setProperty("Content-Disposition", "form-data; name=\"f_sfile\""); + dataList.add(data); + // Add FILE_SEARCH_USE_SIMILARITY_SCAN + if ((searchMode & IMAGE_SEARCH_USE_SIMILARITY_SCAN) != 0) { + data = new HttpHelper.StringData("on"); + data.setProperty("Content-Disposition", "form-data; name=\"fs_similar\""); + dataList.add(data); + } + // Add FILE_SEARCH_ONLY_SEARCH_COVERS + if ((searchMode & IMAGE_SEARCH_ONLY_SEARCH_COVERS) != 0) { + data = new HttpHelper.StringData("on"); + data.setProperty("Content-Disposition", "form-data; name=\"fs_covers\""); + dataList.add(data); + } + // Add FILE_SEARCH_SHOW_EXPUNGED + if ((searchMode & IMAGE_SEARCH_SHOW_EXPUNGED) != 0) { + data = new HttpHelper.StringData("on"); + data.setProperty("Content-Disposition", "form-data; name=\"fs_exp\""); + dataList.add(data); + } + + String body = hh.postFormData(getFileSearchUrl(apiMode), dataList); + GetGListFromImageSearchResponder responder; + if (body == null) { + responder = new GetGListFromImageSearchResponder(listener, checkFlag, hh.getEMsg()); + } else if (!body.contains("<")) { + responder = new GetGListFromImageSearchResponder(listener, checkFlag, body); + } else { + String newUrl = hh.getLastUrl(); + if (newUrl == null) { + responder = new GetGListFromImageSearchResponder(listener, checkFlag, "Location is null"); + } else { + final ListParser parser = new ListParser(); + switch (parser.parser(body, apiMode)) { + case ListParser.ALL: + responder = new GetGListFromImageSearchResponder(listener, checkFlag, + parser.giList, parser.pageNum, newUrl); + break; + case ListParser.NOT_FOUND: + responder = new GetGListFromImageSearchResponder(listener, checkFlag, + parser.giList, 0, newUrl); + break; + case ListParser.INDEX_ERROR: + responder = new GetGListFromImageSearchResponder(listener, checkFlag, + mContext.getString(R.string.em_index_error)); + break; + case ListParser.PARSER_ERROR: + default: + responder = new GetGListFromImageSearchResponder(listener, checkFlag, + mContext.getString(R.string.em_parser_error)); + break; + } + } + } + mHandler.post(responder); + } + }); + thread.setPriority(Process.THREAD_PRIORITY_BACKGROUND); + thread.start(); + } + + // Get gallery Detail + public interface OnGetGDetailListener { + public void onSuccess(GalleryDetail md); + public void onFailure(String eMsg); + } + + private class GetGDetaiResponder implements Runnable { + private final boolean isOk; + private final OnGetGDetailListener listener; + private final GalleryDetail gd; + private final String eMesg; + + public GetGDetaiResponder(OnGetGDetailListener listener, GalleryDetail gd) { + this.isOk = true; + this.listener = listener; + this.gd = gd; + this.eMesg = null; + } + + public GetGDetaiResponder(OnGetGDetailListener listener, String eMsg) { + this.isOk = false; + this.listener = listener; + this.gd = null; + this.eMesg = eMsg; + } + + @Override + public void run() { + if (isOk) + listener.onSuccess(gd); + else + listener.onFailure(eMesg); + } + } + + public void getGDetail(final String url, final GalleryDetail md, + final OnGetGDetailListener listener) { + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + HttpHelper hh = new HttpHelper(mContext); + String body = hh.get(url); + GetGDetaiResponder responder; + if (body == null) { + responder = new GetGDetaiResponder(listener, hh.getEMsg()); + } else if (!body.contains("<")) { + responder = new GetGDetaiResponder(listener, body); + } else { + DetailParser parser = new DetailParser(); + int result = parser.parser(body, DetailParser.DETAIL | DetailParser.TAG + | DetailParser.PREVIEW_INFO | DetailParser.PREVIEW + | DetailParser.COMMENT); + if (result == DetailParser.OFFENSIVE) { + md.firstPage = "offensive"; + responder = new GetGDetaiResponder(listener, md); + } else if (result == DetailParser.PINING) { + md.firstPage = "pining"; + responder = new GetGDetaiResponder(listener, md); + } else if ((result & (DetailParser.DETAIL | DetailParser.PREVIEW_INFO | + DetailParser.PREVIEW)) == (DetailParser.DETAIL | DetailParser.PREVIEW_INFO | + DetailParser.PREVIEW)) { + // At least get detail and preview + md.thumb = parser.thumb; + md.title = parser.title; + md.title_jpn = parser.title_jpn; + md.category = parser.category; + md.uploader = parser.uploader; + md.posted = parser.posted; + md.pages = parser.pages; + md.size = parser.size; + md.resized = parser.resized; + md.parent = parser.parent; + md.visible = parser.visible; + md.language = parser.language; + md.people = parser.people; + md.rating = parser.rating; + md.firstPage = parser.firstPage; + md.previewPerPage = parser.previewPerPage; + md.previewSum = parser.previewSum; + + md.tags = parser.tags; + md.previewLists = new PreviewList[md.previewSum]; + md.previewLists[0] = parser.previewList; + md.comments = parser.comments; + responder = new GetGDetaiResponder(listener, md); + } else if (result == DetailParser.ERROR) { + responder = new GetGDetaiResponder(listener, parser.eMesg); + } else { + responder = new GetGDetaiResponder(listener, + mContext.getString(R.string.em_parser_error)); + } + } + mHandler.post(responder); + } + }); + thread.setPriority(Process.THREAD_PRIORITY_BACKGROUND); + thread.start(); + } + + // Get lofi gallery Detail + public interface OnGetLGDetailListener { + public void onSuccess(LofiGalleryDetail md, boolean isLastPage); + public void onFailure(String eMsg); + } + + private class GetLGDetaiResponder implements Runnable { + private final boolean isOk; + private final OnGetLGDetailListener listener; + private final LofiGalleryDetail gd; + private final boolean isLastPage; + private final String eMesg; + + public GetLGDetaiResponder(OnGetLGDetailListener listener, LofiGalleryDetail gd, boolean isLastPage) { + this.isOk = true; + this.listener = listener; + this.gd = gd; + this.isLastPage = isLastPage; + this.eMesg = null; + } + + public GetLGDetaiResponder(OnGetLGDetailListener listener, String eMsg) { + this.isOk = false; + this.listener = listener; + this.gd = null; + this.isLastPage = false; + this.eMesg = eMsg; + } + + @Override + public void run() { + if (isOk) + listener.onSuccess(gd, isLastPage); + else + listener.onFailure(eMesg); + } + } + + public void getLGDetail(final String url, final LofiGalleryDetail lgd, + final OnGetLGDetailListener listener) { + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + HttpHelper hh = new HttpHelper(mContext); + String body = hh.get(url); + GetLGDetaiResponder responder; + if (body == null) { + responder = new GetLGDetaiResponder(listener, hh.getEMsg()); + } else if (!body.contains("<")) { + responder = new GetLGDetaiResponder(listener, body); + } else { + LofiDetailParser parser = new LofiDetailParser(); + if (parser.parser(body)) { + lgd.setPreview(0, parser.preview); + lgd.previewPerPage = parser.preview.size(); + responder = new GetLGDetaiResponder(listener, lgd, parser.isLastPage); + } else { + responder = new GetLGDetaiResponder(listener, + mContext.getString(R.string.em_parser_error)); + } + } + mHandler.post(responder); + } + }); + thread.setPriority(Process.THREAD_PRIORITY_BACKGROUND); + thread.start(); + } + + + // Get preview list + public interface OnGetPreviewListListener { + public void onSuccess(Object checkFlag, PreviewList pageList, boolean isLastPage); + public void onFailure(Object checkFlag, String eMsg); + } + + private class GetPreviewListResponder implements Runnable { + private final boolean isOk; + private final OnGetPreviewListListener listener; + private final Object checkFlag; + private final PreviewList pageList; + private final boolean isLastPage; + private final String eMesg; + + public GetPreviewListResponder(OnGetPreviewListListener listener, + Object checkFlag, PreviewList pageList, boolean isLastPage) { + this.isOk = true; + this.listener = listener; + this.checkFlag = checkFlag; + this.pageList = pageList; + this.isLastPage = isLastPage; + this.eMesg = null; + } + + public GetPreviewListResponder(OnGetPreviewListListener listener, + Object checkFlag, String eMsg) { + this.isOk = false; + this.listener = listener; + this.checkFlag = checkFlag; + this.pageList = null; + this.isLastPage = false; + this.eMesg = eMsg; + } + + @Override + public void run() { + if (isOk) + listener.onSuccess(checkFlag, pageList, isLastPage); + else + listener.onFailure(checkFlag, eMesg); + } + } + + public void getPreviewList(final String url, final int mode, final Object checkFlag, + final OnGetPreviewListListener listener) { + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + HttpHelper hh = new HttpHelper(mContext); + String body = hh.get(url); + GetPreviewListResponder responder; + if (body != null) { + if (mode != EhClient.MODE_LOFI) { + DetailParser parser = new DetailParser(); + if (parser.parser(body, DetailParser.PREVIEW) == + DetailParser.PREVIEW) + responder = new GetPreviewListResponder(listener, checkFlag, parser.previewList, false); + else + responder = new GetPreviewListResponder(listener, checkFlag, "Parser error"); + } else { + LofiDetailParser parser = new LofiDetailParser(); + if (parser.parser(body)) + responder = new GetPreviewListResponder(listener, checkFlag, parser.preview, parser.isLastPage); + else + responder = new GetPreviewListResponder(listener, checkFlag, "Parser error"); + } + } else { + responder = new GetPreviewListResponder(listener, checkFlag, hh.getEMsg()); + } + mHandler.post(responder); + } + }); + thread.setPriority(Process.THREAD_PRIORITY_BACKGROUND); + thread.start(); + } + + // Post comment + public interface OnCommentListener { + void onSuccess(List comments); + void onFailure(String eMsg); + } + + public void comment(final String detailUrl, final String comment, + final OnCommentListener listener) { + new BgThread(new Runnable() { + @Override + public void run() { + HttpHelper hp = new HttpHelper(mContext); + hp.setOnRespondListener(new HttpHelper.OnRespondListener() { + @Override + public void onSuccess(Object obj) { + String body = (String)obj; + DetailParser parser = new DetailParser(); + if (parser.parser(body, DetailParser.COMMENT) == + DetailParser.COMMENT) { + listener.onSuccess(parser.comments); + } else { + listener.onFailure( mContext.getString(R.string.em_parser_error)); + } + } + + @Override + public void onFailure(String eMsg) { + listener.onFailure(eMsg); + } + }); + hp.postForm(detailUrl, new String[][]{ + new String[]{"commenttext", comment}, + new String[]{"postcomment", "Post New"}}); + } + }).start(); + } + + /********** Use E-hentai API ************/ + + // rate for gallery + public interface OnRateListener { + void onSuccess(float ratingAvg, int ratingCnt); + void onFailure(String eMsg); + } + + public void rate(final int gid, final String token, + final int rating, final OnRateListener listener) { + final JSONObject json = new JSONObject(); + try { + json.put("method", "rategallery"); + json.put("apiuid", APIUID); + json.put("apikey", APIKEY); + json.put("gid", gid); + json.put("token", token); + json.put("rating", rating); + } catch (JSONException e) { + listener.onFailure(e.getMessage()); + return; + } + + new Thread(new Runnable() { + @Override + public void run() { + HttpHelper hp = new HttpHelper(mContext); + hp.setOnRespondListener(new HttpHelper.OnRespondListener() { + @Override + public void onSuccess(Object obj) { + String body = (String)obj; + RateParser parser = new RateParser(); + if (parser.parser(body)) { + listener.onSuccess(parser.mRatingAvg, parser.mRatingCnt); + } else { + listener.onFailure(mContext.getString(R.string.em_parser_error)); + } + } + @Override + public void onFailure(String eMsg) { + listener.onFailure(eMsg); + } + }); + hp.postJson(getApiUrl(), json); + } + }).start(); + } + + // vote for tag + public interface OnVoteListener { + void onSuccess(String tagPane); + void onFailure(String eMsg); + } + + public void vote(final int gid, final String token, + final String group, final String tag, + final boolean isUp, final OnVoteListener listener) { + final JSONObject json = new JSONObject(); + try { + json.put("method", "taggallery"); + json.put("apiuid", APIUID); + json.put("apikey", APIKEY); + json.put("gid", gid); + json.put("token", token); + json.put("tags", group + ":" + tag); + if (isUp) + json.put("vote", 1); + else + json.put("vote", -1); + } catch (JSONException e) { + listener.onFailure(e.getMessage()); + return; + } + + new Thread(new Runnable() { + @Override + public void run() { + HttpHelper hp = new HttpHelper(mContext); + hp.setOnRespondListener(new HttpHelper.OnRespondListener() { + @Override + public void onSuccess(Object obj) { + String body = (String)obj; + VoteParser parser = new VoteParser(); + if (parser.parser(body)) { + listener.onSuccess(parser.mTagPane); + } else { + listener.onFailure( mContext.getString(R.string.em_parser_error)); + } + } + @Override + public void onFailure(String eMsg) { + listener.onFailure(eMsg); + } + }); + hp.postJson(getApiUrl(), json); + } + }).start(); + } + + // Add to favorite + public interface OnAddToFavoriteListener { + void onSuccess(); + void onFailure(String eMsg); + } + + public String getAddFavouriteUrl(int gid, String token) { + return getAddFavouriteUrl(gid, token, Config.getApiMode()); + } + + public static String getAddFavouriteUrl(int gid, String token, int mode) { + StringBuilder sb = new StringBuilder(); + switch (mode) { + case MODE_EX: + sb.append(HEADER_EX); + break; + case MODE_G: + default: + sb.append(HEADER_G); + break; + } + return sb.append("gallerypopups.php?gid=").append(gid).append("&t=") + .append(token).append("&act=addfav").toString(); + } + + /** + * + * @param gid + * @param token + * @param cat -1 for delete, number bigger than 9 or smaller than -1 go to 0 + * @param note max 250 characters + * @param listener + */ + public void addToFavorite(final int gid, final String token, + final int cat, final String note, + final OnAddToFavoriteListener listener) { + + new Thread(new Runnable() { + @Override + public void run() { + HttpHelper hp = new HttpHelper(mContext); + hp.setOnRespondListener(new HttpHelper.OnRespondListener() { + @Override + public void onSuccess(Object obj) { + String body = (String)obj; + AddToFavoriteParser parser = new AddToFavoriteParser(); + if (parser.parser(body)) { + listener.onSuccess(); + // Analytics + Analytics.addToFavoriteGallery(mContext, gid, token); + } else { + listener.onFailure( mContext.getString(R.string.em_parser_error)); + } + } + @Override + public void onFailure(String eMsg) { + listener.onFailure(eMsg); + } + }); + + String catStr; + if (cat == -1) + catStr = "favdel"; + else if (cat >= 0 && cat <= 9) + catStr = String.valueOf(cat); + else + catStr = "0"; + + // submit=Add+to+Favorites is not necessary, just use submit=Apply+Changes all the time + hp.postForm(getAddFavouriteUrl(gid, token), new String[][]{ + new String[]{"favcat", catStr}, + new String[]{"favnote", note == null ? "" : note}, + new String[]{"submit", "Apply Changes"}}); + } + }).start(); + } + + // modifyFavorite + public interface OnModifyFavoriteListener { + void onSuccess(List gis, int pageNum); + void onFailure(String eMsg); + } + + public String getModifyFavoriteUrl() { + return getModifyFavoriteUrl(Config.getMode()); + } + + public static String getModifyFavoriteUrl(int mode) { + StringBuilder sb = new StringBuilder(); + switch (mode) { + case MODE_EX: + sb.append(HEADER_EX); + break; + case MODE_G: + default: + sb.append(HEADER_G); + break; + } + return sb.append("favorites.php").toString(); + } + + /** + * + * @param gids + * @param dstCat -1 for delete + * @param srcCat + * @param listener + */ + public void modifyFavorite(final int[] gids, final int dstCat, final int srcCat, + final OnModifyFavoriteListener listener) { + new BgThread(new Runnable() { + @Override + public void run() { + int i; + + HttpHelper hp = new HttpHelper(mContext); + hp.setOnRespondListener(new HttpHelper.OnRespondListener() { + @Override + public void onSuccess(Object obj) { + String body = (String)obj; + ListParser parser = new ListParser(); + int re = parser.parser(body, MODE_G); // Only MODE_G and MODE_EX + if (re == ListParser.ALL) { + listener.onSuccess(parser.giList, parser.pageNum); + } else if (re == ListParser.NOT_FOUND) { + listener.onSuccess(parser.giList, parser.pageNum); + } else if (re == ListParser.PARSER_ERROR) { + listener.onFailure( mContext.getString(R.string.em_parser_error)); + } + } + @Override + public void onFailure(String eMsg) { + listener.onFailure(eMsg); + } + }); + + String catStr; + if (dstCat == -1) + catStr = "delete"; + else if (dstCat >= 0 && dstCat <= 9) + catStr = "fav" + String.valueOf(dstCat); + else + catStr = "fav0"; + + String[][] args = new String[gids.length + 2][]; + args[0] = new String[]{"ddact", catStr}; + for (i = 1; i <= gids.length; i++) + args[i] = new String[]{"modifygids[]", String.valueOf(gids[i-1])}; + args[i] = new String[]{"apply", "Apply"}; + + hp.postForm(getFavoriteUrlWithCat(srcCat, 0), args); + } + }).start(); + } + + public interface OnGetPopularListener { + void onSuccess(List gis, long timeStamp); + void onFailure(String eMsg); + } + + public void getPopular(final OnGetPopularListener listener) { + final JSONObject json = new JSONObject(); + try { + json.put("method", "popular"); + } catch (JSONException e) { + listener.onFailure(e.getMessage()); + return; + } + new BgThread(new Runnable() { + @Override + public void run() { + HttpHelper hp = new HttpHelper(mContext); + hp.setOnRespondListener(new HttpHelper.OnRespondListener() { + @Override + public void onSuccess(Object obj) { + String body = (String)obj; + try { + List gis = new ArrayList(); + JSONObject js = new JSONObject(body); + js = js.getJSONObject("popular"); + + if (!js.has("galleries")) { + listener.onFailure(js.getString("error")); + return; + } + + JSONArray ja = js.getJSONArray("galleries"); + for (int i = 0; i < ja.length(); i++) { + JSONObject j = ja.getJSONObject(i); + GalleryPopular gi = new GalleryPopular(); + if (j.has("count")) + gi.count = j.getLong("count"); + else + gi.count = -1; + gi.gid = j.getInt("gid"); + gi.token = j.getString("token"); + gi.title = Utils.unescapeXml(j.getString("title")); + gi.posted = Utils.sDate.format(Long.parseLong(j.getString("posted")) * 1000); + gi.thumb = j.getString("thumb"); + gi.category = EhUtils.getCategory(j.getString("category")); + gi.uploader = j.getString("uploader"); + gi.rating = Float.parseFloat(j.getString("rating")); + gi.generateSLang(); + gis.add(gi); + } + + long timeStamp; + if (js.has("time")) + timeStamp = js.getLong("time"); + else + timeStamp = -1; + + listener.onSuccess(gis, timeStamp); + } catch (JSONException e) { + listener.onFailure(e.getMessage()); + } + } + @Override + public void onFailure(String eMsg) { + listener.onFailure(eMsg); + } + }); + hp.postJson(API_EHVIEWER, json); + } + }).start(); + } + + public interface OnGetApiGalleryInfoListener { + public void onSuccess(Object checkFlag, ApiGalleryInfo[] agiArray); + public void onFailure(Object checkFlag, String eMsg); + } + + private class GetApiGalleryInfoResponder implements Runnable { + private final boolean isOk; + private final OnGetApiGalleryInfoListener listener; + private final Object checkFlag; + private final ApiGalleryInfo[] agiArray; + private final String eMesg; + + public GetApiGalleryInfoResponder(OnGetApiGalleryInfoListener listener, + Object checkFlag, ApiGalleryInfo[] agiArray) { + this.isOk = true; + this.listener = listener; + this.checkFlag = checkFlag; + this.agiArray = agiArray; + this.eMesg = null; + } + + public GetApiGalleryInfoResponder(OnGetApiGalleryInfoListener listener, + Object checkFlag, String eMsg) { + this.isOk = false; + this.listener = listener; + this.checkFlag = checkFlag; + this.agiArray = null; + this.eMesg = eMsg; + } + + @Override + public void run() { + if (isOk) + listener.onSuccess(checkFlag, agiArray); + else + listener.onFailure(checkFlag, eMesg); + } + } + + private ApiGalleryInfo Json2Agi(JSONObject js) throws JSONException { + ApiGalleryInfo agi = new ApiGalleryInfo(); + agi.gid = js.getInt("gid"); + agi.token = js.getString("token"); + agi.archiver_key = js.getString("archiver_key"); + agi.title = js.getString("title"); + agi.title_jpn = js.getString("title_jpn"); + agi.category = EhUtils.getCategory(js.getString("category")); + agi.thumb = js.getString("thumb"); + agi.uploader = js.getString("uploader"); + agi.posted = Utils.sDate.format(Long.parseLong(js.getString("posted")) * 1000); + agi.filecount = js.getString("filecount"); + agi.filesize = js.getLong("filesize"); + agi.expunged = js.getBoolean("expunged"); + agi.rating = Float.parseFloat(js.getString("rating")); + agi.torrentcount = js.getString("torrentcount"); + JSONArray tags = js.getJSONArray("tags"); + agi.apiTags = new String[tags.length()]; + for (int j = 0; j < tags.length(); j++) + agi.apiTags[j] = tags.getString(j); + return agi; + } + + public void getApiGalleryInfo(final Object checkFlag, final int[] gids, final String[] tokens, final OnGetApiGalleryInfoListener l) { + new BgThread() { + @Override + public void run() { + GetApiGalleryInfoResponder responder; + ApiGalleryInfo[] agiArray; + int requstLen; + // Create json + JSONObject json = new JSONObject(); + try { + json.put("method", "gdata"); + JSONArray jsonA = new JSONArray(); + requstLen = Math.min(gids.length, tokens.length); + agiArray = new ApiGalleryInfo[requstLen]; + for (int i = 0; i < requstLen; i++) { + JSONArray ja = new JSONArray(); + ja.put(gids[i]); + ja.put(tokens[i]); + jsonA.put(ja); + } + json.put("gidlist", jsonA); + } catch (Throwable e) { + responder = new GetApiGalleryInfoResponder(l, null, "Create json error"); + mHandler.post(responder); + return; + } + // Post + HttpHelper hh = new HttpHelper(mContext); + String body = hh.postJson(getApiUrl(), json); + if (body != null) { + try { + json = new JSONObject(body); + JSONArray jsonA = (JSONArray) json.get("gmetadata"); + for (int i = 0; i < jsonA.length(); i++) { + ApiGalleryInfo agi = Json2Agi(jsonA.getJSONObject(i)); + // Add to agiArray + for (int j = 0; j < requstLen; j++) { + if (agi.gid == gids[j]) + agiArray[j] = agi; + } + } + responder = new GetApiGalleryInfoResponder(l, checkFlag, agiArray); + } catch (Throwable e) { + responder = new GetApiGalleryInfoResponder(l, checkFlag, "Json error"); + } + } else { + responder = new GetApiGalleryInfoResponder(l, checkFlag, hh.getEMsg()); + } + mHandler.post(responder); + } + }.start(); + } + + public ApiGalleryInfo[] getApiGalleryInfo(int[] gids, String[] tokens) { + ApiGalleryInfo[] agiArray; + int requstLen; + // Create json + JSONObject json = new JSONObject(); + try { + json.put("method", "gdata"); + JSONArray jsonA = new JSONArray(); + requstLen = Math.min(gids.length, tokens.length); + agiArray = new ApiGalleryInfo[requstLen]; + for (int i = 0; i < requstLen; i++) { + JSONArray ja = new JSONArray(); + ja.put(gids[i]); + ja.put(tokens[i]); + jsonA.put(ja); + } + json.put("gidlist", jsonA); + } catch (Throwable e) { + return null; + } + // Post api + HttpHelper hh = new HttpHelper(mContext); + String body = hh.postJson(getApiUrl(), json); + if (body == null) { + return null; + } else { + try { + json = new JSONObject(body); + JSONArray jsonA = (JSONArray) json.get("gmetadata"); + for (int i = 0; i < jsonA.length(); i++) { + ApiGalleryInfo agi = Json2Agi(jsonA.getJSONObject(i)); + // Add to agiArray + for (int j = 0; j < requstLen; j++) { + if (agi.gid == gids[j]) + agiArray[j] = agi; + } + } + return agiArray; + } catch (Throwable e) { + return null; + } + } + } + +} diff --git a/app/src/main/java/com/hippo/ehviewer/ehclient/EhInfo.java b/app/src/main/java/com/hippo/ehviewer/ehclient/EhInfo.java new file mode 100644 index 000000000..5f23550cf --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ehclient/EhInfo.java @@ -0,0 +1,267 @@ +/* + * 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.ehclient; + +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; + +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.util.Config; +import com.hippo.ehviewer.util.Utils; +import com.larvalabs.svgandroid.SVGBuilder; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.util.List; + +/** + * + * @author Hippo + * + */ +public class EhInfo { + + private static final String TAG = EhInfo.class.getSimpleName(); + private static final String PREF_NAME = "eh_info"; + private static final String DIR_NAME = "file"; + private static final String AVATAR_NAME = "avatar.png"; + private static final Bitmap.CompressFormat AVATAR_FORMAT = Bitmap.CompressFormat.PNG; + private static Drawable DEFAULT_AVATAR; + + public static final String EX_HOST = "exhentai.org"; + public static final String[] COOKIABLE_HOSTS = {"exhentai.org", + "g.e-hentai.org", "forums.e-hentai.org", "ul.exhentai.org", + "ul.e-hentai.org", "lofi.e-hentai.org"}; + + private static final String KEY_LOGIN = "login"; + private static final boolean DEFAULT_LOGIN = false; + private static final String KEY_USERNAME = "username"; + private static final String KEY_DISPLAYNAME = "displayname"; + private static final String DEFAULT_NAME = "Hippo"; + private static final String KEY_MEMBER_ID = "ipb_member_id"; + private static final String DEFAULT_MEMBER_ID = null; + private static final String KEY_PASS_HASH = "ipb_pass_hash"; + private static final String DEFAULT_PASS_HASH = null; + + private final Context mContext; + private final SharedPreferences mInfoPref; + private String mUconfig; + private boolean mIsLogin; + private String mUsername; + private String mDisplayname; + private BitmapDrawable mAvatar; + private int mDefaultCat; + private String mPreviewMode; + private int mExculdeTagGroup; + private String mExculdeLanguage; + private static EhInfo sInstance; + + private BitmapDrawable getAvatarFromFile() { + File dir = mContext.getDir(DIR_NAME, 0); + File avatarFile = new File(dir, AVATAR_NAME); + if (!avatarFile.exists()) + return null; + + InputStream is = null; + try { + is = new FileInputStream(avatarFile); + return (BitmapDrawable) BitmapDrawable.createFromStream(is, null); + } catch (FileNotFoundException e) { + return null; + } finally { + Utils.closeQuietly(is); + } + } + + private EhInfo(final Context context){ + mContext = context; + mInfoPref = mContext.getSharedPreferences(PREF_NAME, 0); + if (DEFAULT_AVATAR == null) + DEFAULT_AVATAR = new SVGBuilder().readFromResource(context.getResources(), R.raw.default_avatar).build().getDrawable(); + + mIsLogin = mInfoPref.getBoolean(KEY_LOGIN, DEFAULT_LOGIN); + mUsername = mInfoPref.getString(KEY_USERNAME, DEFAULT_NAME); + mDisplayname = mInfoPref.getString(KEY_DISPLAYNAME, DEFAULT_NAME); + mAvatar = getAvatarFromFile(); + + mDefaultCat = Config.getDefaultCat(); + mPreviewMode = Config.getPreviewMode(); + mExculdeTagGroup = Config.getExculdeTagGroup(); + mExculdeLanguage = Config.getExculdeLanguage(); + updateUconfig(); + }; + + private String getUconfigString(String previewMode) { + return new StringBuilder().append("cats_").append(mDefaultCat) + .append("-ts_").append(previewMode == null ? mPreviewMode : previewMode) + .append("-xns_").append(mExculdeTagGroup) + .append("-xl_").append(mExculdeLanguage) + .append("-tl_m-uh_y-tr_2-prn_n-dm_l-ar_0-rc_0-rx_0-ry_0-sa_y-oi_n-qb_n-tf_n-hp_-hk_-ms_n-mt_n") + .toString(); + } + + private void updateUconfig() { + mUconfig = getUconfigString(null); + } + + public final static EhInfo getInstance(final Context context) { + if (sInstance == null) + sInstance = new EhInfo(context.getApplicationContext()); + return sInstance; + } + + private String getCookie(String cookieStr, String key) { + String value = null; + int index1 = -1; + int index2 = -1; + + if ((index1 = cookieStr.indexOf(key + "=")) != -1) { + index1 += key.length() + 1; // size of key + "=" + if ((index2 = cookieStr.indexOf(";", index1)) != -1) + value = cookieStr.substring(index1, index2); + else + value = cookieStr.substring(index1); + } + return value; + } + + public void storeCookie(HttpURLConnection conn) { + String ipb_member_id; + String ipb_pass_hash; + + List cookieList = conn.getHeaderFields().get("Set-Cookie"); + if (cookieList == null) + return; + SharedPreferences.Editor editor = mInfoPref.edit(); + for (String str : cookieList) { + ipb_member_id = getCookie(str, KEY_MEMBER_ID); + ipb_pass_hash = getCookie(str, KEY_PASS_HASH); + + if (ipb_member_id != null) + editor.putString(KEY_MEMBER_ID, ipb_member_id); + if (ipb_pass_hash != null) + editor.putString(KEY_PASS_HASH, ipb_pass_hash); + } + editor.apply(); + } + + public void setCookie(HttpURLConnection conn) { + setCookie(conn, null); + } + + public void setCookie(HttpURLConnection conn, String previewMode) { + StringBuilder sb = new StringBuilder(); + String id = mInfoPref.getString(KEY_MEMBER_ID, DEFAULT_MEMBER_ID); + String hash = mInfoPref.getString(KEY_PASS_HASH, DEFAULT_PASS_HASH); + sb.append("uconfig=").append(previewMode == null ? mUconfig : getUconfigString(previewMode)) + .append("; xres=").append(Config.getLofiResolution()); + if (id != null) + sb.append("; ipb_member_id=").append(id); + if (hash != null) + sb.append("; ipb_pass_hash=").append(hash); + // Fake + sb.append("; igneous="); + sb.append("; lv="); + conn.setRequestProperty("Cookie", sb.toString()); + } + + public boolean isLogin() { + return mIsLogin; + } + + public void login(String username, String displayname) { + mIsLogin = true; + mUsername = username; + mDisplayname = displayname; + + mInfoPref.edit().putBoolean(KEY_LOGIN, true) + .putString(KEY_USERNAME, username) + .putString(KEY_DISPLAYNAME, displayname).apply(); + } + + public void logout() { + mIsLogin = false; + // Remove avatar + if (mAvatar != null) { + mAvatar.getBitmap().recycle(); + mAvatar = null; + File dir = mContext.getDir(DIR_NAME, 0); + File avatarFile = new File(dir, AVATAR_NAME); + avatarFile.delete(); + } + + mInfoPref.edit().putBoolean(KEY_LOGIN, false) + .putString(KEY_MEMBER_ID, DEFAULT_MEMBER_ID) + .putString(KEY_PASS_HASH, DEFAULT_PASS_HASH).apply(); + } + + public String getUsername() { + return mUsername; + } + + public String getDisplayname() { + return mDisplayname; + } + + public Drawable getAvatar() { + return isLogin() && mAvatar != null ? mAvatar : DEFAULT_AVATAR; + } + + public void setAvatar(BitmapDrawable avatar) { + mAvatar = avatar; + + File dir = mContext.getDir(DIR_NAME, 0); + File avatarFile = new File(dir, AVATAR_NAME); + OutputStream os = null; + try { + os = new FileOutputStream(avatarFile); + avatar.getBitmap().compress(AVATAR_FORMAT, 100, os); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } finally { + Utils.closeQuietly(os); + } + } + + public void setDefaultCat(int defaultCat) { + mDefaultCat = defaultCat; + updateUconfig(); + } + + public void setPreviewMode(String previewMode) { + mPreviewMode = previewMode; + updateUconfig(); + } + + public void setExculdeTagGroup(int exculdeTagGroup) { + mExculdeTagGroup = exculdeTagGroup; + updateUconfig(); + } + + public void setExculdeLanguage(String exculdeLanguage) { + mExculdeLanguage = exculdeLanguage; + updateUconfig(); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/ehclient/ExDownloader.java b/app/src/main/java/com/hippo/ehviewer/ehclient/ExDownloader.java new file mode 100644 index 000000000..eef98b30a --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ehclient/ExDownloader.java @@ -0,0 +1,922 @@ +/* + * 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.ehclient; + +import java.io.BufferedInputStream; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.util.HashSet; +import java.util.List; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; + +import android.app.NotificationManager; +import android.content.Context; +import android.os.Process; +import android.support.v4.app.NotificationCompat; + +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.network.HttpHelper; +import com.hippo.ehviewer.util.AutoExpandArray; +import com.hippo.ehviewer.util.BgThread; +import com.hippo.ehviewer.util.Config; +import com.hippo.ehviewer.util.EhUtils; +import com.hippo.ehviewer.util.Log; +import com.hippo.ehviewer.util.Utils; + +// TODO what a mess +public final class ExDownloader implements Runnable { + + private static final String TAG = ExDownloader.class.getSimpleName(); + + private static final int IO_BUFFER_SIZE = 8 * 1024; + + private static final String[] URL_ARRAY_509 = new String[] { + "http://ehgt.org/g/509.gif", + "http://ehgt.org/g/509s.gif", + "http://st.exhentai.net/img/509.gif", + "http://st.exhentai.net/img/509s.gif" + }; + + private final Context mContext; + private final ExDownloaderManager mManager; + + private final int mGid; + private final String mToken; + private final String mTitle; + private final int mMode; + private volatile int mPreviewPageNum = -1; + private volatile int mPreviewPerPage = -1; + private volatile int mImageNum = -1; + private final AutoExpandArray mPageTokeArray = new AutoExpandArray(); + private final AutoExpandArray mImageFilenameArray = new AutoExpandArray(); + + private volatile int mOwnerNum = 0; + private Thread mMainThread = null; + /** Only work for EhClient.MODE_LOFI when do not know image number **/ + private int mCurMaxPreviewPage = -1; + + private volatile int mStartIndex = 0; + private final Queue mRequestIndexArray = new ConcurrentLinkedQueue(); + private final Queue mNoTokenIndexArray = new ConcurrentLinkedQueue(); + private final Queue mRequestPageIndexArray = new ConcurrentLinkedQueue(); + + /** The set contains the requsting index **/ + private final Set mRequstingIndexSet = new HashSet(); + + private volatile int mCurRequestPageIndex = -1; + + private final int mWorkerNum; + private final Worker[] mWorkerArray; + private final HttpHelper.DownloadControlor[] mControlorArray; + + private final Object mPageTokenLock = new Object(); + private final Object mWorkerLock = new Object(); + private final Object mWorkerGetIndexLock = new Object(); + + private volatile boolean mDownloadMode = false; + private volatile boolean mPauseWork = false; + private volatile boolean mStopWork = false; + + private final File mDir; + private ListenerForImageSet mLfis; + private ListenerForDownload mLfd; + + private static final long UPDATE_SPEED_INTERVAL = 500; + private volatile long mLastUpdateSpeedTime = 0; + private volatile long mAccumulateSize; + private volatile HashSet mDownloadIndexSet; + private volatile Object mDownloadLock = new Object(); + + private int mCurReadIndex = 0; + + /** + * Do no work in UI thread + * @author Hippo + */ + public interface ListenerForImageSet { + public void onDownloadStart(int index); + public void onDownloading(int index, float percent); + public void onDownloadComplete(int index); + public void onDownloadFail(int index); + } + + /** + * Do no work in UI thread + * @author Hippo + */ + public interface ListenerForDownload { + public void onStart(int gid); + public void onGetSum(int gid, int sum); + public void onDownload(int gid, int downloadSize, int totalSize); + public void onUpdateSpeed(int gid, int speed); + public void onDownloadOver(int gid, int legacy); + } + + ExDownloader(Context context, int gid, String token, String title, int mode) { + mContext = context; + mManager = ExDownloaderManager.getInstance(); + mGid = gid; + mToken = token; + mTitle = title; + mMode = mode; + + mWorkerNum = Config.getDownloadThread(); + mWorkerArray = new Worker[mWorkerNum]; + mControlorArray = new HttpHelper.DownloadControlor[mWorkerNum]; + + // Make sure dir + mDir = EhUtils.getGalleryDir(mGid, mTitle); + Utils.ensureDir(mDir, true); + } + + public void setListenerForImageSet(ListenerForImageSet l) { + mLfis = l; + } + + public void setListenerDownload(ListenerForDownload l) { + mLfd = l; + } + + void occupy() { + mOwnerNum++; + } + + void free() { + mOwnerNum--; + } + + boolean isOrphans() { + return mOwnerNum == 0; + } + + void stop() { + // TODO stop all thread, release all resources + mStopWork = true; + // Stop download + for (HttpHelper.DownloadControlor c : mControlorArray) { + if (c != null) + c.stop(); + } + // Wake all thread + synchronized(mPageTokenLock) {mPageTokenLock.notifyAll();} + synchronized(mWorkerGetIndexLock) {mWorkerGetIndexLock.notifyAll();} + synchronized(mWorkerLock) {mWorkerLock.notifyAll();} + } + + boolean pause(boolean pause) { + if (mOwnerNum > 1) + return false; + + mPauseWork = pause; + if (mPauseWork) { + // Stop download + for (HttpHelper.DownloadControlor c : mControlorArray) + if (c != null) c.stop(); + // Wake all thread + synchronized(mPageTokenLock) {mPageTokenLock.notifyAll();} + synchronized(mWorkerGetIndexLock) {mWorkerGetIndexLock.notifyAll();} + synchronized(mWorkerLock) {mWorkerLock.notifyAll();} + } else { + // Reset download + for (HttpHelper.DownloadControlor c : mControlorArray) + if (c != null) c.reset(); + ensureStart(); + ensureWorkers(); + } + return true; + } + + public int getGid() { + return mGid; + } + + public String getToken() { + return mToken; + } + + public String getTitle() { + return mTitle; + } + + public void setDownloadMode(boolean downloadMode) { + synchronized (mDownloadLock) { + if (downloadMode) { + setStartIndex(0); + if (mDownloadIndexSet == null) + mDownloadIndexSet = new HashSet(); + if (mDownloadMode && mLfd != null) + mLfd.onStart(mGid); + } else { + mDownloadIndexSet = null; + } + mDownloadMode = downloadMode; + } + } + + public void setStartIndex(int startIndex) { + if (mDownloadMode) { + addTargetIndex(startIndex); + } else { + if (mImageNum != -1 && startIndex >= mImageNum) + mStartIndex = 0; + else + mStartIndex = startIndex; + + ensureStart(); + ensureWorkers(); + } + } + + public void addTargetIndex(int index) { + if (mImageNum != -1 && index >= mImageNum) + return; + + mRequestIndexArray.offer(index); + ensureStart(); + ensureWorkers(); + } + + public void setCurReadIndex(final int index) { + new BgThread() { + @Override + public void run() { + writeCurReadIndex(index); + } + }.start(); + } + + public int getMaxEnsureIndex() { + if (mImageNum != -1) { + return mImageNum - 1; + } else { + return Math.max(Math.max(Math.max(mStartIndex, + Math.max(mCurMaxPreviewPage, mPreviewPageNum - 1) * mPreviewPerPage), + mPageTokeArray.maxValidIndex()), 0); + } + } + + /** + * If do not know image num, return -1. + * You should call getMaxEnsureIndex(). + * + * @return + */ + public int getImageNum() { + return mImageNum; + } + + public AutoExpandArray getImageFilenameArray() { + return mImageFilenameArray; + } + + /** + * If targer index image is downloading, return true + * + * @param index + * @return + */ + public boolean isDownloading(int index) { + return mRequstingIndexSet.contains(index); + } + + private synchronized void ensureStart() { + if (mMainThread != null) + return; + + mMainThread = new BgThread(this); + mMainThread.setPriority(Process.THREAD_PRIORITY_BACKGROUND); + mMainThread.start(); + } + + private File getExDownloadInfoFile() { + return new File(mDir, EhUtils.EH_DOWNLOAD_FILENAME); + } + + private boolean checkMode(int samlpe, int target) { + return samlpe <= EhClient.MODE_EX && target <= EhClient.MODE_EX + || samlpe == EhClient.MODE_LOFI && target == EhClient.MODE_LOFI; + } + + /** + * This function parser the download info file.
+ * The file look like this:
+ * + * 00000000
+ * 728874
+ * 306429c222
+ * 1
+ * 4
+ * 40
+ * 128
+ * 1 43a64e6e79
+ *

+ * Fist line is gid, a integer. + * Second line is token, a ten-character string. + * Third line is mode. + * Fourth line is preview page num. + * Fifth line is preview per page. + * + * @param ediFile + * @return + */ + private synchronized boolean parserEdiFile(File ediFile) { + if (!ediFile.exists() || !ediFile.isFile() || !ediFile.canRead() + || !ediFile.canWrite()) + return false; + + InputStream is = null; + try { + is = new BufferedInputStream(new FileInputStream(ediFile), + IO_BUFFER_SIZE); + // skip read index + Utils.readAsciiLine(is); + if (mGid != Integer.valueOf(Utils.readAsciiLine(is)) || + !mToken.equals(Utils.readAsciiLine(is))) + return false; + + if (!checkMode(Integer.valueOf(Utils.readAsciiLine(is)), mMode)) { + // If preview info is not same + // skip preview page num and preview per page + Utils.readAsciiLine(is); + Utils.readAsciiLine(is); + } else { + mPreviewPageNum = Integer.valueOf(Utils.readAsciiLine(is)); + mPreviewPerPage = Integer.valueOf(Utils.readAsciiLine(is)); + } + + mImageNum = Integer.parseInt(Utils.readAsciiLine(is)); + if (mImageNum != -1) { + mPageTokeArray.setCapacity(mImageNum); + mImageFilenameArray.setCapacity(mImageNum); + } + else if (mPreviewPageNum != -1 && mPreviewPerPage != -1) { + mPageTokeArray.setCapacity(mPreviewPageNum * mPreviewPerPage); + mImageFilenameArray.setCapacity(mPreviewPageNum * mPreviewPerPage); + } + + // read page token info + try { + while(true) { + String line = Utils.readAsciiLine(is); + int pos = line.indexOf(" "); + if (pos == -1 && line.length() - pos != 11) + continue; + try { + int index = Integer.parseInt(line.substring(0, pos)); + String token = line.substring(pos + 1); + mPageTokeArray.set(index, token); + } catch (NumberFormatException e) { + continue; + } + } + } catch (EOFException e) { + // Empty + } + return true; + } catch (Throwable e) { + // If read error + return false; + } finally { + Utils.closeQuietly(is); + } + } + + private synchronized void writeEdiFile(File ediFile) { + try { + FileWriter writer = new FileWriter(ediFile); + writer.write(String.format("%08x", mCurReadIndex)); + writer.write("\n"); + writer.write(String.valueOf(mGid)); + writer.write("\n"); + writer.write(mToken); + writer.write("\n"); + writer.write(mMode + '0'); + writer.write("\n"); + writer.write(String.valueOf(mPreviewPageNum)); + writer.write("\n"); + writer.write(String.valueOf(mPreviewPerPage)); + writer.write("\n"); + writer.write(String.valueOf(mImageNum)); + writer.write("\n"); + for (int i = 0; i < mPageTokeArray.length(); i++) { + String pageToken = mPageTokeArray.get(i); + if (pageToken != null) { + writer.write(String.valueOf(i)); + writer.write(" "); + writer.write(pageToken); + writer.write("\n"); + } + } + writer.flush(); + writer.close(); + } catch (IOException e) {} + } + + private synchronized void writeCurReadIndex(int index) { + mCurReadIndex = index; + try { + RandomAccessFile raf = new RandomAccessFile(getExDownloadInfoFile(), "rw"); + raf.write(String.format("%08x", mCurReadIndex).getBytes()); + raf.close(); + } catch (Throwable e) {} + } + + public static int readCurReadIndex(int gid, String title) { + try { + RandomAccessFile raf = new RandomAccessFile(new File(EhUtils.getGalleryDir(gid, title), + EhUtils.EH_DOWNLOAD_FILENAME), "r"); + int index = Integer.parseInt(raf.readLine(), 16); + raf.close(); + return index; + } catch (Throwable e) { + return 0; + } + } + + private void getDetailInfo(int pageIndex, File ediFile, boolean needPreviewInfo) throws Exception { + HttpHelper hh = new HttpHelper(mContext); + EdDetailParser edp = new EdDetailParser(); + String url = EhClient.getDetailUrl(mGid, mToken, pageIndex, mMode); + hh.setPreviewMode("m"); + String body = hh.get(url); + if (body == null) + throw new Exception(hh.getEMsg() != null ? hh.getEMsg() : "Http error, body == null"); + if (!edp.parser(body, mMode, needPreviewInfo)) + throw new Exception(edp.emsg != null ? edp.emsg : "Parser error, body is \n" + body); + if (edp.previewStartIndex != pageIndex * mPreviewPerPage) + throw new Exception("预测与实际不匹配, 预测: " + edp.previewStartIndex + + ", 实际: " + pageIndex * mPreviewPerPage + ", body is \n" + body); + + List pageTokenArray = edp.pageTokenArray; + if (needPreviewInfo) + mPreviewPerPage = pageTokenArray.size(); + if (mMode == EhClient.MODE_LOFI) { + // update mCurMaxPreviewPage + if (edp.isLastPage) { + mPreviewPageNum = pageIndex + 1; + mImageNum = pageIndex * mPreviewPerPage + pageTokenArray.size(); + } else if (!edp.isLastPage && mPreviewPageNum == -1) { + mCurMaxPreviewPage = Math.max(mCurMaxPreviewPage, pageIndex + 1); + } + } else { + if (needPreviewInfo) { + mPreviewPageNum = edp.previewPageNum; + mImageNum = edp.imageNum; + mPageTokeArray.setCapacity(mPreviewPageNum * mPreviewPerPage); + mImageFilenameArray.setCapacity(mPreviewPageNum * mPreviewPerPage); + } + } + for (int i = 0; i < pageTokenArray.size(); i++) + mPageTokeArray.set(i + edp.previewStartIndex, pageTokenArray.get(i)); + + writeEdiFile(ediFile); + } + + private void ensureWorkers() { + if (mPreviewPerPage == -1) + return; + + synchronized(mWorkerArray) { + for (int i = 0; i < mWorkerArray.length; i++) { + if (mWorkerArray[i] == null) { + Worker worker = new Worker(i); + worker.start(); + mWorkerArray[i] = worker; + mControlorArray[i] = new HttpHelper.DownloadControlor(); + } + } + } + } + + private void updateDownload(int index) { + synchronized (mDownloadLock) { + if (mDownloadIndexSet != null && !mDownloadIndexSet.contains(index)) { + mDownloadIndexSet.add(index); + if (mDownloadMode && mLfd != null) + mLfd.onDownload(mGid, mDownloadIndexSet.size(), getMaxEnsureIndex() + 1); + } + } + } + + private static final int BE_NOTIFY_ID = -3; + + private static final void show509Notification(Context context) { + NotificationManager notifyManager = (NotificationManager) context + .getSystemService(Context.NOTIFICATION_SERVICE); + NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context); + mBuilder.setSmallIcon(R.drawable.ic_stat_eh).setContentTitle(context.getString(R.string.notification_be_title)) + .setContentText(context.getString(R.string.notification_be_mesg)) + .setOngoing(false).setAutoCancel(true); + notifyManager.notify(BE_NOTIFY_ID, mBuilder.build()); + } + + // TODO for download , when to claim download completed + @Override + public void run() { + // Try to get info from file + try { + File ediFile = getExDownloadInfoFile(); + if (!parserEdiFile(ediFile) || mPreviewPerPage == -1 || + ((mMode == EhClient.MODE_EX || mMode == EhClient.MODE_G) && mImageNum == -1)) { + getDetailInfo(0, ediFile, true); + } + + if (mLfd != null) + mLfd.onGetSum(mGid, mImageNum); + + ensureWorkers(); + + // A loop to get page token + while (true) { + // Check stop + if (mStopWork) + break; + + int noTokenIndex = -1; + int pageIndex = -1; + synchronized(mPageTokenLock) { + if (mNoTokenIndexArray.isEmpty()) { + if (mRequestPageIndexArray.isEmpty()) { + try { + mPageTokenLock.wait(); + } catch (InterruptedException e) {} + continue; + } else { + pageIndex = mRequestPageIndexArray.poll(); + mCurRequestPageIndex = pageIndex; + } + } else { + noTokenIndex = mNoTokenIndexArray.poll(); + } + } + + if (noTokenIndex != -1) { + if (mPageTokeArray.get(noTokenIndex) == null) { + getDetailInfo(noTokenIndex / mPreviewPerPage, ediFile, false); + } else { + // The token is already got, no need to re + } + } else if (noTokenIndex == -1){ + getDetailInfo(pageIndex, ediFile, false); + mCurRequestPageIndex = -1; + } + + ensureWorkers(); + + synchronized(mWorkerGetIndexLock) {mWorkerGetIndexLock.notifyAll();} + synchronized(mWorkerLock) {mWorkerLock.notifyAll();} + } + } catch (Exception e) { + + Log.f(e); + + synchronized (mDownloadLock) { + if (mDownloadMode && mLfd != null && mDownloadIndexSet != null) { + mLfd.onDownloadOver(mGid, + getMaxEnsureIndex() + 1 - mDownloadIndexSet.size()); + } + } + + } finally { + synchronized (this) { + mMainThread = null; + } + } + + Log.d(TAG, "ExDownloader over"); + } + + private class Worker extends BgThread { + + private final int mIndex; + + public Worker(int index) { + super(); + mIndex = index; + setPriority(Process.THREAD_PRIORITY_BACKGROUND); + } + + private void addNoTokenIndex(int index) { + synchronized(mPageTokenLock) { + mNoTokenIndexArray.offer(index); + mPageTokenLock.notify(); + } + } + + private void addRequstPageIndex(int pageIndex) { + synchronized(mPageTokenLock) { + if (mCurRequestPageIndex != pageIndex && + !mRequestPageIndexArray.contains(pageIndex)) { + mRequestPageIndexArray.offer(pageIndex); + mPageTokenLock.notify(); + } + } + } + + private int getTargetIndex() { + synchronized(mWorkerGetIndexLock) { + while (true) { + + // Check stop + if (mStopWork || mPauseWork) + return -1; + + if (!mRequestIndexArray.isEmpty()) { + return mRequestIndexArray.poll(); + } else if (mImageNum != -1 && mStartIndex == mImageNum) { + return -1; + } else if (mImageNum == -1 && mStartIndex > getMaxEnsureIndex()) { + // If do not sure the index is valid + + Log.d(TAG, "mPreviewPerPage = " + mPreviewPerPage); + + addRequstPageIndex(getMaxEnsureIndex() / mPreviewPerPage); + try { + mWorkerGetIndexLock.wait(); + } catch (InterruptedException e) {} + } else { + return mStartIndex++; + } + } + } + } + + // TODO what if download failed ? what to do ? + @Override + public void run() { + HttpDownloadListener listener = new HttpDownloadListener(); + HttpHelper hh = new HttpHelper(mContext); + ImagePageParser ipp = new ImagePageParser(); + + while (true) { + // Check stop + if (mStopWork || mPauseWork) + break; + + int targetIndex = getTargetIndex(); + if (targetIndex == -1) + break; + + // Check is the index is requsted + if (mRequstingIndexSet.contains(targetIndex)) + continue; + + // Check is this already downloaded + boolean isAlreadyDownloaded = false; + // First check mImageFilenameArray + String imageFilename = mImageFilenameArray.get(targetIndex); + if (imageFilename != null) { + File file = new File(mDir, imageFilename); + if (file.exists()) + isAlreadyDownloaded = true; + } else { + // Can not find image file name, just guess + for (String possibleFilename : EhUtils.getPossibleImageFilenames(targetIndex)) { + File file = new File(mDir, possibleFilename); + if (file.exists()) { + isAlreadyDownloaded = true; + mImageFilenameArray.set(targetIndex, possibleFilename); + break; + } + } + } + + if (isAlreadyDownloaded) { + Log.d(TAG, "isAlreadyDownloaded"); + updateDownload(targetIndex); + continue; + } + + mRequstingIndexSet.add(targetIndex); + + listener.index = targetIndex; + + String pageToken = null; + while (true) { + // Check stop + if (mStopWork || mPauseWork) + break; + + pageToken = mPageTokeArray.get(targetIndex); + if (pageToken == null) { + addNoTokenIndex(targetIndex); + synchronized(mWorkerLock) { + try {mWorkerLock.wait();} + catch (InterruptedException e) {} + } + } else { + // Get valid page token, jump out of loop + break; + } + } + + // Parser image page + for (int i = 0; i < 2 && !mStopWork && !mPauseWork; i++) { + hh.reset(); + ipp.reset(); + String body = hh.get(EhClient.getPageUrl(mGid, pageToken, targetIndex + 1, mMode) + + ((i == 1) ? "?nl=48" : "")); + if (ipp.parser(body, mMode)) { + // Download image + HttpHelper.DownloadControlor c = mControlorArray[mIndex]; + + if (Config.getDownloadOriginImage() && ipp.originalImageUrl != null) { + + Log.d(TAG, "Download origin image"); + + // Download orgin image + String imageUrl = ipp.originalImageUrl; + // Guess filename + String filename = EhUtils.getImageFilename(targetIndex, "jpg"); + // Download + hh.reset(); + if (hh.downloadOriginEhImage(imageUrl, mDir, filename, false, c, listener) == null) { + if (hh.getException() instanceof HttpHelper.BandwidthExceededException) { + // Get 509 + show509Notification(mContext); + break; + } else { + String newImageUrl = hh.getLastUrl(); + if (newImageUrl != null) { + // Try download from proxy + hh.reset(); + if (hh.downloadEhImage(newImageUrl, mDir, filename, true, c, listener) == null) { + // Download failed + Log.d(TAG, "Download failed"); + } else { + // Download successful + Log.d(TAG, "Download successful"); + } + } else { + // Download failed + Log.d(TAG, "Download failed"); + } + } + } else { + // Download successful + Log.d(TAG, "Download successful"); + } + + // No need to refresh image url + break; + } else { + // Download scale image + String imageUrl = ipp.imageUrl; + + //Check 509 gif + boolean get509 = false; + int length = URL_ARRAY_509.length; + for (int j = 0; j < length; j++) { + if (URL_ARRAY_509[j].equals(imageUrl)) { + // Get 509 gif here + get509 = true; + break; + } + } + if (get509) { + show509Notification(mContext); + break; + } + + // Guess filename + String filename = EhUtils.getImageFilename(targetIndex, Utils.getExtension(imageUrl, "jpg")); + // Don't put into mImageFilenameArray, because extension maybe be wrong + // mImageFilenameArray.set(targetIndex, filename); + hh.reset(); + if (hh.downloadEhImage(imageUrl, mDir, filename, false, c, listener) == null) { + hh.reset(); + if (hh.downloadEhImage(imageUrl, mDir, filename, true, c, listener) == null) { + if (i == 1) { + // TODO download error + Log.d(TAG, "download error"); + } + } else { + // download ok + Log.d(TAG, "download ok proxy"); + break; + } + } else { + // download ok + Log.d(TAG, "download ok"); + break; + } + } + } else { + // TODO parser error, Do somthing + Log.d(TAG, "parser error"); + break; + } + } + + // Remove from requsting set + mRequstingIndexSet.remove(targetIndex); + } + + // Worker over + synchronized(mWorkerArray) { + // Remove from mWorkerArray + mWorkerArray[mIndex] = null; + mControlorArray[mIndex] = null; + + boolean allNull = true; + for (Worker w : mWorkerArray) + if (w != null) {allNull = false; break;} + + synchronized (mDownloadLock) { + if (allNull && mDownloadMode && mLfd != null && mDownloadIndexSet != null) { + mLfd.onDownloadOver(mGid, + getMaxEnsureIndex() + 1 - mDownloadIndexSet.size()); + } + } + } + Log.d(TAG, "ExDownloader Worker over"); + } + } + + public class HttpDownloadListener implements HttpHelper.OnDownloadListener { + + public int index; + private float lastPercent; + private int lastDownloadSize; + + @Override + public void onDownloadStartConnect() { + lastPercent = 0.0f; + lastDownloadSize = 0; + + if (mLfis != null) + mLfis.onDownloadStart(index); + } + + @Override + public void onDownloadStartDownload(int totalSize) { + lastPercent = 0.0f; + lastDownloadSize = 0; + } + + @Override + public void onDownloadStatusUpdate(int downloadSize, int totalSize) { + if (totalSize == -1) + return; + + float percent = (float)downloadSize / totalSize; + if (percent - lastPercent >= 0.05f) { + lastPercent = percent; + + if (mLfis != null) + mLfis.onDownloading(index, percent); + } + + // Update download size + int accumulateSize = downloadSize - lastDownloadSize; + lastDownloadSize = downloadSize; + mAccumulateSize += accumulateSize; + long curTime = System.currentTimeMillis(); + long interval = curTime - mLastUpdateSpeedTime; + if (interval > UPDATE_SPEED_INTERVAL) { + if (mDownloadMode && mLfd != null) + mLfd.onUpdateSpeed(mGid, (int)(mAccumulateSize * 1000 / interval)); + mLastUpdateSpeedTime = curTime; + mAccumulateSize = 0; + } + } + + @Override + public void onDownloadOver(int status, String eMsg) { + if (status == HttpHelper.DOWNLOAD_OK_CODE) { + mRequstingIndexSet.remove(index); + + updateDownload(index); + } + + if (mLfis != null && status == HttpHelper.DOWNLOAD_OK_CODE) + mLfis.onDownloadComplete(index); + else if (mLfis != null && status == HttpHelper.DOWNLOAD_FAIL_CODE) + mLfis.onDownloadFail(index); + + lastPercent = 0.0f; + lastDownloadSize = 0; + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/ehclient/ExDownloaderManager.java b/app/src/main/java/com/hippo/ehviewer/ehclient/ExDownloaderManager.java new file mode 100644 index 000000000..d4c5dc290 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ehclient/ExDownloaderManager.java @@ -0,0 +1,61 @@ +/* + * 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.ehclient; + +import android.content.Context; +import android.util.SparseArray; + +public class ExDownloaderManager { + + private static final String TAG = ExDownloaderManager.class.getSimpleName(); + + private final Context mContext; + private final SparseArray mExDownloadList; + + private static ExDownloaderManager sInstance; + + public static void createInstance(Context context) { + sInstance = new ExDownloaderManager(context.getApplicationContext()); + } + + public static ExDownloaderManager getInstance() { + return sInstance; + } + + private ExDownloaderManager(Context context) { + mContext = context; + mExDownloadList = new SparseArray(); + } + + public synchronized ExDownloader getExDownloader(int gid, String token, String title, int mode) { + ExDownloader exDownloader = mExDownloadList.get(gid); + if (exDownloader == null) { + exDownloader = new ExDownloader(mContext, gid, token, title, mode); + mExDownloadList.append(gid, exDownloader); + } + exDownloader.occupy(); + return exDownloader; + } + + public synchronized void freeExDownloader(ExDownloader exDownloader) { + exDownloader.free(); + if (exDownloader.isOrphans()) { + exDownloader.stop(); + mExDownloadList.remove(exDownloader.getGid()); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/ehclient/ImagePageParser.java b/app/src/main/java/com/hippo/ehviewer/ehclient/ImagePageParser.java new file mode 100644 index 000000000..16b13def7 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ehclient/ImagePageParser.java @@ -0,0 +1,85 @@ +/* + * 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.ehclient; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.hippo.ehviewer.util.Utils; + +public class ImagePageParser { + + public String imageUrl; + public String originalImageUrl; + public int gid; + public String token; + + public void reset() { + imageUrl = null; + originalImageUrl = null; + gid = 0; + token = null; + } + + public boolean parser(String body, int mode) { + Pattern p; + Matcher m; + + if (body == null) + return false; + + if (mode ==EhClient.MODE_LOFI) { + p = Pattern.compile("]+>"); + m = p.matcher(body); + if (m.find()) + imageUrl = Utils.unescapeXml(m.group(1)); + else + return false; + + p = Pattern.compile("
Back"); + m = p.matcher(body); + if (m.find()) { + gid = Integer.parseInt(m.group(1)); + token = m.group(2); + return true; + } else { + return false; + } + + } else { + p = Pattern.compile("
.+?
"); + m = p.matcher(body); + if (m.find()) { + originalImageUrl = Utils.unescapeXml( + new StringBuilder(m.group(1)).append("fullimg.php").append(m.group(2)).toString()); + } + return true; + } else { + return false; + } + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/ehclient/ListParser.java b/app/src/main/java/com/hippo/ehviewer/ehclient/ListParser.java new file mode 100644 index 000000000..a48bf7df9 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ehclient/ListParser.java @@ -0,0 +1,230 @@ +/* + * 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.ehclient; + +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.hippo.ehviewer.data.GalleryInfo; +import com.hippo.ehviewer.data.LofiGalleryInfo; +import com.hippo.ehviewer.util.EhUtils; +import com.hippo.ehviewer.util.Utils; + +public class ListParser { + + public static final int TARGET_PAGE_IS_LAST = -1; + + public static final int ALL = 0x1; + public static final int NOT_FOUND = 0x2; + public static final int PARSER_ERROR = -1; + public static final int INDEX_ERROR = -2; + + public int pageNum; + + public ArrayList giList; + + /** + * If NOT_FOUND maxPage is 0, + * if can not get maxPage, set it Integer.MAX_VALUE + * + * @param pageContext + * @param mode + * @return + */ + public int parser(String body, int mode) { + + Pattern p; + Matcher m; + + if (body == null) + return 0; + + giList = new ArrayList(25); + + switch (mode) { + case EhClient.MODE_LOFI: + p = Pattern.compile("" // detail url + + "" // thumb url + + ".+?(.+?)" // title + + ".+?Posted:(.+?)" // Posted and uploader + + "Category:(.+?)" // Category + + "Tags:(.+?)" // Tags + + "Rating:(.+?)"); // rating + m = p.matcher(body); + DetailUrlParser dup = new DetailUrlParser(); + while (m.find()) { + LofiGalleryInfo lgi = new LofiGalleryInfo(); + String[] pau = new String[2]; + + if (dup.parser(m.group(1))) { + lgi.gid = dup.gid; + lgi.token = dup.token; + } else { + continue; + } + lgi.thumb = Utils.unescapeXml(m.group(2)); + lgi.title = Utils.unescapeXml(m.group(3)); + if (getPostedAndUploader(m.group(4), pau)) { + lgi.posted = pau[0]; + lgi.uploader = pau[1]; + } else { + continue; + } + lgi.category = EhUtils.getCategory(m.group(5)); + String tags = m.group(6); + if (tags.equals("-")) + lgi.lofiTags = null; + else + lgi.lofiTags = tags.split(", "); + String rating = m.group(7); + if (rating.equals("-")) + lgi.rating = Float.NaN; + else + lgi.rating = getStartNum(rating); + lgi.generateSLang(); + + giList.add(lgi); + } + + if (giList.size() == 0) { + if (body.contains("No hits found
")) { + pageNum = 0; + return NOT_FOUND; + } else if (body.contains("No more hits found
")) { + return INDEX_ERROR; + } else { + return PARSER_ERROR; + } + } else { + if (!body.contains("Next Page >")) + pageNum = TARGET_PAGE_IS_LAST; + else + pageNum = Integer.MAX_VALUE; + return ALL; + } + + case EhClient.MODE_G: + case EhClient.MODE_EX: + default: + p = Pattern.compile("]+>([\\d]+)]+>(?:]+>)?&"); + m = p.matcher(body); + if (m.find()) { + pageNum = Integer.parseInt(m.group(1)); + } else if (body.contains("No hits found

")) { + pageNum = 0; + return NOT_FOUND; + } else { + return PARSER_ERROR; + } + + p = Pattern.compile("(?:)?(?:)?" // category + + "(.+?)" // posted + + "" + + "(?:" + + "|init~([^<>\"~]+~[^<>\"~]+)~([^<>]+))" // thumb and title + + "
" + + ".+?" + + "" // url and title + + ".+?" + + "
\"]+)\">" // rating + + ".+?" + + "
(.+?)"); // uploader + m = p.matcher(body); + while (m.find()) { + GalleryInfo gi = new GalleryInfo(); + + gi.category = EhUtils.getCategory(m.group(1)); + gi.posted = m.group(2); + gi.thumbHeight = Integer.parseInt(m.group(3)); + gi.thumbWidth = Integer.parseInt(m.group(4)); + + if (m.group(5) == null) { + gi.thumb = Utils.unescapeXml("http://" + + m.group(7).replace('~', '/')); + gi.title = Utils.unescapeXml(m.group(8)); + } else { + gi.thumb = Utils.unescapeXml(m.group(5)); + gi.title = Utils.unescapeXml(m.group(6)); + } + + Pattern pattern = Pattern + .compile("/(\\d+)/(\\w+)"); + Matcher matcher = pattern.matcher(m.group(9)); + if (matcher.find()) { + gi.gid = Integer.parseInt(matcher.group(1)); + gi.token = matcher.group(2); + } else + continue; + + gi.rating = Float.parseFloat(getRate(m.group(11))); + gi.uploader = m.group(12); + gi.generateSLang(); + + giList.add(gi); + } + return ALL; + } + } + + private String getRate(String rawRate) { + Pattern p = Pattern.compile("\\d+px"); + Matcher m = p.matcher(rawRate); + int num1; + int num2; + int rate = 5; + String re; + if (m.find()) + num1 = Integer.parseInt(m.group().replace("px", "")); + else + return null; + if (m.find()) + num2 = Integer.parseInt(m.group().replace("px", "")); + else + return null; + rate = rate - num1 / 16; + if (num2 == 21) { + rate--; + re = Integer.toString(rate); + re = re + ".5"; + } else + re = Integer.toString(rate); + return re; + } + + private static final String PAU_SPACER = " by "; + + private boolean getPostedAndUploader(String raw, String[] pau) { + int index = raw.indexOf(PAU_SPACER); + if (index == -1) { + return false; + } else { + pau[0] = raw.substring(0, index); + pau[1] = raw.substring(index + PAU_SPACER.length()); + return true; + } + } + + private int getStartNum(String str) { + int startNum = 0; + for (int i = 0; i < str.length(); i++) { + if (str.charAt(i) == '*') + startNum++; + } + return startNum; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/ehclient/LofiDetailParser.java b/app/src/main/java/com/hippo/ehviewer/ehclient/LofiDetailParser.java new file mode 100644 index 000000000..5c9848cb9 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ehclient/LofiDetailParser.java @@ -0,0 +1,56 @@ +/* + * 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.ehclient; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.hippo.ehviewer.data.LargePreviewList; + + +public class LofiDetailParser { + + public boolean isLastPage; + public LargePreviewList preview; + public String emesg; + + public boolean parser(String body) { + Pattern p; + Matcher m; + + if (body == null) + return false; + + // Get preview + p = Pattern.compile("
]+>]+href=\"([^>\"]+)\"[^>]+>]+src=\"([^>\"]+)\"[^>]+>"); + m = p.matcher(body); + while (m.find()) { + if (preview == null) + preview = new LargePreviewList(); + preview.addItem(m.group(2), m.group(1)); + } + // Check is last page + p = Pattern.compile("]+>Next Page >"); + m = p.matcher(body); + if (m.find()) + isLastPage = false; + else + isLastPage = true; + + return preview != null; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/ehclient/LoginParser.java b/app/src/main/java/com/hippo/ehviewer/ehclient/LoginParser.java new file mode 100644 index 000000000..8877a310c --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ehclient/LoginParser.java @@ -0,0 +1,51 @@ +/* + * 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.ehclient; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class LoginParser { + public String displayname; + public String eMesg; + + public boolean parser(String body) { + Pattern p; + Matcher m; + + if (body == null) + return false; + + p = Pattern.compile("

You are now logged in as: (.+?)
"); + m = p.matcher(body); + if (m.find()) { + displayname = m.group(1); + return true; + } else { + p = Pattern.compile("(?:

The error returned was:

\\s*

(.+?)

)" + + "|(?:(.+?))"); + m = p.matcher(body); + + if (m.find()) + eMesg = m.group(1) == null ? m.group(2) : m.group(1); + else + eMesg = "Unknown error"; + + return false; + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/ehclient/RateParser.java b/app/src/main/java/com/hippo/ehviewer/ehclient/RateParser.java new file mode 100644 index 000000000..83a82832a --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ehclient/RateParser.java @@ -0,0 +1,40 @@ +/* + * 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.ehclient; + +import org.json.JSONException; +import org.json.JSONObject; + +public class RateParser { + + public float mRatingAvg; + public int mRatingCnt; + + boolean parser(String body) { + if (body == null) + return false; + + try { + JSONObject jsonObject = new JSONObject(body); + mRatingAvg = (float)jsonObject.getDouble("rating_avg"); + mRatingCnt = jsonObject.getInt("rating_cnt"); + return true; + } catch (JSONException e) { + return false; + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/ehclient/VoteParser.java b/app/src/main/java/com/hippo/ehviewer/ehclient/VoteParser.java new file mode 100644 index 000000000..4de217429 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ehclient/VoteParser.java @@ -0,0 +1,38 @@ +/* + * 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.ehclient; + +import org.json.JSONException; +import org.json.JSONObject; + +public class VoteParser { + + public String mTagPane; + + boolean parser(String body) { + if (body == null) + return false; + + try { + JSONObject jsonObject = new JSONObject(body); + mTagPane = jsonObject.getString("tagpane"); + return true; + } catch (JSONException e) { + return false; + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/exception/FileException.java b/app/src/main/java/com/hippo/ehviewer/exception/FileException.java new file mode 100644 index 000000000..f021b8011 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/exception/FileException.java @@ -0,0 +1,64 @@ +/* + * 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.exception; + +public class FileException extends Exception { + + public static final int MKDIR_ERROR = 0x0; + public static final int CREATE_FILE_ERROR = 0x1; + public static final int NOT_DIR = 0x2; + public static final int NOT_FILE = 0x3; + public static final int CANNOT_EXECUTE = 0x4; + public static final int CANNOT_READ = 0x5; + public static final int CANNOT_WRITE = 0x6; + + private int mErrorCode; + + private static final long serialVersionUID = 1L; + + public FileException(int errorCode) { + mErrorCode = errorCode; + } + + public int getErrorCode() { + return mErrorCode; + } + + + // TODO + public String getMessage() { + switch (mErrorCode) { + case MKDIR_ERROR: + return "mkdir error"; + case CREATE_FILE_ERROR: + return "create file error"; + case NOT_DIR: + return "it is not a dir"; + case NOT_FILE: + return "it is not a file"; + case CANNOT_EXECUTE: + return "can not execute"; + case CANNOT_READ: + return "can not read"; + case CANNOT_WRITE: + return "can not write"; + default: + return "unknown error"; + } + } + +} diff --git a/app/src/main/java/com/hippo/ehviewer/exception/NoSdCardException.java b/app/src/main/java/com/hippo/ehviewer/exception/NoSdCardException.java new file mode 100644 index 000000000..a2354de30 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/exception/NoSdCardException.java @@ -0,0 +1,21 @@ +/* + * 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.exception; + +public class NoSdCardException extends Exception { + private static final long serialVersionUID = 1L; +} diff --git a/app/src/main/java/com/hippo/ehviewer/exception/ParserException.java b/app/src/main/java/com/hippo/ehviewer/exception/ParserException.java new file mode 100644 index 000000000..53ed894ae --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/exception/ParserException.java @@ -0,0 +1,23 @@ +/* + * 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.exception; + +public class ParserException extends Exception{ + + private static final long serialVersionUID = 1L; + +} diff --git a/app/src/main/java/com/hippo/ehviewer/exception/StopRequestException.java b/app/src/main/java/com/hippo/ehviewer/exception/StopRequestException.java new file mode 100644 index 000000000..055f35a01 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/exception/StopRequestException.java @@ -0,0 +1,21 @@ +/* + * 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.exception; + +public class StopRequestException extends Exception { + private static final long serialVersionUID = 1L; +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/GalleryView.java b/app/src/main/java/com/hippo/ehviewer/gallery/GalleryView.java new file mode 100644 index 000000000..21d4a4585 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/GalleryView.java @@ -0,0 +1,1969 @@ +/* + * 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.gallery; + +import java.util.Arrays; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Movie; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.RectF; +import android.view.MotionEvent; + +import com.hippo.ehviewer.AppHandler; +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.gallery.data.ImageSet; +import com.hippo.ehviewer.gallery.glrenderer.BasicTexture; +import com.hippo.ehviewer.gallery.glrenderer.BitmapTexture; +import com.hippo.ehviewer.gallery.glrenderer.GLCanvas; +import com.hippo.ehviewer.gallery.glrenderer.GLPaint; +import com.hippo.ehviewer.gallery.glrenderer.ImageTexture; +import com.hippo.ehviewer.gallery.glrenderer.MovieTexture; +import com.hippo.ehviewer.gallery.glrenderer.StringTexture; +import com.hippo.ehviewer.gallery.glrenderer.Uploaded; +import com.hippo.ehviewer.gallery.image.Image; +import com.hippo.ehviewer.gallery.ui.GLRoot; +import com.hippo.ehviewer.gallery.ui.GLView; +import com.hippo.ehviewer.gallery.ui.GestureRecognizer; +import com.hippo.ehviewer.gallery.ui.SynchronizedHandler; +import com.hippo.ehviewer.gallery.ui.TimeRunner; +import com.hippo.ehviewer.gallery.util.GalleryUtils; +import com.hippo.ehviewer.util.Config; +import com.hippo.ehviewer.util.Constants; +import com.hippo.ehviewer.util.MathUtils; +import com.hippo.ehviewer.util.Ui; +import com.hippo.ehviewer.util.Utils; +import com.hippo.ehviewer.util.ZInterpolator; +import com.hippo.ehviewer.widget.MaterialToast; + +public class GalleryView extends GLView implements ImageSet.ImageListener, + Runnable { + @SuppressWarnings("unused") + private static final String TAG = GalleryView.class.getSimpleName(); + + public static final int INVALID_ID = -1; + + private static final int STATE_FIRST = 0x1; + private static final int STATE_LAST = 0x2; + private static final int STATE_NONE = 0x0; + + // scale mode + private static final int ORGIN = 0; + private static final int FIT_WIDTH = 1; + private static final int FIT_HEIGHT = 2; + private static final int FIT = 3; + private static final int FIXED = 4; + + // Reading direction + private static final int DIRECTION_LEFT_RIGHT = 0; + private static final int DIRECTION_RIGHT_LEFT = 1; + private static final int DIRECTION_TOP_BOTTOM = 2; + private static final int DIRECTION_BOTTOM_TOP = 3; + + // start position mode + private static final int TOP_LEFT = 0; + private static final int TOP_RIGHT = 1; + private static final int BOTTOM_LEFT = 2; + private static final int BOTTOM_RIGHT = 3; + private static final int CENTER = 4; + + // Scroll state + private static final int SCROLL_NONE = 0; + private static final int SCROLL_PRE = 1; + private static final int SCROLL_NEXT = 2; + private static final int SCROLL_ANIME_PRE = 3; + private static final int SCROLL_ANIME_NEXT = 4; + + private static final int PRE_TARGET_INDEX = 0; + private static final int CUR_TARGET_INDEX = 1; + private static final int NEXT_TARGET_INDEX = 2; + private static final int TARGET_INDEX_SIZE = 3; + + // Area + private static final float[] LEFT_AREA_SCALE = { 0, 0, 2 / 7.0f, 1 }; + private static final float[] TOP_AREA_SCALE = { 2 / 7.0f, 0, 5 / 7.0f, + 3 / 8.0f }; + private static final float[] RIGHT_AREA_SCALE = { 5 / 7.0f, 0, 1, 1 }; + private static final float[] BOTTOM_AREA_SCALE = { 2 / 7.0f, 5 / 8.0f, + 5 / 7.0f, 1 }; + private static final float[] CENTER_AREA_SCALE = { 2 / 7.0f, 3 / 8.0f, + 5 / 7.0f, 5 / 8.0f }; + private static final float[] LIGHTNESS_SLIDING_AREA = { + 0.0f, 0.0f, 0.166f, 1.0f }; + + private static final float MILLSEC_PER_DIX = 0.2f; + private static final float CHANGE_PAGE_PROPORTION = 0.1f; + private static final float BORDER_TIP_SENSITIVITY = Ui.dp2pix(48); + + private static final int ANIMATE_MIN = 200; + + // TODO 根据图片的大小调整缩放界限 + private static final float SCALE_MIN = 1 / 10.0f; + private static final float SCALE_MAX = 10.0f; + + private static final float LINE_WIDTH = Ui.dp2pix(3); + private static final int LINE_COLOR = -1; // White + private static final int TAP_AREA_TEXT_COLOR = -1; // White + private static final int TAP_AREA_TEXT_SIZE = Ui.dp2pix(24); + private static final int TAP_AREA_MASK_COLOR = 0x88000000; + + private static final int BACKGROUND_COLOR = 0xff212121; + @SuppressWarnings("unused") + private static final int MASK_COLOR = 0x88000000; + + private GestureRecognizer mGestureRecognizer; + private final Context mContext; + + private final ImageSet mImageSet; + private int mState; + + private int mReadingDirection; + private int mPageScaling; + private int mStartPosition; + + private int mScrollState = SCROLL_NONE; + private int scrollXOffset = 0; + private int scrollYOffset = 0; + private int stopScrollXOffset = 0; + private int stopScrollYOffset = 0; + private float mScale = 1; + + private int mScreenWidth = -1; + private int mScreenHeight = -1; + + private final int mSumOffsetXStore[]; + private final int mSumOffsetYStore[]; + private final int mToNextOffsetXStore[]; + private final int mToNextOffsetYStore[]; + + // at most keep three item + private volatile ShowItem[] showItems; + private int mCurIndex; + private int mSize; + private boolean isEnsureSize; + private boolean mInit = false; + + private final int[] leftArea; + private final int[] topArea; + private final int[] rightArea; + private final int[] bottomArea; + private final int[] centerArea; + private final int[] mLightnessSlidingArea; + + private final Text leftText; + private final Text topText; + private final Text rightText; + private final Text bottomText; + private final Text centerText; + + private boolean mDoubleTapAnimating = false; + + private final boolean mLightnessSliding = false; + + private final boolean mShowTapArea = false; + private boolean mShowTapAreaTurn = false; + + private OnTapTextListener mOnTapTextListener; + private GalleryViewListener mGalleryViewListener; + + public interface GalleryViewListener { + public void onTapCenter(); + + public void onPageChanged(int index); + + public void onSizeUpdate(int size); + + public void onSlideBottom(float dx); + + public void onSlideBottomOver(); + + public void onRightToLeftChanged(boolean value); + } + + public interface OnTapTextListener { + public void onTapText(int index); + + public void onTapDoubleText(int index); + } + + public void setOnTapTextListener(OnTapTextListener l) { + mOnTapTextListener = l; + } + + public void setGalleryViewListener(GalleryViewListener l) { + mGalleryViewListener = l; + } + + public void setReadingDirection(int value) { + // TODO It is not safe when scroll + boolean oldIsRTL = isRightToLeft(); + mReadingDirection = value; + boolean newIsRTL = isRightToLeft(); + if (oldIsRTL != newIsRTL && mGalleryViewListener != null) + mGalleryViewListener.onRightToLeftChanged(newIsRTL); + } + + public void setPageScaling(int value) { + mPageScaling = value; + } + + public void setStartPosition(int value) { + mStartPosition = value; + } + + public GalleryView(Context context, ImageSet imageSet, int startIndex) { + + mSumOffsetXStore = new int[12]; + mSumOffsetYStore = new int[12]; + mToNextOffsetXStore = new int[4]; + mToNextOffsetYStore = new int[4]; + Arrays.fill(mSumOffsetXStore, 0, 12, 0); + Arrays.fill(mSumOffsetYStore, 0, 12, 0); + Arrays.fill(mToNextOffsetXStore, 0, 4, 0); + Arrays.fill(mToNextOffsetYStore, 0, 4, 0); + + leftArea = new int[4]; + topArea = new int[4]; + rightArea = new int[4]; + bottomArea = new int[4]; + centerArea = new int[4]; + mLightnessSlidingArea = new int[4]; + + leftText = new Text(context.getString(R.string.pre_page), + TAP_AREA_TEXT_SIZE, TAP_AREA_TEXT_COLOR); + topText = new Text(context.getString(R.string.zoom_in), + TAP_AREA_TEXT_SIZE, TAP_AREA_TEXT_COLOR); + rightText = new Text(context.getString(R.string.next_page), + TAP_AREA_TEXT_SIZE, TAP_AREA_TEXT_COLOR); + bottomText = new Text(context.getString(R.string.zoom_out), + TAP_AREA_TEXT_SIZE, TAP_AREA_TEXT_COLOR); + centerText = new Text(context.getString(R.string.menu), + TAP_AREA_TEXT_SIZE, TAP_AREA_TEXT_COLOR); + + mContext = context; + mImageSet = imageSet; + mCurIndex = startIndex; + + showItems = new ShowItem[TARGET_INDEX_SIZE]; + + checkSize(); + + // adjust mCurIndex + mCurIndex = MathUtils.clamp(mCurIndex, 0, mSize - 1); + + setState(); + + setBackgroundColor(GalleryUtils + .intColorToFloatARGBArray(BACKGROUND_COLOR)); + + // Init config + mReadingDirection = Config.getReadingDirection(); + mPageScaling = Config.getPageScaling(); + mStartPosition = Config.getStartPosition(); + + mImageSet.setImageListener(this); + + // Start task to check size + AppHandler.getInstance().postDelayed(this, 500); + + // Try to make current index show image + if (mCurIndex != startIndex) + mImageSet.addTargetIndex(mCurIndex); + + // First time to load image in onLayout() + } + + // The task to update size + @Override + public void run() { + checkSize(); + setState(); + if (!isEnsureSize && !mImageSet.isStop()) + AppHandler.getInstance().postDelayed(this, 500); + } + + /** + * Check size + */ + private void checkSize() { + int oldSize = mSize; + int size = mImageSet.getSize(); + if (size == -1) { + isEnsureSize = false; + mSize = mImageSet.getEnsureSize(); + } else { + isEnsureSize = true; + mSize = size; + } + + if (mSize > oldSize && mCurIndex == oldSize - 1 + && showItems[NEXT_TARGET_INDEX] == null) { + mState = STATE_NONE; + loadImage(mCurIndex + 1); + } + + if (mSize != oldSize && mGalleryViewListener != null) + mGalleryViewListener.onSizeUpdate(mSize); + } + + /** + * Set current page state, first page or last page + */ + private void setState() { + mState = STATE_NONE; + if (mCurIndex == 0) + mState = STATE_FIRST; + if (mCurIndex == mSize - 1) + mState = STATE_LAST; + } + + private boolean isIndexLoadable(int index) { + int targetIndex = index - mCurIndex + 1; + return targetIndex == CUR_TARGET_INDEX + || (mState != STATE_FIRST && targetIndex == PRE_TARGET_INDEX) + || (mState != STATE_LAST && targetIndex == NEXT_TARGET_INDEX); + } + + private boolean isIndexValid(int index) { + return index >= 0 && index < mSize; + } + + @Override + public void onGetImage(int index) { + if (isIndexLoadable(index)) { + loadImage(index); + } + } + + @Override + public synchronized void onDownloading(int index, float percent) { + int targetIndex = index - mCurIndex + 1; + if (isIndexLoadable(index)) { + // Free what do not need + if (showItems[targetIndex] != null) + showItems[targetIndex].recycle(); + + showItems[targetIndex] = new Text(String.format( + mContext.getString(R.string.loading_percent), + Math.round(percent * 100))); + resetSizePosition(targetIndex); + } + } + + @Override + public synchronized void onDecodeOver(int index, Object res) { + int targetIndex = index - mCurIndex + 1; + + if (!isIndexLoadable(index)) { + // If it do not need any more, free + if (res != null && res instanceof Bitmap) + ((Bitmap) res).recycle(); + } else { + // Free what do not need + if (showItems[targetIndex] != null) + showItems[targetIndex].recycle(); + + if (res == null) { + showItems[targetIndex] = new Text( + mContext.getString(R.string.read_image_error)); + } else { + if (res instanceof Bitmap) { + BitmapItem bi = new BitmapItem(); + bi.load((Bitmap) res); + showItems[targetIndex] = bi; + } else if (res instanceof Image) { + ImageItem ii = new ImageItem(); + ii.load((Image) res); + showItems[targetIndex] = ii; + } else { + MovieItem mi = new MovieItem(); + mi.load((Movie) res); + showItems[targetIndex] = mi; + } + } + resetSizePosition(targetIndex); + } + } + + public int getCurIndex() { + return mCurIndex; + } + + public int getSize() { + return mSize; + } + + private void drawTapArea(GLCanvas canvas) { + // Background + canvas.fillRect(0, 0, mScreenWidth, mScreenHeight, TAP_AREA_MASK_COLOR); + + drawRect(canvas, leftArea); + drawRect(canvas, topArea); + drawRect(canvas, rightArea); + drawRect(canvas, bottomArea); + drawRect(canvas, centerArea); + leftText.draw(canvas); + topText.draw(canvas); + rightText.draw(canvas); + bottomText.draw(canvas); + centerText.draw(canvas); + } + + private void drawRect(GLCanvas canvas, int[] rect) { + + int left = rect[0]; + int top = rect[1]; + int right = rect[2]; + int bottom = rect[3]; + + GLPaint paint = new GLPaint(LINE_COLOR, LINE_WIDTH); + canvas.drawLine(left, top, right, top, paint); + canvas.drawLine(left, bottom, right, bottom, paint); + canvas.drawLine(left, top, left, bottom, paint); + canvas.drawLine(right, top, right, bottom, paint); + } + + @Override + protected synchronized void render(GLCanvas canvas) { + // If it is not render thread, do not render + super.render(canvas); + + // First of all, upload if needed + for (int i = 0; i < TARGET_INDEX_SIZE; i++) { + ShowItem item = showItems[i]; + if (item instanceof UploadedItem) { + UploadedItem ui = (UploadedItem) item; + if (!ui.isContentValid()) + ui.updateContent(canvas); + } + } + + boolean needRefresh = false; + ShowItem item; + switch (mScrollState) { + case SCROLL_NONE: + item = showItems[CUR_TARGET_INDEX]; + if (item != null) + item.draw(canvas); + if (item instanceof ImageItem && ((ImageItem) item).isAnimated()) + needRefresh |= true; + if (item instanceof MovieItem) + needRefresh |= true; + break; + + case SCROLL_PRE: + case SCROLL_ANIME_PRE: + if (scrollXOffset != 0 || scrollYOffset != 0) { + item = showItems[PRE_TARGET_INDEX]; + if (item != null) + item.draw(canvas, scrollXOffset, scrollYOffset); + if (item instanceof ImageItem && ((ImageItem) item).isAnimated()) + needRefresh |= true; + if (item instanceof MovieItem) + needRefresh |= true; + } + + item = showItems[CUR_TARGET_INDEX]; + if (item != null) + item.draw(canvas, scrollXOffset, scrollYOffset); + if (item instanceof ImageItem && ((ImageItem) item).isAnimated()) + needRefresh |= true; + if (item instanceof MovieItem) + needRefresh |= true; + break; + + case SCROLL_NEXT: + case SCROLL_ANIME_NEXT: + item = showItems[CUR_TARGET_INDEX]; + if (item != null) + item.draw(canvas, scrollXOffset, scrollYOffset); + if (item instanceof ImageItem && ((ImageItem) item).isAnimated()) + needRefresh |= true; + if (item instanceof MovieItem) + needRefresh |= true; + + if (scrollXOffset != 0 || scrollYOffset != 0) { + item = showItems[NEXT_TARGET_INDEX]; + if (item != null) + item.draw(canvas, scrollXOffset, scrollYOffset); + if (item instanceof ImageItem && ((ImageItem) item).isAnimated()) + needRefresh |= true; + if (item instanceof MovieItem) + needRefresh |= true; + } + break; + } + + if (needRefresh) + invalidate(); + } + + private void resetSizePosition() { + resetSizePosition(PRE_TARGET_INDEX); + resetSizePosition(CUR_TARGET_INDEX); + resetSizePosition(NEXT_TARGET_INDEX); + } + + /** + * Reset target index show item position base on mReadingDirection + * or current next previous + * + * @param targetIndex + */ + private void resetSizePosition(int targetIndex) { + ShowItem showItem = showItems[targetIndex]; + if (showItem == null) + return; + + int index = mReadingDirection * TARGET_INDEX_SIZE + targetIndex; + int sumXOffset = mSumOffsetXStore[index]; + int sumYOffset = mSumOffsetYStore[index]; + + if (showItem instanceof Text) { + // Make text in center + Text text = (Text) showItem; + int xOffset; + int yOffset; + Rect rect = text.mRect; + xOffset = (mScreenWidth - text.width) / 2; + yOffset = (mScreenHeight - text.height) / 2; + rect.left = sumXOffset + xOffset; + rect.top = sumYOffset + yOffset; + rect.right = rect.left + text.width; + rect.bottom = rect.top + text.height; + } else if (showItem instanceof BasicItem) { + BasicItem image = (BasicItem) showItem; + + // Set scale + int showWidth = 0; + int showHeight = 0; + Rect rect = image.mRect; + switch (mPageScaling) { + case ORGIN: + image.imageScale = 1; + showWidth = image.width; + showHeight = image.height; + break; + case FIT_WIDTH: + image.imageScale = (float) mScreenWidth / image.width; + showWidth = mScreenWidth; + showHeight = (int) (image.height * image.imageScale); + break; + case FIT_HEIGHT: + image.imageScale = (float) mScreenHeight / image.height; + showWidth = (int) (image.width * image.imageScale); + showHeight = mScreenHeight; + break; + case FIT: + float scaleX = (float) mScreenWidth / image.width; + float scaleY = (float) mScreenHeight / image.height; + if (scaleX < scaleY) { + image.imageScale = scaleX; + showWidth = mScreenWidth; + showHeight = (int) (image.height * image.imageScale); + } else { + image.imageScale = scaleY; + showWidth = (int) (image.width * image.imageScale); + showHeight = mScreenHeight; + break; + } + break; + case FIXED: + default: + image.imageScale = mScale; + showWidth = (int) (image.width * mScale); + showHeight = (int) (image.height * mScale); + break; + } + + // adjust scale + if (image.imageScale < SCALE_MIN) { + image.imageScale = SCALE_MIN; + showWidth = (int) (image.width * SCALE_MIN); + showHeight = (int) (image.height * SCALE_MIN); + } else if (image.imageScale > SCALE_MAX) { + image.imageScale = SCALE_MAX; + showWidth = (int) (image.width * SCALE_MAX); + showHeight = (int) (image.height * SCALE_MAX); + } + + // set start position + int xOffset; + int yOffset; + switch (mStartPosition) { + case TOP_LEFT: + xOffset = 0; + yOffset = 0; + break; + case TOP_RIGHT: + xOffset = mScreenWidth - showWidth; + yOffset = 0; + break; + case BOTTOM_LEFT: + xOffset = 0; + yOffset = mScreenHeight - showHeight; + break; + case BOTTOM_RIGHT: + xOffset = mScreenWidth - showWidth; + yOffset = mScreenHeight - showHeight; + break; + case CENTER: + default: + xOffset = (mScreenWidth - showWidth) / 2; + yOffset = (mScreenHeight - showHeight) / 2; + break; + } + + rect.left = sumXOffset + xOffset; + rect.right = rect.left + showWidth; + rect.top = sumYOffset + yOffset; + rect.bottom = rect.top + showHeight; + + // adjust position + adjustPosition(image); + } + + invalidate(); + } + + private void setCenterInArea(int[] area, ShowItem showItem) { + int xOffset = ((area[2] - area[0]) - showItem.width) / 2; + int yOffset = ((area[3] - area[1]) - showItem.height) / 2; + + Rect rect = showItem.mRect; + + rect.left = area[0] + xOffset; + rect.right = rect.left + showItem.width; + rect.top = area[1] + yOffset; + rect.bottom = rect.top + showItem.height; + } + + private void setTapArea() { + for (int i = 0; i < 4; i++) { + int length = (i % 2 == 0 ? mScreenWidth : mScreenHeight); + leftArea[i] = (int) (LEFT_AREA_SCALE[i] * length); + topArea[i] = (int) (TOP_AREA_SCALE[i] * length); + rightArea[i] = (int) (RIGHT_AREA_SCALE[i] * length); + bottomArea[i] = (int) (BOTTOM_AREA_SCALE[i] * length); + centerArea[i] = (int) (CENTER_AREA_SCALE[i] * length); + mLightnessSlidingArea[i] = (int) (LIGHTNESS_SLIDING_AREA[i] * length); + } + + setCenterInArea(leftArea, leftText); + setCenterInArea(topArea, topText); + setCenterInArea(rightArea, rightText); + setCenterInArea(bottomArea, bottomText); + setCenterInArea(centerArea, centerText); + } + + @Override + protected void onLayout(boolean changeSize, int left, int top, int right, + int bottom) { + // Get View size + mScreenWidth = right - left; + mScreenHeight = bottom - top; + + // Fill store + mSumOffsetXStore[0] = -mScreenWidth; + mSumOffsetXStore[2] = mScreenWidth; + mSumOffsetXStore[3] = mScreenWidth; + mSumOffsetXStore[5] = -mScreenWidth; + + mSumOffsetYStore[6] = -mScreenHeight; + mSumOffsetYStore[8] = mScreenHeight; + mSumOffsetYStore[9] = mScreenHeight; + mSumOffsetYStore[11] = -mScreenHeight; + + mToNextOffsetXStore[DIRECTION_LEFT_RIGHT] = mScreenWidth; + mToNextOffsetXStore[DIRECTION_RIGHT_LEFT] = -mScreenWidth; + mToNextOffsetYStore[DIRECTION_TOP_BOTTOM] = mScreenHeight; + mToNextOffsetYStore[DIRECTION_BOTTOM_TOP] = -mScreenHeight; + + if (!mInit) { + mInit = true; + loadImage(mCurIndex); + if ((mState & STATE_LAST) == 0) + loadImage(mCurIndex + 1); + if ((mState & STATE_FIRST) == 0) + loadImage(mCurIndex - 1); + } else { + resetSizePosition(); + } + + // When screen orientation change, need to reset tap area + setTapArea(); + + invalidate(); + } + + /** + * Requset target image from ImageSet, + * + * @param index + */ + private synchronized void loadImage(int index) { + if (!isIndexLoadable(index)) + return; + + int targetIndex = index - mCurIndex + 1; + Object obj = mImageSet.getImage(index); + + if (showItems[targetIndex] != null) + showItems[targetIndex].recycle(); + + if (obj instanceof Integer) { + int state = (Integer) obj; + if (state == ImageSet.RESULT_DECODE) { + showItems[targetIndex] = null; + } else if (state == ImageSet.RESULT_DOWNLOADING) { + showItems[targetIndex] = new Text( + mContext.getString(R.string.downloading)); + } else if (state == ImageSet.RESULT_NONE) { + showItems[targetIndex] = new Text( + mContext.getString(R.string.not_loaded)); + } + } else if (obj instanceof Float) { + showItems[targetIndex] = new Text(String.format( + mContext.getString(R.string.loading_percent), + Math.round((Float) obj * 100))); + } + + resetSizePosition(targetIndex); + } + + /** + * If cur page is first page return true + * + * @return + */ + private boolean isFirstPage() { + return (mState & STATE_FIRST) != 0; + } + + /** + * If cur page is last page return true + * + * @return + */ + private boolean isLastPage() { + return (mState & STATE_LAST) != 0; + } + + /** + * + * @param mode + * true zoom in, false zoom out + * @return + */ + private boolean zoom(boolean mode) { + ShowItem curShowItem; + curShowItem = showItems[CUR_TARGET_INDEX]; + + if (curShowItem == null || !(curShowItem instanceof BasicItem)) + return false; + BasicItem image = (BasicItem) curShowItem; + float newScale; + if (mode) { + newScale = image.imageScale * 1.1f; + if (newScale == SCALE_MAX) + return false; + if (newScale > SCALE_MAX) + newScale = SCALE_MAX; + } else { + newScale = image.imageScale * 0.9f; + if (newScale == SCALE_MIN) + return false; + if (newScale < SCALE_MIN) + newScale = SCALE_MIN; + } + image.imageScale = newScale; + mScale = newScale; + + Rect rect = image.mRect; + int width = (int) (image.width * newScale); + int height = (int) (image.height * newScale); + int xOffset = (width - rect.width()) / 2; + int yOffset = (height - rect.height()) / 2; + + rect.set(rect.left - xOffset, rect.top - yOffset, rect.right + xOffset, + rect.bottom + yOffset); + + adjustPosition(curShowItem); + + invalidate(); + return true; + } + + /** + * You'd better resetSizePosition(PRE_TARGET_INDEX) before + * + * @return + */ + private synchronized boolean goToPrePage() { + if (isFirstPage()) + return false; + + ShowItem showItem; + if (showItems[NEXT_TARGET_INDEX] != null) { + showItems[NEXT_TARGET_INDEX].recycle(); + showItems[NEXT_TARGET_INDEX] = null; + } + showItems[NEXT_TARGET_INDEX] = showItems[CUR_TARGET_INDEX]; + showItems[CUR_TARGET_INDEX] = showItems[PRE_TARGET_INDEX]; + showItems[PRE_TARGET_INDEX] = null; + + // adjust rect + showItem = showItems[NEXT_TARGET_INDEX]; + if (showItem != null) + showItem.mRect.offset(mToNextOffsetXStore[mReadingDirection], mToNextOffsetYStore[mReadingDirection]); + showItem = showItems[CUR_TARGET_INDEX]; + if (showItem != null) + showItem.mRect.offset(mToNextOffsetXStore[mReadingDirection], mToNextOffsetYStore[mReadingDirection]); + + mCurIndex--; + setState(); + loadImage(mCurIndex - 1); + + mImageSet.setCurReadIndex(mCurIndex); + if (mGalleryViewListener != null) { + mGalleryViewListener.onPageChanged(mCurIndex); + } + + invalidate(); + return true; + } + + /** + * You'd better resetSizePosition(NEXT_TARGET_INDEX) before + * + * @return + */ + private synchronized boolean goToNextPage() { + if (isLastPage()) + return false; + + ShowItem showItem; + if (showItems[PRE_TARGET_INDEX] != null) { + showItems[PRE_TARGET_INDEX].recycle(); + showItems[PRE_TARGET_INDEX] = null; + } + showItems[PRE_TARGET_INDEX] = showItems[CUR_TARGET_INDEX]; + showItems[CUR_TARGET_INDEX] = showItems[NEXT_TARGET_INDEX]; + showItems[NEXT_TARGET_INDEX] = null; + // adjust rect + showItem = showItems[PRE_TARGET_INDEX]; + if (showItem != null) + showItem.mRect.offset(-mToNextOffsetXStore[mReadingDirection], -mToNextOffsetYStore[mReadingDirection]); + showItem = showItems[CUR_TARGET_INDEX]; + if (showItem != null) + showItem.mRect.offset(-mToNextOffsetXStore[mReadingDirection], -mToNextOffsetYStore[mReadingDirection]); + + mCurIndex++; + setState(); + loadImage(mCurIndex + 1); + + mImageSet.setCurReadIndex(mCurIndex); + if (mGalleryViewListener != null) { + mGalleryViewListener.onPageChanged(mCurIndex); + } + + invalidate(); + return true; + } + + public synchronized boolean goToPage(int index) { + if (!isIndexValid(index)) + return false; + + if (index == mCurIndex) { + return true; + } else if (index == mCurIndex - 1) { + return goToPrePage(); + } else if (index == mCurIndex + 1) { + return goToNextPage(); + } else { + // Free + if (showItems[PRE_TARGET_INDEX] != null) { + showItems[PRE_TARGET_INDEX].recycle(); + showItems[PRE_TARGET_INDEX] = null; + } + if (showItems[CUR_TARGET_INDEX] != null) { + showItems[CUR_TARGET_INDEX].recycle(); + showItems[CUR_TARGET_INDEX] = null; + } + if (showItems[NEXT_TARGET_INDEX] != null) { + showItems[NEXT_TARGET_INDEX].recycle(); + showItems[NEXT_TARGET_INDEX] = null; + } + + mCurIndex = index; + setState(); + loadImage(mCurIndex - 1); + loadImage(mCurIndex); + loadImage(mCurIndex + 1); + + mImageSet.setCurReadIndex(mCurIndex); + if (mGalleryViewListener != null) { + mGalleryViewListener.onPageChanged(mCurIndex); + } + + invalidate(); + return true; + } + } + + public synchronized void free() { + for (int i = 0; i < TARGET_INDEX_SIZE; i++) { + if (showItems[i] != null) { + showItems[i].recycle(); + showItems[i] = null; + } + } + } + + @Override + protected void onAttachToRoot(GLRoot root) { + super.onAttachToRoot(root); + mGestureRecognizer = new GestureRecognizer(mContext, + new MyGestureListener()); + } + + @Override + protected boolean onTouch(MotionEvent event) { + if (mGestureRecognizer != null) + mGestureRecognizer.onTouchEvent(event); + return true; + } + + public boolean isRightToLeft() { + if (mReadingDirection == DIRECTION_RIGHT_LEFT || mReadingDirection == DIRECTION_BOTTOM_TOP) + return true; + else + return false; + } + + private class MyGestureListener implements GestureRecognizer.Listener { + + private boolean isScale = false; + private final SynchronizedHandler mSynchronizedHandler; + + private final TimeRunner mToPreTimeRunner = new TimeRunner() { + @Override + protected void onStart() { + // Empty + } + + @Override + protected void onRun(float interpolatedTime, int runningTime) { + scrollXOffset = (int) (stopScrollXOffset + ((mToNextOffsetXStore[mReadingDirection] - stopScrollXOffset) * interpolatedTime)); + scrollYOffset = (int) (stopScrollYOffset + ((mToNextOffsetYStore[mReadingDirection] - stopScrollYOffset) * interpolatedTime)); + invalidate(); + } + + @Override + protected void onEnd() { + // Start animated image + ShowItem showItem; + for (int i = PRE_TARGET_INDEX; i < TARGET_INDEX_SIZE; i++) { + showItem = showItems[i]; + if (showItem instanceof ImageItem) + ((ImageItem) showItem).start(); + } + + scrollXOffset = 0; + scrollYOffset = 0; + goToPrePage(); + mScrollState = SCROLL_NONE; + } + + @Override + protected void onCancel() { + // Empty + } + }; + + private final TimeRunner mToNextTimeRunner = new TimeRunner() { + @Override + protected void onStart() { + // Empty + } + + @Override + protected void onRun(float interpolatedTime, int runningTime) { + scrollXOffset = (int) (stopScrollXOffset + ((-mToNextOffsetXStore[mReadingDirection] - stopScrollXOffset) * interpolatedTime)); + scrollYOffset = (int) (stopScrollYOffset + ((-mToNextOffsetYStore[mReadingDirection] - stopScrollYOffset) * interpolatedTime)); + invalidate(); + } + + @Override + protected void onEnd() { + // Start animated image + ShowItem showItem; + for (int i = PRE_TARGET_INDEX; i < TARGET_INDEX_SIZE; i++) { + showItem = showItems[i]; + if (showItem instanceof ImageItem) + ((ImageItem) showItem).start(); + } + + scrollXOffset = 0; + scrollYOffset = 0; + goToNextPage(); + mScrollState = SCROLL_NONE; + } + + @Override + protected void onCancel() { + // Empty + } + }; + + private final TimeRunner mReturnTimeRunner = new TimeRunner() { + @Override + protected void onStart() { + // Empty + } + + @Override + protected void onRun(float interpolatedTime, int runningTime) { + scrollXOffset = (int) ((1 - interpolatedTime) * stopScrollXOffset); + scrollYOffset = (int) ((1 - interpolatedTime) * stopScrollYOffset); + invalidate(); + } + + @Override + protected void onEnd() { + // Start animated image + ShowItem showItem; + for (int i = PRE_TARGET_INDEX; i < TARGET_INDEX_SIZE; i++) { + showItem = showItems[i]; + if (showItem instanceof ImageItem) + ((ImageItem) showItem).start(); + } + + scrollXOffset = 0; + scrollYOffset = 0; + mScrollState = SCROLL_NONE; + invalidate(); + } + + @Override + protected void onCancel() { + // Empty + } + }; + + private final TimeRunner mDoubleTapRunner = new TimeRunner() { + + private BasicItem image; + private float startScale; + private float endScale; + private final Point startPosition = new Point(); + private final Point endPosition = new Point(); + + private boolean chooseFirst(float a, float b, float standard) { + float offset1 = standard - a; + float offset2 = standard - b; + if ((offset1 > 0.0f && offset2 > 0.0f) + || (offset1 < 0.0f && offset2 < 0.0f)) { + return Math.abs(offset1) < Math.abs(offset2); + } else { + float aa = a / standard; + float bb = b / standard; + aa = aa > 1.0f ? 1 / aa : aa; + bb = bb > 1.0f ? 1 / bb : bb; + return aa < bb; + } + } + + @Override + protected void onStart() { + ShowItem showItem = showItems[CUR_TARGET_INDEX]; + if (showItem == null || !(showItem instanceof BasicItem)) { + mDoubleTapRunner.cancel(); + return; + } + image = (BasicItem) showItem; + int width = image.width; + int height = image.height; + if (width == -1 || height == -1) { + mDoubleTapRunner.cancel(); + return; + } + + // Init start info + Rect rect = image.mRect; + startScale = image.imageScale; + startPosition.x = (rect.left + rect.right) / 2; + startPosition.y = (rect.top + rect.bottom) / 2; + + float fitScreenScale; + float scaleX = (float) mScreenWidth / width; + float scaleY = (float) mScreenHeight / height; + if (scaleX < scaleY) + fitScreenScale = scaleX; + else + fitScreenScale = scaleY; + + if (chooseFirst(fitScreenScale, 1, startScale)) + // To fix screen + endScale = fitScreenScale; + else + // To origin + endScale = 1; + endPosition.x = mScreenWidth / 2; + endPosition.y = mScreenHeight / 2; + } + + @Override + protected void onRun(float interpolatedTime, int runningTime) { + int x = (int) (startPosition.x + (endPosition.x - startPosition.x) + * interpolatedTime); + int y = (int) (startPosition.y + (endPosition.y - startPosition.y) + * interpolatedTime); + float scale = startScale + (endScale - startScale) + * interpolatedTime; + setImageRect(x, y, scale); + invalidate(); + } + + @Override + protected void onEnd() { + // Update global scale + mScale = image.imageScale; + + mDoubleTapAnimating = false; + setImageRect(mScreenWidth / 2, mScreenHeight / 2, endScale); + invalidate(); + } + + @Override + protected void onCancel() { + // Update global scale + mScale = image.imageScale; + + mDoubleTapAnimating = false; + } + + private void setImageRect(int x, int y, float scale) { + image.imageScale = scale; + int showWidth = (int) (image.width * scale); + int showHeight = (int) (image.height * scale); + Rect rect = image.mRect; + rect.left = x - showWidth / 2; + rect.top = y - showHeight / 2; + rect.right = x + showWidth / 2; + rect.bottom = y + showHeight / 2; + } + }; + + public MyGestureListener() { + + mSynchronizedHandler = new SynchronizedHandler(getGLRoot()); + + mToPreTimeRunner.setHandler(mSynchronizedHandler); + mToNextTimeRunner.setHandler(mSynchronizedHandler); + mReturnTimeRunner.setHandler(mSynchronizedHandler); + mDoubleTapRunner.setHandler(mSynchronizedHandler); + mDoubleTapRunner.setDuration(Constants.ANIMATE_TIME); + mDoubleTapRunner.setInterpolator(new ZInterpolator(0.5f)); + } + + private void onTapPre() { + // TODO goto bottom first the to pre page + resetSizePosition(PRE_TARGET_INDEX); + if (!goToPrePage()) { + // Get to first page + MaterialToast.showToast(mContext.getString(R.string.first_page)); + } + } + + private void onTapNext() { + // TODO goto bottom first the to pre page + resetSizePosition(NEXT_TARGET_INDEX); + if (!goToNextPage()) { + // Get to last page + MaterialToast.showToast(mContext.getString(isEnsureSize ? + R.string.the_last_page : R.string.wait_for_more)); + } + } + + @Override + public boolean onSingleTapConfirmed(float x, float y) { + if (mScrollState != SCROLL_NONE) { + return false; + } + if (mShowTapAreaTurn) { + mShowTapAreaTurn = false; + return false; + } + if (mDoubleTapAnimating) + return false; + + if (showItems[CUR_TARGET_INDEX] == null + || showItems[CUR_TARGET_INDEX] instanceof Text) { + if (mOnTapTextListener != null) + mOnTapTextListener.onTapText(mCurIndex); + // return true; + } + + if (Utils.isInArea(leftArea, (int) x, (int) y)) { + if (isRightToLeft()) + onTapNext(); + else + onTapPre(); + + } else if (Utils.isInArea(topArea, (int) x, (int) y)) { + zoom(true); + + } else if (Utils.isInArea(rightArea, (int) x, (int) y)) { + if (isRightToLeft()) + onTapPre(); + else + onTapNext(); + + } else if (Utils.isInArea(bottomArea, (int) x, (int) y)) { + zoom(false); + + } else if (Utils.isInArea(centerArea, (int) x, (int) y)) { + if (mGalleryViewListener != null) + mGalleryViewListener.onTapCenter(); + + } else { + // Can't catch tap + } + + return true; + } + + @Override + public boolean onDoubleTap(float x, float y) { + return false; + } + + @Override + public boolean onDoubleTapConfirmed(float x, float y) { + if (isScale) + return true; + + ShowItem curShowItem = showItems[CUR_TARGET_INDEX]; + if (curShowItem == null || !(curShowItem instanceof BasicItem)) + return true; + + mDoubleTapAnimating = true; + mDoubleTapRunner.start(); + return true; + } + + @Override + public void onLongPress(MotionEvent e) { + if (isScale) + return; + + Utils.vibrator(mContext, 100); + mImageSet.setStartIndex(mCurIndex); + } + + @Override + public boolean onScrollBegin(float dx, float dy, MotionEvent e1, MotionEvent e2) { + //mLightnessSliding = Utils.isInArea(mLightnessSlidingArea, + //(int) e2.getX(), (int) e2.getY()) && Math.abs(dy / dx) > 1.0f; + return onScroll(dx, dy, e1, e2); + } + + /** + * SCROLL_NONE, SCROLL_PRE or SCROLL_NEXT + * + * @param showItem + * @param dx + * @param dy + * @param totalX + * @param totalY + * @return + */ + private int getScrollState(ShowItem showItem, float dx, float dy, float totalX, float totalY) { + Rect rect = showItem.mRect; + boolean needCheckBound = showItem instanceof BasicItem; + + switch (mReadingDirection) { + case DIRECTION_LEFT_RIGHT: + case DIRECTION_RIGHT_LEFT: + if (Math.abs(totalX / totalY) > 1) { + if (dx < 0 && (rect.left >= 0 || !needCheckBound)) + return mReadingDirection == DIRECTION_LEFT_RIGHT ? SCROLL_PRE : SCROLL_NEXT; + else if (dx > 0 && (rect.right <= mScreenWidth || !needCheckBound)) + return mReadingDirection == DIRECTION_LEFT_RIGHT ? SCROLL_NEXT : SCROLL_PRE; + else + return SCROLL_NONE; + } else { + return SCROLL_NONE; + } + + case DIRECTION_TOP_BOTTOM: + case DIRECTION_BOTTOM_TOP: + if (Math.abs(totalY / totalX) > 1) { + if (dy < 0 && (rect.top >= 0 || !needCheckBound)) + return mReadingDirection == DIRECTION_TOP_BOTTOM ? SCROLL_PRE : SCROLL_NEXT; + else if (dy > 0 && (rect.bottom <= mScreenHeight || !needCheckBound)) + return mReadingDirection == DIRECTION_TOP_BOTTOM ? SCROLL_NEXT : SCROLL_PRE; + else + return SCROLL_NONE; + } else { + return SCROLL_NONE; + } + + default: + return SCROLL_NONE; + } + } + + /** + * dx 和 totalX 符号相反,为啥 + */ + @Override + public boolean onScroll(float dx, float dy, MotionEvent e1, MotionEvent e2) { + + if (isScale) + return false; + + ShowItem curShowItem = showItems[CUR_TARGET_INDEX]; + // TODO what if current show item is always null + if (curShowItem == null) + return true; + + if (mDoubleTapAnimating) + return false; + + if (mLightnessSliding) { + if (mGalleryViewListener != null) + mGalleryViewListener.onSlideBottom(dy); + return true; + } + + float totalX = e2.getX() - e1.getX(); + float totalY = e2.getY() - e1.getY(); + + switch (mScrollState) { + case SCROLL_ANIME_PRE: + case SCROLL_ANIME_NEXT: + return false; + case SCROLL_NONE: + // Stop animated image + ShowItem showItem; + for (int i = PRE_TARGET_INDEX; i < TARGET_INDEX_SIZE; i++) { + showItem = showItems[i]; + if (showItem instanceof ImageItem) + ((ImageItem) showItem).stop(); + } + + Rect rect = curShowItem.mRect; + mScrollState = getScrollState(curShowItem, dx, dy, totalX, totalY); + + switch (mScrollState) { + case SCROLL_PRE: + if (isFirstPage()) { + mScrollState = SCROLL_NONE; + } else { + scrollXOffset = 0; + scrollYOffset = 0; + // Prepare previous show item + resetSizePosition(PRE_TARGET_INDEX); + } + break; + case SCROLL_NEXT: + if (isLastPage()) { + mScrollState = SCROLL_NONE; + } else { + scrollXOffset = 0; + scrollYOffset = 0; + // Prepare next show item + resetSizePosition(NEXT_TARGET_INDEX); + } + break; + } + if (mScrollState == SCROLL_NONE) { + int actDx = -(int) dx; + int actDy = -(int) dy; + if (rect.width() <= mScreenWidth) + actDx = 0; + if (rect.height() <= mScreenHeight) + actDy = 0; + rect.offset(actDx, actDy); + + // Fix position + adjustPosition(curShowItem); + } + break; + + case SCROLL_PRE: + case SCROLL_NEXT: + if ((mScrollState == SCROLL_PRE && mReadingDirection == DIRECTION_LEFT_RIGHT) || + (mScrollState == SCROLL_NEXT && mReadingDirection == DIRECTION_RIGHT_LEFT)) { + scrollXOffset -= dx; + if (scrollXOffset <= 0) { + scrollXOffset = 0; + mScrollState = SCROLL_NONE; + } + } else if ((mScrollState == SCROLL_NEXT && mReadingDirection == DIRECTION_LEFT_RIGHT) || + (mScrollState == SCROLL_PRE && mReadingDirection == DIRECTION_RIGHT_LEFT)) { + scrollXOffset -= dx; + if (scrollXOffset >= 0) { + scrollXOffset = 0; + mScrollState = SCROLL_NONE; + } + } else if ((mScrollState == SCROLL_PRE && mReadingDirection == DIRECTION_TOP_BOTTOM) || + (mScrollState == SCROLL_NEXT && mReadingDirection == DIRECTION_BOTTOM_TOP)) { + scrollYOffset -= dy; + if (scrollYOffset <= 0) { + scrollYOffset = 0; + mScrollState = SCROLL_NONE; + } + } else if ((mScrollState == SCROLL_NEXT && mReadingDirection == DIRECTION_TOP_BOTTOM) || + (mScrollState == SCROLL_PRE && mReadingDirection == DIRECTION_BOTTOM_TOP)) { + scrollYOffset -= dy; + if (scrollYOffset >= 0) { + scrollYOffset = 0; + mScrollState = SCROLL_NONE; + } + } + break; + } + + invalidate(); + return true; + } + + private boolean atPreEdge() { + ShowItem showItem = showItems[CUR_TARGET_INDEX]; + if (showItem == null) + return true; + + Rect rect = showItem.mRect; + switch (mReadingDirection) { + case DIRECTION_LEFT_RIGHT: + if (rect.left >= 0) + return true; + else + return false; + case DIRECTION_RIGHT_LEFT: + if (rect.right <= mScreenWidth) + return true; + else + return false; + case DIRECTION_TOP_BOTTOM: + if (rect.top >= 0) + return true; + else + return false; + case DIRECTION_BOTTOM_TOP: + if (rect.bottom <= mScreenHeight) + return true; + else + return false; + } + return true; + } + + private boolean atNextEdge() { + ShowItem showItem = showItems[CUR_TARGET_INDEX]; + if (showItem == null) + return true; + + Rect rect = showItem.mRect; + switch (mReadingDirection) { + case DIRECTION_LEFT_RIGHT: + if (rect.right <= mScreenWidth) + return true; + else + return false; + case DIRECTION_RIGHT_LEFT: + if (rect.left >= 0) + return true; + else + return false; + case DIRECTION_TOP_BOTTOM: + if (rect.bottom <= mScreenHeight) + return true; + else + return false; + case DIRECTION_BOTTOM_TOP: + if (rect.top >= 0) + return true; + else + return false; + } + return true; + } + + @Override + public boolean onScrollEnd(float totalX, float totalY) { + + if (isScale) + return false; + if (mDoubleTapAnimating) + return false; + if (mLightnessSliding) { + if (mGalleryViewListener != null) + mGalleryViewListener.onSlideBottomOver(); + return true; + } + + boolean isHorizon = mReadingDirection == DIRECTION_TOP_BOTTOM || mReadingDirection == DIRECTION_BOTTOM_TOP; + switch (mScrollState) { + case SCROLL_ANIME_PRE: + case SCROLL_ANIME_NEXT: + return false; + + case SCROLL_PRE: + case SCROLL_NEXT: + stopScrollXOffset = scrollXOffset; + stopScrollYOffset = scrollYOffset; + mScrollState = mScrollState == SCROLL_PRE ? SCROLL_ANIME_PRE : SCROLL_ANIME_NEXT; + int absStopScrollXOffset = Math.abs(stopScrollXOffset); + int absStopScrollYOffset = Math.abs(stopScrollYOffset); + + if (Math.abs(stopScrollXOffset) > mScreenWidth * CHANGE_PAGE_PROPORTION || + Math.abs(stopScrollYOffset) > mScreenHeight * CHANGE_PAGE_PROPORTION) { + TimeRunner runner = mScrollState == SCROLL_ANIME_PRE ? mToPreTimeRunner : mToNextTimeRunner; + if (isHorizon) + runner.setDuration(Math.max((int) (MILLSEC_PER_DIX * (mScreenHeight - absStopScrollYOffset)), ANIMATE_MIN)); + else + runner.setDuration(Math.max((int) (MILLSEC_PER_DIX * (mScreenWidth - absStopScrollXOffset)), ANIMATE_MIN)); + runner.start(); + } else { + mReturnTimeRunner.setDuration(Math.max((int) (MILLSEC_PER_DIX * (isHorizon ? absStopScrollYOffset : absStopScrollXOffset)), ANIMATE_MIN)); + mReturnTimeRunner.start(); + } + break; + + case SCROLL_NONE: + // Start animated image + ShowItem showItem; + for (int i = PRE_TARGET_INDEX; i < TARGET_INDEX_SIZE; i++) { + showItem = showItems[i]; + if (showItem instanceof ImageItem) + ((ImageItem) showItem).start(); + } + + showItem = showItems[CUR_TARGET_INDEX]; + if (isHorizon ? Math.abs(totalY) - BORDER_TIP_SENSITIVITY > 0 : Math.abs(totalX) - BORDER_TIP_SENSITIVITY > 0) { + if (isFirstPage() && atPreEdge()) + MaterialToast.showToast(mContext.getString(R.string.first_page)); + else if (isLastPage() && atNextEdge()) + MaterialToast.showToast(mContext.getString(isEnsureSize ? R.string.the_last_page : R.string.wait_for_more)); + } + } + return true; + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, + float velocityY) { + return true; + } + + @Override + public boolean onScaleBegin(float focusX, float focusY) { + if (mDoubleTapAnimating) + return false; + + isScale = true; + return true; + } + + @Override + public boolean onScale(float focusX, float focusY, float scale) { + if (mDoubleTapAnimating) + return false; + + if (mScrollState == SCROLL_ANIME_PRE + || mScrollState == SCROLL_ANIME_NEXT) + return true; + + if (mScrollState == SCROLL_PRE || mScrollState == SCROLL_NEXT) { + scrollXOffset = 0; + scrollYOffset = 0; + mScrollState = SCROLL_NONE; + } + + ShowItem curShowItem = showItems[CUR_TARGET_INDEX]; + if (curShowItem == null || !(curShowItem instanceof BasicItem)) + return true; + + BasicItem image = (BasicItem) curShowItem; + + float newScale = image.imageScale * scale; + if (newScale > SCALE_MAX || newScale < SCALE_MIN) + return true; + image.imageScale = newScale; + mScale = newScale; + + Rect rect = image.mRect; + int left = rect.left; + int top = rect.top; + + left = (int) (focusX - ((focusX - left) * scale)); + top = (int) (focusY - ((focusY - top) * scale)); + + rect.set(left, top, + (int) (left + (image.width * image.imageScale)), + (int) (top + (image.height * image.imageScale))); + + // adjust + adjustPosition(image); + + invalidate(); + + return true; + } + + @Override + public void onScaleEnd() { + isScale = false; + } + + @Override + public void onDown(float x, float y) { + // Empty + } + + @Override + public void onUp() { + // Empty + } + } + + /** + * + * If side is shorter then parent's, make it in parent's center If side is + * longer then parent's, make sure it fill parent + * + * @param showItem + */ + private void adjustPosition(ShowItem showItem) { + int targetIndex = getTargetIndex(showItem); + if (targetIndex == INVALID_ID) + return; + + Rect rect = showItem.mRect; + int showWidth = rect.width(); + int showHeight = rect.height(); + + int index = mReadingDirection * TARGET_INDEX_SIZE + targetIndex; + int sumXOffset = mSumOffsetXStore[index]; + int sumYOffset = mSumOffsetYStore[index]; + + if (showWidth > mScreenWidth) { + int fixXOffset = rect.left - sumXOffset; + if (fixXOffset > 0) { + rect.left -= fixXOffset; + rect.right -= fixXOffset; + } else if ((fixXOffset = sumXOffset + mScreenWidth - rect.right) > 0) { + rect.left += fixXOffset; + rect.right += fixXOffset; + } + } else { + int left = sumXOffset + (mScreenWidth - showWidth) / 2; + rect.offsetTo(left, rect.top); + } + if (showHeight > mScreenHeight) { + int fixYOffset = rect.top - sumYOffset; + if (fixYOffset > 0) { + rect.top -= fixYOffset; + rect.bottom -= fixYOffset; + } else if ((fixYOffset = sumYOffset + mScreenHeight - rect.bottom) > 0) { + rect.top += fixYOffset; + rect.bottom += fixYOffset; + } + } else { + int top = sumYOffset + (mScreenHeight - showHeight) / 2; + rect.offsetTo(rect.left, top); + } + } + + public int getTargetIndex(ShowItem showItem) { + if (mImageSet == null) + return INVALID_ID; + for (int i = 0; i < TARGET_INDEX_SIZE; i++) { + if (showItem == showItems[i]) + return i; + } + return INVALID_ID; + } + + private abstract class ShowItem { + protected int width = -1; + protected int height = -1; + public Rect mRect = new Rect(); + + public void draw(GLCanvas canvas) { + this.draw(canvas, 0, 0); + } + + public abstract void draw(GLCanvas canvas, int xOffset, int yOffset); + + public abstract void recycle(); + } + + private abstract class BasicItem extends ShowItem { + private BasicTexture mTexture; + public float imageScale = 1; + private final RectF source = new RectF(); + private final RectF target = new RectF(); + + + /** + * You must call init before draw + * + * @param texture + */ + public void init(BasicTexture texture) { + mTexture = texture; + imageScale = 1; + } + + public BasicTexture getTexture() { + return mTexture; + } + + @Override + public void draw(GLCanvas canvas, int offsetX, int offsetY) { + if (mTexture == null) + return; + + int index = getTargetIndex(this); + if (index == INVALID_ID) + mTexture.draw(canvas, mRect.left + offsetX, + mRect.top + offsetY, mRect.width(), mRect.height()); + else { + int leftBound = 0; + int topBound = 0; + int rightBound = mScreenWidth; + int bottomBound = mScreenHeight; + // Fix show bound + switch (index) { + case CUR_TARGET_INDEX: + if (offsetX < 0) + rightBound = rightBound + offsetX; + else if (offsetX > 0) + leftBound = offsetX; // leftBound + offsetX + if (offsetY < 0) + bottomBound = bottomBound + offsetX; + else if (offsetY > 0) + topBound = offsetY; // topBound + offsetY + break; + case PRE_TARGET_INDEX: + case NEXT_TARGET_INDEX: + if (offsetX < 0) + leftBound = rightBound + offsetX; + else if (offsetX > 0) + rightBound = offsetX; // leftBound + offsetX + if (offsetY < 0) + topBound = bottomBound + offsetY; + else if (offsetY > 0) + bottomBound = offsetY; // topBound + offsetY + break; + } + + int left = mRect.left + offsetX; + int top = mRect.top + offsetY; + int right = mRect.right + offsetX; + int bottom = mRect.bottom + offsetY; + + // Only show what in the own box + // Only what can be seen + if (left < leftBound || top < topBound || right > rightBound + || bottom > bottomBound) { + + if (left < leftBound) { + target.left = leftBound; + source.left = (leftBound - left) / imageScale; + } else { + target.left = left; + source.left = 0; + } + if (top < topBound) { + target.top = topBound; + source.top = (topBound - top) / imageScale; + } else { + target.top = top; + source.top = 0; + } + if (right > rightBound) { + target.right = rightBound; + source.right = width - ((right - rightBound) / imageScale); + } else { + target.right = right; + source.right = width; + } + if (bottom > bottomBound) { + target.bottom = bottomBound; + source.bottom = height - ((bottom - bottomBound) / imageScale); + } else { + target.bottom = bottom; + source.bottom = height; + } + + mTexture.draw(canvas, source, target); + } else { + mTexture.draw(canvas, left, top, right - left, bottom - top); + } + } + } + } + + private abstract class UploadedItem extends BasicItem { + + public boolean isContentValid() { + return ((Uploaded) getTexture()).isContentValid(); + } + + public void updateContent(GLCanvas canvas) { + ((Uploaded) getTexture()).updateContent(canvas); + } + } + + private class BitmapItem extends UploadedItem { + private BitmapTexture mTexture; + private Bitmap mContextBmp; + + public void load(Bitmap bmp) { + if (mTexture != null) + recycle(); + mContextBmp = bmp; + mTexture = new BitmapTexture(bmp); + width = mContextBmp.getWidth(); + height = mContextBmp.getHeight(); + + super.init(mTexture); + } + + @Override + public void recycle() { + if (mTexture == null) + return; + + mTexture.recycle(); + mContextBmp.recycle(); + + mTexture = null; + mContextBmp = null; + } + } + + private class ImageItem extends UploadedItem { + private ImageTexture mTexture; + + public void load(Image image) { + mTexture = new ImageTexture(image); + width = mTexture.getWidth(); + height = mTexture.getHeight(); + + super.init(mTexture); + } + + @Override + public void draw(GLCanvas canvas, int xOffset, int yOffset) { + super.draw(canvas, xOffset, yOffset); + } + + @Override + public void recycle() { + mTexture.recycle(); + } + + public void start() { + mTexture.start(); + } + + public void stop() { + mTexture.stop(); + } + + public boolean isAnimated() { + return mTexture.isAnimated(); + } + } + + private class MovieItem extends BasicItem { + private MovieTexture mTexture; + private Movie mContextMovie; + + public void load(Movie movie) { + if (mTexture != null) + recycle(); + mContextMovie = movie; + mTexture = MovieTexture.newInstance(mContextMovie); + + width = mContextMovie.width(); + height = mContextMovie.height(); + + super.init(mTexture); + } + + @Override + public void recycle() { + if (mTexture == null) + return; + + mTexture.recycle(); + + mTexture = null; + mContextMovie = null; + } + } + + private class Text extends ShowItem { + private final int mSize = Ui.sp2pix(32); + + private StringTexture mTexture; + + public Text(String str) { + load(str); + } + + public Text(String str, float size, int color) { + load(str, size, color); + } + + public void load(String str) { + load(str, mSize, 0xdeffffff); + } + + public void load(String str, float size, int color) { + if (mTexture != null) + recycle(); + mTexture = StringTexture.newInstance(str, size, color); + width = mTexture.getWidth(); + height = mTexture.getHeight(); + } + + @Override + public void draw(GLCanvas canvas, int xOffset, int yOffset) { + if (mTexture != null) + mTexture.draw(canvas, mRect.left + xOffset, mRect.top + yOffset); + } + + @Override + public void recycle() { + if (mTexture == null) + return; + mTexture.recycle(); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/anim/Animation.java b/app/src/main/java/com/hippo/ehviewer/gallery/anim/Animation.java new file mode 100644 index 000000000..5f2f1046d --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/anim/Animation.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.anim; + +import android.view.animation.Interpolator; + +import com.hippo.ehviewer.util.MathUtils; + +// Animation calculates a value according to the current input time. +// +// 1. First we need to use setDuration(int) to set the duration of the +// animation. The duration is in milliseconds. +// 2. Then we should call start(). The actual start time is the first value +// passed to calculate(long). +// 3. Each time we want to get an animation value, we call +// calculate(long currentTimeMillis) to ask the Animation to calculate it. +// The parameter passed to calculate(long) should be nonnegative. +// 4. Use get() to get that value. +// +// In step 3, onCalculate(float progress) is called so subclasses can calculate +// the value according to progress (progress is a value in [0,1]). +// +// Before onCalculate(float) is called, There is an optional interpolator which +// can change the progress value. The interpolator can be set by +// setInterpolator(Interpolator). If the interpolator is used, the value passed +// to onCalculate may be (for example, the overshoot effect). +// +// The isActive() method returns true after the animation start() is called and +// before calculate is passed a value which reaches the duration of the +// animation. +// +// The start() method can be called again to restart the Animation. +// +abstract public class Animation { + private static final long ANIMATION_START = -1; + private static final long NO_ANIMATION = -2; + + private long mStartTime = NO_ANIMATION; + private int mDuration; + private Interpolator mInterpolator; + + public void setInterpolator(Interpolator interpolator) { + mInterpolator = interpolator; + } + + public void setDuration(int duration) { + mDuration = duration; + } + + public void start() { + mStartTime = ANIMATION_START; + } + + public void setStartTime(long time) { + mStartTime = time; + } + + public boolean isActive() { + return mStartTime != NO_ANIMATION; + } + + public void forceStop() { + mStartTime = NO_ANIMATION; + } + + public boolean calculate(long currentTimeMillis) { + if (mStartTime == NO_ANIMATION) return false; + if (mStartTime == ANIMATION_START) mStartTime = currentTimeMillis; + int elapse = (int) (currentTimeMillis - mStartTime); + float x = MathUtils.clamp((float) elapse / mDuration, 0f, 1f); + Interpolator i = mInterpolator; + onCalculate(i != null ? i.getInterpolation(x) : x); + if (elapse >= mDuration) mStartTime = NO_ANIMATION; + return mStartTime != NO_ANIMATION; + } + + abstract protected void onCalculate(float progress); +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/anim/CanvasAnimation.java b/app/src/main/java/com/hippo/ehviewer/gallery/anim/CanvasAnimation.java new file mode 100644 index 000000000..6a809f833 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/anim/CanvasAnimation.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.anim; + +import com.hippo.ehviewer.gallery.glrenderer.GLCanvas; + +public abstract class CanvasAnimation extends Animation { + + public abstract int getCanvasSaveFlags(); + public abstract void apply(GLCanvas canvas); +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/anim/StateTransitionAnimation.java b/app/src/main/java/com/hippo/ehviewer/gallery/anim/StateTransitionAnimation.java new file mode 100644 index 000000000..9f3b565bc --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/anim/StateTransitionAnimation.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.anim; + +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +import com.hippo.ehviewer.gallery.glrenderer.GLCanvas; +import com.hippo.ehviewer.gallery.glrenderer.RawTexture; +import com.hippo.ehviewer.gallery.ui.GLView; + +public class StateTransitionAnimation extends Animation { + + public static class Spec { + public static final Spec OUTGOING; + public static final Spec INCOMING; + public static final Spec PHOTO_INCOMING; + + private static final Interpolator DEFAULT_INTERPOLATOR = + new DecelerateInterpolator(); + + public int duration = 330; + public float backgroundAlphaFrom = 0; + public float backgroundAlphaTo = 0; + public float backgroundScaleFrom = 0; + public float backgroundScaleTo = 0; + public float contentAlphaFrom = 1; + public float contentAlphaTo = 1; + public float contentScaleFrom = 1; + public float contentScaleTo = 1; + public float overlayAlphaFrom = 0; + public float overlayAlphaTo = 0; + public float overlayScaleFrom = 0; + public float overlayScaleTo = 0; + public Interpolator interpolator = DEFAULT_INTERPOLATOR; + + static { + OUTGOING = new Spec(); + OUTGOING.backgroundAlphaFrom = 0.5f; + OUTGOING.backgroundAlphaTo = 0f; + OUTGOING.backgroundScaleFrom = 1f; + OUTGOING.backgroundScaleTo = 0f; + OUTGOING.contentAlphaFrom = 0.5f; + OUTGOING.contentAlphaTo = 1f; + OUTGOING.contentScaleFrom = 3f; + OUTGOING.contentScaleTo = 1f; + + INCOMING = new Spec(); + INCOMING.overlayAlphaFrom = 1f; + INCOMING.overlayAlphaTo = 0f; + INCOMING.overlayScaleFrom = 1f; + INCOMING.overlayScaleTo = 3f; + INCOMING.contentAlphaFrom = 0f; + INCOMING.contentAlphaTo = 1f; + INCOMING.contentScaleFrom = 0.25f; + INCOMING.contentScaleTo = 1f; + + PHOTO_INCOMING = INCOMING; + } + + private static Spec specForTransition(Transition t) { + switch (t) { + case Outgoing: + return Spec.OUTGOING; + case Incoming: + return Spec.INCOMING; + case PhotoIncoming: + return Spec.PHOTO_INCOMING; + case None: + default: + return null; + } + } + } + + public static enum Transition { None, Outgoing, Incoming, PhotoIncoming } + + private final Spec mTransitionSpec; + private float mCurrentContentScale; + private float mCurrentContentAlpha; + private float mCurrentBackgroundScale; + private float mCurrentBackgroundAlpha; + private float mCurrentOverlayScale; + private float mCurrentOverlayAlpha; + private RawTexture mOldScreenTexture; + + public StateTransitionAnimation(Transition t, RawTexture oldScreen) { + this(Spec.specForTransition(t), oldScreen); + } + + public StateTransitionAnimation(Spec spec, RawTexture oldScreen) { + mTransitionSpec = spec != null ? spec : Spec.OUTGOING; + setDuration(mTransitionSpec.duration); + setInterpolator(mTransitionSpec.interpolator); + mOldScreenTexture = oldScreen; + } + + @Override + public boolean calculate(long currentTimeMillis) { + boolean retval = super.calculate(currentTimeMillis); + if (!isActive()) { + if (mOldScreenTexture != null) { + mOldScreenTexture.recycle(); + mOldScreenTexture = null; + } + } + return retval; + } + + @Override + protected void onCalculate(float progress) { + mCurrentContentScale = mTransitionSpec.contentScaleFrom + + (mTransitionSpec.contentScaleTo - mTransitionSpec.contentScaleFrom) * progress; + mCurrentContentAlpha = mTransitionSpec.contentAlphaFrom + + (mTransitionSpec.contentAlphaTo - mTransitionSpec.contentAlphaFrom) * progress; + mCurrentBackgroundAlpha = mTransitionSpec.backgroundAlphaFrom + + (mTransitionSpec.backgroundAlphaTo - mTransitionSpec.backgroundAlphaFrom) + * progress; + mCurrentBackgroundScale = mTransitionSpec.backgroundScaleFrom + + (mTransitionSpec.backgroundScaleTo - mTransitionSpec.backgroundScaleFrom) + * progress; + mCurrentOverlayScale = mTransitionSpec.overlayScaleFrom + + (mTransitionSpec.overlayScaleTo - mTransitionSpec.overlayScaleFrom) * progress; + mCurrentOverlayAlpha = mTransitionSpec.overlayAlphaFrom + + (mTransitionSpec.overlayAlphaTo - mTransitionSpec.overlayAlphaFrom) * progress; + } + + private void applyOldTexture(GLView view, GLCanvas canvas, float alpha, float scale, boolean clear) { + if (mOldScreenTexture == null) + return; + if (clear) canvas.clearBuffer(view.getBackgroundColor()); + canvas.save(); + canvas.setAlpha(alpha); + int xOffset = view.getWidth() / 2; + int yOffset = view.getHeight() / 2; + canvas.translate(xOffset, yOffset); + canvas.scale(scale, scale, 1); + mOldScreenTexture.draw(canvas, -xOffset, -yOffset); + canvas.restore(); + } + + public void applyBackground(GLView view, GLCanvas canvas) { + if (mCurrentBackgroundAlpha > 0f) { + applyOldTexture(view, canvas, mCurrentBackgroundAlpha, mCurrentBackgroundScale, true); + } + } + + public void applyContentTransform(GLView view, GLCanvas canvas) { + int xOffset = view.getWidth() / 2; + int yOffset = view.getHeight() / 2; + canvas.translate(xOffset, yOffset); + canvas.scale(mCurrentContentScale, mCurrentContentScale, 1); + canvas.translate(-xOffset, -yOffset); + canvas.setAlpha(mCurrentContentAlpha); + } + + public void applyOverlay(GLView view, GLCanvas canvas) { + if (mCurrentOverlayAlpha > 0f) { + applyOldTexture(view, canvas, mCurrentOverlayAlpha, mCurrentOverlayScale, false); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/data/ImageSet.java b/app/src/main/java/com/hippo/ehviewer/gallery/data/ImageSet.java new file mode 100644 index 000000000..75307d24c --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/data/ImageSet.java @@ -0,0 +1,297 @@ +/* + * 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.gallery.data; + +import java.io.File; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +import android.graphics.BitmapFactory; +import android.graphics.Movie; +import android.util.SparseArray; + +import com.hippo.ehviewer.AppHandler; +import com.hippo.ehviewer.ehclient.ExDownloader; +import com.hippo.ehviewer.ehclient.ExDownloaderManager; +import com.hippo.ehviewer.gallery.image.Image; +import com.hippo.ehviewer.util.AutoExpandArray; +import com.hippo.ehviewer.util.BgThread; +import com.hippo.ehviewer.util.Config; +import com.hippo.ehviewer.util.EhUtils; +import com.hippo.ehviewer.util.Log; +import com.hippo.ehviewer.util.Utils; + +public class ImageSet implements ExDownloader.ListenerForImageSet { + + private static final String TAG = ImageSet.class.getSimpleName(); + + /** + * These are code reture by getImage() + */ + public static final int RESULT_NONE = 0x0; + public static final int RESULT_DOWNLOADING = 0x1; + public static final int RESULT_DECODE = 0x2; + + private final ExDownloaderManager mEdManager; + private final ExDownloader mExDownloader; + + private final File mDir; + private final AutoExpandArray mImageFilenameArray; + private ImageListener mListener; + private final Queue mDecodeQueue = new ConcurrentLinkedQueue(); + private final SparseArray mPercentMap = new SparseArray(5); + private final DecodeWorker mDecodeWorker; + /** If true, then wake Worker, worker will stop **/ + private volatile boolean mStopWork = false; + + public class DecodeInfo { + public int index; + public String filename; + + public DecodeInfo(int index, String filename) { + this.index = index; + this.filename = filename; + } + } + + public interface ImageListener { + /** + * It will be invoke when ExDownloader download successful + * + * @param index + */ + void onGetImage(int index); + + void onDownloading(int index, float percent); + + /** + * It will be invoke when decode image over + * + * @param index + * @param res + */ + void onDecodeOver(int index, Object res); + } + + public ImageSet(int gid, String token, String title, int startIndex) { + + mDir = EhUtils.getGalleryDir(gid, title); + Utils.ensureDir(mDir, true); + + // Init ExDownloader + mEdManager = ExDownloaderManager.getInstance(); + mExDownloader = mEdManager.getExDownloader(gid, token, title, Config.getMode()); + mExDownloader.setListenerForImageSet(this); + mExDownloader.setStartIndex(startIndex); + + mImageFilenameArray = mExDownloader.getImageFilenameArray(); + + // Start decode worker + mDecodeWorker = new DecodeWorker(); + mDecodeWorker.start(); + } + + public boolean isStop() { + return mStopWork; + } + + public int getSize() { + return mExDownloader.getImageNum(); + } + + public int getEnsureSize() { + return mExDownloader.getMaxEnsureIndex() + 1; + } + + public void setImageListener(ImageListener l) { + mListener = l; + } + + public void addTargetIndex(int index) { + mExDownloader.addTargetIndex(index); + } + + public void setStartIndex(int startIndex) { + mExDownloader.setStartIndex(startIndex); + } + + public void setCurReadIndex(int index) { + mExDownloader.setCurReadIndex(index); + } + + public String getTitle() { + return mExDownloader.getTitle(); + } + + public File getImageFile(int index) { + File file = null; + String name = getImageFilename(index); + if (name != null) + file = new File(mDir, name); + return file; + } + + private String getImageFilename(int index) { + String filename = mImageFilenameArray.get(index); + if (filename == null) { + // Just guess filename + for (String possibleFilename : EhUtils.getPossibleImageFilenames(index)) { + if (new File(mDir, possibleFilename).exists()) { + // Get filename + filename = possibleFilename; + mImageFilenameArray.set(index, filename); + break; + } + } + } + return filename; + } + + public void redownload(int index) { + String filename = getImageFilename(index); + if (filename != null) { + // Delete file + new File(mDir, filename).delete(); + } + mExDownloader.addTargetIndex(index); + mListener.onGetImage(index); + } + + public Object getImage(int index) { + String filename = getImageFilename(index); + Float percent; + + if ((percent = mPercentMap.get(index)) != null) { + return percent; + } else if (mExDownloader.isDownloading(index)) { + // downloading + return RESULT_DOWNLOADING; + } else if (filename == null || !new File(mDir, filename).exists()) { + // Target index has not being downloading + return RESULT_NONE; + } else { + mDecodeQueue.offer(new DecodeInfo(index, filename)); + // wake decode worker + synchronized(mDecodeQueue) {mDecodeQueue.notify();} + return RESULT_DECODE; + } + } + + /** + * You must call it when you do not need it any more + */ + public void free() { + mStopWork = true; + // wake decode worker to wash wash sleep + synchronized(mDecodeQueue) {mDecodeQueue.notify();} + + // Remove listener for ImageSet + mExDownloader.setListenerForImageSet(null); + // Free ExDownloader + mEdManager.freeExDownloader(mExDownloader); + } + + private class DecodeWorker extends BgThread { + @Override + public void run() { + DecodeInfo decodeInfo; + while (true) { + // Check stop work or not + if (mStopWork) + break; + + // Get decode task + synchronized (mDecodeQueue) { + if (mDecodeQueue.isEmpty()) { + try { + mDecodeQueue.wait(); + } catch (InterruptedException e) {} + continue; + } + decodeInfo = mDecodeQueue.poll(); + } + + // do decode + final int index = decodeInfo.index; + String pathName = Utils.getPathName(mDir.getPath(), decodeInfo.filename); + Object res = null; + + if (Utils.SUPPORT_IMAGE && Config.getCustomCodec()) { + res = Image.decodeFile(pathName, Config.getDecodeFormat()); + } else { + if (Utils.getExtension(pathName, "jpg").equals("gif")) + res = Movie.decodeFile(pathName); + else + res = BitmapFactory.decodeFile(pathName); + } + + final Object _res = res; + // Post to UI thread + AppHandler.getInstance().post(new Runnable() { + @Override + public void run() { + if (mListener != null) + mListener.onDecodeOver(index, _res); + } + }); + } + Log.d(TAG, "DecodeWorker stop working"); + } + } + + @Override + public void onDownloadStart(final int index) { + AppHandler.getInstance().post(new Runnable() { + @Override + public void run() { + if (mListener != null) + mListener.onDownloading(index, 0.0f); + } + }); + } + + @Override + public void onDownloading(final int index, final float percent) { + mPercentMap.append(index, percent); + + AppHandler.getInstance().post(new Runnable() { + @Override + public void run() { + if (mListener != null) + mListener.onDownloading(index, percent); + } + }); + } + + @Override + public void onDownloadComplete(final int index) { + mPercentMap.remove(index); + + AppHandler.getInstance().post(new Runnable() { + @Override + public void run() { + if (mListener != null) + mListener.onGetImage(index); + } + }); + } + + @Override + public void onDownloadFail(int index) { + mPercentMap.remove(index); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/BasicTexture.java b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/BasicTexture.java new file mode 100644 index 000000000..9e1caa27e --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/BasicTexture.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.glrenderer; + +import java.util.WeakHashMap; + +import android.graphics.RectF; +import android.util.Log; + +import com.hippo.ehviewer.util.MathUtils; + +// BasicTexture is a Texture corresponds to a real GL texture. +// The state of a BasicTexture indicates whether its data is loaded to GL memory. +// If a BasicTexture is loaded into GL memory, it has a GL texture id. +public abstract class BasicTexture implements Texture { + + private static final String TAG = "BasicTexture"; + protected static final int UNSPECIFIED = -1; + + protected static final int STATE_UNLOADED = 0; + protected static final int STATE_LOADED = 1; + protected static final int STATE_ERROR = -1; + + // Log a warning if a texture is larger along a dimension + private static final int MAX_TEXTURE_SIZE = 4096; + + protected int mId = -1; + protected int mState; + + protected int mWidth = UNSPECIFIED; + protected int mHeight = UNSPECIFIED; + + protected int mTextureWidth; + protected int mTextureHeight; + + private boolean mHasBorder; + + protected GLCanvas mCanvasRef = null; + private static WeakHashMap sAllTextures + = new WeakHashMap(); + @SuppressWarnings("rawtypes") + private static ThreadLocal sInFinalizer = new ThreadLocal(); + + protected BasicTexture(GLCanvas canvas, int id, int state) { + setAssociatedCanvas(canvas); + mId = id; + mState = state; + synchronized (sAllTextures) { + sAllTextures.put(this, null); + } + } + + protected BasicTexture() { + this(null, 0, STATE_UNLOADED); + } + + protected void setAssociatedCanvas(GLCanvas canvas) { + mCanvasRef = canvas; + } + + /** + * Sets the content size of this texture. In OpenGL, the actual texture + * size must be of power of 2, the size of the content may be smaller. + */ + public void setSize(int width, int height) { + mWidth = width; + mHeight = height; + mTextureWidth = width > 0 ? MathUtils.nextPowerOf2(width) : 0; + mTextureHeight = height > 0 ? MathUtils.nextPowerOf2(height) : 0; + if (mTextureWidth > MAX_TEXTURE_SIZE || mTextureHeight > MAX_TEXTURE_SIZE) { + Log.w(TAG, String.format("texture is too large: %d x %d", + mTextureWidth, mTextureHeight), new Exception()); + } + } + + public boolean isFlippedVertically() { + return false; + } + + public int getId() { + return mId; + } + + @Override + public int getWidth() { + return mWidth; + } + + @Override + public int getHeight() { + return mHeight; + } + + // Returns the width rounded to the next power of 2. + public int getTextureWidth() { + return mTextureWidth; + } + + // Returns the height rounded to the next power of 2. + public int getTextureHeight() { + return mTextureHeight; + } + + // Returns true if the texture has one pixel transparent border around the + // actual content. This is used to avoid jigged edges. + // + // The jigged edges appear because we use GL_CLAMP_TO_EDGE for texture wrap + // mode (GL_CLAMP is not available in OpenGL ES), so a pixel partially + // covered by the texture will use the color of the edge texel. If we add + // the transparent border, the color of the edge texel will be mixed with + // appropriate amount of transparent. + // + // Currently our background is black, so we can draw the thumbnails without + // enabling blending. + public boolean hasBorder() { + return mHasBorder; + } + + protected void setBorder(boolean hasBorder) { + mHasBorder = hasBorder; + } + + @Override + public void draw(GLCanvas canvas, int x, int y) { + canvas.drawTexture(this, x, y, getWidth(), getHeight()); + } + + @Override + public void draw(GLCanvas canvas, int x, int y, int w, int h) { + canvas.drawTexture(this, x, y, w, h); + } + + public void draw(GLCanvas canvas, RectF source, RectF target) { + canvas.drawTexture(this, source, target); + } + + // onBind is called before GLCanvas binds this texture. + // It should make sure the data is uploaded to GL memory. + abstract protected boolean onBind(GLCanvas canvas); + + // Returns the GL texture target for this texture (e.g. GL_TEXTURE_2D). + abstract protected int getTarget(); + + public boolean isLoaded() { + return mState == STATE_LOADED; + } + + // recycle() is called when the texture will never be used again, + // so it can free all resources. + public void recycle() { + freeResource(); + } + + // yield() is called when the texture will not be used temporarily, + // so it can free some resources. + // The default implementation unloads the texture from GL memory, so + // the subclass should make sure it can reload the texture to GL memory + // later, or it will have to override this method. + public void yield() { + freeResource(); + } + + private void freeResource() { + GLCanvas canvas = mCanvasRef; + if (canvas != null && mId != -1) { + canvas.unloadTexture(this); + mId = -1; // Don't free it again. + } + mState = STATE_UNLOADED; + setAssociatedCanvas(null); + } + + @Override + @SuppressWarnings("unchecked") + protected void finalize() { + sInFinalizer.set(BasicTexture.class); + recycle(); + sInFinalizer.set(null); + } + + // This is for deciding if we can call Bitmap's recycle(). + // We cannot call Bitmap's recycle() in finalizer because at that point + // the finalizer of Bitmap may already be called so recycle() will crash. + public static boolean inFinalizer() { + return sInFinalizer.get() != null; + } + + public static void yieldAllTextures() { + synchronized (sAllTextures) { + for (BasicTexture t : sAllTextures.keySet()) { + t.yield(); + } + } + } + + public static void invalidateAllTextures() { + synchronized (sAllTextures) { + for (BasicTexture t : sAllTextures.keySet()) { + t.mState = STATE_UNLOADED; + t.setAssociatedCanvas(null); + } + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/BitmapTexture.java b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/BitmapTexture.java new file mode 100644 index 000000000..120f62c28 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/BitmapTexture.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.glrenderer; + +import junit.framework.Assert; +import android.graphics.Bitmap; + +// BitmapTexture is a texture whose content is specified by a fixed Bitmap. +// +// The texture does not own the Bitmap. The user should make sure the Bitmap +// is valid during the texture's lifetime. When the texture is recycled, it +// does not free the Bitmap. +public class BitmapTexture extends UploadedTexture { + protected Bitmap mContentBitmap; + + public BitmapTexture(Bitmap bitmap) { + this(bitmap, false); + } + + public BitmapTexture(Bitmap bitmap, boolean hasBorder) { + super(hasBorder); + Assert.assertTrue(bitmap != null && !bitmap.isRecycled()); + mContentBitmap = bitmap; + } + + @Override + protected void onFreeBitmap(Bitmap bitmap) { + // Do nothing. + } + + @Override + protected Bitmap onGetBitmap() { + return mContentBitmap; + } + + public Bitmap getBitmap() { + return mContentBitmap; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/CanvasTexture.java b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/CanvasTexture.java new file mode 100644 index 000000000..24b23d90a --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/CanvasTexture.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.glrenderer; + +import android.annotation.SuppressLint; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.Canvas; + +// CanvasTexture is a texture whose content is the drawing on a Canvas. +// The subclasses should override onDraw() to draw on the bitmap. +// By default CanvasTexture is not opaque. +abstract class CanvasTexture extends UploadedTexture { + protected Canvas mCanvas; + private final Config mConfig; + + public CanvasTexture(int width, int height) { + mConfig = Config.ARGB_8888; + setSize(width, height); + setOpaque(false); + } + + @Override + @SuppressLint("WrongCall") + protected Bitmap onGetBitmap() { + Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight, mConfig); + mCanvas = new Canvas(bitmap); + onDraw(mCanvas, bitmap); + return bitmap; + } + + @Override + protected void onFreeBitmap(Bitmap bitmap) { + if (!inFinalizer()) { + bitmap.recycle(); + } + } + + abstract protected void onDraw(Canvas canvas, Bitmap backing); +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/ColorTexture.java b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/ColorTexture.java new file mode 100644 index 000000000..86f6ea8e3 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/ColorTexture.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.glrenderer; + +import com.hippo.ehviewer.util.Utils; + +// ColorTexture is a texture which fills the rectangle with the specified color. +public class ColorTexture implements Texture { + + private final int mColor; + private int mWidth; + private int mHeight; + + public ColorTexture(int color) { + mColor = color; + mWidth = 1; + mHeight = 1; + } + + @Override + public void draw(GLCanvas canvas, int x, int y) { + draw(canvas, x, y, mWidth, mHeight); + } + + @Override + public void draw(GLCanvas canvas, int x, int y, int w, int h) { + canvas.fillRect(x, y, w, h, mColor); + } + + @Override + public boolean isOpaque() { + return Utils.isOpaque(mColor); + } + + public void setSize(int width, int height) { + mWidth = width; + mHeight = height; + } + + @Override + public int getWidth() { + return mWidth; + } + + @Override + public int getHeight() { + return mHeight; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/ExtTexture.java b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/ExtTexture.java new file mode 100644 index 000000000..9b249cc4c --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/ExtTexture.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.glrenderer; + +// ExtTexture is a texture whose content comes from a external texture. +// Before drawing, setSize() should be called. +public class ExtTexture extends BasicTexture { + + private final int mTarget; + + public ExtTexture(GLCanvas canvas, int target) { + GLId glId = canvas.getGLId(); + mId = glId.generateTexture(); + mTarget = target; + } + + private void uploadToCanvas(GLCanvas canvas) { + canvas.setTextureParameters(this); + setAssociatedCanvas(canvas); + mState = STATE_LOADED; + } + + @Override + protected boolean onBind(GLCanvas canvas) { + if (!isLoaded()) { + uploadToCanvas(canvas); + } + + return true; + } + + @Override + public int getTarget() { + return mTarget; + } + + @Override + public boolean isOpaque() { + return true; + } + + @Override + public void yield() { + // we cannot free the texture because we have no backup. + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/FadeInTexture.java b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/FadeInTexture.java new file mode 100644 index 000000000..9d33ef7f3 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/FadeInTexture.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.glrenderer; + + +// FadeInTexture is a texture which begins with a color, then gradually animates +// into a given texture. +public class FadeInTexture extends FadeTexture implements Texture { + @SuppressWarnings("unused") + private static final String TAG = "FadeInTexture"; + + private final int mColor; + private final TiledTexture mTexture; + + public FadeInTexture(int color, TiledTexture texture) { + super(texture.getWidth(), texture.getHeight(), texture.isOpaque()); + mColor = color; + mTexture = texture; + } + + @Override + public void draw(GLCanvas canvas, int x, int y, int w, int h) { + if (isAnimating()) { + mTexture.drawMixed(canvas, mColor, getRatio(), x, y, w, h); + } else { + mTexture.draw(canvas, x, y, w, h); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/FadeOutTexture.java b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/FadeOutTexture.java new file mode 100644 index 000000000..230293ac3 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/FadeOutTexture.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.glrenderer; + + +// FadeOutTexture is a texture which begins with a given texture, then gradually animates +// into fading out totally. +public class FadeOutTexture extends FadeTexture { + @SuppressWarnings("unused") + private static final String TAG = "FadeOutTexture"; + + private final BasicTexture mTexture; + + public FadeOutTexture(BasicTexture texture) { + super(texture.getWidth(), texture.getHeight(), texture.isOpaque()); + mTexture = texture; + } + + @Override + public void draw(GLCanvas canvas, int x, int y, int w, int h) { + if (isAnimating()) { + canvas.save(GLCanvas.SAVE_FLAG_ALPHA); + canvas.setAlpha(getRatio()); + mTexture.draw(canvas, x, y, w, h); + canvas.restore(); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/FadeTexture.java b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/FadeTexture.java new file mode 100644 index 000000000..e8f177866 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/FadeTexture.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.glrenderer; + +import com.hippo.ehviewer.gallery.ui.AnimationTime; +import com.hippo.ehviewer.util.MathUtils; + +// FadeTexture is a texture which fades the given texture along the time. +public abstract class FadeTexture implements Texture { + @SuppressWarnings("unused") + private static final String TAG = "FadeTexture"; + + // The duration of the fading animation in milliseconds + public static final int DURATION = 180; + + private final long mStartTime; + private final int mWidth; + private final int mHeight; + private final boolean mIsOpaque; + private boolean mIsAnimating; + + public FadeTexture(int width, int height, boolean opaque) { + mWidth = width; + mHeight = height; + mIsOpaque = opaque; + mStartTime = now(); + mIsAnimating = true; + } + + @Override + public void draw(GLCanvas canvas, int x, int y) { + draw(canvas, x, y, mWidth, mHeight); + } + + @Override + public boolean isOpaque() { + return mIsOpaque; + } + + @Override + public int getWidth() { + return mWidth; + } + + @Override + public int getHeight() { + return mHeight; + } + + public boolean isAnimating() { + if (mIsAnimating) { + if (now() - mStartTime >= DURATION) { + mIsAnimating = false; + } + } + return mIsAnimating; + } + + protected float getRatio() { + float r = (float)(now() - mStartTime) / DURATION; + return MathUtils.clamp(1.0f - r, 0.0f, 1.0f); + } + + private long now() { + return AnimationTime.get(); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/GLCanvas.java b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/GLCanvas.java new file mode 100644 index 000000000..83cfd5685 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/GLCanvas.java @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.glrenderer; + +import android.graphics.Bitmap; +import android.graphics.Rect; +import android.graphics.RectF; + +// +// GLCanvas gives a convenient interface to draw using OpenGL. +// +// When a rectangle is specified in this interface, it means the region +// [x, x+width) * [y, y+height) +// +public interface GLCanvas { + + public GLId getGLId(); + + // Tells GLCanvas the size of the underlying GL surface. This should be + // called before first drawing and when the size of GL surface is changed. + // This is called by GLRoot and should not be called by the clients + // who only want to draw on the GLCanvas. Both width and height must be + // nonnegative. + public abstract void setSize(int width, int height); + + // Clear the drawing buffers. This should only be used by GLRoot. + public abstract void clearBuffer(); + + public abstract void clearBuffer(float[] argb); + + // Sets and gets the current alpha, alpha must be in [0, 1]. + public abstract void setAlpha(float alpha); + + public abstract float getAlpha(); + + // (current alpha) = (current alpha) * alpha + public abstract void multiplyAlpha(float alpha); + + // Change the current transform matrix. + public abstract void translate(float x, float y, float z); + + public abstract void translate(float x, float y); + + public abstract void scale(float sx, float sy, float sz); + + public abstract void rotate(float angle, float x, float y, float z); + + public abstract void multiplyMatrix(float[] mMatrix, int offset); + + // Pushes the configuration state (matrix, and alpha) onto + // a private stack. + public abstract void save(); + + // Same as save(), but only save those specified in saveFlags. + public abstract void save(int saveFlags); + + public static final int SAVE_FLAG_ALL = 0xFFFFFFFF; + public static final int SAVE_FLAG_ALPHA = 0x01; + public static final int SAVE_FLAG_MATRIX = 0x02; + + // Pops from the top of the stack as current configuration state (matrix, + // alpha, and clip). This call balances a previous call to save(), and is + // used to remove all modifications to the configuration state since the + // last save call. + public abstract void restore(); + + // Draws a line using the specified paint from (x1, y1) to (x2, y2). + // (Both end points are included). + public abstract void drawLine(float x1, float y1, float x2, float y2, GLPaint paint); + + // Draws a rectangle using the specified paint from (x1, y1) to (x2, y2). + // (Both end points are included). + public abstract void drawRect(float x1, float y1, float x2, float y2, GLPaint paint); + + // Fills the specified rectangle with the specified color. + public abstract void fillRect(float x, float y, float width, float height, int color); + + // Draws a texture to the specified rectangle. + public abstract void drawTexture( + BasicTexture texture, int x, int y, int width, int height); + + public abstract void drawMesh(BasicTexture tex, int x, int y, int xyBuffer, + int uvBuffer, int indexBuffer, int indexCount); + + // Draws the source rectangle part of the texture to the target rectangle. + public abstract void drawTexture(BasicTexture texture, RectF source, RectF target); + + // Draw a texture with a specified texture transform. + public abstract void drawTexture(BasicTexture texture, float[] mTextureTransform, + int x, int y, int w, int h); + + // Draw two textures to the specified rectangle. The actual texture used is + // from * (1 - ratio) + to * ratio + // The two textures must have the same size. + public abstract void drawMixed(BasicTexture from, int toColor, + float ratio, int x, int y, int w, int h); + + // Draw a region of a texture and a specified color to the specified + // rectangle. The actual color used is from * (1 - ratio) + to * ratio. + // The region of the texture is defined by parameter "src". The target + // rectangle is specified by parameter "target". + public abstract void drawMixed(BasicTexture from, int toColor, + float ratio, RectF src, RectF target); + + // Unloads the specified texture from the canvas. The resource allocated + // to draw the texture will be released. The specified texture will return + // to the unloaded state. This function should be called only from + // BasicTexture or its descendant + public abstract boolean unloadTexture(BasicTexture texture); + + // Delete the specified buffer object, similar to unloadTexture. + public abstract void deleteBuffer(int bufferId); + + // Delete the textures and buffers in GL side. This function should only be + // called in the GL thread. + public abstract void deleteRecycledResources(); + + // Dump statistics information and clear the counters. For debug only. + public abstract void dumpStatisticsAndClear(); + + public abstract void beginRenderTarget(RawTexture texture); + + public abstract void endRenderTarget(); + + /** + * Sets texture parameters to use GL_CLAMP_TO_EDGE for both + * GL_TEXTURE_WRAP_S and GL_TEXTURE_WRAP_T. Sets texture parameters to be + * GL_LINEAR for GL_TEXTURE_MIN_FILTER and GL_TEXTURE_MAG_FILTER. + * bindTexture() must be called prior to this. + * + * @param texture The texture to set parameters on. + */ + public abstract void setTextureParameters(BasicTexture texture); + + /** + * Initializes the texture to a size by calling texImage2D on it. + * + * @param texture The texture to initialize the size. + * @param format The texture format (e.g. GL_RGBA) + * @param type The texture type (e.g. GL_UNSIGNED_BYTE) + */ + public abstract void initializeTextureSize(BasicTexture texture, int format, int type); + + /** + * Initializes the texture to a size by calling texImage2D on it. + * + * @param texture The texture to initialize the size. + * @param bitmap The bitmap to initialize the bitmap with. + */ + public abstract void initializeTexture(BasicTexture texture, Bitmap bitmap); + + /** + * Calls glTexSubImage2D to upload a bitmap to the texture. + * + * @param texture The target texture to write to. + * @param xOffset Specifies a texel offset in the x direction within the + * texture array. + * @param yOffset Specifies a texel offset in the y direction within the + * texture array. + * @param format The texture format (e.g. GL_RGBA) + * @param type The texture type (e.g. GL_UNSIGNED_BYTE) + */ + public abstract void texSubImage2D(BasicTexture texture, int xOffset, int yOffset, + Bitmap bitmap, + int format, int type); + + /** + * Generates buffers and uploads the buffer data. + * + * @param buffer The buffer to upload + * @return The buffer ID that was generated. + */ + public abstract int uploadBuffer(java.nio.FloatBuffer buffer); + + /** + * Generates buffers and uploads the element array buffer data. + * + * @param buffer The buffer to upload + * @return The buffer ID that was generated. + */ + public abstract int uploadBuffer(java.nio.ByteBuffer buffer); + + /** + * After LightCycle makes GL calls, this method is called to restore the GL + * configuration to the one expected by GLCanvas. + */ + public abstract void recoverFromLightCycle(); + + /** + * Gets the bounds given by x, y, width, and height as well as the internal + * matrix state. There is no special handling for non-90-degree rotations. + * It only considers the lower-left and upper-right corners as the bounds. + * + * @param bounds The output bounds to write to. + * @param x The left side of the input rectangle. + * @param y The bottom of the input rectangle. + * @param width The width of the input rectangle. + * @param height The height of the input rectangle. + */ + public abstract void getBounds(Rect bounds, int x, int y, int width, int height); +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/GLES11Canvas.java b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/GLES11Canvas.java new file mode 100644 index 000000000..79fdf820b --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/GLES11Canvas.java @@ -0,0 +1,995 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.glrenderer; + +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.util.ArrayList; + +import javax.microedition.khronos.opengles.GL10; +import javax.microedition.khronos.opengles.GL11; +import javax.microedition.khronos.opengles.GL11Ext; +import javax.microedition.khronos.opengles.GL11ExtensionPack; + +import junit.framework.Assert; +import android.annotation.SuppressLint; +import android.graphics.Bitmap; +import android.graphics.Rect; +import android.graphics.RectF; +import android.opengl.GLU; +import android.opengl.GLUtils; +import android.opengl.Matrix; +import android.util.Log; + +import com.hippo.ehviewer.util.IntArray; +import com.hippo.ehviewer.util.Utils; + +public class GLES11Canvas implements GLCanvas { + private static final String TAG = "GLCanvasImp"; + + private static final float OPAQUE_ALPHA = 0.95f; + + private static final int OFFSET_FILL_RECT = 0; + private static final int OFFSET_DRAW_LINE = 4; + private static final int OFFSET_DRAW_RECT = 6; + private static final float[] BOX_COORDINATES = { + 0, 0, 1, 0, 0, 1, 1, 1, // used for filling a rectangle + 0, 0, 1, 1, // used for drawing a line + 0, 0, 0, 1, 1, 1, 1, 0}; // used for drawing the outline of a rectangle + + private final GL11 mGL; + + private final float mMatrixValues[] = new float[16]; + private final float mTextureMatrixValues[] = new float[16]; + + // The results of mapPoints are stored in this buffer, and the order is + // x1, y1, x2, y2. + private final float mMapPointsBuffer[] = new float[4]; + + private final float mTextureColor[] = new float[4]; + + private final int mBoxCoords; + + private final GLState mGLState; + private final ArrayList mTargetStack = new ArrayList(); + + private float mAlpha; + private final ArrayList mRestoreStack = new ArrayList(); + private ConfigState mRecycledRestoreAction; + + private final RectF mDrawTextureSourceRect = new RectF(); + private final RectF mDrawTextureTargetRect = new RectF(); + private final float[] mTempMatrix = new float[32]; + private final IntArray mUnboundTextures = new IntArray(); + private final IntArray mDeleteBuffers = new IntArray(); + private int mScreenWidth; + private int mScreenHeight; + private final boolean mBlendEnabled = true; + private final int mFrameBuffer[] = new int[1]; + private static float[] sCropRect = new float[4]; + + private RawTexture mTargetTexture; + + // Drawing statistics + int mCountDrawLine; + int mCountFillRect; + int mCountDrawMesh; + int mCountTextureRect; + int mCountTextureOES; + + private static GLId mGLId = new GLES11IdImpl(); + + public GLES11Canvas(GL11 gl) { + mGL = gl; + mGLState = new GLState(gl); + // First create an nio buffer, then create a VBO from it. + int size = BOX_COORDINATES.length * Float.SIZE / Byte.SIZE; + FloatBuffer xyBuffer = allocateDirectNativeOrderBuffer(size).asFloatBuffer(); + xyBuffer.put(BOX_COORDINATES, 0, BOX_COORDINATES.length).position(0); + + int[] name = new int[1]; + mGLId.glGenBuffers(1, name, 0); + mBoxCoords = name[0]; + + gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBoxCoords); + gl.glBufferData(GL11.GL_ARRAY_BUFFER, xyBuffer.capacity() * (Float.SIZE / Byte.SIZE), + xyBuffer, GL11.GL_STATIC_DRAW); + + gl.glVertexPointer(2, GL11.GL_FLOAT, 0, 0); + gl.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0); + + // Enable the texture coordinate array for Texture 1 + gl.glClientActiveTexture(GL11.GL_TEXTURE1); + gl.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0); + gl.glClientActiveTexture(GL11.GL_TEXTURE0); + gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY); + + // mMatrixValues and mAlpha will be initialized in setSize() + } + + @Override + public void setSize(int width, int height) { + Assert.assertTrue(width >= 0 && height >= 0); + + if (mTargetTexture == null) { + mScreenWidth = width; + mScreenHeight = height; + } + mAlpha = 1.0f; + + GL11 gl = mGL; + gl.glViewport(0, 0, width, height); + gl.glMatrixMode(GL11.GL_PROJECTION); + gl.glLoadIdentity(); + GLU.gluOrtho2D(gl, 0, width, 0, height); + + gl.glMatrixMode(GL11.GL_MODELVIEW); + gl.glLoadIdentity(); + + float matrix[] = mMatrixValues; + Matrix.setIdentityM(matrix, 0); + // to match the graphic coordinate system in android, we flip it vertically. + if (mTargetTexture == null) { + Matrix.translateM(matrix, 0, 0, height, 0); + Matrix.scaleM(matrix, 0, 1, -1, 1); + } + } + + @Override + public void setAlpha(float alpha) { + Assert.assertTrue(alpha >= 0 && alpha <= 1); + mAlpha = alpha; + } + + @Override + public float getAlpha() { + return mAlpha; + } + + @Override + public void multiplyAlpha(float alpha) { + Assert.assertTrue(alpha >= 0 && alpha <= 1); + mAlpha *= alpha; + } + + private static ByteBuffer allocateDirectNativeOrderBuffer(int size) { + return ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder()); + } + + @Override + public void drawRect(float x, float y, float width, float height, GLPaint paint) { + GL11 gl = mGL; + + mGLState.setColorMode(paint.getColor(), mAlpha); + mGLState.setLineWidth(paint.getLineWidth()); + + saveTransform(); + translate(x, y); + scale(width, height, 1); + + gl.glLoadMatrixf(mMatrixValues, 0); + gl.glDrawArrays(GL11.GL_LINE_LOOP, OFFSET_DRAW_RECT, 4); + + restoreTransform(); + mCountDrawLine++; + } + + @Override + public void drawLine(float x1, float y1, float x2, float y2, GLPaint paint) { + GL11 gl = mGL; + + mGLState.setColorMode(paint.getColor(), mAlpha); + mGLState.setLineWidth(paint.getLineWidth()); + + saveTransform(); + translate(x1, y1); + scale(x2 - x1, y2 - y1, 1); + + gl.glLoadMatrixf(mMatrixValues, 0); + gl.glDrawArrays(GL11.GL_LINE_STRIP, OFFSET_DRAW_LINE, 2); + + restoreTransform(); + mCountDrawLine++; + } + + @Override + public void fillRect(float x, float y, float width, float height, int color) { + mGLState.setColorMode(color, mAlpha); + GL11 gl = mGL; + + saveTransform(); + translate(x, y); + scale(width, height, 1); + + gl.glLoadMatrixf(mMatrixValues, 0); + gl.glDrawArrays(GL11.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, 4); + + restoreTransform(); + mCountFillRect++; + } + + @Override + public void translate(float x, float y, float z) { + Matrix.translateM(mMatrixValues, 0, x, y, z); + } + + // This is a faster version of translate(x, y, z) because + // (1) we knows z = 0, (2) we inline the Matrix.translateM call, + // (3) we unroll the loop + @Override + public void translate(float x, float y) { + float[] m = mMatrixValues; + m[12] += m[0] * x + m[4] * y; + m[13] += m[1] * x + m[5] * y; + m[14] += m[2] * x + m[6] * y; + m[15] += m[3] * x + m[7] * y; + } + + @Override + public void scale(float sx, float sy, float sz) { + Matrix.scaleM(mMatrixValues, 0, sx, sy, sz); + } + + @Override + public void rotate(float angle, float x, float y, float z) { + if (angle == 0) return; + float[] temp = mTempMatrix; + Matrix.setRotateM(temp, 0, angle, x, y, z); + Matrix.multiplyMM(temp, 16, mMatrixValues, 0, temp, 0); + System.arraycopy(temp, 16, mMatrixValues, 0, 16); + } + + @Override + public void multiplyMatrix(float matrix[], int offset) { + float[] temp = mTempMatrix; + Matrix.multiplyMM(temp, 0, mMatrixValues, 0, matrix, offset); + System.arraycopy(temp, 0, mMatrixValues, 0, 16); + } + + private void textureRect(float x, float y, float width, float height) { + GL11 gl = mGL; + + saveTransform(); + translate(x, y); + scale(width, height, 1); + + gl.glLoadMatrixf(mMatrixValues, 0); + gl.glDrawArrays(GL11.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, 4); + + restoreTransform(); + mCountTextureRect++; + } + + @Override + public void drawMesh(BasicTexture tex, int x, int y, int xyBuffer, + int uvBuffer, int indexBuffer, int indexCount) { + float alpha = mAlpha; + if (!bindTexture(tex)) return; + + mGLState.setBlendEnabled(mBlendEnabled + && (!tex.isOpaque() || alpha < OPAQUE_ALPHA)); + mGLState.setTextureAlpha(alpha); + + // Reset the texture matrix. We will set our own texture coordinates + // below. + setTextureCoords(0, 0, 1, 1); + + saveTransform(); + translate(x, y); + + mGL.glLoadMatrixf(mMatrixValues, 0); + + mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, xyBuffer); + mGL.glVertexPointer(2, GL11.GL_FLOAT, 0, 0); + + mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, uvBuffer); + mGL.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0); + + mGL.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, indexBuffer); + mGL.glDrawElements(GL11.GL_TRIANGLE_STRIP, + indexCount, GL11.GL_UNSIGNED_BYTE, 0); + + mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBoxCoords); + mGL.glVertexPointer(2, GL11.GL_FLOAT, 0, 0); + mGL.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0); + + restoreTransform(); + mCountDrawMesh++; + } + + // Transforms two points by the given matrix m. The result + // {x1', y1', x2', y2'} are stored in mMapPointsBuffer and also returned. + private float[] mapPoints(float m[], int x1, int y1, int x2, int y2) { + float[] r = mMapPointsBuffer; + + // Multiply m and (x1 y1 0 1) to produce (x3 y3 z3 w3). z3 is unused. + float x3 = m[0] * x1 + m[4] * y1 + m[12]; + float y3 = m[1] * x1 + m[5] * y1 + m[13]; + float w3 = m[3] * x1 + m[7] * y1 + m[15]; + r[0] = x3 / w3; + r[1] = y3 / w3; + + // Same for x2 y2. + float x4 = m[0] * x2 + m[4] * y2 + m[12]; + float y4 = m[1] * x2 + m[5] * y2 + m[13]; + float w4 = m[3] * x2 + m[7] * y2 + m[15]; + r[2] = x4 / w4; + r[3] = y4 / w4; + + return r; + } + + private void drawBoundTexture( + BasicTexture texture, int x, int y, int width, int height) { + // Test whether it has been rotated or flipped, if so, glDrawTexiOES + // won't work + if (isMatrixRotatedOrFlipped(mMatrixValues)) { + if (texture.hasBorder()) { + setTextureCoords( + 1.0f / texture.getTextureWidth(), + 1.0f / texture.getTextureHeight(), + (texture.getWidth() - 1.0f) / texture.getTextureWidth(), + (texture.getHeight() - 1.0f) / texture.getTextureHeight()); + } else { + setTextureCoords(0, 0, + (float) texture.getWidth() / texture.getTextureWidth(), + (float) texture.getHeight() / texture.getTextureHeight()); + } + textureRect(x, y, width, height); + } else { + // draw the rect from bottom-left to top-right + float points[] = mapPoints( + mMatrixValues, x, y + height, x + width, y); + x = (int) (points[0] + 0.5f); + y = (int) (points[1] + 0.5f); + width = (int) (points[2] + 0.5f) - x; + height = (int) (points[3] + 0.5f) - y; + if (width > 0 && height > 0) { + ((GL11Ext) mGL).glDrawTexiOES(x, y, 0, width, height); + mCountTextureOES++; + } + } + } + + @Override + public void drawTexture( + BasicTexture texture, int x, int y, int width, int height) { + drawTexture(texture, x, y, width, height, mAlpha); + } + + private void drawTexture(BasicTexture texture, + int x, int y, int width, int height, float alpha) { + if (width <= 0 || height <= 0) return; + + mGLState.setBlendEnabled(mBlendEnabled + && (!texture.isOpaque() || alpha < OPAQUE_ALPHA)); + if (!bindTexture(texture)) return; + mGLState.setTextureAlpha(alpha); + drawBoundTexture(texture, x, y, width, height); + } + + @Override + public void drawTexture(BasicTexture texture, RectF source, RectF target) { + if (target.width() <= 0 || target.height() <= 0) return; + + // Copy the input to avoid changing it. + mDrawTextureSourceRect.set(source); + mDrawTextureTargetRect.set(target); + source = mDrawTextureSourceRect; + target = mDrawTextureTargetRect; + + mGLState.setBlendEnabled(mBlendEnabled + && (!texture.isOpaque() || mAlpha < OPAQUE_ALPHA)); + if (!bindTexture(texture)) return; + convertCoordinate(source, target, texture); + setTextureCoords(source); + mGLState.setTextureAlpha(mAlpha); + textureRect(target.left, target.top, target.width(), target.height()); + } + + @Override + public void drawTexture(BasicTexture texture, float[] mTextureTransform, + int x, int y, int w, int h) { + mGLState.setBlendEnabled(mBlendEnabled + && (!texture.isOpaque() || mAlpha < OPAQUE_ALPHA)); + if (!bindTexture(texture)) return; + setTextureCoords(mTextureTransform); + mGLState.setTextureAlpha(mAlpha); + textureRect(x, y, w, h); + } + + // This function changes the source coordinate to the texture coordinates. + // It also clips the source and target coordinates if it is beyond the + // bound of the texture. + private static void convertCoordinate(RectF source, RectF target, + BasicTexture texture) { + + int width = texture.getWidth(); + int height = texture.getHeight(); + int texWidth = texture.getTextureWidth(); + int texHeight = texture.getTextureHeight(); + // Convert to texture coordinates + source.left /= texWidth; + source.right /= texWidth; + source.top /= texHeight; + source.bottom /= texHeight; + + // Clip if the rendering range is beyond the bound of the texture. + float xBound = (float) width / texWidth; + if (source.right > xBound) { + target.right = target.left + target.width() * + (xBound - source.left) / source.width(); + source.right = xBound; + } + float yBound = (float) height / texHeight; + if (source.bottom > yBound) { + target.bottom = target.top + target.height() * + (yBound - source.top) / source.height(); + source.bottom = yBound; + } + } + + @Override + public void drawMixed(BasicTexture from, + int toColor, float ratio, int x, int y, int w, int h) { + drawMixed(from, toColor, ratio, x, y, w, h, mAlpha); + } + + private boolean bindTexture(BasicTexture texture) { + if (!texture.onBind(this)) return false; + int target = texture.getTarget(); + mGLState.setTextureTarget(target); + mGL.glBindTexture(target, texture.getId()); + return true; + } + + private void setTextureColor(float r, float g, float b, float alpha) { + float[] color = mTextureColor; + color[0] = r; + color[1] = g; + color[2] = b; + color[3] = alpha; + } + + private void setMixedColor(int toColor, float ratio, float alpha) { + // + // The formula we want: + // alpha * ((1 - ratio) * from + ratio * to) + // + // The formula that GL supports is in the form of: + // combo * from + (1 - combo) * to * scale + // + // So, we have combo = alpha * (1 - ratio) + // and scale = alpha * ratio / (1 - combo) + // + float combo = alpha * (1 - ratio); + float scale = alpha * ratio / (1 - combo); + + // Specify the interpolation factor via the alpha component of + // GL_TEXTURE_ENV_COLORs. + // RGB component are get from toColor and will used as SRC1 + float colorScale = scale * (toColor >>> 24) / (0xff * 0xff); + setTextureColor(((toColor >>> 16) & 0xff) * colorScale, + ((toColor >>> 8) & 0xff) * colorScale, + (toColor & 0xff) * colorScale, combo); + GL11 gl = mGL; + gl.glTexEnvfv(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_COLOR, mTextureColor, 0); + + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_RGB, GL11.GL_INTERPOLATE); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_ALPHA, GL11.GL_INTERPOLATE); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC1_RGB, GL11.GL_CONSTANT); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND1_RGB, GL11.GL_SRC_COLOR); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC1_ALPHA, GL11.GL_CONSTANT); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND1_ALPHA, GL11.GL_SRC_ALPHA); + + // Wire up the interpolation factor for RGB. + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_RGB, GL11.GL_CONSTANT); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_RGB, GL11.GL_SRC_ALPHA); + + // Wire up the interpolation factor for alpha. + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_ALPHA, GL11.GL_CONSTANT); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_ALPHA, GL11.GL_SRC_ALPHA); + + } + + @Override + public void drawMixed(BasicTexture from, int toColor, float ratio, + RectF source, RectF target) { + if (target.width() <= 0 || target.height() <= 0) return; + + if (ratio <= 0.01f) { + drawTexture(from, source, target); + return; + } else if (ratio >= 1) { + fillRect(target.left, target.top, target.width(), target.height(), toColor); + return; + } + + float alpha = mAlpha; + + // Copy the input to avoid changing it. + mDrawTextureSourceRect.set(source); + mDrawTextureTargetRect.set(target); + source = mDrawTextureSourceRect; + target = mDrawTextureTargetRect; + + mGLState.setBlendEnabled(mBlendEnabled && (!from.isOpaque() + || !Utils.isOpaque(toColor) || alpha < OPAQUE_ALPHA)); + + if (!bindTexture(from)) return; + + // Interpolate the RGB and alpha values between both textures. + mGLState.setTexEnvMode(GL11.GL_COMBINE); + setMixedColor(toColor, ratio, alpha); + convertCoordinate(source, target, from); + setTextureCoords(source); + textureRect(target.left, target.top, target.width(), target.height()); + mGLState.setTexEnvMode(GL11.GL_REPLACE); + } + + private void drawMixed(BasicTexture from, int toColor, + float ratio, int x, int y, int width, int height, float alpha) { + // change from 0 to 0.01f to prevent getting divided by zero below + if (ratio <= 0.01f) { + drawTexture(from, x, y, width, height, alpha); + return; + } else if (ratio >= 1) { + fillRect(x, y, width, height, toColor); + return; + } + + mGLState.setBlendEnabled(mBlendEnabled && (!from.isOpaque() + || !Utils.isOpaque(toColor) || alpha < OPAQUE_ALPHA)); + + if (!bindTexture(from)) return; + + // Interpolate the RGB and alpha values between both textures. + mGLState.setTexEnvMode(GL11.GL_COMBINE); + setMixedColor(toColor, ratio, alpha); + + drawBoundTexture(from, x, y, width, height); + mGLState.setTexEnvMode(GL11.GL_REPLACE); + } + + // TODO: the code only work for 2D should get fixed for 3D or removed + private static final int MSKEW_X = 4; + private static final int MSKEW_Y = 1; + private static final int MSCALE_X = 0; + private static final int MSCALE_Y = 5; + + private static boolean isMatrixRotatedOrFlipped(float matrix[]) { + final float eps = 1e-5f; + return Math.abs(matrix[MSKEW_X]) > eps + || Math.abs(matrix[MSKEW_Y]) > eps + || matrix[MSCALE_X] < -eps + || matrix[MSCALE_Y] > eps; + } + + private static class GLState { + + private final GL11 mGL; + + private int mTexEnvMode = GL11.GL_REPLACE; + private float mTextureAlpha = 1.0f; + private int mTextureTarget = GL11.GL_TEXTURE_2D; + private boolean mBlendEnabled = true; + private float mLineWidth = 1.0f; + + public GLState(GL11 gl) { + mGL = gl; + + // Disable unused state + gl.glDisable(GL11.GL_LIGHTING); + + // Enable used features + gl.glEnable(GL11.GL_DITHER); + + gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); + gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY); + gl.glEnable(GL11.GL_TEXTURE_2D); + + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, + GL11.GL_TEXTURE_ENV_MODE, GL11.GL_REPLACE); + + // Set the background color + gl.glClearColor(0f, 0f, 0f, 0f); + + gl.glEnable(GL11.GL_BLEND); + gl.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_ALPHA); + + // We use 565 or 8888 format, so set the alignment to 2 bytes/pixel. + gl.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 2); + } + + public void setTexEnvMode(int mode) { + if (mTexEnvMode == mode) return; + mTexEnvMode = mode; + mGL.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, mode); + } + + public void setLineWidth(float width) { + if (mLineWidth == width) return; + mLineWidth = width; + mGL.glLineWidth(width); + } + + public void setTextureAlpha(float alpha) { + if (mTextureAlpha == alpha) return; + mTextureAlpha = alpha; + if (alpha >= OPAQUE_ALPHA) { + // The alpha is need for those texture without alpha channel + mGL.glColor4f(1, 1, 1, 1); + setTexEnvMode(GL11.GL_REPLACE); + } else { + mGL.glColor4f(alpha, alpha, alpha, alpha); + setTexEnvMode(GL11.GL_MODULATE); + } + } + + public void setColorMode(int color, float alpha) { + setBlendEnabled(!Utils.isOpaque(color) || alpha < OPAQUE_ALPHA); + + // Set mTextureAlpha to an invalid value, so that it will reset + // again in setTextureAlpha(float) later. + mTextureAlpha = -1.0f; + + setTextureTarget(0); + + float prealpha = (color >>> 24) * alpha * 65535f / 255f / 255f; + mGL.glColor4x( + Math.round(((color >> 16) & 0xFF) * prealpha), + Math.round(((color >> 8) & 0xFF) * prealpha), + Math.round((color & 0xFF) * prealpha), + Math.round(255 * prealpha)); + } + + // target is a value like GL_TEXTURE_2D. If target = 0, texturing is disabled. + public void setTextureTarget(int target) { + if (mTextureTarget == target) return; + if (mTextureTarget != 0) { + mGL.glDisable(mTextureTarget); + } + mTextureTarget = target; + if (mTextureTarget != 0) { + mGL.glEnable(mTextureTarget); + } + } + + public void setBlendEnabled(boolean enabled) { + if (mBlendEnabled == enabled) return; + mBlendEnabled = enabled; + if (enabled) { + mGL.glEnable(GL11.GL_BLEND); + } else { + mGL.glDisable(GL11.GL_BLEND); + } + } + } + + @Override + public void clearBuffer(float[] argb) { + if(argb != null && argb.length == 4) { + mGL.glClearColor(argb[1], argb[2], argb[3], argb[0]); + } else { + mGL.glClearColor(0, 0, 0, 1); + } + mGL.glClear(GL10.GL_COLOR_BUFFER_BIT); + } + + @Override + public void clearBuffer() { + clearBuffer(null); + } + + private void setTextureCoords(RectF source) { + setTextureCoords(source.left, source.top, source.right, source.bottom); + } + + private void setTextureCoords(float left, float top, + float right, float bottom) { + mGL.glMatrixMode(GL11.GL_TEXTURE); + mTextureMatrixValues[0] = right - left; + mTextureMatrixValues[5] = bottom - top; + mTextureMatrixValues[10] = 1; + mTextureMatrixValues[12] = left; + mTextureMatrixValues[13] = top; + mTextureMatrixValues[15] = 1; + mGL.glLoadMatrixf(mTextureMatrixValues, 0); + mGL.glMatrixMode(GL11.GL_MODELVIEW); + } + + private void setTextureCoords(float[] mTextureTransform) { + mGL.glMatrixMode(GL11.GL_TEXTURE); + mGL.glLoadMatrixf(mTextureTransform, 0); + mGL.glMatrixMode(GL11.GL_MODELVIEW); + } + + // unloadTexture and deleteBuffer can be called from the finalizer thread, + // so we synchronized on the mUnboundTextures object. + @Override + public boolean unloadTexture(BasicTexture t) { + synchronized (mUnboundTextures) { + if (!t.isLoaded()) return false; + mUnboundTextures.add(t.mId); + return true; + } + } + + @Override + public void deleteBuffer(int bufferId) { + synchronized (mUnboundTextures) { + mDeleteBuffers.add(bufferId); + } + } + + @Override + public void deleteRecycledResources() { + synchronized (mUnboundTextures) { + IntArray ids = mUnboundTextures; + if (ids.size() > 0) { + mGLId.glDeleteTextures(mGL, ids.size(), ids.getInternalArray(), 0); + ids.clear(); + } + + ids = mDeleteBuffers; + if (ids.size() > 0) { + mGLId.glDeleteBuffers(mGL, ids.size(), ids.getInternalArray(), 0); + ids.clear(); + } + } + } + + @Override + public void save() { + save(SAVE_FLAG_ALL); + } + + @Override + public void save(int saveFlags) { + ConfigState config = obtainRestoreConfig(); + + if ((saveFlags & SAVE_FLAG_ALPHA) != 0) { + config.mAlpha = mAlpha; + } else { + config.mAlpha = -1; + } + + if ((saveFlags & SAVE_FLAG_MATRIX) != 0) { + System.arraycopy(mMatrixValues, 0, config.mMatrix, 0, 16); + } else { + config.mMatrix[0] = Float.NEGATIVE_INFINITY; + } + + mRestoreStack.add(config); + } + + @Override + public void restore() { + if (mRestoreStack.isEmpty()) throw new IllegalStateException(); + ConfigState config = mRestoreStack.remove(mRestoreStack.size() - 1); + config.restore(this); + freeRestoreConfig(config); + } + + private void freeRestoreConfig(ConfigState action) { + action.mNextFree = mRecycledRestoreAction; + mRecycledRestoreAction = action; + } + + private ConfigState obtainRestoreConfig() { + if (mRecycledRestoreAction != null) { + ConfigState result = mRecycledRestoreAction; + mRecycledRestoreAction = result.mNextFree; + return result; + } + return new ConfigState(); + } + + private static class ConfigState { + float mAlpha; + float mMatrix[] = new float[16]; + ConfigState mNextFree; + + public void restore(GLES11Canvas canvas) { + if (mAlpha >= 0) canvas.setAlpha(mAlpha); + if (mMatrix[0] != Float.NEGATIVE_INFINITY) { + System.arraycopy(mMatrix, 0, canvas.mMatrixValues, 0, 16); + } + } + } + + @Override + @SuppressLint("DefaultLocale") + public void dumpStatisticsAndClear() { + String line = String.format( + "MESH:%d, TEX_OES:%d, TEX_RECT:%d, FILL_RECT:%d, LINE:%d", + mCountDrawMesh, mCountTextureRect, mCountTextureOES, + mCountFillRect, mCountDrawLine); + mCountDrawMesh = 0; + mCountTextureRect = 0; + mCountTextureOES = 0; + mCountFillRect = 0; + mCountDrawLine = 0; + Log.d(TAG, line); + } + + private void saveTransform() { + System.arraycopy(mMatrixValues, 0, mTempMatrix, 0, 16); + } + + private void restoreTransform() { + System.arraycopy(mTempMatrix, 0, mMatrixValues, 0, 16); + } + + private void setRenderTarget(RawTexture texture) { + GL11ExtensionPack gl11ep = (GL11ExtensionPack) mGL; + + if (mTargetTexture == null && texture != null) { + mGLId.glGenBuffers(1, mFrameBuffer, 0); + gl11ep.glBindFramebufferOES( + GL11ExtensionPack.GL_FRAMEBUFFER_OES, mFrameBuffer[0]); + } + if (mTargetTexture != null && texture == null) { + gl11ep.glBindFramebufferOES(GL11ExtensionPack.GL_FRAMEBUFFER_OES, 0); + gl11ep.glDeleteFramebuffersOES(1, mFrameBuffer, 0); + } + + mTargetTexture = texture; + if (texture == null) { + setSize(mScreenWidth, mScreenHeight); + } else { + setSize(texture.getWidth(), texture.getHeight()); + + if (!texture.isLoaded()) texture.prepare(this); + + gl11ep.glFramebufferTexture2DOES( + GL11ExtensionPack.GL_FRAMEBUFFER_OES, + GL11ExtensionPack.GL_COLOR_ATTACHMENT0_OES, + GL11.GL_TEXTURE_2D, texture.getId(), 0); + + checkFramebufferStatus(gl11ep); + } + } + + @Override + public void endRenderTarget() { + RawTexture texture = mTargetStack.remove(mTargetStack.size() - 1); + setRenderTarget(texture); + restore(); // restore matrix and alpha + } + + @Override + public void beginRenderTarget(RawTexture texture) { + save(); // save matrix and alpha + mTargetStack.add(mTargetTexture); + setRenderTarget(texture); + } + + private static void checkFramebufferStatus(GL11ExtensionPack gl11ep) { + int status = gl11ep.glCheckFramebufferStatusOES(GL11ExtensionPack.GL_FRAMEBUFFER_OES); + if (status != GL11ExtensionPack.GL_FRAMEBUFFER_COMPLETE_OES) { + String msg = ""; + switch (status) { + case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_FORMATS_OES: + msg = "FRAMEBUFFER_FORMATS"; + break; + case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_OES: + msg = "FRAMEBUFFER_ATTACHMENT"; + break; + case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_OES: + msg = "FRAMEBUFFER_MISSING_ATTACHMENT"; + break; + case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_OES: + msg = "FRAMEBUFFER_DRAW_BUFFER"; + break; + case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_OES: + msg = "FRAMEBUFFER_READ_BUFFER"; + break; + case GL11ExtensionPack.GL_FRAMEBUFFER_UNSUPPORTED_OES: + msg = "FRAMEBUFFER_UNSUPPORTED"; + break; + case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_OES: + msg = "FRAMEBUFFER_INCOMPLETE_DIMENSIONS"; + break; + } + throw new RuntimeException(msg + ":" + Integer.toHexString(status)); + } + } + + @Override + public void setTextureParameters(BasicTexture texture) { + int width = texture.getWidth(); + int height = texture.getHeight(); + // Define a vertically flipped crop rectangle for OES_draw_texture. + // The four values in sCropRect are: left, bottom, width, and + // height. Negative value of width or height means flip. + sCropRect[0] = 0; + sCropRect[1] = height; + sCropRect[2] = width; + sCropRect[3] = -height; + + // Set texture parameters. + int target = texture.getTarget(); + mGL.glBindTexture(target, texture.getId()); + mGL.glTexParameterfv(target, GL11Ext.GL_TEXTURE_CROP_RECT_OES, sCropRect, 0); + mGL.glTexParameteri(target, GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP_TO_EDGE); + mGL.glTexParameteri(target, GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP_TO_EDGE); + mGL.glTexParameterf(target, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); + mGL.glTexParameterf(target, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); + } + + @Override + public void initializeTextureSize(BasicTexture texture, int format, int type) { + int target = texture.getTarget(); + mGL.glBindTexture(target, texture.getId()); + int width = texture.getTextureWidth(); + int height = texture.getTextureHeight(); + mGL.glTexImage2D(target, 0, format, width, height, 0, format, type, null); + } + + @Override + public void initializeTexture(BasicTexture texture, Bitmap bitmap) { + int target = texture.getTarget(); + mGL.glBindTexture(target, texture.getId()); + GLUtils.texImage2D(target, 0, bitmap, 0); + } + + @Override + public void texSubImage2D(BasicTexture texture, int xOffset, int yOffset, Bitmap bitmap, + int format, int type) { + int target = texture.getTarget(); + mGL.glBindTexture(target, texture.getId()); + GLUtils.texSubImage2D(target, 0, xOffset, yOffset, bitmap, format, type); + } + + @Override + public int uploadBuffer(FloatBuffer buf) { + return uploadBuffer(buf, Float.SIZE / Byte.SIZE); + } + + @Override + public int uploadBuffer(ByteBuffer buf) { + return uploadBuffer(buf, 1); + } + + private int uploadBuffer(Buffer buf, int elementSize) { + int[] bufferIds = new int[1]; + mGLId.glGenBuffers(bufferIds.length, bufferIds, 0); + int bufferId = bufferIds[0]; + mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, bufferId); + mGL.glBufferData(GL11.GL_ARRAY_BUFFER, buf.capacity() * elementSize, buf, + GL11.GL_STATIC_DRAW); + return bufferId; + } + + @Override + public void recoverFromLightCycle() { + // This is only required for GLES20 + } + + @Override + public void getBounds(Rect bounds, int x, int y, int width, int height) { + // This is only required for GLES20 + } + + @Override + public GLId getGLId() { + return mGLId; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/GLES11IdImpl.java b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/GLES11IdImpl.java new file mode 100644 index 000000000..ab4130fe4 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/GLES11IdImpl.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hippo.ehviewer.gallery.glrenderer; + +import javax.microedition.khronos.opengles.GL11; +import javax.microedition.khronos.opengles.GL11ExtensionPack; + +/** + * Open GL ES 1.1 implementation for generating and destroying texture IDs and + * buffer IDs + */ +public class GLES11IdImpl implements GLId { + private static int sNextId = 1; + // Mutex for sNextId + private static Object sLock = new Object(); + + @Override + public int generateTexture() { + synchronized (sLock) { + return sNextId++; + } + } + + @Override + public void glGenBuffers(int n, int[] buffers, int offset) { + synchronized (sLock) { + while (n-- > 0) { + buffers[offset + n] = sNextId++; + } + } + } + + @Override + public void glDeleteTextures(GL11 gl, int n, int[] textures, int offset) { + synchronized (sLock) { + gl.glDeleteTextures(n, textures, offset); + } + } + + @Override + public void glDeleteBuffers(GL11 gl, int n, int[] buffers, int offset) { + synchronized (sLock) { + gl.glDeleteBuffers(n, buffers, offset); + } + } + + @Override + public void glDeleteFramebuffers(GL11ExtensionPack gl11ep, int n, int[] buffers, int offset) { + synchronized (sLock) { + gl11ep.glDeleteFramebuffersOES(n, buffers, offset); + } + } + + +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/GLES20Canvas.java b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/GLES20Canvas.java new file mode 100644 index 000000000..8bbbdea48 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/GLES20Canvas.java @@ -0,0 +1,1011 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hippo.ehviewer.gallery.glrenderer; + +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.util.ArrayList; +import java.util.Arrays; + +import android.annotation.SuppressLint; +import android.graphics.Bitmap; +import android.graphics.Rect; +import android.graphics.RectF; +import android.opengl.GLES20; +import android.opengl.GLUtils; +import android.opengl.Matrix; +import android.util.Log; + +import com.hippo.ehviewer.util.IntArray; + +public class GLES20Canvas implements GLCanvas { + // ************** Constants ********************** + private static final String TAG = GLES20Canvas.class.getSimpleName(); + private static final int FLOAT_SIZE = Float.SIZE / Byte.SIZE; + private static final float OPAQUE_ALPHA = 0.95f; + + private static final int COORDS_PER_VERTEX = 2; + private static final int VERTEX_STRIDE = COORDS_PER_VERTEX * FLOAT_SIZE; + + private static final int COUNT_FILL_VERTEX = 4; + private static final int COUNT_LINE_VERTEX = 2; + private static final int COUNT_RECT_VERTEX = 4; + private static final int OFFSET_FILL_RECT = 0; + private static final int OFFSET_DRAW_LINE = OFFSET_FILL_RECT + COUNT_FILL_VERTEX; + private static final int OFFSET_DRAW_RECT = OFFSET_DRAW_LINE + COUNT_LINE_VERTEX; + + private static final float[] BOX_COORDINATES = { + 0, 0, // Fill rectangle + 1, 0, + 0, 1, + 1, 1, + 0, 0, // Draw line + 1, 1, + 0, 0, // Draw rectangle outline + 0, 1, + 1, 1, + 1, 0, + }; + + private static final float[] BOUNDS_COORDINATES = { + 0, 0, 0, 1, + 1, 1, 0, 1, + }; + + private static final String POSITION_ATTRIBUTE = "aPosition"; + private static final String COLOR_UNIFORM = "uColor"; + private static final String MATRIX_UNIFORM = "uMatrix"; + private static final String TEXTURE_MATRIX_UNIFORM = "uTextureMatrix"; + private static final String TEXTURE_SAMPLER_UNIFORM = "uTextureSampler"; + private static final String ALPHA_UNIFORM = "uAlpha"; + private static final String TEXTURE_COORD_ATTRIBUTE = "aTextureCoordinate"; + + private static final String DRAW_VERTEX_SHADER = "" + + "uniform mat4 " + MATRIX_UNIFORM + ";\n" + + "attribute vec2 " + POSITION_ATTRIBUTE + ";\n" + + "void main() {\n" + + " vec4 pos = vec4(" + POSITION_ATTRIBUTE + ", 0.0, 1.0);\n" + + " gl_Position = " + MATRIX_UNIFORM + " * pos;\n" + + "}\n"; + + private static final String DRAW_FRAGMENT_SHADER = "" + + "precision mediump float;\n" + + "uniform vec4 " + COLOR_UNIFORM + ";\n" + + "void main() {\n" + + " gl_FragColor = " + COLOR_UNIFORM + ";\n" + + "}\n"; + + private static final String TEXTURE_VERTEX_SHADER = "" + + "uniform mat4 " + MATRIX_UNIFORM + ";\n" + + "uniform mat4 " + TEXTURE_MATRIX_UNIFORM + ";\n" + + "attribute vec2 " + POSITION_ATTRIBUTE + ";\n" + + "varying vec2 vTextureCoord;\n" + + "void main() {\n" + + " vec4 pos = vec4(" + POSITION_ATTRIBUTE + ", 0.0, 1.0);\n" + + " gl_Position = " + MATRIX_UNIFORM + " * pos;\n" + + " vTextureCoord = (" + TEXTURE_MATRIX_UNIFORM + " * pos).xy;\n" + + "}\n"; + + private static final String MESH_VERTEX_SHADER = "" + + "uniform mat4 " + MATRIX_UNIFORM + ";\n" + + "attribute vec2 " + POSITION_ATTRIBUTE + ";\n" + + "attribute vec2 " + TEXTURE_COORD_ATTRIBUTE + ";\n" + + "varying vec2 vTextureCoord;\n" + + "void main() {\n" + + " vec4 pos = vec4(" + POSITION_ATTRIBUTE + ", 0.0, 1.0);\n" + + " gl_Position = " + MATRIX_UNIFORM + " * pos;\n" + + " vTextureCoord = " + TEXTURE_COORD_ATTRIBUTE + ";\n" + + "}\n"; + + private static final String TEXTURE_FRAGMENT_SHADER = "" + + "precision mediump float;\n" + + "varying vec2 vTextureCoord;\n" + + "uniform float " + ALPHA_UNIFORM + ";\n" + + "uniform sampler2D " + TEXTURE_SAMPLER_UNIFORM + ";\n" + + "void main() {\n" + + " gl_FragColor = texture2D(" + TEXTURE_SAMPLER_UNIFORM + ", vTextureCoord);\n" + + " gl_FragColor *= " + ALPHA_UNIFORM + ";\n" + + "}\n"; + + private static final String OES_TEXTURE_FRAGMENT_SHADER = "" + + "#extension GL_OES_EGL_image_external : require\n" + + "precision mediump float;\n" + + "varying vec2 vTextureCoord;\n" + + "uniform float " + ALPHA_UNIFORM + ";\n" + + "uniform samplerExternalOES " + TEXTURE_SAMPLER_UNIFORM + ";\n" + + "void main() {\n" + + " gl_FragColor = texture2D(" + TEXTURE_SAMPLER_UNIFORM + ", vTextureCoord);\n" + + " gl_FragColor *= " + ALPHA_UNIFORM + ";\n" + + "}\n"; + + private static final int INITIAL_RESTORE_STATE_SIZE = 8; + private static final int MATRIX_SIZE = 16; + + // Keep track of restore state + private float[] mMatrices = new float[INITIAL_RESTORE_STATE_SIZE * MATRIX_SIZE]; + private float[] mAlphas = new float[INITIAL_RESTORE_STATE_SIZE]; + private final IntArray mSaveFlags = new IntArray(); + + private int mCurrentAlphaIndex = 0; + private int mCurrentMatrixIndex = 0; + + // Viewport size + private int mWidth; + private int mHeight; + + // Projection matrix + private final float[] mProjectionMatrix = new float[MATRIX_SIZE]; + + // Screen size for when we aren't bound to a texture + private int mScreenWidth; + private int mScreenHeight; + + // GL programs + private final int mDrawProgram; + private final int mTextureProgram; + private final int mOesTextureProgram; + private final int mMeshProgram; + + // GL buffer containing BOX_COORDINATES + private final int mBoxCoordinates; + + // Handle indices -- common + private static final int INDEX_POSITION = 0; + private static final int INDEX_MATRIX = 1; + + // Handle indices -- draw + private static final int INDEX_COLOR = 2; + + // Handle indices -- texture + private static final int INDEX_TEXTURE_MATRIX = 2; + private static final int INDEX_TEXTURE_SAMPLER = 3; + private static final int INDEX_ALPHA = 4; + + // Handle indices -- mesh + private static final int INDEX_TEXTURE_COORD = 2; + + private abstract static class ShaderParameter { + public int handle; + protected final String mName; + + public ShaderParameter(String name) { + mName = name; + } + + public abstract void loadHandle(int program); + } + + private static class UniformShaderParameter extends ShaderParameter { + public UniformShaderParameter(String name) { + super(name); + } + + @Override + public void loadHandle(int program) { + handle = GLES20.glGetUniformLocation(program, mName); + checkError(); + } + } + + private static class AttributeShaderParameter extends ShaderParameter { + public AttributeShaderParameter(String name) { + super(name); + } + + @Override + public void loadHandle(int program) { + handle = GLES20.glGetAttribLocation(program, mName); + checkError(); + } + } + + ShaderParameter[] mDrawParameters = { + new AttributeShaderParameter(POSITION_ATTRIBUTE), // INDEX_POSITION + new UniformShaderParameter(MATRIX_UNIFORM), // INDEX_MATRIX + new UniformShaderParameter(COLOR_UNIFORM), // INDEX_COLOR + }; + ShaderParameter[] mTextureParameters = { + new AttributeShaderParameter(POSITION_ATTRIBUTE), // INDEX_POSITION + new UniformShaderParameter(MATRIX_UNIFORM), // INDEX_MATRIX + new UniformShaderParameter(TEXTURE_MATRIX_UNIFORM), // INDEX_TEXTURE_MATRIX + new UniformShaderParameter(TEXTURE_SAMPLER_UNIFORM), // INDEX_TEXTURE_SAMPLER + new UniformShaderParameter(ALPHA_UNIFORM), // INDEX_ALPHA + }; + ShaderParameter[] mOesTextureParameters = { + new AttributeShaderParameter(POSITION_ATTRIBUTE), // INDEX_POSITION + new UniformShaderParameter(MATRIX_UNIFORM), // INDEX_MATRIX + new UniformShaderParameter(TEXTURE_MATRIX_UNIFORM), // INDEX_TEXTURE_MATRIX + new UniformShaderParameter(TEXTURE_SAMPLER_UNIFORM), // INDEX_TEXTURE_SAMPLER + new UniformShaderParameter(ALPHA_UNIFORM), // INDEX_ALPHA + }; + ShaderParameter[] mMeshParameters = { + new AttributeShaderParameter(POSITION_ATTRIBUTE), // INDEX_POSITION + new UniformShaderParameter(MATRIX_UNIFORM), // INDEX_MATRIX + new AttributeShaderParameter(TEXTURE_COORD_ATTRIBUTE), // INDEX_TEXTURE_COORD + new UniformShaderParameter(TEXTURE_SAMPLER_UNIFORM), // INDEX_TEXTURE_SAMPLER + new UniformShaderParameter(ALPHA_UNIFORM), // INDEX_ALPHA + }; + + private final IntArray mUnboundTextures = new IntArray(); + private final IntArray mDeleteBuffers = new IntArray(); + + // Keep track of statistics for debugging + private int mCountDrawMesh = 0; + private int mCountTextureRect = 0; + private int mCountFillRect = 0; + private int mCountDrawLine = 0; + + // Buffer for framebuffer IDs -- we keep track so we can switch the attached + // texture. + private final int[] mFrameBuffer = new int[1]; + + // Bound textures. + private final ArrayList mTargetTextures = new ArrayList(); + + // Temporary variables used within calculations + private final float[] mTempMatrix = new float[32]; + private final float[] mTempColor = new float[4]; + private final RectF mTempSourceRect = new RectF(); + private final RectF mTempTargetRect = new RectF(); + private final float[] mTempTextureMatrix = new float[MATRIX_SIZE]; + private final int[] mTempIntArray = new int[1]; + + private static final GLId mGLId = new GLES20IdImpl(); + + public GLES20Canvas() { + Matrix.setIdentityM(mTempTextureMatrix, 0); + Matrix.setIdentityM(mMatrices, mCurrentMatrixIndex); + mAlphas[mCurrentAlphaIndex] = 1f; + mTargetTextures.add(null); + + FloatBuffer boxBuffer = createBuffer(BOX_COORDINATES); + mBoxCoordinates = uploadBuffer(boxBuffer); + + int drawVertexShader = loadShader(GLES20.GL_VERTEX_SHADER, DRAW_VERTEX_SHADER); + int textureVertexShader = loadShader(GLES20.GL_VERTEX_SHADER, TEXTURE_VERTEX_SHADER); + int meshVertexShader = loadShader(GLES20.GL_VERTEX_SHADER, MESH_VERTEX_SHADER); + int drawFragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, DRAW_FRAGMENT_SHADER); + int textureFragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, TEXTURE_FRAGMENT_SHADER); + int oesTextureFragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, + OES_TEXTURE_FRAGMENT_SHADER); + + mDrawProgram = assembleProgram(drawVertexShader, drawFragmentShader, mDrawParameters); + mTextureProgram = assembleProgram(textureVertexShader, textureFragmentShader, + mTextureParameters); + mOesTextureProgram = assembleProgram(textureVertexShader, oesTextureFragmentShader, + mOesTextureParameters); + mMeshProgram = assembleProgram(meshVertexShader, textureFragmentShader, mMeshParameters); + GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA); + checkError(); + } + + private static FloatBuffer createBuffer(float[] values) { + // First create an nio buffer, then create a VBO from it. + int size = values.length * FLOAT_SIZE; + FloatBuffer buffer = ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + buffer.put(values, 0, values.length).position(0); + return buffer; + } + + private int assembleProgram(int vertexShader, int fragmentShader, ShaderParameter[] params) { + int program = GLES20.glCreateProgram(); + checkError(); + if (program == 0) { + throw new RuntimeException("Cannot create GL program: " + GLES20.glGetError()); + } + GLES20.glAttachShader(program, vertexShader); + checkError(); + GLES20.glAttachShader(program, fragmentShader); + checkError(); + GLES20.glLinkProgram(program); + checkError(); + int[] mLinkStatus = mTempIntArray; + GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, mLinkStatus, 0); + if (mLinkStatus[0] != GLES20.GL_TRUE) { + Log.e(TAG, "Could not link program: "); + Log.e(TAG, GLES20.glGetProgramInfoLog(program)); + GLES20.glDeleteProgram(program); + program = 0; + } + for (int i = 0; i < params.length; i++) { + params[i].loadHandle(program); + } + return program; + } + + private static int loadShader(int type, String shaderCode) { + // create a vertex shader type (GLES20.GL_VERTEX_SHADER) + // or a fragment shader type (GLES20.GL_FRAGMENT_SHADER) + int shader = GLES20.glCreateShader(type); + + // add the source code to the shader and compile it + GLES20.glShaderSource(shader, shaderCode); + checkError(); + GLES20.glCompileShader(shader); + checkError(); + + return shader; + } + + @Override + public void setSize(int width, int height) { + mWidth = width; + mHeight = height; + GLES20.glViewport(0, 0, mWidth, mHeight); + checkError(); + Matrix.setIdentityM(mMatrices, mCurrentMatrixIndex); + Matrix.orthoM(mProjectionMatrix, 0, 0, width, 0, height, -1, 1); + if (getTargetTexture() == null) { + mScreenWidth = width; + mScreenHeight = height; + Matrix.translateM(mMatrices, mCurrentMatrixIndex, 0, height, 0); + Matrix.scaleM(mMatrices, mCurrentMatrixIndex, 1, -1, 1); + } + } + + @Override + public void clearBuffer() { + GLES20.glClearColor(0f, 0f, 0f, 1f); + checkError(); + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); + checkError(); + } + + @Override + public void clearBuffer(float[] argb) { + GLES20.glClearColor(argb[1], argb[2], argb[3], argb[0]); + checkError(); + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); + checkError(); + } + + @Override + public float getAlpha() { + return mAlphas[mCurrentAlphaIndex]; + } + + @Override + public void setAlpha(float alpha) { + mAlphas[mCurrentAlphaIndex] = alpha; + } + + @Override + public void multiplyAlpha(float alpha) { + setAlpha(getAlpha() * alpha); + } + + @Override + public void translate(float x, float y, float z) { + Matrix.translateM(mMatrices, mCurrentMatrixIndex, x, y, z); + } + + // This is a faster version of translate(x, y, z) because + // (1) we knows z = 0, (2) we inline the Matrix.translateM call, + // (3) we unroll the loop + @Override + public void translate(float x, float y) { + int index = mCurrentMatrixIndex; + float[] m = mMatrices; + m[index + 12] += m[index + 0] * x + m[index + 4] * y; + m[index + 13] += m[index + 1] * x + m[index + 5] * y; + m[index + 14] += m[index + 2] * x + m[index + 6] * y; + m[index + 15] += m[index + 3] * x + m[index + 7] * y; + } + + @Override + public void scale(float sx, float sy, float sz) { + Matrix.scaleM(mMatrices, mCurrentMatrixIndex, sx, sy, sz); + } + + @Override + public void rotate(float angle, float x, float y, float z) { + if (angle == 0f) { + return; + } + float[] temp = mTempMatrix; + Matrix.setRotateM(temp, 0, angle, x, y, z); + float[] matrix = mMatrices; + int index = mCurrentMatrixIndex; + Matrix.multiplyMM(temp, MATRIX_SIZE, matrix, index, temp, 0); + System.arraycopy(temp, MATRIX_SIZE, matrix, index, MATRIX_SIZE); + } + + @Override + public void multiplyMatrix(float[] matrix, int offset) { + float[] temp = mTempMatrix; + float[] currentMatrix = mMatrices; + int index = mCurrentMatrixIndex; + Matrix.multiplyMM(temp, 0, currentMatrix, index, matrix, offset); + System.arraycopy(temp, 0, currentMatrix, index, 16); + } + + @Override + public void save() { + save(SAVE_FLAG_ALL); + } + + @Override + public void save(int saveFlags) { + boolean saveAlpha = (saveFlags & SAVE_FLAG_ALPHA) == SAVE_FLAG_ALPHA; + if (saveAlpha) { + float currentAlpha = getAlpha(); + mCurrentAlphaIndex++; + if (mAlphas.length <= mCurrentAlphaIndex) { + mAlphas = Arrays.copyOf(mAlphas, mAlphas.length * 2); + } + mAlphas[mCurrentAlphaIndex] = currentAlpha; + } + boolean saveMatrix = (saveFlags & SAVE_FLAG_MATRIX) == SAVE_FLAG_MATRIX; + if (saveMatrix) { + int currentIndex = mCurrentMatrixIndex; + mCurrentMatrixIndex += MATRIX_SIZE; + if (mMatrices.length <= mCurrentMatrixIndex) { + mMatrices = Arrays.copyOf(mMatrices, mMatrices.length * 2); + } + System.arraycopy(mMatrices, currentIndex, mMatrices, mCurrentMatrixIndex, MATRIX_SIZE); + } + mSaveFlags.add(saveFlags); + } + + @Override + public void restore() { + int restoreFlags = mSaveFlags.removeLast(); + boolean restoreAlpha = (restoreFlags & SAVE_FLAG_ALPHA) == SAVE_FLAG_ALPHA; + if (restoreAlpha) { + mCurrentAlphaIndex--; + } + boolean restoreMatrix = (restoreFlags & SAVE_FLAG_MATRIX) == SAVE_FLAG_MATRIX; + if (restoreMatrix) { + mCurrentMatrixIndex -= MATRIX_SIZE; + } + } + + @Override + public void drawLine(float x1, float y1, float x2, float y2, GLPaint paint) { + draw(GLES20.GL_LINE_STRIP, OFFSET_DRAW_LINE, COUNT_LINE_VERTEX, x1, y1, x2 - x1, y2 - y1, + paint); + mCountDrawLine++; + } + + @Override + public void drawRect(float x, float y, float width, float height, GLPaint paint) { + draw(GLES20.GL_LINE_LOOP, OFFSET_DRAW_RECT, COUNT_RECT_VERTEX, x, y, width, height, paint); + mCountDrawLine++; + } + + private void draw(int type, int offset, int count, float x, float y, float width, float height, + GLPaint paint) { + draw(type, offset, count, x, y, width, height, paint.getColor(), paint.getLineWidth()); + } + + private void draw(int type, int offset, int count, float x, float y, float width, float height, + int color, float lineWidth) { + prepareDraw(offset, color, lineWidth); + draw(mDrawParameters, type, count, x, y, width, height); + } + + private void prepareDraw(int offset, int color, float lineWidth) { + GLES20.glUseProgram(mDrawProgram); + checkError(); + if (lineWidth > 0) { + GLES20.glLineWidth(lineWidth); + checkError(); + } + float[] colorArray = getColor(color); + boolean blendingEnabled = (colorArray[3] < 1f); + enableBlending(blendingEnabled); + if (blendingEnabled) { + GLES20.glBlendColor(colorArray[0], colorArray[1], colorArray[2], colorArray[3]); + checkError(); + } + + GLES20.glUniform4fv(mDrawParameters[INDEX_COLOR].handle, 1, colorArray, 0); + setPosition(mDrawParameters, offset); + checkError(); + } + + private float[] getColor(int color) { + float alpha = ((color >>> 24) & 0xFF) / 255f * getAlpha(); + float red = ((color >>> 16) & 0xFF) / 255f * alpha; + float green = ((color >>> 8) & 0xFF) / 255f * alpha; + float blue = (color & 0xFF) / 255f * alpha; + mTempColor[0] = red; + mTempColor[1] = green; + mTempColor[2] = blue; + mTempColor[3] = alpha; + return mTempColor; + } + + private void enableBlending(boolean enableBlending) { + if (enableBlending) { + GLES20.glEnable(GLES20.GL_BLEND); + checkError(); + } else { + GLES20.glDisable(GLES20.GL_BLEND); + checkError(); + } + } + + private void setPosition(ShaderParameter[] params, int offset) { + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mBoxCoordinates); + checkError(); + GLES20.glVertexAttribPointer(params[INDEX_POSITION].handle, COORDS_PER_VERTEX, + GLES20.GL_FLOAT, false, VERTEX_STRIDE, offset * VERTEX_STRIDE); + checkError(); + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + checkError(); + } + + private void draw(ShaderParameter[] params, int type, int count, float x, float y, float width, + float height) { + setMatrix(params, x, y, width, height); + int positionHandle = params[INDEX_POSITION].handle; + GLES20.glEnableVertexAttribArray(positionHandle); + checkError(); + GLES20.glDrawArrays(type, 0, count); + checkError(); + GLES20.glDisableVertexAttribArray(positionHandle); + checkError(); + } + + private void setMatrix(ShaderParameter[] params, float x, float y, float width, float height) { + Matrix.translateM(mTempMatrix, 0, mMatrices, mCurrentMatrixIndex, x, y, 0f); + Matrix.scaleM(mTempMatrix, 0, width, height, 1f); + Matrix.multiplyMM(mTempMatrix, MATRIX_SIZE, mProjectionMatrix, 0, mTempMatrix, 0); + GLES20.glUniformMatrix4fv(params[INDEX_MATRIX].handle, 1, false, mTempMatrix, MATRIX_SIZE); + checkError(); + } + + @Override + public void fillRect(float x, float y, float width, float height, int color) { + draw(GLES20.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, COUNT_FILL_VERTEX, x, y, width, height, + color, 0f); + mCountFillRect++; + } + + @Override + public void drawTexture(BasicTexture texture, int x, int y, int width, int height) { + if (width <= 0 || height <= 0) { + return; + } + copyTextureCoordinates(texture, mTempSourceRect); + mTempTargetRect.set(x, y, x + width, y + height); + convertCoordinate(mTempSourceRect, mTempTargetRect, texture); + drawTextureRect(texture, mTempSourceRect, mTempTargetRect); + } + + private static void copyTextureCoordinates(BasicTexture texture, RectF outRect) { + int left = 0; + int top = 0; + int right = texture.getWidth(); + int bottom = texture.getHeight(); + if (texture.hasBorder()) { + left = 1; + top = 1; + right -= 1; + bottom -= 1; + } + outRect.set(left, top, right, bottom); + } + + @Override + public void drawTexture(BasicTexture texture, RectF source, RectF target) { + if (target.width() <= 0 || target.height() <= 0) { + return; + } + mTempSourceRect.set(source); + mTempTargetRect.set(target); + + convertCoordinate(mTempSourceRect, mTempTargetRect, texture); + drawTextureRect(texture, mTempSourceRect, mTempTargetRect); + } + + @Override + public void drawTexture(BasicTexture texture, float[] textureTransform, int x, int y, int w, + int h) { + if (w <= 0 || h <= 0) { + return; + } + mTempTargetRect.set(x, y, x + w, y + h); + drawTextureRect(texture, textureTransform, mTempTargetRect); + } + + private void drawTextureRect(BasicTexture texture, RectF source, RectF target) { + setTextureMatrix(source); + drawTextureRect(texture, mTempTextureMatrix, target); + } + + private void setTextureMatrix(RectF source) { + mTempTextureMatrix[0] = source.width(); + mTempTextureMatrix[5] = source.height(); + mTempTextureMatrix[12] = source.left; + mTempTextureMatrix[13] = source.top; + } + + // This function changes the source coordinate to the texture coordinates. + // It also clips the source and target coordinates if it is beyond the + // bound of the texture. + private static void convertCoordinate(RectF source, RectF target, BasicTexture texture) { + int width = texture.getWidth(); + int height = texture.getHeight(); + int texWidth = texture.getTextureWidth(); + int texHeight = texture.getTextureHeight(); + // Convert to texture coordinates + source.left /= texWidth; + source.right /= texWidth; + source.top /= texHeight; + source.bottom /= texHeight; + + // Clip if the rendering range is beyond the bound of the texture. + float xBound = (float) width / texWidth; + if (source.right > xBound) { + target.right = target.left + target.width() * (xBound - source.left) / source.width(); + source.right = xBound; + } + float yBound = (float) height / texHeight; + if (source.bottom > yBound) { + target.bottom = target.top + target.height() * (yBound - source.top) / source.height(); + source.bottom = yBound; + } + } + + private void drawTextureRect(BasicTexture texture, float[] textureMatrix, RectF target) { + ShaderParameter[] params = prepareTexture(texture); + setPosition(params, OFFSET_FILL_RECT); + GLES20.glUniformMatrix4fv(params[INDEX_TEXTURE_MATRIX].handle, 1, false, textureMatrix, 0); + checkError(); + if (texture.isFlippedVertically()) { + save(SAVE_FLAG_MATRIX); + translate(0, target.centerY()); + scale(1, -1, 1); + translate(0, -target.centerY()); + } + draw(params, GLES20.GL_TRIANGLE_STRIP, COUNT_FILL_VERTEX, target.left, target.top, + target.width(), target.height()); + if (texture.isFlippedVertically()) { + restore(); + } + mCountTextureRect++; + } + + private ShaderParameter[] prepareTexture(BasicTexture texture) { + ShaderParameter[] params; + int program; + if (texture.getTarget() == GLES20.GL_TEXTURE_2D) { + params = mTextureParameters; + program = mTextureProgram; + } else { + params = mOesTextureParameters; + program = mOesTextureProgram; + } + prepareTexture(texture, program, params); + return params; + } + + private void prepareTexture(BasicTexture texture, int program, ShaderParameter[] params) { + GLES20.glUseProgram(program); + checkError(); + enableBlending(!texture.isOpaque() || getAlpha() < OPAQUE_ALPHA); + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + checkError(); + texture.onBind(this); + GLES20.glBindTexture(texture.getTarget(), texture.getId()); + checkError(); + GLES20.glUniform1i(params[INDEX_TEXTURE_SAMPLER].handle, 0); + checkError(); + GLES20.glUniform1f(params[INDEX_ALPHA].handle, getAlpha()); + checkError(); + } + + @Override + public void drawMesh(BasicTexture texture, int x, int y, int xyBuffer, int uvBuffer, + int indexBuffer, int indexCount) { + prepareTexture(texture, mMeshProgram, mMeshParameters); + + GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, indexBuffer); + checkError(); + + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, xyBuffer); + checkError(); + int positionHandle = mMeshParameters[INDEX_POSITION].handle; + GLES20.glVertexAttribPointer(positionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, + VERTEX_STRIDE, 0); + checkError(); + + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, uvBuffer); + checkError(); + int texCoordHandle = mMeshParameters[INDEX_TEXTURE_COORD].handle; + GLES20.glVertexAttribPointer(texCoordHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, + false, VERTEX_STRIDE, 0); + checkError(); + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + checkError(); + + GLES20.glEnableVertexAttribArray(positionHandle); + checkError(); + GLES20.glEnableVertexAttribArray(texCoordHandle); + checkError(); + + setMatrix(mMeshParameters, x, y, 1, 1); + GLES20.glDrawElements(GLES20.GL_TRIANGLE_STRIP, indexCount, GLES20.GL_UNSIGNED_BYTE, 0); + checkError(); + + GLES20.glDisableVertexAttribArray(positionHandle); + checkError(); + GLES20.glDisableVertexAttribArray(texCoordHandle); + checkError(); + GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0); + checkError(); + mCountDrawMesh++; + } + + @Override + public void drawMixed(BasicTexture texture, int toColor, float ratio, int x, int y, int w, int h) { + copyTextureCoordinates(texture, mTempSourceRect); + mTempTargetRect.set(x, y, x + w, y + h); + drawMixed(texture, toColor, ratio, mTempSourceRect, mTempTargetRect); + } + + @Override + public void drawMixed(BasicTexture texture, int toColor, float ratio, RectF source, RectF target) { + if (target.width() <= 0 || target.height() <= 0) { + return; + } + save(SAVE_FLAG_ALPHA); + + float currentAlpha = getAlpha(); + float cappedRatio = Math.min(1f, Math.max(0f, ratio)); + + float textureAlpha = (1f - cappedRatio) * currentAlpha; + setAlpha(textureAlpha); + drawTexture(texture, source, target); + + float colorAlpha = cappedRatio * currentAlpha; + setAlpha(colorAlpha); + fillRect(target.left, target.top, target.width(), target.height(), toColor); + + restore(); + } + + @Override + public boolean unloadTexture(BasicTexture texture) { + boolean unload = texture.isLoaded(); + if (unload) { + synchronized (mUnboundTextures) { + mUnboundTextures.add(texture.getId()); + } + } + return unload; + } + + @Override + public void deleteBuffer(int bufferId) { + synchronized (mUnboundTextures) { + mDeleteBuffers.add(bufferId); + } + } + + @Override + public void deleteRecycledResources() { + synchronized (mUnboundTextures) { + IntArray ids = mUnboundTextures; + if (mUnboundTextures.size() > 0) { + mGLId.glDeleteTextures(null, ids.size(), ids.getInternalArray(), 0); + ids.clear(); + } + + ids = mDeleteBuffers; + if (ids.size() > 0) { + mGLId.glDeleteBuffers(null, ids.size(), ids.getInternalArray(), 0); + ids.clear(); + } + } + } + + @Override + @SuppressLint("DefaultLocale") + public void dumpStatisticsAndClear() { + String line = String.format("MESH:%d, TEX_RECT:%d, FILL_RECT:%d, LINE:%d", mCountDrawMesh, + mCountTextureRect, mCountFillRect, mCountDrawLine); + mCountDrawMesh = 0; + mCountTextureRect = 0; + mCountFillRect = 0; + mCountDrawLine = 0; + Log.d(TAG, line); + } + + @Override + public void endRenderTarget() { + RawTexture oldTexture = mTargetTextures.remove(mTargetTextures.size() - 1); + RawTexture texture = getTargetTexture(); + setRenderTarget(oldTexture, texture); + restore(); // restore matrix and alpha + } + + @Override + public void beginRenderTarget(RawTexture texture) { + save(); // save matrix and alpha and blending + RawTexture oldTexture = getTargetTexture(); + mTargetTextures.add(texture); + setRenderTarget(oldTexture, texture); + } + + private RawTexture getTargetTexture() { + return mTargetTextures.get(mTargetTextures.size() - 1); + } + + private void setRenderTarget(BasicTexture oldTexture, RawTexture texture) { + if (oldTexture == null && texture != null) { + GLES20.glGenFramebuffers(1, mFrameBuffer, 0); + checkError(); + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffer[0]); + checkError(); + } else if (oldTexture != null && texture == null) { + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0); + checkError(); + GLES20.glDeleteFramebuffers(1, mFrameBuffer, 0); + checkError(); + } + + if (texture == null) { + setSize(mScreenWidth, mScreenHeight); + } else { + setSize(texture.getWidth(), texture.getHeight()); + + if (!texture.isLoaded()) { + texture.prepare(this); + } + + GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, + texture.getTarget(), texture.getId(), 0); + checkError(); + + checkFramebufferStatus(); + } + } + + private static void checkFramebufferStatus() { + int status = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER); + if (status != GLES20.GL_FRAMEBUFFER_COMPLETE) { + String msg = ""; + switch (status) { + case GLES20.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: + msg = "GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT"; + break; + case GLES20.GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS: + msg = "GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS"; + break; + case GLES20.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: + msg = "GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT"; + break; + case GLES20.GL_FRAMEBUFFER_UNSUPPORTED: + msg = "GL_FRAMEBUFFER_UNSUPPORTED"; + break; + } + throw new RuntimeException(msg + ":" + Integer.toHexString(status)); + } + } + + @Override + public void setTextureParameters(BasicTexture texture) { + int target = texture.getTarget(); + GLES20.glBindTexture(target, texture.getId()); + checkError(); + GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameterf(target, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameterf(target, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + } + + @Override + public void initializeTextureSize(BasicTexture texture, int format, int type) { + int target = texture.getTarget(); + GLES20.glBindTexture(target, texture.getId()); + checkError(); + int width = texture.getTextureWidth(); + int height = texture.getTextureHeight(); + GLES20.glTexImage2D(target, 0, format, width, height, 0, format, type, null); + } + + @Override + public void initializeTexture(BasicTexture texture, Bitmap bitmap) { + int target = texture.getTarget(); + GLES20.glBindTexture(target, texture.getId()); + checkError(); + GLUtils.texImage2D(target, 0, bitmap, 0); + } + + @Override + public void texSubImage2D(BasicTexture texture, int xOffset, int yOffset, Bitmap bitmap, + int format, int type) { + int target = texture.getTarget(); + GLES20.glBindTexture(target, texture.getId()); + checkError(); + GLUtils.texSubImage2D(target, 0, xOffset, yOffset, bitmap, format, type); + } + + @Override + public int uploadBuffer(FloatBuffer buf) { + return uploadBuffer(buf, FLOAT_SIZE); + } + + @Override + public int uploadBuffer(ByteBuffer buf) { + return uploadBuffer(buf, 1); + } + + private int uploadBuffer(Buffer buffer, int elementSize) { + mGLId.glGenBuffers(1, mTempIntArray, 0); + checkError(); + int bufferId = mTempIntArray[0]; + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, bufferId); + checkError(); + GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, buffer.capacity() * elementSize, buffer, + GLES20.GL_STATIC_DRAW); + checkError(); + return bufferId; + } + + public static void checkError() { + int error = GLES20.glGetError(); + if (error != 0) { + Throwable t = new Throwable(); + Log.e(TAG, "GL error: " + error, t); + } + } + + @SuppressWarnings("unused") + private static void printMatrix(String message, float[] m, int offset) { + StringBuilder b = new StringBuilder(message); + for (int i = 0; i < MATRIX_SIZE; i++) { + b.append(' '); + if (i % 4 == 0) { + b.append('\n'); + } + b.append(m[offset + i]); + } + Log.v(TAG, b.toString()); + } + + @Override + public void recoverFromLightCycle() { + GLES20.glViewport(0, 0, mWidth, mHeight); + GLES20.glDisable(GLES20.GL_DEPTH_TEST); + GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA); + checkError(); + } + + @Override + public void getBounds(Rect bounds, int x, int y, int width, int height) { + Matrix.translateM(mTempMatrix, 0, mMatrices, mCurrentMatrixIndex, x, y, 0f); + Matrix.scaleM(mTempMatrix, 0, width, height, 1f); + Matrix.multiplyMV(mTempMatrix, MATRIX_SIZE, mTempMatrix, 0, BOUNDS_COORDINATES, 0); + Matrix.multiplyMV(mTempMatrix, MATRIX_SIZE + 4, mTempMatrix, 0, BOUNDS_COORDINATES, 4); + bounds.left = Math.round(mTempMatrix[MATRIX_SIZE]); + bounds.right = Math.round(mTempMatrix[MATRIX_SIZE + 4]); + bounds.top = Math.round(mTempMatrix[MATRIX_SIZE + 1]); + bounds.bottom = Math.round(mTempMatrix[MATRIX_SIZE + 5]); + bounds.sort(); + } + + @Override + public GLId getGLId() { + return mGLId; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/GLES20IdImpl.java b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/GLES20IdImpl.java new file mode 100644 index 000000000..11ce2142e --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/GLES20IdImpl.java @@ -0,0 +1,42 @@ +package com.hippo.ehviewer.gallery.glrenderer; + +import javax.microedition.khronos.opengles.GL11; +import javax.microedition.khronos.opengles.GL11ExtensionPack; + +import android.opengl.GLES20; + +public class GLES20IdImpl implements GLId { + private final int[] mTempIntArray = new int[1]; + + @Override + public int generateTexture() { + GLES20.glGenTextures(1, mTempIntArray, 0); + GLES20Canvas.checkError(); + return mTempIntArray[0]; + } + + @Override + public void glGenBuffers(int n, int[] buffers, int offset) { + GLES20.glGenBuffers(n, buffers, offset); + GLES20Canvas.checkError(); + } + + @Override + public void glDeleteTextures(GL11 gl, int n, int[] textures, int offset) { + GLES20.glDeleteTextures(n, textures, offset); + GLES20Canvas.checkError(); + } + + + @Override + public void glDeleteBuffers(GL11 gl, int n, int[] buffers, int offset) { + GLES20.glDeleteBuffers(n, buffers, offset); + GLES20Canvas.checkError(); + } + + @Override + public void glDeleteFramebuffers(GL11ExtensionPack gl11ep, int n, int[] buffers, int offset) { + GLES20.glDeleteFramebuffers(n, buffers, offset); + GLES20Canvas.checkError(); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/GLId.java b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/GLId.java new file mode 100644 index 000000000..9015f2b41 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/GLId.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.glrenderer; + +import javax.microedition.khronos.opengles.GL11; +import javax.microedition.khronos.opengles.GL11ExtensionPack; + +// This mimics corresponding GL functions. +public interface GLId { + public int generateTexture(); + + public void glGenBuffers(int n, int[] buffers, int offset); + + public void glDeleteTextures(GL11 gl, int n, int[] textures, int offset); + + public void glDeleteBuffers(GL11 gl, int n, int[] buffers, int offset); + + public void glDeleteFramebuffers(GL11ExtensionPack gl11ep, int n, int[] buffers, int offset); +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/GLPaint.java b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/GLPaint.java new file mode 100644 index 000000000..84e93554f --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/GLPaint.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.glrenderer; + +import junit.framework.Assert; + +public class GLPaint { + private float mLineWidth = 1f; + private int mColor = 0; + + public GLPaint(int color, float lineWidth) { + setColor(color); + setLineWidth(lineWidth); + } + + public void setColor(int color) { + mColor = color; + } + + public int getColor() { + return mColor; + } + + public void setLineWidth(float width) { + Assert.assertTrue(width >= 0); + mLineWidth = width; + } + + public float getLineWidth() { + return mLineWidth; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/ImageTexture.java b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/ImageTexture.java new file mode 100644 index 000000000..bba87effc --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/ImageTexture.java @@ -0,0 +1,142 @@ +/* + * 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.gallery.glrenderer; + +import javax.microedition.khronos.opengles.GL11; + +import android.opengl.GLES20; +import android.util.Log; + +import com.hippo.ehviewer.gallery.image.Image; + +public class ImageTexture extends BasicTexture implements Uploaded { + + private static final String TAG = ImageTexture.class.getSimpleName(); + + private Image mImage; + private boolean mContentValid = true; + private boolean mOpaque = true; + + public ImageTexture(Image image) { + mImage = image; + setSize(image.getWidth(), image.getHeight()); + } + + public void start() { + if (mImage != null) + mImage.start(); + } + + public void stop() { + if (mImage != null) + mImage.stop(); + } + + public boolean isAnimated() { + if (mImage == null || mImage.isRecycled()) + return false; + else + return mImage.isAnimated(); + } + + @Override + public boolean isOpaque() { + return mOpaque; + } + + public void setOpaque(boolean opaque) { + mOpaque = opaque; + } + + /** + * Whether the content on GPU is valid. + */ + @Override + public boolean isContentValid() { + return isLoaded() && mContentValid; + } + + @Override + protected int getTarget() { + return GL11.GL_TEXTURE_2D; + } + + public int getFormat() { + return mImage.getFormat(); + } + + public int getType() { + return mImage.getType(); + } + + public static void checkError() { + int error = GLES20.glGetError(); + if (error != 0) { + Throwable t = new Throwable(); + Log.e(TAG, "GL error: " + error, t); + } + } + + private void uploadToCanvas(GLCanvas canvas) { + mId = canvas.getGLId().generateTexture(); + int format = getFormat(); + int type = getType(); + + canvas.setTextureParameters(this); + canvas.initializeTextureSize(this, format, type); + + GLES20.glBindTexture(getTarget(), mId); + checkError(); + mImage.render(); + + setAssociatedCanvas(canvas); + mState = STATE_LOADED; + mContentValid = true; + } + + @Override + public void updateContent(GLCanvas canvas) { + if (!isLoaded()) { + uploadToCanvas(canvas); + } else if (isAnimated()) { + GLES20.glBindTexture(getTarget(), mId); + checkError(); + mImage.render(); + + mContentValid = true; + } + } + + @Override + protected boolean onBind(GLCanvas canvas) { + if (mImage == null || mImage.isRecycled()) { + return false; + } else { + updateContent(canvas); + return isContentValid(); + } + } + + @Override + public void recycle() { + super.recycle(); + if (mImage != null) { + mImage.recycle(); + mImage = null; + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/MovieTexture.java b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/MovieTexture.java new file mode 100644 index 000000000..bc7e55a3b --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/MovieTexture.java @@ -0,0 +1,60 @@ +/* + * 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.gallery.glrenderer; + +import junit.framework.Assert; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Movie; + +public class MovieTexture extends CanvasTexture { + private static final int DEFAULT_MOVIE_DURATION = 1000; + + private final Movie mMovie; + private final int mDuration; + private final long mMovieStart; + + private MovieTexture(Movie movie, int width, int height) { + super(width, height); + mMovie = movie; + int dur = mMovie.duration(); + if (dur == 0) + mDuration = DEFAULT_MOVIE_DURATION; + else + mDuration = dur; + mMovieStart = System.currentTimeMillis(); + } + + public static MovieTexture newInstance(Movie movie) { + Assert.assertTrue(movie != null); + return new MovieTexture(movie, movie.width(), movie.height()); + } + + @Override + protected boolean onBind(GLCanvas canvas) { + invalidateContent(); + setSize(mMovie.width(), mMovie.height()); + return super.onBind(canvas); + } + + @Override + protected void onDraw(Canvas canvas, Bitmap backing) { + mMovie.setTime((int)((System.currentTimeMillis() - mMovieStart) % mDuration)); + mMovie.draw(canvas, 0, 0); + } + +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/MultiLineTexture.java b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/MultiLineTexture.java new file mode 100644 index 000000000..13f3dca9f --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/MultiLineTexture.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.glrenderer; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.text.Layout; +import android.text.StaticLayout; +import android.text.TextPaint; + + +// MultiLineTexture is a texture shows the content of a specified String. +// +// To create a MultiLineTexture, use the newInstance() method and specify +// the String, the font size, and the color. +class MultiLineTexture extends CanvasTexture { + private final Layout mLayout; + + private MultiLineTexture(Layout layout) { + super(layout.getWidth(), layout.getHeight()); + mLayout = layout; + } + + public static MultiLineTexture newInstance( + String text, int maxWidth, float textSize, int color, + Layout.Alignment alignment) { + TextPaint paint = StringTexture.getDefaultPaint(textSize, color); + Layout layout = new StaticLayout(text, 0, text.length(), paint, + maxWidth, alignment, 1, 0, true, null, 0); + + return new MultiLineTexture(layout); + } + + @Override + protected void onDraw(Canvas canvas, Bitmap backing) { + mLayout.draw(canvas); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/NinePatchChunk.java b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/NinePatchChunk.java new file mode 100644 index 000000000..df204e852 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/NinePatchChunk.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.glrenderer; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import android.graphics.Rect; + +// See "frameworks/base/include/utils/ResourceTypes.h" for the format of +// NinePatch chunk. +class NinePatchChunk { + + public static final int NO_COLOR = 0x00000001; + public static final int TRANSPARENT_COLOR = 0x00000000; + + public Rect mPaddings = new Rect(); + + public int mDivX[]; + public int mDivY[]; + public int mColor[]; + + private static void readIntArray(int[] data, ByteBuffer buffer) { + for (int i = 0, n = data.length; i < n; ++i) { + data[i] = buffer.getInt(); + } + } + + private static void checkDivCount(int length) { + if (length == 0 || (length & 0x01) != 0) { + throw new RuntimeException("invalid nine-patch: " + length); + } + } + + public static NinePatchChunk deserialize(byte[] data) { + ByteBuffer byteBuffer = + ByteBuffer.wrap(data).order(ByteOrder.nativeOrder()); + + byte wasSerialized = byteBuffer.get(); + if (wasSerialized == 0) return null; + + NinePatchChunk chunk = new NinePatchChunk(); + chunk.mDivX = new int[byteBuffer.get()]; + chunk.mDivY = new int[byteBuffer.get()]; + chunk.mColor = new int[byteBuffer.get()]; + + checkDivCount(chunk.mDivX.length); + checkDivCount(chunk.mDivY.length); + + // skip 8 bytes + byteBuffer.getInt(); + byteBuffer.getInt(); + + chunk.mPaddings.left = byteBuffer.getInt(); + chunk.mPaddings.right = byteBuffer.getInt(); + chunk.mPaddings.top = byteBuffer.getInt(); + chunk.mPaddings.bottom = byteBuffer.getInt(); + + // skip 4 bytes + byteBuffer.getInt(); + + readIntArray(chunk.mDivX, byteBuffer); + readIntArray(chunk.mDivY, byteBuffer); + readIntArray(chunk.mColor, byteBuffer); + + return chunk; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/NinePatchTexture.java b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/NinePatchTexture.java new file mode 100644 index 000000000..dd2d38a05 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/NinePatchTexture.java @@ -0,0 +1,425 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.glrenderer; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Rect; + +import com.hippo.ehviewer.util.MathUtils; + +// NinePatchTexture is a texture backed by a NinePatch resource. +// +// getPaddings() returns paddings specified in the NinePatch. +// getNinePatchChunk() returns the layout data specified in the NinePatch. +// +public class NinePatchTexture extends ResourceTexture { + @SuppressWarnings("unused") + private static final String TAG = "NinePatchTexture"; + private NinePatchChunk mChunk; + private final SmallCache mInstanceCache + = new SmallCache(); + + public NinePatchTexture(Context context, int resId) { + super(context, resId); + } + + @Override + protected Bitmap onGetBitmap() { + if (mBitmap != null) return mBitmap; + + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + Bitmap bitmap = BitmapFactory.decodeResource( + mContext.getResources(), mResId, options); + mBitmap = bitmap; + setSize(bitmap.getWidth(), bitmap.getHeight()); + byte[] chunkData = bitmap.getNinePatchChunk(); + mChunk = chunkData == null + ? null + : NinePatchChunk.deserialize(bitmap.getNinePatchChunk()); + if (mChunk == null) { + throw new RuntimeException("invalid nine-patch image: " + mResId); + } + return bitmap; + } + + public Rect getPaddings() { + // get the paddings from nine patch + if (mChunk == null) onGetBitmap(); + return mChunk.mPaddings; + } + + public NinePatchChunk getNinePatchChunk() { + if (mChunk == null) onGetBitmap(); + return mChunk; + } + + // This is a simple cache for a small number of things. Linear search + // is used because the cache is small. It also tries to remove less used + // item when the cache is full by moving the often-used items to the front. + private static class SmallCache { + private static final int CACHE_SIZE = 16; + private static final int CACHE_SIZE_START_MOVE = CACHE_SIZE / 2; + private final int[] mKey = new int[CACHE_SIZE]; + @SuppressWarnings("unchecked") + private final V[] mValue = (V[]) new Object[CACHE_SIZE]; + private int mCount; // number of items in this cache + + // Puts a value into the cache. If the cache is full, also returns + // a less used item, otherwise returns null. + public V put(int key, V value) { + if (mCount == CACHE_SIZE) { + V old = mValue[CACHE_SIZE - 1]; // remove the last item + mKey[CACHE_SIZE - 1] = key; + mValue[CACHE_SIZE - 1] = value; + return old; + } else { + mKey[mCount] = key; + mValue[mCount] = value; + mCount++; + return null; + } + } + + public V get(int key) { + for (int i = 0; i < mCount; i++) { + if (mKey[i] == key) { + // Move the accessed item one position to the front, so it + // will less likely to be removed when cache is full. Only + // do this if the cache is starting to get full. + if (mCount > CACHE_SIZE_START_MOVE && i > 0) { + int tmpKey = mKey[i]; + mKey[i] = mKey[i - 1]; + mKey[i - 1] = tmpKey; + + V tmpValue = mValue[i]; + mValue[i] = mValue[i - 1]; + mValue[i - 1] = tmpValue; + } + return mValue[i]; + } + } + return null; + } + + public void clear() { + for (int i = 0; i < mCount; i++) { + mValue[i] = null; // make sure it's can be garbage-collected. + } + mCount = 0; + } + + public int size() { + return mCount; + } + + public V valueAt(int i) { + return mValue[i]; + } + } + + private NinePatchInstance findInstance(GLCanvas canvas, int w, int h) { + int key = w; + key = (key << 16) | h; + NinePatchInstance instance = mInstanceCache.get(key); + + if (instance == null) { + instance = new NinePatchInstance(this, w, h); + NinePatchInstance removed = mInstanceCache.put(key, instance); + if (removed != null) { + removed.recycle(canvas); + } + } + + return instance; + } + + @Override + public void draw(GLCanvas canvas, int x, int y, int w, int h) { + if (!isLoaded()) { + mInstanceCache.clear(); + } + + if (w != 0 && h != 0) { + findInstance(canvas, w, h).draw(canvas, this, x, y); + } + } + + @Override + public void recycle() { + super.recycle(); + GLCanvas canvas = mCanvasRef; + if (canvas == null) return; + int n = mInstanceCache.size(); + for (int i = 0; i < n; i++) { + NinePatchInstance instance = mInstanceCache.valueAt(i); + instance.recycle(canvas); + } + mInstanceCache.clear(); + } +} + +// This keeps data for a specialization of NinePatchTexture with the size +// (width, height). We pre-compute the coordinates for efficiency. +class NinePatchInstance { + + @SuppressWarnings("unused") + private static final String TAG = "NinePatchInstance"; + + // We need 16 vertices for a normal nine-patch image (the 4x4 vertices) + private static final int VERTEX_BUFFER_SIZE = 16 * 2; + + // We need 22 indices for a normal nine-patch image, plus 2 for each + // transparent region. Current there are at most 1 transparent region. + private static final int INDEX_BUFFER_SIZE = 22 + 2; + + private FloatBuffer mXyBuffer; + private FloatBuffer mUvBuffer; + private ByteBuffer mIndexBuffer; + + // Names for buffer names: xy, uv, index. + private int mXyBufferName = -1; + private int mUvBufferName; + private int mIndexBufferName; + + private int mIdxCount; + + public NinePatchInstance(NinePatchTexture tex, int width, int height) { + NinePatchChunk chunk = tex.getNinePatchChunk(); + + if (width <= 0 || height <= 0) { + throw new RuntimeException("invalid dimension"); + } + + // The code should be easily extended to handle the general cases by + // allocating more space for buffers. But let's just handle the only + // use case. + if (chunk.mDivX.length != 2 || chunk.mDivY.length != 2) { + throw new RuntimeException("unsupported nine patch"); + } + + float divX[] = new float[4]; + float divY[] = new float[4]; + float divU[] = new float[4]; + float divV[] = new float[4]; + + int nx = stretch(divX, divU, chunk.mDivX, tex.getWidth(), width); + int ny = stretch(divY, divV, chunk.mDivY, tex.getHeight(), height); + + prepareVertexData(divX, divY, divU, divV, nx, ny, chunk.mColor); + } + + /** + * Stretches the texture according to the nine-patch rules. It will + * linearly distribute the strechy parts defined in the nine-patch chunk to + * the target area. + * + *
+     *                      source
+     *          /--------------^---------------\
+     *         u0    u1       u2  u3     u4   u5
+     * div ---> |fffff|ssssssss|fff|ssssss|ffff| ---> u
+     *          |    div0    div1 div2   div3  |
+     *          |     |       /   /      /    /
+     *          |     |      /   /     /    /
+     *          |     |     /   /    /    /
+     *          |fffff|ssss|fff|sss|ffff| ---> x
+     *         x0    x1   x2  x3  x4   x5
+     *          \----------v------------/
+     *                  target
+     *
+     * f: fixed segment
+     * s: stretchy segment
+     * 
+ * + * @param div the stretch parts defined in nine-patch chunk + * @param source the length of the texture + * @param target the length on the drawing plan + * @param u output, the positions of these dividers in the texture + * coordinate + * @param x output, the corresponding position of these dividers on the + * drawing plan + * @return the number of these dividers. + */ + private static int stretch( + float x[], float u[], int div[], int source, int target) { + int textureSize = MathUtils.nextPowerOf2(source); + float textureBound = (float) source / textureSize; + + float stretch = 0; + for (int i = 0, n = div.length; i < n; i += 2) { + stretch += div[i + 1] - div[i]; + } + + float remaining = target - source + stretch; + + float lastX = 0; + float lastU = 0; + + x[0] = 0; + u[0] = 0; + for (int i = 0, n = div.length; i < n; i += 2) { + // Make the stretchy segment a little smaller to prevent sampling + // on neighboring fixed segments. + // fixed segment + x[i + 1] = lastX + (div[i] - lastU) + 0.5f; + u[i + 1] = Math.min((div[i] + 0.5f) / textureSize, textureBound); + + // stretchy segment + float partU = div[i + 1] - div[i]; + float partX = remaining * partU / stretch; + remaining -= partX; + stretch -= partU; + + lastX = x[i + 1] + partX; + lastU = div[i + 1]; + x[i + 2] = lastX - 0.5f; + u[i + 2] = Math.min((lastU - 0.5f)/ textureSize, textureBound); + } + // the last fixed segment + x[div.length + 1] = target; + u[div.length + 1] = textureBound; + + // remove segments with length 0. + int last = 0; + for (int i = 1, n = div.length + 2; i < n; ++i) { + if ((x[i] - x[last]) < 1f) continue; + x[++last] = x[i]; + u[last] = u[i]; + } + return last + 1; + } + + private void prepareVertexData(float x[], float y[], float u[], float v[], + int nx, int ny, int[] color) { + /* + * Given a 3x3 nine-patch image, the vertex order is defined as the + * following graph: + * + * (0) (1) (2) (3) + * | /| /| /| + * | / | / | / | + * (4) (5) (6) (7) + * | \ | \ | \ | + * | \| \| \| + * (8) (9) (A) (B) + * | /| /| /| + * | / | / | / | + * (C) (D) (E) (F) + * + * And we draw the triangle strip in the following index order: + * + * index: 04152637B6A5948C9DAEBF + */ + int pntCount = 0; + float xy[] = new float[VERTEX_BUFFER_SIZE]; + float uv[] = new float[VERTEX_BUFFER_SIZE]; + for (int j = 0; j < ny; ++j) { + for (int i = 0; i < nx; ++i) { + int xIndex = (pntCount++) << 1; + int yIndex = xIndex + 1; + xy[xIndex] = x[i]; + xy[yIndex] = y[j]; + uv[xIndex] = u[i]; + uv[yIndex] = v[j]; + } + } + + int idxCount = 1; + boolean isForward = false; + byte index[] = new byte[INDEX_BUFFER_SIZE]; + for (int row = 0; row < ny - 1; row++) { + --idxCount; + isForward = !isForward; + + int start, end, inc; + if (isForward) { + start = 0; + end = nx; + inc = 1; + } else { + start = nx - 1; + end = -1; + inc = -1; + } + + for (int col = start; col != end; col += inc) { + int k = row * nx + col; + if (col != start) { + int colorIdx = row * (nx - 1) + col; + if (isForward) colorIdx--; + if (color[colorIdx] == NinePatchChunk.TRANSPARENT_COLOR) { + index[idxCount] = index[idxCount - 1]; + ++idxCount; + index[idxCount++] = (byte) k; + } + } + + index[idxCount++] = (byte) k; + index[idxCount++] = (byte) (k + nx); + } + } + + mIdxCount = idxCount; + + int size = (pntCount * 2) * (Float.SIZE / Byte.SIZE); + mXyBuffer = allocateDirectNativeOrderBuffer(size).asFloatBuffer(); + mUvBuffer = allocateDirectNativeOrderBuffer(size).asFloatBuffer(); + mIndexBuffer = allocateDirectNativeOrderBuffer(mIdxCount); + + mXyBuffer.put(xy, 0, pntCount * 2).position(0); + mUvBuffer.put(uv, 0, pntCount * 2).position(0); + mIndexBuffer.put(index, 0, idxCount).position(0); + } + + private static ByteBuffer allocateDirectNativeOrderBuffer(int size) { + return ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder()); + } + + private void prepareBuffers(GLCanvas canvas) { + mXyBufferName = canvas.uploadBuffer(mXyBuffer); + mUvBufferName = canvas.uploadBuffer(mUvBuffer); + mIndexBufferName = canvas.uploadBuffer(mIndexBuffer); + + // These buffers are never used again. + mXyBuffer = null; + mUvBuffer = null; + mIndexBuffer = null; + } + + public void draw(GLCanvas canvas, NinePatchTexture tex, int x, int y) { + if (mXyBufferName == -1) { + prepareBuffers(canvas); + } + canvas.drawMesh(tex, x, y, mXyBufferName, mUvBufferName, mIndexBufferName, mIdxCount); + } + + public void recycle(GLCanvas canvas) { + if (mXyBuffer == null) { + canvas.deleteBuffer(mXyBufferName); + canvas.deleteBuffer(mUvBufferName); + canvas.deleteBuffer(mIndexBufferName); + mXyBufferName = -1; + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/RawTexture.java b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/RawTexture.java new file mode 100644 index 000000000..44e1390b8 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/RawTexture.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.glrenderer; + +import javax.microedition.khronos.opengles.GL11; + +import android.util.Log; + +public class RawTexture extends BasicTexture { + private static final String TAG = "RawTexture"; + + private final boolean mOpaque; + private boolean mIsFlipped; + + public RawTexture(int width, int height, boolean opaque) { + mOpaque = opaque; + setSize(width, height); + } + + @Override + public boolean isOpaque() { + return mOpaque; + } + + @Override + public boolean isFlippedVertically() { + return mIsFlipped; + } + + public void setIsFlippedVertically(boolean isFlipped) { + mIsFlipped = isFlipped; + } + + protected void prepare(GLCanvas canvas) { + GLId glId = canvas.getGLId(); + mId = glId.generateTexture(); + canvas.initializeTextureSize(this, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE); + canvas.setTextureParameters(this); + mState = STATE_LOADED; + setAssociatedCanvas(canvas); + } + + @Override + protected boolean onBind(GLCanvas canvas) { + if (isLoaded()) return true; + Log.w(TAG, "lost the content due to context change"); + return false; + } + + @Override + public void yield() { + // we cannot free the texture because we have no backup. + } + + @Override + protected int getTarget() { + return GL11.GL_TEXTURE_2D; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/ResourceTexture.java b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/ResourceTexture.java new file mode 100644 index 000000000..c2580e630 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/ResourceTexture.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.glrenderer; + +import junit.framework.Assert; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +// ResourceTexture is a texture whose Bitmap is decoded from a resource. +// By default ResourceTexture is not opaque. +public class ResourceTexture extends UploadedTexture { + + protected final Context mContext; + protected final int mResId; + + public ResourceTexture(Context context, int resId) { + Assert.assertNotNull(context); + mContext = context; + mResId = resId; + setOpaque(false); + } + + @Override + protected Bitmap onGetBitmap() { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + return BitmapFactory.decodeResource( + mContext.getResources(), mResId, options); + } + + @Override + protected void onFreeBitmap(Bitmap bitmap) { + if (!inFinalizer()) { + bitmap.recycle(); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/StringTexture.java b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/StringTexture.java new file mode 100644 index 000000000..24f364765 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/StringTexture.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.glrenderer; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint.FontMetricsInt; +import android.graphics.Typeface; +import android.text.TextPaint; +import android.text.TextUtils; + +// StringTexture is a texture shows the content of a specified String. +// +// To create a StringTexture, use the newInstance() method and specify +// the String, the font size, and the color. +public class StringTexture extends CanvasTexture { + private final String mText; + private final TextPaint mPaint; + private final FontMetricsInt mMetrics; + + private StringTexture(String text, TextPaint paint, + FontMetricsInt metrics, int width, int height) { + super(width, height); + mText = text; + mPaint = paint; + mMetrics = metrics; + } + + public static TextPaint getDefaultPaint(float textSize, int color) { + TextPaint paint = new TextPaint(); + paint.setTextSize(textSize); + paint.setAntiAlias(true); + paint.setColor(color); + paint.setShadowLayer(2f, 0f, 0f, Color.BLACK); + return paint; + } + + public static StringTexture newInstance( + String text, float textSize, int color) { + return newInstance(text, getDefaultPaint(textSize, color)); + } + + public static StringTexture newInstance( + String text, float textSize, int color, + float lengthLimit, boolean isBold) { + TextPaint paint = getDefaultPaint(textSize, color); + if (isBold) { + paint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD)); + } + if (lengthLimit > 0) { + text = TextUtils.ellipsize( + text, paint, lengthLimit, TextUtils.TruncateAt.END).toString(); + } + return newInstance(text, paint); + } + + private static StringTexture newInstance(String text, TextPaint paint) { + FontMetricsInt metrics = paint.getFontMetricsInt(); + int width = (int)Math.ceil(paint.measureText(text)); + int height = metrics.bottom - metrics.top; + // The texture size needs to be at least 1x1. + if (width <= 0) width = 1; + if (height <= 0) height = 1; + return new StringTexture(text, paint, metrics, width, height); + } + + @Override + protected void onDraw(Canvas canvas, Bitmap backing) { + canvas.translate(0, -mMetrics.ascent); + canvas.drawText(mText, 0, 0, mPaint); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/Texture.java b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/Texture.java new file mode 100644 index 000000000..2531a1e7c --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/Texture.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.glrenderer; + + +// Texture is a rectangular image which can be drawn on GLCanvas. +// The isOpaque() function gives a hint about whether the texture is opaque, +// so the drawing can be done faster. +// +// This is the current texture hierarchy: +// +// Texture +// -- ColorTexture +// -- FadeInTexture +// -- BasicTexture +// -- UploadedTexture +// -- BitmapTexture +// -- Tile +// -- ResourceTexture +// -- NinePatchTexture +// -- CanvasTexture +// -- StringTexture +// +public interface Texture { + public int getWidth(); + public int getHeight(); + public void draw(GLCanvas canvas, int x, int y); + public void draw(GLCanvas canvas, int x, int y, int w, int h); + public boolean isOpaque(); +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/TextureUploader.java b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/TextureUploader.java new file mode 100644 index 000000000..14332e6e5 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/TextureUploader.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.glrenderer; + +import java.util.ArrayDeque; + +import com.hippo.ehviewer.gallery.ui.GLRoot; +import com.hippo.ehviewer.gallery.ui.GLRoot.OnGLIdleListener; + +public class TextureUploader implements OnGLIdleListener { + private static final int INIT_CAPACITY = 64; + private static final int QUOTA_PER_FRAME = 1; + + private final ArrayDeque mFgTextures = + new ArrayDeque(INIT_CAPACITY); + private final ArrayDeque mBgTextures = + new ArrayDeque(INIT_CAPACITY); + private final GLRoot mGLRoot; + private volatile boolean mIsQueued = false; + + public TextureUploader(GLRoot root) { + mGLRoot = root; + } + + public synchronized void clear() { + while (!mFgTextures.isEmpty()) { + mFgTextures.pop().setIsUploading(false); + } + while (!mBgTextures.isEmpty()) { + mBgTextures.pop().setIsUploading(false); + } + } + + // caller should hold synchronized on "this" + private void queueSelfIfNeed() { + if (mIsQueued) return; + mIsQueued = true; + mGLRoot.addOnGLIdleListener(this); + } + + public synchronized void addBgTexture(UploadedTexture t) { + if (t.isContentValid()) return; + mBgTextures.addLast(t); + t.setIsUploading(true); + queueSelfIfNeed(); + } + + public synchronized void addFgTexture(UploadedTexture t) { + if (t.isContentValid()) return; + mFgTextures.addLast(t); + t.setIsUploading(true); + queueSelfIfNeed(); + } + + private int upload(GLCanvas canvas, ArrayDeque deque, + int uploadQuota, boolean isBackground) { + while (uploadQuota > 0) { + UploadedTexture t; + synchronized (this) { + if (deque.isEmpty()) break; + t = deque.removeFirst(); + t.setIsUploading(false); + if (t.isContentValid()) continue; + + // this has to be protected by the synchronized block + // to prevent the inner bitmap get recycled + t.updateContent(canvas); + } + + // It will took some more time for a texture to be drawn for + // the first time. + // Thus, when scrolling, if a new column appears on screen, + // it may cause a UI jank even these textures are uploaded. + if (isBackground) t.draw(canvas, 0, 0); + --uploadQuota; + } + return uploadQuota; + } + + @Override + public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) { + int uploadQuota = QUOTA_PER_FRAME; + uploadQuota = upload(canvas, mFgTextures, uploadQuota, false); + if (uploadQuota < QUOTA_PER_FRAME) mGLRoot.requestRender(); + upload(canvas, mBgTextures, uploadQuota, true); + synchronized (this) { + mIsQueued = !mFgTextures.isEmpty() || !mBgTextures.isEmpty(); + return mIsQueued; + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/TiledTexture.java b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/TiledTexture.java new file mode 100644 index 000000000..3831a02e0 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/TiledTexture.java @@ -0,0 +1,356 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.glrenderer; + +import java.util.ArrayDeque; +import java.util.ArrayList; + +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PorterDuff.Mode; +import android.graphics.PorterDuffXfermode; +import android.graphics.RectF; +import android.os.SystemClock; + +import com.hippo.ehviewer.gallery.ui.GLRoot; +import com.hippo.ehviewer.gallery.ui.GLRoot.OnGLIdleListener; + +// This class is similar to BitmapTexture, except the bitmap is +// split into tiles. By doing so, we may increase the time required to +// upload the whole bitmap but we reduce the time of uploading each tile +// so it make the animation more smooth and prevents jank. +public class TiledTexture implements Texture { + private static final int CONTENT_SIZE = 254; + private static final int BORDER_SIZE = 1; + private static final int TILE_SIZE = CONTENT_SIZE + 2 * BORDER_SIZE; + private static final int INIT_CAPACITY = 8; + + // We are targeting at 60fps, so we have 16ms for each frame. + // In this 16ms, we use about 4~8 ms to upload tiles. + private static final long UPLOAD_TILE_LIMIT = 4; // ms + + private static Tile sFreeTileHead = null; + private static final Object sFreeTileLock = new Object(); + + private static Bitmap sUploadBitmap; + private static Canvas sCanvas; + private static Paint sBitmapPaint; + private static Paint sPaint; + + private int mUploadIndex = 0; + + private final Tile[] mTiles; // Can be modified in different threads. + // Should be protected by "synchronized." + private final int mWidth; + private final int mHeight; + private final RectF mSrcRect = new RectF(); + private final RectF mDestRect = new RectF(); + + public static class Uploader implements OnGLIdleListener { + private final ArrayDeque mTextures = + new ArrayDeque(INIT_CAPACITY); + + private final GLRoot mGlRoot; + private boolean mIsQueued = false; + + public Uploader(GLRoot glRoot) { + mGlRoot = glRoot; + } + + public synchronized void clear() { + mTextures.clear(); + } + + public synchronized void addTexture(TiledTexture t) { + if (t.isReady()) return; + mTextures.addLast(t); + + if (mIsQueued) return; + mIsQueued = true; + mGlRoot.addOnGLIdleListener(this); + } + + @Override + public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) { + ArrayDeque deque = mTextures; + synchronized (this) { + long now = SystemClock.uptimeMillis(); + long dueTime = now + UPLOAD_TILE_LIMIT; + while (now < dueTime && !deque.isEmpty()) { + TiledTexture t = deque.peekFirst(); + if (t.uploadNextTile(canvas)) { + deque.removeFirst(); + mGlRoot.requestRender(); + } + now = SystemClock.uptimeMillis(); + } + mIsQueued = !mTextures.isEmpty(); + + // return true to keep this listener in the queue + return mIsQueued; + } + } + } + + private static class Tile extends UploadedTexture { + public int offsetX; + public int offsetY; + public Bitmap bitmap; + public Tile nextFreeTile; + public int contentWidth; + public int contentHeight; + + @Override + public void setSize(int width, int height) { + contentWidth = width; + contentHeight = height; + mWidth = width + 2 * BORDER_SIZE; + mHeight = height + 2 * BORDER_SIZE; + mTextureWidth = TILE_SIZE; + mTextureHeight = TILE_SIZE; + } + + @Override + protected Bitmap onGetBitmap() { + // make a local copy of the reference to the bitmap, + // since it might be null'd in a different thread. b/8694871 + Bitmap localBitmapRef = bitmap; + bitmap = null; + + if (localBitmapRef != null) { + int x = BORDER_SIZE - offsetX; + int y = BORDER_SIZE - offsetY; + int r = localBitmapRef.getWidth() + x; + int b = localBitmapRef.getHeight() + y; + sCanvas.drawBitmap(localBitmapRef, x, y, sBitmapPaint); + localBitmapRef = null; + + // draw borders if need + if (x > 0) sCanvas.drawLine(x - 1, 0, x - 1, TILE_SIZE, sPaint); + if (y > 0) sCanvas.drawLine(0, y - 1, TILE_SIZE, y - 1, sPaint); + if (r < CONTENT_SIZE) sCanvas.drawLine(r, 0, r, TILE_SIZE, sPaint); + if (b < CONTENT_SIZE) sCanvas.drawLine(0, b, TILE_SIZE, b, sPaint); + } + + return sUploadBitmap; + } + + @Override + protected void onFreeBitmap(Bitmap bitmap) { + // do nothing + } + } + + private static void freeTile(Tile tile) { + tile.invalidateContent(); + tile.bitmap = null; + synchronized (sFreeTileLock) { + tile.nextFreeTile = sFreeTileHead; + sFreeTileHead = tile; + } + } + + private static Tile obtainTile() { + synchronized (sFreeTileLock) { + Tile result = sFreeTileHead; + if (result == null) return new Tile(); + sFreeTileHead = result.nextFreeTile; + result.nextFreeTile = null; + return result; + } + } + + private boolean uploadNextTile(GLCanvas canvas) { + if (mUploadIndex == mTiles.length) return true; + + synchronized (mTiles) { + Tile next = mTiles[mUploadIndex++]; + + // Make sure tile has not already been recycled by the time + // this is called (race condition in onGLIdle) + if (next.bitmap != null) { + boolean hasBeenLoad = next.isLoaded(); + next.updateContent(canvas); + + // It will take some time for a texture to be drawn for the first + // time. When scrolling, we need to draw several tiles on the screen + // at the same time. It may cause a UI jank even these textures has + // been uploaded. + if (!hasBeenLoad) next.draw(canvas, 0, 0); + } + } + return mUploadIndex == mTiles.length; + } + + public TiledTexture(Bitmap bitmap) { + mWidth = bitmap.getWidth(); + mHeight = bitmap.getHeight(); + ArrayList list = new ArrayList(); + + for (int x = 0, w = mWidth; x < w; x += CONTENT_SIZE) { + for (int y = 0, h = mHeight; y < h; y += CONTENT_SIZE) { + Tile tile = obtainTile(); + tile.offsetX = x; + tile.offsetY = y; + tile.bitmap = bitmap; + tile.setSize( + Math.min(CONTENT_SIZE, mWidth - x), + Math.min(CONTENT_SIZE, mHeight - y)); + list.add(tile); + } + } + mTiles = list.toArray(new Tile[list.size()]); + } + + public boolean isReady() { + return mUploadIndex == mTiles.length; + } + + // Can be called in UI thread. + public void recycle() { + synchronized (mTiles) { + for (int i = 0, n = mTiles.length; i < n; ++i) { + freeTile(mTiles[i]); + } + } + } + + public static void freeResources() { + sUploadBitmap = null; + sCanvas = null; + sBitmapPaint = null; + sPaint = null; + } + + public static void prepareResources() { + sUploadBitmap = Bitmap.createBitmap(TILE_SIZE, TILE_SIZE, Config.ARGB_8888); + sCanvas = new Canvas(sUploadBitmap); + sBitmapPaint = new Paint(Paint.FILTER_BITMAP_FLAG); + sBitmapPaint.setXfermode(new PorterDuffXfermode(Mode.SRC)); + sPaint = new Paint(); + sPaint.setXfermode(new PorterDuffXfermode(Mode.SRC)); + sPaint.setColor(Color.TRANSPARENT); + } + + // We want to draw the "source" on the "target". + // This method is to find the "output" rectangle which is + // the corresponding area of the "src". + // (x,y) target + // (x0,y0) source +---------------+ + // +----------+ | | + // | src | | output | + // | +--+ | linear map | +----+ | + // | +--+ | ----------> | | | | + // | | by (scaleX, scaleY) | +----+ | + // +----------+ | | + // Texture +---------------+ + // Canvas + private static void mapRect(RectF output, + RectF src, float x0, float y0, float x, float y, float scaleX, + float scaleY) { + output.set(x + (src.left - x0) * scaleX, + y + (src.top - y0) * scaleY, + x + (src.right - x0) * scaleX, + y + (src.bottom - y0) * scaleY); + } + + // Draws a mixed color of this texture and a specified color onto the + // a rectangle. The used color is: from * (1 - ratio) + to * ratio. + public void drawMixed(GLCanvas canvas, int color, float ratio, + int x, int y, int width, int height) { + RectF src = mSrcRect; + RectF dest = mDestRect; + float scaleX = (float) width / mWidth; + float scaleY = (float) height / mHeight; + synchronized (mTiles) { + for (int i = 0, n = mTiles.length; i < n; ++i) { + Tile t = mTiles[i]; + src.set(0, 0, t.contentWidth, t.contentHeight); + src.offset(t.offsetX, t.offsetY); + mapRect(dest, src, 0, 0, x, y, scaleX, scaleY); + src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY); + canvas.drawMixed(t, color, ratio, mSrcRect, mDestRect); + } + } + } + + // Draws the texture on to the specified rectangle. + @Override + public void draw(GLCanvas canvas, int x, int y, int width, int height) { + RectF src = mSrcRect; + RectF dest = mDestRect; + float scaleX = (float) width / mWidth; + float scaleY = (float) height / mHeight; + synchronized (mTiles) { + for (int i = 0, n = mTiles.length; i < n; ++i) { + Tile t = mTiles[i]; + src.set(0, 0, t.contentWidth, t.contentHeight); + src.offset(t.offsetX, t.offsetY); + mapRect(dest, src, 0, 0, x, y, scaleX, scaleY); + src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY); + canvas.drawTexture(t, mSrcRect, mDestRect); + } + } + } + + // Draws a sub region of this texture on to the specified rectangle. + public void draw(GLCanvas canvas, RectF source, RectF target) { + RectF src = mSrcRect; + RectF dest = mDestRect; + float x0 = source.left; + float y0 = source.top; + float x = target.left; + float y = target.top; + float scaleX = target.width() / source.width(); + float scaleY = target.height() / source.height(); + + synchronized (mTiles) { + for (int i = 0, n = mTiles.length; i < n; ++i) { + Tile t = mTiles[i]; + src.set(0, 0, t.contentWidth, t.contentHeight); + src.offset(t.offsetX, t.offsetY); + if (!src.intersect(source)) continue; + mapRect(dest, src, x0, y0, x, y, scaleX, scaleY); + src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY); + canvas.drawTexture(t, src, dest); + } + } + } + + @Override + public int getWidth() { + return mWidth; + } + + @Override + public int getHeight() { + return mHeight; + } + + @Override + public void draw(GLCanvas canvas, int x, int y) { + draw(canvas, x, y, mWidth, mHeight); + } + + @Override + public boolean isOpaque() { + return false; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/Uploaded.java b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/Uploaded.java new file mode 100644 index 000000000..53082666a --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/Uploaded.java @@ -0,0 +1,25 @@ +/* + * 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.gallery.glrenderer; + +public interface Uploaded { + + public boolean isContentValid(); + + public void updateContent(GLCanvas canvas); + +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/UploadedTexture.java b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/UploadedTexture.java new file mode 100644 index 000000000..11be8dc9b --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/UploadedTexture.java @@ -0,0 +1,297 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.glrenderer; + +import java.util.HashMap; + +import javax.microedition.khronos.opengles.GL11; + +import junit.framework.Assert; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.opengl.GLUtils; + +// UploadedTextures use a Bitmap for the content of the texture. +// +// Subclasses should implement onGetBitmap() to provide the Bitmap and +// implement onFreeBitmap(mBitmap) which will be called when the Bitmap +// is not needed anymore. +// +// isContentValid() is meaningful only when the isLoaded() returns true. +// It means whether the content needs to be updated. +// +// The user of this class should call recycle() when the texture is not +// needed anymore. +// +// By default an UploadedTexture is opaque (so it can be drawn faster without +// blending). The user or subclass can override it using setOpaque(). +public abstract class UploadedTexture extends BasicTexture implements Uploaded { + + // To prevent keeping allocation the borders, we store those used borders here. + // Since the length will be power of two, it won't use too much memory. + private static HashMap sBorderLines = + new HashMap(); + private static BorderKey sBorderKey = new BorderKey(); + + @SuppressWarnings("unused") + private static final String TAG = "Texture"; + private boolean mContentValid = true; + + // indicate this textures is being uploaded in background + private boolean mIsUploading = false; + private boolean mOpaque = true; + private boolean mThrottled = false; + private static int sUploadedCount; + private static final int UPLOAD_LIMIT = 100; + + protected Bitmap mBitmap; + private int mBorder; + + protected UploadedTexture() { + this(false); + } + + protected UploadedTexture(boolean hasBorder) { + super(null, 0, STATE_UNLOADED); + if (hasBorder) { + setBorder(true); + mBorder = 1; + } + } + + protected void setIsUploading(boolean uploading) { + mIsUploading = uploading; + } + + public boolean isUploading() { + return mIsUploading; + } + + private static class BorderKey implements Cloneable { + public boolean vertical; + public Config config; + public int length; + + @Override + public int hashCode() { + int x = config.hashCode() ^ length; + return vertical ? x : -x; + } + + @Override + public boolean equals(Object object) { + if (!(object instanceof BorderKey)) return false; + BorderKey o = (BorderKey) object; + return vertical == o.vertical + && config == o.config && length == o.length; + } + + @Override + public BorderKey clone() { + try { + return (BorderKey) super.clone(); + } catch (CloneNotSupportedException e) { + throw new AssertionError(e); + } + } + } + + protected void setThrottled(boolean throttled) { + mThrottled = throttled; + } + + private static Bitmap getBorderLine( + boolean vertical, Config config, int length) { + BorderKey key = sBorderKey; + key.vertical = vertical; + key.config = config; + key.length = length; + Bitmap bitmap = sBorderLines.get(key); + if (bitmap == null) { + bitmap = vertical + ? Bitmap.createBitmap(1, length, config) + : Bitmap.createBitmap(length, 1, config); + sBorderLines.put(key.clone(), bitmap); + } + return bitmap; + } + + private Bitmap getBitmap() { + if (mBitmap == null) { + mBitmap = onGetBitmap(); + int w = mBitmap.getWidth() + mBorder * 2; + int h = mBitmap.getHeight() + mBorder * 2; + if (mWidth == UNSPECIFIED) { + setSize(w, h); + } + } + return mBitmap; + } + + private void freeBitmap() { + Assert.assertTrue(mBitmap != null); + onFreeBitmap(mBitmap); + mBitmap = null; + } + + @Override + public int getWidth() { + if (mWidth == UNSPECIFIED) getBitmap(); + return mWidth; + } + + @Override + public int getHeight() { + if (mWidth == UNSPECIFIED) getBitmap(); + return mHeight; + } + + protected abstract Bitmap onGetBitmap(); + + protected abstract void onFreeBitmap(Bitmap bitmap); + + protected void invalidateContent() { + if (mBitmap != null) freeBitmap(); + mContentValid = false; + mWidth = UNSPECIFIED; + mHeight = UNSPECIFIED; + } + + /** + * Whether the content on GPU is valid. + */ + @Override + public boolean isContentValid() { + return isLoaded() && mContentValid; + } + + /** + * Updates the content on GPU's memory. + * @param canvas + */ + @Override + public void updateContent(GLCanvas canvas) { + if (!isLoaded()) { + if (mThrottled && ++sUploadedCount > UPLOAD_LIMIT) { + return; + } + uploadToCanvas(canvas); + } else if (!mContentValid) { + Bitmap bitmap = getBitmap(); + int format = GLUtils.getInternalFormat(bitmap); + int type = GLUtils.getType(bitmap); + canvas.texSubImage2D(this, mBorder, mBorder, bitmap, format, type); + freeBitmap(); + mContentValid = true; + } + } + + public static void resetUploadLimit() { + sUploadedCount = 0; + } + + public static boolean uploadLimitReached() { + return sUploadedCount > UPLOAD_LIMIT; + } + + private void uploadToCanvas(GLCanvas canvas) { + + Bitmap bitmap = getBitmap(); + if (bitmap != null) { + try { + int bWidth = bitmap.getWidth(); + int bHeight = bitmap.getHeight(); + int texWidth = getTextureWidth(); + int texHeight = getTextureHeight(); + + Assert.assertTrue(bWidth <= texWidth && bHeight <= texHeight); + + // Upload the bitmap to a new texture. + mId = canvas.getGLId().generateTexture(); + canvas.setTextureParameters(this); + + if (bWidth == texWidth && bHeight == texHeight) { + canvas.initializeTexture(this, bitmap); + } else { + int format = GLUtils.getInternalFormat(bitmap); + int type = GLUtils.getType(bitmap); + Config config = bitmap.getConfig(); + + canvas.initializeTextureSize(this, format, type); + canvas.texSubImage2D(this, mBorder, mBorder, bitmap, format, type); + + if (mBorder > 0) { + // Left border + Bitmap line = getBorderLine(true, config, texHeight); + canvas.texSubImage2D(this, 0, 0, line, format, type); + + // Top border + line = getBorderLine(false, config, texWidth); + canvas.texSubImage2D(this, 0, 0, line, format, type); + } + + // Right border + if (mBorder + bWidth < texWidth) { + Bitmap line = getBorderLine(true, config, texHeight); + canvas.texSubImage2D(this, mBorder + bWidth, 0, line, format, type); + } + + // Bottom border + if (mBorder + bHeight < texHeight) { + Bitmap line = getBorderLine(false, config, texWidth); + canvas.texSubImage2D(this, 0, mBorder + bHeight, line, format, type); + } + } + } finally { + freeBitmap(); + } + // Update texture state. + setAssociatedCanvas(canvas); + mState = STATE_LOADED; + mContentValid = true; + } else { + mState = STATE_ERROR; + throw new RuntimeException("Texture load fail, no bitmap"); + } + } + + @Override + protected boolean onBind(GLCanvas canvas) { + updateContent(canvas); + return isContentValid(); + } + + @Override + protected int getTarget() { + return GL11.GL_TEXTURE_2D; + } + + public void setOpaque(boolean isOpaque) { + mOpaque = isOpaque; + } + + @Override + public boolean isOpaque() { + return mOpaque; + } + + @Override + public void recycle() { + super.recycle(); + if (mBitmap != null) freeBitmap(); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/image/GifImage.java b/app/src/main/java/com/hippo/ehviewer/gallery/image/GifImage.java new file mode 100644 index 000000000..b2140a487 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/image/GifImage.java @@ -0,0 +1,113 @@ +/* + * 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.gallery.image; + +import com.hippo.ehviewer.util.Log; + +public class GifImage extends Image { + + private static final String TAG = Image.class.getSimpleName(); + + private final int mImageCount; + private final int[] mDelayArray; + private long mDelaySum; + private volatile long mStartTime; + private volatile long mStopTime = -1; + private volatile boolean mRunning = true; + + private int mLastIndex = -1; + + protected GifImage(long nativeImage, int fileFormat, int width, int height, + int format, int type, int[] delayArray) { + super(nativeImage, fileFormat, width, height, format, type); + mImageCount = delayArray.length; + mDelayArray = delayArray; + + mDelaySum = 0; + for (int delay : delayArray) + mDelaySum += delay; + } + + private int getCurIndex() { + int index = 0; + if (mRunning) { + if (mLastIndex == -1) { + index = 0; + mStartTime = System.currentTimeMillis(); + } else { + long curTime = System.currentTimeMillis(); + long time = (curTime - mStartTime) % mDelaySum; + int end = mImageCount - 1; + for (; index < end && (time -= mDelayArray[index]) > 0; index++) + ; + } + return index; + } else { + return mLastIndex == -1 ? index : mLastIndex; + } + } + + @Override + public void start() { + if (!mRunning) { + mRunning = true; + long curTime = System.currentTimeMillis(); + mStartTime = curTime - (mStopTime == -1 ? curTime : mStopTime) + + mStartTime; + } + } + + @Override + public void stop() { + if (mRunning) { + mRunning = false; + mStopTime = System.currentTimeMillis(); + } + } + + @Override + public boolean isAnimated() { + return mImageCount > 1; + } + + @Override + public void render() { + render(getCurIndex()); + } + + public void render(int index) { + if (mNativeImage != 0) { + if (index >= mImageCount || index < 0) { + Log.d(TAG, "GifImage try render index " + index + ", but image count is " + mImageCount); + return; + } + + if (mLastIndex != index) { + nativeRender(mFormat, mType, mNativeImage, FILE_FORMAT_GIF, + index); + mLastIndex = index; + } + } + } + + static { + System.loadLibrary("image"); + } + + private static native void nativeRender(int format, int type, + long nativeImage, int fileFormat, int index); +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/image/Image.java b/app/src/main/java/com/hippo/ehviewer/gallery/image/Image.java new file mode 100644 index 000000000..8371a0b4f --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/image/Image.java @@ -0,0 +1,133 @@ +/* + * 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.gallery.image; + +import java.io.InputStream; + +import javax.microedition.khronos.opengles.GL11; + +public class Image { + + private static final String TAG = Image.class.getSimpleName(); + + public static final int FILE_FORMAT_JPEG = 0x0; + public static final int FILE_FORMAT_PNG = 0x1; + public static final int FILE_FORMAT_BMP = 0x2; + public static final int FILE_FORMAT_GIF = 0x3; + + public static final int FORMAT_AUTO = 0x0; + public static final int FORMAT_GRAY = GL11.GL_LUMINANCE; + public static final int FORMAT_GRAY_ALPHA = GL11.GL_LUMINANCE_ALPHA; + public static final int FORMAT_RGB = GL11.GL_RGB; + public static final int FORMAT_RGBA = GL11.GL_RGBA; + + protected long mNativeImage; + protected final int mFileFormat; + protected final int mWidth; + protected final int mHeight; + + protected final int mFormat; + protected final int mType; + + protected Image(long nativeImage, int fileFormat, int width, int height, + int format, int type) { + mNativeImage = nativeImage; + mFileFormat = fileFormat; + mWidth = width; + mHeight = height; + mFormat = format; + mType = type; + } + + public int getFileFormat() { + return mFileFormat; + } + + public int getWidth() { + return mWidth; + } + + public int getHeight() { + return mHeight; + } + + public int getFormat() { + return mFormat; + } + + public int getType() { + return mType; + } + + public boolean isAnimated() { + return false; + } + + public void start() { + // Empty + } + + public void stop() { + // Empty + } + + public void render() { + if (mNativeImage != 0) { + nativeRender(mFormat, mType, mNativeImage, mFileFormat); + } + } + + public void recycle() { + if (mNativeImage != 0) { + nativeFree(mNativeImage, mFileFormat); + mNativeImage = 0; + } + } + + public boolean isRecycled() { + return mNativeImage == 0; + } + + /** + * format can be GL11.GL_RGB, GL11.GL_RGBA, GL11.GL_LUMINANCE, + * GL11.GL_LUMINANCE_ALPHA + * + * @param is + * @param format + * @return + */ + public static final Image decodeStream(InputStream is, int format) { + return nativeDecodeStream(is, format); + } + + public static final Image decodeFile(String pathName, int format) { + return nativeDecodeFile(pathName, format); + } + + static { + System.loadLibrary("image"); + } + + private static native Image nativeDecodeStream(InputStream is, int format); + + private static native Image nativeDecodeFile(String pathName, int format); + + private static native void nativeFree(long nativeImage, int format); + + private static native void nativeRender(int format, int type, + long nativeImage, int fileFormat); +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/ui/AnimationTime.java b/app/src/main/java/com/hippo/ehviewer/gallery/ui/AnimationTime.java new file mode 100644 index 000000000..19bb91ca2 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/ui/AnimationTime.java @@ -0,0 +1,45 @@ + +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.ui; + +import android.os.SystemClock; + +// +// The animation time should ideally be the vsync time the frame will be +// displayed, but that is an unknown time in the future. So we use the system +// time just after eglSwapBuffers (when GLSurfaceView.onDrawFrame is called) +// as a approximation. +// +public class AnimationTime { + private static volatile long sTime; + + // Sets current time as the animation time. + public static void update() { + sTime = SystemClock.uptimeMillis(); + } + + // Returns the animation time. + public static long get() { + return sTime; + } + + public static long startTime() { + sTime = SystemClock.uptimeMillis(); + return sTime; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/ui/DownUpDetector.java b/app/src/main/java/com/hippo/ehviewer/gallery/ui/DownUpDetector.java new file mode 100644 index 000000000..ebb0bb401 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/ui/DownUpDetector.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.ui; + +import android.view.MotionEvent; + +public class DownUpDetector { + public interface DownUpListener { + void onDown(MotionEvent e); + void onUp(MotionEvent e); + } + + private boolean mStillDown; + private final DownUpListener mListener; + + public DownUpDetector(DownUpListener listener) { + mListener = listener; + } + + private void setState(boolean down, MotionEvent e) { + if (down == mStillDown) return; + mStillDown = down; + if (down) { + mListener.onDown(e); + } else { + mListener.onUp(e); + } + } + + public void onTouchEvent(MotionEvent ev) { + switch (ev.getAction() & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + setState(true, ev); + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_POINTER_DOWN: // Multitouch event - abort. + setState(false, ev); + break; + } + } + + public boolean isDown() { + return mStillDown; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/ui/GLRoot.java b/app/src/main/java/com/hippo/ehviewer/gallery/ui/GLRoot.java new file mode 100644 index 000000000..9a56a4395 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/ui/GLRoot.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.ui; + +import android.content.Context; +import android.graphics.Matrix; + +import com.hippo.ehviewer.gallery.anim.CanvasAnimation; +import com.hippo.ehviewer.gallery.glrenderer.GLCanvas; + +public interface GLRoot { + + // Listener will be called when GL is idle AND before each frame. + // Mainly used for uploading textures. + public static interface OnGLIdleListener { + public boolean onGLIdle( + GLCanvas canvas, boolean renderRequested); + } + + public void addOnGLIdleListener(OnGLIdleListener listener); + public void registerLaunchedAnimation(CanvasAnimation animation); + public void requestRenderForced(); + public void requestRender(); + public void requestLayoutContentPane(); + + public void lockRenderThread(); + public void unlockRenderThread(); + + public void setContentPane(GLView content); + public void setOrientationSource(OrientationSource source); + public int getDisplayRotation(); + public int getCompensation(); + public Matrix getCompensationMatrix(); + public void freeze(); + public void unfreeze(); + public void setLightsOutMode(boolean enabled); + + public Context getContext(); +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/ui/GLRootView.java b/app/src/main/java/com/hippo/ehviewer/gallery/ui/GLRootView.java new file mode 100644 index 000000000..b47e5a4e9 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/ui/GLRootView.java @@ -0,0 +1,585 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.ui; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; +import javax.microedition.khronos.opengles.GL11; + +import android.annotation.TargetApi; +import android.app.ActivityManager; +import android.content.Context; +import android.content.pm.ConfigurationInfo; +import android.graphics.Matrix; +import android.opengl.GLSurfaceView; +import android.os.Build; +import android.os.Process; +import android.os.SystemClock; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.SurfaceHolder; +import android.view.View; + +import com.hippo.ehviewer.gallery.anim.CanvasAnimation; +import com.hippo.ehviewer.gallery.glrenderer.BasicTexture; +import com.hippo.ehviewer.gallery.glrenderer.GLCanvas; +import com.hippo.ehviewer.gallery.glrenderer.GLES11Canvas; +import com.hippo.ehviewer.gallery.glrenderer.GLES20Canvas; +import com.hippo.ehviewer.gallery.glrenderer.UploadedTexture; +import com.hippo.ehviewer.gallery.util.GalleryUtils; +import com.hippo.ehviewer.gallery.util.MotionEventHelper; +import com.hippo.ehviewer.util.Log; +import com.hippo.ehviewer.util.Utils; + +// The root component of all GLViews. The rendering is done in GL +// thread while the event handling is done in the main thread. To synchronize +// the two threads, the entry points of this package need to synchronize on the +// GLRootView instance unless it can be proved that the rendering +// thread won't access the same thing as the method. The entry points include: +// (1) The public methods of HeadUpDisplay +// (2) The public methods of CameraHeadUpDisplay +// (3) The overridden methods in GLRootView. +public class GLRootView extends GLSurfaceView + implements GLSurfaceView.Renderer, GLRoot { + private static final String TAG = "GLRootView"; + + private static final boolean DEBUG_FPS = false; + private int mFrameCount = 0; + private long mFrameCountingStart = 0; + + private static final boolean DEBUG_INVALIDATE = false; + private int mInvalidateColor = 0; + + private static final boolean DEBUG_DRAWING_STAT = false; + + private static final int FLAG_INITIALIZED = 1; + private static final int FLAG_NEED_LAYOUT = 2; + + private GL11 mGL; + private GLCanvas mCanvas; + private GLView mContentView; + + private OrientationSource mOrientationSource; + // mCompensation is the difference between the UI orientation on GLCanvas + // and the framework orientation. See OrientationManager for details. + private int mCompensation; + // mCompensationMatrix maps the coordinates of touch events. It is kept sync + // with mCompensation. + private final Matrix mCompensationMatrix = new Matrix(); + private int mDisplayRotation; + + private int mFlags = FLAG_NEED_LAYOUT; + private volatile boolean mRenderRequested = false; + + private final ArrayList mAnimations = + new ArrayList(); + + private final ArrayDeque mIdleListeners = + new ArrayDeque(); + + private final IdleRunner mIdleRunner = new IdleRunner(); + + private final ReentrantLock mRenderLock = new ReentrantLock(); + private final Condition mFreezeCondition = + mRenderLock.newCondition(); + private boolean mFreeze; + + private boolean mInDownState = false; + + private final boolean mSupportGLES20; + + public GLRootView(Context context) { + this(context, null); + } + + private static boolean checkGLES20Support(Context context) { + ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + ConfigurationInfo info = am.getDeviceConfigurationInfo(); + return info.reqGlEsVersion >= 0x20000; + } + + @SuppressWarnings("deprecation") + public GLRootView(Context context, AttributeSet attrs) { + super(context, attrs); + mSupportGLES20 = checkGLES20Support(context); + mFlags |= FLAG_INITIALIZED; + setBackgroundDrawable(null); + setEGLContextClientVersion(mSupportGLES20 ? 2 : 1); + + // We can't detect 888_PIXEL_FORMAT by Build.VERSION.SDK_INT, + // for example CyanogenMod use ro.opengles.surface.rgb565, + // so just use default EGLConfigChooser. + // And we do not need depth buffer + setEGLConfigChooser(false); + setRenderer(this); + + // Uncomment this to enable gl error check. + // setDebugFlags(DEBUG_CHECK_GL_ERROR); + } + + @Override + public void registerLaunchedAnimation(CanvasAnimation animation) { + // Register the newly launched animation so that we can set the start + // time more precisely. (Usually, it takes much longer for first + // rendering, so we set the animation start time as the time we + // complete rendering) + mAnimations.add(animation); + } + + @Override + public void addOnGLIdleListener(OnGLIdleListener listener) { + synchronized (mIdleListeners) { + mIdleListeners.addLast(listener); + mIdleRunner.enable(); + } + } + + @Override + public void setContentPane(GLView content) { + if (mContentView == content) return; + if (mContentView != null) { + if (mInDownState) { + long now = SystemClock.uptimeMillis(); + MotionEvent cancelEvent = MotionEvent.obtain( + now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0); + mContentView.dispatchTouchEvent(cancelEvent); + cancelEvent.recycle(); + mInDownState = false; + } + mContentView.detachFromRoot(); + BasicTexture.yieldAllTextures(); + } + mContentView = content; + if (content != null) { + content.attachToRoot(this); + requestLayoutContentPane(); + } + } + + @Override + public void requestRenderForced() { + superRequestRender(); + } + + @Override + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public void requestRender() { + if (DEBUG_INVALIDATE) { + StackTraceElement e = Thread.currentThread().getStackTrace()[4]; + String caller = e.getFileName() + ":" + e.getLineNumber() + " "; + Log.d(TAG, "invalidate: " + caller); + } + if (mRenderRequested) return; + mRenderRequested = true; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + postOnAnimation(mRequestRenderOnAnimationFrame); + } else { + super.requestRender(); + } + } + + private final Runnable mRequestRenderOnAnimationFrame = new Runnable() { + @Override + public void run() { + superRequestRender(); + } + }; + + private void superRequestRender() { + super.requestRender(); + } + + @Override + public void requestLayoutContentPane() { + mRenderLock.lock(); + try { + if (mContentView == null || (mFlags & FLAG_NEED_LAYOUT) != 0) return; + + // "View" system will invoke onLayout() for initialization(bug ?), we + // have to ignore it since the GLThread is not ready yet. + if ((mFlags & FLAG_INITIALIZED) == 0) return; + + mFlags |= FLAG_NEED_LAYOUT; + requestRender(); + } finally { + mRenderLock.unlock(); + } + } + + private void layoutContentPane() { + mFlags &= ~FLAG_NEED_LAYOUT; + + int w = getWidth(); + int h = getHeight(); + int displayRotation = 0; + int compensation = 0; + + // Get the new orientation values + if (mOrientationSource != null) { + displayRotation = mOrientationSource.getDisplayRotation(); + compensation = mOrientationSource.getCompensation(); + } else { + displayRotation = 0; + compensation = 0; + } + + if (mCompensation != compensation) { + mCompensation = compensation; + if (mCompensation % 180 != 0) { + mCompensationMatrix.setRotate(mCompensation); + // move center to origin before rotation + mCompensationMatrix.preTranslate(-w / 2, -h / 2); + // align with the new origin after rotation + mCompensationMatrix.postTranslate(h / 2, w / 2); + } else { + mCompensationMatrix.setRotate(mCompensation, w / 2, h / 2); + } + } + mDisplayRotation = displayRotation; + + // Do the actual layout. + if (mCompensation % 180 != 0) { + int tmp = w; + w = h; + h = tmp; + } + Log.i(TAG, "layout content pane " + w + "x" + h + + " (compensation " + mCompensation + ")"); + if (mContentView != null && w != 0 && h != 0) { + mContentView.layout(0, 0, w, h); + } + // Uncomment this to dump the view hierarchy. + //mContentView.dumpTree(""); + } + + @Override + protected void onLayout( + boolean changed, int left, int top, int right, int bottom) { + if (changed) requestLayoutContentPane(); + } + + /** + * Called when the context is created, possibly after automatic destruction. + */ + // This is a GLSurfaceView.Renderer callback + @Override + public void onSurfaceCreated(GL10 gl1, EGLConfig config) { + GL11 gl = (GL11) gl1; + if (mGL != null) { + // The GL Object has changed + Log.i(TAG, "GLObject has changed from " + mGL + " to " + gl); + } + mRenderLock.lock(); + try { + mGL = gl; + mCanvas = mSupportGLES20 ? new GLES20Canvas() : new GLES11Canvas(gl); + BasicTexture.invalidateAllTextures(); + } finally { + mRenderLock.unlock(); + } + + if (DEBUG_FPS) { + setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); + } else { + setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); + } + } + + /** + * Called when the OpenGL surface is recreated without destroying the + * context. + */ + // This is a GLSurfaceView.Renderer callback + @Override + public void onSurfaceChanged(GL10 gl1, int width, int height) { + Log.i(TAG, "onSurfaceChanged: " + width + "x" + height + + ", gl10: " + gl1.toString()); + Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY); + GalleryUtils.setRenderThread(); + + GL11 gl = (GL11) gl1; + Utils.assertTrue(mGL == gl); + + mCanvas.setSize(width, height); + } + + private void outputFps() { + long now = System.nanoTime(); + if (mFrameCountingStart == 0) { + mFrameCountingStart = now; + } else if ((now - mFrameCountingStart) > 1000000000) { + Log.d(TAG, "fps: " + (double) mFrameCount + * 1000000000 / (now - mFrameCountingStart)); + mFrameCountingStart = now; + mFrameCount = 0; + } + ++mFrameCount; + } + + @Override + public void onDrawFrame(GL10 gl) { + AnimationTime.update(); + mRenderLock.lock(); + + while (mFreeze) { + mFreezeCondition.awaitUninterruptibly(); + } + + try { + onDrawFrameLocked(gl); + } finally { + mRenderLock.unlock(); + } + } + + private void onDrawFrameLocked(GL10 gl) { + if (DEBUG_FPS) outputFps(); + + // release the unbound textures and deleted buffers. + mCanvas.deleteRecycledResources(); + + // reset texture upload limit + UploadedTexture.resetUploadLimit(); + + mRenderRequested = false; + + if ((mOrientationSource != null + && mDisplayRotation != mOrientationSource.getDisplayRotation()) + || (mFlags & FLAG_NEED_LAYOUT) != 0) { + layoutContentPane(); + } + + mCanvas.save(GLCanvas.SAVE_FLAG_ALL); + rotateCanvas(-mCompensation); + if (mContentView != null) { + mContentView.render(mCanvas); + } else { + // Make sure we always draw something to prevent displaying garbage + mCanvas.clearBuffer(); + } + mCanvas.restore(); + + if (!mAnimations.isEmpty()) { + long now = AnimationTime.get(); + for (int i = 0, n = mAnimations.size(); i < n; i++) { + mAnimations.get(i).setStartTime(now); + } + mAnimations.clear(); + } + + if (UploadedTexture.uploadLimitReached()) { + requestRender(); + } + + synchronized (mIdleListeners) { + if (!mIdleListeners.isEmpty()) mIdleRunner.enable(); + } + + if (DEBUG_INVALIDATE) { + mCanvas.fillRect(10, 10, 5, 5, mInvalidateColor); + mInvalidateColor = ~mInvalidateColor; + } + + if (DEBUG_DRAWING_STAT) { + mCanvas.dumpStatisticsAndClear(); + } + } + + private void rotateCanvas(int degrees) { + if (degrees == 0) return; + int w = getWidth(); + int h = getHeight(); + int cx = w / 2; + int cy = h / 2; + mCanvas.translate(cx, cy); + mCanvas.rotate(degrees, 0, 0, 1); + if (degrees % 180 != 0) { + mCanvas.translate(-cy, -cx); + } else { + mCanvas.translate(-cx, -cy); + } + } + + @Override + public boolean dispatchTouchEvent(MotionEvent event) { + if (!isEnabled()) return false; + + int action = event.getAction(); + if (action == MotionEvent.ACTION_CANCEL + || action == MotionEvent.ACTION_UP) { + mInDownState = false; + } else if (!mInDownState && action != MotionEvent.ACTION_DOWN) { + return false; + } + + if (mCompensation != 0) { + event = MotionEventHelper.transformEvent(event, mCompensationMatrix); + } + + mRenderLock.lock(); + try { + // If this has been detached from root, we don't need to handle event + boolean handled = mContentView != null + && mContentView.dispatchTouchEvent(event); + if (action == MotionEvent.ACTION_DOWN && handled) { + mInDownState = true; + } + return handled; + } finally { + mRenderLock.unlock(); + } + } + + private class IdleRunner implements Runnable { + // true if the idle runner is in the queue + private boolean mActive = false; + + @Override + public void run() { + OnGLIdleListener listener; + synchronized (mIdleListeners) { + mActive = false; + if (mIdleListeners.isEmpty()) return; + listener = mIdleListeners.removeFirst(); + } + mRenderLock.lock(); + boolean keepInQueue; + try { + keepInQueue = listener.onGLIdle(mCanvas, mRenderRequested); + } finally { + mRenderLock.unlock(); + } + synchronized (mIdleListeners) { + if (keepInQueue) mIdleListeners.addLast(listener); + if (!mRenderRequested && !mIdleListeners.isEmpty()) enable(); + } + } + + public void enable() { + // Who gets the flag can add it to the queue + if (mActive) return; + mActive = true; + queueEvent(this); + } + } + + @Override + public void lockRenderThread() { + mRenderLock.lock(); + } + + @Override + public void unlockRenderThread() { + mRenderLock.unlock(); + } + + @Override + public void onPause() { + unfreeze(); + super.onPause(); + } + + @Override + public void setOrientationSource(OrientationSource source) { + mOrientationSource = source; + } + + @Override + public int getDisplayRotation() { + return mDisplayRotation; + } + + @Override + public int getCompensation() { + return mCompensation; + } + + @Override + public Matrix getCompensationMatrix() { + return mCompensationMatrix; + } + + @Override + public void freeze() { + mRenderLock.lock(); + mFreeze = true; + mRenderLock.unlock(); + } + + @Override + public void unfreeze() { + mRenderLock.lock(); + mFreeze = false; + mFreezeCondition.signalAll(); + mRenderLock.unlock(); + } + + @Override + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public void setLightsOutMode(boolean enabled) { + int flags = 0; + if (enabled) { + flags = View.SYSTEM_UI_FLAG_LOW_PROFILE ; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + flags |= (View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); + } + } + setSystemUiVisibility(flags); + } + + // We need to unfreeze in the following methods and in onPause(). + // These methods will wait on GLThread. If we have freezed the GLRootView, + // the GLThread will wait on main thread to call unfreeze and cause dead + // lock. + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { + unfreeze(); + super.surfaceChanged(holder, format, w, h); + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + unfreeze(); + super.surfaceCreated(holder); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + unfreeze(); + super.surfaceDestroyed(holder); + } + + @Override + protected void onDetachedFromWindow() { + unfreeze(); + super.onDetachedFromWindow(); + } + + @Override + protected void finalize() throws Throwable { + try { + unfreeze(); + } finally { + super.finalize(); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/ui/GLView.java b/app/src/main/java/com/hippo/ehviewer/gallery/ui/GLView.java new file mode 100644 index 000000000..203e3373b --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/ui/GLView.java @@ -0,0 +1,469 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.ui; + +import java.util.ArrayList; + +import android.annotation.SuppressLint; +import android.graphics.Rect; +import android.os.SystemClock; +import android.view.MotionEvent; + +import com.hippo.ehviewer.gallery.anim.CanvasAnimation; +import com.hippo.ehviewer.gallery.anim.StateTransitionAnimation; +import com.hippo.ehviewer.gallery.glrenderer.GLCanvas; +import com.hippo.ehviewer.util.Log; +import com.hippo.ehviewer.util.Utils; + +// GLView is a UI component. It can render to a GLCanvas and accept touch +// events. A GLView may have zero or more child GLView and they form a tree +// structure. The rendering and event handling will pass through the tree +// structure. +// +// A GLView tree should be attached to a GLRoot before event dispatching and +// rendering happens. GLView asks GLRoot to re-render or re-layout the +// GLView hierarchy using requestRender() and requestLayoutContentPane(). +// +// The render() method is called in a separate thread. Before calling +// dispatchTouchEvent() and layout(), GLRoot acquires a lock to avoid the +// rendering thread running at the same time. If there are other entry points +// from main thread (like a Handler) in your GLView, you need to call +// lockRendering() if the rendering thread should not run at the same time. +// +public class GLView { + private static final String TAG = "GLView"; + + public static final int VISIBLE = 0; + public static final int INVISIBLE = 1; + + private static final int FLAG_INVISIBLE = 1; + private static final int FLAG_SET_MEASURED_SIZE = 2; + private static final int FLAG_LAYOUT_REQUESTED = 4; + + public interface OnClickListener { + void onClick(GLView v); + } + + protected final Rect mBounds = new Rect(); + protected final Rect mPaddings = new Rect(); + + private GLRoot mRoot; + protected GLView mParent; + private ArrayList mComponents; + private GLView mMotionTarget; + + private CanvasAnimation mAnimation; + + private int mViewFlags = 0; + + protected int mMeasuredWidth = 0; + protected int mMeasuredHeight = 0; + + private int mLastWidthSpec = -1; + private int mLastHeightSpec = -1; + + protected int mScrollY = 0; + protected int mScrollX = 0; + protected int mScrollHeight = 0; + protected int mScrollWidth = 0; + + private float [] mBackgroundColor; + private StateTransitionAnimation mTransition; + + public void startAnimation(CanvasAnimation animation) { + GLRoot root = getGLRoot(); + if (root == null) throw new IllegalStateException(); + mAnimation = animation; + if (mAnimation != null) { + mAnimation.start(); + root.registerLaunchedAnimation(mAnimation); + } + invalidate(); + } + + // Sets the visiblity of this GLView (either GLView.VISIBLE or + // GLView.INVISIBLE). + public void setVisibility(int visibility) { + if (visibility == getVisibility()) return; + if (visibility == VISIBLE) { + mViewFlags &= ~FLAG_INVISIBLE; + } else { + mViewFlags |= FLAG_INVISIBLE; + } + onVisibilityChanged(visibility); + invalidate(); + } + + // Returns GLView.VISIBLE or GLView.INVISIBLE + public int getVisibility() { + return (mViewFlags & FLAG_INVISIBLE) == 0 ? VISIBLE : INVISIBLE; + } + + // This should only be called on the content pane (the topmost GLView). + public void attachToRoot(GLRoot root) { + Utils.assertTrue(mParent == null && mRoot == null); + onAttachToRoot(root); + } + + // This should only be called on the content pane (the topmost GLView). + public void detachFromRoot() { + Utils.assertTrue(mParent == null && mRoot != null); + onDetachFromRoot(); + } + + // Returns the number of children of the GLView. + public int getComponentCount() { + return mComponents == null ? 0 : mComponents.size(); + } + + // Returns the children for the given index. + public GLView getComponent(int index) { + if (mComponents == null) { + throw new ArrayIndexOutOfBoundsException(index); + } + return mComponents.get(index); + } + + // Adds a child to this GLView. + public void addComponent(GLView component) { + // Make sure the component doesn't have a parent currently. + if (component.mParent != null) throw new IllegalStateException(); + + // Build parent-child links + if (mComponents == null) { + mComponents = new ArrayList(); + } + mComponents.add(component); + component.mParent = this; + + // If this is added after we have a root, tell the component. + if (mRoot != null) { + component.onAttachToRoot(mRoot); + } + } + + // Removes a child from this GLView. + public boolean removeComponent(GLView component) { + if (mComponents == null) return false; + if (mComponents.remove(component)) { + removeOneComponent(component); + return true; + } + return false; + } + + // Removes all children of this GLView. + public void removeAllComponents() { + for (int i = 0, n = mComponents.size(); i < n; ++i) { + removeOneComponent(mComponents.get(i)); + } + mComponents.clear(); + } + + private void removeOneComponent(GLView component) { + if (mMotionTarget == component) { + long now = SystemClock.uptimeMillis(); + MotionEvent cancelEvent = MotionEvent.obtain( + now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0); + dispatchTouchEvent(cancelEvent); + cancelEvent.recycle(); + } + component.onDetachFromRoot(); + component.mParent = null; + } + + public Rect bounds() { + return mBounds; + } + + public int getWidth() { + return mBounds.right - mBounds.left; + } + + public int getHeight() { + return mBounds.bottom - mBounds.top; + } + + public GLRoot getGLRoot() { + return mRoot; + } + + // Request re-rendering of the view hierarchy. + // This is used for animation or when the contents changed. + public void invalidate() { + GLRoot root = getGLRoot(); + if (root != null) root.requestRender(); + } + + // Request re-layout of the view hierarchy. + public void requestLayout() { + mViewFlags |= FLAG_LAYOUT_REQUESTED; + mLastHeightSpec = -1; + mLastWidthSpec = -1; + if (mParent != null) { + mParent.requestLayout(); + } else { + // Is this a content pane ? + GLRoot root = getGLRoot(); + if (root != null) root.requestLayoutContentPane(); + } + } + + protected void render(GLCanvas canvas) { + boolean transitionActive = false; + if (mTransition != null && mTransition.calculate(AnimationTime.get())) { + invalidate(); + transitionActive = mTransition.isActive(); + } + renderBackground(canvas); + canvas.save(); + if (transitionActive) { + mTransition.applyContentTransform(this, canvas); + } + for (int i = 0, n = getComponentCount(); i < n; ++i) { + renderChild(canvas, getComponent(i)); + } + canvas.restore(); + if (transitionActive) { + mTransition.applyOverlay(this, canvas); + } + } + + public void setIntroAnimation(StateTransitionAnimation intro) { + mTransition = intro; + if (mTransition != null) mTransition.start(); + } + + public float [] getBackgroundColor() { + return mBackgroundColor; + } + + public void setBackgroundColor(float [] color) { + mBackgroundColor = color; + } + + protected void renderBackground(GLCanvas view) { + if (mBackgroundColor != null) { + view.clearBuffer(mBackgroundColor); + } + if (mTransition != null && mTransition.isActive()) { + mTransition.applyBackground(this, view); + return; + } + } + + protected void renderChild(GLCanvas canvas, GLView component) { + if (component.getVisibility() != GLView.VISIBLE + && component.mAnimation == null) return; + + int xoffset = component.mBounds.left - mScrollX; + int yoffset = component.mBounds.top - mScrollY; + + canvas.translate(xoffset, yoffset); + + CanvasAnimation anim = component.mAnimation; + if (anim != null) { + canvas.save(anim.getCanvasSaveFlags()); + if (anim.calculate(AnimationTime.get())) { + invalidate(); + } else { + component.mAnimation = null; + } + anim.apply(canvas); + } + component.render(canvas); + if (anim != null) canvas.restore(); + canvas.translate(-xoffset, -yoffset); + } + + protected boolean onTouch(MotionEvent event) { + return false; + } + + protected boolean dispatchTouchEvent(MotionEvent event, + int x, int y, GLView component, boolean checkBounds) { + Rect rect = component.mBounds; + int left = rect.left; + int top = rect.top; + if (!checkBounds || rect.contains(x, y)) { + event.offsetLocation(-left, -top); + if (component.dispatchTouchEvent(event)) { + event.offsetLocation(left, top); + return true; + } + event.offsetLocation(left, top); + } + return false; + } + + protected boolean dispatchTouchEvent(MotionEvent event) { + int x = (int) event.getX(); + int y = (int) event.getY(); + int action = event.getAction(); + if (mMotionTarget != null) { + if (action == MotionEvent.ACTION_DOWN) { + MotionEvent cancel = MotionEvent.obtain(event); + cancel.setAction(MotionEvent.ACTION_CANCEL); + dispatchTouchEvent(cancel, x, y, mMotionTarget, false); + mMotionTarget = null; + } else { + dispatchTouchEvent(event, x, y, mMotionTarget, false); + if (action == MotionEvent.ACTION_CANCEL + || action == MotionEvent.ACTION_UP) { + mMotionTarget = null; + } + return true; + } + } + if (action == MotionEvent.ACTION_DOWN) { + // in the reverse rendering order + for (int i = getComponentCount() - 1; i >= 0; --i) { + GLView component = getComponent(i); + if (component.getVisibility() != GLView.VISIBLE) continue; + if (dispatchTouchEvent(event, x, y, component, true)) { + mMotionTarget = component; + return true; + } + } + } + return onTouch(event); + } + + public Rect getPaddings() { + return mPaddings; + } + + @SuppressLint("WrongCall") + public void layout(int left, int top, int right, int bottom) { + boolean sizeChanged = setBounds(left, top, right, bottom); + mViewFlags &= ~FLAG_LAYOUT_REQUESTED; + // We call onLayout no matter sizeChanged is true or not because the + // orientation may change without changing the size of the View (for + // example, rotate the device by 180 degrees), and we want to handle + // orientation change in onLayout. + onLayout(sizeChanged, left, top, right, bottom); + } + + private boolean setBounds(int left, int top, int right, int bottom) { + boolean sizeChanged = (right - left) != (mBounds.right - mBounds.left) + || (bottom - top) != (mBounds.bottom - mBounds.top); + mBounds.set(left, top, right, bottom); + return sizeChanged; + } + + @SuppressLint("WrongCall") + public void measure(int widthSpec, int heightSpec) { + if (widthSpec == mLastWidthSpec && heightSpec == mLastHeightSpec + && (mViewFlags & FLAG_LAYOUT_REQUESTED) == 0) { + return; + } + + mLastWidthSpec = widthSpec; + mLastHeightSpec = heightSpec; + + mViewFlags &= ~FLAG_SET_MEASURED_SIZE; + onMeasure(widthSpec, heightSpec); + if ((mViewFlags & FLAG_SET_MEASURED_SIZE) == 0) { + throw new IllegalStateException(getClass().getName() + + " should call setMeasuredSize() in onMeasure()"); + } + } + + protected void onMeasure(int widthSpec, int heightSpec) { + } + + protected void setMeasuredSize(int width, int height) { + mViewFlags |= FLAG_SET_MEASURED_SIZE; + mMeasuredWidth = width; + mMeasuredHeight = height; + } + + public int getMeasuredWidth() { + return mMeasuredWidth; + } + + public int getMeasuredHeight() { + return mMeasuredHeight; + } + + protected void onLayout( + boolean changeSize, int left, int top, int right, int bottom) { + } + + /** + * Gets the bounds of the given descendant that relative to this view. + */ + public boolean getBoundsOf(GLView descendant, Rect out) { + int xoffset = 0; + int yoffset = 0; + GLView view = descendant; + while (view != this) { + if (view == null) return false; + Rect bounds = view.mBounds; + xoffset += bounds.left; + yoffset += bounds.top; + view = view.mParent; + } + out.set(xoffset, yoffset, xoffset + descendant.getWidth(), + yoffset + descendant.getHeight()); + return true; + } + + protected void onVisibilityChanged(int visibility) { + for (int i = 0, n = getComponentCount(); i < n; ++i) { + GLView child = getComponent(i); + if (child.getVisibility() == GLView.VISIBLE) { + child.onVisibilityChanged(visibility); + } + } + } + + protected void onAttachToRoot(GLRoot root) { + mRoot = root; + for (int i = 0, n = getComponentCount(); i < n; ++i) { + getComponent(i).onAttachToRoot(root); + } + } + + protected void onDetachFromRoot() { + for (int i = 0, n = getComponentCount(); i < n; ++i) { + getComponent(i).onDetachFromRoot(); + } + mRoot = null; + } + + public void lockRendering() { + if (mRoot != null) { + mRoot.lockRenderThread(); + } + } + + public void unlockRendering() { + if (mRoot != null) { + mRoot.unlockRenderThread(); + } + } + + // This is for debugging only. + // Dump the view hierarchy into log. + void dumpTree(String prefix) { + Log.d(TAG, prefix + getClass().getSimpleName()); + for (int i = 0, n = getComponentCount(); i < n; ++i) { + getComponent(i).dumpTree(prefix + "...."); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/ui/GestureRecognizer.java b/app/src/main/java/com/hippo/ehviewer/gallery/ui/GestureRecognizer.java new file mode 100644 index 000000000..74377d942 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/ui/GestureRecognizer.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.ui; + +import android.content.Context; +import android.os.SystemClock; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; + +// This class aggregates three gesture detectors: GestureDetector, +// ScaleGestureDetector, and DownUpDetector. +public class GestureRecognizer { + @SuppressWarnings("unused") + private static final String TAG = "GestureRecognizer"; + + public interface Listener { + boolean onSingleTapConfirmed(float x, float y); + boolean onDoubleTap(float x, float y); + boolean onDoubleTapConfirmed(float x, float y); + void onLongPress(MotionEvent e); + boolean onScrollBegin(float dx, float dy, MotionEvent e1, MotionEvent e2); + boolean onScroll(float dx, float dy, MotionEvent e1, MotionEvent e2); + boolean onScrollEnd(float totalX, float totalY); + boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY); + boolean onScaleBegin(float focusX, float focusY); + boolean onScale(float focusX, float focusY, float scale); + void onScaleEnd(); + void onDown(float x, float y); + void onUp(); + } + + private final GestureDetector mGestureDetector; + private final ScaleGestureDetector mScaleDetector; + private final DownUpDetector mDownUpDetector; + private final Listener mListener; + + private boolean mStillScroll = false; + + public GestureRecognizer(Context context, Listener listener) { + mListener = listener; + mScaleDetector = new ScaleGestureDetector( + context, new MyScaleListener()); + mGestureDetector = new GestureDetector(context, new MyGestureListener(), + null, true /* ignoreMultitouch */); + mDownUpDetector = new DownUpDetector(new MyDownUpListener()); + } + + public void onTouchEvent(MotionEvent event) { + mGestureDetector.onTouchEvent(event); + mScaleDetector.onTouchEvent(event); + mDownUpDetector.onTouchEvent(event); + } + + public boolean isDown() { + return mDownUpDetector.isDown(); + } + + public void cancelScale() { + long now = SystemClock.uptimeMillis(); + MotionEvent cancelEvent = MotionEvent.obtain( + now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0); + mScaleDetector.onTouchEvent(cancelEvent); + cancelEvent.recycle(); + } + + private class MyGestureListener + extends GestureDetector.SimpleOnGestureListener { + float firstX = 0; + float firstY = 0; + @Override + public boolean onSingleTapConfirmed(MotionEvent e) { + return mListener.onSingleTapConfirmed(e.getX(), e.getY()); + } + + @Override + public boolean onDoubleTap(MotionEvent e) { + return mListener.onDoubleTap(e.getX(), e.getY()); + } + + @Override + public boolean onDoubleTapEvent(MotionEvent e) { + if (e.getAction() == MotionEvent.ACTION_UP || + e.getAction() == MotionEvent.ACTION_CANCEL) + return mListener.onDoubleTapConfirmed(e.getX(), e.getY()); + else + return super.onDoubleTapEvent(e); + } + + @Override + public void onLongPress(MotionEvent e) { + mListener.onLongPress(e); + } + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float dx, float dy) { + if (e1.getX() != firstX || e1.getY() != firstY) { + mStillScroll = true; + firstX = e1.getX(); + firstY = e1.getY(); + return mListener.onScrollBegin(dx, dy, e1, e2); + } else { + return mListener.onScroll(dx, dy, e1, e2); + } + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, + float velocityY) { + return mListener.onFling(e1, e2, velocityX, velocityY); + } + } + + private class MyScaleListener + extends ScaleGestureDetector.SimpleOnScaleGestureListener { + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + return mListener.onScaleBegin( + detector.getFocusX(), detector.getFocusY()); + } + + @Override + public boolean onScale(ScaleGestureDetector detector) { + return mListener.onScale(detector.getFocusX(), + detector.getFocusY(), detector.getScaleFactor()); + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + mListener.onScaleEnd(); + } + } + + private class MyDownUpListener implements DownUpDetector.DownUpListener { + + private float mFirstX; + private float mFirstY; + + @Override + public void onDown(MotionEvent e) { + mListener.onDown(mFirstX = e.getX(), mFirstY = e.getY()); + } + + @Override + public void onUp(MotionEvent e) { + mListener.onUp(); + if (mStillScroll) { + mStillScroll = false; + mListener.onScrollEnd(e.getX() - mFirstX, e.getY() - mFirstY); + } + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/ui/OrientationSource.java b/app/src/main/java/com/hippo/ehviewer/gallery/ui/OrientationSource.java new file mode 100644 index 000000000..2765a130c --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/ui/OrientationSource.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.ui; + +public interface OrientationSource { + public int getDisplayRotation(); + public int getCompensation(); +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/ui/SynchronizedHandler.java b/app/src/main/java/com/hippo/ehviewer/gallery/ui/SynchronizedHandler.java new file mode 100644 index 000000000..798d9cb06 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/ui/SynchronizedHandler.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.ui; + +import android.os.Handler; +import android.os.Message; + +import com.hippo.ehviewer.util.Utils; + +public class SynchronizedHandler extends Handler { + + private final GLRoot mRoot; + + public SynchronizedHandler(GLRoot root) { + mRoot = Utils.checkNotNull(root); + } + + @Override + public void dispatchMessage(Message message) { + mRoot.lockRenderThread(); + try { + super.dispatchMessage(message); + } finally { + mRoot.unlockRenderThread(); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/ui/TimeRunner.java b/app/src/main/java/com/hippo/ehviewer/gallery/ui/TimeRunner.java new file mode 100644 index 000000000..87d44bb38 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/ui/TimeRunner.java @@ -0,0 +1,159 @@ +/* + * 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.gallery.ui; + +import android.animation.TimeInterpolator; +import android.os.Handler; + +public abstract class TimeRunner { + private static final int INTERVAL = 5; + + private static final int RUNNING = 0x0; + private static final int START = 0x1; + private static final int END = 0x2; + private static final int CANCEL = 0x3; + + private boolean mDoInThread = false; + private int mDuration = 0; + private int mDelay = 0; + private TimeInterpolator mInterpolator; + private Handler mHandler; + private volatile boolean mCancel = false; + + private class Respond implements Runnable { + private final int state; + private float interpolatedTime; + private int runningTime; + + public Respond(int state) { + this.state = state; + } + + public Respond(float interpolatedTime, int runningTime) { + this.state = RUNNING; + this.interpolatedTime = interpolatedTime; + this.runningTime = runningTime; + } + + @Override + public void run() { + switch (state) { + case RUNNING: + TimeRunner.this.onRun(interpolatedTime, runningTime); + break; + case START: + TimeRunner.this.onStart(); + break; + case END: + TimeRunner.this.onEnd(); + break; + case CANCEL: + TimeRunner.this.onCancel(); + } + } + } + + public void setDuration(int duration) { + mDuration = duration; + } + + public void setDoInThread(boolean doInThread) { + mDoInThread = doInThread; + } + + public void setHandler(Handler handler) { + mHandler = handler; + mDoInThread = false; + } + + public void setInterpolator(TimeInterpolator interpolator) { + mInterpolator = interpolator; + } + + public void cancel() { + mCancel = true; + } + + public void start() { + start(0); + } + + public void start(int delay) { + mCancel = false; + mDelay = delay; + new Thread(new Runnable() { + @Override + public void run() { + try { + Thread.sleep(mDelay); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + if (mDoInThread || mHandler == null) + onStart(); + else + mHandler.post(new Respond(START)); + + // Check cancel + if (mCancel) { + if (mDoInThread || mHandler == null) + onStart(); + else + mHandler.post(new Respond(CANCEL)); + return; + } + + long startTime = System.currentTimeMillis(); + for (int runningTime = 0; runningTime < mDuration; runningTime = (int)(System.currentTimeMillis() - startTime)) { + // Check cancel + if (mCancel) { + if (mDoInThread || mHandler == null) + onStart(); + else + mHandler.post(new Respond(CANCEL)); + return; + } + + float percent = runningTime / (float)mDuration; + if (mInterpolator != null) + percent = mInterpolator.getInterpolation(percent); + + if (mDoInThread || mHandler == null) + TimeRunner.this.onRun(percent, runningTime); + else + mHandler.post(new Respond(percent, runningTime)); + try { + Thread.sleep(INTERVAL); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + if (mDoInThread || mHandler == null) + onEnd(); + else + mHandler.post(new Respond(END)); + } + }).start(); + } + + protected abstract void onStart(); + protected abstract void onRun(float percent, int runningTime); + protected abstract void onEnd(); + protected abstract void onCancel(); +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/util/GalleryUtils.java b/app/src/main/java/com/hippo/ehviewer/gallery/util/GalleryUtils.java new file mode 100644 index 000000000..348ce71b2 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/util/GalleryUtils.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery.util; + +import android.graphics.Color; + +import com.hippo.ehviewer.util.Log; + +public class GalleryUtils { + private static final String TAG = GalleryUtils.class.getSimpleName(); + + public static float[] intColorToFloatARGBArray(int from) { + return new float[] { + Color.alpha(from) / 255f, + Color.red(from) / 255f, + Color.green(from) / 255f, + Color.blue(from) / 255f + }; + } + + // Below are used the detect using database in the render thread. It only + // works most of the time, but that's ok because it's for debugging only. + + private static volatile Thread sCurrentThread; + private static volatile boolean sWarned; + + public static void setRenderThread() { + sCurrentThread = Thread.currentThread(); + } + + public static void assertNotInRenderThread() { + if (!sWarned) { + if (Thread.currentThread() == sCurrentThread) { + sWarned = true; + Log.w(TAG, new Throwable("Should not do this in render thread")); + } + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/util/MotionEventHelper.java b/app/src/main/java/com/hippo/ehviewer/gallery/util/MotionEventHelper.java new file mode 100644 index 000000000..fe882fb02 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/util/MotionEventHelper.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hippo.ehviewer.gallery.util; + +import android.annotation.SuppressLint; +import android.graphics.Matrix; +import android.os.Build; +import android.view.MotionEvent; +import android.view.MotionEvent.PointerCoords; + +public final class MotionEventHelper { + private MotionEventHelper() {} + + public static MotionEvent transformEvent(MotionEvent e, Matrix m) { + // We try to use the new transform method if possible because it uses + // less memory. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + return transformEventNew(e, m); + } else { + return transformEventOld(e, m); + } + } + + @SuppressLint("Recycle") + private static MotionEvent transformEventNew(MotionEvent e, Matrix m) { + MotionEvent newEvent = MotionEvent.obtain(e); + newEvent.transform(m); + return newEvent; + } + + // This is copied from Input.cpp in the android framework. + @SuppressLint("Recycle") + private static MotionEvent transformEventOld(MotionEvent e, Matrix m) { + long downTime = e.getDownTime(); + long eventTime = e.getEventTime(); + int action = e.getAction(); + int pointerCount = e.getPointerCount(); + int[] pointerIds = getPointerIds(e); + PointerCoords[] pointerCoords = getPointerCoords(e); + int metaState = e.getMetaState(); + float xPrecision = e.getXPrecision(); + float yPrecision = e.getYPrecision(); + int deviceId = e.getDeviceId(); + int edgeFlags = e.getEdgeFlags(); + int source = e.getSource(); + int flags = e.getFlags(); + + // Copy the x and y coordinates into an array, map them, and copy back. + float[] xy = new float[pointerCoords.length * 2]; + for (int i = 0; i < pointerCount;i++) { + xy[2 * i] = pointerCoords[i].x; + xy[2 * i + 1] = pointerCoords[i].y; + } + m.mapPoints(xy); + for (int i = 0; i < pointerCount;i++) { + pointerCoords[i].x = xy[2 * i]; + pointerCoords[i].y = xy[2 * i + 1]; + pointerCoords[i].orientation = transformAngle( + m, pointerCoords[i].orientation); + } + + @SuppressWarnings("deprecation") + MotionEvent n = MotionEvent.obtain(downTime, eventTime, action, + pointerCount, pointerIds, pointerCoords, metaState, xPrecision, + yPrecision, deviceId, edgeFlags, source, flags); + + return n; + } + + private static int[] getPointerIds(MotionEvent e) { + int n = e.getPointerCount(); + int[] r = new int[n]; + for (int i = 0; i < n; i++) { + r[i] = e.getPointerId(i); + } + return r; + } + + private static PointerCoords[] getPointerCoords(MotionEvent e) { + int n = e.getPointerCount(); + PointerCoords[] r = new PointerCoords[n]; + for (int i = 0; i < n; i++) { + r[i] = new PointerCoords(); + e.getPointerCoords(i, r[i]); + } + return r; + } + + private static float transformAngle(Matrix m, float angleRadians) { + // Construct and transform a vector oriented at the specified clockwise + // angle from vertical. Coordinate system: down is increasing Y, right is + // increasing X. + float[] v = new float[2]; + v[0] = (float)Math.sin(angleRadians); + v[1] = -(float)Math.cos(angleRadians); + m.mapVectors(v); + + // Derive the transformed vector's clockwise angle from vertical. + float result = (float) Math.atan2(v[0], -v[1]); + if (result < -Math.PI / 2) { + result += Math.PI; + } else if (result > Math.PI / 2) { + result -= Math.PI; + } + return result; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/network/HttpHelper.java b/app/src/main/java/com/hippo/ehviewer/network/HttpHelper.java new file mode 100644 index 000000000..c41fcdbbf --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/network/HttpHelper.java @@ -0,0 +1,1104 @@ +/* + * 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.network; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLEncoder; +import java.net.UnknownHostException; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.zip.GZIPInputStream; + +import org.apache.http.conn.ConnectTimeoutException; +import org.json.JSONArray; +import org.json.JSONObject; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Message; +import android.webkit.MimeTypeMap; + +import com.hippo.ehviewer.AppHandler; +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.ehclient.EhClient; +import com.hippo.ehviewer.ehclient.EhInfo; +import com.hippo.ehviewer.exception.StopRequestException; +import com.hippo.ehviewer.util.BgThread; +import com.hippo.ehviewer.util.Config; +import com.hippo.ehviewer.util.Constants; +import com.hippo.ehviewer.util.EhUtils; +import com.hippo.ehviewer.util.FastByteArrayOutputStream; +import com.hippo.ehviewer.util.Log; +import com.hippo.ehviewer.util.MathUtils; +import com.hippo.ehviewer.util.Ui; +import com.hippo.ehviewer.util.Utils; + +/** + * It is not thread safe + * + * @author Hippo + * + */ +public class HttpHelper { + private static final String TAG = HttpHelper.class.getSimpleName(); + + public static final String SAD_PANDA_ERROR = "Sad Panda"; + public static final String HAPPY_PANDA_BODY = "Happy Panda"; + + public static final String DOWNLOAD_OK_STR = "OK"; + + public static final int DOWNLOAD_OK_CODE = 0x0; + public static final int DOWNLOAD_FAIL_CODE = 0x1; + public static final int DOWNLOAD_STOP_CODE = 0x2; + + private static final String DEFAULT_CHARSET = "utf-8"; + private static final String CHARSET_KEY = "charset="; + + // TODO Get proxy list from server + private static String[] sProxyUrls = Config.DEFAULT_PROXY_URLS; + private static volatile int sProxyIndex = MathUtils.random(0, sProxyUrls.length); + private static Object sProxyIndexLock = new Object(); + + public class Package { + public Object obj; + public OnRespondListener listener; + public Package(Object obj, OnRespondListener listener) { + this.obj = obj; + this.listener = listener; + } + } + + private final Context mContext; + private Exception mException; + private String mLastUrl; + private OnRespondListener mListener; + private String mPreviewMode; + + private volatile long mLastEhConnectTime = 0; + private static final Object ehLock = new Object(); + + public interface OnRespondListener { + void onSuccess(Object body); + void onFailure(String eMsg); + } + + public static void updateProxyUrls(final Context context) { + new BgThread() { + @Override + public void run() { + try { + HttpHelper hh = new HttpHelper(context); + JSONObject json = new JSONObject(); + json.put("method", "proxy_urls"); + String body = hh.postJson(EhClient.API_EHVIEWER, json); + JSONObject jo = new JSONObject(body); + JSONArray ja = jo.getJSONObject("proxy_urls").getJSONArray("urls"); + String[] newUrls = new String[ja.length()]; + if (newUrls.length < 1) + throw new Throwable(); + for (int i = 0; i < ja.length(); i++) + newUrls[i] = ja.getString(i); + synchronized(sProxyIndexLock) { + sProxyUrls = newUrls; + sProxyIndex = MathUtils.random(0, sProxyUrls.length); + } + // Put proxy into config + Config.setProxyUrls(sProxyUrls); + } catch (Throwable e) { + synchronized(sProxyIndexLock) { + sProxyUrls = Config.getProxyUrls(); + sProxyIndex = MathUtils.random(0, sProxyUrls.length); + } + } + } + }.start(); + } + + public String getProxyUrl() { + synchronized(sProxyIndexLock) { + if (sProxyIndex < 0 || sProxyIndex >= sProxyUrls.length) + sProxyIndex = 0; + return sProxyUrls[sProxyIndex++]; + } + } + + public void reset() { + mListener = null; + mException = null; + mLastUrl = null; + mPreviewMode = null; + } + + public void setOnRespondListener(OnRespondListener l) { + mListener = l; + } + + public HttpHelper(Context context) { + mContext = context; + mContext.getApplicationContext(); + } + + /** + * Get last error message + * @return + */ + public String getEMsg() { + return getEMsg(mContext, mException); + } + + public Exception getException() { + return mException; + } + + public static String getEMsg(Context c, Exception e) { + if (e == null) + return c.getString(R.string.em_unknown_error); + + else if (e instanceof MalformedURLException) + return c.getString(R.string.em_url_format_error) + ": " + e.getMessage(); + + else if (e instanceof ConnectTimeoutException || + e instanceof SocketTimeoutException) + return c.getString(R.string.em_timeout) + ": " + e.getMessage(); + + else if (e instanceof UnknownHostException) + return c.getString(R.string.em_unknown_host) + ": " + e.getMessage(); + + else if (e instanceof ResponseCodeException) + return String.format(c.getString(R.string.em_unexpected_response_code), + ((ResponseCodeException)e).getResponseCode()) + ": " + e.getMessage(); + + else if (e instanceof RedirectionException) + return c.getString(R.string.em_redirection_error) + ": " + e.getMessage(); + + else if (e instanceof SocketException) + return "SocketException: " + e.getMessage(); + + else if (e instanceof SadPandaException) + return SAD_PANDA_ERROR; + + else if (e instanceof GetBodyException) + return c.getString(R.string.em_requst_null); + + else + return e.getClass().getSimpleName() + ": " +e.getMessage(); + } + + public String getLastUrl() { + return mLastUrl; + } + + private boolean isUrlCookiable(URL url) { + String host = url.getHost(); + for (String h : EhInfo.COOKIABLE_HOSTS) { + if (h.equals(host)) + return true; + } + return false; + } + + public void setPreviewMode(String previewMode) { + mPreviewMode = previewMode; + } + + private Object requst(RequestHelper rh) { + URL url = null; + HttpURLConnection conn = null; + boolean isCookiable = false; + + for (int times = 0; times < Config.getHttpRetry(); times++) { + mException = null; + int redirectionCount = 0; + boolean firstTime = true; + try { + url = (mLastUrl == null ? rh.getUrl() : new URL(mLastUrl)); + + isCookiable = isUrlCookiable(url); + + while (isCookiable) { + synchronized(ehLock) { + long cur = System.currentTimeMillis(); + long left = mLastEhConnectTime + Config.getEhMinInterval() - cur; + if (left <= 0) { + mLastEhConnectTime = cur; + break; + } else { + Thread.sleep(left); + } + } + } + + Log.d(TAG, "Requst " + url.toString()); + while (redirectionCount++ < Constants.MAX_REDIRECTS) { + conn = (HttpURLConnection) url.openConnection(); + conn.setInstanceFollowRedirects(false); + conn.setRequestProperty("User-Agent", Constants.userAgent); + conn.setConnectTimeout(Config.getHttpConnectTimeout()); + conn.setReadTimeout(Config.getHttpReadTimeout()); + // Set cookie if necessary + if (isCookiable) + EhInfo.getInstance(mContext).setCookie(conn, mPreviewMode); + // Do custom staff + if (firstTime) { + rh.onBeforeConnect(conn); + firstTime = false; + } + + conn.connect(); + final int responseCode = conn.getResponseCode(); + switch (responseCode) { + case HttpURLConnection.HTTP_OK: + case HttpURLConnection.HTTP_PARTIAL: + // Test sad panda + if (url.getHost().equals(EhInfo.EX_HOST)) { + String contentType = conn.getContentType(); + if (contentType != null && contentType.equals("image/gif") + && conn.getContentLength() == 9615) + throw new SadPandaException(); + } + // Store cookie if necessary + if (isCookiable) + EhInfo.getInstance(mContext).storeCookie(conn); + // Get object connection + Object obj = rh.onAfterConnect(conn); + // Send to UI thread if necessary + if (mListener != null) + AppHandler.getInstance().sendMessage( + Message.obtain(null, AppHandler.HTTP_HELPER_TAG, + Constants.TRUE, 0, new Package(obj, mListener))); + + return obj; + // redirect + case HttpURLConnection.HTTP_MOVED_PERM: + case HttpURLConnection.HTTP_MOVED_TEMP: + case HttpURLConnection.HTTP_SEE_OTHER: + case Constants.HTTP_TEMP_REDIRECT: + final String location = conn.getHeaderField("Location"); + Log.d(TAG, "New location " + location); + mLastUrl = location; + conn.disconnect(); + url = new URL(url, location); + continue; + + default: + String body; + // Get pure text error + if (rh instanceof GetStringHelper + && (body = (String)rh.onAfterConnect(conn)) != null + && !body.contains("<")) + throw new Exception(body); + else + throw new ResponseCodeException(responseCode); + } + } + throw new RedirectionException(); + } catch (Exception e) { + mException = e; + e.printStackTrace(); + // For download, if need stop, just stop + if (mException instanceof StopRequestException) + break; + } finally { + if (conn != null) + conn.disconnect(); + } + } + + // Send to UI thread if necessary + if (mListener != null) + AppHandler.getInstance().sendMessage( + Message.obtain(null, AppHandler.HTTP_HELPER_TAG, + Constants.FALSE, 0, new Package(getEMsg(), mListener))); + // Send exception to helper + if (mException != null) + rh.onGetException(mException); + + return null; + } + + private interface RequestHelper { + + /** + * Get the URL to connect + * @return + */ + public URL getUrl() throws MalformedURLException; + + /** + * Add header or do something else for HttpURLConnection before connect + * @param conn + * @throws Exception + */ + + public void onBeforeConnect(HttpURLConnection conn) throws Exception; + + /** + * Get what do you need from HttpURLConnection after connect + * Return null means get error + * @param conn + * @return + * @throws Exception + */ + public Object onAfterConnect(HttpURLConnection conn) throws Exception; + + /** + * Handle exception + * @param e + */ + public void onGetException(Exception e); + } + + /** + * RequstHelper for check sad panda, use HEAD method + */ + private class CheckSpHelper implements RequestHelper { + @Override + public URL getUrl() throws MalformedURLException { + return new URL(EhClient.HEADER_EX); + } + + @Override + public void onBeforeConnect(HttpURLConnection conn) + throws Exception { + conn.setRequestMethod("HEAD"); + } + + @Override + public Object onAfterConnect(HttpURLConnection conn) + throws Exception { + return HAPPY_PANDA_BODY; + } + + @Override + public void onGetException(Exception e) {} + } + + private abstract class GetStringHelper implements RequestHelper { + private final String mUrl; + + public GetStringHelper(String url) { + mUrl = url; + } + + @Override + public URL getUrl() throws MalformedURLException { + return new URL(mUrl); + } + + @Override + public void onBeforeConnect(HttpURLConnection conn) + throws Exception { + conn.addRequestProperty("Accept-Encoding", "gzip"); + } + + private String getBody(HttpURLConnection conn) + throws Exception { + String body = null; + InputStream is = null; + ByteArrayOutputStream baos = null; + try { + is = conn.getInputStream(); + String encoding = conn.getContentEncoding(); + if (encoding != null && encoding.equals("gzip")) + is = new GZIPInputStream(is); + + int length = conn.getContentLength(); + if (length >= 0) + baos = new ByteArrayOutputStream(length); + else + baos = new ByteArrayOutputStream(); + + Utils.copy(is, baos, Constants.BUFFER_SIZE); + + // Get charset + String charset = null; + String contentType = conn.getContentType(); + int index = -1; + if (contentType != null + && (index = contentType.indexOf(CHARSET_KEY)) != -1) { + charset = contentType.substring(index + CHARSET_KEY.length()); + } else + charset = DEFAULT_CHARSET; + + body = baos.toString(charset); + if (body == null) + throw new GetBodyException(); + } catch (Exception e) { + throw e; + } finally { + Utils.closeQuietly(is); + Utils.closeQuietly(baos); + } + return body; + } + + @Override + public Object onAfterConnect(HttpURLConnection conn) + throws Exception { + return getBody(conn); + } + + @Override + public void onGetException(Exception e) {} + } + + /** + * RequstHelper for GET method + */ + private class GetHelper extends GetStringHelper { + + public GetHelper(String url) { + super(url); + } + + @Override + public void onBeforeConnect(HttpURLConnection conn) + throws Exception { + super.onBeforeConnect(conn); + conn.setRequestMethod("GET"); + } + } + + /** + * RequstHelper for post form data, use POST method + */ + private class PostFormHelper extends GetStringHelper { + private final String[][] mArgs; + + public PostFormHelper(String url, String[][] args) { + super(url); + mArgs = args; + } + + @Override + public void onBeforeConnect(HttpURLConnection conn) + throws Exception { + super.onBeforeConnect(conn); + conn.setDoOutput(true); + conn.setDoInput(true); + conn.setUseCaches(false); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", + "application/x-www-form-urlencoded"); + + DataOutputStream out = new DataOutputStream(conn.getOutputStream()); + StringBuilder sb = new StringBuilder(); + int i = 0; + for (String[] arg : mArgs) { + if (i != 0) + sb.append("&"); + sb.append(URLEncoder.encode(arg[0], "UTF-8")); + sb.append("="); + sb.append(URLEncoder.encode(arg[1], "UTF-8")); + i++; + } + out.writeBytes(sb.toString()); + out.flush(); + out.close(); + } + } + + /** + * RequstHelper for post json, use POST method + */ + private class PostJsonHelper extends GetStringHelper { + private final JSONObject mJo; + + public PostJsonHelper(String url, JSONObject jo) { + super(url); + mJo = jo; + } + + @Override + public void onBeforeConnect(HttpURLConnection conn) throws Exception { + super.onBeforeConnect(conn); + conn.setDoOutput(true); + conn.setDoInput(true); + conn.setUseCaches(false); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "application/json"); + + DataOutputStream out = new DataOutputStream(conn.getOutputStream()); + out.writeBytes(mJo.toString()); + out.flush(); + out.close(); + } + } + + public abstract static class FormData { + private final Map mProperties; + + public FormData() { + mProperties = new LinkedHashMap(); + } + + public void setProperty(String key, String value) { + mProperties.put(key, value); + } + + public void clearProperty(String key) { + mProperties.remove(key); + } + + public void clearAllProperties() { + mProperties.clear(); + } + + /** + * Put information to target OutputStream. + * + * @param os Target OutputStream + * @throws IOException + */ + public abstract void output(OutputStream os) throws IOException; + + public void doOutPut(OutputStream os) throws IOException { + StringBuilder sb = new StringBuilder(); + for (String key : mProperties.keySet()) + sb.append(key).append(": ").append(mProperties.get(key)).append("\r\n"); + sb.append("\r\n"); + os.write(sb.toString().getBytes()); + output(os); + } + } + + public static class StringData extends FormData { + private final String mStr; + + public StringData(String str) { + mStr = str; + } + + @Override + public void output(OutputStream os) throws IOException { + os.write(mStr.getBytes()); + os.write("\r\n".getBytes()); + } + } + + public static class BitmapData extends FormData { + private final Bitmap mBitmap; + + public BitmapData(Bitmap bmp) { + mBitmap = bmp; + setProperty("Content-Type", "image/jpeg"); + } + + @Override + public void output(OutputStream os) throws IOException { + mBitmap.compress(Bitmap.CompressFormat.JPEG, 98, os); //TODO I need a better quality + os.write("\r\n".getBytes()); + } + } + + public static class FileData extends FormData { + private final File mFile; + + public FileData(File file) { + mFile = file; + setProperty("Content-Type", + URLConnection.guessContentTypeFromName(file.getName())); + } + + @Override + public void output(OutputStream os) throws IOException { + InputStream is = new FileInputStream(mFile); + byte[] buffer = new byte[1024]; + int bytesRead; + while((bytesRead = is.read(buffer)) !=-1) + os.write(buffer, 0, bytesRead); + is.close(); + + os.write("\r\n".getBytes()); + } + } + + private class PostFormDataHelper extends GetStringHelper { + private static final String BOUNDARY = "------WebKitFormBoundary7eDB0hDQ91s22Tkf"; + + private final List mDataList; + + + public PostFormDataHelper(String url, List dataList) { + super(url); + mDataList = dataList; + } + + @Override + public void onBeforeConnect(HttpURLConnection conn) throws Exception { + super.onBeforeConnect(conn); + conn.setDoOutput(true); + conn.setDoInput(true); + conn.setUseCaches(false); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=----WebKitFormBoundary7eDB0hDQ91s22Tkf"); + + DataOutputStream out = new DataOutputStream(conn.getOutputStream()); + //OutputStream out = new FileOutputStream(new File("/sdcard/a.txt")); + + for (FormData data : mDataList) { + out.write(BOUNDARY.getBytes()); + out.write("\r\n".getBytes()); + data.doOutPut(out); + } + out.write(BOUNDARY.getBytes()); + out.write("--".getBytes()); + + out.flush(); + out.close(); + } + } + + private class GetImageHelper implements RequestHelper { + private final String mUrl; + + public GetImageHelper(String url) { + mUrl = url; + } + + @Override + public URL getUrl() throws MalformedURLException { + return new URL(mUrl); + } + + @Override + public void onBeforeConnect(HttpURLConnection conn) + throws Exception { + conn.setRequestMethod("GET"); + } + + @Override + public Object onAfterConnect(HttpURLConnection conn) + throws Exception { + // If just + // Bitmap bmp = BitmapFactory.decodeStream(conn.getInputStream(), null, Ui.getBitmapOpt()); + // bitmap might be incomplete. + int size = conn.getContentLength(); + FastByteArrayOutputStream fbaos = new FastByteArrayOutputStream(size == -1 ? 24 * 1024 : (size + 100)); + Utils.copy(conn.getInputStream(), fbaos); + Bitmap bmp = BitmapFactory.decodeByteArray(fbaos.getBuffer(), 0, fbaos.size(), Ui.getBitmapOpt()); + + if (bmp == null) + throw new GetBodyException(); + return bmp; + } + + @Override + public void onGetException(Exception e) {} + } + + public static interface OnDownloadListener { + public void onDownloadStartConnect(); + /** + * If totalSize -1 for can't get length info + * @param totalSize + */ + public void onDownloadStartDownload(int totalSize); + /** + * If totalSize -1 for can't get length info + * @param totalSize + */ + public void onDownloadStatusUpdate(int downloadSize, int totalSize); + public void onDownloadOver(int status, String eMsg); + } + + public static class DownloadControlor { + private boolean mStop = false; + + public void stop() { + mStop = true; + } + + public void reset() { + mStop = false; + } + + public boolean isStop() { + return mStop; + } + } + + private class DownloadHelper implements RequestHelper { + + private static final String DOWNLOAD_EXTENSION = ".download"; + + private final String mUrl; + private final File mDir; + private String mFileName; + private File mFile; + private File mTempFile; + private final boolean mIsProxy; + private final DownloadControlor mControlor; + private final OnDownloadListener mListener; + private int mContentLength; + private int mReceivedSize; + + public DownloadHelper(String url, File dir, String fileName, boolean isProxy, + DownloadControlor controlor, OnDownloadListener listener) { + mUrl = url; + mDir = dir; + mFileName = fileName; + mIsProxy = isProxy; + mControlor = controlor; + mListener = listener; + } + + @Override + public URL getUrl() throws MalformedURLException { + return new URL(mIsProxy? getProxyUrl() : mUrl); + } + + @Override + public void onBeforeConnect(HttpURLConnection conn) throws Exception { + if (mControlor != null && mControlor.isStop()) + throw new StopRequestException(); + + if (mIsProxy) { + conn.setDoOutput(true); + conn.setDoInput(true); + conn.setUseCaches(false); + conn.setRequestMethod("POST"); + + DataOutputStream out = new DataOutputStream(conn.getOutputStream()); + out.writeBytes("{\"url\":\"" + mUrl + "\"}"); + out.flush(); + out.close(); + } else { + conn.setRequestMethod("GET"); + conn.setRequestProperty("Range", "bytes=0-"); + } + if (mListener != null) + mListener.onDownloadStartConnect(); + } + + private int getContentLength(HttpURLConnection conn) { + int contentLength = conn.getContentLength(); + String range; + if (contentLength == -1 && + (range = conn.getHeaderField("Content-Range")) != null) { + // Content-Range looks like bytes 500-999/1234 + contentLength = 0; + int step = 0; + boolean isNum = false; + char ch; + for (int i = 0; i < range.length(); i++) { + ch = range.charAt(i); + if (ch >= '0' && ch <= '9') { + if (!isNum) { + isNum = true; + step++; + } + } else { + isNum = false; + } + if (isNum && step == 3) + contentLength = contentLength * 10 + ch - '0'; + } + } + return contentLength; + } + + protected String fixNewExtension(String ext) { + return null; + } + + @Override + public Object onAfterConnect(HttpURLConnection conn) throws Exception { + // Check stop + if (mControlor != null && mControlor.isStop()) + throw new StopRequestException(); + + String contentType = conn.getContentType(); + int index = contentType.indexOf(';'); + if (index != -1) + contentType = contentType.substring(0, index); + + mContentLength = getContentLength(conn); + if (mListener != null) + mListener.onDownloadStartDownload(mContentLength); + + // Fix extension + MimeTypeMap mime = MimeTypeMap.getSingleton(); + String newExtension = mime.getExtensionFromMimeType(contentType); + if (newExtension == null) { + index = contentType.lastIndexOf('/'); + if (index != -1) + newExtension = contentType.substring(index + 1); + } + newExtension = fixNewExtension(newExtension); + if (newExtension != null) { + index = mFileName.lastIndexOf('.'); + if (index == -1) { + mFileName = mFileName + '.' + newExtension; + } else { + mFileName = mFileName.substring(0, index) + '.' + newExtension; + } + } + + // Make sure parent exist + mDir.mkdirs(); + mFile = new File(mDir, mFileName); + mTempFile = new File(mDir, mFileName + DOWNLOAD_EXTENSION); + // Transfer + transferData(conn.getInputStream(), new FileOutputStream(mTempFile)); + // Get ok, rename + mTempFile.renameTo(mFile); + // Callback + if (mListener != null) + mListener.onDownloadOver(DOWNLOAD_OK_CODE, null); + return DOWNLOAD_OK_STR; + } + + @Override + public void onGetException(Exception e) { + // Delete unfinished file + if (mTempFile != null) + mTempFile.delete(); + if (mFile != null) + mFile.delete(); + // Callback + if (mListener != null) { + if (e instanceof StopRequestException) + mListener.onDownloadOver(DOWNLOAD_STOP_CODE, getEMsg()); + else + mListener.onDownloadOver(DOWNLOAD_FAIL_CODE, getEMsg()); + } + } + + private void transferData(InputStream in, OutputStream out) + throws Exception { + final byte data[] = new byte[Constants.BUFFER_SIZE]; + mReceivedSize = 0; + + while (true) { + // Check stop first + if (mControlor != null && mControlor.isStop()) + throw new StopRequestException(); + + int bytesRead = in.read(data); + if (bytesRead == -1) + break; + out.write(data, 0, bytesRead); + mReceivedSize += bytesRead; + if (mListener != null) + mListener.onDownloadStatusUpdate(mReceivedSize, mContentLength); + } + + if (mContentLength != -1 && mReceivedSize != mContentLength) + throw new UncompletedException("Received size is " + mReceivedSize + + ", but ContentLength is " + mContentLength); + } + + } + + + private class DownloadEhImageHelper extends DownloadHelper { + + public DownloadEhImageHelper(String url, File dir, String fileName, + boolean isProxy, DownloadControlor controlor, + OnDownloadListener listener) { + super(url, dir, fileName, isProxy, controlor, listener); + } + + @Override + protected String fixNewExtension(String ext) { + int length = EhUtils.POSSIBLE_IMAGE_EXTENSION_ARRAY.length; + int i; + for (i = 0; i < length; i++) { + if (EhUtils.POSSIBLE_IMAGE_EXTENSION_ARRAY[i].equals(ext)) + break; + } + if (i == length) + ext = EhUtils.DEFAULT_IMAGE_EXTENSION; + return ext; + } + } + + private class DownloadOriginEhImageHelper extends DownloadEhImageHelper { + + public DownloadOriginEhImageHelper(String url, File dir, String fileName, + boolean isProxy, DownloadControlor controlor, + OnDownloadListener listener) { + super(url, dir, fileName, isProxy, controlor, listener); + } + + @Override + public Object onAfterConnect(HttpURLConnection conn) throws Exception { + if (mLastUrl == null) + throw new BandwidthExceededException(); + + return super.onAfterConnect(conn); + } + } + + + /** + * Check Sad Panda. + * If get Sad Panda, return null, + * else return HttpHelper.HAPPY_PANDA_BODY + * @return + */ + public String checkSadPanda() { + return (String)requst(new CheckSpHelper()); + } + + /** + * Http GET method + * @param url + * @return + */ + public String get(String url) { + return (String)requst(new GetHelper(url)); + } + + /** + * Post form data + * @param url + * @param args + * @return + */ + public String postForm(String url, String[][] args) { + return (String)requst(new PostFormHelper(url, args)); + } + + /** + * Post json data + * @param url + * @param json + * @return + */ + public String postJson(String url, JSONObject json) { + return (String)requst(new PostJsonHelper(url, json)); + } + + /** + * Post form data, multipart/form-data + * @param url + * @param datas + * @return + */ + public String postFormData(String url, List dataList) { + return (String)requst(new PostFormDataHelper(url, dataList)); + } + + /** + * Get image + * @param url + * @return + */ + public Bitmap getImage(String url) { + return (Bitmap) requst(new GetImageHelper(url)); + } + + public String download(String url, File dir, String file, boolean isProxy, + DownloadControlor controlor, OnDownloadListener listener) { + return (String) requst(new DownloadHelper(url, dir, file, isProxy, + controlor, listener)); + } + + public void downloadInThread(final String url, final File dir, + final String file, final boolean isProxy, + final DownloadControlor controlor, final OnDownloadListener listener) { + new BgThread() { + @Override + public void run() { + download(url, dir, file, isProxy, controlor, listener); + } + }.start(); + } + + public String downloadEhImage(String url, File dir, String file, boolean isProxy, + DownloadControlor controlor, OnDownloadListener listener) { + return (String) requst(new DownloadEhImageHelper(url, dir, file, isProxy, + controlor, listener)); + } + + public String downloadOriginEhImage(String url, File dir, String file, boolean isProxy, + DownloadControlor controlor, OnDownloadListener listener) { + return (String) requst(new DownloadOriginEhImageHelper(url, dir, file, isProxy, + controlor, listener)); + } + + /** Exceptions **/ + + public class SadPandaException extends Exception { + private static final long serialVersionUID = 1L; + } + + public class GetBodyException extends Exception { + private static final long serialVersionUID = 1L; + } + + public class RedirectionException extends Exception { + private static final long serialVersionUID = 1L; + } + + public class UncompletedException extends Exception { + + public UncompletedException(String string) { + super(string); + } + + private static final long serialVersionUID = 1L; + } + + public class ResponseCodeException extends Exception { + + private static final long serialVersionUID = 1L; + private static final String eMsg = "Error response code"; + private final int mResponseCode; + + public ResponseCodeException(int responseCode) { + this(responseCode, eMsg); + } + + public ResponseCodeException(int responseCode, String message) { + super(message); + mResponseCode = responseCode; + } + + @Override + public String getMessage() { + return eMsg + ": " + mResponseCode; + } + + public int getResponseCode() { + return mResponseCode; + } + } + + public class BandwidthExceededException extends Exception { + + private static final long serialVersionUID = 1L; + + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/network/Network.java b/app/src/main/java/com/hippo/ehviewer/network/Network.java new file mode 100644 index 000000000..1a5d03674 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/network/Network.java @@ -0,0 +1,41 @@ +/* + * 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.network; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; + +public final class Network { + + public static final int NETWORK_STATE_NONE = 0x0; + public static final int NETWORK_STATE_MOBILE = 0x1; + public static final int NETWORK_STATE_WIFI = 0x2; + + public static int getNetworkState(Context context) { + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo mWifi = cm.getNetworkInfo(ConnectivityManager.TYPE_WIFI); + NetworkInfo mMobile = cm.getNetworkInfo(ConnectivityManager.TYPE_MOBILE); + + if (mWifi != null && mWifi.isConnected()) + return NETWORK_STATE_WIFI; + else if (mMobile != null && mMobile.isConnected()) + return NETWORK_STATE_MOBILE; + else + return NETWORK_STATE_NONE; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/network/UrlBuilder.java b/app/src/main/java/com/hippo/ehviewer/network/UrlBuilder.java new file mode 100644 index 000000000..0951a3407 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/network/UrlBuilder.java @@ -0,0 +1,57 @@ +/* + * 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.network; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +public class UrlBuilder { + + public String mRootUrl; + public Map mQueryMap = new HashMap(); + + public UrlBuilder(String rootUrl) { + mRootUrl = rootUrl; + } + + public void addQuery(String key, Object value) { + mQueryMap.put(key, value); + } + + public String build() { + if (mQueryMap.size() == 0) { + return mRootUrl; + } else { + StringBuilder sb = new StringBuilder(mRootUrl); + sb.append("?"); + + Iterator iter = mQueryMap.keySet().iterator(); + if (iter.hasNext()) { + String key = iter.next(); + Object value = mQueryMap.get(key); + sb.append(key).append("=").append(value); + } + while (iter.hasNext()) { + String key = iter.next(); + Object value = mQueryMap.get(key); + sb.append("&").append(key).append("=").append(value); + } + return sb.toString(); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/preference/ColorPreference.java b/app/src/main/java/com/hippo/ehviewer/preference/ColorPreference.java new file mode 100644 index 000000000..465da5d73 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/preference/ColorPreference.java @@ -0,0 +1,246 @@ +/* + * 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.preference; + +import java.util.Locale; + +import android.annotation.SuppressLint; +import android.app.Dialog; +import android.content.Context; +import android.graphics.Color; +import android.graphics.PixelFormat; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; + +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.app.MaterialAlertDialog; +import com.hippo.ehviewer.util.Config; +import com.hippo.ehviewer.util.Ui; +import com.hippo.ehviewer.util.ViewUtils; +import com.hippo.ehviewer.widget.ColorPickerView; + +public class ColorPreference extends DialogPreference implements ColorPickerView.OnColorChangedListener, View.OnClickListener { + + private View mColorBrick; + + public ColorPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ColorPreference(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public void setColor(int color) { + if (mColorBrick != null) + mColorBrick.setBackgroundColor(color); + } + + @Override + protected void onBindView(View view) { + super.onBindView(view); + + View ll = view.findViewById(android.R.id.widget_frame); + if (ll != null && ll instanceof LinearLayout) { + LinearLayout widgetFrame = (LinearLayout)ll; + widgetFrame.setVisibility(View.VISIBLE); + + mColorBrick = new View(getContext()); + if (isEnabled()) + setColor(Config.getThemeColor()); + else + setColor(getContext().getResources().getColor(R.color.disabled_mask)); + widgetFrame.addView(mColorBrick, new LinearLayout.LayoutParams(Ui.dp2pix(24), Ui.dp2pix(24))); + } + } + + @Override + protected boolean inScrollView() { + return true; + } + + @Override + @SuppressLint("InflateParams") + protected View onCreateDialogView() { + return ViewUtils.inflateDialogView(R.layout.color_scheme_dialog, false); + } + + private void setUpPresets(View view, final int which) { + final Button preset = (Button) view.findViewById(which); + if (preset != null) + preset.setOnClickListener(this); + } + + @Override + protected void onBindDialogView(View view) { + int curColor = Config.getThemeColor(); + final ColorPickerView colorPicker = (ColorPickerView) view.findViewById(R.id.color_picker_view); + ViewUtils.removeHardwareAccelerationSupport(colorPicker); + Button oldColor = (Button) view.findViewById(R.id.color_scheme_dialog_old_color); + oldColor.setOnClickListener(this); + final Button newColor = (Button) view.findViewById(R.id.color_scheme_dialog_new_color); + setUpPresets(view, R.id.color_scheme_dialog_preset_one); + setUpPresets(view, R.id.color_scheme_dialog_preset_two); + setUpPresets(view, R.id.color_scheme_dialog_preset_three); + setUpPresets(view, R.id.color_scheme_dialog_preset_four); + setUpPresets(view, R.id.color_scheme_dialog_preset_five); + setUpPresets(view, R.id.color_scheme_dialog_preset_six); + setUpPresets(view, R.id.color_scheme_dialog_preset_seven); + setUpPresets(view, R.id.color_scheme_dialog_preset_eight); + final EditText hexValue = (EditText) view.findViewById(R.id.color_scheme_dialog_hex_value); + hexValue.addTextChangedListener(new TextWatcher() { + @Override + public void onTextChanged(final CharSequence s, final int start, final int before, + final int count) { + try { + int color = Color.parseColor("#" + + hexValue.getText().toString().toUpperCase(Locale.getDefault())); + colorPicker.setColor(color); + newColor.setBackgroundColor(color); + } catch (final Exception ignored) { + } + } + + @Override + public void beforeTextChanged(final CharSequence s, final int start, final int count, + final int after) { + /* Nothing to do */ + } + + @Override + public void afterTextChanged(final Editable s) { + /* Nothing to do */ + } + }); + + colorPicker.setOnColorChangedListener(this); + oldColor.setBackgroundColor(curColor); + colorPicker.setColor(curColor, true); + } + + @Override + protected void onPrepareDialogBuilder(MaterialAlertDialog.Builder builder) { + // Empty + } + + @Override + public boolean onClick(MaterialAlertDialog dialog, int which) { + super.onClick(dialog, which); + if (which == MaterialAlertDialog.POSITIVE) { + ColorPickerView colorPicker = (ColorPickerView) dialog.findViewById(R.id.color_picker_view); + if (colorPicker == null) + return true; + int color = colorPicker.getColor() | 0xff000000; + if (callChangeListener(color)) { + Config.setThemeColor(color); + setColor(color); + return true; + } else { + return false; + } + } else { + return true; + } + } + + private String padLeft(final String string, final char padChar, final int size) { + if (string.length() >= size) { + return string; + } + final StringBuilder result = new StringBuilder(); + for (int i = string.length(); i < size; i++) { + result.append(padChar); + } + result.append(string); + return result.toString(); + } + + @Override + public void onColorChanged(View view, int color) { + try { + View p = (View) view.getParent(); + EditText hexValue = (EditText) p.findViewById(R.id.color_scheme_dialog_hex_value); + Button newColor = (Button) view.findViewById(R.id.color_scheme_dialog_new_color); + + if (hexValue != null) + hexValue.setText(padLeft(Integer.toHexString(color).toUpperCase(Locale.getDefault()), + '0', 8)); + if (newColor != null) + newColor.setBackgroundColor(color); + } catch (Throwable e) { + } + } + + private int getColor(final int color) { + return getContext().getResources().getColor(color); + } + + @Override + public void onClick(View v) { + + View view = ViewUtils.getAncestor(v, R.id.main); + if (view == null) + return; + ColorPickerView colorPicker = (ColorPickerView) view.findViewById(R.id.color_picker_view); + if (colorPicker == null) + return; + + switch (v.getId()) { + case R.id.color_scheme_dialog_preset_one: + colorPicker.setColor(getColor(R.color.theme_red)); + break; + case R.id.color_scheme_dialog_preset_two: + colorPicker.setColor(getColor(R.color.theme_orange)); + break; + case R.id.color_scheme_dialog_preset_three: + colorPicker.setColor(getColor(R.color.theme_lime)); + break; + case R.id.color_scheme_dialog_preset_four: + colorPicker.setColor(getColor(R.color.theme_green)); + break; + case R.id.color_scheme_dialog_preset_five: + colorPicker.setColor(getColor(R.color.theme_teal)); + break; + case R.id.color_scheme_dialog_preset_six: + colorPicker.setColor(getColor(R.color.theme_blue)); + break; + case R.id.color_scheme_dialog_preset_seven: + colorPicker.setColor(getColor(R.color.theme_purple)); + break; + case R.id.color_scheme_dialog_preset_eight: + colorPicker.setColor(getColor(R.color.theme_brown)); + break; + case R.id.color_scheme_dialog_old_color: + colorPicker.setColor(Config.getThemeColor()); + break; + default: + break; + } + + onColorChanged(colorPicker, colorPicker.getColor()); + } + + @Override + protected void onDialogCreate(Dialog dialog) { + dialog.getWindow().setFormat(PixelFormat.RGBA_8888); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/preference/DialogPreference.java b/app/src/main/java/com/hippo/ehviewer/preference/DialogPreference.java new file mode 100644 index 000000000..394249e96 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/preference/DialogPreference.java @@ -0,0 +1,113 @@ +/* + * 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.preference; + +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.util.AttributeSet; +import android.view.View; + +import com.hippo.ehviewer.app.MaterialAlertDialog; + +public abstract class DialogPreference extends android.preference.DialogPreference implements MaterialAlertDialog.OnClickListener { + + private MaterialAlertDialog.Builder mBuilder; + + /** The dialog, if it is showing. */ + private Dialog mDialog; + /** Which button was clicked. */ + private int mWhichButtonClicked; + + public DialogPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public DialogPreference(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void onClick() { + if (mDialog != null && mDialog.isShowing()) return; + + showDialog(null); + } + + @Override + public boolean onClick(MaterialAlertDialog dialog, int which) { + mWhichButtonClicked = which; + return true; + } + + protected abstract void onPrepareDialogBuilder(MaterialAlertDialog.Builder builder); + + protected abstract boolean inScrollView(); + + @Override + protected void showDialog(Bundle state) { + Context context = getContext(); + + mWhichButtonClicked = MaterialAlertDialog.POSITIVE; + + mBuilder = new MaterialAlertDialog.Builder(context) + .setTitle(getDialogTitle()) + //.setIcon(mDialogIcon) // TODO Not support now + .setPositiveButton(getPositiveButtonText()) + .setNegativeButton(getNegativeButtonText()) + .setButtonListener(this); + + + View contentView = onCreateDialogView(); + if (contentView != null) { + onBindDialogView(contentView); + mBuilder.setView(contentView, inScrollView()); + } else { + mBuilder.setMessage(getDialogMessage()); + } + + onPrepareDialogBuilder(mBuilder); + + // Create the dialog + final Dialog dialog = mDialog = mBuilder.create(); + onDialogCreate(dialog); + if (state != null) { + dialog.onRestoreInstanceState(state); + } + dialog.setOnDismissListener(this); + dialog.show(); + } + + protected void onDialogCreate(Dialog dialog) { + + } + + @Override + protected abstract void onBindDialogView(View view); + + @Override + public void onDismiss(DialogInterface dialog) { + mDialog = null; + onDialogClosed(mWhichButtonClicked == MaterialAlertDialog.POSITIVE); + } + + @Override + public Dialog getDialog() { + return mDialog; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/preference/EditTextPreference.java b/app/src/main/java/com/hippo/ehviewer/preference/EditTextPreference.java new file mode 100644 index 000000000..140cb5bd8 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/preference/EditTextPreference.java @@ -0,0 +1,139 @@ +/* + * 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.preference; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.widget.EditText; + +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.app.MaterialAlertDialog; +import com.hippo.ehviewer.widget.MaterialToast; + +public class EditTextPreference extends DialogPreference { + + private EditText mEditText; + + private int mMax; + private int mMin; + + public EditTextPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs, 0); + } + + public EditTextPreference(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context, attrs, defStyle); + } + + private void init(Context context, AttributeSet attrs, int defStyle) { + mEditText = new EditText(context, attrs); + + TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.EditTextPreference, defStyle, 0); + mMax = a.getInt(R.styleable.EditTextPreference_etpMax, Integer.MAX_VALUE); + mMin = a.getInt(R.styleable.EditTextPreference_etpMin, Integer.MIN_VALUE); + a.recycle(); + } + + @Override + protected boolean inScrollView() { + return true; + } + + @Override + protected View onCreateDialogView() { + // Make sure mEditText is an orphan + ViewParent oldParent = mEditText.getParent(); + if (oldParent != null) + ((ViewGroup) oldParent).removeView(mEditText); + + mEditText.setTextColor(getContext().getResources().getColor( + R.color.secondary_text_dark)); + return mEditText; + } + + @Override + protected void onBindDialogView(View view) { + mEditText.setText(getPersistedString(null)); + } + + @Override + protected void onPrepareDialogBuilder(MaterialAlertDialog.Builder builder) { + // Empty + } + + @Override + protected Object onGetDefaultValue(TypedArray a, int index) { + return a.getString(index); + } + + @Override + protected void onSetInitialValue(boolean restorePersistedValue, + Object defaultValue) { + setSummary(restorePersistedValue ? getPersistedString((String) defaultValue) + : (String) defaultValue); + if (!restorePersistedValue) + persistString((String) defaultValue); + } + + // Return true for close and save + private boolean checkBorder(String value) { + boolean re; + if (mMax == Integer.MAX_VALUE && mMin == Integer.MIN_VALUE) { + return true; + } else { + try { + int intValue = Integer.parseInt(value); + if (intValue > mMax || intValue < mMin) + re = false; + else + re = true; + } catch (Throwable e) { + re = false; + } + } + + // Show Toast + if (!re) + MaterialToast.showToast(R.string.invalid_input); + + return re; + } + + @Override + public boolean onClick(MaterialAlertDialog dialog, int which) { + super.onClick(dialog, which); + if (which == MaterialAlertDialog.POSITIVE) { + String value = mEditText.getText().toString(); + if (checkBorder(value) && callChangeListener(value)) { + persistString(value); + setSummary(value); + return true; + } else { + return false; + } + } else { + return true; + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/preference/ListPreference.java b/app/src/main/java/com/hippo/ehviewer/preference/ListPreference.java new file mode 100644 index 000000000..4f5b41c8f --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/preference/ListPreference.java @@ -0,0 +1,261 @@ +/* + * 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.preference; + +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.View; + +import com.hippo.ehviewer.app.MaterialAlertDialog; +import com.hippo.ehviewer.app.MaterialAlertDialog.Builder; + +public class ListPreference extends DialogPreference { + + private CharSequence[] mEntries; + private CharSequence[] mEntryValues; + private String mValue; + private String mSummary; + private int mClickedDialogEntryIndex; + private boolean mValueSet; + + public ListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs, 0); + } + + public ListPreference(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context, attrs, defStyle); + } + + private void init(Context context, AttributeSet attrs, int defStyle) { + TypedArray a = context.obtainStyledAttributes(attrs, + new int[]{ 16842930, 16843256 }, 0, 0); + mEntries = a.getTextArray(0); + mEntryValues = a.getTextArray(1); + a.recycle(); + + a = context.obtainStyledAttributes(attrs, + new int[]{ 16842754, 16842765, 16842766, + 16842994, 16843233, 16843238, 16843240, + 16843241, 16843242, 16843243, 16843244, + 16843245, 16843246, 16843491 }, 0, 0); + mSummary = a.getString(7); + a.recycle(); + } + + public void setEntries(CharSequence[] entries) { + mEntries = entries; + } + + public void setEntries(int entriesResId) { + setEntries(getContext().getResources().getTextArray(entriesResId)); + } + + public CharSequence[] getEntries() { + return mEntries; + } + + public void setEntryValues(CharSequence[] entryValues) { + mEntryValues = entryValues; + } + + public void setEntryValues(int entryValuesResId) { + setEntryValues(getContext().getResources().getTextArray(entryValuesResId)); + } + + public CharSequence[] getEntryValues() { + return mEntryValues; + } + + public void setValue(String value) { + // Always persist/notify the first time. + final boolean changed = !TextUtils.equals(mValue, value); + if (changed || !mValueSet) { + mValue = value; + mValueSet = true; + persistString(value); + if (changed) { + notifyChanged(); + } + } + } + + @Override + public CharSequence getSummary() { + final CharSequence entry = getEntry(); + if (entry == null) { + return super.getSummary(); + } else if (mSummary == null) { + return entry; + } else { + return String.format(mSummary, entry); + } + } + + @Override + public void setSummary(CharSequence summary) { + super.setSummary(summary); + if (summary == null && mSummary != null) { + mSummary = null; + } else if (summary != null && !summary.equals(mSummary)) { + mSummary = summary.toString(); + } + } + + public void setValueIndex(int index) { + if (mEntryValues != null) { + setValue(mEntryValues[index].toString()); + } + } + + public String getValue() { + return mValue; + } + + public CharSequence getEntry() { + int index = getValueIndex(); + return index >= 0 && mEntries != null ? mEntries[index] : null; + } + + public int findIndexOfValue(String value) { + if (value != null && mEntryValues != null) { + for (int i = mEntryValues.length - 1; i >= 0; i--) { + if (mEntryValues[i].equals(value)) { + return i; + } + } + } + return -1; + } + + private int getValueIndex() { + return findIndexOfValue(mValue); + } + + @Override + protected boolean inScrollView() { + return false; + } + + @Override + protected void onBindDialogView(View view) { + // Empty + } + + @Override + protected void onPrepareDialogBuilder(Builder builder) { + + mClickedDialogEntryIndex = getValueIndex(); + + builder.setSingleChoiceItems(mEntries, mClickedDialogEntryIndex, + new MaterialAlertDialog.OnClickListener() { + @Override + public boolean onClick(MaterialAlertDialog dialog, int which) { + mClickedDialogEntryIndex = which; + ListPreference.this.onClick(dialog, MaterialAlertDialog.POSITIVE); + return true; + } + }); + } + + + @Override + protected void onDialogClosed(boolean positiveResult) { + super.onDialogClosed(positiveResult); + + if (positiveResult && mClickedDialogEntryIndex >= 0 && mEntryValues != null) { + String value = mEntryValues[mClickedDialogEntryIndex].toString(); + if (callChangeListener(value)) { + setValue(value); + } + } + } + + @Override + protected Object onGetDefaultValue(TypedArray a, int index) { + return a.getString(index); + } + + @Override + protected void onSetInitialValue(boolean restoreValue, Object defaultValue) { + setValue(restoreValue ? getPersistedString(mValue) : (String) defaultValue); + } + + @Override + protected Parcelable onSaveInstanceState() { + final Parcelable superState = super.onSaveInstanceState(); + if (isPersistent()) { + // No need to save instance state since it's persistent + return superState; + } + + final SavedState myState = new SavedState(superState); + myState.value = getValue(); + return myState; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (state == null || !state.getClass().equals(SavedState.class)) { + // Didn't save state for us in onSaveInstanceState + super.onRestoreInstanceState(state); + return; + } + + SavedState myState = (SavedState) state; + super.onRestoreInstanceState(myState.getSuperState()); + setValue(myState.value); + } + + private static class SavedState extends BaseSavedState { + String value; + + public SavedState(Parcel source) { + super(source); + value = source.readString(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeString(value); + } + + public SavedState(Parcelable superState) { + super(superState); + } + + @SuppressWarnings("unused") + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/preference/NumberOfColumnsPreference.java b/app/src/main/java/com/hippo/ehviewer/preference/NumberOfColumnsPreference.java new file mode 100644 index 000000000..96dc4502e --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/preference/NumberOfColumnsPreference.java @@ -0,0 +1,129 @@ +/* + * 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.preference; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.View; +import android.widget.EditText; + +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.app.MaterialAlertDialog; +import com.hippo.ehviewer.app.MaterialAlertDialog.Builder; +import com.hippo.ehviewer.util.Config; +import com.hippo.ehviewer.util.ViewUtils; +import com.hippo.ehviewer.widget.MaterialToast; + +public class NumberOfColumnsPreference extends DialogPreference { + + private Context mContext; + private String keyPortrait; + private String keyLandscape; + private int defValuePortrait; + private int defValueLandscape; + + public NumberOfColumnsPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs, 0); + } + + public NumberOfColumnsPreference(Context context, AttributeSet attrs, + int defStyle) { + super(context, attrs, defStyle); + init(context, attrs, defStyle); + } + + private void updateSummary(int portrait, int landscape) { + setSummary(String.format("%s: %d, %s: %d", mContext.getString(R.string.portrait), + portrait, mContext.getString(R.string.landscape), landscape)); + } + + private void init(Context context, AttributeSet attrs, + int defStyle) { + mContext = context; + TypedArray a = mContext.obtainStyledAttributes(attrs, + R.styleable.NumberOfColumnsPreference, defStyle, 0); + + keyPortrait = a.getString(R.styleable.NumberOfColumnsPreference_keyPortrait); + keyLandscape = a.getString(R.styleable.NumberOfColumnsPreference_keyLandscape); + defValuePortrait = a.getInt(R.styleable.NumberOfColumnsPreference_defValuePortrait, 0); + defValueLandscape = a.getInt(R.styleable.NumberOfColumnsPreference_defValueLandscape, 0); + + if (defValuePortrait <= 0 || defValuePortrait >= 100) + defValuePortrait = 1; + if (defValueLandscape <= 0 || defValueLandscape >= 100) + defValueLandscape = 1; + + a.recycle(); + + updateSummary(Config.getInt(keyPortrait, defValuePortrait), + Config.getInt(keyLandscape, defValueLandscape)); + } + + @Override + protected boolean inScrollView() { + return true; + } + + @Override + @SuppressLint("InflateParams") + protected View onCreateDialogView() { + return ViewUtils.inflateDialogView(R.layout.set_column_number, false); + } + + @Override + protected void onBindDialogView(View view) { + final EditText editTextPortrait = (EditText) view.findViewById(R.id.portrait); + editTextPortrait.setText(String.valueOf(Config.getInt(keyPortrait, defValuePortrait))); + final EditText editTextLandscape = (EditText) view.findViewById(R.id.landscape); + editTextLandscape.setText(String.valueOf(Config.getInt(keyLandscape, defValueLandscape))); + } + + @Override + protected void onPrepareDialogBuilder(Builder builder) { + // Empty + } + + @Override + public boolean onClick(MaterialAlertDialog dialog, int which) { + super.onClick(dialog, which); + if (which == MaterialAlertDialog.POSITIVE) { + try { + EditText editTextPortrait = (EditText) dialog.findViewById(R.id.portrait); + EditText editTextLandscape = (EditText) dialog.findViewById(R.id.landscape); + int newPortrait = Integer.valueOf(editTextPortrait.getText().toString()); + int newLandscape = Integer.valueOf(editTextLandscape.getText().toString()); + if (newPortrait > 0 && newPortrait < 25 && + newLandscape > 0 && newLandscape < 25) { // TODO Need a better range + Config.setInt(keyPortrait, newPortrait); + Config.setInt(keyLandscape, newLandscape); + updateSummary(newPortrait, newLandscape); + return true; + } else { + throw new Exception(); + } + } catch (Exception e) { + MaterialToast.showToast(R.string.invalid_input); + return false; + } + } else { + return true; + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/preference/SeekBarDialogPreference.java b/app/src/main/java/com/hippo/ehviewer/preference/SeekBarDialogPreference.java new file mode 100644 index 000000000..709526a1f --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/preference/SeekBarDialogPreference.java @@ -0,0 +1,106 @@ +/* + * 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.preference; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.widget.SeekBar; + +import com.hippo.ehviewer.app.MaterialAlertDialog; +import com.hippo.ehviewer.app.MaterialAlertDialog.Builder; +import com.hippo.ehviewer.util.Utils; + +public class SeekBarDialogPreference extends DialogPreference { + + private SeekBar mSeekBar; + + public SeekBarDialogPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs, 0); + } + + public SeekBarDialogPreference(Context context, AttributeSet attrs, + int defStyle) { + super(context, attrs, defStyle); + init(context, attrs, defStyle); + } + + private void init(Context context, AttributeSet attrs, int defStyle) { + mSeekBar = new SeekBar(context, attrs); + } + + @Override + protected boolean inScrollView() { + return true; + } + + @Override + protected View onCreateDialogView() { + // Make sure mEditText is an orphan + ViewParent oldParent = mSeekBar.getParent(); + if (oldParent != null) + ((ViewGroup) oldParent).removeView(mSeekBar); + + return mSeekBar; + } + + @Override + protected void onBindDialogView(View view) { + mSeekBar.setProgress(Utils.parseIntSafely(getPersistedString(null), 120)); + } + + @Override + protected void onPrepareDialogBuilder(Builder builder) { + // Empty + } + + @Override + protected Object onGetDefaultValue(TypedArray a, int index) { + return a.getString(index); + } + + @Override + protected void onSetInitialValue(boolean restorePersistedValue, + Object defaultValue) { + setSummary(restorePersistedValue ? getPersistedString((String) defaultValue) + : (String) defaultValue); + if (!restorePersistedValue) + persistString((String) defaultValue); + } + + @Override + public boolean onClick(MaterialAlertDialog dialog, int which) { + super.onClick(dialog, which); + if (which == MaterialAlertDialog.POSITIVE) { + String value = Integer.toString(mSeekBar.getProgress()); + if (callChangeListener(value)) { + persistString(value); + setSummary(value); + return true; + } else { + return false; + } + } else { + return true; + } + } + +} diff --git a/app/src/main/java/com/hippo/ehviewer/receiver/NetworkChangeReceiver.java b/app/src/main/java/com/hippo/ehviewer/receiver/NetworkChangeReceiver.java new file mode 100644 index 000000000..be963880a --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/receiver/NetworkChangeReceiver.java @@ -0,0 +1,42 @@ +/* + * 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.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; + +import com.hippo.ehviewer.network.Network; +import com.hippo.ehviewer.util.Secret; + +public class NetworkChangeReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) { + + // Update secret image here + int state = Network.getNetworkState(context); + if (state == Network.NETWORK_STATE_WIFI) { + Secret.updateSecretImage(context); + } + + // TODO stop download when mobile network + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/service/DownloadService.java b/app/src/main/java/com/hippo/ehviewer/service/DownloadService.java new file mode 100644 index 000000000..41f2e3ed2 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/service/DownloadService.java @@ -0,0 +1,479 @@ +/* + * 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.service; + +import java.util.ArrayList; +import java.util.List; + +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Binder; +import android.os.IBinder; +import android.support.v4.app.NotificationCompat; + +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.data.Data; +import com.hippo.ehviewer.data.DownloadInfo; +import com.hippo.ehviewer.data.GalleryInfo; +import com.hippo.ehviewer.ehclient.ExDownloader; +import com.hippo.ehviewer.ehclient.ExDownloaderManager; +import com.hippo.ehviewer.ui.DownloadActivity; +import com.hippo.ehviewer.util.Config; +import com.hippo.ehviewer.util.Utils; + +public class DownloadService extends Service + implements ExDownloader.ListenerForDownload { + + @SuppressWarnings("unused") + private static final String TAG = DownloadService.class.getSimpleName(); + + private static final int DOWNLOADING_NOTIFY_ID = -1; + private static final int DOWNLOAD_NOTIFY_ID = -2; + + public static final String ACTION_UPDATE = "com.hippo.ehviewer.service.DownloadService.UPDATE"; + + public static final String ACTION_ADD = "com.hippo.ehviewer.service.DownloadService.ADD"; + public static final String ACTION_START = "com.hippo.ehviewer.service.DownloadService.START"; + public static final String ACTION_START_ALL = "com.hippo.ehviewer.service.DownloadService.START_ALL"; + public static final String ACTION_STOP = "com.hippo.ehviewer.service.DownloadService.STOP"; + public static final String ACTION_STOP_CURRENT = "com.hippo.ehviewer.service.DownloadService.STOP_CURRENT"; + public static final String ACTION_STOP_ALL = "com.hippo.ehviewer.service.DownloadService.STOP_ALL"; + public static final String ACTION_DELETE = "com.hippo.ehviewer.service.DownloadService.DELETE"; + public static final String ACTION_CLEAR = "com.hippo.ehviewer.service.DownloadService.ACTION_CLEAR"; + + public static final String KEY_GID = "gid"; + public static final String KEY_GALLERY_INFO = "gallery_info"; + + private Context mContext; + private Data mData; + private ExDownloaderManager mEdManager; + private volatile DownloadInfo mCurDownloadInfo = null; + private volatile ExDownloader mCurExDownloader = null; + private Binder mBinder = null; + private NotificationManager mNotifyManager; + private NotificationCompat.Builder mBuilder; + private String mSpeedStr = null; + + private final List mDownloadOk = new ArrayList(); + private final List mDownloadFailed = new ArrayList(); + + @Override + public void onCreate() { + super.onCreate(); + + mContext = getApplication(); + mData = Data.getInstance(); + mEdManager = ExDownloaderManager.getInstance(); + + mBinder = new Binder(); + mNotifyManager = (NotificationManager) + getSystemService(Context.NOTIFICATION_SERVICE); + } + + private synchronized void handleIntent(Intent intent) { + boolean startDownload = false; + + if (intent == null) { + if (!Config.getKeepDownloadService()) { + stopForeground(true); + stopSelf(); + return; + } else { + startDownload = true; + } + } else { + String action = intent.getAction(); + if (ACTION_ADD.equals(action)) { + GalleryInfo gi = intent.getParcelableExtra(KEY_GALLERY_INFO); + if (gi != null) + add(gi); + else + startDownload = true; + + } else if (ACTION_START.equals(action)) { + int gid = intent.getIntExtra(KEY_GID, -1); + if (gid != -1) + start(gid); + else + startDownload = true; + + } else if (ACTION_START_ALL.equals(action)) { + startAll(); + + } else if (ACTION_STOP.equals(action)) { + int gid = intent.getIntExtra(KEY_GID, -1); + if (gid != -1) + stop(gid); + else + startDownload = true; + + } else if (ACTION_STOP_CURRENT.equals(action)) { + stopCurrentTask(); + + } else if (ACTION_STOP_ALL.equals(action)) { + stopAll(); + + } else if (ACTION_DELETE.equals(action)) { + int gid = intent.getIntExtra(KEY_GID, -1); + if (gid != -1) + delete(gid); + else + startDownload = true; + + } else if (ACTION_CLEAR.equals(action)) { + mDownloadOk.clear(); + mDownloadFailed.clear(); + startDownload = true; + + } else { + startDownload = true; + } + } + + if (startDownload) + tryToStartDownload(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + handleIntent(intent); + return START_STICKY; + } + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + @Override + public void onDestroy() { + super.onDestroy(); + + stopForeground(true); + + mBinder = null; + mNotifyManager = null; + } + + /** + * Try to start download, if there is no item to start, stop itself + */ + private synchronized void tryToStartDownload() { + if (mCurDownloadInfo != null || mCurExDownloader != null) + return; + + mCurDownloadInfo = mData.getFirstWaitDownloadInfo(); + if (mCurDownloadInfo == null) { + stopSelf(); + return; + } + + mCurDownloadInfo.state = DownloadInfo.STATE_DOWNLOAD; + mCurDownloadInfo.download = -1; + mCurDownloadInfo.total = -1; + // To update download state + mData.addDownload(mCurDownloadInfo); + + GalleryInfo gi = mCurDownloadInfo.galleryInfo; + mCurExDownloader = mEdManager.getExDownloader(gi.gid, + gi.token, gi.title, mCurDownloadInfo.mode); + mCurExDownloader.setListenerDownload(this); + // setDownloadMode will start download thread + mCurExDownloader.setDownloadMode(true); + } + + /** + * notify download info has changed + */ + private void notifyUpdate() { + Intent it = new Intent(ACTION_UPDATE); + sendBroadcast(it); + } + + /** + * If task is in list, reture false + * @param galleryInfo + * @return + */ + private boolean add(GalleryInfo galleryInfo) { + int gid = galleryInfo.gid; + DownloadInfo di; + if ((di = mData.getDownload(gid)) != null) { + if (di.state != DownloadInfo.STATE_DOWNLOAD) { + mData.setDownloadState(gid, DownloadInfo.STATE_WAIT); + tryToStartDownload(); + notifyUpdate(); + } + return false; + } else { + di = new DownloadInfo(galleryInfo, Config.getMode()); + di.state = DownloadInfo.STATE_WAIT; + mData.addDownload(di); + tryToStartDownload(); + notifyUpdate(); + return true; + } + } + + private synchronized void start(int gid) { + DownloadInfo di = mData.getDownload(gid); + if (di != null) + start(di); + } + + private synchronized void start(DownloadInfo di) { + if (mCurDownloadInfo != di && di.state != DownloadInfo.STATE_WAIT) { + mData.setDownloadState(di, DownloadInfo.STATE_WAIT); + notifyUpdate(); + } + tryToStartDownload(); + } + + public void startAll() { + mData.startAllDownload(); + tryToStartDownload(); + notifyUpdate(); + } + + private synchronized void stop(int gid) { + DownloadInfo di = mData.getDownload(gid); + if (di != null) + stop(di); + } + + private synchronized void stop(DownloadInfo di) { + if (mCurDownloadInfo == di) { + // Cancel download notification + stopForeground(true); + + // Target downloadinfo is downloading + mData.setDownloadState(mCurDownloadInfo, DownloadInfo.STATE_NONE); + mCurDownloadInfo = null; + + mCurExDownloader.setListenerDownload(null); + mCurExDownloader.setDownloadMode(false); + mEdManager.freeExDownloader(mCurExDownloader); + mCurExDownloader = null; + + tryToStartDownload(); + notifyUpdate(); + } else { + di.state = DownloadInfo.STATE_NONE; + tryToStartDownload(); + notifyUpdate(); + } + } + + public void stopCurrentTask() { + if (mCurDownloadInfo != null) + stop(mCurDownloadInfo); + else + tryToStartDownload(); + } + + public synchronized void stopAll() { + mData.stopAllDownload(); + stopCurrentTask(); + } + + private synchronized void delete(int gid) { + DownloadInfo di = mData.getDownload(gid); + if (di != null) + delete(di); + } + + public void delete(DownloadInfo di) { + stop(di); + mData.deleteDownload(di.galleryInfo.gid); + notifyUpdate(); + } + + private void ensureNotification() { + if (mBuilder != null) + return; + + mBuilder = new NotificationCompat.Builder(mContext); + mBuilder.setSmallIcon(android.R.drawable.stat_sys_download); + Intent intent = new Intent(DownloadService.this,DownloadActivity.class); + PendingIntent pendingIntent = PendingIntent.getActivity(DownloadService.this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + mBuilder.setContentIntent(pendingIntent); + mBuilder.setOngoing(true).setAutoCancel(false); + + // Add action + Intent stopIntent = new Intent(this, DownloadService.class); + stopIntent.setAction(ACTION_STOP_CURRENT); + PendingIntent piStop = PendingIntent.getService(this, 0, stopIntent, 0); + mBuilder.addAction(R.drawable.ic_action_stop, getString(R.string.stop), piStop); + + Intent stopAllIntent = new Intent(this, DownloadService.class); + stopAllIntent.setAction(ACTION_STOP_ALL); + PendingIntent piStopAll = PendingIntent.getService(this, 0, stopAllIntent, 0); + mBuilder.addAction(R.drawable.ic_action_close, getString(R.string.stop_all), piStopAll); + } + + @Override + public void onStart(int gid) { + if (mCurDownloadInfo == null) + return; + + mCurDownloadInfo.download = -1; + mCurDownloadInfo.total = -1; + + ensureNotification(); + + mBuilder.setContentTitle(getString(R.string.start_download) + " " + gid) + .setContentText(null) + .setProgress(0, 0, true); + startForeground(DOWNLOADING_NOTIFY_ID, mBuilder.build()); + + notifyUpdate(); + } + + @Override + public void onGetSum(int gid, int sum) { + if (mCurDownloadInfo == null) + return; + + mCurDownloadInfo.download = 0; + mCurDownloadInfo.total = sum; + + ensureNotification(); + + mBuilder.setContentTitle(getString(R.string.downloading) + " " + gid) + .setContentText(null) + .setProgress(mCurDownloadInfo.total, mCurDownloadInfo.download, false); + startForeground(DOWNLOADING_NOTIFY_ID, mBuilder.build()); + + notifyUpdate(); + } + + private long mLastStartForegroundTime = 0; + + @Override + public void onDownload(int gid, int downloadSize, int totalSize) { + if (mCurDownloadInfo == null) + return; + + mCurDownloadInfo.download = downloadSize; + mCurDownloadInfo.total = totalSize; + + ensureNotification(); + + mBuilder.setContentTitle(getString(R.string.downloading) + " " + gid) + .setContentText(mSpeedStr) + .setProgress(totalSize, downloadSize, false); + + long now = System.currentTimeMillis(); + if (now - mLastStartForegroundTime > 500) { + startForeground(DOWNLOADING_NOTIFY_ID, mBuilder.build()); + mLastStartForegroundTime = now; + } + + notifyUpdate(); + } + + @Override + public void onUpdateSpeed(int gid, int speed) { + if (mCurDownloadInfo == null) + return; + + mCurDownloadInfo.speed = speed; + mSpeedStr = Utils.sizeToString(speed) + "/S"; + + ensureNotification(); + + mBuilder.setContentTitle(getString(R.string.downloading) + " " + gid) + .setContentText(mSpeedStr) + .setProgress(mCurDownloadInfo.total, mCurDownloadInfo.download, false); + startForeground(DOWNLOADING_NOTIFY_ID, mBuilder.build()); + + notifyUpdate(); + } + + private synchronized String getDownloadNotificationText() { + + int ok = mDownloadOk.size(); + int failed = mDownloadFailed.size(); + + if (ok == 0 && failed == 0) { + return "null"; + } else if (ok == 1 && failed == 0) { + return String.format(getString(R.string.download_1), mDownloadOk.get(0)); + } else if (failed == 0) { + return String.format(getString(R.string.download_2), mDownloadOk.size()); + } else if (ok == 0 && failed == 1) { + return String.format(getString(R.string.download_3), mDownloadFailed.get(0)); + } else if (ok == 0) { + return String.format(getString(R.string.download_4), mDownloadFailed.size()); + } else if (ok == 1 && failed == 1){ + return String.format(getString(R.string.download_5), mDownloadOk.get(0), mDownloadFailed.get(0)); + } else { + return String.format(getString(R.string.download_6), mDownloadOk.size(), mDownloadFailed.size()); + } + } + + @Override + public synchronized void onDownloadOver(int gid, int legacy) { + if (mCurDownloadInfo == null) + return; + + mCurDownloadInfo.legacy = legacy; + if (mCurDownloadInfo.legacy == 0) { + // Download ok + if (!mDownloadOk.contains(gid)) + mDownloadOk.add(gid); + mDownloadFailed.remove((Integer) gid); + } else { + // Download failed + if (!mDownloadFailed.contains(gid)) + mDownloadFailed.add(gid); + mDownloadOk.remove((Integer) gid); + } + + Intent clearIntent = new Intent(this, DownloadService.class); + clearIntent.setAction(ACTION_CLEAR); + PendingIntent piClear = PendingIntent.getService(this, 0, clearIntent, 0); + + NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext); + builder.setSmallIcon(android.R.drawable.stat_sys_download_done); + Intent intent = new Intent(DownloadService.this,DownloadActivity.class); + PendingIntent pendingIntent = PendingIntent.getActivity(DownloadService.this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + builder.setContentIntent(pendingIntent); + builder.setContentTitle(getString(R.string.download_over)) + .setContentText(getDownloadNotificationText()) + .setDeleteIntent(piClear) + .setOngoing(false).setAutoCancel(true); + mNotifyManager.notify(DOWNLOAD_NOTIFY_ID, builder.build()); + stopForeground(true); + + mCurDownloadInfo.legacy = legacy; + mCurDownloadInfo.state = DownloadInfo.STATE_FINISH; + mData.addDownload(mCurDownloadInfo); + mCurDownloadInfo = null; + + mCurExDownloader.setListenerDownload(null); + mCurExDownloader.setDownloadMode(false); + mEdManager.freeExDownloader(mCurExDownloader); + mCurExDownloader = null; + + tryToStartDownload(); + notifyUpdate(); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/tile/RectDrawable.java b/app/src/main/java/com/hippo/ehviewer/tile/RectDrawable.java new file mode 100644 index 000000000..84f3060b8 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/tile/RectDrawable.java @@ -0,0 +1,84 @@ +/* + * 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.tile; + +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.drawable.Drawable; +import android.util.StateSet; + +public class RectDrawable extends Drawable { + + private final int[][] mStateSets; + private final int[] mColors; + private int mColor; + private int mLastIndex = -1; + + public RectDrawable(int color) { + mStateSets = null; + mColors = null; + mColor = color; + } + + public RectDrawable(int[][] stateSets, int[] colors) { + mStateSets = stateSets; + mColors = colors; + } + + @Override + public boolean isStateful() { + return mStateSets != null; + } + + @Override + protected boolean onStateChange(int[] stateSet) { + final int N = mStateSets.length; + for (int i = 0; i < N; i++) { + if (StateSet.stateSetMatches(mStateSets[i], stateSet)) { + if (mLastIndex == i) + return false; + mLastIndex = i; + + mColor = mColors[i]; + invalidateSelf(); + return true; + } + } + return false; + } + + @Override + public void draw(Canvas canvas) { + canvas.drawColor(mColor); + } + + @Override + public void setAlpha(int alpha) { + // Empty + } + + @Override + public void setColorFilter(ColorFilter cf) { + // Empty + } + + @Override + public int getOpacity() { + // Empty + return 0; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/tile/TileSalon.java b/app/src/main/java/com/hippo/ehviewer/tile/TileSalon.java new file mode 100644 index 000000000..0a54ac948 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/tile/TileSalon.java @@ -0,0 +1,32 @@ +/* + * 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.tile; + +import android.view.View; + +public class TileSalon { + + @SuppressWarnings("deprecation") + public static void reform(View view, int bgColor) { + view.setBackgroundDrawable(new RectDrawable(bgColor)); + } + + @SuppressWarnings("deprecation") + public static void reform(View view, int[][] stateSets, int[] bgColors) { + view.setBackgroundDrawable(new RectDrawable(stateSets, bgColors)); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/ui/AbsActivity.java b/app/src/main/java/com/hippo/ehviewer/ui/AbsActivity.java new file mode 100644 index 000000000..12ac4463c --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ui/AbsActivity.java @@ -0,0 +1,52 @@ +/* + * 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.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; + +public abstract class AbsActivity extends Activity { + + @Override + protected void onResume() { + super.onResume(); + + Ui.adjustOrientation(this); + } + + @Override + protected void onStart() { + super.onStart(); + + if (Config.getAllowAnalyics()) + EasyTracker.getInstance(this).activityStart(this); + } + + @Override + protected void onStop() { + super.onStop(); + + if (Config.getAllowAnalyics()) + EasyTracker.getInstance(this).activityStop(this); + + ImageCache.getInstance(this).flush(); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/ui/AbsPreferenceActivity.java b/app/src/main/java/com/hippo/ehviewer/ui/AbsPreferenceActivity.java new file mode 100644 index 000000000..89299dce6 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ui/AbsPreferenceActivity.java @@ -0,0 +1,51 @@ +/* + * 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.ui; + +import android.preference.PreferenceActivity; + +import com.google.analytics.tracking.android.EasyTracker; +import com.hippo.ehviewer.util.Config; +import com.hippo.ehviewer.util.Ui; + +public abstract class AbsPreferenceActivity extends PreferenceActivity { + + private static final String TAG = AbsPreferenceActivity.class.getSimpleName(); + + @Override + protected void onResume() { + super.onResume(); + + Ui.adjustOrientation(this); + } + + @Override + public void onStart() { + super.onStart(); + + if (Config.getAllowAnalyics()) + EasyTracker.getInstance(this).activityStart(this); + } + + @Override + public void onStop() { + super.onStop(); + + if (Config.getAllowAnalyics()) + EasyTracker.getInstance(this).activityStop(this); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/ui/AbsTranslucentActivity.java b/app/src/main/java/com/hippo/ehviewer/ui/AbsTranslucentActivity.java new file mode 100644 index 000000000..60b6d0eea --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ui/AbsTranslucentActivity.java @@ -0,0 +1,38 @@ +/* + * 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.ui; + +import android.content.res.Configuration; + +import com.hippo.ehviewer.util.Ui; + +public class AbsTranslucentActivity extends AbsActivity { + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + + Ui.updateTranslucent(this); + } + + @Override + protected void onResume() { + super.onResume(); + + Ui.updateTranslucent(this); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/ui/DownloadActivity.java b/app/src/main/java/com/hippo/ehviewer/ui/DownloadActivity.java new file mode 100644 index 000000000..15412b567 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ui/DownloadActivity.java @@ -0,0 +1,397 @@ +/* + * 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.ui; + +import java.util.List; + +import android.annotation.SuppressLint; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.CheckBox; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.TextView; + +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.cardview.CardViewSalon; +import com.hippo.ehviewer.data.Data; +import com.hippo.ehviewer.data.DownloadInfo; +import com.hippo.ehviewer.data.GalleryInfo; +import com.hippo.ehviewer.drawable.MaterialIndicatorDrawable; +import com.hippo.ehviewer.drawable.MaterialIndicatorDrawable.Stroke; +import com.hippo.ehviewer.service.DownloadService; +import com.hippo.ehviewer.util.Config; +import com.hippo.ehviewer.util.EhUtils; +import com.hippo.ehviewer.util.Ui; +import com.hippo.ehviewer.util.Utils; +import com.hippo.ehviewer.util.ViewUtils; +import com.hippo.ehviewer.widget.ExpandingListView; +import com.hippo.ehviewer.widget.FitWindowView; +import com.hippo.ehviewer.widget.LoadImageView; +import com.hippo.ehviewer.windowsanimate.WindowsAnimate; + +public class DownloadActivity extends AbsTranslucentActivity implements FitWindowView.OnFitSystemWindowsListener { + + private AppContext mAppContext; + private WindowsAnimate mWindowsAnimate; + private int mThemeColor; + private List mDownloads; + + private FitWindowView mStandard; + private ExpandingListView mList; + private ListAdapter mAdapter; + + private final BroadcastReceiver mReceiver = new BroadcastReceiver(){ + @Override + public void onReceive(Context context, Intent intent) { + if(intent.getAction().equals(DownloadService.ACTION_UPDATE)) + mAdapter.notifyDataSetChanged(); + } + }; + + @Override + public void onFitSystemWindows(int l, int t, int r, int b) { + mList.setPadding(mList.getPaddingLeft(), t, mList.getPaddingRight(), b); + + Ui.colorStatusBarKK(this, mThemeColor, t - Ui.ACTION_BAR_HEIGHT); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.download); + + mAppContext = (AppContext) getApplication(); + + // Init windows animate + mWindowsAnimate = new WindowsAnimate(); + mWindowsAnimate.init(this); + + // Receiver + IntentFilter filter = new IntentFilter(); + filter.addAction(DownloadService.ACTION_UPDATE); + registerReceiver(mReceiver, filter); + + // Menu + MaterialIndicatorDrawable materialIndicator = new MaterialIndicatorDrawable(this, Color.WHITE, Stroke.THIN); + materialIndicator.setIconState(MaterialIndicatorDrawable.IconState.ARROW); + Ui.setMaterialIndicator(getActionBar(), materialIndicator); + + mDownloads = Data.getInstance().getAllDownloads(); + for (DownloadInfo di : mDownloads) + di.setExpanded(false); + + mStandard = (FitWindowView) findViewById(R.id.standard); + mList = (ExpandingListView)findViewById(R.id.download); + + mStandard.addOnFitSystemWindowsListener(this); + mAdapter = new ListAdapter(); + mList.setAdapter(mAdapter); + mList.setSelector(new ColorDrawable(Color.TRANSPARENT)); + mList.setExpandingId(R.id.buttons); + + mThemeColor = Config.getCustomThemeColor() ? Config.getThemeColor() : Ui.THEME_COLOR; + getActionBar().setBackgroundDrawable(new ColorDrawable(mThemeColor)); + Ui.colorStatusBarL(this, mThemeColor); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + unregisterReceiver(mReceiver); + mWindowsAnimate.free(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.download, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle item selection + Intent it; + switch (item.getItemId()) { + case android.R.id.home: + finish(); + return true; + + case R.id.action_start: + it = new Intent(DownloadActivity.this, DownloadService.class); + it.setAction(DownloadService.ACTION_START_ALL); + startService(it); + return true; + + case R.id.action_stop: + it = new Intent(DownloadActivity.this, DownloadService.class); + it.setAction(DownloadService.ACTION_STOP_ALL); + startService(it); + return true; + + default: + return super.onOptionsItemSelected(item); + } + } + + public class ListAdapter extends BaseAdapter { + @Override + public int getCount() { + return mDownloads.size(); + } + + @Override + public Object getItem(int position) { + return mDownloads.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final DownloadInfo di = mDownloads.get(position); + final GalleryInfo gi = di.galleryInfo; + + if (convertView == null) { + convertView = LayoutInflater.from(DownloadActivity.this) + .inflate(R.layout.download_list_item, parent, false); + CardViewSalon.reformWithShadow(((ViewGroup)convertView).getChildAt(0), + 0xFFFAFAFA, 0, false); // TODO + ((TextView)convertView.findViewById(R.id.action)).setTextColor(mThemeColor); + } + LoadImageView thumb = (LoadImageView)convertView.findViewById(R.id.thumb); + if (!String.valueOf(gi.gid).equals(thumb.getKey())) { + // Set margin top 8dp if position is 0, otherwise 4dp + LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) + convertView.findViewById(R.id.card_view).getLayoutParams(); + if (position == 0) + lp.topMargin = Ui.dp2pix(8); + else + lp.topMargin = Ui.dp2pix(4); + + // Set new thumb + thumb.setImageDrawable(null); + thumb.setLoadInfo(gi.thumb, String.valueOf(gi.gid)); + ImageLoader.getInstance(DownloadActivity.this).add(gi.thumb, String.valueOf(gi.gid), + new LoadImageView.SimpleImageGetListener(thumb).setFixScaleType(true)); + } + TextView title = (TextView)convertView.findViewById(R.id.title); + title.setText(gi.title); + View Buttons = convertView.findViewById(R.id.buttons); + if (di.getExpanded()) + Buttons.setVisibility(View.VISIBLE); + else + Buttons.setVisibility(View.GONE); + + ProgressBar pb = (ProgressBar)convertView.findViewById(R.id.progress_bar); + TextView leftText = (TextView)convertView.findViewById(R.id.text_left); + TextView rightText = (TextView)convertView.findViewById(R.id.text_right); + + View read = convertView.findViewById(R.id.read); + TextView action = (TextView)convertView.findViewById(R.id.action); + View detail = convertView.findViewById(R.id.detail); + View delete = convertView.findViewById(R.id.delete); + + read.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(DownloadActivity.this, + GalleryActivity.class); + intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, gi); + intent.putExtra(GalleryActivity.KEY_START_INDEX, -1); + startActivity(intent); + } + }); + delete.setOnClickListener(new DeleteAction(di)); + detail.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(DownloadActivity.this, + GalleryDetailActivity.class); + intent.putExtra(GalleryDetailActivity.KEY_G_INFO, gi); + startActivity(intent); + } + }); + + switch (di.state) { + case DownloadInfo.STATE_NONE: + pb.setVisibility(View.INVISIBLE); + leftText.setText(R.string.not_started); + rightText.setVisibility(View.GONE); + + action.setText(getString(R.string.start)); + action.setOnClickListener(new StartAction(di)); + break; + + case DownloadInfo.STATE_WAIT: + pb.setVisibility(View.INVISIBLE); + leftText.setText(R.string.waiting); + rightText.setVisibility(View.GONE); + + action.setText(getString(R.string.stop)); + action.setOnClickListener(new StopAction(di)); + break; + + case DownloadInfo.STATE_DOWNLOAD: + leftText.setText(Utils.sizeToString(di.speed) + "/S"); + if (di.total == -1) { + pb.setVisibility(View.VISIBLE); + pb.setIndeterminate(true); + rightText.setVisibility(View.GONE); + } else { + pb.setVisibility(View.VISIBLE); + pb.setIndeterminate(false); + pb.setMax(di.total); + pb.setProgress(di.download); + rightText.setVisibility(View.VISIBLE); + rightText.setText(di.download + "/" + di.total); + } + + action.setText(getString(R.string.stop)); + action.setOnClickListener(new StopAction(di)); + break; + + case DownloadInfo.STATE_FINISH: + pb.setVisibility(View.INVISIBLE); + if (di.legacy == 0) + leftText.setText(R.string.done); + else + leftText.setText(String.format(getApplication().getString(R.string.legacy_pages), di.legacy)); + rightText.setVisibility(View.GONE); + + action.setText(getString(R.string.start)); + action.setOnClickListener(new StartAction(di)); + break; + } + return convertView; + } + + @Override + public void notifyDataSetChanged() { + if (!mList.isAnimating()) + super.notifyDataSetChanged(); + } + + @Override + public void notifyDataSetInvalidated() { + if (!mList.isAnimating()) + super.notifyDataSetInvalidated(); + } + } + + private class DeleteAction implements View.OnClickListener { + + private static final String KEY_INCLUDE_PIC = "include_pic"; + + public DownloadInfo mDownloadInfo; + + public DeleteAction(DownloadInfo di) { + mDownloadInfo = di; + } + + @Override + @SuppressLint("InflateParams") + public void onClick(View v) { + final View view = ViewUtils.inflateDialogView(R.layout.dialog_message_checkbox, false); + final TextView message = (TextView) view.findViewById(R.id.message); + final CheckBox cb = (CheckBox) view.findViewById(R.id.checkbox); + message.setText(R.string.delete_item); + cb.setText(R.string.pic_included); + cb.setChecked(Config.getBoolean(KEY_INCLUDE_PIC, false)); + + new MaterialAlertDialog.Builder(DownloadActivity.this).setTitle(R.string.attention) + .setView(view, true) + .setDefaultButton(MaterialAlertDialog.POSITIVE | MaterialAlertDialog.NEGATIVE) + .setButtonListener(new MaterialAlertDialog.OnClickListener() { + @Override + public boolean onClick(MaterialAlertDialog dialog, int which) { + if (which == MaterialAlertDialog.POSITIVE) { + Intent it = new Intent(DownloadActivity.this, DownloadService.class); + it.setAction(DownloadService.ACTION_DELETE); + it.putExtra(DownloadService.KEY_GID, mDownloadInfo.galleryInfo.gid); + startService(it); + // Delete dir + if (cb.isChecked()) { + GalleryInfo gi = mDownloadInfo.galleryInfo; + Utils.deleteDirInThread(EhUtils.getGalleryDir(gi.gid, gi.title)); + } + // Remember delete choice + Config.setBoolean(KEY_INCLUDE_PIC, cb.isChecked()); + } + return true; + } + }).show(); + } + } + + private class StartAction implements View.OnClickListener { + public DownloadInfo mDownloadInfo; + + public StartAction(DownloadInfo di) { + mDownloadInfo = di; + } + + @Override + public void onClick(View v) { + if (mDownloadInfo.state == DownloadInfo.STATE_NONE || + mDownloadInfo.state == DownloadInfo.STATE_FINISH) { + + Intent it = new Intent(DownloadActivity.this, DownloadService.class); + it.setAction(DownloadService.ACTION_START); + it.putExtra(DownloadService.KEY_GID, mDownloadInfo.galleryInfo.gid); + startService(it); + mAdapter.notifyDataSetChanged(); + } + } + } + + + private class StopAction implements View.OnClickListener { + + public DownloadInfo mDownloadInfo; + + public StopAction(DownloadInfo di) { + mDownloadInfo = di; + } + + @Override + public void onClick(View v) { + Intent it = new Intent(DownloadActivity.this, DownloadService.class); + it.setAction(DownloadService.ACTION_STOP); + it.putExtra(DownloadService.KEY_GID, mDownloadInfo.galleryInfo.gid); + startService(it); + mAdapter.notifyDataSetChanged(); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/ui/FavouriteActivity.java b/app/src/main/java/com/hippo/ehviewer/ui/FavouriteActivity.java new file mode 100644 index 000000000..e3a3b71ca --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ui/FavouriteActivity.java @@ -0,0 +1,594 @@ +/* + * 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.ui; + +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import android.annotation.SuppressLint; +import android.app.ActionBar; +import android.app.AlertDialog; +import android.content.Intent; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Color; +import android.graphics.Path; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.PathShape; +import android.os.Bundle; +import android.view.ActionMode; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.FrameLayout; +import android.widget.ListView; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.app.MaterialAlertDialog; +import com.hippo.ehviewer.app.MaterialProgressDialog; +import com.hippo.ehviewer.data.Data; +import com.hippo.ehviewer.data.GalleryInfo; +import com.hippo.ehviewer.drawable.MaterialIndicatorDrawable; +import com.hippo.ehviewer.drawable.MaterialIndicatorDrawable.Stroke; +import com.hippo.ehviewer.ehclient.EhClient; +import com.hippo.ehviewer.util.Config; +import com.hippo.ehviewer.util.Favorite; +import com.hippo.ehviewer.util.Ui; +import com.hippo.ehviewer.widget.ActionableToastBar; +import com.hippo.ehviewer.widget.ActionableToastBar.ActionClickedListener; +import com.hippo.ehviewer.widget.FitWindowView; +import com.hippo.ehviewer.widget.GalleryListView; +import com.hippo.ehviewer.widget.GalleryListView.OnGetListListener; +import com.hippo.ehviewer.widget.recyclerview.EasyRecyclerView; +import com.hippo.ehviewer.widget.MaterialToast; +import com.hippo.ehviewer.widget.SlidingDrawerLayout; + +public class FavouriteActivity extends AbsTranslucentActivity + implements EasyRecyclerView.MultiChoiceModeListener, + View.OnTouchListener, GalleryListView.GalleryListViewHelper, + FitWindowView.OnFitSystemWindowsListener, EasyRecyclerView.OnItemClickListener { + + private static final String TAG = FavouriteActivity.class.getSimpleName(); + + public static final int LIST_MODE_DETAIL = 0; + public static final int LIST_MODE_THUMB = 1; + + private static final String LOCAL_FAVORITE_URL = "ehviewer://local_favorite"; + + private Data mData; + private Resources mResources; + private EhClient mClient; + private ActionBar mActionBar; + private int mThemeColor; + + private SlidingDrawerLayout mDrawerLayout; + private ListView mMenu; + private FrameLayout mContentView; + private FitWindowView mStandard; + private GalleryListView mGalleryListView; + + private ActionableToastBar mActionableToastBar; + + private Set mChoiceGiSet; + private Set mChoiceGiSetCopy; // Store selected GalleryInfo + private Iterator mSetIter; // For move from local to cloud + private GalleryInfo mTargetGi; + private int mMenuIndex; + private int mTargetCat; + + private AlertDialog mMoveDialog; + private MaterialProgressDialog mProgressDialog; + + private List mLastModifyGiList = null; + private int mLastModifyPageNum = 0; + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + } + + @Override + public void onBackPressed() { + if (mActionableToastBar.isShown()) + mActionableToastBar.hide(true); + else if (mDrawerLayout.isDrawerOpen(mMenu)) + mDrawerLayout.closeDrawer(mMenu); + else + super.onBackPressed(); + } + + private void initLocalFavorite() { + mGalleryListView.setEnabledHeader(false); + mGalleryListView.setEnabledFooter(false); + setTitle(Favorite.FAVORITE_TITLES[mMenuIndex]); + mGalleryListView.refresh(); + } + + private void initFavorite() { + mGalleryListView.setEnabledHeader(true); + mGalleryListView.setEnabledFooter(true); + setTitle(Favorite.FAVORITE_TITLES[mMenuIndex]); + mGalleryListView.refresh(); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.favorite); + + mData = Data.getInstance(); + mResources =getResources(); + mClient = EhClient.getInstance(); + + mActionBar = getActionBar(); + mActionBar.setDisplayHomeAsUpEnabled(true); + + // Menu + MaterialIndicatorDrawable materialIndicator = new MaterialIndicatorDrawable(this, Color.WHITE, Stroke.THIN); + materialIndicator.setIconState(MaterialIndicatorDrawable.IconState.ARROW); + Ui.setMaterialIndicator(getActionBar(), materialIndicator); + + // Get View + mDrawerLayout = (SlidingDrawerLayout) findViewById(R.id.drawerlayout); + mMenu = (ListView) mDrawerLayout.findViewById(R.id.favorite_menu_list); + mContentView = (FrameLayout) mDrawerLayout.findViewById(R.id.content); + mStandard = (FitWindowView) mDrawerLayout.findViewById(R.id.standard); + mGalleryListView = (GalleryListView) mDrawerLayout.findViewById(R.id.gallery_list); + + mActionableToastBar = new ActionableToastBar(this); + mActionableToastBar.setBackgroundColor(mResources.getColor(android.R.color.holo_purple)); + mContentView.addView(mActionableToastBar); + mContentView.setOnTouchListener(this); + + mGalleryListView.setGalleryListViewHelper(this); + mStandard.addOnFitSystemWindowsListener(this); + //mPullViewGroup.setAgainstToChildPadding(true); + + mChoiceGiSet = new LinkedHashSet(); + mChoiceGiSetCopy = new LinkedHashSet(); + + //mList.setOnTouchListener(this); + mGalleryListView.setOnItemClickListener(this); + mGalleryListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL); + mGalleryListView.setMultiChoiceModeListener(this); + + mMenu.setClipToPadding(false); + mMenu.setAdapter(new BaseAdapter() { + private ShapeDrawable d; + private ShapeDrawable createDrawable() { + Path path = new Path(); + path.moveTo(50, 10); + path.lineTo(10, 50); + path.lineTo(50, 90); + path.lineTo(90, 50); + path.close(); + ShapeDrawable d = new ShapeDrawable(new PathShape(path, 100, 100)); + d.getPaint().setColor(0xcdffffff); + d.setBounds(0, 0, Ui.dp2pix(36), Ui.dp2pix(36)); + return d; + } + @Override + public int getCount() { + return EhClient.FAVORITE_SLOT_NUM + 1; + } + @Override + public Object getItem(int position) { + return null; + } + @Override + public long getItemId(int position) { + return position; + } + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) + convertView = LayoutInflater.from(FavouriteActivity.this).inflate(R.layout.menu_item, parent, false); + TextView tv = (TextView)convertView; + tv.setText(Favorite.FAVORITE_TITLES[position]); + if (position == 0) { + Drawable dr = mResources.getDrawable(R.drawable.ic_setting_panda_light); + dr.setBounds(0, 0, Ui.dp2pix(36), Ui.dp2pix(36)); + tv.setCompoundDrawables(dr, null, null, null); + tv.setCompoundDrawablePadding(Ui.dp2pix(8)); + } else { + if (d == null) + d = createDrawable(); + tv.setCompoundDrawables(d, null, null, null); + tv.setCompoundDrawablePadding(Ui.dp2pix(8)); + } + return convertView; + } + }); + mMenu.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, + int position, long id) { + mDrawerLayout.closeDrawers(); + // If same index, do nothing + if (mMenuIndex == position) + return; + + mMenuIndex = position; + setTitle(Favorite.FAVORITE_TITLES[mMenuIndex]); + if (mMenuIndex == 0) + initLocalFavorite(); + else + initFavorite(); + } + }); + + // Set random color + mThemeColor = Config.getCustomThemeColor() ? Config.getThemeColor() : Ui.THEME_COLOR; + mActionBar.setBackgroundDrawable(new ColorDrawable(mThemeColor)); + mMenu.setBackgroundColor(mThemeColor); + Ui.colorStatusBarL(this, mThemeColor); + + // Check login + if (!mClient.isLogin()) { + MaterialToast.showToast(R.string.favorite_warning); + } + + // TODO Should show default favourite + mMenuIndex = 0; + initLocalFavorite(); + } + + @Override + public void onFitSystemWindows(int l, int t, int r, int b) { + int magicSpacing = Ui.dp2pix(20); + FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams( + RelativeLayout.LayoutParams.MATCH_PARENT, Ui.dp2pix(60)); + lp.gravity = Gravity.BOTTOM; + // Make sure actionable is above navigation bar + lp.bottomMargin = b + magicSpacing; + lp.leftMargin = magicSpacing; + lp.rightMargin = magicSpacing; + mActionableToastBar.setLayoutParams(lp); + mGalleryListView.setPaddingTopAndBottom(t, b); + ((SlidingDrawerLayout.LayoutParams) mMenu.getLayoutParams()).topMargin = t; + mMenu.setPadding(mMenu.getPaddingLeft(), mMenu.getPaddingTop(), mMenu.getPaddingRight(), b); + + Ui.colorStatusBarKK(this, mThemeColor, t - Ui.ACTION_BAR_HEIGHT); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.favorite, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle item selection + switch (item.getItemId()) { + case android.R.id.home: + finish(); + return true; + case R.id.action_list: + if (mDrawerLayout.isDrawerOpen(mMenu)) + mDrawerLayout.closeDrawer(mMenu); + else + mDrawerLayout.openDrawer(mMenu); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + } + + private void showToastBar(final ActionClickedListener listener, int descriptionIconResourceId, + CharSequence descriptionText, int actionIconResource, CharSequence actionText, + boolean replaceVisibleToast) { + mActionableToastBar.show(listener, descriptionIconResourceId, + descriptionText, actionIconResource, actionText, replaceVisibleToast); + } + + private void hideToastBar(MotionEvent event) { + if (mActionableToastBar != null) { + if (event != null && mActionableToastBar.isEventInToastBar(event)) { + // Avoid touches inside the undo bar. + return; + } + mActionableToastBar.hide(false); + } + } + + @Override + public boolean onItemClick(EasyRecyclerView parent, View view, + int position, long id) { + Intent intent = new Intent(this, GalleryDetailActivity.class); + GalleryInfo gi = mGalleryListView.getGalleryInfo(position); + intent.putExtra(GalleryDetailActivity.KEY_G_INFO, gi); + startActivity(intent); + return true; + } + + @Override + @SuppressLint("ClickableViewAccessibility") + public boolean onTouch(View v, MotionEvent event) { + hideToastBar(event); + return super.onTouchEvent(event); + } + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.favorite_choice, menu); + mode.setTitle(R.string.select_item); + return true; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return true; + } + + private int[] getGids(Set giSet) { + int[] gids = new int[giSet.size()]; + int i = 0; + for (GalleryInfo gi : giSet) + gids[i++] = gi.gid; + return gids; + } + + private void startMoveFromLocal2Cloud() { + mProgressDialog = MaterialProgressDialog.create(this, getString(R.string.moving), false); + mProgressDialog.setMax(mChoiceGiSetCopy.size()); + mProgressDialog.setProgress(0); + mProgressDialog.show(); + moveFromLocal2Cloud(); + } + + private void moveFromLocal2Cloud() { + if (mSetIter == null) + mSetIter = mChoiceGiSetCopy.iterator(); + + if (mSetIter.hasNext() || mTargetGi != null) { + if (mTargetGi == null) + mTargetGi = mSetIter.next(); + mClient.addToFavorite(mTargetGi.gid, mTargetGi.token, + mTargetCat, null, new EhClient.OnAddToFavoriteListener() { + @Override + public void onSuccess() { + // remove from set iterator, favorite data + mSetIter.remove(); + mData.deleteLocalFavourite(mTargetGi.gid); + mGalleryListView.getGalleryList().remove(mTargetGi); + mGalleryListView.notifyDataSetChanged(); + // set mGiLocal2Cloud null for above + mTargetGi = null; + mProgressDialog.setProgress(mProgressDialog.getProgress() + 1); + moveFromLocal2Cloud(); + } + @Override + public void onFailure(String eMsg) { + mGalleryListView.notifyDataSetChanged(); + mProgressDialog.dismiss(); + mProgressDialog = null; + showToastBar(new ActionClickedListener() { + @Override + public void onActionClicked() { + startMoveFromLocal2Cloud(); + } + }, 0, mResources.getString(R.string.failed_to_move), + R.drawable.ic_warning, mResources.getString(R.string.retry), true); + } + }); + } else { + if (mGalleryListView.getGalleryList().size() == 0) + mGalleryListView.onlyShowNone(); + mSetIter = null; + mProgressDialog.dismiss(); + mProgressDialog = null; + MaterialToast.showToast(R.string.move_successful); + } + } + + private AlertDialog createMoveDialog(final ActionMode mode) { + return new MaterialAlertDialog.Builder(this).setTitle(R.string.where_to_move) + .setItems(Favorite.FAVORITE_TITLES, new MaterialAlertDialog.OnClickListener() { + @Override + public boolean onClick(MaterialAlertDialog dialog, int position) { + if (mMenuIndex == position) { + MaterialToast.showToast(R.string.dst_src_same); + return true; + } + + mTargetCat = position - 1; + if (mMenuIndex == 0) { // From local favorite to cloud + mMoveDialog.dismiss(); + mMoveDialog = null; + + startMoveFromLocal2Cloud(); + } else { + if (position == 0) { // From cloud to local favorite + mMoveDialog.dismiss(); + mMoveDialog = null; + + mClient.modifyFavorite(getGids(mChoiceGiSetCopy), mTargetCat, + mMenuIndex -1, new Modify(mResources.getString(R.string.move_successful), + mResources.getString(R.string.failed_to_move), true)); + mGalleryListView.setHeaderRefreshing(true); + } else { // change cloud dir + mMoveDialog.dismiss(); + mMoveDialog = null; + + mClient.modifyFavorite(getGids(mChoiceGiSetCopy), mTargetCat, + mMenuIndex -1, new Modify(mResources.getString(R.string.move_successful), + mResources.getString(R.string.failed_to_move), false)); + mGalleryListView.setHeaderRefreshing(true); + } + } + return true; + } + }).setNegativeButton(android.R.string.cancel).create(); + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + // get a copy of mChoiceGiSet + mSetIter = null; + mChoiceGiSetCopy.clear(); + for (GalleryInfo gi : mChoiceGiSet) + mChoiceGiSetCopy.add(gi); + + switch (item.getItemId()) { + case R.id.action_delete: + if (mMenuIndex == 0) { + for (GalleryInfo gi : mChoiceGiSet) + mData.deleteLocalFavourite(gi.gid); + mGalleryListView.refresh(); + } else { + mTargetCat = -1; + mClient.modifyFavorite(getGids(mChoiceGiSet), mTargetCat, + mMenuIndex -1, new Modify(mResources.getString(R.string.delete_successfully), + mResources.getString(R.string.failed_to_delete), false)); + mGalleryListView.setHeaderRefreshing(true); + } + mode.finish(); + return true; + case R.id.action_move: + mMoveDialog = createMoveDialog(mode); + mMoveDialog.show(); + mode.finish(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + mChoiceGiSet.clear(); + } + + @Override + public void onItemCheckedStateChanged(ActionMode mode, int position, + long id, boolean checked) { + if (checked) + mChoiceGiSet.add(mGalleryListView.getGalleryInfo(position)); + else + mChoiceGiSet.remove(mGalleryListView.getGalleryInfo(position)); + } + + @Override + public String getTargetUrl(int targetPage) { + if (mMenuIndex == 0) + return LOCAL_FAVORITE_URL; + else + return mClient.getFavoriteUrlWithCat(mMenuIndex - 1, targetPage); + } + + @Override + public void doGetGallerys(String url, final long taskStamp, + final OnGetListListener listener) { + // If get local favorite + if (mLastModifyGiList != null) { + listener.onSuccess(taskStamp, mLastModifyGiList, mLastModifyPageNum); + mLastModifyGiList = null; + mLastModifyPageNum = 0; + } else if (LOCAL_FAVORITE_URL.equals(url)) { + List giList = mData.getAllLocalFavourites(); + if (giList == null || giList.size() == 0) + listener.onSuccess(taskStamp, giList, 0); + else + listener.onSuccess(taskStamp, giList, 1); + } else { // If get cloud favorite + mClient.getGList(url, Config.getApiMode(), null, new EhClient.OnGetGListListener() { + @Override + public void onSuccess(Object checkFlag, List giList, + int maxPage) { + listener.onSuccess(taskStamp, giList, maxPage); + } + @Override + public void onFailure(Object checkFlag, String eMsg) { + listener.onFailure(taskStamp, eMsg); + } + }); + } + } + + private class Modify implements EhClient.OnModifyFavoriteListener { + private final String mSuccStr; + private final String mFailStr; + private final boolean mToLocal; + + public Modify(String succStr, String failStr, boolean toLocal) { + mSuccStr = succStr; + mFailStr = failStr; + mToLocal = toLocal; + } + + @Override + public void onSuccess(List gis, int pageNum) { + mLastModifyGiList = gis; + mLastModifyPageNum = pageNum; + mGalleryListView.refresh(); + MaterialToast.showToast(mSuccStr); + + // add to local + if (mToLocal) + for (GalleryInfo gi : mChoiceGiSetCopy) + mData.addLocalFavourite(gi); + } + @Override + public void onFailure(String eMsg) { + mGalleryListView.setHeaderRefreshing(false); + showToastBar(new Remodify(mSuccStr, mFailStr, mToLocal), 0, mFailStr + ": " + eMsg, + R.drawable.ic_action_redo, mResources.getString(R.string.retry), true); + } + } + + /** + * Redelete when delete error + * @author Hippo + * + */ + private class Remodify implements ActionableToastBar.ActionClickedListener { + private final String mSuccStr; + private final String mFailStr; + private final boolean mToLocal; + + public Remodify(String succStr, String failStr, boolean toLocal) { + mSuccStr = succStr; + mFailStr = failStr; + mToLocal = toLocal; + } + + @Override + public void onActionClicked() { + mClient.modifyFavorite(getGids(mChoiceGiSetCopy), -1, mMenuIndex -1, new Modify(mSuccStr, mFailStr, mToLocal)); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/ui/GalleryActivity.java b/app/src/main/java/com/hippo/ehviewer/ui/GalleryActivity.java new file mode 100644 index 000000000..5d2eaf3b1 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ui/GalleryActivity.java @@ -0,0 +1,551 @@ +/* + * 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.ui; + +import java.io.File; + +import android.annotation.TargetApi; +import android.app.Dialog; +import android.content.Intent; +import android.content.res.Configuration; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import android.widget.AdapterView; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.SeekBar; +import android.widget.Spinner; +import android.widget.TextView; + +import com.hippo.ehviewer.AppHandler; +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.app.DirSelectDialog; +import com.hippo.ehviewer.app.MaterialAlertDialog; +import com.hippo.ehviewer.data.Data; +import com.hippo.ehviewer.data.GalleryInfo; +import com.hippo.ehviewer.ehclient.ExDownloader; +import com.hippo.ehviewer.gallery.GalleryView; +import com.hippo.ehviewer.gallery.data.ImageSet; +import com.hippo.ehviewer.gallery.ui.GLRootView; +import com.hippo.ehviewer.util.Config; +import com.hippo.ehviewer.util.FullScreenHelper; +import com.hippo.ehviewer.util.MathUtils; +import com.hippo.ehviewer.util.Utils; +import com.hippo.ehviewer.widget.ColorView; +import com.hippo.ehviewer.widget.MaterialToast; +import com.hippo.ehviewer.widget.SlidingLayout; + +public class GalleryActivity extends AbsActivity + implements GalleryView.GalleryViewListener, SeekBar.OnSeekBarChangeListener, + FullScreenHelper.OnFullScreenBrokenListener, CompoundButton.OnCheckedChangeListener, + SlidingLayout.OnChildHideListener, AdapterView.OnItemSelectedListener, + View.OnClickListener, Utils.OnCopyOverListener { + + @SuppressWarnings("unused") + private final static String TAG = GalleryActivity.class.getSimpleName(); + + public final static String KEY_GALLERY_INFO = "gallery_info"; + public final static String KEY_START_INDEX = "start_index"; + + public final static String KEY_LIGHTNESS = "lightness"; + public final static float DEFAULT_LIGHTNESS = 0.5f; + + public final static String KEY_FIRST_READ_CONFIG_V1 = "first_read_config_v1"; + public final static boolean DEFAULT_FIRST_READ_CONFIG_V1 = true; + + public final static String KEY_LAST_SAVE_IMAGE_FILE_PATH = "last_save_image_file_path"; + public final static String DEFAULT_LAST_SAVE_IMAGE_FILE_PATH = null; + + private View mClock; + private View mBattery; + + private SlidingLayout mConfigSliding; + /** It store index, start from 0, so max is samller than size **/ + private TextView mCurrentPage; + private TextView mPageSum; + private SeekBar mPageSeeker; + private TextView mRefresh; + private TextView mSend; + private TextView mMoreSettings; + private Spinner mReadingDirection; + private Spinner mPageScaling; + private Spinner mStartPosition; + private CheckBox mKeepScreenOn; + private CheckBox mShowClock; + private CheckBox mShowBattery; + private CheckBox mCustomLightness; + private SeekBar mCustomLightnessValue; + private CheckBox mCustomCodec; + private Spinner mDecodeFormat; + + private ImageSet mImageSet; + + private GalleryView mGalleryView; + + private ColorView mMaskView; + + private GalleryInfo mGi; + + private FullScreenHelper mFullScreenHelper; + + private File mCurImageFile; + + private Dialog mSendDialog; + + private final Runnable mFullScreenTask = new Runnable() { + @Override public void run() { + mFullScreenHelper.setFullScreen(true); + } + }; + + private void startFullScreenTask() { + AppHandler.getInstance().postDelayed(mFullScreenTask, 2000); + } + + private void cancelFullScreenTask() { + AppHandler.getInstance().removeCallbacks(mFullScreenTask); + } + + @Override + public void onCopyOver(boolean success, File src, File dst) { + MaterialToast.showToast(success ? String.format(getString(R.string.save_successful), dst.getPath()) + : getString(R.string.save_failed)); + if (success) + Config.setString(KEY_LAST_SAVE_IMAGE_FILE_PATH, dst.getParent()); + } + + private Dialog createSaveImageDialog() { + return DirSelectDialog.create( + new MaterialAlertDialog.Builder(GalleryActivity.this) + .setDrakTheme(true).setTitle(R.string.select_save_dir) + .setDefaultButton(MaterialAlertDialog.POSITIVE | MaterialAlertDialog.NEGATIVE) + .setButtonListener(new MaterialAlertDialog.OnClickListener() { + @Override + public boolean onClick(MaterialAlertDialog dialog, int which) { + if (which == MaterialAlertDialog.POSITIVE) { + String targetPath = ((DirSelectDialog) dialog).getCurrentPath(); + StringBuilder sb = new StringBuilder(); + sb.append(mImageSet.getTitle()).append("-").append(mGalleryView.getCurIndex() + 1) + .append(".").append(Utils.getExtension(mCurImageFile.getName(), "jpg")); + Utils.copyInNewThread(mCurImageFile, new File(targetPath, Utils.standardizeFilename(sb.toString())), + GalleryActivity.this); + } + return true; + } + }), + Config.getString(KEY_LAST_SAVE_IMAGE_FILE_PATH, null)); + } + + private Dialog createSendDialog() { + return new MaterialAlertDialog.Builder(this).setDrakTheme(true).setTitle(R.string.send) + .setItems(R.array.send_entries, new MaterialAlertDialog.OnClickListener() { + @Override + public boolean onClick(MaterialAlertDialog dialog, int which) { + // Check current image file again + if (mCurImageFile == null || !mCurImageFile.exists()) { + MaterialToast.showToast(R.string.current_image_not_downloaded); + return false; + } + + switch (which) { + case 0: + createSaveImageDialog().show(); + break; + case 1: + Uri uri = Uri.fromFile(mCurImageFile); + Intent shareIntent = new Intent(); + shareIntent.setAction(Intent.ACTION_SEND); + shareIntent.putExtra(Intent.EXTRA_STREAM, uri); + shareIntent.setType(Utils.getMimeType(uri.toString())); + startActivity(Intent.createChooser(shareIntent, getString(R.string.share_image))); + break; + } + return true; + } + }).setNegativeButton(android.R.string.cancel).create(); + } + + + @Override + public void onFullScreenBroken(boolean fullScreen) { + // TODO For some MIUI system, onSystemUiVisibilityChange() do not work fine + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + } + + @Override + protected void onResume() { + super.onResume(); + + updateState(); + mFullScreenHelper.setFullScreen(!mConfigSliding.isShowing()); + } + + private void setScreenLightness(boolean enable, int lightness) { + Window w = getWindow(); + WindowManager.LayoutParams lp = w.getAttributes(); + if (enable) { + lightness = MathUtils.clamp(lightness, 0, 200); + if (lightness > 100) { + mMaskView.setColor(0); + // Avoid BRIGHTNESS_OVERRIDE_OFF, + // screen may be off when lp.screenBrightness is 0.0f + lp.screenBrightness = Math.max((lightness - 100) / 100.0f, 0.05f); + } else { + mMaskView.setColor(MathUtils.lerp(0xde, 0x00, lightness / 100.0f) << 24); + lp.screenBrightness = 0.05f; + } + } else { + mMaskView.setColor(0); + lp.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE; + } + w.setAttributes(lp); + } + + private void updateState() { + mReadingDirection.setSelection(Config.getReadingDirection()); + mPageScaling.setSelection(Config.getPageScaling()); + mStartPosition.setSelection(Config.getStartPosition()); + mGalleryView.setReadingDirection(Config.getReadingDirection()); + mGalleryView.setPageScaling(Config.getPageScaling()); + mGalleryView.setStartPosition(Config.getStartPosition()); + + mKeepScreenOn.setChecked(Config.getKeepSreenOn()); + if (Config.getKeepSreenOn()) + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + else + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + + mShowClock.setChecked(Config.getShowClock()); + mShowBattery.setChecked(Config.getShowBattery()); + mClock.setVisibility(Config.getShowClock() ? View.VISIBLE : View.GONE); + mBattery.setVisibility(Config.getShowBattery() ? View.VISIBLE : View.GONE); + + mCustomLightness.setChecked(Config.getCustomLightness()); + if (mCustomLightness.isChecked()) + mCustomLightnessValue.setEnabled(true); + else + mCustomLightnessValue.setEnabled(false); + mCustomLightnessValue.setProgress(Config.getCustomLightnessValue()); + setScreenLightness(mCustomLightness.isChecked(), mCustomLightnessValue.getProgress()); + + if (Utils.SUPPORT_IMAGE) + mCustomCodec.setEnabled(true); + else + mCustomCodec.setEnabled(false); + mCustomCodec.setChecked(Config.getCustomCodec()); + if (Utils.SUPPORT_IMAGE && mCustomCodec.isChecked()) + mDecodeFormat.setEnabled(true); + else + mDecodeFormat.setEnabled(false); + mDecodeFormat.setSelection(Config.getDecodeFormatIndex()); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.gl_root_group); + + mFullScreenHelper = new FullScreenHelper(this); + mFullScreenHelper.setOnFullScreenBrokenListener(this); + mFullScreenHelper.setFullScreen(true); + + mMaskView = (ColorView)findViewById(R.id.mask); + mClock = findViewById(R.id.clock); + mBattery = findViewById(R.id.battery); + + mConfigSliding = (SlidingLayout) findViewById(R.id.config_sliding); + mCurrentPage = (TextView) mConfigSliding.findViewById(R.id.current_page); + mPageSum = (TextView) mConfigSliding.findViewById(R.id.page_sum); + mPageSeeker = (SeekBar) mConfigSliding.findViewById(R.id.page_seeker); + mRefresh = (TextView) mConfigSliding.findViewById(R.id.refresh); + mSend = (TextView) mConfigSliding.findViewById(R.id.send); + mMoreSettings = (TextView) mConfigSliding.findViewById(R.id.more_settings); + mReadingDirection = (Spinner) mConfigSliding.findViewById(R.id.reading_direction); + mPageScaling = (Spinner) mConfigSliding.findViewById(R.id.page_scaling); + mStartPosition = (Spinner) mConfigSliding.findViewById(R.id.start_position); + mKeepScreenOn = (CheckBox) mConfigSliding.findViewById(R.id.keep_screen_on); + mShowClock = (CheckBox) mConfigSliding.findViewById(R.id.gallery_show_clock); + mShowBattery = (CheckBox) mConfigSliding.findViewById(R.id.gallery_show_battery); + mCustomLightness = (CheckBox) mConfigSliding.findViewById(R.id.custom_lightness); + mCustomLightnessValue = (SeekBar) mConfigSliding.findViewById(R.id.custom_lightness_value); + mCustomCodec = (CheckBox) mConfigSliding.findViewById(R.id.custom_codec); + mDecodeFormat = (Spinner) mConfigSliding.findViewById(R.id.decode_format); + + mConfigSliding.hide(); + + mSendDialog = createSendDialog(); + + Intent intent = getIntent(); + mGi = (GalleryInfo) intent.getParcelableExtra(KEY_GALLERY_INFO); + + if (mGi == null) { + // Get error force finish + new MaterialAlertDialog.Builder(this).setTitle(R.string.error) + .setMessage(R.string.ga_data_error) + .setPositiveButton(R.string.close) + .setButtonListener(new MaterialAlertDialog.OnClickListener() { + @Override + public boolean onClick(MaterialAlertDialog dialog, int which) { + finish(); + return true; + } + }).show(); + } else { + + Data.getInstance().addHistory(mGi, Data.READ); + + int startIndex = intent.getIntExtra(KEY_START_INDEX, 0); + if (startIndex == -1) + startIndex = ExDownloader.readCurReadIndex(mGi.gid, mGi.title); + + mImageSet = new ImageSet(mGi.gid, mGi.token, mGi.title, startIndex); + mGalleryView = new GalleryView(getApplicationContext(), mImageSet, startIndex); + + GLRootView glrv= (GLRootView)findViewById(R.id.gl_root_view); + glrv.setContentPane(mGalleryView); + + // Set listener + mConfigSliding.setOnChildHideListener(this); + mGalleryView.setGalleryViewListener(this); + mPageSeeker.setOnSeekBarChangeListener(this); + mRefresh.setOnClickListener(this); + mSend.setOnClickListener(this); + mMoreSettings.setOnClickListener(this); + mReadingDirection.setOnItemSelectedListener(this); + mPageScaling.setOnItemSelectedListener(this); + mStartPosition.setOnItemSelectedListener(this); + mDecodeFormat.setOnItemSelectedListener(this); + mKeepScreenOn.setOnCheckedChangeListener(this); + mShowClock.setOnCheckedChangeListener(this); + mShowBattery.setOnCheckedChangeListener(this); + mCustomLightness.setOnCheckedChangeListener(this); + mCustomLightnessValue.setOnSeekBarChangeListener(this); + mCustomCodec.setOnCheckedChangeListener(this); + + mKeepScreenOn = (CheckBox) mConfigSliding.findViewById(R.id.keep_screen_on); + mShowClock = (CheckBox) mConfigSliding.findViewById(R.id.gallery_show_clock); + mShowBattery = (CheckBox) mConfigSliding.findViewById(R.id.gallery_show_battery); + mCustomCodec = (CheckBox) mConfigSliding.findViewById(R.id.custom_codec); + + mPageSeeker.setMax(mGalleryView.getSize() - 1); + mPageSeeker.setProgress(startIndex); + + mCurrentPage.setText(Integer.toString(startIndex + 1)); + mPageSum.setText(Integer.toString(mGalleryView.getSize())); + + onRightToLeftChanged(mGalleryView.isRightToLeft()); + + // Show first tip + String keyGalleryFirst = "gallery_first"; + if (Config.getBoolean(keyGalleryFirst, true)) { + Config.setBoolean(keyGalleryFirst, false); + new MaterialAlertDialog.Builder(this).setDrakTheme(true).setTitle(R.string.tip) + .setMessage(R.string.gallery_tip) + .setNegativeButton(android.R.string.cancel) + .setButtonListener(new MaterialAlertDialog.OnClickListener() { + @Override + public boolean onClick(MaterialAlertDialog dialog, int which) { + mFullScreenHelper.setFullScreen(true); + return true; + } + }).show(); + } + + // Update state + updateState(); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + if (mImageSet != null) + mImageSet.free(); + if (mGalleryView != null) + mGalleryView.free(); + } + + @Override + public void onBackPressed() { + if (mConfigSliding.isInHideAnimate()) + return; + + if (mConfigSliding.isShowing()) + mConfigSliding.toBaseLevel(); + else + finish(); + } + + @Override + public void onClick(View v) { + if (v == mRefresh) { + mImageSet.redownload(mGalleryView.getCurIndex()); + } else if (v == mSend) { + mCurImageFile = mImageSet.getImageFile(mGalleryView.getCurIndex()); + if (mCurImageFile == null || !mCurImageFile.exists()) + MaterialToast.showToast(R.string.current_image_not_downloaded); + else + mSendDialog.show(); + + } else if (v == mMoreSettings) { + Intent intent = new Intent(GalleryActivity.this, SettingsActivity.class); + startActivity(intent); + } + } + + @Override + public void onTapCenter() { + if (!mConfigSliding.isShowing()) { + if (mFullScreenHelper.willHideNavBar()) + cancelFullScreenTask(); + mFullScreenHelper.setFullScreen(false); + + boolean firstTime = Config.getBoolean(KEY_FIRST_READ_CONFIG_V1, DEFAULT_FIRST_READ_CONFIG_V1); + mConfigSliding.toReservedLevel(firstTime); + if (firstTime) + Config.setBoolean(KEY_FIRST_READ_CONFIG_V1, false); + } + } + + @Override + public void onPageChanged(int index) { + mPageSeeker.setProgress(index); + mCurrentPage.setText(Integer.toString(index + 1)); + } + + @Override + public void onSizeUpdate(int size) { + mPageSeeker.setMax(size - 1); + mPageSeeker.setProgress(mGalleryView.getCurIndex() + 1); + mCurrentPage.setText(Integer.toString(mGalleryView.getCurIndex() + 1)); + mPageSum.setText(Integer.toString(size)); + } + + @Override + public void onSlideBottom(float dx) { + // Empty + } + + @Override + public void onSlideBottomOver() { + // Empty + } + + @Override + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public void onRightToLeftChanged(boolean value) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + int nevLd = value ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR; + if (mPageSeeker.getLayoutDirection() != nevLd) { + mPageSeeker.setLayoutDirection(nevLd); + } + } + } + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, + boolean fromUser) { + if (seekBar == mPageSeeker) { + mCurrentPage.setText(Integer.toString(progress + 1)); + } else if (seekBar == mCustomLightnessValue) { + setScreenLightness(mCustomLightness.isChecked(), progress); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + // Empty + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + if (seekBar == mPageSeeker) { + mGalleryView.goToPage(seekBar.getProgress()); + } else if (seekBar == mCustomLightnessValue) { + Config.setCustomLightnessValue(seekBar.getProgress()); + } + } + + @Override + public void onChildHide() { + if (mFullScreenHelper.willHideNavBar()) + startFullScreenTask(); + else + mFullScreenHelper.setFullScreen(true); + } + + @Override + public void onItemSelected(AdapterView parent, View view, int position, + long id) { + if (parent == mReadingDirection) { + Config.setReadingDirection(mReadingDirection.getSelectedItemPosition()); + mGalleryView.setReadingDirection(position); + } else if (parent == mPageScaling) { + Config.setPageScaling(mPageScaling.getSelectedItemPosition()); + mGalleryView.setPageScaling(position); + } else if (parent == mStartPosition) { + Config.setStartPosition(mStartPosition.getSelectedItemPosition()); + mGalleryView.setStartPosition(position); + } else if (parent == mDecodeFormat) { + Config.setDecodeFormatFromIndex(mDecodeFormat.getSelectedItemPosition()); + } + } + + @Override + public void onNothingSelected(AdapterView parent) { + // Empty + } + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (buttonView == mShowClock) { + Config.setShowClock(isChecked); + mClock.setVisibility(isChecked ? View.VISIBLE : View.GONE); + } else if (buttonView == mShowBattery) { + Config.setShowBattery(isChecked); + mBattery.setVisibility(isChecked ? View.VISIBLE : View.GONE); + } else if (buttonView == mKeepScreenOn) { + Config.setKeepSreenOn(isChecked); + if (isChecked) + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + else + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } else if (buttonView == mCustomLightness) { + Config.setCustomLightness(isChecked); + if (isChecked) + mCustomLightnessValue.setEnabled(true); + else + mCustomLightnessValue.setEnabled(false); + setScreenLightness(isChecked, mCustomLightnessValue.getProgress()); + } else if (buttonView == mCustomCodec) { + Config.setCustomCodec(isChecked); + if (Utils.SUPPORT_IMAGE && mCustomCodec.isChecked()) + mDecodeFormat.setEnabled(true); + else + mDecodeFormat.setEnabled(false); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/ui/GalleryDetailActivity.java b/app/src/main/java/com/hippo/ehviewer/ui/GalleryDetailActivity.java new file mode 100644 index 000000000..2c484e7e6 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ui/GalleryDetailActivity.java @@ -0,0 +1,1310 @@ +/* + * 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.ui; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.app.AlertDialog; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.graphics.Color; +import android.graphics.Rect; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Bundle; +import android.support.v7.widget.LinearLayoutManager; +import android.text.Html; +import android.view.ContextThemeWrapper; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextSwitcher; +import android.widget.TextView; +import android.widget.ViewSwitcher; + +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; +import com.hippo.ehviewer.data.DownloadInfo; +import com.hippo.ehviewer.data.GalleryDetail; +import com.hippo.ehviewer.data.GalleryInfo; +import com.hippo.ehviewer.data.ListUrls; +import com.hippo.ehviewer.data.LofiDetailImpl; +import com.hippo.ehviewer.data.LofiGalleryDetail; +import com.hippo.ehviewer.data.LofiGalleryInfo; +import com.hippo.ehviewer.data.PreviewImpl; +import com.hippo.ehviewer.data.PreviewList; +import com.hippo.ehviewer.drawable.MaterialIndicatorDrawable; +import com.hippo.ehviewer.drawable.MaterialIndicatorDrawable.Stroke; +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.service.DownloadService; +import com.hippo.ehviewer.util.Config; +import com.hippo.ehviewer.util.Constants; +import com.hippo.ehviewer.util.Favorite; +import com.hippo.ehviewer.util.Ui; +import com.hippo.ehviewer.util.ViewUtils; +import com.hippo.ehviewer.widget.AutoWrapLayout; +import com.hippo.ehviewer.widget.FitWindowView; +import com.hippo.ehviewer.widget.FloatingActionButton; +import com.hippo.ehviewer.widget.LinkifyTextView; +import com.hippo.ehviewer.widget.LoadImageView; +import com.hippo.ehviewer.widget.MaterialProgress; +import com.hippo.ehviewer.widget.MaterialToast; +import com.hippo.ehviewer.widget.ProgressiveRatingBar; +import com.hippo.ehviewer.widget.RefreshTextView; +import com.hippo.ehviewer.widget.ResponedScrollView; +import com.hippo.ehviewer.widget.SimpleGridLayout; +import com.hippo.ehviewer.widget.SuperButton; +import com.hippo.ehviewer.widget.recyclerview.EasyRecyclerView; +import com.hippo.ehviewer.widget.recyclerview.LinearDividerItemDecoration; +import com.hippo.ehviewer.windowsanimate.WindowsAnimate; + +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map.Entry; + +public class GalleryDetailActivity extends AbsTranslucentActivity + implements View.OnClickListener, + ResponedScrollView.OnScrollStateChangedListener, + View.OnTouchListener , ViewSwitcher.ViewFactory, + ProgressiveRatingBar.OnUserRateListener, PreviewList.PreviewHolder, + View.OnLayoutChangeListener, EasyRecyclerView.OnItemClickListener, + EasyRecyclerView.OnItemLongClickListener, RefreshTextView.OnRefreshListener, + FitWindowView.OnFitSystemWindowsListener { + + @SuppressWarnings("unused") + private static final String TAG = GalleryDetailActivity.class.getSimpleName(); + + public static final String KEY_G_INFO = "gallery_info"; + + private AppContext mAppContext; + private Resources mResources; + private EhClient mClient; + private WindowsAnimate mWindowsAnimate; + private GalleryInfo mGalleryInfo; + + private int mRunningAnimateNum = 0; + private boolean isCheckmark = false; + private boolean isSetDrawable = false; + + private FitWindowView mStandard; + private RefreshTextView mRefreshText; + private LoadImageView mThumb; + private TextView mTitle; + private TextView mUploader; + private SuperButton mDownloadButton; + private SuperButton mReadButton; + private TextView mCategory; + private TextView mMoreOfUploader; + private TextView mSimilar; + private TextSwitcher mRatingText; + private ProgressiveRatingBar mRating; + private TextView mFavorite; + private TextView mRate; + private TextView mCommentMoreText; + private SimpleGridLayout mPreview; + private SuperButton mPreviewPage; + private SuperButton mPreviewBack; + private SuperButton mPreviewFront; + private MaterialProgress mPreviewWait; + private SuperButton mPreviewRefresh; + + private ResponedScrollView mDetailScroll; + private ScrollView mMoreDetailScroll; + private View mMoreComment; + + private View mDetailHeader; + private View mDetailButtons; + private TextView mWarning; + private View mDetailActions; + private View mDividerAM; + private View mDetailMore; + private View mDividerMR; + private View mDetailRate; + private View mDividerRT; + private LinearLayout mDetailTag; + private View mDividerTC; + private LinearLayout mDetailComment; + private View mDividerCP; + private LinearLayout mDetailPreview; + + private EasyRecyclerView mCommentList; + private FloatingActionButton mReply; + + private OvalDrawable mCategoryDrawable; + private Drawable mRateDrawable; + private Drawable mCheckmarkDrawable; + + private CommentAdapter mCommentAdapter; + + private int mPaddingTop = -1; + private int mThemeColor; + private int mCurPreviewPage = 0; + private boolean mShowPreview = false; + private boolean mGetPreview = false; + + private String mOldReply; + private boolean mReplyPositive; + + private AlertDialog createGoToDialog() { + CharSequence[] items = new CharSequence[((PreviewImpl)mGalleryInfo).getPreviewPageNum()]; + for (int i = 0; i < items.length; i++) + items[i] = Integer.toString(i + 1); + + // TODO too slow + + return new MaterialAlertDialog.Builder(this).setTitle(R.string.jump) + .setNegativeButton(android.R.string.cancel) + .setItems(items, new MaterialAlertDialog.OnClickListener() { + @Override + public boolean onClick(MaterialAlertDialog dialog, int position) { + if (position != mCurPreviewPage) { + mCurPreviewPage = position; + refreshPreview(); + } + return true; + } + }).create(); + } + + private GalleryInfo handleIntent(Intent intent) { + GalleryInfo gi = null; + if (intent.getAction() == "android.intent.action.VIEW") { + // Start from url + DetailUrlParser parser = new DetailUrlParser(); + if (parser.parser(intent.getData().getPath())) { + if (Config.getMode() == EhClient.MODE_LOFI) { + // We can't get info from lofi detail, we need get info from api + gi = new ApiGalleryDetail(); + gi.gid = parser.gid; + gi.token = parser.token; + } else { + gi = new GalleryDetail(); + gi.gid = parser.gid; + gi.token = parser.token; + } + } else { + // If can not parser url, just keep it null + } + } else { + // + gi = (GalleryInfo)(intent.getParcelableExtra(KEY_G_INFO)); + if (gi != null) { + // Add history + Data.getInstance().addHistory(gi, Data.BROWSE); + + if (gi instanceof LofiGalleryInfo) { + if (Config.getMode() == EhClient.MODE_LOFI) { + gi = new LofiGalleryDetail((LofiGalleryInfo)gi); + } else { + gi = new GalleryDetail(gi); + } + } else { + if (Config.getMode() == EhClient.MODE_LOFI) { + gi = new LofiGalleryDetail(gi); // TAG is null + } else { + gi = new GalleryDetail(gi); + } + } + } + } + return gi; + } + + /* + @Override + protected void onNewIntent(Intent intent) { + if (mGalleryInfo != null) + mGiStack.push(mGalleryInfo); + setIntent(intent); + mGalleryInfo = handleIntent(intent); + + if (mGalleryInfo == null) { + // TODO need a toast ? + if (mGiStack.isEmpty()) + finish(); + else + mGalleryInfo = mGiStack.pop(); + } else { + doPreLayout(); + } + }*/ + + @Override + protected void onDestroy() { + super.onDestroy(); + mWindowsAnimate.free(); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mGalleryInfo = handleIntent(getIntent()); + if (mGalleryInfo == null) + // If no GalleryInfo pass, just finish + finish(); + + setContentView(R.layout.gallery_detail); + setTitle(String.valueOf(mGalleryInfo.gid)); + + mAppContext = (AppContext) getApplication(); + mClient = EhClient.getInstance(); + mWindowsAnimate = new WindowsAnimate(); + mWindowsAnimate.init(this); + + mResources = getResources(); + + // Menu + MaterialIndicatorDrawable materialIndicator = new MaterialIndicatorDrawable(this, Color.WHITE, Stroke.THIN); + materialIndicator.setIconState(MaterialIndicatorDrawable.IconState.ARROW); + Ui.setMaterialIndicator(getActionBar(), materialIndicator); + + // Get view + mStandard = (FitWindowView) findViewById(R.id.standard); + + mDetailScroll = (ResponedScrollView) findViewById(R.id.detail_scroll); + mMoreDetailScroll = (ScrollView) findViewById(R.id.more_detail_scroll); + mMoreComment = findViewById(R.id.more_comment); + mRefreshText = (RefreshTextView) findViewById(R.id.refresh_text); + + mDetailHeader = findViewById(R.id.detail_header); + mDetailButtons = findViewById(R.id.detail_buttons); + mWarning = (TextView) findViewById(R.id.warning); + mDetailActions = findViewById(R.id.detail_actions); + mDividerAM = findViewById(R.id.detail_divider_a_m); + mDetailMore = findViewById(R.id.detail_more); + mDividerMR = findViewById(R.id.detail_divider_m_r); + mDetailRate = findViewById(R.id.detail_rate); + mDividerRT = findViewById(R.id.detail_divider_r_t); + mDetailTag = (LinearLayout) findViewById(R.id.detail_tag); + mDividerTC = findViewById(R.id.detail_divider_t_c); + mDetailComment = (LinearLayout) findViewById(R.id.detail_comment); + mDividerCP = findViewById(R.id.detail_divider_c_p); + mDetailPreview = (LinearLayout) findViewById(R.id.detail_preview); + + mThumb = (LoadImageView) findViewById(R.id.thumb); + mTitle = (TextView) findViewById(R.id.title); + mUploader = (TextView) findViewById(R.id.uploader); + mDownloadButton = (SuperButton) findViewById(R.id.download); + mReadButton = (SuperButton) findViewById(R.id.read); + mCategory = (TextView) findViewById(R.id.category); + mMoreOfUploader = (TextView) findViewById(R.id.more_of_uploader); + mSimilar = (TextView) findViewById(R.id.similar); + mRatingText = (TextSwitcher) mDetailRate.findViewById(R.id.rating_text); + mRating = (ProgressiveRatingBar) mDetailRate.findViewById(R.id.rating); + mFavorite = (TextView) findViewById(R.id.favorite); + mRate = (TextView) findViewById(R.id.rate); + mCommentMoreText = (TextView) findViewById(R.id.comment_more_text); + mPreview = (SimpleGridLayout) findViewById(R.id.preview); + mPreviewPage = (SuperButton) findViewById(R.id.preview_num); + mPreviewBack = (SuperButton) findViewById(R.id.back); + mPreviewFront = (SuperButton) findViewById(R.id.front); + mPreviewWait = (MaterialProgress) findViewById(R.id.preview_wait); + mPreviewRefresh = (SuperButton) findViewById(R.id.preview_refresh); + + mCommentList = (EasyRecyclerView) findViewById(R.id.comment_list); + mReply = (FloatingActionButton) findViewById(R.id.reply); + + // Set Drawable + Rect rect = new Rect(0, 0, Ui.dp2pix(48), Ui.dp2pix(48)); + mCategoryDrawable = new OvalDrawable(0); + mCategoryDrawable.setBounds(0, 0, Ui.dp2pix(48), Ui.dp2pix(48)); + mCategory.setCompoundDrawables(null, mCategoryDrawable, null, null); + Drawable moreOfUploaderDrawable = mResources.getDrawable(R.drawable.ic_more_of_uploader); + moreOfUploaderDrawable.setBounds(rect); + mMoreOfUploader.setCompoundDrawables(null, moreOfUploaderDrawable, null, null); + Drawable similarDrawable = mResources.getDrawable(R.drawable.ic_similar); + similarDrawable.setBounds(rect); + mSimilar.setCompoundDrawables(null, similarDrawable, null, null); + Drawable favoriteDrawable = mResources.getDrawable(R.drawable.ic_favorite); + favoriteDrawable.setBounds(rect); + mFavorite.setCompoundDrawables(null, favoriteDrawable, null, null); + mRateDrawable = mResources.getDrawable(R.drawable.ic_rate); + mRateDrawable.setBounds(rect); + mCheckmarkDrawable = mResources.getDrawable(R.drawable.ic_checkmark); + mCheckmarkDrawable.setBounds(rect); + mRate.setCompoundDrawables(null, mRateDrawable, null, null); + + // Init + mRatingText.setFactory(this); + mPreview.setColumnCountPortrait( + Config.getPreviewColumnsPortrait()); + mPreview.setColumnCountLandscape( + Config.getPreviewColumnsLandscape()); + + // Comment + mCommentList.setLayoutManager(new LinearLayoutManager(this)); + + // Set theme color + setThemeColor(mThemeColor = Config.getCustomThemeColor() ? Config.getThemeColor() : + (mGalleryInfo.title == null ? Ui.THEME_COLOR : Ui.getCategoryColor(mGalleryInfo.category))); + + // Set ripple + /* + mWindowsAnimate.addRippleEffect(mDownloadButton, true); + mWindowsAnimate.addRippleEffect(mFavorite, false); + mWindowsAnimate.addRippleEffect(mRate, false); + mWindowsAnimate.addRippleEffect(mPreviewPage, true); + mWindowsAnimate.addRippleEffect(mPreviewBack, true); + mWindowsAnimate.addRippleEffect(mPreviewFront, true); + */ + + // Set listener + mStandard.addOnFitSystemWindowsListener(this); + mDownloadButton.setOnClickListener(this); + mReadButton.setOnClickListener(this); + mCategory.setOnClickListener(this); + mMoreOfUploader.setOnClickListener(this); + mSimilar.setOnClickListener(this); + mRating.setOnUserRateListener(this); + mFavorite.setOnClickListener(this); + mRate.setOnClickListener(this); + mDetailMore.setOnTouchListener(this); + mDetailComment.setOnTouchListener(this); + mMoreDetailScroll.addOnLayoutChangeListener(this); + mDetailScroll.addOnLayoutChangeListener(this); + mDetailScroll.setOnScrollStateChangedListener(this); + mCommentList.addOnLayoutChangeListener(this); + mPreviewPage.setOnClickListener(this); + mPreviewBack.setOnClickListener(this); + mPreviewFront.setOnClickListener(this); + mPreviewRefresh.setOnClickListener(this); + mReply.setOnClickListener(this); + mRefreshText.setDefaultRefresh(R.string.click_retry, this); + + doPreLayout(); + } + + private void setThemeColor(int themeColor) { + mThemeColor = themeColor; + getActionBar().setBackgroundDrawable(new ColorDrawable(mThemeColor)); + mDownloadButton.setRoundBackground(true, false, mResources.getColor(R.color.background_light), mThemeColor); + mDownloadButton.setTextColor(mThemeColor); + mReadButton.setRoundBackground(true, mThemeColor, 0); + ((TextView)findViewById(R.id.detail_more_text)).setTextColor(mThemeColor); + mCommentMoreText.setTextColor(mThemeColor); + mPreviewPage.setRoundBackground(true, false, mResources.getColor(R.color.background_light), mThemeColor); + mPreviewPage.setTextColor(mThemeColor); + mPreviewBack.setRoundBackground(true, false, mResources.getColor(R.color.background_light), mThemeColor); + mPreviewBack.setTextColor(mThemeColor); + mPreviewFront.setRoundBackground(true, false, mResources.getColor(R.color.background_light), mThemeColor); + mPreviewFront.setTextColor(mThemeColor); + mPreviewWait.setColor(mThemeColor); + mPreviewRefresh.setRoundBackground(true, mThemeColor, 0); + mReply.setColor(mThemeColor); + Ui.colorStatusBarL(this, mThemeColor); + if (mPaddingTop != -1) + Ui.colorStatusBarKK(this, mThemeColor, mPaddingTop - Ui.ACTION_BAR_HEIGHT); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle item selection + switch (item.getItemId()) { + case android.R.id.home: + onBackPressed(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void getDetailInfo() { + String detailUrl = mClient.getDetailUrl( + mGalleryInfo.gid, mGalleryInfo.token); + if (mGalleryInfo instanceof GalleryDetail) + mClient.getGDetail(detailUrl, (GalleryDetail)mGalleryInfo, new GDetailGetListener()); + else if (mGalleryInfo instanceof LofiGalleryDetail) + mClient.getLGDetail(detailUrl, (LofiGalleryDetail)mGalleryInfo, new GLDetailGetListener()); + else if (mGalleryInfo instanceof ApiGalleryDetail) + ; // TODO + } + + private void doPreLayout() { + mRefreshText.setRefreshing(true); + + if (mGalleryInfo.title != null) { + // Not from url + mDetailScroll.setVisibility(View.VISIBLE); + mDetailHeader.setVisibility(View.VISIBLE); + + mThumb.setLoadInfo(mGalleryInfo.thumb, String.valueOf(mGalleryInfo.gid)); + ImageLoader.getInstance(this).add(mGalleryInfo.thumb, String.valueOf(mGalleryInfo.gid), + new LoadImageView.SimpleImageGetListener(mThumb).setTransitabled(false)); + mTitle.setText(mGalleryInfo.title); + mUploader.setText(mGalleryInfo.uploader); + } + + getDetailInfo(); + } + + private String getRatingText(float rating) { + if (rating == Float.NaN) + return "(´_ゝ`)"; + + int resId = 0; + switch (Math.round(rating * 2)) { + case 0: + resId = R.string.rating0; break; + case 1: + resId = R.string.rating1; break; + case 2: + resId = R.string.rating2; break; + case 3: + resId = R.string.rating3; break; + case 4: + resId = R.string.rating4; break; + case 5: + resId = R.string.rating5; break; + case 6: + resId = R.string.rating6; break; + case 7: + resId = R.string.rating7; break; + case 8: + resId = R.string.rating8; break; + case 9: + resId = R.string.rating9; break; + case 10: + resId = R.string.rating10; break; + } + + if (resId == 0) + return "(´_ゝ`)"; + else + return getString(resId); + } + + private String getAllRatingText(float rating, int people) { + StringBuilder sb = new StringBuilder() + .append(String.format(getString(R.string.we_feel), getRatingText(rating))) + .append(" (").append(rating); + if (people != -1) + sb.append(", ").append(people).append(")").toString(); + else + sb.append(")"); + return sb.toString(); + } + + private void addTag(LinkedHashMap> tags) { + int x = Ui.dp2pix(2); + int y = Ui.dp2pix(4); + ContextThemeWrapper ctw = new ContextThemeWrapper(this, R.style.TextTag); + for (Entry> tagGroup : tags.entrySet()) { + + LinearLayout tagGroupLayout = new LinearLayout(this); + tagGroupLayout.setOrientation(LinearLayout.HORIZONTAL); + AutoWrapLayout tagLayout = new AutoWrapLayout(this); + + // Group name + final String groupName = tagGroup.getKey(); + TextView groupNameView = new TextView(ctw); + groupNameView.setText(groupName); + LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + lp.setMargins(x, y, x, y); + tagGroupLayout.addView(groupNameView, lp); + + for (final String tag : tagGroup.getValue()) { + TextView tagView = new TextView(ctw); + tagView.setText(tag); + tagView.setBackgroundColor(mThemeColor); + tagView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(GalleryDetailActivity.this, + GalleryListActivity.class); + intent.setAction(GalleryListActivity.ACTION_GALLERY_LIST); + intent.putExtra(GalleryListActivity.KEY_MODE, + ListUrls.MODE_TAG); + intent.putExtra(GalleryListActivity.KEY_TAG, groupName + ":" + tag); + intent.setAction(GalleryListActivity.ACTION_GALLERY_LIST); + startActivity(intent); + } + }); + AutoWrapLayout.LayoutParams alp = new AutoWrapLayout.LayoutParams(); + alp.setMargins(x, y, x, y); + tagLayout.addView(tagView, alp); + } + tagGroupLayout.addView(tagLayout); + mDetailTag.addView(tagGroupLayout); + } + } + + private void addLofiTag(String[] tags) { + int x = Ui.dp2pix(2); + int y = Ui.dp2pix(4); + ContextThemeWrapper ctw = new ContextThemeWrapper(this, R.style.TextTag); + AutoWrapLayout tagLayout = new AutoWrapLayout(this); + for (String tag : tags) { + TextView tagView = new TextView(ctw); + tagView.setText(tag); + tagView.setBackgroundColor(mThemeColor); + AutoWrapLayout.LayoutParams alp = new AutoWrapLayout.LayoutParams(); + alp.setMargins(x, y, x, y); + tagLayout.addView(tagView, alp); + } + mDetailTag.addView(tagLayout); + } + + private String getPreviewPageNum(int pageNum) { + return pageNum == Integer.MAX_VALUE ? "??" : String.valueOf(pageNum); + } + + private void updateDownloadButton() { + int gid = mGalleryInfo.gid; + DownloadInfo di = Data.getInstance().getDownload(gid); + if (di == null) { + mDownloadButton.setText(R.string.download); + } else { + mDownloadButton.setText(R.string.downloaded); + } + } + + /** + * GalleryDetail + * LofiGalleryDetail + * ApiGalleryDetail + */ + private void doLayout() { + if (mDetailScroll.getVisibility() == View.GONE) { + // If not set header in doPreLayout + // Set theme + setThemeColor(Ui.getCategoryColor(mGalleryInfo.category)); + // Add history first + Data.getInstance().addHistory(mGalleryInfo, Data.BROWSE); + mDetailScroll.setVisibility(View.VISIBLE); + mDetailHeader.setVisibility(View.VISIBLE); + mThumb.setLoadInfo(mGalleryInfo.thumb, String.valueOf(mGalleryInfo.gid)); + ImageLoader.getInstance(this).add(mGalleryInfo.thumb, String.valueOf(mGalleryInfo.gid), + new LoadImageView.SimpleImageGetListener(mThumb)); + mTitle.setText(mGalleryInfo.title); + mUploader.setText(mGalleryInfo.uploader); + } + + // Button + mDetailButtons.setVisibility(View.VISIBLE); + updateDownloadButton(); + + // waring + mWarning.setVisibility(View.VISIBLE); + + // Action + mDetailActions.setVisibility(View.VISIBLE); + mCategory.setText(Ui.getCategoryText(mGalleryInfo.category)); + mCategoryDrawable.setColor(Ui.getCategoryColor(mGalleryInfo.category)); + + // Detail + mDividerAM.setVisibility(View.VISIBLE); + mDetailMore.setVisibility(View.VISIBLE); + if (mGalleryInfo instanceof GalleryDetail) { + GalleryDetail galleryDetail = (GalleryDetail)mGalleryInfo; + ((TextView)mDetailMore.findViewById(R.id.language)).setText( + getString(R.string.language) + ": " + galleryDetail.language); + ((TextView)mDetailMore.findViewById(R.id.posted)).setText(galleryDetail.posted); + ((TextView)mDetailMore.findViewById(R.id.pages)).setText( + getString(R.string.pages) + ": " + String.valueOf(galleryDetail.pages)); + ((TextView)mDetailMore.findViewById(R.id.size)).setText( + getString(R.string.size) + ": " + galleryDetail.size); + StringBuilder sb = new StringBuilder(); + sb.append("gid: ").append(galleryDetail.gid).append("\n\n") + .append("token: ").append(galleryDetail.token).append("\n\n") + .append("title: ").append(galleryDetail.title).append("\n\n") + .append("title_jpn: ").append(galleryDetail.title_jpn).append("\n\n") + .append("thumb: ").append(galleryDetail.thumb).append("\n\n") + .append("category: ").append(Ui.getCategoryText(galleryDetail.category)).append("\n\n") + .append("uploader: ").append(galleryDetail.uploader).append("\n\n") + .append("posted: ").append(galleryDetail.posted).append("\n\n") + .append("pages: ").append(galleryDetail.pages).append("\n\n") + .append("size: ").append(galleryDetail.size).append("\n\n") + .append("resized: ").append(galleryDetail.resized).append("\n\n") + .append("parent: ").append(galleryDetail.parent).append("\n\n") + .append("visible: ").append(galleryDetail.visible).append("\n\n") + .append("language: ").append(galleryDetail.language).append("\n\n") + .append("people: ").append(galleryDetail.people).append("\n\n") + .append("rating: ").append(galleryDetail.rating); + ((TextView)mMoreDetailScroll.findViewById(R.id.more_detail)).setText(sb.toString()); + } else if (mGalleryInfo instanceof LofiGalleryDetail) { + LofiGalleryDetail lofiGalleryDetail = (LofiGalleryDetail)mGalleryInfo; + ((TextView)mDetailMore.findViewById(R.id.language)).setVisibility(View.GONE); + ((TextView)mDetailMore.findViewById(R.id.posted)).setVisibility(View.GONE); + ((TextView)mDetailMore.findViewById(R.id.pages)).setVisibility(View.GONE); + ((TextView)mDetailMore.findViewById(R.id.size)).setText(lofiGalleryDetail.posted); + StringBuilder sb = new StringBuilder(); + sb.append("gid: ").append(lofiGalleryDetail.gid).append("\n\n") + .append("token: ").append(lofiGalleryDetail.token).append("\n\n") + .append("title: ").append(lofiGalleryDetail.title).append("\n\n") + .append("thumb: ").append(lofiGalleryDetail.thumb).append("\n\n") + .append("category: ").append(Ui.getCategoryText(lofiGalleryDetail.category)).append("\n\n") + .append("uploader: ").append(lofiGalleryDetail.uploader).append("\n\n") + .append("posted: ").append(lofiGalleryDetail.posted).append("\n\n") + .append("simpleLanguage: ").append(lofiGalleryDetail.simpleLanguage).append("\n\n") + .append("rating: ").append(lofiGalleryDetail.rating).append("\n\n"); + ((TextView)mMoreDetailScroll.findViewById(R.id.more_detail)).setText(sb.toString()); + } else if (mGalleryInfo instanceof ApiGalleryDetail) { + // TODO + } + + // Rate + mDividerMR.setVisibility(View.VISIBLE); + mDetailRate.setVisibility(View.VISIBLE); + int people = mGalleryInfo instanceof GalleryDetail ? ((GalleryDetail)mGalleryInfo).people : -1; + mRatingText.setCurrentText(getAllRatingText(mGalleryInfo.rating, people)); + mRating.setRating(mGalleryInfo.rating); + mRating.setEnableRate(false); + + // Tag + if (mGalleryInfo instanceof GalleryDetail) { + GalleryDetail galleryDetail = (GalleryDetail)mGalleryInfo; + if (galleryDetail.tags != null && galleryDetail.tags.size() > 0) { + mDividerRT.setVisibility(View.VISIBLE); + mDetailTag.setVisibility(View.VISIBLE); + addTag(galleryDetail.tags); + } + } else if (mGalleryInfo instanceof LofiDetailImpl) { + LofiDetailImpl lofiDetailImpl = (LofiDetailImpl)mGalleryInfo; + String[] tags = lofiDetailImpl.getTags(); + if (tags != null && tags.length > 0) { + mDividerRT.setVisibility(View.VISIBLE); + mDetailTag.setVisibility(View.VISIBLE); + addLofiTag(tags); + } + } + + // Comment + GalleryDetail galleryDetail; + if (mGalleryInfo instanceof GalleryDetail && + (galleryDetail = (GalleryDetail)mGalleryInfo) != null && + galleryDetail.comments != null) { + mDividerTC.setVisibility(View.VISIBLE); + mDetailComment.setVisibility(View.VISIBLE); + + int commentNum = galleryDetail.comments.size(); + if (commentNum == 0) + mCommentMoreText.setText(R.string.no_comment); + else if (commentNum <= 2) + mCommentMoreText.setText(R.string.no_more_comment); + else + mCommentMoreText.setText(R.string.click_more); + int maxShown = Math.min(2, commentNum); + for (int i = 0; i < maxShown; i++) { + mDetailComment.addView(getCommentView(null, galleryDetail.comments.get(i), true), + i, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT)); + } + + LinearDividerItemDecoration itemDecorationnew = new LinearDividerItemDecoration( + LinearDividerItemDecoration.VERTICAL, 0x22000000, Ui.dp2pix(1)); // TODO + itemDecorationnew.setShowLastDivider(true); + itemDecorationnew.setPadding(Ui.dp2pix(16)); + mCommentAdapter = new CommentAdapter(); + mCommentList.setHasFixedSize(true); + mCommentList.setAdapter(mCommentAdapter); + mCommentList.addItemDecoration(itemDecorationnew); + mCommentList.setOnItemClickListener(this); + mCommentList.setOnItemLongClickListener(this); + } + + // Add preview + if (mGalleryInfo instanceof PreviewImpl) { + PreviewImpl previewImpl = (PreviewImpl)mGalleryInfo; + PreviewList pl = previewImpl.getPreview(0); + if (pl != null) { + mDividerCP.setVisibility(View.VISIBLE); + mDetailPreview.setVisibility(View.VISIBLE); + + mPreviewPage.setText((mCurPreviewPage + 1) + "/" + + getPreviewPageNum(previewImpl.getPreviewPageNum())); + pl.setData(this, this, previewImpl, mCurPreviewPage); + pl.addPreview(mPreview); + } + } + } + + private void refreshPreview() { + if (!(mGalleryInfo instanceof PreviewImpl)) + return; + + PreviewImpl previewImpl = (PreviewImpl)mGalleryInfo; + mPreview.removeAllViews(); + mPreviewPage.setText((mCurPreviewPage + 1) + "/" + getPreviewPageNum(previewImpl.getPreviewPageNum())); + + PreviewList previewList = previewImpl.getPreview(mCurPreviewPage); + if (previewList == null) { + mPreviewWait.setVisibility(View.VISIBLE); + mPreviewRefresh.setVisibility(View.GONE); + + String url = mClient.getDetailUrl( + mGalleryInfo.gid, mGalleryInfo.token, mCurPreviewPage); + mClient.getPreviewList(url, Config.getMode(), mCurPreviewPage, + new PListGetListener()); + + mGetPreview = true; + } else { + mPreviewWait.setVisibility(View.GONE); + mPreviewRefresh.setVisibility(View.GONE); + previewList.addPreview(mPreview); + + mShowPreview = true; + } + } + + @SuppressLint("InflateParams") + private View getCommentView(View contentView, Comment comment, boolean restrict) { + if (contentView == null) { + contentView = LayoutInflater.from(this).inflate(R.layout.comments_item, null); + if (!restrict) { + RippleSalon.addRipple(contentView, ColorStateList.valueOf(0x40000000)); + //mWindowsAnimate.addRippleEffect(contentView, true); + } + } + ((TextView)contentView.findViewById(R.id.user)).setText(comment.user); + ((TextView)contentView.findViewById(R.id.time)).setText(comment.time); + TextView commentText = (TextView)contentView.findViewById(R.id.comment); + if (restrict) { + commentText.setMaxLines(3); + // Cancel auto link + commentText.setAutoLinkMask(0); + } + commentText.setText(Html.fromHtml(comment.comment)); + return contentView; + } + + @Override + public void onBackPressed() { + if (mRunningAnimateNum != 0 || mWindowsAnimate.isRunningAnimate()) + return; + + if (mMoreDetailScroll.getVisibility() == View.VISIBLE) { + mDetailScroll.setVisibility(View.VISIBLE); + mWindowsAnimate.addMoveExitTransitions(mMoreDetailScroll, null); + } else if (mMoreComment.getVisibility() == View.VISIBLE) { + mDetailScroll.setVisibility(View.VISIBLE); + mWindowsAnimate.addMoveExitTransitions(mMoreComment, null); + } else { + super.onBackPressed(); + } + } + + @Override + public void onRefresh() { + getDetailInfo(); + } + + @Override + @SuppressWarnings("deprecation") + public void onClick(View v) { + if (mRunningAnimateNum != 0 || mWindowsAnimate.isRunningAnimate()) + return; + + if (v == mDownloadButton) { + Intent it = new Intent(GalleryDetailActivity.this, DownloadService.class); + it.setAction(DownloadService.ACTION_ADD); + it.putExtra(DownloadService.KEY_GALLERY_INFO, mGalleryInfo); + startService(it); + mDownloadButton.setText(R.string.downloaded); + MaterialToast.showToast(R.string.toast_add_download); + } else if (v == mReadButton) { + Intent intent = new Intent(this, + GalleryActivity.class); + intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, mGalleryInfo); + intent.putExtra(GalleryActivity.KEY_START_INDEX, -1); + startActivity(intent); + } else if (v == mCategory) { + Intent intent = new Intent(this, GalleryListActivity.class); + intent.setAction(GalleryListActivity.ACTION_GALLERY_LIST); + intent.putExtra(GalleryListActivity.KEY_MODE, ListUrls.MODE_NORMAL); + intent.putExtra(GalleryListActivity.KEY_CATEGORY, mGalleryInfo.category); + startActivity(intent); + } else if (v == mMoreOfUploader) { + Intent intent = new Intent(this, GalleryListActivity.class); + intent.setAction(GalleryListActivity.ACTION_GALLERY_LIST); + intent.putExtra(GalleryListActivity.KEY_MODE, ListUrls.MODE_UPLOADER); + intent.putExtra(GalleryListActivity.KEY_UPLOADER, mGalleryInfo.uploader); + startActivity(intent); + } else if (v == mSimilar) { + Intent intent = new Intent(this, GalleryListActivity.class); + intent.setAction(GalleryListActivity.ACTION_GALLERY_LIST); + intent.putExtra(GalleryListActivity.KEY_MODE, ListUrls.MODE_IMAGE_SEARCH); + intent.putExtra(GalleryListActivity.KEY_IMAGE_KEY, String.valueOf(mGalleryInfo.gid)); + intent.putExtra(GalleryListActivity.KEY_IMAGE_URL, mGalleryInfo.thumb); + startActivity(intent); + } else if (v == mFavorite) { + Favorite.addToFavorite(this, mGalleryInfo); + } else if (v == mRate) { + if (isCheckmark) { + int rating = (int)(mRating.getRating() * 2 + 0.5); + if (rating <= 0 || rating > 10) { + MaterialToast.showToast(R.string.invalid_rating); + return; + } + mClient.rate(mGalleryInfo.gid, mGalleryInfo.token, rating, new EhClient.OnRateListener() { + @Override + public void onSuccess(float ratingAvg, int ratingCnt) { + mGalleryInfo.rating = ratingAvg; + mRatingText.setText(getAllRatingText(mGalleryInfo.rating, ratingCnt)); + mRating.setRating(mGalleryInfo.rating); + } + @Override + public void onFailure(String eMsg) { + mRatingText.setText(getString(R.string.rate_failed)); + MaterialToast.showToast(eMsg); + } + }); + mRatingText.setText(getString(R.string.rate_succeeded)); + mRating.setEnableRate(false); + } else { + mRatingText.setText(getString(R.string.rate_sensibly)); + mRating.setEnableRate(true); + } + + // Animate to change + ValueAnimator animation = ValueAnimator.ofFloat(0.0f, 90.0f, 180.0f); + animation.setDuration(Constants.ANIMATE_TIME); + animation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + float rotationY = (Float)animation.getAnimatedValue(); + if (rotationY > 90) { + rotationY = 180 - rotationY; + if (!isSetDrawable) { + mRate.setCompoundDrawables(null, isCheckmark ? mRateDrawable : mCheckmarkDrawable, null, null); + mRate.setText(isCheckmark ? R.string.rate : android.R.string.ok); + isSetDrawable = true; + isCheckmark = !isCheckmark; + } + } + mRate.setRotationY(rotationY); + } + }); + animation.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) {} + @Override + public void onAnimationRepeat(Animator animation) {} + @Override + public void onAnimationEnd(Animator animation) { + mRunningAnimateNum--; + isSetDrawable = false; + } + @Override + public void onAnimationCancel(Animator animation) {} + }); + animation.start(); + mRunningAnimateNum++; + } else if (v == mReply) { + mReplyPositive = false; + final EditText et = new EditText(this); + et.setGravity(Gravity.TOP); + et.setHint("评论内容"); // TODO + et.setTextColor(getResources().getColor(R.color.secondary_text_dark)); + et.setBackgroundDrawable(null); + et.setText(mOldReply); + if (mOldReply != null) + et.setSelection(mOldReply.length()); + + new MaterialAlertDialog.Builder(this).setTitle(R.string.reply) + .setView(et, true, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)) + .setNegativeButton(android.R.string.cancel) + .setPositiveButton(android.R.string.ok) + .setButtonListener(new MaterialAlertDialog.OnClickListener() { + @Override + public boolean onClick(MaterialAlertDialog dialog, int which) { + if (which == MaterialAlertDialog.POSITIVE) { + mReplyPositive = true; + mClient.comment(EhClient.getDetailUrl(mGalleryInfo.gid, + mGalleryInfo.token, 0, Config.getApiMode()), et.getText().toString(), + new EhClient.OnCommentListener() { + @Override + public void onSuccess(List comments) { + ((GalleryDetail)mGalleryInfo).comments = comments; + mCommentAdapter.notifyDataSetChanged(); + mOldReply = null; + } + @Override + public void onFailure(String eMsg) { + MaterialToast.showToast(eMsg); + mOldReply = et.getText().toString(); + } + }); + } + return true; + } + }).setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + if (mReplyPositive) + mOldReply = null; + else + mOldReply = et.getText().toString(); + } + }).show(); + + } else if (v == mPreviewPage) { + if (mGalleryInfo instanceof PreviewImpl && + ((PreviewImpl)mGalleryInfo).getPreviewPageNum() != Integer.MAX_VALUE && + ((PreviewImpl)mGalleryInfo).getPreviewPageNum() > 1) + createGoToDialog().show(); + } else if (v == mPreviewBack) { + if (!(mGalleryInfo instanceof PreviewImpl) || + (mGalleryInfo instanceof LofiGalleryDetail && mGetPreview)) + return; + + if (mCurPreviewPage <= 0) + return; + mCurPreviewPage--; + refreshPreview(); + } else if (v == mPreviewFront) { + if (!(mGalleryInfo instanceof PreviewImpl) || + (mGalleryInfo instanceof LofiGalleryDetail && mGetPreview)) + return; + + if (mCurPreviewPage >= ((PreviewImpl)mGalleryInfo).getPreviewPageNum() - 1) + return; + mCurPreviewPage++; + refreshPreview(); + } else if (v == mPreviewRefresh) { + refreshPreview(); + } + } + + @Override + public void onLayoutChange(View v, int left, int top, int right, + int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { + if (v == mMoreDetailScroll) { + mMoreDetailScroll.scrollTo(0, 0); + } else if (v == mCommentList) { + //mCommentList.setSelection(0); + } else if (v == mDetailScroll) { + if (mShowPreview) { + mShowPreview = false; + mDetailScroll.smoothScrollTo(0, mDividerCP.getTop()); + } + } + } + + @Override + public void onFitSystemWindows(int l, int t, int r, int b) { + mDetailScroll.setPadding(mDetailScroll.getPaddingLeft(), t, + mDetailScroll.getPaddingRight(), b); + mMoreDetailScroll.setPadding(mMoreDetailScroll.getPaddingLeft(), t, + mMoreDetailScroll.getPaddingRight(), b); + mCommentList.setPadding(mCommentList.getPaddingLeft(), t, + mCommentList.getPaddingRight(), b + Ui.dp2pix(16 + 56 + 16)); + ((FrameLayout.LayoutParams)mReply.getLayoutParams()).bottomMargin = Ui.dp2pix(16) + b; + + mPaddingTop = t; + Ui.colorStatusBarKK(this, mThemeColor, mPaddingTop - Ui.ACTION_BAR_HEIGHT); + } + + @Override + @SuppressLint("ClickableViewAccessibility") + public boolean onTouch(View v, MotionEvent event) { + if (v == mDetailMore) { + if (ViewUtils.isClickAction(event)) { + mWindowsAnimate.addCircleTransitions(mDetailMore, (int)event.getX(), + (int)event.getY(), mResources.getColor(R.color.background_light), + new WindowsAnimate.OnAnimationEndListener() { + @Override + public void onAnimationEnd() { + mDetailScroll.setVisibility(View.GONE); + mMoreDetailScroll.setVisibility(View.VISIBLE); + AlphaAnimation aa = new AlphaAnimation(0.0f,1.0f); + aa.setDuration(Constants.ANIMATE_TIME); + mMoreDetailScroll.startAnimation(aa); + } + }); + } + } else if (v == mDetailComment) { + if (ViewUtils.isClickAction(event)) { + mWindowsAnimate.addCircleTransitions(mDetailComment, (int)event.getX(), + (int)event.getY(), mResources.getColor(R.color.background_light), + new WindowsAnimate.OnAnimationEndListener() { + @Override + public void onAnimationEnd() { + mDetailScroll.setVisibility(View.GONE); + mMoreComment.setVisibility(View.VISIBLE); + // Add reply icon animation + mReply.setVisibility(View.INVISIBLE); + AlphaAnimation aa = new AlphaAnimation(0.0f,1.0f); + aa.setDuration(Constants.ANIMATE_TIME); + aa.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) {} + @Override + public void onAnimationRepeat(Animation animation) {} + @Override + public void onAnimationEnd(Animation animation) { + mWindowsAnimate.addOvershootEnterTransitions(mReply, null); + } + }); + mMoreComment.startAnimation(aa); + } + }); + } + } + return true; + } + + @Override + public void onUserRate(float rating) { + mRatingText.setText(getRatingText(rating)); + } + + @Override + public boolean onItemClick(EasyRecyclerView parent, View view, int position, + long id) { + // Handler url here + LinkifyTextView comment = ((LinkifyTextView) view.findViewById(R.id.comment)); + String url = comment.getTouchedUrl(); + if (url != null) { + comment.clearTouchedUrl(); + Uri uri = Uri.parse(url); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + startActivity(intent); + return true; + } + return false; + } + + @Override + public boolean onItemLongClick(EasyRecyclerView parent, View view, + int position, long id) { + final Comment c = ((GalleryDetail) mGalleryInfo).comments.get(position); + + new MaterialAlertDialog.Builder(this).setTitle(R.string.what_to_do) + .setNegativeButton(android.R.string.cancel) + .setItems(R.array.comment_long_click, new MaterialAlertDialog.OnClickListener() { + @Override + public boolean onClick(MaterialAlertDialog dialog, int position) { + switch (position) { + case 0: + ClipboardManager cm = (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE); + cm.setPrimaryClip(ClipData.newPlainText(null, c.comment)); + MaterialToast.showToast(R.string.copyed); + break; + + case 1: + Intent intent = new Intent(GalleryDetailActivity.this, GalleryListActivity.class); + intent.setAction(GalleryListActivity.ACTION_GALLERY_LIST); + intent.putExtra(GalleryListActivity.KEY_MODE, ListUrls.MODE_UPLOADER); + intent.putExtra(GalleryListActivity.KEY_UPLOADER, c.user); + startActivity(intent); + break; + } + return true; + } + }).show(); + return true; + } + + @Override + public View makeView() { + TextView tv = new TextView(this); + tv.setGravity(Gravity.CENTER); + return tv; + } + + @Override + public int getCurPreviewPage() { + return mCurPreviewPage; + } + + @Override + public void onGetPreviewImageFailure() { + mPreviewWait.setVisibility(View.GONE); + mPreviewRefresh.setVisibility(View.VISIBLE); + } + + @Override + public void onScrollStateChanged(ResponedScrollView view, int state) { + if (view == mDetailScroll) { + ImageCache imageCache = ImageCache.getInstance(this); + if (state == ResponedScrollView.SCROLL_START) + imageCache.setPauseDiskCache(true); + else + imageCache.setPauseDiskCache(false); + } + } + + private class GDetailGetListener + implements EhClient.OnGetGDetailListener { + @Override + public void onSuccess(GalleryDetail md) { + mRefreshText.setRefreshing(false); + doLayout(); + } + + @Override + public void onFailure(String eMsg) { + mRefreshText.setEmesg(eMsg, true); + } + } + + private class GLDetailGetListener + implements EhClient.OnGetLGDetailListener { + @Override + public void onSuccess(LofiGalleryDetail md, boolean isLastPage) { + if (isLastPage) + ((LofiDetailImpl)mGalleryInfo).setPreviewPageNum(mCurPreviewPage + 1); + mRefreshText.setRefreshing(false); + doLayout(); + } + + @Override + public void onFailure(String eMsg) { + mRefreshText.setEmesg(eMsg, true); + } + } + + private class PListGetListener + implements EhClient.OnGetPreviewListListener { + @Override + public void onSuccess(Object checkFlag, PreviewList pageList, boolean isLastPage) { + if (isFinishing()) + return; + if (!(mGalleryInfo instanceof PreviewImpl)) + return; + + mPreviewWait.setVisibility(View.GONE); + mPreviewRefresh.setVisibility(View.GONE); + + int page = (Integer)checkFlag; + PreviewImpl previewImpl = (PreviewImpl)mGalleryInfo; + previewImpl.setPreview(page, pageList); + pageList.setData(GalleryDetailActivity.this, + GalleryDetailActivity.this, previewImpl, page); + pageList.addPreview(mPreview); + + if (isLastPage && mGalleryInfo instanceof LofiDetailImpl) { + ((LofiDetailImpl)mGalleryInfo).setPreviewPageNum(mCurPreviewPage + 1); + mPreviewPage.setText((mCurPreviewPage + 1) + "/" + + getPreviewPageNum(previewImpl.getPreviewPageNum())); + } + + mGetPreview = false; + mShowPreview = true; + } + + @Override + public void onFailure(Object checkFlag, String eMsg) { + if (isFinishing()) + return; + if (!(mGalleryInfo instanceof PreviewImpl)) + return; + + int page = (Integer)checkFlag; + if (page == mCurPreviewPage) { + MaterialToast.showToast(eMsg); + mPreviewWait.setVisibility(View.GONE); + mPreviewRefresh.setVisibility(View.VISIBLE); + } + + // TODO what to do with mGetPreview + } + } + + + private class CommentHolder extends EasyRecyclerView.ViewHolder { + private final TextView userView; + private final TextView timeView; + private final TextView commentView; + + public CommentHolder(View itemView) { + super(itemView); + + userView = (TextView) itemView.findViewById(R.id.user); + timeView = (TextView) itemView.findViewById(R.id.time); + commentView = (TextView) itemView.findViewById(R.id.comment); + } + + } + + private class CommentAdapter extends EasyRecyclerView.FooterAdapter { + + private final LayoutInflater mInflater; + + public CommentAdapter() { + mInflater = LayoutInflater.from(GalleryDetailActivity.this); + } + + @Override + public CommentHolder onCreateAndBindFooterViewHolder(ViewGroup parent, + View footerView) { + // No footer + return null; + } + + @Override + public CommentHolder onCreateViewHolderActual(ViewGroup parent, + int viewType) { + View view = mInflater.inflate(R.layout.comments_item, parent, false); + RippleSalon.addRipple(view, ColorStateList.valueOf(0x40000000)); // TODO + return new CommentHolder(view); + } + + @Override + public void onBindViewHolderActual(CommentHolder holder, int position) { + final Comment comment = ((GalleryDetail) mGalleryInfo).comments.get(position); + + holder.userView.setText(comment.user); + holder.timeView.setText(comment.time); + holder.commentView.setText(Html.fromHtml(comment.comment)); + } + + @Override + public int getItemCountActual() { + if (mGalleryInfo instanceof GalleryDetail) { + GalleryDetail galleryDetail = (GalleryDetail)mGalleryInfo; + if (galleryDetail.comments == null) + return 0; + else + return galleryDetail.comments.size(); + } else { + return 0; + } + } + + } + +} diff --git a/app/src/main/java/com/hippo/ehviewer/ui/GalleryListActivity.java b/app/src/main/java/com/hippo/ehviewer/ui/GalleryListActivity.java new file mode 100644 index 000000000..09168ae68 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ui/GalleryListActivity.java @@ -0,0 +1,1624 @@ +/* + * 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.ui; + +import java.io.File; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.app.ActionBar; +import android.app.AlertDialog; +import android.app.SearchManager; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Message; +import android.provider.MediaStore; +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.text.style.ImageSpan; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.AutoCompleteTextView; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.Spinner; +import android.widget.TextView; + +import com.hippo.ehviewer.AppContext; +import com.hippo.ehviewer.AppHandler; +import com.hippo.ehviewer.ImageLoader; +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.SimpleSuggestionProvider; +import com.hippo.ehviewer.UpdateHelper; +import com.hippo.ehviewer.app.MaterialAlertDialog; +import com.hippo.ehviewer.data.Data; +import com.hippo.ehviewer.data.GalleryInfo; +import com.hippo.ehviewer.data.ListUrls; +import com.hippo.ehviewer.data.Tag; +import com.hippo.ehviewer.drawable.FreeMaterialDrawable; +import com.hippo.ehviewer.drawable.MaterialIndicatorDrawable; +import com.hippo.ehviewer.drawable.MaterialIndicatorDrawable.Stroke; +import com.hippo.ehviewer.ehclient.EhClient; +import com.hippo.ehviewer.network.HttpHelper; +import com.hippo.ehviewer.service.DownloadService; +import com.hippo.ehviewer.util.Config; +import com.hippo.ehviewer.util.DialogUtils; +import com.hippo.ehviewer.util.Favorite; +import com.hippo.ehviewer.util.Ui; +import com.hippo.ehviewer.util.Utils; +import com.hippo.ehviewer.util.ViewUtils; +import com.hippo.ehviewer.widget.CategoryTable; +import com.hippo.ehviewer.widget.DrawerListView; +import com.hippo.ehviewer.widget.FitWindowView; +import com.hippo.ehviewer.widget.GalleryListView; +import com.hippo.ehviewer.widget.GalleryListView.OnGetListListener; +import com.hippo.ehviewer.widget.MaterialToast; +import com.hippo.ehviewer.widget.SearchView; +import com.hippo.ehviewer.widget.SlidingDrawerLayout; +import com.hippo.ehviewer.widget.SuggestionHelper; +import com.hippo.ehviewer.widget.SuggestionTextView; +import com.hippo.ehviewer.widget.TagListView; +import com.hippo.ehviewer.widget.TagsAdapter; +import com.hippo.ehviewer.widget.recyclerview.EasyRecyclerView; +import com.hippo.ehviewer.windowsanimate.WindowsAnimate; + +/* + * + * Footer 显示产生的 refresh 手动 pull 显示产生的 refresh 按键或其他产生的 refresh + * | | | + * | | | + * ∨ ∨ ∨ + * 触发 onFootRefresh 触发 onRefreshStart setHeaderRefresh(true) + * | | | + * |<---------------------------------|-----------------------------| + * ∨ + * set listUrl + * set getMode + * | + * | + * ∨ + * getList + */ + +/** + * Make sure when getting list pulllistview is refreshing + * + * @author Hippo + */ +public class GalleryListActivity extends AbsTranslucentActivity implements View.OnClickListener, + SearchView.OnQueryTextListener, SearchView.OnFocusChangeListener, + SlidingDrawerLayout.DrawerListener, EhClient.OnLoginListener, + GalleryListView.GalleryListViewHelper, FitWindowView.OnFitSystemWindowsListener, + SearchView.OnActionViewExpandedListener, EasyRecyclerView.OnItemClickListener, + EasyRecyclerView.OnItemLongClickListener { + + @SuppressWarnings("unused") + private static final String TAG = GalleryListActivity.class.getSimpleName(); + + private static int RESULT_LOAD_SEARCH_IMAGE = 1; + + public static final String ACTION_GALLERY_LIST = "com.hippo.ehviewer.intent.action.GALLERY_LIST"; + + public static final String KEY_MODE = "mode"; + public static final String KEY_CATEGORY = "category"; + public static final String KEY_TAG = "tag"; + public static final String KEY_UPLOADER = "uploader"; + public static final String KEY_IMAGE_KEY = "image_key"; + public static final String KEY_IMAGE_URL = "image_url"; + + private AppContext mAppContext; + private EhClient mClient; + private Resources mResources; + private WindowsAnimate mWindowsAnimate; + private SuggestionHelper mSuggestions; + private ActionBar mActionBar; + private int mThemeColor; + + private MaterialIndicatorDrawable mMaterialIndicator; + private boolean mDirection; + + private SlidingDrawerLayout mDrawerLayout; + private View mLeftMenu; + private View mRightMenu; + private FitWindowView mStandard; + private GalleryListView mGalleryListView; + + private View mUserPanel; + private DrawerListView mMenuList; + private ImageView mAvatar; + private TextView mUsernameView; + private Button mLoginButton; + private Button mRegisterButton; + private Button mLogoutButton; + private View mWaitLogView; + + private View mRightDrawerBg; + private TagListView mQuickSearchList; + private View mQuickSearchTip; + + private TextView mSearchImageText; + + private SearchView mSearchView; + private MenuItem mSearchItem; + + private TagsAdapter tagsAdapter; + + private ListUrls lus; + private final ArrayList listMenuTag = new ArrayList(); + + private Data mData; + + private int longClickItemIndex; + private boolean mShowDrawer; + + private AlertDialog loginDialog; + private AlertDialog mSearchDialog; + private AlertDialog longClickDialog; + + // Modify tag + private String newTagName = null; + + // Double click back exit + private long curBackTime = 0; + private static final int BACK_PRESSED_INTERVAL = 2000; + + private String mTitle; + + // private int mListModeThumbHeight; + + private void toRegister() { + Uri uri = Uri.parse("http://forums.e-hentai.org/index.php?act=Reg"); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + startActivity(intent); + } + + @Override + public void onSuccess() { + setUserPanel(); + } + + @Override + public void onGetAvatar(final int code) { + GalleryListActivity.this.runOnUiThread(new Runnable() { + @Override + public void run() { + switch (code) { + case EhClient.GET_AVATAR_OK: + setUserPanel(); + break; + case EhClient.NO_AVATAR: + MaterialToast.showToast(R.string.no_avatar); + break; + case EhClient.GET_AVATAR_ERROR: + default: + MaterialToast.showToast(R.string.get_avatar_error); + break; + } + } + }); + } + + @Override + public void onFailure(String eMesg) { + setUserPanel(); + MaterialToast.showToast(eMesg); + if (!GalleryListActivity.this.isFinishing()) + loginDialog.show(); + } + + @SuppressLint("InflateParams") + private AlertDialog createLoginDialog() { + View view = ViewUtils.inflateDialogView(R.layout.login, false); + + return new MaterialAlertDialog.Builder(this).setCancelable(false).setTitle(R.string.login).setView(view, true) + .setPositiveButton(android.R.string.ok).setNegativeButton(android.R.string.cancel) + .setNeutralButton(R.string.register).setButtonListener(new MaterialAlertDialog.OnClickListener() { + @Override + public boolean onClick(MaterialAlertDialog dialog, int which) { + switch (which) { + case MaterialAlertDialog.POSITIVE: + setUserPanel(WAIT); + String username = ((EditText) loginDialog.findViewById(R.id.username)).getText().toString(); + String password = ((EditText) loginDialog.findViewById(R.id.password)).getText().toString(); + mClient.login(username, password, GalleryListActivity.this); + return true; + case MaterialAlertDialog.NEGATIVE: + setUserPanel(); + return true; + case MaterialAlertDialog.NEUTRAL: + toRegister(); + return false; + default: + return false; + } + } + }).create(); + } + + @SuppressLint("InflateParams") + private AlertDialog createModeDialog() { + View cv = ViewUtils.inflateDialogView(R.layout.select_mode, false); + + final Spinner modeSpinner = (Spinner) cv.findViewById(R.id.mode_list); + modeSpinner.setSelection(Config.getMode()); + final Spinner apiModeSpinner = (Spinner) cv.findViewById(R.id.api_mode_list); + apiModeSpinner.setSelection(Config.getApiMode()); + final Spinner lofiResolutionSpinner = (Spinner) cv.findViewById(R.id.lofi_resolution_list); + lofiResolutionSpinner.setSelection(Config.getLofiResolution() - 1); + + return new MaterialAlertDialog.Builder(this).setTitle(R.string.mode).setView(cv, true) + .setPositiveButton(android.R.string.ok).setNegativeButton(android.R.string.cancel) + .setButtonListener(new MaterialAlertDialog.OnClickListener() { + @Override + public boolean onClick(MaterialAlertDialog dialog, int which) { + if (which == MaterialAlertDialog.POSITIVE) { + Config.setMode(modeSpinner.getSelectedItemPosition()); + Config.setApiMode(apiModeSpinner.getSelectedItemPosition()); + Config.setLofiResolution(lofiResolutionSpinner.getSelectedItemPosition() + 1); + } + return true; + } + }).create(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (requestCode == RESULT_LOAD_SEARCH_IMAGE && resultCode == RESULT_OK && null != data) { + Uri selectedImage = data.getData(); + String[] filePathColumn = { MediaStore.Images.Media.DATA }; + + Cursor cursor = getContentResolver().query(selectedImage, filePathColumn, null, null, null); + cursor.moveToFirst(); + + int columnIndex = cursor.getColumnIndex(filePathColumn[0]); + String ImagePath = cursor.getString(columnIndex); + cursor.close(); + + mSearchImageText.setVisibility(View.VISIBLE); + mSearchImageText.setText(ImagePath); + } + + } + + private void handleSearchView(View view) { + final SuggestionTextView pet = (SuggestionTextView) view.findViewById(R.id.search_text); + pet.setSuggestionHelper(mSuggestions); + CheckBox uploaderCb = (CheckBox) view.findViewById(R.id.checkbox_uploader); + uploaderCb.setOnCheckedChangeListener(new CheckBox.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (isChecked) { + pet.setPrefix("uploader:"); + pet.setSuggestionHelper(null); + } else { + pet.setPrefix(null); + pet.setSuggestionHelper(mSuggestions); + } + } + }); + + final View advance = view.findViewById(R.id.filter_advance); + CheckBox cb = (CheckBox) view.findViewById(R.id.checkbox_advance); + cb.setOnCheckedChangeListener(new CheckBox.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (isChecked) + advance.setVisibility(View.VISIBLE); + else + advance.setVisibility(View.GONE); + } + }); + + final View selectImage = view.findViewById(R.id.select_image); + selectImage.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent i = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI); + startActivityForResult(i, RESULT_LOAD_SEARCH_IMAGE); + } + }); + } + + @SuppressLint("InflateParams") + private AlertDialog createSearchDialog() { + final View view = ViewUtils.inflateDialogView(R.layout.search, false); + final View searchNormal = view.findViewById(R.id.search_normal); + final View searchTag = view.findViewById(R.id.search_tag); + final View searchImage = view.findViewById(R.id.search_image); + mSearchImageText = (TextView) view.findViewById(R.id.target_image); + handleSearchView(view); + + return new MaterialAlertDialog.Builder(this).setTitle(android.R.string.search_go).setView(view, true) + .setActionButton(R.string.mode).setPositiveButton(android.R.string.ok) + .setNegativeButton(android.R.string.cancel).setNeutralButton(R.string.add) + .setButtonListener(new MaterialAlertDialog.OnClickListener() { + @Override + public boolean onClick(final MaterialAlertDialog dialog, int which) { + switch (which) { + case MaterialAlertDialog.ACTION: + if (searchNormal.getVisibility() == View.VISIBLE) { + searchNormal.setVisibility(View.GONE); + searchTag.setVisibility(View.VISIBLE); + } else if (searchTag.getVisibility() == View.VISIBLE) { + searchTag.setVisibility(View.GONE); + searchImage.setVisibility(View.VISIBLE); + } else if (searchImage.getVisibility() == View.VISIBLE) { + searchImage.setVisibility(View.GONE); + searchNormal.setVisibility(View.VISIBLE); + } + return false; + case MaterialAlertDialog.POSITIVE: + ListUrls listUrls = getLus(mSearchDialog); + if (listUrls == null) + return true; + + lus = listUrls; + mGalleryListView.refresh(); + mDrawerLayout.closeDrawers(); + + // Get title + String search = lus.getSearch(); + switch (lus.getMode()) { + case ListUrls.MODE_IMAGE_SEARCH: + mTitle = getString(R.string.image_search); + break; + case ListUrls.MODE_UPLOADER: + mTitle = search; + break; + case ListUrls.MODE_TAG: + mTitle = lus.getTag(); + break; + case ListUrls.MODE_POPULAR: + mTitle = getString(R.string.whatshot); + case ListUrls.MODE_NORMAL: + default: + if (search == null || search.isEmpty()) + mTitle = getString(android.R.string.search_go); + else + mTitle = search; + break; + } + setTitle(mTitle); + return true; + case MaterialAlertDialog.NEUTRAL: + if (searchImage.getVisibility() == View.VISIBLE) + return false; + createSetNameDialog(null, null, new OnSetNameListener() { + @Override + public void onSetVaildName(String newName) { + mData.addTag(new Tag(newName, getLus(mSearchDialog))); + listMenuTag.add(newName); + tagsAdapter.addId(newName); + tagsAdapter.notifyDataSetChanged(); + + dialog.dismiss(); + } + }).show(); + return false; + default: + return true; + } + } + }).create(); + } + + private ListUrls getLus(AlertDialog dialog) { + return getLus(dialog.findViewById(R.id.custom)); + } + + private ListUrls getLus(View view) { + ListUrls lus = null; + + View searchNormal = view.findViewById(R.id.search_normal); + View searchTag = view.findViewById(R.id.search_tag); + View searchImage = view.findViewById(R.id.search_image); + + if (searchNormal.getVisibility() == View.VISIBLE) { + int type; + CategoryTable ct = (CategoryTable) view.findViewById(R.id.category_table); + type = ct.getCategory(); + EditText et = (EditText) view.findViewById(R.id.search_text); + lus = new ListUrls(type, et.getText().toString()); + + CheckBox uploaderCb = (CheckBox) view.findViewById(R.id.checkbox_uploader); + if (uploaderCb.isChecked()) { + lus.setMode(ListUrls.MODE_UPLOADER); + } + + CheckBox cb = (CheckBox) view.findViewById(R.id.checkbox_advance); + if (cb.isChecked()) { + CheckBox checkImageSname = (CheckBox) view.findViewById(R.id.checkbox_sname); + CheckBox checkImageStags = (CheckBox) view.findViewById(R.id.checkbox_stags); + CheckBox checkImageSdesc = (CheckBox) view.findViewById(R.id.checkbox_sdesc); + CheckBox checkImageStorr = (CheckBox) view.findViewById(R.id.checkbox_storr); + CheckBox checkImageSto = (CheckBox) view.findViewById(R.id.checkbox_sto); + CheckBox checkImageSdt1 = (CheckBox) view.findViewById(R.id.checkbox_sdt1); + CheckBox checkImageSdt2 = (CheckBox) view.findViewById(R.id.checkbox_sdt2); + CheckBox checkImageSh = (CheckBox) view.findViewById(R.id.checkbox_sh); + + int advType = 0; + if (checkImageSname.isChecked()) + advType |= ListUrls.SNAME; + if (checkImageStags.isChecked()) + advType |= ListUrls.STAGS; + if (checkImageSdesc.isChecked()) + advType |= ListUrls.SDESC; + if (checkImageStorr.isChecked()) + advType |= ListUrls.STORR; + if (checkImageSto.isChecked()) + advType |= ListUrls.STO; + if (checkImageSdt1.isChecked()) + advType |= ListUrls.STD1; + if (checkImageSdt2.isChecked()) + advType |= ListUrls.STD2; + if (checkImageSh.isChecked()) + advType |= ListUrls.SH; + CheckBox checkImageSr = (CheckBox) view.findViewById(R.id.checkbox_sr); + if (checkImageSr.isChecked()) { + Spinner spinnerMinRating = (Spinner) view.findViewById(R.id.spinner_min_rating); + lus.setAdvance(advType, spinnerMinRating.getSelectedItemPosition() + 2); + } else + lus.setAdvance(advType); + } + + // For tag search + } else if (searchTag.getVisibility() == View.VISIBLE) { + EditText et = (EditText) view.findViewById(R.id.search_tag_text); + lus = new ListUrls(); + lus.setTag(et.getText().toString()); + + // For image search + } else if (searchImage.getVisibility() == View.VISIBLE) { + String filePath = (String) mSearchImageText.getText(); + File file = new File(filePath); + if (!file.exists()) { + MaterialToast.showToast(R.string.image_not_exist); + } else { + if (!file.canRead()) { + MaterialToast.showToast(R.string.image_not_readable); + } else { + + CheckBox similar = (CheckBox) view.findViewById(R.id.checkboxSimilar); + CheckBox covers = (CheckBox) view.findViewById(R.id.checkboxCovers); + CheckBox exp = (CheckBox) view.findViewById(R.id.checkboxExp); + + lus = new ListUrls(); + lus.setSearchFile(file, (similar.isChecked() ? EhClient.IMAGE_SEARCH_USE_SIMILARITY_SCAN : 0) + | (covers.isChecked() ? EhClient.IMAGE_SEARCH_ONLY_SEARCH_COVERS : 0) + | (exp.isChecked() ? EhClient.IMAGE_SEARCH_SHOW_EXPUNGED : 0)); + } + } + } + + return lus; + } + + private AlertDialog createLongClickDialog() { + return new MaterialAlertDialog.Builder(this).setTitle(R.string.what_to_do) + .setItems(R.array.list_item_long_click, new MaterialAlertDialog.OnClickListener() { + @Override + public boolean onClick(MaterialAlertDialog dialog, int position) { + final GalleryInfo gi = mGalleryListView.getGalleryInfo(longClickItemIndex); + switch (position) { + case 0: // Add favourite item + Favorite.addToFavorite(GalleryListActivity.this, gi); + break; + case 1: + Intent it = new Intent(GalleryListActivity.this, DownloadService.class); + it.setAction(DownloadService.ACTION_ADD); + it.putExtra(DownloadService.KEY_GALLERY_INFO, gi); + startService(it); + MaterialToast.showToast(R.string.toast_add_download); + break; + } + return true; + } + }).setNegativeButton(android.R.string.cancel).create(); + } + + @SuppressLint("InflateParams") + private AlertDialog createJumpDialog() { + View view = ViewUtils.inflateDialogView(R.layout.jump, false); + TextView tv = (TextView) view.findViewById(R.id.list_jump_sum); + // For lofi, can not get page num, so use Integer.MAX_VALUE + tv.setText(String.format(getString(R.string.jump_summary), mGalleryListView.getCurPage() + 1, + mGalleryListView.getPageNum() == Integer.MAX_VALUE ? getString(R.string._unknown) : + String.valueOf(mGalleryListView.getPageNum()))); + tv = (TextView) view.findViewById(R.id.list_jump_to); + tv.setText(R.string.jump_to); + final EditText et = (EditText) view.findViewById(R.id.list_jump_edit); + + return new MaterialAlertDialog.Builder(this).setTitle(R.string.jump).setView(view, true) + .setPositiveButton(android.R.string.ok).setNegativeButton(android.R.string.cancel) + .setButtonListener(new MaterialAlertDialog.OnClickListener() { + @Override + public boolean onClick(MaterialAlertDialog dialog, int which) { + if (which == MaterialAlertDialog.POSITIVE) { + int targetPage; + try { + targetPage = Integer.parseInt(et.getText().toString()) - 1; + if (targetPage < 0 || targetPage >= mGalleryListView.getPageNum()) + throw new Exception(); + } catch (Exception e) { + MaterialToast.showToast(R.string.toast_invalid_page); + return false; + } + mGalleryListView.jumpTo(targetPage); + return true; + } else { + return true; + } + } + }).create(); + } + + private interface OnSetNameListener { + public void onSetVaildName(String newName); + } + + /** + * Create a set name dialog + * + * @param hint + * Text to set in edittext first + * @param oldStr + * string can be oldstr, even it is in listMenuTitle + * @param listener + * what to do when set right text + */ + @SuppressLint("InflateParams") + private AlertDialog createSetNameDialog(final String hint, final String oldStr, final OnSetNameListener listener) { + View view = ViewUtils.inflateDialogView(R.layout.set_name, false); + final EditText et = (EditText) view.findViewById(R.id.set_name_edit); + if (hint != null) + et.setText(hint); + + return new MaterialAlertDialog.Builder(this).setTitle(R.string.add_tag).setView(view, true) + .setPositiveButton(android.R.string.ok).setNegativeButton(android.R.string.cancel) + .setButtonListener(new MaterialAlertDialog.OnClickListener() { + @Override + public boolean onClick(MaterialAlertDialog dialog, int which) { + if (which == MaterialAlertDialog.POSITIVE) { + String key = et.getText().toString(); + if (key.length() == 0) { + MaterialToast.showToast(R.string.invalid_input); + return false; + } else if (listMenuTag.contains(key) && !key.equals(oldStr)) { + MaterialToast.showToast("该名称已存在"); // TODO + return false; + } else { + if (listener != null) { + listener.onSetVaildName(key); + } + return true; + } + } else { + return true; + } + } + }).create(); + } + + @SuppressLint("InflateParams") + private AlertDialog createModifyTagDialog(final int position) { + final View view = ViewUtils.inflateDialogView(R.layout.search, false); + final View searchNormal = view.findViewById(R.id.search_normal); + final View searchTag = view.findViewById(R.id.search_tag); + final View searchImage = view.findViewById(R.id.search_image); + handleSearchView(view); + + ListUrls listUrls = mData.getTag(position); + setFilterView(view, listUrls); + + return new MaterialAlertDialog.Builder(this).setTitle(listMenuTag.get(position)).setView(view, true) + .setActionButton(R.string.mode).setPositiveButton(android.R.string.ok) + .setNegativeButton(android.R.string.cancel).setNeutralButton(R.string.tag_change_name) + .setButtonListener(new MaterialAlertDialog.OnClickListener() { + @Override + public boolean onClick(final MaterialAlertDialog dialog, int which) { + switch (which) { + case MaterialAlertDialog.ACTION: + if (searchNormal.getVisibility() == View.GONE) { + searchNormal.setVisibility(View.VISIBLE); + searchTag.setVisibility(View.GONE); + searchImage.setVisibility(View.GONE); + } else { + searchNormal.setVisibility(View.GONE); + searchTag.setVisibility(View.VISIBLE); + searchImage.setVisibility(View.GONE); + } + return false; + case MaterialAlertDialog.POSITIVE: + ListUrls listUrls = getLus(view); + if (newTagName != null) { + mData.setTag(position, new Tag(newTagName, listUrls)); + tagsAdapter.set(listMenuTag.get(position), newTagName); + listMenuTag.set(position, newTagName); + tagsAdapter.notifyDataSetChanged(); + newTagName = null; + } else { + mData.setTag(position, new Tag(listMenuTag.get(position), listUrls)); + } + return true; + case MaterialAlertDialog.NEUTRAL: + String hint = newTagName == null ? listMenuTag.get(position) : newTagName; + createSetNameDialog(hint, listMenuTag.get(position), new OnSetNameListener() { + @Override + public void onSetVaildName(String newName) { + if (newName.equals(listMenuTag.get(position))) { // If + // new + // is + // old + // name + dialog.setTitle(listMenuTag.get(position)); + } else { + newTagName = newName; + dialog.setTitle(String.format(getString(R.string.new_tag_name), newTagName)); + } + } + }).show(); + return false; + case MaterialAlertDialog.NEGATIVE: + default: + return true; + } + } + }).create(); + } + + private void setFilterView(View view, ListUrls listUrls) { + + View searchNormal = view.findViewById(R.id.search_normal); + View searchTag = view.findViewById(R.id.search_tag); + + if (listUrls.getMode() == ListUrls.MODE_TAG) { + searchNormal.setVisibility(View.GONE); + searchTag.setVisibility(View.VISIBLE); + + EditText et = (EditText) view.findViewById(R.id.search_tag_text); + et.setText(listUrls.getTag()); + } else { + searchNormal.setVisibility(View.VISIBLE); + searchTag.setVisibility(View.GONE); + + int category = listUrls.getCategory(); + CategoryTable ct = (CategoryTable) view.findViewById(R.id.category_table); + ct.setCategory(category); + + EditText et = (EditText) view.findViewById(R.id.search_text); + et.setText(listUrls.getSearch()); + + // Advance + CheckBox cb = (CheckBox) view.findViewById(R.id.checkbox_advance); + cb.setChecked(listUrls.isAdvance()); + + CheckBox checkImageSname = (CheckBox) view.findViewById(R.id.checkbox_sname); + CheckBox checkImageStags = (CheckBox) view.findViewById(R.id.checkbox_stags); + CheckBox checkImageSdesc = (CheckBox) view.findViewById(R.id.checkbox_sdesc); + CheckBox checkImageStorr = (CheckBox) view.findViewById(R.id.checkbox_storr); + CheckBox checkImageSto = (CheckBox) view.findViewById(R.id.checkbox_sto); + CheckBox checkImageSdt1 = (CheckBox) view.findViewById(R.id.checkbox_sdt1); + CheckBox checkImageSdt2 = (CheckBox) view.findViewById(R.id.checkbox_sdt2); + CheckBox checkImageSh = (CheckBox) view.findViewById(R.id.checkbox_sh); + + int advType = listUrls.getAdvanceType(); + if (advType != -1) { + if ((advType & ListUrls.SNAME) == 0) + checkImageSname.setChecked(false); + else + checkImageSname.setChecked(true); + if ((advType & ListUrls.STAGS) == 0) + checkImageStags.setChecked(false); + else + checkImageStags.setChecked(true); + if ((advType & ListUrls.SDESC) == 0) + checkImageSdesc.setChecked(false); + else + checkImageSdesc.setChecked(true); + if ((advType & ListUrls.STORR) == 0) + checkImageStorr.setChecked(false); + else + checkImageStorr.setChecked(true); + if ((advType & ListUrls.STO) == 0) + checkImageSto.setChecked(false); + else + checkImageSto.setChecked(true); + if ((advType & ListUrls.STD1) == 0) + checkImageSdt1.setChecked(false); + else + checkImageSdt1.setChecked(true); + if ((advType & ListUrls.STD2) == 0) + checkImageSdt2.setChecked(false); + else + checkImageSdt2.setChecked(true); + if ((advType & ListUrls.SH) == 0) + checkImageSh.setChecked(false); + else + checkImageSh.setChecked(true); + + // MinRating + CheckBox checkImageSr = (CheckBox) view.findViewById(R.id.checkbox_sr); + if (listUrls.isMinRating()) + checkImageSr.setChecked(true); + else + checkImageSr.setChecked(false); + Spinner spinnerMinRating = (Spinner) view.findViewById(R.id.spinner_min_rating); + int index; + if (listUrls.getMinRating() == -1) + index = 0; + else + index = listUrls.getMinRating() - 2; + spinnerMinRating.setSelection(index); + } + + // Show advance if need + final View advance = view.findViewById(R.id.filter_advance); + if (listUrls.isAdvance()) + advance.setVisibility(View.VISIBLE); + } + } + + @Override + protected void onResume() { + super.onResume(); + mGalleryListView.setListMode(Config.getListMode()); + mGalleryListView.updateSpanCount(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + mGalleryListView.updateSpanCount(); + } + + /** + * Get + * + * @param intent + */ + private void handleIntent(Intent intent) { + String action = intent != null ? intent.getAction() : null; + if (Intent.ACTION_SEARCH.equals(action)) { + String query = intent.getStringExtra(SearchManager.QUERY); + if (mSearchView != null && query != null) + mSearchView.setQuery(query, true); + } else if (ACTION_GALLERY_LIST.equals(action)) { + int mode = intent.getIntExtra(GalleryListActivity.KEY_MODE, -1); + switch (mode) { + case ListUrls.MODE_TAG: + lus = new ListUrls(); + String tag = intent.getStringExtra(KEY_TAG); + lus.setTag(tag); + mTitle = tag; + setTitle(mTitle); + break; + + case ListUrls.MODE_UPLOADER: + String uploader = "uploader:" + intent.getStringExtra(KEY_UPLOADER); + lus = new ListUrls(ListUrls.ALL_CATEGORT, uploader); + lus.setMode(ListUrls.MODE_UPLOADER); + mTitle = uploader; + setTitle(mTitle); + break; + + case ListUrls.MODE_IMAGE_SEARCH: + lus = new ListUrls(); + lus.setSearchImage(intent.getStringExtra(KEY_IMAGE_KEY), intent.getStringExtra(KEY_IMAGE_URL), + EhClient.IMAGE_SEARCH_USE_SIMILARITY_SCAN); + mTitle = getString(R.string.similar_content); + setTitle(mTitle); + break; + + case ListUrls.MODE_NORMAL: + // Target is category + int category = intent.getIntExtra(KEY_CATEGORY, ListUrls.ALL_CATEGORT); + lus = new ListUrls(category); + mTitle = Ui.getCategoryText(category); + setTitle(mTitle); + break; + + default: + // Get error, go to home page + lus = new ListUrls(); + mTitle = mResources.getString(R.string.homepage); + setTitle(mTitle); + break; + } + } else { + lus = new ListUrls(); + mTitle = mResources.getString(R.string.homepage); + setTitle(mTitle); + } + } + + @Override + protected void onNewIntent(Intent intent) { + setIntent(intent); + handleIntent(intent); + mGalleryListView.refresh(); + } + + @Override + public void onClick(View v) { + if (v == mLoginButton) { + loginDialog.show(); + } else if (v == mRegisterButton) { + toRegister(); + } else if (v == mLogoutButton) { + mClient.logout(); + setUserPanel(); + } + } + + @Override + public void onDrawerClosed(View view) { + setTitle(mTitle); + invalidateOptionsMenu(); + + if (view == mLeftMenu) + mDirection = false; + } + + @Override + public void onDrawerOpened(View view) { + if (view == mLeftMenu) + setTitle(R.string.app_name); + invalidateOptionsMenu(); + + if (view == mLeftMenu) + mDirection = true; + } + + @Override + public void onDrawerSlide(View view, float f) { + if (view == mLeftMenu) { + mMaterialIndicator.setTransformationOffset( + MaterialIndicatorDrawable.AnimationState.BURGER_ARROW, + mDirection ? 2 - f : f); + } + } + + @Override + public void onDrawerStateChanged(View drawerView, int newState) { + // Empty + } + + @Override + public void onFitSystemWindows(int l, int t, int r, int b) { + mGalleryListView.setPaddingTopAndBottom(t, b); + ((SlidingDrawerLayout.LayoutParams) mLeftMenu.getLayoutParams()).topMargin = t; + ((SlidingDrawerLayout.LayoutParams) mRightMenu.getLayoutParams()).topMargin = t; + mMenuList.setPadding(mMenuList.getPaddingLeft(), mMenuList.getPaddingTop(), + mMenuList.getPaddingRight(), b); + mQuickSearchList.setPadding(mQuickSearchList.getPaddingLeft(), mQuickSearchList.getPaddingTop(), + mQuickSearchList.getPaddingRight(), b); + + Ui.colorStatusBarKK(this, mThemeColor, t - Ui.ACTION_BAR_HEIGHT); + } + + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + mMaterialIndicator.syncState(savedInstanceState); + } + + @Override protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + mMaterialIndicator.onSaveInstanceState(outState); + } + + @Override + public boolean onItemClick(EasyRecyclerView parent, View view, + int position, long id) { + Intent intent = new Intent(GalleryListActivity.this, GalleryDetailActivity.class); + GalleryInfo gi = mGalleryListView.getGalleryInfo(position); + intent.putExtra(GalleryDetailActivity.KEY_G_INFO, gi); + startActivity(intent); + return true; + } + + @Override + public boolean onItemLongClick(EasyRecyclerView parent, View view, + int position, long id) { + longClickItemIndex = position; + longClickDialog.show(); + return true; + } + + @SuppressWarnings("deprecation") + @Override + @SuppressLint("InflateParams") + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.gallery_list); + + mAppContext = (AppContext) getApplication(); + mData = Data.getInstance(); + mClient = EhClient.getInstance(); + mResources = getResources(); + mWindowsAnimate = new WindowsAnimate(); + mWindowsAnimate.init(this); + mSuggestions = SuggestionHelper.getInstance(GalleryListActivity.this, SimpleSuggestionProvider.AUTHORITY, + SimpleSuggestionProvider.MODE); + + handleIntent(getIntent()); + // Check show drawer or not + if (ACTION_GALLERY_LIST.equals(getIntent().getAction())) + mShowDrawer = false; + else + mShowDrawer = true; + + // Init dialog + loginDialog = createLoginDialog(); + mSearchDialog = createSearchDialog(); + longClickDialog = createLongClickDialog(); + + ColorDrawable cd = new ColorDrawable(Color.WHITE); + cd.setBounds(0, 0, Ui.dp2pix(32), Ui.dp2pix(32)); + this. + + // Menu + mMaterialIndicator = new MaterialIndicatorDrawable(this, Color.WHITE, Stroke.THIN); + if (mShowDrawer) + mMaterialIndicator.setIconState(MaterialIndicatorDrawable.IconState.BURGER); + else + mMaterialIndicator.setIconState(MaterialIndicatorDrawable.IconState.ARROW); + mActionBar = getActionBar(); + Ui.setMaterialIndicator(mActionBar, mMaterialIndicator); + + // Get View + mDrawerLayout = (SlidingDrawerLayout) findViewById(R.id.drawerlayout); + + mLeftMenu = mDrawerLayout.findViewById(R.id.list_menu_left); + mRightMenu = mDrawerLayout.findViewById(R.id.list_menu_right); + mStandard = (FitWindowView) mDrawerLayout.findViewById(R.id.standard); + mGalleryListView = (GalleryListView) mDrawerLayout.findViewById(R.id.gallery_list); + + mUserPanel = mLeftMenu.findViewById(R.id.user_panel); + mMenuList = (DrawerListView) mLeftMenu.findViewById(R.id.list_menu_item_list); + mAvatar = (ImageView) mUserPanel.findViewById(R.id.avatar); + mUsernameView = (TextView) mUserPanel.findViewById(R.id.user); + mLoginButton = (Button) mUserPanel.findViewById(R.id.login); + mRegisterButton = (Button) mUserPanel.findViewById(R.id.register); + mLogoutButton = (Button) mUserPanel.findViewById(R.id.logout); + mWaitLogView = mUserPanel.findViewById(R.id.wait); + + mRightDrawerBg = mRightMenu.findViewById(R.id.background); + mQuickSearchList = (TagListView) mRightMenu.findViewById(R.id.list_menu_tag_list); + mQuickSearchTip = mRightMenu.findViewById(R.id.tip_text); + + mLoginButton.setOnClickListener(this); + mRegisterButton.setOnClickListener(this); + mLogoutButton.setOnClickListener(this); + + // Drawer + mDrawerLayout.setDrawerListener(this); + if (!mShowDrawer) + mDrawerLayout.setDrawerLockMode(SlidingDrawerLayout.LOCK_MODE_LOCKED_CLOSED, mLeftMenu); + + // Content + mGalleryListView.setGalleryListViewHelper(this); + mStandard.addOnFitSystemWindowsListener(this); + + // leftDrawer + mUserPanel.setLayerType(View.LAYER_TYPE_SOFTWARE, null); + mUserPanel.setBackgroundDrawable(new FreeMaterialDrawable()); + + final Drawable[] drawableArray = new Drawable[] { + mResources.getDrawable(R.drawable.ic_drawer_home), + mResources.getDrawable(R.drawable.ic_drawer_panda), + mResources.getDrawable(R.drawable.ic_drawer_search), + mResources.getDrawable(R.drawable.ic_drawer_favorite), + mResources.getDrawable(R.drawable.ic_drawer_whatshot), + mResources.getDrawable(R.drawable.ic_drawer_history), + mResources.getDrawable(R.drawable.ic_drawer_download), + mResources.getDrawable(R.drawable.ic_drawer_settings), + }; + final CharSequence[] titleArray = new CharSequence[] { + mResources.getString(R.string.homepage), + mResources.getString(R.string.mode), + mResources.getString(android.R.string.search_go), + mResources.getString(R.string.favourite), + mResources.getString(R.string.whatshot), + mResources.getString(R.string.history), + mResources.getString(R.string.download), + mResources.getString(R.string.action_settings) + }; + + mMenuList.setClipToPadding(false); + mMenuList.setData(drawableArray, titleArray); + mMenuList.setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + Intent intent; + switch (position) { + case 0: // Home page + lus = new ListUrls(ListUrls.NONE, null, 0); + mGalleryListView.refresh(); + mDrawerLayout.closeDrawers(); + mTitle = mResources.getString(R.string.homepage); + setTitle(mTitle); + break; + + case 1: // Mode + createModeDialog().show(); + break; + + case 2: // Search + mSearchDialog.show(); + break; + + case 3: // Favourite + intent = new Intent(GalleryListActivity.this, FavouriteActivity.class); + startActivity(intent); + break; + + case 4: // Popular + lus = new ListUrls(); + lus.setMode(ListUrls.MODE_POPULAR); + mTitle = mResources.getString(R.string.whatshot); + setTitle(mTitle); + mGalleryListView.refresh(); + mDrawerLayout.closeDrawers(); + break; + + case 5: // HistoryActivity + intent = new Intent(GalleryListActivity.this, HistoryActivity.class); + startActivity(intent); + break; + + case 6: // Download + intent = new Intent(GalleryListActivity.this, DownloadActivity.class); + startActivity(intent); + break; + + case 7: // Settings + intent = new Intent(GalleryListActivity.this, SettingsActivity.class); + startActivity(intent); + break; + } + } + }); + + mRightDrawerBg.setLayerType(View.LAYER_TYPE_SOFTWARE, null); + mRightDrawerBg.setBackgroundDrawable(new FreeMaterialDrawable().setBgIndex(2).setKeepTop(true)); + + List keys = mData.getAllTagNames(); + for (int i = 0; i < keys.size(); i++) + listMenuTag.add(keys.get(i)); + tagsAdapter = new TagsAdapter(this, R.layout.menu_tag, listMenuTag); + tagsAdapter.setOnDataSetChangedListener(new TagsAdapter.OnDataSetChangedListener() { + @Override + public void OnDataSetChanged() { + if (listMenuTag.size() == 0) + mQuickSearchTip.setVisibility(View.VISIBLE); + else + mQuickSearchTip.setVisibility(View.GONE); + } + }); + if (listMenuTag.size() == 0) + mQuickSearchTip.setVisibility(View.VISIBLE); + else + mQuickSearchTip.setVisibility(View.GONE); + + mQuickSearchList.setClipToPadding(false); + mQuickSearchList.setAdapter(tagsAdapter); + mQuickSearchList.setChoiceMode(ListView.CHOICE_MODE_SINGLE); + mQuickSearchList.setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(AdapterView arg0, View arg1, int position, long arg3) { + lus = mData.getTag(position); + mGalleryListView.refresh(); + mDrawerLayout.closeDrawers(); + mTitle = listMenuTag.get(position); + setTitle(mTitle); + } + }); + mQuickSearchList.setOnModifyListener(new TagListView.OnModifyListener() { + @Override + public void onModify(int position) { + createModifyTagDialog(position).show(); + } + }); + mQuickSearchList.setOnSwapListener(new TagListView.OnSwapListener() { + @Override + public void onSwap(int positionOne, int positionTwo) { + String temp = listMenuTag.get(positionOne); + mData.swapTag(positionOne, positionTwo); + listMenuTag.set(positionOne, listMenuTag.get(positionTwo)); + listMenuTag.set(positionTwo, temp); + tagsAdapter.notifyDataSetChanged(); + } + }); + mQuickSearchList.setOnDeleteListener(new TagListView.OnDeleteListener() { + @Override + public void onDelete(int position) { + String removeItem = listMenuTag.remove(position); + mData.deleteTag(position); + tagsAdapter.remove(removeItem); + tagsAdapter.notifyDataSetChanged(); + } + }); + + // Listview + mGalleryListView.setOnItemClickListener(this); + mGalleryListView.setOnItemLongClickListener(this); + + // Set random color + mThemeColor = Config.getCustomThemeColor() ? Config.getThemeColor() : Ui.THEME_COLOR; + mActionBar.setBackgroundDrawable(new ColorDrawable(mThemeColor)); + Ui.colorStatusBarL(this, mThemeColor); + + // Update user panel + setUserPanel(); + + // get MangeList + mGalleryListView.firstTimeRefresh(); + + // If not show drawer, just return + if (!mShowDrawer) { + return; + } + + // Check update + if (Config.isAutoCheckForUpdate()) + checkUpdate(); + + String keyFirstTime = "first_time"; + if (Config.getBoolean(keyFirstTime, true)) { + Config.setBoolean(keyFirstTime, false); + + // Show left menu, Can't invoke showMenu() immediately + Message m = Message.obtain(null, new Runnable() { + @Override + public void run() { + mDrawerLayout.openDrawer(mLeftMenu); + } + }); + AppHandler.getInstance().sendMessageDelayed(m, 500L); + } + + String keyTcWarning = "traditional_chinese_warning"; + String country = Locale.getDefault().getCountry(); + if (Config.getBoolean(keyTcWarning, true) && (country.equals("HK") || country.equals("TW"))) { + Config.setBoolean(keyTcWarning, false); + new MaterialAlertDialog.Builder(this).setTitle("注意") + .setMessage("正體中文的翻譯由 OpenCC 自動完成,若有任何錯誤或不妥之處歡迎指出。") + .show(); + } + } + + @Override + public boolean onQueryTextSubmit(String query) { + // Store suggestion + mSuggestions.saveRecentQuery(query, null); + // collapse search view + if (mSearchItem != null) + mSearchItem.collapseActionView(); + + String t = null; + if (query == null || query.isEmpty()) + t = getString(android.R.string.search_go); + else + t = query; + + lus = new ListUrls(ListUrls.NONE, query); + mGalleryListView.refresh(); + mTitle = t; + setTitle(mTitle); + return true; + } + + @Override + public boolean onQueryTextChange(String newText) { + return false; + } + + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (!hasFocus && mSearchItem != null) + mSearchItem.collapseActionView(); + } + + @Override + public void onActionViewExpanded() { + if (mSearchView != null && lus != null) + mSearchView.setQuery(lus.toString(), false); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + if (mDrawerLayout.isDrawerOpen(mRightMenu)) + getMenuInflater().inflate(R.menu.quick_search, menu); + else if (mDrawerLayout.isDrawerOpen(mLeftMenu)) + return true; + else + getMenuInflater().inflate(R.menu.gallery_list, menu); + + SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); + mSearchItem = menu.findItem(R.id.action_search); + mSearchView = (SearchView) mSearchItem.getActionView(); + mSearchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName())); + mSearchView.setOnQueryTextListener(this); + mSearchView.setOnQueryTextFocusChangeListener(this); + mSearchView.setOnActionViewExpandedListener(this); + + // Make search view custom look + int searchTextID = mResources.getIdentifier("android:id/search_src_text", null, null); + AutoCompleteTextView searchText = null; + if (searchTextID > 0 && (searchText = (AutoCompleteTextView) mSearchView.findViewById(searchTextID)) != null) { + + int searchCursorID = mResources.getIdentifier("android:drawable/text_cursor_holo_dark", null, null); + if (searchCursorID > 0) { + try { + Field f = Class.forName("android.widget.TextView").getDeclaredField("mCursorDrawableRes"); + f.setAccessible(true); + f.setInt(searchText, searchCursorID); + } catch (Exception e) { + } + } + + searchText.setTextColor(Color.WHITE); + searchText.setHintTextColor(Color.WHITE); + + Drawable searchImage = mResources.getDrawable(R.drawable.ic_action_search); + if (searchImage != null) { + SpannableStringBuilder ssb = new SpannableStringBuilder(" "); + ssb.append(getString(R.string.advance_search_left)); + int textSize = (int) (searchText.getTextSize() * 1.25); + searchImage.setBounds(0, 0, textSize, textSize); + ssb.setSpan(new ImageSpan(searchImage), 1, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + searchText.setHint(ssb); + } + } + + int plateViewID = mResources.getIdentifier("android:id/search_plate", null, null); + View plateView = null; + if (plateViewID > 0 && (plateView = mSearchView.findViewById(plateViewID)) != null) { + plateView.setBackgroundResource(R.drawable.textfield_searchview); + } + + int plateRightViewID = mResources.getIdentifier("android:id/submit_area", null, null); + View plateRightView = null; + if (plateRightViewID > 0 && (plateRightView = mSearchView.findViewById(plateRightViewID)) != null) { + plateRightView.setBackgroundResource(R.drawable.textfield_searchview_right); + } + + int closeViewID = mResources.getIdentifier("android:id/search_close_btn", null, null); + ImageView closeView = null; + Drawable closeImage = mResources.getDrawable(R.drawable.ic_action_close); + if (closeViewID > 0 && (closeView = (ImageView) mSearchView.findViewById(closeViewID)) != null + && closeImage != null) { + closeView.setImageDrawable(closeImage); + } + + int voiceViewID = mResources.getIdentifier("android:id/search_voice_btn", null, null); + ImageView voiceView = null; + Drawable voiceImage = mResources.getDrawable(R.drawable.ic_action_voice_search); + if (voiceViewID > 0 && (voiceView = (ImageView) mSearchView.findViewById(voiceViewID)) != null + && voiceImage != null) { + voiceView.setImageDrawable(voiceImage); + } + + // Hide some menu item + if (mGalleryListView.getListMode() == GalleryListView.LIST_MODE_DETAIL) + menu.removeItem(R.id.action_detail); + else if (mGalleryListView.getListMode() == GalleryListView.LIST_MODE_THUMB) + menu.removeItem(R.id.action_thumb); + + return true; + } + + // Double click back exit + @Override + public void onBackPressed() { + if (mDrawerLayout.isDrawerOpen(mLeftMenu) || mDrawerLayout.isDrawerOpen(mRightMenu)) { + mDrawerLayout.closeDrawers(); + } else { + if (!mShowDrawer) { + finish(); + } else { + if (System.currentTimeMillis() - curBackTime > BACK_PRESSED_INTERVAL) { + curBackTime = System.currentTimeMillis(); + MaterialToast.showToast(R.string.exit_tip); + } else + finish(); + } + } + } + + private void jump() { + createJumpDialog().show(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle item selection + switch (item.getItemId()) { + case android.R.id.home: + if (mShowDrawer) { + if (mDrawerLayout.isDrawerOpen(mLeftMenu)) + mDrawerLayout.closeDrawers(); + else + mDrawerLayout.openDrawer(mLeftMenu); + } else { + finish(); + } + return true; + case R.id.action_refresh: + mGalleryListView.refresh(); + return true; + case R.id.action_jump: + if (!mGalleryListView.isRefreshing() && mGalleryListView.isGetGalleryOk()) + jump(); + return true; + case R.id.action_detail: + mGalleryListView.setListMode(GalleryListView.LIST_MODE_DETAIL); + Config.setListMode(GalleryListView.LIST_MODE_DETAIL); + invalidateOptionsMenu(); + return true; + case R.id.action_thumb: + mGalleryListView.setListMode(GalleryListView.LIST_MODE_THUMB); + Config.setListMode(GalleryListView.LIST_MODE_THUMB); + invalidateOptionsMenu(); + return true; + case R.id.action_add: + // TODO + if (lus != null) { + View view = ViewUtils.inflateDialogView(R.layout.set_name, false); + final EditText et = (EditText) view.findViewById(R.id.set_name_edit); + new MaterialAlertDialog.Builder(this).setTitle("添加当前状态至快速搜索") // TODO + .setView(view, true) + .setDefaultButton(MaterialAlertDialog.POSITIVE | MaterialAlertDialog.NEGATIVE) + .setButtonListener(new MaterialAlertDialog.OnClickListener() { + @Override + public boolean onClick(MaterialAlertDialog dialog, int which) { + if (which == MaterialAlertDialog.POSITIVE) { + String name = et.getText().toString(); + if (name == null || name.isEmpty()) { + MaterialToast.showToast(R.string.invalid_input); + return false; + } + if (listMenuTag.contains(name)) { + MaterialToast.showToast("该名称已存在"); // TODO + return false; + } + mData.addTag(new Tag(name, lus)); + listMenuTag.add(name); + tagsAdapter.addId(name); + tagsAdapter.notifyDataSetChanged(); + } + return true; + } + }).show(); + } + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + mWindowsAnimate.free(); + } + + private void checkUpdate() { + new UpdateHelper((AppContext) getApplication()).SetOnCheckUpdateListener( + new UpdateHelper.OnCheckUpdateListener() { + @Override + public void onSuccess(String version, long size, final String url, final String fileName, + String info) { + String sizeStr = Utils.sizeToString(size); + AlertDialog dialog = DialogUtils.createUpdateDialog(GalleryListActivity.this, version, sizeStr, + info, new MaterialAlertDialog.OnClickListener() { + @Override + public boolean onClick(MaterialAlertDialog dialog, int which) { + if (which == MaterialAlertDialog.POSITIVE) { + HttpHelper hh = new HttpHelper(GalleryListActivity.this); + hh.downloadInThread(url, new File(Config.getDownloadPath()), fileName, + false, null, new UpdateHelper.UpdateListener( + GalleryListActivity.this, fileName)); + } + return true; + } + }).create(); + if (!GalleryListActivity.this.isFinishing()) + dialog.show(); + } + + @Override + public void onNoUpdate() { + UpdateHelper.setEnabled(true); + } + + @Override + public void onFailure(String eMsg) { + UpdateHelper.setEnabled(true); + } + }).autoCheckUpdate(); + } + + private static final int LOGIN = 0x0; + private static final int LOGOUT = 0x1; + private static final int WAIT = 0x2; + + private void setUserPanel() { + if (mClient.isLogin()) + setUserPanel(LOGOUT); + else + setUserPanel(LOGIN); + } + + private void setUserPanel(int state) { + + switch (state) { + case LOGIN: + mAvatar.setImageDrawable(mClient.getAvatar()); + mUsernameView.setVisibility(View.GONE); + mLoginButton.setVisibility(View.VISIBLE); + mRegisterButton.setVisibility(View.VISIBLE); + mLogoutButton.setVisibility(View.GONE); + mWaitLogView.setVisibility(View.GONE); + break; + case LOGOUT: + mAvatar.setImageDrawable(mClient.getAvatar()); + mUsernameView.setText(mClient.getDisplayname()); + mUsernameView.setVisibility(View.VISIBLE); + mLoginButton.setVisibility(View.GONE); + mRegisterButton.setVisibility(View.GONE); + mLogoutButton.setVisibility(View.VISIBLE); + mWaitLogView.setVisibility(View.GONE); + break; + case WAIT: + mAvatar.setImageDrawable(mClient.getAvatar()); + mUsernameView.setVisibility(View.GONE); + mLoginButton.setVisibility(View.GONE); + mRegisterButton.setVisibility(View.GONE); + mLogoutButton.setVisibility(View.GONE); + mWaitLogView.setVisibility(View.VISIBLE); + break; + } + } + + @Override + public String getTargetUrl(int targetPage) { + lus.setPage(targetPage); + return lus.getUrl(); + } + + @Override + public void doGetGallerys(String url, final long taskStamp, final OnGetListListener listener) { + if (lus.getMode() == ListUrls.MODE_IMAGE_SEARCH) { + if (url == null) { + // No result url + if (lus.getSearchFile() == null) { + ImageLoader.getInstance(GalleryListActivity.this).add(lus.getSearchImageUrl(), + lus.getSearchImageKey(), new ImageLoader.OnGetImageListener() { + @Override + public void onGetImage(String key, Bitmap bmp, int state) { + if (bmp != null) { + mClient.getGListFromImageSearch(bmp, lus.getImageSearchMode(), taskStamp, + new EhClient.OnGetGListFromImageSearchListener() { + @Override + public void onSuccess(Object checkFlag, List giList, + int maxPage, String newUrl) { + + System.out.println(giList.size()); + + lus.setSearchResult(newUrl); + listener.onSuccess(taskStamp, giList, maxPage); + } + + @Override + public void onFailure(Object checkFlag, String eMsg) { + listener.onFailure(taskStamp, eMsg); // TODO + } + }); + } else { + listener.onFailure(taskStamp, "~~~~~~~~~~"); // TODO + } + } + }); + } else { + // File search + mClient.getGListFromImageSearch(lus.getSearchFile(), lus.getImageSearchMode(), taskStamp, + new EhClient.OnGetGListFromImageSearchListener() { + @Override + public void onSuccess(Object checkFlag, List giList, int maxPage, + String newUrl) { + lus.setSearchResult(newUrl); + listener.onSuccess(taskStamp, giList, maxPage); + } + + @Override + public void onFailure(Object checkFlag, String eMsg) { + listener.onFailure(taskStamp, eMsg); + } + }); + } + } else { + // Get result url + mClient.getGList(url, null, new EhClient.OnGetGListListener() { + @Override + public void onSuccess(Object checkFlag, List lmdArray, int pageNum) { + listener.onSuccess(taskStamp, lmdArray, pageNum); + } + + @Override + public void onFailure(Object checkFlag, String eMsg) { + listener.onFailure(taskStamp, eMsg); + } + }); + } + } else if (lus.getMode() == ListUrls.MODE_POPULAR) { + mClient.getPopular(new EhClient.OnGetPopularListener() { + @Override + public void onSuccess(List gis, long timeStamp) { + listener.onSuccess(taskStamp, gis, gis.size() == 0 ? 0 : 1); + // Show update time + if (timeStamp != -1 && Config.getShowPopularUpdateTime()) + MaterialToast.showToast(String.format(getString(R.string.popular_update_time), + Utils.sDate.format(timeStamp))); + } + + @Override + public void onFailure(String eMsg) { + listener.onFailure(taskStamp, eMsg); + } + }); + } else { + mClient.getGList(url, null, new EhClient.OnGetGListListener() { + @Override + public void onSuccess(Object checkFlag, List lmdArray, int pageNum) { + listener.onSuccess(taskStamp, lmdArray, pageNum); + } + + @Override + public void onFailure(Object checkFlag, String eMsg) { + listener.onFailure(taskStamp, eMsg); + } + }); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/ui/HistoryActivity.java b/app/src/main/java/com/hippo/ehviewer/ui/HistoryActivity.java new file mode 100644 index 000000000..2a175e960 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ui/HistoryActivity.java @@ -0,0 +1,238 @@ +/* + * 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.ui; + +import java.util.ArrayList; +import java.util.List; + +import android.annotation.SuppressLint; +import android.app.Dialog; +import android.content.Intent; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.CheckBox; + +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.app.MaterialAlertDialog; +import com.hippo.ehviewer.data.Data; +import com.hippo.ehviewer.data.GalleryInfo; +import com.hippo.ehviewer.drawable.MaterialIndicatorDrawable; +import com.hippo.ehviewer.drawable.MaterialIndicatorDrawable.Stroke; +import com.hippo.ehviewer.util.Config; +import com.hippo.ehviewer.util.Ui; +import com.hippo.ehviewer.util.ViewUtils; +import com.hippo.ehviewer.widget.FitWindowView; +import com.hippo.ehviewer.widget.GalleryListView; +import com.hippo.ehviewer.widget.GalleryListView.OnGetListListener; +import com.hippo.ehviewer.widget.recyclerview.EasyRecyclerView; + +public class HistoryActivity extends AbsTranslucentActivity + implements EasyRecyclerView.OnItemClickListener, MaterialAlertDialog.OnClickListener, + GalleryListView.GalleryListViewHelper, FitWindowView.OnFitSystemWindowsListener { + + private static final String TAG = HistoryActivity.class.getSimpleName(); + + public static final int LIST_MODE_DETAIL = 0; + public static final int LIST_MODE_THUMB = 1; + + private Data mData; + private int mThemeColor; + + private FitWindowView mStandard; + private GalleryListView mGalleryListView; + + private int mFilterMode; + + private Dialog mFilterDialog; + private CheckBox mJustBrowse; + private CheckBox mHaveRead; + + private Dialog mClearDialog; + + private static final String HISTORY_URL = "ehviewer://history"; + private static final String KEY_HISTORY_FILTER = "history_filter"; + + private void setFilterDialogMode(int filterMode) { + if ((filterMode & Data.BROWSE) == 0) + mJustBrowse.setChecked(false); + else + mJustBrowse.setChecked(true); + if ((filterMode & Data.READ) == 0) + mHaveRead.setChecked(false); + else + mHaveRead.setChecked(true); + } + + private int getFilterDialogMode() { + int filterMode = 0; + filterMode |= mJustBrowse.isChecked() ? Data.BROWSE : 0; + filterMode |= mHaveRead.isChecked() ? Data.READ : 0; + return filterMode; + } + + @Override + public boolean onClick(MaterialAlertDialog dialog, int which) { + if (dialog == mFilterDialog) { + switch (which) { + case MaterialAlertDialog.POSITIVE: + int newFilterMode = getFilterDialogMode(); + if (newFilterMode != mFilterMode) { + mFilterMode = newFilterMode; + Config.setInt(KEY_HISTORY_FILTER, mFilterMode); + mGalleryListView.refresh(); + } + } + return true; + + } else if (dialog == mClearDialog) { + switch (which) { + case MaterialAlertDialog.POSITIVE: + Data.getInstance().clearHistory(); + mGalleryListView.refresh(); + } + return true; + + } + return true; + } + + @SuppressLint("InflateParams") + private void createFilterDialog() { + View view = ViewUtils.inflateDialogView(R.layout.history_filter, false); + mJustBrowse = (CheckBox) view.findViewById(R.id.just_browse); + mHaveRead = (CheckBox) view.findViewById(R.id.have_read); + + mFilterDialog = new MaterialAlertDialog.Builder(this).setTitle(R.string.filter) + .setView(view, true) + .setDefaultButton(MaterialAlertDialog.POSITIVE | MaterialAlertDialog.NEGATIVE) + .setButtonListener(this).create(); + } + + private void createClearDialog() { + mClearDialog = new MaterialAlertDialog.Builder(this).setTitle(R.string.attention) + .setMessage(R.string.clear_history_message) + .setDefaultButton(MaterialAlertDialog.POSITIVE | MaterialAlertDialog.NEGATIVE) + .setButtonListener(this).create(); + } + + @Override + public void onFitSystemWindows(int l, int t, int r, int b) { + mGalleryListView.setPaddingTopAndBottom(t, b); + Ui.colorStatusBarKK(this, mThemeColor, t - Ui.ACTION_BAR_HEIGHT); + } + + @Override + protected void onResume() { + super.onResume(); + + mGalleryListView.refresh(); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.history); + + mData = Data.getInstance(); + mFilterMode = Config.getInt(KEY_HISTORY_FILTER, Data.BROWSE | Data.READ); + + // Menu + MaterialIndicatorDrawable materialIndicator = new MaterialIndicatorDrawable(this, Color.WHITE, Stroke.THIN); + materialIndicator.setIconState(MaterialIndicatorDrawable.IconState.ARROW); + Ui.setMaterialIndicator(getActionBar(), materialIndicator); + + // Theme + mThemeColor = Config.getCustomThemeColor() ? Config.getThemeColor() : Ui.THEME_COLOR; + getActionBar().setBackgroundDrawable(new ColorDrawable(mThemeColor)); + Ui.colorStatusBarL(this, mThemeColor); + + mStandard = (FitWindowView) findViewById(R.id.standard); + mGalleryListView = (GalleryListView) findViewById(R.id.gallery_list); + + mGalleryListView.setGalleryListViewHelper(this); + mStandard.addOnFitSystemWindowsListener(this); + + // Do not need header an footer + mGalleryListView.setEnabledHeader(false); + mGalleryListView.setEnabledFooter(false); + mGalleryListView.setOnItemClickListener(this); + + createFilterDialog(); + createClearDialog(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.history, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + finish(); + return true; + + case R.id.action_filter: + if (!mFilterDialog.isShowing()) { + setFilterDialogMode(mFilterMode); + mFilterDialog.show(); + } + return true; + + case R.id.action_clear: + if (!mClearDialog.isShowing()) + mClearDialog.show(); + return true; + + default: + return super.onOptionsItemSelected(item); + } + } + + @Override + public String getTargetUrl(int targetPage) { + return HISTORY_URL; + } + + @Override + public void doGetGallerys(String url, long taskStamp, + OnGetListListener listener) { + if (url.equals(HISTORY_URL)) { + List giList = new ArrayList(mData.getHistory(mFilterMode, true)); + if (giList == null || giList.size() == 0) + listener.onSuccess(taskStamp, giList, 0); + else + listener.onSuccess(taskStamp, giList, 1); + } + } + + @Override + public boolean onItemClick(EasyRecyclerView parent, View view, + int position, long id) { + Intent intent = new Intent(this, GalleryDetailActivity.class); + GalleryInfo gi = mGalleryListView.getGalleryInfo(position); + intent.putExtra(GalleryDetailActivity.KEY_G_INFO, gi); + startActivity(intent); + return true; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/ui/SecretActivity.java b/app/src/main/java/com/hippo/ehviewer/ui/SecretActivity.java new file mode 100644 index 000000000..9f3d82c7e --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ui/SecretActivity.java @@ -0,0 +1,81 @@ +/* + * 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.ui; + + +import java.util.Arrays; + +import android.graphics.Bitmap; +import android.os.Bundle; +import android.os.SystemClock; +import android.view.View; +import android.widget.ImageView; + +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.effect.DrawableTransition; +import com.hippo.ehviewer.util.Config; +import com.hippo.ehviewer.util.Secret; +import com.hippo.ehviewer.util.Ui; +import com.hippo.ehviewer.widget.MaterialToast; + +public class SecretActivity extends AbsActivity implements View.OnClickListener { + + private final long[] mHits = new long[3]; + private Bitmap mBmp; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mBmp = Secret.getSecretImage(this); + if (mBmp == null) { + finish(); + return; + } + + int themeColor = Config.getCustomThemeColor() ? Config.getThemeColor() : Ui.THEME_COLOR; + Ui.colorStatusBarL(this, themeColor); + + ImageView iv = new ImageView(this); + iv.setScaleType(ImageView.ScaleType.CENTER_CROP); + iv.setImageBitmap(mBmp); + iv.setOnClickListener(this); + iv.setSoundEffectsEnabled(false); + setContentView(iv); + DrawableTransition.transit(iv.getDrawable(), false, 3000); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + if (mBmp != null) { + mBmp.recycle(); + mBmp = null; + } + } + + @Override + public void onClick(View v) { + System.arraycopy(mHits, 1, mHits, 0, mHits.length-1); + mHits[mHits.length-1] = SystemClock.uptimeMillis(); + if (mHits[0] >= (SystemClock.uptimeMillis() - 500)) { + Arrays.fill(mHits, 0); + MaterialToast.showToast(R.string.photo_footnotes); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/ui/SettingsActivity.java b/app/src/main/java/com/hippo/ehviewer/ui/SettingsActivity.java new file mode 100644 index 000000000..3cff5379f --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ui/SettingsActivity.java @@ -0,0 +1,921 @@ +/* + * 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.ui; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.SystemClock; +import android.preference.CheckBoxPreference; +import android.preference.Preference; +import android.preference.PreferenceFragment; +import android.provider.SearchRecentSuggestions; +import android.text.util.Linkify; +import android.view.MenuItem; +import android.webkit.WebView; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TableLayout; + +import com.hippo.ehviewer.AppContext; +import com.hippo.ehviewer.AppHandler; +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.SimpleSuggestionProvider; +import com.hippo.ehviewer.UpdateHelper; +import com.hippo.ehviewer.app.DirSelectDialog; +import com.hippo.ehviewer.app.MaterialAlertDialog; +import com.hippo.ehviewer.app.MaterialProgressDialog; +import com.hippo.ehviewer.data.ApiGalleryInfo; +import com.hippo.ehviewer.data.Data; +import com.hippo.ehviewer.data.DownloadInfo; +import com.hippo.ehviewer.drawable.MaterialIndicatorDrawable; +import com.hippo.ehviewer.drawable.MaterialIndicatorDrawable.Stroke; +import com.hippo.ehviewer.ehclient.EhClient; +import com.hippo.ehviewer.ehclient.EhInfo; +import com.hippo.ehviewer.network.HttpHelper; +import com.hippo.ehviewer.preference.ListPreference; +import com.hippo.ehviewer.util.BgThread; +import com.hippo.ehviewer.util.Config; +import com.hippo.ehviewer.util.DialogUtils; +import com.hippo.ehviewer.util.EhUtils; +import com.hippo.ehviewer.util.Favorite; +import com.hippo.ehviewer.util.Secret; +import com.hippo.ehviewer.util.Ui; +import com.hippo.ehviewer.util.Utils; +import com.hippo.ehviewer.util.ViewUtils; +import com.hippo.ehviewer.widget.CategoryTable; +import com.hippo.ehviewer.widget.MaterialToast; +import com.hippo.ehviewer.widget.SuggestionHelper; + +public class SettingsActivity extends AbsPreferenceActivity { + @SuppressWarnings("unused") + private static String TAG = SettingsActivity.class.getSimpleName(); + + private int mThemeColor; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Set random color + mThemeColor = Config.getCustomThemeColor() ? Config.getThemeColor() : Ui.THEME_COLOR; + getActionBar().setBackgroundDrawable(new ColorDrawable(mThemeColor)); + Ui.colorStatusBarL(this, mThemeColor); + + // Menu + MaterialIndicatorDrawable materialIndicator = new MaterialIndicatorDrawable(this, Color.WHITE, Stroke.THIN); + materialIndicator.setIconState(MaterialIndicatorDrawable.IconState.ARROW); + Ui.setMaterialIndicator(getActionBar(), materialIndicator); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + finish(); + return true; + } + + return super.onOptionsItemSelected(item); + } + + @Override + public void onBuildHeaders(List
target) { + loadHeadersFromResource(R.xml.settings_headers, target); + } + + private static final String[] ENTRY_FRAGMENTS = { DisplayFragment.class.getName(), EhFragment.class.getName(), + ReadFragment.class.getName(), DownloadFragment.class.getName(), AdvancedFragment.class.getName(), + AboutFragment.class.getName() }; + + private static final int[] FRAGMENT_ICONS = { R.drawable.ic_setting_display, R.drawable.ic_setting_panda_light, + R.drawable.ic_setting_read, R.drawable.ic_setting_download, R.drawable.ic_setting_advanced, + R.drawable.ic_setting_about }; + + @Override + protected boolean isValidFragment(String fragmentName) { + for (int i = 0; i < ENTRY_FRAGMENTS.length; i++) { + if (ENTRY_FRAGMENTS[i].equals(fragmentName)) + return true; + } + return false; + } + + public static class DisplayFragment extends PreferenceFragment implements + Preference.OnPreferenceChangeListener { + + private static final String KEY_SCREEN_ORIENTATION = "screen_orientation"; + + private ListPreference mScreenOrientation; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.display_settings); + + mScreenOrientation = (ListPreference) findPreference(KEY_SCREEN_ORIENTATION); + mScreenOrientation.setOnPreferenceChangeListener(this); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final String key = preference.getKey(); + if (KEY_SCREEN_ORIENTATION.equals(key)) { + getActivity().setRequestedOrientation(Config.screenOriPre2Value(Integer.parseInt((String) newValue))); + + } + + return true; + } + } + + public static class EhFragment extends PreferenceFragment implements + Preference.OnPreferenceChangeListener, Preference.OnPreferenceClickListener { + + private static final int[] EXCULDE_TAG_GROUP_RESID = { R.id.tag_group_reclass, R.id.tag_group_language, + R.id.tag_group_parody, R.id.tag_group_character, R.id.tag_group_group, R.id.tag_group_artist, + R.id.tag_group_male, R.id.tag_group_female }; + + private static final int[] EXCULDE_TAG_GROUP_ID = { 0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80 }; + + private static final int[] EXCULDE_LANGUAGE_RESID = { R.id.el_japanese_translated, R.id.el_japanese_rewrite, + R.id.el_english_original, R.id.el_english_translated, R.id.el_english_rewrite, + R.id.el_chinese_original, R.id.el_chinese_translated, R.id.el_chinese_rewrite, R.id.el_dutch_original, + R.id.el_dutch_translated, R.id.el_dutch_rewrite, R.id.el_french_original, R.id.el_french_translated, + R.id.el_french_rewrite, R.id.el_german_original, R.id.el_german_translated, R.id.el_german_rewrite, + R.id.el_hungarian_original, R.id.el_hungarian_translated, R.id.el_hungarian_rewrite, + R.id.el_italian_original, R.id.el_italian_translated, R.id.el_italian_rewrite, R.id.el_korean_original, + R.id.el_korean_translated, R.id.el_korean_rewrite, R.id.el_polish_original, R.id.el_polish_translated, + R.id.el_polish_rewrite, R.id.el_portuguese_original, R.id.el_portuguese_translated, + R.id.el_portuguese_rewrite, R.id.el_russian_original, R.id.el_russian_translated, + R.id.el_russian_rewrite, R.id.el_spanish_original, R.id.el_spanish_translated, R.id.el_spanish_rewrite, + R.id.el_thai_original, R.id.el_thai_translated, R.id.el_thai_rewrite, R.id.el_vietnamese_original, + R.id.el_vietnamese_translated, R.id.el_vietnamese_rewrite, R.id.el_other_original, + R.id.el_other_translated, R.id.el_other_rewrite, }; + + private static final String[] EXCULDE_LANGUAGE_ID = { "1024", "2048", "1", "1025", "2049", "10", "1034", + "2058", "20", "1044", "2068", "30", "1054", "2078", "40", "1064", "2088", "50", "1074", "2098", "60", + "1084", "2108", "70", "1094", "2118", "80", "1104", "2128", "90", "1114", "2138", "100", "1124", + "2148", "110", "1134", "2158", "120", "1144", "2168", "130", "1154", "2178", "255", "1279", "2303" }; + + private static final String KEY_LIST_DEFAULT_CATEGORY = "list_default_category"; + private static final String KEY_EXCULDE_TAG_GROUP = "exculde_tag_group"; + private static final String KEY_EXCULDE_LANGUAGE = "exculde_language"; + private static final String KEY_CLEAR_SUGGESTIONS = "clear_suggestions"; + private static final String KEY_PREVIEW_MODE = "preview_mode"; + private static final String KEY_DEFAULT_FAVORITE = "default_favorite"; + + private Preference mListDefaultCategory; + private Preference mExculdeTagGroup; + private Preference mExculdeLanguage; + private Preference mClearSuggestions; + private ListPreference mPreviewMode; + private ListPreference mDefaultFavorite; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.eh_settings); + + mListDefaultCategory = findPreference(KEY_LIST_DEFAULT_CATEGORY); + mListDefaultCategory.setOnPreferenceClickListener(this); + mExculdeTagGroup = findPreference(KEY_EXCULDE_TAG_GROUP); + mExculdeTagGroup.setOnPreferenceClickListener(this); + mExculdeLanguage = findPreference(KEY_EXCULDE_LANGUAGE); + mExculdeLanguage.setOnPreferenceClickListener(this); + mClearSuggestions = findPreference(KEY_CLEAR_SUGGESTIONS); + mClearSuggestions.setOnPreferenceClickListener(this); + mPreviewMode = (ListPreference) findPreference(KEY_PREVIEW_MODE); + mPreviewMode.setOnPreferenceChangeListener(this); + mDefaultFavorite = (ListPreference) findPreference(KEY_DEFAULT_FAVORITE); + + int i = 0; + String[] entrise = new String[Favorite.FAVORITE_TITLES.length + 1]; + entrise[i++] = getString(R.string.none); + for (String str : Favorite.FAVORITE_TITLES) + entrise[i++] = str; + mDefaultFavorite.setEntries(entrise); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final String key = preference.getKey(); + if (KEY_PREVIEW_MODE.equals(key)) { + String newPreviewMode = (String) newValue; + EhInfo.getInstance(getActivity()).setPreviewMode(newPreviewMode); + } + return true; + } + + @SuppressLint("InflateParams") + @Override + public boolean onPreferenceClick(Preference preference) { + final String key = preference.getKey(); + if (KEY_LIST_DEFAULT_CATEGORY.equals(key)) { + int defaultCat = Config.getDefaultCat(); + final CategoryTable ct = new CategoryTable(getActivity()); + ct.setCategory(defaultCat); + + new MaterialAlertDialog.Builder(getActivity()).setTitle(R.string.list_default_category_title) + .setView(ct, true).setPositiveButton(android.R.string.ok) + .setNegativeButton(android.R.string.cancel) + .setButtonListener(new MaterialAlertDialog.OnClickListener() { + @Override + public boolean onClick(MaterialAlertDialog dialog, int which) { + if (which == MaterialAlertDialog.POSITIVE) { + int defaultCat = ct.getCategory(); + Config.setDefaultCat(defaultCat); + EhInfo.getInstance(getActivity()).setDefaultCat(defaultCat); + } + return true; + } + }).show(); + + } else if (KEY_EXCULDE_TAG_GROUP.equals(key)) { + final TableLayout tl = (TableLayout) ViewUtils.inflateDialogView(R.layout.exculde_tag_group, false); + setExculdeTagGroup(tl, Config.getExculdeTagGroup()); + + new MaterialAlertDialog.Builder(getActivity()).setTitle(R.string.exculde_tag_group_title).setView(tl, true) + .setPositiveButton(android.R.string.ok).setNegativeButton(android.R.string.cancel) + .setButtonListener(new MaterialAlertDialog.OnClickListener() { + @Override + public boolean onClick(MaterialAlertDialog dialog, int which) { + int newValue = getExculdeTagGroup(tl); + Config.setExculdeTagGroup(newValue); + EhInfo.getInstance(getActivity()).setExculdeTagGroup(newValue); + return true; + } + }).show(); + + } else if (KEY_EXCULDE_LANGUAGE.equals(key)) { + final TableLayout tl = (TableLayout) ViewUtils.inflateDialogView(R.layout.exculde_language, false); + setExculdeLanguage(tl, Config.getExculdeLanguage()); + + new MaterialAlertDialog.Builder(getActivity()).setTitle(R.string.exculde_language_title).setView(tl, true) + .setPositiveButton(android.R.string.ok).setNegativeButton(android.R.string.cancel) + .setButtonListener(new MaterialAlertDialog.OnClickListener() { + @Override + public boolean onClick(MaterialAlertDialog dialog, int which) { + String newValue = getExculdeLanguage(tl); + Config.setExculdeLanguage(newValue); + EhInfo.getInstance(getActivity()).setExculdeLanguage(newValue); + return true; + } + }).show(); + + } else if (KEY_CLEAR_SUGGESTIONS.equals(key)) { + SearchRecentSuggestions suggestions = SuggestionHelper.getInstance(getActivity(), + SimpleSuggestionProvider.AUTHORITY, SimpleSuggestionProvider.MODE); + suggestions.clearHistory(); + } + return true; + } + + private static void setExculdeTagGroup(TableLayout tl, int value) { + for (int i = 0; i < EXCULDE_TAG_GROUP_RESID.length; i++) { + CheckBox cb = (CheckBox) tl.findViewById(EXCULDE_TAG_GROUP_RESID[i]); + cb.setChecked(Utils.int2boolean(value & EXCULDE_TAG_GROUP_ID[i])); + } + } + + private static int getExculdeTagGroup(TableLayout tl) { + int newValue = 0; + for (int i = 0; i < EXCULDE_TAG_GROUP_RESID.length; i++) { + CheckBox cb = (CheckBox) tl.findViewById(EXCULDE_TAG_GROUP_RESID[i]); + if (cb.isChecked()) + newValue |= EXCULDE_TAG_GROUP_ID[i]; + } + return newValue; + } + + private static int getLanguage(String id) { + for (int i = 0; i < EXCULDE_LANGUAGE_ID.length; i++) { + if (EXCULDE_LANGUAGE_ID[i].equals(id)) + return EXCULDE_LANGUAGE_RESID[i]; + } + return 0; + } + + private static void setExculdeLanguage(TableLayout tl, String value) { + String[] items = value.split("x"); + for (String item : items) { + int resId = getLanguage(item); + if (resId != 0) + ((CheckBox) tl.findViewById(resId)).setChecked(true); + } + } + + private static String getExculdeLanguage(TableLayout tl) { + StringBuilder sb = new StringBuilder(); + boolean isFirst = true; + for (int i = 0; i < EXCULDE_LANGUAGE_RESID.length; i++) { + if (((CheckBox) tl.findViewById(EXCULDE_LANGUAGE_RESID[i])).isChecked()) { + if (isFirst) + isFirst = false; + else + sb.append('x'); + sb.append(EXCULDE_LANGUAGE_ID[i]); + } + } + return sb.toString(); + } + } + + public static class ReadFragment extends PreferenceFragment implements + Preference.OnPreferenceClickListener { + + private static final String KEY_CUSTOM_CODEC = "custom_codec"; + private static final String KEY_CLEAN_REDUNDANCY = "clean_redundancy"; + + private CheckBoxPreference mCustomCodec; + private Preference mCleanRedundancy; + + private MaterialProgressDialog mMaterialProgressDialog; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.read_settings); + + mCustomCodec = (CheckBoxPreference) findPreference(KEY_CUSTOM_CODEC); + mCleanRedundancy = findPreference(KEY_CLEAN_REDUNDANCY); + mCleanRedundancy.setOnPreferenceClickListener(this); + + if (!Utils.SUPPORT_IMAGE) { + mCustomCodec.setEnabled(false); + mCustomCodec.setChecked(Config.getCustomCodec()); + mCustomCodec.setSummary(R.string.custom_codec_summary_not); + } else { + mCustomCodec.setChecked(Config.getCustomCodec()); + } + } + + private boolean isInDownloadList(List diList, String filename) { + for (DownloadInfo di : diList) + if (filename.startsWith(String.valueOf(di.galleryInfo.gid))) + return true; + return false; + } + + private class CleanResponder implements Runnable { + + public static final int STATE_NONE = 0x0; + public static final int STATE_START = 0x1; + public static final int STATE_DOING = 0x2; + public static final int STATE_DONE = 0x3; + + private final int mState; + private final int mMax; + private final int mProgress; + + public CleanResponder(int state, int max, int progress) { + mState = state; + mMax = max; + mProgress = progress; + } + + @Override + public void run() { + + switch (mState) { + case STATE_NONE: + MaterialToast.showToast(R.string.no_redundancy); + break; + case STATE_START: + if (!getActivity().isFinishing()) { + mMaterialProgressDialog = MaterialProgressDialog.create(getActivity(), getActivity().getString(R.string.clean_redundancy_title), false); + mMaterialProgressDialog.show(); + } + break; + case STATE_DOING: + if (mMaterialProgressDialog != null) { + mMaterialProgressDialog.setMax(mMax); + mMaterialProgressDialog.setProgress(mProgress); + } + break; + case STATE_DONE: + if (mMaterialProgressDialog != null) { + mMaterialProgressDialog.dismiss(); + MaterialToast.showToast(String.format(getString(R.string.clean_redundancy), mMax)); + } + mMaterialProgressDialog = null; + break; + } + } + } + + @Override + public boolean onPreferenceClick(Preference preference) { + final String key = preference.getKey(); + if (KEY_CLEAN_REDUNDANCY.equals(key)) { + + // Create a thread to do clean task + new BgThread() { + @Override + public void run() { + Data data = Data.getInstance(); + List diList = data.getAllDownloads(); + File downloadDir = new File(Config.getDownloadPath()); + String[] files = downloadDir.list(); + Handler handler = AppHandler.getInstance(); + List targetDirList = new ArrayList(); + + if (files == null) { + // Check files null + handler.post(new CleanResponder(CleanResponder.STATE_NONE, 0, 0)); + return; + } + + for (String filename : files) { + // If in download list, just continue + if (isInDownloadList(diList, filename)) + continue; + File dir = new File(downloadDir, filename); + // If there is no tag file, just continue + if (!new File(dir, EhUtils.EH_DOWNLOAD_FILENAME).exists()) + continue; + // Add to list + targetDirList.add(dir); + } + + if (targetDirList.isEmpty()) { + handler.post(new CleanResponder(CleanResponder.STATE_NONE, 0, 0)); + return; + } else { + handler.post(new CleanResponder(CleanResponder.STATE_START, 0, 0)); + } + // Do delete + for (int i = 0; i < targetDirList.size(); i++) { + File dir = targetDirList.get(i); + Utils.deleteFile(dir); + handler.post(new CleanResponder(CleanResponder.STATE_DOING, targetDirList.size(), i + 1)); + } + // Close windows + handler.post(new CleanResponder(CleanResponder.STATE_DONE, targetDirList.size(), targetDirList + .size())); + } + }.start(); + } + return true; + } + } + + public static class DownloadFragment extends PreferenceFragment implements + Preference.OnPreferenceChangeListener, Preference.OnPreferenceClickListener { + + private static final String KEY_DOWNLOAD_PATH = "download_path"; + private static final String KEY_MEDIA_SCAN = "media_scan"; + + private DirSelectDialog mDirSelectDialog; + + private Preference mDownloadPath; + private CheckBoxPreference mMediaScan; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.download_settings); + + mDownloadPath = findPreference(KEY_DOWNLOAD_PATH); + mDownloadPath.setOnPreferenceClickListener(this); + mMediaScan = (CheckBoxPreference) findPreference(KEY_MEDIA_SCAN); + mMediaScan.setOnPreferenceChangeListener(this); + + mDownloadPath.setSummary(Config.getDownloadPath()); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final String key = preference.getKey(); + if (KEY_MEDIA_SCAN.equals(key)) { + boolean value = (Boolean) newValue; + File nomedia = new File(Config.getDownloadPath(), ".nomedia"); + if (value) { + nomedia.delete(); + } else { + try { + nomedia.createNewFile(); + } catch (IOException e) { + } + } + } + return true; + } + + @Override + @SuppressLint("InflateParams") + public boolean onPreferenceClick(Preference preference) { + final String key = preference.getKey(); + if (KEY_DOWNLOAD_PATH.equals(key)) { + mDirSelectDialog = DirSelectDialog.create(new MaterialAlertDialog.Builder(getActivity()) + .setTitle(R.string.download_path_title) + .setDefaultButton(MaterialAlertDialog.POSITIVE | MaterialAlertDialog.NEGATIVE) + .setActionButton(R.string._new) + .setButtonListener(new MaterialAlertDialog.OnClickListener() { + @Override + public boolean onClick(MaterialAlertDialog dialog, + int which) { + switch (which) { + case MaterialAlertDialog.ACTION: + final EditText et = new EditText(getActivity()); + et.setText(R.string.new_folder); + LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + int x = Ui.dp2pix(8); + lp.leftMargin = x; + lp.rightMargin = x; + lp.topMargin = x; + lp.bottomMargin = x; + new MaterialAlertDialog.Builder(getActivity()).setTitle(R.string.new_folder) + .setView(et, true, lp).setPositiveButton(R.string._new) + .setNegativeButton(android.R.string.cancel) + .setButtonListener(new MaterialAlertDialog.OnClickListener() { + @Override + public boolean onClick(MaterialAlertDialog dialog, int which) { + if (which == MaterialAlertDialog.POSITIVE) { + File dir = new File(mDirSelectDialog.getCurrentPath(), et.getText() + .toString()); + dir.mkdirs(); + mDirSelectDialog.refresh(); + } + return true; + } + }).show(); + return false; + case MaterialAlertDialog.POSITIVE: + if (!mDirSelectDialog.canWrite()) { + MaterialToast.showToast(R.string.cur_dir_not_writable); + return false; + } + String downloadPath = mDirSelectDialog.getCurrentPath(); + // Update .nomedia file + // TODO Should I delete .nomedia in old + // download dir ? + if (!Config.getMediaScan()) { + try { + new File(Config.getDownloadPath(), ".nomedia").createNewFile(); + } catch (IOException e) { + } + } + Config.setDownloadPath(downloadPath); + mDownloadPath.setSummary(downloadPath); + return true; + case MaterialAlertDialog.NEGATIVE: + default: + return true; + } + } + }) , Config.getDownloadPath()); + mDirSelectDialog.show(); + } + + return true; + } + } + + public static class AdvancedFragment extends PreferenceFragment implements + Preference.OnPreferenceClickListener { + + private static final String KEY_FIX_DIRNAME = "fix_dirname"; + + private Preference mFixDirname; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.advanced_settings); + + mFixDirname = findPreference(KEY_FIX_DIRNAME); + mFixDirname.setOnPreferenceClickListener(this); + } + + @Override + public boolean onPreferenceClick(Preference preference) { + final String key = preference.getKey(); + if (KEY_FIX_DIRNAME.equals(key)) { + mFixDirname.setEnabled(false); + new BgThread() { + @Override + public void run() { + File downloadDir = new File(Config.getDownloadPath()); + List targetDirList = new ArrayList(); + + String[] list = downloadDir.list(); + if (list != null) { + for (String str : list) { + if (str.contains("-")) + continue; + File dir = new File(downloadDir, str); + if (new File(dir, EhUtils.EH_DOWNLOAD_FILENAME).exists()) + targetDirList.add(dir); + } + } + + if (targetDirList.size() == 0) { + AppHandler.getInstance().post(new Runnable() { + @Override + public void run() { + mFixDirname.setEnabled(true); + MaterialToast.showToast(R.string.no_need_fixing); + } + }); + return; + } else { + AppHandler.getInstance().post(new Runnable() { + @Override + public void run() { + MaterialToast.showToast(R.string.start_fixing); + } + }); + } + + int handleNum = 0; + List dirs = new ArrayList(); + List gids = new ArrayList(); + List tokens = new ArrayList(); + // Do get title from api + for (int i = 0; i < targetDirList.size(); i++) { + File dir = targetDirList.get(i); + File info = new File(dir, EhUtils.EH_DOWNLOAD_FILENAME); + InputStream is = null; + int gid; + String token; + try { + is = new BufferedInputStream(new FileInputStream(info), 128); + // skip read index + Utils.readAsciiLine(is); + gid = Integer.parseInt(Utils.readAsciiLine(is)); + token = Utils.readAsciiLine(is); + Utils.closeQuietly(is); + dirs.add(dir); + gids.add(gid); + tokens.add(token); + // Post api when to 25 or the end + if (gids.size() == 25 || i == targetDirList.size() - 1) { + ApiGalleryInfo[] agiArray = EhClient.getInstance().getApiGalleryInfo( + Utils.toIntArray(gids), tokens.toArray(new String[tokens.size()])); + // Change dirname + if (agiArray != null) { + for (int j = 0; j < agiArray.length; j++) { + ApiGalleryInfo agi = agiArray[j]; + if (agi != null) { + handleNum++; + dirs.get(j).renameTo(EhUtils.generateGalleryDir(agi.gid, agi.title)); + } + } + } + dirs.clear(); + gids.clear(); + tokens.clear(); + // Wait for CD, if not last + if (i != targetDirList.size() - 1) + Thread.sleep(3000); + } + } catch (Throwable e) { + continue; + } + } + final int _handleNum = handleNum; + AppHandler.getInstance().post(new Runnable() { + @Override + public void run() { + mFixDirname.setEnabled(true); + MaterialToast.showToast(String.format(getString(R.string.fixing_over), _handleNum)); + } + }); + } + }.start(); + } + return true; + } + } + + public static class AboutFragment extends PreferenceFragment implements + Preference.OnPreferenceClickListener { + + private static final String KEY_AUTHOR = "author"; + private static final String KEY_TWITTER = "twitter"; + private static final String KEY_GOOGLE_PLUS = "google_plus"; + private static final String KEY_CHANGELOG = "changelog"; + private static final String KEY_THANKS = "thanks"; + private static final String KEY_WEBSITE = "website"; + private static final String KEY_SOURCE = "source"; + private static final String KEY_VERSION = "version"; + private static final String KEY_CHECK_UPDATE = "check_for_update"; + private static final String KEY_CLOUD_DRIVE = "cloud_drive"; + private static final String KEY_ABOUT_ANALYICS = "about_analyics"; + + private Preference mAuthor; + private Preference mTwitter; + private Preference mGooglePlus; + private Preference mChangelog; + private Preference mThanks; + private Preference mWebsite; + private Preference mSource; + private Preference mVersion; + private Preference mCheckUpdate; + private Preference mCloudDrive; + private Preference mAboutAnalyics; + + private final long[] mHits = new long[3]; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.about_settings); + + mAuthor = findPreference(KEY_AUTHOR); + mAuthor.setOnPreferenceClickListener(this); + mTwitter = findPreference(KEY_TWITTER); + mTwitter.setOnPreferenceClickListener(this); + mGooglePlus = findPreference(KEY_GOOGLE_PLUS); + mGooglePlus.setOnPreferenceClickListener(this); + mChangelog = findPreference(KEY_CHANGELOG); + mChangelog.setOnPreferenceClickListener(this); + mThanks = findPreference(KEY_THANKS); + mThanks.setOnPreferenceClickListener(this); + mWebsite = findPreference(KEY_WEBSITE); + mWebsite.setOnPreferenceClickListener(this); + mSource = findPreference(KEY_SOURCE); + mSource.setOnPreferenceClickListener(this); + mVersion = findPreference(KEY_VERSION); + mVersion.setOnPreferenceClickListener(this); + mCheckUpdate = findPreference(KEY_CHECK_UPDATE); + mCheckUpdate.setOnPreferenceClickListener(this); + mCloudDrive = findPreference(KEY_CLOUD_DRIVE); + mCloudDrive.setOnPreferenceClickListener(this); + mAboutAnalyics = findPreference(KEY_ABOUT_ANALYICS); + mAboutAnalyics.setOnPreferenceClickListener(this); + + try { + Context c = getActivity().getApplicationContext(); + PackageInfo pi= c.getPackageManager().getPackageInfo(c.getPackageName(), 0); + mVersion.setSummary(pi.versionName); + } catch (NameNotFoundException e) {} + } + + @Override + public boolean onPreferenceClick(Preference preference) { + final String key = preference.getKey(); + if (KEY_VERSION.equals(key)) { + System.arraycopy(mHits, 1, mHits, 0, mHits.length-1); + mHits[mHits.length-1] = SystemClock.uptimeMillis(); + if (mHits[0] >= (SystemClock.uptimeMillis() - 500)) { + Arrays.fill(mHits, 0); + Activity a = getActivity(); + if (Secret.hasSecretImage(a)) { + Intent intent = new Intent(a, SecretActivity.class); + a.startActivity(intent); + getActivity().overridePendingTransition(0, 0); + } + } + + } else if (KEY_AUTHOR.equals(key)) { + Intent i = new Intent(Intent.ACTION_SENDTO); + i.setData(Uri.parse("mailto:ehviewersu@gmail.com")); + i.putExtra(Intent.EXTRA_SUBJECT, "About EhViewer"); + startActivity(i); + + } else if (KEY_TWITTER.equals(key)) { + new MaterialAlertDialog.Builder(getActivity()) + .setTitle(R.string.twitter_title) + .setItems(new CharSequence[] { "@EhViewer", "@jkjvinn" }, + new MaterialAlertDialog.OnClickListener() { + @Override + public boolean onClick(MaterialAlertDialog dialog, int which) { + Uri uri = null; + switch (which) { + case 0: + uri = Uri.parse("https://twitter.com/EhViewer"); + break; + case 1: + uri = Uri.parse("https://twitter.com/jkjvinn"); + break; + } + if (uri != null) { + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + startActivity(intent); + } + return true; + } + }).setNegativeButton(android.R.string.cancel).show(); + + } else if (KEY_GOOGLE_PLUS.equals(key)) { + Uri uri = Uri.parse("https://plus.google.com/communities/103823982034655188459"); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + startActivity(intent); + + } else if (KEY_CHANGELOG.equals(key)) { + InputStream is = getActivity().getResources().openRawResource(R.raw.change_log); + new MaterialAlertDialog.Builder(getActivity()).setTitle(R.string.changelog) + .setMessage(Utils.InputStream2String(is, "utf-8")).setNegativeButton(android.R.string.cancel) + .show(); + + } else if (KEY_THANKS.equals(key)) { + InputStream is = getActivity().getResources().openRawResource(R.raw.thanks); + final WebView webView = new WebView(getActivity()); + webView.loadData(Utils.InputStream2String(is, "utf-8"), "text/html; charset=UTF-8", null); + new MaterialAlertDialog.Builder(getActivity())// .setTitle(R.string.thanks) + .setView(webView, true).setNegativeButton(android.R.string.cancel).show(); + + } else if (KEY_WEBSITE.equals(key)) { + Uri uri = Uri.parse("http://www.ehviewer.com"); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + startActivity(intent); + + } else if (KEY_SOURCE.equals(key)) { + Uri uri = Uri.parse("https://github.com/seven332/EhViewer"); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + startActivity(intent); + + } else if (KEY_CHECK_UPDATE.equals(key)) { + mCheckUpdate.setSummary(R.string.checking_update); + mCheckUpdate.setEnabled(false); + new UpdateHelper((AppContext) getActivity().getApplication()).SetOnCheckUpdateListener( + new UpdateHelper.OnCheckUpdateListener() { + @Override + public void onSuccess(String version, long size, final String url, final String fileName, + String info) { + mCheckUpdate.setSummary(R.string.found_update); + String sizeStr = Utils.sizeToString(size); + AlertDialog dialog = DialogUtils.createUpdateDialog(getActivity(), version, sizeStr, info, + new MaterialAlertDialog.OnClickListener() { + @Override + public boolean onClick(MaterialAlertDialog dialog, int which) { + if (which == MaterialAlertDialog.POSITIVE) { + HttpHelper hh = new HttpHelper(getActivity()); + hh.downloadInThread(url, new File(Config.getDownloadPath()), + fileName, false, null, new UpdateHelper.UpdateListener( + getActivity(), fileName)); + } + return true; + } + }).create(); + if (!getActivity().isFinishing()) + dialog.show(); + } + + @Override + public void onNoUpdate() { + mCheckUpdate.setSummary(R.string.up_to_date); + mCheckUpdate.setEnabled(true); + UpdateHelper.setEnabled(true); + } + + @Override + public void onFailure(String eMsg) { + mCheckUpdate.setSummary(eMsg); + mCheckUpdate.setEnabled(true); + UpdateHelper.setEnabled(true); + } + }).checkUpdate(); + + } else if (KEY_CLOUD_DRIVE.equals(key)) { + Uri uri = Uri.parse("https://mega.co.nz/#F!xkMChYrI!Q85i3d7kNkhVkwvDePbahw"); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + startActivity(intent); + + } else if (KEY_ABOUT_ANALYICS.equals(key)) { + new MaterialAlertDialog.Builder(getActivity()).setTitle(R.string.about_analyics_title) + .setMessageAutoLink(Linkify.WEB_URLS).setMessage(R.string.about_analyics_comment) + .setNegativeButton(android.R.string.cancel).show(); + + } + return true; + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/util/AnimatorUtils.java b/app/src/main/java/com/hippo/ehviewer/util/AnimatorUtils.java new file mode 100644 index 000000000..728b64452 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/util/AnimatorUtils.java @@ -0,0 +1,30 @@ +/* + * 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.util; + +import android.animation.Animator; +import android.animation.ValueAnimator; + +public final class AnimatorUtils { + + public static Animator createDelayAnimator(long delay) { + Animator anim = ValueAnimator.ofFloat(0f, 1f); + anim.setDuration(delay);; + return anim; + } + +} diff --git a/app/src/main/java/com/hippo/ehviewer/util/AutoExpandArray.java b/app/src/main/java/com/hippo/ehviewer/util/AutoExpandArray.java new file mode 100644 index 000000000..796798445 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/util/AutoExpandArray.java @@ -0,0 +1,84 @@ +/* + * 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.util; + +public class AutoExpandArray { + + @SuppressWarnings("unused") + private static final String TAG = AutoExpandArray.class.getSimpleName(); + + private static final int MIN_CAPACITY_INCREMENT = 12; + + private int maxValidIndex = 0; + private transient Object[] array; + + public AutoExpandArray(int capacity) { + if (capacity < 0) { + throw new IllegalArgumentException("capacity < 0: " + capacity); + } + array = new Object[capacity]; + } + + public AutoExpandArray() { + array = new Object[MIN_CAPACITY_INCREMENT]; + } + + private synchronized void ensureCapacity(int index) { + if (index < array.length) + return; + + Object[] newArray = new Object[(index + 1) << 1]; + System.arraycopy(array, 0, newArray, 0, array.length); + array = newArray; + } + + public synchronized void setCapacity(int newCapacity) { + if (newCapacity <= array.length) + return; + + Object[] newArray = new Object[newCapacity]; + System.arraycopy(array, 0, newArray, 0, array.length); + array = newArray; + } + + public synchronized E set(int index, E object) { + if (index > maxValidIndex) + maxValidIndex = index; + + ensureCapacity(index); + @SuppressWarnings("unchecked") + E result = (E)array[index]; + array[index] = object; + return result; + } + + @SuppressWarnings("unchecked") + public synchronized E get(int index) { + if (index >= array.length || index < 0) { + return null; + } + return (E)array[index]; + } + + public synchronized int length() { + return array.length; + } + + public synchronized int maxValidIndex() { + return maxValidIndex; + } + } diff --git a/app/src/main/java/com/hippo/ehviewer/util/BgThread.java b/app/src/main/java/com/hippo/ehviewer/util/BgThread.java new file mode 100644 index 000000000..cdefd3187 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/util/BgThread.java @@ -0,0 +1,62 @@ +/* + * 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.util; + +import android.os.Process; + +public class BgThread extends Thread { + + public BgThread() { + super(); + setPriority(Process.THREAD_PRIORITY_BACKGROUND); + } + + public BgThread(Runnable runnable) { + super(runnable); + setPriority(Process.THREAD_PRIORITY_BACKGROUND); + } + + public BgThread(Runnable runnable, String threadName) { + super(runnable, threadName); + setPriority(Process.THREAD_PRIORITY_BACKGROUND); + } + + public BgThread(String threadName) { + super(threadName); + setPriority(Process.THREAD_PRIORITY_BACKGROUND); + } + + public BgThread(ThreadGroup group, Runnable runnable) { + super(group, runnable); + setPriority(Process.THREAD_PRIORITY_BACKGROUND); + } + + public BgThread(ThreadGroup group, Runnable runnable, String threadName) { + super(group, runnable, threadName); + setPriority(Process.THREAD_PRIORITY_BACKGROUND); + } + + public BgThread(ThreadGroup group, String threadName) { + super(group, threadName); + setPriority(Process.THREAD_PRIORITY_BACKGROUND); + } + + public BgThread(ThreadGroup group, Runnable runnable, String threadName, long stackSize) { + super(group, runnable, threadName, stackSize); + setPriority(Process.THREAD_PRIORITY_BACKGROUND); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/util/Config.java b/app/src/main/java/com/hippo/ehviewer/util/Config.java new file mode 100644 index 000000000..839055ed7 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/util/Config.java @@ -0,0 +1,643 @@ +/* + * 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.util; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.ActivityInfo; +import android.os.Environment; +import android.preference.PreferenceManager; + +import com.hippo.ehviewer.data.ListUrls; +import com.hippo.ehviewer.ehclient.EhClient; + +import java.io.File; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public final class Config { + @SuppressWarnings("unused") + private static final String TAG = Config.class.getSimpleName(); + + public static final String EXTERANL_DIR_NAME = "EhViewer"; + public static File sExternalDir; + + private static final String KEY_UPDATE_DATE = "update_date"; + + private static boolean mInit = false; + + private static Context mContext; + private static SharedPreferences mConfigPre; + + /** + * Init Config + * + * @param context Application context + */ + public static void init(Context context) { + if (mInit) + return; + mInit = true; + + mContext = context; + mConfigPre = PreferenceManager.getDefaultSharedPreferences(mContext); + + if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + sExternalDir = new File(Environment.getExternalStorageDirectory(), EXTERANL_DIR_NAME); + Utils.ensureDir(sExternalDir, true); + } else { + sExternalDir = null; + } + } + + /** + * Is init + * @return True if init + */ + public static boolean isInit() { + return mInit; + } + + public static boolean getBoolean(String key, boolean defValue) { + return mConfigPre.getBoolean(key, defValue); + } + + public static void setBoolean(String key, boolean value) { + mConfigPre.edit().putBoolean(key, value).apply(); + } + + public static int getInt(String key, int defValue) { + return mConfigPre.getInt(key, defValue); + } + + public static void setInt(String key, int value) { + mConfigPre.edit().putInt(key, value).apply(); + } + + public static String getString(String key, String defValue) { + return mConfigPre.getString(key, defValue); + } + + public static void setString(String key, String value) { + mConfigPre.edit().putString(key, value).apply(); + } + + public static int getIntFromStr(String key, int defValue) { + return Utils.parseIntSafely(mConfigPre.getString(key, Integer.toString(defValue)), defValue); + } + + public static void setIntToStr(String key, int value) { + mConfigPre.edit().putString(key, Integer.toString(value)).apply(); + } + + public static float getFloat(String key, float defValue) { + return mConfigPre.getFloat(key, defValue); + } + + public static void setFloat(String key, float value) { + mConfigPre.edit().putFloat(key, value).apply(); + } + + + /****** Display ******/ + + private static final String KEY_CUSTOM_THEME_COLOR = "custom_theme_color"; + private static final boolean DEFAULT_CUSTOM_THEME_COLOR = false; + + private static final String KEY_THEME_COLOR = "theme_color"; + private static final int DEFAULT_THEME_COLOR = Ui.THEME_COLOR; + + private static final String KEY_SCREEN_ORIENTATION = "screen_orientation"; + private static final int DEFAULT_SCREEN_ORIENTATION = 0; + + private static final String KEY_LIST_MODE = "list_mode"; + private static final int DEFAULT_LIST_MODE = 0; + + private static final String KEY_LIST_THUMB_COLUMNS_PORTRAIT = "list_thumb_columns_portrait"; + private static final int DEFAULT_LIST_THUMB_COLUMNS_PORTRAIT = 3; + + private static final String KEY_LIST_THUMB_COLUMNS_LANDSCAPE = "list_thumb_columns_landscape"; + private static final int DEFAULT_LIST_THUMB_COLUMNS_LANDSCAPE = 5; + + private static final String KEY_PREVIEW_COLUMNS_PORTRAIT = "preview_columns_portrait"; + private static final int DEFAULT_PREVIEW_COLUMNS_PORTRAIT = 3; + + private static final String KEY_PREVIEW_COLUMNS_LANDSCAPE = "preview_columns_landscape"; + private static final int DEFAULT_PREVIEW_COLUMNS_LANDSCAPE = 5; + + private static final String KEY_SHOW_POPULAR_UPDATE_TIME = "show_popular_update_time"; + private static final boolean DEFAULT_SHOW_POPULAR_UPDATE_TIME = false; + + public static boolean getCustomThemeColor() { + return getBoolean(KEY_CUSTOM_THEME_COLOR, DEFAULT_CUSTOM_THEME_COLOR); + } + + public static int getThemeColor() { + return getInt(KEY_THEME_COLOR, DEFAULT_THEME_COLOR); + } + + public static void setThemeColor(int color) { + setInt(KEY_THEME_COLOR, color); + } + + public static int getScreenOriMode() { + return screenOriPre2Value(getIntFromStr(KEY_SCREEN_ORIENTATION, DEFAULT_SCREEN_ORIENTATION)); + } + + public static int screenOriPre2Value(int screenOriModePre) { + switch (screenOriModePre) { + case 0: + return ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; + case 1: + return ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; + case 2: + return ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; + default: + return ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; + } + } + + public static int getListMode() { + return getIntFromStr(KEY_LIST_MODE, DEFAULT_LIST_MODE); + } + + public static void setListMode(int listMode) { + setIntToStr(KEY_LIST_MODE, listMode); + } + + public static int getListThumbColumnsPortrait() { + return getInt(KEY_LIST_THUMB_COLUMNS_PORTRAIT, DEFAULT_LIST_THUMB_COLUMNS_PORTRAIT); + } + + public static int getListThumbColumnsLandscape() { + return getInt(KEY_LIST_THUMB_COLUMNS_LANDSCAPE, DEFAULT_LIST_THUMB_COLUMNS_LANDSCAPE); + } + + public static int getPreviewColumnsPortrait() { + return getInt(KEY_PREVIEW_COLUMNS_PORTRAIT, DEFAULT_PREVIEW_COLUMNS_PORTRAIT); + } + + public static int getPreviewColumnsLandscape() { + return getInt(KEY_PREVIEW_COLUMNS_LANDSCAPE, DEFAULT_PREVIEW_COLUMNS_LANDSCAPE); + } + + public static boolean getShowPopularUpdateTime() { + return getBoolean(KEY_SHOW_POPULAR_UPDATE_TIME, DEFAULT_SHOW_POPULAR_UPDATE_TIME); + } + + + /****** Eh Config ******/ + + private static final String KEY_DEFAULT_CAT = "default_cat"; + private static final int DEFAULT_DEFAULT_CAT = ListUrls.ALL_CATEGORT; + + // m for normal, l for large + public static final String KEY_PREVIEW_MODE = "preview_mode"; + public static final String PREVIEW_MODE_NORMAL = "m"; + public static final String PREVIEW_MODE_LARGE = "l"; + private static final String DEFAULT_PREVIEW_MODE = PREVIEW_MODE_LARGE; + + /** + * reclass 0x1 + * language 0x2 + * parody 0x4 + * character 0x8 + * group 0x10 + * artist 0x20 + * male 0x40 + * female 0x80 + */ + private static final String KEY_EXCLUDE_TAG_GROUP = "exculde_tag_group"; + private static final int DEFAULT_EXCLUDE_TAG_GROUP = 0; + + /** + * Original Translated Rewrite All + * Japanese 1024 2048 + * English 1 1025 2049 + * Chinese 10 1034 2058 + * Dutch 20 1044 2068 + * French 30 1054 2078 + * German 40 1064 2088 + * Hungarian 50 1074 2098 + * Italian 60 1084 2108 + * Korean 70 1094 2118 + * Polish 80 1104 2128 + * Portuguese 90 1114 2138 + * Russian 100 1124 2148 + * Spanish 110 1134 2158 + * Thai 120 1144 2168 + * Vietnamese 130 1154 2178 + * Other 255 1279 2303 + */ + private static final String KEY_EXCLUDE_LANGUAGE = "exculde_language"; + private static final String DEFAULT_EXCLUDE_LANGUAGE = ""; + + private static final String KEY_DEFAULT_FAVORITE = "default_favorite"; + private static final int DEFAULT_DEFAULT_FAVORITE = -2; + + private static final String KEY_MAX_HISTORY_COUNT = "max_history_count"; + private static final int DEFAULT_MAX_HISTORY_COUNT = 100; + + public static int getDefaultCat() { + return getInt(KEY_DEFAULT_CAT, DEFAULT_DEFAULT_CAT); + } + + public static void setDefaultCat(int defaultCat) { + setInt(KEY_DEFAULT_CAT, defaultCat); + } + + public static String getPreviewMode() { + return getString(KEY_PREVIEW_MODE, DEFAULT_PREVIEW_MODE); + } + + public static int getExculdeTagGroup() { + return getInt(KEY_EXCLUDE_TAG_GROUP, DEFAULT_EXCLUDE_TAG_GROUP); + } + + public static void setExculdeTagGroup(int exculdeTagGroup) { + setInt(KEY_EXCLUDE_TAG_GROUP, exculdeTagGroup); + } + + public static String getExculdeLanguage() { + return getString(KEY_EXCLUDE_LANGUAGE, DEFAULT_EXCLUDE_LANGUAGE); + } + + public static void setExculdeLanguage(String exculdeLanguage) { + setString(KEY_EXCLUDE_LANGUAGE, exculdeLanguage); + } + + public static int getDefaultFavorite() { + return getIntFromStr(KEY_DEFAULT_FAVORITE, DEFAULT_DEFAULT_FAVORITE); + } + + public static void setDefaultFavorite(int defaultFavorite) { + setIntToStr(KEY_DEFAULT_FAVORITE, defaultFavorite); + } + + public static int getMaxHistoryCount() { + return getIntFromStr(KEY_MAX_HISTORY_COUNT, DEFAULT_MAX_HISTORY_COUNT); + } + + + /****** Read ******/ + + private static final String KEY_READING_DIRECTION = "reading_direction"; + private static final int DEFAULT_READING_DIRECTION = 0; + + private static final String KEY_PAGE_SCALING = "page_scaling"; + private static final int DEFAULT_PAGE_SCALING = 3; + + private static final String KEY_START_POSITION = "start_position"; + private static final int DEFAULT_START_POSITION = 1; + + private static final String KEY_GALLERY_SHOW_CLOCK = "gallery_show_clock"; + private static final boolean DEFAULT_GALLERY_SHOW_CLOCK = true; + + private static final String KEY_KEEP_SCREEN_ON = "keep_screen_on"; + private static final boolean DEFAULT_KEEP_SCREEN_ON = false; + + private static final String KEY_GALLERY_SHOW_BATTERY = "gallery_show_battery"; + private static final boolean DEFAULT_GALLERY_SHOW_BATTERY = true; + + private static final String KEY_CUSTOM_LIGHTNESS = "custom_lightness"; + private static final boolean DEFAULT_CUSTOM_LIGHTNESS = false; + + private static final String KEY_CUSTOM_LIGHTNESS_VALUE = "custom_lightness_value"; + private static final int DEFAULT_CUSTOM_LIGHTNESS_VALUE = 120; + + private static final String KEY_CUSTOM_CODEC = "custom_codec"; + + private static final String KEY_DECODE_FORMAT = "decode_format"; + private static final int DEFAULT_DECODE_FORMAT = 0; + + public static int getReadingDirection() { + return getIntFromStr(KEY_READING_DIRECTION, DEFAULT_READING_DIRECTION); + } + + public static void setReadingDirection(int value) { + setIntToStr(KEY_READING_DIRECTION, value); + } + + public static int getPageScaling() { + return getIntFromStr(KEY_PAGE_SCALING, DEFAULT_PAGE_SCALING); + } + + public static void setPageScaling(int value) { + setIntToStr(KEY_PAGE_SCALING, value); + } + + public static int getStartPosition() { + return getIntFromStr(KEY_START_POSITION, DEFAULT_START_POSITION); + } + + public static void setStartPosition(int value) { + setIntToStr(KEY_START_POSITION, value); + } + + public static boolean getKeepSreenOn() { + return getBoolean(KEY_KEEP_SCREEN_ON, DEFAULT_KEEP_SCREEN_ON); + } + + public static void setKeepSreenOn(boolean value) { + setBoolean(KEY_KEEP_SCREEN_ON, value); + } + + public static boolean getShowClock() { + return getBoolean(KEY_GALLERY_SHOW_CLOCK, DEFAULT_GALLERY_SHOW_CLOCK); + } + + public static void setShowClock(boolean value) { + setBoolean(KEY_GALLERY_SHOW_CLOCK, value); + } + + public static boolean getShowBattery() { + return getBoolean(KEY_GALLERY_SHOW_BATTERY, DEFAULT_GALLERY_SHOW_BATTERY); + } + + public static void setShowBattery(boolean value) { + setBoolean(KEY_GALLERY_SHOW_BATTERY, value); + } + + public static boolean getCustomLightness() { + return getBoolean(KEY_CUSTOM_LIGHTNESS, DEFAULT_CUSTOM_LIGHTNESS); + } + + public static void setCustomLightness(boolean value) { + setBoolean(KEY_CUSTOM_LIGHTNESS, value); + } + + public static int getCustomLightnessValue() { + return getIntFromStr(KEY_CUSTOM_LIGHTNESS_VALUE, DEFAULT_CUSTOM_LIGHTNESS_VALUE); + } + + public static void setCustomLightnessValue(int value) { + setIntToStr(KEY_CUSTOM_LIGHTNESS_VALUE, value); + } + + public static boolean getCustomCodec() { + return getBoolean(KEY_CUSTOM_CODEC, Utils.SUPPORT_IMAGE); + } + + public static void setCustomCodec(boolean value) { + setBoolean(KEY_CUSTOM_CODEC, value); + } + + public static int getDecodeFormat() { + return getIntFromStr(KEY_DECODE_FORMAT, DEFAULT_DECODE_FORMAT); + } + + public static int getDecodeFormatIndex() { + int index = getIntFromStr(KEY_DECODE_FORMAT, DEFAULT_DECODE_FORMAT); + switch (index) { + case 6409: + index = 1; + break; + case 6410: + index = 2; + break; + case 6407: + index = 3; + break; + case 6408: + index = 4; + break; + case 0: + default: + index = 0; + break; + } + return index; + } + + public static void setDecodeFormatFromIndex(int index) { + switch (index) { + case 1: + index = 6409; + break; + case 2: + index = 6410; + break; + case 3: + index = 6407; + break; + case 4: + index = 6408; + break; + case 0: + default: + index = 0; + break; + } + setIntToStr(KEY_DECODE_FORMAT, index); + } + + + /****** Download ******/ + + private static final String KEY_DOWNLOAD_PATH = "download_path"; + private static final String DEFAULT_DOWNLOAD_PATH = + Environment.getExternalStorageDirectory() + "/EhViewer/download/"; + + private static final String KEY_MEDIA_SCAN = "media_scan"; + private static final boolean DEFAULT_MEDIA_SCAN = false; + + private static final String KEY_DOWNLOAD_THREAD = "download_thread"; + private static final int DEFAULT_DOWNLOAD_THREAD = 3; + + private static final String KEY_DOWNLOAD_ORIGIN_IMAGE = "download_origin_image"; + private static final boolean DEFAULT_DOWNLOAD_ORIGIN_IMAGE = false; + + private static final String KEY_KEEP_DOWNLOAD_SERVICE = "keep_download_service"; + private static final boolean DEFAULT_KEEP_DOWNLOAD_SERVICE = true; + + public static String getDownloadPath() { + return getString(KEY_DOWNLOAD_PATH, DEFAULT_DOWNLOAD_PATH); + } + + public static void setDownloadPath(String path) { + setString(KEY_DOWNLOAD_PATH, path); + } + + public static boolean getMediaScan() { + return getBoolean(KEY_MEDIA_SCAN, DEFAULT_MEDIA_SCAN); + } + + public static int getDownloadThread() { + return getIntFromStr(KEY_DOWNLOAD_THREAD, DEFAULT_DOWNLOAD_THREAD); + } + + public static boolean getDownloadOriginImage() { + return getBoolean(KEY_DOWNLOAD_ORIGIN_IMAGE, DEFAULT_DOWNLOAD_ORIGIN_IMAGE); + } + + public static boolean getKeepDownloadService() { + return getBoolean(KEY_KEEP_DOWNLOAD_SERVICE, DEFAULT_KEEP_DOWNLOAD_SERVICE); + } + + + /****** Advanced ******/ + + private static final String KEY_HTTP_RETRY = "http_retry"; + private static final int DEFAULT_HTTP_RETRY = 3; + + private static final String KEY_HTTP_CONNECT_TIMEOUT = "http_connect_timeout"; + private static final int DEFAULT_HTTP_CONNECT_TIMEOUT = 5000; + + private static final String KEY_HTTP_READ_TIMEOUT = "http_read_timeout"; + private static final int DEFAULT_HTTP_READ_TIMEOUT = 5000; + + private static final String KEY_EH_MIN_INTERVAL = "eh_min_interval"; + private static final int DEFAULT_EH_MIN_INTERVAL = 0; + + public static int getHttpRetry() { + return getIntFromStr(KEY_HTTP_RETRY, DEFAULT_HTTP_RETRY); + } + + public static int getHttpConnectTimeout() { + return getIntFromStr(KEY_HTTP_CONNECT_TIMEOUT, DEFAULT_HTTP_CONNECT_TIMEOUT); + } + + public static int getHttpReadTimeout() { + return getIntFromStr(KEY_HTTP_READ_TIMEOUT, DEFAULT_HTTP_READ_TIMEOUT); + } + + public static int getEhMinInterval() { + return getIntFromStr(KEY_EH_MIN_INTERVAL, DEFAULT_EH_MIN_INTERVAL); + } + + + /****** About ******/ + + private static final String KEY_AUTO_CHECK_FOR_UPDATE = "auto_check_for_update"; + private static final boolean DEFAULT_AUTO_CHECK_FOR_UPDATE = true; + + private static final String KEY_UPDATE_SERVER = "update_server"; + private static final int DEFAULT_UPDATE_SERVER = 1; + + private static final String KEY_ALLOW_ANALYTICS = "allow_analyics"; + private static final boolean DEFAULT_ALLOW_ANALYTICS = false; + + public static boolean isAutoCheckForUpdate() { + return getBoolean(KEY_AUTO_CHECK_FOR_UPDATE, DEFAULT_AUTO_CHECK_FOR_UPDATE); + } + + public static void setAutoCheckForUpdate(boolean autoCheckForUpdate) { + setBoolean(KEY_AUTO_CHECK_FOR_UPDATE, autoCheckForUpdate); + } + + public static int getUpdateDate() { + return getInt(KEY_UPDATE_DATE, 0); + } + + public static void setUpdateDate() { + setUpdateDate(Utils.getDate()); + } + + public static void setUpdateDate(int date) { + setInt(KEY_UPDATE_DATE, date); + } + + public static String getUpdateServer() { + int value = getIntFromStr(KEY_UPDATE_SERVER, DEFAULT_UPDATE_SERVER); + switch (value) { + case 1: + return "qiniu"; + case 0: + default: + return "google"; + } + } + + public static boolean getAllowAnalyics() { + return getBoolean(KEY_ALLOW_ANALYTICS, DEFAULT_ALLOW_ANALYTICS); + } + + public static void setAllowAnalyics(boolean setAnalyics) { + setBoolean(KEY_ALLOW_ANALYTICS, setAnalyics); + } + + + /****** Mode an API Mode ******/ + + private static final String KEY_MODE = "mode"; + private static final int DEFAULT_MODE = EhClient.MODE_G; + + private static final String KEY_API_MODE = "api_mode"; + private static final int DEFAULT_API_MODE = EhClient.MODE_G; + + private static final String KEY_LOFI_RESOLUTION = "lofi_resolution"; + private static final int DEFAULT_LOFI_RESOLUTION = 1; + + public static int getMode() { + return getInt(KEY_MODE, DEFAULT_MODE); + } + + public static void setMode(int mode) { + setInt(KEY_MODE, mode); + } + + public static int getApiMode() { + return getInt(KEY_API_MODE, DEFAULT_API_MODE); + } + + public static void setApiMode(int apiMode) { + setInt(KEY_API_MODE, apiMode); + } + + public static int getLofiResolution() { + return getInt(KEY_LOFI_RESOLUTION, DEFAULT_LOFI_RESOLUTION); + } + + public static void setLofiResolution(int lofiResolution) { + setInt(KEY_LOFI_RESOLUTION, lofiResolution); + } + + + // Proxy urls + private static final String PROXY_URLS_KEY = "proxy_urls"; + public static final String[] DEFAULT_PROXY_URLS = { + "http://proxyy0000.appsp0t.com/proxy", + "http://proxyy0001.appsp0t.com/proxy", + "http://proxyy0002.appsp0t.com/proxy", + "http://proxyy0003.appsp0t.com/proxy", + "http://proxyy0004.appsp0t.com/proxy", + "http://proxyy0005.appsp0t.com/proxy", + "http://proxyy0006.appsp0t.com/proxy", + "http://proxyy0007.appsp0t.com/proxy", + "http://proxyy0008.appsp0t.com/proxy", + "http://proxyy0009.appsp0t.com/proxy", + "http://proxyy000a.appsp0t.com/proxy", + "http://proxyy000b.appsp0t.com/proxy", + "http://proxyy000c.appsp0t.com/proxy", + "http://proxyy000d.appsp0t.com/proxy", + "http://proxyy000e.appsp0t.com/proxy", + "http://proxyy000f.appsp0t.com/proxy" + }; + + public static String[] getProxyUrls() { + Set re = mConfigPre.getStringSet(PROXY_URLS_KEY, null); + if (re != null && re.size() > 0) + return re.toArray(new String[re.size()]); + else + return DEFAULT_PROXY_URLS; + } + + public static void setProxyUrls(String[] urls) { + mConfigPre.edit().putStringSet(PROXY_URLS_KEY, new HashSet(Arrays.asList(urls))).apply(); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/util/Constants.java b/app/src/main/java/com/hippo/ehviewer/util/Constants.java new file mode 100644 index 000000000..407ef371a --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/util/Constants.java @@ -0,0 +1,33 @@ +/* + * 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.util; + +public final class Constants { + + public static final int FALSE = 0; + public static final int TRUE = 1; + + public static final int ANIMATE_TIME = 300; + + public static final int HTTP_TEMP_REDIRECT = 307; + public static String defaultUserAgent = "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.76 Safari/537.36"; + public static String userAgent = System.getProperty("http.agent", defaultUserAgent); + public static final int DEFAULT_TIMEOUT = 5 * 1000; + public static final int MAX_REDIRECTS = 5; + + public static final int BUFFER_SIZE = 4096; +} diff --git a/app/src/main/java/com/hippo/ehviewer/util/Crash.java b/app/src/main/java/com/hippo/ehviewer/util/Crash.java new file mode 100644 index 000000000..f0e322fe2 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/util/Crash.java @@ -0,0 +1,209 @@ +/* + * 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.util; + +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Build; +import android.os.Environment; + +public class Crash { + private static final String TAG = "Crash"; + + private static Context mContext; + + private static String externalCrashFilePath = "/EhViewer/crash"; + private static String internalCrashFilePath = "/crash"; + + private static final String NEW_CRAHS = "new_crash"; + private static final String LAST_CRASH_POSITION = "last_crash_position"; + private static final String LAST_CRASH_NAME = "last_crash_name"; + + private static boolean mInit = false; + + /** + * Init Crash + * + * @param context Application context + */ + public static void init(Context context) { + if (mInit) + return; + mInit = true; + + mContext = context; + } + + /** + * Is init + * @return True if init + */ + public static boolean isInit() { + return mInit; + } + + public static final String getThrowableInfo(Throwable t) { + Writer writer = new StringWriter(); + PrintWriter printWriter = new PrintWriter(writer); + t.printStackTrace(printWriter); + Throwable cause = t.getCause(); + while (cause != null) { + cause.printStackTrace(printWriter); + cause = cause.getCause(); + } + printWriter.close(); + return writer.toString(); + } + + /** + * Save throwable infomation to file + * + * @param ex The throwable to store + */ + public static void saveCrashInfo2File(Throwable ex) { + StringBuffer sb = new StringBuffer(); + collectDeviceInfo(sb); + + String result = getThrowableInfo(ex); + Log.e(TAG, result); + sb.append("======== CrashInfo ========\n").append(result).append("\n"); + try { + long timestamp = System.currentTimeMillis(); + String time = Utils.sDate.format(System.currentTimeMillis()); + String fileName = "crash-" + time + "-" + timestamp + ".log"; + String path = null; + boolean position = false; + if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + path = Environment.getExternalStorageDirectory() + externalCrashFilePath; + position = true; + } + else { + path = mContext.getFilesDir() + internalCrashFilePath; + position = false; + } + File dir = new File(path); + if (!dir.exists()) { + dir.mkdirs(); + } + FileOutputStream fos = new FileOutputStream(new File(path, fileName)); + fos.write(sb.toString().getBytes()); + fos.close(); + // Record info for next time upload + SharedPreferences configPre = mContext.getSharedPreferences("config", 0); + SharedPreferences.Editor editor = configPre.edit(); + editor.putBoolean(NEW_CRAHS, true); + editor.putBoolean(LAST_CRASH_POSITION, position); + editor.putString(LAST_CRASH_NAME, fileName); + editor.commit(); + } catch (Exception e) { + Log.e(TAG, "An error occured while writing crash file...", e); + } + } + + private static void collectDeviceInfo(StringBuffer sb) { + try { + PackageManager pm = mContext.getPackageManager(); + PackageInfo pi = pm.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES); + if (pi != null) { + String versionName = pi.versionName == null ? "null" : pi.versionName; + String versionCode = String.valueOf(pi.versionCode); + sb.append("======== PackageInfo ========\n"); + sb.append("PackageName=" + pi.packageName + "\n"); + sb.append("VersionName=" + versionName + "\n"); + sb.append("VersionCode=" + versionCode + "\n"); + sb.append("\n"); + } + } catch (NameNotFoundException e) { + Log.e(TAG, "an error occured when collect package info", e); + } + sb.append("======== DeviceInfo ========\n"); + sb.append("BOARD=" + Build.BOARD + "\n"); + sb.append("BOOTLOADER=" + Build.BOOTLOADER + "\n"); + sb.append("CPU_ABI=" + Build.CPU_ABI + "\n"); + sb.append("CPU_ABI2=" + Build.CPU_ABI2 + "\n"); + sb.append("DEVICE=" + Build.DEVICE + "\n"); + sb.append("DISPLAY=" + Build.DISPLAY + "\n"); + sb.append("FINGERPRINT=" + Build.FINGERPRINT + "\n"); + sb.append("HARDWARE=" + Build.HARDWARE + "\n"); + sb.append("HOST=" + Build.HOST + "\n"); + sb.append("ID=" + Build.ID + "\n"); + sb.append("MANUFACTURER=" + Build.MANUFACTURER + "\n"); + sb.append("MODEL=" + Build.MODEL + "\n"); + sb.append("PRODUCT=" + Build.PRODUCT + "\n"); + sb.append("RADIO=" + Build.getRadioVersion() + "\n"); + sb.append("SERIAL=" + Build.SERIAL + "\n"); + sb.append("TAGS=" + Build.TAGS + "\n"); + sb.append("TYPE=" + Build.TYPE + "\n"); + sb.append("USER=" + Build.USER + "\n"); + sb.append("CODENAME=" + Build.VERSION.CODENAME + "\n"); + sb.append("INCREMENTAL=" + Build.VERSION.INCREMENTAL + "\n"); + sb.append("RELEASE=" + Build.VERSION.RELEASE + "\n"); + sb.append("SDK=" + Build.VERSION.SDK_INT + "\n"); + sb.append("\n"); + } + + /** + * Check is there last crash + * + * @return Return the crash String or null if there is no + */ + public static String getLastCrash() { + SharedPreferences configPre = mContext.getSharedPreferences("config", 0); + if (!configPre.getBoolean(NEW_CRAHS, false)) + return null; + + boolean position = configPre.getBoolean(LAST_CRASH_POSITION, false); + String lastCrashName; + if ((lastCrashName = configPre.getString(LAST_CRASH_NAME, null)) == null) + return null; + + File lastCrashFile; + if (position) { + if(!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) + return null; + lastCrashFile = new File(Environment.getExternalStorageDirectory() + externalCrashFilePath, lastCrashName); + } + else { + lastCrashFile = new File(mContext.getFilesDir() + internalCrashFilePath, lastCrashName); + } + if (!lastCrashFile.isFile()) + return null; + + try { + byte[] buffer = new byte[(int) lastCrashFile.length()]; + DataInputStream din = new DataInputStream(new FileInputStream(lastCrashFile)); + din.readFully(buffer); + din.close(); + configPre.edit().putBoolean(NEW_CRAHS, false).commit(); + return new String(buffer); + } catch (Exception e) { + Log.e(TAG, "An error occured while reading crash file...", e); + } + return null; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/util/DateUtils.java b/app/src/main/java/com/hippo/ehviewer/util/DateUtils.java new file mode 100644 index 000000000..a6f8b22cd --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/util/DateUtils.java @@ -0,0 +1,81 @@ +/* + * 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.util; + +import android.text.format.DateFormat; + +public final class DateUtils { + + @SuppressWarnings("deprecation") + public static boolean hasSeconds(CharSequence inFormat) { + return hasDesignator(inFormat, DateFormat.SECONDS); + } + + + @SuppressWarnings("deprecation") + public static boolean hasDesignator(CharSequence inFormat, char designator) { + if (inFormat == null) return false; + + final int length = inFormat.length(); + + int c; + int count; + + for (int i = 0; i < length; i += count) { + count = 1; + c = inFormat.charAt(i); + + if (c == DateFormat.QUOTE) { + count = skipQuotedText(inFormat, i, length); + } else if (c == designator) { + return true; + } + } + + return false; + } + + @SuppressWarnings("deprecation") + private static int skipQuotedText(CharSequence s, int i, int len) { + if (i + 1 < len && s.charAt(i + 1) == DateFormat.QUOTE) { + return 2; + } + + int count = 1; + // skip leading quote + i++; + + while (i < len) { + char c = s.charAt(i); + + if (c == DateFormat.QUOTE) { + count++; + // QUOTEQUOTE -> QUOTE + if (i + 1 < len && s.charAt(i + 1) == DateFormat.QUOTE) { + i++; + } else { + break; + } + } else { + i++; + count++; + } + } + + return count; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/util/DialogUtils.java b/app/src/main/java/com/hippo/ehviewer/util/DialogUtils.java new file mode 100644 index 000000000..d3f06805f --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/util/DialogUtils.java @@ -0,0 +1,33 @@ +/* + * 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.util; + +import android.content.Context; + +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.app.MaterialAlertDialog; + +public class DialogUtils { + + public static MaterialAlertDialog.Builder createUpdateDialog(Context context, String version, String size, + String info, MaterialAlertDialog.OnClickListener l) { + + return new MaterialAlertDialog.Builder(context).setTitle(R.string.update).setCancelable(false) + .setMessage(String.format(context.getString(R.string.update_message), version, size, info)) + .setPositiveButton(android.R.string.ok).setNegativeButton(android.R.string.cancel).setButtonListener(l); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/util/EhUtils.java b/app/src/main/java/com/hippo/ehviewer/util/EhUtils.java new file mode 100644 index 000000000..1544845e0 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/util/EhUtils.java @@ -0,0 +1,124 @@ +/* + * 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.util; + +import java.io.File; + +import com.hippo.ehviewer.data.ListUrls; + +public final class EhUtils { + + public static final String EH_DOWNLOAD_FILENAME = ".ehviewer"; + + private static final int[] CATEGORY_VALUES = { ListUrls.MISC, ListUrls.DOUJINSHI, ListUrls.MANGA, + ListUrls.ARTIST_CG, ListUrls.GAME_CG, ListUrls.IMAGE_SET, ListUrls.COSPLAY, ListUrls.ASIAN_PORN, + ListUrls.NON_H, ListUrls.WESTERN, ListUrls.UNKNOWN }; + + // TODO How about "Private" + private static final String[][] CATEGORY_STRINGS = { new String[] { "misc" }, new String[] { "doujinshi" }, + new String[] { "manga" }, new String[] { "artistcg", "Artist CG Sets" }, + new String[] { "gamecg", "Game CG Sets" }, new String[] { "imageset", "Image Sets" }, + new String[] { "cosplay" }, new String[] { "asianporn", "Asian Porn" }, new String[] { "non-h" }, + new String[] { "western" }, new String[] { "unknown" } }; + + public static int getCategory(String type) { + int i; + for (i = 0; i < CATEGORY_STRINGS.length - 1; i++) { + for (String str : CATEGORY_STRINGS[i]) + if (str.equalsIgnoreCase(type)) + return CATEGORY_VALUES[i]; + } + + return CATEGORY_VALUES[i]; + } + + public static String getCategory(int type) { + int i; + for (i = 0; i < CATEGORY_VALUES.length - 1; i++) { + if (CATEGORY_VALUES[i] == type) + break; + } + return CATEGORY_STRINGS[i][0]; + } + + /** + * Get gallery for read and download Gid is constant, but title may be + * changed + * + * @param gid + * @param title + * @return + */ + public static File getGalleryDir(int gid, String title) { + + File downloadDir = new File(Config.getDownloadPath()); + File gDir = new File(downloadDir, Utils.standardizeFilename(Integer.toString(gid))); + File defaultDir = generateGalleryDir(gid, title); + + if (gDir.exists()) { + if (gDir.renameTo(defaultDir)) + return defaultDir; + else + return gDir; + } else { + // TODO I need a better way or only call it in non-UI thread + String[] fileList = downloadDir.list(); + if (fileList != null) { + for (String str : fileList) { + // For title changed + if (str.startsWith(gid + "-") && (gDir = new File(downloadDir, str)) != null && gDir.isDirectory()) + return gDir; + } + } + + return defaultDir; + } + } + + public static File generateGalleryDir(int gid, String title) { + return new File(Config.getDownloadPath(), Utils.standardizeFilename(gid + "-" + title)); + } + + /** + * Index start from 0 + * + * @param index + * @param extension + * @return + */ + public static String getImageFilename(int index, String extension) { + return String.format("%08d.%s", index + 1, extension); + } + + public static String[] POSSIBLE_IMAGE_EXTENSION_ARRAY = new String[] { + "jpg", "jpeg", "png", "gif" + }; + + public static String DEFAULT_IMAGE_EXTENSION = POSSIBLE_IMAGE_EXTENSION_ARRAY[0]; + + public static String[] getPossibleImageFilenames(int index) { + String prefix = String.format("%08d.", index + 1); + + int length = POSSIBLE_IMAGE_EXTENSION_ARRAY.length; + String[] strs = new String[length]; + for (int i = 0; i < length; i++) + strs[i] = prefix + POSSIBLE_IMAGE_EXTENSION_ARRAY[i]; + + return strs; + } + +} diff --git a/app/src/main/java/com/hippo/ehviewer/util/FastByteArrayOutputStream.java b/app/src/main/java/com/hippo/ehviewer/util/FastByteArrayOutputStream.java new file mode 100644 index 000000000..d629a11c0 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/util/FastByteArrayOutputStream.java @@ -0,0 +1,227 @@ +/* + * 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.util; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; + +public class FastByteArrayOutputStream extends OutputStream { + /** + * The byte array containing the bytes written. + */ + protected byte[] buf; + + /** + * The number of bytes written. + */ + protected int count; + + /** + * Constructs a new ByteArrayOutputStream with a default size of 32 bytes. + * If more than 32 bytes are written to this instance, the underlying byte + * array will expand. + */ + public FastByteArrayOutputStream() { + buf = new byte[32]; + } + + /** + * Constructs a new {@code ByteArrayOutputStream} with a default size of + * {@code size} bytes. If more than {@code size} bytes are written to this + * instance, the underlying byte array will expand. + * + * @param size + * initial size for the underlying byte array, must be + * non-negative. + * @throws IllegalArgumentException + * if {@code size} < 0. + */ + public FastByteArrayOutputStream(int size) { + if (size >= 0) { + buf = new byte[size]; + } else { + throw new IllegalArgumentException("size < 0"); + } + } + + /** + * Closes this stream. This releases system resources used for this stream. + * + * @throws IOException + * if an error occurs while attempting to close this stream. + */ + @Override + public void close() throws IOException { + /** + * Although the spec claims "A closed stream cannot perform output + * operations and cannot be reopened.", this implementation must do + * nothing. + */ + super.close(); + } + + private void expand(int i) { + /* Can the buffer handle @i more bytes, if not expand it */ + if (count + i <= buf.length) { + return; + } + + byte[] newbuf = new byte[(count + i) * 2]; + System.arraycopy(buf, 0, newbuf, 0, count); + buf = newbuf; + } + + /** + * Resets this stream to the beginning of the underlying byte array. All + * subsequent writes will overwrite any bytes previously stored in this + * stream. + */ + public synchronized void reset() { + count = 0; + } + + /** + * Returns the total number of bytes written to this stream so far. + * + * @return the number of bytes written to this stream. + */ + public int size() { + return count; + } + + /** + * Returns the contents of this ByteArrayOutputStream as a byte array. Any + * changes made to the receiver after returning will not be reflected in the + * byte array returned to the caller. + * + * @return this stream's current contents as a byte array. + */ + public synchronized byte[] getBuffer() { + return buf; + } + + /** + * Returns the contents of this ByteArrayOutputStream as a string. Any + * changes made to the receiver after returning will not be reflected in the + * string returned to the caller. + * + * @return this stream's current contents as a string. + */ + + @Override + public String toString() { + return new String(buf, 0, count); + } + + /** + * Returns the contents of this ByteArrayOutputStream as a string. Each byte + * {@code b} in this stream is converted to a character {@code c} using the + * following function: + * {@code c == (char)(((hibyte & 0xff) << 8) | (b & 0xff))}. This method is + * deprecated and either {@link #toString()} or {@link #toString(String)} + * should be used. + * + * @param hibyte + * the high byte of each resulting Unicode character. + * @return this stream's current contents as a string with the high byte set + * to {@code hibyte}. + * @deprecated Use {@link #toString()} instead. + */ + @Deprecated + public String toString(int hibyte) { + char[] newBuf = new char[size()]; + for (int i = 0; i < newBuf.length; i++) { + newBuf[i] = (char) (((hibyte & 0xff) << 8) | (buf[i] & 0xff)); + } + return new String(newBuf); + } + + /** + * Returns the contents of this ByteArrayOutputStream as a string converted + * according to the encoding declared in {@code charsetName}. + * + * @param charsetName + * a string representing the encoding to use when translating + * this stream to a string. + * @return this stream's current contents as an encoded string. + * @throws UnsupportedEncodingException + * if the provided encoding is not supported. + */ + public String toString(String charsetName) throws UnsupportedEncodingException { + return new String(buf, 0, count, charsetName); + } + + /** + * Writes {@code count} bytes from the byte array {@code buffer} starting at + * offset {@code index} to this stream. + * + * @param buffer + * the buffer to be written. + * @param offset + * the initial position in {@code buffer} to retrieve bytes. + * @param len + * the number of bytes of {@code buffer} to write. + * @throws NullPointerException + * if {@code buffer} is {@code null}. + * @throws IndexOutOfBoundsException + * if {@code offset < 0} or {@code len < 0}, or if + * {@code offset + len} is greater than the length of + * {@code buffer}. + */ + @Override + public synchronized void write(byte[] buffer, int offset, int len) { + if ((offset | count) < 0 || offset > buffer.length || buffer.length - offset < count) { + throw new ArrayIndexOutOfBoundsException("length=" + buffer.length + "; regionStart=" + offset + + "; regionLength=" + count); + } + if (len == 0) { + return; + } + expand(len); + System.arraycopy(buffer, offset, buf, this.count, len); + this.count += len; + } + + /** + * Writes the specified byte {@code oneByte} to the OutputStream. Only the + * low order byte of {@code oneByte} is written. + * + * @param oneByte + * the byte to be written. + */ + @Override + public synchronized void write(int oneByte) { + if (count == buf.length) { + expand(1); + } + buf[count++] = (byte) oneByte; + } + + /** + * Takes the contents of this stream and writes it to the output stream + * {@code out}. + * + * @param out + * an OutputStream on which to write the contents of this stream. + * @throws IOException + * if an error occurs while writing to {@code out}. + */ + public synchronized void writeTo(OutputStream out) throws IOException { + out.write(buf, 0, count); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/util/Favorite.java b/app/src/main/java/com/hippo/ehviewer/util/Favorite.java new file mode 100644 index 000000000..a75afa76f --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/util/Favorite.java @@ -0,0 +1,127 @@ +/* + * 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.util; + +import android.annotation.SuppressLint; +import android.app.AlertDialog; +import android.content.Context; +import android.content.res.Resources; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.CheckBox; +import android.widget.ListView; + +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.app.MaterialAlertDialog; +import com.hippo.ehviewer.data.Data; +import com.hippo.ehviewer.data.GalleryInfo; +import com.hippo.ehviewer.ehclient.EhClient; +import com.hippo.ehviewer.widget.MaterialToast; + +public class Favorite { + + private static Context mContext; + private static Resources mResources; + + private static AlertDialog lastAddToFavoriteDialog; + + public static String[] FAVORITE_TITLES; + + public static void init(Context context) { + mContext = context; + mResources = mContext.getResources(); + + FAVORITE_TITLES = new String[EhClient.FAVORITE_SLOT_NUM + 1]; + FAVORITE_TITLES[0] = mResources.getString(R.string.local_favorite); + for (int i = 1; i < EhClient.FAVORITE_SLOT_NUM + 1; i++) { + FAVORITE_TITLES[i] = mResources.getString(R.string.favourite) + " "+ (i - 1); + } + } + + public static void addToFavorite(final Context context, GalleryInfo gi) { + int defaultFavorite = Config.getDefaultFavorite(); + switch (defaultFavorite) { + case -2: + Favorite.getAddToFavoriteDialog(context, gi).show(); + break; + case -1: + Data.getInstance().addLocalFavourite(gi); + MaterialToast.showToast(R.string.toast_add_favourite); + break; + default: + EhClient.getInstance().addToFavorite(gi.gid, + gi.token, defaultFavorite, null, new EhClient.OnAddToFavoriteListener() { + @Override + public void onSuccess() { + MaterialToast.showToast(R.string.toast_add_favourite); + } + @Override + public void onFailure(String eMsg) { + MaterialToast.showToast(R.string.failed_to_add); + } + }); + } + } + + @SuppressLint("InflateParams") + public static AlertDialog getAddToFavoriteDialog(final Context context, final GalleryInfo gi) { + + // TODO List do not work final + + View view = ViewUtils.inflateDialogView(R.layout.add_to_favorite, false); + final CheckBox cb = (CheckBox) view.findViewById(R.id.set_default); + ListView lv = (ListView) view.findViewById(R.id.list); + lv.setAdapter(new ArrayAdapter(mContext, + R.layout.select_dialog_item, FAVORITE_TITLES)); + lv.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, + int position, long id) { + lastAddToFavoriteDialog.dismiss(); + lastAddToFavoriteDialog = null; + + if (cb.isChecked()) + Config.setDefaultFavorite(position - 1); + + switch (position) { + case 0: + Data.getInstance().addLocalFavourite(gi); + MaterialToast.showToast(R.string.toast_add_favourite); + break; + default: + EhClient.getInstance().addToFavorite(gi.gid, + gi.token, position - 1, null, new EhClient.OnAddToFavoriteListener() { + @Override + public void onSuccess() { + MaterialToast.showToast(R.string.toast_add_favourite); + } + @Override + public void onFailure(String eMsg) { + MaterialToast.showToast(R.string.failed_to_add); + } + }); + } + } + }); + + return lastAddToFavoriteDialog = new MaterialAlertDialog.Builder(context) + .setTitle(R.string.where_to_add) + .setView(view, false).setNegativeButton(android.R.string.cancel) + .setPositiveButton(android.R.string.ok).create(); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/util/FullScreenHelper.java b/app/src/main/java/com/hippo/ehviewer/util/FullScreenHelper.java new file mode 100644 index 000000000..e5aa371dd --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/util/FullScreenHelper.java @@ -0,0 +1,156 @@ +/* + * 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.util; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.os.Build; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.Window; +import android.view.WindowManager; + +@SuppressLint("InlinedApi") +public final class FullScreenHelper implements View.OnSystemUiVisibilityChangeListener { + + private static final String TAG = FullScreenHelper.class.getSimpleName(); + + private final int NOT_FULL_SCREEN_JELLY_BEAN = + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | + View.SYSTEM_UI_FLAG_LAYOUT_STABLE; + + private final int FULL_SCREEN_JELLY_BEAN = + NOT_FULL_SCREEN_JELLY_BEAN | + View.SYSTEM_UI_FLAG_LOW_PROFILE | + View.SYSTEM_UI_FLAG_FULLSCREEN; + + private final int FULL_SCREEN_FLAG_JELLY_BEAN = + View.SYSTEM_UI_FLAG_LOW_PROFILE | + View.SYSTEM_UI_FLAG_FULLSCREEN; + + private final int NOT_FULL_SCREEN_KITKAT = + NOT_FULL_SCREEN_JELLY_BEAN | + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; + + private final int FULL_SCREEN_KITKAT = + NOT_FULL_SCREEN_KITKAT | + View.SYSTEM_UI_FLAG_LOW_PROFILE | + View.SYSTEM_UI_FLAG_FULLSCREEN | + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + + private boolean mFullScreen; + private final Activity mActivity; + private final Window mWindow; + private final View mDecorView; + + private boolean mHasNavBar; + + private OnFullScreenBrokenListener mListener; + + private final int TEST_SDK = Build.VERSION.SDK_INT; + + public interface OnFullScreenBrokenListener { + /** + * FullScreen state should be fullScreen or not, + * but user or system broke it + * + * @param fullScreen support to be + */ + public void onFullScreenBroken(boolean fullScreen); + } + + public FullScreenHelper(Activity activity) { + mFullScreen = false; + mActivity = activity; + mWindow = mActivity.getWindow(); + mDecorView = mWindow.getDecorView(); + mDecorView.setOnSystemUiVisibilityChangeListener(this); + + String mainKey = Utils.getSystemProperties("qemu.hw.mainkeys"); + int resourceId = activity.getResources().getIdentifier("config_showNavigationBar", "bool", "android"); + if (resourceId != 0) { + mHasNavBar = activity.getResources().getBoolean(resourceId); + // check override flag (see static block) + if ("1".equals(mainKey)) { + mHasNavBar = false; + } else if ("0".equals(mainKey)) { + mHasNavBar = true; + } + } else { + mHasNavBar = !ViewConfiguration.get(activity).hasPermanentMenuKey(); + } + } + + public boolean willHideNavBar() { + if (TEST_SDK >= Build.VERSION_CODES.KITKAT && mHasNavBar) + return true; + else + return false; + } + + @Override + public void onSystemUiVisibilityChange(int visibility) { + if (TEST_SDK >= Build.VERSION_CODES.JELLY_BEAN && + TEST_SDK < Build.VERSION_CODES.KITKAT) { + if ((mFullScreen && visibility != FULL_SCREEN_FLAG_JELLY_BEAN) || + (!mFullScreen && visibility != 0)) { + // User or system change visibility + if (mListener != null) + mListener.onFullScreenBroken(mFullScreen); + } + } + } + + public void setOnFullScreenBrokenListener(OnFullScreenBrokenListener l) { + mListener = l; + } + + public boolean getFullScreen() { + return mFullScreen; + } + + public void setFullScreen(boolean fullScreen) { + mFullScreen = fullScreen; + if (fullScreen) { + if (TEST_SDK < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + // Empty + } else if (TEST_SDK < Build.VERSION_CODES.JELLY_BEAN) { + mWindow.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, + WindowManager.LayoutParams.FLAG_FULLSCREEN); + + } else if (TEST_SDK < Build.VERSION_CODES.KITKAT) { + mDecorView.setSystemUiVisibility(FULL_SCREEN_JELLY_BEAN); + + } else { + mDecorView.setSystemUiVisibility(FULL_SCREEN_KITKAT); + } + } else { + if (TEST_SDK < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + // Empty + } else if (TEST_SDK < Build.VERSION_CODES.JELLY_BEAN) { + mWindow.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + + } else if (TEST_SDK < Build.VERSION_CODES.KITKAT) { + mDecorView.setSystemUiVisibility(NOT_FULL_SCREEN_JELLY_BEAN); + + } else { + mDecorView.setSystemUiVisibility(NOT_FULL_SCREEN_KITKAT); + } + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/util/IntArray.java b/app/src/main/java/com/hippo/ehviewer/util/IntArray.java new file mode 100644 index 000000000..b107cbff6 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/util/IntArray.java @@ -0,0 +1,60 @@ +/* + * 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.util; + +public class IntArray { + private static final int INIT_CAPACITY = 8; + + private int mData[] = new int[INIT_CAPACITY]; + private int mSize = 0; + + public void add(int value) { + if (mData.length == mSize) { + int temp[] = new int[mSize + mSize]; + System.arraycopy(mData, 0, temp, 0, mSize); + mData = temp; + } + mData[mSize++] = value; + } + + public int removeLast() { + mSize--; + return mData[mSize]; + } + + public int size() { + return mSize; + } + + // For testing only + public int[] toArray(int[] result) { + if (result == null || result.length < mSize) { + result = new int[mSize]; + } + System.arraycopy(mData, 0, result, 0, mSize); + return result; + } + + public int[] getInternalArray() { + return mData; + } + + public void clear() { + mSize = 0; + if (mData.length != INIT_CAPACITY) mData = new int[INIT_CAPACITY]; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/util/Log.java b/app/src/main/java/com/hippo/ehviewer/util/Log.java new file mode 100644 index 000000000..90b1b25d6 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/util/Log.java @@ -0,0 +1,149 @@ +/* + * 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.util; + +import android.content.Context; + +import com.hippo.ehviewer.BuildConfig; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; + +public final class Log { + private static final String NULL = "null"; + private static final String LOG_FILENAME = "log.log"; + + private static final Object sLock = new Object(); + + private static File sLogFile; + + public static void init(Context context) { + if (Config.sExternalDir != null) + sLogFile = new File(Config.sExternalDir, LOG_FILENAME); + else + sLogFile = new File(context.getFilesDir(), LOG_FILENAME); + } + + public static void f(final String msg) { + new BgThread() { + @Override + public void run() { + synchronized(sLock) { + try { + FileWriter fw = new FileWriter(sLogFile, true); + fw.append(Utils.sDate.format(System.currentTimeMillis())) + .append(": ").append(msg).append("\n"); + fw.flush(); + fw.close(); + } catch (IOException e) {} + } + } + }.start(); + } + + public static void f(final Throwable tr) { + new BgThread() { + @Override + public void run() { + synchronized(sLock) { + try { + FileWriter fw = new FileWriter(sLogFile, true); + fw.append(Utils.sDate.format(System.currentTimeMillis())) + .append(": ").append(Crash.getThrowableInfo(tr)).append("\n"); + fw.flush(); + fw.close(); + } catch (IOException e) {} + } + } + }.start(); + } + + public static int v(String tag, String msg) { + if (BuildConfig.DEBUG) + return android.util.Log.v(tag, msg == null ? NULL : msg); + else + return 0; + } + + public static int v(String tag, String msg, Throwable tr) { + if (BuildConfig.DEBUG) + return android.util.Log.v(tag, msg == null ? NULL : msg, tr); + else + return 0; + } + + public static int d(String tag, String msg) { + if (BuildConfig.DEBUG) + return android.util.Log.d(tag, msg == null ? NULL : msg); + else + return 0; + } + + public static int d(String tag, String msg, Throwable tr) { + if (BuildConfig.DEBUG) + return android.util.Log.d(tag, msg == null ? NULL : msg, tr); + else + return 0; + } + + public static int i(String tag, String msg) { + if (BuildConfig.DEBUG) + return android.util.Log.i(tag, msg == null ? NULL : msg); + else + return 0; + } + + public static int i(String tag, String msg, Throwable tr) { + if (BuildConfig.DEBUG) + return android.util.Log.i(tag, msg == null ? NULL : msg, tr); + else + return 0; + } + + public static int w(String tag, String msg) { + if (BuildConfig.DEBUG) + return android.util.Log.w(tag, msg == null ? NULL : msg); + else + return 0; + } + + public static int w(String tag, String msg, Throwable tr) { + if (BuildConfig.DEBUG) + return android.util.Log.w(tag, msg == null ? NULL : msg, tr); + else + return 0; + } + + public static int w(String tag, Throwable tr) { + return android.util.Log.w(tag, tr); + } + + public static int e(String tag, String msg) { + if (BuildConfig.DEBUG) + return android.util.Log.e(tag, msg == null ? NULL : msg); + else + return 0; + } + + public static int e(String tag, String msg, Throwable tr) { + if (BuildConfig.DEBUG) + return android.util.Log.e(tag, msg == null ? NULL : msg, tr); + else + return 0; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/util/MathUtils.java b/app/src/main/java/com/hippo/ehviewer/util/MathUtils.java new file mode 100644 index 000000000..634380195 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/util/MathUtils.java @@ -0,0 +1,249 @@ +/* + * 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.util; + +import java.util.Random; + +// Get code from android.util.MathUtils +public final class MathUtils { + private static final Random sRandom = new Random(); + private static final float DEG_TO_RAD = 3.1415926f / 180.0f; + private static final float RAD_TO_DEG = 180.0f / 3.1415926f; + + public static float abs(float v) { + return v > 0 ? v : -v; + } + + public static float log(float a) { + return (float) Math.log(a); + } + + public static float exp(float a) { + return (float) Math.exp(a); + } + + public static float pow(float a, float b) { + return (float) Math.pow(a, b); + } + + public static float max(float a, float b) { + return a > b ? a : b; + } + + public static float max(int a, int b) { + return a > b ? a : b; + } + + public static float max(float a, float b, float c) { + return a > b ? (a > c ? a : c) : (b > c ? b : c); + } + + public static float max(int a, int b, int c) { + return a > b ? (a > c ? a : c) : (b > c ? b : c); + } + + public static float min(float a, float b) { + return a < b ? a : b; + } + + public static float min(int a, int b) { + return a < b ? a : b; + } + + public static float min(float a, float b, float c) { + return a < b ? (a < c ? a : c) : (b < c ? b : c); + } + + public static float min(int a, int b, int c) { + return a < b ? (a < c ? a : c) : (b < c ? b : c); + } + + public static float dist(float x1, float y1, float x2, float y2) { + final float x = (x2 - x1); + final float y = (y2 - y1); + return (float) Math.hypot(x, y); + } + + public static float dist(float x1, float y1, float z1, float x2, float y2, float z2) { + final float x = (x2 - x1); + final float y = (y2 - y1); + final float z = (z2 - z1); + return (float) Math.sqrt(x * x + y * y + z * z); + } + + public static float mag(float a, float b) { + return (float) Math.hypot(a, b); + } + + public static float mag(float a, float b, float c) { + return (float) Math.sqrt(a * a + b * b + c * c); + } + + public static float sq(float v) { + return v * v; + } + + public static float radians(float degrees) { + return degrees * DEG_TO_RAD; + } + + public static float degrees(float radians) { + return radians * RAD_TO_DEG; + } + + public static float acos(float value) { + return (float) Math.acos(value); + } + + public static float asin(float value) { + return (float) Math.asin(value); + } + + public static float atan(float value) { + return (float) Math.atan(value); + } + + public static float atan2(float a, float b) { + return (float) Math.atan2(a, b); + } + + public static float tan(float angle) { + return (float) Math.tan(angle); + } + + public static int lerp(int start, int stop, float amount) { + return start + (int) ((stop - start) * amount); + } + + public static float lerp(float start, float stop, float amount) { + return start + (stop - start) * amount; + } + + public static float norm(float start, float stop, float value) { + return (value - start) / (stop - start); + } + + public static float map(float minStart, float minStop, float maxStart, float maxStop, float value) { + return maxStart + (maxStart - maxStop) * ((value - minStart) / (minStop - minStart)); + } + + /** + * Returns the input value x clamped to the range [min, max]. + * + * @param x + * @param min + * @param max + * @return + */ + public static int clamp(int x, int min, int max) { + if (x > max) return max; + if (x < min) return min; + return x; + } + + /** + * Returns the input value x clamped to the range [min, max]. + * + * @param x + * @param min + * @param max + * @return + */ + public static float clamp(float x, float min, float max) { + if (x > max) return max; + if (x < min) return min; + return x; + } + + /** + * Returns the input value x clamped to the range [min, max]. + * @param x + * @param min + * @param max + * @return + */ + public static long clamp(long x, long min, long max) { + if (x > max) return max; + if (x < min) return min; + return x; + } + + /** + * Returns the next power of two. + * Returns the input if it is already power of 2. + * Throws IllegalArgumentException if the input is <= 0 or + * the answer overflows. + * + * @param n + * @return + */ + public static int nextPowerOf2(int n) { + if (n <= 0 || n > (1 << 30)) throw new IllegalArgumentException("n is invalid: " + n); + n -= 1; + n |= n >> 16; + n |= n >> 8; + n |= n >> 4; + n |= n >> 2; + n |= n >> 1; + return n + 1; + } + + public static boolean isSameSide(float a, float b, float standard) { + float offset1 = standard - a; + float offset2 = standard - b; + return (offset1 >= 0 && offset2 >= 0) || (offset1 >= 0 && offset2 >= 0); + } + + public static boolean closer(float a, float b, float standard) { + float offset1 = standard - a; + float offset2 = standard - b; + if ((offset1 >= 0 && offset2 >= 0) || + (offset1 >= 0 && offset2 >= 0)) { + return Math.abs(offset1) < Math.abs(offset2); + } else { + float aa = a / standard; + float bb = b / standard; + aa = aa > 1.0f ? 1 / aa : aa; + bb = bb > 1.0f ? 1 / bb : bb; + return aa > bb; + } + } + + public static int random(int howbig) { + return (int) (sRandom.nextFloat() * howbig); + } + + public static int random(int howsmall, int howbig) { + if (howsmall >= howbig) + return howsmall; + return (int) (sRandom.nextFloat() * (howbig - howsmall) + howsmall); + } + + public static float random(float howbig) { + return sRandom.nextFloat() * howbig; + } + + public static float random(float howsmall, float howbig) { + if (howsmall >= howbig) + return howsmall; + return sRandom.nextFloat() * (howbig - howsmall) + howsmall; + } + + public static void randomSeed(long seed) { + sRandom.setSeed(seed); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/util/Secret.java b/app/src/main/java/com/hippo/ehviewer/util/Secret.java new file mode 100644 index 000000000..26bd27e0c --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/util/Secret.java @@ -0,0 +1,89 @@ +/* + * 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.util; + +import java.io.File; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +import com.hippo.ehviewer.network.HttpHelper; + +public final class Secret { + + private static final String DIR_NAME = "images"; + private static final String IMAGE_NAME = "sercet.jpg"; + private static final String SERCET_IMAGE_URL = "http://www.ehviewer.com/static/images/pandas.jpg"; + + private static volatile boolean sIsGetSecretImage = false; + + public static void updateSecretImage(Context context) { + if (sIsGetSecretImage) + return; + sIsGetSecretImage = true; + + new UpdateSercetImageThread(context).start(); + } + + public static boolean hasSecretImage(Context context) { + if (sIsGetSecretImage) + return false; + + File dir = context.getExternalFilesDir(DIR_NAME); + File image = new File(dir, IMAGE_NAME); + BitmapFactory.Options opts = new BitmapFactory.Options(); + opts.inJustDecodeBounds = true; + BitmapFactory.decodeFile(image.getAbsolutePath(), opts); + return opts.outWidth > 0 && opts.outHeight > 0; + } + + public static Bitmap getSecretImage(Context context) { + if (sIsGetSecretImage) + return null; + + File dir = context.getExternalFilesDir(DIR_NAME); + File image = new File(dir, IMAGE_NAME); + return BitmapFactory.decodeFile(image.getAbsolutePath(), null); + } + + private static class UpdateSercetImageThread extends BgThread { + + private final Context mContext; + + public UpdateSercetImageThread(Context context) { + mContext = context; + } + + @Override + public void run() { + // TODO check usable space + File dir = mContext.getExternalFilesDir(DIR_NAME); + File image = new File(dir, IMAGE_NAME); + BitmapFactory.Options opts = new BitmapFactory.Options(); + opts.inJustDecodeBounds = true; + BitmapFactory.decodeFile(image.getAbsolutePath(), opts); + if (opts.outWidth <= 0 || opts.outHeight <= 0) { + // Get image error, try to get image from internet + HttpHelper hh = new HttpHelper(mContext); + hh.download(SERCET_IMAGE_URL, mContext.getExternalFilesDir(DIR_NAME), + IMAGE_NAME, false, null, null); + } + sIsGetSecretImage = false; + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/util/SqlUtils.java b/app/src/main/java/com/hippo/ehviewer/util/SqlUtils.java new file mode 100644 index 000000000..80b20cd9f --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/util/SqlUtils.java @@ -0,0 +1,55 @@ +/* + * 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.util; + +import java.util.ArrayList; +import java.util.List; + +import android.database.Cursor; +import android.database.SQLException; +import android.database.sqlite.SQLiteDatabase; + +public final class SqlUtils { + + public static void exeSQLSafely(SQLiteDatabase db, String sql) { + try { + db.execSQL(sql); + } catch (SQLException e) {} + } + + public static void dropTable(SQLiteDatabase db, String tableName) { + exeSQLSafely(db, "DROP TABLE IF EXISTS " + tableName); + } + + public static void dropAllTable(SQLiteDatabase db) { + List tables = new ArrayList(); + Cursor cursor = db.rawQuery("SELECT * FROM sqlite_master WHERE type='table';", null); + cursor.moveToFirst(); + while (!cursor.isAfterLast()) { + String tableName = cursor.getString(1); + if (!tableName.equals("android_metadata") && + !tableName.equals("sqlite_sequence")) + tables.add(tableName); + cursor.moveToNext(); + } + cursor.close(); + + for(String tableName : tables) { + dropTable(db, tableName); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/util/Theme.java b/app/src/main/java/com/hippo/ehviewer/util/Theme.java new file mode 100644 index 000000000..3e4035b63 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/util/Theme.java @@ -0,0 +1,84 @@ +/* + * 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.util; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.StateListDrawable; + +public final class Theme { + + public static final int GREY_COLOR = 0xffaaaaaa; + public static final ColorDrawable GREY_DRAWABLE = + new ColorDrawable(0xffaaaaaa); + + private static final int[] DARK_COLOR_TABLE = { + 0xffe91e63, 0xff9c27b0, 0xff673ab7, + 0xff3f51b5, 0xff5677fc, 0xff009688, + 0xff259b24 + }; + + /** + * get darker color of target color, alpha not change + * @param color + * @return + */ + public static int getDarkerColor(int color) { + float[] hsv = new float[3]; + Color.colorToHSV(color, hsv); + hsv[2] *= 0.9f; + return Color.HSVToColor(hsv); + } + + public static StateListDrawable getClickDrawable(Context context, int color) { + StateListDrawable sld = new StateListDrawable(); + ColorDrawable normal = new ColorDrawable(color); + ColorDrawable dark = new ColorDrawable(getDarkerColor(color)); + + sld.addState(new int[]{-android.R.attr.state_enabled}, GREY_DRAWABLE); + sld.addState(new int[]{android.R.attr.state_enabled, android.R.attr.state_selected}, dark); + sld.addState(new int[]{android.R.attr.state_enabled, android.R.attr.state_focused}, dark); + sld.addState(new int[]{android.R.attr.state_enabled, android.R.attr.state_pressed}, dark); + sld.addState(new int[]{}, normal); + return sld; + } + + public static StateListDrawable getRadiiClickDrawable(Context context, int color) { + int corner = Ui.dp2pix(2); + float[] cornerRadii = new float[]{ + corner, corner, corner, corner, corner, corner, corner, corner}; + + StateListDrawable sld = new StateListDrawable(); + GradientDrawable normal = new GradientDrawable(); + normal.setShape(GradientDrawable.RECTANGLE); + normal.setCornerRadii(cornerRadii); + normal.setColor(color); + GradientDrawable dark = new GradientDrawable(); + dark.setShape(GradientDrawable.RECTANGLE); + dark.setCornerRadii(cornerRadii); + dark.setColor(getDarkerColor(color)); + + sld.addState(new int[]{-android.R.attr.state_enabled}, GREY_DRAWABLE); + sld.addState(new int[]{android.R.attr.state_enabled, android.R.attr.state_selected}, dark); + sld.addState(new int[]{android.R.attr.state_enabled, android.R.attr.state_focused}, dark); + sld.addState(new int[]{android.R.attr.state_enabled, android.R.attr.state_pressed}, dark); + sld.addState(new int[]{}, normal); + return sld; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/util/Ui.java b/app/src/main/java/com/hippo/ehviewer/util/Ui.java new file mode 100644 index 000000000..5d328e3de --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/util/Ui.java @@ -0,0 +1,376 @@ +/* + * 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.util; + +import android.annotation.TargetApi; +import android.app.ActionBar; +import android.app.Activity; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.View; +import android.view.View.MeasureSpec; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.widget.FrameLayout.LayoutParams; + +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.data.ListUrls; +import com.hippo.ehviewer.drawable.MaterialIndicatorDrawable; + +public class Ui { + @SuppressWarnings("unused") + private static final String TAG = Ui.class.getSimpleName(); + private static Context mContext; + private static Resources mResources; + private static final BitmapFactory.Options opt = new BitmapFactory.Options(); + + public static float mDensity; + public static float mScaledDensity; + + public static int DOUJINSHI_BG_COLOR; + public static int MANGA_BG_COLOR; + public static int ARTIST_CG_BG_COLOR; + public static int GAME_CG_BG_COLOR; + public static int WESTERN_BG_COLOR; + public static int NON_H_BG_COLOR; + public static int IMAGE_SET_BG_COLOR; + public static int COSPLAY_BG_COLOR; + public static int ASIAN_PORN_BG_COLOR; + public static int MISC_BG_COLOR; + public static int UNKNOWN_BG_COLOR; + + public static int THEME_COLOR; + + public static Drawable transparentDrawable; + + public static int ACTION_BAR_HEIGHT; + + /** + * Init Crash + * + * @param context Application context + */ + public static void init(Context context) { + mContext = context; + + opt.inPreferredConfig = Bitmap.Config.RGB_565; + opt.inPurgeable = true; + opt.inInputShareable = true; + + mResources = mContext.getResources(); + + mDensity = mResources.getDisplayMetrics().density; + mScaledDensity = mResources.getDisplayMetrics().scaledDensity; + + // init color + + DOUJINSHI_BG_COLOR = mResources.getColor(R.color.doujinshi_bg); + MANGA_BG_COLOR = mResources.getColor(R.color.manga_bg); + ARTIST_CG_BG_COLOR = mResources.getColor(R.color.artist_cg_bg); + GAME_CG_BG_COLOR = mResources.getColor(R.color.game_cg_bg); + WESTERN_BG_COLOR = mResources.getColor(R.color.western_bg); + NON_H_BG_COLOR = mResources.getColor(R.color.non_h_bg); + IMAGE_SET_BG_COLOR = mResources.getColor(R.color.image_set_bg); + COSPLAY_BG_COLOR = mResources.getColor(R.color.cosplay_bg); + ASIAN_PORN_BG_COLOR = mResources.getColor(R.color.asian_porn_bg); + MISC_BG_COLOR = mResources.getColor(R.color.misc_bg); + UNKNOWN_BG_COLOR = mResources.getColor(R.color.unknown_bg); + + THEME_COLOR = mResources.getColor(R.color.theme); + + // init drawable + transparentDrawable = new ColorDrawable(Color.TRANSPARENT); + + ACTION_BAR_HEIGHT = mResources.getDimensionPixelSize(R.dimen.action_bar_height); + } + + public static int getInternalDimensionSize(Resources res, String key) { + int result = 0; + int resourceId = res.getIdentifier(key, "dimen", "android"); + if (resourceId > 0) { + result = res.getDimensionPixelSize(resourceId); + } + return result; + } + + public static int getThemeDimensionPixelSize(android.content.res.Resources.Theme theme, int attrId) { + TypedValue tv = new TypedValue(); + theme.resolveAttribute(attrId, tv, true); + return TypedValue.complexToDimensionPixelSize(tv.data, mContext.getResources().getDisplayMetrics()); + } + + public static String getCategoryText(int category) { + switch (category) { + case ListUrls.DOUJINSHI: + return mResources.getString(R.string.doujinshi); + case ListUrls.MANGA: + return mResources.getString(R.string.manga); + case ListUrls.ARTIST_CG: + return mResources.getString(R.string.artist_cg); + case ListUrls.GAME_CG: + return mResources.getString(R.string.game_cg); + case ListUrls.WESTERN: + return mResources.getString(R.string.western); + case ListUrls.NON_H: + return mResources.getString(R.string.non_h); + case ListUrls.IMAGE_SET: + return mResources.getString(R.string.image_set); + case ListUrls.COSPLAY: + return mResources.getString(R.string.cosplay); + case ListUrls.ASIAN_PORN: + return mResources.getString(R.string.asian_porn); + case ListUrls.MISC: + return mResources.getString(R.string.misc); + default: + return mResources.getString(R.string.unknown); + } + } + + public static int getCategoryColor(int category) { + switch (category) { + case ListUrls.DOUJINSHI: + return DOUJINSHI_BG_COLOR; + case ListUrls.MANGA: + return MANGA_BG_COLOR; + case ListUrls.ARTIST_CG: + return ARTIST_CG_BG_COLOR; + case ListUrls.GAME_CG: + return GAME_CG_BG_COLOR; + case ListUrls.WESTERN: + return WESTERN_BG_COLOR; + case ListUrls.NON_H: + return NON_H_BG_COLOR; + case ListUrls.IMAGE_SET: + return IMAGE_SET_BG_COLOR; + case ListUrls.COSPLAY: + return COSPLAY_BG_COLOR; + case ListUrls.ASIAN_PORN: + return ASIAN_PORN_BG_COLOR; + case ListUrls.MISC: + return MISC_BG_COLOR; + default: + return UNKNOWN_BG_COLOR; + } + } + + /** + * dp conversion to pix + * + * @param dp The value you want to conversion + * @return value in pix + */ + public static int dp2pix(float dp) { + return (int) (mDensity * dp + 0.5f); + } + + public static float pix2dp(int pix) { + return pix/mDensity; + } + + public static int sp2pix(float sp) { + return (int) (sp * mScaledDensity + 0.5f); + } + + public static float pix2sp(float pix) { + return pix / mScaledDensity; + } + + /** + * Get default BitmapFactory.Options + * + * @return + */ + public static BitmapFactory.Options getBitmapOpt() { + return opt; + } + + /** + * Check has navigation bar or not + * @return + */ + public static boolean hasNavigationBar() { + boolean hasNavigationBar = false; + boolean hasMenuKey = ViewConfiguration.get(mContext).hasPermanentMenuKey(); + boolean hasBackKey = KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_BACK); + + if(!hasMenuKey && !hasBackKey) + hasNavigationBar = true; + return hasNavigationBar; + } + + /** + * Get height of status bar + * @return + */ + public static int getStatusBarHeight() { + Resources resources = mContext.getResources(); + int sbhResourceId = resources.getIdentifier("status_bar_height", + "dimen", "android"); + if (sbhResourceId > 0) + return resources.getDimensionPixelSize(sbhResourceId); + else return 0; + } + + /** + * Get height of navigation bar + * @param force If false, alway return 0 if the device has no navigation bar + * @return + */ + public static int getNavigationBarHeight(boolean force) { + if (!force && !hasNavigationBar()) + return 0; + int nbhResourceId = mResources.getIdentifier( + "navigation_bar_height", "dimen", "android"); + if (nbhResourceId > 0) + return mResources.getDimensionPixelSize(nbhResourceId); + else + return 0; + } + + public static void measureView(View child) { + ViewGroup.LayoutParams p = child.getLayoutParams(); + if (p == null) { + p = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + } + int childWidthSpec = ViewGroup.getChildMeasureSpec(0, 0 + 0, p.width); + int lpHeight = p.height; + int childHeightSpec; + if (lpHeight > 0) { + childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, + MeasureSpec.EXACTLY); + } else { + childHeightSpec = MeasureSpec.makeMeasureSpec(0, + MeasureSpec.UNSPECIFIED); + } + child.measure(childWidthSpec, childHeightSpec); + } + + /** + * Update translucent state for API >= 19, KK or more powerful + * + * @param activity + */ + @TargetApi(Build.VERSION_CODES.KITKAT) + public static void updateTranslucent(Activity activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + Configuration c = activity.getResources().getConfiguration(); + Window w = activity.getWindow(); + + if (c.orientation == Configuration.ORIENTATION_PORTRAIT || + activity.getResources().getBoolean(R.bool.is_table)) + w.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + else + w.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + + // For KK set a color view at top to make color status bar + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) + w.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + } + } + + /** + * Return Configuration.ORIENTATION_PORTRAIT only + * if orientation != Configuration.ORIENTATION_LANDSCAPE + * + * @param orientation + * @return + */ + public static int filterOrientation(int orientation) { + if (orientation != Configuration.ORIENTATION_LANDSCAPE) + orientation = Configuration.ORIENTATION_PORTRAIT; + return orientation; + } + + /** + * Set config Orientation + * + * @param activity + */ + public static void adjustOrientation(Activity activity) { + int screenOri = Config.getScreenOriMode(); + if (screenOri != activity.getRequestedOrientation()) + activity.setRequestedOrientation(screenOri); + } + + public static void colorStatusBarKK(Activity activity, int color, int height) { + int darkColor = Theme.getDarkerColor(color); + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { + View statusBarBgView; + FrameLayout.LayoutParams lp; + ViewGroup decorViewGroup = (ViewGroup) activity.getWindow().getDecorView(); + + // Try to find old translucent view + if (decorViewGroup.getChildCount() > 1 && + (statusBarBgView = decorViewGroup.getChildAt(1)) != null && + statusBarBgView.getId() == R.id.translucent_view) { + lp = (LayoutParams) statusBarBgView.getLayoutParams(); + lp.height = height; + lp.gravity = Gravity.TOP; + statusBarBgView.setBackgroundColor(darkColor); + statusBarBgView.setLayoutParams(lp); + } else { + statusBarBgView = new View(activity); + statusBarBgView.setId(R.id.translucent_view); + statusBarBgView.setBackgroundColor(darkColor); + lp = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, height); + lp.gravity = Gravity.TOP; + decorViewGroup.addView(statusBarBgView, lp); + } + } + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public static void colorStatusBarL(Activity activity, int color) { + int darkColor = Theme.getDarkerColor(color); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + Window w = activity.getWindow(); + w.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + w.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + w.setStatusBarColor(Theme.getDarkerColor(darkColor)); + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) + public static void setMaterialIndicator(ActionBar actionBar, MaterialIndicatorDrawable d) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + actionBar.setDisplayShowHomeEnabled(true); + actionBar.setHomeButtonEnabled(true); + actionBar.setDisplayHomeAsUpEnabled(false); + actionBar.setIcon(d); + } else { + actionBar.setHomeButtonEnabled(true); + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setHomeAsUpIndicator(d); + actionBar.setIcon(null); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/util/Utils.java b/app/src/main/java/com/hippo/ehviewer/util/Utils.java new file mode 100644 index 000000000..dd6d7a903 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/util/Utils.java @@ -0,0 +1,743 @@ +/* + * 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.util; + +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Method; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Vibrator; +import android.text.TextUtils; +import android.webkit.MimeTypeMap; + +import com.hippo.ehviewer.AppHandler; + +public final class Utils { + @SuppressWarnings("unused") + private static String TAG = Utils.class.getSimpleName(); + + public static final boolean SUPPORT_IMAGE; + @SuppressLint("SimpleDateFormat") + public static final DateFormat sDate = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + @SuppressLint("SimpleDateFormat") + public static final DateFormat sDate2 = new SimpleDateFormat("yyyyMMdd"); + + + static { + String cpu = Build.CPU_ABI; + SUPPORT_IMAGE = cpu.equals("armeabi") || cpu.equals("armeabi-v7a") || + cpu.equals("arm64-v8a") || cpu.equals("x86") || cpu.equals("x86_64"); + } + + /** + * Put InputStream to File, default bufferSize is 512 * 1024 + * + * @param is + * @param file + * @throws IOException + */ + public static void inputStream2File(InputStream is, File file) throws IOException { + OutputStream os = new FileOutputStream(file); + copy(is, os); + } + + public static void copy(InputStream is, OutputStream os) throws IOException { + copy(is, os, 512 * 1024); + } + + public static void copy(InputStream is, OutputStream os, int size) throws IOException { + byte[] buffer = new byte[size]; + int bytesRead; + while((bytesRead = is.read(buffer)) !=-1) + os.write(buffer, 0, bytesRead); + is.close(); + os.flush(); + os.close(); + buffer = null; + } + + public static boolean copy(File src, File dst) { + try { + copy(new FileInputStream(src), new FileOutputStream(dst)); + } catch (Exception e) { + return false; + } + return true; + } + + public static interface OnCopyOverListener { + public void onCopyOver(boolean success, File src, File dst); + } + + public static void copyInNewThread(final File src, final File dst, final OnCopyOverListener l) { + new BgThread(new Runnable() { + @Override + public void run() { + final boolean ok = copy(src, dst); + if (l != null) { + AppHandler.getInstance().post(new Runnable() { + @Override + public void run() { + l.onCopyOver(ok, src, dst); + } + }); + } + } + }).start(); + } + + public static boolean isNumber(String str) { + for (int i = 0; i < str.length(); i++) { + char ch = str.charAt(i); + if (ch < '0' || ch > '9') + return false; + } + return true; + } + + public static String byteArrayToHexString(byte[] b) { + StringBuilder sb = new StringBuilder(b.length * 2); + for (byte element : b) { + int v = element & 0xFF; + if (v < 16) { + sb.append('0'); + } + sb.append(Integer.toHexString(v)); + } + return sb.toString().toUpperCase(Locale.getDefault()); + } + + public static byte[] hexStringToByteArray(String s) { + int len = s.length()/2*2; + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[(i / 2)] = ((byte) ((Character.digit(s.charAt(i), 16) << 4) + Character + .digit(s.charAt(i + 1), 16))); + } + return data; + } + + /** + * Convernt context in stream to string + * + * @param is + * @param charset + * @return + */ + public static String InputStream2String(InputStream is, String charset) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + String str = null; + try { + Utils.copy(is, baos, 1024); + str = baos.toString(charset); + } catch (IOException e) { + e.printStackTrace(); + } finally { + Utils.closeQuietly(baos); + } + return str; + } + + public static String[] getStrings(SharedPreferences shaper, String key) { + String str = shaper.getString(key, null); + if (str == null || str.length() == 0) + return null; + return new String(Utils.hexStringToByteArray(str)).split("\n"); + } + + public static void putStrings(SharedPreferences shaper, String key, List strs) { + StringBuffer sb = new StringBuffer(); + for (String item : strs) { + sb.append(item); + sb.append('\n'); + } + int length = sb.length(); + if (length != 0) + sb.delete(length - 1, length); + shaper.edit().putString(key, byteArrayToHexString(sb.toString().getBytes())).apply(); + } + + public static void closeQuietly (Closeable is) { + try { + if (is != null) + is.close(); + } catch (IOException e) { + } + } + + /** + * Try to delete file, dir and it's children + * + * @param dir + * The dir to deleted + */ + public static void deleteFile(File file) { + File[] files = file.listFiles(); + if (files != null) { + for (File f : files) + deleteFile(f); + } + final File to = new File(file.getAbsolutePath() + + System.currentTimeMillis()); + if (file.renameTo(to)) + to.delete(); + else + file.delete(); + } + + /** + * Start a new thread to delete dir + */ + public static void deleteDirInThread(final File file) { + new BgThread() { + @Override + public void run() { + deleteFile(file); + } + }.start(); + } + + static { + + } + + public static int getDate() { + int time = 0; + try { + time = Integer.parseInt(sDate2.format(new Date())); + } catch (NumberFormatException e) {} + return time; + } + + + private static String[] SIZE_UNIT = {"%.2f B", "%.2f KB", "%.2f MB", "%.2f GB"}; + + public static String sizeToString(long size) { + int length = SIZE_UNIT.length; + + float sizeFloat = size; + for (int i = 0; i < length; i++) { + if (sizeFloat < 1024 || i == length - 1) { + return String.format(SIZE_UNIT[i], sizeFloat); + } + sizeFloat /= 1024; + } + return null; + } + + /** + * Execute an {@link AsyncTask} on a thread pool + * + * @param forceSerial True to force the task to run in serial order + * @param task Task to execute + * @param args Optional arguments to pass to + * {@link AsyncTask#execute(Object[])} + * @param Task argument type + */ + @SuppressLint("NewApi") + public static void execute(final boolean forceSerial, final AsyncTask task, + final T... args) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.DONUT) { + throw new UnsupportedOperationException( + "This class can only be used on API 4 and newer."); + } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB || forceSerial) { + task.execute(args); + } else { + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, args); + } + } + + private static final String[] ESCAPE_CHARATER_LIST = { + "&", + "<", + ">", + """, + "'", + "×" + }; + + private static final String[] UNESCAPE_CHARATER_LIST = { + "&", + "<", + ">", + "\"", + "'", + "×" + }; + + /** + * Mostly title and url need it + * @param str + * @return + */ + public static String unescapeXml(String str) { + return replaceEach(str, ESCAPE_CHARATER_LIST, UNESCAPE_CHARATER_LIST); + } + + /** + * Make file name valid by removing invalid character and set max length + * @param name + * @return + */ + public static String standardizeFilename(String name) { + name = name.replaceAll("[\\\\/:*?\"<>\\|]", ""); + name = name.length() > 255 ? name.substring(0, 255) : name; + return name.replaceAll("^[\\s]+", "").replace("[\\s]+$", ""); + } + + /** + * Joins the elements of the provided array into a single String + * containing the provided list of elements. + * + * @param array + * @param separator + * @return + */ + public static String join(final Object[] array, + final Object separator) { + final StringBuilder sb = new StringBuilder(array.length * 16); + for (int i = 0; i < array.length; i++) { + if (i != 0) + sb.append(separator); + sb.append(array[i]); + } + return sb.toString(); + } + + /** + *

+ * Replaces all occurrences of Strings within another String. + *

+ * + *

+ * A {@code null} reference passed to this method is a no-op, or if + * any "search string" or "string to replace" is null, that replace will be + * ignored. This will not repeat. For repeating replaces, call the + * overloaded method. + *

+ * + *
+     *  StringUtils.replaceEach(null, *, *)        = null
+     *  StringUtils.replaceEach("", *, *)          = ""
+     *  StringUtils.replaceEach("aba", null, null) = "aba"
+     *  StringUtils.replaceEach("aba", new String[0], null) = "aba"
+     *  StringUtils.replaceEach("aba", null, new String[0]) = "aba"
+     *  StringUtils.replaceEach("aba", new String[]{"a"}, null)  = "aba"
+     *  StringUtils.replaceEach("aba", new String[]{"a"}, new String[]{""})  = "b"
+     *  StringUtils.replaceEach("aba", new String[]{null}, new String[]{"a"})  = "aba"
+     *  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"w", "t"})  = "wcte"
+     *  (example of how it does not repeat)
+     *  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "t"})  = "dcte"
+     * 
+ * + * @param text + * text to search and replace in, no-op if null + * @param searchList + * the Strings to search for, no-op if null + * @param replacementList + * the Strings to replace them with, no-op if null + * @return the text with any replacements processed, {@code null} if + * null String input + * @throws IllegalArgumentException + * if the lengths of the arrays are not the same (null is ok, + * and/or size 0) + */ + // Get from org.apache.commons.lang3.StringUtils + public static String replaceEach(final String text, final String[] searchList, final String[] replacementList) { + return replaceEach(text, searchList, replacementList, false, 0); + } + + /** + *

+ * Replaces all occurrences of Strings within another String. + *

+ * + *

+ * A {@code null} reference passed to this method is a no-op, or if + * any "search string" or "string to replace" is null, that replace will be + * ignored. + *

+ * + *
+     *  StringUtils.replaceEach(null, *, *, *) = null
+     *  StringUtils.replaceEach("", *, *, *) = ""
+     *  StringUtils.replaceEach("aba", null, null, *) = "aba"
+     *  StringUtils.replaceEach("aba", new String[0], null, *) = "aba"
+     *  StringUtils.replaceEach("aba", null, new String[0], *) = "aba"
+     *  StringUtils.replaceEach("aba", new String[]{"a"}, null, *) = "aba"
+     *  StringUtils.replaceEach("aba", new String[]{"a"}, new String[]{""}, *) = "b"
+     *  StringUtils.replaceEach("aba", new String[]{null}, new String[]{"a"}, *) = "aba"
+     *  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"w", "t"}, *) = "wcte"
+     *  (example of how it repeats)
+     *  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "t"}, false) = "dcte"
+     *  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "t"}, true) = "tcte"
+     *  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "ab"}, *) = IllegalStateException
+     * 
+ * + * @param text + * text to search and replace in, no-op if null + * @param searchList + * the Strings to search for, no-op if null + * @param replacementList + * the Strings to replace them with, no-op if null + * @param repeat if true, then replace repeatedly + * until there are no more possible replacements or timeToLive < 0 + * @param timeToLive + * if less than 0 then there is a circular reference and endless + * loop + * @return the text with any replacements processed, {@code null} if + * null String input + * @throws IllegalStateException + * if the search is repeating and there is an endless loop due + * to outputs of one being inputs to another + * @throws IllegalArgumentException + * if the lengths of the arrays are not the same (null is ok, + * and/or size 0) + */ + // Get from org.apache.commons.lang3.StringUtils + private static String replaceEach( + final String text, final String[] searchList, final String[] replacementList, final boolean repeat, final int timeToLive) { + + // mchyzer Performance note: This creates very few new objects (one major goal) + // let me know if there are performance requests, we can create a harness to measure + + if (text == null || text.isEmpty() || searchList == null || + searchList.length == 0 || replacementList == null || replacementList.length == 0) { + return text; + } + + // if recursing, this shouldn't be less than 0 + if (timeToLive < 0) { + throw new IllegalStateException("Aborting to protect against StackOverflowError - " + + "output of one loop is the input of another"); + } + + final int searchLength = searchList.length; + final int replacementLength = replacementList.length; + + // make sure lengths are ok, these need to be equal + if (searchLength != replacementLength) { + throw new IllegalArgumentException("Search and Replace array lengths don't match: " + + searchLength + + " vs " + + replacementLength); + } + + // keep track of which still have matches + final boolean[] noMoreMatchesForReplIndex = new boolean[searchLength]; + + // index on index that the match was found + int textIndex = -1; + int replaceIndex = -1; + int tempIndex = -1; + + // index of replace array that will replace the search string found + // NOTE: logic duplicated below START + for (int i = 0; i < searchLength; i++) { + if (noMoreMatchesForReplIndex[i] || searchList[i] == null || + searchList[i].isEmpty() || replacementList[i] == null) { + continue; + } + tempIndex = text.indexOf(searchList[i]); + + // see if we need to keep searching for this + if (tempIndex == -1) { + noMoreMatchesForReplIndex[i] = true; + } else { + if (textIndex == -1 || tempIndex < textIndex) { + textIndex = tempIndex; + replaceIndex = i; + } + } + } + // NOTE: logic mostly below END + + // no search strings found, we are done + if (textIndex == -1) { + return text; + } + + int start = 0; + + // get a good guess on the size of the result buffer so it doesn't have to double if it goes over a bit + int increase = 0; + + // count the replacement text elements that are larger than their corresponding text being replaced + for (int i = 0; i < searchList.length; i++) { + if (searchList[i] == null || replacementList[i] == null) { + continue; + } + final int greater = replacementList[i].length() - searchList[i].length(); + if (greater > 0) { + increase += 3 * greater; // assume 3 matches + } + } + // have upper-bound at 20% increase, then let Java take over + increase = Math.min(increase, text.length() / 5); + + final StringBuilder buf = new StringBuilder(text.length() + increase); + + while (textIndex != -1) { + + for (int i = start; i < textIndex; i++) { + buf.append(text.charAt(i)); + } + buf.append(replacementList[replaceIndex]); + + start = textIndex + searchList[replaceIndex].length(); + + textIndex = -1; + replaceIndex = -1; + tempIndex = -1; + // find the next earliest match + // NOTE: logic mostly duplicated above START + for (int i = 0; i < searchLength; i++) { + if (noMoreMatchesForReplIndex[i] || searchList[i] == null || + searchList[i].isEmpty() || replacementList[i] == null) { + continue; + } + tempIndex = text.indexOf(searchList[i], start); + + // see if we need to keep searching for this + if (tempIndex == -1) { + noMoreMatchesForReplIndex[i] = true; + } else { + if (textIndex == -1 || tempIndex < textIndex) { + textIndex = tempIndex; + replaceIndex = i; + } + } + } + // NOTE: logic duplicated above END + + } + final int textLength = text.length(); + for (int i = start; i < textLength; i++) { + buf.append(text.charAt(i)); + } + final String result = buf.toString(); + if (!repeat) { + return result; + } + + return replaceEach(result, searchList, replacementList, repeat, timeToLive - 1); + } + + /** + * 0 for false, Non 0 for true + * + * @param integer + * @return + */ + public static boolean int2boolean(int integer) { + return integer != 0; + } + + /** + * false for 0, true for 1 + * + * @param bool + * @return + */ + public static int boolean2int(boolean bool) { + return bool ? 1 : 0; + } + + /** + * Returns the ASCII characters up to but not including the next "\r\n", or + * "\n". + * + * @throws java.io.EOFException if the stream is exhausted before the next + * newline character. + */ + public static String readAsciiLine(final InputStream in) throws IOException { + final StringBuilder result = new StringBuilder(80); + while (true) { + final int c = in.read(); + if (c == -1) { + throw new EOFException(); + } else if (c == '\n') { + break; + } + + result.append((char)c); + } + final int length = result.length(); + if (length > 0 && result.charAt(length - 1) == '\r') { + result.setLength(length - 1); + } + return result.toString(); + } + + /** + * Try to make dir exists + * + * @param dir + * @param force + */ + public static void ensureDir(File dir, boolean force) { + if (!dir.exists()) { + dir.mkdirs(); + } else if (force && dir.isFile()) { + dir.delete(); + dir.mkdirs(); + } + } + + /** + * Return true if point is in the rect + * + * @param area + * @param x + * @param y + * @return + */ + public static boolean isInArea(int[] area, int x, int y) { + if (area == null) + throw new NullPointerException("area is null in " + + Utils.class.getName() + ".isInArea"); + if (area.length != 4) + throw new IllegalArgumentException( + "area's length should be 4, but it's length is " + area.length); + if (x >= area[0] && x < area[2] && y >= area[1] && y < area[3]) + return true; + else + return false; + } + + public static boolean isOpaque(int color) { + return color >>> 24 == 0xFF; + } + + /** + * Throws AssertionError if the input is false. + * @param cond + */ + public static void assertTrue(boolean cond) { + if (!cond) { + throw new AssertionError(); + } + } + + /** + * vibrator + */ + public static void vibrator(Context context, long milliseconds) { + Vibrator v = (Vibrator)context.getSystemService(Context.VIBRATOR_SERVICE); + v.vibrate(milliseconds); + } + + public static String getPathName(String parent, String name) { + StringBuilder sb = new StringBuilder(parent); + if (parent.charAt(parent.length() - 1) != File.separatorChar) + sb.append(File.separatorChar); + sb.append(name); + return sb.toString(); + } + + public static int parseIntSafely(String str, int defaultValue) { + try { + return Integer.parseInt(str); + } catch (Throwable e) { + return defaultValue; + } + } + + public static int[] toIntArray(Collection collection) { + Object[] boxedArray = collection.toArray(); + int len = boxedArray.length; + int[] array = new int[len]; + for (int i = 0; i < len; i++) { + // checkNotNull for GWT (do not optimize) + array[i] = ((Number) (boxedArray[i])).intValue(); + } + return array; + } + + // Throws NullPointerException if the input is null. + public static T checkNotNull(T object) { + if (object == null) throw new NullPointerException(); + return object; + } + + /** + * Get extension from target string, int lower case + * + * @param name + * @param defautl + * @return + */ + public static String getExtension(String name, String defautl) { + String ext = MimeTypeMap.getFileExtensionFromUrl(name); + if (TextUtils.isEmpty(ext)) + ext = defautl; + return ext; + } + + /** + * It will return null if can't find mime + * + * @param url + * @return + */ + public static String getMimeType(String url) { + String type = null; + String extension = MimeTypeMap.getFileExtensionFromUrl(url); + if (extension != null) { + MimeTypeMap mime = MimeTypeMap.getSingleton(); + type = mime.getMimeTypeFromExtension(extension); + } + return type; + } + + + /** + * If get error return ""; + * + * @param key + * @return + */ + public static String getSystemProperties(String key) { + try { + Class c = Class.forName("android.os.SystemProperties"); + Method m = c.getDeclaredMethod("get", String.class); + m.setAccessible(true); + return (String) m.invoke(null, key); + } catch (Throwable e) { + return ""; + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/util/ViewUtils.java b/app/src/main/java/com/hippo/ehviewer/util/ViewUtils.java new file mode 100644 index 000000000..cabdddd73 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/util/ViewUtils.java @@ -0,0 +1,205 @@ +/* + * 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.util; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.MeasureSpec; +import android.view.ViewGroup; +import android.view.ViewParent; + +import com.hippo.ehviewer.R; + +public final class ViewUtils { + + public static ContextThemeWrapper sContextThemeWrapper; + public static ContextThemeWrapper sDarkContextThemeWrapper; + private static LayoutInflater sDialogViewInflater; + private static LayoutInflater sDarkDialogViewInflater; + + public static final void initDialogViewInflater(Context context) { + sContextThemeWrapper = new ContextThemeWrapper(context, R.style.AppTheme_Dialog); + sDarkContextThemeWrapper = new ContextThemeWrapper(context, R.style.AppTheme_Dark_Dialog); + sDialogViewInflater = LayoutInflater.from(sContextThemeWrapper); + sDarkDialogViewInflater = LayoutInflater.from(sDarkContextThemeWrapper); + } + + public static final View inflateDialogView(int resId, boolean dark) { + LayoutInflater inflater = dark ? sDarkDialogViewInflater : sDialogViewInflater; + return inflater.inflate(resId, null); + } + + /** + * Get view center location in window + * + * @param view + * @param location + */ + public static void getCenterInWindows(View view, int[] location) { + getLocationInWindow(view, location); + location[0] += view.getWidth() / 2; + location[1] += view.getHeight() / 2; + } + + /** + * Get view location in window + * + * @param view + * @param location + */ + public static void getLocationInWindow(View view, int[] location) { + if (location == null || location.length < 2) { + throw new IllegalArgumentException( + "location must be an array of two integers"); + } + + float[] position = new float[2]; + + position[0] = view.getLeft(); + position[1] = view.getTop(); + + ViewParent viewParent = view.getParent(); + while (viewParent instanceof View) { + view = (View) viewParent; + if (view.getId() == android.R.id.content) { + break; + } + + position[0] -= view.getScrollX(); + position[1] -= view.getScrollY(); + + position[0] += view.getLeft(); + position[1] += view.getTop(); + + viewParent = view.getParent(); + } + + location[0] = (int) (position[0] + 0.5f); + location[1] = (int) (position[1] + 0.5f); + } + + public static View getAncestor(View view, int id) { + ViewParent viewParent = view.getParent(); + while (viewParent instanceof View) { + view = (View) viewParent; + if (view.getId() == id) + return view; + viewParent = view.getParent(); + } + return null; + } + + /** + * Returns a bitmap showing a screenshot of the view passed in. + * + * @param v + * @return + */ + public static Bitmap getBitmapFromView(View v) { + int width = v.getWidth(); + int height = v.getHeight(); + if (width == 0 && height == 0) { + width = v.getMeasuredWidth(); + height = v.getMeasuredHeight(); + } + Bitmap bitmap = Bitmap.createBitmap(width, height, + Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + // TODO I need to know why I need it, when ScrollView + canvas.translate(-v.getScrollX(), -v.getScrollY()); + v.draw(canvas); + return bitmap; + } + + public static boolean isClickAction(MotionEvent event) { + // TODO bad idea to check click + return event.getAction() == MotionEvent.ACTION_UP + && System.nanoTime() / 1000000 - event.getDownTime() < 200; + } + + public static void removeFromParent(View view) { + ViewParent vp = view.getParent(); + if (vp instanceof ViewGroup) + ((ViewGroup) vp).removeView(view); + } + + /** + * Method that removes the support for HardwareAcceleration from a + * {@link View}.
+ *
+ * Check AOSP notice:
+ * + *
+     * 'ComposeShader can only contain shaders of different types (a BitmapShader and a
+     * LinearGradient for instance, but not two instances of BitmapShader)'. But, 'If your
+     * application is affected by any of these missing features or limitations, you can turn
+     * off hardware acceleration for just the affected portion of your application by calling
+     * setLayerType(View.LAYER_TYPE_SOFTWARE, null).'
+     * 
+ * + * @param v + * The view + */ + public static void removeHardwareAccelerationSupport(View v) { + if (v.getLayerType() != View.LAYER_TYPE_SOFTWARE) { + v.setLayerType(View.LAYER_TYPE_SOFTWARE, null); + } + } + + public static void measureView(View v, int width, int height) { + int widthMeasureSpec; + int heightMeasureSpec; + if (width == ViewGroup.LayoutParams.WRAP_CONTENT) + widthMeasureSpec = MeasureSpec.makeMeasureSpec(0, + MeasureSpec.UNSPECIFIED); + else + widthMeasureSpec = MeasureSpec.makeMeasureSpec(Math.max(width, 0), + MeasureSpec.EXACTLY); + if (height == ViewGroup.LayoutParams.WRAP_CONTENT) + heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, + MeasureSpec.UNSPECIFIED); + else + heightMeasureSpec = MeasureSpec.makeMeasureSpec(Math.max(height, 0), + MeasureSpec.EXACTLY); + + v.measure(widthMeasureSpec, heightMeasureSpec); + } + + /** + * Determine if the supplied view is under the given point in the + * parent view's coordinate system. + * + * @param view Child view of the parent to hit test + * @param x X position to test in the parent's coordinate system + * @param y Y position to test in the parent's coordinate system + * @return true if the supplied view is under the given point, false otherwise + */ + public static boolean isViewUnder(View view, int x, int y) { + if (view == null || view.getVisibility() != View.VISIBLE) { + return false; + } + return x >= view.getLeft() && + x < view.getRight() && + y >= view.getTop() && + y < view.getBottom(); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/util/ZInterpolator.java b/app/src/main/java/com/hippo/ehviewer/util/ZInterpolator.java new file mode 100644 index 000000000..4792c2afe --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/util/ZInterpolator.java @@ -0,0 +1,38 @@ +/* + * 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.util; + +import android.animation.TimeInterpolator; + +public class ZInterpolator implements TimeInterpolator { + + private final float focalLength; + + public ZInterpolator() { + this(0.5f); + } + + public ZInterpolator(float foc) { + focalLength = foc; + } + + @Override + public float getInterpolation(float input) { + return (1.0f - focalLength / (focalLength + input)) / + (1.0f - focalLength / (focalLength + 1.0f)); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/ActionableToastBar.java b/app/src/main/java/com/hippo/ehviewer/widget/ActionableToastBar.java new file mode 100644 index 000000000..c2297f9cf --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/ActionableToastBar.java @@ -0,0 +1,229 @@ +/* + * 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.widget; + +import com.hippo.ehviewer.R; + +import android.animation.Animator; +import android.animation.AnimatorInflater; +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +/** + * A custom {@link View} that exposes an action to the user. + *

+ * This is a copy of packages/apps/UnifiedEmail/src/com/android/mail/ui/ActionableToastBar.java + * with minor modifications. + */ +public class ActionableToastBar extends LinearLayout { + private boolean mHidden = true; + private Animator mShowAnimation; + private Animator mHideAnimation; + + /** Icon for the description. */ + private ImageView mActionDescriptionIcon; + /** The clickable view */ + private View mActionButton; + /** Icon for the action button. */ + private ImageView mActionIcon; + /** The view that contains the description. */ + private TextView mActionDescriptionView; + /** The view that contains the text for the action button. */ + private TextView mActionText; + //private ToastBarOperation mOperation; + + public ActionableToastBar(Context context) { + this(context, null); + } + + public ActionableToastBar(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ActionableToastBar(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + LayoutInflater.from(context).inflate(R.layout.actionable_toast_row, this, true); + setVisibility(View.GONE); + + mActionDescriptionIcon = (ImageView)findViewById(R.id.description_icon); + mActionDescriptionView = (TextView)findViewById(R.id.description_text); + mActionButton = findViewById(R.id.action_button); + mActionIcon = (ImageView)findViewById(R.id.action_icon); + mActionText = (TextView)findViewById(R.id.action_text); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mActionDescriptionIcon = (ImageView)findViewById(R.id.description_icon); + mActionDescriptionView = (TextView)findViewById(R.id.description_text); + mActionButton = findViewById(R.id.action_button); + mActionIcon = (ImageView)findViewById(R.id.action_icon); + mActionText = (TextView)findViewById(R.id.action_text); + } + + /** + * Displays the toast bar and makes it visible. Allows the setting of + * parameters to customize the display. + * @param listener performs some action when the action button is clicked + * @param descriptionIconResourceId resource ID for the description icon or + * 0 if no icon should be shown + * @param descriptionText a description text to show in the toast bar + * @param showActionIcon if true, the action button icon should be shown + * @param actionTextResource resource ID for the text to show in the action button + * @param replaceVisibleToast if true, this toast should replace any currently visible toast. + * Otherwise, skip showing this toast. + */ + public void show(final ActionClickedListener listener, int descriptionIconResourceId, + CharSequence descriptionText, int actionIconResource, CharSequence actionText, + boolean replaceVisibleToast) { + + if (!mHidden && !replaceVisibleToast) { + return; + } + + mActionButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View widget) { + if (listener != null) { + listener.onActionClicked(); + } + hide(true); + } + }); + + // Set description icon. + if (descriptionIconResourceId == 0) { + mActionDescriptionIcon.setVisibility(GONE); + } else { + mActionDescriptionIcon.setVisibility(VISIBLE); + mActionDescriptionIcon.setImageResource(descriptionIconResourceId); + } + + mActionDescriptionView.setText(descriptionText); + if (actionIconResource == 0) { + mActionIcon.setVisibility(GONE); + } else { + mActionIcon.setVisibility(VISIBLE); + mActionIcon.setImageResource(actionIconResource); + } + mActionText.setText(actionText); + + mHidden = false; + getShowAnimation().start(); + } + + /** + * Hides the view and resets the state. + */ + public void hide(boolean animate) { + // Prevent multiple call to hide. + // Also prevent hiding if show animation is going on. + if (!mHidden && !getShowAnimation().isRunning()) { + mHidden = true; + if (getVisibility() == View.VISIBLE) { + mActionDescriptionView.setText(""); + mActionButton.setOnClickListener(null); + // Hide view once it's clicked. + if (animate) { + getHideAnimation().start(); + } else { + setAlpha(0); + setVisibility(View.GONE); + } + } + } + } + + private Animator getShowAnimation() { + if (mShowAnimation == null) { + mShowAnimation = AnimatorInflater.loadAnimator(getContext(), + R.anim.fade_in); + mShowAnimation.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + setVisibility(View.VISIBLE); + } + @Override + public void onAnimationEnd(Animator animation) { + // There is a tiny change that and hide animation could have finished right + // before the show animation finished. In that case, the hide will mark the + // view as GONE. We need to make sure the last one wins. + setVisibility(View.VISIBLE); + } + @Override + public void onAnimationCancel(Animator animation) { + } + @Override + public void onAnimationRepeat(Animator animation) { + } + }); + mShowAnimation.setTarget(this); + } + return mShowAnimation; + } + + private Animator getHideAnimation() { + if (mHideAnimation == null) { + mHideAnimation = AnimatorInflater.loadAnimator(getContext(), + R.anim.fade_out); + mHideAnimation.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + } + @Override + public void onAnimationRepeat(Animator animation) { + } + @Override + public void onAnimationEnd(Animator animation) { + setVisibility(View.GONE); + } + @Override + public void onAnimationCancel(Animator animation) { + } + }); + mHideAnimation.setTarget(this); + } + return mHideAnimation; + } + + public boolean isEventInToastBar(MotionEvent event) { + if (!isShown()) { + return false; + } + int[] xy = new int[2]; + float x = event.getX(); + float y = event.getY(); + getLocationOnScreen(xy); + return (x > xy[0] && x < (xy[0] + getWidth()) && y > xy[1] && y < xy[1] + getHeight()); + } + + /** + * Classes that wish to perform some action when the action button is clicked + * should implement this interface. + */ + public interface ActionClickedListener { + public void onActionClicked(); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/AlphaPatternDrawable.java b/app/src/main/java/com/hippo/ehviewer/widget/AlphaPatternDrawable.java new file mode 100644 index 000000000..61752fc1d --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/AlphaPatternDrawable.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2010 Daniel Nilsson Copyright (C) 2012 THe CyanogenMod 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; + +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; + +/** + * This drawable that draws a simple white and gray chess board pattern. It's + * pattern you will often see as a background behind a partly transparent image + * in many applications. + * + * @author Daniel Nilsson + */ +public class AlphaPatternDrawable extends Drawable { + + private final Paint mPaint = new Paint(); + + private final Paint mPaintWhite = new Paint(); + + private final Paint mPaintGray = new Paint(); + + private int mRectangleSize = 10; + + private int numRectanglesHorizontal; + + private int numRectanglesVertical; + + /* Bitmap in which the pattern will be cached. */ + private Bitmap mBitmap; + + /**/ + public AlphaPatternDrawable(final int rectangleSize) { + mRectangleSize = rectangleSize; + mPaintWhite.setColor(0xffffffff); + mPaintGray.setColor(0xffcbcbcb); + } + + /** + * {@inheritDoc} + */ + @Override + public void draw(final Canvas canvas) { + canvas.drawBitmap(mBitmap, null, getBounds(), mPaint); + } + + /** + * {@inheritDoc} + */ + @Override + public int getOpacity() { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public void setAlpha(final int alpha) { + throw new UnsupportedOperationException("Alpha is not supported by this drawable."); + } + + /** + * {@inheritDoc} + */ + @Override + public void setColorFilter(final ColorFilter cf) { + throw new UnsupportedOperationException("ColorFilter is not supported by this drawable."); + } + + /** + * {@inheritDoc} + */ + @Override + protected void onBoundsChange(final Rect bounds) { + super.onBoundsChange(bounds); + + final int mHeight = bounds.height(); + final int mWidth = bounds.width(); + + numRectanglesHorizontal = (int)Math.ceil((mWidth / mRectangleSize)); + numRectanglesVertical = (int)Math.ceil(mHeight / mRectangleSize); + + generatePatternBitmap(); + } + + /** + * This will generate a bitmap with the pattern as big as the rectangle we + * were allow to draw on. We do this to cache the bitmap so we don't need to + * recreate it each time draw() is called since it takes a few milliseconds. + */ + private void generatePatternBitmap() { + + if (getBounds().width() <= 0 || getBounds().height() <= 0) { + return; + } + + mBitmap = Bitmap.createBitmap(getBounds().width(), getBounds().height(), Config.ARGB_8888); + final Canvas mCanvas = new Canvas(mBitmap); + + final Rect mRect = new Rect(); + boolean mVerticalStartWhite = true; + for (int i = 0; i <= numRectanglesVertical; i++) { + boolean mIsWhite = mVerticalStartWhite; + for (int j = 0; j <= numRectanglesHorizontal; j++) { + mRect.top = i * mRectangleSize; + mRect.left = j * mRectangleSize; + mRect.bottom = mRect.top + mRectangleSize; + mRect.right = mRect.left + mRectangleSize; + + mCanvas.drawRect(mRect, mIsWhite ? mPaintWhite : mPaintGray); + + mIsWhite = !mIsWhite; + } + mVerticalStartWhite = !mVerticalStartWhite; + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/AutoWrapLayout.java b/app/src/main/java/com/hippo/ehviewer/widget/AutoWrapLayout.java new file mode 100644 index 000000000..d2e3fd208 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/AutoWrapLayout.java @@ -0,0 +1,283 @@ +/* + * 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 java.util.ArrayList; +import java.util.List; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; + +import com.hippo.ehviewer.R; + +/** + * A ViewGroup that can layout views in line and auto wrap + * + * @author Hippo + * + */ +public class AutoWrapLayout extends ViewGroup { + @SuppressWarnings("unused") + private final static String TAG = "AutoWrapLayout"; + private final List rectList = new ArrayList(); + + private Alignment mAlignment; + + private static final Alignment[] sBaseLineArray = { Alignment.TOP, + Alignment.CENTER, Alignment.BOTTOM }; + + + public enum Alignment { + TOP(0), + CENTER(1), + BOTTOM(2); + + Alignment(int ni) { + nativeInt = ni; + } + + final int nativeInt; + } + + public AutoWrapLayout(Context context) { + super(context); + } + public AutoWrapLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + public AutoWrapLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.AutoWrapLayout, defStyle, 0); + + int index = a.getInt(R.styleable.AutoWrapLayout_alignment, -1); + if (index >= 0) + setAlignment(sBaseLineArray[index]); + + a.recycle(); + } + + public void setAlignment(Alignment baseLine) { + if (baseLine == null) + return; + + if (mAlignment != baseLine) { + mAlignment = baseLine; + + requestLayout(); + invalidate(); + } + } + + public Alignment getScaleType() { + return mAlignment; + } + + private void adjustBaseLine(int lineHeight, int startIndex, int endIndex) { + if (mAlignment == Alignment.TOP) + return; + + for (int index = startIndex; index < endIndex; index++) { + final View child = getChildAt(index); + final AutoWrapLayout.LayoutParams lp = + (AutoWrapLayout.LayoutParams)child.getLayoutParams(); + Rect rect = rectList.get(index); + int offsetRaw = lineHeight - rect.height() - lp.topMargin - lp.bottomMargin; + if (mAlignment == Alignment.CENTER) + rect.offset(0, offsetRaw/2); + else if (mAlignment == Alignment.BOTTOM) + rect.offset(0, offsetRaw); + } + } + + + // TODO Take vertical mode + /** + * each row or line at least show one child + * + * horizontal only show child can show or partly show in parent + */ + @SuppressLint("DrawAllocation") + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + + int maxWidth = MeasureSpec.getSize(widthMeasureSpec); + int maxHeight = MeasureSpec.getSize(heightMeasureSpec); + + if (widthMode == MeasureSpec.UNSPECIFIED) + maxWidth = Integer.MAX_VALUE; + if (heightMode == MeasureSpec.UNSPECIFIED) + maxHeight = Integer.MAX_VALUE; + + int paddingLeft = getPaddingLeft(); + int paddingTop = getPaddingTop(); + int paddingRight = getPaddingRight(); + int paddingBottom = getPaddingBottom(); + + int maxRightBound = maxWidth - paddingRight; + int maxBottomBound = maxHeight - paddingBottom; + + int left; + int top; + int right; + int bottom; + int rightBound = paddingLeft; + int maxRightNoPadding = rightBound; + int bottomBound; + int lastMaxBottom = paddingTop; + int maxBottom = lastMaxBottom; + int childWidth; + int childHeight; + + int lineStartIndex = 0; + int lineEndIndex = 0; // endIndex + 1 + + rectList.clear(); + int childCount = getChildCount(); + for (int index = 0; index < childCount; index++) { + final View child = getChildAt(index); + child.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + if (child.getVisibility() == View.GONE) + continue; + final AutoWrapLayout.LayoutParams lp = + (AutoWrapLayout.LayoutParams)child.getLayoutParams(); + childWidth = child.getMeasuredWidth(); + childHeight = child.getMeasuredHeight(); + + left = rightBound + lp.leftMargin; + right = left + childWidth; + rightBound = right + lp.rightMargin; + if (rightBound > maxRightBound) { // Go to next row + lineEndIndex = index; + // Adjust child position base on baseline + adjustBaseLine(maxBottom - lastMaxBottom, lineStartIndex, lineEndIndex); + + // If child can't show in parent begin this line + if (maxBottom >= maxBottomBound) + break; + + // If it is first item in line, try to show it all + if (lineEndIndex == lineStartIndex) { + child.measure(MeasureSpec.makeMeasureSpec( + maxWidth - paddingLeft - paddingRight + - lp.leftMargin - lp.rightMargin, MeasureSpec.AT_MOST), + MeasureSpec.UNSPECIFIED); + childWidth = child.getMeasuredWidth(); + childHeight = child.getMeasuredHeight(); + } + left = paddingLeft + lp.leftMargin; + right = left + childWidth; + rightBound = right + lp.rightMargin; + + lastMaxBottom = maxBottom; + top = lastMaxBottom + lp.topMargin; + bottom = top + childHeight; + bottomBound = bottom + lp.bottomMargin; + + lineStartIndex = index; + } else { + top = lastMaxBottom + lp.topMargin; + bottom = top + childHeight; + bottomBound = bottom + lp.bottomMargin; + } + // Update max + if (rightBound > maxRightNoPadding) + maxRightNoPadding = rightBound; + if (bottomBound > maxBottom) + maxBottom = bottomBound; + Rect rect = new Rect(); + rect.left = left; + rect.top = top; + rect.right = right; + rect.bottom = bottom; + rectList.add(rect); + } + + // Handle last line baseline + adjustBaseLine(maxBottom - lastMaxBottom, lineStartIndex, rectList.size()); + + int measuredWidth; + int measuredHeight; + + if (widthMode == MeasureSpec.EXACTLY) + measuredWidth = maxWidth; + else + measuredWidth = maxRightNoPadding + paddingRight; + if (heightMode == MeasureSpec.EXACTLY) + measuredHeight = maxHeight; + else { + measuredHeight = maxBottom + paddingBottom; + if (heightMode == MeasureSpec.AT_MOST) + measuredHeight = measuredHeight > maxHeight ? maxHeight : measuredHeight; + } + + setMeasuredDimension(measuredWidth, measuredHeight); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + final int count = rectList.size(); + for(int i = 0; i < count; i++){ + final View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) + continue; + Rect rect = rectList.get(i); + child.layout(rect.left, rect.top, rect.right, rect.bottom); + } + } + + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new AutoWrapLayout.LayoutParams(getContext(), attrs); + } + + @Override + protected LayoutParams generateDefaultLayoutParams() { + return new AutoWrapLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + } + + @Override + protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return new LayoutParams(p); + } + + public static class LayoutParams extends ViewGroup.MarginLayoutParams { + public LayoutParams() { + this(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + } + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + } + public LayoutParams(int width, int height) { + super(width, height); + } + public LayoutParams(android.view.ViewGroup.LayoutParams source) { + super(source); + } + public LayoutParams(MarginLayoutParams source) { + super(source); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/BatteryView.java b/app/src/main/java/com/hippo/ehviewer/widget/BatteryView.java new file mode 100644 index 000000000..ad62a80eb --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/BatteryView.java @@ -0,0 +1,158 @@ +/* + * 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.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.os.BatteryManager; +import android.util.AttributeSet; +import android.widget.TextView; + +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.drawable.BatteryDrawable; +import com.hippo.ehviewer.util.Ui; + +public class BatteryView extends TextView { + @SuppressWarnings("unused") + private static final String TAG = BatteryView.class.getSimpleName(); + + private int mLevel = 0; + private boolean mCharging = false; + + private BatteryDrawable mDrawable; + + private boolean mAttached = false; + private boolean mIsChargerWorking = false; + + private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + + int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0) + * 100 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, 0); + boolean charging = (intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1) + == BatteryManager.BATTERY_STATUS_CHARGING); + + if (mLevel != level || mCharging != charging) { + mLevel = level; + mCharging = charging; + if (mCharging && mLevel != 100) { + startCharger(); + } else { + stopCharger(); + mDrawable.setElect(mLevel); + } + setText(String.valueOf(mLevel) + "%"); + } + } + }; + + private final Runnable mCharger = new Runnable() { + + private int level = 0; + + @Override + public void run() { + level += 2; + if (level > mLevel) + level = 0; + mDrawable.setElect(level, mLevel <= BatteryDrawable.WARN_LIMIT); + getHandler().postDelayed(mCharger, 200); + } + }; + + public BatteryView(Context context) { + super(context); + init(); + } + + public BatteryView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public BatteryView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + init(); + + TypedArray typedArray = context.obtainStyledAttributes( + attrs, R.styleable.BatteryView, defStyleAttr, 0); + int color = typedArray.getColor(R.styleable.BatteryView_bvColor, Color.WHITE); + typedArray.recycle(); + + mDrawable.setColor(color); + } + + private void init() { + mDrawable = new BatteryDrawable(); + // TODO how to get size ? + int height = Ui.dp2pix(12); + mDrawable.setBounds(0, 0, (int) (height / 0.618f), height); + setCompoundDrawables(mDrawable, null, null, null); + setCompoundDrawablePadding(Ui.dp2pix(5)); + } + + private void startCharger() { + if (!mIsChargerWorking) { + getHandler().post(mCharger); + mIsChargerWorking = true; + } + } + + private void stopCharger() { + if (mIsChargerWorking) { + getHandler().removeCallbacks(mCharger); + mIsChargerWorking = false; + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + if (!mAttached) { + mAttached = true; + + registerReceiver(); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + if (mAttached) { + unregisterReceiver(); + stopCharger(); + mAttached = false; + } + } + + private void registerReceiver() { + final IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_BATTERY_CHANGED); + getContext().registerReceiver(mIntentReceiver, filter, null, getHandler()); + } + + private void unregisterReceiver() { + getContext().unregisterReceiver(mIntentReceiver); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/CategoryTable.java b/app/src/main/java/com/hippo/ehviewer/widget/CategoryTable.java new file mode 100644 index 000000000..d36e6d0e7 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/CategoryTable.java @@ -0,0 +1,106 @@ +/* + * 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.util.AttributeSet; +import android.view.LayoutInflater; +import android.widget.TableLayout; + +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.data.ListUrls; +import com.hippo.ehviewer.util.Utils; + +public class CategoryTable extends TableLayout { + + private CheckTextView mDoujinshi; + private CheckTextView mManga; + private CheckTextView mArtistCG; + private CheckTextView mGameCG; + private CheckTextView mWestern; + private CheckTextView mNonH; + private CheckTextView mImageSets; + private CheckTextView mCosplay; + private CheckTextView mAsianPorn; + private CheckTextView mMisc; + + + public CategoryTable(Context context) { + super(context); + init(); + } + + public CategoryTable(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public void init() { + LayoutInflater inflater = LayoutInflater.from(getContext()); + inflater.inflate(R.layout.category_table, this); + + setStretchAllColumns(true); + + mDoujinshi = (CheckTextView)findViewById(R.id.button_doujinshi); + mManga = (CheckTextView)findViewById(R.id.button_manga); + mArtistCG = (CheckTextView)findViewById(R.id.button_artistcg); + mGameCG = (CheckTextView)findViewById(R.id.button_gamecg); + mWestern = (CheckTextView)findViewById(R.id.button_western); + mNonH = (CheckTextView)findViewById(R.id.button_non_h); + mImageSets = (CheckTextView)findViewById(R.id.button_imageset); + mCosplay = (CheckTextView)findViewById(R.id.button_cosplay); + mAsianPorn = (CheckTextView)findViewById(R.id.button_asianporn); + mMisc = (CheckTextView)findViewById(R.id.button_misc); + } + + /** + * Set each button checked or not according to category. + * + * @param category + */ + public void setCategory(int category) { + mDoujinshi.setChecked(!Utils.int2boolean(category & ListUrls.DOUJINSHI)); + mManga.setChecked(!Utils.int2boolean(category & ListUrls.MANGA)); + mArtistCG.setChecked(!Utils.int2boolean(category & ListUrls.ARTIST_CG)); + mGameCG.setChecked(!Utils.int2boolean(category & ListUrls.GAME_CG)); + mWestern.setChecked(!Utils.int2boolean(category & ListUrls.WESTERN)); + mNonH.setChecked(!Utils.int2boolean(category & ListUrls.NON_H)); + mImageSets.setChecked(!Utils.int2boolean(category & ListUrls.IMAGE_SET)); + mCosplay.setChecked(!Utils.int2boolean(category & ListUrls.COSPLAY)); + mAsianPorn.setChecked(!Utils.int2boolean(category & ListUrls.ASIAN_PORN)); + mMisc.setChecked(!Utils.int2boolean(category & ListUrls.MISC)); + } + + /** + * Get category according to button. + * @return + */ + public int getCategory() { + int category = 0; + if (!mDoujinshi.isChecked()) category |= ListUrls.DOUJINSHI; + if (!mManga.isChecked()) category |= ListUrls.MANGA; + if (!mArtistCG.isChecked()) category |= ListUrls.ARTIST_CG; + if (!mGameCG.isChecked()) category |= ListUrls.GAME_CG; + if (!mWestern.isChecked()) category |= ListUrls.WESTERN; + if (!mNonH.isChecked()) category |= ListUrls.NON_H; + if (!mImageSets.isChecked()) category |= ListUrls.IMAGE_SET; + if (!mCosplay.isChecked()) category |= ListUrls.COSPLAY; + if (!mAsianPorn.isChecked()) category |= ListUrls.ASIAN_PORN; + if (!mMisc.isChecked()) category |= ListUrls.MISC; + return category; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/CheckTextView.java b/app/src/main/java/com/hippo/ehviewer/widget/CheckTextView.java new file mode 100644 index 000000000..0ffe23935 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/CheckTextView.java @@ -0,0 +1,80 @@ +/* + * 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.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.TextView; + +public class CheckTextView extends TextView implements OnClickListener{ + @SuppressWarnings("unused") + private static String TAG = "CheckImage"; + + private static int MASK = 0xbb000000; + + private Paint mPaint; + + private boolean mChecked = false; + + public CheckTextView(Context context) { + super(context); + init(); + } + + public CheckTextView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public CheckTextView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(); + } + + private void init() { + mPaint = new Paint(); + mPaint.setColor(MASK); + setOnClickListener(this); + } + + @Override + public void onClick(View v) { + mChecked = !mChecked; + invalidate(); + } + + @SuppressLint("DrawAllocation") + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (mChecked) + canvas.drawRect(canvas.getClipBounds(), mPaint); + } + + public void setChecked(boolean checked) { + mChecked = checked; + invalidate(); + } + + public boolean isChecked() { + return mChecked; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/ColorPickerView.java b/app/src/main/java/com/hippo/ehviewer/widget/ColorPickerView.java new file mode 100644 index 000000000..da6f657c7 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/ColorPickerView.java @@ -0,0 +1,947 @@ +/* + * Copyright (C) 2010 Daniel Nilsson Copyright (C) 2012 THe CyanogenMod 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; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ComposeShader; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Paint.Style; +import android.graphics.Point; +import android.graphics.PorterDuff; +import android.graphics.RectF; +import android.graphics.Shader; +import android.graphics.Shader.TileMode; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + +/** + * Displays a color picker to the user and allow them to select a color. A + * slider for the alpha channel is also available. Enable it by setting + * setAlphaSliderVisible(boolean) to true. + * + * @author Daniel Nilsson + */ +public class ColorPickerView extends View { + + public interface OnColorChangedListener { + public void onColorChanged(View view, int color); + } + + private final static int PANEL_SAT_VAL = 0; + + private final static int PANEL_HUE = 1; + + private final static int PANEL_ALPHA = 2; + + /** + * The width in pixels of the border surrounding all color panels. + */ + private final static float BORDER_WIDTH_PX = 1; + + /** + * The width in dp of the hue panel. + */ + private float HUE_PANEL_WIDTH = 30f; + + /** + * The height in dp of the alpha panel + */ + private float ALPHA_PANEL_HEIGHT = 20f; + + /** + * The distance in dp between the different color panels. + */ + private float PANEL_SPACING = 10f; + + /** + * The radius in dp of the color palette tracker circle. + */ + private float PALETTE_CIRCLE_TRACKER_RADIUS = 5f; + + /** + * The dp which the tracker of the hue or alpha panel will extend outside of + * its bounds. + */ + private float RECTANGLE_TRACKER_OFFSET = 2f; + + private static float mDensity = 1f; + + private OnColorChangedListener mListener; + + private Paint mSatValPaint; + + private Paint mSatValTrackerPaint; + + private Paint mHuePaint; + + private Paint mHueTrackerPaint; + + private Paint mAlphaPaint; + + private Paint mAlphaTextPaint; + + private Paint mBorderPaint; + + private Shader mValShader; + + private Shader mSatShader; + + private Shader mHueShader; + + private Shader mAlphaShader; + + private int mAlpha = 0xff; + + private float mHue = 360f; + + private float mSat = 0f; + + private float mVal = 0f; + + private String mAlphaSliderText = "Alpha"; + + private int mSliderTrackerColor = 0xff1c1c1c; + + private int mBorderColor = 0xff6E6E6E; + + private boolean mShowAlphaPanel = false; + + /* + * To remember which panel that has the "focus" when processing hardware + * button data. + */ + private int mLastTouchedPanel = PANEL_SAT_VAL; + + /** + * Offset from the edge we must have or else the finger tracker will get + * clipped when it is drawn outside of the view. + */ + private float mDrawingOffset; + + /* + * Distance form the edges of the view of where we are allowed to draw. + */ + private RectF mDrawingRect; + + private RectF mSatValRect; + + private RectF mHueRect; + + private RectF mAlphaRect; + + private AlphaPatternDrawable mAlphaPattern; + + private Point mStartTouchPoint = null; + + public ColorPickerView(final Context context) { + this(context, null); + } + + public ColorPickerView(final Context context, final AttributeSet attrs) { + this(context, attrs, 0); + } + + public ColorPickerView(final Context context, final AttributeSet attrs, final int defStyle) { + super(context, attrs, defStyle); + init(); + } + + private void init() { + mDensity = getContext().getResources().getDisplayMetrics().density; + PALETTE_CIRCLE_TRACKER_RADIUS *= mDensity; + RECTANGLE_TRACKER_OFFSET *= mDensity; + HUE_PANEL_WIDTH *= mDensity; + ALPHA_PANEL_HEIGHT *= mDensity; + PANEL_SPACING = PANEL_SPACING * mDensity; + + mDrawingOffset = calculateRequiredOffset(); + + initPaintTools(); + + // Needed for receiving track ball motion events. + setFocusable(true); + setFocusableInTouchMode(true); + } + + private void initPaintTools() { + + mSatValPaint = new Paint(); + mSatValTrackerPaint = new Paint(); + mHuePaint = new Paint(); + mHueTrackerPaint = new Paint(); + mAlphaPaint = new Paint(); + mAlphaTextPaint = new Paint(); + mBorderPaint = new Paint(); + + mSatValTrackerPaint.setStyle(Style.STROKE); + mSatValTrackerPaint.setStrokeWidth(2f * mDensity); + mSatValTrackerPaint.setAntiAlias(true); + + mHueTrackerPaint.setColor(mSliderTrackerColor); + mHueTrackerPaint.setStyle(Style.STROKE); + mHueTrackerPaint.setStrokeWidth(2f * mDensity); + mHueTrackerPaint.setAntiAlias(true); + + mAlphaTextPaint.setColor(0xff1c1c1c); + mAlphaTextPaint.setTextSize(14f * mDensity); + mAlphaTextPaint.setAntiAlias(true); + mAlphaTextPaint.setTextAlign(Align.CENTER); + mAlphaTextPaint.setFakeBoldText(true); + + } + + private float calculateRequiredOffset() { + float offset = Math.max(PALETTE_CIRCLE_TRACKER_RADIUS, RECTANGLE_TRACKER_OFFSET); + offset = Math.max(offset, BORDER_WIDTH_PX * mDensity); + + return offset * 1.5f; + } + + private int[] buildHueColorArray() { + + final int[] hue = new int[361]; + + int count = 0; + for (int i = hue.length - 1; i >= 0; i--, count++) { + hue[count] = Color.HSVToColor(new float[] { + i, 1f, 1f + }); + } + + return hue; + } + + @Override + protected void onDraw(final Canvas canvas) { + + if (mDrawingRect.width() <= 0 || mDrawingRect.height() <= 0) { + return; + } + + drawSatValPanel(canvas); + drawHuePanel(canvas); + drawAlphaPanel(canvas); + + } + + private void drawSatValPanel(final Canvas canvas) { + + final RectF rect = mSatValRect; + + if (BORDER_WIDTH_PX > 0) { + mBorderPaint.setColor(mBorderColor); + canvas.drawRect(mDrawingRect.left, mDrawingRect.top, rect.right + BORDER_WIDTH_PX, + rect.bottom + BORDER_WIDTH_PX, mBorderPaint); + } + + if (mValShader == null) { + mValShader = new LinearGradient(rect.left, rect.top, rect.left, rect.bottom, + 0xffffffff, 0xff000000, TileMode.CLAMP); + } + + final int rgb = Color.HSVToColor(new float[] { + mHue, 1f, 1f + }); + + mSatShader = new LinearGradient(rect.left, rect.top, rect.right, rect.top, 0xffffffff, rgb, + TileMode.CLAMP); + final ComposeShader mShader = new ComposeShader(mValShader, mSatShader, + PorterDuff.Mode.MULTIPLY); + mSatValPaint.setShader(mShader); + + canvas.drawRect(rect, mSatValPaint); + + final Point p = satValToPoint(mSat, mVal); + + mSatValTrackerPaint.setColor(0xff000000); + canvas.drawCircle(p.x, p.y, PALETTE_CIRCLE_TRACKER_RADIUS - 1f * mDensity, + mSatValTrackerPaint); + + mSatValTrackerPaint.setColor(0xffdddddd); + canvas.drawCircle(p.x, p.y, PALETTE_CIRCLE_TRACKER_RADIUS, mSatValTrackerPaint); + + } + + private void drawHuePanel(final Canvas canvas) { + + final RectF rect = mHueRect; + + if (BORDER_WIDTH_PX > 0) { + mBorderPaint.setColor(mBorderColor); + canvas.drawRect(rect.left - BORDER_WIDTH_PX, rect.top - BORDER_WIDTH_PX, rect.right + + BORDER_WIDTH_PX, rect.bottom + BORDER_WIDTH_PX, mBorderPaint); + } + + if (mHueShader == null) { + mHueShader = new LinearGradient(rect.left, rect.top, rect.left, rect.bottom, + buildHueColorArray(), null, TileMode.CLAMP); + mHuePaint.setShader(mHueShader); + } + + canvas.drawRect(rect, mHuePaint); + + final float rectHeight = 4 * mDensity / 2; + + final Point p = hueToPoint(mHue); + + final RectF r = new RectF(); + r.left = rect.left - RECTANGLE_TRACKER_OFFSET; + r.right = rect.right + RECTANGLE_TRACKER_OFFSET; + r.top = p.y - rectHeight; + r.bottom = p.y + rectHeight; + + canvas.drawRoundRect(r, 2, 2, mHueTrackerPaint); + + } + + private void drawAlphaPanel(final Canvas canvas) { + + if (!mShowAlphaPanel || mAlphaRect == null || mAlphaPattern == null) { + return; + } + + final RectF rect = mAlphaRect; + + if (BORDER_WIDTH_PX > 0) { + mBorderPaint.setColor(mBorderColor); + canvas.drawRect(rect.left - BORDER_WIDTH_PX, rect.top - BORDER_WIDTH_PX, rect.right + + BORDER_WIDTH_PX, rect.bottom + BORDER_WIDTH_PX, mBorderPaint); + } + + mAlphaPattern.draw(canvas); + + final float[] hsv = new float[] { + mHue, mSat, mVal + }; + final int color = Color.HSVToColor(hsv); + final int acolor = Color.HSVToColor(0, hsv); + + mAlphaShader = new LinearGradient(rect.left, rect.top, rect.right, rect.top, color, acolor, + TileMode.CLAMP); + + mAlphaPaint.setShader(mAlphaShader); + + canvas.drawRect(rect, mAlphaPaint); + + if (mAlphaSliderText != null && mAlphaSliderText != "") { + canvas.drawText(mAlphaSliderText, rect.centerX(), rect.centerY() + 4 * mDensity, + mAlphaTextPaint); + } + + final float rectWidth = 4 * mDensity / 2; + + final Point p = alphaToPoint(mAlpha); + + final RectF r = new RectF(); + r.left = p.x - rectWidth; + r.right = p.x + rectWidth; + r.top = rect.top - RECTANGLE_TRACKER_OFFSET; + r.bottom = rect.bottom + RECTANGLE_TRACKER_OFFSET; + + canvas.drawRoundRect(r, 2, 2, mHueTrackerPaint); + + } + + private Point hueToPoint(final float hue) { + + final RectF rect = mHueRect; + final float height = rect.height(); + + final Point p = new Point(); + + p.y = (int)(height - hue * height / 360f + rect.top); + p.x = (int)rect.left; + + return p; + } + + private Point satValToPoint(final float sat, final float val) { + + final RectF rect = mSatValRect; + final float height = rect.height(); + final float width = rect.width(); + + final Point p = new Point(); + + p.x = (int)(sat * width + rect.left); + p.y = (int)((1f - val) * height + rect.top); + + return p; + } + + private Point alphaToPoint(final int alpha) { + + final RectF rect = mAlphaRect; + final float width = rect.width(); + + final Point p = new Point(); + + p.x = (int)(width - alpha * width / 0xff + rect.left); + p.y = (int)rect.top; + + return p; + + } + + private float[] pointToSatVal(float x, float y) { + + final RectF rect = mSatValRect; + final float[] result = new float[2]; + + final float width = rect.width(); + final float height = rect.height(); + + if (x < rect.left) { + x = 0f; + } else if (x > rect.right) { + x = width; + } else { + x = x - rect.left; + } + + if (y < rect.top) { + y = 0f; + } else if (y > rect.bottom) { + y = height; + } else { + y = y - rect.top; + } + + result[0] = 1.f / width * x; + result[1] = 1.f - 1.f / height * y; + + return result; + } + + private float pointToHue(float y) { + + final RectF rect = mHueRect; + + final float height = rect.height(); + + if (y < rect.top) { + y = 0f; + } else if (y > rect.bottom) { + y = height; + } else { + y = y - rect.top; + } + + return 360f - y * 360f / height; + } + + private int pointToAlpha(int x) { + + final RectF rect = mAlphaRect; + final int width = (int)rect.width(); + + if (x < rect.left) { + x = 0; + } else if (x > rect.right) { + x = width; + } else { + x = x - (int)rect.left; + } + + return 0xff - x * 0xff / width; + + } + + @Override + public boolean onTrackballEvent(final MotionEvent event) { + + final float x = event.getX(); + final float y = event.getY(); + + boolean update = false; + + if (event.getAction() == MotionEvent.ACTION_MOVE) { + + switch (mLastTouchedPanel) { + + case PANEL_SAT_VAL: + + float sat, + val; + + sat = mSat + x / 50f; + val = mVal - y / 50f; + + if (sat < 0f) { + sat = 0f; + } else if (sat > 1f) { + sat = 1f; + } + + if (val < 0f) { + val = 0f; + } else if (val > 1f) { + val = 1f; + } + + mSat = sat; + mVal = val; + + update = true; + + break; + + case PANEL_HUE: + + float hue = mHue - y * 10f; + + if (hue < 0f) { + hue = 0f; + } else if (hue > 360f) { + hue = 360f; + } + + mHue = hue; + + update = true; + + break; + + case PANEL_ALPHA: + + if (!mShowAlphaPanel || mAlphaRect == null) { + update = false; + } else { + + int alpha = (int)(mAlpha - x * 10); + + if (alpha < 0) { + alpha = 0; + } else if (alpha > 0xff) { + alpha = 0xff; + } + + mAlpha = alpha; + + update = true; + } + + break; + } + + } + + if (update) { + + if (mListener != null) { + mListener.onColorChanged(this, Color.HSVToColor(mAlpha, new float[] { + mHue, mSat, mVal + })); + } + + invalidate(); + return true; + } + + return super.onTrackballEvent(event); + } + + @Override + public boolean onTouchEvent(final MotionEvent event) { + + boolean update = false; + + switch (event.getAction()) { + + case MotionEvent.ACTION_DOWN: + + mStartTouchPoint = new Point((int)event.getX(), (int)event.getY()); + + update = moveTrackersIfNeeded(event); + + break; + + case MotionEvent.ACTION_MOVE: + + update = moveTrackersIfNeeded(event); + + break; + + case MotionEvent.ACTION_UP: + + mStartTouchPoint = null; + + update = moveTrackersIfNeeded(event); + + break; + + } + + if (update) { + + if (mListener != null) { + mListener.onColorChanged(this, Color.HSVToColor(mAlpha, new float[] { + mHue, mSat, mVal + })); + } + + invalidate(); + return true; + } + + return super.onTouchEvent(event); + } + + private boolean moveTrackersIfNeeded(final MotionEvent event) { + + if (mStartTouchPoint == null) { + return false; + } + + boolean update = false; + + final int startX = mStartTouchPoint.x; + final int startY = mStartTouchPoint.y; + + if (mHueRect.contains(startX, startY)) { + mLastTouchedPanel = PANEL_HUE; + + mHue = pointToHue(event.getY()); + + update = true; + } else if (mSatValRect.contains(startX, startY)) { + + mLastTouchedPanel = PANEL_SAT_VAL; + + final float[] result = pointToSatVal(event.getX(), event.getY()); + + mSat = result[0]; + mVal = result[1]; + + update = true; + } else if (mAlphaRect != null && mAlphaRect.contains(startX, startY)) { + + mLastTouchedPanel = PANEL_ALPHA; + + mAlpha = pointToAlpha((int)event.getX()); + + update = true; + } + + return update; + } + + @Override + protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { + + int width = 0; + int height = 0; + + final int widthMode = MeasureSpec.getMode(widthMeasureSpec); + final int heightMode = MeasureSpec.getMode(heightMeasureSpec); + + int widthAllowed = MeasureSpec.getSize(widthMeasureSpec); + int heightAllowed = MeasureSpec.getSize(heightMeasureSpec); + + widthAllowed = chooseWidth(widthMode, widthAllowed); + heightAllowed = chooseHeight(heightMode, heightAllowed); + + if (!mShowAlphaPanel) { + height = (int)(widthAllowed - PANEL_SPACING - HUE_PANEL_WIDTH); + + // If calculated height (based on the width) is more than the + // allowed height. + if (height > heightAllowed) { + height = heightAllowed; + width = (int)(height + PANEL_SPACING + HUE_PANEL_WIDTH); + } else { + width = widthAllowed; + } + } else { + + width = (int)(heightAllowed - ALPHA_PANEL_HEIGHT + HUE_PANEL_WIDTH); + + if (width > widthAllowed) { + width = widthAllowed; + height = (int)(widthAllowed - HUE_PANEL_WIDTH + ALPHA_PANEL_HEIGHT); + } else { + height = heightAllowed; + } + + } + + setMeasuredDimension(width, height); + } + + private int chooseWidth(final int mode, final int size) { + if (mode == MeasureSpec.AT_MOST || mode == MeasureSpec.EXACTLY) { + return size; + } else { // (mode == MeasureSpec.UNSPECIFIED) + return getPrefferedWidth(); + } + } + + private int chooseHeight(final int mode, final int size) { + if (mode == MeasureSpec.AT_MOST || mode == MeasureSpec.EXACTLY) { + return size; + } else { // (mode == MeasureSpec.UNSPECIFIED) + return getPrefferedHeight(); + } + } + + private int getPrefferedWidth() { + + int width = getPrefferedHeight(); + + if (mShowAlphaPanel) { + width -= PANEL_SPACING + ALPHA_PANEL_HEIGHT; + } + + return (int)(width + HUE_PANEL_WIDTH + PANEL_SPACING); + + } + + private int getPrefferedHeight() { + + int height = (int)(200 * mDensity); + + if (mShowAlphaPanel) { + height += PANEL_SPACING + ALPHA_PANEL_HEIGHT; + } + + return height; + } + + @Override + protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + mDrawingRect = new RectF(); + mDrawingRect.left = mDrawingOffset + getPaddingLeft(); + mDrawingRect.right = w - mDrawingOffset - getPaddingRight(); + mDrawingRect.top = mDrawingOffset + getPaddingTop(); + mDrawingRect.bottom = h - mDrawingOffset - getPaddingBottom(); + + setUpSatValRect(); + setUpHueRect(); + setUpAlphaRect(); + } + + private void setUpSatValRect() { + + final RectF dRect = mDrawingRect; + float panelSide = dRect.height() - BORDER_WIDTH_PX * 2; + + if (mShowAlphaPanel) { + panelSide -= PANEL_SPACING + ALPHA_PANEL_HEIGHT; + } + + final float left = dRect.left + BORDER_WIDTH_PX; + final float top = dRect.top + BORDER_WIDTH_PX; + final float bottom = top + panelSide; + final float right = left + panelSide; + + mSatValRect = new RectF(left, top, right, bottom); + } + + private void setUpHueRect() { + final RectF dRect = mDrawingRect; + + final float left = dRect.right - HUE_PANEL_WIDTH + BORDER_WIDTH_PX; + final float top = dRect.top + BORDER_WIDTH_PX; + final float bottom = dRect.bottom - BORDER_WIDTH_PX + - (mShowAlphaPanel ? PANEL_SPACING + ALPHA_PANEL_HEIGHT : 0); + final float right = dRect.right - BORDER_WIDTH_PX; + + mHueRect = new RectF(left, top, right, bottom); + } + + private void setUpAlphaRect() { + + if (!mShowAlphaPanel) { + return; + } + + final RectF dRect = mDrawingRect; + + final float left = dRect.left + BORDER_WIDTH_PX; + final float top = dRect.bottom - ALPHA_PANEL_HEIGHT + BORDER_WIDTH_PX; + final float bottom = dRect.bottom - BORDER_WIDTH_PX; + final float right = dRect.right - BORDER_WIDTH_PX; + + mAlphaRect = new RectF(left, top, right, bottom); + + mAlphaPattern = new AlphaPatternDrawable((int)(5 * mDensity)); + mAlphaPattern.setBounds(Math.round(mAlphaRect.left), Math.round(mAlphaRect.top), + Math.round(mAlphaRect.right), Math.round(mAlphaRect.bottom)); + + } + + /** + * Set a OnColorChangedListener to get notified when the color selected by + * the user has changed. + * + * @param listener + */ + public void setOnColorChangedListener(final OnColorChangedListener listener) { + mListener = listener; + } + + /** + * Set the color of the border surrounding all panels. + * + * @param color + */ + public void setBorderColor(final int color) { + mBorderColor = color; + invalidate(); + } + + /** + * Get the color of the border surrounding all panels. + */ + public int getBorderColor() { + return mBorderColor; + } + + /** + * Get the current color this view is showing. + * + * @return the current color. + */ + public int getColor() { + return Color.HSVToColor(mAlpha, new float[] { + mHue, mSat, mVal + }); + } + + /** + * Set the color the view should show. + * + * @param color The color that should be selected. + */ + public void setColor(final int color) { + setColor(color, false); + } + + /** + * Set the color this view should show. + * + * @param color The color that should be selected. + * @param callback If you want to get a callback to your + * OnColorChangedListener. + */ + public void setColor(final int color, final boolean callback) { + + final int alpha = Color.alpha(color); + final int red = Color.red(color); + final int blue = Color.blue(color); + final int green = Color.green(color); + + final float[] hsv = new float[3]; + + Color.RGBToHSV(red, green, blue, hsv); + + mAlpha = alpha; + mHue = hsv[0]; + mSat = hsv[1]; + mVal = hsv[2]; + + if (callback && mListener != null) { + mListener.onColorChanged(this, Color.HSVToColor(mAlpha, new float[] { + mHue, mSat, mVal + })); + } + + invalidate(); + } + + /** + * Get the drawing offset of the color picker view. The drawing offset is + * the distance from the side of a panel to the side of the view minus the + * padding. Useful if you want to have your own panel below showing the + * currently selected color and want to align it perfectly. + * + * @return The offset in pixels. + */ + public float getDrawingOffset() { + return mDrawingOffset; + } + + /** + * Set if the user is allowed to adjust the alpha panel. Default is false. + * If it is set to false no alpha will be set. + * + * @param visible + */ + public void setAlphaSliderVisible(final boolean visible) { + + if (mShowAlphaPanel != visible) { + mShowAlphaPanel = visible; + + /* + * Reset all shader to force a recreation. Otherwise they will not + * look right after the size of the view has changed. + */ + mValShader = null; + mSatShader = null; + mHueShader = null; + mAlphaShader = null; + + requestLayout(); + } + + } + + public void setSliderTrackerColor(final int color) { + mSliderTrackerColor = color; + + mHueTrackerPaint.setColor(mSliderTrackerColor); + + invalidate(); + } + + public int getSliderTrackerColor() { + return mSliderTrackerColor; + } + + /** + * Set the text that should be shown in the alpha slider. Set to null to + * disable text. + * + * @param res string resource id. + */ + public void setAlphaSliderText(final int res) { + final String text = getContext().getString(res); + setAlphaSliderText(text); + } + + /** + * Set the text that should be shown in the alpha slider. Set to null to + * disable text. + * + * @param text Text that should be shown. + */ + public void setAlphaSliderText(final String text) { + mAlphaSliderText = text; + invalidate(); + } + + /** + * Get the current value of the text that will be shown in the alpha slider. + * + * @return + */ + public String getAlphaSliderText() { + return mAlphaSliderText; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/ColorView.java b/app/src/main/java/com/hippo/ehviewer/widget/ColorView.java new file mode 100644 index 000000000..5bfc07c13 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/ColorView.java @@ -0,0 +1,59 @@ +/* + * 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.annotation.TargetApi; +import android.content.Context; +import android.graphics.Canvas; +import android.os.Build; +import android.util.AttributeSet; +import android.view.View; + +public class ColorView extends View { + + private int mColor; + + public ColorView(Context context) { + super(context); + } + + public ColorView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ColorView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public ColorView(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public void setColor(int color) { + if (mColor != color) { + mColor = color; + invalidate(); + } + } + + @Override + protected void onDraw(Canvas canvas) { + canvas.drawColor(mColor); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/DrawerListView.java b/app/src/main/java/com/hippo/ehviewer/widget/DrawerListView.java new file mode 100644 index 000000000..f1517b051 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/DrawerListView.java @@ -0,0 +1,115 @@ +/* + * 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.annotation.TargetApi; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ListView; +import android.widget.TextView; + +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.util.Ui; + +public class DrawerListView extends ListView { + + private Context mContext; + private DrawerListAdapter mAdapter; + + private static final int DRAWABLE_SIDE = Ui.dp2pix(24); + + public DrawerListView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public DrawerListView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public DrawerListView(Context context, AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context); + } + + private void init(Context context) { + mContext = context; + mAdapter = new DrawerListAdapter(); + setAdapter(mAdapter); + } + + public void setData(Drawable[] drawableArray, CharSequence[] titleArray) { + mAdapter.setData(drawableArray, titleArray); + } + + private class DrawerListAdapter extends BaseAdapter { + + private Drawable[] mDrawableArray; + private CharSequence[] mTitleArray; + + private void setData(Drawable[] drawableArray, CharSequence[] titleArray) { + if (drawableArray != null && titleArray != null) { + if (drawableArray.length != titleArray.length) + throw new java.lang.IllegalStateException("drawableArray.length != titleArray.length"); + } else if (drawableArray == null && titleArray == null) { + throw new java.lang.IllegalStateException("drawableArray and titleArray should be all null or not null"); + } + mDrawableArray = drawableArray; + mTitleArray = titleArray; + notifyDataSetChanged(); + } + + @Override + public int getCount() { + return mTitleArray == null ? 0 : mTitleArray.length; + } + + @Override + public Object getItem(int position) { + return position; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) + convertView = LayoutInflater.from(mContext).inflate(R.layout.drawer_list_item, parent, false); + + TextView tv = (TextView) convertView; + Drawable d = mDrawableArray[position]; + CharSequence t = mTitleArray[position]; + tv.setText(t); + d.setBounds(0, 0, DRAWABLE_SIDE, DRAWABLE_SIDE); + tv.setCompoundDrawables(d, null, null, null); + + return convertView; + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/ExpandableListItem.java b/app/src/main/java/com/hippo/ehviewer/widget/ExpandableListItem.java new file mode 100644 index 000000000..76d2af7d8 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/ExpandableListItem.java @@ -0,0 +1,24 @@ +/* + * 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; + +public interface ExpandableListItem { + + public void setExpanded(boolean expanding); + + public boolean getExpanded(); +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/ExpandingListView.java b/app/src/main/java/com/hippo/ehviewer/widget/ExpandingListView.java new file mode 100644 index 000000000..ef58a707c --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/ExpandingListView.java @@ -0,0 +1,563 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * 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 java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.content.Context; +import android.graphics.Canvas; +import android.support.v4.view.ViewCompat; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewTreeObserver; +import android.widget.AbsListView; +import android.widget.AdapterView; +import android.widget.ListView; + +/** + * A custom listview which supports the preview of extra content corresponding to each cell + * by clicking on the cell to hide and show the extra content. + */ +public class ExpandingListView extends ListView { + + @SuppressWarnings("unused") + private static final String TAG = ExpandingListView.class.getSimpleName(); + + private int mExpandingId; + private boolean mShouldRemoveObserver = false; + private boolean mAnimating = false; + + private final List mViewsToDraw = new ArrayList(); + + private int[] mTranslate; + + public ExpandingListView(Context context) { + super(context); + init(); + } + + public ExpandingListView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public ExpandingListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(); + } + + private void init() { + setOnItemClickListener(mItemClickListener); + } + + public void setExpandingId(int id) { + mExpandingId = id; + } + + public boolean isAnimating() { + return mAnimating; + } + + /** + * Listens for item clicks and expands or collapses the selected view depending on + * its current state. + */ + private final AdapterView.OnItemClickListener mItemClickListener = new AdapterView + .OnItemClickListener() { + @Override + public void onItemClick (AdapterView parent, View view, int position, long id) { + if (view.findViewById(mExpandingId).getVisibility() == View.GONE) { + expandView(view); + } else { + collapseView(view); + } + } + }; + + private int[] getTopAndBottomTranslationsE(int top, int bottom, int yDelta) { + int yTranslateTop = 0; + int yTranslateBottom = yDelta; + + int height = bottom - top; + int bottomLine = getHeight() - getPaddingBottom(); + + boolean isOverTop = top < getPaddingTop(); + boolean isBelowBottom = (top + height + yDelta) > bottomLine; + if (isOverTop) { + yTranslateTop = top - getPaddingTop(); + yTranslateBottom = yDelta - yTranslateTop; + } else if (isBelowBottom){ + int deltaBelow = top + height + yDelta - bottomLine; + yTranslateTop = top - deltaBelow < getPaddingTop() ? (top - getPaddingTop()) : deltaBelow; + yTranslateBottom = yDelta - yTranslateTop; + } + + return new int[] {yTranslateTop, yTranslateBottom}; + } + + private int[] getTopAndBottomTranslationsC(int top, int bottom, int yDelta, int firstTop, View view) { + int yTranslateTop = 0; + int yTranslateBottom = yDelta; + + int bottomLine = getHeight() - getPaddingBottom(); + + int index = 0; + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + if (getChildAt(i).equals(view)) { + index = i; + break; + } + } + + int maxBottom = bottom; + for (int i = index + 1; i < getChildCount(); i++) { + View v = getChildAt(i); + maxBottom += v.getVisibility() != View.GONE ? v.getHeight() + getDividerHeight() : 0; + } + + int newMaxBottom = maxBottom - yDelta; + int maxYTranslateTop = getPaddingTop() - firstTop; + + boolean isCollapsingAboveBottom = maxBottom >= bottomLine && newMaxBottom < bottomLine; + + if (isCollapsingAboveBottom) { + yTranslateTop = bottomLine - newMaxBottom; + if (yTranslateTop > maxYTranslateTop) + yTranslateTop = maxYTranslateTop; + yTranslateBottom = yDelta - yTranslateTop; + } + + return new int[] {yTranslateTop, yTranslateBottom}; + } + + + /** + * This method expands the view that was clicked and animates all the views + * around it to make room for the expanding view. There are several steps required + * to do this which are outlined below. + * + * 1. Store the current top and bottom bounds of each visible item in the listview. + * 2. Update the layout parameters of the selected view. In the context of this + * method, the view should be originally collapsed and set to some custom height. + * The layout parameters are updated so as to wrap the content of the additional + * text that is to be displayed. + * + * After invoking a layout to take place, the listview will order all the items + * such that there is space for each view. This layout will be independent of what + * the bounds of the items were prior to the layout so two pre-draw passes will + * be made. This is necessary because after the layout takes place, some views that + * were visible before the layout may now be off bounds but a reference to these + * views is required so the animation completes as intended. + * + * 3. The first predraw pass will set the bounds of all the visible items to + * their original location before the layout took place and then force another + * layout. Since the bounds of the cells cannot be set directly, the method + * setSelectionFromTop can be used to achieve a very similar effect. + * 4. The expanding view's bounds are animated to what the final values should be + * from the original bounds. + * 5. The bounds above the expanding view are animated upwards while the bounds + * below the expanding view are animated downwards. + * 6. The extra text is faded in as its contents become visible throughout the + * animation process. + * + * It is important to note that the listview is disabled during the animation + * because the scrolling behaviour is unpredictable if the bounds of the items + * within the listview are not constant during the scroll. + */ + + private void expandView(final View view) { + mAnimating = true; + + final ExpandableListItem viewObject = (ExpandableListItem)getItemAtPosition + (getPositionForView(view)); + + /* Store the original top and bottom bounds of all the cells.*/ + final int oldTop = view.getTop(); + final int oldBottom = view.getBottom(); + + final HashMap oldCoordinates = new HashMap(); + + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View v = getChildAt(i); + ViewCompat.setHasTransientState(v, true); + oldCoordinates.put(v, new int[] {v.getTop(), v.getBottom()}); + } + + /* Update the layout so the extra content becomes visible.*/ + final View expandingLayout = view.findViewById(mExpandingId); + expandingLayout.setVisibility(View.VISIBLE); + + /* Add an onPreDraw Listener to the listview. onPreDraw will get invoked after onLayout + * and onMeasure have run but before anything has been drawn. This + * means that the final post layout properties for all the items have already been + * determined, but still have not been rendered onto the screen.*/ + final ViewTreeObserver observer = getViewTreeObserver(); + observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { + + @Override + public boolean onPreDraw() { + /* Determine if this is the first or second pass.*/ + if (!mShouldRemoveObserver) { + mShouldRemoveObserver = true; + + /* Calculate what the parameters should be for setSelectionFromTop. + * The ListView must be offset in a way, such that after the animation + * takes place, all the cells that remain visible are rendered completely + * by the ListView.*/ + int newTop = view.getTop(); + int newBottom = view.getBottom(); + + int newHeight = newBottom - newTop; + int oldHeight = oldBottom - oldTop; + int delta = newHeight - oldHeight; + + mTranslate = getTopAndBottomTranslationsE(oldTop, oldBottom, delta); + + int currentTop = view.getTop(); + int futureTop = oldTop - mTranslate[0]; + + int firstChildStartTop = getChildAt(0).getTop(); + int firstVisiblePosition = getFirstVisiblePosition(); + int deltaTop = currentTop - futureTop; + + int i; + int childCount = getChildCount(); + for (i = 0; i < childCount; i++) { + View v = getChildAt(i); + int height = v.getBottom() - Math.max(0, v.getTop()); + if (deltaTop - height > 0) { + firstVisiblePosition++; + deltaTop -= height; + } else { + break; + } + } + + if (i > 0) { + firstChildStartTop = 0; + } + + setSelectionFromTop(firstVisiblePosition, firstChildStartTop - deltaTop - getPaddingTop()); + + /* Request another layout to update the layout parameters of the cells.*/ + requestLayout(); + + /* Return false such that the ListView does not redraw its contents on + * this layout but only updates all the parameters associated with its + * children.*/ + return false; + } + + /* Remove the predraw listener so this method does not keep getting called. */ + mShouldRemoveObserver = false; + observer.removeOnPreDrawListener(this); + + int yTranslateTop = mTranslate[0]; + int yTranslateBottom = mTranslate[1]; + + ArrayList animations = new ArrayList(); + + int index = indexOfChild(view); + + /* Loop through all the views that were on the screen before the cell was + * expanded. Some cells will still be children of the ListView while + * others will not. The cells that remain children of the ListView + * simply have their bounds animated appropriately. The cells that are no + * longer children of the ListView also have their bounds animated, but + * must also be added to a list of views which will be drawn in dispatchDraw.*/ + for (View v: oldCoordinates.keySet()) { + int[] old = oldCoordinates.get(v); + v.setTop(old[0]); + v.setBottom(old[1]); + if (v.getParent() == null) { + mViewsToDraw.add(v); + int delta = old[0] < oldTop ? -yTranslateTop : yTranslateBottom; + animations.add(getAnimation(v, delta, delta)); + } else { + int i = indexOfChild(v); + if (v != view) { + int delta = i > index ? yTranslateBottom : -yTranslateTop; + animations.add(getAnimation(v, delta, delta)); + } + ViewCompat.setHasTransientState(v, false); + } + } + + /* Adds animation for expanding the cell that was clicked. */ + animations.add(getAnimation(view, -yTranslateTop, yTranslateBottom)); + + /* Adds an animation for fading in the extra content. */ + animations.add(ObjectAnimator.ofFloat(view.findViewById(mExpandingId), + View.ALPHA, 0, 1)); + + /* Disabled the ListView for the duration of the animation.*/ + setEnabled(false); + setClickable(false); + + /* Play all the animations created above together at the same time. */ + AnimatorSet s = new AnimatorSet(); + s.playTogether(animations); + s.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mAnimating = false; + viewObject.setExpanded(true); + setEnabled(true); + setClickable(true); + if (mViewsToDraw.size() > 0) { + for (View v : mViewsToDraw) { + ViewCompat.setHasTransientState(v, false); + } + } + mViewsToDraw.clear(); + } + }); + s.start(); + return true; + } + }); + } + + /** + * By overriding dispatchDraw, we can draw the cells that disappear during the + * expansion process. When the cell expands, some items below or above the expanding + * cell may be moved off screen and are thus no longer children of the ListView's + * layout. By storing a reference to these views prior to the layout, and + * guaranteeing that these cells do not get recycled, the cells can be drawn + * directly onto the canvas during the animation process. After the animation + * completes, the references to the extra views can then be discarded. + */ + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + + if (mViewsToDraw.size() == 0) { + return; + } + + for (View v: mViewsToDraw) { + canvas.translate(0, v.getTop()); + v.draw(canvas); + canvas.translate(0, -v.getTop()); + } + } + + /** + * This method collapses the view that was clicked and animates all the views + * around it to close around the collapsing view. There are several steps required + * to do this which are outlined below. + * + * 1. Update the layout parameters of the view clicked so as to minimize its height + * to the original collapsed (default) state. + * 2. After invoking a layout, the listview will shift all the cells so as to display + * them most efficiently. Therefore, during the first predraw pass, the listview + * must be offset by some amount such that given the custom bound change upon + * collapse, all the cells that need to be on the screen after the layout + * are rendered by the listview. + * 3. On the second predraw pass, all the items are first returned to their original + * location (before the first layout). + * 4. The collapsing view's bounds are animated to what the final values should be. + * 5. The bounds above the collapsing view are animated downwards while the bounds + * below the collapsing view are animated upwards. + * 6. The extra text is faded out as its contents become visible throughout the + * animation process. + */ + + private void collapseView(final View view) { + mAnimating = true; + + final ExpandableListItem viewObject = (ExpandableListItem)getItemAtPosition + (getPositionForView(view)); + + /* Store the original top and bottom bounds of all the cells.*/ + final int oldTop = view.getTop(); + final int oldBottom = view.getBottom(); + final int firstTop = getChildAt(0).getTop(); + + final HashMap oldCoordinates = new HashMap(); + + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View v = getChildAt(i); + ViewCompat.setHasTransientState(v, true); + oldCoordinates.put(v, new int [] {v.getTop(), v.getBottom()}); + } + + /* Update the layout so the extra content becomes gone.*/ + final View expandingLayout = view.findViewById(mExpandingId); + expandingLayout.setVisibility(View.GONE); + + /* Add an onPreDraw listener. */ + final ViewTreeObserver observer = getViewTreeObserver(); + observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { + + @Override + public boolean onPreDraw() { + + if (!mShouldRemoveObserver) { + /*Same as for expandingView, the parameters for setSelectionFromTop must + * be determined such that the necessary cells of the ListView are rendered + * and added to it.*/ + mShouldRemoveObserver = true; + + expandingLayout.setVisibility(View.VISIBLE); + + int newTop = view.getTop(); + int newBottom = view.getBottom(); + + int newHeight = newBottom - newTop; + int oldHeight = oldBottom - oldTop; + int deltaHeight = oldHeight - newHeight; + + mTranslate = getTopAndBottomTranslationsC(oldTop, oldBottom, deltaHeight, firstTop, view); + + int currentTop = view.getTop(); + int futureTop = oldTop + mTranslate[0]; + + int firstChildStartTop = getChildAt(0).getTop(); + int firstVisiblePosition = getFirstVisiblePosition(); + int deltaTop = currentTop - futureTop; + + int i; + int childCount = getChildCount(); + for (i = 0; i < childCount; i++) { + View v = getChildAt(i); + int height = v.getBottom() - Math.max(0, v.getTop()); + if (deltaTop - height > 0) { + firstVisiblePosition++; + deltaTop -= height; + } else { + break; + } + } + + if (i > 0) { + firstChildStartTop = 0; + } + + setSelectionFromTop(firstVisiblePosition, firstChildStartTop - deltaTop - getPaddingTop()); + + requestLayout(); + + return false; + } + + mShouldRemoveObserver = false; + observer.removeOnPreDrawListener(this); + + int yTranslateTop = mTranslate[0]; + int yTranslateBottom = mTranslate[1]; + + int index = indexOfChild(view); + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View v = getChildAt(i); + int [] old = oldCoordinates.get(v); + if (old != null) { + /* If the cell was present in the ListView before the collapse and + * after the collapse then the bounds are reset to their old values.*/ + v.setTop(old[0]); + v.setBottom(old[1]); + ViewCompat.setHasTransientState(v, false); + } else { + /* If the cell is present in the ListView after the collapse but + * not before the collapse then the bounds are calculated using + * the bottom and top translation of the collapsing cell.*/ + int delta = i > index ? yTranslateBottom : -yTranslateTop; + v.setTop(v.getTop() + delta); + v.setBottom(v.getBottom() + delta); + } + } + + final View expandingLayout = view.findViewById(mExpandingId); + + /* Animates all the cells present on the screen after the collapse. */ + ArrayList animations = new ArrayList(); + for (int i = 0; i < childCount; i++) { + View v = getChildAt(i); + if (v != view) { + float diff = i > index ? -yTranslateBottom : yTranslateTop; + animations.add(getAnimation(v, diff, diff)); + } + } + + /* Adds animation for collapsing the cell that was clicked. */ + animations.add(getAnimation(view, yTranslateTop, -yTranslateBottom)); + + /* Adds an animation for fading out the extra content. */ + animations.add(ObjectAnimator.ofFloat(expandingLayout, View.ALPHA, 1, 0)); + + /* Disabled the ListView for the duration of the animation.*/ + setEnabled(false); + setClickable(false); + + /* Play all the animations created above together at the same time. */ + AnimatorSet s = new AnimatorSet(); + s.playTogether(animations); + s.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mAnimating = false; + expandingLayout.setVisibility(View.GONE); + view.setLayoutParams(new AbsListView.LayoutParams(AbsListView + .LayoutParams.MATCH_PARENT, AbsListView.LayoutParams.WRAP_CONTENT)); + viewObject.setExpanded(false); + setEnabled(true); + setClickable(true); + /* Note that alpha must be set back to 1 in case this view is reused + * by a cell that was expanded, but not yet collapsed, so its state + * should persist in an expanded state with the extra content visible.*/ + expandingLayout.setAlpha(1); + } + }); + s.start(); + + return true; + } + }); + } + + /** + * This method takes some view and the values by which its top and bottom bounds + * should be changed by. Given these params, an animation which will animate + * these bound changes is created and returned. + */ + private Animator getAnimation(final View view, float translateTop, float translateBottom) { + + int top = view.getTop(); + int bottom = view.getBottom(); + + int endTop = (int)(top + translateTop); + int endBottom = (int)(bottom + translateBottom); + + PropertyValuesHolder translationTop = PropertyValuesHolder.ofInt("top", top, endTop); + PropertyValuesHolder translationBottom = PropertyValuesHolder.ofInt("bottom", bottom, + endBottom); + + return ObjectAnimator.ofPropertyValuesHolder(view, translationTop, translationBottom); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/FileExplorerView.java b/app/src/main/java/com/hippo/ehviewer/widget/FileExplorerView.java new file mode 100644 index 000000000..c595a78a0 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/FileExplorerView.java @@ -0,0 +1,274 @@ +/* + * 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 java.io.File; +import java.io.FileFilter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Environment; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.ListView; +import android.widget.TextView; + +import com.hippo.ehviewer.R; + +public class FileExplorerView extends ListView + implements AdapterView.OnItemClickListener { + + @SuppressWarnings("unused") + private static final String TAG = "FileExplorerView"; + + private static final File PARENT_DIR = null; + private static final String PARENT_DIR_NAME = ".."; + + private static final DirFileFilter dff = new DirFileFilter(); + private static final FileSort fs = new FileSort(); + + private Context mContext; + + private Filter mFilter; + private static final Filter[] sFilterArray = { Filter.ALL, + Filter.DIR}; + + private File mCurDir; + + private BaseAdapter mAdapter; + private List mFileList; + + public enum Filter { + ALL(0), + DIR(1); + + Filter(int ni) { + nativeInt = ni; + } + + final int nativeInt; + } + + public FileExplorerView(Context context) { + super(context); + init(context); + } + + public FileExplorerView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs, 0); + } + + public FileExplorerView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context, attrs, defStyle); + } + + private void init(Context context, AttributeSet attrs, int defStyle) { + mContext = context; + + TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.FileExplorerView, defStyle, 0); + + int modeIndex = a.getInt(R.styleable.FileExplorerView_filter, 0); + if (modeIndex >=0 && modeIndex < sFilterArray.length) + mFilter = sFilterArray[modeIndex]; + else + mFilter = Filter.ALL; + + String path = a.getString(R.styleable.FileExplorerView_path); + if (path != null) { + mCurDir = new File(path); + if (!mCurDir.isDirectory()) + mCurDir = null; + } + if (mCurDir == null) + mCurDir = getSafeDir(); + + a.recycle(); + + init(); + } + + private void init(Context context) { + mContext = context; + + mFilter = Filter.ALL; + mCurDir = getSafeDir(); + + init(); + } + + private void init() { + mFileList = new ArrayList(); + getDirList(); + mAdapter = new FileAdapter(); + setAdapter(mAdapter); + setOnItemClickListener(this); + } + + private void getDirList() { + + File[] files = null; + if (mFilter == Filter.DIR) + files = mCurDir.listFiles(dff); + else + files = mCurDir.listFiles(); + + mFileList.clear(); + if (mCurDir.getParent() != null) + mFileList.add(PARENT_DIR); + if (files != null) { + for (File file : files) + mFileList.add(file); + } + // sort + Collections.sort(mFileList, fs); + } + + /** + * Reget dir list and go to top in the list + */ + public void refresh() { + getDirList(); + mAdapter.notifyDataSetChanged(); + // Go to top + setSelection(0); + } + + /** + * Set mode + * @param mode + */ + public void setMode(Filter mode) { + mFilter = mode; + refresh(); + } + + /** + * @param path The path you want to set for current path + * @return True if the path is refer to a directory + */ + public boolean setPath(String path) { + File dir = new File(path); + if (!dir.isDirectory()) + return false; + + mCurDir = dir; + refresh(); + return true; + } + + /** + * True if current dir can write + * @return + */ + public boolean canWrite() { + return mCurDir.canWrite(); + } + + /** + * Get current path + * @return + */ + public String getCurPath() { + return mCurDir.getPath(); + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, + long id) { + File newFile; + File file = mFileList.get(position); + if (file == PARENT_DIR) + newFile = mCurDir.getParentFile(); + else + newFile = file; + mCurDir = newFile; + refresh(); + } + + class FileAdapter extends BaseAdapter { + LayoutInflater mInflater = LayoutInflater.from(mContext); + @Override + public int getCount() { + return mFileList.size(); + } + + @Override + public Object getItem(int position) { + return mFileList.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) + convertView = mInflater.inflate(R.layout.file_explorer_item, parent, false); + + TextView tv = (TextView) convertView; + File file = mFileList.get(position); + String name; + if (file == PARENT_DIR) + name = PARENT_DIR_NAME; + else + name = file.getName(); + tv.setText(name); + + return convertView; + } + } + + private static File getSafeDir() { + File mDir; + mDir = Environment.getExternalStorageDirectory(); + if (mDir == null || !mDir.isDirectory()) + mDir = new File("/"); + return mDir; + } + + static class DirFileFilter implements FileFilter { + @Override + public boolean accept(File pathname) { + return pathname.isDirectory(); + } + } + + static class FileSort implements Comparator { + @Override + public int compare(File lhs, File rhs) { + if (lhs == null) + return Integer.MIN_VALUE; + else if (rhs == null) + return Integer.MAX_VALUE; + else + return lhs.getName().compareToIgnoreCase(rhs.getName()); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/FitWindowView.java b/app/src/main/java/com/hippo/ehviewer/widget/FitWindowView.java new file mode 100644 index 000000000..7333e240f --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/FitWindowView.java @@ -0,0 +1,82 @@ +/* + * 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 java.util.LinkedList; +import java.util.List; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Rect; +import android.os.Build; +import android.util.AttributeSet; +import android.view.View; + +public class FitWindowView extends View { + + private List mListeners; + + public FitWindowView(Context context) { + super(context); + init(); + } + + public FitWindowView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public FitWindowView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public FitWindowView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + + private void init() { + mListeners = new LinkedList(); + setFitsSystemWindows(true); + } + + public void addOnFitSystemWindowsListener(OnFitSystemWindowsListener l) { + mListeners.add(l); + } + + public void removeOnFitSystemWindowsListener(OnFitSystemWindowsListener l) { + mListeners.remove(l); + } + + @Override + @SuppressWarnings("deprecation") + protected boolean fitSystemWindows(Rect insets) { + int l = insets.left; + int t = insets.top; + int r = insets.right; + int b = insets.bottom; + for (OnFitSystemWindowsListener listener : mListeners) + listener.onFitSystemWindows(l, t, r, b); + return super.fitSystemWindows(insets); + } + + public static interface OnFitSystemWindowsListener { + public void onFitSystemWindows(int l, int t, int r, int b); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/FixedAspectImageView.java b/app/src/main/java/com/hippo/ehviewer/widget/FixedAspectImageView.java new file mode 100644 index 000000000..a83d6709d --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/FixedAspectImageView.java @@ -0,0 +1,248 @@ +/* + * 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.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.util.AttributeSet; +import android.widget.ImageView; + +import com.hippo.ehviewer.R; + +/** + * aspect is width / height + * + * @author Hippo + * + */ + +public class FixedAspectImageView extends ImageView { + + private static final String TAG = FixedAspectImageView.class.getSimpleName(); + + private int mMaxWidth = Integer.MAX_VALUE; + private int mMaxHeight = Integer.MAX_VALUE; + private boolean mAdjustViewBounds = false; + + private boolean mAdjustViewBoundsCompat = false; + private float mAspect = -1f; + + public FixedAspectImageView(Context context) { + super(context); + } + + public FixedAspectImageView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public FixedAspectImageView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray typedArray = context.obtainStyledAttributes( + attrs, R.styleable.FixedAspectImageView, defStyle, 0); + setAspect(typedArray.getFloat(R.styleable.FixedAspectImageView_aspect, -1f)); + typedArray.recycle(); + } + + @Override + public void setMaxWidth(int maxWidth) { + super.setMaxWidth(maxWidth); + mMaxWidth = maxWidth; + } + + @Override + public void setMaxHeight(int maxHeight) { + super.setMaxHeight(maxHeight); + mMaxHeight = maxHeight; + } + + @Override + public void setAdjustViewBounds(boolean adjustViewBounds) { + super.setAdjustViewBounds(adjustViewBounds); + mAdjustViewBounds = adjustViewBounds; + } + + public void init() { + mAdjustViewBoundsCompat = getContext().getApplicationInfo().targetSdkVersion <= + Build.VERSION_CODES.JELLY_BEAN_MR1; + } + + /** + * Enable aspect will set AdjustViewBounds true. + * Any negative float to disable it, + * disable Aspect will not disable AdjustViewBounds. + * + * @param ratio + */ + public void setAspect(float ratio) { + if (ratio > 0) { + mAspect = ratio; + setAdjustViewBounds(true); + } else { + mAspect = -1f; + } + requestLayout(); + } + + private int resolveAdjustedSize(int desiredSize, int maxSize, + int measureSpec) { + int result = desiredSize; + int specMode = MeasureSpec.getMode(measureSpec); + int specSize = MeasureSpec.getSize(measureSpec); + switch (specMode) { + case MeasureSpec.UNSPECIFIED: + /* Parent says we can be as big as we want. Just don't be larger + * than max size imposed on ourselves. + */ + result = Math.min(desiredSize, maxSize); + break; + case MeasureSpec.AT_MOST: + // Parent says we can be as big as we want, up to specSize. + // Don't be larger than specSize, and don't be larger than + // the max size imposed on ourselves. + result = Math.min(Math.min(desiredSize, specSize), maxSize); + break; + case MeasureSpec.EXACTLY: + // No choice. Do what we are told. + result = specSize; + break; + } + return result; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int w = 0; + int h = 0; + + // Desired aspect ratio of the view's contents (not including padding) + float desiredAspect = 0.0f; + + // We are allowed to change the view's width + boolean resizeWidth = false; + + // We are allowed to change the view's height + boolean resizeHeight = false; + + final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); + final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); + + Drawable drawable = getDrawable(); + if (drawable == null) { + // If no drawable, its intrinsic size is 0. + w = h = 0; + + if (mAdjustViewBounds && mAspect > 0.0f) { + resizeWidth = widthSpecMode != MeasureSpec.EXACTLY; + resizeHeight = heightSpecMode != MeasureSpec.EXACTLY; + desiredAspect = mAspect; + } + } else { + w = drawable.getIntrinsicWidth(); + h = drawable.getIntrinsicHeight(); + if (w <= 0) w = 1; + if (h <= 0) h = 1; + + // We are supposed to adjust view bounds to match the aspect + // ratio of our drawable. See if that is possible. + if (mAdjustViewBounds) { + resizeWidth = widthSpecMode != MeasureSpec.EXACTLY; + resizeHeight = heightSpecMode != MeasureSpec.EXACTLY; + + desiredAspect = mAspect <= 0.0f ? ((float) w / (float) h) : mAspect; + } + } + + int pleft = getPaddingLeft(); + int pright = getPaddingRight(); + int ptop = getPaddingTop(); + int pbottom = getPaddingBottom(); + + int widthSize; + int heightSize; + + if (resizeWidth || resizeHeight) { + // If we get here, it means we want to resize to match the + // drawables aspect ratio, and we have the freedom to change at + // least one dimension. + + // Get the max possible width given our constraints + widthSize = resolveAdjustedSize(w + pleft + pright, mMaxWidth, widthMeasureSpec); + + // Get the max possible height given our constraints + heightSize = resolveAdjustedSize(h + ptop + pbottom, mMaxHeight, heightMeasureSpec); + + if (desiredAspect != 0.0f) { + // See what our actual aspect ratio is + float actualAspect = (float)(widthSize - pleft - pright) / + (heightSize - ptop - pbottom); + + if (Math.abs(actualAspect - desiredAspect) > 0.0000001) { + boolean done = false; + + // Try adjusting width to be proportional to height + if (resizeWidth) { + int newWidth = (int)(desiredAspect * (heightSize - ptop - pbottom)) + + pleft + pright; + + // Allow the width to outgrow its original estimate if height is fixed. + if (!resizeHeight && !mAdjustViewBoundsCompat) { + widthSize = resolveAdjustedSize(newWidth, mMaxWidth, widthMeasureSpec); + } + + if (newWidth <= widthSize) { + widthSize = newWidth; + done = true; + } + } + + // Try adjusting height to be proportional to width + if (!done && resizeHeight) { + int newHeight = (int)((widthSize - pleft - pright) / desiredAspect) + + ptop + pbottom; + + // Allow the height to outgrow its original estimate if width is fixed. + if (!resizeWidth && !mAdjustViewBoundsCompat) { + heightSize = resolveAdjustedSize(newHeight, mMaxHeight, + heightMeasureSpec); + } + + if (newHeight <= heightSize) { + heightSize = newHeight; + } + } + } + } + } else { + // We are either don't want to preserve the drawables aspect ratio, + // or we are not allowed to change view dimensions. Just measure in + // the normal way. + w += pleft + pright; + h += ptop + pbottom; + + w = Math.max(w, getSuggestedMinimumWidth()); + h = Math.max(h, getSuggestedMinimumHeight()); + + widthSize = resolveSizeAndState(w, widthMeasureSpec, 0); + heightSize = resolveSizeAndState(h, heightMeasureSpec, 0); + } + + setMeasuredDimension(widthSize, heightSize); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/FloatingActionButton.java b/app/src/main/java/com/hippo/ehviewer/widget/FloatingActionButton.java new file mode 100644 index 000000000..a427628a4 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/FloatingActionButton.java @@ -0,0 +1,151 @@ +package com.hippo.ehviewer.widget; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + +import com.hippo.ehviewer.R; + +public class FloatingActionButton extends View { + + @SuppressWarnings("unused") + private static final String TAG = FloatingActionButton.class.getSimpleName(); + + private final Paint mButtonPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint mDrawablePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private Bitmap mBitmap; + private int mColor; + private final int mDrawableSide; + private final float mShadowRadius; + private final float mShadowDx; + private final float mShadowDy; + + private int mCircleX; + private int mCircleY; + private int mCircleRadius; + private final Rect mSrc = new Rect(); + private final RectF mDst = new RectF(); + + + public FloatingActionButton(Context context) { + this(context, null); + } + + public FloatingActionButton(Context context, AttributeSet attributeSet) { + this(context, attributeSet, 0); + } + + public FloatingActionButton(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FloatingActionButton); + mDrawableSide = a.getDimensionPixelSize(R.styleable.FloatingActionButton_fabDrawableSide, 72); + mColor = a.getColor(R.styleable.FloatingActionButton_fabColor, Color.WHITE); + mButtonPaint.setStyle(Paint.Style.FILL); + mButtonPaint.setColor(mColor); + mShadowRadius = a.getDimension(R.styleable.FloatingActionButton_fabShadowRadius, 10.0f); + mShadowDx = a.getDimension(R.styleable.FloatingActionButton_fabShadowDx, 0.0f); + mShadowDy = a.getDimension(R.styleable.FloatingActionButton_fabShadowDy, 3.0f); + int shadowColor = a.getColor(R.styleable.FloatingActionButton_fabShadowColor, 0x8a000000); + mButtonPaint.setShadowLayer(mShadowRadius, mShadowDx, mShadowDy, shadowColor); + Drawable drawable = a.getDrawable(R.styleable.FloatingActionButton_fabDrawable); + if (drawable != null && drawable instanceof BitmapDrawable) + mBitmap = ((BitmapDrawable) drawable).getBitmap(); + a.recycle(); + + setWillNotDraw(false); + setLayerType(View.LAYER_TYPE_SOFTWARE, null); + } + + public void setColor(int color) { + mColor = color; + mButtonPaint.setColor(mColor); + invalidate(); + } + + public void setDrawable(Drawable drawable) { + mBitmap = ((BitmapDrawable) drawable).getBitmap(); + updateValue(); + invalidate(); + } + + public static int getDefaultSize(int size, int measureSpec) { + int result = size; + int specMode = MeasureSpec.getMode(measureSpec); + int specSize = MeasureSpec.getSize(measureSpec); + + switch (specMode) { + case MeasureSpec.UNSPECIFIED: + case MeasureSpec.AT_MOST: + result = size; + break; + case MeasureSpec.EXACTLY: + result = specSize; + break; + } + return result; + } + + private void updateValue() { + float half = Math.min(getWidth(), getHeight()) / 2; + mCircleX = (int) (half - mShadowDx); + mCircleY = (int) (half - mShadowDy); + mCircleRadius = (int) (half - mShadowRadius); + float halfDrawableSide = mDrawableSide / 2.0f; + if (mBitmap != null) { + mSrc.set(0, 0, mBitmap.getWidth(), mBitmap.getHeight()); + mDst.set(mCircleX - halfDrawableSide, mCircleY - halfDrawableSide, mCircleX + halfDrawableSide, mCircleY + halfDrawableSide); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), + getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + updateValue(); + } + + @Override + protected void onDraw(Canvas canvas) { + canvas.drawCircle(mCircleX, mCircleY, mCircleRadius, mButtonPaint); + if (null != mBitmap) + canvas.drawBitmap(mBitmap, mSrc, mDst, mDrawablePaint); + } + + @Override + @SuppressLint("ClickableViewAccessibility") + public boolean onTouchEvent(MotionEvent event) { + int color; + if (event.getAction() == MotionEvent.ACTION_UP) { + color = mColor; + } else { + color = darkenColor(mColor); + } + mButtonPaint.setColor(color); + invalidate(); + return super.onTouchEvent(event); + } + + public static int darkenColor(int color) { + float[] hsv = new float[3]; + Color.colorToHSV(color, hsv); + hsv[2] *= 0.8f; + return Color.HSVToColor(hsv); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hippo/ehviewer/widget/GalleryListView.java b/app/src/main/java/com/hippo/ehviewer/widget/GalleryListView.java new file mode 100644 index 000000000..c5fd47df6 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/GalleryListView.java @@ -0,0 +1,795 @@ +/* + * 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 java.util.ArrayList; +import java.util.List; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Configuration; +import android.os.Build; +import android.support.v7.widget.DefaultItemAnimator; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.StaggeredGridLayoutManager; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +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.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; + +public class GalleryListView extends FrameLayout implements RefreshLayout.OnFooterRefreshListener, + RefreshLayout.OnRefreshListener { + + private static final String TAG = GalleryListView.class.getSimpleName(); + + public static final int LIST_MODE_DETAIL = 0; + public static final int LIST_MODE_THUMB = 1; + + private static final int MODE_REFRESH = 0; + private static final int MODE_NEXT_PAGE = 1; + private static final int MODE_PRE_PAGE = 2; + private static final int MODE_SOMEWHERE = 3; + + private Context mContext; + private GalleryListViewHelper mHelper; + + private List mGiList; + + private RefreshLayout mRefreshLayout; + private EasyRecyclerView mEasyRecyclerView; + private RefreshTextView mRefreshTextView; + + private int mListMode = LIST_MODE_DETAIL; + + private GalleryAdapter mAdapter; + private StaggeredGridLayoutManager mLayoutManager; + private MarginItemDecoration mItemDecoration; + private final int[] mFirstPositionTemp = new int[50]; // TODO + + private long mTaskStamp; + + /** + * First index of current page + */ + private int mFirstIndex; + /** + * Last index of current page + */ + private int mLastIndex; + private int mCurPage; + private int mFirstPage = 0; + private int mLastPage = 0; + /** + * The number of page in sum + */ + private int mPageNum; + private int mItemPerPage; + private int mGetMode; + + private OnGetListListener mListener; + + /** + * If true, list will make showed item not changed after get + */ + private boolean mIsKeepPosition; + + private int mTargetPage; + private String mTargetUrl; + private boolean isFootRefresh = false; + + private LayoutInflater mInflater; + + private int mListDetailThumbWidth; + private int mListDetailThumbHeight; + + public GalleryListView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs, 0, 0); + } + + public GalleryListView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs, defStyleAttr, 0); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public GalleryListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context, attrs, defStyleAttr, defStyleRes); + } + + private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + mContext = context; + 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(); + mRefreshTextView = (RefreshTextView) findViewById(R.id.refresh_text); + + mAdapter = new GalleryAdapter(mContext, mGiList); + mLayoutManager = new StaggeredGridLayoutManager(1, StaggeredGridLayoutManager.VERTICAL); + mItemDecoration = new MarginItemDecoration(); + mEasyRecyclerView.setOnScrollListener(new OnScrollListener()); + mEasyRecyclerView.setClipToPadding(false); + mEasyRecyclerView.setAdapter(mAdapter); + mEasyRecyclerView.setLayoutManager(mLayoutManager); + mEasyRecyclerView.addItemDecoration(mItemDecoration); + mEasyRecyclerView.setItemAnimator(new DefaultItemAnimator()); + mEasyRecyclerView.setHasFixedSize(true); + setItemMargin(Ui.dp2pix(8)); // TODO for tablet margin should be greater + + mRefreshTextView.setDefaultRefresh(R.string.click_retry, new RefreshTextView.OnRefreshListener() { + @Override + public void onRefresh() { + retry(); + } + }); + + mRefreshLayout.setColorSchemeResources( + 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(); + + // Caculate gallery detail height + mInflater = LayoutInflater.from(mContext); + View v = mInflater.inflate(R.layout.test_gallery_list_detail_thumb, null); + ((TextView) v.findViewById(R.id.title)).setText("haha\nhaha"); + ((TextView) v.findViewById(R.id.uploader)).setText("haha"); + ((RatingView) v.findViewById(R.id.rate)).setRating(2.3f); + ((TextView) v.findViewById(R.id.category)).setText("haha"); + ViewUtils.measureView(v, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + mListDetailThumbHeight = Math.max(v.getMeasuredHeight(), Ui.dp2pix(120)); + mListDetailThumbWidth = mListDetailThumbHeight * 2 / 3; + } + + public int getListMode() { + return mListMode; + } + + public void setListMode(int listMode) { + if (mListMode != listMode) { + mListMode = listMode; + // Update span + updateSpanCount(); + mAdapter.notifyDataSetChanged(); + } + } + + private boolean isLandscape() { + return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; + } + + private static final String[] KEY_SPAN_COUNT_ARRAY = { + "list_detail_columns_portrait", "list_detail_columns_landscape", + "list_thumb_columns_portrait", "list_thumb_columns_landscape" + }; + + private static final int[] DEFAULT_SPAN_COUNT_ARRAY = { + 1, 2, 3, 5 + }; + + public void updateSpanCount() { + int index = mListMode * 2 + (isLandscape() ? 1 : 0); + try { + mLayoutManager.setSpanCount(Config.getInt(KEY_SPAN_COUNT_ARRAY[index], + DEFAULT_SPAN_COUNT_ARRAY[index])); + } catch (Throwable e) { + e.printStackTrace(); + } + } + + public void setGalleryListViewHelper(GalleryListViewHelper h) { + mHelper = h; + } + + public void setPaddingTopAndBottom(int top, int bottom) { + setPadding(getPaddingLeft(), top, getPaddingRight(), getPaddingBottom()); + mEasyRecyclerView.setPadding(mEasyRecyclerView.getPaddingLeft(), mEasyRecyclerView.getPaddingTop(), + mEasyRecyclerView.getPaddingRight(), mEasyRecyclerView.getPaddingBottom() + bottom); + } + + private void setItemMargin(int margin) { + int halfMargin = margin / 2; + mItemDecoration.setMargin(margin); + mEasyRecyclerView.setPadding(halfMargin, halfMargin, + halfMargin, halfMargin); + } + + public void setEnabledHeader(boolean enabled) { + mRefreshLayout.setEnabledHeader(enabled); + } + + public void setEnabledFooter(boolean enabled) { + mRefreshLayout.setEnabledFooter(enabled); + } + + public void setHeaderRefreshing(boolean refreshing) { + mRefreshLayout.setRefreshing(refreshing); + } + + public void setOnItemClickListener(EasyRecyclerView.OnItemClickListener l) { + mEasyRecyclerView.setOnItemClickListener(l); + } + + public void setOnItemLongClickListener(EasyRecyclerView.OnItemLongClickListener l) { + mEasyRecyclerView.setOnItemLongClickListener(l); + } + + public void setChoiceMode(int choiceMode) { + mEasyRecyclerView.setChoiceMode(choiceMode); + } + + public void setMultiChoiceModeListener(EasyRecyclerView.MultiChoiceModeListener listener) { + mEasyRecyclerView.setMultiChoiceModeListener(listener); + } + + public void notifyDataSetChanged() { + mAdapter.notifyDataSetChanged(); + } + + + public void setNoneText(CharSequence text) { + mRefreshTextView.setVisibility(View.VISIBLE); + mRefreshTextView.setEmesg(text, false); + } + + public void onlyShowList() { + mRefreshLayout.setVisibility(View.VISIBLE); + mRefreshTextView.setRefreshing(false); + mRefreshTextView.setVisibility(View.GONE); + } + + public void onlyShowNone() { + mRefreshLayout.setVisibility(View.GONE); + mRefreshTextView.setVisibility(View.VISIBLE); + mRefreshTextView.setEmesg(R.string.none, false); + } + + private void getGallerys() { + setGallerysLayout(); + + mTaskStamp = System.currentTimeMillis(); + mHelper.doGetGallerys(mTargetUrl, mTaskStamp, mListener); + } + + @Override + public void onRefresh() { + // It is invokened by user pull, so no need to + if (mFirstPage > 0) { + getPrePage(true); + } else { + refresh(); + } + } + + @Override + public boolean onFooterRefresh() { + if (!isRefreshing() && mLastPage < mPageNum - 1) { + isFootRefresh = true; + getNextPage(true); + isFootRefresh = false; + return true; + } else { + return false; + } + } + + public void firstTimeRefresh() { + // set mPullViewGroup gone, make wait view show + mRefreshLayout.setVisibility(View.GONE); + mRefreshTextView.setVisibility(View.VISIBLE); + mRefreshTextView.setRefreshing(true); + + refresh(); + } + + private void setGallerysLayout() { + if (mRefreshLayout.getVisibility() == View.VISIBLE) { + if (!isFootRefresh) + mRefreshLayout.setRefreshing(true); + mRefreshTextView.setVisibility(View.GONE); + } else { + mRefreshLayout.setVisibility(View.GONE); + mRefreshTextView.setVisibility(View.VISIBLE); + mRefreshTextView.setRefreshing(true); + } + } + + protected boolean retry() { + // Need to update url, because mode may be changed + mTargetUrl = mHelper.getTargetUrl(mTargetPage); + getGallerys(); + return true; + } + + /** + * Go to page 0.
+ * You should know you can refresh or not. + */ + public void refresh() { + mGetMode = MODE_REFRESH; + mTargetPage = 0; + mTargetUrl = mHelper.getTargetUrl(mTargetPage); + mIsKeepPosition = false; + getGallerys(); + } + + /** + * Get previous page.
+ * You should know you can get previous page or not + * + * @param isKeepPosition + */ + protected void getPrePage(boolean isKeepPosition) { + mGetMode = MODE_PRE_PAGE; + mTargetPage = mFirstPage - 1; + mTargetUrl = mHelper.getTargetUrl(mTargetPage); + mIsKeepPosition = isKeepPosition; + getGallerys(); + } + + /** + * Get next page.
+ * You should know you can get next page or not + * + * @param isKeepPosition + */ + protected void getNextPage(boolean isKeepPosition) { + mGetMode = MODE_NEXT_PAGE; + mTargetPage = mLastPage + 1; + mTargetUrl = mHelper.getTargetUrl(mTargetPage); + mIsKeepPosition = isKeepPosition; + getGallerys(); + } + + /** + * Get some page.
+ * You should know you can get that page or not + * + * @param page + */ + protected void getSomewhere(int page) { + mGetMode = MODE_SOMEWHERE; + mTargetPage = page; + mTargetUrl = mHelper.getTargetUrl(mTargetPage); + mIsKeepPosition = false; + getGallerys(); + } + + public void jumpTo(int page) { + if (page >= mFirstPage && page <= mLastPage) { + int position = (page - mFirstPage) * mItemPerPage; + mLayoutManager.scrollToPosition(position); + } else if (page == mFirstPage - 1) { + getPrePage(false); + } else if (page == mLastPage + 1) { + getNextPage(false); + } else { + getSomewhere(page); + } + } + + /** + * @return True if actionbar or footer is refreshing + */ + public boolean isRefreshing() { + return mRefreshLayout.isRefreshing(); + } + + public boolean isGetGalleryOk() { + return mRefreshLayout.getVisibility() == View.VISIBLE; + } + + public int getPageNum() { + return mPageNum; + } + + public int getCurPage() { + return mCurPage; + } + + public List getGalleryList() { + return mGiList; + } + + public GalleryInfo getGalleryInfo(int position) { + return mGiList.get(position); + } + + public static interface OnGetListListener { + public void onSuccess(long taskStamp, List gis, int maxPage); + public void onFailure(long taskStamp, String eMsg); + } + + private class OnGetGalleryListListener implements OnGetListListener, Runnable { + + @Override + public void run() { + int preIndex = mLayoutManager.findFirstVisibleItemPositions(mFirstPositionTemp)[0] - 1; + if (preIndex >= 0) + mEasyRecyclerView.smoothScrollToPosition(preIndex); + } + + public void smoothScrollToPrePosition() { + AppHandler.getInstance().post(this); + } + + @Override + public void onSuccess(long taskStamp, List gis, int pageNum) { + if (mTaskStamp != taskStamp) + return; + + if (pageNum == ListParser.TARGET_PAGE_IS_LAST) + mPageNum = mTargetPage + 1; + else + mPageNum = pageNum; + + int itemPerPage = gis == null ? 0 : gis.size(); + if (mItemPerPage < itemPerPage) + mItemPerPage = itemPerPage; + + if (mPageNum == 0) { // Get none + onlyShowNone(); + + mFirstPage = 0; + mLastPage = 0; + mGiList.clear(); + mAdapter.notifyDataSetChanged(); + } else { + onlyShowList(); + + int start; + switch (mGetMode) { + case MODE_REFRESH: + mFirstPage = 0; + mLastPage = 0; + mGiList.clear(); + mGiList.addAll(gis); + mAdapter.notifyDataSetChanged(); + // For current page + mFirstIndex = 0; + mLastIndex = gis.size() - 1; + mCurPage = 0; + + mLayoutManager.scrollToPosition(0); + break; + + case MODE_PRE_PAGE: + mFirstPage--; + mGiList.addAll(0, gis); + mAdapter.notifyItemRangeInserted(0, gis.size()); + + if (mIsKeepPosition) { + mFirstIndex += gis.size(); + mLastIndex += gis.size(); + smoothScrollToPrePosition(); + } else { + mFirstIndex = 0; + mLastIndex = gis.size()-1; + mCurPage = mTargetPage; + mLayoutManager.scrollToPosition(0); + } + break; + + case MODE_NEXT_PAGE: + mLastPage++; + start = mGiList.size(); + mGiList.addAll(gis); + mAdapter.notifyItemRangeInserted(start, gis.size()); + + if (!mIsKeepPosition) { + mFirstIndex = mGiList.size() - gis.size(); + mLastIndex = mGiList.size() - 1; + mCurPage = mTargetPage; + mLayoutManager.scrollToPosition(0); + } + break; + + case MODE_SOMEWHERE: + mFirstPage = mTargetPage; + mLastPage = mTargetPage; + mGiList.clear(); + mGiList.addAll(gis); + mAdapter.notifyDataSetChanged(); + // For current page + mFirstIndex = 0; + mLastIndex = gis.size() - 1; + mCurPage = mTargetPage; + + mLayoutManager.scrollToPosition(0); + } + } + + mRefreshLayout.setAnyRefreshComplete(true); + } + + @Override + public void onFailure(long taskStamp, String eMsg) { + if (mTaskStamp != taskStamp) + return; + + switch (mGetMode) { + case MODE_REFRESH: + case MODE_SOMEWHERE: + mRefreshLayout.setVisibility(View.GONE); + mRefreshTextView.setVisibility(View.VISIBLE); + if (eMsg.equals(mContext.getString(R.string.em_index_error))) { + mRefreshTextView.setEmesg(eMsg, mContext.getString(R.string.click_first_page), + new RefreshTextView.OnRefreshListener() { + @Override + public void onRefresh() { + mRefreshTextView.setRefreshing(true); + refresh(); + } + }); + } else { + mRefreshTextView.setEmesg(eMsg, true); + } + break; + default: + mRefreshTextView.setRefreshing(false); + mRefreshTextView.setVisibility(View.GONE); + MaterialToast.showToast(eMsg); + } + mRefreshLayout.setAnyRefreshComplete(false); + } + } + + public static interface GalleryListViewHelper { + /** + * Get url string for very page + * + * @param targetPage + * @return + */ + public String getTargetUrl(int targetPage); + + /** + * Do get gallarys here, you shuold invoke the onSuccess or onFailure of + * listener when over + * + * @param url + * @param taskStamp + * @param listener + */ + public void doGetGallerys(String url, long taskStamp, OnGetListListener listener); + } + + public class OnScrollListener extends RecyclerView.OnScrollListener { + + @Override + public void onScrollStateChanged(RecyclerView recyclerView, int newState) { + switch (newState) { + case RecyclerView.SCROLL_STATE_DRAGGING: + ImageCache.getInstance(mContext).setPauseDiskCache(true); + break; + case RecyclerView.SCROLL_STATE_IDLE: + ImageCache.getInstance(mContext).setPauseDiskCache(false); + break; + } + } + + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + mRefreshLayout.onScrolled(recyclerView, dx, dy); + + int itemCount = mLayoutManager.getChildCount(); + int firstVisibleItem = mLayoutManager.findFirstVisibleItemPositions(mFirstPositionTemp)[0]; + + // itemCount might include some view can't be seen + // Just check top line child and bottom line child + int visibleItemCount = itemCount; + int height = mEasyRecyclerView.getHeight(); + int span = mLayoutManager.getSpanCount(); + // Top line + int start = 0; + int end = Math.min(span, itemCount); + for (int i = start; i < end; i++) { + View view = mLayoutManager.getChildAt(i); + if (view.getBottom() <= 0 || view.getTop() > height) + visibleItemCount--; + } + // Bottom line + start = Math.max(itemCount - 1 - span, 0); + end = itemCount; + for (int i = start; i < end; i++) { + View view = mLayoutManager.getChildAt(i); + if (view.getBottom() <= 0 || view.getTop() > height) + visibleItemCount--; + } + + if (mItemPerPage == 0) + return; + if (mLastIndex == 0) + mLastIndex = mItemPerPage - 1; + int pageChanged = (firstVisibleItem - mFirstIndex) / mItemPerPage; + if (pageChanged == 0) + pageChanged = (firstVisibleItem + visibleItemCount - mLastIndex - 1) / mItemPerPage; + + if (pageChanged != 0) { + mCurPage = mCurPage + pageChanged; + mFirstIndex += pageChanged * mItemPerPage; + mLastIndex += pageChanged * mItemPerPage; + } + } + } + + private class GalleryViewHolder extends RecyclerView.ViewHolder { + + public int viewType; + public LoadImageView thumb; + public TextView title; + public TextView uploader; + public TextView category; + public RatingView rate; + public TextView posted; + public TextView simpleLanguage; + + public GalleryViewHolder(View itemView, int viewType) { + super(itemView); + + this.viewType = viewType; + switch (viewType) { + case LIST_MODE_DETAIL: + thumb = (LoadImageView) itemView.findViewById(R.id.thumb); + title = (TextView) itemView.findViewById(R.id.title); + category = (TextView) itemView.findViewById(R.id.category); + uploader = (TextView) itemView.findViewById(R.id.uploader); + rate = (RatingView) itemView.findViewById(R.id.rate); + posted = (TextView) itemView.findViewById(R.id.posted); + simpleLanguage = (TextView) itemView.findViewById(R.id.simple_language); + + ViewGroup.LayoutParams lp = thumb.getLayoutParams(); + lp.width = mListDetailThumbWidth; + lp.height = mListDetailThumbHeight; + break; + case LIST_MODE_THUMB: + thumb = (LoadImageView) itemView.findViewById(R.id.thumb); + category = (TextView) itemView.findViewById(R.id.category); + simpleLanguage = (TextView) itemView.findViewById(R.id.simple_language); + } + } + } + + public class GalleryAdapter extends EasyRecyclerView.FooterAdapter { + + private final Context mContext; + private final List mGiList; + private final ImageLoader mImageLoader; + + + public GalleryAdapter(Context context, List gilist) { + mContext = context; + mGiList = gilist; + mImageLoader = ImageLoader.getInstance(mContext); + } + + @Override + public GalleryViewHolder onCreateAndBindFooterViewHolder( + ViewGroup parent, View footerView) { + return new GalleryViewHolder(footerView, FooterAdapter.TYPE_FOOTER); + } + + @Override + public GalleryViewHolder onCreateViewHolderActual(ViewGroup parent, + int viewType) { + int resId; + if (viewType == LIST_MODE_DETAIL) + resId = R.layout.gallery_list_detail_item; + else + resId = R.layout.gallery_list_thumb_item; + View view = mInflater.inflate(resId, parent, false); + CardViewSalon.reformWithShadow(view, new int[][]{ + new int[]{android.R.attr.state_pressed}, + new int[]{android.R.attr.state_activated}, + new int[]{}}, + new int[]{0xff84cae4, 0xff33b5e5, 0xFFFAFAFA}, null, false); + return new GalleryViewHolder(view, viewType); + } + + @Override + public void onBindViewHolderActual(GalleryViewHolder holder, + int position) { + GalleryInfo gi = mGiList.get(position); + final LoadImageView thumb = holder.thumb; + if (!String.valueOf(gi.gid).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)); + } + // Set simple language + TextView simpleLanguage = holder.simpleLanguage; + if (gi.simpleLanguage == null) { + simpleLanguage.setVisibility(View.GONE); + } else { + simpleLanguage.setVisibility(View.VISIBLE); + simpleLanguage.setText(gi.simpleLanguage); + } + + // For detail mode + if (holder.viewType == LIST_MODE_DETAIL) { + // Set category + TextView category = holder.category; + String newText = Ui.getCategoryText(gi.category); + if (!newText.equals(category.getText())) { + category.setText(newText); + category.setBackgroundColor(Ui.getCategoryColor(gi.category)); + } + // Set manga title + TextView title = holder.title; + title.setText(gi.title); + // Set uploder + TextView uploader = holder.uploader; + uploader.setText(gi.uploader); + // Set star + RatingView rate = holder.rate; + rate.setRating(gi.rating); + // set posted + TextView posted = holder.posted; + posted.setText(gi.posted); + } else { + // Set category + TextView category = holder.category; + String newText = Ui.getCategoryText(gi.category); + if (!newText.equals(category.getText())) { + category.setText(newText); + category.setBackgroundColor(Ui.getCategoryColor(gi.category) + & 0x00ffffff | 0xca000000); + } + } + } + + @Override + public int getItemViewTypeActual(int position) { + return mListMode; + } + + @Override + public int getItemCountActual() { + return mGiList.size(); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/HotspotTouchHelper.java b/app/src/main/java/com/hippo/ehviewer/widget/HotspotTouchHelper.java new file mode 100644 index 000000000..2051de64f --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/HotspotTouchHelper.java @@ -0,0 +1,299 @@ +/* + * 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 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.widget; + +import android.annotation.TargetApi; +import android.os.Build; +import android.view.MotionEvent; +import android.view.TouchDelegate; +import android.view.View; +import android.view.ViewConfiguration; + +public class HotspotTouchHelper implements View.OnTouchListener { + + private final Hotspotable mOwner; + + private boolean mPrePressed = false; + private boolean mHasPerformedLongPress = false; + + private CheckForLongPress mPendingCheckForLongPress = null; + private CheckForTap mPendingCheckForTap = null; + private PerformClick mPerformClick = null; + private UnsetPressedState mUnsetPressedState = null; + + /** + * Cache the touch slop from the context that created the view. + */ + private int mTouchSlop = -1; + + public HotspotTouchHelper(Hotspotable owner){ + mOwner = owner; + } + + @Override + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public boolean onTouch(View v, MotionEvent event) { + final float x = event.getX(); + final float y = event.getY(); + + if (event.getAction() == MotionEvent.ACTION_DOWN || + event.getAction() == MotionEvent.ACTION_MOVE) + mOwner.setHotspot(x, y); + + if (!v.isEnabled()) { + if (event.getAction() == MotionEvent.ACTION_UP && v.isPressed()) { + v.setPressed(false); + } + // A disabled view that is clickable still consumes the touch + // events, it just doesn't respond to them. + return (v.isClickable() || v.isLongClickable()); + } + + TouchDelegate touchDelegate = v.getTouchDelegate(); + if (touchDelegate != null) { + if (touchDelegate.onTouchEvent(event)) { + return true; + } + } + + if (v.isClickable() || v.isLongClickable()) { + switch (event.getAction()) { + case MotionEvent.ACTION_UP: + if (v.isPressed() || mPrePressed) { + // take focus if we don't have it already and we should in + // touch mode. + boolean focusTaken = false; + if (v.isFocusable() && v.isFocusableInTouchMode() && !v.isFocused()) { + focusTaken = v.requestFocus(); + } + + if (mPrePressed) { + // The button is being released before we actually + // showed it as pressed. Make it show the pressed + // state now (before scheduling the click) to ensure + // the user sees it. + setPressed(v, true, x, y); + } + + if (!mHasPerformedLongPress) { + // This is a tap, so remove the longpress check + removeLongPressCallback(v); + + // Only perform take click actions if we were in the pressed state + if (!focusTaken) { + // Use a Runnable and post this rather than calling + // performClick directly. This lets other visual state + // of the view update before click actions start. + if (mPerformClick == null) { + mPerformClick = new PerformClick(v); + } + if (!v.post(mPerformClick)) { + v.performClick(); + } + } + } + + if (mUnsetPressedState == null) { + mUnsetPressedState = new UnsetPressedState(v); + } + + if (mPrePressed) { + v.postDelayed(mUnsetPressedState, + ViewConfiguration.getPressedStateDuration()); + } else if (!v.post(mUnsetPressedState)) { + // If the post failed, unpress right now + mUnsetPressedState.run(); + } + + removeTapCallback(v); + } + break; + + case MotionEvent.ACTION_DOWN: + mHasPerformedLongPress = false; + + if (performButtonActionOnTouchDown(v, event)) { + break; + } + + mPrePressed = true; + if (mPendingCheckForTap == null) { + mPendingCheckForTap = new CheckForTap(v); + } + mPendingCheckForTap.x = event.getX(); + mPendingCheckForTap.y = event.getY(); + v.postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); + break; + + case MotionEvent.ACTION_CANCEL: + v.setPressed(false); + removeTapCallback(v); + removeLongPressCallback(v); + break; + + case MotionEvent.ACTION_MOVE: + mOwner.setHotspot(x, y); + + if (mTouchSlop == -1) + mTouchSlop = ViewConfiguration.get(v.getContext()).getScaledTouchSlop(); + + // Be lenient about moving outside of buttons + if (!pointInView(v, x, y, mTouchSlop)) { + // Outside button + removeTapCallback(v); + if (v.isPressed()) { + // Remove any future long press/tap checks + removeLongPressCallback(v); + + v.setPressed(false); + } + } + break; + } + + return true; + } + + return false; + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void setPressed(View v, boolean pressed, float x, float y){ + v.setPressed(pressed); + mOwner.setHotspot(x, y); + } + + /** + * Utility method to determine whether the given point, in local coordinates, + * is inside the view, where the area of the view is expanded by the slop factor. + * This method is called while processing touch-move events to determine if the event + * is still within the view. + */ + public boolean pointInView(View v, float localX, float localY, float slop) { + return localX >= -slop && localY >= -slop && localX < ((v.getRight() - v.getLeft()) + slop) && + localY < ((v.getBottom() - v.getTop()) + slop); + } + + /** + * Performs button-related actions during a touch down event. + * + * @param event The event. + * @return True if the down was consumed. + */ + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + protected boolean performButtonActionOnTouchDown(View v, MotionEvent event) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + if ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) { + if (v.showContextMenu()) { + return true; + } + } + } + return false; + } + + /** + * Remove the longpress detection timer. + */ + private void removeLongPressCallback(View v) { + if (mPendingCheckForLongPress != null) { + v.removeCallbacks(mPendingCheckForLongPress); + } + } + + /** + * Remove the tap detection timer. + */ + private void removeTapCallback(View v) { + if (mPendingCheckForTap != null) { + mPrePressed = false; + v.removeCallbacks(mPendingCheckForTap); + } + } + + private void checkForLongClick(View v, int delayOffset) { + if (v.isLongClickable()) { + mHasPerformedLongPress = false; + + if (mPendingCheckForLongPress == null) { + mPendingCheckForLongPress = new CheckForLongPress(v); + } + v.postDelayed(mPendingCheckForLongPress, + ViewConfiguration.getLongPressTimeout() - delayOffset); + } + } + + private final class CheckForLongPress implements Runnable { + + private final View mView; + + public CheckForLongPress(View v) { + mView = v; + } + + @Override + public void run() { + if (mView.isPressed() && (mView.getParent() != null)) { + if (mView.performLongClick()) { + mHasPerformedLongPress = true; + } + } + } + } + + private final class CheckForTap implements Runnable { + public float x; + public float y; + private final View mView; + + public CheckForTap(View v) { + mView = v; + } + + @Override + public void run() { + mPrePressed = false; + setPressed(mView, true, x, y); + checkForLongClick(mView, ViewConfiguration.getTapTimeout()); + } + } + + private final class PerformClick implements Runnable { + private final View mView; + + public PerformClick(View v) { + mView = v; + } + + @Override + public void run() { + mView.performClick(); + } + } + + private final class UnsetPressedState implements Runnable { + private final View mView; + + public UnsetPressedState(View v) { + mView = v; + } + + @Override + public void run() { + mView.setPressed(false); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/Hotspotable.java b/app/src/main/java/com/hippo/ehviewer/widget/Hotspotable.java new file mode 100644 index 000000000..170c3bda5 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/Hotspotable.java @@ -0,0 +1,21 @@ +/* + * 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 + * 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; + +public interface Hotspotable { + + public void setHotspot(float x, float y); +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/LinkifyTextView.java b/app/src/main/java/com/hippo/ehviewer/widget/LinkifyTextView.java new file mode 100644 index 000000000..b635eda10 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/LinkifyTextView.java @@ -0,0 +1,104 @@ +/* + * 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.annotation.SuppressLint; +import android.content.Context; +import android.text.Layout; +import android.text.Spannable; +import android.text.style.ClickableSpan; +import android.text.style.URLSpan; +import android.text.util.Linkify; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.widget.TextView; + +public class LinkifyTextView extends TextView { + + private String mUrl; + + public LinkifyTextView(Context context) { + super(context); + init(); + } + + public LinkifyTextView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public LinkifyTextView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(); + } + + public void init() { + // Just make urls striking + // LinkifyTextView itself should not handle touch url event + setAutoLinkMask(Linkify.WEB_URLS); + setLinksClickable(false); + } + + public String getTouchedUrl() { + return mUrl; + } + + /** + * Call it when you do not need the url any more + */ + public void clearTouchedUrl() { + mUrl = null; + } + + @Override + @SuppressLint("ClickableViewAccessibility") + public boolean onTouchEvent(MotionEvent event) { + if (getText() instanceof Spannable) { + // Get this code from android.text.method.LinkMovementMethod. + // Work fine ! + int x = (int) event.getX(); + int y = (int) event.getY(); + boolean isGetUrl = false; + + x -= getTotalPaddingLeft(); + y -= getTotalPaddingTop(); + + x += getScrollX(); + y += getScrollY(); + + Layout layout = getLayout(); + int line = layout.getLineForVertical(y); + int off = layout.getOffsetForHorizontal(line, x); + + ClickableSpan[] links = ((Spannable)getText()).getSpans(off, off, ClickableSpan.class); + + for (ClickableSpan link : links) { + if (link instanceof URLSpan) { + mUrl = ((URLSpan)link).getURL(); + isGetUrl = true; + break; + } + } + if (!isGetUrl) + mUrl = null; + } else { + mUrl = null; + } + + return super.onTouchEvent(event); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/LoadImageView.java b/app/src/main/java/com/hippo/ehviewer/widget/LoadImageView.java new file mode 100644 index 000000000..7554f947c --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/LoadImageView.java @@ -0,0 +1,160 @@ +/* + * 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.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageView; + +import com.hippo.ehviewer.ImageLoader; +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.effect.DrawableTransition; + +public class LoadImageView extends FixedAspectImageView { + @SuppressWarnings("unused") + private static final String TAG = LoadImageView.class.getSimpleName(); + + public static final int NONE = 0x0; + public static final int LOADING = 0x1; + public static final int LOADED = 0x2; + public static final int FAIL = 0x3; + + private static final int TOUCH_IMAGE_ID = R.drawable.ic_touch; + + private int mState = NONE; + + private String mUrl; + private String mKey; + + public LoadImageView(Context context) { + super(context); + } + public LoadImageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + public LoadImageView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public void setLoadInfo(String url, String key) { + mUrl = url; + mKey = key; + } + + public synchronized void setState(int state) { + mState = state; + if (mState != FAIL) + setClickable(false); + } + + public void setOnClick() { + setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + setClickable(false); + setImageDrawable(null); + ImageLoader.getInstance(getContext()).add(mUrl, mKey, + new SimpleImageGetListener(LoadImageView.this)); + } + }); + } + + public String getUrl() { + return mUrl; + } + + public String getKey() { + return mKey; + } + + public synchronized int getState() { + return mState; + } + + public void setTouchImage() { + setImageResource(TOUCH_IMAGE_ID); + } + + /** + * Load target bmp, set progressive animation + * @param bmp + */ + public void setContextImage(Bitmap bmp, int state) { + setImageBitmap(bmp); + if (state != ImageLoader.STATE_FROM_MEMORY) { + final Drawable drawable = getDrawable(); + DrawableTransition.transit(drawable, false, 1000); + } + } + + public static class SimpleImageGetListener + implements ImageLoader.OnGetImageListener { + private boolean mTransitabled = true; + private boolean mFixScaleType = false; + private final LoadImageView mLiv; + + public SimpleImageGetListener(LoadImageView liv) { + mLiv = liv; + mLiv.setState(LoadImageView.LOADING); + } + + public SimpleImageGetListener setTransitabled(boolean transitabled) { + mTransitabled = transitabled; + return this; + } + + public SimpleImageGetListener setFixScaleType(boolean fixScaleType) { + mFixScaleType = fixScaleType; + return this; + } + + public boolean isVaild(String key) { + return key.equals(mLiv.getKey()) + && mLiv.getParent() != null; + } + + @Override + 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); + else + mLiv.setImageBitmap(bmp); + mLiv.setState(LoadImageView.LOADED); + } else { + if (mFixScaleType) + mLiv.setScaleType(ImageView.ScaleType.FIT_CENTER); + mLiv.setTouchImage(); + mLiv.setOnClick(); + mLiv.setState(LoadImageView.FAIL); + } + } + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/MaterialProgress.java b/app/src/main/java/com/hippo/ehviewer/widget/MaterialProgress.java new file mode 100644 index 000000000..4f8382958 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/MaterialProgress.java @@ -0,0 +1,82 @@ +/* + * 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.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.View; + +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.drawable.MaterialProgressDrawable; + +public class MaterialProgress extends View { + + private final MaterialProgressDrawable mDrawable; + + @SuppressWarnings("deprecation") + public MaterialProgress(Context context) { + super(context); + + mDrawable = new MaterialProgressDrawable(); + setBackgroundDrawable(mDrawable); + if (getVisibility() == View.VISIBLE) + start(); + } + + public MaterialProgress(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + @SuppressWarnings("deprecation") + public MaterialProgress(Context context, AttributeSet attrs, + int defStyleAttr) { + super(context, attrs, defStyleAttr); + + TypedArray typedArray = context.obtainStyledAttributes( + attrs, R.styleable.MaterialProgress, defStyleAttr, 0); + + int width = typedArray.getDimensionPixelSize(R.styleable.MaterialProgress_mpWidth, -1); + int height = typedArray.getDimensionPixelSize(R.styleable.MaterialProgress_mpHeight, -1); + int color = typedArray.getColor(R.styleable.MaterialProgress_mpColor, 0); + int thickness = typedArray.getDimensionPixelSize(R.styleable.MaterialProgress_mpThickness, 5); + int innerRadius = typedArray.getDimensionPixelSize(R.styleable.MaterialProgress_mpInnerRadius, -1); + + typedArray.recycle(); + + mDrawable = new MaterialProgressDrawable(width, height, color, thickness, innerRadius); + setBackgroundDrawable(mDrawable); + if (getVisibility() == View.VISIBLE) + start(); + } + + public void setColor(int color) { + mDrawable.setColor(color); + } + + public boolean isRunning() { + return mDrawable.isRunning(); + } + + public void start() { + mDrawable.start(); + } + + public void stop() { + mDrawable.stop(); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/MaterialToast.java b/app/src/main/java/com/hippo/ehviewer/widget/MaterialToast.java new file mode 100644 index 000000000..ae8803361 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/MaterialToast.java @@ -0,0 +1,64 @@ +/* + * 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.annotation.SuppressLint; +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; +import android.widget.Toast; + +import com.hippo.ehviewer.R; + +public class MaterialToast extends Toast { + + private static Context sContext; + + public static void setContext(Context context) { + sContext = context; + } + + @SuppressLint("InflateParams") + public MaterialToast(String mesg) { + super(sContext); + init(mesg); + } + + @SuppressLint("InflateParams") + public MaterialToast(int resId) { + this(sContext.getString(resId)); + } + + @SuppressLint("InflateParams") + private void init(String mesg) { + View v = LayoutInflater.from(sContext) + .inflate(R.layout.material_toast, null); + TextView tv = (TextView)v.findViewById(R.id.message); + tv.setText(mesg); + setView(v); + setDuration(LENGTH_SHORT); + } + + public static final void showToast(String mesg) { + new MaterialToast(mesg).show(); + } + + public static final void showToast(int resId) { + new MaterialToast(resId).show(); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/PrefixEditText.java b/app/src/main/java/com/hippo/ehviewer/widget/PrefixEditText.java new file mode 100644 index 000000000..5513cb8c2 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/PrefixEditText.java @@ -0,0 +1,93 @@ +/* + * 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.text.Editable; +import android.text.Selection; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.widget.AutoCompleteTextView; + +public class PrefixEditText extends AutoCompleteTextView + implements TextWatcher { + + private Context mContext; + private String mPrefix; + + public PrefixEditText(Context context) { + super(context); + init(context); + } + public PrefixEditText(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + public PrefixEditText(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context); + } + + private void init(Context context) { + mContext = context; + addTextChangedListener(this); + } + + public void setPrefix(int resId) { + setPrefix(mContext.getString(resId)); + } + + public void setPrefix(String prefix) { + mPrefix = prefix; + setText(mPrefix); + if (mPrefix != null) + Selection.setSelection(getText(), mPrefix.length()); + } + + @Override + protected void onSelectionChanged(int selStart, int selEnd) { + super.onSelectionChanged(selStart, selEnd); + if(mPrefix != null) { + int newSelStart = selStart; + int newSelEnd = selEnd; + int prefixLength = mPrefix.length(); + if (newSelStart < prefixLength) + newSelStart = prefixLength; + if (newSelEnd < prefixLength) + newSelEnd = prefixLength; + setSelection(newSelStart, newSelEnd); + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, + int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, + int before, int count) {} + + @Override + public void afterTextChanged(Editable s) { + if(mPrefix != null && + !s.toString().startsWith(mPrefix)){ + setText(mPrefix); + setSelection(mPrefix.length()); + } + } + +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/ProgressiveRatingBar.java b/app/src/main/java/com/hippo/ehviewer/widget/ProgressiveRatingBar.java new file mode 100644 index 000000000..aa28a92a9 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/ProgressiveRatingBar.java @@ -0,0 +1,77 @@ +/* + * 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.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.widget.RatingBar; + +@SuppressLint("ClickableViewAccessibility") +public class ProgressiveRatingBar extends RatingBar + implements View.OnTouchListener { + + private boolean mEnableRate = true; + private OnUserRateListener mListener; + + public ProgressiveRatingBar(Context context) { + super(context); + setOnTouchListener(this); + } + public ProgressiveRatingBar(Context context, AttributeSet attrs) { + super(context, attrs); + setOnTouchListener(this); + } + public ProgressiveRatingBar(Context context, AttributeSet attrs, + int defStyle) { + super(context, attrs, defStyle); + setOnTouchListener(this); + } + + public void setEnableRate(boolean enableRate) { + mEnableRate = enableRate; + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + if (mEnableRate) + return false; + else + return true; + } + + @Override + @SuppressLint("WrongCall") + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (isPressed() && mListener != null) { + mListener.onUserRate(getRating()); + } + } + + public void setOnUserRateListener(OnUserRateListener l) { + mListener = l; + } + + public interface OnUserRateListener { + void onUserRate(float rating); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/ProgressiveTextView.java b/app/src/main/java/com/hippo/ehviewer/widget/ProgressiveTextView.java new file mode 100644 index 000000000..5279cca0e --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/ProgressiveTextView.java @@ -0,0 +1,99 @@ +/* + * 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.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.widget.TextView; + +/** + * + * A simple view to show new text replacing old text with animation + * + * @author Hippo + * + */ +public class ProgressiveTextView extends TextView { + @SuppressWarnings("unused") + private static final String TAG = "ProgressiveTextView"; + + private boolean isProgress = false; + private CharSequence mNewText; + private Bitmap mShowBmp; + private float xOffset; + private float yOffset; + + public ProgressiveTextView(Context context) { + super(context); + } + public ProgressiveTextView(Context context, AttributeSet attrs) { + super(context, attrs); + } + public ProgressiveTextView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public final void setNewText(int resid) { + setNewText(getContext().getResources().getText(resid)); + } + + public final void setNewText(CharSequence newText) { + mNewText = newText; + isProgress = true; + invalidate(); + } + + @SuppressLint("DrawAllocation") + @Override + protected void onDraw(Canvas canvas) { + if (isProgress && (mShowBmp != null || mNewText != null)) { + int width = canvas.getWidth(); + int height = canvas.getHeight(); + + if (mShowBmp == null) { + mShowBmp = Bitmap.createBitmap(width, 2 * height, + Bitmap.Config.ARGB_8888); + Canvas showCanvas = new Canvas(mShowBmp); + xOffset = 0.0f; + yOffset = 0.0f; + + super.onDraw(showCanvas); + setText(mNewText); + mNewText = null; + showCanvas.translate(0, height); + super.onDraw(showCanvas); + } + + if (yOffset > -height) { + canvas.drawBitmap(mShowBmp, xOffset, yOffset, null); + yOffset -= 1; + invalidate(); + } else { + yOffset = -height; + canvas.drawBitmap(mShowBmp, xOffset, yOffset, null); + mShowBmp.recycle(); + mShowBmp = null; + isProgress = false; + } + } else { + super.onDraw(canvas); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/RatingView.java b/app/src/main/java/com/hippo/ehviewer/widget/RatingView.java new file mode 100644 index 000000000..5ee367c05 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/RatingView.java @@ -0,0 +1,64 @@ +/* + * 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.util.AttributeSet; +import android.widget.TextView; + +import com.hippo.ehviewer.AppContext; + +public class RatingView extends TextView { + + private static final char START_EMPTY = '\uFA73'; + private static final char START_HALF = '\uFA74'; + private static final char START_FULL = '\uFA75'; + + private int mRating = -1; + + public RatingView(Context context) { + super(context); + init(context); + } + public RatingView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + public RatingView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context); + } + + private void init(Context context) { + setTypeface(((AppContext)context.getApplicationContext()).getFaceTypeface()); + } + + public void setRating(float rating) { + mRating = Math.min(10, Math.round(rating * 2)); + int fullNum = mRating / 2; + int halfNum = mRating % 2; + int emptyNum = 5 - fullNum - halfNum; + StringBuilder sb = new StringBuilder(5); + for (int i = 0; i < fullNum; i++) + sb.append(START_FULL); + if (halfNum == 1) + sb.append(START_HALF); + for (int i = 0; i < emptyNum; i++) + sb.append(START_EMPTY); + setText(sb.toString()); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/RefreshLayout.java b/app/src/main/java/com/hippo/ehviewer/widget/RefreshLayout.java new file mode 100644 index 000000000..5420a825b --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/RefreshLayout.java @@ -0,0 +1,238 @@ +/* + * 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(); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/RefreshTextView.java b/app/src/main/java/com/hippo/ehviewer/widget/RefreshTextView.java new file mode 100644 index 000000000..7c9e0f438 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/RefreshTextView.java @@ -0,0 +1,155 @@ +/* + * 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.util.AttributeSet; +import android.view.View; +import android.widget.TextView; + +import com.hippo.ehviewer.AppContext; +import com.hippo.ehviewer.AppHandler; + +// TODO add animation +public class RefreshTextView extends TextView + implements View.OnClickListener{ + + private volatile boolean mIsRefreshing = false; + private String mDefaultTip; + private OnRefreshListener mDefaultListener; + private OnRefreshListener mListener; + + private static final String[] REFRESHING_STRINGS = { + "(\uFA70π\uFA71)", + "(\uFA71π\uFA70)" + }; + + private static final int mSize = REFRESHING_STRINGS.length; + + public RefreshTextView(Context context) { + super(context); + init(context); + } + + public RefreshTextView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public RefreshTextView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context); + } + + public void init(Context context) { + setTypeface(((AppContext)context.getApplicationContext()).getFaceTypeface()); + setOnClickListener(this); + } + + public void setDefaultRefresh(int tiprResId, OnRefreshListener listener) { + setDefaultRefresh(getContext().getString(tiprResId), listener); + } + + public void setDefaultRefresh(String tip, OnRefreshListener listener) { + mDefaultTip = tip; + mDefaultListener = listener; + } + + @Override + public void onClick(View v) { + if (mIsRefreshing) + return; + + if (mListener != null) + mListener.onRefresh(); + + setRefreshing(true); + } + + public void setRefreshing(boolean refreshing) { + if (mIsRefreshing == refreshing) { + return; + } + + mIsRefreshing = refreshing; + if (mIsRefreshing) + new RefreshingThread().start(); + else + setText(null); + } + + public void setEmesg(int resId, boolean canRefresh) { + setEmesg(getContext().getString(resId), canRefresh); + } + + public void setEmesg(CharSequence emseg, boolean canRefresh) { + if (canRefresh) + setEmesg(emseg, mDefaultTip, mDefaultListener); + else + setEmesg(emseg, null, null); + } + + public void setEmesg(CharSequence emseg, String tip, + OnRefreshListener listener) { + mIsRefreshing = false; + boolean canRefresh = listener == null ? false : true; + + StringBuilder sb = new StringBuilder("==█ \n(>π<)\n\n"); + sb.append(emseg); + if (canRefresh) { + sb.append("\n\n").append(tip); + mListener = listener; + } else { + mListener = null; + } + + setText(sb.toString()); + } + + /** + * The thread to control refreshing animate + * + * @author Hippo + * + */ + private class RefreshingThread extends Thread { + int mIndex; + @Override + public void run() { + mIndex = 0; + while (mIsRefreshing) { + AppHandler.getInstance().post(new Runnable() { + @Override + public void run() { + if (mIsRefreshing) + RefreshTextView.this.setText(REFRESHING_STRINGS[mIndex]); + } + }); + try { + Thread.sleep(500); + } catch (InterruptedException e) {} + + if (++mIndex >= mSize) + mIndex = 0; + } + } + } + + public interface OnRefreshListener { + void onRefresh(); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/ResponedScrollView.java b/app/src/main/java/com/hippo/ehviewer/widget/ResponedScrollView.java new file mode 100644 index 000000000..03b8710ff --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/ResponedScrollView.java @@ -0,0 +1,86 @@ +/* + * 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.annotation.SuppressLint; +import android.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.widget.ScrollView; + +public class ResponedScrollView extends ScrollView { + + public static final int SCROLL_START = 0x0; + public static final int SCROLL_END = 0x1; + + public static final long CHECK_DELAY = 100L; + + private boolean isPosted = false; + private int mLastScrollY; + private OnScrollStateChangedListener mListener; + + public ResponedScrollView(Context context) { + super(context); + } + + public ResponedScrollView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ResponedScrollView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public void setOnScrollStateChangedListener(OnScrollStateChangedListener l) { + mListener = l; + } + + private void wakeScrollObserver() { + mLastScrollY = getScrollY(); + postDelayed(mScrollObserver, CHECK_DELAY); + isPosted = true; + } + + @Override + @SuppressLint("ClickableViewAccessibility") + public boolean onTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_UP && !isPosted) { + wakeScrollObserver(); + if (mListener != null) + mListener.onScrollStateChanged(this, SCROLL_START); + } + return super.onTouchEvent(ev); + } + + private final Runnable mScrollObserver = new Runnable() { + @Override + public void run() { + int newScrollY = getScrollY(); + if(newScrollY == mLastScrollY) { + isPosted = false; + if (mListener != null) + mListener.onScrollStateChanged(ResponedScrollView.this, SCROLL_END); + } else { + wakeScrollObserver(); + } + } + }; + + public interface OnScrollStateChangedListener { + public void onScrollStateChanged(ResponedScrollView view, int state); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/SearchView.java b/app/src/main/java/com/hippo/ehviewer/widget/SearchView.java new file mode 100644 index 000000000..8d2ec5b91 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/SearchView.java @@ -0,0 +1,64 @@ +/* + * 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.annotation.TargetApi; +import android.content.Context; +import android.os.Build; +import android.util.AttributeSet; + +public class SearchView extends android.widget.SearchView { + + private static final String TAG = SearchView.class.getSimpleName(); + + private OnActionViewExpandedListener mListener; + + public SearchView(Context context) { + super(context); + } + + public SearchView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public SearchView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public SearchView(Context context, AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public void onActionViewExpanded() { + super.onActionViewExpanded(); + + if (mListener != null) + mListener.onActionViewExpanded(); + } + + public void setOnActionViewExpandedListener(OnActionViewExpandedListener l) { + mListener = l; + } + + public interface OnActionViewExpandedListener { + public void onActionViewExpanded(); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/SimpleGridLayout.java b/app/src/main/java/com/hippo/ehviewer/widget/SimpleGridLayout.java new file mode 100644 index 000000000..f04868be3 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/SimpleGridLayout.java @@ -0,0 +1,249 @@ +/* + * 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.content.res.Configuration; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; + +import com.hippo.ehviewer.R; + +/** + * not scrollable + * + * @author Hippo + * + */ +public class SimpleGridLayout extends ViewGroup { + + private static final int DEFAULT_COLUMNS_PORTRAIT = 2; + private static final int DEFAULT_COLUMNS_LANDSCAPE = 3; + + + private int mItemMargin; + private int mColumnCount; + + private int mColumnCountPortrait = 1; + private int mColumnCountLandscape = 1; + + private int[] mItemHeights; + private int mItemWidth; + + public SimpleGridLayout(Context context) { + super(context); + init(); + } + + public SimpleGridLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SimpleGridLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(); + + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.SimpleGridLayout, defStyle, 0); + + mColumnCount = typedArray.getInteger( + R.styleable.SimpleGridLayout_sgl_column_count, 0); + + if (mColumnCount > 0) { + mColumnCountPortrait = mColumnCount; + mColumnCountLandscape = mColumnCount; + } + else { + mColumnCountPortrait = typedArray.getInteger( + R.styleable.SimpleGridLayout_sgl_column_count_portrait, + DEFAULT_COLUMNS_PORTRAIT); + mColumnCountLandscape = typedArray.getInteger( + R.styleable.SimpleGridLayout_sgl_column_count_landscape, + DEFAULT_COLUMNS_LANDSCAPE); + } + + mItemMargin = typedArray.getDimensionPixelSize( + R.styleable.SimpleGridLayout_sgl_item_margin, 0); + + typedArray.recycle(); + } + + public void init() { + setWillNotDraw(true); + } + + public void setItemMargin(int itemMargin) { + if (mItemMargin != itemMargin) { + mItemMargin = itemMargin; + requestLayout(); + } + } + + public void setColumnCount(int columnCount) { + if (columnCount > 0) { + boolean needRequestLayout = + isLandscape() ? (mColumnCountLandscape != columnCount ? true : false) : + (mColumnCountPortrait != columnCount ? true : false); + + mColumnCountPortrait = columnCount; + mColumnCountLandscape = columnCount; + + if (needRequestLayout) + requestLayout(); + } + } + + public void setColumnCountPortrait(int columnCountPortrait) { + if (columnCountPortrait > 0 && + mColumnCountPortrait != columnCountPortrait) { + mColumnCountPortrait = columnCountPortrait; + if (!isLandscape()) + requestLayout(); + } + } + + public void setColumnCountLandscape(int columnCountLandscape) { + if (columnCountLandscape > 0 && + mColumnCountLandscape != columnCountLandscape) { + mColumnCountLandscape = columnCountLandscape; + + if (isLandscape()) + requestLayout(); + } + } + + private boolean isLandscape() { + return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; + } + + private int getMaxValue(int[] array) { + int max = array[0]; + for (int i = 1; i < array.length; i++) + if (array[i] > max) max = array[i]; + return max; + } + + private int getMaxValue(int[] array, int length) { + int max = array[0]; + for (int i = 1; i < length; i++) + if (array[i] > max) max = array[i]; + return max; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + + int maxWidth = MeasureSpec.getSize(widthMeasureSpec); + int maxHeight = MeasureSpec.getSize(heightMeasureSpec); + + if (widthMode == MeasureSpec.UNSPECIFIED) + maxWidth = Integer.MAX_VALUE; + if (heightMode == MeasureSpec.UNSPECIFIED) + maxHeight = Integer.MAX_VALUE; + + mColumnCount = isLandscape() ? mColumnCountLandscape : mColumnCountPortrait; + + // Get item width MeasureSpec + int itemWidthMeasureSpec; + if (widthMode == MeasureSpec.UNSPECIFIED) { + mItemWidth = Integer.MAX_VALUE; + itemWidthMeasureSpec = MeasureSpec.makeMeasureSpec(mItemWidth, MeasureSpec.UNSPECIFIED); + } else { + mItemWidth = (maxWidth - (mColumnCount + 1) * mItemMargin) / mColumnCount; + itemWidthMeasureSpec = MeasureSpec.makeMeasureSpec(mItemWidth, MeasureSpec.EXACTLY); + } + + int maxItemWidth = 0; + int indexInRow = 0; + int lastbottom = mItemMargin; + int[] heightsOneRow = new int[mColumnCount]; + int childCount = getChildCount(); + if (mItemHeights == null || mItemHeights.length != childCount) + mItemHeights = new int[childCount]; + + for (int index = 0; index < childCount; index++, indexInRow++) { + final View child = getChildAt(index); + + child.measure(itemWidthMeasureSpec, MeasureSpec.UNSPECIFIED); + int childwidth = child.getMeasuredWidth(); + int childHeight = child.getMeasuredHeight(); + + if (maxItemWidth < childwidth) + maxItemWidth = childwidth; + + // Get a new row + if (indexInRow == mColumnCount) + indexInRow = 0; + heightsOneRow[indexInRow] = childHeight; + + // Get row end + if (indexInRow == mColumnCount - 1) { + int maxItemHeight = getMaxValue(heightsOneRow); + for (int i = 0; i < mColumnCount; i++) + mItemHeights[index - mColumnCount + 1 + i] = lastbottom + maxItemHeight - heightsOneRow[i]; + // Add margin + lastbottom += maxItemHeight + mItemMargin; + } + } + + // Get some item left + if (indexInRow != mColumnCount) { + int maxItemHeight = getMaxValue(heightsOneRow, indexInRow); + for (int i = 0; i < indexInRow; i++) + mItemHeights[childCount - indexInRow + i] = lastbottom + maxItemHeight - heightsOneRow[i]; + // Add margin + lastbottom += maxItemHeight + mItemMargin; + } + + int measuredWidth; + int measuredHeight; + if (widthMode == MeasureSpec.UNSPECIFIED) + measuredWidth = maxItemWidth; + else + measuredWidth = maxWidth; + if (heightMode == MeasureSpec.EXACTLY) + measuredHeight = maxHeight; + else if (heightMode == MeasureSpec.AT_MOST) + measuredHeight = maxHeight < lastbottom ? maxHeight : lastbottom; + else + measuredHeight = lastbottom; + + setMeasuredDimension(measuredWidth, measuredHeight); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + int lastRight = 0; + int childCount = getChildCount(); + for (int index = 0; index < childCount; index++) { + final View child = getChildAt(index); + + int indexInRow = index % mColumnCount; + if (indexInRow == 0) + lastRight = mItemMargin; + + int itemWidth = mItemWidth == Integer.MAX_VALUE ? child.getMeasuredWidth(): mItemWidth; + child.layout(lastRight, mItemHeights[index], lastRight + itemWidth, + mItemHeights[index] + child.getMeasuredHeight()); + + lastRight += itemWidth + mItemMargin; + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/SlidingDrawerLayout.java b/app/src/main/java/com/hippo/ehviewer/widget/SlidingDrawerLayout.java new file mode 100644 index 000000000..03e90db6a --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/SlidingDrawerLayout.java @@ -0,0 +1,1184 @@ +/* + * 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.animation.Animator; +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.support.v4.content.ContextCompat; +import android.support.v4.view.GravityCompat; +import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.ViewCompat; +import android.support.v4.widget.ViewDragHelper; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowInsets; + +import com.hippo.ehviewer.util.Constants; +import com.hippo.ehviewer.util.MathUtils; +import com.hippo.ehviewer.util.Ui; +import com.hippo.ehviewer.util.ViewUtils; +import com.hippo.ehviewer.util.ZInterpolator; + +@SuppressLint("RtlHardcoded") +public class SlidingDrawerLayout extends ViewGroup implements ValueAnimator.AnimatorUpdateListener, + Animator.AnimatorListener { + + private static final String TAG = SlidingDrawerLayout.class.getSimpleName(); + + private static final int[] LAYOUT_ATTRS = new int[] { + android.R.attr.layout_gravity + }; + + private static final int STATE_CLOSED = 0; + private static final int STATE_SLIDING = 1; + private static final int STATE_OPEN = 2; + + /** + * The drawer is unlocked. + */ + public static final int LOCK_MODE_UNLOCKED = 0; + + /** + * The drawer is locked closed. The user may not open it, though the app may + * open it programmatically. + */ + public static final int LOCK_MODE_LOCKED_CLOSED = 1; + + /** + * The drawer is locked open. The user may not close it, though the app may + * close it programmatically. + */ + public static final int LOCK_MODE_LOCKED_OPEN = 2; + + private static final float OPEN_SENSITIVITY = 0.1f; + private static final float CLOSE_SENSITIVITY = 0.9f; + + private int mMinDrawerMargin; + private boolean mInLayout; + + private View mContentView; + private View mLeftDrawer; + private View mRightDrawer; + private ShadowView mShadow; + + private ViewDragHelper mDragHelper; + private float mInitialMotionX; + private float mInitialMotionY; + + private boolean mLeftOpened; + private boolean mRightOpened; + private int mLeftState; + private int mRightState; + private float mLeftPercent; + private float mRightPercent; + private int mLeftLockMode; + private int mRightLockMode; + + private ValueAnimator mAnimator; + private View mTargetView; + private int mStartLeft; + private int mEndLeft; + private boolean mToOpen; + private boolean mCancelAnimation; + private View mOpenTask; + + private DrawerListener mListener; + + private Drawable mStatusBarBackground; + + private boolean mIntercepted; + private int mInterceptPointNum; + + private Object mLastInsets; + private boolean mDrawStatusBarBackground; + + private static int sDefaultMinDrawerMargin = 56; + + private static final SlidingDrawerLayoutCompatImpl IMPL; + + static { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + IMPL = new SlidingDrawerLayoutCompatImplApi21(); + } else { + IMPL = new SlidingDrawerLayoutCompatImplBase(); + } + } + + interface SlidingDrawerLayoutCompatImpl { + void configureApplyInsets(View drawerLayout); + void dispatchChildInsets(View child, Object insets, int drawerGravity); + void applyMarginInsets(MarginLayoutParams lp, Object insets, int drawerGravity); + int getTopInset(Object lastInsets); + } + + static class SlidingDrawerLayoutCompatImplBase implements SlidingDrawerLayoutCompatImpl { + @Override + public void configureApplyInsets(View drawerLayout) { + // This space for rent + } + + @Override + public void dispatchChildInsets(View child, Object insets, int drawerGravity) { + // This space for rent + } + + @Override + public void applyMarginInsets(MarginLayoutParams lp, Object insets, int drawerGravity) { + // This space for rent + } + + @Override + public int getTopInset(Object insets) { + return 0; + } + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + static class SlidingDrawerLayoutCompatImplApi21 implements SlidingDrawerLayoutCompatImpl { + @Override + public void configureApplyInsets(View drawerLayout) { + if (drawerLayout instanceof SlidingDrawerLayout) { + drawerLayout.setOnApplyWindowInsetsListener(new InsetsListener()); + drawerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + } + } + + @Override + public void dispatchChildInsets(View child, Object insets, int gravity) { + WindowInsets wi = (WindowInsets) insets; + if (gravity == Gravity.LEFT) { + wi = wi.replaceSystemWindowInsets(wi.getSystemWindowInsetLeft(), + wi.getSystemWindowInsetTop(), 0, wi.getSystemWindowInsetBottom()); + } else if (gravity == Gravity.RIGHT) { + wi = wi.replaceSystemWindowInsets(0, wi.getSystemWindowInsetTop(), + wi.getSystemWindowInsetRight(), wi.getSystemWindowInsetBottom()); + } + child.dispatchApplyWindowInsets(wi); + } + + @Override + public void applyMarginInsets(MarginLayoutParams lp, Object insets, int gravity) { + WindowInsets wi = (WindowInsets) insets; + if (gravity == Gravity.LEFT) { + wi = wi.replaceSystemWindowInsets(wi.getSystemWindowInsetLeft(), + wi.getSystemWindowInsetTop(), 0, wi.getSystemWindowInsetBottom()); + } else if (gravity == Gravity.RIGHT) { + wi = wi.replaceSystemWindowInsets(0, wi.getSystemWindowInsetTop(), + wi.getSystemWindowInsetRight(), wi.getSystemWindowInsetBottom()); + } + lp.leftMargin = wi.getSystemWindowInsetLeft(); + lp.topMargin = wi.getSystemWindowInsetTop(); + lp.rightMargin = wi.getSystemWindowInsetRight(); + lp.bottomMargin = wi.getSystemWindowInsetBottom(); + } + + @Override + public int getTopInset(Object insets) { + return insets != null ? ((WindowInsets) insets).getSystemWindowInsetTop() : 0; + } + + static class InsetsListener implements View.OnApplyWindowInsetsListener { + @Override + public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { + final SlidingDrawerLayout drawerLayout = (SlidingDrawerLayout) v; + drawerLayout.setChildInsets(insets, insets.getSystemWindowInsetTop() > 0); + return insets.consumeSystemWindowInsets(); + } + } + } + + /** + * Listener for monitoring events about drawers. + */ + public interface DrawerListener { + /** + * Called when a drawer's position changes. + * @param drawerView The child view that was moved + * @param slideOffset The new offset of this drawer within its range, from 0-1 + */ + public void onDrawerSlide(View drawerView, float percent); + + /** + * Called when a drawer has settled in a completely open state. + * The drawer is interactive at this point. + * + * @param drawerView Drawer view that is now open + */ + public void onDrawerOpened(View drawerView); + + /** + * Called when a drawer has settled in a completely closed state. + * + * @param drawerView Drawer view that is now closed + */ + public void onDrawerClosed(View drawerView); + + /** + * Called when the drawer motion state changes. The new state will + * be one of {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}. + * + * @param newState The new drawer motion state + */ + public void onDrawerStateChanged(View drawerView, int newState); + } + + /** + * minMargin should be app bar height in Material design + * + * @param minMargin + */ + public static final void setDefaultMinDrawerMargin(int minMargin) { + sDefaultMinDrawerMargin = minMargin; + } + + public SlidingDrawerLayout(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public SlidingDrawerLayout(Context context, AttributeSet attrs, + int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public SlidingDrawerLayout(Context context, AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + + private void init() { + mLeftOpened = false; + mRightOpened = false; + mLeftState = STATE_CLOSED; + mRightState = STATE_CLOSED; + mLeftPercent = 0.0f; + mRightPercent = 0.0f; + mMinDrawerMargin = sDefaultMinDrawerMargin; + mDragHelper = ViewDragHelper.create(this, 0.5f, new DragHelperCallback()); + mAnimator = ValueAnimator.ofFloat(0.0f, 1.0f); + mAnimator.addUpdateListener(this); + mAnimator.addListener(this); + mAnimator.setInterpolator(new ZInterpolator(0.5f)); + mCancelAnimation = false; + + setWillNotDraw(false); + } + + public void setDrawerListener(DrawerListener listener) { + mListener = listener; + } + + /** + * Close all currently open drawer views by animating them out of view. + */ + public void closeDrawers() { + closeDrawer(mLeftDrawer); + closeDrawer(mRightDrawer); + } + + /** + * Open the specified drawer view by animating it into view. + * + * @param drawerView + * Drawer view to open + */ + public void openDrawer(View drawerView) { + if (drawerView == null || (drawerView != mLeftDrawer && drawerView != mRightDrawer)) + return; + + boolean isLeft = drawerView == mLeftDrawer; + switch (isLeft ? mLeftState : mRightState) { + case STATE_CLOSED: + // Check other side first + switch (isLeft ? mRightState : mLeftState) { + case STATE_CLOSED: + startAnimation(isLeft, true, false); + break; + case STATE_SLIDING: + if (mAnimator.isRunning()) { + if (mToOpen) { + cancelAnimation(); + mOpenTask = isLeft ? mLeftDrawer : mRightDrawer; + startAnimation(!isLeft, false, false); + } else { + mOpenTask = isLeft ? mLeftDrawer : mRightDrawer; + } + } else { + // You finger is on Screen! + } + break; + case STATE_OPEN: + // Close other side first + mOpenTask = isLeft ? mLeftDrawer : mRightDrawer; + startAnimation(!isLeft, false, false); + break; + } + break; + + case STATE_SLIDING: + if (mAnimator.isRunning()) { + if (mToOpen) { + // Same purpose + } else { + cancelAnimation(); + startAnimation(isLeft, true, false); + } + } else { + // You finger is on Screen! + } + break; + + case STATE_OPEN: + // Ok it is opened + break; + } + } + + public void openDrawer(int gravity) { + if (gravity == Gravity.LEFT) + openDrawer(mLeftDrawer); + else if (gravity == Gravity.RIGHT) + openDrawer(mRightDrawer); + } + + /** + * Close the specified drawer view by animating it into view. + * + * @param drawerView + * Drawer view to close + */ + public void closeDrawer(View drawerView) { + if (drawerView == null || (drawerView != mLeftDrawer && drawerView != mRightDrawer)) + return; + + boolean isLeft = drawerView == mLeftDrawer; + switch (isLeft ? mLeftState : mRightState) { + case STATE_CLOSED: + // Ok it is closed + break; + case STATE_SLIDING: + if (mAnimator.isRunning()) { + if (mToOpen) { + cancelAnimation(); + startAnimation(isLeft, false, false); + } else { + // Same purpose + } + } else { + // You finger is on Screen! + } + break; + case STATE_OPEN: + startAnimation(isLeft, false, false); + break; + } + } + + public void closeDrawer(int gravity) { + if (gravity == Gravity.LEFT) + closeDrawer(mLeftDrawer); + else if (gravity == Gravity.RIGHT) + closeDrawer(mRightDrawer); + } + + public boolean isDrawerOpen(View drawer) { + if (drawer == mLeftDrawer) + return mLeftOpened; + else if (drawer == mRightDrawer) + return mRightOpened; + else + throw new IllegalArgumentException("The view is not drawer"); + } + + public boolean isDrawerOpen(int gravity) { + if (gravity == Gravity.LEFT) + return isDrawerOpen(mLeftDrawer); + else if (gravity == Gravity.RIGHT) + return isDrawerOpen(mRightDrawer); + else + throw new IllegalArgumentException("gravity must be Gravity.LEFT or Gravity.RIGHT"); + } + + public void setDrawerLockMode(int lockMode, int gravity) { + if (gravity == Gravity.LEFT) + setDrawerLockMode(gravity, mLeftDrawer); + else if (gravity == Gravity.RIGHT) + setDrawerLockMode(gravity, mRightDrawer); + else + throw new IllegalArgumentException("gravity must be Gravity.LEFT or Gravity.RIGHT"); + } + + public void setDrawerLockMode(int lockMode, View drawer) { + if (drawer != mLeftDrawer && drawer != mRightDrawer) + throw new IllegalArgumentException("The view is not drawer"); + + int oldLockMode; + int otherSideLockMode; + if (drawer == mLeftDrawer) { + oldLockMode = mLeftLockMode; + otherSideLockMode = mRightLockMode; + mLeftLockMode = lockMode; + } else { + oldLockMode = mRightLockMode; + otherSideLockMode = mLeftLockMode; + mRightLockMode = lockMode; + } + if (oldLockMode == lockMode) + return; + + if (otherSideLockMode == LOCK_MODE_LOCKED_OPEN && lockMode == LOCK_MODE_LOCKED_OPEN) + throw new IllegalArgumentException("Only on side could be LOCK_MODE_LOCKED_OPEN"); + + switch (lockMode) { + // TODO What if open or close fail ? + case LOCK_MODE_LOCKED_OPEN: + openDrawer(drawer); + break; + case LOCK_MODE_LOCKED_CLOSED: + closeDrawer(drawer); + break; + } + } + + public int getDrawerLockMode(int gravity) { + if (gravity == Gravity.LEFT) + return getDrawerLockMode(mLeftDrawer); + else if (gravity == Gravity.RIGHT) + return getDrawerLockMode(mRightDrawer); + else + throw new IllegalArgumentException("gravity must be Gravity.LEFT or Gravity.RIGHT"); + } + + public int getDrawerLockMode(View drawer) { + if (drawer == mLeftDrawer) + return mLeftLockMode; + else if (drawer == mRightDrawer) + return mRightLockMode; + else + throw new IllegalArgumentException("The view is not drawer"); + } + + @Override + protected void onFinishInflate() { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final int absGravity = GravityCompat.getAbsoluteGravity( + ((LayoutParams) child.getLayoutParams()).gravity, + ViewCompat.getLayoutDirection(child)); + + if (absGravity == Gravity.NO_GRAVITY) { + if (mContentView != null) + throw new IllegalStateException("There is more than one content view"); + mContentView = child; + } else if ((absGravity & Gravity.LEFT) == Gravity.LEFT) { + if (mLeftDrawer != null) + throw new IllegalStateException("There is more than one left menu"); + mLeftDrawer = child; + } else if ((absGravity & Gravity.RIGHT) == Gravity.RIGHT) { + if (mRightDrawer != null) + throw new IllegalStateException("There is more than one right menu"); + mRightDrawer = child; + } else { + throw new IllegalStateException("Child " + child + " at index " + i + + " does not have a valid layout_gravity - must be Gravity.LEFT, " + + "Gravity.RIGHT or Gravity.NO_GRAVITY"); + } + } + + if (mContentView == null) + throw new IllegalStateException("There is no content view"); + // Material is solid. + // Input events cannot pass through material. + if (mLeftDrawer != null) + mLeftDrawer.setClickable(true); + if (mRightDrawer != null) + mRightDrawer.setClickable(true); + + mShadow = new ShadowView(getContext()); + addView(mShadow, 1); + } + + /** + * @hide Internal use only; called to apply window insets when configured + * with fitsSystemWindows="true" + */ + public void setChildInsets(Object insets, boolean draw) { + mLastInsets = insets; + mDrawStatusBarBackground = draw; + setWillNotDraw(!draw && getBackground() == null); + requestLayout(); + } + + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + if (widthMode != MeasureSpec.EXACTLY || heightMode != MeasureSpec.EXACTLY) + throw new IllegalArgumentException( + "SlidingDrawerLayout must be measured with MeasureSpec.EXACTLY."); + + setMeasuredDimension(widthSize, heightSize); + + final boolean applyInsets = mLastInsets != null && ViewCompat.getFitsSystemWindows(this); + final int layoutDirection = ViewCompat.getLayoutDirection(this); + + // Gravity value for each drawer we've seen. Only one of each permitted. + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + + if (child.getVisibility() == GONE) { + continue; + } + + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (applyInsets) { + final int cgrav = GravityCompat.getAbsoluteGravity(lp.gravity, layoutDirection); + if (ViewCompat.getFitsSystemWindows(child)) { + IMPL.dispatchChildInsets(child, mLastInsets, cgrav); + } else { + IMPL.applyMarginInsets(lp, mLastInsets, cgrav); + } + } + + if (child == mContentView) { + // Content views get measured at exactly the layout's size. + final int contentWidthSpec = MeasureSpec.makeMeasureSpec( + widthSize - lp.leftMargin - lp.rightMargin, MeasureSpec.EXACTLY); + final int contentHeightSpec = MeasureSpec.makeMeasureSpec( + heightSize - lp.topMargin - lp.bottomMargin, MeasureSpec.EXACTLY); + child.measure(contentWidthSpec, contentHeightSpec); + } else if (child == mLeftDrawer || child == mRightDrawer) { + final int drawerWidthSpec = getChildMeasureSpec(widthMeasureSpec, + lp.leftMargin + lp.rightMargin, + Math.min(widthSize - mMinDrawerMargin, lp.width)); + final int drawerHeightSpec = getChildMeasureSpec(heightMeasureSpec, + lp.topMargin + lp.bottomMargin, + lp.height); + child.measure(drawerWidthSpec, drawerHeightSpec); + } else if (child == mShadow) { + child.measure(widthMeasureSpec, heightMeasureSpec); + } else { + throw new IllegalStateException("Don't call addView"); + } + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + mInLayout = true; + final int width = r - l; + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + + if (child.getVisibility() == GONE) { + continue; + } + + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if (child == mContentView) { + child.layout(lp.leftMargin, lp.topMargin, + lp.leftMargin + child.getMeasuredWidth(), + lp.topMargin + child.getMeasuredHeight()); + } else if (child == mShadow) { + child.layout(l, t, r, b); + } else { // Drawer, if it wasn't onMeasure would have thrown an exception. + final int childWidth = child.getMeasuredWidth(); + final int childHeight = child.getMeasuredHeight(); + int childLeft; + float percent; + + if (child == mLeftDrawer) { + percent = mLeftPercent; + childLeft = -childWidth + (int) (childWidth * percent); + } else { // Right; onMeasure checked for us. + percent = mRightPercent; + childLeft = width - (int) (childWidth * percent); + } + + final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; + + switch (vgrav) { + default: + case Gravity.TOP: { + child.layout(childLeft, lp.topMargin, childLeft + childWidth, + lp.topMargin + childHeight); + break; + } + + case Gravity.BOTTOM: { + final int height = b - t; + child.layout(childLeft, + height - lp.bottomMargin - child.getMeasuredHeight(), + childLeft + childWidth, + height - lp.bottomMargin); + break; + } + + case Gravity.CENTER_VERTICAL: { + final int height = b - t; + int childTop = (height - childHeight) / 2; + + // Offset for margins. If things don't fit right because of + // bad measurement before, oh well. + if (childTop < lp.topMargin) { + childTop = lp.topMargin; + } else if (childTop + childHeight > height - lp.bottomMargin) { + childTop = height - lp.bottomMargin - childHeight; + } + child.layout(childLeft, childTop, childLeft + childWidth, + childTop + childHeight); + break; + } + } + + final int newVisibility = percent > 0 ? VISIBLE : INVISIBLE; + if (child.getVisibility() != newVisibility) { + child.setVisibility(newVisibility); + } + } + } + mInLayout = false; + } + + @Override + public void requestLayout() { + if (!mInLayout) { + super.requestLayout(); + } + } + + private boolean isDrawerUnder(int x, int y) { + return ViewUtils.isViewUnder(mLeftDrawer, x, y) || ViewUtils.isViewUnder(mRightDrawer, x, y); + } + + private boolean isDrawersTouchable() { + if ((mLeftDrawer != null && mLeftLockMode == LOCK_MODE_UNLOCKED) + || (mRightDrawer != null && mRightLockMode == LOCK_MODE_UNLOCKED)) + return true; + else + return false; + } + + private int getActualDxLeft(int dx) { + int leftWidth = mLeftDrawer.getWidth(); + int oldLeft = mLeftDrawer.getLeft(); + int newLeft = MathUtils.clamp(oldLeft + dx, -leftWidth, 0); + int newVisible = newLeft == -leftWidth ? View.INVISIBLE : View.VISIBLE; + if (mLeftDrawer.getVisibility() != newVisible) + mLeftDrawer.setVisibility(newVisible); + + updateDrawerSlide(mLeftDrawer, (newLeft + leftWidth) / (float) leftWidth); + + return newLeft - oldLeft; + } + + private int getActualDxRight(int dx) { + int width = getWidth(); + int rightWidth = mRightDrawer.getWidth(); + int oldLeft = mRightDrawer.getLeft(); + int newLeft = MathUtils.clamp(oldLeft + dx, width - rightWidth, width); + int newVisible = newLeft == width ? View.INVISIBLE : View.VISIBLE; + if (mRightDrawer.getVisibility() != newVisible) + mRightDrawer.setVisibility(newVisible); + + updateDrawerSlide(mRightDrawer, (width - newLeft) / (float) rightWidth); + + return newLeft - oldLeft; + } + + private void slideLeftDrawer(int dx) { + mLeftDrawer.offsetLeftAndRight(getActualDxLeft(dx)); + } + + private void slideRightDrawer(int dx) { + mRightDrawer.offsetLeftAndRight(getActualDxRight(dx)); + } + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + float value = (Float) animation.getAnimatedValue(); + int oldLeft = mTargetView.getLeft(); + int newLeft = MathUtils.lerp(mStartLeft, mEndLeft, value); + if (mTargetView == mLeftDrawer) + slideLeftDrawer(newLeft - oldLeft); + else + slideRightDrawer(newLeft - oldLeft); + } + + @Override + public void onAnimationStart(Animator animation) { + updateDrawerState(mTargetView, STATE_SLIDING); + } + + @Override + public void onAnimationEnd(Animator animation) { + if (!mCancelAnimation) { + updateDrawerSlide(mTargetView, mToOpen ? 1.0f : 0.0f); + updateDrawerState(mTargetView, mToOpen ? STATE_OPEN : STATE_CLOSED); + if (mToOpen) + dispatchOnDrawerOpened(mTargetView); + else + dispatchOnDrawerClosed(mTargetView); + + if (mOpenTask != null && !mToOpen && + (mOpenTask == mLeftDrawer || mOpenTask == mRightDrawer)) { + // If call startAnimation directly, + // onAnimationStart and onAnimationEnd will not be called + final boolean isLeft = mOpenTask == mLeftDrawer; + getHandler().post(new Runnable() { + @Override + public void run() { + startAnimation(isLeft, true, false); + } + }); + } + } + + mCancelAnimation = false; + mOpenTask = null; + } + + @Override + public void onAnimationCancel(Animator animation) { + // Empty + } + + @Override + public void onAnimationRepeat(Animator animation) { + // Empty + } + + private void startAnimation(final boolean isLeft, final boolean isOpen, + boolean staticDuration) { + mToOpen = isOpen; + if (isLeft) { + mTargetView = mLeftDrawer; + mStartLeft = mLeftDrawer.getLeft(); + mEndLeft = mToOpen ? 0 : -mLeftDrawer.getWidth(); + } else { + mTargetView = mRightDrawer; + mStartLeft = mRightDrawer.getLeft(); + mEndLeft = mToOpen ? getWidth() - mRightDrawer.getWidth() : getWidth(); + } + + if (mStartLeft == mEndLeft) { + // No need to animate + updateDrawerSlide(mTargetView, mToOpen ? 1.0f : 0.0f); + updateDrawerState(mTargetView, mToOpen ? STATE_OPEN : STATE_CLOSED); + if (mToOpen) + dispatchOnDrawerOpened(mTargetView); + else + dispatchOnDrawerClosed(mTargetView); + return; + } + + mAnimator.setDuration(staticDuration ? Constants.ANIMATE_TIME : + (long) (Ui.pix2dp(Math.abs(mEndLeft - mStartLeft)) * 1.25)); + mAnimator.start(); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + + final int action = MotionEventCompat.getActionMasked(ev); + final float x = ev.getX(); + final float y = ev.getY(); + boolean interceptSlide = false; + + if (!isDrawersTouchable()) { + return false; + } + + if ((mLeftState == STATE_OPEN || mRightState == STATE_OPEN) && + !isDrawerUnder((int) x, (int) y)) + return true; + + switch (action) { + case MotionEvent.ACTION_DOWN: + mIntercepted = false; + mInterceptPointNum = 0; + mInitialMotionX = x; + mInitialMotionY = y; + break; + case MotionEvent.ACTION_MOVE: + final float adx = Math.abs(x - mInitialMotionX); + final float ady = Math.abs(y - mInitialMotionY); + final int slop = mDragHelper.getTouchSlop(); + if (adx > slop && adx > ady) + interceptSlide = true; + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mDragHelper.cancel(); + break; + } + + if (++mInterceptPointNum > 8) + return false; + + mIntercepted = mDragHelper.shouldInterceptTouchEvent(ev) || interceptSlide; + return mIntercepted; + } + + private void cancelAnimation() { + if (mAnimator.isRunning()) { + mOpenTask = null; + mCancelAnimation = true; + mAnimator.cancel(); + } + } + + @Override + @SuppressLint("ClickableViewAccessibility") + public boolean onTouchEvent(MotionEvent ev) { + + // Cancel animate + cancelAnimation(); + + try { + mDragHelper.processTouchEvent(ev); + } catch (Throwable e) { + // TODO Another finger touch screen may make trouble + } + + final int action = MotionEventCompat.getActionMasked(ev); + final float x = ev.getX(); + final float y = ev.getY(); + + if (action == MotionEvent.ACTION_DOWN) { + mInterceptPointNum = 1; + mIntercepted = true; + } + + if (!mIntercepted) + return false; + + if (!isDrawersTouchable()) + return false; + + switch (action) { + case MotionEvent.ACTION_DOWN: + mInitialMotionX = x; + mInitialMotionY = y; + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mInterceptPointNum = 0; + + if (mLeftState == STATE_SLIDING) { + if (mLeftOpened && mLeftPercent < CLOSE_SENSITIVITY) + startAnimation(true, false, false); + else if (!mLeftOpened && mLeftPercent < OPEN_SENSITIVITY) + startAnimation(true, false, true); + else if (mLeftOpened && mLeftPercent >= CLOSE_SENSITIVITY) + startAnimation(true, true, true); + else if (!mLeftOpened && mLeftPercent >= OPEN_SENSITIVITY) + startAnimation(true, true, false); + } else if (mRightState == STATE_SLIDING) { + if (mRightOpened && mRightPercent < CLOSE_SENSITIVITY) + startAnimation(false, false, false); + else if (!mRightOpened && mRightPercent < OPEN_SENSITIVITY) + startAnimation(false, false, true); + else if (mRightOpened && mRightPercent >= CLOSE_SENSITIVITY) + startAnimation(false, true, true); + else if (!mRightOpened && mRightPercent >= OPEN_SENSITIVITY) + startAnimation(false, true, false); + } + break; + } + + return true; + } + + @Override + protected int getChildDrawingOrder(int childCount, int i) { + return i; + } + + /** + * Set a drawable to draw in the insets area for the status bar. + * Note that this will only be activated if this DrawerLayout fitsSystemWindows. + * + * @param bg Background drawable to draw behind the status bar + */ + public void setStatusBarBackground(Drawable bg) { + mStatusBarBackground = bg; + } + + /** + * Set a drawable to draw in the insets area for the status bar. + * Note that this will only be activated if this DrawerLayout fitsSystemWindows. + * + * @param resId Resource id of a background drawable to draw behind the status bar + */ + public void setStatusBarBackground(int resId) { + mStatusBarBackground = resId != 0 ? ContextCompat.getDrawable(getContext(), resId) : null; + } + + /** + * Set a drawable to draw in the insets area for the status bar. + * Note that this will only be activated if this DrawerLayout fitsSystemWindows. + * + * @param color Color to use as a background drawable to draw behind the status bar + * in 0xAARRGGBB format. + */ + public void setStatusBarBackgroundColor(int color) { + mStatusBarBackground = new ColorDrawable(color); + } + + @Override + public void onDraw(Canvas c) { + super.onDraw(c); + if (mDrawStatusBarBackground && mStatusBarBackground != null) { + final int inset = IMPL.getTopInset(mLastInsets); + if (inset > 0) { + mStatusBarBackground.setBounds(0, 0, getWidth(), inset); + mStatusBarBackground.draw(c); + } + } + } + + private void updateDrawerSlide(View drawerView, float percent) { + boolean update = false; + // Update percent + if (drawerView == mLeftDrawer) { + update = mLeftPercent != percent; + mLeftPercent = percent; + } else if (drawerView == mRightDrawer) { + update = mRightPercent != percent; + mRightPercent = percent; + } + + if (update) + mShadow.setPercent(percent); + + // Callback + if (update && mListener != null) { + mListener.onDrawerSlide(drawerView, percent); + } + } + + private void updateDrawerState(View drawerView, int state) { + boolean update = false; + // Update state + if (drawerView == mLeftDrawer) { + update = mLeftState != state; + mLeftState = state; + } else if (drawerView == mRightDrawer) { + update = mRightState != state; + mRightState = state; + } + + // Callback + if (update && mListener != null) { + mListener.onDrawerStateChanged(drawerView, state); + } + } + + private void dispatchOnDrawerClosed(View drawerView) { + boolean update = false; + // Update + if (drawerView == mLeftDrawer) { + update = mLeftOpened; + mLeftOpened = false; + } else if (drawerView == mRightDrawer) { + update = mRightOpened; + mRightOpened = false; + } + + // Callback + if (update && mListener != null) { + mListener.onDrawerClosed(drawerView); + } + } + + private void dispatchOnDrawerOpened(View drawerView) { + boolean update = false; + // Update + if (drawerView == mLeftDrawer) { + update = !mLeftOpened; + mLeftOpened = true; + } else if (drawerView == mRightDrawer) { + update = !mRightOpened; + mRightOpened = true; + } + + // Callback + if (update && mListener != null) { + mListener.onDrawerOpened(drawerView); + } + } + + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams + ? new LayoutParams((LayoutParams) p) + : p instanceof ViewGroup.MarginLayoutParams + ? new LayoutParams((MarginLayoutParams) p) + : new LayoutParams(p); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams && super.checkLayoutParams(p); + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(getContext(), attrs); + } + + public static class LayoutParams extends ViewGroup.MarginLayoutParams { + + public int gravity = Gravity.NO_GRAVITY; + + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + + final TypedArray a = c.obtainStyledAttributes(attrs, LAYOUT_ATTRS); + this.gravity = a.getInt(0, Gravity.NO_GRAVITY); + a.recycle(); + } + + public LayoutParams(int width, int height) { + super(width, height); + } + + public LayoutParams(int width, int height, int gravity) { + this(width, height); + this.gravity = gravity; + } + + public LayoutParams(LayoutParams source) { + super(source); + this.gravity = source.gravity; + } + + public LayoutParams(ViewGroup.LayoutParams source) { + super(source); + } + + public LayoutParams(ViewGroup.MarginLayoutParams source) { + super(source); + } + } + + private class DragHelperCallback extends ViewDragHelper.Callback { + + @Override + public boolean tryCaptureView(View child, int pointerId) { + return true; + } + + @Override + public int clampViewPositionVertical(View child, int top, int dy) { + return child.getTop(); + } + + @Override + public int clampViewPositionHorizontal(View child, int left, int dx) { + + if (!mIntercepted) + return child.getLeft(); + + if (child == mContentView || child == mShadow) { + if (mLeftState == STATE_CLOSED && mRightState == STATE_CLOSED) { + if (dx > 0 && mLeftDrawer != null && mLeftLockMode == LOCK_MODE_UNLOCKED) { + slideLeftDrawer(dx); + updateDrawerState(mLeftDrawer, STATE_SLIDING); + } else if (dx < 0 && mRightDrawer != null && mRightLockMode == LOCK_MODE_UNLOCKED) { + slideRightDrawer(dx); + updateDrawerState(mRightDrawer, STATE_SLIDING); + } + } else if (mLeftState != STATE_CLOSED) { + slideLeftDrawer(dx); + updateDrawerState(mLeftDrawer, STATE_SLIDING); + } else if (mRightState != STATE_CLOSED) { + slideRightDrawer(dx); + updateDrawerState(mRightDrawer, STATE_SLIDING); + } + return child.getLeft(); + + } else if (child == mLeftDrawer) { + updateDrawerState(mLeftDrawer, STATE_SLIDING); + return child.getLeft() + getActualDxLeft(dx); + + } else if (child == mRightDrawer) { + updateDrawerState(mRightDrawer, STATE_SLIDING); + return child.getLeft() + getActualDxRight(dx); + + } else { + return child.getLeft(); + } + } + + @Override + public int getViewHorizontalDragRange(View child) { + return Integer.MAX_VALUE; + } + } + + private class ShadowView extends View { + + private final int mForm = 0x0; + private final int mTo = 0x61; + private float mPercent = 0; + + public ShadowView(Context context) { + super(context); + } + + public void setPercent(float percent) { + mPercent = percent; + + invalidate(); + } + + @Override + protected void onDraw(Canvas c) { + c.drawARGB(MathUtils.lerp(mForm, mTo, mPercent), 0, 0, 0); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/SlidingLayout.java b/app/src/main/java/com/hippo/ehviewer/widget/SlidingLayout.java new file mode 100644 index 000000000..df8f601b9 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/SlidingLayout.java @@ -0,0 +1,598 @@ +/* + * 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.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Build; +import android.support.v4.view.MotionEventCompat; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.CycleInterpolator; + +import com.hippo.ehviewer.util.AnimatorUtils; +import com.hippo.ehviewer.util.MathUtils; +import com.hippo.ehviewer.util.Ui; +import com.hippo.ehviewer.util.ViewUtils; +import com.hippo.ehviewer.util.ZInterpolator; + +public class SlidingLayout extends ViewGroup { + + private static final String TAG = SlidingLayout.class.getSimpleName(); + + /* + * There are three level, first is base level, second is reserved level, third is full screen level. + */ + + private static final int LEVEL_BASE = 0; + private static final int LEVEL_RESERVED = 1; + private static final int LEVEL_FULL_SCREEN = 2; + + private int mLevel = LEVEL_BASE; + + private View mChild; + private int mMaxWidth; + private int mSlop; + private float mShowPercent = 0.0f; + private boolean isBottom = true; + + private int mPaddingLeft; + private int mPaddingTop; + private int mPaddingRight; + private int mPaddingBottom; + + private ValueAnimator mToBaseLevelAnimate; + private ValueAnimator mToReservedLevelAnimate; + private ValueAnimator mToFullScreenLevelAnimate; + private SetYListener mToBaseLevelListener; + private SetYListener mToReservedLevelListener; + private SetYListener mToFullScreenLevelListener; + + private float mInitialMotionX; + private float mInitialMotionY; + private float mLastMotionY; + + private int mMinTop; + private int mMaxTop; + + /* + * ---------------------- mFullScreenTop + * + * ---------------------- m5Top + * + * + * ---------------------- m4Top + * + * ---------------------- mReservedTop + * + * ---------------------- m2Top + * + * + * ---------------------- m1Top + * + * ---------------------- mBaseTop + */ + + private int mBaseTop; + private int m1Top; + private int m2Top; + private int mReservedTop; + private int m4Top; + private int m5Top; + private int mFullScreenTop; + + private boolean mInHideAnimate = false; + + private OnChildHideListener mOnChildHideListener; + + private static final ZInterpolator ZINTERPOLATOR = new ZInterpolator(); + + public interface OnChildHideListener { + public void onChildHide(); + } + + + public SlidingLayout(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public SlidingLayout(Context context, AttributeSet attrs, + int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public SlidingLayout(Context context, AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + + private void init() { + mMaxWidth = Ui.dp2pix(480); + mSlop = Ui.dp2pix(0); + + mToBaseLevelAnimate = new ValueAnimator(); + mToBaseLevelListener = new SetYListener(); + mToBaseLevelAnimate.addUpdateListener(mToBaseLevelListener); + mToBaseLevelAnimate.setDuration(300); + mToBaseLevelAnimate.setInterpolator(ZINTERPOLATOR); + mToBaseLevelAnimate.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + mInHideAnimate = true; + } + + @Override + public void onAnimationEnd(Animator animation) { + mLevel = LEVEL_BASE; + if (mChild.getVisibility() != View.INVISIBLE) + mChild.setVisibility(View.INVISIBLE); + + if (mOnChildHideListener != null) + mOnChildHideListener.onChildHide(); + + mInHideAnimate = false; + } + + @Override + public void onAnimationCancel(Animator animation) { + mInHideAnimate = false; + } + + @Override + public void onAnimationRepeat(Animator animation) { + // Empty + } + }); + + mToReservedLevelAnimate = new ValueAnimator(); + mToReservedLevelListener = new SetYListener(); + mToReservedLevelAnimate.addUpdateListener(mToReservedLevelListener); + mToReservedLevelAnimate.setDuration(300); + mToReservedLevelAnimate.setInterpolator(ZINTERPOLATOR); + mToReservedLevelAnimate.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + // Empty + } + + @Override + public void onAnimationEnd(Animator animation) { + mLevel = LEVEL_RESERVED; + } + + @Override + public void onAnimationCancel(Animator animation) { + // Empty + } + + @Override + public void onAnimationRepeat(Animator animation) { + // Empty + } + }); + + mToFullScreenLevelAnimate = new ValueAnimator(); + mToFullScreenLevelListener = new SetYListener(); + mToFullScreenLevelAnimate.addUpdateListener(mToFullScreenLevelListener); + mToFullScreenLevelAnimate.setDuration(300); + mToFullScreenLevelAnimate.setInterpolator(ZINTERPOLATOR); + mToFullScreenLevelAnimate.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + // Empty + } + + @Override + public void onAnimationEnd(Animator animation) { + mLevel = LEVEL_FULL_SCREEN; + } + + @Override + public void onAnimationCancel(Animator animation) { + // Empty + } + + @Override + public void onAnimationRepeat(Animator animation) { + // Empty + } + }); + } + + private class SetYListener implements ValueAnimator.AnimatorUpdateListener { + + private boolean mFirstShow = false; + private int mStartTop; + + public void reset() { + mFirstShow = true; + mStartTop = mChild.getTop(); + } + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + int offsetY = (int) (float) (Float) animation.getAnimatedValue() - mChild.getTop(); + mChild.offsetTopAndBottom(offsetY); + updateShowPercent(); + + if (mFirstShow && Math.abs(mStartTop - mChild.getTop()) > 5) { + mFirstShow = false; + invalidate(); + } + } + } + + public void setOnChildHideListener(OnChildHideListener l) { + mOnChildHideListener = l; + } + + @Override + protected void onFinishInflate() { + final int childCount = getChildCount(); + if (childCount != 1) + throw new IllegalStateException("There must be only one child"); + + mChild = getChildAt(0); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + if (widthMode != MeasureSpec.EXACTLY || heightMode != MeasureSpec.EXACTLY) + throw new IllegalArgumentException( + "SlidingDrawerLayout must be measured with MeasureSpec.EXACTLY."); + + setMeasuredDimension(widthSize, heightSize); + + final View child = mChild; + if (child.getVisibility() != GONE) { + final int childWidthSpec = MeasureSpec.makeMeasureSpec( + Math.min(widthSize - getPaddingLeft() - getPaddingRight(), mMaxWidth), MeasureSpec.EXACTLY); + final int childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + child.measure(childWidthSpec, childHeightSpec); + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + final View child = mChild; + final int width = r - l; + final int height = b - t; + final int childWidth = child.getMeasuredWidth(); + final int childHeight = child.getMeasuredHeight(); + mPaddingLeft = getPaddingLeft(); + mPaddingTop = getPaddingTop(); + mPaddingRight = getPaddingRight(); + mPaddingBottom = getPaddingBottom(); + + // Set mReservedLength and mFullScreenLength + int heightWithoutPadding = height - mPaddingTop - mPaddingBottom; + int defaultReservedLength = Math.min(heightWithoutPadding - Ui.dp2pix(48), Ui.dp2pix(248)); + int defaultFullScreenLength = Math.max(heightWithoutPadding, defaultReservedLength); + int reservedLength = Math.min(defaultReservedLength, childHeight); + int fullScreenLength = Math.min(defaultFullScreenLength, childHeight); + + int showHeight = MathUtils.lerp(0, childHeight, mShowPercent); + + int childLeft = mPaddingLeft + (width - mPaddingLeft - mPaddingRight - childWidth) / 2; + + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; + final int maxGap = Ui.dp2pix(32); + final int brGap = Math.min(Math.abs((mBaseTop - mReservedTop) / 3), maxGap); + final int rfGap = Math.min(Math.abs((mReservedTop - mFullScreenTop) / 3), maxGap); + switch (vgrav) { + case Gravity.TOP: { + isBottom = false; + mMinTop = mPaddingTop; + mMaxTop = mMinTop + childHeight; + mBaseTop = mMinTop; + mReservedTop = mMinTop + reservedLength; + mFullScreenTop = mMinTop + fullScreenLength; + + m1Top = mBaseTop + brGap; + m2Top = mReservedTop - brGap; + m4Top = mReservedTop + rfGap; + m5Top = mFullScreenTop - rfGap; + + int childBottom = mMinTop + showHeight; + child.layout(childLeft, childBottom - childHeight, childLeft + childWidth, childBottom); + break; + } + default: + case Gravity.BOTTOM: { + isBottom = true; + mMaxTop = height - mPaddingBottom; + mMinTop = mMaxTop - childHeight; + mBaseTop = mMaxTop; + mReservedTop = mMaxTop - reservedLength; + mFullScreenTop = mMaxTop - fullScreenLength; + + m1Top = mBaseTop - brGap; + m2Top = mReservedTop + brGap; + m4Top = mReservedTop - rfGap; + m5Top = mFullScreenTop + rfGap; + + int childTop = mMaxTop - showHeight; + child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); + break; + } + } + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + boolean getTouchEvent = false; + final int action = MotionEventCompat.getActionMasked(ev); + final float x = ev.getX(); + final float y = ev.getY(); + final boolean isChildUnder = ViewUtils.isViewUnder(mChild, (int) x, (int) y); + + switch (action) { + case MotionEvent.ACTION_DOWN: + mInitialMotionX = x; + mInitialMotionY = y; + mLastMotionY = mInitialMotionY; + break; + case MotionEvent.ACTION_MOVE: + final float adx = Math.abs(x - mInitialMotionX); + final float ady = Math.abs(y - mInitialMotionY); + if (isChildUnder && adx > mSlop && ady > adx) + getTouchEvent = true; + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + break; + } + + return getTouchEvent; + } + + private void updateShowPercent() { + int top = mChild.getTop(); + mShowPercent = (isBottom ? (mMaxTop - top) : (top - mMinTop)) / + (float) mChild.getHeight(); + } + + private void cancelAnimator() { + if (mToBaseLevelAnimate.isRunning()) + mToBaseLevelAnimate.cancel(); + if (mToReservedLevelAnimate.isRunning()) + mToReservedLevelAnimate.cancel(); + if (mToFullScreenLevelAnimate.isRunning()) + mToFullScreenLevelAnimate.cancel(); + } + + @Override + @SuppressLint("ClickableViewAccessibility") + public boolean onTouchEvent(MotionEvent ev) { + final int action = MotionEventCompat.getActionMasked(ev); + final float x = ev.getX(); + final float y = ev.getY(); + boolean getTouchEvent = false; + + cancelAnimator(); + + int top; + switch (action) { + case MotionEvent.ACTION_DOWN: + mInitialMotionX = x; + mInitialMotionY = y; + mLastMotionY = mInitialMotionY; + if (isShowing()) + getTouchEvent = true; + break; + case MotionEvent.ACTION_MOVE: + final float dy = y - mLastMotionY; + mLastMotionY = y; + getTouchEvent = true; + + top = mChild.getTop(); + int newTop = top + (int) dy; + newTop = MathUtils.clamp(newTop, mMinTop, mMaxTop); + mChild.offsetTopAndBottom(newTop - top); + updateShowPercent(); + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + getTouchEvent = true; + + top = mChild.getTop(); + if (isBottom) { + switch (mLevel) { + default: + case LEVEL_BASE: + if (top < mFullScreenTop) + ; + else if (top < m4Top) + toFullScreenLevel(); + else if (top < m1Top) + toReservedLevel(false); + else + toBaseLevel(); + break; + case LEVEL_RESERVED: + if (top < mFullScreenTop) + ; + else if (top < m4Top) + toFullScreenLevel(); + else if (top < m2Top) + toReservedLevel(false); + else + toBaseLevel(); + break; + case LEVEL_FULL_SCREEN: + if (top < mFullScreenTop) + ; + else if (top < m5Top) + toFullScreenLevel(); + else if (top < m2Top) + toReservedLevel(false); + else + toBaseLevel(); + break; + } + } else { + // TODO + } + break; + } + return getTouchEvent; + } + + public void hide() { + if (mChild.getVisibility() != View.INVISIBLE) + mChild.setVisibility(View.INVISIBLE); + } + + public boolean isShowing() { + return mChild.getVisibility() == View.VISIBLE; + } + + public boolean isInHideAnimate() { + return mInHideAnimate; + } + + public void toReservedLevel(boolean withShake) { + cancelAnimator(); + // Show child + if (mChild.getVisibility() != View.VISIBLE) + mChild.setVisibility(View.VISIBLE); + + mToReservedLevelListener.reset(); + mToReservedLevelAnimate.setFloatValues(mChild.getTop(), mReservedTop); + + if (withShake) { + AnimatorSet bouncer = new AnimatorSet(); + Animator delayAnimator = AnimatorUtils.createDelayAnimator(200); + ValueAnimator shakeAnimator = ValueAnimator.ofFloat(mReservedTop, + isBottom ? mReservedTop - Ui.dp2pix(8) : mReservedTop + Ui.dp2pix(8)); + shakeAnimator.setDuration(500); + shakeAnimator.setInterpolator(new CycleInterpolator(1)); + shakeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + int offsetY = (int) (float) (Float) animation.getAnimatedValue() - mChild.getTop(); + mChild.offsetTopAndBottom(offsetY); + updateShowPercent(); + } + }); + bouncer.play(mToReservedLevelAnimate).before(delayAnimator); + bouncer.play(delayAnimator).before(shakeAnimator); + bouncer.start(); + + } else { + mToReservedLevelAnimate.start(); + } + } + + public void toBaseLevel() { + cancelAnimator(); + // Show child + if (mChild.getVisibility() != View.VISIBLE) + mChild.setVisibility(View.VISIBLE); + + mToBaseLevelListener.reset(); + mToBaseLevelAnimate.setFloatValues(mChild.getTop(), mBaseTop); + mToBaseLevelAnimate.start(); + } + + public void toFullScreenLevel() { + cancelAnimator(); + // Show child + if (mChild.getVisibility() != View.VISIBLE) + mChild.setVisibility(View.VISIBLE); + + mToFullScreenLevelListener.reset(); + mToFullScreenLevelAnimate.setFloatValues(mChild.getTop(), mFullScreenTop); + mToFullScreenLevelAnimate.start(); + } + + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams + ? new LayoutParams((LayoutParams) p) + : new LayoutParams(p); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams && super.checkLayoutParams(p); + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(getContext(), attrs); + } + + public static class LayoutParams extends ViewGroup.LayoutParams { + + private static final int[] LAYOUT_ATTRS = new int[] { + android.R.attr.layout_gravity + }; + + public int gravity = Gravity.BOTTOM; + + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + + final TypedArray a = c.obtainStyledAttributes(attrs, LAYOUT_ATTRS); + this.gravity = a.getInt(0, Gravity.BOTTOM); + a.recycle(); + } + + public LayoutParams(int width, int height) { + super(width, height); + } + + public LayoutParams(int width, int height, int gravity) { + this(width, height); + this.gravity = gravity; + } + + public LayoutParams(LayoutParams source) { + super(source); + gravity = source.gravity; + } + + public LayoutParams(ViewGroup.LayoutParams source) { + super(source); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/SuggestionHelper.java b/app/src/main/java/com/hippo/ehviewer/widget/SuggestionHelper.java new file mode 100644 index 000000000..0b4bac549 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/SuggestionHelper.java @@ -0,0 +1,90 @@ +/* + * 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 java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.SearchRecentSuggestions; + +public class SuggestionHelper extends SearchRecentSuggestions { + + private static final String QUERY = "query"; + private static final String ORDER_BY = "date DESC"; + + private static final int MAX_HISTORY_COUNT = 250; + + private final List mQueryList = new ArrayList(); + + private static Map sInstances = new HashMap(); + + public static SuggestionHelper getInstance(Context context, String authority, int mode) { + SuggestionHelper s = sInstances.get(authority); + if (s == null) { + s = new SuggestionHelper(context, authority, mode); + sInstances.put(authority, s); + } + return s; + } + + private SuggestionHelper(Context context, String authority, int mode) { + super(context, authority, mode); + + Uri suggestionsUri = Uri.parse("content://" + authority + "/suggestions"); + + // Get all suggestion + ContentResolver cr = context.getContentResolver(); + Cursor cursor = cr.query(suggestionsUri, new String[]{QUERY}, null, null, ORDER_BY); + while(cursor.moveToNext()) + mQueryList.add(cursor.getString(cursor.getColumnIndex(QUERY))); + cursor.close(); + } + + public List getQueries() { + return mQueryList; + } + + @Override + public void saveRecentQuery(final String queryString, final String line2) { + super.saveRecentQuery(queryString, line2); + if (!mQueryList.contains(queryString)) + mQueryList.add(0, queryString); + truncateHistory(MAX_HISTORY_COUNT); + } + + @Override + public void clearHistory() { + super.clearHistory(); + truncateHistory(0); + } + + private void truncateHistory(int maxEntries) { + if (maxEntries == 0) { + mQueryList.clear(); + } else { + for (int i = mQueryList.size() - 1; i >= maxEntries; i--) { + mQueryList.remove(i); + } + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/SuggestionTextView.java b/app/src/main/java/com/hippo/ehviewer/widget/SuggestionTextView.java new file mode 100644 index 000000000..38ebc01c6 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/SuggestionTextView.java @@ -0,0 +1,124 @@ +/* + * 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 java.util.List; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.inputmethod.InputMethodManager; +import android.widget.ArrayAdapter; +import android.widget.AutoCompleteTextView; + +import com.hippo.ehviewer.util.ViewUtils; + +public class SuggestionTextView extends PrefixEditText { + + private static final String TAG = SuggestionTextView.class.getSimpleName(); + + private List mAllSuggestions; + private ArrayAdapter mAdapte; + + private boolean mKeyboardIsShowing = true; + + public SuggestionTextView(Context context) { + super(context); + } + + public SuggestionTextView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public SuggestionTextView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + /** + * We override this method to avoid an extra onItemClick being called on + * the drop-down's OnItemClickListener by + * {@link AutoCompleteTextView#onKeyUp(int, KeyEvent)} when an item is + * clicked with the trackball. + */ + @Override + public void performCompletion() { + } + + /** + * We override this method so that we can allow a threshold of zero, + * which ACTV does not. + */ + @Override + public boolean enoughToFilter() { + return true; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (!isPopupShowing()) + showDropDown(); + + return super.onTouchEvent(event); + } + + @Override + public boolean onKeyPreIme(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + // special case for the back key, we do not even try to send it + // to the drop down list but instead, consume it immediately + if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { + KeyEvent.DispatcherState state = getKeyDispatcherState(); + if (state != null) { + state.startTracking(event, this); + } + return true; + } else if (event.getAction() == KeyEvent.ACTION_UP) { + KeyEvent.DispatcherState state = getKeyDispatcherState(); + if (state != null) { + state.handleUpEvent(event); + } + if (event.isTracking() && !event.isCanceled()) { + InputMethodManager imm = (InputMethodManager) + getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null && mKeyboardIsShowing) { + imm.hideSoftInputFromWindow(getWindowToken(), 0); + mKeyboardIsShowing = false; + } else { + clearFocus(); + mKeyboardIsShowing = true; + } + return true; + } + } + } + return super.onKeyPreIme(keyCode, event); + } + + public void setSuggestionHelper(SuggestionHelper helper) { + if (helper != null) { + mAllSuggestions = helper.getQueries(); + mAdapte = new ArrayAdapter(ViewUtils.sContextThemeWrapper, + android.R.layout.simple_dropdown_item_1line, mAllSuggestions); + setAdapter(mAdapte); + } else { + mAllSuggestions = null; + mAdapte = null; + setAdapter(null); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/SuperButton.java b/app/src/main/java/com/hippo/ehviewer/widget/SuperButton.java new file mode 100644 index 000000000..b88ce0463 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/SuperButton.java @@ -0,0 +1,119 @@ +/* + * 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.content.res.TypedArray; +import android.util.AttributeSet; +import android.widget.Button; + +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.cardview.CardViewSalon; +import com.hippo.ehviewer.tile.TileSalon; +import com.hippo.ehviewer.util.Theme; + +public class SuperButton extends Button { + + public static final int BACKGROUND_TYPE_ROUND = 0x0; + public static final int BACKGROUND_TYPE_TILE = 0x1; + + public SuperButton(Context context) { + this(context, null); + init(context, null, 0); + } + + public SuperButton(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs, 0); + } + + public SuperButton(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context, attrs, defStyle); + } + + public void init(Context context, AttributeSet attrs, int defStyle) { + int backgroundType = BACKGROUND_TYPE_ROUND; + int bgColor = context.getResources().getColor(R.color.background_light); + int boundColor = 0; + boolean withShadow = true; + + if (attrs != null) { + TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.SuperButton, defStyle, 0); + backgroundType = a.getInt(R.styleable.SuperButton_backgroundType, backgroundType); + bgColor = a.getColor(R.styleable.SuperButton_backgroundColor, bgColor); + boundColor = a.getColor(R.styleable.SuperButton_boundColor, boundColor); + withShadow = a.getBoolean(R.styleable.SuperButton_withShadow, withShadow); + a.recycle(); + } + + switch (backgroundType) { + case BACKGROUND_TYPE_ROUND: + setRoundBackground(withShadow, bgColor, boundColor); + break; + case BACKGROUND_TYPE_TILE: + setTileBackground(bgColor); + break; + } + + } + + private void setStatefulRoundBackground(boolean withShadow, + int[][] stateSets, int[] bgColors, int[] boundColors) { + if (withShadow) { + CardViewSalon.reformWithShadow(this, stateSets, bgColors, boundColors, true); + } else { + CardViewSalon.reform(this, stateSets, bgColors, boundColors); + } + } + + private void setNotStatefulRoundBackground(boolean withShadow, + int bgColors, int boundColors) { + if (withShadow) { + CardViewSalon.reformWithShadow(this, bgColors, boundColors, true); + } else { + CardViewSalon.reform(this, bgColors, boundColors); + } + } + + public void setRoundBackground(boolean withShadow, int bgColor, int boundColor) { + setRoundBackground(withShadow, true, bgColor, boundColor); + } + + public void setRoundBackground(boolean withShadow, boolean stateful, int bgColor, int boundColor) { + if (stateful) + setStatefulRoundBackground(withShadow, new int[][]{ + new int[]{-android.R.attr.state_enabled}, + new int[]{android.R.attr.state_enabled, android.R.attr.state_pressed}, + new int[]{}}, new int[]{0xffcccccc, Theme.getDarkerColor(bgColor), bgColor}, + boundColor == 0 ? null : new int[]{0xffcccccc, Theme.getDarkerColor(boundColor), boundColor}); // TODO + else + setNotStatefulRoundBackground(withShadow, bgColor, boundColor); + } + + private void setTileBackground(int[][] stateSets, int[] bgColors) { + TileSalon.reform(this, stateSets, bgColors); + } + + public void setTileBackground(int bgColor) { + setTileBackground(new int[][]{ + new int[]{-android.R.attr.state_enabled}, + new int[]{android.R.attr.state_enabled, android.R.attr.state_pressed}, + new int[]{}}, new int[]{0xffcccccc, Theme.getDarkerColor(bgColor), bgColor}); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/SuperSwipeRefreshLayout.java b/app/src/main/java/com/hippo/ehviewer/widget/SuperSwipeRefreshLayout.java new file mode 100644 index 000000000..ba8966014 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/SuperSwipeRefreshLayout.java @@ -0,0 +1,501 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * 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.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.Animation; +import android.view.animation.Animation.AnimationListener; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Transformation; +import android.widget.AbsListView; + +public class SuperSwipeRefreshLayout extends ViewGroup { + private static final long RETURN_TO_ORIGINAL_POSITION_TIMEOUT = 300; + private static final float ACCELERATE_INTERPOLATION_FACTOR = 1.5f; + private static final float DECELERATE_INTERPOLATION_FACTOR = 2f; + private static final float PROGRESS_BAR_HEIGHT = 4; + private static final float MAX_SWIPE_DISTANCE_FACTOR = .6f; + private static final int REFRESH_TRIGGER_DISTANCE = 120; + + private final SwipeProgressBar mProgressBar; //the thing that shows progress is going + private boolean mIsProgressBarVisible = true; + private boolean mIsAgainstToChildPadding = false; + private View mTarget; //the content that gets pulled down + private int mOriginalOffsetTop; + private OnRefreshListener mListener; + private MotionEvent mDownEvent; + private int mFrom; + private boolean mRefreshing = false; + private final int mTouchSlop; + private float mDistanceToTriggerSync = -1; + private float mPrevY; + private final int mMediumAnimationDuration; + private float mFromPercentage = 0; + private float mCurrPercentage = 0; + private final int mProgressBarHeight; + private int mCurrentTargetOffsetTop; + // Target is returning to its start offset because it was cancelled or a + // refresh was triggered. + private boolean mReturningToStart; + private final DecelerateInterpolator mDecelerateInterpolator; + private final AccelerateInterpolator mAccelerateInterpolator; + private static final int[] LAYOUT_ATTRS = new int[] { + android.R.attr.enabled + }; + + private final Animation mAnimateToStartPosition = new Animation() { + @Override + public void applyTransformation(float interpolatedTime, Transformation t) { + int targetTop = 0; + if (mFrom != mOriginalOffsetTop) { + targetTop = (mFrom + (int)((mOriginalOffsetTop - mFrom) * interpolatedTime)); + } + int offset = targetTop - mTarget.getTop(); + final int currentTop = mTarget.getTop(); + if (offset + currentTop < 0) { + offset = 0 - currentTop; + } + setTargetOffsetTopAndBottom(offset); + } + }; + + private final Animation mShrinkTrigger = new Animation() { + @Override + public void applyTransformation(float interpolatedTime, Transformation t) { + float percent = mFromPercentage + ((0 - mFromPercentage) * interpolatedTime); + mProgressBar.setTriggerPercentage(percent); + } + }; + + private final AnimationListener mReturnToStartPositionListener = new BaseAnimationListener() { + @Override + public void onAnimationEnd(Animation animation) { + // Once the target content has returned to its start position, reset + // the target offset to 0 + mCurrentTargetOffsetTop = 0; + } + }; + + private final AnimationListener mShrinkAnimationListener = new BaseAnimationListener() { + @Override + public void onAnimationEnd(Animation animation) { + mCurrPercentage = 0; + } + }; + + private final Runnable mReturnToStartPosition = new Runnable() { + + @Override + public void run() { + mReturningToStart = true; + animateOffsetToStartPosition(mCurrentTargetOffsetTop + getPaddingTop(), + mReturnToStartPositionListener); + } + + }; + + // Cancel the refresh gesture and animate everything back to its original state. + private final Runnable mCancel = new Runnable() { + + @Override + public void run() { + mReturningToStart = true; + // Timeout fired since the user last moved their finger; animate the + // trigger to 0 and put the target back at its original position + if (mProgressBar != null) { + mFromPercentage = mCurrPercentage; + mShrinkTrigger.setDuration(mMediumAnimationDuration); + mShrinkTrigger.setAnimationListener(mShrinkAnimationListener); + mShrinkTrigger.reset(); + mShrinkTrigger.setInterpolator(mDecelerateInterpolator); + startAnimation(mShrinkTrigger); + } + animateOffsetToStartPosition(mCurrentTargetOffsetTop + getPaddingTop(), + mReturnToStartPositionListener); + } + + }; + + /** + * Simple constructor to use when creating a SwipeRefreshLayout from code. + * @param context + */ + public SuperSwipeRefreshLayout(Context context) { + this(context, null); + } + + /** + * Constructor that is called when inflating SwipeRefreshLayout from XML. + * @param context + * @param attrs + */ + public SuperSwipeRefreshLayout(Context context, AttributeSet attrs) { + super(context, attrs); + + mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + + mMediumAnimationDuration = getResources().getInteger( + android.R.integer.config_mediumAnimTime); + + setWillNotDraw(false); + mProgressBar = new SwipeProgressBar(this); + final DisplayMetrics metrics = getResources().getDisplayMetrics(); + mProgressBarHeight = (int) (metrics.density * PROGRESS_BAR_HEIGHT); + mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR); + mAccelerateInterpolator = new AccelerateInterpolator(ACCELERATE_INTERPOLATION_FACTOR); + + final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS); + setEnabled(a.getBoolean(0, true)); + a.recycle(); + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + removeCallbacks(mCancel); + removeCallbacks(mReturnToStartPosition); + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + removeCallbacks(mReturnToStartPosition); + removeCallbacks(mCancel); + } + + private void animateOffsetToStartPosition(int from, AnimationListener listener) { + mFrom = from; + mAnimateToStartPosition.reset(); + mAnimateToStartPosition.setDuration(mMediumAnimationDuration); + mAnimateToStartPosition.setAnimationListener(listener); + mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator); + mTarget.startAnimation(mAnimateToStartPosition); + } + + /** + * Set the listener to be notified when a refresh is triggered via the swipe + * gesture. + */ + public void setOnRefreshListener(OnRefreshListener listener) { + mListener = listener; + } + + private void setTriggerPercentage(float percent) { + if (percent == 0f) { + // No-op. A null trigger means it's uninitialized, and setting it to zero-percent + // means we're trying to reset state, so there's nothing to reset in this case. + mCurrPercentage = 0; + return; + } + mCurrPercentage = percent; + mProgressBar.setTriggerPercentage(percent); + } + + /** + * Notify the widget that refresh state has changed. Do not call this when + * refresh is triggered by a swipe gesture. + * + * @param refreshing Whether or not the view should show refresh progress. + */ + public void setRefreshing(boolean refreshing) { + if (mRefreshing != refreshing) { + ensureTarget(); + mCurrPercentage = 0; + mRefreshing = refreshing; + if (mRefreshing) { + mProgressBar.start(); + } else { + mProgressBar.stop(); + } + } + } + + /** + * 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 colorRes1 Color resource. + * @param colorRes2 Color resource. + * @param colorRes3 Color resource. + * @param colorRes4 Color resource. + */ + public void setColorScheme(int colorRes1, int colorRes2, int colorRes3, int colorRes4) { + ensureTarget(); + final Resources res = getResources(); + final int color1 = res.getColor(colorRes1); + final int color2 = res.getColor(colorRes2); + final int color3 = res.getColor(colorRes3); + final int color4 = res.getColor(colorRes4); + mProgressBar.setColorScheme(color1, color2, color3,color4); + } + + /** + * @return Whether the SwipeRefreshWidget is actively showing refresh + * progress. + */ + public boolean isRefreshing() { + return mRefreshing; + } + + private void ensureTarget() { + // Don't bother getting the parent height if the parent hasn't been laid out yet. + if (mTarget == null) { + if (getChildCount() > 1 && !isInEditMode()) { + throw new IllegalStateException( + "SwipeRefreshLayout can host only one direct child"); + } + mTarget = getChildAt(0); + mOriginalOffsetTop = mTarget.getTop() + getPaddingTop(); + } + if (mDistanceToTriggerSync == -1) { + if (getParent() != null && ((View)getParent()).getHeight() > 0) { + final DisplayMetrics metrics = getResources().getDisplayMetrics(); + mDistanceToTriggerSync = (int) Math.min( + ((View) getParent()) .getHeight() * MAX_SWIPE_DISTANCE_FACTOR, + REFRESH_TRIGGER_DISTANCE * metrics.density); + } + } + } + + /** + * Show or hide progress bar at same time + */ + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + mIsProgressBarVisible = enabled; + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + if (mIsProgressBarVisible) + mProgressBar.draw(canvas); + } + + /** + * If set true, refresh action will be triggered when + * the first item reaches the paddingTop of AbsListView. + * + * @param againstToChildPadding + */ + public void setAgainstToChildPadding(boolean againstToChildPadding) { + if (mIsAgainstToChildPadding != againstToChildPadding) { + mIsAgainstToChildPadding = againstToChildPadding; + setProgressBarBounds(); + } + } + + private void setProgressBarBounds() { + final int width = getMeasuredWidth(); + + if (mIsAgainstToChildPadding && getChildCount() != 0) { + final View child = getChildAt(0); + mProgressBar.setBounds(0, child.getPaddingTop(), width, mProgressBarHeight + child.getPaddingTop()); + } else { + mProgressBar.setBounds(0, 0, width, mProgressBarHeight); + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + if (getChildCount() != 0) { + final int width = getMeasuredWidth(); + final int height = getMeasuredHeight(); + + final View child = getChildAt(0); + final int childLeft = getPaddingLeft(); + final int childTop = mCurrentTargetOffsetTop + getPaddingTop(); + final int childWidth = width - getPaddingLeft() - getPaddingRight(); + final int childHeight = height - getPaddingTop() - getPaddingBottom(); + child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); + } + + setProgressBarBounds(); + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + if (getChildCount() > 1 && !isInEditMode()) { + throw new IllegalStateException("SwipeRefreshLayout can host only one direct child"); + } + if (getChildCount() > 0) { + getChildAt(0).measure( + MeasureSpec.makeMeasureSpec( + getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), + MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec( + getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), + MeasureSpec.EXACTLY)); + } + } + + /** + * @return Whether it is possible for the child view of this layout to + * scroll up. Override this if the child view is a custom view. + */ + public boolean canChildScrollUp() { + if (mTarget instanceof AbsListView) { + final AbsListView absListView = (AbsListView) mTarget; + return absListView.getChildCount() > 0 + && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0) + .getTop() < absListView.getPaddingTop()); + } else { + return mTarget.getScrollY() > 0; + } + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + ensureTarget(); + boolean handled = false; + if (mReturningToStart && ev.getAction() == MotionEvent.ACTION_DOWN) { + mReturningToStart = false; + } + if (isEnabled() && !mReturningToStart && !canChildScrollUp()) { + handled = onTouchEvent(ev); + } + return !handled ? super.onInterceptTouchEvent(ev) : handled; + } + + @Override + public void requestDisallowInterceptTouchEvent(boolean b) { + // Nope. + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + final int action = event.getAction(); + boolean handled = false; + switch (action) { + case MotionEvent.ACTION_DOWN: + mCurrPercentage = 0; + mDownEvent = MotionEvent.obtain(event); + mPrevY = mDownEvent.getY(); + break; + case MotionEvent.ACTION_MOVE: + if (mDownEvent != null && !mReturningToStart) { + final float eventY = event.getY(); + float yDiff = eventY - mDownEvent.getY(); + if (yDiff > mTouchSlop) { + // User velocity passed min velocity; trigger a refresh + if (yDiff > mDistanceToTriggerSync) { + // User movement passed distance; trigger a refresh + startRefresh(); + handled = true; + break; + } else { + // Just track the user's movement + setTriggerPercentage( + mAccelerateInterpolator.getInterpolation( + yDiff / mDistanceToTriggerSync)); + float offsetTop = yDiff; + if (mPrevY > eventY) { + offsetTop = yDiff - mTouchSlop; + } + updateContentOffsetTop((int) (offsetTop)); + if (mPrevY > eventY && (mTarget.getTop() < mTouchSlop)) { + // If the user puts the view back at the top, we + // don't need to. This shouldn't be considered + // cancelling the gesture as the user can restart from the top. + removeCallbacks(mCancel); + } else { + updatePositionTimeout(); + } + mPrevY = event.getY(); + handled = true; + } + } + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + if (mDownEvent != null) { + mDownEvent.recycle(); + mDownEvent = null; + } + break; + } + return handled; + } + + private void startRefresh() { + removeCallbacks(mCancel); + mReturnToStartPosition.run(); + setRefreshing(true); + mListener.onHeaderRefresh(); + } + + private void updateContentOffsetTop(int targetTop) { + final int currentTop = mTarget.getTop(); + if (targetTop > mDistanceToTriggerSync) { + targetTop = (int) mDistanceToTriggerSync; + } else if (targetTop < 0) { + targetTop = 0; + } + setTargetOffsetTopAndBottom(targetTop - currentTop); + } + + private void setTargetOffsetTopAndBottom(int offset) { + mTarget.offsetTopAndBottom(offset); + mCurrentTargetOffsetTop = mTarget.getTop(); + } + + private void updatePositionTimeout() { + removeCallbacks(mCancel); + postDelayed(mCancel, RETURN_TO_ORIGINAL_POSITION_TIMEOUT); + } + + /** + * Classes that wish to be notified when the swipe gesture correctly + * triggers a refresh should implement this interface. + */ + public interface OnRefreshListener { + public void onHeaderRefresh(); + } + + /** + * Simple AnimationListener to avoid having to implement unneeded methods in + * AnimationListeners. + */ + private class BaseAnimationListener implements AnimationListener { + @Override + public void onAnimationStart(Animation animation) { + } + + @Override + public void onAnimationEnd(Animation animation) { + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/SwipeProgressBar.java b/app/src/main/java/com/hippo/ehviewer/widget/SwipeProgressBar.java new file mode 100644 index 000000000..221f2e1e2 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/SwipeProgressBar.java @@ -0,0 +1,337 @@ +/* + * 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; + +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 final View mParent; + + private final 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); + } + + /** + * 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 = mBounds.top + 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); + } else { + // Otherwise if we're in the middle of a trigger, draw that. + if (mTriggerPercentage > 0 && mTriggerPercentage <= 1.0) { + drawTrigger(canvas, cx, cy); + } + } + canvas.restoreToCount(restoreCount); + } + + private void drawTrigger(Canvas canvas, int cx, int cy) { + mPaint.setColor(mColor1); + canvas.drawCircle(cx, cy, cx * mTriggerPercentage, mPaint); + } + + /** + * Draws a circle centered in the view. + * + * @param canvas the canvas to draw on + * @param cx the center x coordinate + * @param cy the center y coordinate + * @param color the color to draw + * @param pct the percentage of the view that the circle should cover + */ + private void drawCircle(Canvas canvas, float cx, float cy, int color, float pct) { + mPaint.setColor(color); + canvas.save(); + canvas.translate(cx, cy); + float radiusScale = INTERPOLATOR.getInterpolation(pct); + canvas.scale(radiusScale, radiusScale); + canvas.drawCircle(0, 0, cx, mPaint); + canvas.restore(); + } + + /** + * Set the drawing bounds of this SwipeProgressBar. + */ + void setBounds(int left, int top, int right, int bottom) { + mBounds.left = left; + mBounds.top = top; + mBounds.right = right; + mBounds.bottom = bottom; + } + + /** + * A pre-baked bezier-curved interpolator for indeterminate progress animations. + */ + final static 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]); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hippo/ehviewer/widget/TagListView.java b/app/src/main/java/com/hippo/ehviewer/widget/TagListView.java new file mode 100644 index 000000000..faa272b12 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/TagListView.java @@ -0,0 +1,833 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * 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 java.util.ArrayList; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.TypeEvaluator; +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.os.Build; +import android.support.v4.util.LongSparseArray; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewTreeObserver; +import android.widget.AbsListView; +import android.widget.AdapterView; +import android.widget.ListView; + +import com.hippo.ehviewer.util.ViewUtils; + +/** + * The dynamic listview is an extension of listview that supports cell dragging + * and swapping. + * + * This layout is in charge of positioning the hover cell in the correct location + * on the screen in response to user touch events. It uses the position of the + * hover cell to determine when two cells should be swapped. If two cells should + * be swapped, all the corresponding data set and layout changes are handled here. + * + * If no cell is selected, all the touch events are passed down to the listview + * and behave normally. If one of the items in the listview experiences a + * long press event, the contents of its current visible state are captured as + * a bitmap and its visibility is set to INVISIBLE. A hover cell is then created and + * added to this layout as an overlaying BitmapDrawable above the listview. Once the + * hover cell is translated some distance to signify an item swap, a data set change + * accompanied by animation takes place. When the user releases the hover cell, + * it animates into its corresponding position in the listview. + * + * When the hover cell is either above or below the bounds of the listview, this + * listview also scrolls on its own so as to reveal additional content. + */ +public class TagListView extends ListView { + + @SuppressWarnings("unused") + private static final String TAG = TagListView.class.getSimpleName(); + + private final int SMOOTH_SCROLL_AMOUNT_AT_EDGE = 15; + private final int MOVE_DURATION = 150; + private final int SENSITY = 5; + + //public ArrayList mItemList; + + private int mLastEventX = -1; + private int mLastEventY = -1; + + private int mDownY = -1; + private int mDownX = -1; + + private int mTotalOffset = 0; + + private boolean mCellIsMobile = false; + private boolean mIsMobileScrolling = false; + private int mSmoothScrollAmountAtEdge = 0; + + private final int INVALID_ID = -1; + private long mAboveItemId = INVALID_ID; + private long mMobileItemId = INVALID_ID; + private long mBelowItemId = INVALID_ID; + + private int mStableItemCount = 0; + + private BitmapDrawable mHoverCell; + private BitmapDrawable mDeleteCell; + private BitmapDrawable mModifyCell; + private Rect mHoverCellCurrentBounds; + private Rect mHoverCellOriginalBounds; + + private boolean mIsDelete = false; + private boolean mIsModify = false; + + private final int INVALID_POINTER_ID = -1; + private int mActivePointerId = INVALID_POINTER_ID; + + private boolean mIsWaitingForScrollFinish = false; + private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE; + + private OnModifyListener mModifylistener = null; + private OnMoveLister mMovelistener = null; + private OnDeleteListener mOnDeleteListener = null; + private OnSwapListener mOnSwapListener = null; + + public TagListView(Context context) { + super(context); + init(context); + } + + public TagListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context); + } + + public TagListView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public void init(Context context) { + setOnItemLongClickListener(mOnItemLongClickListener); + setOnScrollListener(mScrollListener); + DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + mSmoothScrollAmountAtEdge = (int)(SMOOTH_SCROLL_AMOUNT_AT_EDGE / metrics.density); + } + + /** + * Listens for long clicks on any items in the listview. When a cell has + * been selected, the hover cell is created and set up. + */ + private final AdapterView.OnItemLongClickListener mOnItemLongClickListener = + new AdapterView.OnItemLongClickListener() { + @Override + public boolean onItemLongClick(AdapterView arg0, View arg1, int pos, long id) { + if (pos < mStableItemCount) + return true; + + if (mMovelistener != null) + mMovelistener.onMoveStart(); + + mTotalOffset = 0; + + int position = pointToPosition(mDownX, mDownY); + int itemNum = position - getFirstVisiblePosition(); + + View selectedView = getChildAt(itemNum); + mMobileItemId = getAdapter().getItemId(position); + mHoverCell = getAndAddHoverView(selectedView); + mDeleteCell = getDeleteView(selectedView); + mModifyCell = getModifyView(selectedView); + selectedView.setVisibility(INVISIBLE); + + mCellIsMobile = true; + + updateNeighborViewsForID(mMobileItemId); + + return true; + } + }; + + /** + * Creates the hover cell with the appropriate bitmap and of appropriate + * size. The hover cell's BitmapDrawable is drawn on top of the bitmap every + * single time an invalidate call is made. + */ + private BitmapDrawable getAndAddHoverView(View v) { + + int w = v.getWidth(); + int h = v.getHeight(); + int top = v.getTop(); + int left = v.getLeft(); + + Bitmap b = getHoverBitmap(v); + + BitmapDrawable drawable = new BitmapDrawable(getResources(), b); + + mHoverCellOriginalBounds = new Rect(left, top, left + w, top + h); + mHoverCellCurrentBounds = new Rect(mHoverCellOriginalBounds); + + drawable.setBounds(mHoverCellCurrentBounds); + + return drawable; + } + + private BitmapDrawable getDeleteView(View v) { + Bitmap b = getDeleteBitmap(v); + BitmapDrawable drawable = new BitmapDrawable(getResources(), b); + drawable.setBounds(mHoverCellCurrentBounds); + + return drawable; + } + + private Bitmap getDeleteBitmap(View v) { + Bitmap bitmap = ViewUtils.getBitmapFromView(v); + Canvas can = new Canvas(bitmap); + + Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); + Paint paint = new Paint(); + paint.setColor(0x8fff0000); + can.drawRect(rect, paint); + + return bitmap; + } + + private BitmapDrawable getModifyView(View v) { + Bitmap b = getModifyBitmap(v); + BitmapDrawable drawable = new BitmapDrawable(getResources(), b); + drawable.setBounds(mHoverCellCurrentBounds); + + return drawable; + } + + private Bitmap getModifyBitmap(View v) { + Bitmap bitmap = ViewUtils.getBitmapFromView(v); + Canvas can = new Canvas(bitmap); + + Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); + Paint paint = new Paint(); + paint.setColor(0x8faa00ff); + can.drawRect(rect, paint); + + return bitmap; + } + + /** Draws a black border over the screenshot of the view passed in. */ + private Bitmap getHoverBitmap(View v) { + Bitmap bitmap = ViewUtils.getBitmapFromView(v); + Canvas can = new Canvas(bitmap); + + Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); + Paint paint = new Paint(); + paint.setColor(0x8f00aadd); + can.drawRect(rect, paint); + + return bitmap; + } + + /** + * Stores a reference to the views above and below the item currently + * corresponding to the hover cell. It is important to note that if this + * item is either at the top or bottom of the list, mAboveItemId or mBelowItemId + * may be invalid. + */ + private void updateNeighborViewsForID(long itemID) { + int position = getPositionForID(itemID); + TagsAdapter adapter = ((TagsAdapter)getAdapter()); + mAboveItemId = adapter.getItemId(position - 1); + mBelowItemId = adapter.getItemId(position + 1); + } + + /** Retrieves the view in the list corresponding to itemID */ + public View getViewForID (long itemID) { + int firstVisiblePosition = getFirstVisiblePosition(); + TagsAdapter adapter = ((TagsAdapter)getAdapter()); + for(int i = 0; i < getChildCount(); i++) { + View v = getChildAt(i); + int position = firstVisiblePosition + i; + long id = adapter.getItemId(position); + if (id == itemID) { + return v; + } + } + return null; + } + + /** Retrieves the position in the list corresponding to itemID */ + public int getPositionForID (long itemID) { + View v = getViewForID(itemID); + if (v == null) { + return -1; + } else { + return getPositionForView(v); + } + } + + public void setStableItemCount(int stableItemCount) { + mStableItemCount = stableItemCount; + } + + public interface OnModifyListener { + public void onModify(int position); + } + + public void setOnModifyListener(OnModifyListener listener) { + this.mModifylistener = listener; + } + + public interface OnMoveLister { + public void onMoveStart(); + public void onMoveOver(); + } + + public void setOnMoveLister(OnMoveLister listener) { + this.mMovelistener = listener; + } + + public interface OnDeleteListener { + public void onDelete(int position); + } + + public void setOnDeleteListener(OnDeleteListener listener) { + mOnDeleteListener = listener; + } + + public interface OnSwapListener { + public void onSwap(int positionOne, int positionTwo); + } + + public void setOnSwapListener(OnSwapListener listener) { + mOnSwapListener = listener; + } + + /** + * dispatchDraw gets invoked when all the child views are about to be drawn. + * By overriding this method, the hover cell (BitmapDrawable) can be drawn + * over the listview's items whenever the listview is redrawn. + */ + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + if (mIsDelete && mDeleteCell != null) { + mDeleteCell.setBounds(mHoverCellCurrentBounds); + mDeleteCell.draw(canvas); + } else if (mIsModify && mModifyCell != null){ + mModifyCell.setBounds(mHoverCellCurrentBounds); + mModifyCell.draw(canvas); + } else if (mHoverCell != null) { + mHoverCell.setBounds(mHoverCellCurrentBounds); + mHoverCell.draw(canvas); + } + } + + @Override + @SuppressLint("ClickableViewAccessibility") + public boolean onTouchEvent (MotionEvent event) { + switch (event.getAction() & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + mDownX = (int)event.getX(); + mDownY = (int)event.getY(); + mActivePointerId = event.getPointerId(0); + break; + case MotionEvent.ACTION_MOVE: + if (mActivePointerId == INVALID_POINTER_ID) { + break; + } + + int pointerIndex = event.findPointerIndex(mActivePointerId); + + mLastEventX = (int) event.getX(pointerIndex); + mLastEventY = (int) event.getY(pointerIndex); + int deltaX = mLastEventX - mDownX; + int deltaY = mLastEventY - mDownY; + + if (mCellIsMobile) { + if (deltaX > mHoverCellOriginalBounds.width() / SENSITY) + mIsDelete = true; + else + mIsDelete = false; + + if (deltaX < -mHoverCellOriginalBounds.width() / SENSITY) + mIsModify = true; + else + mIsModify = false; + + mHoverCellCurrentBounds.offsetTo(mHoverCellOriginalBounds.left + deltaX, + mHoverCellOriginalBounds.top + deltaY + mTotalOffset); + //mHoverCell.setBounds(mHoverCellCurrentBounds); + invalidate(); + + handleCellSwitch(); + + mIsMobileScrolling = false; + handleMobileCellScroll(); + + return false; + } + break; + case MotionEvent.ACTION_UP: + if (mMovelistener != null) + mMovelistener.onMoveOver(); + touchEventsEnded(); + break; + case MotionEvent.ACTION_CANCEL: + if (mMovelistener != null) + mMovelistener.onMoveOver(); + touchEventsCancelled(); + break; + case MotionEvent.ACTION_POINTER_UP: + /* If a multitouch event took place and the original touch dictating + * the movement of the hover cell has ended, then the dragging event + * ends and the hover cell is animated to its corresponding position + * in the listview. */ + pointerIndex = (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> + MotionEvent.ACTION_POINTER_INDEX_SHIFT; + final int pointerId = event.getPointerId(pointerIndex); + if (pointerId == mActivePointerId) { + if (mMovelistener != null) + mMovelistener.onMoveOver(); + touchEventsEnded(); + } + break; + default: + break; + } + + return super.onTouchEvent(event) | mCellIsMobile; + } + + /** + * This method determines whether the hover cell has been shifted far enough + * to invoke a cell swap. If so, then the respective cell swap candidate is + * determined and the data set is changed. Upon posting a notification of the + * data set change, a layout is invoked to place the cells in the right place. + * Using a ViewTreeObserver and a corresponding OnPreDrawListener, we can + * offset the cell being swapped to where it previously was and then animate it to + * its new position. + */ + private void handleCellSwitch() { + final int deltaY = mLastEventY - mDownY; + int deltaYTotal = mHoverCellOriginalBounds.top + mTotalOffset + deltaY; + + View belowView = getViewForID(mBelowItemId); + View mobileView = getViewForID(mMobileItemId); + View aboveView = getPositionForID(mAboveItemId) < mStableItemCount ? null : getViewForID(mAboveItemId); + + boolean isBelow = (belowView != null) && (deltaYTotal > belowView.getTop()); + boolean isAbove = (aboveView != null) && (deltaYTotal < aboveView.getTop()); + + if (isBelow || isAbove) { + + final long switchItemID = isBelow ? mBelowItemId : mAboveItemId; + View switchView = isBelow ? belowView : aboveView; + final int originalItem = getPositionForView(mobileView); + + if (switchView == null) { + updateNeighborViewsForID(mMobileItemId); + return; + } + + if (mOnSwapListener != null) { + mOnSwapListener.onSwap(originalItem, getPositionForView(switchView)); + } + + mDownY = mLastEventY; + + final int switchViewStartTop = switchView.getTop(); + + // TODO where does the different come from + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + mobileView.setVisibility(View.VISIBLE); + switchView.setVisibility(View.INVISIBLE); + } else { + mobileView.setVisibility(View.INVISIBLE); + switchView.setVisibility(View.VISIBLE); + } + + updateNeighborViewsForID(mMobileItemId); + + final ViewTreeObserver observer = getViewTreeObserver(); + observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + observer.removeOnPreDrawListener(this); + + View switchView = getViewForID(switchItemID); + + mTotalOffset += deltaY; + + int switchViewNewTop = switchView.getTop(); + int delta = switchViewStartTop - switchViewNewTop; + + switchView.setTranslationY(delta); + + ObjectAnimator animator = ObjectAnimator.ofFloat(switchView, + View.TRANSLATION_Y, 0); + animator.setDuration(MOVE_DURATION); + animator.start(); + + return true; + } + }); + } + } + + @SuppressWarnings({ "rawtypes", "unchecked", "unused" }) + private void swapElements(ArrayList arrayList, int indexOne, int indexTwo) { + Object temp = arrayList.get(indexOne); + arrayList.set(indexOne, arrayList.get(indexTwo)); + arrayList.set(indexTwo, temp); + } + + /** + * Resets all the appropriate fields to a default state while also animating + * the hover cell back to its correct location. + */ + private void touchEventsEnded () { + final View mobileView = getViewForID(mMobileItemId); + if (mCellIsMobile|| mIsWaitingForScrollFinish) { + mCellIsMobile = false; + mIsWaitingForScrollFinish = false; + mIsMobileScrolling = false; + mActivePointerId = INVALID_POINTER_ID; + + if (mIsDelete) { + animateRemoval(mobileView); + mIsDelete = false; + mAboveItemId = INVALID_ID; + mMobileItemId = INVALID_ID; + mBelowItemId = INVALID_ID; + mobileView.setVisibility(VISIBLE); + mHoverCell = null; + mDeleteCell = null; + mModifyCell = null; + invalidate(); + return; + } + + if (mIsModify) { + mIsModify = false; + if (mModifylistener != null) { + mModifylistener.onModify(getPositionForID(mMobileItemId)); + } + } + + // If the autoscroller has not completed scrolling, we need to wait for it to + // finish in order to determine the final location of where the hover cell + // should be animated to. + if (mScrollState != OnScrollListener.SCROLL_STATE_IDLE) { + mIsWaitingForScrollFinish = true; + return; + } + + mHoverCellCurrentBounds.offsetTo(mHoverCellOriginalBounds.left, mobileView.getTop()); + + ObjectAnimator hoverViewAnimator = ObjectAnimator.ofObject(mHoverCell, "bounds", + sBoundEvaluator, mHoverCellCurrentBounds); + hoverViewAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + invalidate(); + } + }); + hoverViewAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + setEnabled(false); + } + + @Override + public void onAnimationEnd(Animator animation) { + mAboveItemId = INVALID_ID; + mMobileItemId = INVALID_ID; + mBelowItemId = INVALID_ID; + mobileView.setVisibility(VISIBLE); + mHoverCell = null; + mDeleteCell = null; + mModifyCell = null; + setEnabled(true); + invalidate(); + } + }); + hoverViewAnimator.start(); + } else { + touchEventsCancelled(); + } + } + + private void animateRemoval(View viewToRemove) { + int firstVisiblePosition = getFirstVisiblePosition(); + final TagsAdapter mAdapter = (TagsAdapter)getAdapter(); + final LongSparseArray mItemIdTopMap = new LongSparseArray(); + + for (int i = 0; i < getChildCount(); ++i) { + View child = getChildAt(i); + if (child != viewToRemove) { + int position = firstVisiblePosition + i; + long itemId = mAdapter.getItemId(position); + mItemIdTopMap.put(itemId, child.getTop()); + } + } + // Delete the item from the adapter and tag + int position = getPositionForView(viewToRemove); + if (mOnDeleteListener != null) + mOnDeleteListener.onDelete(position); + + final ViewTreeObserver observer = getViewTreeObserver(); + observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + observer.removeOnPreDrawListener(this); + boolean firstAnimation = true; + int firstVisiblePosition = getFirstVisiblePosition(); + for (int i = 0; i < getChildCount(); ++i) { + final View child = getChildAt(i); + int position = firstVisiblePosition + i; + long itemId = mAdapter.getItemId(position); + Integer startTop = mItemIdTopMap.get(itemId); + int top = child.getTop(); + if (startTop != null) { + if (startTop != top) { + int delta = startTop - top; + child.setTranslationY(delta); + ObjectAnimator animator = ObjectAnimator.ofFloat(child, + View.TRANSLATION_Y, 0); + animator.setDuration(MOVE_DURATION); + animator.start(); + if (firstAnimation) { + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + setEnabled(false); + } + @Override + public void onAnimationEnd(Animator animation) { + setEnabled(true); + invalidate(); + } + }); + firstAnimation = false; + } + } + } else { + // Animate new views along with the others. The catch is that they did not + // exist in the start state, so we must calculate their starting position + // based on neighboring views. + int childHeight = child.getHeight() + getDividerHeight(); + startTop = top + (i > 0 ? childHeight : -childHeight); + int delta = startTop - top; + child.setTranslationY(delta); + + ObjectAnimator animator = ObjectAnimator.ofFloat(child, + View.TRANSLATION_Y, 0); + animator.setDuration(MOVE_DURATION); + animator.start(); + if (firstAnimation) { + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + setEnabled(false); + } + @Override + public void onAnimationEnd(Animator animation) { + setEnabled(true); + invalidate(); + } + }); + firstAnimation = false; + } + } + } + mItemIdTopMap.clear(); + return true; + } + }); + } + + + + /** + * Resets all the appropriate fields to a default state. + */ + private void touchEventsCancelled () { + View mobileView = getViewForID(mMobileItemId); + if (mCellIsMobile) { + mAboveItemId = INVALID_ID; + mMobileItemId = INVALID_ID; + mBelowItemId = INVALID_ID; + mobileView.setVisibility(VISIBLE); + mHoverCell = null; + mDeleteCell = null; + mModifyCell = null; + invalidate(); + } + mCellIsMobile = false; + mIsDelete = false; + mIsModify = false; + mIsMobileScrolling = false; + mActivePointerId = INVALID_POINTER_ID; + } + + /** + * This TypeEvaluator is used to animate the BitmapDrawable back to its + * final location when the user lifts his finger by modifying the + * BitmapDrawable's bounds. + */ + private final static TypeEvaluator sBoundEvaluator = new TypeEvaluator() { + @Override + public Rect evaluate(float fraction, Rect startValue, Rect endValue) { + return new Rect(interpolate(startValue.left, endValue.left, fraction), + interpolate(startValue.top, endValue.top, fraction), + interpolate(startValue.right, endValue.right, fraction), + interpolate(startValue.bottom, endValue.bottom, fraction)); + } + + public int interpolate(int start, int end, float fraction) { + return (int)(start + fraction * (end - start)); + } + }; + + /** + * Determines whether this listview is in a scrolling state invoked + * by the fact that the hover cell is out of the bounds of the listview; + */ + private void handleMobileCellScroll() { + mIsMobileScrolling = handleMobileCellScroll(mHoverCellCurrentBounds); + } + + /** + * This method is in charge of determining if the hover cell is above + * or below the bounds of the listview. If so, the listview does an appropriate + * upward or downward smooth scroll so as to reveal new items. + */ + public boolean handleMobileCellScroll(Rect r) { + int offset = computeVerticalScrollOffset(); + int height = getHeight(); + int extent = computeVerticalScrollExtent(); + int range = computeVerticalScrollRange(); + int hoverViewTop = r.top; + int hoverHeight = r.height(); + + if (hoverViewTop <= 0 && offset > 0) { + smoothScrollBy(-mSmoothScrollAmountAtEdge, 0); + return true; + } + + if (hoverViewTop + hoverHeight >= height && (offset + extent) < range) { + smoothScrollBy(mSmoothScrollAmountAtEdge, 0); + return true; + } + + return false; + } + + /** + * This scroll listener is added to the listview in order to handle cell swapping + * when the cell is either at the top or bottom edge of the listview. If the hover + * cell is at either edge of the listview, the listview will begin scrolling. As + * scrolling takes place, the listview continuously checks if new cells became visible + * and determines whether they are potential candidates for a cell swap. + */ + private final AbsListView.OnScrollListener mScrollListener = new AbsListView.OnScrollListener () { + + private int mPreviousFirstVisibleItem = -1; + private int mPreviousVisibleItemCount = -1; + private int mCurrentFirstVisibleItem; + private int mCurrentVisibleItemCount; + private int mCurrentScrollState; + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, + int totalItemCount) { + mCurrentFirstVisibleItem = firstVisibleItem; + mCurrentVisibleItemCount = visibleItemCount; + + mPreviousFirstVisibleItem = (mPreviousFirstVisibleItem == -1) ? mCurrentFirstVisibleItem + : mPreviousFirstVisibleItem; + mPreviousVisibleItemCount = (mPreviousVisibleItemCount == -1) ? mCurrentVisibleItemCount + : mPreviousVisibleItemCount; + + checkAndHandleFirstVisibleCellChange(); + checkAndHandleLastVisibleCellChange(); + + mPreviousFirstVisibleItem = mCurrentFirstVisibleItem; + mPreviousVisibleItemCount = mCurrentVisibleItemCount; + } + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + mCurrentScrollState = scrollState; + mScrollState = scrollState; + isScrollCompleted(); + } + + /** + * This method is in charge of invoking 1 of 2 actions. Firstly, if the listview + * is in a state of scrolling invoked by the hover cell being outside the bounds + * of the listview, then this scrolling event is continued. Secondly, if the hover + * cell has already been released, this invokes the animation for the hover cell + * to return to its correct position after the listview has entered an idle scroll + * state. + */ + private void isScrollCompleted() { + if (mCurrentVisibleItemCount > 0 && mCurrentScrollState == SCROLL_STATE_IDLE) { + if (mCellIsMobile && mIsMobileScrolling) { + handleMobileCellScroll(); + } else if (mIsWaitingForScrollFinish) { + touchEventsEnded(); + } + } + } + + /** + * Determines if the listview scrolled up enough to reveal a new cell at the + * top of the list. If so, then the appropriate parameters are updated. + */ + public void checkAndHandleFirstVisibleCellChange() { + if (mCurrentFirstVisibleItem != mPreviousFirstVisibleItem) { + if (mCellIsMobile && mMobileItemId != INVALID_ID) { + updateNeighborViewsForID(mMobileItemId); + handleCellSwitch(); + } + } + } + + /** + * Determines if the listview scrolled down enough to reveal a new cell at the + * bottom of the list. If so, then the appropriate parameters are updated. + */ + public void checkAndHandleLastVisibleCellChange() { + int currentLastVisibleItem = mCurrentFirstVisibleItem + mCurrentVisibleItemCount; + int previousLastVisibleItem = mPreviousFirstVisibleItem + mPreviousVisibleItemCount; + if (currentLastVisibleItem != previousLastVisibleItem) { + if (mCellIsMobile && mMobileItemId != INVALID_ID) { + updateNeighborViewsForID(mMobileItemId); + handleCellSwitch(); + } + } + } + }; +} \ No newline at end of file diff --git a/app/src/main/java/com/hippo/ehviewer/widget/TagsAdapter.java b/app/src/main/java/com/hippo/ehviewer/widget/TagsAdapter.java new file mode 100644 index 000000000..37fb33296 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/TagsAdapter.java @@ -0,0 +1,83 @@ +/* + * 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; + +import android.content.Context; +import android.widget.ArrayAdapter; + +import java.util.HashMap; +import java.util.List; + +public class TagsAdapter extends ArrayAdapter { + + final int INVALID_ID = -1; + int lastId = -1; + + HashMap mIdMap = new HashMap(); + + private OnDataSetChangedListener mListener; + + public TagsAdapter(Context context, int textViewResourceId, List objects) { + super(context, textViewResourceId, objects); + for (int i = 0; i < objects.size(); ++i) { + mIdMap.put(objects.get(i), ++lastId); + } + } + + @Override + public long getItemId(int position) { + if (position < 0 || position >= mIdMap.size()) { + return INVALID_ID; + } + String item = getItem(position); + return mIdMap.get(item); + } + + @Override + public boolean hasStableIds() { + return true; + } + + public void set(String oldKey, String newKey) { + mIdMap.put(newKey, mIdMap.remove(oldKey)); + } + + @Override + public void remove(String key) { + super.remove(key); + mIdMap.remove(key); + } + + public void addId(String item) { + mIdMap.put(item, ++lastId); + } + + @Override + public void notifyDataSetChanged() { + super.notifyDataSetChanged(); + if (mListener != null) + mListener.OnDataSetChanged(); + } + + public void setOnDataSetChangedListener(OnDataSetChangedListener l) { + mListener = l; + } + + public interface OnDataSetChangedListener { + public void OnDataSetChanged(); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/TextClock.java b/app/src/main/java/com/hippo/ehviewer/widget/TextClock.java new file mode 100644 index 000000000..42b41f236 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/TextClock.java @@ -0,0 +1,272 @@ +/* + * 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 java.util.Calendar; +import java.util.TimeZone; + +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Build; +import android.os.Handler; +import android.os.SystemClock; +import android.provider.Settings; +import android.text.format.DateFormat; +import android.util.AttributeSet; +import android.view.ViewDebug.ExportedProperty; +import android.widget.TextView; + +import com.hippo.ehviewer.util.DateUtils; + +public class TextClock extends TextView { + + public static final CharSequence DEFAULT_FORMAT_12_HOUR = "hh:mm a"; + public static final CharSequence DEFAULT_FORMAT_24_HOUR; + + static { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) + DEFAULT_FORMAT_24_HOUR = "kk:mm"; + else + DEFAULT_FORMAT_24_HOUR = "HH:mm"; + } + + private CharSequence mFormat12; + private CharSequence mFormat24; + + @ExportedProperty + private CharSequence mFormat; + @ExportedProperty + private boolean mHasSeconds; + + private boolean mAttached; + + private Calendar mTime; + private String mTimeZone; + + private final ContentObserver mFormatChangeObserver = new ContentObserver(new Handler()) { + @Override + public void onChange(boolean selfChange) { + chooseFormat(); + onTimeChanged(); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + chooseFormat(); + onTimeChanged(); + } + }; + + private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (mTimeZone == null && Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) { + final String timeZone = intent.getStringExtra("time-zone"); + createTime(timeZone); + } + onTimeChanged(); + } + }; + + private final Runnable mTicker = new Runnable() { + @Override + public void run() { + onTimeChanged(); + + long now = SystemClock.uptimeMillis(); + long next = now + (1000 - now % 1000); + + getHandler().postAtTime(mTicker, next); + } + }; + + public TextClock(Context context) { + this(context, null); + } + + public TextClock(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public TextClock(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + mFormat12 = DEFAULT_FORMAT_12_HOUR; + mFormat24 = DEFAULT_FORMAT_24_HOUR; + mTimeZone = TimeZone.getDefault().getID(); + + init(); + } + + private void init() { + createTime(mTimeZone); + // Wait until onAttachedToWindow() to handle the ticker + chooseFormat(false); + } + + private void createTime(String timeZone) { + if (timeZone != null) { + mTime = Calendar.getInstance(TimeZone.getTimeZone(timeZone)); + } else { + mTime = Calendar.getInstance(); + } + } + + public CharSequence getFormat12Hour() { + return mFormat12; + } + + public void setFormat12Hour(CharSequence format) { + mFormat12 = format; + + chooseFormat(); + onTimeChanged(); + } + + public CharSequence getFormat24Hour() { + return mFormat24; + } + + public void setFormat24Hour(CharSequence format) { + mFormat24 = format; + + chooseFormat(); + onTimeChanged(); + } + + public boolean is24HourModeEnabled() { + return DateFormat.is24HourFormat(getContext()); + } + + public String getTimeZone() { + return mTimeZone; + } + + public void setTimeZone(String timeZone) { + mTimeZone = timeZone; + + createTime(timeZone); + onTimeChanged(); + } + + /** + * Selects either one of {@link #getFormat12Hour()} or {@link #getFormat24Hour()} + * depending on whether the user has selected 24-hour format. + * + * Calling this method does not schedule or unschedule the time ticker. + */ + private void chooseFormat() { + chooseFormat(true); + } + + public CharSequence getFormat() { + return mFormat; + } + + /** + * Selects either one of {@link #getFormat12Hour()} or {@link #getFormat24Hour()} + * depending on whether the user has selected 24-hour format. + * + * @param handleTicker true if calling this method should schedule/unschedule the + * time ticker, false otherwise + */ + private void chooseFormat(boolean handleTicker) { + final boolean format24Requested = is24HourModeEnabled(); + + if (format24Requested) { + mFormat = mFormat24; + } else { + mFormat = mFormat12; + } + + boolean hadSeconds = mHasSeconds; + mHasSeconds = DateUtils.hasSeconds(mFormat); + + if (handleTicker && mAttached && hadSeconds != mHasSeconds) { + if (hadSeconds) getHandler().removeCallbacks(mTicker); + else mTicker.run(); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + if (!mAttached) { + mAttached = true; + + registerReceiver(); + registerObserver(); + + createTime(mTimeZone); + + if (mHasSeconds) { + mTicker.run(); + } else { + onTimeChanged(); + } + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + if (mAttached) { + unregisterReceiver(); + unregisterObserver(); + + getHandler().removeCallbacks(mTicker); + + mAttached = false; + } + } + + private void registerReceiver() { + final IntentFilter filter = new IntentFilter(); + + filter.addAction(Intent.ACTION_TIME_TICK); + filter.addAction(Intent.ACTION_TIME_CHANGED); + filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); + + getContext().registerReceiver(mIntentReceiver, filter, null, getHandler()); + } + + private void registerObserver() { + final ContentResolver resolver = getContext().getContentResolver(); + resolver.registerContentObserver(Settings.System.CONTENT_URI, true, mFormatChangeObserver); + } + + private void unregisterReceiver() { + getContext().unregisterReceiver(mIntentReceiver); + } + + private void unregisterObserver() { + final ContentResolver resolver = getContext().getContentResolver(); + resolver.unregisterContentObserver(mFormatChangeObserver); + } + + private void onTimeChanged() { + mTime.setTimeInMillis(System.currentTimeMillis()); + setText(DateFormat.format(mFormat, mTime)); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hippo/ehviewer/widget/TransitionLayout.java b/app/src/main/java/com/hippo/ehviewer/widget/TransitionLayout.java new file mode 100644 index 000000000..b86131b8d --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/TransitionLayout.java @@ -0,0 +1,87 @@ +/* + * 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.annotation.TargetApi; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.BitmapDrawable; +import android.os.Build; +import android.util.AttributeSet; +import android.widget.LinearLayout; + +import com.hippo.ehviewer.effect.DrawableTransition; + +public class TransitionLayout extends LinearLayout { + + private Bitmap mBmp; + private int mOldWidth; + private int mOldHeight; + + private static final String TAG = TransitionLayout.class.getSimpleName(); + + public TransitionLayout(Context context) { + super(context); + } + + public TransitionLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public TransitionLayout(Context context, AttributeSet attrs, + int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public TransitionLayout(Context context, AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void dispatchDraw(Canvas canvas) { + int width = getWidth(); + int height = getHeight(); + + if (mOldWidth != width || mOldHeight != height || mBmp == null) { + mOldWidth = width; + mOldHeight = height; + + if (mBmp != null && !mBmp.isRecycled()) + mBmp.recycle(); + + mBmp = Bitmap.createBitmap(getWidth(), getHeight(), + Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(mBmp); + c.translate(-getScrollX(), -getScrollY()); + super.dispatchDraw(c); + BitmapDrawable b = new BitmapDrawable(getResources(), mBmp); + DrawableTransition.transit(b, false, 3000); + setBackgroundDrawable(b); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + setBackgroundDrawable(null); + if (mBmp != null && !mBmp.isRecycled()) + mBmp.recycle(); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/recyclerview/EasyRecyclerView.java b/app/src/main/java/com/hippo/ehviewer/widget/recyclerview/EasyRecyclerView.java new file mode 100644 index 000000000..3698ddb25 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/recyclerview/EasyRecyclerView.java @@ -0,0 +1,889 @@ +/* + * 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.recyclerview; + +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build; +import android.support.v4.util.LongSparseArray; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.StaggeredGridLayoutManager; +import android.util.AttributeSet; +import android.util.SparseBooleanArray; +import android.util.TypedValue; +import android.view.ActionMode; +import android.view.InputDevice; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.Checkable; +import android.widget.ListAdapter; + +/** + * + * Get some code from twoway-view and AbsListView + * + * @author Hippo + * + */ +public class EasyRecyclerView extends RecyclerView implements View.OnClickListener, + View.OnLongClickListener { + + private static final String TAG = EasyRecyclerView.class.getSimpleName(); + + private static final String STATE_KEY_CHOICE_MODE = "choiceMode"; + private static final String STATE_KEY_CHECKED_STATES = "checkedStates"; + private static final String STATE_KEY_CHECKED_ID_STATES = "checkedIdStates"; + private static final String STATE_KEY_CHECKED_COUNT = "checkedCount"; + + /** + * Represents an invalid position. All valid positions are in the range 0 to 1 less than the + * number of items in the current adapter. + */ + public static final int INVALID_POSITION = -1; + + /** + * Normal list that does not indicate choices + */ + public static final int CHOICE_MODE_NONE = 0; + + /** + * The list allows up to one choice + */ + public static final int CHOICE_MODE_SINGLE = 1; + + /** + * The list allows multiple choices + */ + public static final int CHOICE_MODE_MULTIPLE = 2; + + /** + * The list allows multiple choices in a modal selection mode + */ + public static final int CHOICE_MODE_MULTIPLE_MODAL = 3; + + /** + * Controls if/how the user may choose/check items in the list + */ + private int mChoiceMode = CHOICE_MODE_NONE; + + /** + * Controls CHOICE_MODE_MULTIPLE_MODAL. null when inactive. + */ + private ActionMode mChoiceActionMode; + + /** + * Wrapper for the multiple choice mode callback; AbsListView needs to perform + * a few extra actions around what application code does. + */ + MultiChoiceModeWrapper mMultiChoiceModeCallback; + + /** + * Running count of how many items are currently checked + */ + private int mCheckedItemCount; + + /** + * Running state of which positions are currently checked + */ + private SparseBooleanArray mCheckStates; + + /** + * Running state of which IDs are currently checked. + * If there is a value for a given key, the checked state for that ID is true + * and the value holds the last known position in the adapter for that id. + */ + private LongSparseArray mCheckedIdStates; + + private OnItemClickListener mOnItemClickListener; + private OnItemLongClickListener mOnItemLongClickListener; + private boolean mNeedOnClickListener = false; + private boolean mNeedOnLongClickListener = false; + + private FooterAdapter mAdapter; + + private float mVerticalScrollFactor = 0; + + public EasyRecyclerView(Context context) { + super(context); + } + + public EasyRecyclerView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public EasyRecyclerView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public View getFootView() { + if (mAdapter != null) + return mAdapter.getFooterView(); + else + return null; + } + + private boolean isNeedOnClickListener() { + if (mOnItemClickListener == null && mChoiceMode == CHOICE_MODE_NONE) + return false; + else + return true; + } + + private boolean isNeedOnLongClickListener() { + if (mOnItemLongClickListener != null || mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL) + return true; + else + return false; + } + + private void setAllOnClickListener() { + final int count = getChildCount(); + final View footView = getFootView(); + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + if (child != footView) { + child.setOnClickListener(this); + } + } + } + + private void resetAllOnClickListener() { + final int count = getChildCount(); + final View footView = getFootView(); + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + if (child != footView) { + child.setOnClickListener(null); + child.setClickable(false); + } + } + } + + private void setAllOnLongClickListener() { + final int count = getChildCount(); + final View footView = getFootView(); + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + if (child != footView) { + child.setOnLongClickListener(this); + } + } + } + + private void resetAllOnLongClickListener() { + final int count = getChildCount(); + final View footView = getFootView(); + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + if (child != footView) { + child.setOnLongClickListener(null); + child.setLongClickable(false); + } + } + } + + public void updateOnClickState() { + boolean newNeedOnClickListener = isNeedOnClickListener(); + if (mNeedOnClickListener != newNeedOnClickListener) { + mNeedOnClickListener = newNeedOnClickListener; + if (mNeedOnClickListener) + setAllOnClickListener(); + else + resetAllOnClickListener(); + } + } + + public void updateOnLongClickState() { + boolean newNeedOnLongClickListener = isNeedOnLongClickListener(); + if (mNeedOnLongClickListener != newNeedOnLongClickListener) { + mNeedOnLongClickListener = newNeedOnLongClickListener; + if (mNeedOnLongClickListener) + setAllOnLongClickListener(); + else + resetAllOnLongClickListener(); + } + } + + /** + * Register a callback to be invoked when an item in the + * RecyclerView has been clicked. + * + * @param listener The callback that will be invoked. + */ + public void setOnItemClickListener(OnItemClickListener listener) { + mOnItemClickListener = listener; + updateOnClickState(); + } + + /** + * Register a callback to be invoked when an item in the + * RecyclerView has been clicked and held. + * + * @param listener The callback that will be invoked. + */ + public void setOnItemLongClickListener(OnItemLongClickListener listener) { + mOnItemLongClickListener = listener; + updateOnLongClickState(); + } + + /** + * + * adapter must be {@link com.hippo.ehviewer.widget.recyclerview.EasyRecyclerView.FooterAdapter} + */ + @Override + public void setAdapter(RecyclerView.Adapter adapter) { + super.setAdapter(adapter); + + mAdapter = (FooterAdapter) adapter; + mAdapter.mHolder = this; + if (adapter != null) { + if (mChoiceMode != CHOICE_MODE_NONE && adapter.hasStableIds() && + mCheckedIdStates == null) { + mCheckedIdStates = new LongSparseArray(); + } + } + + if (mCheckStates != null) { + mCheckStates.clear(); + } + + if (mCheckedIdStates != null) { + mCheckedIdStates.clear(); + } + } + + /** + * Returns the number of items currently selected. This will only be valid + * if the choice mode is not {@link #CHOICE_MODE_NONE} (default). + * + *

To determine the specific items that are currently selected, use one of + * the getChecked* methods. + * + * @return The number of items currently selected + * + * @see #getCheckedItemPosition() + * @see #getCheckedItemPositions() + * @see #getCheckedItemIds() + */ + public int getCheckedItemCount() { + return mCheckedItemCount; + } + + /** + * Returns the checked state of the specified position. The result is only + * valid if the choice mode has been set to {@link #CHOICE_MODE_SINGLE} + * or {@link #CHOICE_MODE_MULTIPLE}. + * + * @param position The item whose checked state to return + * @return The item's checked state or false if choice mode + * is invalid + * + * @see #setChoiceMode(int) + */ + public boolean isItemChecked(int position) { + if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) { + return mCheckStates.get(position); + } + + return false; + } + + /** + * Returns the currently checked item. The result is only valid if the choice + * mode has been set to {@link #CHOICE_MODE_SINGLE}. + * + * @return The position of the currently checked item or + * {@link #INVALID_POSITION} if nothing is selected + * + * @see #setChoiceMode(int) + */ + public int getCheckedItemPosition() { + if (mChoiceMode == CHOICE_MODE_SINGLE && mCheckStates != null && mCheckStates.size() == 1) { + return mCheckStates.keyAt(0); + } + + return INVALID_POSITION; + } + + /** + * Returns the set of checked items in the list. The result is only valid if + * the choice mode has not been set to {@link #CHOICE_MODE_NONE}. + * + * @return A SparseBooleanArray which will return true for each call to + * get(int position) where position is a checked position in the + * list and false otherwise, or null if the choice + * mode is set to {@link #CHOICE_MODE_NONE}. + */ + public SparseBooleanArray getCheckedItemPositions() { + if (mChoiceMode != CHOICE_MODE_NONE) { + return mCheckStates; + } + return null; + } + + /** + * Returns the set of checked items ids. The result is only valid if the + * choice mode has not been set to {@link #CHOICE_MODE_NONE} and the adapter + * has stable IDs. ({@link ListAdapter#hasStableIds()} == {@code true}) + * + * @return A new array which contains the id of each checked item in the + * list. + */ + public long[] getCheckedItemIds() { + if (mChoiceMode == CHOICE_MODE_NONE || mCheckedIdStates == null || mAdapter == null) { + return new long[0]; + } + + final LongSparseArray idStates = mCheckedIdStates; + final int count = idStates.size(); + final long[] ids = new long[count]; + + for (int i = 0; i < count; i++) { + ids[i] = idStates.keyAt(i); + } + + return ids; + } + + /** + * Clear any choices previously set + */ + public void clearChoices() { + if (mCheckStates != null) { + mCheckStates.clear(); + } + if (mCheckedIdStates != null) { + mCheckedIdStates.clear(); + } + mCheckedItemCount = 0; + updateOnScreenCheckedViews(); + } + + /** + * Sets the checked state of the specified position. The is only valid if + * the choice mode has been set to {@link #CHOICE_MODE_SINGLE} or + * {@link #CHOICE_MODE_MULTIPLE}. + * + * @param position The item whose checked state is to be checked + * @param value The new checked state for the item + */ + public void setItemChecked(int position, boolean value) { + if (mChoiceMode == CHOICE_MODE_NONE) { + return; + } + + // Start selection mode if needed. We don't need to if we're unchecking something. + if (value && mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL && mChoiceActionMode == null) { + if (mMultiChoiceModeCallback == null || + !mMultiChoiceModeCallback.hasWrappedCallback()) { + throw new IllegalStateException("AbsListView: attempted to start selection mode " + + "for CHOICE_MODE_MULTIPLE_MODAL but no choice mode callback was " + + "supplied. Call setMultiChoiceModeListener to set a callback."); + } + mChoiceActionMode = startActionMode(mMultiChoiceModeCallback); + } + + if (mChoiceMode == CHOICE_MODE_MULTIPLE || mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL) { + boolean oldValue = mCheckStates.get(position); + mCheckStates.put(position, value); + if (mCheckedIdStates != null && mAdapter.hasStableIds()) { + if (value) { + mCheckedIdStates.put(mAdapter.getItemId(position), position); + } else { + mCheckedIdStates.delete(mAdapter.getItemId(position)); + } + } + if (oldValue != value) { + if (value) { + mCheckedItemCount++; + } else { + mCheckedItemCount--; + } + } + if (mChoiceActionMode != null) { + final long id = mAdapter.getItemId(position); + mMultiChoiceModeCallback.onItemCheckedStateChanged(mChoiceActionMode, + position, id, value); + } + } else { + boolean updateIds = mCheckedIdStates != null && mAdapter.hasStableIds(); + // Clear all values if we're checking something, or unchecking the currently + // selected item + if (value || isItemChecked(position)) { + mCheckStates.clear(); + if (updateIds) { + mCheckedIdStates.clear(); + } + } + // this may end up selecting the value we just cleared but this way + // we ensure length of mCheckStates is 1, a fact getCheckedItemPosition relies on + if (value) { + mCheckStates.put(position, true); + if (updateIds) { + mCheckedIdStates.put(mAdapter.getItemId(position), position); + } + mCheckedItemCount = 1; + } else if (mCheckStates.size() == 0 || !mCheckStates.valueAt(0)) { + mCheckedItemCount = 0; + } + } + + updateOnScreenCheckedViews(); + } + + /** + * @see #setChoiceMode(int) + * + * @return The current choice mode + */ + public int getChoiceMode() { + return mChoiceMode; + } + + /** + * Defines the choice behavior for the List. By default, Lists do not have any choice behavior + * ({@link #CHOICE_MODE_NONE}). By setting the choiceMode to {@link #CHOICE_MODE_SINGLE}, the + * List allows up to one item to be in a chosen state. By setting the choiceMode to + * {@link #CHOICE_MODE_MULTIPLE}, the list allows any number of items to be chosen. + * + * @param choiceMode One of {@link #CHOICE_MODE_NONE}, {@link #CHOICE_MODE_SINGLE}, or + * {@link #CHOICE_MODE_MULTIPLE} + */ + public void setChoiceMode(int choiceMode) { + mChoiceMode = choiceMode; + if (mChoiceActionMode != null) { + mChoiceActionMode.finish(); + mChoiceActionMode = null; + } + if (mChoiceMode != CHOICE_MODE_NONE) { + if (mCheckStates == null) { + mCheckStates = new SparseBooleanArray(0); + } + if (mCheckedIdStates == null && mAdapter != null && mAdapter.hasStableIds()) { + mCheckedIdStates = new LongSparseArray(0); + } + // Modal multi-choice mode only has choices when the mode is active. Clear them. + if (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL) { + clearChoices(); + } + } + + updateOnClickState(); + updateOnLongClickState(); + } + + /** + * Set a {@link MultiChoiceModeListener} that will manage the lifecycle of the + * selection {@link ActionMode}. Only used when the choice mode is set to + * {@link #CHOICE_MODE_MULTIPLE_MODAL}. + * + * @param listener Listener that will manage the selection mode + * + * @see #setChoiceMode(int) + */ + public void setMultiChoiceModeListener(MultiChoiceModeListener listener) { + if (mMultiChoiceModeCallback == null) { + mMultiChoiceModeCallback = new MultiChoiceModeWrapper(); + } + mMultiChoiceModeCallback.setWrapped(listener); + } + + /** + * Perform a quick, in-place update of the checked or activated state + * on all visible item views. This should only be called when a valid + * choice mode is active. + */ + private void updateOnScreenCheckedViews() { + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + final int position = getChildPosition(child); + setViewChecked(child, mCheckStates.get(position)); + } + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public static void setViewChecked(View view, boolean checked) { + final boolean useActivated = Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB; + if (view instanceof Checkable) { + ((Checkable) view).setChecked(checked); + } else if (useActivated) { + view.setActivated(checked); + } + } + + @Override + public boolean onGenericMotionEvent(MotionEvent event) { + if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { + switch (event.getAction()) { + case MotionEvent.ACTION_SCROLL: { + if (getScrollState() != SCROLL_STATE_DRAGGING) { + final float vscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL); + if (vscroll != 0) { + final int delta = (int) (vscroll * getVerticalScrollFactor()); + scrollBy(0, -delta); + } + } + } + } + } + return super.onGenericMotionEvent(event); + } + + /** + * Gets a scale factor that determines the distance the view should scroll + * vertically in response to {@link MotionEvent#ACTION_SCROLL}. + * @return The vertical scroll scale factor. + */ + protected float getVerticalScrollFactor() { + if (mVerticalScrollFactor == 0) { + TypedValue outValue = new TypedValue(); + if (!getContext().getTheme().resolveAttribute( + 0x0101004d, outValue, true)) {//com.android.internal.R.attr.listPreferredItemHeight + throw new IllegalStateException( + "Expected theme to define listPreferredItemHeight."); + } + mVerticalScrollFactor = outValue.getDimension( + getContext().getResources().getDisplayMetrics()); + } + return mVerticalScrollFactor; + } + + /* + @Override + public Parcelable onSaveInstanceState() { + final Parcelable state = new Bundle(super.onSaveInstanceState()); + + state.putInt(STATE_KEY_CHOICE_MODE, mChoiceMode); + state.putParcelable(STATE_KEY_CHECKED_STATES, mCheckStates); + state.putParcelable(STATE_KEY_CHECKED_ID_STATES, mCheckedIdStates); + state.putInt(STATE_KEY_CHECKED_COUNT, mCheckedItemCount); + + return state; + } + + public void onRestoreInstanceState(Bundle state) { + mChoiceMode = ChoiceMode.values()[state.getInt(STATE_KEY_CHOICE_MODE)]; + mCheckedStates = state.getParcelable(STATE_KEY_CHECKED_STATES); + mCheckedIdStates = state.getParcelable(STATE_KEY_CHECKED_ID_STATES); + mCheckedCount = state.getInt(STATE_KEY_CHECKED_COUNT); + + // TODO confirm ids here + }*/ + + /** + * Interface definition for a callback to be invoked when an item in the + * EasyRecyclerView has been clicked. + */ + public interface OnItemClickListener { + /** + * Callback method to be invoked when an item in the EasyRecyclerView + * has been clicked. + * + * @param parent The EasyRecyclerView where the click happened. + * @param view The view within the EasyRecyclerView that was clicked + * @param position The position of the view in the adapter. + * @param id The row id of the item that was clicked. + * + * @return true if the callback consumed the click, false otherwise + */ + boolean onItemClick(EasyRecyclerView parent, View view, int position, long id); + } + + /** + * Interface definition for a callback to be invoked when an item in the + * EasyRecyclerView has been clicked and held. + */ + public interface OnItemLongClickListener { + /** + * Callback method to be invoked when an item in the EasyRecyclerView + * has been clicked and held. + * + * @param parent The EasyRecyclerView where the click happened + * @param view The view within the EasyRecyclerView that was clicked + * @param position The position of the view in the list + * @param id The row id of the item that was clicked + * + * @return true if the callback consumed the long click, false otherwise + */ + boolean onItemLongClick(EasyRecyclerView parent, View view, int position, long id); + } + + + /* + * EasyRecyclerView.OnItemTouchListener will not work fine + * when item view's child need click action, because they make + * touch event deliver different + */ + + @Override + public void onClick(View view) { + final int position = getChildPosition(view); + final long id = mAdapter.getItemId(position); + + boolean dispatchItemClick = true; + + if (mChoiceMode != CHOICE_MODE_NONE) { + boolean checkedStateChanged = false; + + if (mChoiceMode == CHOICE_MODE_MULTIPLE || + (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL && mChoiceActionMode != null)) { + boolean checked = !mCheckStates.get(position, false); + mCheckStates.put(position, checked); + if (mCheckedIdStates != null && mAdapter.hasStableIds()) { + if (checked) { + mCheckedIdStates.put(mAdapter.getItemId(position), position); + } else { + mCheckedIdStates.delete(mAdapter.getItemId(position)); + } + } + if (checked) { + mCheckedItemCount++; + } else { + mCheckedItemCount--; + } + if (mChoiceActionMode != null) { + mMultiChoiceModeCallback.onItemCheckedStateChanged(mChoiceActionMode, + position, id, checked); + dispatchItemClick = false; + } + checkedStateChanged = true; + } else if (mChoiceMode == CHOICE_MODE_SINGLE) { + boolean checked = !mCheckStates.get(position, false); + if (checked) { + mCheckStates.clear(); + mCheckStates.put(position, true); + if (mCheckedIdStates != null && mAdapter.hasStableIds()) { + mCheckedIdStates.clear(); + mCheckedIdStates.put(mAdapter.getItemId(position), position); + } + mCheckedItemCount = 1; + } else if (mCheckStates.size() == 0 || !mCheckStates.valueAt(0)) { + mCheckedItemCount = 0; + } + checkedStateChanged = true; + } + + if (checkedStateChanged) { + updateOnScreenCheckedViews(); + } + } + + if (dispatchItemClick && mOnItemClickListener != null) { + mOnItemClickListener.onItemClick(this, view, position, id); + } + } + + + @Override + public boolean onLongClick(View view) { + final int position = getChildPosition(view); + final long id = mAdapter.getItemId(position); + + // CHOICE_MODE_MULTIPLE_MODAL takes over long press. + if (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL) { + if (mChoiceActionMode == null && + (mChoiceActionMode = startActionMode(mMultiChoiceModeCallback)) != null) { + setItemChecked(position, true); + } + return true; + } + + boolean handled = false; + if (mOnItemLongClickListener != null) { + handled = mOnItemLongClickListener.onItemLongClick(this, view, + position, id); + } + return handled; + } + + /** + * A MultiChoiceModeListener receives events for {@link AbsListView#CHOICE_MODE_MULTIPLE_MODAL}. + * It acts as the {@link ActionMode.Callback} for the selection mode and also receives + * {@link #onItemCheckedStateChanged(ActionMode, int, long, boolean)} events when the user + * selects and deselects list items. + */ + public interface MultiChoiceModeListener extends ActionMode.Callback { + /** + * Called when an item is checked or unchecked during selection mode. + * + * @param mode The {@link ActionMode} providing the selection mode + * @param position Adapter position of the item that was checked or unchecked + * @param id Adapter ID of the item that was checked or unchecked + * @param checked true if the item is now checked, false + * if the item is now unchecked. + */ + public void onItemCheckedStateChanged(ActionMode mode, + int position, long id, boolean checked); + } + + class MultiChoiceModeWrapper implements MultiChoiceModeListener { + private MultiChoiceModeListener mWrapped; + + public void setWrapped(MultiChoiceModeListener wrapped) { + mWrapped = wrapped; + } + + public boolean hasWrappedCallback() { + return mWrapped != null; + } + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + if (mWrapped.onCreateActionMode(mode, menu)) { + // Initialize checked graphic state? + return true; + } + return false; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return mWrapped.onPrepareActionMode(mode, menu); + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + return mWrapped.onActionItemClicked(mode, item); + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + mWrapped.onDestroyActionMode(mode); + mChoiceActionMode = null; + + // Ending selection mode means deselecting everything. + clearChoices(); + + requestLayout(); + } + + @Override + public void onItemCheckedStateChanged(ActionMode mode, + int position, long id, boolean checked) { + mWrapped.onItemCheckedStateChanged(mode, position, id, checked); + + // If there are no items selected we no longer need the selection mode. + if (getCheckedItemCount() == 0) { + mode.finish(); + } + } + } + + public static abstract class FooterAdapter extends RecyclerView.Adapter { + + public static final int TYPE_FOOTER = Integer.MAX_VALUE; + + private EasyRecyclerView mHolder; + private View mFooterView; + + public boolean hasFootView() { + return mFooterView != null; + } + + /** + * footerView == null to remove foot view + * @param footerView + */ + public void setFooterView(View footerView) { + if (mFooterView == footerView) + return; + + int itemCountActual = getItemCountActual(); + View originView = mFooterView; + mFooterView = footerView; + if (originView != null) { + if (mFooterView != null) + notifyItemChanged(itemCountActual); + else + notifyItemRemoved(itemCountActual); + } else { + notifyItemInserted(itemCountActual); + } + } + + public View getFooterView() { + return mFooterView; + } + + @Override + public final VH onCreateViewHolder(ViewGroup parent, int viewType) { + if (mFooterView != null && viewType == TYPE_FOOTER) { + StaggeredGridLayoutManager.LayoutParams lp = new StaggeredGridLayoutManager.LayoutParams( + StaggeredGridLayoutManager.LayoutParams.WRAP_CONTENT, + StaggeredGridLayoutManager.LayoutParams.WRAP_CONTENT); + lp.setFullSpan(true); + mFooterView.setLayoutParams(lp); + return onCreateAndBindFooterViewHolder(parent, mFooterView); + } else { + return onCreateViewHolderActual(parent, viewType); + } + } + + @Override + public final void onBindViewHolder(VH holder, int position) { + if (mFooterView == null || position != getItemCountActual()) { + onBindViewHolderActual(holder, position); + if (mHolder.mNeedOnClickListener) + holder.itemView.setOnClickListener(mHolder); + if (mHolder.mNeedOnLongClickListener) + holder.itemView.setOnLongClickListener(mHolder); + EasyRecyclerView.setViewChecked(holder.itemView, + mHolder.isItemChecked(position)); + } + } + + @Override + public final int getItemViewType(int position) { + if (mFooterView != null && position == getItemCountActual()) + return TYPE_FOOTER; + else + return getItemViewTypeActual(position); + } + + @Override + public final int getItemCount() { + int itemCount = getItemCountActual(); + if (mFooterView != null) + itemCount++; + return itemCount; + } + + public abstract VH onCreateAndBindFooterViewHolder(ViewGroup parent, View footerView); + + public abstract VH onCreateViewHolderActual(ViewGroup parent, int viewType); + + public abstract void onBindViewHolderActual(VH holder, int position); + + /** + * Foot view type is Integer.MAX_VALUE, do not be the same + * + * @param position + * @return + */ + public int getItemViewTypeActual(int position) { + return 0; + } + + public abstract int getItemCountActual(); + + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/recyclerview/LinearDividerItemDecoration.java b/app/src/main/java/com/hippo/ehviewer/widget/recyclerview/LinearDividerItemDecoration.java new file mode 100644 index 000000000..a8dac20e0 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/recyclerview/LinearDividerItemDecoration.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2015 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.widget.recyclerview; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.View; + +public class LinearDividerItemDecoration extends RecyclerView.ItemDecoration { + public static final int HORIZONTAL = LinearLayoutManager.HORIZONTAL; + public static final int VERTICAL = LinearLayoutManager.VERTICAL; + + private boolean mShowFirstDivider = false; + private boolean mShowLastDivider = false; + + private final Rect mRect; + private final Paint mPaint; + + private int mOrientation; + private int mThickness; + private int mPaddingStart = 0; + private int mPaddingEnd = 0; + + public LinearDividerItemDecoration(int orientation, int color, int thickness) { + mRect = new Rect(); + mPaint = new Paint(); + mPaint.setStyle(Paint.Style.FILL); + setOrientation(orientation); + setColor(color); + setThickness(thickness); + } + + public void setOrientation(int orientation) { + if (orientation != HORIZONTAL && orientation != VERTICAL) { + throw new IllegalArgumentException("invalid orientation"); + } + mOrientation = orientation; + } + + public void setColor(int color) { + mPaint.setColor(color); + } + + public void setThickness(int thickness) { + mThickness = thickness; + } + + public void setShowFirstDivider(boolean showFirstDivider) { + mShowFirstDivider = showFirstDivider; + } + + public void setShowLastDivider(boolean showLastDivider) { + mShowLastDivider = showLastDivider; + } + + public void setPadding(int padding) { + setPaddingStart(padding); + setPaddingEnd(padding); + } + + public void setPaddingStart(int paddingStart) { + mPaddingStart = paddingStart; + } + + public void setPaddingEnd(int paddingEnd) { + mPaddingEnd = paddingEnd; + } + + @Override + public void getItemOffsets(Rect outRect, View view, + RecyclerView parent, RecyclerView.State state) { + final int position = parent.getChildPosition(view); + final int itemCount = parent.getAdapter().getItemCount(); + if (mOrientation == VERTICAL) { + if (position == 0 && mShowFirstDivider) + outRect.top = mThickness; + outRect.bottom = mThickness; + if (position == itemCount - 1 && !mShowLastDivider) + outRect.bottom = 0; + } else { + if (position == 0 && mShowFirstDivider) + outRect.left = mThickness; + outRect.right = mThickness; + if (position == itemCount - 1 && !mShowLastDivider) + outRect.left = 0; + } + } + + @Override + public void onDrawOver(Canvas c, RecyclerView parent, + RecyclerView.State state) { + int itemCount = parent.getAdapter().getItemCount(); + + if (mOrientation == VERTICAL) { + final int left = parent.getPaddingLeft() + mPaddingStart; + final int right = parent.getWidth() - parent.getPaddingRight() - mPaddingEnd; + final int childCount = parent.getChildCount(); + + for (int i = 0; i < childCount; i++) { + final View child = parent.getChildAt(i); + final RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams(); + + if (i != childCount - 1 || mShowLastDivider + || parent.getChildPosition(child) != itemCount - 1) { + final int top = child.getBottom() + lp.bottomMargin; + final int bottom = top + mThickness; + mRect.set(left, top, right, bottom); + c.drawRect(mRect, mPaint); + } + + if (i == 0 && mShowFirstDivider && parent.getChildPosition(child) == 0) { + final int bottom = child.getTop() + lp.topMargin; + final int top = bottom - mThickness; + mRect.set(left, top, right, bottom); + c.drawRect(mRect, mPaint); + } + } + } else { + final int top = parent.getPaddingTop() + mPaddingStart; + final int bottom = parent.getHeight() - parent.getPaddingBottom() - mPaddingEnd; + final int childCount = parent.getChildCount(); + + for (int i = 0; i < childCount; i++) { + final View child = parent.getChildAt(i); + final RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams(); + + if (i != childCount - 1 || mShowLastDivider + || parent.getChildPosition(child) != itemCount - 1) { + final int left = child.getRight() + lp.rightMargin; + final int right = left + mThickness; + mRect.set(left, top, right, bottom); + c.drawRect(mRect, mPaint); + } + + if (i == 0 && mShowFirstDivider && parent.getChildPosition(child) == 0) { + final int right = child.getLeft() + lp.leftMargin; + final int left = right - mThickness; + mRect.set(left, top, right, bottom); + c.drawRect(mRect, mPaint); + } + } + } + } + +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/recyclerview/MarginItemDecoration.java b/app/src/main/java/com/hippo/ehviewer/widget/recyclerview/MarginItemDecoration.java new file mode 100644 index 000000000..388ac4aa6 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/recyclerview/MarginItemDecoration.java @@ -0,0 +1,46 @@ +/* + * 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.recyclerview; + +import android.graphics.Rect; +import android.support.v7.widget.RecyclerView; +import android.view.View; + +/** + * Only work for StaggeredGridLayoutManager, FooterAdapter + * + * @author Hippo + * + */ +public class MarginItemDecoration extends RecyclerView.ItemDecoration { + + private int mHalfMargin; + + @Override + public void getItemOffsets(Rect outRect, View view, + RecyclerView parent, RecyclerView.State state) { + outRect.set(mHalfMargin, mHalfMargin, mHalfMargin, mHalfMargin); + } + + public void setMargin(int margin) { + mHalfMargin = margin / 2; + } + + public int getMargin() { + return mHalfMargin * 2; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/windowsanimate/AnimateCanvas.java b/app/src/main/java/com/hippo/ehviewer/windowsanimate/AnimateCanvas.java new file mode 100644 index 000000000..4d7f657f8 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/windowsanimate/AnimateCanvas.java @@ -0,0 +1,48 @@ +/* + * 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.windowsanimate; + +import java.util.LinkedList; +import java.util.List; + +import android.content.Context; +import android.graphics.Canvas; +import android.view.View; + +public class AnimateCanvas extends View { + + private final List mSpriteList = new LinkedList(); + + public AnimateCanvas(Context context) { + super(context); + } + + void addSprite(Sprite sprite) { + mSpriteList.add(sprite); + } + + void removeSprite(Sprite sprite) { + mSpriteList.remove(sprite); + } + + @Override + public void onDraw(Canvas canvas) { + for (Sprite sprite : mSpriteList) { + sprite.draw(canvas); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/windowsanimate/BitmapSprite.java b/app/src/main/java/com/hippo/ehviewer/windowsanimate/BitmapSprite.java new file mode 100644 index 000000000..5d49e93eb --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/windowsanimate/BitmapSprite.java @@ -0,0 +1,54 @@ +/* + * 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.windowsanimate; + +import android.graphics.Bitmap; +import android.graphics.Canvas; + +public class BitmapSprite extends Sprite { + + private int x; + private int y; + private Bitmap mBitmap; + + public BitmapSprite(WindowsAnimate holder, Bitmap bmp, int startX, int startY) { + super(holder); + mBitmap = bmp; + x = startX; + y = startY; + } + + public void setX(int x) { + this.x = x; + updateCanvas(); + } + + public void setY(int y) { + this.y = y; + updateCanvas(); + } + + @Override + public void draw(Canvas canvas) { + canvas.drawBitmap(mBitmap, x, y, null); + } + + public void free() { + mBitmap.recycle(); + mBitmap = null; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/windowsanimate/RippleHelpDrawable.java b/app/src/main/java/com/hippo/ehviewer/windowsanimate/RippleHelpDrawable.java new file mode 100644 index 000000000..16cfe356e --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/windowsanimate/RippleHelpDrawable.java @@ -0,0 +1,120 @@ +/* + * 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.windowsanimate; + +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.view.View; + +import com.hippo.ehviewer.cardview.RoundRectDrawableWithShadow; + +/** + * @author Hippo + * + * Get lots of code from android.graphics.drawable.RippleDrawable + */ +public class RippleHelpDrawable extends Drawable { + + private final RippleSprite mRipple; + private final Drawable mActualDrawable; + + private boolean mActive = false; + + @SuppressWarnings("deprecation") + public RippleHelpDrawable(WindowsAnimate windowsAnimate, View view, boolean keepBound) { + mActualDrawable = view.getBackground(); + Rect padding = mActualDrawable == null || !(mActualDrawable instanceof RoundRectDrawableWithShadow) ? + null : ((RoundRectDrawableWithShadow)mActualDrawable).getSuggestionPadding(); + mRipple = new RippleSprite(windowsAnimate, view, getBounds(), padding, keepBound); + view.setBackgroundDrawable(this); + } + + @Override + public boolean isStateful() { + return true; + } + + @Override + protected boolean onStateChange(int[] stateSet) { + boolean active = false; + boolean enabled = false; + int N = stateSet.length; + for (int i = 0; i < N; i++) { + if (stateSet[i] == android.R.attr.state_enabled) { + enabled = true; + } + if ((stateSet[i] == android.R.attr.state_focused) || + (stateSet[i] == android.R.attr.state_pressed)) { + active = true; + } + } + + setActive((active) && (enabled)); + + if (mActualDrawable != null && mActualDrawable.isStateful()) + return mActualDrawable.setState(stateSet); + else + return false; + } + + private void setActive(boolean active) { + if (mActive != active) { + mActive = active; + if (active) { + mRipple.enter(); + } else { + mRipple.exit(); + } + } + } + + @Override + protected void onBoundsChange(Rect bounds) { + mRipple.onBoundsChanged(bounds); + if (mActualDrawable != null) + mActualDrawable.setBounds(bounds); + } + + @Override + public void draw(Canvas canvas) { + if (mActualDrawable != null) + mActualDrawable.draw(canvas); + } + + @Override + public void setAlpha(int alpha) { + if (mActualDrawable != null) + mActualDrawable.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter cf) { + if (mActualDrawable != null) + mActualDrawable.setColorFilter(cf); + } + + @Override + public int getOpacity() { + if (mActualDrawable != null) + return mActualDrawable.getOpacity(); + else + return PixelFormat.TRANSPARENT; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/windowsanimate/RippleSprite.java b/app/src/main/java/com/hippo/ehviewer/windowsanimate/RippleSprite.java new file mode 100644 index 000000000..a38ac0def --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/windowsanimate/RippleSprite.java @@ -0,0 +1,470 @@ +/* + * 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.windowsanimate; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.TimeInterpolator; +import android.annotation.TargetApi; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PointF; +import android.graphics.Rect; +import android.os.Build; +import android.view.View; +import android.view.animation.LinearInterpolator; + +import com.hippo.ehviewer.util.MathUtils; +import com.hippo.ehviewer.util.Ui; +import com.hippo.ehviewer.util.ViewUtils; + +/** + * @author Hippo + * + * Get lots of code from android.graphics.drawable.Ripple + */ +public class RippleSprite extends Sprite { + private static final String TAG = RippleSprite.class.getSimpleName(); + + private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); + private static final TimeInterpolator DECEL_INTERPOLATOR = new LogInterpolator(); + + private static final float GLOBAL_SPEED = 1.0F; + private static final float WAVE_TOUCH_DOWN_ACCELERATION = 1024.0F; + private static final float WAVE_TOUCH_UP_ACCELERATION = 3400.0F; + private static final float WAVE_OPACITY_DECAY_VELOCITY = 3.0F; + private static final float WAVE_OUTER_OPACITY_VELOCITY_MAX = 4.5F; + private static final float WAVE_OUTER_OPACITY_VELOCITY_MIN = 1.5F; + private static final float WAVE_OUTER_SIZE_INFLUENCE_MAX = 200.0F; + private static final float WAVE_OUTER_SIZE_INFLUENCE_MIN = 40.0F; + private static final long RIPPLE_ENTER_DELAY = 80L; + + private final WindowsAnimate mHolder; + private final View mView; + private final Rect mBounds; + private final Rect mPadding; + private final boolean mKeepBound; + private float mOuterRadius; + private final int[] mViewPosition = new int[2]; + + private float mStartingX; + private float mStartingY; + private float mClampedStartingX; + private float mClampedStartingY; + + private ObjectAnimator mAnimRadius; + private ObjectAnimator mAnimOpacity; + private ObjectAnimator mAnimOuterOpacity; + private ObjectAnimator mAnimX; + private ObjectAnimator mAnimY; + private float mOuterOpacity = 0.0F; + private float mOpacity = 1.0F; + private float mTweenRadius = 0.0F; + private float mTweenX = 0.0F; + private float mTweenY = 0.0F; + + private boolean isRunning = false; + + private static Paint mPaint; + + static { + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mPaint.setColor(0x20444444); + } + + public RippleSprite(WindowsAnimate holder, View view, Rect bounds, Rect padding, boolean keepBound) { + super(holder); + mHolder = holder; + mView = view; + mBounds = new Rect(bounds); + mPadding = padding; + mKeepBound = keepBound; + + float halfWidth = mBounds.width() / 2.0F; + float halfHeight = mBounds.height() / 2.0F; + mOuterRadius = ((float) Math.sqrt(halfWidth * halfWidth + + halfHeight * halfHeight)); + + mStartingX = mBounds.exactCenterX(); + mStartingY = mBounds.exactCenterY(); + clampStartingPosition(); + } + + public void onBoundsChanged(Rect bounds) { + mBounds.set(bounds); + float halfWidth = mBounds.width() / 2.0F; + float halfHeight = mBounds.height() / 2.0F; + mOuterRadius = ((float) Math.sqrt(halfWidth * halfWidth + + halfHeight * halfHeight)); + + if (!mKeepBound) { + mStartingX = mBounds.exactCenterX(); + mStartingY = mBounds.exactCenterY(); + } + clampStartingPosition(); + } + + private void clampStartingPosition() { + float dX = mStartingX - mBounds.exactCenterX(); + float dY = mStartingY - mBounds.exactCenterY(); + float r = mOuterRadius; + if (dX * dX + dY * dY > r * r) { + double angle = Math.atan2(dY, dX); + mClampedStartingX = ((float)(Math.cos(angle) * r)); + mClampedStartingY = ((float)(Math.sin(angle) * r)); + } else { + mClampedStartingX = mStartingX; + mClampedStartingY = mStartingY; + } + } + + public void setOpacity(float a) { + mOpacity = a; + updateCanvas(); + } + + public float getOpacity() { + return mOpacity; + } + + public void setOuterOpacity(float a) { + mOuterOpacity = a; + updateCanvas(); + } + + public float getOuterOpacity() { + return mOuterOpacity; + } + + public void setRadiusGravity(float r) { + mTweenRadius = r; + updateCanvas(); + } + + public float getRadiusGravity() { + return mTweenRadius; + } + + public void setXGravity(float x) { + mTweenX = x; + updateCanvas(); + } + + public float getXGravity() { + return mTweenX; + } + + public void setYGravity(float y) { + mTweenY = y; + updateCanvas(); + } + + public float getYGravity() { + return mTweenY; + } + + @Override + public void draw(Canvas c) { + ViewUtils.getCenterInWindows(mView, mViewPosition); + + int saved = c.save(); + c.translate(mViewPosition[0], mViewPosition[1]); + if (mKeepBound) { + int halfWidth = mBounds.width() / 2; + int halfHeight = mBounds.height() / 2; + + if (mPadding != null) { + c.clipRect(-halfWidth + mPadding.left, -halfHeight + mPadding.top, + halfWidth - mPadding.right, halfHeight - mPadding.bottom); + } else + c.clipRect(-halfWidth, -halfHeight, + halfWidth, halfHeight); + } + + drawSoftware(c, mPaint); + + c.restoreToCount(saved); + } + + private void drawSoftware(Canvas c, Paint p) { + int paintAlpha = p.getAlpha(); + + int outerAlpha = (int) (paintAlpha * mOuterOpacity + 0.5F); + if ((outerAlpha > 0) && (mOuterRadius > 0.0F)) { + p.setAlpha(outerAlpha); + p.setStyle(Paint.Style.FILL); + c.drawCircle(0.0F, 0.0F, mOuterRadius, p); + } + + int alpha = (int) (paintAlpha * mOpacity + 0.5F); + float radius = MathUtils.lerp(0.0F, mOuterRadius, + mTweenRadius); + if ((alpha > 0) && (radius > 0.0F)) { + float x = MathUtils.lerp( + this.mClampedStartingX - this.mBounds.exactCenterX(), + 0.0F, this.mTweenX); + float y = MathUtils.lerp( + this.mClampedStartingY - this.mBounds.exactCenterY(), + 0.0F, this.mTweenY); + p.setAlpha(alpha); + p.setStyle(Paint.Style.FILL); + c.drawCircle(x, y, radius, p); + } + + p.setAlpha(paintAlpha); + } + + public void setStartPosition(float x, float y) { + mStartingX = x; + mStartingY = y; + clampStartingPosition(); + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) + public void enter() { + if (isRunning) + return; + isRunning = true; + + addSelf(); + + // If keepBound, set position + if (mKeepBound) { + Object obj = mView.getTag(); + if (obj != null && obj instanceof PointF) { + setStartPosition(((PointF)obj).x, ((PointF)obj).y); + } + } + + mOuterOpacity = 0.0F; + mOpacity = 1.0F; + mTweenRadius = 0.0F; + mTweenX = 0.0F; + mTweenY = 0.0F; + + int radiusDuration = (int) (1000.0D * Math.sqrt(mOuterRadius + / WAVE_TOUCH_DOWN_ACCELERATION * Ui.mDensity) + 0.5D); + + long outerDuration = 666L; + + ObjectAnimator radius = ObjectAnimator.ofFloat(this, "radiusGravity", + new float[] { 1.0F }); + radius.setDuration(radiusDuration); + radius.setInterpolator(LINEAR_INTERPOLATOR); + radius.setStartDelay(RIPPLE_ENTER_DELAY); + + ObjectAnimator cX = ObjectAnimator.ofFloat(this, "xGravity", + new float[] { 1.0F }); + cX.setDuration(radiusDuration); + cX.setInterpolator(LINEAR_INTERPOLATOR); + cX.setStartDelay(RIPPLE_ENTER_DELAY); + + ObjectAnimator cY = ObjectAnimator.ofFloat(this, "yGravity", + new float[] { 1.0F }); + cY.setDuration(radiusDuration); + cY.setInterpolator(LINEAR_INTERPOLATOR); + cY.setStartDelay(RIPPLE_ENTER_DELAY); + + ObjectAnimator outer = ObjectAnimator.ofFloat(this, "outerOpacity", + new float[] { 0.0F, 1.0F }); + outer.setDuration(outerDuration); + outer.setInterpolator(LINEAR_INTERPOLATOR); + + if (Build.VERSION.SDK_INT >= 18) { + radius.setAutoCancel(true); + cX.setAutoCancel(true); + cY.setAutoCancel(true); + outer.setAutoCancel(true); + } + + mAnimRadius = radius; + mAnimOuterOpacity = outer; + mAnimX = cX; + mAnimY = cY; + + radius.start(); + outer.start(); + cX.start(); + cY.start(); + } + + public void exit() { + if (!isRunning) + return; + + cancelSoftwareAnimations(); + float radius = MathUtils.lerp(0.0F, mOuterRadius, + mTweenRadius); + float remaining; + if ((mAnimRadius != null) && (mAnimRadius.isRunning())) { + remaining = mOuterRadius - radius; + } else { + remaining = mOuterRadius; + } + + int radiusDuration = (int) (1000.0D * Math.sqrt(remaining / 4424.0F + * Ui.mDensity) + 0.5D); + + int opacityDuration = (int) (1000.0F * mOpacity / 3.0F + 0.5F); + + float outerSizeInfluence = MathUtils.clamp( + (mOuterRadius - WAVE_OUTER_SIZE_INFLUENCE_MIN * Ui.mDensity) + / (WAVE_OUTER_SIZE_INFLUENCE_MAX * Ui.mDensity), 0.0F, 1.0F); + + float outerOpacityVelocity = MathUtils.lerp(WAVE_OUTER_OPACITY_VELOCITY_MIN, + WAVE_OUTER_OPACITY_VELOCITY_MAX, outerSizeInfluence); + + int outerInflection = Math.max(0, (int) (1000.0F + * (mOpacity - mOuterOpacity) + / (3.0F + outerOpacityVelocity) + 0.5F)); + + int inflectionOpacity = (int) (255.0F * (mOuterOpacity + outerInflection + * outerOpacityVelocity * outerSizeInfluence / 1000.0F) + 0.5F); + + exitSoftware(radiusDuration, opacityDuration, outerInflection, + inflectionOpacity); + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) + private void exitSoftware(int radiusDuration, int opacityDuration, + int outerInflection, int inflectionOpacity) { + ObjectAnimator radiusAnim = ObjectAnimator.ofFloat(this, + "radiusGravity", new float[] { 1.0F }); + radiusAnim.setDuration(radiusDuration); + radiusAnim.setInterpolator(DECEL_INTERPOLATOR); + + ObjectAnimator xAnim = ObjectAnimator.ofFloat(this, "xGravity", + new float[] { 1.0F }); + xAnim.setDuration(radiusDuration); + xAnim.setInterpolator(DECEL_INTERPOLATOR); + + ObjectAnimator yAnim = ObjectAnimator.ofFloat(this, "yGravity", + new float[] { 1.0F }); + yAnim.setDuration(radiusDuration); + yAnim.setInterpolator(DECEL_INTERPOLATOR); + + ObjectAnimator opacityAnim = ObjectAnimator.ofFloat(this, "opacity", + new float[] { 0.0F }); + opacityAnim.setDuration(opacityDuration); + opacityAnim.setInterpolator(LINEAR_INTERPOLATOR); + + ObjectAnimator outerOpacityAnim; + if (outerInflection > 0) { + outerOpacityAnim = ObjectAnimator.ofFloat(this, + "outerOpacity", new float[] { inflectionOpacity / 255.0F }); + outerOpacityAnim.setDuration(outerInflection); + outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR); + + final int outerDuration = opacityDuration - outerInflection; + if (outerDuration > 0) { + outerOpacityAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + ObjectAnimator outerFadeOutAnim = ObjectAnimator + .ofFloat(RippleSprite.this, "outerOpacity", + new float[] { 0.0F }); + if (Build.VERSION.SDK_INT >= 18) + outerFadeOutAnim.setAutoCancel(true); + outerFadeOutAnim.setDuration(outerDuration); + outerFadeOutAnim + .setInterpolator(RippleSprite.LINEAR_INTERPOLATOR); + outerFadeOutAnim + .addListener(mAnimationListener); + + mAnimOuterOpacity = outerFadeOutAnim; + + outerFadeOutAnim.start(); + } + + @Override + public void onAnimationCancel(Animator animation) { + animation.removeListener(this); + } + }); + } else { + outerOpacityAnim.addListener(mAnimationListener); + } + } else { + outerOpacityAnim = ObjectAnimator.ofFloat(this, "outerOpacity", + new float[] { 0.0F }); + outerOpacityAnim.setDuration(opacityDuration); + outerOpacityAnim.addListener(mAnimationListener); + } + + if (Build.VERSION.SDK_INT >= 18) { + radiusAnim.setAutoCancel(true); + xAnim.setAutoCancel(true); + yAnim.setAutoCancel(true); + opacityAnim.setAutoCancel(true); + outerOpacityAnim.setAutoCancel(true); + } + + mAnimRadius = radiusAnim; + mAnimOpacity = opacityAnim; + mAnimOuterOpacity = outerOpacityAnim; + mAnimX = opacityAnim; + mAnimY = opacityAnim; + + radiusAnim.start(); + opacityAnim.start(); + outerOpacityAnim.start(); + xAnim.start(); + yAnim.start(); + } + + public void cancel() { + cancelSoftwareAnimations(); + } + + private void cancelSoftwareAnimations() { + if (mAnimRadius != null) { + mAnimRadius.cancel(); + } + + if (mAnimOpacity != null) { + mAnimOpacity.cancel(); + } + + if (mAnimOuterOpacity != null) { + mAnimOuterOpacity.cancel(); + } + + if (mAnimX != null) { + mAnimX.cancel(); + } + + if (mAnimY != null) { + mAnimY.cancel(); + } + } + + private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + removeSelf(); + isRunning = false; + } + }; + + private static class LogInterpolator implements TimeInterpolator { + @Override + public float getInterpolation(float input) { + return 1.0F - (float) Math.pow(400.0D, -input * 1.4D); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/windowsanimate/RoundSprite.java b/app/src/main/java/com/hippo/ehviewer/windowsanimate/RoundSprite.java new file mode 100644 index 000000000..7c377768e --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/windowsanimate/RoundSprite.java @@ -0,0 +1,46 @@ +/* + * 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.windowsanimate; + +import android.graphics.Canvas; +import android.graphics.Paint; + +public class RoundSprite extends Sprite { + + private final int startX; + private final int startY; + private float radius; + private final Paint mPaint; + + public RoundSprite(WindowsAnimate holder, int startX, int startY, int color) { + super(holder); + this.startX = startX; + this.startY = startY; + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mPaint.setColor(color); + } + + public void setRadius(float radius) { + this.radius = radius; + updateCanvas(); + } + + @Override + void draw(Canvas canvas) { + canvas.drawCircle(startX, startY, radius, mPaint); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/windowsanimate/Sprite.java b/app/src/main/java/com/hippo/ehviewer/windowsanimate/Sprite.java new file mode 100644 index 000000000..2b101ceb6 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/windowsanimate/Sprite.java @@ -0,0 +1,42 @@ +/* + * 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.windowsanimate; + +import android.graphics.Canvas; + +public abstract class Sprite { + + private final WindowsAnimate mHolder; + + public Sprite(WindowsAnimate holder) { + mHolder = holder; + } + + public void addSelf() { + mHolder.addRenderingSprite(this); + } + + public void removeSelf() { + mHolder.removeRenderingSprite(this); + } + + protected void updateCanvas() { + mHolder.updateCanvas(); + } + + abstract void draw(Canvas canvas); +} diff --git a/app/src/main/java/com/hippo/ehviewer/windowsanimate/WindowsAnimate.java b/app/src/main/java/com/hippo/ehviewer/windowsanimate/WindowsAnimate.java new file mode 100644 index 000000000..b68e736dd --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/windowsanimate/WindowsAnimate.java @@ -0,0 +1,283 @@ +/* + * 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.windowsanimate; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.animation.TimeInterpolator; +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Context; +import android.graphics.PointF; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.OvershootInterpolator; + +import com.hippo.ehviewer.util.Constants; +import com.hippo.ehviewer.util.ViewUtils; + +public final class WindowsAnimate + implements View.OnTouchListener { + + @SuppressWarnings("unused") + private static final String TAG = WindowsAnimate.class.getSimpleName(); + + private static final TimeInterpolator ACCELERATE_INTERPOLATOR = new AccelerateInterpolator(); + private static final TimeInterpolator OVERSHOOT_INTERPOLATOR = new OvershootInterpolator(); + + private Context mContext; + private ViewGroup mContentViewGroup; + private AnimateCanvas mAnimateCanvas; + private int mRunningAnimateNum = 0; + + public boolean init(Activity activity) { + mContentViewGroup = (ViewGroup)activity.getWindow().getDecorView() + .findViewById(android.R.id.content); + if (mContentViewGroup == null) + return false; + + mContext = activity.getApplicationContext(); + mAnimateCanvas = new AnimateCanvas(mContext); + mContentViewGroup.addView(mAnimateCanvas, new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + return true; + } + + public void free() { + if (mContentViewGroup != null) { + mContentViewGroup.removeView(mAnimateCanvas); + mContentViewGroup = null; + } + } + + public boolean isRunningAnimate() { + return mRunningAnimateNum != 0; + } + + private void ensureCanvas() { + if (mContentViewGroup == null) + throw new RuntimeException("Call init(Activity) first, or call it after call free()"); + } + + @Override + @SuppressLint("ClickableViewAccessibility") + public boolean onTouch(View v, MotionEvent event) { + Object obj = v.getTag(); + if (obj == null) { + PointF p = new PointF(); + p.set(event.getX(), event.getY()); + v.setTag(p); + } else { + PointF p = (PointF)obj; + p.set(event.getX(), event.getY()); + } + return false; + } + + void updateCanvas() { + if (mAnimateCanvas != null) + mAnimateCanvas.invalidate(); + } + + /** + * Add ripple effecr when pressed, focus, just like in Android-L + * + * @param view + * @param keepBound If true, ripple will not out of view's area, + * and ripple start where you pressed. Attention, it will + * setOnTouchListener, to get position. + */ + /* + public void addRippleEffect(View view, boolean keepBound) { + ensureCanvas(); + + new RippleHelpDrawable(this, view, keepBound); + if (keepBound) { + view.setOnTouchListener(this); + } + }*/ + + void addRenderingSprite(Sprite sprite) { + if (mAnimateCanvas != null) + mAnimateCanvas.addSprite(sprite); + } + + void removeRenderingSprite(Sprite sprite) { + if (mAnimateCanvas != null) + mAnimateCanvas.removeSprite(sprite); + } + + /** + * Show a circle transitions at view center + * + * @param view + * @param color + * @param listener + */ + public void addCircleTransitions(View view, int color, OnAnimationEndListener listener) { + ensureCanvas(); + + int[] loaction = new int[2]; + ViewUtils.getCenterInWindows(view, loaction); + addCircleTransitions(loaction[0], loaction[1], color, listener); + } + + /** + * Show a circle transitions at specific position in view + * + * @param view + * @param x + * @param y + * @param color + * @param listener + */ + public void addCircleTransitions(View view, int x, int y, int color, OnAnimationEndListener listener) { + ensureCanvas(); + + int[] loaction = new int[2]; + ViewUtils.getLocationInWindow(view, loaction); + addCircleTransitions(loaction[0] + x, loaction[1] + y, color, listener); + } + + /** + * Show a circle transitions at specific position in window + * + * @param x + * @param y + * @param color + * @param listener + */ + public void addCircleTransitions(final int x, final int y, int color, final OnAnimationEndListener listener) { + ensureCanvas(); + + // Get max radius + int maxWidth = mAnimateCanvas.getWidth(); + int maxHeight = mAnimateCanvas.getHeight(); + float maxRadius = (float)Math.sqrt(maxWidth * maxWidth + maxHeight * maxHeight); + + final Sprite sprite = new RoundSprite(this, x, y, color); + ObjectAnimator oa = ObjectAnimator.ofFloat(sprite, "radius", + new float[] {0, maxRadius}); + oa.setDuration(Constants.ANIMATE_TIME); + oa.setInterpolator(ACCELERATE_INTERPOLATOR); + oa.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + sprite.addSelf(); + } + @Override + public void onAnimationRepeat(Animator animation) {} + @Override + public void onAnimationEnd(Animator animation) { + sprite.removeSelf(); + if (listener != null) + listener.onAnimationEnd(); + + mRunningAnimateNum--; + } + @Override + public void onAnimationCancel(Animator animation) {} + }); + oa.start(); + + mRunningAnimateNum++; + } + + /** + * Move view out of window, view will set View.GONE + * @param view + * @param listener + */ + // TODO not just from left to right + public void addMoveExitTransitions(final View view, final OnAnimationEndListener listener) { + ensureCanvas(); + + int[] location = new int[2]; + ViewUtils.getLocationInWindow(view, location); + final BitmapSprite bs = new BitmapSprite(this, ViewUtils.getBitmapFromView(view), location[0], location[1]); + + ObjectAnimator oa = ObjectAnimator.ofInt(bs, "x", + new int[] {location[0], mAnimateCanvas.getWidth()}); + oa.setDuration(Constants.ANIMATE_TIME); + oa.setInterpolator(ACCELERATE_INTERPOLATOR); + oa.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + bs.addSelf(); + view.setVisibility(View.GONE); + } + @Override + public void onAnimationRepeat(Animator animation) {} + @Override + public void onAnimationEnd(Animator animation) { + bs.removeSelf(); + bs.free(); + if (listener != null) + listener.onAnimationEnd(); + + mRunningAnimateNum--; + } + @Override + public void onAnimationCancel(Animator animation) {} + }); + oa.start(); + + mRunningAnimateNum++; + } + + public void addOvershootEnterTransitions(final View view, final OnAnimationEndListener listener) { + ensureCanvas(); + + int[] location = new int[2]; + ViewUtils.getLocationInWindow(view, location); + final BitmapSprite bs = new BitmapSprite(this, ViewUtils.getBitmapFromView(view), location[0], mAnimateCanvas.getHeight()); + + ObjectAnimator oa = ObjectAnimator.ofInt(bs, "y", + new int[] {mAnimateCanvas.getHeight(), location[1]}); + oa.setDuration(Constants.ANIMATE_TIME); + oa.setInterpolator(OVERSHOOT_INTERPOLATOR); + oa.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + bs.addSelf(); + } + @Override + public void onAnimationRepeat(Animator animation) {} + @Override + public void onAnimationEnd(Animator animation) { + bs.removeSelf(); + bs.free(); + view.setVisibility(View.VISIBLE); + if (listener != null) + listener.onAnimationEnd(); + + mRunningAnimateNum--; + } + @Override + public void onAnimationCancel(Animator animation) {} + }); + oa.start(); + + mRunningAnimateNum++; + } + + public interface OnAnimationEndListener { + public void onAnimationEnd(); + } +} diff --git a/app/src/main/jniLibs/armeabi-v7a/libimage.so b/app/src/main/jniLibs/armeabi-v7a/libimage.so new file mode 100644 index 000000000..ae80f344d Binary files /dev/null and b/app/src/main/jniLibs/armeabi-v7a/libimage.so differ diff --git a/app/src/main/jniLibs/armeabi/libimage.so b/app/src/main/jniLibs/armeabi/libimage.so new file mode 100644 index 000000000..ccfbde9f5 Binary files /dev/null and b/app/src/main/jniLibs/armeabi/libimage.so differ diff --git a/app/src/main/jniLibs/x86/libimage.so b/app/src/main/jniLibs/x86/libimage.so new file mode 100644 index 000000000..a53c17bf5 Binary files /dev/null and b/app/src/main/jniLibs/x86/libimage.so differ diff --git a/app/src/main/res/anim/anim_fade_in.xml b/app/src/main/res/anim/anim_fade_in.xml new file mode 100644 index 000000000..d9353fdd4 --- /dev/null +++ b/app/src/main/res/anim/anim_fade_in.xml @@ -0,0 +1,20 @@ + + + diff --git a/app/src/main/res/anim/anim_fade_out.xml b/app/src/main/res/anim/anim_fade_out.xml new file mode 100644 index 000000000..fe4132b74 --- /dev/null +++ b/app/src/main/res/anim/anim_fade_out.xml @@ -0,0 +1,20 @@ + + + diff --git a/app/src/main/res/anim/fade_in.xml b/app/src/main/res/anim/fade_in.xml new file mode 100644 index 000000000..49dfa2b6e --- /dev/null +++ b/app/src/main/res/anim/fade_in.xml @@ -0,0 +1,22 @@ + + + diff --git a/app/src/main/res/anim/fade_out.xml b/app/src/main/res/anim/fade_out.xml new file mode 100644 index 000000000..7394c196c --- /dev/null +++ b/app/src/main/res/anim/fade_out.xml @@ -0,0 +1,22 @@ + + + diff --git a/app/src/main/res/drawable-hdpi/btn_check_off.png b/app/src/main/res/drawable-hdpi/btn_check_off.png new file mode 100644 index 000000000..f14f60d7f Binary files /dev/null and b/app/src/main/res/drawable-hdpi/btn_check_off.png differ diff --git a/app/src/main/res/drawable-hdpi/btn_check_off_dark.png b/app/src/main/res/drawable-hdpi/btn_check_off_dark.png new file mode 100644 index 000000000..a5ddb4586 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/btn_check_off_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/btn_check_off_dark_disabled.png b/app/src/main/res/drawable-hdpi/btn_check_off_dark_disabled.png new file mode 100644 index 000000000..791b8fea7 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/btn_check_off_dark_disabled.png differ diff --git a/app/src/main/res/drawable-hdpi/btn_check_off_disabled.png b/app/src/main/res/drawable-hdpi/btn_check_off_disabled.png new file mode 100644 index 000000000..66d07f821 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/btn_check_off_disabled.png differ diff --git a/app/src/main/res/drawable-hdpi/btn_check_on.png b/app/src/main/res/drawable-hdpi/btn_check_on.png new file mode 100644 index 000000000..91edae05d Binary files /dev/null and b/app/src/main/res/drawable-hdpi/btn_check_on.png differ diff --git a/app/src/main/res/drawable-hdpi/btn_check_on_dark.png b/app/src/main/res/drawable-hdpi/btn_check_on_dark.png new file mode 100644 index 000000000..173e01faf Binary files /dev/null and b/app/src/main/res/drawable-hdpi/btn_check_on_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/btn_check_on_dark_disabled.png b/app/src/main/res/drawable-hdpi/btn_check_on_dark_disabled.png new file mode 100644 index 000000000..d9814696b Binary files /dev/null and b/app/src/main/res/drawable-hdpi/btn_check_on_dark_disabled.png differ diff --git a/app/src/main/res/drawable-hdpi/btn_check_on_disabled.png b/app/src/main/res/drawable-hdpi/btn_check_on_disabled.png new file mode 100644 index 000000000..f3e396cb0 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/btn_check_on_disabled.png differ diff --git a/app/src/main/res/drawable-hdpi/btn_radio_off.png b/app/src/main/res/drawable-hdpi/btn_radio_off.png new file mode 100644 index 000000000..68574b5e2 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/btn_radio_off.png differ diff --git a/app/src/main/res/drawable-hdpi/btn_radio_off_dark.png b/app/src/main/res/drawable-hdpi/btn_radio_off_dark.png new file mode 100644 index 000000000..2c2910791 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/btn_radio_off_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/btn_radio_off_dark_disabled.png b/app/src/main/res/drawable-hdpi/btn_radio_off_dark_disabled.png new file mode 100644 index 000000000..52df43358 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/btn_radio_off_dark_disabled.png differ diff --git a/app/src/main/res/drawable-hdpi/btn_radio_off_disabled.png b/app/src/main/res/drawable-hdpi/btn_radio_off_disabled.png new file mode 100644 index 000000000..fce0f7166 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/btn_radio_off_disabled.png differ diff --git a/app/src/main/res/drawable-hdpi/btn_radio_on.png b/app/src/main/res/drawable-hdpi/btn_radio_on.png new file mode 100644 index 000000000..7ae368e9a Binary files /dev/null and b/app/src/main/res/drawable-hdpi/btn_radio_on.png differ diff --git a/app/src/main/res/drawable-hdpi/btn_radio_on_dark.png b/app/src/main/res/drawable-hdpi/btn_radio_on_dark.png new file mode 100644 index 000000000..c57d06c06 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/btn_radio_on_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/btn_radio_on_dark_disabled.png b/app/src/main/res/drawable-hdpi/btn_radio_on_dark_disabled.png new file mode 100644 index 000000000..09c161b8f Binary files /dev/null and b/app/src/main/res/drawable-hdpi/btn_radio_on_dark_disabled.png differ diff --git a/app/src/main/res/drawable-hdpi/btn_radio_on_disabled.png b/app/src/main/res/drawable-hdpi/btn_radio_on_disabled.png new file mode 100644 index 000000000..17b66dcab Binary files /dev/null and b/app/src/main/res/drawable-hdpi/btn_radio_on_disabled.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_add.png b/app/src/main/res/drawable-hdpi/ic_action_add.png new file mode 100644 index 000000000..f52b21f33 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_add.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_clear.png b/app/src/main/res/drawable-hdpi/ic_action_clear.png new file mode 100644 index 000000000..1e4cc7a8f Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_clear.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_close.png b/app/src/main/res/drawable-hdpi/ic_action_close.png new file mode 100644 index 000000000..68138ede1 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_close.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_core_overflow.png b/app/src/main/res/drawable-hdpi/ic_action_core_overflow.png new file mode 100644 index 000000000..599c7b12f Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_core_overflow.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_delete.png b/app/src/main/res/drawable-hdpi/ic_action_delete.png new file mode 100644 index 000000000..c2a51e4c9 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_delete.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_filter.png b/app/src/main/res/drawable-hdpi/ic_action_filter.png new file mode 100644 index 000000000..f8e86ea57 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_filter.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_list.png b/app/src/main/res/drawable-hdpi/ic_action_list.png new file mode 100644 index 000000000..a73215409 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_list.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_move.png b/app/src/main/res/drawable-hdpi/ic_action_move.png new file mode 100644 index 000000000..3a0419433 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_move.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_redo.png b/app/src/main/res/drawable-hdpi/ic_action_redo.png new file mode 100644 index 000000000..05d17379d Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_redo.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_search.png b/app/src/main/res/drawable-hdpi/ic_action_search.png new file mode 100644 index 000000000..8731851a0 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_search.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_start.png b/app/src/main/res/drawable-hdpi/ic_action_start.png new file mode 100644 index 000000000..1ffedad77 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_start.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_stop.png b/app/src/main/res/drawable-hdpi/ic_action_stop.png new file mode 100644 index 000000000..f8f42c379 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_stop.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_voice_search.png b/app/src/main/res/drawable-hdpi/ic_action_voice_search.png new file mode 100644 index 000000000..adb138794 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_voice_search.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_checkmark.png b/app/src/main/res/drawable-hdpi/ic_checkmark.png new file mode 100644 index 000000000..42f0d9831 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_checkmark.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_dir_dark_dialog.png b/app/src/main/res/drawable-hdpi/ic_dir_dark_dialog.png new file mode 100644 index 000000000..cf2b22c9f Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_dir_dark_dialog.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_drawer_download.png b/app/src/main/res/drawable-hdpi/ic_drawer_download.png new file mode 100644 index 000000000..1546c3a3f Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_drawer_download.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_drawer_favorite.png b/app/src/main/res/drawable-hdpi/ic_drawer_favorite.png new file mode 100644 index 000000000..376f6a03d Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_drawer_favorite.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_drawer_history.png b/app/src/main/res/drawable-hdpi/ic_drawer_history.png new file mode 100644 index 000000000..8c0bc2b12 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_drawer_history.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_drawer_home.png b/app/src/main/res/drawable-hdpi/ic_drawer_home.png new file mode 100644 index 000000000..f3ca18fa4 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_drawer_home.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_drawer_panda.png b/app/src/main/res/drawable-hdpi/ic_drawer_panda.png new file mode 100644 index 000000000..19ed5921c Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_drawer_panda.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_drawer_search.png b/app/src/main/res/drawable-hdpi/ic_drawer_search.png new file mode 100644 index 000000000..a3b922183 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_drawer_search.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_drawer_settings.png b/app/src/main/res/drawable-hdpi/ic_drawer_settings.png new file mode 100644 index 000000000..4811d8b75 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_drawer_settings.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_drawer_whatshot.png b/app/src/main/res/drawable-hdpi/ic_drawer_whatshot.png new file mode 100644 index 000000000..e3ab2aaec Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_drawer_whatshot.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_favorite.png b/app/src/main/res/drawable-hdpi/ic_favorite.png new file mode 100644 index 000000000..bcb852abc Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_favorite.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_more_of_uploader.png b/app/src/main/res/drawable-hdpi/ic_more_of_uploader.png new file mode 100644 index 000000000..f793e5612 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_more_of_uploader.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_rate.png b/app/src/main/res/drawable-hdpi/ic_rate.png new file mode 100644 index 000000000..83991cc57 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_rate.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_reading_action_refresh.png b/app/src/main/res/drawable-hdpi/ic_reading_action_refresh.png new file mode 100644 index 000000000..0d186a2e8 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_reading_action_refresh.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_reading_action_send.png b/app/src/main/res/drawable-hdpi/ic_reading_action_send.png new file mode 100644 index 000000000..ef2547a69 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_reading_action_send.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_reading_action_settings.png b/app/src/main/res/drawable-hdpi/ic_reading_action_settings.png new file mode 100644 index 000000000..58d9beacf Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_reading_action_settings.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_reply.png b/app/src/main/res/drawable-hdpi/ic_reply.png new file mode 100644 index 000000000..ac9b64466 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_reply.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_setting_about_dark.png b/app/src/main/res/drawable-hdpi/ic_setting_about_dark.png new file mode 100644 index 000000000..b5a1d4e5a Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_setting_about_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_setting_about_light.png b/app/src/main/res/drawable-hdpi/ic_setting_about_light.png new file mode 100644 index 000000000..c7555c21c Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_setting_about_light.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_setting_advanced_dark.png b/app/src/main/res/drawable-hdpi/ic_setting_advanced_dark.png new file mode 100644 index 000000000..ba3182ab3 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_setting_advanced_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_setting_advanced_light.png b/app/src/main/res/drawable-hdpi/ic_setting_advanced_light.png new file mode 100644 index 000000000..dcf4ad145 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_setting_advanced_light.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_setting_display_dark.png b/app/src/main/res/drawable-hdpi/ic_setting_display_dark.png new file mode 100644 index 000000000..cc33863a7 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_setting_display_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_setting_display_light.png b/app/src/main/res/drawable-hdpi/ic_setting_display_light.png new file mode 100644 index 000000000..b2a422c5c Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_setting_display_light.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_setting_download_dark.png b/app/src/main/res/drawable-hdpi/ic_setting_download_dark.png new file mode 100644 index 000000000..42292689a Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_setting_download_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_setting_download_light.png b/app/src/main/res/drawable-hdpi/ic_setting_download_light.png new file mode 100644 index 000000000..2e8ad83ca Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_setting_download_light.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_setting_panda_dark.png b/app/src/main/res/drawable-hdpi/ic_setting_panda_dark.png new file mode 100644 index 000000000..56998b6d2 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_setting_panda_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_setting_panda_light.png b/app/src/main/res/drawable-hdpi/ic_setting_panda_light.png new file mode 100644 index 000000000..d3881d9ce Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_setting_panda_light.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_setting_read_dark.png b/app/src/main/res/drawable-hdpi/ic_setting_read_dark.png new file mode 100644 index 000000000..47688fe77 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_setting_read_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_setting_read_light.png b/app/src/main/res/drawable-hdpi/ic_setting_read_light.png new file mode 100644 index 000000000..ed2b7f5b3 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_setting_read_light.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_similar.png b/app/src/main/res/drawable-hdpi/ic_similar.png new file mode 100644 index 000000000..fcbec34f6 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_similar.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_stat_eh.png b/app/src/main/res/drawable-hdpi/ic_stat_eh.png new file mode 100644 index 000000000..4b7d9b4db Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_stat_eh.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_touch.png b/app/src/main/res/drawable-hdpi/ic_touch.png new file mode 100644 index 000000000..e5c39e554 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_touch.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_warning.png b/app/src/main/res/drawable-hdpi/ic_warning.png new file mode 100644 index 000000000..28df816d8 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_warning.png differ diff --git a/app/src/main/res/drawable-hdpi/toast_frame.9.png b/app/src/main/res/drawable-hdpi/toast_frame.9.png new file mode 100644 index 000000000..a804a8a94 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/toast_frame.9.png differ diff --git a/app/src/main/res/drawable-mdpi/btn_check_off.png b/app/src/main/res/drawable-mdpi/btn_check_off.png new file mode 100644 index 000000000..f497bd0f3 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/btn_check_off.png differ diff --git a/app/src/main/res/drawable-mdpi/btn_check_off_dark.png b/app/src/main/res/drawable-mdpi/btn_check_off_dark.png new file mode 100644 index 000000000..1217af98f Binary files /dev/null and b/app/src/main/res/drawable-mdpi/btn_check_off_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/btn_check_off_dark_disabled.png b/app/src/main/res/drawable-mdpi/btn_check_off_dark_disabled.png new file mode 100644 index 000000000..99a3b6c12 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/btn_check_off_dark_disabled.png differ diff --git a/app/src/main/res/drawable-mdpi/btn_check_off_disabled.png b/app/src/main/res/drawable-mdpi/btn_check_off_disabled.png new file mode 100644 index 000000000..22b05c9ef Binary files /dev/null and b/app/src/main/res/drawable-mdpi/btn_check_off_disabled.png differ diff --git a/app/src/main/res/drawable-mdpi/btn_check_on.png b/app/src/main/res/drawable-mdpi/btn_check_on.png new file mode 100644 index 000000000..02b6b0981 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/btn_check_on.png differ diff --git a/app/src/main/res/drawable-mdpi/btn_check_on_dark.png b/app/src/main/res/drawable-mdpi/btn_check_on_dark.png new file mode 100644 index 000000000..8cd06bf0d Binary files /dev/null and b/app/src/main/res/drawable-mdpi/btn_check_on_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/btn_check_on_dark_disabled.png b/app/src/main/res/drawable-mdpi/btn_check_on_dark_disabled.png new file mode 100644 index 000000000..087e0d0be Binary files /dev/null and b/app/src/main/res/drawable-mdpi/btn_check_on_dark_disabled.png differ diff --git a/app/src/main/res/drawable-mdpi/btn_check_on_disabled.png b/app/src/main/res/drawable-mdpi/btn_check_on_disabled.png new file mode 100644 index 000000000..c6e848a17 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/btn_check_on_disabled.png differ diff --git a/app/src/main/res/drawable-mdpi/btn_radio_off.png b/app/src/main/res/drawable-mdpi/btn_radio_off.png new file mode 100644 index 000000000..6c80b6f0d Binary files /dev/null and b/app/src/main/res/drawable-mdpi/btn_radio_off.png differ diff --git a/app/src/main/res/drawable-mdpi/btn_radio_off_dark.png b/app/src/main/res/drawable-mdpi/btn_radio_off_dark.png new file mode 100644 index 000000000..6a9ad95d2 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/btn_radio_off_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/btn_radio_off_dark_disabled.png b/app/src/main/res/drawable-mdpi/btn_radio_off_dark_disabled.png new file mode 100644 index 000000000..2a3f16f3a Binary files /dev/null and b/app/src/main/res/drawable-mdpi/btn_radio_off_dark_disabled.png differ diff --git a/app/src/main/res/drawable-mdpi/btn_radio_off_disabled.png b/app/src/main/res/drawable-mdpi/btn_radio_off_disabled.png new file mode 100644 index 000000000..8579198b3 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/btn_radio_off_disabled.png differ diff --git a/app/src/main/res/drawable-mdpi/btn_radio_on.png b/app/src/main/res/drawable-mdpi/btn_radio_on.png new file mode 100644 index 000000000..5f467dd26 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/btn_radio_on.png differ diff --git a/app/src/main/res/drawable-mdpi/btn_radio_on_dark.png b/app/src/main/res/drawable-mdpi/btn_radio_on_dark.png new file mode 100644 index 000000000..c1b88ed3f Binary files /dev/null and b/app/src/main/res/drawable-mdpi/btn_radio_on_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/btn_radio_on_dark_disabled.png b/app/src/main/res/drawable-mdpi/btn_radio_on_dark_disabled.png new file mode 100644 index 000000000..6d72a1947 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/btn_radio_on_dark_disabled.png differ diff --git a/app/src/main/res/drawable-mdpi/btn_radio_on_disabled.png b/app/src/main/res/drawable-mdpi/btn_radio_on_disabled.png new file mode 100644 index 000000000..a16f5e04c Binary files /dev/null and b/app/src/main/res/drawable-mdpi/btn_radio_on_disabled.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_add.png b/app/src/main/res/drawable-mdpi/ic_action_add.png new file mode 100644 index 000000000..1f50f9af9 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_add.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_clear.png b/app/src/main/res/drawable-mdpi/ic_action_clear.png new file mode 100644 index 000000000..302763f35 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_clear.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_close.png b/app/src/main/res/drawable-mdpi/ic_action_close.png new file mode 100644 index 000000000..e105a646a Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_close.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_core_overflow.png b/app/src/main/res/drawable-mdpi/ic_action_core_overflow.png new file mode 100644 index 000000000..ed30fe90b Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_core_overflow.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_delete.png b/app/src/main/res/drawable-mdpi/ic_action_delete.png new file mode 100644 index 000000000..e268fc5a3 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_delete.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_filter.png b/app/src/main/res/drawable-mdpi/ic_action_filter.png new file mode 100644 index 000000000..f2bc9ca78 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_filter.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_list.png b/app/src/main/res/drawable-mdpi/ic_action_list.png new file mode 100644 index 000000000..6204bb0f2 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_list.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_move.png b/app/src/main/res/drawable-mdpi/ic_action_move.png new file mode 100644 index 000000000..e0b3275e1 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_move.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_redo.png b/app/src/main/res/drawable-mdpi/ic_action_redo.png new file mode 100644 index 000000000..626cf54fc Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_redo.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_search.png b/app/src/main/res/drawable-mdpi/ic_action_search.png new file mode 100644 index 000000000..168e0405c Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_search.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_start.png b/app/src/main/res/drawable-mdpi/ic_action_start.png new file mode 100644 index 000000000..1c2420b12 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_start.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_stop.png b/app/src/main/res/drawable-mdpi/ic_action_stop.png new file mode 100644 index 000000000..74425f147 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_stop.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_voice_search.png b/app/src/main/res/drawable-mdpi/ic_action_voice_search.png new file mode 100644 index 000000000..a2c43aaef Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_voice_search.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_checkmark.png b/app/src/main/res/drawable-mdpi/ic_checkmark.png new file mode 100644 index 000000000..5f3bad307 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_checkmark.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_dir_dark_dialog.png b/app/src/main/res/drawable-mdpi/ic_dir_dark_dialog.png new file mode 100644 index 000000000..2c6580719 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_dir_dark_dialog.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_drawer_download.png b/app/src/main/res/drawable-mdpi/ic_drawer_download.png new file mode 100644 index 000000000..8c8beb8ca Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_drawer_download.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_drawer_favorite.png b/app/src/main/res/drawable-mdpi/ic_drawer_favorite.png new file mode 100644 index 000000000..83786475b Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_drawer_favorite.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_drawer_history.png b/app/src/main/res/drawable-mdpi/ic_drawer_history.png new file mode 100644 index 000000000..7e40d37c1 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_drawer_history.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_drawer_home.png b/app/src/main/res/drawable-mdpi/ic_drawer_home.png new file mode 100644 index 000000000..19f2a14dc Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_drawer_home.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_drawer_panda.png b/app/src/main/res/drawable-mdpi/ic_drawer_panda.png new file mode 100644 index 000000000..a9b274b62 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_drawer_panda.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_drawer_search.png b/app/src/main/res/drawable-mdpi/ic_drawer_search.png new file mode 100644 index 000000000..fdbfd229b Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_drawer_search.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_drawer_settings.png b/app/src/main/res/drawable-mdpi/ic_drawer_settings.png new file mode 100644 index 000000000..32eae12ec Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_drawer_settings.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_drawer_whatshot.png b/app/src/main/res/drawable-mdpi/ic_drawer_whatshot.png new file mode 100644 index 000000000..c1a9b8886 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_drawer_whatshot.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_favorite.png b/app/src/main/res/drawable-mdpi/ic_favorite.png new file mode 100644 index 000000000..7aed1698d Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_favorite.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_more_of_uploader.png b/app/src/main/res/drawable-mdpi/ic_more_of_uploader.png new file mode 100644 index 000000000..edcaa3716 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_more_of_uploader.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_rate.png b/app/src/main/res/drawable-mdpi/ic_rate.png new file mode 100644 index 000000000..72c0f0784 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_rate.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_reading_action_refresh.png b/app/src/main/res/drawable-mdpi/ic_reading_action_refresh.png new file mode 100644 index 000000000..f4bc6a47c Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_reading_action_refresh.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_reading_action_send.png b/app/src/main/res/drawable-mdpi/ic_reading_action_send.png new file mode 100644 index 000000000..afc3523bc Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_reading_action_send.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_reading_action_settings.png b/app/src/main/res/drawable-mdpi/ic_reading_action_settings.png new file mode 100644 index 000000000..848c33ad7 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_reading_action_settings.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_reply.png b/app/src/main/res/drawable-mdpi/ic_reply.png new file mode 100644 index 000000000..561a015eb Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_reply.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_setting_about_dark.png b/app/src/main/res/drawable-mdpi/ic_setting_about_dark.png new file mode 100644 index 000000000..15de3a756 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_setting_about_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_setting_about_light.png b/app/src/main/res/drawable-mdpi/ic_setting_about_light.png new file mode 100644 index 000000000..b24a03ad2 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_setting_about_light.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_setting_advanced_dark.png b/app/src/main/res/drawable-mdpi/ic_setting_advanced_dark.png new file mode 100644 index 000000000..ac8f928cc Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_setting_advanced_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_setting_advanced_light.png b/app/src/main/res/drawable-mdpi/ic_setting_advanced_light.png new file mode 100644 index 000000000..67bb5b2cf Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_setting_advanced_light.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_setting_display_dark.png b/app/src/main/res/drawable-mdpi/ic_setting_display_dark.png new file mode 100644 index 000000000..f5257543d Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_setting_display_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_setting_display_light.png b/app/src/main/res/drawable-mdpi/ic_setting_display_light.png new file mode 100644 index 000000000..6d538956f Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_setting_display_light.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_setting_download_dark.png b/app/src/main/res/drawable-mdpi/ic_setting_download_dark.png new file mode 100644 index 000000000..4471cd160 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_setting_download_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_setting_download_light.png b/app/src/main/res/drawable-mdpi/ic_setting_download_light.png new file mode 100644 index 000000000..618e756f3 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_setting_download_light.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_setting_panda_dark.png b/app/src/main/res/drawable-mdpi/ic_setting_panda_dark.png new file mode 100644 index 000000000..299705998 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_setting_panda_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_setting_panda_light.png b/app/src/main/res/drawable-mdpi/ic_setting_panda_light.png new file mode 100644 index 000000000..6ef4e2ff0 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_setting_panda_light.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_setting_read_dark.png b/app/src/main/res/drawable-mdpi/ic_setting_read_dark.png new file mode 100644 index 000000000..d5582975d Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_setting_read_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_setting_read_light.png b/app/src/main/res/drawable-mdpi/ic_setting_read_light.png new file mode 100644 index 000000000..25de69238 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_setting_read_light.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_similar.png b/app/src/main/res/drawable-mdpi/ic_similar.png new file mode 100644 index 000000000..621bd9517 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_similar.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_stat_eh.png b/app/src/main/res/drawable-mdpi/ic_stat_eh.png new file mode 100644 index 000000000..f2c5e340c Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_stat_eh.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_touch.png b/app/src/main/res/drawable-mdpi/ic_touch.png new file mode 100644 index 000000000..f7101db31 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_touch.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_warning.png b/app/src/main/res/drawable-mdpi/ic_warning.png new file mode 100644 index 000000000..20c65f2c4 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_warning.png differ diff --git a/app/src/main/res/drawable-mdpi/toast_frame.9.png b/app/src/main/res/drawable-mdpi/toast_frame.9.png new file mode 100644 index 000000000..778e4e676 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/toast_frame.9.png differ diff --git a/app/src/main/res/drawable-xhdpi/btn_check_off.png b/app/src/main/res/drawable-xhdpi/btn_check_off.png new file mode 100644 index 000000000..f80d00383 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/btn_check_off.png differ diff --git a/app/src/main/res/drawable-xhdpi/btn_check_off_dark.png b/app/src/main/res/drawable-xhdpi/btn_check_off_dark.png new file mode 100644 index 000000000..f2235b963 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/btn_check_off_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/btn_check_off_dark_disabled.png b/app/src/main/res/drawable-xhdpi/btn_check_off_dark_disabled.png new file mode 100644 index 000000000..eef768156 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/btn_check_off_dark_disabled.png differ diff --git a/app/src/main/res/drawable-xhdpi/btn_check_off_disabled.png b/app/src/main/res/drawable-xhdpi/btn_check_off_disabled.png new file mode 100644 index 000000000..f0d1cac77 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/btn_check_off_disabled.png differ diff --git a/app/src/main/res/drawable-xhdpi/btn_check_on.png b/app/src/main/res/drawable-xhdpi/btn_check_on.png new file mode 100644 index 000000000..f1c57ee89 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/btn_check_on.png differ diff --git a/app/src/main/res/drawable-xhdpi/btn_check_on_dark.png b/app/src/main/res/drawable-xhdpi/btn_check_on_dark.png new file mode 100644 index 000000000..b3c8bd17e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/btn_check_on_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/btn_check_on_dark_disabled.png b/app/src/main/res/drawable-xhdpi/btn_check_on_dark_disabled.png new file mode 100644 index 000000000..33a7b2303 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/btn_check_on_dark_disabled.png differ diff --git a/app/src/main/res/drawable-xhdpi/btn_check_on_disabled.png b/app/src/main/res/drawable-xhdpi/btn_check_on_disabled.png new file mode 100644 index 000000000..f29b47720 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/btn_check_on_disabled.png differ diff --git a/app/src/main/res/drawable-xhdpi/btn_radio_off.png b/app/src/main/res/drawable-xhdpi/btn_radio_off.png new file mode 100644 index 000000000..a3dc979ba Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/btn_radio_off.png differ diff --git a/app/src/main/res/drawable-xhdpi/btn_radio_off_dark.png b/app/src/main/res/drawable-xhdpi/btn_radio_off_dark.png new file mode 100644 index 000000000..08d505783 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/btn_radio_off_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/btn_radio_off_dark_disabled.png b/app/src/main/res/drawable-xhdpi/btn_radio_off_dark_disabled.png new file mode 100644 index 000000000..576b92f10 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/btn_radio_off_dark_disabled.png differ diff --git a/app/src/main/res/drawable-xhdpi/btn_radio_off_disabled.png b/app/src/main/res/drawable-xhdpi/btn_radio_off_disabled.png new file mode 100644 index 000000000..db27b1e17 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/btn_radio_off_disabled.png differ diff --git a/app/src/main/res/drawable-xhdpi/btn_radio_on.png b/app/src/main/res/drawable-xhdpi/btn_radio_on.png new file mode 100644 index 000000000..7e7e401e2 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/btn_radio_on.png differ diff --git a/app/src/main/res/drawable-xhdpi/btn_radio_on_dark.png b/app/src/main/res/drawable-xhdpi/btn_radio_on_dark.png new file mode 100644 index 000000000..0a24dcd1c Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/btn_radio_on_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/btn_radio_on_dark_disabled.png b/app/src/main/res/drawable-xhdpi/btn_radio_on_dark_disabled.png new file mode 100644 index 000000000..76277477a Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/btn_radio_on_dark_disabled.png differ diff --git a/app/src/main/res/drawable-xhdpi/btn_radio_on_disabled.png b/app/src/main/res/drawable-xhdpi/btn_radio_on_disabled.png new file mode 100644 index 000000000..918ebef7a Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/btn_radio_on_disabled.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_add.png b/app/src/main/res/drawable-xhdpi/ic_action_add.png new file mode 100644 index 000000000..d944ead9e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_add.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_clear.png b/app/src/main/res/drawable-xhdpi/ic_action_clear.png new file mode 100644 index 000000000..89decfbf9 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_clear.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_close.png b/app/src/main/res/drawable-xhdpi/ic_action_close.png new file mode 100644 index 000000000..4bc0c7773 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_close.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_core_overflow.png b/app/src/main/res/drawable-xhdpi/ic_action_core_overflow.png new file mode 100644 index 000000000..5d6318437 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_core_overflow.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_delete.png b/app/src/main/res/drawable-xhdpi/ic_action_delete.png new file mode 100644 index 000000000..78179ad79 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_delete.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_filter.png b/app/src/main/res/drawable-xhdpi/ic_action_filter.png new file mode 100644 index 000000000..3a7d2ac36 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_filter.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_list.png b/app/src/main/res/drawable-xhdpi/ic_action_list.png new file mode 100644 index 000000000..8fadb7eb6 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_list.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_move.png b/app/src/main/res/drawable-xhdpi/ic_action_move.png new file mode 100644 index 000000000..8b195ec5e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_move.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_redo.png b/app/src/main/res/drawable-xhdpi/ic_action_redo.png new file mode 100644 index 000000000..329cb60cd Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_redo.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_search.png b/app/src/main/res/drawable-xhdpi/ic_action_search.png new file mode 100644 index 000000000..e57f46cfb Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_search.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_start.png b/app/src/main/res/drawable-xhdpi/ic_action_start.png new file mode 100644 index 000000000..3633a202c Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_start.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_stop.png b/app/src/main/res/drawable-xhdpi/ic_action_stop.png new file mode 100644 index 000000000..3e911d45b Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_stop.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_voice_search.png b/app/src/main/res/drawable-xhdpi/ic_action_voice_search.png new file mode 100644 index 000000000..8e746d597 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_voice_search.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_checkmark.png b/app/src/main/res/drawable-xhdpi/ic_checkmark.png new file mode 100644 index 000000000..799f8bfdb Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_checkmark.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_dir_dark_dialog.png b/app/src/main/res/drawable-xhdpi/ic_dir_dark_dialog.png new file mode 100644 index 000000000..f2e1b5c6a Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_dir_dark_dialog.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_drawer_download.png b/app/src/main/res/drawable-xhdpi/ic_drawer_download.png new file mode 100644 index 000000000..42292689a Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_drawer_download.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_drawer_favorite.png b/app/src/main/res/drawable-xhdpi/ic_drawer_favorite.png new file mode 100644 index 000000000..8cc62fc16 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_drawer_favorite.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_drawer_history.png b/app/src/main/res/drawable-xhdpi/ic_drawer_history.png new file mode 100644 index 000000000..aa689ee97 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_drawer_history.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_drawer_home.png b/app/src/main/res/drawable-xhdpi/ic_drawer_home.png new file mode 100644 index 000000000..a4980c665 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_drawer_home.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_drawer_panda.png b/app/src/main/res/drawable-xhdpi/ic_drawer_panda.png new file mode 100644 index 000000000..56998b6d2 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_drawer_panda.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_drawer_search.png b/app/src/main/res/drawable-xhdpi/ic_drawer_search.png new file mode 100644 index 000000000..d59b8f370 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_drawer_search.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_drawer_settings.png b/app/src/main/res/drawable-xhdpi/ic_drawer_settings.png new file mode 100644 index 000000000..adf96ce1f Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_drawer_settings.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_drawer_whatshot.png b/app/src/main/res/drawable-xhdpi/ic_drawer_whatshot.png new file mode 100644 index 000000000..9232a187d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_drawer_whatshot.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_favorite.png b/app/src/main/res/drawable-xhdpi/ic_favorite.png new file mode 100644 index 000000000..7ae2a47ef Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_favorite.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_more_of_uploader.png b/app/src/main/res/drawable-xhdpi/ic_more_of_uploader.png new file mode 100644 index 000000000..a445a66f2 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_more_of_uploader.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_rate.png b/app/src/main/res/drawable-xhdpi/ic_rate.png new file mode 100644 index 000000000..9a8e02cc4 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_rate.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_reading_action_refresh.png b/app/src/main/res/drawable-xhdpi/ic_reading_action_refresh.png new file mode 100644 index 000000000..0250a1b73 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_reading_action_refresh.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_reading_action_send.png b/app/src/main/res/drawable-xhdpi/ic_reading_action_send.png new file mode 100644 index 000000000..ac71dfd49 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_reading_action_send.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_reading_action_settings.png b/app/src/main/res/drawable-xhdpi/ic_reading_action_settings.png new file mode 100644 index 000000000..e718c0d8c Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_reading_action_settings.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_reply.png b/app/src/main/res/drawable-xhdpi/ic_reply.png new file mode 100644 index 000000000..94faebae0 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_reply.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_setting_about_dark.png b/app/src/main/res/drawable-xhdpi/ic_setting_about_dark.png new file mode 100644 index 000000000..66458df7a Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_setting_about_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_setting_about_light.png b/app/src/main/res/drawable-xhdpi/ic_setting_about_light.png new file mode 100644 index 000000000..0589e49ab Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_setting_about_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_setting_advanced_dark.png b/app/src/main/res/drawable-xhdpi/ic_setting_advanced_dark.png new file mode 100644 index 000000000..d02839d74 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_setting_advanced_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_setting_advanced_light.png b/app/src/main/res/drawable-xhdpi/ic_setting_advanced_light.png new file mode 100644 index 000000000..ca7f1e1b0 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_setting_advanced_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_setting_display_dark.png b/app/src/main/res/drawable-xhdpi/ic_setting_display_dark.png new file mode 100644 index 000000000..3f673c6af Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_setting_display_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_setting_display_light.png b/app/src/main/res/drawable-xhdpi/ic_setting_display_light.png new file mode 100644 index 000000000..0957109d4 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_setting_display_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_setting_download_dark.png b/app/src/main/res/drawable-xhdpi/ic_setting_download_dark.png new file mode 100644 index 000000000..e18773bcb Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_setting_download_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_setting_download_light.png b/app/src/main/res/drawable-xhdpi/ic_setting_download_light.png new file mode 100644 index 000000000..5637493d2 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_setting_download_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_setting_panda_dark.png b/app/src/main/res/drawable-xhdpi/ic_setting_panda_dark.png new file mode 100644 index 000000000..f5b6e14b4 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_setting_panda_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_setting_panda_light.png b/app/src/main/res/drawable-xhdpi/ic_setting_panda_light.png new file mode 100644 index 000000000..d2f1588ac Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_setting_panda_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_setting_read_dark.png b/app/src/main/res/drawable-xhdpi/ic_setting_read_dark.png new file mode 100644 index 000000000..eabb5a7da Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_setting_read_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_setting_read_light.png b/app/src/main/res/drawable-xhdpi/ic_setting_read_light.png new file mode 100644 index 000000000..cd02d2d99 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_setting_read_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_similar.png b/app/src/main/res/drawable-xhdpi/ic_similar.png new file mode 100644 index 000000000..115c31694 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_similar.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_stat_eh.png b/app/src/main/res/drawable-xhdpi/ic_stat_eh.png new file mode 100644 index 000000000..1bb617fa3 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_stat_eh.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_touch.png b/app/src/main/res/drawable-xhdpi/ic_touch.png new file mode 100644 index 000000000..0b8dc9966 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_touch.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_warning.png b/app/src/main/res/drawable-xhdpi/ic_warning.png new file mode 100644 index 000000000..c4640270c Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_warning.png differ diff --git a/app/src/main/res/drawable-xhdpi/toast_frame.9.png b/app/src/main/res/drawable-xhdpi/toast_frame.9.png new file mode 100644 index 000000000..77e69c72a Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/toast_frame.9.png differ diff --git a/app/src/main/res/drawable-xxhdpi/btn_check_off.png b/app/src/main/res/drawable-xxhdpi/btn_check_off.png new file mode 100644 index 000000000..ccfc04c2e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_check_off.png differ diff --git a/app/src/main/res/drawable-xxhdpi/btn_check_off_dark.png b/app/src/main/res/drawable-xxhdpi/btn_check_off_dark.png new file mode 100644 index 000000000..9d03c2fcb Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_check_off_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/btn_check_off_dark_disabled.png b/app/src/main/res/drawable-xxhdpi/btn_check_off_dark_disabled.png new file mode 100644 index 000000000..c8dad1a4d Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_check_off_dark_disabled.png differ diff --git a/app/src/main/res/drawable-xxhdpi/btn_check_off_disabled.png b/app/src/main/res/drawable-xxhdpi/btn_check_off_disabled.png new file mode 100644 index 000000000..aa0598033 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_check_off_disabled.png differ diff --git a/app/src/main/res/drawable-xxhdpi/btn_check_on.png b/app/src/main/res/drawable-xxhdpi/btn_check_on.png new file mode 100644 index 000000000..c3ca649a2 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_check_on.png differ diff --git a/app/src/main/res/drawable-xxhdpi/btn_check_on_dark.png b/app/src/main/res/drawable-xxhdpi/btn_check_on_dark.png new file mode 100644 index 000000000..94c2a204c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_check_on_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/btn_check_on_dark_disabled.png b/app/src/main/res/drawable-xxhdpi/btn_check_on_dark_disabled.png new file mode 100644 index 000000000..a9eaccdf0 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_check_on_dark_disabled.png differ diff --git a/app/src/main/res/drawable-xxhdpi/btn_check_on_disabled.png b/app/src/main/res/drawable-xxhdpi/btn_check_on_disabled.png new file mode 100644 index 000000000..8bbf651b6 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_check_on_disabled.png differ diff --git a/app/src/main/res/drawable-xxhdpi/btn_radio_off.png b/app/src/main/res/drawable-xxhdpi/btn_radio_off.png new file mode 100644 index 000000000..320a35cae Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_radio_off.png differ diff --git a/app/src/main/res/drawable-xxhdpi/btn_radio_off_dark.png b/app/src/main/res/drawable-xxhdpi/btn_radio_off_dark.png new file mode 100644 index 000000000..3447a9322 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_radio_off_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/btn_radio_off_dark_disabled.png b/app/src/main/res/drawable-xxhdpi/btn_radio_off_dark_disabled.png new file mode 100644 index 000000000..ce56c1abf Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_radio_off_dark_disabled.png differ diff --git a/app/src/main/res/drawable-xxhdpi/btn_radio_off_disabled.png b/app/src/main/res/drawable-xxhdpi/btn_radio_off_disabled.png new file mode 100644 index 000000000..d515d6d45 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_radio_off_disabled.png differ diff --git a/app/src/main/res/drawable-xxhdpi/btn_radio_on.png b/app/src/main/res/drawable-xxhdpi/btn_radio_on.png new file mode 100644 index 000000000..f54566024 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_radio_on.png differ diff --git a/app/src/main/res/drawable-xxhdpi/btn_radio_on_dark.png b/app/src/main/res/drawable-xxhdpi/btn_radio_on_dark.png new file mode 100644 index 000000000..31855618d Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_radio_on_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/btn_radio_on_dark_disabled.png b/app/src/main/res/drawable-xxhdpi/btn_radio_on_dark_disabled.png new file mode 100644 index 000000000..e1bc64b1f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_radio_on_dark_disabled.png differ diff --git a/app/src/main/res/drawable-xxhdpi/btn_radio_on_disabled.png b/app/src/main/res/drawable-xxhdpi/btn_radio_on_disabled.png new file mode 100644 index 000000000..9b63efcfe Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_radio_on_disabled.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_add.png b/app/src/main/res/drawable-xxhdpi/ic_action_add.png new file mode 100644 index 000000000..37064d19a Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_add.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_clear.png b/app/src/main/res/drawable-xxhdpi/ic_action_clear.png new file mode 100644 index 000000000..0a3856346 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_clear.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_close.png b/app/src/main/res/drawable-xxhdpi/ic_action_close.png new file mode 100644 index 000000000..bdf84cfc5 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_close.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_core_overflow.png b/app/src/main/res/drawable-xxhdpi/ic_action_core_overflow.png new file mode 100644 index 000000000..eb6a7e8e7 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_core_overflow.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_delete.png b/app/src/main/res/drawable-xxhdpi/ic_action_delete.png new file mode 100644 index 000000000..27039e6b1 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_delete.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_filter.png b/app/src/main/res/drawable-xxhdpi/ic_action_filter.png new file mode 100644 index 000000000..eb3f7176f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_filter.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_list.png b/app/src/main/res/drawable-xxhdpi/ic_action_list.png new file mode 100644 index 000000000..987afa57c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_list.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_move.png b/app/src/main/res/drawable-xxhdpi/ic_action_move.png new file mode 100644 index 000000000..033bb733b Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_move.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_redo.png b/app/src/main/res/drawable-xxhdpi/ic_action_redo.png new file mode 100644 index 000000000..09d16ec19 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_redo.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_search.png b/app/src/main/res/drawable-xxhdpi/ic_action_search.png new file mode 100644 index 000000000..811d639d1 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_search.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_start.png b/app/src/main/res/drawable-xxhdpi/ic_action_start.png new file mode 100644 index 000000000..0ba37e36f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_start.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_stop.png b/app/src/main/res/drawable-xxhdpi/ic_action_stop.png new file mode 100644 index 000000000..65b614c8c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_stop.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_voice_search.png b/app/src/main/res/drawable-xxhdpi/ic_action_voice_search.png new file mode 100644 index 000000000..17c18d0ac Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_voice_search.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_checkmark.png b/app/src/main/res/drawable-xxhdpi/ic_checkmark.png new file mode 100644 index 000000000..bb577d0b1 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_checkmark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_dir_dark_dialog.png b/app/src/main/res/drawable-xxhdpi/ic_dir_dark_dialog.png new file mode 100644 index 000000000..daa02c347 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_dir_dark_dialog.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_drawer_download.png b/app/src/main/res/drawable-xxhdpi/ic_drawer_download.png new file mode 100644 index 000000000..44790c929 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_drawer_download.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_drawer_favorite.png b/app/src/main/res/drawable-xxhdpi/ic_drawer_favorite.png new file mode 100644 index 000000000..9b4039219 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_drawer_favorite.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_drawer_history.png b/app/src/main/res/drawable-xxhdpi/ic_drawer_history.png new file mode 100644 index 000000000..c15f80bb9 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_drawer_history.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_drawer_home.png b/app/src/main/res/drawable-xxhdpi/ic_drawer_home.png new file mode 100644 index 000000000..2ffa1c23d Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_drawer_home.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_drawer_panda.png b/app/src/main/res/drawable-xxhdpi/ic_drawer_panda.png new file mode 100644 index 000000000..62a10146e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_drawer_panda.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_drawer_search.png b/app/src/main/res/drawable-xxhdpi/ic_drawer_search.png new file mode 100644 index 000000000..b667092e1 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_drawer_search.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_drawer_settings.png b/app/src/main/res/drawable-xxhdpi/ic_drawer_settings.png new file mode 100644 index 000000000..ec216320f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_drawer_settings.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_drawer_whatshot.png b/app/src/main/res/drawable-xxhdpi/ic_drawer_whatshot.png new file mode 100644 index 000000000..7ca743651 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_drawer_whatshot.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_favorite.png b/app/src/main/res/drawable-xxhdpi/ic_favorite.png new file mode 100644 index 000000000..35af11889 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_favorite.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_more_of_uploader.png b/app/src/main/res/drawable-xxhdpi/ic_more_of_uploader.png new file mode 100644 index 000000000..6f6c1e066 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_more_of_uploader.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_rate.png b/app/src/main/res/drawable-xxhdpi/ic_rate.png new file mode 100644 index 000000000..12eb574a8 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_rate.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_reading_action_refresh.png b/app/src/main/res/drawable-xxhdpi/ic_reading_action_refresh.png new file mode 100644 index 000000000..f7147892a Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_reading_action_refresh.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_reading_action_send.png b/app/src/main/res/drawable-xxhdpi/ic_reading_action_send.png new file mode 100644 index 000000000..4eb22fec7 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_reading_action_send.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_reading_action_settings.png b/app/src/main/res/drawable-xxhdpi/ic_reading_action_settings.png new file mode 100644 index 000000000..4ce6e358c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_reading_action_settings.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_reply.png b/app/src/main/res/drawable-xxhdpi/ic_reply.png new file mode 100644 index 000000000..faa019b2e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_reply.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_setting_about_dark.png b/app/src/main/res/drawable-xxhdpi/ic_setting_about_dark.png new file mode 100644 index 000000000..458344c66 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_setting_about_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_setting_about_light.png b/app/src/main/res/drawable-xxhdpi/ic_setting_about_light.png new file mode 100644 index 000000000..a0c5fedde Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_setting_about_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_setting_advanced_dark.png b/app/src/main/res/drawable-xxhdpi/ic_setting_advanced_dark.png new file mode 100644 index 000000000..1c938ddd9 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_setting_advanced_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_setting_advanced_light.png b/app/src/main/res/drawable-xxhdpi/ic_setting_advanced_light.png new file mode 100644 index 000000000..8036f5903 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_setting_advanced_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_setting_display_dark.png b/app/src/main/res/drawable-xxhdpi/ic_setting_display_dark.png new file mode 100644 index 000000000..78f0ffe0a Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_setting_display_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_setting_display_light.png b/app/src/main/res/drawable-xxhdpi/ic_setting_display_light.png new file mode 100644 index 000000000..1c6cd285b Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_setting_display_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_setting_download_dark.png b/app/src/main/res/drawable-xxhdpi/ic_setting_download_dark.png new file mode 100644 index 000000000..0d4f35944 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_setting_download_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_setting_download_light.png b/app/src/main/res/drawable-xxhdpi/ic_setting_download_light.png new file mode 100644 index 000000000..8fa427143 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_setting_download_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_setting_panda_dark.png b/app/src/main/res/drawable-xxhdpi/ic_setting_panda_dark.png new file mode 100644 index 000000000..f1bd0443b Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_setting_panda_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_setting_panda_light.png b/app/src/main/res/drawable-xxhdpi/ic_setting_panda_light.png new file mode 100644 index 000000000..908235c07 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_setting_panda_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_setting_read_dark.png b/app/src/main/res/drawable-xxhdpi/ic_setting_read_dark.png new file mode 100644 index 000000000..a74596086 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_setting_read_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_setting_read_light.png b/app/src/main/res/drawable-xxhdpi/ic_setting_read_light.png new file mode 100644 index 000000000..10a65a441 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_setting_read_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_similar.png b/app/src/main/res/drawable-xxhdpi/ic_similar.png new file mode 100644 index 000000000..589149fd9 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_similar.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_stat_eh.png b/app/src/main/res/drawable-xxhdpi/ic_stat_eh.png new file mode 100644 index 000000000..8b36c09ed Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_stat_eh.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_touch.png b/app/src/main/res/drawable-xxhdpi/ic_touch.png new file mode 100644 index 000000000..3047663a7 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_touch.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_warning.png b/app/src/main/res/drawable-xxhdpi/ic_warning.png new file mode 100644 index 000000000..425cd4b46 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_warning.png differ diff --git a/app/src/main/res/drawable-xxhdpi/textfield_search_default.9.png b/app/src/main/res/drawable-xxhdpi/textfield_search_default.9.png new file mode 100644 index 000000000..ed011cb5a Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/textfield_search_default.9.png differ diff --git a/app/src/main/res/drawable-xxhdpi/textfield_search_right_default.9.png b/app/src/main/res/drawable-xxhdpi/textfield_search_right_default.9.png new file mode 100644 index 000000000..ff164aca4 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/textfield_search_right_default.9.png differ diff --git a/app/src/main/res/drawable-xxhdpi/textfield_search_right_selected.9.png b/app/src/main/res/drawable-xxhdpi/textfield_search_right_selected.9.png new file mode 100644 index 000000000..cda96f218 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/textfield_search_right_selected.9.png differ diff --git a/app/src/main/res/drawable-xxhdpi/textfield_search_selected.9.png b/app/src/main/res/drawable-xxhdpi/textfield_search_selected.9.png new file mode 100644 index 000000000..521c497a1 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/textfield_search_selected.9.png differ diff --git a/app/src/main/res/drawable-xxhdpi/toast_frame.9.png b/app/src/main/res/drawable-xxhdpi/toast_frame.9.png new file mode 100644 index 000000000..edecb6320 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/toast_frame.9.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/btn_check_off.png b/app/src/main/res/drawable-xxxhdpi/btn_check_off.png new file mode 100644 index 000000000..b9bc7eac0 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/btn_check_off.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/btn_check_off_dark.png b/app/src/main/res/drawable-xxxhdpi/btn_check_off_dark.png new file mode 100644 index 000000000..9159c3b1d Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/btn_check_off_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/btn_check_off_dark_disabled.png b/app/src/main/res/drawable-xxxhdpi/btn_check_off_dark_disabled.png new file mode 100644 index 000000000..76f0be4c7 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/btn_check_off_dark_disabled.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/btn_check_off_disabled.png b/app/src/main/res/drawable-xxxhdpi/btn_check_off_disabled.png new file mode 100644 index 000000000..d565db74a Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/btn_check_off_disabled.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/btn_check_on.png b/app/src/main/res/drawable-xxxhdpi/btn_check_on.png new file mode 100644 index 000000000..cec991f77 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/btn_check_on.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/btn_check_on_dark.png b/app/src/main/res/drawable-xxxhdpi/btn_check_on_dark.png new file mode 100644 index 000000000..d24e665cc Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/btn_check_on_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/btn_check_on_dark_disabled.png b/app/src/main/res/drawable-xxxhdpi/btn_check_on_dark_disabled.png new file mode 100644 index 000000000..256fe8b78 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/btn_check_on_dark_disabled.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/btn_check_on_disabled.png b/app/src/main/res/drawable-xxxhdpi/btn_check_on_disabled.png new file mode 100644 index 000000000..6c0685cc1 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/btn_check_on_disabled.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/btn_radio_off.png b/app/src/main/res/drawable-xxxhdpi/btn_radio_off.png new file mode 100644 index 000000000..0b364f74b Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/btn_radio_off.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/btn_radio_off_dark.png b/app/src/main/res/drawable-xxxhdpi/btn_radio_off_dark.png new file mode 100644 index 000000000..c1fd41f93 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/btn_radio_off_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/btn_radio_off_dark_disabled.png b/app/src/main/res/drawable-xxxhdpi/btn_radio_off_dark_disabled.png new file mode 100644 index 000000000..5d9e81d2d Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/btn_radio_off_dark_disabled.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/btn_radio_off_disabled.png b/app/src/main/res/drawable-xxxhdpi/btn_radio_off_disabled.png new file mode 100644 index 000000000..a67121439 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/btn_radio_off_disabled.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/btn_radio_on.png b/app/src/main/res/drawable-xxxhdpi/btn_radio_on.png new file mode 100644 index 000000000..6acfb0f55 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/btn_radio_on.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/btn_radio_on_dark.png b/app/src/main/res/drawable-xxxhdpi/btn_radio_on_dark.png new file mode 100644 index 000000000..3dc5be66e Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/btn_radio_on_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/btn_radio_on_dark_disabled.png b/app/src/main/res/drawable-xxxhdpi/btn_radio_on_dark_disabled.png new file mode 100644 index 000000000..f9951f378 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/btn_radio_on_dark_disabled.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/btn_radio_on_disabled.png b/app/src/main/res/drawable-xxxhdpi/btn_radio_on_disabled.png new file mode 100644 index 000000000..c63012cde Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/btn_radio_on_disabled.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_add.png b/app/src/main/res/drawable-xxxhdpi/ic_action_add.png new file mode 100644 index 000000000..d4a73b7f3 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_add.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_clear.png b/app/src/main/res/drawable-xxxhdpi/ic_action_clear.png new file mode 100644 index 000000000..cfdc962c4 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_clear.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_close.png b/app/src/main/res/drawable-xxxhdpi/ic_action_close.png new file mode 100644 index 000000000..63ae4b09d Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_close.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_core_overflow.png b/app/src/main/res/drawable-xxxhdpi/ic_action_core_overflow.png new file mode 100644 index 000000000..ea887bdf0 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_core_overflow.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_delete.png b/app/src/main/res/drawable-xxxhdpi/ic_action_delete.png new file mode 100644 index 000000000..03e076e6c Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_delete.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_filter.png b/app/src/main/res/drawable-xxxhdpi/ic_action_filter.png new file mode 100644 index 000000000..94ff150cf Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_filter.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_list.png b/app/src/main/res/drawable-xxxhdpi/ic_action_list.png new file mode 100644 index 000000000..d23716b21 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_list.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_move.png b/app/src/main/res/drawable-xxxhdpi/ic_action_move.png new file mode 100644 index 000000000..8da72a2ce Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_move.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_search.png b/app/src/main/res/drawable-xxxhdpi/ic_action_search.png new file mode 100644 index 000000000..0b20ec928 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_search.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_start.png b/app/src/main/res/drawable-xxxhdpi/ic_action_start.png new file mode 100644 index 000000000..4a992efa6 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_start.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_stop.png b/app/src/main/res/drawable-xxxhdpi/ic_action_stop.png new file mode 100644 index 000000000..35fb7267a Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_stop.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_voice_search.png b/app/src/main/res/drawable-xxxhdpi/ic_action_voice_search.png new file mode 100644 index 000000000..27350532b Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_voice_search.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_drawer_download.png b/app/src/main/res/drawable-xxxhdpi/ic_drawer_download.png new file mode 100644 index 000000000..0d4f35944 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_drawer_download.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_drawer_favorite.png b/app/src/main/res/drawable-xxxhdpi/ic_drawer_favorite.png new file mode 100644 index 000000000..426d4130d Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_drawer_favorite.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_drawer_history.png b/app/src/main/res/drawable-xxxhdpi/ic_drawer_history.png new file mode 100644 index 000000000..df29a9481 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_drawer_history.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_drawer_home.png b/app/src/main/res/drawable-xxxhdpi/ic_drawer_home.png new file mode 100644 index 000000000..f3aa1c8b9 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_drawer_home.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_drawer_panda.png b/app/src/main/res/drawable-xxxhdpi/ic_drawer_panda.png new file mode 100644 index 000000000..f1bd0443b Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_drawer_panda.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_drawer_search.png b/app/src/main/res/drawable-xxxhdpi/ic_drawer_search.png new file mode 100644 index 000000000..7876e8692 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_drawer_search.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_drawer_settings.png b/app/src/main/res/drawable-xxxhdpi/ic_drawer_settings.png new file mode 100644 index 000000000..fff01bdff Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_drawer_settings.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_drawer_whatshot.png b/app/src/main/res/drawable-xxxhdpi/ic_drawer_whatshot.png new file mode 100644 index 000000000..b30b33bb9 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_drawer_whatshot.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_reading_action_refresh.png b/app/src/main/res/drawable-xxxhdpi/ic_reading_action_refresh.png new file mode 100644 index 000000000..dffae4202 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_reading_action_refresh.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_reading_action_send.png b/app/src/main/res/drawable-xxxhdpi/ic_reading_action_send.png new file mode 100644 index 000000000..27f90bdd8 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_reading_action_send.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_reading_action_settings.png b/app/src/main/res/drawable-xxxhdpi/ic_reading_action_settings.png new file mode 100644 index 000000000..e9952bd6c Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_reading_action_settings.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_reply.png b/app/src/main/res/drawable-xxxhdpi/ic_reply.png new file mode 100644 index 000000000..2bda05747 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_reply.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_setting_about_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_setting_about_dark.png new file mode 100644 index 000000000..c193341cb Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_setting_about_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_setting_about_light.png b/app/src/main/res/drawable-xxxhdpi/ic_setting_about_light.png new file mode 100644 index 000000000..54e4563db Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_setting_about_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_setting_advanced_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_setting_advanced_dark.png new file mode 100644 index 000000000..5bea36a0a Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_setting_advanced_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_setting_advanced_light.png b/app/src/main/res/drawable-xxxhdpi/ic_setting_advanced_light.png new file mode 100644 index 000000000..1e68dd961 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_setting_advanced_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_setting_display_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_setting_display_dark.png new file mode 100644 index 000000000..668042f19 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_setting_display_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_setting_display_light.png b/app/src/main/res/drawable-xxxhdpi/ic_setting_display_light.png new file mode 100644 index 000000000..027edf0b8 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_setting_display_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_setting_download_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_setting_download_dark.png new file mode 100644 index 000000000..07a269985 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_setting_download_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_setting_download_light.png b/app/src/main/res/drawable-xxxhdpi/ic_setting_download_light.png new file mode 100644 index 000000000..f5b3e5948 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_setting_download_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_setting_panda_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_setting_panda_dark.png new file mode 100644 index 000000000..5b7015252 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_setting_panda_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_setting_panda_light.png b/app/src/main/res/drawable-xxxhdpi/ic_setting_panda_light.png new file mode 100644 index 000000000..1e72ffc1c Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_setting_panda_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_setting_read_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_setting_read_dark.png new file mode 100644 index 000000000..e82007a6b Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_setting_read_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_setting_read_light.png b/app/src/main/res/drawable-xxxhdpi/ic_setting_read_light.png new file mode 100644 index 000000000..808871ce1 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_setting_read_light.png differ diff --git a/app/src/main/res/drawable/btn_check_material.xml b/app/src/main/res/drawable/btn_check_material.xml new file mode 100644 index 000000000..4376097c3 --- /dev/null +++ b/app/src/main/res/drawable/btn_check_material.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/btn_check_material_dark.xml b/app/src/main/res/drawable/btn_check_material_dark.xml new file mode 100644 index 000000000..fbe1b0664 --- /dev/null +++ b/app/src/main/res/drawable/btn_check_material_dark.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/btn_radio_material.xml b/app/src/main/res/drawable/btn_radio_material.xml new file mode 100644 index 000000000..b07ddb118 --- /dev/null +++ b/app/src/main/res/drawable/btn_radio_material.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/btn_radio_material_dark.xml b/app/src/main/res/drawable/btn_radio_material_dark.xml new file mode 100644 index 000000000..8313f13aa --- /dev/null +++ b/app/src/main/res/drawable/btn_radio_material_dark.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/clickable_bg_purple.xml b/app/src/main/res/drawable/clickable_bg_purple.xml new file mode 100644 index 000000000..0065b845f --- /dev/null +++ b/app/src/main/res/drawable/clickable_bg_purple.xml @@ -0,0 +1,32 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dialog_background_material.xml b/app/src/main/res/drawable/dialog_background_material.xml new file mode 100644 index 000000000..3039bdb3e --- /dev/null +++ b/app/src/main/res/drawable/dialog_background_material.xml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/dialog_background_material_dark.xml b/app/src/main/res/drawable/dialog_background_material_dark.xml new file mode 100644 index 000000000..cc7489f30 --- /dev/null +++ b/app/src/main/res/drawable/dialog_background_material_dark.xml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_setting_about.xml b/app/src/main/res/drawable/ic_setting_about.xml new file mode 100644 index 000000000..8ce846851 --- /dev/null +++ b/app/src/main/res/drawable/ic_setting_about.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_setting_advanced.xml b/app/src/main/res/drawable/ic_setting_advanced.xml new file mode 100644 index 000000000..016800879 --- /dev/null +++ b/app/src/main/res/drawable/ic_setting_advanced.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_setting_display.xml b/app/src/main/res/drawable/ic_setting_display.xml new file mode 100644 index 000000000..fba817a30 --- /dev/null +++ b/app/src/main/res/drawable/ic_setting_display.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_setting_download.xml b/app/src/main/res/drawable/ic_setting_download.xml new file mode 100644 index 000000000..be0733332 --- /dev/null +++ b/app/src/main/res/drawable/ic_setting_download.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_setting_panda.xml b/app/src/main/res/drawable/ic_setting_panda.xml new file mode 100644 index 000000000..cba077c32 --- /dev/null +++ b/app/src/main/res/drawable/ic_setting_panda.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_setting_read.xml b/app/src/main/res/drawable/ic_setting_read.xml new file mode 100644 index 000000000..a293671cb --- /dev/null +++ b/app/src/main/res/drawable/ic_setting_read.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/textfield_searchview.xml b/app/src/main/res/drawable/textfield_searchview.xml new file mode 100644 index 000000000..6d8d9cb1e --- /dev/null +++ b/app/src/main/res/drawable/textfield_searchview.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/textfield_searchview_right.xml b/app/src/main/res/drawable/textfield_searchview_right.xml new file mode 100644 index 000000000..98e361eb8 --- /dev/null +++ b/app/src/main/res/drawable/textfield_searchview_right.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/start_activity.xml b/app/src/main/res/layout-land/start_activity.xml new file mode 100644 index 000000000..dc5abe85c --- /dev/null +++ b/app/src/main/res/layout-land/start_activity.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/actionable_toast_row.xml b/app/src/main/res/layout/actionable_toast_row.xml new file mode 100644 index 000000000..f91745c90 --- /dev/null +++ b/app/src/main/res/layout/actionable_toast_row.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/add_to_favorite.xml b/app/src/main/res/layout/add_to_favorite.xml new file mode 100644 index 000000000..b0c6ccac1 --- /dev/null +++ b/app/src/main/res/layout/add_to_favorite.xml @@ -0,0 +1,39 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/alert_dialog_material.xml b/app/src/main/res/layout/alert_dialog_material.xml new file mode 100644 index 000000000..5a01244f1 --- /dev/null +++ b/app/src/main/res/layout/alert_dialog_material.xml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +