From 528f3e59dcb4e1e0989d602de1b2aa089e59c7dd Mon Sep 17 00:00:00 2001 From: SuperMonster003 Date: Sat, 7 Mar 2026 19:35:05 +0800 Subject: [PATCH] =?UTF-8?q?6.7.0=20-=20Alpha23=20-=20=E7=A7=BB=E9=99=A4=20?= =?UTF-8?q?"android.enableJetifier=3Dtrue";=20=E6=A8=A1=E5=9D=97=E5=8C=96?= =?UTF-8?q?=20Expandable=20Layout,=20Expandable=20RecyclerView,=20Recycler?= =?UTF-8?q?view=20Flexible=20Divider?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changelog/lang_ar.json | 4 +- .changelog/lang_en.json | 4 +- .changelog/lang_es.json | 4 +- .changelog/lang_fr.json | 4 +- .changelog/lang_ja.json | 4 +- .changelog/lang_ko.json | 4 +- .changelog/lang_ru.json | 4 +- .changelog/lang_zh-Hans.json | 19 +- .changelog/lang_zh-Hant-HK.json | 4 +- .changelog/lang_zh-Hant-TW.json | 4 +- app/build.gradle.kts | 14 +- .../runtime/api/augment/selector/Selector.kt | 2 +- gradle.properties | 22 +- gradle/libs.versions.toml | 16 +- libs/expandable-layout-1_6_0/build.gradle.kts | 7 - .../expandable-layout-1.6.0.aar | Bin 40689 -> 0 bytes .../build.gradle.kts | 7 - .../recyclerview-flexibledivider-1.4.0.aar | Bin 35185 -> 0 bytes modules/expandable-layout/build.gradle | 22 + modules/expandable-layout/proguard-rules.pro | 17 + .../src/main/AndroidManifest.xml | 1 + .../expandablelayout/ExpandableLayout.java | 115 ++ .../ExpandableLayoutListener.java | 35 + .../ExpandableLayoutListenerAdapter.java | 45 + .../ExpandableLinearLayout.java | 565 +++++++ .../ExpandableRelativeLayout.java | 553 +++++++ .../ExpandableSavedState.java | 54 + .../ExpandableWeightLayout.java | 385 +++++ .../github/aakira/expandablelayout/Utils.java | 65 + .../src/main/res/values/attrs.xml | 26 + modules/expandable-recyclerview/build.gradle | 21 + .../proguard-rules.pro | 17 + .../src/main/AndroidManifest.xml | 1 + .../ChildViewHolder.java | 70 + .../ExpandableRecyclerAdapter.java | 1425 +++++++++++++++++ .../ParentViewHolder.java | 201 +++ .../model/ExpandableWrapper.java | 125 ++ .../expandablerecyclerview/model/Parent.java | 26 + .../model/SimpleParent.java | 32 + .../build.gradle.kts | 2 +- modules/material-dialogs/build.gradle.kts | 8 +- .../recyclerview-flexibledivider/build.gradle | 20 + .../src/main/AndroidManifest.xml | 1 + .../FlexibleDividerDecoration.java | 439 +++++ .../HorizontalDividerItemDecoration.java | 174 ++ .../VerticalDividerItemDecoration.java | 174 ++ version.properties | 6 +- 47 files changed, 4673 insertions(+), 75 deletions(-) delete mode 100644 libs/expandable-layout-1_6_0/build.gradle.kts delete mode 100644 libs/expandable-layout-1_6_0/expandable-layout-1.6.0.aar delete mode 100644 libs/recyclerview-flexibledivider-1_4_0/build.gradle.kts delete mode 100644 libs/recyclerview-flexibledivider-1_4_0/recyclerview-flexibledivider-1.4.0.aar create mode 100644 modules/expandable-layout/build.gradle create mode 100644 modules/expandable-layout/proguard-rules.pro create mode 100644 modules/expandable-layout/src/main/AndroidManifest.xml create mode 100644 modules/expandable-layout/src/main/java/com/github/aakira/expandablelayout/ExpandableLayout.java create mode 100644 modules/expandable-layout/src/main/java/com/github/aakira/expandablelayout/ExpandableLayoutListener.java create mode 100644 modules/expandable-layout/src/main/java/com/github/aakira/expandablelayout/ExpandableLayoutListenerAdapter.java create mode 100644 modules/expandable-layout/src/main/java/com/github/aakira/expandablelayout/ExpandableLinearLayout.java create mode 100644 modules/expandable-layout/src/main/java/com/github/aakira/expandablelayout/ExpandableRelativeLayout.java create mode 100644 modules/expandable-layout/src/main/java/com/github/aakira/expandablelayout/ExpandableSavedState.java create mode 100644 modules/expandable-layout/src/main/java/com/github/aakira/expandablelayout/ExpandableWeightLayout.java create mode 100644 modules/expandable-layout/src/main/java/com/github/aakira/expandablelayout/Utils.java create mode 100644 modules/expandable-layout/src/main/res/values/attrs.xml create mode 100644 modules/expandable-recyclerview/build.gradle create mode 100644 modules/expandable-recyclerview/proguard-rules.pro create mode 100644 modules/expandable-recyclerview/src/main/AndroidManifest.xml create mode 100644 modules/expandable-recyclerview/src/main/java/com/bignerdranch/expandablerecyclerview/ChildViewHolder.java create mode 100644 modules/expandable-recyclerview/src/main/java/com/bignerdranch/expandablerecyclerview/ExpandableRecyclerAdapter.java create mode 100644 modules/expandable-recyclerview/src/main/java/com/bignerdranch/expandablerecyclerview/ParentViewHolder.java create mode 100644 modules/expandable-recyclerview/src/main/java/com/bignerdranch/expandablerecyclerview/model/ExpandableWrapper.java create mode 100644 modules/expandable-recyclerview/src/main/java/com/bignerdranch/expandablerecyclerview/model/Parent.java create mode 100644 modules/expandable-recyclerview/src/main/java/com/bignerdranch/expandablerecyclerview/model/SimpleParent.java create mode 100644 modules/recyclerview-flexibledivider/build.gradle create mode 100644 modules/recyclerview-flexibledivider/src/main/AndroidManifest.xml create mode 100644 modules/recyclerview-flexibledivider/src/main/java/com/yqritc/recyclerviewflexibledivider/FlexibleDividerDecoration.java create mode 100644 modules/recyclerview-flexibledivider/src/main/java/com/yqritc/recyclerviewflexibledivider/HorizontalDividerItemDecoration.java create mode 100644 modules/recyclerview-flexibledivider/src/main/java/com/yqritc/recyclerviewflexibledivider/VerticalDividerItemDecoration.java 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 8bcf51e003859c74720b054fbed4196ac9efa1d3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40689 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
  • ^_XE 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℘ucg+hBwwrgI2M;59329 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 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