diff --git a/.changelog/lang_ar.json b/.changelog/lang_ar.json index 4607fa62..472e5fe8 100644 --- a/.changelog/lang_ar.json +++ b/.changelog/lang_ar.json @@ -86,8 +86,8 @@ "نص بناء Gradle أصبح أكثر قدرة على التكيّف مع الإصدارات الجديدة _[`discussion #369`](http://discussions.autojs6.com/369)_" ], "dependency": [ - "مرفق Material Dialogs الإصدار 0.9.6.0 (محلي)", - "مرفق Material Date Time Picker الإصدار 4.2.3 (محلي)", + "مرفق Material Dialogs الإصدار 0.9.6.0 (تجزئة إلى وحدات)", + "مرفق Material Date Time Picker الإصدار 4.2.3 (تجزئة إلى وحدات)", "مرفق libimagequant الإصدار 2.17.0 (محلي)", "مرفق libpng الإصدار 1.6.49 (محلي)", "مضاف ICU4J الإصدار 77.1", diff --git a/.changelog/lang_en.json b/.changelog/lang_en.json index efeb5e87..8b0fbb23 100644 --- a/.changelog/lang_en.json +++ b/.changelog/lang_en.json @@ -86,8 +86,8 @@ "Gradle build script now better adapts to newer versions _[`discussion #369`](http://discussions.autojs6.com/369)_" ], "dependency": [ - "Bundled Material Dialogs 0.9.6.0 (localized)", - "Bundled Material Date Time Picker 4.2.3 (localized)", + "Bundled Material Dialogs 0.9.6.0 (modularized)", + "Bundled Material Date Time Picker 4.2.3 (modularized)", "Bundled libimagequant 2.17.0 (localized)", "Bundled libpng 1.6.49 (localized)", "Added ICU4J 77.1", diff --git a/.changelog/lang_es.json b/.changelog/lang_es.json index 64e167a1..774160e9 100644 --- a/.changelog/lang_es.json +++ b/.changelog/lang_es.json @@ -86,8 +86,8 @@ "El script de compilación Gradle mejora su capacidad de adaptación de versiones _[`discussion #369`](http://discussions.autojs6.com/369)_" ], "dependency": [ - "Incluido Material Dialogs versión 0.9.6.0 (localizado)", - "Incluido Material Date Time Picker versión 4.2.3 (localizado)", + "Incluido Material Dialogs versión 0.9.6.0 (modularizado)", + "Incluido Material Date Time Picker versión 4.2.3 (modularizado)", "Incluido libimagequant versión 2.17.0 (localizado)", "Incluido libpng versión 1.6.49 (localizado)", "Añadido ICU4J versión 77.1", diff --git a/.changelog/lang_fr.json b/.changelog/lang_fr.json index d5dab09e..f690d06f 100644 --- a/.changelog/lang_fr.json +++ b/.changelog/lang_fr.json @@ -86,8 +86,8 @@ "Le script de build Gradle améliore sa capacité d'adaptation aux nouvelles versions _[`discussion #369`](http://discussions.autojs6.com/369)_" ], "dependency": [ - "Inclus Material Dialogs version 0.9.6.0 (localisé)", - "Inclus Material Date Time Picker version 4.2.3 (localisé)", + "Inclus Material Dialogs version 0.9.6.0 (modularise)", + "Inclus Material Date Time Picker version 4.2.3 (modularise)", "Inclus libimagequant version 2.17.0 (localisé)", "Inclus libpng version 1.6.49 (localisé)", "Ajouté ICU4J version 77.1", diff --git a/.changelog/lang_ja.json b/.changelog/lang_ja.json index 909de851..163f1138 100644 --- a/.changelog/lang_ja.json +++ b/.changelog/lang_ja.json @@ -86,8 +86,8 @@ "Gradle ビルドスクリプトが新しいバージョンに柔軟に対応 _[`discussion #369`](http://discussions.autojs6.com/369)_" ], "dependency": [ - "同梱 Material Dialogs バージョン 0.9.6.0 (ローカライズ済み)", - "同梱 Material Date Time Picker バージョン 4.2.3 (ローカライズ済み)", + "同梱 Material Dialogs バージョン 0.9.6.0 (モジュール化)", + "同梱 Material Date Time Picker バージョン 4.2.3 (モジュール化)", "同梱 libimagequant バージョン 2.17.0 (ローカライズ済み)", "同梱 libpng バージョン 1.6.49 (ローカライズ済み)", "追加 ICU4J バージョン 77.1", diff --git a/.changelog/lang_ko.json b/.changelog/lang_ko.json index 214ccca6..ac84b127 100644 --- a/.changelog/lang_ko.json +++ b/.changelog/lang_ko.json @@ -86,8 +86,8 @@ "Gradle 빌드 스크립트가 최신 버전에 더 유연하게 대응 _[`discussion #369`](http://discussions.autojs6.com/369)_" ], "dependency": [ - "내장 Material Dialogs 버전 0.9.6.0 (현지화)", - "내장 Material Date Time Picker 버전 4.2.3 (현지화)", + "내장 Material Dialogs 버전 0.9.6.0 (모듈화)", + "내장 Material Date Time Picker 버전 4.2.3 (모듈화)", "내장 libimagequant 버전 2.17.0 (현지화)", "내장 libpng 버전 1.6.49 (현지화)", "추가 ICU4J 버전 77.1", diff --git a/.changelog/lang_ru.json b/.changelog/lang_ru.json index 2d88f54d..dbf56a20 100644 --- a/.changelog/lang_ru.json +++ b/.changelog/lang_ru.json @@ -86,8 +86,8 @@ "Скрипт сборки Gradle лучше адаптируется к новым версиям _[`discussion #369`](http://discussions.autojs6.com/369)_" ], "dependency": [ - "Встроено Material Dialogs версии 0.9.6.0 (локализовано)", - "Встроено Material Date Time Picker версии 4.2.3 (локализовано)", + "Встроено Material Dialogs версии 0.9.6.0 (модульный)", + "Встроено Material Date Time Picker версии 4.2.3 (модульный)", "Встроено libimagequant версии 2.17.0 (локализовано)", "Встроено libpng версии 1.6.49 (локализовано)", "Добавлено ICU4J версии 77.1", diff --git a/.changelog/lang_zh-Hans.json b/.changelog/lang_zh-Hans.json index 228158fa..14769cae 100644 --- a/.changelog/lang_zh-Hans.json +++ b/.changelog/lang_zh-Hans.json @@ -177,18 +177,22 @@ "使用 [版本目录 (Version Catalogs)](https://developer.android.com/build/migrate-to-catalogs) 集中管理 Gradle 依赖和插件版本", "模块化 Gradle 构建脚本, 将共享构建逻辑迁移至 build-logic 并抽象为约定插件", "使用 Gradle 约定插件简化本地 AAR 库加载逻辑", + "移除 \"android.enableJetifier=true\" Gradle 配置选项以适配 Gradle 9", "使用 [Toolchain](https://docs.gradle.org/current/userguide/toolchains.html) 替代 sourceCompatibility/targetCompatibility 以降低构建环境差异" ], "dependency": [ + "模块化 Expandable Layout 版本 1.6.0", + "模块化 Expandable RecyclerView 版本 3.0.0-RC1", + "模块化 Recyclerview Flexible Divider 版本 1.4.0", "本地化 Root Shell 版本 1.6", - "本地化 Expandable Layout 版本 1.6.0", - "本地化 Recyclerview Flexible Divider 版本 1.4.0", "附加 AndroidX Activity (KTX) 版本 1.12.2", "附加 AndroidX Core (KTX) 版本 1.15.0", "附加 AndroidX Work Runtime 版本 2.11.0", + "附加 AndroidX Interpolator 版本 1.0.0", "附加 Room RxJava2 版本 2.8.1", "升级 Android SDK 版本 35 -> 36", - "升级 Gradle 版本 8.14 -> 8.14.3", + "升级 Gradle 8 版本 8.14 -> 8.14.4", + "升级 Gradle 9 版本 8.14 -> 9.4.0", "升级 Rhino 版本 1.8.1-SNAPSHOT -> 2.0.0-SNAPSHOT", "升级 Desugar JDK Libs 版本 2.0.4 -> 2.1.5", "升级 Android Material 版本 1.12.0 -> 1.13.0", @@ -205,7 +209,10 @@ "升级 ARSCLib 版本 1.3.1 -> 1.3.5", "升级 Jsoup 版本 1.19.1 -> 1.20.1", "升级 Junit Jupiter 版本 5.10.3 -> 5.13.0", - "升级 OpenCC 版本 1.2.0 -> 1.2.2" + "升级 OpenCC 版本 1.2.0 -> 1.2.2", + "移除 Android Support Annotation v7 版本 28.0.0", + "移除 Android Support AppCompat v7 版本 28.0.0", + "移除 Android Support RecyclerView v7 版本 28.0.0" ] }, "v6.6.4": { @@ -294,8 +301,8 @@ "Gradle 构建脚本提升版本自适应能力 _[`discussion #369`](http://discussions.autojs6.com/369)_" ], "dependency": [ - "本地化 Material Dialogs 版本 0.9.6.0", - "本地化 Material Date Time Picker 版本 4.2.3", + "模块化 Material Dialogs 版本 0.9.6.0", + "模块化 Material Date Time Picker 版本 4.2.3", "本地化 libimagequant 版本 2.17.0", "本地化 libpng 版本 1.6.49", "附加 ICU4J 版本 77.1", diff --git a/.changelog/lang_zh-Hant-HK.json b/.changelog/lang_zh-Hant-HK.json index 99e48d61..2e8e5cec 100644 --- a/.changelog/lang_zh-Hant-HK.json +++ b/.changelog/lang_zh-Hant-HK.json @@ -86,8 +86,8 @@ "Gradle 構建腳本提升版本自適應能力 _[`discussion #369`](http://discussions.autojs6.com/369)_" ], "dependency": [ - "本地化 Material Dialogs 版本 0.9.6.0", - "本地化 Material Date Time Picker 版本 4.2.3", + "模塊化 Material Dialogs 版本 0.9.6.0", + "模塊化 Material Date Time Picker 版本 4.2.3", "本地化 libimagequant 版本 2.17.0", "本地化 libpng 版本 1.6.49", "附加 ICU4J 版本 77.1", diff --git a/.changelog/lang_zh-Hant-TW.json b/.changelog/lang_zh-Hant-TW.json index 6b379ac3..57f4c7ba 100644 --- a/.changelog/lang_zh-Hant-TW.json +++ b/.changelog/lang_zh-Hant-TW.json @@ -86,8 +86,8 @@ "Gradle 構建指令碼提升版本自適應能力 _[`discussion #369`](http://discussions.autojs6.com/369)_" ], "dependency": [ - "本地化 Material Dialogs 版本 0.9.6.0", - "本地化 Material Date Time Picker 版本 4.2.3", + "模組化 Material Dialogs 版本 0.9.6.0", + "模組化 Material Date Time Picker 版本 4.2.3", "本地化 libimagequant 版本 2.17.0", "本地化 libpng 版本 1.6.49", "附加 ICU4J 版本 77.1", diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2b2e3d72..ee7adebb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -284,9 +284,9 @@ dependencies /* Test */ { dependencies /* Annotations */ { // Android Annotations - implementation(libs.androidannotations.api) + implementation(libs.android.annotations.api) implementation(libs.annotation) - ksp(libs.androidannotations) + ksp(libs.android.annotations) // JCIP Annotations implementation(libs.jcip.annotations) @@ -332,14 +332,12 @@ dependencies /* Material Dialogs */ { dependencies /* Layout */ { // Expandable Layout // implementation("com.github.aakira:expandable-layout:1.6.0") - implementation(project(":libs:expandable-layout-1_6_0")) + // implementation(project(":libs:expandable-layout-1_6_0")) + implementation(project(":modules:expandable-layout")) - // Expandable RecyclerView - implementation(libs.expandablerecyclerview) + implementation(project(":modules:expandable-recyclerview")) - // Flexible Divider - // implementation("com.yqritc:recyclerview-flexibledivider:1.4.0") - implementation(project(":libs:recyclerview-flexibledivider-1_4_0")) + implementation(project(":modules:recyclerview-flexibledivider")) } dependencies /* View */ { diff --git a/app/src/main/java/org/autojs/autojs/runtime/api/augment/selector/Selector.kt b/app/src/main/java/org/autojs/autojs/runtime/api/augment/selector/Selector.kt index 327c3ebe..1c8e7847 100644 --- a/app/src/main/java/org/autojs/autojs/runtime/api/augment/selector/Selector.kt +++ b/app/src/main/java/org/autojs/autojs/runtime/api/augment/selector/Selector.kt @@ -115,7 +115,7 @@ class Selector(private val scriptRuntime: ScriptRuntime) : Augmentable(scriptRun when { arg !is Double -> arg paramType == Double::class.java || paramType == java.lang.Double.TYPE -> arg - paramType == Float::class.java || paramType == java.lang.Float.TYPE -> arg.toDouble() + paramType == Float::class.java || paramType == java.lang.Float.TYPE -> arg.toFloat() paramType == Long::class.java || paramType == java.lang.Long.TYPE -> arg.roundToLong() paramType == Int::class.java || paramType == Integer.TYPE -> arg.roundToInt() paramType == Short::class.java || paramType == java.lang.Short.TYPE -> arg.toInt().toShort() diff --git a/gradle.properties b/gradle.properties index e2dc57d2..ce4dea3b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,7 +10,7 @@ # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -#Thu Nov 11 16:38:18 CST 2021 +# Thu Nov 11 16:38:18 CST 2021 # Configure memory settings for Gradle daemon and Kotlin daemon # -Xms: Initial heap size # -Xmx: Maximum heap size @@ -29,11 +29,6 @@ org.gradle.parallel=true # https://developer.android.com/topic/libraries/support-library/androidx-rn # https://developer.android.com/jetpack/androidx/migrate android.useAndroidX=true -# When this flag is set to true, -# the Android plugin automatically migrates existing third-party libraries -# to use AndroidX dependencies by rewriting their binaries. -# https://developer.android.com/jetpack/androidx/migrate -android.enableJetifier=true # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library @@ -44,8 +39,15 @@ android.nonFinalResIds=true # auto-download: Automatically download required Java version if not found org.gradle.java.installations.auto-detect=true org.gradle.java.installations.auto-download=true -# BuildConfig feature flag history: -# - Added in Gradle Build Tool 8.0.0-alpha09 -# - Removed in Gradle Build Tool 8.2.0-alpha15 -# - Controls generation of BuildConfig class +# ====== Archived ====== +# ------ Jetifier ------ +# When this flag is set to true, +# the Android plugin automatically migrates existing third-party libraries +# to use AndroidX dependencies by rewriting their binaries. +# https://developer.android.com/jetpack/androidx/migrate +# // Removed since Gradle 9.4.0 +# android.enableJetifier=true +# ------ BuildConfig ------ +# Controls generation of BuildConfig class +# // Removed since Gradle Build Tool 8.2.0-alpha15 # android.defaults.buildfeatures.buildconfig=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 90169f9e..ad5938f2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -40,10 +40,9 @@ log4j = "1.2.17" desugar = "2.1.5" activity-ktx = "1.12.2" +android-annotations = "4.8.0" android-device-names = "2.1.1" android-logging-log4j = "1.0.3" -android-support-v7 = "28.0.0" -androidannotations = "4.8.0" animated-svg-view = "1.0.6" annotation = "1.9.1" appcompat = "1.7.1" @@ -60,13 +59,13 @@ commons-lang3 = "3.18.0" constraintlayout = "2.2.1" easywindow = "10.3" eventbus = "3.3.1" -expandablerecyclerview = "3.0.0-RC1" flexbox = "3.0.0" flexmark-html2md = "0.64.8" foojay-resolver-convention = "1.0.0" glide = "5.0.5" gson = "2.13.2" icu4j = "77.1" +interpolator = "1.0.0" jcip-annotations = "1.0" jdeferred = "1.2.6" joda-time = "2.14.0" @@ -112,15 +111,14 @@ zip4j = "2.11.5" [libraries] activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activity-ktx" } +android-annotations = { module = "org.androidannotations:androidannotations", version.ref = "android-annotations" } +android-annotations-api = { module = "org.androidannotations:androidannotations-api", version.ref = "android-annotations" } android-device-names = { module = "com.jaredrummler:android-device-names", version.ref = "android-device-names" } android-logging-log4j = { module = "de.mindpipe.android:android-logging-log4j", version.ref = "android-logging-log4j" } -androidannotations = { module = "org.androidannotations:androidannotations", version.ref = "androidannotations" } -androidannotations-api = { module = "org.androidannotations:androidannotations-api", version.ref = "androidannotations" } animated-svg-view = { module = "com.jaredrummler:animated-svg-view", version.ref = "animated-svg-view" } annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" } apache-commons-compress = { module = "org.apache.commons:commons-compress", version.ref = "commons-compress" } appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } -appcompat-v7 = { module = "com.android.support:appcompat-v7", version.ref = "android-support-v7" } arsclib = { module = "io.github.reandroid:ARSCLib", version.ref = "arsclib" } barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version.ref = "barcode-scanning" } bcpkix-jdk15on = { module = "org.bouncycastle:bcpkix-jdk15on", version.ref = "bcpkix-jdk15on" } @@ -136,13 +134,13 @@ core-ktx = { module = "androidx.core:core-ktx", version.ref = "core-ktx" } desugar = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar" } easywindow = { module = "com.github.getActivity:EasyWindow", version.ref = "easywindow" } eventbus = { module = "org.greenrobot:eventbus", version.ref = "eventbus" } -expandablerecyclerview = { module = "com.bignerdranch.android:expandablerecyclerview", version.ref = "expandablerecyclerview" } flexbox = { module = "com.google.android.flexbox:flexbox", version.ref = "flexbox" } flexmark-html2md = { module = "com.vladsch.flexmark:flexmark-html2md-converter", version.ref = "flexmark-html2md" } glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" } glide-ksp = { module = "com.github.bumptech.glide:ksp", version.ref = "glide" } gson = { module = "com.google.code.gson:gson", version.ref = "gson" } icu4j = { module = "com.ibm.icu:icu4j", version.ref = "icu4j" } +interpolator = { group = "androidx.interpolator", name = "interpolator", version.ref = "interpolator" } jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson-databind" } jcip-annotations = { module = "net.jcip:jcip-annotations", version.ref = "jcip-annotations" } jdeferred = { module = "org.jdeferred:jdeferred-android-aar", version.ref = "jdeferred" } @@ -170,7 +168,6 @@ preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = prov = { module = "com.madgag.spongycastle:prov", version.ref = "prov" } r8 = { module = "com.android.tools:r8", version.ref = "r8" } recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" } -recyclerview-v7 = { module = "com.android.support:recyclerview-v7", version.ref = "android-support-v7" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } retrofit-adapter-rxjava2 = { module = "com.squareup.retrofit2:adapter-rxjava2", version.ref = "retrofit" } retrofit-converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" } @@ -184,7 +181,6 @@ rxandroid = { module = "io.reactivex.rxjava2:rxandroid", version.ref = "rxandroi rxjava = { module = "io.reactivex.rxjava2:rxjava", version.ref = "rxjava" } shizuku-api = { module = "dev.rikka.shizuku:api", version.ref = "shizuku" } shizuku-provider = { module = "dev.rikka.shizuku:provider", version.ref = "shizuku" } -support-annotations = { module = "com.android.support:support-annotations", version.ref = "android-support-v7" } swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "swiperefreshlayout" } test-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "test-espresso-core" } test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "test-junit-version" } @@ -200,4 +196,4 @@ zip4j = { module = "net.lingala.zip4j:zip4j", version.ref = "zip4j" } [plugins] -foojay-resolver-convention = { id = "org.gradle.toolchains.foojay-resolver-convention", version.ref = "foojay-resolver-convention" } \ No newline at end of file +foojay-resolver-convention = { id = "org.gradle.toolchains.foojay-resolver-convention", version.ref = "foojay-resolver-convention" } diff --git a/libs/expandable-layout-1_6_0/build.gradle.kts b/libs/expandable-layout-1_6_0/build.gradle.kts deleted file mode 100644 index 42049c16..00000000 --- a/libs/expandable-layout-1_6_0/build.gradle.kts +++ /dev/null @@ -1,7 +0,0 @@ -plugins { - id("org.autojs.build.local-arr-register-convention") -} - -localAars { - files += "expandable-layout-1.6.0.aar" -} diff --git a/libs/expandable-layout-1_6_0/expandable-layout-1.6.0.aar b/libs/expandable-layout-1_6_0/expandable-layout-1.6.0.aar deleted file mode 100644 index 8bcf51e0..00000000 Binary files a/libs/expandable-layout-1_6_0/expandable-layout-1.6.0.aar and /dev/null differ diff --git a/libs/recyclerview-flexibledivider-1_4_0/build.gradle.kts b/libs/recyclerview-flexibledivider-1_4_0/build.gradle.kts deleted file mode 100644 index 23d4676e..00000000 --- a/libs/recyclerview-flexibledivider-1_4_0/build.gradle.kts +++ /dev/null @@ -1,7 +0,0 @@ -plugins { - id("org.autojs.build.local-arr-register-convention") -} - -localAars { - files += "recyclerview-flexibledivider-1.4.0.aar" -} diff --git a/libs/recyclerview-flexibledivider-1_4_0/recyclerview-flexibledivider-1.4.0.aar b/libs/recyclerview-flexibledivider-1_4_0/recyclerview-flexibledivider-1.4.0.aar deleted file mode 100644 index ce3040ef..00000000 Binary files a/libs/recyclerview-flexibledivider-1_4_0/recyclerview-flexibledivider-1.4.0.aar and /dev/null differ diff --git a/modules/expandable-layout/build.gradle b/modules/expandable-layout/build.gradle new file mode 100644 index 00000000..c7b54453 --- /dev/null +++ b/modules/expandable-layout/build.gradle @@ -0,0 +1,22 @@ +plugins { + id("org.autojs.build.versions") + id("org.autojs.build.jvm-convention") + id("com.android.library") +} + +android { + namespace "com.github.aakira.expandablelayout" + compileSdkVersion versions.sdkVersionCompile + + defaultConfig { + minSdkVersion versions.sdkVersionMin + targetSdkVersion versions.sdkVersionTarget + versionCode 12 + versionName "1.6.0 Mod" + } +} + +dependencies { + implementation libs.annotation + implementation libs.interpolator +} diff --git a/modules/expandable-layout/proguard-rules.pro b/modules/expandable-layout/proguard-rules.pro new file mode 100644 index 00000000..7ed59259 --- /dev/null +++ b/modules/expandable-layout/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Applications/eclipse/android/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/modules/expandable-layout/src/main/AndroidManifest.xml b/modules/expandable-layout/src/main/AndroidManifest.xml new file mode 100644 index 00000000..cc947c56 --- /dev/null +++ b/modules/expandable-layout/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/modules/expandable-layout/src/main/java/com/github/aakira/expandablelayout/ExpandableLayout.java b/modules/expandable-layout/src/main/java/com/github/aakira/expandablelayout/ExpandableLayout.java new file mode 100644 index 00000000..bd0e3cbd --- /dev/null +++ b/modules/expandable-layout/src/main/java/com/github/aakira/expandablelayout/ExpandableLayout.java @@ -0,0 +1,115 @@ +package com.github.aakira.expandablelayout; + +import android.animation.TimeInterpolator; +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +public interface ExpandableLayout { + + /** + * Duration of expand animation + */ + int DEFAULT_DURATION = 300; + /** + * Visibility of the layout when the layout attaches + */ + boolean DEFAULT_EXPANDED = false; + /** + * Orientation of child views + */ + int HORIZONTAL = 0; + /** + * Orientation of child views + */ + int VERTICAL = 1; + + /** + * Orientation of layout + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({HORIZONTAL, VERTICAL}) + @interface Orientation { + } + + /** + * Starts animation the state of the view to the inverse of its current state. + */ + void toggle(); + + /** + * Starts animation the state of the view to the inverse of its current state. + * + * @param duration + * @param interpolator use the default interpolator if the argument is null. + */ + void toggle(final long duration, @Nullable final TimeInterpolator interpolator); + + /** + * Starts expand animation. + */ + void expand(); + + /** + * Starts expand animation. + * + * @param duration + * @param interpolator use the default interpolator if the argument is null. + */ + void expand(final long duration, @Nullable final TimeInterpolator interpolator); + + /** + * Starts collapse animation. + */ + void collapse(); + + /** + * Starts collapse animation. + * + * @param duration + * @param interpolator use the default interpolator if the argument is null. + */ + void collapse(final long duration, @Nullable final TimeInterpolator interpolator); + + /** + * Sets the expandable layout listener. + * + * @param listener ExpandableLayoutListener + */ + void setListener(@NonNull final ExpandableLayoutListener listener); + + /** + * Sets the length of the animation. + * The default duration is 300 milliseconds. + * + * @param duration + */ + void setDuration(final int duration); + + /** + * Sets state of expanse. + * + * @param expanded The layout is visible if expanded is true + */ + void setExpanded(final boolean expanded); + + /** + * Gets state of expanse. + * + * @return true if the layout is visible + */ + boolean isExpanded(); + + /** + * The time interpolator used in calculating the elapsed fraction of this animation. The + * interpolator determines whether the animation runs with linear or non-linear motion, + * such as acceleration and deceleration. + * The default value is {@link android.view.animation.AccelerateDecelerateInterpolator} + * + * @param interpolator + */ + void setInterpolator(@NonNull final TimeInterpolator interpolator); +} \ No newline at end of file diff --git a/modules/expandable-layout/src/main/java/com/github/aakira/expandablelayout/ExpandableLayoutListener.java b/modules/expandable-layout/src/main/java/com/github/aakira/expandablelayout/ExpandableLayoutListener.java new file mode 100644 index 00000000..a1310a77 --- /dev/null +++ b/modules/expandable-layout/src/main/java/com/github/aakira/expandablelayout/ExpandableLayoutListener.java @@ -0,0 +1,35 @@ +package com.github.aakira.expandablelayout; + +public interface ExpandableLayoutListener { + /** + * Notifies the start of the animation. + * Sync from android.animation.Animator.AnimatorListener.onAnimationStart(Animator animation) + */ + void onAnimationStart(); + + /** + * Notifies the end of the animation. + * Sync from android.animation.Animator.AnimatorListener.onAnimationEnd(Animator animation) + */ + void onAnimationEnd(); + + /** + * Notifies the layout is going to open. + */ + void onPreOpen(); + + /** + * Notifies the layout is going to equal close size. + */ + void onPreClose(); + + /** + * Notifies the layout opened. + */ + void onOpened(); + + /** + * Notifies the layout size equal closed size. + */ + void onClosed(); +} \ No newline at end of file diff --git a/modules/expandable-layout/src/main/java/com/github/aakira/expandablelayout/ExpandableLayoutListenerAdapter.java b/modules/expandable-layout/src/main/java/com/github/aakira/expandablelayout/ExpandableLayoutListenerAdapter.java new file mode 100644 index 00000000..23e97735 --- /dev/null +++ b/modules/expandable-layout/src/main/java/com/github/aakira/expandablelayout/ExpandableLayoutListenerAdapter.java @@ -0,0 +1,45 @@ +package com.github.aakira.expandablelayout; + +public abstract class ExpandableLayoutListenerAdapter implements ExpandableLayoutListener { + /** + * {@inheritDoc} + */ + @Override + public void onAnimationStart() { + } + + /** + * {@inheritDoc} + */ + @Override + public void onAnimationEnd() { + } + + /** + * {@inheritDoc} + */ + @Override + public void onPreOpen() { + } + + /** + * {@inheritDoc} + */ + @Override + public void onPreClose() { + } + + /** + * {@inheritDoc} + */ + @Override + public void onOpened() { + } + + /** + * {@inheritDoc} + */ + @Override + public void onClosed() { + } +} \ No newline at end of file diff --git a/modules/expandable-layout/src/main/java/com/github/aakira/expandablelayout/ExpandableLinearLayout.java b/modules/expandable-layout/src/main/java/com/github/aakira/expandablelayout/ExpandableLinearLayout.java new file mode 100644 index 00000000..92485b2c --- /dev/null +++ b/modules/expandable-layout/src/main/java/com/github/aakira/expandablelayout/ExpandableLinearLayout.java @@ -0,0 +1,565 @@ +package com.github.aakira.expandablelayout; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Build; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewTreeObserver; +import android.view.animation.LinearInterpolator; +import android.widget.LinearLayout; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public class ExpandableLinearLayout extends LinearLayout implements ExpandableLayout { + + private int duration; + private TimeInterpolator interpolator = new LinearInterpolator(); + /** + * Default state of expanse + * + * @see #defaultChildIndex + * @see #defaultPosition + */ + private boolean defaultExpanded; + /** + * You cannot define {@link #defaultExpanded}, {@link #defaultChildIndex} + * and {@link #defaultPosition} at the same time. + * {@link #defaultPosition} has priority over {@link #defaultExpanded} + * and {@link #defaultChildIndex} if you set them at the same time. + *

+ *

+ * Priority + * {@link #defaultPosition} > {@link #defaultChildIndex} > {@link #defaultExpanded} + */ + private int defaultChildIndex; + private int defaultPosition; + /** + * The close position is width from left of layout if orientation is horizontal. + * The close position is height from top of layout if orientation is vertical. + */ + private int closePosition = 0; + + private ExpandableLayoutListener listener; + private ExpandableSavedState savedState; + private boolean isExpanded; + private int layoutSize = 0; + private boolean inRecyclerView = false; + private boolean isArranged = false; + private boolean isCalculatedSize = false; + private boolean isAnimating = false; + /** + * State of expanse in recycler view. + */ + private boolean recyclerExpanded = false; + /** + * view size of children + **/ + private List childSizeList = new ArrayList<>(); + private ViewTreeObserver.OnGlobalLayoutListener mGlobalLayoutListener; + + public ExpandableLinearLayout(final Context context) { + this(context, null); + } + + public ExpandableLinearLayout(final Context context, final AttributeSet attrs) { + this(context, attrs, 0); + } + + public ExpandableLinearLayout(final Context context, final AttributeSet attrs, + final int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs, defStyleAttr); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public ExpandableLinearLayout(final Context context, final AttributeSet attrs, + final int defStyleAttr, final int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context, attrs, defStyleAttr); + } + + private void init(final Context context, final AttributeSet attrs, final int defStyleAttr) { + final TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.expandableLayout, defStyleAttr, 0); + duration = a.getInteger(R.styleable.expandableLayout_ael_duration, DEFAULT_DURATION); + defaultExpanded = a.getBoolean(R.styleable.expandableLayout_ael_expanded, DEFAULT_EXPANDED); + defaultChildIndex = a.getInteger(R.styleable.expandableLayout_ael_defaultChildIndex, + Integer.MAX_VALUE); + defaultPosition = a.getDimensionPixelSize(R.styleable.expandableLayout_ael_defaultPosition, + Integer.MIN_VALUE); + final int interpolatorType = a.getInteger(R.styleable.expandableLayout_ael_interpolator, + Utils.LINEAR_INTERPOLATOR); + a.recycle(); + interpolator = Utils.createInterpolator(interpolatorType); + isExpanded = defaultExpanded; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + if (!isCalculatedSize) { + // calculate a size of children + childSizeList.clear(); + final int childCount = getChildCount(); + + if (childCount > 0) { + int sumSize = 0; + View view; + LayoutParams params; + for (int i = 0; i < childCount; i++) { + view = getChildAt(i); + params = (LayoutParams) view.getLayoutParams(); + + if (0 < i) { + sumSize = childSizeList.get(i - 1); + } + childSizeList.add( + (isVertical() + ? view.getMeasuredHeight() + params.topMargin + params.bottomMargin + : view.getMeasuredWidth() + params.leftMargin + params.rightMargin + ) + sumSize); + } + layoutSize = childSizeList.get(childCount - 1) + + (isVertical() + ? getPaddingTop() + getPaddingBottom() + : getPaddingLeft() + getPaddingRight() + ); + isCalculatedSize = true; + } else { + throw new IllegalStateException("The expandableLinearLayout must have at least one child"); + } + } + + if (isArranged) return; + + // adjust default position if a user set a value. + if (!defaultExpanded) { + setLayoutSize(closePosition); + } + if (inRecyclerView) { + setLayoutSize(recyclerExpanded ? layoutSize : closePosition); + } + final int childNumbers = childSizeList.size(); + if (childNumbers > defaultChildIndex && childNumbers > 0) { + moveChild(defaultChildIndex, 0, null); + } + if (defaultPosition > 0 && layoutSize >= defaultPosition && layoutSize > 0) { + move(defaultPosition, 0, null); + } + isArranged = true; + + if (savedState == null) return; + setLayoutSize(savedState.getSize()); + } + + @Override + protected Parcelable onSaveInstanceState() { + final Parcelable parcelable = super.onSaveInstanceState(); + final ExpandableSavedState ss = new ExpandableSavedState(parcelable); + ss.setSize(getCurrentPosition()); + return ss; + } + + @Override + protected void onRestoreInstanceState(final Parcelable state) { + if (!(state instanceof ExpandableSavedState)) { + super.onRestoreInstanceState(state); + return; + } + + final ExpandableSavedState ss = (ExpandableSavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + savedState = ss; + } + + /** + * {@inheritDoc} + */ + @Override + public void setListener(@NonNull ExpandableLayoutListener listener) { + this.listener = listener; + } + + /** + * {@inheritDoc} + */ + @Override + public void toggle() { + toggle(duration, interpolator); + } + + /** + * {@inheritDoc} + */ + @Override + public void toggle(final long duration, final @Nullable TimeInterpolator interpolator) { + if (closePosition < getCurrentPosition()) { + collapse(duration, interpolator); + } else { + expand(duration, interpolator); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void expand() { + if (isAnimating) return; + + createExpandAnimator(getCurrentPosition(), layoutSize, duration, interpolator).start(); + } + + /** + * {@inheritDoc} + */ + @Override + public void expand(final long duration, final @Nullable TimeInterpolator interpolator) { + if (isAnimating) return; + + if (duration <= 0) { + move(layoutSize, duration, interpolator); + return; + } + createExpandAnimator(getCurrentPosition(), layoutSize, duration, interpolator).start(); + } + + /** + * {@inheritDoc} + */ + @Override + public void collapse() { + if (isAnimating) return; + + createExpandAnimator(getCurrentPosition(), closePosition, duration, interpolator).start(); + } + + /** + * {@inheritDoc} + */ + @Override + public void collapse(final long duration, final @Nullable TimeInterpolator interpolator) { + if (isAnimating) return; + + if (duration <= 0) { + move(closePosition, duration, interpolator); + return; + } + createExpandAnimator(getCurrentPosition(), closePosition, duration, interpolator).start(); + } + + /** + * {@inheritDoc} + */ + @Override + public void setDuration(final int duration) { + if (duration < 0) { + throw new IllegalArgumentException("Animators cannot have negative duration: " + + duration); + } + this.duration = duration; + } + + /** + * {@inheritDoc} + */ + @Override + public void setExpanded(final boolean expanded) { + if (inRecyclerView) recyclerExpanded = expanded; + + final int currentPosition = getCurrentPosition(); + if ((expanded && (currentPosition == layoutSize)) + || (!expanded && currentPosition == closePosition)) return; + + isExpanded = expanded; + setLayoutSize(expanded ? layoutSize : closePosition); + requestLayout(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isExpanded() { + return isExpanded; + } + + /** + * {@inheritDoc} + */ + @Override + public void setInterpolator(@NonNull final TimeInterpolator interpolator) { + this.interpolator = interpolator; + } + + /** + * Initializes this layout. + */ + public void initLayout() { + closePosition = 0; + layoutSize = 0; + isArranged = false; + isCalculatedSize = false; + savedState = null; + + if (isVertical()) { + measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.UNSPECIFIED)); + } else { + measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.UNSPECIFIED), + MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY)); + } + } + + /** + * @param position + * + * @see #move(int, long, TimeInterpolator) + */ + public void move(int position) { + move(position, duration, interpolator); + } + + /** + * Moves to position. + * Sets 0 to duration if you want to move immediately. + * + * @param position + * @param duration + * @param interpolator use the default interpolator if the argument is null. + */ + public void move(int position, long duration, @Nullable TimeInterpolator interpolator) { + if (isAnimating || 0 > position || layoutSize < position) return; + + if (duration <= 0) { + isExpanded = position > closePosition; + setLayoutSize(position); + requestLayout(); + notifyListeners(); + return; + } + createExpandAnimator(getCurrentPosition(), position, duration, + interpolator == null ? this.interpolator : interpolator).start(); + } + + /** + * @param index child view index + * + * @see #moveChild(int, long, TimeInterpolator) + */ + public void moveChild(int index) { + moveChild(index, duration, interpolator); + } + + /** + * Moves to bottom(VERTICAL) or right(HORIZONTAL) of child view + * Sets 0 to duration if you want to move immediately. + * + * @param index index child view index + * @param duration + * @param interpolator use the default interpolator if the argument is null. + */ + public void moveChild(int index, long duration, @Nullable TimeInterpolator interpolator) { + if (isAnimating) return; + + final int destination = getChildPosition(index) + + (isVertical() ? getPaddingBottom() : getPaddingRight()); + if (duration <= 0) { + isExpanded = destination > closePosition; + setLayoutSize(destination); + requestLayout(); + notifyListeners(); + return; + } + createExpandAnimator(getCurrentPosition(), destination, + duration, interpolator == null ? this.interpolator : interpolator).start(); + } + + /** + * Gets the width from left of layout if orientation is horizontal. + * Gets the height from top of layout if orientation is vertical. + * + * @param index index of child view + * + * @return position from top or left + */ + public int getChildPosition(final int index) { + if (0 > index || childSizeList.size() <= index) { + throw new IllegalArgumentException("There aren't the view having this index."); + } + return childSizeList.get(index); + } + + /** + * Gets the width from left of layout if orientation is horizontal. + * Gets the height from top of layout if orientation is vertical. + * + * @return + * + * @see #closePosition + */ + public int getClosePosition() { + return closePosition; + } + + /** + * Sets the close position directly. + * + * @param position + * + * @see #closePosition + * @see #setClosePositionIndex(int) + */ + public void setClosePosition(final int position) { + this.closePosition = position; + } + + /** + * Gets the current position. + * + * @return + */ + public int getCurrentPosition() { + return isVertical() ? getMeasuredHeight() : getMeasuredWidth(); + } + + /** + * Sets close position using index of child view. + * + * @param childIndex + * + * @see #closePosition + * @see #setClosePosition(int) + */ + public void setClosePositionIndex(final int childIndex) { + this.closePosition = getChildPosition(childIndex); + } + + /** + * Set true if expandable layout is used in recycler view. + * + * @param inRecyclerView + */ + public void setInRecyclerView(final boolean inRecyclerView) { + this.inRecyclerView = inRecyclerView; + } + + private boolean isVertical() { + return getOrientation() == LinearLayout.VERTICAL; + } + + private void setLayoutSize(int size) { + if (isVertical()) { + getLayoutParams().height = size; + } else { + getLayoutParams().width = size; + } + } + + /** + * Creates value animator. + * Expand the layout if {@param to} is bigger than {@param from}. + * Collapse the layout if {@param from} is bigger than {@param to}. + * + * @param from + * @param to + * @param duration + * @param interpolator + * + * @return + */ + private ValueAnimator createExpandAnimator( + final int from, final int to, final long duration, final TimeInterpolator interpolator) { + final ValueAnimator valueAnimator = ValueAnimator.ofInt(from, to); + valueAnimator.setDuration(duration); + valueAnimator.setInterpolator(interpolator); + valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(final ValueAnimator animator) { + if (isVertical()) { + getLayoutParams().height = (int) animator.getAnimatedValue(); + } else { + getLayoutParams().width = (int) animator.getAnimatedValue(); + } + requestLayout(); + } + }); + valueAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + isAnimating = true; + if (listener == null) return; + + listener.onAnimationStart(); + if (layoutSize == to) { + listener.onPreOpen(); + return; + } + if (closePosition == to) { + listener.onPreClose(); + } + } + + @Override + public void onAnimationEnd(Animator animator) { + isAnimating = false; + isExpanded = to > closePosition; + + if (listener == null) return; + + listener.onAnimationEnd(); + if (to == layoutSize) { + listener.onOpened(); + return; + } + if (to == closePosition) { + listener.onClosed(); + } + } + }); + return valueAnimator; + } + + /** + * Notify listeners + */ + private void notifyListeners() { + if (listener == null) return; + + listener.onAnimationStart(); + if (isExpanded) { + listener.onPreOpen(); + } else { + listener.onPreClose(); + } + mGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { + getViewTreeObserver().removeGlobalOnLayoutListener(mGlobalLayoutListener); + } else { + getViewTreeObserver().removeOnGlobalLayoutListener(mGlobalLayoutListener); + } + + listener.onAnimationEnd(); + if (isExpanded) { + listener.onOpened(); + } else { + listener.onClosed(); + } + } + }; + getViewTreeObserver().addOnGlobalLayoutListener(mGlobalLayoutListener); + } +} \ No newline at end of file diff --git a/modules/expandable-layout/src/main/java/com/github/aakira/expandablelayout/ExpandableRelativeLayout.java b/modules/expandable-layout/src/main/java/com/github/aakira/expandablelayout/ExpandableRelativeLayout.java new file mode 100644 index 00000000..43772101 --- /dev/null +++ b/modules/expandable-layout/src/main/java/com/github/aakira/expandablelayout/ExpandableRelativeLayout.java @@ -0,0 +1,553 @@ +package com.github.aakira.expandablelayout; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Build; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewTreeObserver; +import android.view.animation.LinearInterpolator; +import android.widget.RelativeLayout; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public class ExpandableRelativeLayout extends RelativeLayout implements ExpandableLayout { + + private int duration; + private TimeInterpolator interpolator = new LinearInterpolator(); + private int orientation; + /** + * Default state of expanse + * + * @see #defaultChildIndex + * @see #defaultPosition + */ + private boolean defaultExpanded; + /** + * You cannot define {@link #defaultExpanded}, {@link #defaultChildIndex} + * and {@link #defaultPosition} at the same time. + * {@link #defaultPosition} has priority over {@link #defaultExpanded} + * and {@link #defaultChildIndex} if you set them at the same time. + *

+ *

+ * Priority + * {@link #defaultPosition} > {@link #defaultChildIndex} > {@link #defaultExpanded} + */ + private int defaultChildIndex; + private int defaultPosition; + /** + * The close position is width from left of layout if orientation is horizontal. + * The close position is height from top of layout if orientation is vertical. + */ + private int closePosition = 0; + + private ExpandableLayoutListener listener; + private ExpandableSavedState savedState; + private boolean isExpanded; + private int layoutSize = 0; + private boolean isArranged = false; + private boolean isCalculatedSize = false; + private boolean isAnimating = false; + /** + * view size of children + **/ + private List childSizeList = new ArrayList<>(); + /** + * view position top or left of children + **/ + private List childPositionList = new ArrayList<>(); + private ViewTreeObserver.OnGlobalLayoutListener mGlobalLayoutListener; + + public ExpandableRelativeLayout(final Context context) { + this(context, null); + } + + public ExpandableRelativeLayout(final Context context, final AttributeSet attrs) { + this(context, attrs, 0); + } + + public ExpandableRelativeLayout(final Context context, final AttributeSet attrs, + final int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs, defStyleAttr); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public ExpandableRelativeLayout(final Context context, final AttributeSet attrs, + final int defStyleAttr, final int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context, attrs, defStyleAttr); + } + + private void init(final Context context, final AttributeSet attrs, final int defStyleAttr) { + final TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.expandableLayout, defStyleAttr, 0); + duration = a.getInteger(R.styleable.expandableLayout_ael_duration, DEFAULT_DURATION); + defaultExpanded = a.getBoolean(R.styleable.expandableLayout_ael_expanded, DEFAULT_EXPANDED); + orientation = a.getInteger(R.styleable.expandableLayout_ael_orientation, VERTICAL); + defaultChildIndex = a.getInteger(R.styleable.expandableLayout_ael_defaultChildIndex, + Integer.MAX_VALUE); + defaultPosition = a.getDimensionPixelSize(R.styleable.expandableLayout_ael_defaultPosition, + Integer.MIN_VALUE); + final int interpolatorType = a.getInteger(R.styleable.expandableLayout_ael_interpolator, + Utils.LINEAR_INTERPOLATOR); + a.recycle(); + interpolator = Utils.createInterpolator(interpolatorType); + isExpanded = defaultExpanded; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + if (isCalculatedSize) return; + + final int measureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + if (isVertical()) { + int measuredHeight = getMeasuredHeight(); + + super.onMeasure(widthMeasureSpec, measureSpec); + layoutSize = getMeasuredHeight(); + + setMeasuredDimension(getMeasuredWidth(), measuredHeight); + } else { + int measuredWidth = getMeasuredWidth(); + + super.onMeasure(measureSpec, heightMeasureSpec); + layoutSize = getMeasuredWidth(); + + setMeasuredDimension(measuredWidth, getMeasuredHeight()); + } + + // calculate a size of children + childSizeList.clear(); + View view; + LayoutParams params; + for (int i = 0; i < getChildCount(); i++) { + view = getChildAt(i); + params = (LayoutParams) view.getLayoutParams(); + + childSizeList.add(isVertical() + ? view.getMeasuredHeight() + params.topMargin + params.bottomMargin + : view.getMeasuredWidth() + params.leftMargin + params.rightMargin); + } + isCalculatedSize = true; + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + + if (isArranged) return; + + childPositionList.clear(); + // calculate a top position of children + for (int i = 0; i < getChildCount(); i++) { + childPositionList.add((int) (isVertical() ? getChildAt(i).getY() : getChildAt(i).getX())); + } + + // adjust default position if a user set a value. + if (!defaultExpanded) { + setLayoutSize(closePosition); + } + final int childNumbers = childSizeList.size(); + if (childNumbers > defaultChildIndex && childNumbers > 0) { + moveChild(defaultChildIndex, 0, null); + } + if (defaultPosition > 0 && layoutSize >= defaultPosition && layoutSize > 0) { + move(defaultPosition, 0, null); + } + isArranged = true; + + if (savedState == null) return; + setLayoutSize(savedState.getSize()); + } + + @Override + protected Parcelable onSaveInstanceState() { + final Parcelable parcelable = super.onSaveInstanceState(); + final ExpandableSavedState ss = new ExpandableSavedState(parcelable); + ss.setSize(getCurrentPosition()); + return ss; + } + + @Override + protected void onRestoreInstanceState(final Parcelable state) { + if (!(state instanceof ExpandableSavedState)) { + super.onRestoreInstanceState(state); + return; + } + + final ExpandableSavedState ss = (ExpandableSavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + savedState = ss; + } + + /** + * {@inheritDoc} + */ + @Override + public void setListener(@NonNull ExpandableLayoutListener listener) { + this.listener = listener; + } + + /** + * {@inheritDoc} + */ + @Override + public void toggle() { + toggle(duration, interpolator); + } + + /** + * {@inheritDoc} + */ + @Override + public void toggle(final long duration, final @Nullable TimeInterpolator interpolator) { + if (closePosition < getCurrentPosition()) { + collapse(duration, interpolator); + } else { + expand(duration, interpolator); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void expand() { + if (isAnimating) return; + + createExpandAnimator(getCurrentPosition(), layoutSize, duration, interpolator).start(); + } + + /** + * {@inheritDoc} + */ + @Override + public void expand(final long duration, final @Nullable TimeInterpolator interpolator) { + if (isAnimating) return; + + if (duration <= 0) { + move(layoutSize, duration, interpolator); + return; + } + createExpandAnimator(getCurrentPosition(), layoutSize, duration, interpolator).start(); + } + + /** + * {@inheritDoc} + */ + @Override + public void collapse() { + if (isAnimating) return; + + createExpandAnimator(getCurrentPosition(), closePosition, duration, interpolator).start(); + } + + /** + * {@inheritDoc} + */ + @Override + public void collapse(final long duration, final @Nullable TimeInterpolator interpolator) { + if (isAnimating) return; + + if (duration <= 0) { + move(closePosition, duration, interpolator); + return; + } + createExpandAnimator(getCurrentPosition(), closePosition, duration, interpolator).start(); + } + + /** + * {@inheritDoc} + */ + @Override + public void setDuration(final int duration) { + if (duration < 0) { + throw new IllegalArgumentException("Animators cannot have negative duration: " + + duration); + } + this.duration = duration; + } + + /** + * {@inheritDoc} + */ + @Override + public void setExpanded(boolean expanded) { + final int currentPosition = getCurrentPosition(); + if ((expanded && (currentPosition == layoutSize)) + || (!expanded && currentPosition == closePosition)) return; + + isExpanded = expanded; + setLayoutSize(expanded ? layoutSize : closePosition); + requestLayout(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isExpanded() { + return isExpanded; + } + + /** + * {@inheritDoc} + */ + @Override + public void setInterpolator(@NonNull final TimeInterpolator interpolator) { + this.interpolator = interpolator; + } + + /** + * @param position + * + * @see #move(int, long, TimeInterpolator) + */ + public void move(int position) { + move(position, duration, interpolator); + } + + /** + * Moves to position. + * Sets 0 to duration if you want to move immediately. + * + * @param position + * @param duration + * @param interpolator use the default interpolator if the argument is null. + */ + public void move(int position, long duration, @Nullable TimeInterpolator interpolator) { + if (isAnimating || 0 > position || layoutSize < position) return; + + if (duration <= 0) { + isExpanded = position > closePosition; + setLayoutSize(position); + requestLayout(); + notifyListeners(); + return; + } + createExpandAnimator(getCurrentPosition(), position, duration, + interpolator == null ? this.interpolator : interpolator).start(); + } + + /** + * @param index child view index + * + * @see #moveChild(int, long, TimeInterpolator) + */ + public void moveChild(int index) { + moveChild(index, duration, interpolator); + } + + /** + * Moves to bottom(VERTICAL) or right(HORIZONTAL) of child view + * Sets 0 to duration if you want to move immediately. + * + * @param index index child view index + * @param duration + * @param interpolator use the default interpolator if the argument is null. + */ + public void moveChild(int index, long duration, @Nullable TimeInterpolator interpolator) { + if (isAnimating) return; + + final int destination = getChildPosition(index) + + (isVertical() ? getPaddingBottom() : getPaddingRight()); + if (duration <= 0) { + isExpanded = destination > closePosition; + setLayoutSize(destination); + requestLayout(); + notifyListeners(); + return; + } + createExpandAnimator(getCurrentPosition(), destination, + duration, interpolator == null ? this.interpolator : interpolator).start(); + } + + /** + * Sets orientation of expanse animation. + * + * @param orientation Set 0 if orientation is horizontal, 1 if orientation is vertical + */ + public void setOrientation(@Orientation final int orientation) { + this.orientation = orientation; + } + + /** + * Gets the width from left of layout if orientation is horizontal. + * Gets the height from top of layout if orientation is vertical. + * + * @param index index of child view + * + * @return position from top or left + */ + public int getChildPosition(final int index) { + if (0 > index || childSizeList.size() <= index) { + throw new IllegalArgumentException("There aren't the view having this index."); + } + return childPositionList.get(index) + childSizeList.get(index); + } + + /** + * Gets the width from left of layout if orientation is horizontal. + * Gets the height from top of layout if orientation is vertical. + * + * @return + * + * @see #closePosition + */ + public int getClosePosition() { + return closePosition; + } + + /** + * Sets the close position directly. + * + * @param position + * + * @see #closePosition + * @see #setClosePositionIndex(int) + */ + public void setClosePosition(final int position) { + this.closePosition = position; + } + + /** + * Gets the current position. + * + * @return + */ + public int getCurrentPosition() { + return isVertical() ? getMeasuredHeight() : getMeasuredWidth(); + } + + /** + * Sets close position using index of child view. + * + * @param childIndex + * + * @see #closePosition + * @see #setClosePosition(int) + */ + public void setClosePositionIndex(final int childIndex) { + this.closePosition = getChildPosition(childIndex); + } + + private boolean isVertical() { + return orientation == VERTICAL; + } + + private void setLayoutSize(int size) { + if (isVertical()) { + getLayoutParams().height = size; + } else { + getLayoutParams().width = size; + } + } + + /** + * Creates value animator. + * Expand the layout if {@param to} is bigger than {@param from}. + * Collapse the layout if {@param from} is bigger than {@param to}. + * + * @param from + * @param to + * @param duration + * @param interpolator + * + * @return + */ + private ValueAnimator createExpandAnimator( + final int from, final int to, final long duration, final TimeInterpolator interpolator) { + final ValueAnimator valueAnimator = ValueAnimator.ofInt(from, to); + valueAnimator.setDuration(duration); + valueAnimator.setInterpolator(interpolator); + valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(final ValueAnimator animator) { + if (isVertical()) { + getLayoutParams().height = (int) animator.getAnimatedValue(); + } else { + getLayoutParams().width = (int) animator.getAnimatedValue(); + } + requestLayout(); + } + }); + valueAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + isAnimating = true; + if (listener == null) return; + + listener.onAnimationStart(); + if (layoutSize == to) { + listener.onPreOpen(); + return; + } + if (closePosition == to) { + listener.onPreClose(); + } + } + + @Override + public void onAnimationEnd(Animator animator) { + isAnimating = false; + isExpanded = to > closePosition; + + if (listener == null) return; + + listener.onAnimationEnd(); + if (to == layoutSize) { + listener.onOpened(); + return; + } + if (to == closePosition) { + listener.onClosed(); + } + } + }); + return valueAnimator; + } + + /** + * Notify listeners + */ + private void notifyListeners() { + if (listener == null) return; + + listener.onAnimationStart(); + if (isExpanded) { + listener.onPreOpen(); + } else { + listener.onPreClose(); + } + mGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { + getViewTreeObserver().removeGlobalOnLayoutListener(mGlobalLayoutListener); + } else { + getViewTreeObserver().removeOnGlobalLayoutListener(mGlobalLayoutListener); + } + + listener.onAnimationEnd(); + if (isExpanded) { + listener.onOpened(); + } else { + listener.onClosed(); + } + } + }; + getViewTreeObserver().addOnGlobalLayoutListener(mGlobalLayoutListener); + } +} \ No newline at end of file diff --git a/modules/expandable-layout/src/main/java/com/github/aakira/expandablelayout/ExpandableSavedState.java b/modules/expandable-layout/src/main/java/com/github/aakira/expandablelayout/ExpandableSavedState.java new file mode 100644 index 00000000..ee66f6df --- /dev/null +++ b/modules/expandable-layout/src/main/java/com/github/aakira/expandablelayout/ExpandableSavedState.java @@ -0,0 +1,54 @@ +package com.github.aakira.expandablelayout; + +import android.os.Parcel; +import android.os.Parcelable; +import android.view.View; + +public class ExpandableSavedState extends View.BaseSavedState { + private int size; + private float weight; + + ExpandableSavedState(Parcelable superState) { + super(superState); + } + + private ExpandableSavedState(Parcel in) { + super(in); + this.size = in.readInt(); + this.weight = in.readFloat(); + } + + public int getSize() { + return this.size; + } + + public void setSize(int size) { + this.size = size; + } + + public float getWeight() { + return this.weight; + } + + public void setWeight(float weight) { + this.weight = weight; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeInt(this.size); + out.writeFloat(this.weight); + } + + public static final Creator CREATOR = + new Creator() { + public ExpandableSavedState createFromParcel(Parcel in) { + return new ExpandableSavedState(in); + } + + public ExpandableSavedState[] newArray(int size) { + return new ExpandableSavedState[size]; + } + }; +} diff --git a/modules/expandable-layout/src/main/java/com/github/aakira/expandablelayout/ExpandableWeightLayout.java b/modules/expandable-layout/src/main/java/com/github/aakira/expandablelayout/ExpandableWeightLayout.java new file mode 100644 index 00000000..71931afd --- /dev/null +++ b/modules/expandable-layout/src/main/java/com/github/aakira/expandablelayout/ExpandableWeightLayout.java @@ -0,0 +1,385 @@ +package com.github.aakira.expandablelayout; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Build; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.view.ViewTreeObserver; +import android.view.animation.LinearInterpolator; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class ExpandableWeightLayout extends RelativeLayout implements ExpandableLayout { + + private int duration; + private TimeInterpolator interpolator = new LinearInterpolator(); + private boolean defaultExpanded; + + private ExpandableLayoutListener listener; + private ExpandableSavedState savedState; + private boolean isExpanded; + private float layoutWeight = 0.0f; + private boolean isArranged = false; + private boolean isCalculatedSize = false; + private boolean isAnimating = false; + private ViewTreeObserver.OnGlobalLayoutListener mGlobalLayoutListener; + + public ExpandableWeightLayout(final Context context) { + this(context, null); + } + + public ExpandableWeightLayout(final Context context, final AttributeSet attrs) { + this(context, attrs, 0); + } + + public ExpandableWeightLayout(final Context context, final AttributeSet attrs, + final int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs, defStyleAttr); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public ExpandableWeightLayout(final Context context, final AttributeSet attrs, + final int defStyleAttr, final int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context, attrs, defStyleAttr); + } + + private void init(final Context context, final AttributeSet attrs, final int defStyleAttr) { + final TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.expandableLayout, defStyleAttr, 0); + duration = a.getInteger(R.styleable.expandableLayout_ael_duration, DEFAULT_DURATION); + defaultExpanded = a.getBoolean(R.styleable.expandableLayout_ael_expanded, DEFAULT_EXPANDED); + final int interpolatorType = a.getInteger(R.styleable.expandableLayout_ael_interpolator, + Utils.LINEAR_INTERPOLATOR); + a.recycle(); + interpolator = Utils.createInterpolator(interpolatorType); + isExpanded = defaultExpanded; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + // Check this layout using the attribute of weight + if (!(getLayoutParams() instanceof LinearLayout.LayoutParams)) { + throw new AssertionError("You must arrange in LinearLayout."); + } + if (0 >= getCurrentWeight()) throw new AssertionError("You must set a weight than 0."); + + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + if (!isCalculatedSize) { + layoutWeight = getCurrentWeight(); + isCalculatedSize = true; + } + + if (isArranged) return; + setWeight(defaultExpanded ? layoutWeight : 0); + isArranged = true; + + if (savedState == null) return; + setWeight(savedState.getWeight()); + } + + @Override + protected Parcelable onSaveInstanceState() { + final Parcelable parcelable = super.onSaveInstanceState(); + + final ExpandableSavedState ss = new ExpandableSavedState(parcelable); + ss.setWeight(getCurrentWeight()); + return ss; + } + + @Override + protected void onRestoreInstanceState(final Parcelable state) { + if (!(state instanceof ExpandableSavedState)) { + super.onRestoreInstanceState(state); + return; + } + final ExpandableSavedState ss = (ExpandableSavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + savedState = ss; + } + + /** + * {@inheritDoc} + */ + @Override + public void setListener(@NonNull ExpandableLayoutListener listener) { + this.listener = listener; + } + + /** + * {@inheritDoc} + */ + @Override + public void toggle() { + toggle(duration, interpolator); + } + + /** + * {@inheritDoc} + */ + @Override + public void toggle(final long duration, @Nullable final TimeInterpolator interpolator) { + if (0 < getCurrentWeight()) { + collapse(duration, interpolator); + } else { + expand(duration, interpolator); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void expand() { + if (isAnimating) return; + + createExpandAnimator(0, layoutWeight, duration, interpolator).start(); + } + + /** + * {@inheritDoc} + */ + @Override + public void expand(final long duration, @Nullable final TimeInterpolator interpolator) { + if (isAnimating) return; + + if (duration <= 0) { + isExpanded = true; + setWeight(layoutWeight); + requestLayout(); + notifyListeners(); + return; + } + createExpandAnimator(getCurrentWeight(), layoutWeight, duration, interpolator).start(); + } + + /** + * {@inheritDoc} + */ + @Override + public void collapse() { + if (isAnimating) return; + + createExpandAnimator(getCurrentWeight(), 0, duration, interpolator).start(); + } + + /** + * {@inheritDoc} + */ + @Override + public void collapse(final long duration, @Nullable final TimeInterpolator interpolator) { + if (isAnimating) return; + + if (duration <= 0) { + isExpanded = false; + setWeight(0); + requestLayout(); + notifyListeners(); + return; + } + createExpandAnimator(getCurrentWeight(), 0, duration, interpolator).start(); + } + + /** + * {@inheritDoc} + */ + @Override + public void setDuration(@NonNull final int duration) { + if (duration < 0) { + throw new IllegalArgumentException("Animators cannot have negative duration: " + + duration); + } + this.duration = duration; + } + + /** + * {@inheritDoc} + */ + @Override + public void setExpanded(boolean expanded) { + final float currentWeight = getCurrentWeight(); + if ((expanded && (currentWeight == layoutWeight)) + || (!expanded && currentWeight == 0)) return; + + isExpanded = expanded; + setWeight(expanded ? layoutWeight : 0); + requestLayout(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isExpanded() { + return isExpanded; + } + + /** + * {@inheritDoc} + */ + @Override + public void setInterpolator(@NonNull final TimeInterpolator interpolator) { + this.interpolator = interpolator; + } + + /** + * Sets weight of expandable layout. + * + * @param expandWeight expand to this weight by {@link #expand()} + */ + public void setExpandWeight(final float expandWeight) { + layoutWeight = expandWeight; + } + + /** + * Gets current weight of expandable layout. + * + * @return weight + */ + public float getCurrentWeight() { + return ((LinearLayout.LayoutParams) getLayoutParams()).weight; + } + + /** + * @param weight + * + * @see #move(float, long, TimeInterpolator) + */ + public void move(float weight) { + move(weight, duration, interpolator); + } + + /** + * Change to weight. + * Sets 0 to duration if you want to move immediately. + * + * @param weight + * @param duration + * @param interpolator use the default interpolator if the argument is null. + */ + public void move(float weight, long duration, @Nullable TimeInterpolator interpolator) { + if (isAnimating) return; + + if (duration <= 0L) { + isExpanded = weight > 0; + setWeight(weight); + requestLayout(); + notifyListeners(); + return; + } + createExpandAnimator(getCurrentWeight(), weight, duration, interpolator).start(); + } + + /** + * Creates value animator. + * Expand the layout if @param.to is bigger than @param.from. + * Collapse the layout if @param.from is bigger than @param.to. + * + * @param from + * @param to + * @param duration + * @param interpolator TimeInterpolator + * + * @return + */ + private ValueAnimator createExpandAnimator(final float from, final float to, final long duration, + @Nullable final TimeInterpolator interpolator) { + final ValueAnimator valueAnimator = ValueAnimator.ofFloat(from, to); + valueAnimator.setDuration(duration); + valueAnimator.setInterpolator(interpolator == null ? this.interpolator : interpolator); + valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(final ValueAnimator animation) { + setWeight((float) animation.getAnimatedValue()); + requestLayout(); + } + }); + valueAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + isAnimating = true; + + if (listener == null) return; + + listener.onAnimationStart(); + if (layoutWeight == to) { + listener.onPreOpen(); + return; + } + if (0 == to) { + listener.onPreClose(); + } + } + + @Override + public void onAnimationEnd(Animator animation) { + isAnimating = false; + isExpanded = to > 0; + + if (listener == null) return; + + listener.onAnimationEnd(); + if (to == layoutWeight) { + listener.onOpened(); + return; + } + if (to == 0) { + listener.onClosed(); + } + } + }); + return valueAnimator; + } + + private void setWeight(final float weight) { + ((LinearLayout.LayoutParams) getLayoutParams()).weight = weight; + } + + /** + * Notify listeners + */ + private void notifyListeners() { + if (listener == null) return; + + listener.onAnimationStart(); + if (isExpanded) { + listener.onPreOpen(); + } else { + listener.onPreClose(); + } + mGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { + getViewTreeObserver().removeGlobalOnLayoutListener(mGlobalLayoutListener); + } else { + getViewTreeObserver().removeOnGlobalLayoutListener(mGlobalLayoutListener); + } + + listener.onAnimationEnd(); + if (isExpanded) { + listener.onOpened(); + } else { + listener.onClosed(); + } + } + }; + getViewTreeObserver().addOnGlobalLayoutListener(mGlobalLayoutListener); + } +} \ No newline at end of file diff --git a/modules/expandable-layout/src/main/java/com/github/aakira/expandablelayout/Utils.java b/modules/expandable-layout/src/main/java/com/github/aakira/expandablelayout/Utils.java new file mode 100644 index 00000000..472c5d61 --- /dev/null +++ b/modules/expandable-layout/src/main/java/com/github/aakira/expandablelayout/Utils.java @@ -0,0 +1,65 @@ +package com.github.aakira.expandablelayout; + +import android.animation.TimeInterpolator; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.AnticipateInterpolator; +import android.view.animation.AnticipateOvershootInterpolator; +import android.view.animation.BounceInterpolator; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.LinearInterpolator; +import android.view.animation.OvershootInterpolator; +import androidx.annotation.IntRange; +import androidx.interpolator.view.animation.FastOutLinearInInterpolator; +import androidx.interpolator.view.animation.FastOutSlowInInterpolator; +import androidx.interpolator.view.animation.LinearOutSlowInInterpolator; + +public class Utils { + + public static final int ACCELERATE_DECELERATE_INTERPOLATOR = 0; + public static final int ACCELERATE_INTERPOLATOR = 1; + public static final int ANTICIPATE_INTERPOLATOR = 2; + public static final int ANTICIPATE_OVERSHOOT_INTERPOLATOR = 3; + public static final int BOUNCE_INTERPOLATOR = 4; + public static final int DECELERATE_INTERPOLATOR = 5; + public static final int FAST_OUT_LINEAR_IN_INTERPOLATOR = 6; + public static final int FAST_OUT_SLOW_IN_INTERPOLATOR = 7; + public static final int LINEAR_INTERPOLATOR = 8; + public static final int LINEAR_OUT_SLOW_IN_INTERPOLATOR = 9; + public static final int OVERSHOOT_INTERPOLATOR = 10; + + /** + * Creates interpolator. + * + * @param interpolatorType + * @return + */ + public static TimeInterpolator createInterpolator(@IntRange(from = 0, to = 10) final int interpolatorType) { + switch (interpolatorType) { + case ACCELERATE_DECELERATE_INTERPOLATOR: + return new AccelerateDecelerateInterpolator(); + case ACCELERATE_INTERPOLATOR: + return new AccelerateInterpolator(); + case ANTICIPATE_INTERPOLATOR: + return new AnticipateInterpolator(); + case ANTICIPATE_OVERSHOOT_INTERPOLATOR: + return new AnticipateOvershootInterpolator(); + case BOUNCE_INTERPOLATOR: + return new BounceInterpolator(); + case DECELERATE_INTERPOLATOR: + return new DecelerateInterpolator(); + case FAST_OUT_LINEAR_IN_INTERPOLATOR: + return new FastOutLinearInInterpolator(); + case FAST_OUT_SLOW_IN_INTERPOLATOR: + return new FastOutSlowInInterpolator(); + case LINEAR_INTERPOLATOR: + return new LinearInterpolator(); + case LINEAR_OUT_SLOW_IN_INTERPOLATOR: + return new LinearOutSlowInInterpolator(); + case OVERSHOOT_INTERPOLATOR: + return new OvershootInterpolator(); + default: + return new LinearInterpolator(); + } + } +} \ No newline at end of file diff --git a/modules/expandable-layout/src/main/res/values/attrs.xml b/modules/expandable-layout/src/main/res/values/attrs.xml new file mode 100644 index 00000000..89062030 --- /dev/null +++ b/modules/expandable-layout/src/main/res/values/attrs.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/expandable-recyclerview/build.gradle b/modules/expandable-recyclerview/build.gradle new file mode 100644 index 00000000..cb52c9f7 --- /dev/null +++ b/modules/expandable-recyclerview/build.gradle @@ -0,0 +1,21 @@ +plugins { + id("org.autojs.build.versions") + id("org.autojs.build.jvm-convention") + id("com.android.library") +} + +android { + namespace "com.bignerdranch.expandablerecyclerview" + compileSdkVersion versions.sdkVersionCompile + + defaultConfig { + minSdkVersion versions.sdkVersionMin + targetSdkVersion versions.sdkVersionTarget + versionName "3.0.0-RC1 Mod" + } +} + +dependencies { + implementation libs.annotation + implementation libs.recyclerview +} diff --git a/modules/expandable-recyclerview/proguard-rules.pro b/modules/expandable-recyclerview/proguard-rules.pro new file mode 100644 index 00000000..7ed59259 --- /dev/null +++ b/modules/expandable-recyclerview/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Applications/eclipse/android/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/modules/expandable-recyclerview/src/main/AndroidManifest.xml b/modules/expandable-recyclerview/src/main/AndroidManifest.xml new file mode 100644 index 00000000..cc947c56 --- /dev/null +++ b/modules/expandable-recyclerview/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/modules/expandable-recyclerview/src/main/java/com/bignerdranch/expandablerecyclerview/ChildViewHolder.java b/modules/expandable-recyclerview/src/main/java/com/bignerdranch/expandablerecyclerview/ChildViewHolder.java new file mode 100644 index 00000000..8ab77900 --- /dev/null +++ b/modules/expandable-recyclerview/src/main/java/com/bignerdranch/expandablerecyclerview/ChildViewHolder.java @@ -0,0 +1,70 @@ +package com.bignerdranch.expandablerecyclerview; + +import androidx.annotation.NonNull; +import androidx.annotation.UiThread; +import androidx.recyclerview.widget.RecyclerView; +import android.view.View; + +/** + * ViewHolder for a child list item. + *

+ * The user should extend this class and implement as they wish for their + * child list item. + */ +public class ChildViewHolder extends RecyclerView.ViewHolder { + C mChild; + ExpandableRecyclerAdapter mExpandableAdapter; + + /** + * Default constructor. + * + * @param itemView The {@link View} being hosted in this ViewHolder + */ + public ChildViewHolder(@NonNull View itemView) { + super(itemView); + } + + /** + * @return the childListItem associated with this view holder + */ + @UiThread + public C getChild() { + return mChild; + } + + /** + * Returns the adapter position of the Parent associated with this ChildViewHolder + * + * @return The adapter position of the Parent if it still exists in the adapter. + * RecyclerView.NO_POSITION if item has been removed from the adapter, + * RecyclerView.Adapter.notifyDataSetChanged() has been called after the last + * layout pass or the ViewHolder has already been recycled. + */ + @UiThread + public int getParentAdapterPosition() { + int flatPosition = getAdapterPosition(); + if (mExpandableAdapter == null || flatPosition == RecyclerView.NO_POSITION) { + return RecyclerView.NO_POSITION; + } + + return mExpandableAdapter.getNearestParentPosition(flatPosition); + } + + /** + * Returns the adapter position of the Child associated with this ChildViewHolder + * + * @return The adapter position of the Child if it still exists in the adapter. + * RecyclerView.NO_POSITION if item has been removed from the adapter, + * RecyclerView.Adapter.notifyDataSetChanged() has been called after the last + * layout pass or the ViewHolder has already been recycled. + */ + @UiThread + public int getChildAdapterPosition() { + int flatPosition = getAdapterPosition(); + if (mExpandableAdapter == null || flatPosition == RecyclerView.NO_POSITION) { + return RecyclerView.NO_POSITION; + } + + return mExpandableAdapter.getChildPosition(flatPosition); + } +} \ No newline at end of file diff --git a/modules/expandable-recyclerview/src/main/java/com/bignerdranch/expandablerecyclerview/ExpandableRecyclerAdapter.java b/modules/expandable-recyclerview/src/main/java/com/bignerdranch/expandablerecyclerview/ExpandableRecyclerAdapter.java new file mode 100644 index 00000000..a1cc9a4b --- /dev/null +++ b/modules/expandable-recyclerview/src/main/java/com/bignerdranch/expandablerecyclerview/ExpandableRecyclerAdapter.java @@ -0,0 +1,1425 @@ +package com.bignerdranch.expandablerecyclerview; + +import android.app.Activity; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.UiThread; +import androidx.recyclerview.widget.RecyclerView; +import android.view.ViewGroup; + +import com.bignerdranch.expandablerecyclerview.ParentViewHolder.ParentViewHolderExpandCollapseListener; +import com.bignerdranch.expandablerecyclerview.model.ExpandableWrapper; +import com.bignerdranch.expandablerecyclerview.model.Parent; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * RecyclerView.Adapter implementation that + * adds the ability to expand and collapse list items. + *

+ * Changes should be notified through: + * {@link #notifyParentInserted(int)} + * {@link #notifyParentRemoved(int)} + * {@link #notifyParentChanged(int)} + * {@link #notifyParentRangeInserted(int, int)} + * {@link #notifyChildInserted(int, int)} + * {@link #notifyChildRemoved(int, int)} + * {@link #notifyChildChanged(int, int)} + * methods and not the notify methods of RecyclerView.Adapter. + * + */ +public abstract class ExpandableRecyclerAdapter

, C, PVH extends ParentViewHolder, CVH extends ChildViewHolder> + extends RecyclerView.Adapter { + + private static final String EXPANDED_STATE_MAP = "ExpandableRecyclerAdapter.ExpandedStateMap"; + /** + * Default ViewType for parent rows + */ + public static final int TYPE_PARENT = 0; + /** + * Default ViewType for children rows + */ + public static final int TYPE_CHILD = 1; + /** + * Start of user-defined view types + */ + public static final int TYPE_FIRST_USER = 2; + private static final int INVALID_FLAT_POSITION = -1; + + /** + * A {@link List} of all currently expanded parents and their children, in order. + * Changes to this list should be made through the add/remove methods + * available in {@link ExpandableRecyclerAdapter}. + */ + @NonNull + protected List> mFlatItemList; + + @NonNull + private List

mParentList; + + @Nullable + private ExpandCollapseListener mExpandCollapseListener; + + @NonNull + private List mAttachedRecyclerViewPool; + + private Map mExpansionStateMap; + + /** + * Allows objects to register themselves as expand/collapse listeners to be + * notified of change events. + *

+ * NOTE: This is not called when the expanding/collapsing is triggered programmatically via + * {@link #expandParent} and other methods on the adapter. (Only called for changes triggered + * from the viewholder) + *

+ * Implement this in your {@link android.app.Activity} or {@link android.app.Fragment} + * to receive these callbacks. + */ + public interface ExpandCollapseListener { + /** + * Called when a parent is expanded. + * + * @param parentPosition The position of the parent in the list being expanded + */ + @UiThread + void onParentExpanded(int parentPosition); + + /** + * Called when a parent is collapsed. + * + * @param parentPosition The position of the parent in the list being collapsed + */ + @UiThread + void onParentCollapsed(int parentPosition); + } + + /** + * Primary constructor. Sets up {@link #mParentList} and {@link #mFlatItemList}. + *

+ * Any changes to {@link #mParentList} should be made on the original instance, and notified via + * {@link #notifyParentInserted(int)} + * {@link #notifyParentRemoved(int)} + * {@link #notifyParentChanged(int)} + * {@link #notifyParentRangeInserted(int, int)} + * {@link #notifyChildInserted(int, int)} + * {@link #notifyChildRemoved(int, int)} + * {@link #notifyChildChanged(int, int)} + * methods and not the notify methods of RecyclerView.Adapter. + * + * @param parentList List of all parents to be displayed in the RecyclerView that this + * adapter is linked to + */ + public ExpandableRecyclerAdapter(@NonNull List

parentList) { + super(); + mParentList = parentList; + mFlatItemList = generateFlattenedParentChildList(parentList); + mAttachedRecyclerViewPool = new ArrayList<>(); + mExpansionStateMap = new HashMap<>(mParentList.size()); + } + + /** + * Implementation of Adapter.onCreateViewHolder(ViewGroup, int) + * that determines if the list item is a parent or a child and calls through + * to the appropriate implementation of either {@link #onCreateParentViewHolder(ViewGroup, int)} + * or {@link #onCreateChildViewHolder(ViewGroup, int)}. + * + * @param viewGroup The {@link ViewGroup} into which the new {@link android.view.View} + * will be added after it is bound to an adapter position. + * @param viewType The view type of the new {@code android.view.View}. + * @return A new RecyclerView.ViewHolder + * that holds a {@code android.view.View} of the given view type. + */ + @NonNull + @Override + @UiThread + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) { + if (isParentViewType(viewType)) { + PVH pvh = onCreateParentViewHolder(viewGroup, viewType); + pvh.setParentViewHolderExpandCollapseListener(mParentViewHolderExpandCollapseListener); + pvh.mExpandableAdapter = this; + return pvh; + } else { + CVH cvh = onCreateChildViewHolder(viewGroup, viewType); + cvh.mExpandableAdapter = this; + return cvh; + } + } + + /** + * Implementation of Adapter.onBindViewHolder(RecyclerView.ViewHolder, int) + * that determines if the list item is a parent or a child and calls through + * to the appropriate implementation of either + * {@link #onBindParentViewHolder(ParentViewHolder, int, Parent)} or + * {@link #onBindChildViewHolder(ChildViewHolder, int, int, Object)}. + * + * @param holder The RecyclerView.ViewHolder to bind data to + * @param flatPosition The index in the merged list of children and parents at which to bind + */ + @Override + @SuppressWarnings("unchecked") + @UiThread + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int flatPosition) { + if (flatPosition > mFlatItemList.size()) { + throw new IllegalStateException("Trying to bind item out of bounds, size " + mFlatItemList.size() + + " flatPosition " + flatPosition + ". Was the data changed without a call to notify...()?"); + } + + ExpandableWrapper listItem = mFlatItemList.get(flatPosition); + if (listItem.isParent()) { + PVH parentViewHolder = (PVH) holder; + + if (parentViewHolder.shouldItemViewClickToggleExpansion()) { + parentViewHolder.setMainItemClickToExpand(); + } + + parentViewHolder.setExpanded(listItem.isExpanded()); + parentViewHolder.mParent = listItem.getParent(); + onBindParentViewHolder(parentViewHolder, getNearestParentPosition(flatPosition), listItem.getParent()); + } else { + CVH childViewHolder = (CVH) holder; + childViewHolder.mChild = listItem.getChild(); + onBindChildViewHolder(childViewHolder, getNearestParentPosition(flatPosition), getChildPosition(flatPosition), listItem.getChild()); + } + } + + /** + * Callback called from {@link #onCreateViewHolder(ViewGroup, int)} when + * the list item created is a parent. + * + * @param parentViewGroup The {@link ViewGroup} in the list for which a {@link PVH} is being + * created + * @return A {@code PVH} corresponding to the parent with the {@code ViewGroup} parentViewGroup + */ + @NonNull + @UiThread + public abstract PVH onCreateParentViewHolder(@NonNull ViewGroup parentViewGroup, int viewType); + + /** + * Callback called from {@link #onCreateViewHolder(ViewGroup, int)} when + * the list item created is a child. + * + * @param childViewGroup The {@link ViewGroup} in the list for which a {@link CVH} + * is being created + * @return A {@code CVH} corresponding to the child with the {@code ViewGroup} childViewGroup + */ + @NonNull + @UiThread + public abstract CVH onCreateChildViewHolder(@NonNull ViewGroup childViewGroup, int viewType); + + /** + * Callback called from onBindViewHolder(RecyclerView.ViewHolder, int) + * when the list item bound to is a parent. + *

+ * Bind data to the {@link PVH} here. + * + * @param parentViewHolder The {@code PVH} to bind data to + * @param parentPosition The position of the parent to bind + * @param parent The parent which holds the data to be bound to the {@code PVH} + */ + @UiThread + public abstract void onBindParentViewHolder(@NonNull PVH parentViewHolder, int parentPosition, @NonNull P parent); + + /** + * Callback called from onBindViewHolder(RecyclerView.ViewHolder, int) + * when the list item bound to is a child. + *

+ * Bind data to the {@link CVH} here. + * + * @param childViewHolder The {@code CVH} to bind data to + * @param parentPosition The position of the parent that contains the child to bind + * @param childPosition The position of the child to bind + * @param child The child which holds that data to be bound to the {@code CVH} + */ + @UiThread + public abstract void onBindChildViewHolder(@NonNull CVH childViewHolder, int parentPosition, int childPosition, @NonNull C child); + + /** + * Gets the number of parents and children currently expanded. + * + * @return The size of {@link #mFlatItemList} + */ + @Override + @UiThread + public int getItemCount() { + return mFlatItemList.size(); + } + + /** + * For multiple view type support look at overriding {@link #getParentViewType(int)} and + * {@link #getChildViewType(int, int)}. Almost all cases should override those instead + * of this method. + * + * @param flatPosition The index in the merged list of children and parents to get the view type of + * @return Gets the view type of the item at the given flatPosition. + */ + @Override + @UiThread + public int getItemViewType(int flatPosition) { + ExpandableWrapper listItem = mFlatItemList.get(flatPosition); + if (listItem.isParent()) { + return getParentViewType(getNearestParentPosition(flatPosition)); + } else { + return getChildViewType(getNearestParentPosition(flatPosition), getChildPosition(flatPosition)); + } + } + + /** + * Return the view type of the parent at {@code parentPosition} for the purposes of view recycling. + *

+ * The default implementation of this method returns {@link #TYPE_PARENT}, making the assumption of + * a single view type for the parents in this adapter. Unlike ListView adapters, types need not + * be contiguous. Consider using id resources to uniquely identify item view types. + *

+ * If you are overriding this method make sure to override {@link #isParentViewType(int)} as well. + *

+ * Start your defined viewtypes at {@link #TYPE_FIRST_USER} + * + * @param parentPosition The index of the parent to query + * @return integer value identifying the type of the view needed to represent the parent at + * {@code parentPosition}. Type codes need not be contiguous. + */ + public int getParentViewType(int parentPosition) { + return TYPE_PARENT; + } + + + /** + * Return the view type of the child {@code parentPosition} contained within the parent + * at {@code parentPosition} for the purposes of view recycling. + *

+ * The default implementation of this method returns {@link #TYPE_CHILD}, making the assumption of + * a single view type for the children in this adapter. Unlike ListView adapters, types need not + * be contiguous. Consider using id resources to uniquely identify item view types. + *

+ * Start your defined viewtypes at {@link #TYPE_FIRST_USER} + * + * @param parentPosition The index of the parent continaing the child to query + * @param childPosition The index of the child within the parent to query + * @return integer value identifying the type of the view needed to represent the child at + * {@code parentPosition}. Type codes need not be contiguous. + */ + public int getChildViewType(int parentPosition, int childPosition) { + return TYPE_CHILD; + } + + /** + * Used to determine whether a viewType is that of a parent or not, for ViewHolder creation purposes. + *

+ * Only override if {@link #getParentViewType(int)} is being overriden + * + * @param viewType the viewType identifier in question + * @return whether the given viewType belongs to a parent view + */ + public boolean isParentViewType(int viewType) { + return viewType == TYPE_PARENT; + } + + /** + * Gets the list of parents that is backing this adapter. + * Changes can be made to the list and the adapter notified via the + * {@link #notifyParentInserted(int)} + * {@link #notifyParentRemoved(int)} + * {@link #notifyParentChanged(int)} + * {@link #notifyParentRangeInserted(int, int)} + * {@link #notifyChildInserted(int, int)} + * {@link #notifyChildRemoved(int, int)} + * {@link #notifyChildChanged(int, int)} + * methods. + * + * + * @return The list of parents that this adapter represents + */ + @NonNull + @UiThread + public List

getParentList() { + return mParentList; + } + + /** + * Set a new list of parents and notify any registered observers that the data set has changed. + *

+ * This setter does not specify what about the data set has changed, forcing + * any observers to assume that all existing items and structure may no longer be valid. + * LayoutManagers will be forced to fully rebind and relayout all visible views.

+ *

+ * It will always be more efficient to use the more specific change events if you can. + * Rely on {@code #setParentList(List, boolean)} as a last resort. There will be no animation + * of changes, unlike the more specific change events listed below. + * + * @see #notifyParentInserted(int) + * @see #notifyParentRemoved(int) + * @see #notifyParentChanged(int) + * @see #notifyParentRangeInserted(int, int) + * @see #notifyChildInserted(int, int) + * @see #notifyChildRemoved(int, int) + * @see #notifyChildChanged(int, int) + * + * @param preserveExpansionState If true, the adapter will attempt to preserve your parent's last expanded + * state. This depends on object equality for comparisons of + * old parents to parents in the new list. + * + * If false, only {@link Parent#isInitiallyExpanded()} + * will be used to determine expanded state. + * + */ + @UiThread + public void setParentList(@NonNull List

parentList, boolean preserveExpansionState) { + mParentList = parentList; + notifyParentDataSetChanged(preserveExpansionState); + } + + /** + * Implementation of Adapter#onAttachedToRecyclerView(RecyclerView). + *

+ * Called when this {@link ExpandableRecyclerAdapter} is attached to a RecyclerView. + * + * @param recyclerView The {@code RecyclerView} this {@code ExpandableRecyclerAdapter} + * is being attached to + */ + @Override + @UiThread + public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { + super.onAttachedToRecyclerView(recyclerView); + mAttachedRecyclerViewPool.add(recyclerView); + } + + + /** + * Implementation of Adapter.onDetachedFromRecyclerView(RecyclerView) + *

+ * Called when this ExpandableRecyclerAdapter is detached from a RecyclerView. + * + * @param recyclerView The {@code RecyclerView} this {@code ExpandableRecyclerAdapter} + * is being detached from + */ + @Override + @UiThread + public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) { + super.onDetachedFromRecyclerView(recyclerView); + mAttachedRecyclerViewPool.remove(recyclerView); + } + + @UiThread + public void setExpandCollapseListener(@Nullable ExpandCollapseListener expandCollapseListener) { + mExpandCollapseListener = expandCollapseListener; + } + + /** + * Called when a ParentViewHolder has triggered an expansion for it's parent + * + * @param flatParentPosition the position of the parent that is calling to be expanded + */ + @UiThread + protected void parentExpandedFromViewHolder(int flatParentPosition) { + ExpandableWrapper parentWrapper = mFlatItemList.get(flatParentPosition); + updateExpandedParent(parentWrapper, flatParentPosition, true); + } + + /** + * Called when a ParentViewHolder has triggered a collapse for it's parent + * + * @param flatParentPosition the position of the parent that is calling to be collapsed + */ + @UiThread + protected void parentCollapsedFromViewHolder(int flatParentPosition) { + ExpandableWrapper parentWrapper = mFlatItemList.get(flatParentPosition); + updateCollapsedParent(parentWrapper, flatParentPosition, true); + } + + private ParentViewHolderExpandCollapseListener mParentViewHolderExpandCollapseListener = new ParentViewHolderExpandCollapseListener() { + + /** + * Implementation of {@link ParentViewHolderExpandCollapseListener#onParentExpanded(int)}. + *

+ * Called when a {@link P} is triggered to expand. + * + * @param flatParentPosition The index of the item in the list being expanded, relative to the flattened list + */ + @Override + @UiThread + public void onParentExpanded(int flatParentPosition) { + parentExpandedFromViewHolder(flatParentPosition); + } + + /** + * Implementation of {@link ParentViewHolderExpandCollapseListener#onParentCollapsed(int)}. + *

+ * Called when a {@link P} is triggered to collapse. + * + * @param flatParentPosition The index of the item in the list being collapsed, relative to the flattened list + */ + @Override + @UiThread + public void onParentCollapsed(int flatParentPosition) { + parentCollapsedFromViewHolder(flatParentPosition); + } + }; + + // region Programmatic Expansion/Collapsing + + /** + * Expands the parent associated with a specified {@link P} in the list of parents. + * + * @param parent The {@code P} of the parent to expand + */ + @UiThread + public void expandParent(@NonNull P parent) { + ExpandableWrapper parentWrapper = new ExpandableWrapper<>(parent); + int flatParentPosition = mFlatItemList.indexOf(parentWrapper); + if (flatParentPosition == INVALID_FLAT_POSITION) { + return; + } + + expandViews(mFlatItemList.get(flatParentPosition), flatParentPosition); + } + + /** + * Expands the parent with the specified index in the list of parents. + * + * @param parentPosition The position of the parent to expand + */ + @UiThread + public void expandParent(int parentPosition) { + expandParent(mParentList.get(parentPosition)); + } + + /** + * Expands all parents in a range of indices in the list of parents. + * + * @param startParentPosition The index at which to to start expanding parents + * @param parentCount The number of parents to expand + */ + @UiThread + public void expandParentRange(int startParentPosition, int parentCount) { + int endParentPosition = startParentPosition + parentCount; + for (int i = startParentPosition; i < endParentPosition; i++) { + expandParent(i); + } + } + + /** + * Expands all parents in the list. + */ + @UiThread + public void expandAllParents() { + for (P parent : mParentList) { + expandParent(parent); + } + } + + /** + * Collapses the parent associated with a specified {@link P} in the list of parents. + * + * @param parent The {@code P} of the parent to collapse + */ + @UiThread + public void collapseParent(@NonNull P parent) { + ExpandableWrapper parentWrapper = new ExpandableWrapper<>(parent); + int flatParentPosition = mFlatItemList.indexOf(parentWrapper); + if (flatParentPosition == INVALID_FLAT_POSITION) { + return; + } + + collapseViews(mFlatItemList.get(flatParentPosition), flatParentPosition); + } + + /** + * Collapses the parent with the specified index in the list of parents. + * + * @param parentPosition The index of the parent to collapse + */ + @UiThread + public void collapseParent(int parentPosition) { + collapseParent(mParentList.get(parentPosition)); + } + + /** + * Collapses all parents in a range of indices in the list of parents. + * + * @param startParentPosition The index at which to to start collapsing parents + * @param parentCount The number of parents to collapse + */ + @UiThread + public void collapseParentRange(int startParentPosition, int parentCount) { + int endParentPosition = startParentPosition + parentCount; + for (int i = startParentPosition; i < endParentPosition; i++) { + collapseParent(i); + } + } + + /** + * Collapses all parents in the list. + */ + @UiThread + public void collapseAllParents() { + for (P parent : mParentList) { + collapseParent(parent); + } + } + + /** + * Stores the expanded state map across state loss. + *

+ * Should be called from {@link Activity#onSaveInstanceState(Bundle)} in + * the {@link Activity} that hosts the RecyclerView that this + * {@link ExpandableRecyclerAdapter} is attached to. + *

+ * This will make sure to add the expanded state map as an extra to the + * instance state bundle to be used in {@link #onRestoreInstanceState(Bundle)}. + * + * @param savedInstanceState The {@code Bundle} into which to store the + * expanded state map + */ + @UiThread + public void onSaveInstanceState(@NonNull Bundle savedInstanceState) { + savedInstanceState.putSerializable(EXPANDED_STATE_MAP, generateExpandedStateMap()); + } + + /** + * Fetches the expandable state map from the saved instance state {@link Bundle} + * and restores the expanded states of all of the parents. + *

+ * Should be called from {@link Activity#onRestoreInstanceState(Bundle)} in + * the {@link Activity} that hosts the RecyclerView that this + * {@link ExpandableRecyclerAdapter} is attached to. + *

+ * Assumes that the list of parents is the same as when the saved + * instance state was stored. + * + * @param savedInstanceState The {@code Bundle} from which the expanded + * state map is loaded + */ + @SuppressWarnings("unchecked") + @UiThread + public void onRestoreInstanceState(@Nullable Bundle savedInstanceState) { + if (savedInstanceState == null + || !savedInstanceState.containsKey(EXPANDED_STATE_MAP)) { + return; + } + + HashMap expandedStateMap = (HashMap) savedInstanceState.getSerializable(EXPANDED_STATE_MAP); + if (expandedStateMap == null) { + return; + } + + List> itemList = new ArrayList<>(); + int parentsCount = mParentList.size(); + for (int i = 0; i < parentsCount; i++) { + ExpandableWrapper parentWrapper = new ExpandableWrapper<>(mParentList.get(i)); + itemList.add(parentWrapper); + + if (expandedStateMap.containsKey(i)) { + boolean expanded = expandedStateMap.get(i); + parentWrapper.setExpanded(expanded); + + if (expanded) { + List> wrappedChildList = parentWrapper.getWrappedChildList(); + int childrenCount = wrappedChildList.size(); + for (int j = 0; j < childrenCount; j++) { + ExpandableWrapper childWrapper = wrappedChildList.get(j); + itemList.add(childWrapper); + } + } + } + } + + mFlatItemList = itemList; + + notifyDataSetChanged(); + } + + /** + * Calls through to the ParentViewHolder to expand views for each + * RecyclerView the specified parent is a child of. + *

+ * These calls to the ParentViewHolder are made so that animations can be + * triggered at the ViewHolder level. + * + * @param flatParentPosition The index of the parent to expand + */ + @SuppressWarnings("unchecked") + @UiThread + private void expandViews(@NonNull ExpandableWrapper parentWrapper, int flatParentPosition) { + PVH viewHolder; + for (RecyclerView recyclerView : mAttachedRecyclerViewPool) { + viewHolder = (PVH) recyclerView.findViewHolderForAdapterPosition(flatParentPosition); + if (viewHolder != null && !viewHolder.isExpanded()) { + viewHolder.setExpanded(true); + viewHolder.onExpansionToggled(false); + } + } + + updateExpandedParent(parentWrapper, flatParentPosition, false); + } + + /** + * Calls through to the ParentViewHolder to collapse views for each + * RecyclerView a specified parent is a child of. + *

+ * These calls to the ParentViewHolder are made so that animations can be + * triggered at the ViewHolder level. + * + * @param flatParentPosition The index of the parent to collapse + */ + @SuppressWarnings("unchecked") + @UiThread + private void collapseViews(@NonNull ExpandableWrapper parentWrapper, int flatParentPosition) { + PVH viewHolder; + for (RecyclerView recyclerView : mAttachedRecyclerViewPool) { + viewHolder = (PVH) recyclerView.findViewHolderForAdapterPosition(flatParentPosition); + if (viewHolder != null && viewHolder.isExpanded()) { + viewHolder.setExpanded(false); + viewHolder.onExpansionToggled(true); + } + } + + updateCollapsedParent(parentWrapper, flatParentPosition, false); + } + + /** + * Expands a specified parent. Calls through to the + * ExpandCollapseListener and adds children of the specified parent to the + * flat list of items. + * + * @param parentWrapper The ExpandableWrapper of the parent to expand + * @param flatParentPosition The index of the parent to expand + * @param expansionTriggeredByListItemClick true if expansion was triggered + * by a click event, false otherwise. + */ + @UiThread + private void updateExpandedParent(@NonNull ExpandableWrapper parentWrapper, int flatParentPosition, boolean expansionTriggeredByListItemClick) { + if (parentWrapper.isExpanded()) { + return; + } + + parentWrapper.setExpanded(true); + mExpansionStateMap.put(parentWrapper.getParent(), true); + + List> wrappedChildList = parentWrapper.getWrappedChildList(); + if (wrappedChildList != null) { + int childCount = wrappedChildList.size(); + for (int i = 0; i < childCount; i++) { + mFlatItemList.add(flatParentPosition + i + 1, wrappedChildList.get(i)); + } + + notifyItemRangeInserted(flatParentPosition + 1, childCount); + } + + if (expansionTriggeredByListItemClick && mExpandCollapseListener != null) { + mExpandCollapseListener.onParentExpanded(getNearestParentPosition(flatParentPosition)); + } + } + + /** + * Collapses a specified parent item. Calls through to the + * ExpandCollapseListener and removes children of the specified parent from the + * flat list of items. + * + * @param parentWrapper The ExpandableWrapper of the parent to collapse + * @param flatParentPosition The index of the parent to collapse + * @param collapseTriggeredByListItemClick true if expansion was triggered + * by a click event, false otherwise. + */ + @UiThread + private void updateCollapsedParent(@NonNull ExpandableWrapper parentWrapper, int flatParentPosition, boolean collapseTriggeredByListItemClick) { + if (!parentWrapper.isExpanded()) { + return; + } + + parentWrapper.setExpanded(false); + mExpansionStateMap.put(parentWrapper.getParent(), false); + + List> wrappedChildList = parentWrapper.getWrappedChildList(); + if (wrappedChildList != null) { + int childCount = wrappedChildList.size(); + for (int i = childCount - 1; i >= 0; i--) { + mFlatItemList.remove(flatParentPosition + i + 1); + } + + notifyItemRangeRemoved(flatParentPosition + 1, childCount); + } + + if (collapseTriggeredByListItemClick && mExpandCollapseListener != null) { + mExpandCollapseListener.onParentCollapsed(getNearestParentPosition(flatParentPosition)); + } + } + + /** + * Given the index relative to the entire RecyclerView, returns the nearest + * ParentPosition without going past the given index. + *

+ * If it is the index of a parent, will return the corresponding parent position. + * If it is the index of a child within the RV, will return the position of that child's parent. + */ + @UiThread + int getNearestParentPosition(int flatPosition) { + if (flatPosition == 0) { + return 0; + } + + int parentCount = -1; + for (int i = 0; i <= flatPosition; i++) { + ExpandableWrapper listItem = mFlatItemList.get(i); + if (listItem.isParent()) { + parentCount++; + } + } + return parentCount; + } + + /** + * Given the index relative to the entire RecyclerView for a child item, + * returns the child position within the child list of the parent. + */ + @UiThread + int getChildPosition(int flatPosition) { + if (flatPosition == 0) { + return 0; + } + + int childCount = 0; + for (int i = 0; i < flatPosition; i++) { + ExpandableWrapper listItem = mFlatItemList.get(i); + if (listItem.isParent()) { + childCount = 0; + } else { + childCount++; + } + } + return childCount; + } + + // endregion + + // region Data Manipulation + + /** + * Notify any registered observers that the data set has changed. + *

+ * This event does not specify what about the data set has changed, forcing + * any observers to assume that all existing items and structure may no longer be valid. + * LayoutManagers will be forced to fully rebind and relayout all visible views.

+ *

+ * It will always be more efficient to use the more specific change events if you can. + * Rely on {@code #notifyParentDataSetChanged(boolean)} as a last resort. There will be no animation + * of changes, unlike the more specific change events listed below. + * + * @see #notifyParentInserted(int) + * @see #notifyParentRemoved(int) + * @see #notifyParentChanged(int) + * @see #notifyParentRangeInserted(int, int) + * @see #notifyChildInserted(int, int) + * @see #notifyChildRemoved(int, int) + * @see #notifyChildChanged(int, int) + * + * @param preserveExpansionState If true, the adapter will attempt to preserve your parent's last expanded + * state. This depends on object equality for comparisons of + * old parents to parents in the new list. + * + * If false, only {@link Parent#isInitiallyExpanded()} + * will be used to determine expanded state. + * + */ + @UiThread + public void notifyParentDataSetChanged(boolean preserveExpansionState) { + if (preserveExpansionState) { + mFlatItemList = generateFlattenedParentChildList(mParentList, mExpansionStateMap); + } else { + mFlatItemList = generateFlattenedParentChildList(mParentList); + } + notifyDataSetChanged(); + } + + /** + * Notify any registered observers that the parent reflected at {@code parentPosition} + * has been newly inserted. The parent previously at {@code parentPosition} is now at + * position {@code parentPosition + 1}. + *

+ * This is a structural change event. Representations of other existing items in the + * data set are still considered up to date and will not be rebound, though their + * positions may be altered. + * + * @param parentPosition Position of the newly inserted parent in the data set, relative + * to the list of parents only. + * + * @see #notifyParentRangeInserted(int, int) + */ + @UiThread + public void notifyParentInserted(int parentPosition) { + P parent = mParentList.get(parentPosition); + + int flatParentPosition; + if (parentPosition < mParentList.size() - 1) { + flatParentPosition = getFlatParentPosition(parentPosition); + } else { + flatParentPosition = mFlatItemList.size(); + } + + int sizeChanged = addParentWrapper(flatParentPosition, parent); + notifyItemRangeInserted(flatParentPosition, sizeChanged); + } + + /** + * Notify any registered observers that the currently reflected {@code itemCount} + * parents starting at {@code parentPositionStart} have been newly inserted. + * The parents previously located at {@code parentPositionStart} and beyond + * can now be found starting at position {@code parentPositionStart + itemCount}. + *

+ * This is a structural change event. Representations of other existing items in the + * data set are still considered up to date and will not be rebound, though their positions + * may be altered. + * + * @param parentPositionStart Position of the first parent that was inserted, relative + * to the list of parents only. + * @param itemCount Number of items inserted + * + * @see #notifyParentInserted(int) + */ + @UiThread + public void notifyParentRangeInserted(int parentPositionStart, int itemCount) { + int initialFlatParentPosition; + if (parentPositionStart < mParentList.size() - itemCount) { + initialFlatParentPosition = getFlatParentPosition(parentPositionStart); + } else { + initialFlatParentPosition = mFlatItemList.size(); + } + + int sizeChanged = 0; + int flatParentPosition = initialFlatParentPosition; + int changed; + int parentPositionEnd = parentPositionStart + itemCount; + for (int i = parentPositionStart; i < parentPositionEnd; i++) { + P parent = mParentList.get(i); + changed = addParentWrapper(flatParentPosition, parent); + flatParentPosition += changed; + sizeChanged += changed; + } + + notifyItemRangeInserted(initialFlatParentPosition, sizeChanged); + } + + @UiThread + private int addParentWrapper(int flatParentPosition, P parent) { + int sizeChanged = 1; + ExpandableWrapper parentWrapper = new ExpandableWrapper<>(parent); + mFlatItemList.add(flatParentPosition, parentWrapper); + if (parentWrapper.isParentInitiallyExpanded()) { + parentWrapper.setExpanded(true); + List> wrappedChildList = parentWrapper.getWrappedChildList(); + mFlatItemList.addAll(flatParentPosition + sizeChanged, wrappedChildList); + sizeChanged += wrappedChildList.size(); + } + return sizeChanged; + } + + /** + * Notify any registered observers that the parents previously located at {@code parentPosition} + * has been removed from the data set. The parents previously located at and after + * {@code parentPosition} may now be found at {@code oldPosition - 1}. + *

+ * This is a structural change event. Representations of other existing items in the + * data set are still considered up to date and will not be rebound, though their positions + * may be altered. + * + * @param parentPosition Position of the parent that has now been removed, relative + * to the list of parents only. + */ + @UiThread + public void notifyParentRemoved(int parentPosition) { + int flatParentPosition = getFlatParentPosition(parentPosition); + int sizeChanged = removeParentWrapper(flatParentPosition); + + notifyItemRangeRemoved(flatParentPosition, sizeChanged); + } + + /** + * Notify any registered observers that the {@code itemCount} parents previously + * located at {@code parentPositionStart} have been removed from the data set. The parents + * previously located at and after {@code parentPositionStart + itemCount} may now be + * found at {@code oldPosition - itemCount}. + *

+ * This is a structural change event. Representations of other existing items in the + * data set are still considered up to date and will not be rebound, though their positions + * may be altered. + * + * @param parentPositionStart The previous position of the first parent that was + * removed, relative to list of parents only. + * @param itemCount Number of parents removed from the data set + */ + public void notifyParentRangeRemoved(int parentPositionStart, int itemCount) { + int sizeChanged = 0; + int flatParentPositionStart = getFlatParentPosition(parentPositionStart); + for (int i = 0; i < itemCount; i++) { + sizeChanged += removeParentWrapper(flatParentPositionStart); + } + + notifyItemRangeRemoved(flatParentPositionStart, sizeChanged); + } + + @UiThread + private int removeParentWrapper(int flatParentPosition) { + int sizeChanged = 1; + ExpandableWrapper parentWrapper = mFlatItemList.remove(flatParentPosition); + if (parentWrapper.isExpanded()) { + int childListSize = parentWrapper.getWrappedChildList().size(); + for (int i = 0; i < childListSize; i++) { + mFlatItemList.remove(flatParentPosition); + sizeChanged++; + } + } + return sizeChanged; + } + + /** + * Notify any registered observers that the parent at {@code parentPosition} has changed. + * This will also trigger an item changed for children of the parent list specified. + *

+ * This is an item change event, not a structural change event. It indicates that any + * reflection of the data at {@code parentPosition} is out of date and should be updated. + * The parent at {@code parentPosition} retains the same identity. This means + * the number of children must stay the same. + * + * @param parentPosition Position of the item that has changed + */ + @UiThread + public void notifyParentChanged(int parentPosition) { + P parent = mParentList.get(parentPosition); + int flatParentPositionStart = getFlatParentPosition(parentPosition); + int sizeChanged = changeParentWrapper(flatParentPositionStart, parent); + + notifyItemRangeChanged(flatParentPositionStart, sizeChanged); + } + + /** + * Notify any registered observers that the {@code itemCount} parents starting + * at {@code parentPositionStart} have changed. This will also trigger an item changed + * for children of the parent list specified. + *

+ * This is an item change event, not a structural change event. It indicates that any + * reflection of the data in the given position range is out of date and should be updated. + * The parents in the given range retain the same identity. This means that the number of + * children must stay the same. + * + * @param parentPositionStart Position of the item that has changed + * @param itemCount Number of parents changed in the data set + */ + @UiThread + public void notifyParentRangeChanged(int parentPositionStart, int itemCount) { + int flatParentPositionStart = getFlatParentPosition(parentPositionStart); + + int flatParentPosition = flatParentPositionStart; + int sizeChanged = 0; + int changed; + P parent; + for (int j = 0; j < itemCount; j++) { + parent = mParentList.get(parentPositionStart); + changed = changeParentWrapper(flatParentPosition, parent); + sizeChanged += changed; + flatParentPosition += changed; + parentPositionStart++; + } + notifyItemRangeChanged(flatParentPositionStart, sizeChanged); + } + + private int changeParentWrapper(int flatParentPosition, P parent) { + ExpandableWrapper parentWrapper = mFlatItemList.get(flatParentPosition); + parentWrapper.setParent(parent); + int sizeChanged = 1; + if (parentWrapper.isExpanded()) { + List> wrappedChildList = parentWrapper.getWrappedChildList(); + int childSize = wrappedChildList.size(); + for (int i = 0; i < childSize; i++) { + mFlatItemList.set(flatParentPosition + i + 1, wrappedChildList.get(i)); + sizeChanged++; + } + } + + return sizeChanged; + } + + /** + * Notify any registered observers that the parent and its children reflected at + * {@code fromParentPosition} has been moved to {@code toParentPosition}. + *

+ *

This is a structural change event. Representations of other existing items in the + * data set are still considered up to date and will not be rebound, though their + * positions may be altered.

+ * + * @param fromParentPosition Previous position of the parent, relative to the list of + * parents only. + * @param toParentPosition New position of the parent, relative to the list of parents only. + */ + @UiThread + public void notifyParentMoved(int fromParentPosition, int toParentPosition) { + int fromFlatParentPosition = getFlatParentPosition(fromParentPosition); + ExpandableWrapper fromParentWrapper = mFlatItemList.get(fromFlatParentPosition); + + // If the parent is collapsed we can take advantage of notifyItemMoved otherwise + // we are forced to do a "manual" move by removing and then adding the parent + children + // (no notifyItemRangeMovedAvailable) + boolean isCollapsed = !fromParentWrapper.isExpanded(); + boolean isExpandedNoChildren = !isCollapsed && (fromParentWrapper.getWrappedChildList().size() == 0); + if (isCollapsed || isExpandedNoChildren) { + int toFlatParentPosition = getFlatParentPosition(toParentPosition); + ExpandableWrapper toParentWrapper = mFlatItemList.get(toFlatParentPosition); + mFlatItemList.remove(fromFlatParentPosition); + int childOffset = 0; + if (toParentWrapper.isExpanded()) { + childOffset = toParentWrapper.getWrappedChildList().size(); + } + mFlatItemList.add(toFlatParentPosition + childOffset, fromParentWrapper); + + notifyItemMoved(fromFlatParentPosition, toFlatParentPosition + childOffset); + } else { + // Remove the parent and children + int sizeChanged = 0; + int childListSize = fromParentWrapper.getWrappedChildList().size(); + for (int i = 0; i < childListSize + 1; i++) { + mFlatItemList.remove(fromFlatParentPosition); + sizeChanged++; + } + notifyItemRangeRemoved(fromFlatParentPosition, sizeChanged); + + + // Add the parent and children at new position + int toFlatParentPosition = getFlatParentPosition(toParentPosition); + int childOffset = 0; + if (toFlatParentPosition != INVALID_FLAT_POSITION) { + ExpandableWrapper toParentWrapper = mFlatItemList.get(toFlatParentPosition); + if (toParentWrapper.isExpanded()) { + childOffset = toParentWrapper.getWrappedChildList().size(); + } + } else { + toFlatParentPosition = mFlatItemList.size(); + } + + mFlatItemList.add(toFlatParentPosition + childOffset, fromParentWrapper); + List> wrappedChildList = fromParentWrapper.getWrappedChildList(); + sizeChanged = wrappedChildList.size() + 1; + + mFlatItemList.addAll(toFlatParentPosition + childOffset + 1, wrappedChildList); + + notifyItemRangeInserted(toFlatParentPosition + childOffset, sizeChanged); + } + } + + /** + * Notify any registered observers that the parent reflected at {@code parentPosition} + * has a child list item that has been newly inserted at {@code childPosition}. + * The child list item previously at {@code childPosition} is now at + * position {@code childPosition + 1}. + *

+ * This is a structural change event. Representations of other existing items in the + * data set are still considered up to date and will not be rebound, though their + * positions may be altered. + * + * @param parentPosition Position of the parent which has been added a child, relative + * to the list of parents only. + * @param childPosition Position of the child that has been inserted, relative to children + * of the parent specified by {@code parentPosition} only. + * + */ + @UiThread + public void notifyChildInserted(int parentPosition, int childPosition) { + int flatParentPosition = getFlatParentPosition(parentPosition); + ExpandableWrapper parentWrapper = mFlatItemList.get(flatParentPosition); + + parentWrapper.setParent(mParentList.get(parentPosition)); + if (parentWrapper.isExpanded()) { + ExpandableWrapper child = parentWrapper.getWrappedChildList().get(childPosition); + mFlatItemList.add(flatParentPosition + childPosition + 1, child); + notifyItemInserted(flatParentPosition + childPosition + 1); + } + } + + /** + * Notify any registered observers that the parent reflected at {@code parentPosition} + * has {@code itemCount} child list items that have been newly inserted at {@code childPositionStart}. + * The child list item previously at {@code childPositionStart} and beyond are now at + * position {@code childPositionStart + itemCount}. + *

+ * This is a structural change event. Representations of other existing items in the + * data set are still considered up to date and will not be rebound, though their + * positions may be altered. + * + * @param parentPosition Position of the parent which has been added a child, relative + * to the list of parents only. + * @param childPositionStart Position of the first child that has been inserted, + * relative to children of the parent specified by + * {@code parentPosition} only. + * @param itemCount number of children inserted + */ + @UiThread + public void notifyChildRangeInserted(int parentPosition, int childPositionStart, int itemCount) { + int flatParentPosition = getFlatParentPosition(parentPosition); + ExpandableWrapper parentWrapper = mFlatItemList.get(flatParentPosition); + + parentWrapper.setParent(mParentList.get(parentPosition)); + if (parentWrapper.isExpanded()) { + List> wrappedChildList = parentWrapper.getWrappedChildList(); + for (int i = 0; i < itemCount; i++) { + ExpandableWrapper child = wrappedChildList.get(childPositionStart + i); + mFlatItemList.add(flatParentPosition + childPositionStart + i + 1, child); + } + notifyItemRangeInserted(flatParentPosition + childPositionStart + 1, itemCount); + } + } + + /** + * Notify any registered observers that the parent located at {@code parentPosition} + * has a child that has been removed from the data set, previously located at {@code childPosition}. + * The child list item previously located at and after {@code childPosition} may + * now be found at {@code childPosition - 1}. + *

+ * This is a structural change event. Representations of other existing items in the + * data set are still considered up to date and will not be rebound, though their positions + * may be altered. + * + * @param parentPosition Position of the parent which has a child removed from, relative + * to the list of parents only. + * @param childPosition Position of the child that has been removed, relative to children + * of the parent specified by {@code parentPosition} only. + */ + @UiThread + public void notifyChildRemoved(int parentPosition, int childPosition) { + int flatParentPosition = getFlatParentPosition(parentPosition); + ExpandableWrapper parentWrapper = mFlatItemList.get(flatParentPosition); + parentWrapper.setParent(mParentList.get(parentPosition)); + + if (parentWrapper.isExpanded()) { + mFlatItemList.remove(flatParentPosition + childPosition + 1); + notifyItemRemoved(flatParentPosition + childPosition + 1); + } + } + + /** + * Notify any registered observers that the parent located at {@code parentPosition} + * has {@code itemCount} children that have been removed from the data set, previously + * located at {@code childPositionStart} onwards. The child previously located at and + * after {@code childPositionStart} may now be found at {@code childPositionStart - itemCount}. + *

+ * This is a structural change event. Representations of other existing items in the + * data set are still considered up to date and will not be rebound, though their positions + * may be altered. + * + * @param parentPosition Position of the parent which has a child removed from, relative + * to the list of parents only. + * @param childPositionStart Position of the first child that has been removed, relative + * to children of the parent specified by {@code parentPosition} only. + * @param itemCount number of children removed + */ + @UiThread + public void notifyChildRangeRemoved(int parentPosition, int childPositionStart, int itemCount) { + int flatParentPosition = getFlatParentPosition(parentPosition); + ExpandableWrapper parentWrapper = mFlatItemList.get(flatParentPosition); + parentWrapper.setParent(mParentList.get(parentPosition)); + + if (parentWrapper.isExpanded()) { + for (int i = 0; i < itemCount; i++) { + mFlatItemList.remove(flatParentPosition + childPositionStart + 1); + } + notifyItemRangeRemoved(flatParentPosition + childPositionStart + 1, itemCount); + } + } + + /** + * Notify any registered observers that the parent at {@code parentPosition} has + * a child located at {@code childPosition} that has changed. + *

+ * This is an item change event, not a structural change event. It indicates that any + * reflection of the data at {@code childPosition} is out of date and should be updated. + * The parent at {@code childPosition} retains the same identity. + * + * @param parentPosition Position of the parent which has a child that has changed + * @param childPosition Position of the child that has changed + */ + @UiThread + public void notifyChildChanged(int parentPosition, int childPosition) { + P parent = mParentList.get(parentPosition); + int flatParentPosition = getFlatParentPosition(parentPosition); + ExpandableWrapper parentWrapper = mFlatItemList.get(flatParentPosition); + parentWrapper.setParent(parent); + if (parentWrapper.isExpanded()) { + int flatChildPosition = flatParentPosition + childPosition + 1; + ExpandableWrapper child = parentWrapper.getWrappedChildList().get(childPosition); + mFlatItemList.set(flatChildPosition, child); + notifyItemChanged(flatChildPosition); + } + } + + /** + * Notify any registered observers that the parent at {@code parentPosition} has + * {@code itemCount} children starting at {@code childPositionStart} that have changed. + *

+ * This is an item change event, not a structural change event. It indicates that any + * The parent at {@code childPositionStart} retains the same identity. + * reflection of the set of {@code itemCount} children starting at {@code childPositionStart} + * are out of date and should be updated. + * + * @param parentPosition Position of the parent who has a child that has changed + * @param childPositionStart Position of the first child that has changed + * @param itemCount number of children changed + */ + @UiThread + public void notifyChildRangeChanged(int parentPosition, int childPositionStart, int itemCount) { + P parent = mParentList.get(parentPosition); + int flatParentPosition = getFlatParentPosition(parentPosition); + ExpandableWrapper parentWrapper = mFlatItemList.get(flatParentPosition); + parentWrapper.setParent(parent); + if (parentWrapper.isExpanded()) { + int flatChildPosition = flatParentPosition + childPositionStart + 1; + for (int i = 0; i < itemCount; i++) { + ExpandableWrapper child + = parentWrapper.getWrappedChildList().get(childPositionStart + i); + mFlatItemList.set(flatChildPosition + i, child); + } + notifyItemRangeChanged(flatChildPosition, itemCount); + } + } + + /** + * Notify any registered observers that the child list item contained within the parent + * at {@code parentPosition} has moved from {@code fromChildPosition} to {@code toChildPosition}. + *

+ *

This is a structural change event. Representations of other existing items in the + * data set are still considered up to date and will not be rebound, though their + * positions may be altered.

+ * + * @param parentPosition Position of the parent which has a child that has moved + * @param fromChildPosition Previous position of the child + * @param toChildPosition New position of the child + */ + @UiThread + public void notifyChildMoved(int parentPosition, int fromChildPosition, int toChildPosition) { + P parent = mParentList.get(parentPosition); + int flatParentPosition = getFlatParentPosition(parentPosition); + + ExpandableWrapper parentWrapper = mFlatItemList.get(flatParentPosition); + parentWrapper.setParent(parent); + if (parentWrapper.isExpanded()) { + ExpandableWrapper fromChild = mFlatItemList.remove(flatParentPosition + 1 + fromChildPosition); + mFlatItemList.add(flatParentPosition + 1 + toChildPosition, fromChild); + notifyItemMoved(flatParentPosition + 1 + fromChildPosition, flatParentPosition + 1 + toChildPosition); + } + } + + // endregion + + /** + * Generates a full list of all parents and their children, in order. + * + * @param parentList A list of the parents from + * the {@link ExpandableRecyclerAdapter} + * @return A list of all parents and their children, expanded + */ + private List> generateFlattenedParentChildList(List

parentList) { + List> flatItemList = new ArrayList<>(); + + int parentCount = parentList.size(); + for (int i = 0; i < parentCount; i++) { + P parent = parentList.get(i); + generateParentWrapper(flatItemList, parent, parent.isInitiallyExpanded()); + } + + return flatItemList; + } + + /** + * Generates a full list of all parents and their children, in order. Uses Map to preserve + * last expanded state. + * + * @param parentList A list of the parents from + * the {@link ExpandableRecyclerAdapter} + * @param savedLastExpansionState A map of the last expanded state for a given parent key. + * @return A list of all parents and their children, expanded accordingly + */ + private List> generateFlattenedParentChildList(List

parentList, Map savedLastExpansionState) { + List> flatItemList = new ArrayList<>(); + + int parentCount = parentList.size(); + for (int i = 0; i < parentCount; i++) { + P parent = parentList.get(i); + Boolean lastExpandedState = savedLastExpansionState.get(parent); + boolean shouldExpand = lastExpandedState == null ? parent.isInitiallyExpanded() : lastExpandedState; + + generateParentWrapper(flatItemList, parent, shouldExpand); + } + + return flatItemList; + } + + private void generateParentWrapper(List> flatItemList, P parent, boolean shouldExpand) { + ExpandableWrapper parentWrapper = new ExpandableWrapper<>(parent); + flatItemList.add(parentWrapper); + if (shouldExpand) { + generateExpandedChildren(flatItemList, parentWrapper); + } + } + + private void generateExpandedChildren(List> flatItemList, ExpandableWrapper parentWrapper) { + parentWrapper.setExpanded(true); + + List> wrappedChildList = parentWrapper.getWrappedChildList(); + int childCount = wrappedChildList.size(); + for (int j = 0; j < childCount; j++) { + ExpandableWrapper childWrapper = wrappedChildList.get(j); + flatItemList.add(childWrapper); + } + } + + /** + * Generates a HashMap used to store expanded state for items in the list + * on configuration change or whenever onResume is called. + * + * @return A HashMap containing the expanded state of all parents + */ + @NonNull + @UiThread + private HashMap generateExpandedStateMap() { + HashMap parentHashMap = new HashMap<>(); + int childCount = 0; + + int listItemCount = mFlatItemList.size(); + for (int i = 0; i < listItemCount; i++) { + if (mFlatItemList.get(i) != null) { + ExpandableWrapper listItem = mFlatItemList.get(i); + if (listItem.isParent()) { + parentHashMap.put(i - childCount, listItem.isExpanded()); + } else { + childCount++; + } + } + } + + return parentHashMap; + } + + /** + * Gets the index of a ExpandableWrapper within the helper item list based on + * the index of the ExpandableWrapper. + * + * @param parentPosition The index of the parent in the list of parents + * @return The index of the parent in the merged list of children and parents + */ + @UiThread + private int getFlatParentPosition(int parentPosition) { + int parentCount = 0; + int listItemCount = mFlatItemList.size(); + for (int i = 0; i < listItemCount; i++) { + if (mFlatItemList.get(i).isParent()) { + parentCount++; + + if (parentCount > parentPosition) { + return i; + } + } + } + + return INVALID_FLAT_POSITION; + } +} diff --git a/modules/expandable-recyclerview/src/main/java/com/bignerdranch/expandablerecyclerview/ParentViewHolder.java b/modules/expandable-recyclerview/src/main/java/com/bignerdranch/expandablerecyclerview/ParentViewHolder.java new file mode 100644 index 00000000..b72e7a05 --- /dev/null +++ b/modules/expandable-recyclerview/src/main/java/com/bignerdranch/expandablerecyclerview/ParentViewHolder.java @@ -0,0 +1,201 @@ +package com.bignerdranch.expandablerecyclerview; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.UiThread; +import androidx.recyclerview.widget.RecyclerView; +import android.view.View; + +import com.bignerdranch.expandablerecyclerview.model.Parent; + +/** + * ViewHolder for a {@link Parent} + * Keeps track of expanded state and holds callbacks which can be used to + * trigger expansion-based events. + * + * @author Ryan Brooks + * @version 1.0 + * @since 5/27/2015 + */ +public class ParentViewHolder

, C> extends RecyclerView.ViewHolder implements View.OnClickListener { + @Nullable + private ParentViewHolderExpandCollapseListener mParentViewHolderExpandCollapseListener; + private boolean mExpanded; + P mParent; + ExpandableRecyclerAdapter mExpandableAdapter; + + /** + * Empowers {@link com.bignerdranch.expandablerecyclerview.ExpandableRecyclerAdapter} + * implementations to be notified of expand/collapse state change events. + */ + interface ParentViewHolderExpandCollapseListener { + + /** + * Called when a parent is expanded. + * + * @param flatParentPosition The index of the parent in the list being expanded + */ + @UiThread + void onParentExpanded(int flatParentPosition); + + /** + * Called when a parent is collapsed. + * + * @param flatParentPosition The index of the parent in the list being collapsed + */ + @UiThread + void onParentCollapsed(int flatParentPosition); + } + + /** + * Default constructor. + * + * @param itemView The {@link View} being hosted in this ViewHolder + */ + @UiThread + public ParentViewHolder(@NonNull View itemView) { + super(itemView); + mExpanded = false; + } + + /** + * @return the Parent associated with this ViewHolder + */ + @UiThread + public P getParent() { + return mParent; + } + + /** + * Returns the adapter position of the Parent associated with this ParentViewHolder + * + * @return The adapter position of the Parent if it still exists in the adapter. + * RecyclerView.NO_POSITION if item has been removed from the adapter, + * RecyclerView.Adapter.notifyDataSetChanged() has been called after the last + * layout pass or the ViewHolder has already been recycled. + */ + @UiThread + public int getParentAdapterPosition() { + int flatPosition = getAdapterPosition(); + if (flatPosition == RecyclerView.NO_POSITION) { + return flatPosition; + } + + return mExpandableAdapter.getNearestParentPosition(flatPosition); + } + + /** + * Sets a {@link android.view.View.OnClickListener} on the entire parent + * view to trigger expansion. + */ + @UiThread + public void setMainItemClickToExpand() { + itemView.setOnClickListener(this); + } + + /** + * Returns expanded state for the {@link Parent} + * corresponding to this {@link ParentViewHolder}. + * + * @return true if expanded, false if not + */ + @UiThread + public boolean isExpanded() { + return mExpanded; + } + + /** + * Setter method for expanded state, used for initialization of expanded state. + * changes to the state are given in {@link #onExpansionToggled(boolean)} + * + * @param expanded true if expanded, false if not + */ + @UiThread + public void setExpanded(boolean expanded) { + mExpanded = expanded; + } + + /** + * Callback triggered when expansion state is changed, but not during + * initialization. + *

+ * Useful for implementing animations on expansion. + * + * @param expanded true if view is expanded before expansion is toggled, + * false if not + */ + @UiThread + public void onExpansionToggled(boolean expanded) { + + } + + /** + * Setter for the {@link ParentViewHolderExpandCollapseListener} implemented in + * {@link com.bignerdranch.expandablerecyclerview.ExpandableRecyclerAdapter}. + * + * @param parentViewHolderExpandCollapseListener The {@link ParentViewHolderExpandCollapseListener} to set on the {@link ParentViewHolder} + */ + @UiThread + void setParentViewHolderExpandCollapseListener(ParentViewHolderExpandCollapseListener parentViewHolderExpandCollapseListener) { + mParentViewHolderExpandCollapseListener = parentViewHolderExpandCollapseListener; + } + + /** + * {@link android.view.View.OnClickListener} to listen for click events on + * the entire parent {@link View}. + *

+ * Only registered if {@link #shouldItemViewClickToggleExpansion()} is true. + * + * @param v The {@link View} that is the trigger for expansion + */ + @Override + @UiThread + public void onClick(View v) { + if (mExpanded) { + collapseView(); + } else { + expandView(); + } + } + + /** + * Used to determine whether a click in the entire parent {@link View} + * should trigger row expansion. + *

+ * If you return false, you can call {@link #expandView()} to trigger an + * expansion in response to a another event or {@link #collapseView()} to + * trigger a collapse. + * + * @return true to set an {@link android.view.View.OnClickListener} on the item view + */ + @UiThread + public boolean shouldItemViewClickToggleExpansion() { + return true; + } + + /** + * Triggers expansion of the parent. + */ + @UiThread + protected void expandView() { + setExpanded(true); + onExpansionToggled(false); + + if (mParentViewHolderExpandCollapseListener != null) { + mParentViewHolderExpandCollapseListener.onParentExpanded(getAdapterPosition()); + } + } + + /** + * Triggers collapse of the parent. + */ + @UiThread + protected void collapseView() { + setExpanded(false); + onExpansionToggled(true); + + if (mParentViewHolderExpandCollapseListener != null) { + mParentViewHolderExpandCollapseListener.onParentCollapsed(getAdapterPosition()); + } + } +} diff --git a/modules/expandable-recyclerview/src/main/java/com/bignerdranch/expandablerecyclerview/model/ExpandableWrapper.java b/modules/expandable-recyclerview/src/main/java/com/bignerdranch/expandablerecyclerview/model/ExpandableWrapper.java new file mode 100644 index 00000000..f567a1b4 --- /dev/null +++ b/modules/expandable-recyclerview/src/main/java/com/bignerdranch/expandablerecyclerview/model/ExpandableWrapper.java @@ -0,0 +1,125 @@ +package com.bignerdranch.expandablerecyclerview.model; + +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.List; + +/** + * Wrapper used to link metadata with a list item. + * + * @param

Parent list item + * @param Child list item + */ +public class ExpandableWrapper

, C> { + + private P mParent; + private C mChild; + private boolean mWrappedParent; + private boolean mExpanded; + + private List> mWrappedChildList; + + /** + * Constructor to wrap a parent object of type {@link P}. + * + * @param parent The parent object to wrap + */ + public ExpandableWrapper(@NonNull P parent) { + mParent = parent; + mWrappedParent = true; + mExpanded = false; + + mWrappedChildList = generateChildItemList(parent); + } + + /** + * Constructor to wrap a child object of type {@link C} + * + * @param child The child object to wrap + */ + public ExpandableWrapper(@NonNull C child) { + mChild = child; + mWrappedParent = false; + mExpanded = false; + } + + public P getParent() { + return mParent; + } + + public void setParent(@NonNull P parent) { + mParent = parent; + mWrappedChildList = generateChildItemList(parent); + } + + public C getChild() { + return mChild; + } + + public boolean isExpanded() { + return mExpanded; + } + + public void setExpanded(boolean expanded) { + mExpanded = expanded; + } + + public boolean isParent() { + return mWrappedParent; + } + + /** + * @return The initial expanded state of a parent + * @throws IllegalStateException If a parent isn't being wrapped + */ + public boolean isParentInitiallyExpanded() { + if (!mWrappedParent) { + throw new IllegalStateException("Parent not wrapped"); + } + + return mParent.isInitiallyExpanded(); + } + + /** + * @return The list of children of a parent + * @throws IllegalStateException If a parent isn't being wrapped + */ + public List> getWrappedChildList() { + if (!mWrappedParent) { + throw new IllegalStateException("Parent not wrapped"); + } + + return mWrappedChildList; + } + + private List> generateChildItemList(P parentListItem) { + List> childItemList = new ArrayList<>(); + + for (C child : parentListItem.getChildList()) { + childItemList.add(new ExpandableWrapper(child)); + } + + return childItemList; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final ExpandableWrapper that = (ExpandableWrapper) o; + + if (mParent != null ? !mParent.equals(that.mParent) : that.mParent != null) + return false; + return mChild != null ? mChild.equals(that.mChild) : that.mChild == null; + + } + + @Override + public int hashCode() { + int result = mParent != null ? mParent.hashCode() : 0; + result = 31 * result + (mChild != null ? mChild.hashCode() : 0); + return result; + } +} diff --git a/modules/expandable-recyclerview/src/main/java/com/bignerdranch/expandablerecyclerview/model/Parent.java b/modules/expandable-recyclerview/src/main/java/com/bignerdranch/expandablerecyclerview/model/Parent.java new file mode 100644 index 00000000..d4bc551a --- /dev/null +++ b/modules/expandable-recyclerview/src/main/java/com/bignerdranch/expandablerecyclerview/model/Parent.java @@ -0,0 +1,26 @@ +package com.bignerdranch.expandablerecyclerview.model; + +import java.util.List; + +/** + * Interface for implementing required methods in a parent. + */ +public interface Parent { + + /** + * Getter for the list of this parent's child items. + *

+ * If list is empty, the parent has no children. + * + * @return A {@link List} of the children of this {@link Parent} + */ + List getChildList(); + + /** + * Getter used to determine if this {@link Parent}'s + * {@link android.view.View} should show up initially as expanded. + * + * @return true if expanded, false if not + */ + boolean isInitiallyExpanded(); +} \ No newline at end of file diff --git a/modules/expandable-recyclerview/src/main/java/com/bignerdranch/expandablerecyclerview/model/SimpleParent.java b/modules/expandable-recyclerview/src/main/java/com/bignerdranch/expandablerecyclerview/model/SimpleParent.java new file mode 100644 index 00000000..14f497bb --- /dev/null +++ b/modules/expandable-recyclerview/src/main/java/com/bignerdranch/expandablerecyclerview/model/SimpleParent.java @@ -0,0 +1,32 @@ +package com.bignerdranch.expandablerecyclerview.model; + +import java.util.List; + +/** + * Simple implementation of the ParentListItem interface, + * by default all items are not initially expanded. + * + * @param Type of the Child Items held by the Parent. + */ +public class SimpleParent implements Parent { + + private List mChildList; + + protected SimpleParent(List childItemList) { + mChildList = childItemList; + } + + @Override + public boolean isInitiallyExpanded() { + return false; + } + + @Override + public List getChildList() { + return mChildList; + } + + public void setChildList(List childList) { + mChildList = childList; + } +} diff --git a/modules/material-date-time-picker/build.gradle.kts b/modules/material-date-time-picker/build.gradle.kts index a15a5693..7b243a73 100644 --- a/modules/material-date-time-picker/build.gradle.kts +++ b/modules/material-date-time-picker/build.gradle.kts @@ -6,7 +6,7 @@ plugins { android { namespace = "com.wdullaer.materialdatetimepicker" - version = "4.2.3 (54)" + version = "4.2.3 (54) Mod" compileSdk = versions.sdkVersionCompile defaultConfig { diff --git a/modules/material-dialogs/build.gradle.kts b/modules/material-dialogs/build.gradle.kts index 8a64947a..c854df17 100644 --- a/modules/material-dialogs/build.gradle.kts +++ b/modules/material-dialogs/build.gradle.kts @@ -6,7 +6,7 @@ plugins { android { namespace = "com.afollestad.materialdialogs" - version = "0.9.6.0 (179)" + version = "0.9.6.0 (179) Mod" compileSdk = versions.sdkVersionCompile @@ -27,8 +27,8 @@ repositories { } dependencies { - implementation(libs.appcompat.v7) - implementation(libs.support.annotations) - implementation(libs.recyclerview.v7) + implementation(libs.appcompat) + implementation(libs.annotation) + implementation(libs.recyclerview) implementation(libs.materialprogressbar) } diff --git a/modules/recyclerview-flexibledivider/build.gradle b/modules/recyclerview-flexibledivider/build.gradle new file mode 100644 index 00000000..f4a57626 --- /dev/null +++ b/modules/recyclerview-flexibledivider/build.gradle @@ -0,0 +1,20 @@ +plugins { + id("org.autojs.build.versions") + id("org.autojs.build.jvm-convention") + id("com.android.library") +} + +android { + namespace "com.yqritc.recyclerviewflexibledivider" + compileSdkVersion versions.sdkVersionCompile + + defaultConfig { + minSdkVersion versions.sdkVersionMin + targetSdkVersion versions.sdkVersionTarget + versionCode 15 + versionName "1.4.0 Mod" + } +} +dependencies { + implementation libs.recyclerview +} diff --git a/modules/recyclerview-flexibledivider/src/main/AndroidManifest.xml b/modules/recyclerview-flexibledivider/src/main/AndroidManifest.xml new file mode 100644 index 00000000..f8776a67 --- /dev/null +++ b/modules/recyclerview-flexibledivider/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/modules/recyclerview-flexibledivider/src/main/java/com/yqritc/recyclerviewflexibledivider/FlexibleDividerDecoration.java b/modules/recyclerview-flexibledivider/src/main/java/com/yqritc/recyclerviewflexibledivider/FlexibleDividerDecoration.java new file mode 100644 index 00000000..ed570e91 --- /dev/null +++ b/modules/recyclerview-flexibledivider/src/main/java/com/yqritc/recyclerviewflexibledivider/FlexibleDividerDecoration.java @@ -0,0 +1,439 @@ +package com.yqritc.recyclerviewflexibledivider; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.view.View; +import androidx.annotation.ColorRes; +import androidx.annotation.DimenRes; +import androidx.annotation.DrawableRes; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +/** + * Created by yqritc on 2015/01/08. + */ +public abstract class FlexibleDividerDecoration extends RecyclerView.ItemDecoration { + + private static final int DEFAULT_SIZE = 2; + private static final int[] ATTRS = new int[]{ + android.R.attr.listDivider + }; + + protected enum DividerType { + DRAWABLE, PAINT, COLOR + } + + protected DividerType mDividerType = DividerType.DRAWABLE; + protected VisibilityProvider mVisibilityProvider; + protected PaintProvider mPaintProvider; + protected ColorProvider mColorProvider; + protected DrawableProvider mDrawableProvider; + protected SizeProvider mSizeProvider; + protected boolean mShowLastDivider; + protected boolean mPositionInsideItem; + private Paint mPaint; + + protected FlexibleDividerDecoration(Builder builder) { + if (builder.mPaintProvider != null) { + mDividerType = DividerType.PAINT; + mPaintProvider = builder.mPaintProvider; + } else if (builder.mColorProvider != null) { + mDividerType = DividerType.COLOR; + mColorProvider = builder.mColorProvider; + mPaint = new Paint(); + setSizeProvider(builder); + } else { + mDividerType = DividerType.DRAWABLE; + if (builder.mDrawableProvider == null) { + TypedArray a = builder.mContext.obtainStyledAttributes(ATTRS); + final Drawable divider = a.getDrawable(0); + a.recycle(); + mDrawableProvider = new DrawableProvider() { + @Override + public Drawable drawableProvider(int position, RecyclerView parent) { + return divider; + } + }; + } else { + mDrawableProvider = builder.mDrawableProvider; + } + mSizeProvider = builder.mSizeProvider; + } + + mVisibilityProvider = builder.mVisibilityProvider; + mShowLastDivider = builder.mShowLastDivider; + mPositionInsideItem = builder.mPositionInsideItem; + } + + private void setSizeProvider(Builder builder) { + mSizeProvider = builder.mSizeProvider; + if (mSizeProvider == null) { + mSizeProvider = new SizeProvider() { + @Override + public int dividerSize(int position, RecyclerView parent) { + return DEFAULT_SIZE; + } + }; + } + } + + @Override + public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { + RecyclerView.Adapter adapter = parent.getAdapter(); + if (adapter == null) { + return; + } + + int itemCount = adapter.getItemCount(); + int lastDividerOffset = getLastDividerOffset(parent); + int validChildCount = parent.getChildCount(); + int lastChildPosition = -1; + for (int i = 0; i < validChildCount; i++) { + View child = parent.getChildAt(i); + int childPosition = parent.getChildAdapterPosition(child); + + if (childPosition < lastChildPosition) { + // Avoid remaining divider when animation starts + continue; + } + lastChildPosition = childPosition; + + if (!mShowLastDivider && childPosition >= itemCount - lastDividerOffset) { + // Don't draw divider for last line if mShowLastDivider = false + continue; + } + + if (wasDividerAlreadyDrawn(childPosition, parent)) { + // No need to draw divider again as it was drawn already by previous column + continue; + } + + int groupIndex = getGroupIndex(childPosition, parent); + if (mVisibilityProvider.shouldHideDivider(groupIndex, parent)) { + continue; + } + + Rect bounds = getDividerBound(groupIndex, parent, child); + switch (mDividerType) { + case DRAWABLE: + Drawable drawable = mDrawableProvider.drawableProvider(groupIndex, parent); + drawable.setBounds(bounds); + drawable.draw(c); + break; + case PAINT: + mPaint = mPaintProvider.dividerPaint(groupIndex, parent); + c.drawLine(bounds.left, bounds.top, bounds.right, bounds.bottom, mPaint); + break; + case COLOR: + mPaint.setColor(mColorProvider.dividerColor(groupIndex, parent)); + mPaint.setStrokeWidth(mSizeProvider.dividerSize(groupIndex, parent)); + c.drawLine(bounds.left, bounds.top, bounds.right, bounds.bottom, mPaint); + break; + } + } + } + + @Override + public void getItemOffsets(Rect rect, View v, RecyclerView parent, RecyclerView.State state) { + int position = parent.getChildAdapterPosition(v); + int itemCount = parent.getAdapter().getItemCount(); + int lastDividerOffset = getLastDividerOffset(parent); + if (!mShowLastDivider && position >= itemCount - lastDividerOffset) { + // Don't set item offset for last line if mShowLastDivider = false + return; + } + + int groupIndex = getGroupIndex(position, parent); + if (mVisibilityProvider.shouldHideDivider(groupIndex, parent)) { + return; + } + + setItemOffsets(rect, groupIndex, parent); + } + + /** + * Check if recyclerview is reverse layout + * + * @param parent RecyclerView + * @return true if recyclerview is reverse layout + */ + protected boolean isReverseLayout(RecyclerView parent) { + RecyclerView.LayoutManager layoutManager = parent.getLayoutManager(); + if (layoutManager instanceof LinearLayoutManager) { + return ((LinearLayoutManager) layoutManager).getReverseLayout(); + } else { + return false; + } + } + + /** + * In the case mShowLastDivider = false, + * Returns offset for how many views we don't have to draw a divider for, + * for LinearLayoutManager it is as simple as not drawing the last child divider, + * but for a GridLayoutManager it needs to take the span count for the last items into account + * until we use the span count configured for the grid. + * + * @param parent RecyclerView + * @return offset for how many views we don't have to draw a divider or 1 if its a + * LinearLayoutManager + */ + private int getLastDividerOffset(RecyclerView parent) { + if (parent.getLayoutManager() instanceof GridLayoutManager) { + GridLayoutManager layoutManager = (GridLayoutManager) parent.getLayoutManager(); + GridLayoutManager.SpanSizeLookup spanSizeLookup = layoutManager.getSpanSizeLookup(); + int spanCount = layoutManager.getSpanCount(); + int itemCount = parent.getAdapter().getItemCount(); + for (int i = itemCount - 1; i >= 0; i--) { + if (spanSizeLookup.getSpanIndex(i, spanCount) == 0) { + return itemCount - i; + } + } + } + + return 1; + } + + /** + * Determines whether divider was already drawn for the row the item is in, + * effectively only makes sense for a grid + * + * @param position current view position to draw divider + * @param parent RecyclerView + * @return true if the divider can be skipped as it is in the same row as the previous one. + */ + private boolean wasDividerAlreadyDrawn(int position, RecyclerView parent) { + if (parent.getLayoutManager() instanceof GridLayoutManager) { + GridLayoutManager layoutManager = (GridLayoutManager) parent.getLayoutManager(); + GridLayoutManager.SpanSizeLookup spanSizeLookup = layoutManager.getSpanSizeLookup(); + int spanCount = layoutManager.getSpanCount(); + return spanSizeLookup.getSpanIndex(position, spanCount) > 0; + } + + return false; + } + + /** + * Returns a group index for GridLayoutManager. + * for LinearLayoutManager, always returns position. + * + * @param position current view position to draw divider + * @param parent RecyclerView + * @return group index of items + */ + private int getGroupIndex(int position, RecyclerView parent) { + if (parent.getLayoutManager() instanceof GridLayoutManager) { + GridLayoutManager layoutManager = (GridLayoutManager) parent.getLayoutManager(); + GridLayoutManager.SpanSizeLookup spanSizeLookup = layoutManager.getSpanSizeLookup(); + int spanCount = layoutManager.getSpanCount(); + return spanSizeLookup.getSpanGroupIndex(position, spanCount); + } + + return position; + } + + protected abstract Rect getDividerBound(int position, RecyclerView parent, View child); + + protected abstract void setItemOffsets(Rect outRect, int position, RecyclerView parent); + + /** + * Interface for controlling divider visibility + */ + public interface VisibilityProvider { + + /** + * Returns true if divider should be hidden. + * + * @param position Divider position (or group index for GridLayoutManager) + * @param parent RecyclerView + * @return True if the divider at position should be hidden + */ + boolean shouldHideDivider(int position, RecyclerView parent); + } + + /** + * Interface for controlling paint instance for divider drawing + */ + public interface PaintProvider { + + /** + * Returns {@link android.graphics.Paint} for divider + * + * @param position Divider position (or group index for GridLayoutManager) + * @param parent RecyclerView + * @return Paint instance + */ + Paint dividerPaint(int position, RecyclerView parent); + } + + /** + * Interface for controlling divider color + */ + public interface ColorProvider { + + /** + * Returns {@link android.graphics.Color} value of divider + * + * @param position Divider position (or group index for GridLayoutManager) + * @param parent RecyclerView + * @return Color value + */ + int dividerColor(int position, RecyclerView parent); + } + + /** + * Interface for controlling drawable object for divider drawing + */ + public interface DrawableProvider { + + /** + * Returns drawable instance for divider + * + * @param position Divider position (or group index for GridLayoutManager) + * @param parent RecyclerView + * @return Drawable instance + */ + Drawable drawableProvider(int position, RecyclerView parent); + } + + /** + * Interface for controlling divider size + */ + public interface SizeProvider { + + /** + * Returns size value of divider. + * Height for horizontal divider, width for vertical divider + * + * @param position Divider position (or group index for GridLayoutManager) + * @param parent RecyclerView + * @return Size of divider + */ + int dividerSize(int position, RecyclerView parent); + } + + public static class Builder { + + private Context mContext; + protected Resources mResources; + private PaintProvider mPaintProvider; + private ColorProvider mColorProvider; + private DrawableProvider mDrawableProvider; + private SizeProvider mSizeProvider; + private VisibilityProvider mVisibilityProvider = new VisibilityProvider() { + @Override + public boolean shouldHideDivider(int position, RecyclerView parent) { + return false; + } + }; + private boolean mShowLastDivider = false; + private boolean mPositionInsideItem = false; + + public Builder(Context context) { + mContext = context; + mResources = context.getResources(); + } + + public T paint(final Paint paint) { + return paintProvider(new PaintProvider() { + @Override + public Paint dividerPaint(int position, RecyclerView parent) { + return paint; + } + }); + } + + public T paintProvider(PaintProvider provider) { + mPaintProvider = provider; + return (T) this; + } + + public T color(final int color) { + return colorProvider(new ColorProvider() { + @Override + public int dividerColor(int position, RecyclerView parent) { + return color; + } + }); + } + + public T colorResId(@ColorRes int colorId) { + return color(ContextCompat.getColor(mContext, colorId)); + } + + public T colorProvider(ColorProvider provider) { + mColorProvider = provider; + return (T) this; + } + + public T drawable(@DrawableRes int id) { + return drawable(ContextCompat.getDrawable(mContext, id)); + } + + public T drawable(final Drawable drawable) { + return drawableProvider(new DrawableProvider() { + @Override + public Drawable drawableProvider(int position, RecyclerView parent) { + return drawable; + } + }); + } + + public T drawableProvider(DrawableProvider provider) { + mDrawableProvider = provider; + return (T) this; + } + + public T size(final int size) { + return sizeProvider(new SizeProvider() { + @Override + public int dividerSize(int position, RecyclerView parent) { + return size; + } + }); + } + + public T sizeResId(@DimenRes int sizeId) { + return size(mResources.getDimensionPixelSize(sizeId)); + } + + public T sizeProvider(SizeProvider provider) { + mSizeProvider = provider; + return (T) this; + } + + public T visibilityProvider(VisibilityProvider provider) { + mVisibilityProvider = provider; + return (T) this; + } + + public T showLastDivider() { + mShowLastDivider = true; + return (T) this; + } + + public T positionInsideItem(boolean positionInsideItem) { + mPositionInsideItem = positionInsideItem; + return (T) this; + } + + protected void checkBuilderParams() { + if (mPaintProvider != null) { + if (mColorProvider != null) { + throw new IllegalArgumentException( + "Use setColor method of Paint class to specify line color. Do not provider ColorProvider if you set PaintProvider."); + } + if (mSizeProvider != null) { + throw new IllegalArgumentException( + "Use setStrokeWidth method of Paint class to specify line size. Do not provider SizeProvider if you set PaintProvider."); + } + } + } + } +} \ No newline at end of file diff --git a/modules/recyclerview-flexibledivider/src/main/java/com/yqritc/recyclerviewflexibledivider/HorizontalDividerItemDecoration.java b/modules/recyclerview-flexibledivider/src/main/java/com/yqritc/recyclerviewflexibledivider/HorizontalDividerItemDecoration.java new file mode 100644 index 00000000..71bed83e --- /dev/null +++ b/modules/recyclerview-flexibledivider/src/main/java/com/yqritc/recyclerviewflexibledivider/HorizontalDividerItemDecoration.java @@ -0,0 +1,174 @@ +package com.yqritc.recyclerviewflexibledivider; + +import android.content.Context; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.view.View; +import androidx.annotation.DimenRes; +import androidx.core.view.ViewCompat; +import androidx.recyclerview.widget.RecyclerView; + +/** + * Created by yqritc on 2015/01/15. + */ +public class HorizontalDividerItemDecoration extends FlexibleDividerDecoration { + + private MarginProvider mMarginProvider; + + protected HorizontalDividerItemDecoration(Builder builder) { + super(builder); + mMarginProvider = builder.mMarginProvider; + } + + @Override + protected Rect getDividerBound(int position, RecyclerView parent, View child) { + Rect bounds = new Rect(0, 0, 0, 0); + int transitionX = (int) ViewCompat.getTranslationX(child); + int transitionY = (int) ViewCompat.getTranslationY(child); + RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams(); + bounds.left = parent.getPaddingLeft() + + mMarginProvider.dividerLeftMargin(position, parent) + transitionX; + bounds.right = parent.getWidth() - parent.getPaddingRight() - + mMarginProvider.dividerRightMargin(position, parent) + transitionX; + + int dividerSize = getDividerSize(position, parent); + boolean isReverseLayout = isReverseLayout(parent); + if (mDividerType == DividerType.DRAWABLE) { + // set top and bottom position of divider + if (isReverseLayout) { + bounds.bottom = child.getTop() - params.topMargin + transitionY; + bounds.top = bounds.bottom - dividerSize; + } else { + bounds.top = child.getBottom() + params.bottomMargin + transitionY; + bounds.bottom = bounds.top + dividerSize; + } + } else { + // set center point of divider + int halfSize = dividerSize / 2; + if (isReverseLayout) { + bounds.top = child.getTop() - params.topMargin - halfSize + transitionY; + } else { + bounds.top = child.getBottom() + params.bottomMargin + halfSize + transitionY; + } + bounds.bottom = bounds.top; + } + + if (mPositionInsideItem) { + if (isReverseLayout) { + bounds.top += dividerSize; + bounds.bottom += dividerSize; + } else { + bounds.top -= dividerSize; + bounds.bottom -= dividerSize; + } + } + + return bounds; + } + + @Override + protected void setItemOffsets(Rect outRect, int position, RecyclerView parent) { + if (mPositionInsideItem) { + outRect.set(0, 0, 0, 0); + return; + } + + if (isReverseLayout(parent)) { + outRect.set(0, getDividerSize(position, parent), 0, 0); + } else { + outRect.set(0, 0, 0, getDividerSize(position, parent)); + } + } + + private int getDividerSize(int position, RecyclerView parent) { + if (mPaintProvider != null) { + return (int) mPaintProvider.dividerPaint(position, parent).getStrokeWidth(); + } else if (mSizeProvider != null) { + return mSizeProvider.dividerSize(position, parent); + } else if (mDrawableProvider != null) { + Drawable drawable = mDrawableProvider.drawableProvider(position, parent); + return drawable.getIntrinsicHeight(); + } + throw new RuntimeException("failed to get size"); + } + + /** + * Interface for controlling divider margin + */ + public interface MarginProvider { + + /** + * Returns left margin of divider. + * + * @param position Divider position (or group index for GridLayoutManager) + * @param parent RecyclerView + * @return left margin + */ + int dividerLeftMargin(int position, RecyclerView parent); + + /** + * Returns right margin of divider. + * + * @param position Divider position (or group index for GridLayoutManager) + * @param parent RecyclerView + * @return right margin + */ + int dividerRightMargin(int position, RecyclerView parent); + } + + public static class Builder extends FlexibleDividerDecoration.Builder { + + private MarginProvider mMarginProvider = new MarginProvider() { + @Override + public int dividerLeftMargin(int position, RecyclerView parent) { + return 0; + } + + @Override + public int dividerRightMargin(int position, RecyclerView parent) { + return 0; + } + }; + + public Builder(Context context) { + super(context); + } + + public Builder margin(final int leftMargin, final int rightMargin) { + return marginProvider(new MarginProvider() { + @Override + public int dividerLeftMargin(int position, RecyclerView parent) { + return leftMargin; + } + + @Override + public int dividerRightMargin(int position, RecyclerView parent) { + return rightMargin; + } + }); + } + + public Builder margin(int horizontalMargin) { + return margin(horizontalMargin, horizontalMargin); + } + + public Builder marginResId(@DimenRes int leftMarginId, @DimenRes int rightMarginId) { + return margin(mResources.getDimensionPixelSize(leftMarginId), + mResources.getDimensionPixelSize(rightMarginId)); + } + + public Builder marginResId(@DimenRes int horizontalMarginId) { + return marginResId(horizontalMarginId, horizontalMarginId); + } + + public Builder marginProvider(MarginProvider provider) { + mMarginProvider = provider; + return this; + } + + public HorizontalDividerItemDecoration build() { + checkBuilderParams(); + return new HorizontalDividerItemDecoration(this); + } + } +} \ No newline at end of file diff --git a/modules/recyclerview-flexibledivider/src/main/java/com/yqritc/recyclerviewflexibledivider/VerticalDividerItemDecoration.java b/modules/recyclerview-flexibledivider/src/main/java/com/yqritc/recyclerviewflexibledivider/VerticalDividerItemDecoration.java new file mode 100644 index 00000000..d2b0f7c3 --- /dev/null +++ b/modules/recyclerview-flexibledivider/src/main/java/com/yqritc/recyclerviewflexibledivider/VerticalDividerItemDecoration.java @@ -0,0 +1,174 @@ +package com.yqritc.recyclerviewflexibledivider; + +import android.content.Context; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.view.View; +import androidx.annotation.DimenRes; +import androidx.core.view.ViewCompat; +import androidx.recyclerview.widget.RecyclerView; + +/** + * Created by yqritc on 2015/01/15. + */ +public class VerticalDividerItemDecoration extends FlexibleDividerDecoration { + + private MarginProvider mMarginProvider; + + protected VerticalDividerItemDecoration(Builder builder) { + super(builder); + mMarginProvider = builder.mMarginProvider; + } + + @Override + protected Rect getDividerBound(int position, RecyclerView parent, View child) { + Rect bounds = new Rect(0, 0, 0, 0); + int transitionX = (int) ViewCompat.getTranslationX(child); + int transitionY = (int) ViewCompat.getTranslationY(child); + RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams(); + bounds.top = parent.getPaddingTop() + + mMarginProvider.dividerTopMargin(position, parent) + transitionY; + bounds.bottom = parent.getHeight() - parent.getPaddingBottom() - + mMarginProvider.dividerBottomMargin(position, parent) + transitionY; + + int dividerSize = getDividerSize(position, parent); + boolean isReverseLayout = isReverseLayout(parent); + if (mDividerType == DividerType.DRAWABLE) { + // set left and right position of divider + if (isReverseLayout) { + bounds.right = child.getLeft() - params.leftMargin + transitionX; + bounds.left = bounds.right - dividerSize; + } else { + bounds.left = child.getRight() + params.rightMargin + transitionX; + bounds.right = bounds.left + dividerSize; + } + } else { + // set center point of divider + int halfSize = dividerSize / 2; + if (isReverseLayout) { + bounds.left = child.getLeft() - params.leftMargin - halfSize + transitionX; + } else { + bounds.left = child.getRight() + params.rightMargin + halfSize + transitionX; + } + bounds.right = bounds.left; + } + + if (mPositionInsideItem) { + if (isReverseLayout) { + bounds.left += dividerSize; + bounds.right += dividerSize; + } else { + bounds.left -= dividerSize; + bounds.right -= dividerSize; + } + } + + return bounds; + } + + @Override + protected void setItemOffsets(Rect outRect, int position, RecyclerView parent) { + if (mPositionInsideItem) { + outRect.set(0, 0, 0, 0); + return; + } + + if (isReverseLayout(parent)) { + outRect.set(getDividerSize(position, parent), 0, 0, 0); + } else { + outRect.set(0, 0, getDividerSize(position, parent), 0); + } + } + + private int getDividerSize(int position, RecyclerView parent) { + if (mPaintProvider != null) { + return (int) mPaintProvider.dividerPaint(position, parent).getStrokeWidth(); + } else if (mSizeProvider != null) { + return mSizeProvider.dividerSize(position, parent); + } else if (mDrawableProvider != null) { + Drawable drawable = mDrawableProvider.drawableProvider(position, parent); + return drawable.getIntrinsicWidth(); + } + throw new RuntimeException("failed to get size"); + } + + /** + * Interface for controlling divider margin + */ + public interface MarginProvider { + + /** + * Returns top margin of divider. + * + * @param position Divider position (or group index for GridLayoutManager) + * @param parent RecyclerView + * @return top margin + */ + int dividerTopMargin(int position, RecyclerView parent); + + /** + * Returns bottom margin of divider. + * + * @param position Divider position (or group index for GridLayoutManager) + * @param parent RecyclerView + * @return bottom margin + */ + int dividerBottomMargin(int position, RecyclerView parent); + } + + public static class Builder extends FlexibleDividerDecoration.Builder { + + private MarginProvider mMarginProvider = new MarginProvider() { + @Override + public int dividerTopMargin(int position, RecyclerView parent) { + return 0; + } + + @Override + public int dividerBottomMargin(int position, RecyclerView parent) { + return 0; + } + }; + + public Builder(Context context) { + super(context); + } + + public Builder margin(final int topMargin, final int bottomMargin) { + return marginProvider(new MarginProvider() { + @Override + public int dividerTopMargin(int position, RecyclerView parent) { + return topMargin; + } + + @Override + public int dividerBottomMargin(int position, RecyclerView parent) { + return bottomMargin; + } + }); + } + + public Builder margin(int verticalMargin) { + return margin(verticalMargin, verticalMargin); + } + + public Builder marginResId(@DimenRes int topMarginId, @DimenRes int bottomMarginId) { + return margin(mResources.getDimensionPixelSize(topMarginId), + mResources.getDimensionPixelSize(bottomMarginId)); + } + + public Builder marginResId(@DimenRes int verticalMarginId) { + return marginResId(verticalMarginId, verticalMarginId); + } + + public Builder marginProvider(MarginProvider provider) { + mMarginProvider = provider; + return this; + } + + public VerticalDividerItemDecoration build() { + checkBuilderParams(); + return new VerticalDividerItemDecoration(this); + } + } +} \ No newline at end of file diff --git a/version.properties b/version.properties index 35a85917..a858c910 100644 --- a/version.properties +++ b/version.properties @@ -1,5 +1,5 @@ -#Sat Mar 07 12:33:49 CST 2026 -BUILD_TIME=1772858029128 +#Sat Mar 07 19:14:09 CST 2026 +BUILD_TIME=1772882049270 COMPILE_SDK_VERSION=36 IMAGE_QUANT_CMAKE_VERSION=3.22.1 IMAGE_QUANT_NDK_VERSION=26.1.10909125 @@ -27,6 +27,6 @@ RAPID_OCR_OPENCV_MOBILE_LABEL_VERSION=13 RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3 TARGET_SDK_VERSION=36 TARGET_SDK_VERSION_INRT=29 -VERSION_BUILD=3778 +VERSION_BUILD=3780 VERSION_NAME=6.7.0 Alpha23 VSCODE_EXT_REQUIRED_VERSION=1.0.13