From 355cb73e441c877d33834438b4a3bd5b8c864978 Mon Sep 17 00:00:00 2001 From: YURY TILMAN Date: Mon, 20 Oct 2025 23:03:38 +0300 Subject: [PATCH 01/17] lips upgrade. migrate from groovy to kotlin --- app/build.gradle | 44 ------------------- app/build.gradle.kts | 56 ++++++++++++++++++++++++ build.gradle | 10 ----- build.gradle.kts | 9 ++++ gradle/libs.versions.toml | 28 ++++++++++++ gradle/wrapper/gradle-wrapper.properties | 2 +- settings.gradle => settings.gradle.kts | 5 ++- 7 files changed, 97 insertions(+), 57 deletions(-) delete mode 100644 app/build.gradle create mode 100644 app/build.gradle.kts delete mode 100644 build.gradle create mode 100644 build.gradle.kts create mode 100644 gradle/libs.versions.toml rename settings.gradle => settings.gradle.kts (94%) diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100644 index 54e4eac..0000000 --- a/app/build.gradle +++ /dev/null @@ -1,44 +0,0 @@ -plugins { - id 'com.android.application' - id 'org.jetbrains.kotlin.android' -} - -android { - compileSdk 34 - namespace "otus.gpb.recyclerview" - - defaultConfig { - applicationId "otus.gpb.recyclerview" - minSdk 26 - targetSdk 34 - versionCode 1 - versionName "1.0" - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - kotlinOptions { - jvmTarget = '1.8' - } -} - -dependencies { - - implementation 'androidx.core:core-ktx:1.7.0' - implementation 'androidx.appcompat:appcompat:1.5.1' - implementation 'com.google.android.material:material:1.7.0' - implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.3' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' -} \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..1f07645 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,56 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.jetbrains.kotlin.android) +} + +android { + namespace = "otus.gpb.recyclerview" + compileSdk = 36 + + defaultConfig { + applicationId = "otus.gpb.recyclerview" + minSdk = 26 + targetSdk = 36 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + buildFeatures { + viewBinding = true + } + + kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_1_8 + } + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.material) + implementation(libs.androidx.activity) + implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.recyclerview) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) +} \ No newline at end of file diff --git a/build.gradle b/build.gradle deleted file mode 100644 index bd20018..0000000 --- a/build.gradle +++ /dev/null @@ -1,10 +0,0 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. -plugins { - id 'com.android.application' version '8.7.2' apply false - id 'com.android.library' version '8.7.2' apply false - id 'org.jetbrains.kotlin.android' version '2.0.21' apply false -} - -task clean(type: Delete) { - delete rootProject.buildDir -} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..25e3a4c --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,9 @@ + + +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.jetbrains.kotlin.android) apply false +} +tasks.register("clean", Delete::class) { + delete(rootProject.layout.buildDirectory) +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..de8f7be --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,28 @@ +[versions] +agp = "8.13.0" +kotlin = "2.2.20" +coreKtx = "1.17.0" +junit = "4.13.2" +junitVersion = "1.3.0" +espressoCore = "3.7.0" +appcompat = "1.7.1" +material = "1.13.0" +activity = "1.11.0" +constraintlayout = "2.2.1" +recyclerview = "1.4.0" + +[libraries] +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +junit = { group = "junit", name = "junit", version.ref = "junit" } +androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } +androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +material = { group = "com.google.android.material", name = "material", version.ref = "material" } +androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" } +androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } +androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } + diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8c7b120..a4f35f8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Nov 05 09:40:34 CET 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle b/settings.gradle.kts similarity index 94% rename from settings.gradle rename to settings.gradle.kts index a7dbdf4..a3dfaa3 100644 --- a/settings.gradle +++ b/settings.gradle.kts @@ -1,8 +1,8 @@ pluginManagement { repositories { - gradlePluginPortal() google() mavenCentral() + gradlePluginPortal() } } dependencyResolutionManagement { @@ -12,5 +12,6 @@ dependencyResolutionManagement { mavenCentral() } } + rootProject.name = "RecyclerView" -include ':app' +include(":app") From 438e4213eb7e5a521dd022054523b761e26585c5 Mon Sep 17 00:00:00 2001 From: YURY TILMAN Date: Mon, 20 Oct 2025 23:08:21 +0300 Subject: [PATCH 02/17] target jvm fix --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1f07645..712fa8e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -38,7 +38,7 @@ android { kotlin { compilerOptions { - jvmTarget = JvmTarget.JVM_1_8 + jvmTarget = JvmTarget.JVM_17 } } } From 771a2227bc0f572e02e3319b7e9fabcf30c73e2d Mon Sep 17 00:00:00 2001 From: YURY TILMAN Date: Tue, 21 Oct 2025 22:27:11 +0300 Subject: [PATCH 03/17] add colors from figma --- app/src/main/res/values-night/colors.xml | 143 +++++++++++++++++++++ app/src/main/res/values-night/themes.xml | 64 +++++++--- app/src/main/res/values/colors.xml | 151 +++++++++++++++++++++-- app/src/main/res/values/themes.xml | 64 +++++++--- 4 files changed, 383 insertions(+), 39 deletions(-) create mode 100644 app/src/main/res/values-night/colors.xml diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml new file mode 100644 index 0000000..0d10485 --- /dev/null +++ b/app/src/main/res/values-night/colors.xml @@ -0,0 +1,143 @@ + + #CFBDFE + #36275D + #4D3D75 + #E9DDFF + #CFBDFE + #36275D + #4D3D75 + #E9DDFF + #FFB0C9 + #541D33 + #6F3349 + #FFD9E3 + #FFB3AD + #571E1B + #73332F + #FFDAD7 + #141218 + #E6E0E9 + #141318 + #E6E1E9 + #48454E + #C9C4D0 + #938F99 + #48454E + #000000 + #E6E1E9 + #322F35 + #65558F + #E9DDFF + #201047 + #CFBDFE + #4D3D75 + #E9DDFF + #201047 + #CFBDFE + #4D3D75 + #FFD9E3 + #3A071E + #FFB0C9 + #6F3349 + #141318 + #3A383E + #0F0D13 + #1C1B20 + #211F24 + #2B292F + #36343A + #E3D6FF + #2B1B52 + #9887C5 + #000000 + #E3D6FF + #2B1B52 + #9887C5 + #000000 + #FFD0DD + #471228 + #C57B93 + #000000 + #FFD2CE + #481311 + #CC7B74 + #000000 + #141218 + #E6E0E9 + #141318 + #FFFFFF + #48454E + #E0DAE5 + #B5B0BB + #938F99 + #000000 + #E6E1E9 + #2B292F + #4E3F77 + #E9DDFF + #16033D + #CFBDFE + #3C2D63 + #E9DDFF + #16033D + #CFBDFE + #3C2D63 + #FFD9E3 + #2B0013 + #FFB0C9 + #5B2238 + #141318 + #46434A + #08070B + #1E1D22 + #29272D + #343238 + #3F3D43 + #F5EDFF + #000000 + #CBB9FA + #0F0033 + #F5EDFF + #000000 + #CBB9FA + #0F0033 + #FFEBEF + #000000 + #FEABC5 + #20000D + #FFECEA + #000000 + #FFAEA7 + #220001 + #141218 + #E6E0E9 + #141318 + #FFFFFF + #48454E + #FFFFFF + #F4EEF9 + #C6C1CC + #000000 + #E6E1E9 + #000000 + #4E3F77 + #E9DDFF + #000000 + #CFBDFE + #16033D + #E9DDFF + #000000 + #CFBDFE + #16033D + #FFD9E3 + #000000 + #FFB0C9 + #2B0013 + #141318 + #524F55 + #000000 + #211F24 + #322F35 + #3D3A41 + #48464C + diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 114f376..266cdd2 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -1,16 +1,50 @@ - - - - \ No newline at end of file + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index f8c6127..55b2e62 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,10 +1,143 @@ - - #FFBB86FC - #FF6200EE - #FF3700B3 - #FF03DAC5 - #FF018786 - #FF000000 - #FFFFFFFF - \ No newline at end of file + #65558F + #FFFFFF + #E9DDFF + #4D3D75 + #65558F + #FFFFFF + #E9DDFF + #4D3D75 + #8B4A61 + #FFFFFF + #FFD9E3 + #6F3349 + #904A45 + #FFFFFF + #FFDAD7 + #73332F + #FDF7FF + #1D1B20 + #FDF7FF + #1C1B20 + #E6E0EC + #48454E + #79757F + #C9C4D0 + #000000 + #322F35 + #F5EFF7 + #CFBDFE + #E9DDFF + #201047 + #CFBDFE + #4D3D75 + #E9DDFF + #201047 + #CFBDFE + #4D3D75 + #FFD9E3 + #3A071E + #FFB0C9 + #6F3349 + #DED8E0 + #FDF7FF + #FFFFFF + #F7F2FA + #F2ECF4 + #ECE6EE + #E6E1E9 + #3C2D63 + #FFFFFF + #74649F + #FFFFFF + #3C2D63 + #FFFFFF + #74649F + #FFFFFF + #5B2238 + #FFFFFF + #9C5870 + #FFFFFF + #5E2320 + #FFFFFF + #A15853 + #FFFFFF + #FDF7FF + #1D1B20 + #FDF7FF + #121016 + #E6E0EC + #37353E + #54515A + #6F6B75 + #000000 + #322F35 + #F5EFF7 + #CFBDFE + #74649F + #FFFFFF + #5B4C84 + #FFFFFF + #74649F + #FFFFFF + #5B4C84 + #FFFFFF + #9C5870 + #FFFFFF + #804057 + #FFFFFF + #CAC5CD + #FDF7FF + #FFFFFF + #F7F2FA + #ECE6EE + #E0DBE3 + #D5D0D8 + #312259 + #FFFFFF + #4F4078 + #FFFFFF + #312259 + #FFFFFF + #4F4078 + #FFFFFF + #4F182E + #FFFFFF + #72354C + #FFFFFF + #511917 + #FFFFFF + #763632 + #FFFFFF + #FDF7FF + #1D1B20 + #FDF7FF + #000000 + #E6E0EC + #000000 + #2D2B33 + #4A4851 + #000000 + #322F35 + #FFFFFF + #CFBDFE + #4F4078 + #FFFFFF + #382960 + #FFFFFF + #4F4078 + #FFFFFF + #382960 + #FFFFFF + #72354C + #FFFFFF + #571F35 + #FFFFFF + #BCB7BF + #FDF7FF + #FFFFFF + #F5EFF7 + #E6E1E9 + #D8D3DA + #CAC5CD + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 2187cf1..f694812 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,16 +1,50 @@ - - - - \ No newline at end of file + From 437b3702331c700c50a1c2eb8642576780fff9ab Mon Sep 17 00:00:00 2001 From: YURY TILMAN Date: Tue, 21 Oct 2025 22:57:34 +0300 Subject: [PATCH 04/17] add icons from figma --- app/src/main/res/drawable/archive.xml | 10 +++++++++ app/src/main/res/drawable/check.xml | 13 ++++++++++++ app/src/main/res/drawable/lock.xml | 10 +++++++++ app/src/main/res/drawable/mention.xml | 12 +++++++++++ app/src/main/res/drawable/mute.xml | 13 ++++++++++++ app/src/main/res/drawable/pinned.xml | 17 +++++++++++++++ app/src/main/res/drawable/read.xml | 13 ++++++++++++ app/src/main/res/drawable/reorder.xml | 12 +++++++++++ app/src/main/res/drawable/scame.xml | 26 +++++++++++++++++++++++ app/src/main/res/drawable/verified_ic.xml | 10 +++++++++ 10 files changed, 136 insertions(+) create mode 100644 app/src/main/res/drawable/archive.xml create mode 100644 app/src/main/res/drawable/check.xml create mode 100644 app/src/main/res/drawable/lock.xml create mode 100644 app/src/main/res/drawable/mention.xml create mode 100644 app/src/main/res/drawable/mute.xml create mode 100644 app/src/main/res/drawable/pinned.xml create mode 100644 app/src/main/res/drawable/read.xml create mode 100644 app/src/main/res/drawable/reorder.xml create mode 100644 app/src/main/res/drawable/scame.xml create mode 100644 app/src/main/res/drawable/verified_ic.xml diff --git a/app/src/main/res/drawable/archive.xml b/app/src/main/res/drawable/archive.xml new file mode 100644 index 0000000..a2f1f70 --- /dev/null +++ b/app/src/main/res/drawable/archive.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/check.xml b/app/src/main/res/drawable/check.xml new file mode 100644 index 0000000..68140c3 --- /dev/null +++ b/app/src/main/res/drawable/check.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/lock.xml b/app/src/main/res/drawable/lock.xml new file mode 100644 index 0000000..bb3fdf0 --- /dev/null +++ b/app/src/main/res/drawable/lock.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/mention.xml b/app/src/main/res/drawable/mention.xml new file mode 100644 index 0000000..4e54912 --- /dev/null +++ b/app/src/main/res/drawable/mention.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable/mute.xml b/app/src/main/res/drawable/mute.xml new file mode 100644 index 0000000..e45c74f --- /dev/null +++ b/app/src/main/res/drawable/mute.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/pinned.xml b/app/src/main/res/drawable/pinned.xml new file mode 100644 index 0000000..e270350 --- /dev/null +++ b/app/src/main/res/drawable/pinned.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/drawable/read.xml b/app/src/main/res/drawable/read.xml new file mode 100644 index 0000000..4eb1fdc --- /dev/null +++ b/app/src/main/res/drawable/read.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/reorder.xml b/app/src/main/res/drawable/reorder.xml new file mode 100644 index 0000000..5e3231e --- /dev/null +++ b/app/src/main/res/drawable/reorder.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/scame.xml b/app/src/main/res/drawable/scame.xml new file mode 100644 index 0000000..f6d9704 --- /dev/null +++ b/app/src/main/res/drawable/scame.xml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/verified_ic.xml b/app/src/main/res/drawable/verified_ic.xml new file mode 100644 index 0000000..dac159e --- /dev/null +++ b/app/src/main/res/drawable/verified_ic.xml @@ -0,0 +1,10 @@ + + + From b62485ad496d3d5f098475a1d979270f88f8ce13 Mon Sep 17 00:00:00 2001 From: YURY TILMAN Date: Fri, 31 Oct 2025 21:52:15 +0300 Subject: [PATCH 05/17] material version update --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index de8f7be..9c9c566 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,7 @@ junit = "4.13.2" junitVersion = "1.3.0" espressoCore = "3.7.0" appcompat = "1.7.1" -material = "1.13.0" +material = "1.14.0-alpha05" activity = "1.11.0" constraintlayout = "2.2.1" recyclerview = "1.4.0" @@ -21,7 +21,7 @@ material = { group = "com.google.android.material", name = "material", version.r androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" } androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" } - +#implementation 'com.google.android.material:material:1.14.0-alpha05' [plugins] android-application = { id = "com.android.application", version.ref = "agp" } jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } From 3f592688c48cfd28d9156dc528c09d214b74b4f8 Mon Sep 17 00:00:00 2001 From: YURY TILMAN Date: Mon, 3 Nov 2025 20:58:41 +0300 Subject: [PATCH 06/17] contact items done --- app/src/main/res/drawable/menu.xml | 9 ++ app/src/main/res/layout/activity_main.xml | 43 +++++- app/src/main/res/layout/contact_item.xml | 135 +++++++++++++++++++ app/src/main/res/values-night/colors.xml | 143 -------------------- app/src/main/res/values-night/themes.xml | 64 +++------ app/src/main/res/values/colors.xml | 152 ++-------------------- app/src/main/res/values/themes.xml | 19 ++- 7 files changed, 228 insertions(+), 337 deletions(-) create mode 100644 app/src/main/res/drawable/menu.xml create mode 100644 app/src/main/res/layout/contact_item.xml delete mode 100644 app/src/main/res/values-night/colors.xml diff --git a/app/src/main/res/drawable/menu.xml b/app/src/main/res/drawable/menu.xml new file mode 100644 index 0000000..9543621 --- /dev/null +++ b/app/src/main/res/drawable/menu.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 2d026df..b4e29ec 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,13 +1,52 @@ + + + + + + + + + + + android:layout_height="20dp" + android:background="@color/grey" + app:layout_constraintBottom_toBottomOf="parent" /> \ No newline at end of file diff --git a/app/src/main/res/layout/contact_item.xml b/app/src/main/res/layout/contact_item.xml new file mode 100644 index 0000000..0bca10c --- /dev/null +++ b/app/src/main/res/layout/contact_item.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml deleted file mode 100644 index 0d10485..0000000 --- a/app/src/main/res/values-night/colors.xml +++ /dev/null @@ -1,143 +0,0 @@ - - #CFBDFE - #36275D - #4D3D75 - #E9DDFF - #CFBDFE - #36275D - #4D3D75 - #E9DDFF - #FFB0C9 - #541D33 - #6F3349 - #FFD9E3 - #FFB3AD - #571E1B - #73332F - #FFDAD7 - #141218 - #E6E0E9 - #141318 - #E6E1E9 - #48454E - #C9C4D0 - #938F99 - #48454E - #000000 - #E6E1E9 - #322F35 - #65558F - #E9DDFF - #201047 - #CFBDFE - #4D3D75 - #E9DDFF - #201047 - #CFBDFE - #4D3D75 - #FFD9E3 - #3A071E - #FFB0C9 - #6F3349 - #141318 - #3A383E - #0F0D13 - #1C1B20 - #211F24 - #2B292F - #36343A - #E3D6FF - #2B1B52 - #9887C5 - #000000 - #E3D6FF - #2B1B52 - #9887C5 - #000000 - #FFD0DD - #471228 - #C57B93 - #000000 - #FFD2CE - #481311 - #CC7B74 - #000000 - #141218 - #E6E0E9 - #141318 - #FFFFFF - #48454E - #E0DAE5 - #B5B0BB - #938F99 - #000000 - #E6E1E9 - #2B292F - #4E3F77 - #E9DDFF - #16033D - #CFBDFE - #3C2D63 - #E9DDFF - #16033D - #CFBDFE - #3C2D63 - #FFD9E3 - #2B0013 - #FFB0C9 - #5B2238 - #141318 - #46434A - #08070B - #1E1D22 - #29272D - #343238 - #3F3D43 - #F5EDFF - #000000 - #CBB9FA - #0F0033 - #F5EDFF - #000000 - #CBB9FA - #0F0033 - #FFEBEF - #000000 - #FEABC5 - #20000D - #FFECEA - #000000 - #FFAEA7 - #220001 - #141218 - #E6E0E9 - #141318 - #FFFFFF - #48454E - #FFFFFF - #F4EEF9 - #C6C1CC - #000000 - #E6E1E9 - #000000 - #4E3F77 - #E9DDFF - #000000 - #CFBDFE - #16033D - #E9DDFF - #000000 - #CFBDFE - #16033D - #FFD9E3 - #000000 - #FFB0C9 - #2B0013 - #141318 - #524F55 - #000000 - #211F24 - #322F35 - #3D3A41 - #48464C - diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 266cdd2..114f376 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -1,50 +1,16 @@ - - - + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 55b2e62..e279a08 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,143 +1,13 @@ - #65558F - #FFFFFF - #E9DDFF - #4D3D75 - #65558F - #FFFFFF - #E9DDFF - #4D3D75 - #8B4A61 - #FFFFFF - #FFD9E3 - #6F3349 - #904A45 - #FFFFFF - #FFDAD7 - #73332F - #FDF7FF - #1D1B20 - #FDF7FF - #1C1B20 - #E6E0EC - #48454E - #79757F - #C9C4D0 - #000000 - #322F35 - #F5EFF7 - #CFBDFE - #E9DDFF - #201047 - #CFBDFE - #4D3D75 - #E9DDFF - #201047 - #CFBDFE - #4D3D75 - #FFD9E3 - #3A071E - #FFB0C9 - #6F3349 - #DED8E0 - #FDF7FF - #FFFFFF - #F7F2FA - #F2ECF4 - #ECE6EE - #E6E1E9 - #3C2D63 - #FFFFFF - #74649F - #FFFFFF - #3C2D63 - #FFFFFF - #74649F - #FFFFFF - #5B2238 - #FFFFFF - #9C5870 - #FFFFFF - #5E2320 - #FFFFFF - #A15853 - #FFFFFF - #FDF7FF - #1D1B20 - #FDF7FF - #121016 - #E6E0EC - #37353E - #54515A - #6F6B75 - #000000 - #322F35 - #F5EFF7 - #CFBDFE - #74649F - #FFFFFF - #5B4C84 - #FFFFFF - #74649F - #FFFFFF - #5B4C84 - #FFFFFF - #9C5870 - #FFFFFF - #804057 - #FFFFFF - #CAC5CD - #FDF7FF - #FFFFFF - #F7F2FA - #ECE6EE - #E0DBE3 - #D5D0D8 - #312259 - #FFFFFF - #4F4078 - #FFFFFF - #312259 - #FFFFFF - #4F4078 - #FFFFFF - #4F182E - #FFFFFF - #72354C - #FFFFFF - #511917 - #FFFFFF - #763632 - #FFFFFF - #FDF7FF - #1D1B20 - #FDF7FF - #000000 - #E6E0EC - #000000 - #2D2B33 - #4A4851 - #000000 - #322F35 - #FFFFFF - #CFBDFE - #4F4078 - #FFFFFF - #382960 - #FFFFFF - #4F4078 - #FFFFFF - #382960 - #FFFFFF - #72354C - #FFFFFF - #571F35 - #FFFFFF - #BCB7BF - #FDF7FF - #FFFFFF - #F5EFF7 - #E6E1E9 - #D8D3DA - #CAC5CD + #517DA2 + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + #F0F0F0 + #51AEE7 diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index f694812..22661a7 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,4 +1,19 @@ - + + + - + \ No newline at end of file From 9e0c1e7d7a3b1f338bd8f5338674ada1a4bf56a4 Mon Sep 17 00:00:00 2001 From: YURY TILMAN Date: Mon, 3 Nov 2025 22:28:41 +0300 Subject: [PATCH 07/17] added chat view holder --- .../otus/gpb/recyclerview/ChatViewHolder.kt | 48 +++++++++++++++++++ .../otus/gpb/recyclerview/ItemListener.kt | 8 ++++ .../otus/gpb/recyclerview/model/ChatItem.kt | 28 +++++++++++ app/src/main/res/layout/activity_main.xml | 4 +- .../{contact_item.xml => chat_item.xml} | 15 +++--- app/src/main/res/values/colors.xml | 4 +- 6 files changed, 96 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/otus/gpb/recyclerview/ChatViewHolder.kt create mode 100644 app/src/main/java/otus/gpb/recyclerview/ItemListener.kt create mode 100644 app/src/main/java/otus/gpb/recyclerview/model/ChatItem.kt rename app/src/main/res/layout/{contact_item.xml => chat_item.xml} (93%) diff --git a/app/src/main/java/otus/gpb/recyclerview/ChatViewHolder.kt b/app/src/main/java/otus/gpb/recyclerview/ChatViewHolder.kt new file mode 100644 index 0000000..681bcf6 --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/ChatViewHolder.kt @@ -0,0 +1,48 @@ +package otus.gpb.recyclerview + +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import otus.gpb.recyclerview.databinding.ChatItemBinding +import otus.gpb.recyclerview.model.ChatItem + +class ChatViewHolder( + private val cartItemBinding: ChatItemBinding, + private val itemListener: ItemListener +) : RecyclerView.ViewHolder(cartItemBinding.root) { + + val chatImage: ImageView = cartItemBinding.chatImage + val chatNameText: TextView = cartItemBinding.chatName + val lastUserNameText: TextView = cartItemBinding.lastUserName + val lastMessageText: TextView = cartItemBinding.lastMessage + val verifiedImage: ImageView = cartItemBinding.verified + val muteImage: ImageView = cartItemBinding.mute + val messageStatusImage: ImageView = cartItemBinding.messageStatus + val lastMessageTimeText: TextView = cartItemBinding.lastMessageTime + val chatPinnedImage: ImageView = cartItemBinding.pinned + + + fun bind(chatItem: ChatItem) { + with(chatItem) { + chatImage.setImageResource(imageId) + chatNameText.text = chatName + lastUserNameText.text = lastUserName + lastMessageText.text = lastMessage + verifiedImage.visibility = if (isVerified) View.VISIBLE else View.GONE + muteImage.visibility = if (isMuted) View.VISIBLE else View.GONE + + messageStatusImage.setImageResource(messageStatus.iconId) + messageStatusImage.setBackgroundColor( + cartItemBinding.root.resources.getColor( + messageStatus.colorId + ) + ) + lastMessageTimeText.text = lastMessageTime + chatPinnedImage.visibility = if (isPinned) View.VISIBLE else View.GONE + + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/ItemListener.kt b/app/src/main/java/otus/gpb/recyclerview/ItemListener.kt new file mode 100644 index 0000000..f9b8bd5 --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/ItemListener.kt @@ -0,0 +1,8 @@ +package otus.gpb.recyclerview + +import java.util.UUID + +interface ItemListener { + fun onItemClick(id: UUID) + +} \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/model/ChatItem.kt b/app/src/main/java/otus/gpb/recyclerview/model/ChatItem.kt new file mode 100644 index 0000000..116fc3d --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/model/ChatItem.kt @@ -0,0 +1,28 @@ +package otus.gpb.recyclerview.model + +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import otus.gpb.recyclerview.R + +data class ChatItem( + val id: Int, + val imageId:Int, + val chatName: String, + val lastUserName: String, + val lastMessage: String, + val title: String, + val isVerified: Boolean, + val isMuted: Boolean, + val messageStatus: MessageStatus, + val lastMessageTime: String, + val isPinned: Boolean +) + +enum class MessageStatus( + @param:ColorRes val colorId:Int, + @param:DrawableRes val iconId: Int +) { + SENT(R.color.grey,R.drawable.check), + DELIVERED(R.color.grey, R.drawable.read), + READ(R.color.green, R.drawable.read);// @ColorRes colorId:Int, +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index b4e29ec..11e370c 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -40,13 +40,13 @@ app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layout_constraintTop_toBottomOf="@+id/cart_item_app_bar" tools:itemCount="9" - tools:listitem="@layout/contact_item" /> + tools:listitem="@layout/chat_item" /> \ No newline at end of file diff --git a/app/src/main/res/layout/contact_item.xml b/app/src/main/res/layout/chat_item.xml similarity index 93% rename from app/src/main/res/layout/contact_item.xml rename to app/src/main/res/layout/chat_item.xml index 0bca10c..3e3dd29 100644 --- a/app/src/main/res/layout/contact_item.xml +++ b/app/src/main/res/layout/chat_item.xml @@ -11,7 +11,7 @@ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index e279a08..b63b656 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -8,6 +8,8 @@ #FF018786 #FF000000 #FFFFFFFF - #F0F0F0 + #F0F0F0 + #868686 #51AEE7 + #48A938 From b0210f6882a6257e56a9cda1088c7cadbbd41c98 Mon Sep 17 00:00:00 2001 From: YURY TILMAN Date: Tue, 4 Nov 2025 22:15:24 +0300 Subject: [PATCH 08/17] added chat adapter. first start doen --- .../java/otus/gpb/recyclerview/ChatAdapter.kt | 35 +++++++++++ .../otus/gpb/recyclerview/ChatViewHolder.kt | 9 ++- .../otus/gpb/recyclerview/MainActivity.kt | 58 +++++++++++++++++-- .../otus/gpb/recyclerview/model/ChatItem.kt | 3 +- app/src/main/res/drawable/chat_item_icon.xml | 29 ++++++++++ app/src/main/res/layout/activity_main.xml | 2 +- app/src/main/res/layout/chat_item.xml | 17 +++--- app/src/main/res/values/themes.xml | 49 +--------------- 8 files changed, 134 insertions(+), 68 deletions(-) create mode 100644 app/src/main/java/otus/gpb/recyclerview/ChatAdapter.kt create mode 100644 app/src/main/res/drawable/chat_item_icon.xml diff --git a/app/src/main/java/otus/gpb/recyclerview/ChatAdapter.kt b/app/src/main/java/otus/gpb/recyclerview/ChatAdapter.kt new file mode 100644 index 0000000..3a3ce37 --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/ChatAdapter.kt @@ -0,0 +1,35 @@ +package otus.gpb.recyclerview + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import otus.gpb.recyclerview.databinding.ChatItemBinding +import otus.gpb.recyclerview.model.ChatItem + +class ChatAdapter( + private val dataSet: MutableList, + private val itemListener: ItemListener, +) : RecyclerView.Adapter() { + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): ChatViewHolder { + val chatItemBinding = + ChatItemBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return ChatViewHolder(chatItemBinding, itemListener) + } + + override fun onBindViewHolder( + holder: ChatViewHolder, + position: Int + ) { + holder.bind(dataSet[position]) + } + + override fun getItemCount() = dataSet.size +} \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/ChatViewHolder.kt b/app/src/main/java/otus/gpb/recyclerview/ChatViewHolder.kt index 681bcf6..0b13cc8 100644 --- a/app/src/main/java/otus/gpb/recyclerview/ChatViewHolder.kt +++ b/app/src/main/java/otus/gpb/recyclerview/ChatViewHolder.kt @@ -3,6 +3,7 @@ package otus.gpb.recyclerview import android.view.View import android.widget.ImageView import android.widget.TextView +import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView import otus.gpb.recyclerview.databinding.ChatItemBinding import otus.gpb.recyclerview.model.ChatItem @@ -23,6 +24,7 @@ class ChatViewHolder( val chatPinnedImage: ImageView = cartItemBinding.pinned + fun bind(chatItem: ChatItem) { with(chatItem) { chatImage.setImageResource(imageId) @@ -33,11 +35,8 @@ class ChatViewHolder( muteImage.visibility = if (isMuted) View.VISIBLE else View.GONE messageStatusImage.setImageResource(messageStatus.iconId) - messageStatusImage.setBackgroundColor( - cartItemBinding.root.resources.getColor( - messageStatus.colorId - ) - ) + messageStatusImage.setColorFilter(ContextCompat.getColor(cartItemBinding.root.context, messageStatus.colorId), + android.graphics.PorterDuff.Mode.SRC_IN) lastMessageTimeText.text = lastMessageTime chatPinnedImage.visibility = if (isPinned) View.VISIBLE else View.GONE diff --git a/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt b/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt index e2cdca7..c9190bb 100644 --- a/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt +++ b/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt @@ -1,12 +1,62 @@ package otus.gpb.recyclerview -import androidx.appcompat.app.AppCompatActivity import android.os.Bundle +import android.widget.LinearLayout +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.DividerItemDecoration +import otus.gpb.recyclerview.databinding.ActivityMainBinding +import otus.gpb.recyclerview.model.ChatItem +import otus.gpb.recyclerview.model.MessageStatus +import java.time.LocalTime +import java.time.format.DateTimeFormatter +import java.util.Random +import java.util.UUID + +class MainActivity : AppCompatActivity(), ItemListener { + + lateinit var activityMainBinding: ActivityMainBinding -class MainActivity : AppCompatActivity() { + private val chatList: MutableList by lazy { generateTestData() } + private val chatAdapter: ChatAdapter by lazy { ChatAdapter(chatList, this) } + private val random = Random() + + private val formatter = DateTimeFormatter.ofPattern("HH:mm") + private val now = LocalTime.now() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) + activityMainBinding = ActivityMainBinding.inflate(layoutInflater) + setContentView(activityMainBinding.root) + + with(activityMainBinding.chatListView) { + addItemDecoration(DividerItemDecoration(this@MainActivity, LinearLayout.VERTICAL)) + adapter = chatAdapter + } } -} \ No newline at end of file + + + private fun generateTestData(): MutableList { + return Array(15) { index: Int -> + ChatItem( + UUID.randomUUID(), + R.drawable.chat_item_icon, + "chatName " + index, + "lastUserName " + index, + "lastMessage " + index, + "title " + index, + random.nextBoolean(), + random.nextBoolean(), + MessageStatus.entries[random.nextInt(3)], + now.minusSeconds(random.nextInt(3600 * 24 * 7).toLong()).format(formatter) + .toString(), + random.nextBoolean() + ) + }.toMutableList() + } + + override fun onItemClick(id: UUID) { + TODO("Not yet implemented") + } + +} + diff --git a/app/src/main/java/otus/gpb/recyclerview/model/ChatItem.kt b/app/src/main/java/otus/gpb/recyclerview/model/ChatItem.kt index 116fc3d..a813a33 100644 --- a/app/src/main/java/otus/gpb/recyclerview/model/ChatItem.kt +++ b/app/src/main/java/otus/gpb/recyclerview/model/ChatItem.kt @@ -3,9 +3,10 @@ package otus.gpb.recyclerview.model import androidx.annotation.ColorRes import androidx.annotation.DrawableRes import otus.gpb.recyclerview.R +import java.util.UUID data class ChatItem( - val id: Int, + val id: UUID, val imageId:Int, val chatName: String, val lastUserName: String, diff --git a/app/src/main/res/drawable/chat_item_icon.xml b/app/src/main/res/drawable/chat_item_icon.xml new file mode 100644 index 0000000..f2987f4 --- /dev/null +++ b/app/src/main/res/drawable/chat_item_icon.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 11e370c..2f87d23 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -33,7 +33,7 @@ - + + android:src="@drawable/verified_ic" /> + android:src="@drawable/mute" /> @@ -109,7 +108,7 @@ android:layout_height="19dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - tools:src="@drawable/check" /> + android:src="@drawable/check" /> + android:src="@drawable/pinned" /> \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 22661a7..0cda76c 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -14,52 +14,5 @@ ?attr/colorPrimaryVariant - + \ No newline at end of file From 2762a3bd0b38e7ac5ff1e45e71a8c28cba609221 Mon Sep 17 00:00:00 2001 From: YURY TILMAN Date: Thu, 6 Nov 2025 22:52:03 +0300 Subject: [PATCH 09/17] added ChatDiffAdapter --- .../otus/gpb/recyclerview/ChatDiffAdapter.kt | 37 +++++++++++++++++++ .../otus/gpb/recyclerview/MainActivity.kt | 13 ++++--- 2 files changed, 45 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/otus/gpb/recyclerview/ChatDiffAdapter.kt diff --git a/app/src/main/java/otus/gpb/recyclerview/ChatDiffAdapter.kt b/app/src/main/java/otus/gpb/recyclerview/ChatDiffAdapter.kt new file mode 100644 index 0000000..97efc32 --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/ChatDiffAdapter.kt @@ -0,0 +1,37 @@ +package otus.gpb.recyclerview + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import otus.gpb.recyclerview.databinding.ChatItemBinding +import otus.gpb.recyclerview.model.ChatItem + +class ChatDiffAdapter( + private val itemListener: ItemListener, +) : ListAdapter(DiffUtilItem()) { + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): ChatViewHolder { + val chatItemBinding = + ChatItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ChatViewHolder(chatItemBinding, itemListener) + } + + override fun onBindViewHolder(holder: ChatViewHolder, position: Int) = + holder.bind(getItem(position)) + +} + +private class DiffUtilItem : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: ChatItem, newItem: ChatItem): Boolean { + return oldItem::class == newItem::class && oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: ChatItem, newItem: ChatItem): Boolean { + return oldItem == newItem + } + +} \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt b/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt index c9190bb..a0c65dd 100644 --- a/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt +++ b/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt @@ -18,6 +18,7 @@ class MainActivity : AppCompatActivity(), ItemListener { private val chatList: MutableList by lazy { generateTestData() } private val chatAdapter: ChatAdapter by lazy { ChatAdapter(chatList, this) } + private val chatDiffAdapter: ChatDiffAdapter by lazy { ChatDiffAdapter( this) } private val random = Random() private val formatter = DateTimeFormatter.ofPattern("HH:mm") @@ -30,20 +31,22 @@ class MainActivity : AppCompatActivity(), ItemListener { with(activityMainBinding.chatListView) { addItemDecoration(DividerItemDecoration(this@MainActivity, LinearLayout.VERTICAL)) - adapter = chatAdapter + adapter = chatDiffAdapter + chatDiffAdapter.submitList(chatList) } } + /* TODO: вынести в модель */ private fun generateTestData(): MutableList { return Array(15) { index: Int -> ChatItem( UUID.randomUUID(), R.drawable.chat_item_icon, - "chatName " + index, - "lastUserName " + index, - "lastMessage " + index, - "title " + index, + "Chat Name " + index, + "User Name " + index, + "Last Message " + index, + "Title " + index, random.nextBoolean(), random.nextBoolean(), MessageStatus.entries[random.nextInt(3)], From 957790a7a2ae6c956c7854836de2f30b666a1386 Mon Sep 17 00:00:00 2001 From: YURY TILMAN Date: Fri, 7 Nov 2025 23:05:55 +0300 Subject: [PATCH 10/17] added ChatItemTouchHelper --- .../gpb/recyclerview/ChatItemTouchHelper.kt | 35 +++++++++++++++++++ .../otus/gpb/recyclerview/ItemListener.kt | 1 + .../otus/gpb/recyclerview/MainActivity.kt | 13 +++++-- .../main/java/otus/gpb/recyclerview/Utils.kt | 3 ++ 4 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/otus/gpb/recyclerview/ChatItemTouchHelper.kt create mode 100644 app/src/main/java/otus/gpb/recyclerview/Utils.kt diff --git a/app/src/main/java/otus/gpb/recyclerview/ChatItemTouchHelper.kt b/app/src/main/java/otus/gpb/recyclerview/ChatItemTouchHelper.kt new file mode 100644 index 0000000..d092265 --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/ChatItemTouchHelper.kt @@ -0,0 +1,35 @@ +package otus.gpb.recyclerview + +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView +import java.util.UUID + +class ChatItemTouchHelper(private val listener: ItemListener) : ItemTouchHelper.Callback() { + override fun getMovementFlags( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder + ) = makeMovementFlags( + ItemTouchHelper.ACTION_STATE_IDLE, + ItemTouchHelper.LEFT + ) + + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ) = false + + override fun onSwiped( + viewHolder: RecyclerView.ViewHolder, + direction: Int + ) { + viewHolder.getElementId()?.let { listener.onSwipe(it) } + } + + private fun RecyclerView.ViewHolder.getElementId(): UUID? { + return bindingAdapter + ?.asClass() + ?.currentList + ?.getOrNull(bindingAdapterPosition)?.id + } +} \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/ItemListener.kt b/app/src/main/java/otus/gpb/recyclerview/ItemListener.kt index f9b8bd5..dd5bc2f 100644 --- a/app/src/main/java/otus/gpb/recyclerview/ItemListener.kt +++ b/app/src/main/java/otus/gpb/recyclerview/ItemListener.kt @@ -4,5 +4,6 @@ import java.util.UUID interface ItemListener { fun onItemClick(id: UUID) + fun onSwipe(id: UUID) } \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt b/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt index a0c65dd..d34cedd 100644 --- a/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt +++ b/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt @@ -2,8 +2,10 @@ package otus.gpb.recyclerview import android.os.Bundle import android.widget.LinearLayout +import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.ItemTouchHelper import otus.gpb.recyclerview.databinding.ActivityMainBinding import otus.gpb.recyclerview.model.ChatItem import otus.gpb.recyclerview.model.MessageStatus @@ -18,7 +20,7 @@ class MainActivity : AppCompatActivity(), ItemListener { private val chatList: MutableList by lazy { generateTestData() } private val chatAdapter: ChatAdapter by lazy { ChatAdapter(chatList, this) } - private val chatDiffAdapter: ChatDiffAdapter by lazy { ChatDiffAdapter( this) } + private val chatDiffAdapter: ChatDiffAdapter by lazy { ChatDiffAdapter(this) } private val random = Random() private val formatter = DateTimeFormatter.ofPattern("HH:mm") @@ -31,8 +33,9 @@ class MainActivity : AppCompatActivity(), ItemListener { with(activityMainBinding.chatListView) { addItemDecoration(DividerItemDecoration(this@MainActivity, LinearLayout.VERTICAL)) + ItemTouchHelper(ChatItemTouchHelper(this@MainActivity)).attachToRecyclerView(this) adapter = chatDiffAdapter - chatDiffAdapter.submitList(chatList) + chatDiffAdapter.submitList(chatList.toList()) } } @@ -61,5 +64,11 @@ class MainActivity : AppCompatActivity(), ItemListener { TODO("Not yet implemented") } + override fun onSwipe(id: UUID) { + chatList.removeIf { it.id == id } + chatDiffAdapter.submitList(chatList.toList()) + Toast.makeText(this, "Item swiped: $id", Toast.LENGTH_SHORT).show() + } + } diff --git a/app/src/main/java/otus/gpb/recyclerview/Utils.kt b/app/src/main/java/otus/gpb/recyclerview/Utils.kt new file mode 100644 index 0000000..8d219cd --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/Utils.kt @@ -0,0 +1,3 @@ +package otus.gpb.recyclerview + +fun Any.asClass(): T? = if (this as? T == null) null else this \ No newline at end of file From cc76e3834a9e7b981f8e13b0ac2be2f21c8323e8 Mon Sep 17 00:00:00 2001 From: YURY TILMAN Date: Fri, 7 Nov 2025 23:06:08 +0300 Subject: [PATCH 11/17] added item click listener --- app/src/main/java/otus/gpb/recyclerview/ChatViewHolder.kt | 4 +++- app/src/main/java/otus/gpb/recyclerview/MainActivity.kt | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/otus/gpb/recyclerview/ChatViewHolder.kt b/app/src/main/java/otus/gpb/recyclerview/ChatViewHolder.kt index 0b13cc8..e5a09c6 100644 --- a/app/src/main/java/otus/gpb/recyclerview/ChatViewHolder.kt +++ b/app/src/main/java/otus/gpb/recyclerview/ChatViewHolder.kt @@ -39,7 +39,9 @@ class ChatViewHolder( android.graphics.PorterDuff.Mode.SRC_IN) lastMessageTimeText.text = lastMessageTime chatPinnedImage.visibility = if (isPinned) View.VISIBLE else View.GONE - + cartItemBinding.root.setOnClickListener { + itemListener.onItemClick(id) + } } } diff --git a/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt b/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt index d34cedd..e22d1ef 100644 --- a/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt +++ b/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt @@ -61,7 +61,7 @@ class MainActivity : AppCompatActivity(), ItemListener { } override fun onItemClick(id: UUID) { - TODO("Not yet implemented") + Toast.makeText(this, "Item clicked: $id", Toast.LENGTH_SHORT).show() } override fun onSwipe(id: UUID) { From 7b8d1586ef6e69107ecd87e6bd9aeeaad915ea05 Mon Sep 17 00:00:00 2001 From: YURY TILMAN Date: Sat, 8 Nov 2025 23:37:17 +0300 Subject: [PATCH 12/17] added remove item animation --- .../gpb/recyclerview/ChatItemTouchHelper.kt | 101 ++++++++++++++++-- app/src/main/res/values/colors.xml | 1 + 2 files changed, 94 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/otus/gpb/recyclerview/ChatItemTouchHelper.kt b/app/src/main/java/otus/gpb/recyclerview/ChatItemTouchHelper.kt index d092265..26dda89 100644 --- a/app/src/main/java/otus/gpb/recyclerview/ChatItemTouchHelper.kt +++ b/app/src/main/java/otus/gpb/recyclerview/ChatItemTouchHelper.kt @@ -1,18 +1,24 @@ package otus.gpb.recyclerview +import android.content.Context +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.drawable.VectorDrawable +import android.util.DisplayMetrics +import androidx.core.content.ContextCompat +import androidx.core.graphics.createBitmap import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView import java.util.UUID +import kotlin.math.roundToInt -class ChatItemTouchHelper(private val listener: ItemListener) : ItemTouchHelper.Callback() { - override fun getMovementFlags( - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder - ) = makeMovementFlags( - ItemTouchHelper.ACTION_STATE_IDLE, - ItemTouchHelper.LEFT - ) +class ChatItemTouchHelper(private val listener: ItemListener) : ItemTouchHelper.SimpleCallback( + ItemTouchHelper.ACTION_STATE_IDLE, + ItemTouchHelper.LEFT +) { override fun onMove( recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, @@ -26,6 +32,85 @@ class ChatItemTouchHelper(private val listener: ItemListener) : ItemTouchHelper. viewHolder.getElementId()?.let { listener.onSwipe(it) } } + /** + * source: https://stackoverflow.com/questions/30820806/adding-a-colored-background-with-text-icon-under-swiped-row-when-using-androids + * */ + override fun onChildDraw( + c: Canvas, + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + dX: Float, + dY: Float, + actionState: Int, + isCurrentlyActive: Boolean + ) { + + // Get RecyclerView item from the ViewHolder + val itemView = viewHolder.itemView + + val p = Paint() + val context = itemView.context + val resources: Resources = context.resources + + if (dX < 0) { + /* Set your color for negative displacement */ + p.setColor(ContextCompat.getColor(context, R.color.light_blue)) + // Draw Rect with varying left side, equal to the item's right side plus negative displacement dX + + val itemViewTop = itemView.top.toFloat() + val itemViewRight = itemView.right.toFloat() + val itemViewBottom = itemView.bottom.toFloat() + c.drawRect( + /* left = */ itemViewRight + dX, + /* top = */ itemViewTop, + /* right = */ itemViewRight, + /* bottom = */ itemViewBottom, + /* paint = */ p + ) + +// fixme magic numbers to dimensions + getVectorBitmap(context, R.drawable.archive)?.let { bitmap -> + val left = itemViewRight - convertDpToPx(24, resources) - bitmap.getWidth() + val top = itemViewTop + (itemViewBottom - itemViewTop - bitmap.getHeight()) * 0.4f + c.drawBitmap( + /* bitmap = */ bitmap, + /* left = */ left, + /* top = */ top, + /* paint = */ p + ) + + c.drawText( + "Archive", + left - convertDpToPx(15, resources), + top + convertDpToPx(40, resources), + Paint().apply { + color = ContextCompat.getColor(context, R.color.white) + textSize = 36f + } + ) + } + } + + super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) + } + + /** + * source https://stackoverflow.com/questions/44612353/using-vector-drawable-by-drawing-in-canvas + * */ + private fun getVectorBitmap(context: Context, drawableId: Int) = + ContextCompat.getDrawable(context, drawableId)?.asClass() + ?.let { drawable -> + val bitmap: Bitmap = createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight) + val canvas = Canvas(bitmap) + drawable.setBounds(0, 0, canvas.width, canvas.height) + drawable.draw(canvas) + bitmap + } + + private fun convertDpToPx(dp: Int, resources: Resources): Int { + return (dp * (resources.displayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT)).roundToInt() + } + private fun RecyclerView.ViewHolder.getElementId(): UUID? { return bindingAdapter ?.asClass() diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index b63b656..05ca42f 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -11,5 +11,6 @@ #F0F0F0 #868686 #51AEE7 + #66A9E0 #48A938 From 2cc9b489478b8131a188f87717e6c0dd36f93000 Mon Sep 17 00:00:00 2001 From: YURY TILMAN Date: Sat, 8 Nov 2025 23:46:01 +0300 Subject: [PATCH 13/17] added sources from lesson --- app/src/main/java/otus/gpb/recyclerview/ChatItemTouchHelper.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/otus/gpb/recyclerview/ChatItemTouchHelper.kt b/app/src/main/java/otus/gpb/recyclerview/ChatItemTouchHelper.kt index 26dda89..d8b2e3f 100644 --- a/app/src/main/java/otus/gpb/recyclerview/ChatItemTouchHelper.kt +++ b/app/src/main/java/otus/gpb/recyclerview/ChatItemTouchHelper.kt @@ -34,6 +34,7 @@ class ChatItemTouchHelper(private val listener: ItemListener) : ItemTouchHelper. /** * source: https://stackoverflow.com/questions/30820806/adding-a-colored-background-with-text-icon-under-swiped-row-when-using-androids + * Источник из ДЗ (не загружается без vpn) https://www.digitalocean.com/community/tutorials/android-recyclerview-swipe-to-delete-undo * */ override fun onChildDraw( c: Canvas, From 24d435f3963ded76a4fc3ce744fb9abafe91a4c0 Mon Sep 17 00:00:00 2001 From: YURY TILMAN Date: Sun, 9 Nov 2025 11:49:12 +0300 Subject: [PATCH 14/17] added stat service --- app/src/main/AndroidManifest.xml | 1 + .../main/java/otus/gpb/recyclerview/App.kt | 21 ++++++ .../otus/gpb/recyclerview/MainActivity.kt | 6 ++ .../gpb/recyclerview/prefs/KeyValueStorage.kt | 67 +++++++++++++++++++ .../java/otus/gpb/recyclerview/stat/Event.kt | 9 +++ .../otus/gpb/recyclerview/stat/PageEvent.kt | 39 +++++++++++ .../gpb/recyclerview/stat/PageEventHelper.kt | 42 ++++++++++++ .../gpb/recyclerview/stat/StatEventLogger.kt | 24 +++++++ 8 files changed, 209 insertions(+) create mode 100644 app/src/main/java/otus/gpb/recyclerview/App.kt create mode 100644 app/src/main/java/otus/gpb/recyclerview/prefs/KeyValueStorage.kt create mode 100644 app/src/main/java/otus/gpb/recyclerview/stat/Event.kt create mode 100644 app/src/main/java/otus/gpb/recyclerview/stat/PageEvent.kt create mode 100644 app/src/main/java/otus/gpb/recyclerview/stat/PageEventHelper.kt create mode 100644 app/src/main/java/otus/gpb/recyclerview/stat/StatEventLogger.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ef75335..048f327 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ + + /** + * Clears all keys + */ + fun clear() +} + +/** + * Uses shared preferences as a storage + */ +class SharedPreferencesStorage(context: Context, name: String) : KeyValueStorage { + private val prefs = context.getSharedPreferences(name, Context.MODE_PRIVATE) + + override fun get(key: String): String? = prefs.getString(key, null) + + override fun set(key: String, value: String?) { + prefs.edit { putString(key, value) } + } + + override fun liveData(key: String): LiveData = object : LiveData(get(key)) { + private val preferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, changedKey -> + if (changedKey == key) { + postValue(get(key)) + } + } + + override fun onActive() { + super.onActive() + postValue(get(key)) + prefs.registerOnSharedPreferenceChangeListener(preferenceChangeListener) + } + + override fun onInactive() { + super.onInactive() + prefs.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener) + } + } + + override fun clear() { + prefs.edit { clear() } + } +} \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/stat/Event.kt b/app/src/main/java/otus/gpb/recyclerview/stat/Event.kt new file mode 100644 index 0000000..12e570d --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/stat/Event.kt @@ -0,0 +1,9 @@ +package otus.gpb.recyclerview.stat + +/** + * Event + */ +interface Event { + val name: String + val properties: Map +} \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/stat/PageEvent.kt b/app/src/main/java/otus/gpb/recyclerview/stat/PageEvent.kt new file mode 100644 index 0000000..5509984 --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/stat/PageEvent.kt @@ -0,0 +1,39 @@ +package otus.gpb.recyclerview.stat + +import androidx.annotation.VisibleForTesting +import java.time.Instant +import kotlin.time.ExperimentalTime + +data class PageEvent @OptIn(ExperimentalTime::class) constructor( + private val page: String, + private val action: String, + private val time: Instant +) : Event { + override val name: String get() = PAGE + + override val properties: Map + get() = mapOf( + NAME to page, + TIME to time.toString(), + ACTION to action + ) + + @VisibleForTesting + companion object { + const val PAGE = "page" + const val NAME = "name" + const val TIME = "time" + const val ACTION = "action" + } +} + +fun createPageEvent( + name: String, + action: String, + time: Instant = Instant.now() +) = PageEvent(name, action, time) + +fun T.createPageEvent( + action: String, + time: Instant = Instant.now() +) = createPageEvent(requireNotNull(this::class.simpleName), action, time) \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/stat/PageEventHelper.kt b/app/src/main/java/otus/gpb/recyclerview/stat/PageEventHelper.kt new file mode 100644 index 0000000..afd00ab --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/stat/PageEventHelper.kt @@ -0,0 +1,42 @@ +package otus.gpb.recyclerview.stat + +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner + +/** + * Helper to log page events + */ +class PageEventHelper(private val statService: StatService): DefaultLifecycleObserver { + private fun logEvent(owner: LifecycleOwner, action: String) { + statService.logEvent( + createPageEvent( + name = owner::class.simpleName ?: "Unknown", + action = action + ) + ) + } + + override fun onCreate(owner: LifecycleOwner) { + logEvent(owner, "onCreate") + } + + override fun onStart(owner: LifecycleOwner) { + logEvent(owner, "onStart") + } + + override fun onResume(owner: LifecycleOwner) { + logEvent(owner,"onResume") + } + + override fun onPause(owner: LifecycleOwner) { + logEvent(owner, "onPause") + } + + override fun onStop(owner: LifecycleOwner) { + logEvent(owner, "onStop") + } + + override fun onDestroy(owner: LifecycleOwner) { + logEvent(owner, "onDestroy") + } +} \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/stat/StatEventLogger.kt b/app/src/main/java/otus/gpb/recyclerview/stat/StatEventLogger.kt new file mode 100644 index 0000000..948bc16 --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/stat/StatEventLogger.kt @@ -0,0 +1,24 @@ +package otus.gpb.recyclerview.stat + +import android.util.Log + +/** + * Stat service + */ +interface StatService { + + /** + * Logs event + */ + fun logEvent(event: Event) + + class Logger: StatService { + override fun logEvent(event: Event) { + Log.i( TAG, "Event: ${event.name}, Properties ${event.properties}") + } + + companion object { + private const val TAG = "StatService" + } + } +} \ No newline at end of file From 2a2df1555433fa9950be91d130ee7c030bde0f4d Mon Sep 17 00:00:00 2001 From: YURY TILMAN Date: Sun, 9 Nov 2025 20:04:12 +0300 Subject: [PATCH 15/17] added view model and pagination --- .../otus/gpb/recyclerview/ChatViewHolder.kt | 2 +- .../otus/gpb/recyclerview/MainActivity.kt | 56 ++++++------- .../otus/gpb/recyclerview/model/ChatItem.kt | 23 ++++-- .../recyclerview/repository/ChatRepository.kt | 82 +++++++++++++++++++ .../recyclerview/view_model/ChatViewModel.kt | 33 ++++++++ 5 files changed, 156 insertions(+), 40 deletions(-) create mode 100644 app/src/main/java/otus/gpb/recyclerview/repository/ChatRepository.kt create mode 100644 app/src/main/java/otus/gpb/recyclerview/view_model/ChatViewModel.kt diff --git a/app/src/main/java/otus/gpb/recyclerview/ChatViewHolder.kt b/app/src/main/java/otus/gpb/recyclerview/ChatViewHolder.kt index e5a09c6..21f04be 100644 --- a/app/src/main/java/otus/gpb/recyclerview/ChatViewHolder.kt +++ b/app/src/main/java/otus/gpb/recyclerview/ChatViewHolder.kt @@ -37,7 +37,7 @@ class ChatViewHolder( messageStatusImage.setImageResource(messageStatus.iconId) messageStatusImage.setColorFilter(ContextCompat.getColor(cartItemBinding.root.context, messageStatus.colorId), android.graphics.PorterDuff.Mode.SRC_IN) - lastMessageTimeText.text = lastMessageTime + lastMessageTimeText.text = lastMessageViewTime chatPinnedImage.visibility = if (isPinned) View.VISIBLE else View.GONE cartItemBinding.root.setOnClickListener { itemListener.onItemClick(id) diff --git a/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt b/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt index c5f3350..828fa3b 100644 --- a/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt +++ b/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt @@ -3,40 +3,40 @@ package otus.gpb.recyclerview import android.os.Bundle import android.widget.LinearLayout import android.widget.Toast +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView import otus.gpb.recyclerview.databinding.ActivityMainBinding import otus.gpb.recyclerview.model.ChatItem -import otus.gpb.recyclerview.model.MessageStatus import otus.gpb.recyclerview.stat.PageEventHelper -import java.time.LocalTime -import java.time.format.DateTimeFormatter -import java.util.Random +import otus.gpb.recyclerview.view_model.ChatViewModel import java.util.UUID class MainActivity : AppCompatActivity(), ItemListener { lateinit var activityMainBinding: ActivityMainBinding - private val chatList: MutableList by lazy { generateTestData() } - private val chatAdapter: ChatAdapter by lazy { ChatAdapter(chatList, this) } private val chatDiffAdapter: ChatDiffAdapter by lazy { ChatDiffAdapter(this) } - private val random = Random() - private val formatter = DateTimeFormatter.ofPattern("HH:mm") - private val now = LocalTime.now() + private val viewModel: ChatViewModel by viewModels() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) activityMainBinding = ActivityMainBinding.inflate(layoutInflater) setContentView(activityMainBinding.root) - - with(activityMainBinding.chatListView) { + val recyclerView = activityMainBinding.chatListView + with(recyclerView) { addItemDecoration(DividerItemDecoration(this@MainActivity, LinearLayout.VERTICAL)) ItemTouchHelper(ChatItemTouchHelper(this@MainActivity)).attachToRecyclerView(this) adapter = chatDiffAdapter - chatDiffAdapter.submitList(chatList.toList()) + viewModel.content.observe(this@MainActivity) { it: MutableList? -> + chatDiffAdapter.submitList(it) + } + + addOnScrollListener(OnScrollListener { viewModel.loadNextPage() }) } val app = application as? App app?.let { @@ -45,25 +45,16 @@ class MainActivity : AppCompatActivity(), ItemListener { } - - /* TODO: вынести в модель */ - private fun generateTestData(): MutableList { - return Array(15) { index: Int -> - ChatItem( - UUID.randomUUID(), - R.drawable.chat_item_icon, - "Chat Name " + index, - "User Name " + index, - "Last Message " + index, - "Title " + index, - random.nextBoolean(), - random.nextBoolean(), - MessageStatus.entries[random.nextInt(3)], - now.minusSeconds(random.nextInt(3600 * 24 * 7).toLong()).format(formatter) - .toString(), - random.nextBoolean() - ) - }.toMutableList() + class OnScrollListener(private val action: () -> Unit) : RecyclerView.OnScrollListener() { + override fun onScrollStateChanged( + recyclerView: RecyclerView, + newState: Int + ) { + super.onScrollStateChanged(recyclerView, newState) + if (!recyclerView.canScrollVertically(1)) { + action.invoke() + } + } } override fun onItemClick(id: UUID) { @@ -71,8 +62,7 @@ class MainActivity : AppCompatActivity(), ItemListener { } override fun onSwipe(id: UUID) { - chatList.removeIf { it.id == id } - chatDiffAdapter.submitList(chatList.toList()) + viewModel.removeItem(id) Toast.makeText(this, "Item swiped: $id", Toast.LENGTH_SHORT).show() } diff --git a/app/src/main/java/otus/gpb/recyclerview/model/ChatItem.kt b/app/src/main/java/otus/gpb/recyclerview/model/ChatItem.kt index a813a33..a56e12d 100644 --- a/app/src/main/java/otus/gpb/recyclerview/model/ChatItem.kt +++ b/app/src/main/java/otus/gpb/recyclerview/model/ChatItem.kt @@ -3,27 +3,38 @@ package otus.gpb.recyclerview.model import androidx.annotation.ColorRes import androidx.annotation.DrawableRes import otus.gpb.recyclerview.R +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter import java.util.UUID +private val formatter = DateTimeFormatter.ofPattern("d MMM HH:mm") + data class ChatItem( val id: UUID, - val imageId:Int, + val imageId: Int, val chatName: String, val lastUserName: String, val lastMessage: String, val title: String, - val isVerified: Boolean, + var isVerified: Boolean, val isMuted: Boolean, val messageStatus: MessageStatus, - val lastMessageTime: String, + var lastMessageTime: LocalDateTime, + var lastMessageViewTime: String = lastMessageTime.format(formatter) + .toString(), val isPinned: Boolean -) +) { + fun setTime(time: LocalDateTime) { + lastMessageTime = time + lastMessageViewTime = time.format(formatter).toString() + } +} enum class MessageStatus( - @param:ColorRes val colorId:Int, + @param:ColorRes val colorId: Int, @param:DrawableRes val iconId: Int ) { - SENT(R.color.grey,R.drawable.check), + SENT(R.color.grey, R.drawable.check), DELIVERED(R.color.grey, R.drawable.read), READ(R.color.green, R.drawable.read);// @ColorRes colorId:Int, } \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/repository/ChatRepository.kt b/app/src/main/java/otus/gpb/recyclerview/repository/ChatRepository.kt new file mode 100644 index 0000000..19f3375 --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/repository/ChatRepository.kt @@ -0,0 +1,82 @@ +package otus.gpb.recyclerview.repository + +import otus.gpb.recyclerview.R +import otus.gpb.recyclerview.model.ChatItem +import otus.gpb.recyclerview.model.MessageStatus +import java.time.LocalDateTime +import java.util.Random +import java.util.UUID + +class ChatRepository( + private val serverListener: ChatServerListener +) { + + private val random = Random() + val pageSize = 10 + private var page = 1 + private var testData: MutableList + + init { + testData = generateTestData() + object : Thread() { + override fun run() { + while (true) { + sleep(10_000) + val item = testData[random.nextInt(testData.size)] + item.setTime(LocalDateTime.now()) + item.isVerified = false + testData = + testData.toMutableList().apply { sortByDescending { it.lastMessageTime } } + serverListener.onChatItemUpdated(getDataPage()) + } + } + }.start() + + } + + fun loadNextItems() { + page++ + serverListener.onChatItemUpdated(getDataPage()) + } + + fun removeItem(id: UUID) { + testData = testData.filter { it.id != id } + .toMutableList() // удаляем элемент по id и возвращаем новый список testData.removeIf { it.id != id } + serverListener.onChatItemUpdated(getDataPage()) + } + + fun getDataPage(): MutableList { + val toIndex = (page * pageSize).coerceAtMost(testData.size) // не выйти за пределы + return if (toIndex in 0 until testData.size) { + testData.subList(0, toIndex) + } else { + testData.toMutableList() + } + } + + private fun generateTestData(): MutableList { + val now = LocalDateTime.now() + return Array(25) { index: Int -> + val time: LocalDateTime = now.minusSeconds(random.nextInt(3600 * 24 * 7).toLong()) + ChatItem( + id = UUID.randomUUID(), + imageId = R.drawable.chat_item_icon, + chatName = "Chat Name " + index, + lastUserName = "User Name " + index, + lastMessage = "Last Message " + index, + title = "Title " + index, + isVerified = random.nextBoolean(), + isMuted = random.nextBoolean(), + messageStatus = MessageStatus.entries[random.nextInt(3)], + lastMessageTime = time, + isPinned = random.nextBoolean() + ) + } + .apply { sortByDescending { it.lastMessageTime } } + .toMutableList() + } +} + +interface ChatServerListener { + fun onChatItemUpdated(testData: MutableList) +} \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/view_model/ChatViewModel.kt b/app/src/main/java/otus/gpb/recyclerview/view_model/ChatViewModel.kt new file mode 100644 index 0000000..438de49 --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/view_model/ChatViewModel.kt @@ -0,0 +1,33 @@ +package otus.gpb.recyclerview.view_model + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import otus.gpb.recyclerview.model.ChatItem +import otus.gpb.recyclerview.repository.ChatRepository +import otus.gpb.recyclerview.repository.ChatServerListener +import java.util.UUID + +class ChatViewModel : ViewModel(), ChatServerListener { + + val chatRepository = ChatRepository(this) + + private val _content: MutableLiveData> = + MutableLiveData(chatRepository.getDataPage()) + + val content: LiveData> get() = _content + + fun loadNextPage() { + chatRepository.loadNextItems() + } + + fun removeItem(id: UUID) { + chatRepository.removeItem(id) + } + + override fun onChatItemUpdated(testData: MutableList) { + _content.postValue(testData) + } + + +} \ No newline at end of file From 0c3392701104b9f5ed757aa21fd0b9c958008a4d Mon Sep 17 00:00:00 2001 From: YURY TILMAN Date: Sun, 9 Nov 2025 20:16:02 +0300 Subject: [PATCH 16/17] fix background --- .../gpb/recyclerview/ChatItemTouchHelper.kt | 8 +++---- .../otus/gpb/recyclerview/MainActivity.kt | 23 +++++++++++-------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/otus/gpb/recyclerview/ChatItemTouchHelper.kt b/app/src/main/java/otus/gpb/recyclerview/ChatItemTouchHelper.kt index d8b2e3f..7b45f45 100644 --- a/app/src/main/java/otus/gpb/recyclerview/ChatItemTouchHelper.kt +++ b/app/src/main/java/otus/gpb/recyclerview/ChatItemTouchHelper.kt @@ -69,9 +69,9 @@ class ChatItemTouchHelper(private val listener: ItemListener) : ItemTouchHelper. /* paint = */ p ) -// fixme magic numbers to dimensions +// fixme magic numbers to dimensions, add relative counters getVectorBitmap(context, R.drawable.archive)?.let { bitmap -> - val left = itemViewRight - convertDpToPx(24, resources) - bitmap.getWidth() + val left = itemViewRight - convertDpToPx(50, resources) - bitmap.getWidth() val top = itemViewTop + (itemViewBottom - itemViewTop - bitmap.getHeight()) * 0.4f c.drawBitmap( /* bitmap = */ bitmap, @@ -82,8 +82,8 @@ class ChatItemTouchHelper(private val listener: ItemListener) : ItemTouchHelper. c.drawText( "Archive", - left - convertDpToPx(15, resources), - top + convertDpToPx(40, resources), + left - convertDpToPx(30, resources), + top + convertDpToPx(50, resources), Paint().apply { color = ContextCompat.getColor(context, R.color.white) textSize = 36f diff --git a/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt b/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt index 828fa3b..29b7a03 100644 --- a/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt +++ b/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt @@ -9,7 +9,6 @@ import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView import otus.gpb.recyclerview.databinding.ActivityMainBinding -import otus.gpb.recyclerview.model.ChatItem import otus.gpb.recyclerview.stat.PageEventHelper import otus.gpb.recyclerview.view_model.ChatViewModel import java.util.UUID @@ -27,25 +26,31 @@ class MainActivity : AppCompatActivity(), ItemListener { super.onCreate(savedInstanceState) activityMainBinding = ActivityMainBinding.inflate(layoutInflater) setContentView(activityMainBinding.root) + initRecyclerView() + addObservers() + } + + private fun addObservers() { + val app = application as? App + app?.let { + lifecycle.addObserver(PageEventHelper((application as App).statService)) + } + } + + private fun initRecyclerView() { val recyclerView = activityMainBinding.chatListView with(recyclerView) { addItemDecoration(DividerItemDecoration(this@MainActivity, LinearLayout.VERTICAL)) ItemTouchHelper(ChatItemTouchHelper(this@MainActivity)).attachToRecyclerView(this) adapter = chatDiffAdapter - viewModel.content.observe(this@MainActivity) { it: MutableList? -> + viewModel.content.observe(this@MainActivity) { chatDiffAdapter.submitList(it) } - addOnScrollListener(OnScrollListener { viewModel.loadNextPage() }) } - val app = application as? App - app?.let { - lifecycle.addObserver(PageEventHelper((application as App).statService)) - } - } - class OnScrollListener(private val action: () -> Unit) : RecyclerView.OnScrollListener() { + class OnScrollListener(private val action: () -> Unit) : RecyclerView.OnScrollListener() { override fun onScrollStateChanged( recyclerView: RecyclerView, newState: Int From 9f47e64d58b0b85b624391ffa708640ca3e9f7ea Mon Sep 17 00:00:00 2001 From: YURY TILMAN Date: Sun, 9 Nov 2025 21:47:26 +0300 Subject: [PATCH 17/17] move chat repository to app --- .../main/java/otus/gpb/recyclerview/App.kt | 9 +++++- .../otus/gpb/recyclerview/MainActivity.kt | 12 +++++--- .../recyclerview/repository/ChatRepository.kt | 22 +++++++++++---- .../recyclerview/view_model/ChatViewModel.kt | 28 +++++++++++++++++-- 4 files changed, 58 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/otus/gpb/recyclerview/App.kt b/app/src/main/java/otus/gpb/recyclerview/App.kt index 5f392d6..8299372 100644 --- a/app/src/main/java/otus/gpb/recyclerview/App.kt +++ b/app/src/main/java/otus/gpb/recyclerview/App.kt @@ -1,9 +1,10 @@ package otus.gpb.recyclerview import android.app.Application -import otus.gpb.recyclerview.stat.StatService import otus.gpb.recyclerview.prefs.KeyValueStorage import otus.gpb.recyclerview.prefs.SharedPreferencesStorage +import otus.gpb.recyclerview.repository.ChatRepository +import otus.gpb.recyclerview.stat.StatService class App : Application() { @@ -14,8 +15,14 @@ class App : Application() { /** * Preferences storage + * Можно подписаться через LiveData на изменение по ключу и получасть изменения в разных activities */ val preferences: KeyValueStorage by lazy { SharedPreferencesStorage(this, "prefs") } + + val chatRepository by lazy { + ChatRepository() + } + } \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt b/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt index 29b7a03..5fb4b45 100644 --- a/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt +++ b/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt @@ -9,6 +9,7 @@ import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView import otus.gpb.recyclerview.databinding.ActivityMainBinding +import otus.gpb.recyclerview.repository.ChatRepository import otus.gpb.recyclerview.stat.PageEventHelper import otus.gpb.recyclerview.view_model.ChatViewModel import java.util.UUID @@ -19,7 +20,11 @@ class MainActivity : AppCompatActivity(), ItemListener { private val chatDiffAdapter: ChatDiffAdapter by lazy { ChatDiffAdapter(this) } - private val viewModel: ChatViewModel by viewModels() + private val chatRepository: ChatRepository by lazy { application.asClass()?.chatRepository + ?: throw IllegalStateException("Can`t get App class") } + private val viewModel: ChatViewModel by viewModels { + ChatViewModel.Factory(chatRepository) + } override fun onCreate(savedInstanceState: Bundle?) { @@ -31,9 +36,8 @@ class MainActivity : AppCompatActivity(), ItemListener { } private fun addObservers() { - val app = application as? App - app?.let { - lifecycle.addObserver(PageEventHelper((application as App).statService)) + application.asClass()?.let { + lifecycle.addObserver(PageEventHelper(it.statService)) } } diff --git a/app/src/main/java/otus/gpb/recyclerview/repository/ChatRepository.kt b/app/src/main/java/otus/gpb/recyclerview/repository/ChatRepository.kt index 19f3375..4559ab9 100644 --- a/app/src/main/java/otus/gpb/recyclerview/repository/ChatRepository.kt +++ b/app/src/main/java/otus/gpb/recyclerview/repository/ChatRepository.kt @@ -7,17 +7,23 @@ import java.time.LocalDateTime import java.util.Random import java.util.UUID -class ChatRepository( - private val serverListener: ChatServerListener -) { +class ChatRepository() { private val random = Random() val pageSize = 10 private var page = 1 private var testData: MutableList + private val listeners: MutableList = mutableListOf() + + fun subscribe(listener: ChatServerListener) { + listeners.add(listener) + } + init { testData = generateTestData() + + // имитация подгрузки данных через с сервера через socket object : Thread() { override fun run() { while (true) { @@ -27,22 +33,26 @@ class ChatRepository( item.isVerified = false testData = testData.toMutableList().apply { sortByDescending { it.lastMessageTime } } - serverListener.onChatItemUpdated(getDataPage()) + notifyListeners() } } }.start() } + private fun notifyListeners() { + listeners.forEach { it.onChatItemUpdated(getDataPage()) } + } + fun loadNextItems() { page++ - serverListener.onChatItemUpdated(getDataPage()) + notifyListeners() } fun removeItem(id: UUID) { testData = testData.filter { it.id != id } .toMutableList() // удаляем элемент по id и возвращаем новый список testData.removeIf { it.id != id } - serverListener.onChatItemUpdated(getDataPage()) + notifyListeners() } fun getDataPage(): MutableList { diff --git a/app/src/main/java/otus/gpb/recyclerview/view_model/ChatViewModel.kt b/app/src/main/java/otus/gpb/recyclerview/view_model/ChatViewModel.kt index 438de49..2b6649a 100644 --- a/app/src/main/java/otus/gpb/recyclerview/view_model/ChatViewModel.kt +++ b/app/src/main/java/otus/gpb/recyclerview/view_model/ChatViewModel.kt @@ -1,16 +1,22 @@ package otus.gpb.recyclerview.view_model +import androidx.lifecycle.AbstractSavedStateViewModelFactory import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import otus.gpb.recyclerview.model.ChatItem import otus.gpb.recyclerview.repository.ChatRepository import otus.gpb.recyclerview.repository.ChatServerListener import java.util.UUID -class ChatViewModel : ViewModel(), ChatServerListener { +class ChatViewModel( + private val chatRepository: ChatRepository +) : ViewModel(), ChatServerListener { - val chatRepository = ChatRepository(this) + init { + chatRepository.subscribe(this) + } private val _content: MutableLiveData> = MutableLiveData(chatRepository.getDataPage()) @@ -25,9 +31,27 @@ class ChatViewModel : ViewModel(), ChatServerListener { chatRepository.removeItem(id) } + /** + * Подписались на информацию в репозитории + * */ override fun onChatItemUpdated(testData: MutableList) { _content.postValue(testData) } + @Suppress("UNCHECKED_CAST") + class Factory(private val chatRepository: ChatRepository) : + AbstractSavedStateViewModelFactory() { + override fun create( + key: String, + modelClass: Class, + handle: SavedStateHandle + ): T { + if (modelClass.isAssignableFrom(ChatViewModel::class.java)) { + return ChatViewModel(chatRepository) as? T + ?: throw IllegalArgumentException("Can`t create ChatViewModel by factory") + } + throw IllegalArgumentException("Unknown ViewModel class") + } + } } \ No newline at end of file