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