├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── app ├── build.gradle ├── libs │ └── XposedBridgeApi.jar ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── assets │ ├── licenses.html │ └── xposed_init │ ├── java │ └── info │ │ └── papdt │ │ └── swipeback │ │ ├── helper │ │ ├── MySwipeBackHelper.java │ │ ├── Settings.java │ │ ├── Utility.java │ │ └── WindowInsetsColorDrawable.java │ │ ├── mod │ │ ├── ModKK441.java │ │ ├── ModRotationFix.java │ │ ├── ModSDK21.java │ │ └── ModSwipeBack.java │ │ ├── receiver │ │ └── ClassNameReceiver.java │ │ └── ui │ │ ├── adapter │ │ ├── ActivityAdapter.java │ │ └── AppAdapter.java │ │ ├── app │ │ ├── AboutFragment.java │ │ ├── LicenseFragment.java │ │ ├── PerActivityFragment.java │ │ ├── PerAppFragment.java │ │ └── SettingsFragment.java │ │ ├── base │ │ ├── BaseFragment.java │ │ ├── BaseListFragment.java │ │ ├── BasePreferenceFragment.java │ │ └── GlobalActivity.java │ │ ├── model │ │ ├── ActivityModel.java │ │ └── AppModel.java │ │ ├── preference │ │ └── DiscreteSeekBarPreference.java │ │ └── utils │ │ └── UiUtility.java │ └── res │ ├── drawable-hdpi │ ├── ic_action_search.png │ └── ic_settings_backup_restore.png │ ├── drawable-xhdpi │ ├── ic_action_search.png │ ├── ic_launcher.png │ └── ic_settings_backup_restore.png │ ├── drawable-xxhdpi │ ├── ic_action_search_holo.png │ ├── ic_launcher.png │ └── ic_settings_backup_restore.png │ ├── layout │ ├── app.xml │ ├── base.xml │ ├── discrete_seekbar_preference.xml │ ├── list.xml │ └── webview.xml │ ├── menu │ ├── app.xml │ └── settings.xml │ ├── values-zh-rCN │ └── strings.xml │ ├── values │ ├── attrs.xml │ ├── colors.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ ├── about.xml │ └── pref.xml ├── art ├── Logo.psd ├── Logo_FullSize_512.png └── banner.png ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── libraries ├── SwipeBackLayout │ ├── build.gradle │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── me │ │ │ └── imid │ │ │ └── swipebacklayout │ │ │ └── lib │ │ │ ├── SwipeBackLayout.java │ │ │ ├── Utils.java │ │ │ ├── ViewDragHelper.java │ │ │ └── app │ │ │ ├── SwipeBackActivity.java │ │ │ ├── SwipeBackActivityBase.java │ │ │ ├── SwipeBackActivityHelper.java │ │ │ └── SwipeBackPreferenceActivity.java │ │ └── res │ │ ├── drawable-xhdpi │ │ ├── shadow_bottom.png │ │ ├── shadow_left.png │ │ └── shadow_right.png │ │ └── values │ │ ├── attrs.xml │ │ └── styles.xml └── discreteSeekBar │ ├── .gitignore │ ├── build.gradle │ └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── org │ │ └── adw │ │ └── library │ │ └── widgets │ │ └── discreteseekbar │ │ ├── DiscreteSeekBar.java │ │ └── internal │ │ ├── Marker.java │ │ ├── PopupIndicator.java │ │ ├── compat │ │ ├── AnimatorCompat.java │ │ ├── AnimatorCompatV11.java │ │ ├── SeekBarCompat.java │ │ └── SeekBarCompatDontCrash.java │ │ └── drawable │ │ ├── AlmostRippleDrawable.java │ │ ├── MarkerDrawable.java │ │ ├── StateDrawable.java │ │ ├── ThumbDrawable.java │ │ ├── TrackOvalDrawable.java │ │ └── TrackRectDrawable.java │ └── res │ ├── color │ ├── dsb_progress_color_list.xml │ ├── dsb_ripple_color_list.xml │ └── dsb_track_color_list.xml │ └── values │ ├── attrs.xml │ └── styles.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | gen/ 3 | obj/ 4 | libs/*/*.so 5 | .gradle 6 | .idea 7 | build 8 | *.iml 9 | local.properties 10 | signing.properties 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | branches: 3 | only: 4 | - master 5 | before_script: 6 | - wget https://dl-ssl.google.com/android/repository/build-tools_r21.0.2-linux.zip 7 | - unzip build-tools_r21.0.2-linux.zip -d $ANDROID_HOME >> /dev/null 8 | - mv $ANDROID_HOME/android-5.0 $ANDROID_HOME/build-tools/21.0.2 9 | - rm gradlew 10 | - wget https://raw.githubusercontent.com/google/iosched/master/gradlew 11 | - chmod +x gradlew 12 | 13 | script: ./gradlew :app:build 14 | android: 15 | componenents: 16 | - build-tools-21.0.2 17 | - android-21 18 | - extra-android-support 19 | - extra-android-m2repository 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SwipeBack 2 | --- 3 | [![Build Status](https://travis-ci.org/PaperAirplane-Dev-Team/SwipeBack.svg?branch=master)](https://travis-ci.org/PaperAirplane-Dev-Team/SwipeBack) 4 | 5 | ![BANNER](https://raw.githubusercontent.com/PaperAirplane-Dev-Team/SwipeBack/master/art/banner.png) 6 | 7 | ![LOGO](https://raw.githubusercontent.com/PaperAirplane-Dev-Team/SwipeBack/master/art/Logo_FullSize_512.png) 8 | 9 | SwipeBack is an Xposed module which implements a global swipe-to-return gesture inspired by [SwipeBackLayout](https://github.com/Ikew0ng/SwipeBackLayout) but more than swipe-to-return. 10 | 11 | How to build 12 | --- 13 | If you build with gradle - pull branch `master` 14 | If you build with AIDE - pull branch `aide` 15 | 16 | Contributing 17 | --- 18 | 1. Fork 19 | 2. Create a new branch `patch-X`out of branch `master` 20 | 3. Modify 21 | 4. Send a pull request to branch `master` here. Do not send pull request to `aide`. 22 | 5. NOTICE: DO NOT make a commit named "Routine:" or "Merge in xxx after reset to xxx" 23 | 6. NOTICE: DO NOT INCLUDE ANY CHANGE TO BUILD CONFIGS IN REGULAR COMMITS 24 | 25 | Why anothor branch 'aide'? 26 | --- 27 | The branch 'aide' is for builds with the AIDE(Android IDE), which lacks support of adding a jar as a reference. So we have to do some hacks that is not compatible with normal gradle. The two branches are synced automatically by a script. 28 | 29 | License 30 | --- 31 | See [LICENSE](https://raw.githubusercontent.com/PaperAirplane-Dev-Team/SwipeBack/master/LICENSE) 32 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 21 5 | buildToolsVersion "21.0.2" 6 | 7 | signingConfigs { 8 | release 9 | } 10 | defaultConfig { 11 | applicationId "info.papdt.swipeback" 12 | minSdkVersion 14 13 | targetSdkVersion 21 14 | versionCode 3 15 | versionName "1.0.2" 16 | } 17 | 18 | lintOptions { 19 | abortOnError false 20 | } 21 | 22 | buildTypes { 23 | release { 24 | //minifyEnabled false 25 | //proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 26 | signingConfig signingConfigs.release 27 | } 28 | } 29 | } 30 | 31 | dependencies { 32 | compile 'com.android.support:appcompat-v7:+' 33 | provided files('libs/XposedBridgeApi.jar') 34 | compile project(':libraries:SwipeBackLayout') 35 | compile project(':libraries:discreteSeekBar') 36 | } 37 | 38 | def propFile = file('../signing.properties') 39 | if( propFile.canRead() ) { 40 | def Properties p = new Properties() 41 | p.load(new FileInputStream(propFile)) 42 | 43 | if( p!=null 44 | && p.containsKey('STORE_FILE') 45 | && p.containsKey('STORE_PASSWORD') 46 | && p.containsKey('KEY_ALIAS') 47 | && p.containsKey('KEY_PASSWORD') 48 | ) { 49 | println "RELEASE_BUILD: Signing..." 50 | 51 | android.signingConfigs.release.storeFile = file( p['STORE_FILE'] ) 52 | android.signingConfigs.release.storePassword = p['STORE_PASSWORD'] 53 | android.signingConfigs.release.keyAlias = p['KEY_ALIAS'] 54 | android.signingConfigs.release.keyPassword = p['KEY_PASSWORD'] 55 | 56 | } else { 57 | println "RELEASE_BUILD: Required properties in signing.properties are missing" 58 | android.buildTypes.release.signingConfig = null 59 | } 60 | 61 | } else { 62 | println "RELEASE_BUILD: signing.properties not found" 63 | android.buildTypes.release.signingConfig = null 64 | } 65 | -------------------------------------------------------------------------------- /app/libs/XposedBridgeApi.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PaperAirplane-Dev-Team/SwipeBack/62e17c4515b1018f07fdbd4a392751d73c67ed51/app/libs/XposedBridgeApi.jar -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in C:\tools\adt-bundle-windows-x86_64-20131030\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 13 | 16 | 19 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /app/src/main/assets/xposed_init: -------------------------------------------------------------------------------- 1 | info.papdt.swipeback.mod.ModSwipeBack 2 | -------------------------------------------------------------------------------- /app/src/main/java/info/papdt/swipeback/helper/MySwipeBackHelper.java: -------------------------------------------------------------------------------- 1 | package info.papdt.swipeback.helper; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.graphics.PixelFormat; 6 | import android.graphics.drawable.ColorDrawable; 7 | 8 | import me.imid.swipebacklayout.lib.app.SwipeBackActivityHelper; 9 | import me.imid.swipebacklayout.lib.SwipeBackLayout; 10 | 11 | public class MySwipeBackHelper extends SwipeBackActivityHelper 12 | { 13 | private boolean hasSetBackground = false; 14 | 15 | public MySwipeBackHelper(Activity activity) { 16 | super(activity); 17 | } 18 | 19 | @Override 20 | public void onActivityCreate() { 21 | super.onActivityCreate(); 22 | 23 | // Set pixel format on start 24 | mActivity.getWindow().setFormat(PixelFormat.TRANSLUCENT); 25 | 26 | // Set background on swiped 27 | getSwipeBackLayout().addSwipeListener(new SwipeBackLayout.SwipeListener() { 28 | @Override 29 | public void onScrollStateChange(int state, float scrollPercent) { 30 | 31 | } 32 | 33 | @Override 34 | public void onEdgeTouch(int edgeFlag) { 35 | setBackground(); 36 | } 37 | 38 | @Override 39 | public void onScrollOverThreshold() { 40 | 41 | } 42 | }); 43 | } 44 | 45 | public void onFinish() { 46 | setBackground(); 47 | } 48 | 49 | @Override 50 | protected Context getGlobalContext() { 51 | try { 52 | return mActivity.createPackageContext("info.papdt.swipeback", Context.CONTEXT_IGNORE_SECURITY); 53 | } catch (Exception e) { 54 | return super.getGlobalContext(); 55 | } 56 | } 57 | 58 | private void setBackground() { 59 | if (!hasSetBackground) { 60 | mActivity.getWindow().setBackgroundDrawable(new ColorDrawable(0)); 61 | hasSetBackground = true; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/info/papdt/swipeback/helper/Settings.java: -------------------------------------------------------------------------------- 1 | package info.papdt.swipeback.helper; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.util.Log; 6 | 7 | import de.robv.android.xposed.XSharedPreferences; 8 | 9 | import static info.papdt.swipeback.BuildConfig.DEBUG; 10 | 11 | public abstract class Settings 12 | { 13 | private static final String TAG = Settings.class.getSimpleName(); 14 | 15 | public static final String PREF = "pref"; 16 | public static final String GLOBAL = "global"; 17 | public static final String ENABLE = "enable"; 18 | public static final String EDGE = "edge"; 19 | public static final String SENSITIVITY = "sensitivity"; 20 | public static final String LOLLIPOP_HACK = "lollipop_hack"; 21 | public static final String SCROLL_TO_RETURN = "scroll_to_return"; 22 | public static final String NOTIFY_SHORTCUT = "notify_shortcut"; 23 | 24 | // A list of the existing settings keys. Needed for resetting. 25 | private static final String[] KEYS = { 26 | ENABLE, 27 | EDGE, 28 | SENSITIVITY, 29 | LOLLIPOP_HACK, 30 | SCROLL_TO_RETURN, 31 | /* NOTIFY_SHORTCUT does not need resetting */ 32 | }; 33 | 34 | private static XSettings sXSettings; 35 | private static AppSettings sAppSettings; 36 | 37 | public static Settings getInstance(Context context) { 38 | if (context != null) { 39 | if (sAppSettings == null) { 40 | sAppSettings = new AppSettings(context.getApplicationContext()); 41 | } 42 | 43 | return sAppSettings; 44 | } else { 45 | if (sXSettings == null) { 46 | sXSettings = new XSettings(); 47 | } 48 | 49 | return sXSettings; 50 | } 51 | } 52 | 53 | public void reload() { 54 | 55 | } 56 | 57 | // Reset all to default 58 | public void reset(String packageName, String className) { 59 | for (String key : KEYS) { 60 | remove(packageName, className, key); 61 | } 62 | } 63 | 64 | public abstract boolean getBoolean(String packageName, String className, String key, boolean defValue); 65 | public abstract int getInt(String packageName, String className, String key, int defValue); 66 | public abstract void putBoolean(String packageName, String className, String key, boolean value); 67 | public abstract void putInt(String packageName, String className, String key, int value); 68 | public abstract void remove(String packageName, String className, String key); 69 | protected abstract boolean contains(String key); 70 | 71 | protected String fallback(String packageName, String className, String key) { 72 | String keyStr = buildKey(packageName, className, key); 73 | if (contains(keyStr)) { 74 | return keyStr; 75 | } 76 | 77 | if (!className.equals(GLOBAL)) { 78 | keyStr = buildKey(packageName, GLOBAL, key); 79 | 80 | if (DEBUG) { 81 | Log.d(TAG, "falling back to " + keyStr); 82 | } 83 | 84 | if (contains(keyStr)) { 85 | return keyStr; 86 | } 87 | } 88 | 89 | if (!packageName.equals(GLOBAL)) { 90 | keyStr = buildKey(GLOBAL, GLOBAL, key); 91 | 92 | if (DEBUG) { 93 | Log.d(TAG, "falling back to " + keyStr); 94 | } 95 | } 96 | 97 | return keyStr; 98 | } 99 | 100 | private static String buildKey(String packageName, String className, String key) { 101 | return packageName + ":" + className + ":" + key; 102 | } 103 | 104 | private static class XSettings extends Settings { 105 | private XSharedPreferences mPref; 106 | 107 | XSettings() { 108 | mPref = new XSharedPreferences("info.papdt.swipeback", PREF); 109 | mPref.makeWorldReadable(); 110 | } 111 | 112 | @Override 113 | public void reload() { 114 | mPref.reload(); 115 | } 116 | 117 | @Override 118 | public void remove(String packageName, String className, String key) { 119 | throwException(); 120 | } 121 | 122 | @Override 123 | protected boolean contains(String key) { 124 | return mPref.contains(key); 125 | } 126 | 127 | @Override 128 | public boolean getBoolean(String packageName, String className, String key, boolean defValue) { 129 | return mPref.getBoolean(fallback(packageName, className, key), defValue); 130 | } 131 | 132 | @Override 133 | public int getInt(String packageName, String className, String key, int defValue) { 134 | return mPref.getInt(fallback(packageName, className, key), defValue); 135 | } 136 | 137 | @Override 138 | public void putBoolean(String packageName, String className, String key, boolean value) { 139 | throwException(); 140 | } 141 | 142 | @Override 143 | public void putInt(String packageName, String className, String key, int value) { 144 | throwException(); 145 | } 146 | 147 | private void throwException() { 148 | throw new RuntimeException(new IllegalAccessException("Cannot access editor with XSettings")); 149 | } 150 | } 151 | 152 | private static class AppSettings extends Settings { 153 | private SharedPreferences mPref; 154 | 155 | AppSettings(Context context) { 156 | mPref = context.getSharedPreferences(PREF, Context.MODE_WORLD_READABLE); 157 | } 158 | 159 | @Override 160 | public void remove(String packageName, String className, String key) { 161 | mPref.edit().remove(buildKey(packageName, className, key)).commit(); 162 | } 163 | 164 | @Override 165 | protected boolean contains(String key) { 166 | return mPref.contains(key); 167 | } 168 | 169 | @Override 170 | public boolean getBoolean(String packageName, String className, String key, boolean defValue) { 171 | return mPref.getBoolean(fallback(packageName, className, key), defValue); 172 | } 173 | 174 | @Override 175 | public int getInt(String packageName, String className, String key, int defValue) { 176 | return mPref.getInt(fallback(packageName, className, key), defValue); 177 | } 178 | 179 | @Override 180 | public void putBoolean(String packageName, String className, String key, boolean value) { 181 | mPref.edit().putBoolean(buildKey(packageName, className, key), value).commit(); 182 | } 183 | 184 | @Override 185 | public void putInt(String packageName, String className, String key, int value) { 186 | mPref.edit().putInt(buildKey(packageName, className, key), value).commit(); 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /app/src/main/java/info/papdt/swipeback/helper/Utility.java: -------------------------------------------------------------------------------- 1 | package info.papdt.swipeback.helper; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.content.pm.ActivityInfo; 6 | 7 | public class Utility 8 | { 9 | public static boolean isLauncher(Context context, String packageName) { 10 | ActivityInfo homeInfo = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME).resolveActivityInfo(context.getPackageManager(), 0); 11 | if (homeInfo != null) { 12 | return homeInfo.packageName.equals(packageName); 13 | } else { 14 | return false; 15 | } 16 | } 17 | 18 | public static T $(Object obj) { 19 | return (T) obj; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/info/papdt/swipeback/helper/WindowInsetsColorDrawable.java: -------------------------------------------------------------------------------- 1 | package info.papdt.swipeback.helper; 2 | 3 | import android.graphics.Canvas; 4 | import android.graphics.ColorFilter; 5 | import android.graphics.drawable.Drawable; 6 | 7 | public class WindowInsetsColorDrawable extends Drawable 8 | { 9 | private Drawable mDef, mTop; 10 | private int mTopInset = 0; 11 | 12 | public WindowInsetsColorDrawable(Drawable def) { 13 | mDef = def; 14 | } 15 | 16 | @Override 17 | public void draw(Canvas canvas) { 18 | mDef.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); 19 | mDef.draw(canvas); 20 | 21 | if (mTop != null) { 22 | mTop.setBounds(0, 0, canvas.getWidth(), mTopInset); 23 | mTop.draw(canvas); 24 | } 25 | } 26 | 27 | @Override 28 | public void setColorFilter(ColorFilter p1) { 29 | // nothing is here 30 | } 31 | 32 | @Override 33 | public void setAlpha(int alpha) { 34 | mDef.setAlpha(alpha); 35 | 36 | if (mTop != null) { 37 | mTop.setAlpha(alpha); 38 | } 39 | } 40 | 41 | @Override 42 | public int getOpacity() { 43 | return 0; 44 | } 45 | 46 | public void setTopDrawable(Drawable d) { 47 | mTop = d; 48 | invalidateSelf(); 49 | } 50 | 51 | public void setTopInset(int inset) { 52 | mTopInset = inset; 53 | invalidateSelf(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/info/papdt/swipeback/mod/ModKK441.java: -------------------------------------------------------------------------------- 1 | package info.papdt.swipeback.mod; 2 | 3 | import android.os.Build; 4 | 5 | import de.robv.android.xposed.XC_MethodHook; 6 | import de.robv.android.xposed.XC_MethodReplacement; 7 | import static de.robv.android.xposed.XposedHelpers.*; 8 | 9 | // Fixes for KitKat 4.4.{1,2} 10 | public class ModKK441 11 | { 12 | public static void hookKK441() throws Throwable { 13 | if (!(Build.VERSION.RELEASE.equals("4.4.1") || Build.VERSION.RELEASE.equals("4.4.2"))) 14 | return; 15 | 16 | Class ar = findClass("com.android.server.am.ActivityRecord", null); 17 | findAndHookMethod("com.android.server.am.ActivityStack", null, "isActivityOverHome", ar, new XC_MethodReplacement() { 18 | @Override 19 | protected Object replaceHookedMethod(XC_MethodHook.MethodHookParam mhparams) throws Throwable { 20 | // Android 4.4.1 introduced a bug that a stack is not calculated correctly 21 | // All the activities are considered as over Home. 22 | // This replacement is from 4.4 and will fix this. 23 | Object recordTask = getObjectField(mhparams.args[0], "task"); 24 | Object activities = getObjectField(recordTask, "mActivities"); 25 | int rIndex = (Integer) callMethod(activities, "indexOf", mhparams.args[0]); 26 | for (--rIndex; rIndex >= 0; --rIndex) { 27 | // Look down in tasks 28 | final Object blocker = callMethod(activities, "get", rIndex); 29 | boolean finishing = getBooleanField(blocker, "finishing"); 30 | if (!finishing) { 31 | break; 32 | } 33 | } 34 | 35 | // Arrived bottom, but nothing found 36 | if (rIndex < 0) { 37 | return true; 38 | } 39 | 40 | return false; 41 | } 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/info/papdt/swipeback/mod/ModRotationFix.java: -------------------------------------------------------------------------------- 1 | package info.papdt.swipeback.mod; 2 | 3 | import android.app.Activity; 4 | import android.content.pm.ActivityInfo; 5 | import android.database.ContentObserver; 6 | import android.os.Handler; 7 | import android.os.Message; 8 | import android.provider.Settings.System; 9 | 10 | import static de.robv.android.xposed.XposedHelpers.*; 11 | import static info.papdt.swipeback.helper.Utility.*; 12 | 13 | /** 14 | * Fix rotation failure 15 | */ 16 | public class ModRotationFix { 17 | static void fixOnActivityCreate(Activity activity) { 18 | boolean isRotationLocked = System.getInt(activity.getContentResolver(), System.ACCELEROMETER_ROTATION, 0) == 0; 19 | if (!isRotationLocked) { 20 | activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR); 21 | } 22 | ContentObserver observer = new RotateObserver(activity, new RotateHandler(activity)); 23 | activity.getContentResolver().registerContentObserver(System.getUriFor(System.ACCELEROMETER_ROTATION), false, observer); 24 | setAdditionalInstanceField(activity, "rotateObserver", observer); 25 | } 26 | 27 | static void fixOnActivityFinish(Activity activity) { 28 | ContentObserver observer = $(getAdditionalInstanceField(activity, "rotateObserver")); 29 | if (observer != null) 30 | activity.getContentResolver().unregisterContentObserver(observer); 31 | } 32 | 33 | private static class RotateObserver extends ContentObserver { 34 | private Activity mActivity; 35 | private Handler mHandler; 36 | 37 | public RotateObserver(Activity activity, Handler handler) { 38 | super(handler); 39 | mActivity = activity; 40 | mHandler = handler; 41 | } 42 | 43 | @Override 44 | public void onChange(boolean selfChange) { 45 | boolean isRotationLocked = (System.getInt(mActivity.getContentResolver(), System.ACCELEROMETER_ROTATION, 0) == 0); 46 | mHandler.sendMessage(mHandler.obtainMessage(0, isRotationLocked)); 47 | } 48 | } 49 | 50 | private static class RotateHandler extends Handler { 51 | private Activity mActivity; 52 | 53 | public RotateHandler(Activity activity) { 54 | mActivity = activity; 55 | } 56 | 57 | @Override 58 | public void handleMessage(Message msg) { 59 | if (msg.what == 0) { 60 | if (!(Boolean) msg.obj) { 61 | mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR); 62 | } else { 63 | mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED); 64 | } 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/info/papdt/swipeback/mod/ModSDK21.java: -------------------------------------------------------------------------------- 1 | package info.papdt.swipeback.mod; 2 | 3 | import android.annotation.TargetApi; 4 | import android.app.Activity; 5 | import android.content.res.TypedArray; 6 | import android.graphics.drawable.ColorDrawable; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.view.WindowInsets; 10 | 11 | import de.robv.android.xposed.XC_MethodHook; 12 | import static de.robv.android.xposed.XposedHelpers.*; 13 | 14 | import me.imid.swipebacklayout.lib.app.SwipeBackActivityHelper; 15 | 16 | import java.lang.reflect.Method; 17 | 18 | import info.papdt.swipeback.helper.WindowInsetsColorDrawable; 19 | import info.papdt.swipeback.helper.Settings; 20 | import static info.papdt.swipeback.helper.Utility.*; 21 | 22 | public class ModSDK21 23 | { 24 | private static Settings mSettings; 25 | 26 | public static void zygoteSDK21() { 27 | mSettings = Settings.getInstance(null); 28 | findAndHookMethod("com.android.internal.policy.impl.PhoneWindow", null, "setStatusBarColor", int.class, new XC_MethodHook() { 29 | @Override 30 | protected void afterHookedMethod(XC_MethodHook.MethodHookParam mhparams) throws Throwable { 31 | int color = Integer.valueOf(mhparams.args[0].toString()); 32 | if (color == 0) 33 | return; 34 | SwipeBackActivityHelper helper = $(getAdditionalInstanceField(mhparams.thisObject, "helper")); 35 | if (helper != null) { 36 | ViewGroup root = $(helper.getSwipeBackLayout().getChildAt(0)); 37 | View content = root.getChildAt(0); 38 | 39 | if (content.getBackground() instanceof WindowInsetsColorDrawable) { 40 | WindowInsetsColorDrawable d = $(content.getBackground()); 41 | d.setTopDrawable(new ColorDrawable(color)); 42 | ((Method) mhparams.method).invoke(mhparams.thisObject, 0); 43 | } 44 | } 45 | } 46 | }); 47 | } 48 | 49 | public static void afterOnCreateSDK21(SwipeBackActivityHelper helper, Activity activity, String packageName, String className) throws Throwable { 50 | mSettings.reload(); 51 | if (mSettings.getBoolean(packageName, className, Settings.LOLLIPOP_HACK, false)) { 52 | setAdditionalInstanceField(activity.getWindow(), "helper", helper); 53 | } 54 | } 55 | 56 | @TargetApi(21) 57 | public static void afterOnPostCreateSDK21(XC_MethodHook.MethodHookParam mhparams) throws Throwable { 58 | Class internalStyleable = findClass("com.android.internal.R.styleable", null); 59 | int[] internalTheme = $(getStaticObjectField(internalStyleable, "Theme")); 60 | int internalColorPrimary = getStaticIntField(internalStyleable, "Theme_colorPrimaryDark"); 61 | 62 | SwipeBackActivityHelper helper = $(getAdditionalInstanceField(mhparams.thisObject, "helper")); 63 | if (helper != null) { 64 | final Activity activity = $(mhparams.thisObject); 65 | 66 | String packageName = activity.getApplicationInfo().packageName; 67 | String className = activity.getClass().getName(); 68 | 69 | mSettings.reload(); 70 | if (!mSettings.getBoolean(packageName, className, Settings.LOLLIPOP_HACK, false)) 71 | return; 72 | 73 | ViewGroup root = $(helper.getSwipeBackLayout().getChildAt(0)); 74 | View content = root.getChildAt(0); 75 | final WindowInsetsColorDrawable bkg = new WindowInsetsColorDrawable(content.getBackground()); 76 | content.setBackground(bkg); 77 | 78 | TypedArray a = activity.getTheme().obtainStyledAttributes(internalTheme); 79 | int primary = a.getColor(internalColorPrimary, 0); 80 | a.recycle(); 81 | 82 | if (primary != 0) { 83 | bkg.setTopDrawable(new ColorDrawable(primary)); 84 | } else { 85 | content.setSystemUiVisibility(content.getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); 86 | content.setFitsSystemWindows(true); 87 | } 88 | 89 | root.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() { 90 | @Override 91 | public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { 92 | bkg.setTopInset(insets.getSystemWindowInsetTop()); 93 | activity.getWindow().setStatusBarColor(0); 94 | return insets; 95 | } 96 | }); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /app/src/main/java/info/papdt/swipeback/mod/ModSwipeBack.java: -------------------------------------------------------------------------------- 1 | package info.papdt.swipeback.mod; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.content.pm.ActivityInfo; 6 | import android.os.Bundle; 7 | import android.os.Build; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import android.view.WindowManager; 11 | 12 | import de.robv.android.xposed.IXposedHookLoadPackage; 13 | import de.robv.android.xposed.IXposedHookZygoteInit; 14 | import de.robv.android.xposed.XposedBridge; 15 | import de.robv.android.xposed.XC_MethodHook; 16 | import de.robv.android.xposed.XC_MethodReplacement; 17 | import de.robv.android.xposed.callbacks.XC_LoadPackage; 18 | import static de.robv.android.xposed.XposedHelpers.*; 19 | 20 | import me.imid.swipebacklayout.lib.SwipeBackLayout; 21 | import me.imid.swipebacklayout.lib.app.SwipeBackActivityHelper; 22 | 23 | import info.papdt.swipeback.helper.MySwipeBackHelper; 24 | import info.papdt.swipeback.helper.Settings; 25 | import info.papdt.swipeback.receiver.ClassNameReceiver; 26 | import static info.papdt.swipeback.helper.Utility.*; 27 | import static info.papdt.swipeback.BuildConfig.DEBUG; 28 | 29 | public class ModSwipeBack implements IXposedHookLoadPackage, IXposedHookZygoteInit 30 | { 31 | private static final String TAG = ModSwipeBack.class.getSimpleName(); 32 | 33 | private Settings mSettings = Settings.getInstance(null); 34 | 35 | @Override 36 | public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable { 37 | if (lpparam.packageName.equals("android")) { 38 | log("detected android framework"); 39 | try { 40 | Class ar = findClass("com.android.server.am.ActivityRecord", lpparam.classLoader); 41 | 42 | if (ar != null) 43 | hookActivityRecord(ar); 44 | } catch (ClassNotFoundError e) { 45 | // Exception is thrown in pre-Lollipop system 46 | } 47 | } 48 | } 49 | 50 | @Override 51 | public void initZygote(IXposedHookZygoteInit.StartupParam startupParam) throws Throwable { 52 | 53 | try { 54 | Class ar = findClass("com.android.server.am.ActivityRecord", null); 55 | 56 | if (ar != null) 57 | hookActivityRecord(ar); 58 | } catch (ClassNotFoundError e) { 59 | // Exception is thrown in Lollipop 60 | } 61 | 62 | findAndHookMethod(Activity.class, "onCreate", Bundle.class, new XC_MethodHook() { 63 | @Override 64 | protected void afterHookedMethod(XC_MethodHook.MethodHookParam mhparams) throws Throwable { 65 | Activity activity = $(mhparams.thisObject); 66 | 67 | String packageName = activity.getApplicationInfo().packageName; 68 | String className = activity.getClass().getName(); 69 | 70 | if (shouldExclude(packageName, className)) 71 | return; 72 | 73 | if (isLauncher(activity, packageName)) 74 | return; 75 | 76 | SwipeBackActivityHelper helper = new MySwipeBackHelper(activity); 77 | helper.onActivityCreate(); 78 | helper.getSwipeBackLayout().setEnableGesture(true); 79 | 80 | mSettings.reload(); 81 | int edge = mSettings.getInt(packageName, className, Settings.EDGE, SwipeBackLayout.EDGE_LEFT); 82 | helper.getSwipeBackLayout().setEdgeTrackingEnabled(edge); 83 | 84 | int sensitivity = mSettings.getInt(packageName, className, Settings.SENSITIVITY, 100); 85 | helper.getSwipeBackLayout().setSensitivity(activity, (float) sensitivity / 100.0f); 86 | 87 | setAdditionalInstanceField(activity, "helper", helper); 88 | 89 | // Fix rotation 90 | ModRotationFix.fixOnActivityCreate(activity); 91 | 92 | if (Build.VERSION.SDK_INT >= 21) 93 | ModSDK21.afterOnCreateSDK21(helper, activity, packageName, className); 94 | } 95 | }); 96 | 97 | findAndHookMethod(Activity.class, "onPostCreate", Bundle.class, new XC_MethodHook() { 98 | @Override 99 | protected void afterHookedMethod(XC_MethodHook.MethodHookParam mhparams) throws Throwable { 100 | SwipeBackActivityHelper helper = $(getAdditionalInstanceField(mhparams.thisObject, "helper")); 101 | if (helper != null) { 102 | Activity activity = $(mhparams.thisObject); 103 | 104 | int isFloating = getStaticIntField(findClass("com.android.internal.R.styleable", null), "Window_windowIsFloating"); 105 | if (activity.getWindow().getWindowStyle().getBoolean(isFloating, false)) { 106 | setAdditionalInstanceField(mhparams.thisObject, "helper", null); 107 | return; 108 | } 109 | 110 | if ((activity.getWindow().getAttributes().flags & WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER) != 0) 111 | return; 112 | 113 | helper.onPostCreate(); 114 | 115 | if (Build.VERSION.SDK_INT == 21) 116 | ModSDK21.afterOnPostCreateSDK21(mhparams); 117 | } 118 | } 119 | }); 120 | 121 | findAndHookMethod(Activity.class, "onResume", new XC_MethodHook() { 122 | @Override 123 | protected void afterHookedMethod(XC_MethodHook.MethodHookParam mhparams) throws Throwable { 124 | if (Build.VERSION.SDK_INT < 16) 125 | return; 126 | 127 | mSettings.reload(); 128 | if (!mSettings.getBoolean("global", "global", Settings.NOTIFY_SHORTCUT, false)) 129 | return; 130 | 131 | // Notify the user the current class name 132 | Activity activity = $(mhparams.thisObject); 133 | String className = activity.getClass().getName(); 134 | String packageName = activity.getPackageName(); 135 | Intent i = new Intent(ClassNameReceiver.ACTION); 136 | 137 | // Do not include the app itself 138 | if (!packageName.equals("info.papdt.swipeback")) { 139 | i.putExtra(ClassNameReceiver.EXTRA_CLASSNAME, className); 140 | i.putExtra(ClassNameReceiver.EXTRA_PACKAGENAME, packageName); 141 | } 142 | 143 | activity.sendBroadcast(i); 144 | } 145 | }); 146 | 147 | findAndHookMethod(Activity.class, "findViewById", int.class, new XC_MethodHook() { 148 | @Override 149 | protected void afterHookedMethod(XC_MethodHook.MethodHookParam mhparams) throws Throwable { 150 | if (mhparams.getResult() == null) { 151 | SwipeBackActivityHelper helper = $(getAdditionalInstanceField(mhparams.thisObject, "helper")); 152 | if (helper != null) { 153 | mhparams.setResult(helper.findViewById((Integer) mhparams.args[0])); 154 | } 155 | } 156 | } 157 | }); 158 | 159 | findAndHookMethod(Activity.class, "finish", new XC_MethodHook() { 160 | @Override 161 | protected void beforeHookedMethod(XC_MethodHook.MethodHookParam mhparams) throws Throwable { 162 | Activity activity = $(mhparams.thisObject); 163 | 164 | // Fix rotation first 165 | ModRotationFix.fixOnActivityFinish(activity); 166 | 167 | MySwipeBackHelper helper = $(getAdditionalInstanceField(mhparams.thisObject, "helper")); 168 | if (helper != null && helper.getSwipeBackLayout().getScrollPercent() < 1) { 169 | String packageName = activity.getApplicationInfo().packageName; 170 | String className = activity.getClass().getName(); 171 | 172 | mSettings.reload(); 173 | if (!mSettings.getBoolean(packageName, className, Settings.SCROLL_TO_RETURN, false)) 174 | return; 175 | 176 | helper.onFinish(); 177 | Object isFinishing = getAdditionalInstanceField(mhparams.thisObject, "isFinishing"); 178 | 179 | // Replace the default 'finish' by scrollToFinish 180 | if (isFinishing == null || !(Boolean) isFinishing) { 181 | setAdditionalInstanceField(mhparams.thisObject, "isFinishing", true); 182 | mhparams.setResult(null); 183 | helper.getSwipeBackLayout().scrollToFinishActivity(); 184 | } 185 | } 186 | } 187 | }); 188 | 189 | ModKK441.hookKK441(); 190 | 191 | if (Build.VERSION.SDK_INT < 21) 192 | return; 193 | 194 | ModSDK21.zygoteSDK21(); 195 | } 196 | 197 | private void hookActivityRecord(Class clazz) throws Throwable { 198 | XposedBridge.hookAllConstructors(clazz, new XC_MethodHook() { 199 | @Override 200 | protected void afterHookedMethod(XC_MethodHook.MethodHookParam mhparams) throws Throwable { 201 | String packageName = $(getObjectField(mhparams.thisObject, "packageName")); 202 | ActivityInfo activity = $(getObjectField(mhparams.thisObject, "info")); 203 | if (shouldExclude(packageName, activity.name)) 204 | return; 205 | 206 | boolean isHome = false; 207 | if (Build.VERSION.SDK_INT >= 19) { 208 | isHome = (Boolean) callMethod(mhparams.thisObject, "isHomeActivity"); 209 | } else { 210 | isHome = getBooleanField(mhparams.thisObject, "isHomeActivity"); 211 | } 212 | 213 | if (!isHome) { 214 | // fullscreen = false means transparent 215 | setBooleanField(mhparams.thisObject, "fullscreen", false); 216 | } 217 | } 218 | }); 219 | } 220 | 221 | private boolean shouldExclude(String packageName, String className) { 222 | if (packageName.equals("com.android.systemui") || className.contains("InCall")) { 223 | return true; 224 | } else { 225 | mSettings.reload(); 226 | return !mSettings.getBoolean(packageName, className, Settings.ENABLE, true); 227 | } 228 | } 229 | 230 | private static void log(String log) { 231 | if (DEBUG) { 232 | XposedBridge.log(TAG + ": " + log); 233 | } 234 | } 235 | 236 | } 237 | -------------------------------------------------------------------------------- /app/src/main/java/info/papdt/swipeback/receiver/ClassNameReceiver.java: -------------------------------------------------------------------------------- 1 | package info.papdt.swipeback.receiver; 2 | 3 | import android.app.Notification; 4 | import android.app.NotificationManager; 5 | import android.app.PendingIntent; 6 | import android.content.BroadcastReceiver; 7 | import android.content.Context; 8 | import android.content.Intent; 9 | import android.os.Build; 10 | 11 | import info.papdt.swipeback.R; 12 | import info.papdt.swipeback.ui.base.GlobalActivity; 13 | 14 | public class ClassNameReceiver extends BroadcastReceiver 15 | { 16 | public static final String ACTION = "info.papdt.swipeback.intent.CLASS_NAME"; 17 | public static final String EXTRA_CLASSNAME = "classname"; 18 | public static final String EXTRA_PACKAGENAME = "packagename"; 19 | 20 | @Override 21 | public void onReceive(Context context, Intent intent) { 22 | if (Build.VERSION.SDK_INT < 16) 23 | return; 24 | 25 | String packageName = intent.getStringExtra(EXTRA_PACKAGENAME); 26 | String className = intent.getStringExtra(EXTRA_CLASSNAME); 27 | 28 | NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 29 | 30 | if (packageName != null && className != null) { 31 | Notification.Builder builder = new Notification.Builder(context); 32 | 33 | // Notification content 34 | String title = context.getResources().getString(R.string.notify_title); 35 | String summary = context.getResources().getString(R.string.notify_summary); 36 | String content = String.format(context.getResources().getString(R.string.notify_content), packageName, className); 37 | builder.setContentTitle(title); 38 | builder.setContentText(content); 39 | builder.setSmallIcon(android.R.color.transparent); 40 | builder.setPriority(Notification.PRIORITY_LOW); 41 | Notification.BigTextStyle style = new Notification.BigTextStyle(); 42 | style.setBigContentTitle(context.getResources().getString(R.string.notify_title)); 43 | style.bigText(content); 44 | style.setSummaryText(summary); 45 | builder.setStyle(style); 46 | 47 | // Per-app action 48 | Intent i = new Intent(context, GlobalActivity.class); 49 | i.putExtra("pass", packageName); 50 | i.putExtra("fragment", "peract"); 51 | PendingIntent pending = PendingIntent.getActivity(context, 0, i, PendingIntent.FLAG_CANCEL_CURRENT); 52 | builder.addAction(android.R.color.transparent, context.getResources().getString(R.string.notify_app), pending); 53 | 54 | // Per-activity button 55 | i = new Intent(context, GlobalActivity.class); 56 | i.putExtra("pass", packageName + "," + className); 57 | i.putExtra("fragment", "peract"); 58 | pending = PendingIntent.getActivity(context, 1, i, PendingIntent.FLAG_CANCEL_CURRENT); 59 | builder.addAction(android.R.color.transparent, context.getResources().getString(R.string.notify_activity), pending); 60 | builder.setContentIntent(pending); 61 | nm.notify(R.drawable.ic_launcher, builder.build()); 62 | } else { 63 | nm.cancel(R.drawable.ic_launcher); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/info/papdt/swipeback/ui/adapter/ActivityAdapter.java: -------------------------------------------------------------------------------- 1 | package info.papdt.swipeback.ui.adapter; 2 | 3 | import android.content.Context; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.BaseAdapter; 8 | import android.widget.ImageView; 9 | import android.widget.TextView; 10 | 11 | import java.util.List; 12 | 13 | import info.papdt.swipeback.R; 14 | import info.papdt.swipeback.ui.model.ActivityModel; 15 | import static info.papdt.swipeback.ui.utils.UiUtility.*; 16 | 17 | public class ActivityAdapter extends BaseAdapter 18 | { 19 | private List mList; 20 | private LayoutInflater mInflater; 21 | 22 | public ActivityAdapter(Context context, List list) { 23 | mList = list; 24 | mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 25 | } 26 | 27 | @Override 28 | public int getCount() { 29 | return mList.size(); 30 | } 31 | 32 | @Override 33 | public Object getItem(int position) { 34 | return mList.get(position); 35 | } 36 | 37 | @Override 38 | public long getItemId(int position) { 39 | return position; 40 | } 41 | 42 | @Override 43 | public View getView(int position, View convertView, ViewGroup container) { 44 | if (position >= getCount()) 45 | return convertView; 46 | 47 | View v = convertView; 48 | if (v == null) 49 | v = mInflater.inflate(R.layout.app, container, false); 50 | 51 | ActivityModel activity = mList.get(position); 52 | 53 | ImageView icon = $(v, R.id.app_icon); 54 | icon.setVisibility(View.GONE); 55 | 56 | TextView title = $(v, R.id.app_name); 57 | title.setText(activity.title); 58 | 59 | TextView pkg = $(v, R.id.app_pkg); 60 | pkg.setText(activity.className); 61 | 62 | return v; 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/info/papdt/swipeback/ui/adapter/AppAdapter.java: -------------------------------------------------------------------------------- 1 | package info.papdt.swipeback.ui.adapter; 2 | 3 | import android.content.Context; 4 | import android.graphics.drawable.Drawable; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.BaseAdapter; 9 | import android.widget.Filter; 10 | import android.widget.Filterable; 11 | import android.widget.ImageView; 12 | import android.widget.TextView; 13 | 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | 17 | import info.papdt.swipeback.R; 18 | import info.papdt.swipeback.ui.model.AppModel; 19 | import static info.papdt.swipeback.ui.utils.UiUtility.*; 20 | 21 | public class AppAdapter extends BaseAdapter implements Filterable 22 | { 23 | private List mList; 24 | private List mFullList; 25 | private LayoutInflater mInflater; 26 | 27 | public AppAdapter(Context context, List list) { 28 | mList = list; 29 | mFullList = list; 30 | mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 31 | } 32 | 33 | @Override 34 | public int getCount() { 35 | return mList.size(); 36 | } 37 | 38 | @Override 39 | public AppModel getItem(int position) { 40 | return mList.get(position); 41 | } 42 | 43 | @Override 44 | public long getItemId(int position) { 45 | return position; 46 | } 47 | 48 | @Override 49 | public View getView(final int position, View convertView, ViewGroup container) { 50 | if (position >= getCount()) 51 | return convertView; 52 | 53 | View v = convertView; 54 | if (v == null) 55 | v = mInflater.inflate(R.layout.app, container, false); 56 | 57 | final AppModel app = mList.get(position); 58 | 59 | final ImageView icon = $(v, R.id.app_icon); 60 | icon.setTag(position); 61 | Drawable iconDrawable = app.icon != null ? app.icon.get() : null; 62 | if (iconDrawable != null) { 63 | icon.setImageDrawable(iconDrawable); 64 | } else { 65 | icon.setImageDrawable(null); 66 | 67 | // Update recycled drawable 68 | new Thread() { 69 | @Override 70 | public void run() { 71 | app.refreshIcon(); 72 | if (icon.getTag().equals(position)) { 73 | icon.post(new Runnable() { 74 | @Override 75 | public void run() { 76 | if (app.icon != null) 77 | icon.setImageDrawable(app.icon.get()); 78 | } 79 | }); 80 | } 81 | } 82 | }.start(); 83 | } 84 | 85 | TextView title = $(v, R.id.app_name); 86 | title.setText(app.title); 87 | 88 | TextView pkg = $(v, R.id.app_pkg); 89 | pkg.setText(app.packageName); 90 | 91 | return v; 92 | } 93 | 94 | @Override 95 | public Filter getFilter() { 96 | return new Filter() { 97 | @Override 98 | protected FilterResults performFiltering(CharSequence constraint) { 99 | String query = constraint.toString().toLowerCase(); 100 | List filtered = new ArrayList(); 101 | 102 | if (query.equals("")) { 103 | filtered = mFullList; 104 | } else { 105 | for (AppModel app : mFullList) { 106 | if (app.title.toLowerCase().contains(query) 107 | || app.packageName.toLowerCase().contains(query)) 108 | 109 | filtered.add(app); 110 | } 111 | } 112 | 113 | FilterResults result = new FilterResults(); 114 | result.count = filtered.size(); 115 | result.values = filtered; 116 | 117 | return result; 118 | } 119 | 120 | @Override 121 | @SuppressWarnings("unchecked") 122 | protected void publishResults(CharSequence constraint, FilterResults result) { 123 | mList = (List) result.values; 124 | notifyDataSetChanged(); 125 | } 126 | }; 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /app/src/main/java/info/papdt/swipeback/ui/app/AboutFragment.java: -------------------------------------------------------------------------------- 1 | package info.papdt.swipeback.ui.app; 2 | 3 | import android.app.AlertDialog; 4 | import android.content.DialogInterface; 5 | import android.content.Intent; 6 | import android.net.Uri; 7 | import android.preference.Preference; 8 | 9 | import info.papdt.swipeback.R; 10 | import info.papdt.swipeback.ui.base.BasePreferenceFragment; 11 | import info.papdt.swipeback.ui.preference.DiscreteSeekBarPreference; 12 | import static info.papdt.swipeback.ui.utils.UiUtility.*; 13 | 14 | public class AboutFragment extends BasePreferenceFragment 15 | { 16 | private static final String DONATION_URI = "https://www.paypal.com/cgi-bin/webscr?cmd=_xclick&business=xqsx43cxy@126.com&lc=US&amount=%d&item_name=SwipeBack+Donation&no_note=1&no_shipping=1¤cy_code=USD"; 17 | 18 | private static final String VERSION = "version"; 19 | private static final String MADE_BY = "made_by"; 20 | private static final String SOURCE_CODE = "source_code"; 21 | private static final String LICENSE = "license"; 22 | private static final String DONATION = "donation"; 23 | 24 | private Preference mVersion, mMadeBy, mSourceCode, mLicense; 25 | private DiscreteSeekBarPreference mDonation; 26 | 27 | @Override 28 | protected void onPreferenceLoaded() { 29 | setTitle(getString(R.string.about)); 30 | showHomeAsUp(); 31 | 32 | // Obtain preferences 33 | mVersion = $(this, VERSION); 34 | mMadeBy = $(this, MADE_BY); 35 | mSourceCode = $(this, SOURCE_CODE); 36 | mLicense = $(this, LICENSE); 37 | mDonation = $(this, DONATION); 38 | 39 | // TODO: Make donations available 40 | getPreferenceScreen().removePreference(mDonation); 41 | 42 | // Set values 43 | String ver; 44 | try { 45 | ver = getActivity().getPackageManager().getPackageInfo("info.papdt.swipeback", 0).versionName; 46 | } catch (Exception e) { 47 | ver = "?"; 48 | } 49 | mVersion.setSummary(ver); 50 | 51 | $$(mVersion, mMadeBy, mSourceCode, mLicense, mDonation); 52 | } 53 | 54 | @Override 55 | protected int getPreferenceXml() { 56 | return R.xml.about; 57 | } 58 | 59 | @Override 60 | public boolean onPreferenceClick(Preference pref) { 61 | if (pref == mMadeBy) { 62 | Intent i = new Intent(Intent.ACTION_VIEW); 63 | i.setData(Uri.parse("https://github.com/PaperAirplane-Dev-Team")); 64 | startActivity(i); 65 | return true; 66 | } else if (pref == mSourceCode) { 67 | Intent i = new Intent(Intent.ACTION_VIEW); 68 | i.setData(Uri.parse("https://github.com/PaperAirplane-Dev-Team/SwipeBack")); 69 | startActivity(i); 70 | return true; 71 | } else if (pref == mLicense) { 72 | startFragment("license"); 73 | return true; 74 | } else { 75 | return super.onPreferenceClick(pref); 76 | } 77 | } 78 | 79 | @Override 80 | public boolean onPreferenceChange(Preference preference, Object newValue) { 81 | if (preference == mDonation) { 82 | final int amount = (Integer) newValue; 83 | new AlertDialog.Builder(getActivity()) 84 | .setMessage(String.format(getString(R.string.ask_donate), amount)) 85 | .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() { 86 | @Override 87 | public void onClick(DialogInterface p1, int p2) { 88 | 89 | } 90 | }) 91 | .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { 92 | @Override 93 | public void onClick(DialogInterface dialog, int id) { 94 | Intent i = new Intent(Intent.ACTION_VIEW); 95 | i.setData(Uri.parse(String.format(DONATION_URI, amount))); 96 | startActivity(i); 97 | } 98 | }) 99 | .create() 100 | .show(); 101 | return true; 102 | } else { 103 | return super.onPreferenceChange(preference, newValue); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /app/src/main/java/info/papdt/swipeback/ui/app/LicenseFragment.java: -------------------------------------------------------------------------------- 1 | package info.papdt.swipeback.ui.app; 2 | 3 | import android.view.View; 4 | import android.webkit.WebView; 5 | 6 | import info.papdt.swipeback.R; 7 | import info.papdt.swipeback.ui.base.BaseFragment; 8 | import static info.papdt.swipeback.ui.utils.UiUtility.*; 9 | 10 | public class LicenseFragment extends BaseFragment 11 | { 12 | 13 | @Override 14 | protected int getLayoutId() { 15 | return R.layout.webview; 16 | } 17 | 18 | @Override 19 | protected void onFinishInflate(View view) { 20 | showHomeAsUp(); 21 | setTitle(getString(R.string.license)); 22 | WebView w = $(view, R.id.web); 23 | w.loadUrl("file:///android_asset/licenses.html"); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/info/papdt/swipeback/ui/app/PerActivityFragment.java: -------------------------------------------------------------------------------- 1 | package info.papdt.swipeback.ui.app; 2 | 3 | import android.content.pm.ActivityInfo; 4 | import android.content.pm.PackageInfo; 5 | import android.content.pm.PackageManager; 6 | import android.widget.BaseAdapter; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | import info.papdt.swipeback.R; 12 | import info.papdt.swipeback.ui.adapter.ActivityAdapter; 13 | import info.papdt.swipeback.ui.base.BaseListFragment; 14 | import info.papdt.swipeback.ui.base.GlobalActivity; 15 | import info.papdt.swipeback.ui.model.ActivityModel; 16 | 17 | public class PerActivityFragment extends BaseListFragment 18 | { 19 | private BaseAdapter mAdapter; 20 | private String mTitle = "", mDefaultClassName, mPkgName; 21 | private int mDefaultClassPosition = -1; 22 | 23 | @Override 24 | protected BaseAdapter buildAdapter() { 25 | mAdapter = new ActivityAdapter(getActivity(), getItemList()); 26 | return mAdapter; 27 | } 28 | 29 | @Override 30 | protected List loadData(ProgressCallback callback) { 31 | List list = new ArrayList(); 32 | PackageManager pm = getActivity().getPackageManager(); 33 | 34 | if (getExtraPass().contains(",")) { 35 | String[] s = getExtraPass().split(","); 36 | mPkgName = s[0]; 37 | mDefaultClassName = s[1]; 38 | } else { 39 | mPkgName = getExtraPass(); 40 | } 41 | 42 | ActivityInfo[] ai; 43 | 44 | try { 45 | PackageInfo pkg = pm.getPackageInfo(mPkgName, PackageManager.GET_ACTIVITIES); 46 | ai = pkg.activities; 47 | mTitle = pm.getApplicationLabel(pkg.applicationInfo).toString(); 48 | } catch (Exception e) { 49 | ai = new ActivityInfo[0]; 50 | mTitle = getString(R.string.global_short); 51 | } 52 | 53 | // Add the default one 54 | ActivityModel global = new ActivityModel(); 55 | global.className = "global"; 56 | global.title = getString(R.string.global); 57 | list.add(global); 58 | 59 | if (ai != null) { 60 | for (ActivityInfo info : ai) { 61 | ActivityModel activity = new ActivityModel(); 62 | activity.className = info.name; 63 | activity.title = info.loadLabel(pm).toString(); 64 | list.add(activity); 65 | callback.updateProgress(list.size() - 1, ai.length); 66 | 67 | if (activity.className.equals(mDefaultClassName)) { 68 | mDefaultClassPosition = list.size() - 1; 69 | } 70 | } 71 | } 72 | 73 | return list; 74 | } 75 | 76 | @Override 77 | protected void onDataLoaded(List data) { 78 | super.onDataLoaded(data); 79 | if (!mTitle.equals("")) { 80 | showHomeAsUp(); 81 | setTitle(mTitle + " - " + getString(R.string.app_name)); 82 | } 83 | 84 | if (mDefaultClassPosition > 0) { 85 | onItemClick(mDefaultClassPosition); 86 | } 87 | // If size is smaller than 2, which means the app has only one or no activity 88 | // Then we can skip this fragment and go to settings directly 89 | else if (data.size() <= 2) { 90 | onItemClick(0); 91 | getActivity().finish(); 92 | } 93 | } 94 | 95 | @Override 96 | protected void onItemClick(int pos) { 97 | ActivityModel activity = getItemList().get(pos); 98 | startFragment("settings", mPkgName + "," + activity.className + "," + activity.title + "," + mTitle); 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /app/src/main/java/info/papdt/swipeback/ui/app/PerAppFragment.java: -------------------------------------------------------------------------------- 1 | package info.papdt.swipeback.ui.app; 2 | 3 | import android.content.pm.ApplicationInfo; 4 | import android.content.pm.PackageManager; 5 | import android.graphics.drawable.Drawable; 6 | import android.os.Bundle; 7 | import android.view.Menu; 8 | import android.view.MenuItem; 9 | import android.view.MenuInflater; 10 | import android.widget.BaseAdapter; 11 | 12 | import android.support.v4.view.MenuItemCompat; 13 | import android.support.v7.widget.SearchView; 14 | 15 | import java.lang.ref.WeakReference; 16 | import java.text.Collator; 17 | import java.util.ArrayList; 18 | import java.util.Collections; 19 | import java.util.Comparator; 20 | import java.util.List; 21 | 22 | import info.papdt.swipeback.R; 23 | import info.papdt.swipeback.ui.adapter.AppAdapter; 24 | import info.papdt.swipeback.ui.base.BaseListFragment; 25 | import info.papdt.swipeback.ui.base.GlobalActivity; 26 | import info.papdt.swipeback.ui.model.AppModel; 27 | import static info.papdt.swipeback.ui.utils.UiUtility.*; 28 | 29 | public class PerAppFragment extends BaseListFragment 30 | { 31 | private AppAdapter mAdapter; 32 | private MenuItem mSearchItem; 33 | 34 | @Override 35 | public void onCreate(Bundle savedInstanceState) { 36 | super.onCreate(savedInstanceState); 37 | setHasOptionsMenu(true); 38 | } 39 | 40 | @Override 41 | protected BaseAdapter buildAdapter() { 42 | mAdapter = new AppAdapter(getActivity(), getItemList()); 43 | return mAdapter; 44 | } 45 | 46 | @Override 47 | protected List loadData(ProgressCallback callback) { 48 | List list = new ArrayList(); 49 | final PackageManager pm = getActivity().getPackageManager(); 50 | List la = pm.getInstalledApplications(PackageManager.GET_META_DATA); 51 | 52 | for (final ApplicationInfo info : la) { 53 | final AppModel app = new AppModel(); 54 | app.packageName = info.packageName; 55 | app.title = pm.getApplicationLabel(info).toString(); 56 | app.iconRefreshRunnable = new Runnable() { 57 | @Override 58 | public void run() { 59 | app.icon = new WeakReference(pm.getApplicationIcon(info)); 60 | } 61 | }; 62 | list.add(app); 63 | callback.updateProgress(list.size(), la.size()); 64 | } 65 | 66 | Collections.sort(list, new Comparator() { 67 | @Override 68 | public int compare(AppModel p1, AppModel p2) { 69 | return Collator.getInstance().compare(p1.title, p2.title); 70 | } 71 | }); 72 | 73 | // Add the Global config entry 74 | AppModel global = new AppModel(); 75 | global.packageName = "global"; 76 | global.title = getString(R.string.global); 77 | global.icon = null; 78 | list.add(0, global); 79 | 80 | return list; 81 | } 82 | 83 | @Override 84 | protected void onItemClick(int pos) { 85 | startFragment("peract", mAdapter.getItem(pos).packageName); 86 | } 87 | 88 | @Override 89 | public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 90 | inflater.inflate(R.menu.app, menu); 91 | 92 | mSearchItem = menu.findItem(R.id.search); 93 | final SearchView search = $(MenuItemCompat.getActionView(mSearchItem)); 94 | search.setOnQueryTextListener(new SearchView.OnQueryTextListener() { 95 | @Override 96 | public boolean onQueryTextSubmit(String query) { 97 | return false; 98 | } 99 | 100 | @Override 101 | public boolean onQueryTextChange(String query) { 102 | mAdapter.getFilter().filter(query); 103 | return true; 104 | } 105 | }); 106 | 107 | search.setIconified(true); 108 | 109 | MenuItemCompat.setOnActionExpandListener(mSearchItem, new MenuItemCompat.OnActionExpandListener() { 110 | @Override 111 | public boolean onMenuItemActionExpand(MenuItem item) { 112 | showHomeAsUp(); 113 | search.setIconified(false); 114 | return true; 115 | } 116 | 117 | @Override 118 | public boolean onMenuItemActionCollapse(MenuItem p1) { 119 | hideHomeAsUp(); 120 | search.setQuery("", false); 121 | search.setIconified(true); 122 | return true; 123 | } 124 | }); 125 | } 126 | 127 | @Override 128 | public boolean onOptionsItemSelected(MenuItem item) { 129 | switch (item.getItemId()) { 130 | case R.id.about: 131 | startFragment("about"); 132 | return true; 133 | default: 134 | return super.onOptionsItemSelected(item); 135 | } 136 | } 137 | 138 | @Override 139 | protected void onReturn() { 140 | MenuItemCompat.collapseActionView(mSearchItem); 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /app/src/main/java/info/papdt/swipeback/ui/app/SettingsFragment.java: -------------------------------------------------------------------------------- 1 | package info.papdt.swipeback.ui.app; 2 | 3 | import android.app.AlertDialog; 4 | import android.content.DialogInterface; 5 | import android.os.Build; 6 | import android.preference.CheckBoxPreference; 7 | import android.preference.MultiSelectListPreference; 8 | import android.preference.Preference; 9 | import android.preference.SwitchPreference; 10 | import android.view.Menu; 11 | import android.view.MenuInflater; 12 | import android.view.MenuItem; 13 | 14 | import me.imid.swipebacklayout.lib.SwipeBackLayout; 15 | 16 | import java.util.HashSet; 17 | import java.util.Set; 18 | 19 | import info.papdt.swipeback.R; 20 | import info.papdt.swipeback.helper.Settings; 21 | import info.papdt.swipeback.ui.base.BasePreferenceFragment; 22 | import info.papdt.swipeback.ui.preference.DiscreteSeekBarPreference; 23 | import static info.papdt.swipeback.ui.utils.UiUtility.*; 24 | 25 | public class SettingsFragment extends BasePreferenceFragment 26 | { 27 | private static final String EDGE_LEFT = "0", 28 | EDGE_RIGHT = "1", 29 | EDGE_BOTTOM = "2"; 30 | 31 | private Settings mSettings; 32 | 33 | private SwitchPreference mEnable; 34 | private MultiSelectListPreference mEdge; 35 | private DiscreteSeekBarPreference mSensitivity; 36 | private CheckBoxPreference mLollipop, mScrollToReturn, mShortcut; 37 | 38 | private String mPackageName, mClassName; 39 | 40 | @Override 41 | protected int getPreferenceXml() { 42 | return R.xml.pref; 43 | } 44 | 45 | @Override 46 | protected void onPreferenceLoaded() { 47 | initPackage(); 48 | setHasOptionsMenu(true); 49 | mSettings = Settings.getInstance(getActivity()); 50 | 51 | // Obtain preferences 52 | mEnable = $(this, Settings.ENABLE); 53 | mEdge = $(this, Settings.EDGE); 54 | mSensitivity = $(this, Settings.SENSITIVITY); 55 | mLollipop = $(this, Settings.LOLLIPOP_HACK); 56 | mScrollToReturn = $(this, Settings.SCROLL_TO_RETURN); 57 | mShortcut = $(this, Settings.NOTIFY_SHORTCUT); 58 | 59 | // Default values 60 | reload(); 61 | 62 | // Bind 63 | $$(mEnable, mEdge, mSensitivity, mLollipop, mScrollToReturn, mShortcut); 64 | } 65 | 66 | @Override 67 | @SuppressWarnings("unchecked") 68 | public boolean onPreferenceChange(Preference preference, Object newValue) { 69 | if (preference == mEnable) { 70 | putBoolean(Settings.ENABLE, (Boolean) newValue); 71 | return true; 72 | } else if (preference == mEdge) { 73 | Set values = (Set) newValue; 74 | putInt(Settings.EDGE, buildEdgePref(values)); 75 | mEdge.setSummary(buildEdgeText(values)); 76 | return true; 77 | } else if (preference == mSensitivity) { 78 | putInt(Settings.SENSITIVITY, (Integer) newValue); 79 | return true; 80 | } else if (preference == mLollipop) { 81 | putBoolean(Settings.LOLLIPOP_HACK, (Boolean) newValue); 82 | return true; 83 | } else if (preference == mScrollToReturn) { 84 | putBoolean(Settings.SCROLL_TO_RETURN, (Boolean) newValue); 85 | return true; 86 | } else if (preference == mShortcut) { 87 | putBoolean(Settings.NOTIFY_SHORTCUT, (Boolean) newValue); 88 | return true; 89 | } else { 90 | return false; 91 | } 92 | } 93 | 94 | private Set parseEdgePref(int pref) { 95 | HashSet ret = new HashSet<>(); 96 | if ((pref & SwipeBackLayout.EDGE_LEFT) != 0) { 97 | ret.add(EDGE_LEFT); 98 | } 99 | 100 | if ((pref & SwipeBackLayout.EDGE_RIGHT) != 0) { 101 | ret.add(EDGE_RIGHT); 102 | } 103 | 104 | if ((pref & SwipeBackLayout.EDGE_BOTTOM) != 0) { 105 | ret.add(EDGE_BOTTOM); 106 | } 107 | 108 | return ret; 109 | } 110 | 111 | @Override 112 | public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 113 | inflater.inflate(R.menu.settings, menu); 114 | } 115 | 116 | @Override 117 | public boolean onOptionsItemSelected(MenuItem item) { 118 | switch (item.getItemId()) { 119 | case R.id.reset: 120 | doReset(); 121 | return true; 122 | default: 123 | return super.onOptionsItemSelected(item); 124 | } 125 | } 126 | 127 | private String buildEdgeText(Set edges) { 128 | StringBuilder sb = new StringBuilder(); 129 | CharSequence[] entries = mEdge.getEntries(); 130 | for (String edge : edges) { 131 | sb.append(entries[Integer.parseInt(edge)]).append(", "); 132 | } 133 | 134 | return sb.toString().substring(0, sb.length() - 2); 135 | } 136 | 137 | private int buildEdgePref(Set values) { 138 | int pref = 0; 139 | 140 | for (String value : values) { 141 | switch (value) { 142 | case EDGE_LEFT: 143 | pref |= SwipeBackLayout.EDGE_LEFT; 144 | break; 145 | case EDGE_RIGHT: 146 | pref |= SwipeBackLayout.EDGE_RIGHT; 147 | break; 148 | case EDGE_BOTTOM: 149 | pref |= SwipeBackLayout.EDGE_BOTTOM; 150 | break; 151 | } 152 | } 153 | 154 | return pref; 155 | } 156 | 157 | private void initPackage() { 158 | String[] str = getExtraPass().split(","); 159 | mPackageName = str[0]; 160 | mClassName = str[1]; 161 | 162 | if (str[2].equals(getString(R.string.global))) { 163 | str[2] = getString(R.string.global_short); 164 | } 165 | 166 | setTitle(str[2] + " - " + str[3]); 167 | showHomeAsUp(); 168 | } 169 | 170 | private void reload() { 171 | mEnable.setChecked(getBoolean(Settings.ENABLE, true)); 172 | Set edges = parseEdgePref(getInt(Settings.EDGE, SwipeBackLayout.EDGE_LEFT)); 173 | mEdge.setValues(edges); 174 | mEdge.setSummary(buildEdgeText(edges)); 175 | mSensitivity.setValue(getInt(Settings.SENSITIVITY, 100)); 176 | mScrollToReturn.setChecked(getBoolean(Settings.SCROLL_TO_RETURN, false)); 177 | 178 | // Shortcut only available in global settings above API 16 179 | if (mPackageName.equals("global")) { 180 | if (Build.VERSION.SDK_INT >= 16) { 181 | mShortcut.setEnabled(true); 182 | mShortcut.setChecked(getBoolean(Settings.NOTIFY_SHORTCUT, false)); 183 | } 184 | } else { 185 | getPreferenceScreen().removePreference(mShortcut); 186 | } 187 | 188 | if (Build.VERSION.SDK_INT >= 21) { 189 | mLollipop.setEnabled(true); 190 | mLollipop.setChecked(getBoolean(Settings.LOLLIPOP_HACK, false)); 191 | } 192 | } 193 | 194 | private void doReset() { 195 | new AlertDialog.Builder(getActivity()) 196 | .setMessage(R.string.reset_confirm) 197 | .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() { 198 | @Override 199 | public void onClick(DialogInterface dialog, int buttonId) { 200 | 201 | } 202 | }) 203 | .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { 204 | @Override 205 | public void onClick(DialogInterface dialog, int buttonId) { 206 | mSettings.reset(mPackageName, mClassName); 207 | reload(); 208 | } 209 | }) 210 | .show(); 211 | } 212 | 213 | private boolean getBoolean(String key, boolean defValue) { 214 | return mSettings.getBoolean(mPackageName, mClassName, key, defValue); 215 | } 216 | 217 | private int getInt(String key, int defValue) { 218 | return mSettings.getInt(mPackageName, mClassName, key, defValue); 219 | } 220 | 221 | private void putBoolean(String key, boolean value) { 222 | mSettings.putBoolean(mPackageName, mClassName, key, value); 223 | } 224 | 225 | private void putInt(String key, int value) { 226 | mSettings.putInt(mPackageName, mClassName, key, value); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /app/src/main/java/info/papdt/swipeback/ui/base/BaseFragment.java: -------------------------------------------------------------------------------- 1 | package info.papdt.swipeback.ui.base; 2 | 3 | import android.app.Fragment; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | 10 | import android.support.v7.widget.Toolbar; 11 | 12 | public abstract class BaseFragment extends Fragment 13 | { 14 | 15 | @Override 16 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 17 | View v = inflater.inflate(getLayoutId(), container, false); 18 | onFinishInflate(v); 19 | 20 | getGlobalActivity().setOnReturnCallback(new GlobalActivity.OnReturnCallback() { 21 | @Override 22 | public void onReturn() { 23 | BaseFragment.this.onReturn(); 24 | } 25 | }); 26 | 27 | return v; 28 | } 29 | 30 | protected GlobalActivity getGlobalActivity() { 31 | return (GlobalActivity) getActivity(); 32 | } 33 | 34 | protected Toolbar getToolbar() { 35 | return getGlobalActivity().getToolbar(); 36 | } 37 | 38 | protected String getExtraPass() { 39 | return getGlobalActivity().getExtraPass(); 40 | } 41 | 42 | protected void setTitle(String title) { 43 | getGlobalActivity().getSupportActionBar().setTitle(title); 44 | } 45 | 46 | protected void showHomeAsUp() { 47 | getGlobalActivity().getSupportActionBar().setDisplayHomeAsUpEnabled(true); 48 | } 49 | 50 | protected void hideHomeAsUp() { 51 | getGlobalActivity().getSupportActionBar().setDisplayHomeAsUpEnabled(false); 52 | } 53 | 54 | protected void onReturn() { 55 | getActivity().finish(); 56 | } 57 | 58 | protected void startFragment(String name) { 59 | Intent i = new Intent(); 60 | i.setAction(Intent.ACTION_MAIN); 61 | i.setClass(getActivity(), GlobalActivity.class); 62 | i.putExtra("fragment", name); 63 | startActivity(i); 64 | } 65 | 66 | protected void startFragment(String name, String pass) { 67 | Intent i = new Intent(); 68 | i.setAction(Intent.ACTION_MAIN); 69 | i.setClass(getActivity(), GlobalActivity.class); 70 | i.putExtra("fragment", name); 71 | i.putExtra("pass", pass); 72 | startActivity(i); 73 | } 74 | 75 | protected abstract int getLayoutId(); 76 | protected abstract void onFinishInflate(View view); 77 | } 78 | -------------------------------------------------------------------------------- /app/src/main/java/info/papdt/swipeback/ui/base/BaseListFragment.java: -------------------------------------------------------------------------------- 1 | package info.papdt.swipeback.ui.base; 2 | 3 | import android.app.Activity; 4 | import android.app.ProgressDialog; 5 | import android.os.AsyncTask; 6 | import android.view.View; 7 | import android.widget.AdapterView; 8 | import android.widget.BaseAdapter; 9 | import android.widget.ListView; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | import info.papdt.swipeback.R; 15 | import static info.papdt.swipeback.ui.utils.UiUtility.*; 16 | 17 | public abstract class BaseListFragment extends BaseFragment 18 | { 19 | private ListView mList; 20 | private List mItemList = new ArrayList(); 21 | 22 | @Override 23 | protected void onFinishInflate(View view) { 24 | mList = $(view, R.id.list); 25 | mList.setAdapter(buildAdapter()); 26 | mList.setOnItemClickListener(new AdapterView.OnItemClickListener() { 27 | @Override 28 | public void onItemClick(AdapterView parent, View view, int pos, long id) { 29 | BaseListFragment.this.onItemClick(pos); 30 | } 31 | }); 32 | } 33 | 34 | @Override 35 | public void onAttach(Activity activity) { 36 | super.onAttach(activity); 37 | new LoadDataTask().execute(); 38 | } 39 | 40 | @Override 41 | protected int getLayoutId() { 42 | return R.layout.list; 43 | } 44 | 45 | protected ListView getListView() { 46 | return mList; 47 | } 48 | 49 | protected List getItemList() { 50 | return mItemList; 51 | } 52 | 53 | protected void onItemClick(int pos) { 54 | 55 | } 56 | 57 | protected void onDataLoaded(List data) { 58 | mItemList.clear(); 59 | mItemList.addAll(data); 60 | } 61 | 62 | protected abstract BaseAdapter buildAdapter(); 63 | protected abstract List loadData(ProgressCallback callback); 64 | 65 | protected interface ProgressCallback { 66 | void updateProgress(int progress, int max); 67 | } 68 | 69 | private class LoadDataTask extends AsyncTask> { 70 | private ProgressDialog prog; 71 | 72 | @Override 73 | protected void onPreExecute() { 74 | prog = new ProgressDialog(getActivity()); 75 | prog.setMessage(getString(R.string.plz_wait)); 76 | prog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); 77 | prog.setCancelable(false); 78 | prog.show(); 79 | } 80 | 81 | @Override 82 | protected List doInBackground(Void... params) { 83 | return loadData(new ProgressCallback() { 84 | @Override 85 | public void updateProgress(int progress, int max) { 86 | publishProgress(progress, max); 87 | } 88 | }); 89 | } 90 | 91 | @Override 92 | protected void onProgressUpdate(Integer... values) { 93 | prog.setMax(values[1]); 94 | prog.setProgress(values[0]); 95 | } 96 | 97 | @Override 98 | protected void onPostExecute(List result) { 99 | prog.dismiss(); 100 | onDataLoaded(result); 101 | ((BaseAdapter) mList.getAdapter()).notifyDataSetChanged(); 102 | } 103 | 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /app/src/main/java/info/papdt/swipeback/ui/base/BasePreferenceFragment.java: -------------------------------------------------------------------------------- 1 | package info.papdt.swipeback.ui.base; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.preference.Preference; 6 | import android.preference.PreferenceFragment; 7 | 8 | import android.support.v7.widget.Toolbar; 9 | 10 | public abstract class BasePreferenceFragment extends PreferenceFragment implements Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener 11 | { 12 | 13 | @Override 14 | public void onCreate(Bundle savedInstanceState) { 15 | super.onCreate(savedInstanceState); 16 | addPreferencesFromResource(getPreferenceXml()); 17 | onPreferenceLoaded(); 18 | } 19 | 20 | @Override 21 | public boolean onPreferenceClick(Preference pref) { 22 | return false; 23 | } 24 | 25 | @Override 26 | public boolean onPreferenceChange(Preference preference, Object newValue) { 27 | return false; 28 | } 29 | 30 | protected GlobalActivity getGlobalActivity() { 31 | return (GlobalActivity) getActivity(); 32 | } 33 | 34 | protected Toolbar getToolbar() { 35 | return getGlobalActivity().getToolbar(); 36 | } 37 | 38 | protected String getExtraPass() { 39 | return getGlobalActivity().getExtraPass(); 40 | } 41 | 42 | protected void setTitle(String title) { 43 | getGlobalActivity().getSupportActionBar().setTitle(title); 44 | } 45 | 46 | protected void showHomeAsUp() { 47 | getGlobalActivity().getSupportActionBar().setDisplayHomeAsUpEnabled(true); 48 | } 49 | 50 | protected void $$(Preference... preferences) { 51 | for (Preference preference : preferences) { 52 | preference.setOnPreferenceClickListener(this); 53 | preference.setOnPreferenceChangeListener(this); 54 | } 55 | } 56 | 57 | protected void startFragment(String name) { 58 | Intent i = new Intent(); 59 | i.setAction(Intent.ACTION_MAIN); 60 | i.setClass(getActivity(), GlobalActivity.class); 61 | i.putExtra("fragment", name); 62 | startActivity(i); 63 | } 64 | 65 | protected void startFragment(String name, String pass) { 66 | Intent i = new Intent(); 67 | i.setAction(Intent.ACTION_MAIN); 68 | i.setClass(getActivity(), GlobalActivity.class); 69 | i.putExtra("fragment", name); 70 | i.putExtra("pass", pass); 71 | startActivity(i); 72 | } 73 | 74 | protected abstract int getPreferenceXml(); 75 | protected abstract void onPreferenceLoaded(); 76 | } 77 | -------------------------------------------------------------------------------- /app/src/main/java/info/papdt/swipeback/ui/base/GlobalActivity.java: -------------------------------------------------------------------------------- 1 | package info.papdt.swipeback.ui.base; 2 | 3 | import android.app.Activity; 4 | import android.app.Fragment; 5 | import android.content.Intent; 6 | import android.os.Bundle; 7 | import android.view.MenuItem; 8 | 9 | import android.support.v4.view.ViewCompat; 10 | import android.support.v7.app.ActionBarActivity; 11 | import android.support.v7.widget.Toolbar; 12 | 13 | import info.papdt.swipeback.R; 14 | import info.papdt.swipeback.ui.app.AboutFragment; 15 | import info.papdt.swipeback.ui.app.LicenseFragment; 16 | import info.papdt.swipeback.ui.app.PerActivityFragment; 17 | import info.papdt.swipeback.ui.app.PerAppFragment; 18 | import info.papdt.swipeback.ui.app.SettingsFragment; 19 | import static info.papdt.swipeback.ui.utils.UiUtility.*; 20 | 21 | public class GlobalActivity extends ActionBarActivity 22 | { 23 | static interface OnReturnCallback { 24 | void onReturn(); 25 | } 26 | 27 | private Toolbar mToolbar; 28 | private String mExtraPass; 29 | private OnReturnCallback mOnReturn; 30 | 31 | @Override 32 | public void onCreate(Bundle savedInstanceState) { 33 | super.onCreate(savedInstanceState); 34 | setContentView(R.layout.base); 35 | 36 | mToolbar = $(this, R.id.toolbar); 37 | ViewCompat.setElevation(mToolbar, 15.0f); 38 | 39 | setSupportActionBar(mToolbar); 40 | 41 | Intent i = getIntent(); 42 | 43 | Class clazz = null; 44 | String extra = i.getStringExtra("fragment"); 45 | mExtraPass = i.getStringExtra("pass"); 46 | 47 | switch (extra != null ? extra : "") { 48 | case "perapp": 49 | clazz = PerAppFragment.class; 50 | break; 51 | case "peract": 52 | clazz = PerActivityFragment.class; 53 | break; 54 | case "settings": 55 | clazz = SettingsFragment.class; 56 | break; 57 | case "about": 58 | clazz = AboutFragment.class; 59 | break; 60 | case "license": 61 | clazz = LicenseFragment.class; 62 | break; 63 | default: 64 | clazz = PerAppFragment.class; 65 | } 66 | 67 | try { 68 | getFragmentManager().beginTransaction().replace(R.id.container, clazz.newInstance()).commit(); 69 | } catch (Exception e) { 70 | throw new RuntimeException(e); 71 | } 72 | } 73 | 74 | @Override 75 | public boolean onOptionsItemSelected(MenuItem item) { 76 | if (item.getItemId() == android.R.id.home) { 77 | if (mOnReturn != null) 78 | mOnReturn.onReturn(); 79 | else 80 | finish(); 81 | return true; 82 | } else { 83 | return super.onOptionsItemSelected(item); 84 | } 85 | } 86 | 87 | Toolbar getToolbar() { 88 | return mToolbar; 89 | } 90 | 91 | String getExtraPass() { 92 | return mExtraPass; 93 | } 94 | 95 | void setOnReturnCallback(OnReturnCallback callback) { 96 | mOnReturn = callback; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /app/src/main/java/info/papdt/swipeback/ui/model/ActivityModel.java: -------------------------------------------------------------------------------- 1 | package info.papdt.swipeback.ui.model; 2 | 3 | public class ActivityModel 4 | { 5 | public String title; 6 | public String className; 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/info/papdt/swipeback/ui/model/AppModel.java: -------------------------------------------------------------------------------- 1 | package info.papdt.swipeback.ui.model; 2 | 3 | import android.content.pm.PackageInfo; 4 | import android.graphics.drawable.Drawable; 5 | 6 | import java.lang.ref.WeakReference; 7 | 8 | public class AppModel 9 | { 10 | public String packageName; 11 | public String title; 12 | public WeakReference icon; 13 | 14 | public Runnable iconRefreshRunnable; 15 | 16 | public void refreshIcon() { 17 | if (iconRefreshRunnable != null) 18 | iconRefreshRunnable.run(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/info/papdt/swipeback/ui/preference/DiscreteSeekBarPreference.java: -------------------------------------------------------------------------------- 1 | package info.papdt.swipeback.ui.preference; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.preference.Preference; 6 | import android.util.AttributeSet; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | 11 | import org.adw.library.widgets.discreteseekbar.DiscreteSeekBar; 12 | 13 | import info.papdt.swipeback.R; 14 | import static info.papdt.swipeback.ui.utils.UiUtility.*; 15 | 16 | public class DiscreteSeekBarPreference extends Preference implements DiscreteSeekBar.OnProgressChangeListener 17 | { 18 | 19 | private int mMin = 0, mMax = 1, mValue = 0, mTmp = Integer.MIN_VALUE; 20 | private String mFormat; 21 | private DiscreteSeekBar mSeekbar; 22 | 23 | public DiscreteSeekBarPreference(Context context) { 24 | this(context, null); 25 | } 26 | 27 | public DiscreteSeekBarPreference(Context context, AttributeSet attrs) { 28 | this(context, attrs, 0); 29 | } 30 | 31 | public DiscreteSeekBarPreference(Context context, AttributeSet attrs, int defStyle) { 32 | super(context, attrs, defStyle); 33 | 34 | if (attrs != null) { 35 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DiscreteSeekBarPreference); 36 | mMax = a.getInt(R.styleable.DiscreteSeekBarPreference_dsbp_max, 1); 37 | mMin = a.getInt(R.styleable.DiscreteSeekBarPreference_dsbp_min, 0); 38 | mFormat = a.getString(R.styleable.DiscreteSeekBarPreference_dsbp_format); 39 | mValue = mMin; 40 | a.recycle(); 41 | 42 | if (mFormat == null || mFormat.trim().equals("")) { 43 | mFormat = "%d"; 44 | } 45 | } 46 | } 47 | 48 | @Override 49 | protected View onCreateView(ViewGroup parent) { 50 | LayoutInflater inflater = $(getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE)); 51 | return inflater.inflate(R.layout.discrete_seekbar_preference, parent, false); 52 | } 53 | 54 | @Override 55 | protected void onBindView(View view) { 56 | super.onBindView(view); 57 | mSeekbar = $(view, R.id.dsbp_seek); 58 | mSeekbar.setMin(mMin); 59 | mSeekbar.setMax(mMax); 60 | mSeekbar.setIndicatorFormatter(mFormat); 61 | mSeekbar.setProgress(mValue); 62 | mSeekbar.setOnProgressChangeListener(this); 63 | } 64 | 65 | @Override 66 | public void onStartTrackingTouch(DiscreteSeekBar seekBar) { 67 | 68 | } 69 | 70 | @Override 71 | public void onProgressChanged(DiscreteSeekBar seekBar, int value, boolean fromUser) { 72 | if (fromUser) { 73 | mTmp = value; 74 | } 75 | } 76 | 77 | @Override 78 | public void onStopTrackingTouch(DiscreteSeekBar seekBar) { 79 | if (mTmp >= mMin && mTmp <= mMax) { 80 | OnPreferenceChangeListener listener = getOnPreferenceChangeListener(); 81 | if (listener != null) { 82 | listener.onPreferenceChange(this, mTmp); 83 | } 84 | 85 | mValue = mTmp; 86 | mTmp = Integer.MIN_VALUE; 87 | } 88 | } 89 | 90 | @Override 91 | protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) { 92 | setValue(restorePersistedValue ? getPersistedInt(mValue) : (Integer) defaultValue); 93 | } 94 | 95 | @Override 96 | protected Object onGetDefaultValue(TypedArray a, int index) { 97 | return a.getInt(index, mMin); 98 | } 99 | 100 | public void setValue(int value) { 101 | if (value >= mMin && value <= mMax) { 102 | mValue = value; 103 | 104 | if (mSeekbar != null) 105 | mSeekbar.setProgress(value); 106 | } 107 | } 108 | 109 | public int getValue() { 110 | return mValue; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /app/src/main/java/info/papdt/swipeback/ui/utils/UiUtility.java: -------------------------------------------------------------------------------- 1 | package info.papdt.swipeback.ui.utils; 2 | 3 | import android.app.Activity; 4 | import android.preference.Preference; 5 | import android.preference.PreferenceFragment; 6 | import android.view.View; 7 | 8 | public class UiUtility 9 | { 10 | public static T $(View v, int id) { 11 | return (T) v.findViewById(id); 12 | } 13 | 14 | public static T $(Activity activity, int id) { 15 | return (T) activity.findViewById(id); 16 | } 17 | 18 | public static T $(PreferenceFragment preference, String key) { 19 | return (T) preference.findPreference(key); 20 | } 21 | 22 | public static T $(Object obj) { 23 | return (T) obj; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PaperAirplane-Dev-Team/SwipeBack/62e17c4515b1018f07fdbd4a392751d73c67ed51/app/src/main/res/drawable-hdpi/ic_action_search.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_settings_backup_restore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PaperAirplane-Dev-Team/SwipeBack/62e17c4515b1018f07fdbd4a392751d73c67ed51/app/src/main/res/drawable-hdpi/ic_settings_backup_restore.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PaperAirplane-Dev-Team/SwipeBack/62e17c4515b1018f07fdbd4a392751d73c67ed51/app/src/main/res/drawable-xhdpi/ic_action_search.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PaperAirplane-Dev-Team/SwipeBack/62e17c4515b1018f07fdbd4a392751d73c67ed51/app/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_settings_backup_restore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PaperAirplane-Dev-Team/SwipeBack/62e17c4515b1018f07fdbd4a392751d73c67ed51/app/src/main/res/drawable-xhdpi/ic_settings_backup_restore.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_action_search_holo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PaperAirplane-Dev-Team/SwipeBack/62e17c4515b1018f07fdbd4a392751d73c67ed51/app/src/main/res/drawable-xxhdpi/ic_action_search_holo.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PaperAirplane-Dev-Team/SwipeBack/62e17c4515b1018f07fdbd4a392751d73c67ed51/app/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_settings_backup_restore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PaperAirplane-Dev-Team/SwipeBack/62e17c4515b1018f07fdbd4a392751d73c67ed51/app/src/main/res/drawable-xxhdpi/ic_settings_backup_restore.png -------------------------------------------------------------------------------- /app/src/main/res/layout/app.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | 13 | 25 | 26 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/res/layout/base.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/layout/discrete_seekbar_preference.xml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 25 | 26 | 36 | 37 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/layout/webview.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/menu/app.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | 12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/menu/settings.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rCN/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SwipeBack2 5 | 全局滑动返回手势 6 | 7 | 8 | 全局(会被具体设置所覆盖) 9 | 全局 10 | 重置 11 | 真的要重置这些设置吗? 12 | 启用 SwipeBack 13 | 触发边缘 14 | 灵敏度 15 | 通知栏快捷方式 16 | 在通知栏显示快捷方式,点击可直接进入当前App页面的滑动返回设置页面。 17 | 试验性功能 18 | Lollipop 优化 (SDK >= 21) 19 | 对 Lollipop 及以上的系统进行优化(例如使有颜色的状态栏在滑动时少一些违和感)。\n可能在一些程序中引起布局问题。\n请*不要*在全局设置中打开这个选项。 20 | 滑动返回动画 21 | 用滑动返回动画覆盖系统内置的返回动画。可能与一些程序冲突。 22 | 23 | 左侧 24 | 右侧 25 | 底部 26 | 27 | 28 | 0 29 | 1 30 | 2 31 | 32 | 33 | 34 | 滑动返回设置 35 | 包名: %s\n类名: %s 36 | 点击打开设置页面 37 | 应用 38 | 当前页 39 | 40 | 41 | 关于 42 | 版本 43 | 发行者 44 | PaperAirplane Dev Team(纸飞机开发团队) 45 | 维护者 46 | Peter Cai (XDA @PeterCxy) 47 | 源代码 48 | https://github.com/PaperAirplane-Dev-Team/SwipeBack 49 | 开放源代码许可证 50 | 显示 SwipeBack 和其使用的库所采用的开源许可证。非常感谢这些库的作者,没有他们就不会有 SwipeBack。 51 | 捐赠 52 | PaperAirplane Dev Team(纸飞机开发团队)是一个由学生组成的非盈利性团队。我们为此模块的开发付出了自己的精力。如果您喜欢这个模块并想给我们捐赠的话,可以滑动下面的选择器来决定捐赠的金额,捐赠页面将会自动打开。 53 | 54 | 55 | 搜索 56 | 请稍候…… 57 | 您想给我们捐赠 $%d 吗? 58 | 59 | 60 | -------------------------------------------------------------------------------- /app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #ffffff 4 | #dddddd 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SwipeBack2 5 | Global swipe-to-return gesture 6 | 7 | 8 | Global (Overriden by specific settings) 9 | Global 10 | Reset 11 | Really want to reset these settings to default? 12 | Enable SwipeBack 13 | Trigger Edge 14 | Sensitivity 15 | Notification shortcut 16 | Show a notification in notification drawer to lead you directly to the SwipeBack settings page of the current app / activity. With this you can get rid of the huge app list and activity list. 17 | Experimental 18 | Lollipop hacks (SDK >= 21) 19 | Hacks specified for Lollipop and above. This includes optimizations for Lollipop, for example, this makes the status bar (if colored) look better while swiping.\nMay cause layout issues in some apps.\nIt is NOT recommended to enable this globally. 20 | Scroll-to-return animation 21 | Override the system returning animation with a scroll-to-return animation the way the swipe gesture does. Might cause conflicts with some apps. 22 | 23 | Left 24 | Right 25 | Bottom 26 | 27 | 28 | 0 29 | 1 30 | 2 31 | 32 | 33 | 34 | SwipeBack settings 35 | Package name: %s\nClass name: %s 36 | Click to open settings page 37 | App 38 | Activity 39 | 40 | 41 | About 42 | Version 43 | Made by 44 | PaperAirplane Dev Team 45 | Maintainer 46 | Peter Cai (XDA @PeterCxy) 47 | Source Code 48 | https://github.com/PaperAirplane-Dev-Team/SwipeBack 49 | Open-source License 50 | Obtain the open-source license of SwipeBack and the open-source libraries that is used. Huge thanks to those developers of open-source libraries. Without their great works, SwipeBack cannot be what it is today. 51 | Donate 52 | PaperAirplane Dev Team is a non-profit dev team made up of mostly students. We have devoted our time to develop this module. If you like this module and feel like donating to us, swipe below to select a donation amount and open donation page. 53 | 54 | 55 | Search 56 | Please wait... 57 | Would you like to donate $%d to us? 58 | 59 | 60 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/xml/about.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 9 | 10 | 14 | 15 | 19 | 20 | 27 | 28 | 32 | 33 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/res/xml/pref.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | 18 | 19 | 26 | 27 | 33 | 34 | 36 | 37 | 44 | 45 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /art/Logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PaperAirplane-Dev-Team/SwipeBack/62e17c4515b1018f07fdbd4a392751d73c67ed51/art/Logo.psd -------------------------------------------------------------------------------- /art/Logo_FullSize_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PaperAirplane-Dev-Team/SwipeBack/62e17c4515b1018f07fdbd4a392751d73c67ed51/art/Logo_FullSize_512.png -------------------------------------------------------------------------------- /art/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PaperAirplane-Dev-Team/SwipeBack/62e17c4515b1018f07fdbd4a392751d73c67ed51/art/banner.png -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:1.0.0' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Settings specified in this file will override any Gradle settings 5 | # configured through the IDE. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PaperAirplane-Dev-Team/SwipeBack/62e17c4515b1018f07fdbd4a392751d73c67ed51/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Apr 10 15:27:10 PDT 2013 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=http\://services.gradle.org/distributions/gradle-2.2.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /libraries/SwipeBackLayout/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | repositories { 4 | mavenCentral() 5 | } 6 | 7 | dependencies { 8 | 9 | } 10 | 11 | android { 12 | compileSdkVersion 21 13 | buildToolsVersion "21.0.2" 14 | 15 | defaultConfig { 16 | minSdkVersion 7 17 | targetSdkVersion 19 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /libraries/SwipeBackLayout/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /libraries/SwipeBackLayout/src/main/java/me/imid/swipebacklayout/lib/SwipeBackLayout.java: -------------------------------------------------------------------------------- 1 | package me.imid.swipebacklayout.lib; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.res.TypedArray; 6 | import android.graphics.Canvas; 7 | import android.graphics.Rect; 8 | import android.graphics.drawable.Drawable; 9 | import android.util.AttributeSet; 10 | import android.view.MotionEvent; 11 | import android.view.View; 12 | import android.view.ViewGroup; 13 | import android.widget.FrameLayout; 14 | 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | 18 | public class SwipeBackLayout extends FrameLayout { 19 | /** 20 | * Minimum velocity that will be detected as a fling 21 | */ 22 | private static final int MIN_FLING_VELOCITY = 400; // dips per second 23 | 24 | private static final int DEFAULT_SCRIM_COLOR = 0x99000000; 25 | 26 | private static final int FULL_ALPHA = 255; 27 | 28 | /** 29 | * Edge flag indicating that the left edge should be affected. 30 | */ 31 | public static final int EDGE_LEFT = ViewDragHelper.EDGE_LEFT; 32 | 33 | /** 34 | * Edge flag indicating that the right edge should be affected. 35 | */ 36 | public static final int EDGE_RIGHT = ViewDragHelper.EDGE_RIGHT; 37 | 38 | /** 39 | * Edge flag indicating that the bottom edge should be affected. 40 | */ 41 | public static final int EDGE_BOTTOM = ViewDragHelper.EDGE_BOTTOM; 42 | 43 | /** 44 | * Edge flag set indicating all edges should be affected. 45 | */ 46 | public static final int EDGE_ALL = EDGE_LEFT | EDGE_RIGHT | EDGE_BOTTOM; 47 | 48 | /** 49 | * A view is not currently being dragged or animating as a result of a 50 | * fling/snap. 51 | */ 52 | public static final int STATE_IDLE = ViewDragHelper.STATE_IDLE; 53 | 54 | /** 55 | * A view is currently being dragged. The position is currently changing as 56 | * a result of user input or simulated user input. 57 | */ 58 | public static final int STATE_DRAGGING = ViewDragHelper.STATE_DRAGGING; 59 | 60 | /** 61 | * A view is currently settling into place as a result of a fling or 62 | * predefined non-interactive motion. 63 | */ 64 | public static final int STATE_SETTLING = ViewDragHelper.STATE_SETTLING; 65 | 66 | /** 67 | * Default threshold of scroll 68 | */ 69 | private static final float DEFAULT_SCROLL_THRESHOLD = 0.3f; 70 | 71 | private static final int OVERSCROLL_DISTANCE = 10; 72 | 73 | private static final int[] EDGE_FLAGS = { 74 | EDGE_LEFT, EDGE_RIGHT, EDGE_BOTTOM, EDGE_ALL 75 | }; 76 | 77 | private int mEdgeFlag; 78 | 79 | /** 80 | * Threshold of scroll, we will close the activity, when scrollPercent over 81 | * this value; 82 | */ 83 | private float mScrollThreshold = DEFAULT_SCROLL_THRESHOLD; 84 | 85 | private Activity mActivity; 86 | 87 | private boolean mEnable = true; 88 | 89 | private View mContentView; 90 | 91 | private ViewDragHelper mDragHelper; 92 | 93 | private float mScrollPercent; 94 | 95 | private int mContentLeft; 96 | 97 | private int mContentTop; 98 | 99 | /** 100 | * The set of listeners to be sent events through. 101 | */ 102 | private List mListeners; 103 | 104 | private Drawable mShadowLeft; 105 | 106 | private Drawable mShadowRight; 107 | 108 | private Drawable mShadowBottom; 109 | 110 | private float mScrimOpacity; 111 | 112 | private int mScrimColor = DEFAULT_SCRIM_COLOR; 113 | 114 | private boolean mInLayout; 115 | 116 | private Rect mTmpRect = new Rect(); 117 | 118 | /** 119 | * Edge being dragged 120 | */ 121 | private int mTrackingEdge; 122 | 123 | public SwipeBackLayout(Context context, Context gbContext) { 124 | this(context, gbContext, null); 125 | } 126 | 127 | public SwipeBackLayout(Context context, Context gbContext, AttributeSet attrs) { 128 | this(context, gbContext, attrs, R.attr.SwipeBackLayoutStyle); 129 | } 130 | 131 | public SwipeBackLayout(Context context, Context gbContext, AttributeSet attrs, int defStyle) { 132 | super(context, attrs); 133 | mDragHelper = ViewDragHelper.create(this, new ViewDragCallback()); 134 | 135 | TypedArray a = gbContext.obtainStyledAttributes(attrs, R.styleable.SwipeBackLayout, defStyle, 136 | R.style.SwipeBackLayout); 137 | 138 | int edgeSize = a.getDimensionPixelSize(R.styleable.SwipeBackLayout_edge_size, -1); 139 | if (edgeSize > 0) 140 | setEdgeSize(edgeSize); 141 | int mode = EDGE_FLAGS[a.getInt(R.styleable.SwipeBackLayout_edge_flag, 0)]; 142 | setEdgeTrackingEnabled(mode); 143 | 144 | int shadowLeft = a.getResourceId(R.styleable.SwipeBackLayout_shadow_left, 145 | R.drawable.shadow_left); 146 | int shadowRight = a.getResourceId(R.styleable.SwipeBackLayout_shadow_right, 147 | R.drawable.shadow_right); 148 | int shadowBottom = a.getResourceId(R.styleable.SwipeBackLayout_shadow_bottom, 149 | R.drawable.shadow_bottom); 150 | setShadow(gbContext.getResources().getDrawable(shadowLeft), EDGE_LEFT); 151 | setShadow(gbContext.getResources().getDrawable(shadowRight), EDGE_RIGHT); 152 | setShadow(gbContext.getResources().getDrawable(shadowBottom), EDGE_BOTTOM); 153 | a.recycle(); 154 | final float density = getResources().getDisplayMetrics().density; 155 | final float minVel = MIN_FLING_VELOCITY * density; 156 | mDragHelper.setMinVelocity(minVel); 157 | mDragHelper.setMaxVelocity(minVel * 2f); 158 | } 159 | 160 | /** 161 | * Sets the sensitivity of the NavigationLayout. 162 | * 163 | * @param context The application context. 164 | * @param sensitivity value between 0 and 1, the final value for touchSlop = 165 | * ViewConfiguration.getScaledTouchSlop * (1 / s); 166 | */ 167 | public void setSensitivity(Context context, float sensitivity) { 168 | mDragHelper.setSensitivity(context, sensitivity); 169 | } 170 | 171 | /** 172 | * Set up contentView which will be moved by user gesture 173 | * 174 | * @param view 175 | */ 176 | private void setContentView(View view) { 177 | mContentView = view; 178 | } 179 | 180 | public void setEnableGesture(boolean enable) { 181 | mEnable = enable; 182 | } 183 | 184 | /** 185 | * Enable edge tracking for the selected edges of the parent view. The 186 | * callback's 187 | * {@link me.imid.swipebacklayout.lib.ViewDragHelper.Callback#onEdgeTouched(int, int)} 188 | * and 189 | * {@link me.imid.swipebacklayout.lib.ViewDragHelper.Callback#onEdgeDragStarted(int, int)} 190 | * methods will only be invoked for edges for which edge tracking has been 191 | * enabled. 192 | * 193 | * @param edgeFlags Combination of edge flags describing the edges to watch 194 | * @see #EDGE_LEFT 195 | * @see #EDGE_RIGHT 196 | * @see #EDGE_BOTTOM 197 | */ 198 | public void setEdgeTrackingEnabled(int edgeFlags) { 199 | mEdgeFlag = edgeFlags; 200 | mDragHelper.setEdgeTrackingEnabled(mEdgeFlag); 201 | } 202 | 203 | /** 204 | * Set a color to use for the scrim that obscures primary content while a 205 | * drawer is open. 206 | * 207 | * @param color Color to use in 0xAARRGGBB format. 208 | */ 209 | public void setScrimColor(int color) { 210 | mScrimColor = color; 211 | invalidate(); 212 | } 213 | 214 | /** 215 | * Set the size of an edge. This is the range in pixels along the edges of 216 | * this view that will actively detect edge touches or drags if edge 217 | * tracking is enabled. 218 | * 219 | * @param size The size of an edge in pixels 220 | */ 221 | public void setEdgeSize(int size) { 222 | mDragHelper.setEdgeSize(size); 223 | } 224 | 225 | /** 226 | * Register a callback to be invoked when a swipe event is sent to this 227 | * view. 228 | * 229 | * @param listener the swipe listener to attach to this view 230 | * @deprecated use {@link #addSwipeListener} instead 231 | */ 232 | @Deprecated 233 | public void setSwipeListener(SwipeListener listener) { 234 | addSwipeListener(listener); 235 | } 236 | 237 | /** 238 | * Add a callback to be invoked when a swipe event is sent to this view. 239 | * 240 | * @param listener the swipe listener to attach to this view 241 | */ 242 | public void addSwipeListener(SwipeListener listener) { 243 | if (mListeners == null) { 244 | mListeners = new ArrayList(); 245 | } 246 | mListeners.add(listener); 247 | } 248 | 249 | /** 250 | * Removes a listener from the set of listeners 251 | * 252 | * @param listener 253 | */ 254 | public void removeSwipeListener(SwipeListener listener) { 255 | if (mListeners == null) { 256 | return; 257 | } 258 | mListeners.remove(listener); 259 | } 260 | 261 | public float getScrollPercent() { 262 | return mScrollPercent; 263 | } 264 | 265 | public static interface SwipeListener { 266 | /** 267 | * Invoke when state change 268 | * 269 | * @param state flag to describe scroll state 270 | * @param scrollPercent scroll percent of this view 271 | * @see #STATE_IDLE 272 | * @see #STATE_DRAGGING 273 | * @see #STATE_SETTLING 274 | */ 275 | public void onScrollStateChange(int state, float scrollPercent); 276 | 277 | /** 278 | * Invoke when edge touched 279 | * 280 | * @param edgeFlag edge flag describing the edge being touched 281 | * @see #EDGE_LEFT 282 | * @see #EDGE_RIGHT 283 | * @see #EDGE_BOTTOM 284 | */ 285 | public void onEdgeTouch(int edgeFlag); 286 | 287 | /** 288 | * Invoke when scroll percent over the threshold for the first time 289 | */ 290 | public void onScrollOverThreshold(); 291 | } 292 | 293 | /** 294 | * Set scroll threshold, we will close the activity, when scrollPercent over 295 | * this value 296 | * 297 | * @param threshold 298 | */ 299 | public void setScrollThresHold(float threshold) { 300 | if (threshold >= 1.0f || threshold <= 0) { 301 | throw new IllegalArgumentException("Threshold value should be between 0 and 1.0"); 302 | } 303 | mScrollThreshold = threshold; 304 | } 305 | 306 | /** 307 | * Set a drawable used for edge shadow. 308 | * 309 | * @param shadow Drawable to use 310 | * @param edgeFlags Combination of edge flags describing the edge to set 311 | * @see #EDGE_LEFT 312 | * @see #EDGE_RIGHT 313 | * @see #EDGE_BOTTOM 314 | */ 315 | public void setShadow(Drawable shadow, int edgeFlag) { 316 | if ((edgeFlag & EDGE_LEFT) != 0) { 317 | mShadowLeft = shadow; 318 | } else if ((edgeFlag & EDGE_RIGHT) != 0) { 319 | mShadowRight = shadow; 320 | } else if ((edgeFlag & EDGE_BOTTOM) != 0) { 321 | mShadowBottom = shadow; 322 | } 323 | invalidate(); 324 | } 325 | 326 | /** 327 | * Set a drawable used for edge shadow. 328 | * 329 | * @param resId Resource of drawable to use 330 | * @param edgeFlags Combination of edge flags describing the edge to set 331 | * @see #EDGE_LEFT 332 | * @see #EDGE_RIGHT 333 | * @see #EDGE_BOTTOM 334 | */ 335 | public void setShadow(int resId, int edgeFlag) { 336 | setShadow(getResources().getDrawable(resId), edgeFlag); 337 | } 338 | 339 | /** 340 | * Scroll out contentView and finish the activity 341 | */ 342 | public void scrollToFinishActivity() { 343 | final int childWidth = mContentView.getWidth(); 344 | final int childHeight = mContentView.getHeight(); 345 | 346 | int left = 0, top = 0; 347 | if ((mEdgeFlag & EDGE_LEFT) != 0) { 348 | left = childWidth + mShadowLeft.getIntrinsicWidth() + OVERSCROLL_DISTANCE; 349 | mTrackingEdge = EDGE_LEFT; 350 | } else if ((mEdgeFlag & EDGE_RIGHT) != 0) { 351 | left = -childWidth - mShadowRight.getIntrinsicWidth() - OVERSCROLL_DISTANCE; 352 | mTrackingEdge = EDGE_RIGHT; 353 | } else if ((mEdgeFlag & EDGE_BOTTOM) != 0) { 354 | top = -childHeight - mShadowBottom.getIntrinsicHeight() - OVERSCROLL_DISTANCE; 355 | mTrackingEdge = EDGE_BOTTOM; 356 | } 357 | 358 | mDragHelper.smoothSlideViewTo(mContentView, left, top); 359 | invalidate(); 360 | } 361 | 362 | @Override 363 | public boolean onInterceptTouchEvent(MotionEvent event) { 364 | if (!mEnable) { 365 | return false; 366 | } 367 | try { 368 | return mDragHelper.shouldInterceptTouchEvent(event); 369 | } catch (ArrayIndexOutOfBoundsException e) { 370 | // FIXME: handle exception 371 | // issues #9 372 | return false; 373 | } 374 | } 375 | 376 | @Override 377 | public boolean onTouchEvent(MotionEvent event) { 378 | if (!mEnable) { 379 | return false; 380 | } 381 | mDragHelper.processTouchEvent(event); 382 | return true; 383 | } 384 | 385 | @Override 386 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 387 | mInLayout = true; 388 | if (mContentView != null) 389 | mContentView.layout(mContentLeft, mContentTop, 390 | mContentLeft + mContentView.getMeasuredWidth(), 391 | mContentTop + mContentView.getMeasuredHeight()); 392 | mInLayout = false; 393 | } 394 | 395 | @Override 396 | public void requestLayout() { 397 | if (!mInLayout) { 398 | super.requestLayout(); 399 | } 400 | } 401 | 402 | @Override 403 | protected boolean drawChild(Canvas canvas, View child, long drawingTime) { 404 | final boolean drawContent = child == mContentView; 405 | 406 | boolean ret = super.drawChild(canvas, child, drawingTime); 407 | if (mScrimOpacity > 0 && drawContent 408 | && mDragHelper.getViewDragState() != ViewDragHelper.STATE_IDLE) { 409 | drawShadow(canvas, child); 410 | drawScrim(canvas, child); 411 | } 412 | return ret; 413 | } 414 | 415 | private void drawScrim(Canvas canvas, View child) { 416 | final int baseAlpha = (mScrimColor & 0xff000000) >>> 24; 417 | final int alpha = (int) (baseAlpha * mScrimOpacity); 418 | final int color = alpha << 24 | (mScrimColor & 0xffffff); 419 | 420 | if ((mTrackingEdge & EDGE_LEFT) != 0) { 421 | canvas.clipRect(0, 0, child.getLeft(), getHeight()); 422 | } else if ((mTrackingEdge & EDGE_RIGHT) != 0) { 423 | canvas.clipRect(child.getRight(), 0, getRight(), getHeight()); 424 | } else if ((mTrackingEdge & EDGE_BOTTOM) != 0) { 425 | canvas.clipRect(child.getLeft(), child.getBottom(), getRight(), getHeight()); 426 | } 427 | canvas.drawColor(color); 428 | } 429 | 430 | private void drawShadow(Canvas canvas, View child) { 431 | final Rect childRect = mTmpRect; 432 | child.getHitRect(childRect); 433 | 434 | if ((mEdgeFlag & EDGE_LEFT) != 0) { 435 | mShadowLeft.setBounds(childRect.left - mShadowLeft.getIntrinsicWidth(), childRect.top, 436 | childRect.left, childRect.bottom); 437 | mShadowLeft.setAlpha((int) (mScrimOpacity * FULL_ALPHA)); 438 | mShadowLeft.draw(canvas); 439 | } 440 | 441 | if ((mEdgeFlag & EDGE_RIGHT) != 0) { 442 | mShadowRight.setBounds(childRect.right, childRect.top, 443 | childRect.right + mShadowRight.getIntrinsicWidth(), childRect.bottom); 444 | mShadowRight.setAlpha((int) (mScrimOpacity * FULL_ALPHA)); 445 | mShadowRight.draw(canvas); 446 | } 447 | 448 | if ((mEdgeFlag & EDGE_BOTTOM) != 0) { 449 | mShadowBottom.setBounds(childRect.left, childRect.bottom, childRect.right, 450 | childRect.bottom + mShadowBottom.getIntrinsicHeight()); 451 | mShadowBottom.setAlpha((int) (mScrimOpacity * FULL_ALPHA)); 452 | mShadowBottom.draw(canvas); 453 | } 454 | } 455 | 456 | public void attachToActivity(Activity activity) { 457 | mActivity = activity; 458 | TypedArray a = activity.getTheme().obtainStyledAttributes(new int[]{ 459 | android.R.attr.windowBackground 460 | }); 461 | int background = a.getResourceId(0, 0); 462 | a.recycle(); 463 | 464 | ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView(); 465 | ViewGroup decorChild = (ViewGroup) decor.getChildAt(0); 466 | decorChild.setBackgroundResource(background); 467 | LayoutParams p = (LayoutParams) decorChild.getLayoutParams(); 468 | FrameLayout newRoot = new FrameLayout(getContext()); 469 | decor.removeView(decorChild); 470 | newRoot.addView(decorChild, p); 471 | p = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); 472 | addView(newRoot, p); 473 | setContentView(newRoot); 474 | setLayoutParams(p); 475 | decor.addView(this, p); 476 | } 477 | 478 | @Override 479 | public void computeScroll() { 480 | mScrimOpacity = 1 - mScrollPercent; 481 | if (mDragHelper.continueSettling(true)) { 482 | postInvalidateOnAnimation(); 483 | } 484 | } 485 | 486 | private class ViewDragCallback extends ViewDragHelper.Callback { 487 | private boolean mIsScrollOverValid; 488 | 489 | @Override 490 | public boolean tryCaptureView(View view, int i) { 491 | boolean ret = mDragHelper.isEdgeTouched(mEdgeFlag, i); 492 | if (ret) { 493 | if (mDragHelper.isEdgeTouched(EDGE_LEFT, i)) { 494 | mTrackingEdge = EDGE_LEFT; 495 | } else if (mDragHelper.isEdgeTouched(EDGE_RIGHT, i)) { 496 | mTrackingEdge = EDGE_RIGHT; 497 | } else if (mDragHelper.isEdgeTouched(EDGE_BOTTOM, i)) { 498 | mTrackingEdge = EDGE_BOTTOM; 499 | } 500 | if (mListeners != null && !mListeners.isEmpty()) { 501 | for (SwipeListener listener : mListeners) { 502 | listener.onEdgeTouch(mTrackingEdge); 503 | } 504 | } 505 | mIsScrollOverValid = true; 506 | } 507 | return ret; 508 | } 509 | 510 | @Override 511 | public int getViewHorizontalDragRange(View child) { 512 | return mEdgeFlag & (EDGE_LEFT | EDGE_RIGHT); 513 | } 514 | 515 | @Override 516 | public int getViewVerticalDragRange(View child) { 517 | return mEdgeFlag & EDGE_BOTTOM; 518 | } 519 | 520 | @Override 521 | public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { 522 | super.onViewPositionChanged(changedView, left, top, dx, dy); 523 | if ((mTrackingEdge & EDGE_LEFT) != 0) { 524 | mScrollPercent = Math.abs((float) left 525 | / (mContentView.getWidth() + mShadowLeft.getIntrinsicWidth())); 526 | } else if ((mTrackingEdge & EDGE_RIGHT) != 0) { 527 | mScrollPercent = Math.abs((float) left 528 | / (mContentView.getWidth() + mShadowRight.getIntrinsicWidth())); 529 | } else if ((mTrackingEdge & EDGE_BOTTOM) != 0) { 530 | mScrollPercent = Math.abs((float) top 531 | / (mContentView.getHeight() + mShadowBottom.getIntrinsicHeight())); 532 | } 533 | mContentLeft = left; 534 | mContentTop = top; 535 | invalidate(); 536 | if (mScrollPercent < mScrollThreshold && !mIsScrollOverValid) { 537 | mIsScrollOverValid = true; 538 | } 539 | if (mListeners != null && !mListeners.isEmpty() 540 | && mDragHelper.getViewDragState() == STATE_DRAGGING 541 | && mScrollPercent >= mScrollThreshold && mIsScrollOverValid) { 542 | mIsScrollOverValid = false; 543 | for (SwipeListener listener : mListeners) { 544 | listener.onScrollOverThreshold(); 545 | } 546 | } 547 | 548 | if (mScrollPercent >= 1) { 549 | if (!mActivity.isFinishing()) 550 | mActivity.finish(); 551 | } 552 | } 553 | 554 | @Override 555 | public void onViewReleased(View releasedChild, float xvel, float yvel) { 556 | final int childWidth = releasedChild.getWidth(); 557 | final int childHeight = releasedChild.getHeight(); 558 | 559 | int left = 0, top = 0; 560 | if ((mTrackingEdge & EDGE_LEFT) != 0) { 561 | left = xvel > 0 || xvel == 0 && mScrollPercent > mScrollThreshold ? childWidth 562 | + mShadowLeft.getIntrinsicWidth() + OVERSCROLL_DISTANCE : 0; 563 | } else if ((mTrackingEdge & EDGE_RIGHT) != 0) { 564 | left = xvel < 0 || xvel == 0 && mScrollPercent > mScrollThreshold ? -(childWidth 565 | + mShadowLeft.getIntrinsicWidth() + OVERSCROLL_DISTANCE) : 0; 566 | } else if ((mTrackingEdge & EDGE_BOTTOM) != 0) { 567 | top = yvel < 0 || yvel == 0 && mScrollPercent > mScrollThreshold ? -(childHeight 568 | + mShadowBottom.getIntrinsicHeight() + OVERSCROLL_DISTANCE) : 0; 569 | } 570 | 571 | mDragHelper.settleCapturedViewAt(left, top); 572 | invalidate(); 573 | } 574 | 575 | @Override 576 | public int clampViewPositionHorizontal(View child, int left, int dx) { 577 | int ret = 0; 578 | if ((mTrackingEdge & EDGE_LEFT) != 0) { 579 | ret = Math.min(child.getWidth(), Math.max(left, 0)); 580 | } else if ((mTrackingEdge & EDGE_RIGHT) != 0) { 581 | ret = Math.min(0, Math.max(left, -child.getWidth())); 582 | } 583 | return ret; 584 | } 585 | 586 | @Override 587 | public int clampViewPositionVertical(View child, int top, int dy) { 588 | int ret = 0; 589 | if ((mTrackingEdge & EDGE_BOTTOM) != 0) { 590 | ret = Math.min(0, Math.max(top, -child.getHeight())); 591 | } 592 | return ret; 593 | } 594 | 595 | @Override 596 | public void onViewDragStateChanged(int state) { 597 | super.onViewDragStateChanged(state); 598 | if (mListeners != null && !mListeners.isEmpty()) { 599 | for (SwipeListener listener : mListeners) { 600 | listener.onScrollStateChange(state, mScrollPercent); 601 | } 602 | } 603 | } 604 | } 605 | } 606 | -------------------------------------------------------------------------------- /libraries/SwipeBackLayout/src/main/java/me/imid/swipebacklayout/lib/Utils.java: -------------------------------------------------------------------------------- 1 | 2 | package me.imid.swipebacklayout.lib; 3 | 4 | import android.app.Activity; 5 | 6 | import java.lang.reflect.Method; 7 | 8 | /** 9 | * Created by Chaojun Wang on 6/9/14. 10 | */ 11 | public class Utils { 12 | private Utils() { 13 | } 14 | 15 | /** 16 | * Convert a translucent themed Activity 17 | * {@link android.R.attr#windowIsTranslucent} to a fullscreen opaque 18 | * Activity. 19 | *

20 | * Call this whenever the background of a translucent Activity has changed 21 | * to become opaque. Doing so will allow the {@link android.view.Surface} of 22 | * the Activity behind to be released. 23 | *

24 | * This call has no effect on non-translucent activities or on activities 25 | * with the {@link android.R.attr#windowIsFloating} attribute. 26 | */ 27 | public static void convertActivityFromTranslucent(Activity activity) { 28 | /*try { 29 | Method method = Activity.class.getDeclaredMethod("convertFromTranslucent"); 30 | method.setAccessible(true); 31 | method.invoke(activity); 32 | } catch (Throwable t) { 33 | }*/ 34 | } 35 | 36 | /** 37 | * Convert a translucent themed Activity 38 | * {@link android.R.attr#windowIsTranslucent} back from opaque to 39 | * translucent following a call to 40 | * {@link #convertActivityFromTranslucent(android.app.Activity)} . 41 | *

42 | * Calling this allows the Activity behind this one to be seen again. Once 43 | * all such Activities have been redrawn 44 | *

45 | * This call has no effect on non-translucent activities or on activities 46 | * with the {@link android.R.attr#windowIsFloating} attribute. 47 | */ 48 | public static void convertActivityToTranslucent(Activity activity) { 49 | /*try { 50 | Class[] classes = Activity.class.getDeclaredClasses(); 51 | Class translucentConversionListenerClazz = null; 52 | for (Class clazz : classes) { 53 | if (clazz.getSimpleName().contains("TranslucentConversionListener")) { 54 | translucentConversionListenerClazz = clazz; 55 | } 56 | } 57 | Method method = Activity.class.getDeclaredMethod("convertToTranslucent", 58 | translucentConversionListenerClazz); 59 | method.setAccessible(true); 60 | method.invoke(activity, new Object[] { 61 | null 62 | }); 63 | } catch (Throwable t) { 64 | }*/ 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /libraries/SwipeBackLayout/src/main/java/me/imid/swipebacklayout/lib/app/SwipeBackActivity.java: -------------------------------------------------------------------------------- 1 | 2 | package me.imid.swipebacklayout.lib.app; 3 | 4 | import android.app.Activity; 5 | import android.os.Bundle; 6 | import android.view.View; 7 | 8 | import me.imid.swipebacklayout.lib.SwipeBackLayout; 9 | import me.imid.swipebacklayout.lib.Utils; 10 | 11 | public class SwipeBackActivity extends Activity implements SwipeBackActivityBase { 12 | private SwipeBackActivityHelper mHelper; 13 | 14 | @Override 15 | protected void onCreate(Bundle savedInstanceState) { 16 | super.onCreate(savedInstanceState); 17 | mHelper = new SwipeBackActivityHelper(this); 18 | mHelper.onActivityCreate(); 19 | } 20 | 21 | @Override 22 | protected void onPostCreate(Bundle savedInstanceState) { 23 | super.onPostCreate(savedInstanceState); 24 | mHelper.onPostCreate(); 25 | } 26 | 27 | @Override 28 | public View findViewById(int id) { 29 | View v = super.findViewById(id); 30 | if (v == null && mHelper != null) 31 | return mHelper.findViewById(id); 32 | return v; 33 | } 34 | 35 | @Override 36 | public SwipeBackLayout getSwipeBackLayout() { 37 | return mHelper.getSwipeBackLayout(); 38 | } 39 | 40 | @Override 41 | public void setSwipeBackEnable(boolean enable) { 42 | getSwipeBackLayout().setEnableGesture(enable); 43 | } 44 | 45 | @Override 46 | public void scrollToFinishActivity() { 47 | Utils.convertActivityToTranslucent(this); 48 | getSwipeBackLayout().scrollToFinishActivity(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /libraries/SwipeBackLayout/src/main/java/me/imid/swipebacklayout/lib/app/SwipeBackActivityBase.java: -------------------------------------------------------------------------------- 1 | package me.imid.swipebacklayout.lib.app; 2 | 3 | import me.imid.swipebacklayout.lib.SwipeBackLayout; 4 | /** 5 | * @author Yrom 6 | */ 7 | public interface SwipeBackActivityBase { 8 | /** 9 | * @return the SwipeBackLayout associated with this activity. 10 | */ 11 | public abstract SwipeBackLayout getSwipeBackLayout(); 12 | 13 | public abstract void setSwipeBackEnable(boolean enable); 14 | 15 | /** 16 | * Scroll out contentView and finish the activity 17 | */ 18 | public abstract void scrollToFinishActivity(); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /libraries/SwipeBackLayout/src/main/java/me/imid/swipebacklayout/lib/app/SwipeBackActivityHelper.java: -------------------------------------------------------------------------------- 1 | package me.imid.swipebacklayout.lib.app; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.graphics.Color; 6 | import android.graphics.drawable.ColorDrawable; 7 | import android.view.View; 8 | 9 | import me.imid.swipebacklayout.lib.SwipeBackLayout; 10 | import me.imid.swipebacklayout.lib.Utils; 11 | 12 | /** 13 | * @author Yrom 14 | * @author PeterCxy 15 | */ 16 | public class SwipeBackActivityHelper { 17 | protected Activity mActivity; 18 | 19 | private SwipeBackLayout mSwipeBackLayout; 20 | 21 | public SwipeBackActivityHelper(Activity activity) { 22 | mActivity = activity; 23 | } 24 | 25 | @SuppressWarnings("deprecation") 26 | public void onActivityCreate() { 27 | mSwipeBackLayout = new SwipeBackLayout(mActivity, getGlobalContext()); 28 | } 29 | 30 | public void onPostCreate() { 31 | mSwipeBackLayout.attachToActivity(mActivity); 32 | } 33 | 34 | public View findViewById(int id) { 35 | if (mSwipeBackLayout != null) { 36 | return mSwipeBackLayout.findViewById(id); 37 | } 38 | return null; 39 | } 40 | 41 | public SwipeBackLayout getSwipeBackLayout() { 42 | return mSwipeBackLayout; 43 | } 44 | 45 | protected Context getGlobalContext() { 46 | return mActivity; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /libraries/SwipeBackLayout/src/main/java/me/imid/swipebacklayout/lib/app/SwipeBackPreferenceActivity.java: -------------------------------------------------------------------------------- 1 | 2 | package me.imid.swipebacklayout.lib.app; 3 | 4 | import me.imid.swipebacklayout.lib.SwipeBackLayout; 5 | import android.os.Bundle; 6 | import android.preference.PreferenceActivity; 7 | import android.view.View; 8 | 9 | public class SwipeBackPreferenceActivity extends PreferenceActivity implements SwipeBackActivityBase { 10 | private SwipeBackActivityHelper mHelper; 11 | 12 | @Override 13 | protected void onCreate(Bundle savedInstanceState) { 14 | super.onCreate(savedInstanceState); 15 | mHelper = new SwipeBackActivityHelper(this); 16 | mHelper.onActivityCreate(); 17 | } 18 | 19 | @Override 20 | protected void onPostCreate(Bundle savedInstanceState) { 21 | super.onPostCreate(savedInstanceState); 22 | mHelper.onPostCreate(); 23 | } 24 | 25 | @Override 26 | public View findViewById(int id) { 27 | View v = super.findViewById(id); 28 | if (v == null && mHelper != null) 29 | return mHelper.findViewById(id); 30 | return v; 31 | } 32 | 33 | @Override 34 | public SwipeBackLayout getSwipeBackLayout() { 35 | return mHelper.getSwipeBackLayout(); 36 | } 37 | @Override 38 | public void setSwipeBackEnable(boolean enable) { 39 | getSwipeBackLayout().setEnableGesture(enable); 40 | } 41 | 42 | @Override 43 | public void scrollToFinishActivity() { 44 | getSwipeBackLayout().scrollToFinishActivity(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /libraries/SwipeBackLayout/src/main/res/drawable-xhdpi/shadow_bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PaperAirplane-Dev-Team/SwipeBack/62e17c4515b1018f07fdbd4a392751d73c67ed51/libraries/SwipeBackLayout/src/main/res/drawable-xhdpi/shadow_bottom.png -------------------------------------------------------------------------------- /libraries/SwipeBackLayout/src/main/res/drawable-xhdpi/shadow_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PaperAirplane-Dev-Team/SwipeBack/62e17c4515b1018f07fdbd4a392751d73c67ed51/libraries/SwipeBackLayout/src/main/res/drawable-xhdpi/shadow_left.png -------------------------------------------------------------------------------- /libraries/SwipeBackLayout/src/main/res/drawable-xhdpi/shadow_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PaperAirplane-Dev-Team/SwipeBack/62e17c4515b1018f07fdbd4a392751d73c67ed51/libraries/SwipeBackLayout/src/main/res/drawable-xhdpi/shadow_right.png -------------------------------------------------------------------------------- /libraries/SwipeBackLayout/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /libraries/SwipeBackLayout/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | -------------------------------------------------------------------------------- /libraries/discreteSeekBar/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /libraries/discreteSeekBar/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | apply plugin: 'com.android.library' 18 | 19 | android { 20 | compileSdkVersion 21 21 | buildToolsVersion "21.0.2" 22 | 23 | defaultConfig { 24 | minSdkVersion 4 25 | targetSdkVersion 21 26 | versionCode 1 27 | versionName "1.0" 28 | } 29 | } 30 | 31 | dependencies { 32 | compile 'com.android.support:support-v4:21.0.2' 33 | } 34 | -------------------------------------------------------------------------------- /libraries/discreteSeekBar/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /libraries/discreteSeekBar/src/main/java/org/adw/library/widgets/discreteseekbar/internal/Marker.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.adw.library.widgets.discreteseekbar.internal; 18 | 19 | import android.content.Context; 20 | import android.content.res.ColorStateList; 21 | import android.content.res.TypedArray; 22 | import android.graphics.Canvas; 23 | import android.graphics.drawable.Drawable; 24 | import android.os.Build; 25 | import android.support.v4.view.ViewCompat; 26 | import android.util.AttributeSet; 27 | import android.util.DisplayMetrics; 28 | import android.view.Gravity; 29 | import android.view.View; 30 | import android.view.ViewGroup; 31 | import android.widget.FrameLayout; 32 | import android.widget.TextView; 33 | 34 | import org.adw.library.widgets.discreteseekbar.R; 35 | import org.adw.library.widgets.discreteseekbar.internal.compat.SeekBarCompat; 36 | import org.adw.library.widgets.discreteseekbar.internal.drawable.MarkerDrawable; 37 | import org.adw.library.widgets.discreteseekbar.internal.drawable.ThumbDrawable; 38 | 39 | /** 40 | * {@link android.view.ViewGroup} to be used as the real indicator. 41 | *

42 | * I've used this to be able to acomodate the TextView 43 | * and the {@link org.adw.library.widgets.discreteseekbar.internal.drawable.MarkerDrawable} 44 | * with the required positions and offsets 45 | *

46 | * 47 | * @hide 48 | */ 49 | public class Marker extends ViewGroup implements MarkerDrawable.MarkerAnimationListener { 50 | private static final int PADDING_DP = 4; 51 | private static final int ELEVATION_DP = 8; 52 | private static final int SEPARATION_DP = 30; 53 | //The TextView to show the info 54 | private TextView mNumber; 55 | //The max width of this View 56 | private int mWidth; 57 | //some distance between the thumb and our bubble marker. 58 | //This will be added to our measured height 59 | private int mSeparation; 60 | MarkerDrawable mMarkerDrawable; 61 | 62 | public Marker(Context context) { 63 | this(context, null); 64 | } 65 | 66 | public Marker(Context context, AttributeSet attrs) { 67 | this(context, attrs, R.attr.discreteSeekBarStyle); 68 | } 69 | 70 | public Marker(Context context, AttributeSet attrs, int defStyleAttr) { 71 | this(context, attrs, defStyleAttr, "0"); 72 | } 73 | 74 | public Marker(Context context, AttributeSet attrs, int defStyleAttr, String maxValue) { 75 | super(context, attrs, defStyleAttr); 76 | DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); 77 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DiscreteSeekBar, 78 | R.attr.discreteSeekBarStyle, R.style.DefaultSeekBar); 79 | 80 | int padding = (int) (PADDING_DP * displayMetrics.density) * 2; 81 | int textAppearanceId = a.getResourceId(R.styleable.DiscreteSeekBar_dsb_indicatorTextAppearance, 82 | R.style.DefaultIndicatorTextAppearance); 83 | mNumber = new TextView(context); 84 | //Add some padding to this textView so the bubble has some space to breath 85 | mNumber.setPadding(padding, 0, padding, 0); 86 | mNumber.setTextAppearance(context, textAppearanceId); 87 | mNumber.setGravity(Gravity.CENTER); 88 | mNumber.setText(maxValue); 89 | mNumber.setMaxLines(1); 90 | mNumber.setSingleLine(true); 91 | SeekBarCompat.setTextDirection(mNumber, TEXT_DIRECTION_LOCALE); 92 | mNumber.setVisibility(View.INVISIBLE); 93 | 94 | //add some padding for the elevation shadow not to be clipped 95 | //I'm sure there are better ways of doing this... 96 | setPadding(padding, padding, padding, padding); 97 | 98 | resetSizes(maxValue); 99 | 100 | mSeparation = (int) (SEPARATION_DP * displayMetrics.density); 101 | int thumbSize = (int) (ThumbDrawable.DEFAULT_SIZE_DP * displayMetrics.density); 102 | ColorStateList color = a.getColorStateList(R.styleable.DiscreteSeekBar_dsb_indicatorColor); 103 | mMarkerDrawable = new MarkerDrawable(color, thumbSize); 104 | mMarkerDrawable.setCallback(this); 105 | mMarkerDrawable.setMarkerListener(this); 106 | mMarkerDrawable.setExternalOffset(padding); 107 | 108 | //Elevation for anroid 5+ 109 | float elevation = a.getDimension(R.styleable.DiscreteSeekBar_dsb_indicatorElevation, ELEVATION_DP * displayMetrics.density); 110 | ViewCompat.setElevation(this, elevation); 111 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 112 | SeekBarCompat.setOutlineProvider(this, mMarkerDrawable); 113 | } 114 | a.recycle(); 115 | } 116 | 117 | public void resetSizes(String maxValue) { 118 | DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); 119 | //Account for negative numbers... is there any proper way of getting the biggest string between our range???? 120 | mNumber.setText("-" + maxValue); 121 | //Do a first forced measure call for the TextView (with the biggest text content), 122 | //to calculate the max width and use always the same. 123 | //this avoids the TextView from shrinking and growing when the text content changes 124 | int wSpec = MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels, MeasureSpec.AT_MOST); 125 | int hSpec = MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels, MeasureSpec.AT_MOST); 126 | mNumber.measure(wSpec, hSpec); 127 | mWidth = Math.max(mNumber.getMeasuredWidth(), mNumber.getMeasuredHeight()); 128 | removeView(mNumber); 129 | addView(mNumber, new FrameLayout.LayoutParams(mWidth, mWidth, Gravity.LEFT | Gravity.TOP)); 130 | } 131 | 132 | @Override 133 | protected void dispatchDraw(Canvas canvas) { 134 | mMarkerDrawable.draw(canvas); 135 | super.dispatchDraw(canvas); 136 | } 137 | 138 | @Override 139 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 140 | measureChildren(widthMeasureSpec, heightMeasureSpec); 141 | int widthSize = mWidth + getPaddingLeft() + getPaddingRight(); 142 | int heightSize = mWidth + getPaddingTop() + getPaddingBottom(); 143 | //This diff is the basic calculation of the difference between 144 | //a square side size and its diagonal 145 | //this helps us account for the visual offset created by MarkerDrawable 146 | //when leaving one of the corners un-rounded 147 | int diff = (int) ((1.41f * mWidth) - mWidth) / 2; 148 | setMeasuredDimension(widthSize, heightSize + diff + mSeparation); 149 | } 150 | 151 | @Override 152 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 153 | int left = getPaddingLeft(); 154 | int top = getPaddingTop(); 155 | int right = getWidth() - getPaddingRight(); 156 | int bottom = getHeight() - getPaddingBottom(); 157 | //the TetView is always layout at the top 158 | mNumber.layout(left, top, left + mWidth, top + mWidth); 159 | //the MarkerDrawable uses the whole view, it will adapt itself... 160 | // or it seems so... 161 | mMarkerDrawable.setBounds(left, top, right, bottom); 162 | } 163 | 164 | @Override 165 | protected boolean verifyDrawable(Drawable who) { 166 | return who == mMarkerDrawable || super.verifyDrawable(who); 167 | } 168 | 169 | @Override 170 | protected void onAttachedToWindow() { 171 | super.onAttachedToWindow(); 172 | //HACK: Sometimes, the animateOpen() call is made before the View is attached 173 | //so the drawable cannot schedule itself to run the animation 174 | //I think we can call it here safely. 175 | //I've seen it happen in android 2.3.7 176 | animateOpen(); 177 | } 178 | 179 | public void setValue(CharSequence value) { 180 | mNumber.setText(value); 181 | } 182 | 183 | public CharSequence getValue() { 184 | return mNumber.getText(); 185 | } 186 | 187 | public void animateOpen() { 188 | mMarkerDrawable.stop(); 189 | mMarkerDrawable.animateToPressed(); 190 | } 191 | 192 | public void animateClose() { 193 | mMarkerDrawable.stop(); 194 | mNumber.setVisibility(View.INVISIBLE); 195 | mMarkerDrawable.animateToNormal(); 196 | } 197 | 198 | @Override 199 | public void onOpeningComplete() { 200 | mNumber.setVisibility(View.VISIBLE); 201 | if (getParent() instanceof MarkerDrawable.MarkerAnimationListener) { 202 | ((MarkerDrawable.MarkerAnimationListener) getParent()).onOpeningComplete(); 203 | } 204 | } 205 | 206 | @Override 207 | public void onClosingComplete() { 208 | if (getParent() instanceof MarkerDrawable.MarkerAnimationListener) { 209 | ((MarkerDrawable.MarkerAnimationListener) getParent()).onClosingComplete(); 210 | } 211 | } 212 | 213 | @Override 214 | protected void onDetachedFromWindow() { 215 | super.onDetachedFromWindow(); 216 | mMarkerDrawable.stop(); 217 | } 218 | 219 | public void setColors(int startColor, int endColor) { 220 | mMarkerDrawable.setColors(startColor, endColor); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /libraries/discreteSeekBar/src/main/java/org/adw/library/widgets/discreteseekbar/internal/PopupIndicator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.adw.library.widgets.discreteseekbar.internal; 18 | 19 | import android.content.Context; 20 | import android.graphics.PixelFormat; 21 | import android.graphics.Point; 22 | import android.graphics.Rect; 23 | import android.os.Build; 24 | import android.os.IBinder; 25 | import android.support.v4.view.GravityCompat; 26 | import android.util.AttributeSet; 27 | import android.util.DisplayMetrics; 28 | import android.view.Gravity; 29 | import android.view.View; 30 | import android.view.ViewGroup; 31 | import android.view.WindowManager; 32 | import android.widget.FrameLayout; 33 | 34 | import org.adw.library.widgets.discreteseekbar.internal.compat.SeekBarCompat; 35 | import org.adw.library.widgets.discreteseekbar.internal.drawable.MarkerDrawable; 36 | 37 | /** 38 | * Class to manage the floating bubble thing, similar (but quite worse tested than {@link android.widget.PopupWindow} 39 | *

40 | *

41 | * This will attach a View to the Window (full-width, measured-height, positioned just under the thumb) 42 | *

43 | * 44 | * @hide 45 | * @see #showIndicator(android.view.View, android.graphics.Rect) 46 | * @see #dismiss() 47 | * @see #dismissComplete() 48 | * @see org.adw.library.widgets.discreteseekbar.internal.PopupIndicator.Floater 49 | */ 50 | public class PopupIndicator { 51 | 52 | private final WindowManager mWindowManager; 53 | private boolean mShowing; 54 | private Floater mPopupView; 55 | //Outside listener for the DiscreteSeekBar to get MarkerDrawable animation events. 56 | //The whole chain of events goes this way: 57 | //MarkerDrawable->Marker->Floater->mListener->DiscreteSeekBar.... 58 | //... phew! 59 | private MarkerDrawable.MarkerAnimationListener mListener; 60 | private int[] mDrawingLocation = new int[2]; 61 | Point screenSize = new Point(); 62 | 63 | public PopupIndicator(Context context, AttributeSet attrs, int defStyleAttr, String maxValue) { 64 | mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 65 | mPopupView = new Floater(context, attrs, defStyleAttr, maxValue); 66 | DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); 67 | screenSize.set(displayMetrics.widthPixels, displayMetrics.heightPixels); 68 | } 69 | 70 | public void updateSizes(String maxValue) { 71 | dismissComplete(); 72 | if (mPopupView != null) { 73 | mPopupView.mMarker.resetSizes(maxValue); 74 | } 75 | } 76 | 77 | public void setListener(MarkerDrawable.MarkerAnimationListener listener) { 78 | mListener = listener; 79 | } 80 | 81 | /** 82 | * We want the Floater to be full-width because the contents will be moved from side to side. 83 | * We may/should change this in the future to use just the PARENT View width and/or pass it in the constructor 84 | */ 85 | private void measureFloater() { 86 | int specWidth = View.MeasureSpec.makeMeasureSpec(screenSize.x, View.MeasureSpec.EXACTLY); 87 | int specHeight = View.MeasureSpec.makeMeasureSpec(screenSize.y, View.MeasureSpec.AT_MOST); 88 | mPopupView.measure(specWidth, specHeight); 89 | } 90 | 91 | public void setValue(CharSequence value) { 92 | mPopupView.mMarker.setValue(value); 93 | } 94 | 95 | public boolean isShowing() { 96 | return mShowing; 97 | } 98 | 99 | public void showIndicator(View parent, Rect touchBounds) { 100 | if (isShowing()) { 101 | mPopupView.mMarker.animateOpen(); 102 | return; 103 | } 104 | 105 | IBinder windowToken = parent.getWindowToken(); 106 | if (windowToken != null) { 107 | WindowManager.LayoutParams p = createPopupLayout(windowToken); 108 | 109 | p.gravity = Gravity.TOP | GravityCompat.START; 110 | updateLayoutParamsForPosiion(parent, p, touchBounds.bottom); 111 | mShowing = true; 112 | 113 | translateViewIntoPosition(touchBounds.centerX()); 114 | invokePopup(p); 115 | } 116 | } 117 | 118 | public void move(int x) { 119 | if (!isShowing()) { 120 | return; 121 | } 122 | translateViewIntoPosition(x); 123 | } 124 | 125 | public void setColors(int startColor, int endColor) { 126 | mPopupView.setColors(startColor, endColor); 127 | } 128 | 129 | /** 130 | * This will start the closing animation of the Marker and call onClosingComplete when finished 131 | */ 132 | public void dismiss() { 133 | mPopupView.mMarker.animateClose(); 134 | } 135 | 136 | /** 137 | * FORCE the popup window to be removed. 138 | * You typically calls this when the parent view is being removed from the window to avoid a Window Leak 139 | */ 140 | public void dismissComplete() { 141 | if (isShowing()) { 142 | mShowing = false; 143 | try { 144 | mWindowManager.removeViewImmediate(mPopupView); 145 | } finally { 146 | } 147 | } 148 | } 149 | 150 | private void updateLayoutParamsForPosiion(View anchor, WindowManager.LayoutParams p, int yOffset) { 151 | measureFloater(); 152 | int measuredHeight = mPopupView.getMeasuredHeight(); 153 | int paddingBottom = mPopupView.mMarker.getPaddingBottom(); 154 | anchor.getLocationInWindow(mDrawingLocation); 155 | p.x = 0; 156 | p.y = mDrawingLocation[1] - measuredHeight + yOffset + paddingBottom; 157 | p.width = screenSize.x; 158 | p.height = measuredHeight; 159 | } 160 | 161 | private void translateViewIntoPosition(final int x) { 162 | mPopupView.setFloatOffset(x + mDrawingLocation[0]); 163 | } 164 | 165 | private void invokePopup(WindowManager.LayoutParams p) { 166 | mWindowManager.addView(mPopupView, p); 167 | mPopupView.mMarker.animateOpen(); 168 | } 169 | 170 | private WindowManager.LayoutParams createPopupLayout(IBinder token) { 171 | WindowManager.LayoutParams p = new WindowManager.LayoutParams(); 172 | p.gravity = Gravity.START | Gravity.TOP; 173 | p.width = ViewGroup.LayoutParams.MATCH_PARENT; 174 | p.height = ViewGroup.LayoutParams.MATCH_PARENT; 175 | p.format = PixelFormat.TRANSLUCENT; 176 | p.flags = computeFlags(p.flags); 177 | p.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; 178 | p.token = token; 179 | p.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN; 180 | p.setTitle("DiscreteSeekBar Indicator:" + Integer.toHexString(hashCode())); 181 | 182 | return p; 183 | } 184 | 185 | /** 186 | * I'm NOT completely sure how all this bitwise things work... 187 | * 188 | * @param curFlags 189 | * @return 190 | */ 191 | private int computeFlags(int curFlags) { 192 | curFlags &= ~( 193 | WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES | 194 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | 195 | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | 196 | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH | 197 | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS | 198 | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); 199 | curFlags |= WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES; 200 | curFlags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; 201 | curFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; 202 | curFlags |= WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; 203 | return curFlags; 204 | } 205 | 206 | /** 207 | * Small FrameLayout class to hold and move the bubble around when requested 208 | * I wanted to use the {@link Marker} directly 209 | * but doing so would make some things harder to implement 210 | * (like moving the marker around, having the Marker's outline to work, etc) 211 | */ 212 | private class Floater extends FrameLayout implements MarkerDrawable.MarkerAnimationListener { 213 | private Marker mMarker; 214 | private int mOffset; 215 | 216 | public Floater(Context context, AttributeSet attrs, int defStyleAttr, String maxValue) { 217 | super(context); 218 | mMarker = new Marker(context, attrs, defStyleAttr, maxValue); 219 | addView(mMarker, new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.LEFT | Gravity.TOP)); 220 | } 221 | 222 | @Override 223 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 224 | measureChildren(widthMeasureSpec, heightMeasureSpec); 225 | int widthSize = MeasureSpec.getSize(widthMeasureSpec); 226 | int heightSie = mMarker.getMeasuredHeight(); 227 | setMeasuredDimension(widthSize, heightSie); 228 | } 229 | 230 | @Override 231 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 232 | int centerDiffX = mMarker.getMeasuredWidth() / 2; 233 | int offset = (mOffset - centerDiffX); 234 | mMarker.layout(offset, 0, offset + mMarker.getMeasuredWidth(), mMarker.getMeasuredHeight()); 235 | } 236 | 237 | public void setFloatOffset(int x) { 238 | mOffset = x; 239 | int centerDiffX = mMarker.getMeasuredWidth() / 2; 240 | int offset = (x - centerDiffX); 241 | mMarker.offsetLeftAndRight(offset - mMarker.getLeft()); 242 | //Without hardware acceleration (or API levels<11), offsetting a view seems to NOT invalidate the proper area. 243 | //We should calc the proper invalidate Rect but this will be for now... 244 | if (!SeekBarCompat.isHardwareAccelerated(this)) { 245 | invalidate(); 246 | } 247 | } 248 | 249 | @Override 250 | public void onClosingComplete() { 251 | if (mListener != null) { 252 | mListener.onClosingComplete(); 253 | } 254 | dismissComplete(); 255 | } 256 | 257 | @Override 258 | public void onOpeningComplete() { 259 | if (mListener != null) { 260 | mListener.onOpeningComplete(); 261 | } 262 | } 263 | 264 | public void setColors(int startColor, int endColor) { 265 | mMarker.setColors(startColor, endColor); 266 | } 267 | } 268 | 269 | } 270 | -------------------------------------------------------------------------------- /libraries/discreteSeekBar/src/main/java/org/adw/library/widgets/discreteseekbar/internal/compat/AnimatorCompat.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.adw.library.widgets.discreteseekbar.internal.compat; 18 | 19 | import android.os.Build; 20 | 21 | /** 22 | * Currently, there's no {@link android.animation.ValueAnimator} compatibility version 23 | * and as we didn't want to throw in external dependencies, we made this small class. 24 | *

25 | *

26 | * This will work like {@link android.support.v4.view.ViewPropertyAnimatorCompat}, that is, 27 | * not doing anything on API<11 and using the default {@link android.animation.ValueAnimator} 28 | * on API>=11 29 | *

30 | *

31 | * This class is used to provide animation to the {@link org.adw.library.widgets.discreteseekbar.DiscreteSeekBar} 32 | * when navigating with the Keypad 33 | *

34 | * 35 | * @hide 36 | */ 37 | public abstract class AnimatorCompat { 38 | public interface AnimationFrameUpdateListener { 39 | public void onAnimationFrame(float currentValue); 40 | } 41 | 42 | AnimatorCompat() { 43 | 44 | } 45 | 46 | public abstract void cancel(); 47 | 48 | public abstract boolean isRunning(); 49 | 50 | public abstract void setDuration(int progressAnimationDuration); 51 | 52 | public abstract void start(); 53 | 54 | public static final AnimatorCompat create(float start, float end, AnimationFrameUpdateListener listener) { 55 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 56 | return new AnimatorCompatV11(start, end, listener); 57 | } else { 58 | return new AnimatorCompatBase(start, end, listener); 59 | } 60 | } 61 | 62 | private static class AnimatorCompatBase extends AnimatorCompat { 63 | 64 | private final AnimationFrameUpdateListener mListener; 65 | private final float mEndValue; 66 | 67 | public AnimatorCompatBase(float start, float end, AnimationFrameUpdateListener listener) { 68 | mListener = listener; 69 | mEndValue = end; 70 | } 71 | 72 | @Override 73 | public void cancel() { 74 | 75 | } 76 | 77 | @Override 78 | public boolean isRunning() { 79 | return false; 80 | } 81 | 82 | @Override 83 | public void setDuration(int progressAnimationDuration) { 84 | 85 | } 86 | 87 | @Override 88 | public void start() { 89 | mListener.onAnimationFrame(mEndValue); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /libraries/discreteSeekBar/src/main/java/org/adw/library/widgets/discreteseekbar/internal/compat/AnimatorCompatV11.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.adw.library.widgets.discreteseekbar.internal.compat; 18 | 19 | import android.animation.ValueAnimator; 20 | import android.annotation.TargetApi; 21 | import android.os.Build; 22 | 23 | /** 24 | * Class to wrap a {@link android.animation.ValueAnimator} 25 | * for use with AnimatorCompat 26 | * 27 | * @hide 28 | * @see {@link org.adw.library.widgets.discreteseekbar.internal.compat.AnimatorCompat} 29 | */ 30 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 31 | public class AnimatorCompatV11 extends AnimatorCompat { 32 | 33 | ValueAnimator animator; 34 | 35 | public AnimatorCompatV11(float start, float end, final AnimationFrameUpdateListener listener) { 36 | super(); 37 | animator = ValueAnimator.ofFloat(start, end); 38 | animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 39 | @Override 40 | public void onAnimationUpdate(ValueAnimator animation) { 41 | listener.onAnimationFrame((Float) animation.getAnimatedValue()); 42 | } 43 | }); 44 | } 45 | 46 | @Override 47 | public void cancel() { 48 | animator.cancel(); 49 | } 50 | 51 | @Override 52 | public boolean isRunning() { 53 | return animator.isRunning(); 54 | } 55 | 56 | @Override 57 | public void setDuration(int duration) { 58 | animator.setDuration(duration); 59 | } 60 | 61 | @Override 62 | public void start() { 63 | animator.start(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /libraries/discreteSeekBar/src/main/java/org/adw/library/widgets/discreteseekbar/internal/compat/SeekBarCompat.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.adw.library.widgets.discreteseekbar.internal.compat; 18 | 19 | import android.content.res.ColorStateList; 20 | import android.graphics.drawable.Drawable; 21 | import android.os.Build; 22 | import android.support.v4.graphics.drawable.DrawableCompat; 23 | import android.view.View; 24 | import android.view.ViewParent; 25 | import android.widget.TextView; 26 | 27 | import org.adw.library.widgets.discreteseekbar.internal.drawable.AlmostRippleDrawable; 28 | import org.adw.library.widgets.discreteseekbar.internal.drawable.MarkerDrawable; 29 | 30 | /** 31 | * Wrapper compatibility class to call some API-Specific methods 32 | * And offer alternate procedures when possible 33 | * 34 | * @hide 35 | */ 36 | public class SeekBarCompat { 37 | 38 | /** 39 | * Sets the custom Outline provider on API>=21. 40 | * Does nothing on API<21 41 | * 42 | * @param view 43 | * @param markerDrawable 44 | */ 45 | public static void setOutlineProvider(View view, final MarkerDrawable markerDrawable) { 46 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 47 | SeekBarCompatDontCrash.setOutlineProvider(view, markerDrawable); 48 | } 49 | } 50 | 51 | /** 52 | * Our DiscreteSeekBar implementation uses a circular drawable on API < 21 53 | * because we don't set it as Background, but draw it ourselves 54 | * 55 | * @param colorStateList 56 | * @return 57 | */ 58 | public static Drawable getRipple(ColorStateList colorStateList) { 59 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 60 | return SeekBarCompatDontCrash.getRipple(colorStateList); 61 | } else { 62 | return new AlmostRippleDrawable(colorStateList); 63 | } 64 | } 65 | 66 | /** 67 | * As our DiscreteSeekBar implementation uses a circular drawable on API < 21 68 | * we want to use the same method to set its bounds as the Ripple's hotspot bounds. 69 | * 70 | * @param drawable 71 | * @param left 72 | * @param top 73 | * @param right 74 | * @param bottom 75 | */ 76 | public static void setHotspotBounds(Drawable drawable, int left, int top, int right, int bottom) { 77 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 78 | //We don't want the full size rect, Lollipop ripple would be too big 79 | int size = (right - left) / 8; 80 | DrawableCompat.setHotspotBounds(drawable, left + size, top + size, right - size, bottom - size); 81 | } else { 82 | drawable.setBounds(left, top, right, bottom); 83 | } 84 | } 85 | 86 | /** 87 | * android.support.v4.view.ViewCompat SHOULD include this once and for all!! 88 | * But it doesn't... 89 | * 90 | * @param view 91 | * @param background 92 | */ 93 | @SuppressWarnings("deprecation") 94 | public static void setBackground(View view, Drawable background) { 95 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { 96 | SeekBarCompatDontCrash.setBackground(view, background); 97 | } else { 98 | view.setBackgroundDrawable(background); 99 | } 100 | } 101 | 102 | /** 103 | * Sets the TextView text direction attribute when possible 104 | * 105 | * @param textView 106 | * @param textDirection 107 | * @see android.widget.TextView#setTextDirection(int) 108 | */ 109 | public static void setTextDirection(TextView textView, int textDirection) { 110 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { 111 | SeekBarCompatDontCrash.setTextDirection(textView, textDirection); 112 | } 113 | } 114 | 115 | public static boolean isInScrollingContainer(ViewParent p) { 116 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 117 | return SeekBarCompatDontCrash.isInScrollingContainer(p); 118 | } 119 | return false; 120 | } 121 | 122 | public static boolean isHardwareAccelerated(View view) { 123 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 124 | return SeekBarCompatDontCrash.isHardwareAccelerated(view); 125 | } 126 | return false; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /libraries/discreteSeekBar/src/main/java/org/adw/library/widgets/discreteseekbar/internal/compat/SeekBarCompatDontCrash.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.adw.library.widgets.discreteseekbar.internal.compat; 18 | 19 | import android.annotation.TargetApi; 20 | import android.content.res.ColorStateList; 21 | import android.graphics.Outline; 22 | import android.graphics.drawable.Drawable; 23 | import android.graphics.drawable.RippleDrawable; 24 | import android.view.View; 25 | import android.view.ViewGroup; 26 | import android.view.ViewOutlineProvider; 27 | import android.view.ViewParent; 28 | import android.widget.TextView; 29 | 30 | import org.adw.library.widgets.discreteseekbar.internal.drawable.MarkerDrawable; 31 | 32 | /** 33 | * Wrapper compatibility class to call some API-Specific methods 34 | * And offer alternate procedures when possible 35 | * 36 | * @hide 37 | */ 38 | @TargetApi(21) 39 | class SeekBarCompatDontCrash { 40 | public static void setOutlineProvider(View marker, final MarkerDrawable markerDrawable) { 41 | marker.setOutlineProvider(new ViewOutlineProvider() { 42 | @Override 43 | public void getOutline(View view, Outline outline) { 44 | outline.setConvexPath(markerDrawable.getPath()); 45 | } 46 | }); 47 | } 48 | 49 | public static Drawable getRipple(ColorStateList colorStateList) { 50 | return new RippleDrawable(colorStateList, null, null); 51 | } 52 | 53 | public static void setBackground(View view, Drawable background) { 54 | view.setBackground(background); 55 | } 56 | 57 | public static void setTextDirection(TextView number, int textDirection) { 58 | number.setTextDirection(textDirection); 59 | } 60 | 61 | public static boolean isInScrollingContainer(ViewParent p) { 62 | while (p != null && p instanceof ViewGroup) { 63 | if (((ViewGroup) p).shouldDelayChildPressedState()) { 64 | return true; 65 | } 66 | p = p.getParent(); 67 | } 68 | return false; 69 | } 70 | 71 | public static boolean isHardwareAccelerated(View view) { 72 | return view.isHardwareAccelerated(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /libraries/discreteSeekBar/src/main/java/org/adw/library/widgets/discreteseekbar/internal/drawable/AlmostRippleDrawable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.adw.library.widgets.discreteseekbar.internal.drawable; 18 | 19 | import android.content.res.ColorStateList; 20 | import android.graphics.Canvas; 21 | import android.graphics.Color; 22 | import android.graphics.Paint; 23 | import android.graphics.Rect; 24 | import android.graphics.drawable.Animatable; 25 | import android.os.SystemClock; 26 | import android.support.annotation.NonNull; 27 | import android.view.animation.AccelerateDecelerateInterpolator; 28 | import android.view.animation.Interpolator; 29 | 30 | public class AlmostRippleDrawable extends StateDrawable implements Animatable { 31 | private static final long FRAME_DURATION = 1000 / 60; 32 | private static final int ANIMATION_DURATION = 250; 33 | 34 | private static final float INACTIVE_SCALE = 0f; 35 | private static final float ACTIVE_SCALE = 1f; 36 | private float mCurrentScale = INACTIVE_SCALE; 37 | private Interpolator mInterpolator; 38 | private long mStartTime; 39 | private boolean mReverse = false; 40 | private boolean mRunning = false; 41 | private int mDuration = ANIMATION_DURATION; 42 | private float mAnimationInitialValue; 43 | //We don't use colors just with our drawable state because of animations 44 | private int mPressedColor; 45 | private int mFocusedColor; 46 | private int mDisabledColor; 47 | private int mRippleColor; 48 | private int mRippleBgColor; 49 | 50 | public AlmostRippleDrawable(@NonNull ColorStateList tintStateList) { 51 | super(tintStateList); 52 | mInterpolator = new AccelerateDecelerateInterpolator(); 53 | mFocusedColor = tintStateList.getColorForState(new int[]{android.R.attr.state_focused}, 0xFFFF0000); 54 | mPressedColor = tintStateList.getColorForState(new int[]{android.R.attr.state_pressed}, 0xFFFF0000); 55 | mDisabledColor = tintStateList.getColorForState(new int[]{-android.R.attr.state_enabled}, 0xFFFF0000); 56 | } 57 | 58 | @Override 59 | public void doDraw(Canvas canvas, Paint paint) { 60 | Rect bounds = getBounds(); 61 | int size = Math.min(bounds.width(), bounds.height()); 62 | float scale = mCurrentScale; 63 | int rippleColor = mRippleColor; 64 | int bgColor = mRippleBgColor; 65 | float radius = (size / 2); 66 | float radiusAnimated = radius * scale; 67 | if (scale > INACTIVE_SCALE) { 68 | if (bgColor != 0) { 69 | paint.setColor(bgColor); 70 | paint.setAlpha(decreasedAlpha(Color.alpha(bgColor))); 71 | canvas.drawCircle(bounds.centerX(), bounds.centerY(), radius, paint); 72 | } 73 | if (rippleColor != 0) { 74 | paint.setColor(rippleColor); 75 | paint.setAlpha(modulateAlpha(Color.alpha(rippleColor))); 76 | canvas.drawCircle(bounds.centerX(), bounds.centerY(), radiusAnimated, paint); 77 | } 78 | } 79 | } 80 | 81 | private int decreasedAlpha(int alpha) { 82 | int scale = 100 + (100 >> 7); 83 | return alpha * scale >> 8; 84 | } 85 | 86 | @Override 87 | public boolean setState(int[] stateSet) { 88 | int[] oldState = getState(); 89 | boolean oldPressed = false; 90 | for (int i : oldState) { 91 | if (i == android.R.attr.state_pressed) { 92 | oldPressed = true; 93 | } 94 | } 95 | super.setState(stateSet); 96 | boolean focused = false; 97 | boolean pressed = false; 98 | boolean disabled = true; 99 | for (int i : stateSet) { 100 | if (i == android.R.attr.state_focused) { 101 | focused = true; 102 | } else if (i == android.R.attr.state_pressed) { 103 | pressed = true; 104 | } else if (i == android.R.attr.state_enabled) { 105 | disabled = false; 106 | } 107 | } 108 | 109 | if (disabled) { 110 | unscheduleSelf(mUpdater); 111 | mRippleColor = mDisabledColor; 112 | mRippleBgColor = 0; 113 | mCurrentScale = ACTIVE_SCALE / 2; 114 | invalidateSelf(); 115 | } else { 116 | if (pressed) { 117 | animateToPressed(); 118 | mRippleColor = mRippleBgColor = mPressedColor; 119 | } else if (oldPressed) { 120 | mRippleColor = mRippleBgColor = mPressedColor; 121 | animateToNormal(); 122 | } else if (focused) { 123 | mRippleColor = mFocusedColor; 124 | mRippleBgColor = 0; 125 | mCurrentScale = ACTIVE_SCALE; 126 | invalidateSelf(); 127 | } else { 128 | mRippleColor = 0; 129 | mRippleBgColor = 0; 130 | mCurrentScale = INACTIVE_SCALE; 131 | invalidateSelf(); 132 | } 133 | } 134 | return true; 135 | } 136 | 137 | public void animateToPressed() { 138 | unscheduleSelf(mUpdater); 139 | if (mCurrentScale < ACTIVE_SCALE) { 140 | mReverse = false; 141 | mRunning = true; 142 | mAnimationInitialValue = mCurrentScale; 143 | float durationFactor = 1f - ((mAnimationInitialValue - INACTIVE_SCALE) / (ACTIVE_SCALE - INACTIVE_SCALE)); 144 | mDuration = (int) (ANIMATION_DURATION * durationFactor); 145 | mStartTime = SystemClock.uptimeMillis(); 146 | scheduleSelf(mUpdater, mStartTime + FRAME_DURATION); 147 | } 148 | } 149 | 150 | public void animateToNormal() { 151 | unscheduleSelf(mUpdater); 152 | if (mCurrentScale > INACTIVE_SCALE) { 153 | mReverse = true; 154 | mRunning = true; 155 | mAnimationInitialValue = mCurrentScale; 156 | float durationFactor = 1f - ((mAnimationInitialValue - ACTIVE_SCALE) / (INACTIVE_SCALE - ACTIVE_SCALE)); 157 | mDuration = (int) (ANIMATION_DURATION * durationFactor); 158 | mStartTime = SystemClock.uptimeMillis(); 159 | scheduleSelf(mUpdater, mStartTime + FRAME_DURATION); 160 | } 161 | } 162 | 163 | private void updateAnimation(float factor) { 164 | float initial = mAnimationInitialValue; 165 | float destination = mReverse ? INACTIVE_SCALE : ACTIVE_SCALE; 166 | mCurrentScale = initial + (destination - initial) * factor; 167 | invalidateSelf(); 168 | } 169 | 170 | private final Runnable mUpdater = new Runnable() { 171 | 172 | @Override 173 | public void run() { 174 | 175 | long currentTime = SystemClock.uptimeMillis(); 176 | long diff = currentTime - mStartTime; 177 | if (diff < mDuration) { 178 | float interpolation = mInterpolator.getInterpolation((float) diff / (float) mDuration); 179 | scheduleSelf(mUpdater, currentTime + FRAME_DURATION); 180 | updateAnimation(interpolation); 181 | } else { 182 | unscheduleSelf(mUpdater); 183 | mRunning = false; 184 | updateAnimation(1f); 185 | } 186 | } 187 | }; 188 | 189 | @Override 190 | public void start() { 191 | //No-Op. We control our own animation 192 | } 193 | 194 | @Override 195 | public void stop() { 196 | //No-Op. We control our own animation 197 | } 198 | 199 | @Override 200 | public boolean isRunning() { 201 | return mRunning; 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /libraries/discreteSeekBar/src/main/java/org/adw/library/widgets/discreteseekbar/internal/drawable/MarkerDrawable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.adw.library.widgets.discreteseekbar.internal.drawable; 18 | 19 | import android.content.res.ColorStateList; 20 | import android.graphics.Canvas; 21 | import android.graphics.Color; 22 | import android.graphics.Matrix; 23 | import android.graphics.Paint; 24 | import android.graphics.Path; 25 | import android.graphics.Rect; 26 | import android.graphics.RectF; 27 | import android.graphics.drawable.Animatable; 28 | import android.os.SystemClock; 29 | import android.support.annotation.NonNull; 30 | import android.view.animation.AccelerateDecelerateInterpolator; 31 | import android.view.animation.Interpolator; 32 | 33 | /** 34 | * Implementation of {@link StateDrawable} to draw a morphing marker symbol. 35 | *

36 | * It's basically an implementation of an {@link android.graphics.drawable.Animatable} Drawable with the following details: 37 | *

38 | *
    39 | *
  • Animates from a circle shape to a "marker" shape just using a RoundRect
  • 40 | *
  • Animates color change from the normal state color to the pressed state color
  • 41 | *
  • Uses a {@link android.graphics.Path} to also serve as Outline for API>=21
  • 42 | *
43 | * 44 | * @hide 45 | */ 46 | public class MarkerDrawable extends StateDrawable implements Animatable { 47 | private static final long FRAME_DURATION = 1000 / 60; 48 | private static final int ANIMATION_DURATION = 250; 49 | 50 | private float mCurrentScale = 0f; 51 | private Interpolator mInterpolator; 52 | private long mStartTime; 53 | private boolean mReverse = false; 54 | private boolean mRunning = false; 55 | private int mDuration = ANIMATION_DURATION; 56 | //size of the actual thumb drawable to use as circle state size 57 | private float mClosedStateSize; 58 | //value to store que current scale when starting an animation and interpolate from it 59 | private float mAnimationInitialValue; 60 | //extra offset directed from the View to account 61 | //for its internal padding between circle state and marker state 62 | private int mExternalOffset; 63 | //colors for interpolation 64 | private int mStartColor; 65 | private int mEndColor; 66 | 67 | Path mPath = new Path(); 68 | RectF mRect = new RectF(); 69 | Matrix mMatrix = new Matrix(); 70 | private MarkerAnimationListener mMarkerListener; 71 | 72 | public MarkerDrawable(@NonNull ColorStateList tintList, int closedSize) { 73 | super(tintList); 74 | mInterpolator = new AccelerateDecelerateInterpolator(); 75 | mClosedStateSize = closedSize; 76 | mStartColor = tintList.getColorForState(new int[]{android.R.attr.state_pressed}, tintList.getDefaultColor()); 77 | mEndColor = tintList.getDefaultColor(); 78 | 79 | } 80 | 81 | public void setExternalOffset(int offset) { 82 | mExternalOffset = offset; 83 | } 84 | 85 | /** 86 | * The two colors that will be used for the seek thumb. 87 | * 88 | * @param startColor Color used for the seek thumb 89 | * @param endColor Color used for popup indicator 90 | */ 91 | public void setColors(int startColor, int endColor) { 92 | mStartColor = startColor; 93 | mEndColor = endColor; 94 | } 95 | 96 | @Override 97 | void doDraw(Canvas canvas, Paint paint) { 98 | if (!mPath.isEmpty()) { 99 | paint.setStyle(Paint.Style.FILL); 100 | int color = blendColors(mStartColor, mEndColor, mCurrentScale); 101 | paint.setColor(color); 102 | canvas.drawPath(mPath, paint); 103 | } 104 | } 105 | 106 | public Path getPath() { 107 | return mPath; 108 | } 109 | 110 | @Override 111 | protected void onBoundsChange(Rect bounds) { 112 | super.onBoundsChange(bounds); 113 | computePath(bounds); 114 | } 115 | 116 | private void computePath(Rect bounds) { 117 | final float currentScale = mCurrentScale; 118 | final Path path = mPath; 119 | final RectF rect = mRect; 120 | final Matrix matrix = mMatrix; 121 | 122 | path.reset(); 123 | int totalSize = Math.min(bounds.width(), bounds.height()); 124 | 125 | float initial = mClosedStateSize; 126 | float destination = totalSize; 127 | float currentSize = initial + (destination - initial) * currentScale; 128 | 129 | float halfSize = currentSize / 2f; 130 | float inverseScale = 1f - currentScale; 131 | float cornerSize = halfSize * inverseScale; 132 | float[] corners = new float[]{halfSize, halfSize, halfSize, halfSize, halfSize, halfSize, cornerSize, cornerSize}; 133 | rect.set(bounds.left, bounds.top, bounds.left + currentSize, bounds.top + currentSize); 134 | path.addRoundRect(rect, corners, Path.Direction.CCW); 135 | matrix.reset(); 136 | matrix.postRotate(-45, bounds.left + halfSize, bounds.top + halfSize); 137 | matrix.postTranslate((bounds.width() - currentSize) / 2, 0); 138 | float hDiff = (bounds.bottom - currentSize - mExternalOffset) * inverseScale; 139 | matrix.postTranslate(0, hDiff); 140 | path.transform(matrix); 141 | } 142 | 143 | private void updateAnimation(float factor) { 144 | float initial = mAnimationInitialValue; 145 | float destination = mReverse ? 0f : 1f; 146 | mCurrentScale = initial + (destination - initial) * factor; 147 | computePath(getBounds()); 148 | invalidateSelf(); 149 | } 150 | 151 | public void animateToPressed() { 152 | unscheduleSelf(mUpdater); 153 | mReverse = false; 154 | if (mCurrentScale < 1) { 155 | mRunning = true; 156 | mAnimationInitialValue = mCurrentScale; 157 | float durationFactor = 1f - mCurrentScale; 158 | mDuration = (int) (ANIMATION_DURATION * durationFactor); 159 | mStartTime = SystemClock.uptimeMillis(); 160 | scheduleSelf(mUpdater, mStartTime + FRAME_DURATION); 161 | } else { 162 | notifyFinishedToListener(); 163 | } 164 | } 165 | 166 | public void animateToNormal() { 167 | mReverse = true; 168 | unscheduleSelf(mUpdater); 169 | if (mCurrentScale > 0) { 170 | mRunning = true; 171 | mAnimationInitialValue = mCurrentScale; 172 | float durationFactor = 1f - mCurrentScale; 173 | mDuration = ANIMATION_DURATION - (int) (ANIMATION_DURATION * durationFactor); 174 | mStartTime = SystemClock.uptimeMillis(); 175 | scheduleSelf(mUpdater, mStartTime + FRAME_DURATION); 176 | } else { 177 | notifyFinishedToListener(); 178 | } 179 | } 180 | 181 | private final Runnable mUpdater = new Runnable() { 182 | 183 | @Override 184 | public void run() { 185 | 186 | long currentTime = SystemClock.uptimeMillis(); 187 | long diff = currentTime - mStartTime; 188 | if (diff < mDuration) { 189 | float interpolation = mInterpolator.getInterpolation((float) diff / (float) mDuration); 190 | scheduleSelf(mUpdater, currentTime + FRAME_DURATION); 191 | updateAnimation(interpolation); 192 | } else { 193 | unscheduleSelf(mUpdater); 194 | mRunning = false; 195 | updateAnimation(1f); 196 | notifyFinishedToListener(); 197 | } 198 | } 199 | }; 200 | 201 | public void setMarkerListener(MarkerAnimationListener listener) { 202 | mMarkerListener = listener; 203 | } 204 | 205 | private void notifyFinishedToListener() { 206 | if (mMarkerListener != null) { 207 | if (mReverse) { 208 | mMarkerListener.onClosingComplete(); 209 | } else { 210 | mMarkerListener.onOpeningComplete(); 211 | } 212 | } 213 | } 214 | 215 | @Override 216 | public void start() { 217 | //No-Op. We control our own animation 218 | } 219 | 220 | @Override 221 | public void stop() { 222 | unscheduleSelf(mUpdater); 223 | } 224 | 225 | @Override 226 | public boolean isRunning() { 227 | return mRunning; 228 | } 229 | 230 | private static int blendColors(int color1, int color2, float factor) { 231 | final float inverseFactor = 1f - factor; 232 | float a = (Color.alpha(color1) * factor) + (Color.alpha(color2) * inverseFactor); 233 | float r = (Color.red(color1) * factor) + (Color.red(color2) * inverseFactor); 234 | float g = (Color.green(color1) * factor) + (Color.green(color2) * inverseFactor); 235 | float b = (Color.blue(color1) * factor) + (Color.blue(color2) * inverseFactor); 236 | return Color.argb((int) a, (int) r, (int) g, (int) b); 237 | } 238 | 239 | 240 | /** 241 | * A listener interface to porpagate animation events 242 | * This is the "poor's man" AnimatorListener for this Drawable 243 | */ 244 | public interface MarkerAnimationListener { 245 | public void onClosingComplete(); 246 | 247 | public void onOpeningComplete(); 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /libraries/discreteSeekBar/src/main/java/org/adw/library/widgets/discreteseekbar/internal/drawable/StateDrawable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.adw.library.widgets.discreteseekbar.internal.drawable; 18 | 19 | import android.content.res.ColorStateList; 20 | import android.graphics.Canvas; 21 | import android.graphics.Color; 22 | import android.graphics.ColorFilter; 23 | import android.graphics.Paint; 24 | import android.graphics.PixelFormat; 25 | import android.graphics.drawable.Drawable; 26 | import android.support.annotation.NonNull; 27 | 28 | /** 29 | * A drawable that changes it's Paint color depending on the Drawable State 30 | *

31 | * Subclasses should implement {@link #doDraw(android.graphics.Canvas, android.graphics.Paint)} 32 | *

33 | * 34 | * @hide 35 | */ 36 | public abstract class StateDrawable extends Drawable { 37 | private ColorStateList mTintStateList; 38 | private final Paint mPaint; 39 | private int mCurrentColor; 40 | private int mAlpha = 255; 41 | 42 | public StateDrawable(@NonNull ColorStateList tintStateList) { 43 | super(); 44 | mTintStateList = tintStateList; 45 | mCurrentColor = tintStateList.getDefaultColor(); 46 | mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 47 | } 48 | 49 | @Override 50 | public boolean isStateful() { 51 | return (mTintStateList.isStateful()) || super.isStateful(); 52 | } 53 | 54 | @Override 55 | public boolean setState(int[] stateSet) { 56 | boolean handled = super.setState(stateSet); 57 | handled = updateTint(stateSet) || handled; 58 | return handled; 59 | } 60 | 61 | @Override 62 | public int getOpacity() { 63 | return PixelFormat.TRANSLUCENT; 64 | } 65 | 66 | private boolean updateTint(int[] state) { 67 | final int color = mTintStateList.getColorForState(state, mCurrentColor); 68 | if (color != mCurrentColor) { 69 | mCurrentColor = color; 70 | //We've changed states 71 | invalidateSelf(); 72 | return true; 73 | } 74 | return false; 75 | } 76 | 77 | @Override 78 | public void draw(Canvas canvas) { 79 | mPaint.setColor(mCurrentColor); 80 | int alpha = modulateAlpha(Color.alpha(mCurrentColor)); 81 | mPaint.setAlpha(alpha); 82 | doDraw(canvas, mPaint); 83 | } 84 | 85 | public void setColorStateList(@NonNull ColorStateList tintStateList) { 86 | mTintStateList = tintStateList; 87 | } 88 | 89 | /** 90 | * Subclasses should implement this method to do the actual drawing 91 | * 92 | * @param canvas The current {@link android.graphics.Canvas} to draw into 93 | * @param paint The {@link android.graphics.Paint} preconfigurred with the current 94 | * {@link android.content.res.ColorStateList} color 95 | */ 96 | abstract void doDraw(Canvas canvas, Paint paint); 97 | 98 | @Override 99 | public void setAlpha(int alpha) { 100 | mAlpha = alpha; 101 | invalidateSelf(); 102 | } 103 | 104 | int modulateAlpha(int alpha) { 105 | int scale = mAlpha + (mAlpha >> 7); 106 | return alpha * scale >> 8; 107 | } 108 | 109 | @Override 110 | public int getAlpha() { 111 | return mAlpha; 112 | } 113 | 114 | @Override 115 | public void setColorFilter(ColorFilter cf) { 116 | mPaint.setColorFilter(cf); 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /libraries/discreteSeekBar/src/main/java/org/adw/library/widgets/discreteseekbar/internal/drawable/ThumbDrawable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.adw.library.widgets.discreteseekbar.internal.drawable; 18 | 19 | import android.content.res.ColorStateList; 20 | import android.graphics.Canvas; 21 | import android.graphics.Paint; 22 | import android.graphics.Rect; 23 | import android.graphics.drawable.Animatable; 24 | import android.os.SystemClock; 25 | import android.support.annotation.NonNull; 26 | 27 | /** 28 | *

HACK

29 | *

30 | * Special {@link org.adw.library.widgets.discreteseekbar.internal.drawable.StateDrawable} implementation 31 | * to draw the Thumb circle. 32 | *

33 | *

34 | * It's special because it will stop drawing once the state is pressed/focused BUT only after a small delay. 35 | *

36 | *

37 | * This special delay is meant to help avoiding frame glitches while the {@link org.adw.library.widgets.discreteseekbar.internal.Marker} is added to the Window 38 | *

39 | * 40 | * @hide 41 | */ 42 | public class ThumbDrawable extends StateDrawable implements Animatable { 43 | //The current size for this drawable. Must be converted to real DPs 44 | public static final int DEFAULT_SIZE_DP = 12; 45 | private final int mSize; 46 | private boolean mOpen; 47 | private boolean mRunning; 48 | 49 | public ThumbDrawable(@NonNull ColorStateList tintStateList, int size) { 50 | super(tintStateList); 51 | mSize = size; 52 | } 53 | 54 | @Override 55 | public int getIntrinsicWidth() { 56 | return mSize; 57 | } 58 | 59 | @Override 60 | public int getIntrinsicHeight() { 61 | return mSize; 62 | } 63 | 64 | @Override 65 | public void doDraw(Canvas canvas, Paint paint) { 66 | if (!mOpen) { 67 | Rect bounds = getBounds(); 68 | float radius = (mSize / 2); 69 | canvas.drawCircle(bounds.centerX(), bounds.centerY(), radius, paint); 70 | } 71 | } 72 | 73 | public void animateToPressed() { 74 | scheduleSelf(opener, SystemClock.uptimeMillis() + 100); 75 | mRunning = true; 76 | } 77 | 78 | public void animateToNormal() { 79 | mOpen = false; 80 | mRunning = false; 81 | unscheduleSelf(opener); 82 | invalidateSelf(); 83 | } 84 | 85 | private Runnable opener = new Runnable() { 86 | @Override 87 | public void run() { 88 | mOpen = true; 89 | invalidateSelf(); 90 | mRunning = false; 91 | } 92 | }; 93 | 94 | @Override 95 | public void start() { 96 | //NOOP 97 | } 98 | 99 | @Override 100 | public void stop() { 101 | animateToNormal(); 102 | } 103 | 104 | @Override 105 | public boolean isRunning() { 106 | return mRunning; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /libraries/discreteSeekBar/src/main/java/org/adw/library/widgets/discreteseekbar/internal/drawable/TrackOvalDrawable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.adw.library.widgets.discreteseekbar.internal.drawable; 18 | 19 | import android.content.res.ColorStateList; 20 | import android.graphics.Canvas; 21 | import android.graphics.Paint; 22 | import android.graphics.RectF; 23 | import android.support.annotation.NonNull; 24 | 25 | /** 26 | * Simple {@link org.adw.library.widgets.discreteseekbar.internal.drawable.StateDrawable} implementation 27 | * to draw circles/ovals 28 | * 29 | * @hide 30 | */ 31 | public class TrackOvalDrawable extends StateDrawable { 32 | private RectF mRectF = new RectF(); 33 | 34 | public TrackOvalDrawable(@NonNull ColorStateList tintStateList) { 35 | super(tintStateList); 36 | } 37 | 38 | @Override 39 | void doDraw(Canvas canvas, Paint paint) { 40 | mRectF.set(getBounds()); 41 | canvas.drawOval(mRectF, paint); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /libraries/discreteSeekBar/src/main/java/org/adw/library/widgets/discreteseekbar/internal/drawable/TrackRectDrawable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.adw.library.widgets.discreteseekbar.internal.drawable; 18 | 19 | import android.content.res.ColorStateList; 20 | import android.graphics.Canvas; 21 | import android.graphics.Paint; 22 | import android.support.annotation.NonNull; 23 | 24 | /** 25 | * Simple {@link org.adw.library.widgets.discreteseekbar.internal.drawable.StateDrawable} implementation 26 | * to draw rectangles 27 | * 28 | * @hide 29 | */ 30 | public class TrackRectDrawable extends StateDrawable { 31 | public TrackRectDrawable(@NonNull ColorStateList tintStateList) { 32 | super(tintStateList); 33 | } 34 | 35 | @Override 36 | void doDraw(Canvas canvas, Paint paint) { 37 | canvas.drawRect(getBounds(), paint); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /libraries/discreteSeekBar/src/main/res/color/dsb_progress_color_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /libraries/discreteSeekBar/src/main/res/color/dsb_ripple_color_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /libraries/discreteSeekBar/src/main/res/color/dsb_track_color_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /libraries/discreteSeekBar/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /libraries/discreteSeekBar/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | #ff009688 19 | #ff939393 20 | #66939393 21 | #77939393 22 | #99999999 23 | 24 | 33 | 34 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':libraries:SwipeBackLayout', ':libraries:discreteSeekBar' --------------------------------------------------------------------------------