Skip to content

Commit c7a6422

Browse files
authored
Cleanup Alert Animations and include Android Sample (#6)
* Fix up project with latest updates * Add Sample Android project * WIP: Handling alert animations * Enhance alert banner animations and update Gradle settings * Explicit imports * Refactor alert banner display logic to show only when alerts are present * Bump version to 0.1.0-beta01 in build.gradle.kts
1 parent ca24f60 commit c7a6422

31 files changed

Lines changed: 646 additions & 41 deletions

alert-banner/build.gradle.kts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ kotlin {
3333
implementation(compose.material)
3434
implementation(compose.material3)
3535
implementation(compose.components.resources)
36+
implementation(compose.components.uiToolingPreview)
3637

3738
implementation(libs.kotlinx.datetime)
3839

@@ -48,7 +49,8 @@ kotlin {
4849

4950
val androidMain by getting {
5051
dependencies {
51-
api(libs.androidx.appcompat)
52+
implementation(libs.androidx.appcompat)
53+
implementation(libs.androidx.activity.compose)
5254
}
5355
}
5456
}
@@ -75,11 +77,14 @@ android {
7577
kotlin {
7678
jvmToolchain(17)
7779
}
80+
}
7881

82+
dependencies {
83+
debugImplementation(compose.uiTooling)
7984
}
8085

8186
group = "com.mofeejegi.alert"
82-
version = "0.1.0-alpha03"
87+
version = "0.1.0-beta01"
8388

8489
mavenPublishing {
8590
publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)

alert-banner/src/commonMain/kotlin/com/mofeejegi/alert/ui/composable/AlertBannerView.kt

Lines changed: 92 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
package com.mofeejegi.alert.ui.composable
22

33
import androidx.compose.animation.AnimatedVisibility
4-
import androidx.compose.animation.animateContentSize
54
import androidx.compose.animation.core.EaseInOut
5+
import androidx.compose.animation.core.FiniteAnimationSpec
66
import androidx.compose.animation.core.Spring
77
import androidx.compose.animation.core.spring
88
import androidx.compose.animation.core.tween
9+
import androidx.compose.animation.expandVertically
910
import androidx.compose.animation.fadeOut
1011
import androidx.compose.animation.scaleOut
12+
import androidx.compose.animation.shrinkVertically
1113
import androidx.compose.animation.slideInVertically
1214
import androidx.compose.foundation.ExperimentalFoundationApi
1315
import androidx.compose.foundation.background
@@ -20,7 +22,6 @@ import androidx.compose.foundation.layout.systemBarsPadding
2022
import androidx.compose.foundation.layout.widthIn
2123
import androidx.compose.foundation.layout.wrapContentHeight
2224
import androidx.compose.foundation.lazy.LazyColumn
23-
import androidx.compose.foundation.lazy.LazyItemScope
2425
import androidx.compose.foundation.lazy.items
2526
import androidx.compose.foundation.shape.RoundedCornerShape
2627
import androidx.compose.material.Icon
@@ -30,24 +31,29 @@ import androidx.compose.runtime.Composable
3031
import androidx.compose.runtime.DisposableEffect
3132
import androidx.compose.runtime.LaunchedEffect
3233
import androidx.compose.runtime.collectAsState
34+
import androidx.compose.runtime.derivedStateOf
3335
import androidx.compose.runtime.getValue
3436
import androidx.compose.ui.Alignment
3537
import androidx.compose.ui.Modifier
3638
import androidx.compose.ui.graphics.Color
3739
import androidx.compose.ui.text.TextStyle
3840
import androidx.compose.ui.text.style.TextOverflow
3941
import androidx.compose.ui.unit.dp
42+
import androidx.compose.ui.window.Popup
43+
import androidx.compose.ui.window.PopupProperties
4044
import com.mofeejegi.alert.alert_banner.generated.resources.Res
4145
import com.mofeejegi.alert.alert_banner.generated.resources.ic_close
46+
import com.mofeejegi.alert.ui.bannertype.AlertBannerType
4247
import com.mofeejegi.alert.ui.state.AlertAnimatedIn
4348
import com.mofeejegi.alert.ui.state.AlertAnimatedOut
4449
import com.mofeejegi.alert.ui.state.AlertBannerState
45-
import com.mofeejegi.alert.ui.bannertype.AlertBannerType
4650
import com.mofeejegi.alert.ui.state.AlertBannerViewEvent
4751
import com.mofeejegi.alert.ui.state.AlertBannerViewModel
4852
import com.mofeejegi.alert.ui.state.AlertDismissed
53+
import com.mofeejegi.alert.ui.theme.AlertTheme
4954
import kotlinx.coroutines.delay
5055
import org.jetbrains.compose.resources.painterResource
56+
import org.jetbrains.compose.ui.tooling.preview.Preview
5157

5258
@Composable
5359
internal fun AlertBannerView(
@@ -56,32 +62,43 @@ internal fun AlertBannerView(
5662
onAlertColor: Color,
5763
) {
5864
val viewState by vm.viewState.collectAsState()
65+
val alertsToDisplay by derivedStateOf { viewState.orderedAlerts() }
5966

60-
LazyColumn(
61-
Modifier
62-
.systemBarsPadding()
63-
.fillMaxWidth()
64-
.wrapContentHeight()
65-
.animateContentSize(),
66-
userScrollEnabled = false,
67-
) {
68-
items(
69-
items = viewState.orderedAlerts(),
70-
key = { alertState -> alertState.id },
71-
) {
72-
AlertBannerWrapper(
73-
alertState = it,
74-
textStyle = textStyle,
75-
onAlertColor = onAlertColor,
76-
eventProcessor = vm::processEvent,
67+
if (alertsToDisplay.isNotEmpty()) {
68+
Popup(
69+
alignment = Alignment.TopCenter,
70+
properties = PopupProperties(
71+
focusable = false,
72+
dismissOnBackPress = false,
73+
dismissOnClickOutside = false,
7774
)
75+
) {
76+
LazyColumn(
77+
Modifier
78+
.systemBarsPadding()
79+
.fillMaxWidth()
80+
.wrapContentHeight(),
81+
userScrollEnabled = false,
82+
) {
83+
items(
84+
items = alertsToDisplay,
85+
key = { alertState -> alertState.id },
86+
) {
87+
AlertBannerWrapper(
88+
alertState = it,
89+
textStyle = textStyle,
90+
onAlertColor = onAlertColor,
91+
eventProcessor = vm::processEvent,
92+
)
93+
}
94+
}
7895
}
7996
}
8097
}
8198

8299
@OptIn(ExperimentalFoundationApi::class)
83100
@Composable
84-
private fun LazyItemScope.AlertBannerWrapper(
101+
private fun AlertBannerWrapper(
85102
alertState: AlertBannerState,
86103
textStyle: TextStyle,
87104
onAlertColor: Color,
@@ -101,17 +118,18 @@ private fun LazyItemScope.AlertBannerWrapper(
101118
}
102119
}
103120

121+
fun <T> animationSpec(): FiniteAnimationSpec<T> = spring(
122+
dampingRatio = Spring.DampingRatioNoBouncy,
123+
stiffness = Spring.StiffnessLow,
124+
)
125+
104126
AnimatedVisibility(
105-
modifier = Modifier.animateItemPlacement(),
106127
visible = alertState.visible,
107-
enter = slideInVertically(
108-
animationSpec = spring(
109-
dampingRatio = Spring.DampingRatioNoBouncy,
110-
stiffness = Spring.StiffnessLow,
111-
),
112-
) { -it },
113-
exit = scaleOut(animationSpec = tween(easing = EaseInOut))
114-
+ fadeOut(animationSpec = tween(easing = EaseInOut)),
128+
enter = expandVertically(animationSpec = animationSpec())
129+
+ slideInVertically(animationSpec = animationSpec()) { -it },
130+
exit = shrinkVertically(shrinkTowards = Alignment.CenterVertically, animationSpec = animationSpec())
131+
+ fadeOut(animationSpec = tween(durationMillis = 200, easing = EaseInOut))
132+
+ scaleOut(animationSpec = tween(easing = EaseInOut)),
115133
) {
116134
AlertBanner(
117135
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp),
@@ -184,3 +202,47 @@ private fun AlertBanner(
184202
}
185203
}
186204
}
205+
206+
@Composable
207+
@Preview
208+
fun PreviewSuccessBanner() {
209+
AlertTheme(darkTheme = false) {
210+
Box(
211+
modifier = Modifier
212+
.background(Color.White)
213+
.size(400.dp)
214+
) {
215+
AlertBanner(
216+
id = "success-banner",
217+
message = "Success message",
218+
type = AlertBannerType.Success,
219+
eventProcessor = {},
220+
textStyle = TextStyle.Default,
221+
onAlertColor = Color.White,
222+
onDismiss = {},
223+
)
224+
}
225+
}
226+
}
227+
228+
@Composable
229+
@Preview
230+
fun PreviewErrorBanner() {
231+
AlertTheme(darkTheme = false) {
232+
Box(
233+
modifier = Modifier
234+
.background(Color.White)
235+
.size(400.dp)
236+
) {
237+
AlertBanner(
238+
id = "error-banner",
239+
message = "Error message",
240+
type = AlertBannerType.Error,
241+
eventProcessor = {},
242+
textStyle = TextStyle.Default,
243+
onAlertColor = Color.White,
244+
onDismiss = {},
245+
)
246+
}
247+
}
248+
}

build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
plugins {
22
alias(libs.plugins.androidLibrary) apply false
3+
alias(libs.plugins.androidApplication) apply false
34
alias(libs.plugins.kotlin.multiplatform) apply false
45
alias(libs.plugins.jetbrains.compose) apply false
56
alias(libs.plugins.compose.compiler) apply false
67
alias(libs.plugins.vanniktech.mavenPublish) apply false
8+
alias(libs.plugins.kotlin.android) apply false
79
}

gradle/libs.versions.toml

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
[versions]
2-
activity-compose = "1.9.3"
3-
agp = "8.5.2"
4-
appcompat = "1.7.0"
5-
kotlin = "2.0.20"
2+
activity-compose = "1.10.1"
3+
agp = "8.7.3"
4+
appcompat = "1.7.1"
5+
kotlin = "2.1.20"
66
android-minSdk = "24"
7-
android-compileSdk = "34"
8-
compose-plugin = "1.7.0"
7+
android-compileSdk = "35"
8+
android-targetSdk = "35"
9+
compose-plugin = "1.8.2"
10+
jetpack-compose = "1.8.3"
911
kotlinx-datetime = "0.6.0"
1012
lifecycle-viewmodel = "2.8.2"
1113
vanniktech-maven = "0.30.0"
14+
coreKtx = "1.16.0"
15+
lifecycleRuntimeKtx = "2.9.2"
16+
composeBom = "2025.07.00"
1217

1318
[libraries]
1419
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" }
@@ -17,13 +22,26 @@ androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "a
1722
lifecycle-viewmodel = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle-viewmodel" }
1823

1924
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
20-
2125
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
2226

27+
# Libs for the sample project
28+
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
29+
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
30+
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
31+
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
32+
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
33+
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
34+
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
35+
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
36+
androidx-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "jetpack-compose" }
37+
androidx-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "jetpack-compose" }
38+
2339

2440
[plugins]
2541
androidLibrary = { id = "com.android.library", version.ref = "agp" }
42+
androidApplication = { id = "com.android.application", version.ref = "agp" }
2643
jetbrains-compose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" }
2744
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
45+
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
2846
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
29-
vanniktech-mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "vanniktech-maven" }
47+
vanniktech-mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "vanniktech-maven" }
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
distributionBase=GRADLE_USER_HOME
22
distributionPath=wrapper/dists
3-
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
3+
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
44
networkTimeout=10000
55
zipStoreBase=GRADLE_USER_HOME
66
zipStorePath=wrapper/dists

sample/android/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build

sample/android/build.gradle.kts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
plugins {
2+
alias(libs.plugins.androidApplication)
3+
alias(libs.plugins.kotlin.android)
4+
alias(libs.plugins.compose.compiler)
5+
}
6+
7+
android {
8+
namespace = "com.mofeejegi.alert.sample"
9+
compileSdk = libs.versions.android.compileSdk.get().toInt()
10+
11+
defaultConfig {
12+
applicationId = "com.mofeejegi.alert.sample"
13+
minSdk = 24
14+
targetSdk = libs.versions.android.targetSdk.get().toInt()
15+
versionCode = 1
16+
versionName = "1.0"
17+
18+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
19+
}
20+
21+
buildTypes {
22+
release {
23+
isMinifyEnabled = false
24+
proguardFiles(
25+
getDefaultProguardFile("proguard-android-optimize.txt"),
26+
"proguard-rules.pro"
27+
)
28+
}
29+
}
30+
compileOptions {
31+
sourceCompatibility = JavaVersion.VERSION_17
32+
targetCompatibility = JavaVersion.VERSION_17
33+
}
34+
kotlinOptions {
35+
jvmTarget = "17"
36+
}
37+
buildFeatures {
38+
compose = true
39+
}
40+
}
41+
42+
dependencies {
43+
44+
implementation(libs.androidx.core.ktx)
45+
implementation(libs.androidx.lifecycle.runtime.ktx)
46+
implementation(libs.androidx.activity.compose)
47+
implementation(platform(libs.androidx.compose.bom))
48+
implementation(libs.androidx.ui)
49+
implementation(libs.androidx.ui.graphics)
50+
implementation(libs.androidx.ui.tooling.preview)
51+
implementation(libs.androidx.material3)
52+
53+
implementation(projects.alertBanner)
54+
55+
androidTestImplementation(platform(libs.androidx.compose.bom))
56+
androidTestImplementation(libs.androidx.ui.test.junit4)
57+
debugImplementation(libs.androidx.ui.tooling)
58+
debugImplementation(libs.androidx.ui.test.manifest)
59+
}

sample/android/proguard-rules.pro

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Add project specific ProGuard rules here.
2+
# You can control the set of applied configuration files using the
3+
# proguardFiles setting in build.gradle.
4+
#
5+
# For more details, see
6+
# http://developer.android.com/guide/developing/tools/proguard.html
7+
8+
# If your project uses WebView with JS, uncomment the following
9+
# and specify the fully qualified class name to the JavaScript interface
10+
# class:
11+
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12+
# public *;
13+
#}
14+
15+
# Uncomment this to preserve the line number information for
16+
# debugging stack traces.
17+
#-keepattributes SourceFile,LineNumberTable
18+
19+
# If you keep the line number information, uncomment this to
20+
# hide the original source file name.
21+
#-renamesourcefileattribute SourceFile

0 commit comments

Comments
 (0)