├── app
├── .gitignore
├── src
│ └── main
│ │ ├── res
│ │ ├── drawable
│ │ │ ├── cat.jpg
│ │ │ ├── ic_launcher_foreground.xml
│ │ │ └── ic_launcher_background.xml
│ │ ├── values
│ │ │ ├── strings.xml
│ │ │ ├── themes.xml
│ │ │ └── colors.xml
│ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.webp
│ │ │ └── ic_launcher_round.webp
│ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.webp
│ │ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.webp
│ │ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.webp
│ │ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.webp
│ │ │ └── ic_launcher_round.webp
│ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ └── xml
│ │ │ ├── backup_rules.xml
│ │ │ └── data_extraction_rules.xml
│ │ ├── java
│ │ └── com
│ │ │ └── oussamameg
│ │ │ └── contentrevealdemo
│ │ │ ├── ui
│ │ │ └── theme
│ │ │ │ ├── Color.kt
│ │ │ │ ├── Type.kt
│ │ │ │ └── Theme.kt
│ │ │ └── MainActivity.kt
│ │ └── AndroidManifest.xml
├── proguard-rules.pro
└── build.gradle.kts
├── contentreveal
├── .gitignore
├── src
│ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── com
│ │ └── oussamameg
│ │ └── contentreveal
│ │ ├── ContentRevealScope.kt
│ │ ├── ParticleState.kt
│ │ ├── Particle.kt
│ │ ├── Particles.kt
│ │ └── ContentReveal.kt
├── proguard-rules.pro
└── build.gradle.kts
├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
└── libs.versions.toml
├── .gitignore
├── scripts
└── publish-module.gradle.kts
├── settings.gradle.kts
├── .github
└── workflows
│ └── publish.yml
├── gradle.properties
├── gradlew.bat
├── README.md
├── gradlew
└── LICENSE
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/contentreveal/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/contentreveal/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/cat.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Meglali20/Content-Reveal/HEAD/app/src/main/res/drawable/cat.jpg
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Content Reveal Demo
3 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Meglali20/Content-Reveal/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Meglali20/Content-Reveal/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Meglali20/Content-Reveal/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Meglali20/Content-Reveal/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Meglali20/Content-Reveal/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Meglali20/Content-Reveal/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Meglali20/Content-Reveal/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Meglali20/Content-Reveal/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Meglali20/Content-Reveal/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Meglali20/Content-Reveal/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Meglali20/Content-Reveal/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Jan 08 11:35:10 GMT+01:00 2025
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | .idea
4 | .kotlin
5 | /local.properties
6 | /.idea/caches
7 | /.idea/libraries
8 | /.idea/modules.xml
9 | /.idea/workspace.xml
10 | /.idea/navEditor.xml
11 | /.idea/assetWizardSettings.xml
12 | .DS_Store
13 | /build
14 | /captures
15 | .externalNativeBuild
16 | .cxx
17 | local.properties
18 | /.idea/
19 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/oussamameg/contentrevealdemo/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.oussamameg.contentrevealdemodemo.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/scripts/publish-module.gradle.kts:
--------------------------------------------------------------------------------
1 | apply(plugin = "com.vanniktech.maven.publish.base")
2 |
3 | rootProject.extra.apply {
4 | val majorVersion = 1
5 | val minorVersion = 0
6 | val patchVersion = 0
7 | val snapshot = System.getenv("SNAPSHOT").toBoolean()
8 | val libVersion = if (snapshot) {
9 | "$majorVersion.$minorVersion.${patchVersion + 1}-SNAPSHOT"
10 | } else {
11 | "$majorVersion.$minorVersion.$patchVersion"
12 | }
13 | set("libVersion", libVersion)
14 | }
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google {
4 | content {
5 | includeGroupByRegex("com\\.android.*")
6 | includeGroupByRegex("com\\.google.*")
7 | includeGroupByRegex("androidx.*")
8 | }
9 | }
10 | mavenCentral()
11 | gradlePluginPortal()
12 | }
13 | }
14 | dependencyResolutionManagement {
15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
16 | repositories {
17 | google()
18 | mavenCentral()
19 | }
20 | }
21 |
22 | include(":app")
23 | include(":contentreveal")
24 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
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
--------------------------------------------------------------------------------
/contentreveal/proguard-rules.pro:
--------------------------------------------------------------------------------
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
--------------------------------------------------------------------------------
/contentreveal/src/main/java/com/oussamameg/contentreveal/ContentRevealScope.kt:
--------------------------------------------------------------------------------
1 | package com.oussamameg.contentreveal
2 |
3 | import androidx.compose.runtime.Composable
4 |
5 |
6 | class ContentRevealScope {
7 | private var visibleContentSet = false
8 | private var hiddenContentSet = false
9 |
10 | var visibleContent: (@Composable () -> Unit)? = null
11 | private set
12 | var hiddenContent: (@Composable () -> Unit)? = null
13 | private set
14 |
15 | fun visibleContent(content: @Composable () -> Unit) {
16 | visibleContentSet = true
17 | visibleContent = content
18 | }
19 |
20 | fun hiddenContent(content: @Composable () -> Unit) {
21 | hiddenContentSet = true
22 | hiddenContent = content
23 | }
24 |
25 | internal fun validate() {
26 | require(visibleContentSet) { "Visible Content must be provided" }
27 | require(hiddenContentSet) { "Hidden Content must be provided" }
28 | }
29 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
15 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish
2 |
3 | on:
4 | release:
5 | types: [ released ]
6 | workflow_dispatch:
7 |
8 | jobs:
9 | publish:
10 | name: Snapshot build and publish
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Check out code
14 | uses: actions/checkout@v4.2.2
15 |
16 | - name: Set up JDK 17
17 | uses: actions/setup-java@v4.6.0
18 | with:
19 | distribution: 'zulu'
20 | java-version: 17
21 |
22 | - name: Grant Permission to Execute Gradle
23 | run: chmod +x gradlew
24 |
25 | - name: Publish to MavenCentral
26 | run: |
27 | ./gradlew publishAllPublicationsToMavenCentral --no-configuration-cache
28 | env:
29 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.CENTRAL_USERNAME }}
30 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.CENTRAL_PASSWORD }}
31 | ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.SIGNING_KEY_ID }}
32 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }}
33 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }}
--------------------------------------------------------------------------------
/app/src/main/java/com/oussamameg/contentrevealdemo/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.oussamameg.contentrevealdemodemo.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/contentreveal/src/main/java/com/oussamameg/contentreveal/ParticleState.kt:
--------------------------------------------------------------------------------
1 | package com.oussamameg.contentreveal
2 |
3 | import androidx.compose.runtime.mutableStateListOf
4 | import androidx.compose.ui.unit.IntSize
5 |
6 | /**
7 | * Manages the state of a particle system, maintaining a collection of particles and handling their initialization.
8 | * This class ensures particles persist positioning of particles across recompositions.
9 | */
10 | class ParticleState {
11 | private var _particles = mutableStateListOf()
12 | val particles: List = _particles
13 |
14 | /**
15 | * @param size The size of the container where particles will be rendered (so that the particles don't go out of bounds)
16 | * @param count The number of particles to create
17 | */
18 | fun initializeIfNeeded(size: IntSize, count: Int) {
19 | if (_particles.isEmpty()) {
20 | _particles.addAll(List(count) {
21 | Particle(
22 | currentPosition = generatePosition(size),
23 | targetPosition = generatePosition(size),
24 | initialPosition = generatePosition(size)
25 | )
26 | })
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/java/com/oussamameg/contentrevealdemo/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.oussamameg.contentrevealdemodemo.ui.theme
2 |
3 | import android.os.Build
4 | import androidx.compose.foundation.isSystemInDarkTheme
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.compose.material3.darkColorScheme
7 | import androidx.compose.material3.dynamicDarkColorScheme
8 | import androidx.compose.material3.dynamicLightColorScheme
9 | import androidx.compose.material3.lightColorScheme
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.platform.LocalContext
12 |
13 | private val DarkColorScheme = darkColorScheme(
14 | primary = Purple80,
15 | secondary = PurpleGrey80,
16 | tertiary = Pink80
17 | )
18 |
19 | private val LightColorScheme = lightColorScheme(
20 | primary = Purple40,
21 | secondary = PurpleGrey40,
22 | tertiary = Pink40
23 |
24 | /* Other default colors to override
25 | background = Color(0xFFFFFBFE),
26 | surface = Color(0xFFFFFBFE),
27 | onPrimary = Color.White,
28 | onSecondary = Color.White,
29 | onTertiary = Color.White,
30 | onBackground = Color(0xFF1C1B1F),
31 | onSurface = Color(0xFF1C1B1F),
32 | */
33 | )
34 |
35 | @Composable
36 | fun ContentRevealTheme(
37 | darkTheme: Boolean = isSystemInDarkTheme(),
38 | // Dynamic color is available on Android 12+
39 | dynamicColor: Boolean = true,
40 | content: @Composable () -> Unit
41 | ) {
42 | val colorScheme = when {
43 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
44 | val context = LocalContext.current
45 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
46 | }
47 |
48 | darkTheme -> DarkColorScheme
49 | else -> LightColorScheme
50 | }
51 |
52 | MaterialTheme(
53 | colorScheme = colorScheme,
54 | typography = Typography,
55 | content = content
56 | )
57 | }
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.application)
3 | alias(libs.plugins.kotlin.android)
4 | alias(libs.plugins.kotlin.compose)
5 | }
6 |
7 | android {
8 | namespace = "com.oussamameg.contentrevealdemo"
9 | compileSdk = 35
10 |
11 | defaultConfig {
12 | applicationId = "com.oussamameg.contentrevealdemo"
13 | minSdk = 24
14 | targetSdk = 35
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_11
32 | targetCompatibility = JavaVersion.VERSION_11
33 | }
34 | kotlinOptions {
35 | jvmTarget = "11"
36 | }
37 | buildFeatures {
38 | compose = true
39 | }
40 | }
41 |
42 | dependencies {
43 | implementation(project(":contentreveal"))
44 |
45 | implementation(libs.androidx.core.ktx)
46 | implementation(libs.androidx.lifecycle.runtime.ktx)
47 | implementation(libs.androidx.activity.compose)
48 | implementation(platform(libs.androidx.compose.bom))
49 | implementation(libs.androidx.ui)
50 | implementation(libs.androidx.ui.graphics)
51 | implementation(libs.androidx.ui.tooling.preview)
52 | implementation(libs.androidx.material3)
53 | testImplementation(libs.junit)
54 | androidTestImplementation(libs.androidx.junit)
55 | androidTestImplementation(libs.androidx.espresso.core)
56 | androidTestImplementation(platform(libs.androidx.compose.bom))
57 | androidTestImplementation(libs.androidx.ui.test.junit4)
58 | debugImplementation(libs.androidx.ui.tooling)
59 | debugImplementation(libs.androidx.ui.test.manifest)
60 | }
--------------------------------------------------------------------------------
/contentreveal/src/main/java/com/oussamameg/contentreveal/Particle.kt:
--------------------------------------------------------------------------------
1 | package com.oussamameg.contentreveal
2 |
3 | import androidx.compose.ui.geometry.Offset
4 | import kotlin.random.Random
5 |
6 | /**
7 | * Represents an animated particle with movement and twinkling visual effects.
8 | *
9 | * @property currentPosition Current x,y coordinates of the particle in the animation space
10 | * @property targetPosition Destination point that the particle is moving towards
11 | * @property initialPosition Starting point of the particle's current movement path
12 | * @property opacity Current rendered opacity value, calculated from twinkling animation
13 | * @property scale Current rendered scale factor, calculated from twinkling animation
14 | * @property moveProgress Progress (0.0 to 1.0) of particle's movement from initial to target position
15 | * @property twinkleProgress Progress of the twinkling animation cycle (0.0 to 2π), initialized with random phase
16 | * @property movementDuration Time in seconds for particle to complete one movement path (20-30 seconds)
17 | * @property currentOpacity Base opacity value for twinkling interpolation (0.3-1.0)
18 | * @property targetOpacity Target opacity value that the twinkling animation moves towards (0.3-1.0)
19 | * @property currentScale Base scale value for twinkling interpolation (0.8-1.2)
20 | * @property targetScale Target scale value that the twinkling animation moves towards (0.8-1.2)
21 | */
22 | data class Particle(
23 | var currentPosition: Offset,
24 | var targetPosition: Offset,
25 | var initialPosition: Offset,
26 | var opacity: Float = 1f,
27 | var scale: Float = 1f,
28 | var moveProgress: Float = 0f,
29 | var twinkleProgress: Float = Random.nextFloat(),
30 | var movementDuration: Float = Random.nextFloat() * 10f + 20f,
31 | var currentOpacity: Float = Random.nextFloat() * 0.7f + 0.3f,
32 | var targetOpacity: Float = Random.nextFloat() * 0.7f + 0.3f,
33 | var currentScale: Float = Random.nextFloat() * 0.4f + 0.8f,
34 | var targetScale: Float = Random.nextFloat() * 0.4f + 0.8f
35 | )
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. For more details, visit
12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
24 |
25 | POM_URL=https://github.com/Meglali20/Content-Reveal/
26 | POM_SCM_URL=https://github.com/Meglali20/Content-Reveal/
27 | POM_SCM_CONNECTION=scm:git:git://github.com/Meglali20/Content-Reveal.git
28 | POM_SCM_DEV_CONNECTION=scm:git:git://github.com/Meglali20/Content-Reveal.git
29 |
30 | POM_LICENCE_NAME=The Apache Software License, Version 2.0
31 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt
32 | POM_LICENCE_DIST=repo
33 |
34 | POM_DEVELOPER_ID=meglali20
35 | POM_DEVELOPER_NAME=Oussama Meglali
36 | POM_DEVELOPER_URL=https://github.com/Meglali20/
37 |
38 | SONATYPE_HOST=DEFAULT
39 | RELEASE_SIGNING_ENABLED=true
40 | SONATYPE_AUTOMATIC_RELEASE=true
41 |
42 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | agp = "8.8.0"
3 | kotlin = "2.0.0"
4 | coreKtx = "1.15.0"
5 | junit = "4.13.2"
6 | junitVersion = "1.2.1"
7 | espressoCore = "3.6.1"
8 | lifecycleRuntimeKtx = "2.8.7"
9 | activityCompose = "1.9.3"
10 | composeBom = "2024.12.01"
11 | appcompat = "1.7.0"
12 | material = "1.12.0"
13 | nexusPlugin = "0.30.0"
14 |
15 | [libraries]
16 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
17 | junit = { group = "junit", name = "junit", version.ref = "junit" }
18 | androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
19 | androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
20 | androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
21 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
22 | androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
23 | androidx-ui = { group = "androidx.compose.ui", name = "ui" }
24 | androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
25 | androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
26 | androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
27 | androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
28 | androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
29 | androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
30 | androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
31 | material = { group = "com.google.android.material", name = "material", version.ref = "material" }
32 |
33 | [plugins]
34 | android-application = { id = "com.android.application", version.ref = "agp" }
35 | android-library = { id = "com.android.library", version.ref = "agp" }
36 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
37 | kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
38 | nexus-plugin = { id = "com.vanniktech.maven.publish.base", version.ref = "nexusPlugin" }
39 |
40 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/contentreveal/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.vanniktech.maven.publish.AndroidMultiVariantLibrary
2 | import com.vanniktech.maven.publish.SonatypeHost
3 |
4 | plugins {
5 | alias(libs.plugins.android.library)
6 | alias(libs.plugins.kotlin.android)
7 | alias(libs.plugins.kotlin.compose)
8 | alias(libs.plugins.nexus.plugin)
9 | }
10 |
11 | apply(from = "${rootDir}/scripts/publish-module.gradle.kts")
12 | mavenPublishing {
13 | val artifactId = "content-reveal"
14 |
15 | configure(
16 | AndroidMultiVariantLibrary(
17 | sourcesJar = true
18 | )
19 | )
20 |
21 | publishToMavenCentral(host = SonatypeHost.CENTRAL_PORTAL, automaticRelease = true)
22 | signAllPublications()
23 |
24 | coordinates(
25 | "io.github.meglali20",
26 | artifactId,
27 | rootProject.extra.get("libVersion").toString()
28 | )
29 |
30 | pom {
31 | name.set(artifactId)
32 | description.set("Reveal content with clipping on Android with Jetpack Compose.")
33 | url.set("https://github.com/Meglali20/Content-Reveal")
34 |
35 | licenses {
36 | license {
37 | name.set("The Apache License, Version 2.0")
38 | url.set("http://www.apache.org/licenses/LICENSE-2.0.txt")
39 | }
40 | }
41 |
42 | developers {
43 | developer {
44 | name.set("Oussama Meglali")
45 | organizationUrl.set("https://github.com/Meglali20")
46 | organization.set("Oussama Meglali")
47 | }
48 | }
49 |
50 | scm {
51 | connection.set("scm:git:git://github.com/Meglali20/Content-Reveal.git")
52 | developerConnection.set("scm:git:ssh://github.com/Meglali20/Content-Reveal.git")
53 | url.set("https://github.com/Meglali20/Content-Reveal")
54 | }
55 |
56 | }
57 | }
58 |
59 | android {
60 | namespace = "com.oussamameg.contentreveal"
61 | compileSdk = 35
62 |
63 | defaultConfig {
64 | minSdk = 24
65 | }
66 |
67 | buildTypes {
68 | release {
69 | isMinifyEnabled = false
70 | proguardFiles(
71 | getDefaultProguardFile("proguard-android-optimize.txt"),
72 | "proguard-rules.pro"
73 | )
74 | }
75 | }
76 | compileOptions {
77 | sourceCompatibility = JavaVersion.VERSION_11
78 | targetCompatibility = JavaVersion.VERSION_11
79 | }
80 | kotlinOptions {
81 | jvmTarget = "11"
82 | }
83 | buildFeatures {
84 | compose = true
85 | }
86 | }
87 |
88 | dependencies {
89 |
90 | implementation(libs.androidx.core.ktx)
91 | implementation(libs.androidx.lifecycle.runtime.ktx)
92 | implementation(libs.androidx.activity.compose)
93 | implementation(platform(libs.androidx.compose.bom))
94 | implementation(libs.androidx.ui)
95 | implementation(libs.androidx.ui.graphics)
96 | implementation(libs.androidx.ui.tooling.preview)
97 | implementation(libs.androidx.material3)
98 | testImplementation(libs.junit)
99 | androidTestImplementation(libs.androidx.junit)
100 | androidTestImplementation(libs.androidx.espresso.core)
101 | androidTestImplementation(platform(libs.androidx.compose.bom))
102 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Compose Content Reveal
2 |
3 | A sophisticated reveal effect library for Jetpack Compose that creates an interactive sliding reveal transition between two content layouts with particle effects. Perfect for before/after comparisons, progressive content revelation, and engaging user interactions in Android applications.
4 |
5 |
6 | [](https://search.maven.org/search?q=g:%22io.github.meglali20%22%20AND%20a:%22content-reveal%22)
7 | [](LICENSE)
8 |
9 |
10 | https://github.com/user-attachments/assets/72fa9b5a-ddea-4eb8-b2c4-872f7c7a4115
11 |
12 |
13 | ## Features
14 |
15 | - Interactive touch-enabled reveal with smooth animations
16 | - Progress tracking and callbacks
17 | - Support for various Composable content
18 | - Controllable reveal progress
19 |
20 | ## Installation
21 |
22 | Add the dependency to your module's `build.gradle` file:
23 |
24 | ```gradle
25 | dependencies {
26 | implementation("io.github.meglali20:content-reveal:1.0.0")
27 | }
28 | ```
29 |
30 | ## Usage
31 |
32 | Here's a basic example of how to use ContentReveal:
33 |
34 | ```kotlin
35 | ContentReveal(
36 | modifier = Modifier.fillMaxWidth().height(300.dp),
37 | onRevealProgress = { progress ->
38 | // Handle progress updates
39 | },
40 | showParticles = true,
41 | particleColor = Color(0xFF9FEC5B),
42 | dividerColor = Color(0xFF3990FA)
43 | ) {
44 | visibleContent {
45 | Image(
46 | painter = painterResource(id = R.drawable.before_image),
47 | contentDescription = "Before",
48 | modifier = Modifier.fillMaxSize(),
49 | contentScale = ContentScale.Crop
50 | )
51 | }
52 | hiddenContent {
53 | Image(
54 | painter = painterResource(id = R.drawable.after_image),
55 | contentDescription = "After",
56 | modifier = Modifier.fillMaxSize(),
57 | contentScale = ContentScale.Crop
58 | )
59 | }
60 | }
61 | ```
62 |
63 | ### Advanced Usage
64 |
65 | ```kotlin
66 | ContentReveal(
67 | modifier = Modifier.fillMaxWidth().height(400.dp),
68 | touchEnabled = true,
69 | currentProgress = progress,
70 | animateProgressChange = true,
71 | progressRange = 0..100,
72 | onRevealProgress = { currentProgress ->
73 | // Track progress
74 | },
75 | onFullyRevealed = { isRevealed ->
76 | // Handle fully revealed state
77 | },
78 | dividerRotationEnabled = true,
79 | showParticles = true,
80 | particlesCount = 60,
81 | particlesSpeedMultiplier = 0.8f,
82 | particleColor = Color(0xFF9FEC5B),
83 | clipParticlesWithHiddenContent = true
84 | ) {
85 | visibleContent {
86 | // Your visible content
87 | }
88 | hiddenContent {
89 | // Your hidden content
90 | }
91 | }
92 | ```
93 |
94 | ## Customization
95 |
96 | The ContentReveal composable offers extensive customization options:
97 |
98 | | Parameter | Description | Default |
99 | |-----------|-------------|---------|
100 | | touchEnabled | Enable/disable touch interaction | true |
101 | | currentProgress | Control reveal progress programmatically | 0f |
102 | | animateProgressChange | Animate progress changes | true |
103 | | dividerRotationEnabled | Enable divider rotation effect | true |
104 | | showParticles | Show/hide particle effects | true |
105 | | particlesCount | Number of particles | 60 |
106 | | particleColor | Color of particles | #9FEC5B |
107 |
108 | ## License
109 |
110 | ```
111 | Copyright 2024 [Oussama Meglali]
112 |
113 | Licensed under the Apache License, Version 2.0 (the "License");
114 | you may not use this file except in compliance with the License.
115 | You may obtain a copy of the License at
116 |
117 | http://www.apache.org/licenses/LICENSE-2.0
118 |
119 | Unless required by applicable law or agreed to in writing, software
120 | distributed under the License is distributed on an "AS IS" BASIS,
121 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
122 | See the License for the specific language governing permissions and
123 | limitations under the License.
124 | ```
125 |
126 | ## Contributing
127 |
128 | Contributions are welcome! Please feel free to submit a Pull Request.
129 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/contentreveal/src/main/java/com/oussamameg/contentreveal/Particles.kt:
--------------------------------------------------------------------------------
1 | package com.oussamameg.contentreveal
2 |
3 | import android.graphics.BlurMaskFilter
4 | import android.graphics.Paint
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.runtime.LaunchedEffect
8 | import androidx.compose.runtime.getValue
9 | import androidx.compose.runtime.mutableLongStateOf
10 | import androidx.compose.runtime.mutableStateListOf
11 | import androidx.compose.runtime.remember
12 | import androidx.compose.runtime.setValue
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.draw.drawWithContent
15 | import androidx.compose.ui.geometry.Offset
16 | import androidx.compose.ui.graphics.Color
17 | import androidx.compose.ui.graphics.RadialGradientShader
18 | import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
19 | import androidx.compose.ui.graphics.nativeCanvas
20 | import androidx.compose.ui.unit.IntSize
21 | import kotlinx.coroutines.delay
22 | import kotlin.math.PI
23 | import kotlin.math.sin
24 | import kotlin.random.Random
25 |
26 | /**
27 | * A composable that renders an animated particle system with twinkling effects.
28 | *
29 | * @param particleState State management class that maintains particle positions and properties
30 | * @param size The size of the container where particles will be rendered
31 | * @param particlesCount The number of particles to display (default: 60)
32 | * @param speedMultiplier Controls the speed of both movement and twinkling animations (default: 0.8f)
33 | * Higher values make animations faster, lower values make them slower
34 | * @param particleColor The color of all particles in the system (default: Light green #9FEC5B)
35 | *
36 | * Example usage:
37 | * ```
38 | * val particleState = remember { ParticleState() }
39 | * Particles(
40 | * particleState = particleState,
41 | * size = IntSize(width, height),
42 | * particlesCount = 60,
43 | * speedMultiplier = 0.8f,
44 | * particleColor = Color(0xFF9FEC5B)
45 | * )
46 | * ```
47 | */
48 | @Composable
49 | fun Particles(
50 | particleState: ParticleState,
51 | size: IntSize,
52 | particlesCount: Int = 60,
53 | speedMultiplier: Float = 0.8f,
54 | particleColor: Color = Color(0xFF9FEC5B)
55 | ) {
56 | if (size.width == 0 || size.height == 0) return
57 |
58 | var updateTrigger by remember { mutableLongStateOf(0L) }
59 | LaunchedEffect(Unit) {
60 | particleState.initializeIfNeeded(size, particlesCount)
61 | }
62 |
63 | LaunchedEffect(size, speedMultiplier) {
64 | while (true) {
65 | particleState.particles.forEach { particle ->
66 |
67 | particle.moveProgress += (0.016f * speedMultiplier) / particle.movementDuration
68 | if (particle.moveProgress >= 1f) {
69 | particle.initialPosition = particle.targetPosition
70 | // Generate new target because particle reached target position
71 | particle.targetPosition = generatePosition(size)
72 | particle.moveProgress = 0f
73 | particle.movementDuration = Random.nextFloat() * 10f + 20f
74 | }
75 |
76 | // lerp interpolation between initial and target position
77 | particle.currentPosition = Offset(
78 | x = lerp(
79 | particle.initialPosition.x,
80 | particle.targetPosition.x,
81 | particle.moveProgress
82 | ),
83 | y = lerp(
84 | particle.initialPosition.y,
85 | particle.targetPosition.y,
86 | particle.moveProgress
87 | )
88 | )
89 |
90 | particle.twinkleProgress += 0.016f * speedMultiplier
91 | if (particle.twinkleProgress >= PI.toFloat() * 2) {
92 | particle.twinkleProgress = 0f
93 | particle.targetOpacity = Random.nextFloat() * 0.7f + 0.3f
94 | particle.targetScale = Random.nextFloat() * 0.4f + 0.8f
95 | }
96 |
97 | val twinkleT = (sin(particle.twinkleProgress) + 1) / 2
98 |
99 | particle.opacity = lerp(
100 | particle.currentOpacity,
101 | particle.targetOpacity,
102 | twinkleT
103 | )
104 | particle.scale = lerp(
105 | particle.currentScale,
106 | particle.targetScale,
107 | twinkleT
108 | )
109 |
110 | if (twinkleT >= 0.99f) {
111 | particle.currentOpacity = particle.targetOpacity
112 | particle.currentScale = particle.targetScale
113 | }
114 | }
115 |
116 | updateTrigger = System.currentTimeMillis()
117 | delay(16)
118 | }
119 | }
120 |
121 | Box(
122 | modifier = Modifier.drawWithContent {
123 | updateTrigger
124 | drawIntoCanvas { canvas ->
125 | val nativeCanvas = canvas.nativeCanvas
126 | nativeCanvas.save()
127 | particleState.particles.forEach { particle ->
128 | val paint = Paint().apply {
129 | shader = RadialGradientShader(
130 | center = particle.currentPosition,
131 | radius = 10f * particle.scale,
132 | colors = listOf(
133 | particleColor,
134 | particleColor.copy(alpha = 0.5f),
135 | Color.Transparent,
136 | )
137 | )
138 | alpha = (particle.opacity * 255).toInt()
139 | maskFilter = BlurMaskFilter(
140 | 1.5f,
141 | BlurMaskFilter.Blur.NORMAL
142 | )
143 | }
144 |
145 | nativeCanvas.drawCircle(
146 | particle.currentPosition.x,
147 | particle.currentPosition.y,
148 | 5f * particle.scale,
149 | paint
150 | )
151 | }
152 |
153 | nativeCanvas.restore()
154 | }
155 | }
156 | )
157 | }
158 |
159 | fun randomMove(): Float = (Random.nextFloat() * 4f - 2f)
160 |
161 | fun randomOpacity(): Float = Random.nextFloat()
162 |
163 | fun generatePosition(parentSize: IntSize): Offset {
164 | val x = Random.nextFloat() * parentSize.width
165 | val y = Random.nextFloat() * parentSize.height
166 | val moveX = randomMove()
167 | val moveY = randomMove()
168 | return Offset(x + moveX, y + moveY)
169 | }
170 |
171 |
172 | private fun lerp(start: Float, end: Float, fraction: Float): Float {
173 | return start + (end - start) * fraction
174 | }
175 |
--------------------------------------------------------------------------------
/app/src/main/java/com/oussamameg/contentrevealdemo/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.oussamameg.contentrevealdemo
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.activity.enableEdgeToEdge
7 | import androidx.compose.foundation.Image
8 | import androidx.compose.foundation.background
9 | import androidx.compose.foundation.layout.Arrangement
10 | import androidx.compose.foundation.layout.Box
11 | import androidx.compose.foundation.layout.Column
12 | import androidx.compose.foundation.layout.fillMaxSize
13 | import androidx.compose.foundation.layout.fillMaxWidth
14 | import androidx.compose.foundation.layout.height
15 | import androidx.compose.foundation.layout.padding
16 | import androidx.compose.foundation.shape.RoundedCornerShape
17 | import androidx.compose.material3.Scaffold
18 | import androidx.compose.material3.Slider
19 | import androidx.compose.material3.Text
20 | import androidx.compose.runtime.Composable
21 | import androidx.compose.runtime.getValue
22 | import androidx.compose.runtime.mutableFloatStateOf
23 | import androidx.compose.runtime.mutableStateOf
24 | import androidx.compose.runtime.remember
25 | import androidx.compose.runtime.setValue
26 | import androidx.compose.ui.Alignment
27 | import androidx.compose.ui.Modifier
28 | import androidx.compose.ui.draw.clip
29 | import androidx.compose.ui.graphics.Color
30 | import androidx.compose.ui.layout.ContentScale
31 | import androidx.compose.ui.res.painterResource
32 | import androidx.compose.ui.text.font.FontWeight
33 | import androidx.compose.ui.tooling.preview.Preview
34 | import androidx.compose.ui.unit.dp
35 | import androidx.compose.ui.unit.sp
36 | import com.oussamameg.contentreveal.ContentReveal
37 | import com.oussamameg.contentrevealdemodemo.ui.theme.ContentRevealTheme
38 |
39 | class MainActivity : ComponentActivity() {
40 | override fun onCreate(savedInstanceState: Bundle?) {
41 | super.onCreate(savedInstanceState)
42 | enableEdgeToEdge()
43 | setContent {
44 | ContentRevealTheme {
45 | var progress by remember { mutableFloatStateOf(0f) }
46 | var imageContentRevealed by remember { mutableStateOf(false) }
47 | var imageContentHidden by remember { mutableStateOf(true) }
48 | var textContentRevealed by remember { mutableStateOf(false) }
49 | var textContentHidden by remember { mutableStateOf(true) }
50 | Scaffold { _ ->
51 | Column(
52 | modifier = Modifier.fillMaxSize().padding(15.dp),
53 | verticalArrangement = Arrangement.SpaceEvenly
54 | ) {
55 | Slider(
56 | modifier = Modifier.padding(20.dp),
57 | value = progress,
58 | valueRange = 0f..100f,
59 | onValueChange = {
60 | progress = it
61 | },
62 | enabled = true
63 | )
64 | Text(text="Progress ${progress.toInt()}")
65 |
66 |
67 | ContentReveal(
68 | clipParticlesWithHiddenContent = true,
69 | currentProgress = progress,
70 | progressRange = 0..100,
71 | touchEnabled = true,
72 | particlesCount = 120,
73 | onFullyRevealed = {
74 | imageContentRevealed = it
75 | },
76 | onFullyHidden = {
77 | imageContentHidden = it
78 | },
79 | particleColor = Color(0xFFEFD7CA),
80 | particlesSpeedMultiplier = 2.5f,
81 | dividerColor = Color(0xFF2D251C),
82 | dividerRotationEnabled = false
83 | ) {
84 | val modifier = Modifier
85 | .fillMaxWidth()
86 | .height(350.dp)
87 | .background(Color(0xFF927C6D), shape = RoundedCornerShape(16.dp))
88 | .clip(RoundedCornerShape(16.dp))
89 | visibleContent {
90 | Box(modifier = modifier)
91 | }
92 |
93 | hiddenContent {
94 | Image(
95 | modifier = modifier,
96 | painter = painterResource(R.drawable.cat),
97 | contentDescription = "",
98 | contentScale = ContentScale.Crop
99 | )
100 | }
101 | }
102 | Text(text="Image revealed ? $imageContentRevealed | hidden ? $imageContentHidden")
103 |
104 | Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center){
105 | ContentReveal(
106 | clipParticlesWithHiddenContent = true,
107 | currentProgress = progress,
108 | progressRange = 0..100,
109 | touchEnabled = true,
110 | onFullyRevealed = {
111 | textContentRevealed = it
112 | },
113 | onFullyHidden = {
114 | textContentHidden = it
115 | },
116 | particlesCount = 80,
117 | particleColor = Color(0xFF4280EA),
118 | particlesSpeedMultiplier = 1.2f
119 | ) {
120 | visibleContent {
121 | Text(
122 | modifier = Modifier.padding(5.dp),
123 | text = "Jetpack Compose",
124 | color = Color(0xFF5FD580),
125 | fontSize = 26.sp,
126 | fontWeight = FontWeight.SemiBold
127 | )
128 | }
129 | hiddenContent {
130 | Text(
131 | modifier = Modifier.padding(5.dp),
132 | text = "is AWESOME",
133 | color = Color(0xFF4280EA),
134 | fontSize = 26.sp,
135 | fontWeight = FontWeight.ExtraBold
136 | )
137 | }
138 | }
139 | }
140 | Text(text="Text revealed ? $textContentRevealed | hidden ? $textContentHidden")
141 |
142 | }
143 | }
144 | }
145 | }
146 | }
147 | }
148 |
149 | @Preview(showBackground = true)
150 | @Composable
151 | fun ContentRevealPreview() {
152 | var text by remember { mutableStateOf("") }
153 | ContentReveal(
154 | clipParticlesWithHiddenContent = true,
155 | dividerRotationEnabled = false,
156 | onFullyRevealed = {
157 | if (it)
158 | text = "VISIBLE"
159 | },
160 | onFullyHidden = {
161 | if (it)
162 | text = "HIDDEN"
163 | }
164 | ) {
165 | val modifier = Modifier
166 | .fillMaxWidth()
167 | .height(350.dp)
168 | .background(Color(0xFF927C6D), shape = RoundedCornerShape(16.dp))
169 | .clip(RoundedCornerShape(16.dp))
170 | visibleContent {
171 | Box(modifier = modifier)
172 | /*Text(
173 | text = "This text is passed $text",
174 | color = Color(0xFF93E211),
175 | fontSize = 38.sp,
176 | fontWeight = FontWeight.W200
177 | )*/
178 | }
179 |
180 | hiddenContent {
181 | Image(
182 | modifier = modifier,
183 | painter = painterResource(R.drawable.cat),
184 | contentDescription = "",
185 | contentScale = ContentScale.Crop
186 | )
187 | /*Text(
188 | text = "passed Hidden $text",
189 | color = Color(0xFF7F3D2D),
190 | fontSize = 38.sp,
191 | fontWeight = FontWeight.ExtraBold
192 | )*/
193 | }
194 | }
195 | }
196 |
197 |
198 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/contentreveal/src/main/java/com/oussamameg/contentreveal/ContentReveal.kt:
--------------------------------------------------------------------------------
1 | package com.oussamameg.contentreveal
2 |
3 |
4 | import androidx.compose.animation.core.Animatable
5 | import androidx.compose.animation.core.tween
6 | import androidx.compose.foundation.gestures.awaitEachGesture
7 | import androidx.compose.foundation.gestures.awaitFirstDown
8 | import androidx.compose.foundation.layout.Box
9 | import androidx.compose.foundation.layout.size
10 | import androidx.compose.material3.Text
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.LaunchedEffect
13 | import androidx.compose.runtime.getValue
14 | import androidx.compose.runtime.mutableLongStateOf
15 | import androidx.compose.runtime.mutableStateOf
16 | import androidx.compose.runtime.remember
17 | import androidx.compose.runtime.rememberCoroutineScope
18 | import androidx.compose.runtime.setValue
19 | import androidx.compose.ui.Alignment
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.draw.drawWithContent
22 | import androidx.compose.ui.geometry.Offset
23 | import androidx.compose.ui.geometry.Size
24 | import androidx.compose.ui.graphics.BlendMode
25 | import androidx.compose.ui.graphics.Brush
26 | import androidx.compose.ui.graphics.Color
27 | import androidx.compose.ui.graphics.CompositingStrategy
28 | import androidx.compose.ui.graphics.drawscope.rotate
29 | import androidx.compose.ui.graphics.graphicsLayer
30 | import androidx.compose.ui.input.pointer.PointerEvent
31 | import androidx.compose.ui.input.pointer.PointerInputChange
32 | import androidx.compose.ui.input.pointer.pointerInput
33 | import androidx.compose.ui.layout.onGloballyPositioned
34 | import androidx.compose.ui.layout.positionOnScreen
35 | import androidx.compose.ui.platform.LocalDensity
36 | import androidx.compose.ui.text.font.FontWeight
37 | import androidx.compose.ui.unit.DpSize
38 | import androidx.compose.ui.unit.IntSize
39 | import androidx.compose.ui.unit.dp
40 | import androidx.compose.ui.unit.sp
41 | import kotlinx.coroutines.launch
42 |
43 | /**
44 | * A composable that provides a reveal with a clipping effect between two content layouts with an interactive divider and particle effects.
45 | *
46 | * @param modifier modifier for the box holding this composable
47 | * @param touchEnabled Whether the divider can be dragged by touch input (default: true)
48 | * @param currentProgress Current reveal progress value (default: 0f)
49 | * @param animateProgressChange Whether to animate progress changes (default: true)
50 | * @param progressRange Valid range for the reveal progress value (default: 0..100)
51 | * @param onRevealProgress Callback invoked with current progress value as content is revealed
52 | * @param onFullyHidden Callback invoked when content is fully hidden with boolean indicating if the content is hidden
53 | * @param onFullyRevealed Callback invoked when content is fully revealed with boolean indicating if the hidden content has been revealed
54 | * @param dividerRotationEnabled Whether the divider should slightly rotate during progress change (default: true)
55 | * @param dividerColor Color of the divider (default: #3990FA)
56 | * @param showParticles Whether to display particle effects (default: true)
57 | * @param particlesCount Number of particles to display when enabled (default: 60)
58 | * @param particlesSpeedMultiplier Controls particle animation speed (default: 0.8f)
59 | * @param particleColor Color of the particles (default: #9FEC5B)
60 | * @param clipParticlesWithHiddenContent Whether particles should be clipped to hidden content area (default: true)
61 | * @param content Scoped content block where visible and hidden content are defined
62 | *
63 | * Example usage:
64 | * ```
65 | * ContentReveal(
66 | * onRevealProgress = { progress ->
67 | * // Handle progress updates
68 | * }
69 | * ) {
70 | * visibleContent {
71 | * // Content shown initially Box() Image() Text()....
72 | * }
73 | * hiddenContent {
74 | * // Content to be revealed Box() Image() Text()....
75 | * }
76 | * }
77 | * ```
78 | */
79 | @Composable
80 | fun ContentReveal(
81 | modifier: Modifier = Modifier,
82 | touchEnabled: Boolean = true,
83 | currentProgress: Float = 0f,
84 | animateProgressChange: Boolean = true,
85 | progressRange: ClosedRange = 0..100,
86 | onRevealProgress: (Int) -> Unit = {},
87 | onFullyHidden: (Boolean) -> Unit = {},
88 | onFullyRevealed: (Boolean) -> Unit = {},
89 | dividerRotationEnabled: Boolean = true,
90 | dividerColor: Color = Color(0xFF3990FA),
91 | showParticles: Boolean = true,
92 | particlesCount: Int = 60,
93 | particlesSpeedMultiplier: Float = 0.8f,
94 | particleColor: Color = Color(0xFF9FEC5B),
95 | clipParticlesWithHiddenContent: Boolean = true,
96 | content: @Composable ContentRevealScope.() -> Unit
97 | ) {
98 | val contentScope = ContentRevealScope()
99 | content(contentScope)
100 | contentScope.validate()
101 | var isTouching by remember { mutableStateOf(false) }
102 | val xOffSetPercentageAnimation = remember {
103 | Animatable(
104 | currentProgress.coerceIn(
105 | progressRange.start.toFloat(),
106 | progressRange.endInclusive.toFloat(),
107 | )
108 | )
109 | }
110 |
111 | val scope = rememberCoroutineScope()
112 | var parentOffSet = Offset(0f, 0f)
113 | var parentSize by remember { mutableStateOf(IntSize(0, 0)) }
114 | val revealDividerWidth = 12.dp
115 | var visibleTextSize = IntSize(0, 0)
116 | var hiddenTextSize = IntSize(0, 0)
117 | var visibleTextOffset = Offset(0f, 0f)
118 | var hiddenTextOffset = Offset(0f, 0f)
119 | var lastProgressUpdateTime by remember { mutableLongStateOf(System.currentTimeMillis()) }
120 | val particleState = remember { ParticleState() }
121 |
122 |
123 | fun updateRevealState(offsetPercentage: Float) {
124 | val progress = offsetPercentage.toInt()
125 | onRevealProgress(progress)
126 | onFullyHidden(progress == 0)
127 | onFullyRevealed(progress == 100)
128 | }
129 |
130 | var parentBoxModifier = modifier
131 | .drawWithContent {
132 | drawContent()
133 | val rotation =
134 | if (dividerRotationEnabled) (xOffSetPercentageAnimation.value - 50) * 0.1f else 0f
135 | rotate(degrees = rotation) {
136 | val revealDividerHeight = parentSize.height * 1.5f
137 | drawLine(
138 | brush = Brush.verticalGradient(
139 | listOf(
140 | Color.Transparent,
141 | dividerColor,
142 | Color.Transparent
143 | )
144 | ),
145 | start = Offset((xOffSetPercentageAnimation.value * size.width) / 100, 0f),
146 | end = Offset(
147 | (xOffSetPercentageAnimation.value * size.width) / 100,
148 | revealDividerHeight
149 | ),
150 | strokeWidth = revealDividerWidth.value
151 | )
152 | }
153 |
154 | }
155 | .onGloballyPositioned {
156 | if (parentSize.width != it.size.width || parentSize.height != it.size.height)
157 | parentSize = it.size
158 | parentOffSet = it.positionOnScreen()
159 | }
160 | if (touchEnabled) {
161 | parentBoxModifier = parentBoxModifier.pointerInput(Unit) {
162 | awaitEachGesture {
163 | var actionUpCoordinates = Offset.Zero
164 | // ACTION_DOWN here
165 | val down = awaitFirstDown()
166 | down.consume()
167 | isTouching = true
168 | scope.launch {
169 | val relativeX = down.position.x
170 | val offsetPercentage = ((relativeX / parentSize.width) * 100).coerceIn(
171 | progressRange.start.toFloat(),
172 | progressRange.endInclusive.toFloat(),
173 | )
174 | xOffSetPercentageAnimation.animateTo(
175 | offsetPercentage,
176 | animationSpec = tween(
177 | durationMillis = 600
178 | )
179 | )
180 | updateRevealState(offsetPercentage)
181 | }
182 |
183 | do {
184 | val event: PointerEvent = awaitPointerEvent()
185 | // ACTION_MOVE loop
186 | event.changes.forEach { change: PointerInputChange ->
187 | change.consume()
188 | val relativeX = change.position.x
189 | val offsetPercentage =
190 | ((relativeX / parentSize.width) * 100).coerceIn(
191 | progressRange.start.toFloat(),
192 | progressRange.endInclusive.toFloat(),
193 | )
194 | scope.launch {
195 | xOffSetPercentageAnimation.snapTo(offsetPercentage)
196 | }
197 | updateRevealState(offsetPercentage)
198 | actionUpCoordinates = change.position
199 | }
200 | } while (event.changes.any { it.pressed })
201 | //ACTION_UP
202 | isTouching = false
203 | scope.launch {
204 | val offsetPercentage =
205 | ((actionUpCoordinates.x / parentSize.width) * 100).coerceIn(
206 | progressRange.start.toFloat(),
207 | progressRange.endInclusive.toFloat(),
208 | )
209 | xOffSetPercentageAnimation.animateTo(
210 | offsetPercentage,
211 | animationSpec = tween(
212 | durationMillis = 600
213 | )
214 | )
215 | }
216 | }
217 | }
218 | }
219 |
220 | LaunchedEffect(
221 | currentProgress.coerceIn(
222 | progressRange.start.toFloat(),
223 | progressRange.endInclusive.toFloat(),
224 | )
225 | ) {
226 | scope.launch {
227 | val currentTime = System.currentTimeMillis()
228 | val timeDifference = currentTime - lastProgressUpdateTime
229 | lastProgressUpdateTime = currentTime
230 | val timeThreshold = 100L
231 | val progressPercentage = currentProgress.coerceIn(
232 | progressRange.start.toFloat(),
233 | progressRange.endInclusive.toFloat(),
234 | )
235 | val immediate = timeDifference < timeThreshold
236 | if (immediate || !animateProgressChange) {
237 | xOffSetPercentageAnimation.snapTo(progressPercentage)
238 | } else {
239 | xOffSetPercentageAnimation.animateTo(
240 | progressPercentage,
241 | animationSpec = tween(durationMillis = 850)
242 | )
243 | }
244 | updateRevealState(progressPercentage)
245 | }
246 | }
247 | Box(
248 | modifier = parentBoxModifier, contentAlignment = Alignment.Center
249 | ) {
250 | Box(modifier = Modifier
251 | .graphicsLayer {
252 | compositingStrategy = CompositingStrategy.Offscreen
253 | }
254 | .drawWithContent {
255 | drawContent()
256 | val offsetDiff =
257 | if (hiddenTextSize.width > visibleTextSize.width) (hiddenTextOffset.x - visibleTextOffset.x) else 0f
258 |
259 | val widthPlusOffset =
260 | if (hiddenTextSize.width > visibleTextSize.width) size.width + offsetDiff else size.width
261 |
262 | val relativeXWithOffset =
263 | ((xOffSetPercentageAnimation.value * parentSize.width) / 100) + offsetDiff
264 | val offsetDiffPercentage = (relativeXWithOffset / widthPlusOffset) * 100
265 | drawRect(
266 | color = Color.Red,
267 | size = Size(
268 | ((widthPlusOffset) - (((100 - (offsetDiffPercentage)) * (widthPlusOffset)) / 100)),
269 | size.height
270 | ),
271 | blendMode = BlendMode.Clear
272 | )
273 | }) {
274 | //Visible Content
275 | Box(modifier = Modifier
276 | .onGloballyPositioned {
277 | visibleTextOffset = it.positionOnScreen()
278 | visibleTextSize = it.size
279 | }) {
280 | contentScope.visibleContent?.invoke() ?: Text(
281 | text = "Visible",
282 | color = Color(0xFF3990FA),
283 | fontSize = 30.sp,
284 | fontWeight = FontWeight.Bold
285 | )
286 | }
287 | if (clipParticlesWithHiddenContent)
288 | if (showParticles && parentSize != IntSize.Zero)
289 | Particles(
290 | particleState = particleState,
291 | size = parentSize,
292 | particlesCount = particlesCount,
293 | particleColor = particleColor,
294 | speedMultiplier = particlesSpeedMultiplier
295 | )
296 | }
297 |
298 | if (!clipParticlesWithHiddenContent) {
299 | val density = LocalDensity.current
300 | if (showParticles && parentSize != IntSize.Zero) {
301 | Box(modifier = Modifier.size(
302 | with(density) { DpSize(parentSize.width.toDp(), parentSize.height.toDp()) }
303 | )
304 | ) {
305 | Particles(
306 | particleState = particleState,
307 | size = parentSize,
308 | particlesCount = particlesCount,
309 | particleColor = particleColor,
310 | speedMultiplier = particlesSpeedMultiplier
311 | )
312 | }
313 | }
314 | }
315 |
316 | //Hidden Content
317 | Box(modifier = Modifier
318 | .onGloballyPositioned {
319 | hiddenTextOffset = it.positionOnScreen()
320 | hiddenTextSize = it.size
321 | }
322 | .graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen }
323 | .drawWithContent {
324 | drawContent()
325 | val offsetDiff =
326 | if (hiddenTextSize.width < visibleTextSize.width) (hiddenTextOffset.x - visibleTextOffset.x) else 0f
327 |
328 | val widthPlusOffset =
329 | if (hiddenTextSize.width < visibleTextSize.width) size.width + offsetDiff else size.width
330 |
331 | val relativeXWithOffset =
332 | ((xOffSetPercentageAnimation.value * parentSize.width) / 100) - offsetDiff
333 |
334 | val offsetDiffPercentage = (relativeXWithOffset / widthPlusOffset) * 100
335 |
336 | drawRect(
337 | color = Color.Yellow,
338 | topLeft = Offset((offsetDiffPercentage * widthPlusOffset) / 100, 0f),
339 | size = Size(
340 | widthPlusOffset - ((offsetDiffPercentage * widthPlusOffset) / 100),
341 | size.height
342 | ),
343 | blendMode = BlendMode.Clear
344 | )
345 | }) {
346 | contentScope.hiddenContent?.invoke() ?: Text(
347 | text = "Hidden",
348 | color = Color(0xFFFA39E7),
349 | fontSize = 30.sp,
350 | fontWeight = FontWeight.Bold
351 | )
352 | }
353 |
354 | }
355 | }
356 |
--------------------------------------------------------------------------------