From f44a298d59c146ce1facde80c02bd4a05cf7b64f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:08:56 +0000 Subject: [PATCH 01/36] Initial plan From 0c0268c128340c83cd79e4d87ed00a457a1c7612 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:23:26 +0000 Subject: [PATCH 02/36] Major library improvements: update deps, add XML views support, light source, themes, and animations Co-authored-by: obieda-hussien <184546202+obieda-hussien@users.noreply.github.com> --- README.md | 342 +++++++++---- README_AR.md | 159 +++++++ app/build.gradle | 38 +- app/src/main/AndroidManifest.xml | 3 +- build.gradle | 7 +- gradle.properties | 8 +- gradle/wrapper/gradle-wrapper.properties | 4 +- library-views/build.gradle | 53 +++ library-views/consumer-rules.pro | 2 + library-views/proguard-rules.pro | 14 + library-views/src/main/AndroidManifest.xml | 3 + .../library/views/NeumorphicButton.kt | 442 +++++++++++++++++ .../library/views/NeumorphicCardView.kt | 448 ++++++++++++++++++ .../library/views/NeumorphicView.kt | 430 +++++++++++++++++ library-views/src/main/res/values/attrs.xml | 95 ++++ library/build.gradle | 36 +- library/src/main/AndroidManifest.xml | 3 +- .../nikhilchaudhari/library/NeuExtensions.kt | 125 +++++ .../me/nikhilchaudhari/library/NeuTheme.kt | 117 +++++ .../me/nikhilchaudhari/library/Neumorphic.kt | 79 ++- .../nikhilchaudhari/library/internal/Blur.kt | 32 +- .../nikhilchaudhari/library/shapes/Drawing.kt | 129 +++-- .../library/shapes/ShapeConfig.kt | 4 +- settings.gradle | 9 + 24 files changed, 2386 insertions(+), 196 deletions(-) create mode 100644 README_AR.md create mode 100644 library-views/build.gradle create mode 100644 library-views/consumer-rules.pro create mode 100644 library-views/proguard-rules.pro create mode 100644 library-views/src/main/AndroidManifest.xml create mode 100644 library-views/src/main/java/me/nikhilchaudhari/library/views/NeumorphicButton.kt create mode 100644 library-views/src/main/java/me/nikhilchaudhari/library/views/NeumorphicCardView.kt create mode 100644 library-views/src/main/java/me/nikhilchaudhari/library/views/NeumorphicView.kt create mode 100644 library-views/src/main/res/values/attrs.xml create mode 100644 library/src/main/java/me/nikhilchaudhari/library/NeuExtensions.kt create mode 100644 library/src/main/java/me/nikhilchaudhari/library/NeuTheme.kt diff --git a/README.md b/README.md index c13e7cff..b0584c58 100644 --- a/README.md +++ b/README.md @@ -1,128 +1,286 @@ -# Neumorphism UI with Jetpack Compose -This library is an experimentation with [Neumorphism UI](https://uxdesign.cc/neumorphism-in-user-interfaces-b47cef3bf3a6) (New + Skeuomorphism) on Android with **Jetpack Compose**. +# Neumorphism UI Library for Android -[![](https://img.shields.io/badge/mavencentral-1.0.0--alpha02-brightgreen)]() ![List of Awesome List Badge](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg) [![Awesome Kotlin Badge](https://kotlin.link/awesome-kotlin.svg)](https://github.com/KotlinBy/awesome-kotlin) +A modern, flexible Neumorphism UI library for Android supporting both **Jetpack Compose** and traditional **XML/Java Android Views**. +[![](https://img.shields.io/badge/mavencentral-2.0.0-brightgreen)]() ![List of Awesome List Badge](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg) [![Awesome Kotlin Badge](https://kotlin.link/awesome-kotlin.svg)](https://github.com/KotlinBy/awesome-kotlin)

-## How to use? +## Features ✨ -### 1. Add the dependency +- 🎨 **Jetpack Compose Support** - Use the `neumorphic()` modifier with any composable +- 📱 **XML/Java Views Support** - Traditional Android Views (NeumorphicView, NeumorphicButton, NeumorphicCardView) +- 🌓 **Dark Theme Support** - Built-in light and dark theme color schemes +- 🎭 **Material You Integration** - Dynamic colors on Android 12+ +- 💫 **Animation Support** - Smooth press animations +- 🔆 **Configurable Light Source** - TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT +- 🛠 **Modern Implementation** - Migrated from deprecated RenderScript to StackBlur -And your app level `build.gradle`. (Make sure you have compose dependencies as well) +## Installation + +### Jetpack Compose Library + +Add to your app level `build.gradle`: ```kotlin - implementation("me.nikhilchaudhari:composeNeumorphism:1.0.0-alpha02") +implementation("me.nikhilchaudhari:composeNeumorphism:2.0.0") ``` -### 2. Use +### XML/Views Library -Just add `modifier = Modifier.neumorphic()` to any of your UI element of Jetpack-Compose (just like you do for other modifiers). +```kotlin +implementation("me.nikhilchaudhari:neumorphismViews:2.0.0") +``` -![image](https://user-images.githubusercontent.com/16976114/120097042-12adfe00-c14c-11eb-861c-a826108194d6.png) +## Quick Start +### Jetpack Compose -## How do I configure shape, color, etc.? +```kotlin +// Basic usage +Card( + modifier = Modifier + .padding(16.dp) + .size(200.dp) + .neumorphic() +) { + // Your content +} + +// With customization +Button( + modifier = Modifier + .neumorphic( + neuShape = Punched.Rounded(radius = 12.dp), + elevation = 8.dp, + lightShadowColor = Color.White, + darkShadowColor = Color.Gray, + lightSource = LightSource.TOP_LEFT + ) +) { + Text("Click Me") +} +``` -### 1. Shapes -You've three basic **Neu** shapes available - -1. `Punched`, 2. `Pressed`, 3. `Pot` +### XML Layout + +```xml + + + + + + + +``` -These are not standard names for Neumorphism UI, I just named them based on how they look (at least to me). +### Java Usage -And you can also configure the corner families of those shapes as **`Oval`** or **`Rounded`** +```java +NeumorphicButton button = new NeumorphicButton(context); +button.setNeuShapeType(NeuShapeType.PUNCHED); +button.setNeuCornerRadius(dpToPx(12)); +button.setNeuElevation(dpToPx(8)); +button.setNeuLightSource(LightSource.TOP_LEFT); +``` + +## Shapes + +Three neumorphic shapes are available: + +| Shape | Description | Visual | +|-------|-------------|--------| +| `Punched` | Raised/bumped effect | `__/‾‾‾‾‾‾‾\__` | +| `Pressed` | Depressed/sunken effect | `‾‾\________/‾‾` | +| `Pot` | Combined raised + depressed | `_/\‾‾‾‾‾/\_` | -![image](https://user-images.githubusercontent.com/16976114/120097777-03c94a80-c150-11eb-847b-3a9547f3a56d.png) +Each shape supports two corner types: +- **Rounded** - Configurable corner radius +- **Oval** - Circular/elliptical shape ```kotlin -Card( - backgroundColor = Color(236, 234, 235), - shape = RoundedCornerShape(12.dp), - modifier = Modifier - .padding(16.dp) - .size(300.dp, 100.dp) - .neumorphic( - neuShape = - // Punched shape - Punched.Rounded(radius = 8.dp) - Punched.Oval() - - // Presssed Shape - Pressed.Rounded(radius = 4.dp) - Presssed.Oval() - - // Pot shape - Pot.Rounded(radius = 8.dp) - Pot.Oval() - ) - ){/*card content */} +// Compose +Punched.Rounded(radius = 12.dp) +Punched.Oval() +Pressed.Rounded(radius = 8.dp) +Pressed.Oval() +Pot.Rounded(radius = 16.dp) +Pot.Oval() +``` + +## Configuration Options + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `neuShape` | `Punched.Rounded(12.dp)` | Shape type and corner configuration | +| `lightShadowColor` | `Color.White` | Light shadow color | +| `darkShadowColor` | `Color.LightGray` | Dark shadow color | +| `elevation` | `6.dp` | Shadow depth/elevation | +| `strokeWidth` | `6.dp` | Inner shadow stroke width | +| `neuInsets` | `NeuInsets(6.dp, 6.dp)` | Shadow insets (horizontal, vertical) | +| `lightSource` | `LightSource.TOP_LEFT` | Direction of light source | + +## Light Source + +Configure the direction of the light source to change shadow placement: + +```kotlin +// Compose +Modifier.neumorphic( + lightSource = LightSource.TOP_LEFT // Default + // or + lightSource = LightSource.TOP_RIGHT + lightSource = LightSource.BOTTOM_LEFT + lightSource = LightSource.BOTTOM_RIGHT +) +``` + +## Theme Integration + +### Using Theme Colors + +```kotlin +@Composable +fun ThemedCard() { + val colorScheme = NeuTheme.colorScheme() // Auto light/dark + + Card( + backgroundColor = colorScheme.backgroundColor, + modifier = Modifier.themedNeumorphic(colorScheme) + ) { + // Content + } +} +``` + +### Material You Dynamic Colors (Android 12+) + +```kotlin +@Composable +fun DynamicThemedCard() { + val colorScheme = NeuTheme.dynamicColorScheme() + + Card( + backgroundColor = colorScheme.backgroundColor, + modifier = Modifier.themedNeumorphic(colorScheme) + ) { + // Content + } +} ``` -For `Rounded` corner-family, you can configure the radius of how much rounded corner you want. The default Neu shape is `Punched.Rounded(radius = 12.dp)`. -The `Punched` shape would work with Cards, Buttons, etc. -You should use `Modifier.clip()` while using `Pressed` shape, because there's known issue of shadow placeents for Pressed shape. Check the sample app for more code snippets. - -P.S: Even I'm yet to experiment it with all UI elements. - -### 2. Shadows - -The idea of Neumorphism UI is simple, just create two shadows light and dark and draw those around the UI component from top-left and bottom-right corners. -**Just make sure you are using the same colors for your Surface and for the UI elements.** -You can configure the shadow `insets`, `color` and `elevation` - -Params | Description -------------------- | ------------- -`lightShadowColor` | Default value - `Color.White` - Set the light shadow color you want -`darkShadowColor` | Default value - `Color.Gray` - Set the dark shadow color you want. -`elevation` | Default value - `6.dp` - Set the elevation for the shadow. -`neuInsets` | Default values - `NeuInsets(6.dp, 6.dp)` - Insets = Horizontal, Vertical - Placements of your shadows i.e How do you want to place your | shadows vertically and horizontally. -`strokeWidth` | Default value - `6.dp` - Stroke width of the internal shadows. Stroke width is for only `Pressed` and `Punched`. - - - ```kotlin - Button( - onClick = { /*TODO*/ }, - modifier = Modifier - .padding(12.dp) - .neumorphic( - // assing neuShape - //... - lightShadowColor = Color.White, - darkShadowColor = Color.Gray, - elevation = 4.dp, - strokeWidth: Dp = 6.dp, - neuInsets = NeuInsets(6.dp, 6.dp) - ) - ) +### Custom Color Scheme + +```kotlin +val customScheme = NeuTheme.customColorScheme( + backgroundColor = Color(0xFFE0E5EC) +) ``` -The overall configutation -![image](https://user-images.githubusercontent.com/16976114/120100461-780aea80-c15e-11eb-9626-10b684f9c982.png) +## Animation +### Animated Press Effect (Compose) -## ToDos - - [ ] Add `LightSource` option and place the shadows accordingly (It is, by default, top-left right now) - - [ ] Migrate from RenderScript - - [ ] Fix clipping and shadow positioning with insets - - [ ] Update sample app and code with Jetpack-Compose guidelines +```kotlin +Modifier.animatedNeumorphic( + neuShape = Punched.Rounded(), + elevation = 8.dp, + pressed = isPressed, // from interaction state + animationDuration = 150 +) +``` -## FAQs -### What's the idea? Is there any performance overhead? -Idea is simple - just draw the two shadows light and dark around the UI element. I'm using [RenderScript](https://developer.android.com/guide/topics/renderscript/compute) to blur two GradientDrawables. I know it's going to be deprecated in the Android 12. I'll surely update the code to migrate from RenderScript. If Renderscript throws any exception / not able to blur the drawable, I'm using [StackBlur](https://github.com/CuriousNikhil/neumorphic-compose/blob/main/library/src/main/java/me/nikhilchaudhari/library/internal/StackBlur.kt) algorithm from Mario Klingemann as a workaround. +### Clickable with Animation -### Does this library work with all the Jetpack Compose UI elements? -I can't surely say. I'm already experimenting drawing shadow on UIs with Jetpack Compose and improving this library. I request you to try and please help to make it work. +```kotlin +Modifier.neumorphicClickable( + onClick = { /* action */ }, + elevation = 8.dp, + neuShape = Punched.Rounded() +) +``` -### When can I use this? -You can use this right now if you want to play with shiny Neumorphism UI. The library is in alpha and there are some known issues regarding shadow rendering and clipping. Please raise issue if you found any. +### XML Views Animation -## Contribution Guide -Please contribute! I'm just getting my hands dirty with Jetpack-Compose. -There is heavy chance that the code may/may not be correct/holding best practices. I request you to contribute/ raise issues/ send PRs so I can learn too. -You can use the Github **Discussion** to discuss and ask questions. Or you can reach out to me on Twitter [@CuriousNikhil](https://twitter.com/curiousnikhil). +Buttons have built-in press animations. Enable/disable with: + +```kotlin +button.enablePressAnimation = true +``` + +## Utility Extensions + +```kotlin +// Soft neumorphic (reduced elevation) +Modifier.softNeumorphic() + +// Deep neumorphic (increased elevation) +Modifier.deepNeumorphic() + +// Generate shadow colors from background +val (lightShadow, darkShadow) = backgroundColor.toNeuColors() + +// Lighten/darken colors +val lighter = color.lighten(0.2f) +val darker = color.darken(0.2f) +``` + +## Best Practices + +1. **Use matching colors**: Background and shadow colors should be similar for best effect +2. **Avoid pure white/black**: Use off-white and dark gray for realistic shadows +3. **Consider light source**: Keep light source consistent across your UI +4. **Use appropriate elevation**: 4-12dp works best for most cases +5. **Clip for Pressed shape**: Use `Modifier.clip()` when using `Pressed` shape + +## Migration from v1.x + +```kotlin +// v1.x +Modifier.neumorphic( + neuShape = Punched.Rounded(), + elevation = 6.dp +) + +// v2.0 - same API, new features +Modifier.neumorphic( + neuShape = Punched.Rounded(), + elevation = 6.dp, + lightSource = LightSource.TOP_LEFT // NEW +) +``` + +## Requirements + +- **Minimum SDK**: 21 (Android 5.0) +- **Target SDK**: 34 (Android 14) +- **Compose**: 1.5.4+ +- **Kotlin**: 1.9.20+ ## License + Licensed under Apache License, Version 2.0 [here](https://github.com/CuriousNikhil/neumorphic-compose/blob/main/LICENSE) + +## Contributing + +Contributions are welcome! Please feel free to submit issues and pull requests. + +## Acknowledgments + +- Stack Blur Algorithm by Mario Klingemann +- Original Neumorphism design concept from Alexander Plyuto diff --git a/README_AR.md b/README_AR.md new file mode 100644 index 00000000..d6b63643 --- /dev/null +++ b/README_AR.md @@ -0,0 +1,159 @@ +
+ +# مكتبة Neumorphism UI لأندرويد + +مكتبة حديثة ومرنة لواجهة Neumorphism لأندرويد تدعم كل من **Jetpack Compose** و **Views/XML** التقليدية. + +## المميزات ✨ + +- 🎨 **دعم Jetpack Compose** - استخدم الـ `neumorphic()` modifier مع أي composable +- 📱 **دعم XML/Java Views** - NeumorphicView, NeumorphicButton, NeumorphicCardView +- 🌓 **دعم الوضع الداكن** - مخططات ألوان مدمجة للوضع الفاتح والداكن +- 🎭 **دعم Material You** - ألوان ديناميكية على أندرويد 12+ +- 💫 **دعم الأنيميشن** - تأثيرات ضغط سلسة +- 🔆 **مصدر الإضاءة قابل للتخصيص** - أعلى اليسار، أعلى اليمين، أسفل اليسار، أسفل اليمين +- 🛠 **تنفيذ حديث** - انتقال من RenderScript القديم إلى StackBlur + +## التثبيت + +### مكتبة Jetpack Compose + +```kotlin +implementation("me.nikhilchaudhari:composeNeumorphism:2.0.0") +``` + +### مكتبة XML/Views + +```kotlin +implementation("me.nikhilchaudhari:neumorphismViews:2.0.0") +``` + +## البداية السريعة + +### Jetpack Compose + +```kotlin +// استخدام بسيط +Card( + modifier = Modifier + .padding(16.dp) + .size(200.dp) + .neumorphic() +) { + // المحتوى +} + +// مع التخصيص +Button( + modifier = Modifier + .neumorphic( + neuShape = Punched.Rounded(radius = 12.dp), + elevation = 8.dp, + lightShadowColor = Color.White, + darkShadowColor = Color.Gray, + lightSource = LightSource.TOP_LEFT + ) +) { + Text("اضغط هنا") +} +``` + +### XML Layout + +```xml + +``` + +### استخدام Java + +```java +NeumorphicButton button = new NeumorphicButton(context); +button.setNeuShapeType(NeuShapeType.PUNCHED); +button.setNeuCornerRadius(dpToPx(12)); +button.setNeuElevation(dpToPx(8)); +``` + +## الأشكال + +ثلاثة أشكال متاحة: + +| الشكل | الوصف | +|-------|--------| +| `Punched` | تأثير بارز/مرتفع | +| `Pressed` | تأثير مضغوط/غائر | +| `Pot` | مزيج من البارز والغائر | + +## خيارات التخصيص + +| المعامل | القيمة الافتراضية | الوصف | +|---------|-------------------|--------| +| `neuShape` | `Punched.Rounded(12.dp)` | نوع الشكل | +| `lightShadowColor` | `Color.White` | لون الظل الفاتح | +| `darkShadowColor` | `Color.LightGray` | لون الظل الداكن | +| `elevation` | `6.dp` | عمق الظل | +| `lightSource` | `LightSource.TOP_LEFT` | اتجاه مصدر الإضاءة | + +## دعم الثيمات + +### استخدام ألوان الثيم + +```kotlin +@Composable +fun ThemedCard() { + val colorScheme = NeuTheme.colorScheme() // فاتح/داكن تلقائياً + + Card( + backgroundColor = colorScheme.backgroundColor, + modifier = Modifier.themedNeumorphic(colorScheme) + ) { + // المحتوى + } +} +``` + +### ألوان Material You الديناميكية (أندرويد 12+) + +```kotlin +@Composable +fun DynamicThemedCard() { + val colorScheme = NeuTheme.dynamicColorScheme() + + Card( + backgroundColor = colorScheme.backgroundColor, + modifier = Modifier.themedNeumorphic(colorScheme) + ) { + // المحتوى + } +} +``` + +## أفضل الممارسات + +1. **استخدم ألوان متطابقة**: يجب أن تكون ألوان الخلفية والظل متشابهة +2. **تجنب الأبيض/الأسود النقي**: استخدم ألوان رمادية للحصول على ظلال واقعية +3. **حافظ على اتساق مصدر الإضاءة**: اجعل مصدر الإضاءة ثابتاً في واجهتك +4. **استخدم elevation مناسب**: 4-12dp يعمل بشكل أفضل لمعظم الحالات + +## المتطلبات + +- **الحد الأدنى للـ SDK**: 21 (أندرويد 5.0) +- **الـ SDK المستهدف**: 34 (أندرويد 14) +- **Compose**: 1.5.4+ +- **Kotlin**: 1.9.20+ + +## الرخصة + +مرخص تحت Apache License, Version 2.0 + +## المساهمة + +المساهمات مرحب بها! لا تتردد في تقديم issues و pull requests. + +
diff --git a/app/build.gradle b/app/build.gradle index e8415852..b36f33f3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,15 +4,15 @@ plugins { } android { - compileSdk 30 - buildToolsVersion "30.0.3" + namespace 'me.nikhilchaudhari.neumorphiccompose' + compileSdk 34 defaultConfig { applicationId "me.nikhilchaudhari.neumorphiccompose" minSdk 21 - targetSdk 30 - versionCode 1 - versionName "1.0" + targetSdk 34 + versionCode 2 + versionName "2.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -27,36 +27,36 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '1.8' - useIR = true + jvmTarget = '17' } buildFeatures { compose true } composeOptions { - kotlinCompilerExtensionVersion compose_version - kotlinCompilerVersion kotlin_version + kotlinCompilerExtensionVersion compose_compiler_version } } dependencies { - implementation 'androidx.core:core-ktx:1.6.0' - implementation 'androidx.appcompat:appcompat:1.3.1' - implementation 'com.google.android.material:material:1.4.0' + implementation 'androidx.core:core-ktx:1.12.0' + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.11.0' implementation "androidx.compose.ui:ui:$compose_version" implementation "androidx.compose.material:material:$compose_version" + implementation "androidx.compose.material3:material3:1.1.2" implementation "androidx.compose.ui:ui-tooling:$compose_version" - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' - implementation 'androidx.activity:activity-compose:1.3.1' + implementation "androidx.compose.animation:animation:$compose_version" + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2' + implementation 'androidx.activity:activity-compose:1.8.2' implementation project(path: ':library') // implementation "me.nikhilchaudhari:composeNeumorphism:1.0.0-alpha" - testImplementation 'junit:junit:4.+' - androidTestImplementation 'androidx.test.ext:junit:1.1.3' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d8002eaa..030443c4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + ; +} diff --git a/library-views/src/main/AndroidManifest.xml b/library-views/src/main/AndroidManifest.xml new file mode 100644 index 00000000..9a40236b --- /dev/null +++ b/library-views/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/library-views/src/main/java/me/nikhilchaudhari/library/views/NeumorphicButton.kt b/library-views/src/main/java/me/nikhilchaudhari/library/views/NeumorphicButton.kt new file mode 100644 index 00000000..6501cea8 --- /dev/null +++ b/library-views/src/main/java/me/nikhilchaudhari/library/views/NeumorphicButton.kt @@ -0,0 +1,442 @@ +package me.nikhilchaudhari.library.views + +import android.content.Context +import android.graphics.* +import android.graphics.drawable.GradientDrawable +import android.util.AttributeSet +import android.view.MotionEvent +import androidx.appcompat.widget.AppCompatButton +import me.nikhilchaudhari.library.LightSource +import me.nikhilchaudhari.library.internal.BlurMaker +import me.nikhilchaudhari.library.internal.stackBlur +import kotlin.math.min +import kotlin.math.roundToInt + +/** + * A Button with neumorphic styling for traditional Android Views/XML layouts + * + * Usage in XML: + * ```xml + * + * ``` + * + * Usage in Java: + * ```java + * NeumorphicButton button = new NeumorphicButton(context); + * button.setNeuShapeType(NeuShapeType.PUNCHED); + * button.setNeuElevation(dpToPx(8)); + * ``` + */ +class NeumorphicButton @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = android.R.attr.buttonStyle +) : AppCompatButton(context, attrs, defStyleAttr) { + + // Neumorphic properties + var neuShapeType: NeuShapeType = NeuShapeType.PUNCHED + set(value) { + field = value + invalidate() + } + + var neuCornerType: NeuCornerType = NeuCornerType.ROUNDED + set(value) { + field = value + invalidate() + } + + var neuCornerRadius: Float = 12f.dpToPx() + set(value) { + field = value + invalidate() + } + + var neuLightShadowColor: Int = Color.WHITE + set(value) { + field = value + invalidate() + } + + var neuDarkShadowColor: Int = Color.LTGRAY + set(value) { + field = value + invalidate() + } + + var neuElevation: Float = 6f.dpToPx() + set(value) { + field = value + invalidate() + } + + var neuStrokeWidth: Float = 6f.dpToPx() + set(value) { + field = value + invalidate() + } + + var neuInsetHorizontal: Float = 6f.dpToPx() + set(value) { + field = value + invalidate() + } + + var neuInsetVertical: Float = 6f.dpToPx() + set(value) { + field = value + invalidate() + } + + var neuLightSource: LightSource = LightSource.TOP_LEFT + set(value) { + field = value + invalidate() + } + + var neuBackgroundColor: Int = Color.parseColor("#ECEAEB") + set(value) { + field = value + invalidate() + } + + // Animation properties + var enablePressAnimation: Boolean = true + private var isPressed = false + private var currentElevation: Float = neuElevation + + // Internal blur maker + private var blurMaker: BlurMaker? = null + + // Cached bitmaps + private var lightShadowBitmap: Bitmap? = null + private var darkShadowBitmap: Bitmap? = null + private var foregroundShadowBitmap: Bitmap? = null + private var needsRedraw = true + + // Paint objects + private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG) + private val shadowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + isFilterBitmap = true + } + + init { + blurMaker = BlurMaker(context, calculateDefaultBlurRadius()) + + // Parse XML attributes + attrs?.let { + val typedArray = context.obtainStyledAttributes(it, R.styleable.NeumorphicButton) + + neuShapeType = when (typedArray.getInt(R.styleable.NeumorphicButton_neuShape, 0)) { + 0 -> NeuShapeType.PUNCHED + 1 -> NeuShapeType.PRESSED + 2 -> NeuShapeType.POT + else -> NeuShapeType.PUNCHED + } + + neuCornerType = when (typedArray.getInt(R.styleable.NeumorphicButton_neuCornerType, 0)) { + 0 -> NeuCornerType.ROUNDED + 1 -> NeuCornerType.OVAL + else -> NeuCornerType.ROUNDED + } + + neuCornerRadius = typedArray.getDimension( + R.styleable.NeumorphicButton_neuCornerRadius, 12f.dpToPx() + ) + + neuLightShadowColor = typedArray.getColor( + R.styleable.NeumorphicButton_neuLightShadowColor, Color.WHITE + ) + + neuDarkShadowColor = typedArray.getColor( + R.styleable.NeumorphicButton_neuDarkShadowColor, Color.LTGRAY + ) + + neuElevation = typedArray.getDimension( + R.styleable.NeumorphicButton_neuElevation, 6f.dpToPx() + ) + currentElevation = neuElevation + + neuStrokeWidth = typedArray.getDimension( + R.styleable.NeumorphicButton_neuStrokeWidth, 6f.dpToPx() + ) + + neuInsetHorizontal = typedArray.getDimension( + R.styleable.NeumorphicButton_neuInsetHorizontal, 6f.dpToPx() + ) + + neuInsetVertical = typedArray.getDimension( + R.styleable.NeumorphicButton_neuInsetVertical, 6f.dpToPx() + ) + + neuLightSource = when (typedArray.getInt(R.styleable.NeumorphicButton_neuLightSource, 0)) { + 0 -> LightSource.TOP_LEFT + 1 -> LightSource.TOP_RIGHT + 2 -> LightSource.BOTTOM_LEFT + 3 -> LightSource.BOTTOM_RIGHT + else -> LightSource.TOP_LEFT + } + + neuBackgroundColor = typedArray.getColor( + R.styleable.NeumorphicButton_neuBackgroundColor, Color.parseColor("#ECEAEB") + ) + + typedArray.recycle() + } + + // Remove default button background + background = null + stateListAnimator = null + + // Set layer type for proper shadow rendering + setLayerType(LAYER_TYPE_SOFTWARE, null) + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + needsRedraw = true + invalidateShadowBitmaps() + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + if (enablePressAnimation && isEnabled) { + when (event.action) { + MotionEvent.ACTION_DOWN -> { + isPressed = true + currentElevation = neuElevation * 0.5f + needsRedraw = true + invalidate() + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + isPressed = false + currentElevation = neuElevation + needsRedraw = true + invalidate() + } + } + } + return super.onTouchEvent(event) + } + + override fun onDraw(canvas: Canvas) { + if (width <= 0 || height <= 0) return + + if (needsRedraw) { + generateShadowBitmaps() + needsRedraw = false + } + + when (neuShapeType) { + NeuShapeType.PUNCHED -> drawPunched(canvas) + NeuShapeType.PRESSED -> drawPressed(canvas) + NeuShapeType.POT -> drawPot(canvas) + } + + super.onDraw(canvas) + } + + private fun drawPunched(canvas: Canvas) { + drawBackgroundShadows(canvas) + drawNeumorphicBackground(canvas) + } + + private fun drawPressed(canvas: Canvas) { + drawNeumorphicBackground(canvas) + drawForegroundShadows(canvas) + } + + private fun drawPot(canvas: Canvas) { + drawBackgroundShadows(canvas) + drawNeumorphicBackground(canvas) + drawForegroundShadows(canvas) + } + + private fun drawBackgroundShadows(canvas: Canvas) { + val (lightOffsetX, lightOffsetY) = getLightOffset() + val (darkOffsetX, darkOffsetY) = getDarkOffset() + + lightShadowBitmap?.let { + canvas.drawBitmap( + it, + -neuInsetHorizontal - currentElevation + lightOffsetX, + -neuInsetVertical - currentElevation + lightOffsetY, + shadowPaint + ) + } + + darkShadowBitmap?.let { + canvas.drawBitmap( + it, + neuInsetHorizontal - currentElevation + darkOffsetX, + neuInsetVertical - currentElevation + darkOffsetY, + shadowPaint + ) + } + } + + private fun drawForegroundShadows(canvas: Canvas) { + foregroundShadowBitmap?.let { + canvas.drawBitmap(it, 0f, 0f, shadowPaint) + } + } + + private fun drawNeumorphicBackground(canvas: Canvas) { + backgroundPaint.color = neuBackgroundColor + + when (neuCornerType) { + NeuCornerType.OVAL -> { + canvas.drawOval( + 0f, 0f, width.toFloat(), height.toFloat(), + backgroundPaint + ) + } + NeuCornerType.ROUNDED -> { + canvas.drawRoundRect( + 0f, 0f, width.toFloat(), height.toFloat(), + neuCornerRadius, neuCornerRadius, + backgroundPaint + ) + } + } + } + + private fun generateShadowBitmaps() { + invalidateShadowBitmaps() + + if (width <= 0 || height <= 0) return + + lightShadowBitmap = generateShadowBitmap(neuLightShadowColor) + darkShadowBitmap = generateShadowBitmap(neuDarkShadowColor) + + if (neuShapeType == NeuShapeType.PRESSED || neuShapeType == NeuShapeType.POT) { + foregroundShadowBitmap = generateForegroundShadowBitmap() + } + } + + private fun generateShadowBitmap(shadowColor: Int): Bitmap? { + val bitmapWidth = (width + currentElevation * 2).roundToInt() + val bitmapHeight = (height + currentElevation * 2).roundToInt() + + if (bitmapWidth <= 0 || bitmapHeight <= 0) return null + + val drawable = GradientDrawable().apply { + setColor(shadowColor) + setSize(width, height) + setBounds(0, 0, width, height) + when (neuCornerType) { + NeuCornerType.OVAL -> shape = GradientDrawable.OVAL + NeuCornerType.ROUNDED -> { + shape = GradientDrawable.RECTANGLE + cornerRadius = neuCornerRadius + } + } + } + + val bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + canvas.translate(currentElevation, currentElevation) + drawable.draw(canvas) + + return blurMaker?.blur(bitmap) ?: bitmap.stackBlur(calculateDefaultBlurRadius()) + } + + private fun generateForegroundShadowBitmap(): Bitmap? { + if (width <= 0 || height <= 0) return null + + val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + + val (lightOffsetX, lightOffsetY) = getLightOffset() + + val lightDrawable = GradientDrawable().apply { + setSize(width + currentElevation.toInt(), height + currentElevation.toInt()) + setStroke(neuStrokeWidth.toInt(), neuLightShadowColor) + setBounds(0, 0, width + currentElevation.toInt(), height + currentElevation.toInt()) + setColor(Color.TRANSPARENT) + when (neuCornerType) { + NeuCornerType.OVAL -> shape = GradientDrawable.OVAL + NeuCornerType.ROUNDED -> { + shape = GradientDrawable.RECTANGLE + cornerRadius = neuCornerRadius + } + } + } + + val darkDrawable = GradientDrawable().apply { + setSize(width + currentElevation.toInt(), height + currentElevation.toInt()) + setStroke(neuStrokeWidth.toInt(), neuDarkShadowColor) + setBounds(0, 0, width + currentElevation.toInt(), height + currentElevation.toInt()) + setColor(Color.TRANSPARENT) + when (neuCornerType) { + NeuCornerType.OVAL -> shape = GradientDrawable.OVAL + NeuCornerType.ROUNDED -> { + shape = GradientDrawable.RECTANGLE + cornerRadius = neuCornerRadius + } + } + } + + canvas.save() + canvas.translate(lightOffsetX * 0.5f, lightOffsetY * 0.5f) + lightDrawable.draw(canvas) + canvas.restore() + + darkDrawable.draw(canvas) + + return blurMaker?.blur(bitmap) ?: bitmap.stackBlur(calculateDefaultBlurRadius()) + } + + private fun getLightOffset(): Pair { + return when (neuLightSource) { + LightSource.TOP_LEFT -> Pair(-currentElevation * 0.5f, -currentElevation * 0.5f) + LightSource.TOP_RIGHT -> Pair(currentElevation * 0.5f, -currentElevation * 0.5f) + LightSource.BOTTOM_LEFT -> Pair(-currentElevation * 0.5f, currentElevation * 0.5f) + LightSource.BOTTOM_RIGHT -> Pair(currentElevation * 0.5f, currentElevation * 0.5f) + } + } + + private fun getDarkOffset(): Pair { + return when (neuLightSource) { + LightSource.TOP_LEFT -> Pair(currentElevation * 0.5f, currentElevation * 0.5f) + LightSource.TOP_RIGHT -> Pair(-currentElevation * 0.5f, currentElevation * 0.5f) + LightSource.BOTTOM_LEFT -> Pair(currentElevation * 0.5f, -currentElevation * 0.5f) + LightSource.BOTTOM_RIGHT -> Pair(-currentElevation * 0.5f, -currentElevation * 0.5f) + } + } + + private fun invalidateShadowBitmaps() { + lightShadowBitmap?.recycle() + darkShadowBitmap?.recycle() + foregroundShadowBitmap?.recycle() + lightShadowBitmap = null + darkShadowBitmap = null + foregroundShadowBitmap = null + } + + private fun calculateDefaultBlurRadius(): Int { + val density = resources.displayMetrics.density + return min(25, (density * 10).roundToInt()) + } + + private fun Float.dpToPx(): Float { + return this * resources.displayMetrics.density + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + invalidateShadowBitmaps() + } + + fun refresh() { + needsRedraw = true + invalidate() + } +} diff --git a/library-views/src/main/java/me/nikhilchaudhari/library/views/NeumorphicCardView.kt b/library-views/src/main/java/me/nikhilchaudhari/library/views/NeumorphicCardView.kt new file mode 100644 index 00000000..d82b4006 --- /dev/null +++ b/library-views/src/main/java/me/nikhilchaudhari/library/views/NeumorphicCardView.kt @@ -0,0 +1,448 @@ +package me.nikhilchaudhari.library.views + +import android.content.Context +import android.graphics.* +import android.graphics.drawable.GradientDrawable +import android.util.AttributeSet +import android.widget.FrameLayout +import me.nikhilchaudhari.library.LightSource +import me.nikhilchaudhari.library.internal.BlurMaker +import me.nikhilchaudhari.library.internal.stackBlur +import kotlin.math.min +import kotlin.math.roundToInt + +/** + * A CardView with neumorphic styling for traditional Android Views/XML layouts + * + * Usage in XML: + * ```xml + * + * + * + * + * + * + * ``` + * + * Usage in Java: + * ```java + * NeumorphicCardView cardView = new NeumorphicCardView(context); + * cardView.setNeuShapeType(NeuShapeType.POT); + * cardView.setNeuCornerRadius(dpToPx(16)); + * ``` + */ +class NeumorphicCardView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr) { + + // Neumorphic properties + var neuShapeType: NeuShapeType = NeuShapeType.PUNCHED + set(value) { + field = value + invalidate() + } + + var neuCornerType: NeuCornerType = NeuCornerType.ROUNDED + set(value) { + field = value + invalidate() + } + + var neuCornerRadius: Float = 16f.dpToPx() + set(value) { + field = value + invalidate() + } + + var neuLightShadowColor: Int = Color.WHITE + set(value) { + field = value + invalidate() + } + + var neuDarkShadowColor: Int = Color.LTGRAY + set(value) { + field = value + invalidate() + } + + var neuElevation: Float = 8f.dpToPx() + set(value) { + field = value + invalidate() + } + + var neuStrokeWidth: Float = 6f.dpToPx() + set(value) { + field = value + invalidate() + } + + var neuInsetHorizontal: Float = 8f.dpToPx() + set(value) { + field = value + invalidate() + } + + var neuInsetVertical: Float = 8f.dpToPx() + set(value) { + field = value + invalidate() + } + + var neuLightSource: LightSource = LightSource.TOP_LEFT + set(value) { + field = value + invalidate() + } + + var neuBackgroundColor: Int = Color.parseColor("#ECEAEB") + set(value) { + field = value + invalidate() + } + + // Internal blur maker + private var blurMaker: BlurMaker? = null + + // Cached bitmaps + private var lightShadowBitmap: Bitmap? = null + private var darkShadowBitmap: Bitmap? = null + private var foregroundShadowBitmap: Bitmap? = null + private var needsRedraw = true + + // Paint objects + private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG) + private val shadowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + isFilterBitmap = true + } + + init { + blurMaker = BlurMaker(context, calculateDefaultBlurRadius()) + + // Parse XML attributes + attrs?.let { + val typedArray = context.obtainStyledAttributes(it, R.styleable.NeumorphicCardView) + + neuShapeType = when (typedArray.getInt(R.styleable.NeumorphicCardView_neuShape, 0)) { + 0 -> NeuShapeType.PUNCHED + 1 -> NeuShapeType.PRESSED + 2 -> NeuShapeType.POT + else -> NeuShapeType.PUNCHED + } + + neuCornerType = when (typedArray.getInt(R.styleable.NeumorphicCardView_neuCornerType, 0)) { + 0 -> NeuCornerType.ROUNDED + 1 -> NeuCornerType.OVAL + else -> NeuCornerType.ROUNDED + } + + neuCornerRadius = typedArray.getDimension( + R.styleable.NeumorphicCardView_neuCornerRadius, 16f.dpToPx() + ) + + neuLightShadowColor = typedArray.getColor( + R.styleable.NeumorphicCardView_neuLightShadowColor, Color.WHITE + ) + + neuDarkShadowColor = typedArray.getColor( + R.styleable.NeumorphicCardView_neuDarkShadowColor, Color.LTGRAY + ) + + neuElevation = typedArray.getDimension( + R.styleable.NeumorphicCardView_neuElevation, 8f.dpToPx() + ) + + neuStrokeWidth = typedArray.getDimension( + R.styleable.NeumorphicCardView_neuStrokeWidth, 6f.dpToPx() + ) + + neuInsetHorizontal = typedArray.getDimension( + R.styleable.NeumorphicCardView_neuInsetHorizontal, 8f.dpToPx() + ) + + neuInsetVertical = typedArray.getDimension( + R.styleable.NeumorphicCardView_neuInsetVertical, 8f.dpToPx() + ) + + neuLightSource = when (typedArray.getInt(R.styleable.NeumorphicCardView_neuLightSource, 0)) { + 0 -> LightSource.TOP_LEFT + 1 -> LightSource.TOP_RIGHT + 2 -> LightSource.BOTTOM_LEFT + 3 -> LightSource.BOTTOM_RIGHT + else -> LightSource.TOP_LEFT + } + + neuBackgroundColor = typedArray.getColor( + R.styleable.NeumorphicCardView_neuBackgroundColor, Color.parseColor("#ECEAEB") + ) + + typedArray.recycle() + } + + // Enable drawing + setWillNotDraw(false) + + // Set layer type for proper shadow rendering + setLayerType(LAYER_TYPE_SOFTWARE, null) + + // Add padding for shadows + setPadding( + (neuInsetHorizontal + neuElevation).toInt(), + (neuInsetVertical + neuElevation).toInt(), + (neuInsetHorizontal + neuElevation).toInt(), + (neuInsetVertical + neuElevation).toInt() + ) + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + needsRedraw = true + invalidateShadowBitmaps() + } + + override fun onDraw(canvas: Canvas) { + if (width <= 0 || height <= 0) return + + if (needsRedraw) { + generateShadowBitmaps() + needsRedraw = false + } + + when (neuShapeType) { + NeuShapeType.PUNCHED -> drawPunched(canvas) + NeuShapeType.PRESSED -> drawPressed(canvas) + NeuShapeType.POT -> drawPot(canvas) + } + + super.onDraw(canvas) + } + + private fun drawPunched(canvas: Canvas) { + drawBackgroundShadows(canvas) + drawNeumorphicBackground(canvas) + } + + private fun drawPressed(canvas: Canvas) { + drawNeumorphicBackground(canvas) + drawForegroundShadows(canvas) + } + + private fun drawPot(canvas: Canvas) { + drawBackgroundShadows(canvas) + drawNeumorphicBackground(canvas) + drawForegroundShadows(canvas) + } + + private fun drawBackgroundShadows(canvas: Canvas) { + val (lightOffsetX, lightOffsetY) = getLightOffset() + val (darkOffsetX, darkOffsetY) = getDarkOffset() + + val contentLeft = neuInsetHorizontal + neuElevation + val contentTop = neuInsetVertical + neuElevation + val contentWidth = width - 2 * contentLeft + val contentHeight = height - 2 * contentTop + + if (contentWidth <= 0 || contentHeight <= 0) return + + lightShadowBitmap?.let { + canvas.drawBitmap( + it, + contentLeft - neuElevation + lightOffsetX, + contentTop - neuElevation + lightOffsetY, + shadowPaint + ) + } + + darkShadowBitmap?.let { + canvas.drawBitmap( + it, + contentLeft - neuElevation + darkOffsetX + neuInsetHorizontal * 2, + contentTop - neuElevation + darkOffsetY + neuInsetVertical * 2, + shadowPaint + ) + } + } + + private fun drawForegroundShadows(canvas: Canvas) { + val contentLeft = neuInsetHorizontal + neuElevation + val contentTop = neuInsetVertical + neuElevation + + foregroundShadowBitmap?.let { + canvas.drawBitmap(it, contentLeft, contentTop, shadowPaint) + } + } + + private fun drawNeumorphicBackground(canvas: Canvas) { + backgroundPaint.color = neuBackgroundColor + + val contentLeft = neuInsetHorizontal + neuElevation + val contentTop = neuInsetVertical + neuElevation + val contentWidth = width - 2 * contentLeft + val contentHeight = height - 2 * contentTop + + if (contentWidth <= 0 || contentHeight <= 0) return + + when (neuCornerType) { + NeuCornerType.OVAL -> { + canvas.drawOval( + contentLeft, contentTop, + contentLeft + contentWidth, contentTop + contentHeight, + backgroundPaint + ) + } + NeuCornerType.ROUNDED -> { + canvas.drawRoundRect( + contentLeft, contentTop, + contentLeft + contentWidth, contentTop + contentHeight, + neuCornerRadius, neuCornerRadius, + backgroundPaint + ) + } + } + } + + private fun generateShadowBitmaps() { + invalidateShadowBitmaps() + + val contentLeft = neuInsetHorizontal + neuElevation + val contentTop = neuInsetVertical + neuElevation + val contentWidth = (width - 2 * contentLeft).toInt() + val contentHeight = (height - 2 * contentTop).toInt() + + if (contentWidth <= 0 || contentHeight <= 0) return + + lightShadowBitmap = generateShadowBitmap(neuLightShadowColor, contentWidth, contentHeight) + darkShadowBitmap = generateShadowBitmap(neuDarkShadowColor, contentWidth, contentHeight) + + if (neuShapeType == NeuShapeType.PRESSED || neuShapeType == NeuShapeType.POT) { + foregroundShadowBitmap = generateForegroundShadowBitmap(contentWidth, contentHeight) + } + } + + private fun generateShadowBitmap(shadowColor: Int, contentWidth: Int, contentHeight: Int): Bitmap? { + val bitmapWidth = (contentWidth + neuElevation * 2).roundToInt() + val bitmapHeight = (contentHeight + neuElevation * 2).roundToInt() + + if (bitmapWidth <= 0 || bitmapHeight <= 0) return null + + val drawable = GradientDrawable().apply { + setColor(shadowColor) + setSize(contentWidth, contentHeight) + setBounds(0, 0, contentWidth, contentHeight) + when (neuCornerType) { + NeuCornerType.OVAL -> shape = GradientDrawable.OVAL + NeuCornerType.ROUNDED -> { + shape = GradientDrawable.RECTANGLE + cornerRadius = neuCornerRadius + } + } + } + + val bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + canvas.translate(neuElevation, neuElevation) + drawable.draw(canvas) + + return blurMaker?.blur(bitmap) ?: bitmap.stackBlur(calculateDefaultBlurRadius()) + } + + private fun generateForegroundShadowBitmap(contentWidth: Int, contentHeight: Int): Bitmap? { + if (contentWidth <= 0 || contentHeight <= 0) return null + + val bitmap = Bitmap.createBitmap(contentWidth, contentHeight, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + + val (lightOffsetX, lightOffsetY) = getLightOffset() + + val lightDrawable = GradientDrawable().apply { + setSize(contentWidth + neuElevation.toInt(), contentHeight + neuElevation.toInt()) + setStroke(neuStrokeWidth.toInt(), neuLightShadowColor) + setBounds(0, 0, contentWidth + neuElevation.toInt(), contentHeight + neuElevation.toInt()) + setColor(Color.TRANSPARENT) + when (neuCornerType) { + NeuCornerType.OVAL -> shape = GradientDrawable.OVAL + NeuCornerType.ROUNDED -> { + shape = GradientDrawable.RECTANGLE + cornerRadius = neuCornerRadius + } + } + } + + val darkDrawable = GradientDrawable().apply { + setSize(contentWidth + neuElevation.toInt(), contentHeight + neuElevation.toInt()) + setStroke(neuStrokeWidth.toInt(), neuDarkShadowColor) + setBounds(0, 0, contentWidth + neuElevation.toInt(), contentHeight + neuElevation.toInt()) + setColor(Color.TRANSPARENT) + when (neuCornerType) { + NeuCornerType.OVAL -> shape = GradientDrawable.OVAL + NeuCornerType.ROUNDED -> { + shape = GradientDrawable.RECTANGLE + cornerRadius = neuCornerRadius + } + } + } + + canvas.save() + canvas.translate(lightOffsetX * 0.5f, lightOffsetY * 0.5f) + lightDrawable.draw(canvas) + canvas.restore() + + darkDrawable.draw(canvas) + + return blurMaker?.blur(bitmap) ?: bitmap.stackBlur(calculateDefaultBlurRadius()) + } + + private fun getLightOffset(): Pair { + return when (neuLightSource) { + LightSource.TOP_LEFT -> Pair(-neuElevation * 0.5f, -neuElevation * 0.5f) + LightSource.TOP_RIGHT -> Pair(neuElevation * 0.5f, -neuElevation * 0.5f) + LightSource.BOTTOM_LEFT -> Pair(-neuElevation * 0.5f, neuElevation * 0.5f) + LightSource.BOTTOM_RIGHT -> Pair(neuElevation * 0.5f, neuElevation * 0.5f) + } + } + + private fun getDarkOffset(): Pair { + return when (neuLightSource) { + LightSource.TOP_LEFT -> Pair(neuElevation * 0.5f, neuElevation * 0.5f) + LightSource.TOP_RIGHT -> Pair(-neuElevation * 0.5f, neuElevation * 0.5f) + LightSource.BOTTOM_LEFT -> Pair(neuElevation * 0.5f, -neuElevation * 0.5f) + LightSource.BOTTOM_RIGHT -> Pair(-neuElevation * 0.5f, -neuElevation * 0.5f) + } + } + + private fun invalidateShadowBitmaps() { + lightShadowBitmap?.recycle() + darkShadowBitmap?.recycle() + foregroundShadowBitmap?.recycle() + lightShadowBitmap = null + darkShadowBitmap = null + foregroundShadowBitmap = null + } + + private fun calculateDefaultBlurRadius(): Int { + val density = resources.displayMetrics.density + return min(25, (density * 10).roundToInt()) + } + + private fun Float.dpToPx(): Float { + return this * resources.displayMetrics.density + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + invalidateShadowBitmaps() + } + + fun refresh() { + needsRedraw = true + invalidate() + } +} diff --git a/library-views/src/main/java/me/nikhilchaudhari/library/views/NeumorphicView.kt b/library-views/src/main/java/me/nikhilchaudhari/library/views/NeumorphicView.kt new file mode 100644 index 00000000..701d3c32 --- /dev/null +++ b/library-views/src/main/java/me/nikhilchaudhari/library/views/NeumorphicView.kt @@ -0,0 +1,430 @@ +package me.nikhilchaudhari.library.views + +import android.content.Context +import android.graphics.* +import android.graphics.drawable.GradientDrawable +import android.os.Build +import android.util.AttributeSet +import android.view.View +import androidx.core.content.ContextCompat +import me.nikhilchaudhari.library.LightSource +import me.nikhilchaudhari.library.internal.BlurMaker +import me.nikhilchaudhari.library.internal.stackBlur +import kotlin.math.min +import kotlin.math.roundToInt + +/** + * Shape types for neumorphic views + */ +enum class NeuShapeType { + PUNCHED, + PRESSED, + POT +} + +/** + * Corner types for neumorphic views + */ +enum class NeuCornerType { + ROUNDED, + OVAL +} + +/** + * Base class for neumorphic views that can be used in XML layouts + * Supports Java/Kotlin Android development with traditional Views + */ +open class NeumorphicView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + + // Neumorphic properties + var neuShapeType: NeuShapeType = NeuShapeType.PUNCHED + set(value) { + field = value + invalidate() + } + + var neuCornerType: NeuCornerType = NeuCornerType.ROUNDED + set(value) { + field = value + invalidate() + } + + var neuCornerRadius: Float = 12f.dpToPx() + set(value) { + field = value + invalidate() + } + + var neuLightShadowColor: Int = Color.WHITE + set(value) { + field = value + invalidate() + } + + var neuDarkShadowColor: Int = Color.LTGRAY + set(value) { + field = value + invalidate() + } + + var neuElevation: Float = 6f.dpToPx() + set(value) { + field = value + invalidate() + } + + var neuStrokeWidth: Float = 6f.dpToPx() + set(value) { + field = value + invalidate() + } + + var neuInsetHorizontal: Float = 6f.dpToPx() + set(value) { + field = value + invalidate() + } + + var neuInsetVertical: Float = 6f.dpToPx() + set(value) { + field = value + invalidate() + } + + var neuLightSource: LightSource = LightSource.TOP_LEFT + set(value) { + field = value + invalidate() + } + + var neuBackgroundColor: Int = Color.parseColor("#ECEAEB") + set(value) { + field = value + invalidate() + } + + // Internal blur maker + private var blurMaker: BlurMaker? = null + + // Cached bitmaps for performance + private var lightShadowBitmap: Bitmap? = null + private var darkShadowBitmap: Bitmap? = null + private var foregroundShadowBitmap: Bitmap? = null + private var needsRedraw = true + + // Paint objects + private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG) + private val shadowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + isFilterBitmap = true + } + + init { + // Initialize blur maker + blurMaker = BlurMaker(context, calculateDefaultBlurRadius()) + + // Parse XML attributes + attrs?.let { + val typedArray = context.obtainStyledAttributes(it, R.styleable.NeumorphicView) + + neuShapeType = when (typedArray.getInt(R.styleable.NeumorphicView_neuShape, 0)) { + 0 -> NeuShapeType.PUNCHED + 1 -> NeuShapeType.PRESSED + 2 -> NeuShapeType.POT + else -> NeuShapeType.PUNCHED + } + + neuCornerType = when (typedArray.getInt(R.styleable.NeumorphicView_neuCornerType, 0)) { + 0 -> NeuCornerType.ROUNDED + 1 -> NeuCornerType.OVAL + else -> NeuCornerType.ROUNDED + } + + neuCornerRadius = typedArray.getDimension( + R.styleable.NeumorphicView_neuCornerRadius, 12f.dpToPx() + ) + + neuLightShadowColor = typedArray.getColor( + R.styleable.NeumorphicView_neuLightShadowColor, Color.WHITE + ) + + neuDarkShadowColor = typedArray.getColor( + R.styleable.NeumorphicView_neuDarkShadowColor, Color.LTGRAY + ) + + neuElevation = typedArray.getDimension( + R.styleable.NeumorphicView_neuElevation, 6f.dpToPx() + ) + + neuStrokeWidth = typedArray.getDimension( + R.styleable.NeumorphicView_neuStrokeWidth, 6f.dpToPx() + ) + + neuInsetHorizontal = typedArray.getDimension( + R.styleable.NeumorphicView_neuInsetHorizontal, 6f.dpToPx() + ) + + neuInsetVertical = typedArray.getDimension( + R.styleable.NeumorphicView_neuInsetVertical, 6f.dpToPx() + ) + + neuLightSource = when (typedArray.getInt(R.styleable.NeumorphicView_neuLightSource, 0)) { + 0 -> LightSource.TOP_LEFT + 1 -> LightSource.TOP_RIGHT + 2 -> LightSource.BOTTOM_LEFT + 3 -> LightSource.BOTTOM_RIGHT + else -> LightSource.TOP_LEFT + } + + neuBackgroundColor = typedArray.getColor( + R.styleable.NeumorphicView_neuBackgroundColor, Color.parseColor("#ECEAEB") + ) + + typedArray.recycle() + } + + // Enable drawing + setWillNotDraw(false) + + // Set layer type for proper shadow rendering + setLayerType(LAYER_TYPE_SOFTWARE, null) + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + needsRedraw = true + invalidateShadowBitmaps() + } + + override fun onDraw(canvas: Canvas) { + if (width <= 0 || height <= 0) return + + if (needsRedraw) { + generateShadowBitmaps() + needsRedraw = false + } + + when (neuShapeType) { + NeuShapeType.PUNCHED -> drawPunched(canvas) + NeuShapeType.PRESSED -> drawPressed(canvas) + NeuShapeType.POT -> drawPot(canvas) + } + + super.onDraw(canvas) + } + + private fun drawPunched(canvas: Canvas) { + // Draw background shadows first + drawBackgroundShadows(canvas) + // Draw the actual background + drawNeumorphicBackground(canvas) + } + + private fun drawPressed(canvas: Canvas) { + // Draw the actual background first + drawNeumorphicBackground(canvas) + // Draw foreground shadows + drawForegroundShadows(canvas) + } + + private fun drawPot(canvas: Canvas) { + // Draw background shadows + drawBackgroundShadows(canvas) + // Draw the actual background + drawNeumorphicBackground(canvas) + // Draw foreground shadows + drawForegroundShadows(canvas) + } + + private fun drawBackgroundShadows(canvas: Canvas) { + val (lightOffsetX, lightOffsetY) = getLightOffset() + val (darkOffsetX, darkOffsetY) = getDarkOffset() + + lightShadowBitmap?.let { + canvas.drawBitmap( + it, + -neuInsetHorizontal - neuElevation + lightOffsetX, + -neuInsetVertical - neuElevation + lightOffsetY, + shadowPaint + ) + } + + darkShadowBitmap?.let { + canvas.drawBitmap( + it, + neuInsetHorizontal - neuElevation + darkOffsetX, + neuInsetVertical - neuElevation + darkOffsetY, + shadowPaint + ) + } + } + + private fun drawForegroundShadows(canvas: Canvas) { + foregroundShadowBitmap?.let { + canvas.drawBitmap(it, 0f, 0f, shadowPaint) + } + } + + private fun drawNeumorphicBackground(canvas: Canvas) { + backgroundPaint.color = neuBackgroundColor + + when (neuCornerType) { + NeuCornerType.OVAL -> { + canvas.drawOval( + 0f, 0f, width.toFloat(), height.toFloat(), + backgroundPaint + ) + } + NeuCornerType.ROUNDED -> { + canvas.drawRoundRect( + 0f, 0f, width.toFloat(), height.toFloat(), + neuCornerRadius, neuCornerRadius, + backgroundPaint + ) + } + } + } + + private fun generateShadowBitmaps() { + invalidateShadowBitmaps() + + if (width <= 0 || height <= 0) return + + // Generate background shadows + lightShadowBitmap = generateShadowBitmap(neuLightShadowColor) + darkShadowBitmap = generateShadowBitmap(neuDarkShadowColor) + + // Generate foreground shadows for pressed and pot shapes + if (neuShapeType == NeuShapeType.PRESSED || neuShapeType == NeuShapeType.POT) { + foregroundShadowBitmap = generateForegroundShadowBitmap() + } + } + + private fun generateShadowBitmap(shadowColor: Int): Bitmap? { + val bitmapWidth = (width + neuElevation * 2).roundToInt() + val bitmapHeight = (height + neuElevation * 2).roundToInt() + + if (bitmapWidth <= 0 || bitmapHeight <= 0) return null + + val drawable = GradientDrawable().apply { + setColor(shadowColor) + setSize(width, height) + setBounds(0, 0, width, height) + when (neuCornerType) { + NeuCornerType.OVAL -> shape = GradientDrawable.OVAL + NeuCornerType.ROUNDED -> { + shape = GradientDrawable.RECTANGLE + cornerRadius = neuCornerRadius + } + } + } + + val bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + canvas.translate(neuElevation, neuElevation) + drawable.draw(canvas) + + return blurMaker?.blur(bitmap) ?: bitmap.stackBlur(calculateDefaultBlurRadius()) + } + + private fun generateForegroundShadowBitmap(): Bitmap? { + if (width <= 0 || height <= 0) return null + + val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + + val (lightOffsetX, lightOffsetY) = getLightOffset() + + // Light shadow stroke + val lightDrawable = GradientDrawable().apply { + setSize(width + neuElevation.toInt(), height + neuElevation.toInt()) + setStroke(neuStrokeWidth.toInt(), neuLightShadowColor) + setBounds(0, 0, width + neuElevation.toInt(), height + neuElevation.toInt()) + setColor(Color.TRANSPARENT) + when (neuCornerType) { + NeuCornerType.OVAL -> shape = GradientDrawable.OVAL + NeuCornerType.ROUNDED -> { + shape = GradientDrawable.RECTANGLE + cornerRadius = neuCornerRadius + } + } + } + + // Dark shadow stroke + val darkDrawable = GradientDrawable().apply { + setSize(width + neuElevation.toInt(), height + neuElevation.toInt()) + setStroke(neuStrokeWidth.toInt(), neuDarkShadowColor) + setBounds(0, 0, width + neuElevation.toInt(), height + neuElevation.toInt()) + setColor(Color.TRANSPARENT) + when (neuCornerType) { + NeuCornerType.OVAL -> shape = GradientDrawable.OVAL + NeuCornerType.ROUNDED -> { + shape = GradientDrawable.RECTANGLE + cornerRadius = neuCornerRadius + } + } + } + + canvas.save() + canvas.translate(lightOffsetX, lightOffsetY) + lightDrawable.draw(canvas) + canvas.restore() + + darkDrawable.draw(canvas) + + return blurMaker?.blur(bitmap) ?: bitmap.stackBlur(calculateDefaultBlurRadius()) + } + + private fun getLightOffset(): Pair { + return when (neuLightSource) { + LightSource.TOP_LEFT -> Pair(-neuElevation * 0.5f, -neuElevation * 0.5f) + LightSource.TOP_RIGHT -> Pair(neuElevation * 0.5f, -neuElevation * 0.5f) + LightSource.BOTTOM_LEFT -> Pair(-neuElevation * 0.5f, neuElevation * 0.5f) + LightSource.BOTTOM_RIGHT -> Pair(neuElevation * 0.5f, neuElevation * 0.5f) + } + } + + private fun getDarkOffset(): Pair { + return when (neuLightSource) { + LightSource.TOP_LEFT -> Pair(neuElevation * 0.5f, neuElevation * 0.5f) + LightSource.TOP_RIGHT -> Pair(-neuElevation * 0.5f, neuElevation * 0.5f) + LightSource.BOTTOM_LEFT -> Pair(neuElevation * 0.5f, -neuElevation * 0.5f) + LightSource.BOTTOM_RIGHT -> Pair(-neuElevation * 0.5f, -neuElevation * 0.5f) + } + } + + private fun invalidateShadowBitmaps() { + lightShadowBitmap?.recycle() + darkShadowBitmap?.recycle() + foregroundShadowBitmap?.recycle() + lightShadowBitmap = null + darkShadowBitmap = null + foregroundShadowBitmap = null + } + + private fun calculateDefaultBlurRadius(): Int { + val density = resources.displayMetrics.density + return min(25, (density * 10).roundToInt()) + } + + private fun Float.dpToPx(): Float { + return this * resources.displayMetrics.density + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + invalidateShadowBitmaps() + } + + /** + * Refresh the neumorphic effect + * Call this after changing properties programmatically + */ + fun refresh() { + needsRedraw = true + invalidate() + } +} diff --git a/library-views/src/main/res/values/attrs.xml b/library-views/src/main/res/values/attrs.xml new file mode 100644 index 00000000..7f2fdbde --- /dev/null +++ b/library-views/src/main/res/values/attrs.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/library/build.gradle b/library/build.gradle index 81b04edb..7e1e1270 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -5,21 +5,21 @@ plugins { ext { PUBLISH_GROUP_ID = 'me.nikhilchaudhari' - PUBLISH_VERSION = '1.0.0-alpha02' + PUBLISH_VERSION = '2.0.0' PUBLISH_ARTIFACT_ID = 'composeNeumorphism' } apply from: "${rootProject.projectDir}/scripts/publish-module.gradle" android { - compileSdk 30 - buildToolsVersion "30.0.3" + namespace 'me.nikhilchaudhari.library' + compileSdk 34 defaultConfig { minSdk 21 - targetSdk 30 - versionCode 1 - versionName "1.0" + targetSdk 34 + versionCode 2 + versionName "2.0.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" @@ -33,13 +33,12 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '1.8' - useIR = true + jvmTarget = '17' } buildFeatures { @@ -47,20 +46,21 @@ android { } composeOptions { - kotlinCompilerExtensionVersion compose_version - kotlinCompilerVersion kotlin_version + kotlinCompilerExtensionVersion compose_compiler_version } } dependencies { - implementation 'androidx.core:core-ktx:1.6.0' - implementation 'androidx.appcompat:appcompat:1.3.1' - implementation 'com.google.android.material:material:1.4.0' + implementation 'androidx.core:core-ktx:1.12.0' + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.11.0' implementation "androidx.compose.ui:ui:$compose_version" implementation "androidx.compose.material:material:$compose_version" + implementation "androidx.compose.material3:material3:1.1.2" implementation "androidx.compose.ui:ui-tooling:$compose_version" - testImplementation 'junit:junit:4.+' - androidTestImplementation 'androidx.test.ext:junit:1.1.2' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + implementation "androidx.compose.animation:animation:$compose_version" + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' } \ No newline at end of file diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml index 4afb35bb..a5918e68 100644 --- a/library/src/main/AndroidManifest.xml +++ b/library/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - + \ No newline at end of file diff --git a/library/src/main/java/me/nikhilchaudhari/library/NeuExtensions.kt b/library/src/main/java/me/nikhilchaudhari/library/NeuExtensions.kt new file mode 100644 index 00000000..49516e01 --- /dev/null +++ b/library/src/main/java/me/nikhilchaudhari/library/NeuExtensions.kt @@ -0,0 +1,125 @@ +package me.nikhilchaudhari.library + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import me.nikhilchaudhari.library.shapes.NeuShape +import me.nikhilchaudhari.library.shapes.Punched + +/** + * Clickable neumorphic modifier with press animation + * + * @param onClick Action to perform when clicked + * @param enabled Whether the click is enabled + * @param neuInsets Shadow insets + * @param neuShape Shape type + * @param lightShadowColor Light shadow color + * @param darkShadowColor Dark shadow color + * @param strokeWidth Stroke width + * @param elevation Shadow elevation + * @param lightSource Light source direction + */ +fun Modifier.neumorphicClickable( + onClick: () -> Unit, + enabled: Boolean = true, + neuInsets: NeuInsets = NeuInsets(), + neuShape: NeuShape = Punched.Rounded(), + lightShadowColor: Color = Color.White, + darkShadowColor: Color = Color.LightGray, + strokeWidth: Dp = 6.dp, + elevation: Dp = 6.dp, + lightSource: LightSource = LightSource.TOP_LEFT +) = composed { + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + + this + .animatedNeumorphic( + neuInsets = neuInsets, + neuShape = neuShape, + lightShadowColor = lightShadowColor, + darkShadowColor = darkShadowColor, + strokeWidth = strokeWidth, + elevation = elevation, + lightSource = lightSource, + pressed = isPressed + ) + .clickable( + interactionSource = interactionSource, + indication = null, + enabled = enabled, + onClick = onClick + ) +} + +/** + * Apply neumorphic effect using theme colors + * + * @param colorScheme The color scheme to use + * @param neuInsets Shadow insets + * @param neuShape Shape type + * @param strokeWidth Stroke width + * @param elevation Shadow elevation + * @param lightSource Light source direction + */ +fun Modifier.themedNeumorphic( + colorScheme: NeuTheme.NeuColorScheme, + neuInsets: NeuInsets = NeuInsets(), + neuShape: NeuShape = Punched.Rounded(), + strokeWidth: Dp = 6.dp, + elevation: Dp = 6.dp, + lightSource: LightSource = LightSource.TOP_LEFT +) = this.neumorphic( + neuInsets = neuInsets, + neuShape = neuShape, + lightShadowColor = colorScheme.lightShadowColor, + darkShadowColor = colorScheme.darkShadowColor, + strokeWidth = strokeWidth, + elevation = elevation, + lightSource = lightSource +) + +/** + * Apply soft neumorphic effect with reduced elevation + */ +fun Modifier.softNeumorphic( + neuInsets: NeuInsets = NeuInsets(4.dp, 4.dp), + neuShape: NeuShape = Punched.Rounded(8.dp), + lightShadowColor: Color = Color.White.copy(alpha = 0.8f), + darkShadowColor: Color = Color.LightGray.copy(alpha = 0.8f), + lightSource: LightSource = LightSource.TOP_LEFT +) = this.neumorphic( + neuInsets = neuInsets, + neuShape = neuShape, + lightShadowColor = lightShadowColor, + darkShadowColor = darkShadowColor, + strokeWidth = 4.dp, + elevation = 4.dp, + lightSource = lightSource +) + +/** + * Apply deep neumorphic effect with increased elevation + */ +fun Modifier.deepNeumorphic( + neuInsets: NeuInsets = NeuInsets(10.dp, 10.dp), + neuShape: NeuShape = Punched.Rounded(16.dp), + lightShadowColor: Color = Color.White, + darkShadowColor: Color = Color.Gray, + lightSource: LightSource = LightSource.TOP_LEFT +) = this.neumorphic( + neuInsets = neuInsets, + neuShape = neuShape, + lightShadowColor = lightShadowColor, + darkShadowColor = darkShadowColor, + strokeWidth = 10.dp, + elevation = 10.dp, + lightSource = lightSource +) diff --git a/library/src/main/java/me/nikhilchaudhari/library/NeuTheme.kt b/library/src/main/java/me/nikhilchaudhari/library/NeuTheme.kt new file mode 100644 index 00000000..bd3844da --- /dev/null +++ b/library/src/main/java/me/nikhilchaudhari/library/NeuTheme.kt @@ -0,0 +1,117 @@ +package me.nikhilchaudhari.library + +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.ui.graphics.Color + +/** + * Theme configuration for neumorphic components + * Supports Material You dynamic colors on Android 12+ + */ +object NeuTheme { + + /** + * Default neumorphic color scheme for light mode + */ + data class NeuColorScheme( + val backgroundColor: Color, + val lightShadowColor: Color, + val darkShadowColor: Color + ) + + /** + * Light theme color scheme + */ + val LightColorScheme = NeuColorScheme( + backgroundColor = Color(0xFFECEAEB), + lightShadowColor = Color.White, + darkShadowColor = Color(0xFFC8C8C8) + ) + + /** + * Dark theme color scheme + */ + val DarkColorScheme = NeuColorScheme( + backgroundColor = Color(0xFF2D2D2D), + lightShadowColor = Color(0xFF3D3D3D), + darkShadowColor = Color(0xFF1D1D1D) + ) + + /** + * Get the appropriate color scheme based on system theme + */ + @Composable + @ReadOnlyComposable + fun colorScheme(): NeuColorScheme { + return if (isSystemInDarkTheme()) DarkColorScheme else LightColorScheme + } + + /** + * Create a custom color scheme + */ + fun customColorScheme( + backgroundColor: Color, + lightShadowColor: Color = backgroundColor.lighten(0.15f), + darkShadowColor: Color = backgroundColor.darken(0.15f) + ) = NeuColorScheme( + backgroundColor = backgroundColor, + lightShadowColor = lightShadowColor, + darkShadowColor = darkShadowColor + ) + + /** + * Get Material You dynamic colors based color scheme (Android 12+) + * Falls back to default scheme on older versions + */ + @Composable + @ReadOnlyComposable + fun dynamicColorScheme(): NeuColorScheme { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val surface = MaterialTheme.colorScheme.surface + NeuColorScheme( + backgroundColor = surface, + lightShadowColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.8f), + darkShadowColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) + ) + } else { + colorScheme() + } + } +} + +/** + * Lighten a color by a percentage + */ +fun Color.lighten(factor: Float): Color { + return Color( + red = (red + (1f - red) * factor).coerceIn(0f, 1f), + green = (green + (1f - green) * factor).coerceIn(0f, 1f), + blue = (blue + (1f - blue) * factor).coerceIn(0f, 1f), + alpha = alpha + ) +} + +/** + * Darken a color by a percentage + */ +fun Color.darken(factor: Float): Color { + return Color( + red = (red * (1f - factor)).coerceIn(0f, 1f), + green = (green * (1f - factor)).coerceIn(0f, 1f), + blue = (blue * (1f - factor)).coerceIn(0f, 1f), + alpha = alpha + ) +} + +/** + * Generate neumorphic shadow colors from a background color + */ +fun Color.toNeuColors(): Pair { + return Pair( + this.lighten(0.2f), + this.darken(0.15f) + ) +} diff --git a/library/src/main/java/me/nikhilchaudhari/library/Neumorphic.kt b/library/src/main/java/me/nikhilchaudhari/library/Neumorphic.kt index 745ff9fa..d34d3571 100644 --- a/library/src/main/java/me/nikhilchaudhari/library/Neumorphic.kt +++ b/library/src/main/java/me/nikhilchaudhari/library/Neumorphic.kt @@ -3,6 +3,10 @@ package me.nikhilchaudhari.library import android.content.Context import android.os.Build import android.util.DisplayMetrics +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.draw.DrawModifier @@ -22,18 +26,43 @@ import me.nikhilchaudhari.library.shapes.ShapeConfig import kotlin.math.min import kotlin.math.roundToInt +/** + * Insets configuration for neumorphic shadows + */ data class NeuInsets( val horizontal: Dp = 6.dp, val vertical: Dp = 6.dp ) +/** + * Light source direction for neumorphic shadows + */ +enum class LightSource { + TOP_LEFT, + TOP_RIGHT, + BOTTOM_LEFT, + BOTTOM_RIGHT +} + +/** + * Apply neumorphic effect to a composable + * + * @param neuInsets Shadow insets (horizontal and vertical) + * @param neuShape Shape type (Punched, Pressed, Pot) + * @param lightShadowColor Color of the light shadow (typically white or light gray) + * @param darkShadowColor Color of the dark shadow (typically dark gray) + * @param strokeWidth Stroke width for internal shadows + * @param elevation Shadow elevation + * @param lightSource Direction of the light source for shadow placement + */ fun Modifier.neumorphic( neuInsets: NeuInsets = NeuInsets(), neuShape: NeuShape = Punched.Rounded(), lightShadowColor: Color = Color.White, darkShadowColor: Color = Color.LightGray, strokeWidth: Dp = 6.dp, - elevation: Dp = 6.dp + elevation: Dp = 6.dp, + lightSource: LightSource = LightSource.TOP_LEFT ) = composed { val context = LocalContext.current this.then( @@ -45,6 +74,7 @@ fun Modifier.neumorphic( darkShadowColor, strokeWidth, elevation, + lightSource, inspectorInfo = debugInspectorInfo { name = "neumorphic" properties["context"] = context @@ -54,11 +84,53 @@ fun Modifier.neumorphic( properties["strokeWidth"] = strokeWidth properties["lightShadowColor"] = lightShadowColor properties["darkShadowColor"] = darkShadowColor + properties["lightSource"] = lightSource } ) ) } +/** + * Animated neumorphic effect with smooth transitions + * + * @param neuInsets Shadow insets (horizontal and vertical) + * @param neuShape Shape type (Punched, Pressed, Pot) + * @param lightShadowColor Color of the light shadow + * @param darkShadowColor Color of the dark shadow + * @param strokeWidth Stroke width for internal shadows + * @param elevation Shadow elevation + * @param lightSource Direction of the light source + * @param pressed Whether the component is pressed (for animation) + * @param animationDuration Duration of the animation in milliseconds + */ +fun Modifier.animatedNeumorphic( + neuInsets: NeuInsets = NeuInsets(), + neuShape: NeuShape = Punched.Rounded(), + lightShadowColor: Color = Color.White, + darkShadowColor: Color = Color.LightGray, + strokeWidth: Dp = 6.dp, + elevation: Dp = 6.dp, + lightSource: LightSource = LightSource.TOP_LEFT, + pressed: Boolean = false, + animationDuration: Int = 150 +) = composed { + val animatedElevation by animateDpAsState( + targetValue = if (pressed) elevation * 0.5f else elevation, + animationSpec = tween(durationMillis = animationDuration), + label = "elevationAnimation" + ) + + neumorphic( + neuInsets = neuInsets, + neuShape = neuShape, + lightShadowColor = lightShadowColor, + darkShadowColor = darkShadowColor, + strokeWidth = strokeWidth, + elevation = animatedElevation, + lightSource = lightSource + ) +} + internal class NeumorphicModifier( context: Context, private val insets: NeuInsets, @@ -67,6 +139,7 @@ internal class NeumorphicModifier( private val darkShadowColor: Color, private val strokeWidth: Dp, private val elevation: Dp, + private val lightSource: LightSource, inspectorInfo: InspectorInfo.() -> Unit ) : DrawModifier, InspectorValueInfo(inspectorInfo) { @@ -79,12 +152,12 @@ internal class NeumorphicModifier( elevation, lightShadowColor, darkShadowColor, - strokeWidth + strokeWidth, + lightSource = lightSource ) neuShape.drawShadows(this, blurMaker, shapeConfig) } - // TODO: Try tweaking values private fun calculateDefaultBlurRadius(displayMetrics: DisplayMetrics): Int { val densityStable = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { DisplayMetrics.DENSITY_DEVICE_STABLE / DisplayMetrics.DENSITY_DEFAULT.toFloat() diff --git a/library/src/main/java/me/nikhilchaudhari/library/internal/Blur.kt b/library/src/main/java/me/nikhilchaudhari/library/internal/Blur.kt index b2ec494c..68383222 100644 --- a/library/src/main/java/me/nikhilchaudhari/library/internal/Blur.kt +++ b/library/src/main/java/me/nikhilchaudhari/library/internal/Blur.kt @@ -2,12 +2,13 @@ package me.nikhilchaudhari.library.internal import android.content.Context import android.graphics.* +import android.os.Build import android.renderscript.* import java.lang.ref.WeakReference /** - * Holds required data for of blur operation + * Holds required data for blur operation */ data class BlurConfig( val width: Int, @@ -22,7 +23,10 @@ data class BlurConfig( } } -//TODO: Make sure if this is being invoked properly +/** + * Modern blur implementation with fallback support + * Uses RenderScript for API < 31, StackBlur as fallback for all versions + */ class BlurMaker(context: Context, private val defaultBlurRadius: Int) { private val contextRef = WeakReference(context) @@ -47,7 +51,7 @@ class BlurMaker(context: Context, private val defaultBlurRadius: Int) { if (width == 0 || height == 0) { return null } - //create a bitmap + // Create a bitmap val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) Canvas(bitmap).run { @@ -59,15 +63,21 @@ class BlurMaker(context: Context, private val defaultBlurRadius: Int) { drawBitmap(source, 0f, 0f, paint) } - //try blur with renderscript + // Try blur with appropriate method based on API level val blurBitmap: Bitmap? = try { - blurWithRenderScript(bitmap, blurConfig.radius) - } catch (e: RSRuntimeException) { - // if renderscript fails, use stack blur + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // Use StackBlur for Android 12+ as RenderScript is deprecated + bitmap.stackBlur(blurConfig.radius) + } else { + // Use RenderScript for older versions with StackBlur fallback + blurWithRenderScript(bitmap, blurConfig.radius) + } + } catch (e: Exception) { + // Fallback to StackBlur if anything fails bitmap.stackBlur(blurConfig.radius) } - // return blurred bitmap + // Return blurred bitmap return blurBitmap?.let { if (blurConfig.sampling == BlurConfig.DEFAULT_SAMPLING) { it @@ -80,8 +90,14 @@ class BlurMaker(context: Context, private val defaultBlurRadius: Int) { } } + @Suppress("DEPRECATION") @Throws(RSRuntimeException::class) private fun blurWithRenderScript(bitmap: Bitmap, radius: Int): Bitmap? { + // Only use RenderScript on API < 31 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + return bitmap.stackBlur(radius) + } + val context = contextRef.get() ?: return null var blur: ScriptIntrinsicBlur? = null var input: Allocation? = null diff --git a/library/src/main/java/me/nikhilchaudhari/library/shapes/Drawing.kt b/library/src/main/java/me/nikhilchaudhari/library/shapes/Drawing.kt index a0efeb5d..1242461f 100644 --- a/library/src/main/java/me/nikhilchaudhari/library/shapes/Drawing.kt +++ b/library/src/main/java/me/nikhilchaudhari/library/shapes/Drawing.kt @@ -10,12 +10,34 @@ import androidx.compose.ui.graphics.drawscope.ContentDrawScope import androidx.compose.ui.graphics.drawscope.inset import androidx.compose.ui.graphics.toArgb import androidx.core.graphics.withTranslation +import me.nikhilchaudhari.library.LightSource import me.nikhilchaudhari.library.internal.BlurMaker import kotlin.math.roundToInt -// TODO: Try tweaking canvas values -/*pressed shape - draw after the content */ +/** + * Calculate shadow offsets based on light source direction + */ +internal fun getLightShadowOffset(lightSource: LightSource, elevation: Float): Pair { + return when (lightSource) { + LightSource.TOP_LEFT -> Pair(-elevation, -elevation) + LightSource.TOP_RIGHT -> Pair(elevation, -elevation) + LightSource.BOTTOM_LEFT -> Pair(-elevation, elevation) + LightSource.BOTTOM_RIGHT -> Pair(elevation, elevation) + } +} + +internal fun getDarkShadowOffset(lightSource: LightSource, elevation: Float): Pair { + return when (lightSource) { + LightSource.TOP_LEFT -> Pair(elevation, elevation) + LightSource.TOP_RIGHT -> Pair(-elevation, elevation) + LightSource.BOTTOM_LEFT -> Pair(elevation, -elevation) + LightSource.BOTTOM_RIGHT -> Pair(-elevation, -elevation) + } +} + + +/* pressed shape - draw after the content */ internal fun ContentDrawScope.drawOnForeground( shapeConfig: ShapeConfig, blurMaker: BlurMaker @@ -40,7 +62,7 @@ internal fun ContentDrawScope.drawOnForeground( setStroke(strokeWidth, shapeConfig.lightShadowColor.toArgb()) setBounds(0, 0, width, height) setColor(Color.Transparent.toArgb()) - setNeuShape(cornerType, ShadowForm.LightShadow, radius) + setNeuShape(cornerType, ShadowForm.LightShadow, radius, shapeConfig.lightSource) } darkShadowDrawable.apply { @@ -48,9 +70,11 @@ internal fun ContentDrawScope.drawOnForeground( setStroke(strokeWidth, shapeConfig.darkShadowColor.toArgb()) setColor(Color.Transparent.toArgb()) setBounds(0, 0, width, height) - setNeuShape(cornerType, ShadowForm.DarkShadow, radius) + setNeuShape(cornerType, ShadowForm.DarkShadow, radius, shapeConfig.lightSource) } + val lightOffset = getLightShadowOffset(shapeConfig.lightSource, elevation) + val darkOffset = getDarkShadowOffset(shapeConfig.lightSource, elevation) generateShadowBitmap( size.width.toInt(), @@ -58,7 +82,9 @@ internal fun ContentDrawScope.drawOnForeground( lightShadowDrawable, darkShadowDrawable, elevation, - blurMaker + blurMaker, + lightOffset, + darkOffset )?.asImageBitmap()?.let { drawScope.drawImage(it) } @@ -71,17 +97,19 @@ private fun generateShadowBitmap( lightShadowDrawable: GradientDrawable, darkShadowDrawable: GradientDrawable, elevation: Float, - blurMaker: BlurMaker + blurMaker: BlurMaker, + lightOffset: Pair, + darkOffset: Pair ) = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888).blurred(blurMaker) { withTranslation( - x = -elevation, - y = -elevation + x = lightOffset.first, + y = lightOffset.second ) { lightShadowDrawable.draw(this) } withTranslation( -// x = elevation, -// y = elevation + x = darkOffset.first * 0f, // Minimal offset for inner shadow + y = darkOffset.second * 0f ) { darkShadowDrawable.draw(this) } @@ -112,14 +140,14 @@ internal fun ContentDrawScope.drawOnBackground( setColor(shapeConfig.lightShadowColor.toArgb()) setSize(width, height) setBounds(0, 0, width, height) - setNeuShape(cornerType, ShadowForm.Default, radius) + setNeuShape(cornerType, ShadowForm.Default, radius, shapeConfig.lightSource) } darkShadowDrawable.apply { setColor(shapeConfig.darkShadowColor.toArgb()) setSize(width, height) setBounds(0, 0, width, height) - setNeuShape(cornerType, ShadowForm.Default, radius) + setNeuShape(cornerType, ShadowForm.Default, radius, shapeConfig.lightSource) } val lightShadowBitmap = @@ -131,20 +159,29 @@ internal fun ContentDrawScope.drawOnBackground( .toBlurredBitmap(width, height, elevation, blurMaker) ?.asImageBitmap() + // Calculate insets based on light source + val (lightHInset, lightVInset) = when (shapeConfig.lightSource) { + LightSource.TOP_LEFT -> Pair(-(horizontalInset + elevation), -(verticalInset + elevation)) + LightSource.TOP_RIGHT -> Pair(horizontalInset - elevation, -(verticalInset + elevation)) + LightSource.BOTTOM_LEFT -> Pair(-(horizontalInset + elevation), verticalInset - elevation) + LightSource.BOTTOM_RIGHT -> Pair(horizontalInset - elevation, verticalInset - elevation) + } + + val (darkHInset, darkVInset) = when (shapeConfig.lightSource) { + LightSource.TOP_LEFT -> Pair(horizontalInset - elevation, verticalInset - elevation) + LightSource.TOP_RIGHT -> Pair(-(horizontalInset + elevation), verticalInset - elevation) + LightSource.BOTTOM_LEFT -> Pair(horizontalInset - elevation, -(verticalInset + elevation)) + LightSource.BOTTOM_RIGHT -> Pair(-(horizontalInset + elevation), -(verticalInset + elevation)) + } + lightShadowBitmap?.let { bitmap -> - drawScope.inset( - -(horizontalInset + elevation), - -(verticalInset + elevation) - ) { + drawScope.inset(lightHInset, lightVInset) { drawImage(bitmap) } } darkShadowBitmap?.let { bitmap -> - drawScope.inset( - horizontalInset - elevation, - verticalInset - elevation - ) { + drawScope.inset(darkHInset, darkVInset) { drawImage(bitmap) } } @@ -190,7 +227,8 @@ internal sealed class ShadowForm { private fun GradientDrawable.setNeuShape( cornerType: CornerType, shadowForm: ShadowForm, - radius: Float + radius: Float, + lightSource: LightSource = LightSource.TOP_LEFT ) { when (cornerType) { is CornerType.Oval -> { @@ -203,33 +241,40 @@ private fun GradientDrawable.setNeuShape( cornerRadius = radius } is ShadowForm.LightShadow -> { - cornerRadii = - floatArrayOf( - 0f, - 0f, - radius, - radius, - radius, - radius, - radius, - radius + // Adjust corner radii based on light source + cornerRadii = when (lightSource) { + LightSource.TOP_LEFT -> floatArrayOf( + 0f, 0f, radius, radius, radius, radius, radius, radius + ) + LightSource.TOP_RIGHT -> floatArrayOf( + radius, radius, 0f, 0f, radius, radius, radius, radius + ) + LightSource.BOTTOM_LEFT -> floatArrayOf( + radius, radius, radius, radius, 0f, 0f, radius, radius + ) + LightSource.BOTTOM_RIGHT -> floatArrayOf( + radius, radius, radius, radius, radius, radius, 0f, 0f ) + } } is ShadowForm.DarkShadow -> { - cornerRadii = - floatArrayOf( - radius, - radius, - radius, - radius, - 0f, - 0f, - radius, - radius + // Adjust corner radii based on light source (opposite of light) + cornerRadii = when (lightSource) { + LightSource.TOP_LEFT -> floatArrayOf( + radius, radius, radius, radius, 0f, 0f, radius, radius ) + LightSource.TOP_RIGHT -> floatArrayOf( + radius, radius, radius, radius, radius, radius, 0f, 0f + ) + LightSource.BOTTOM_LEFT -> floatArrayOf( + 0f, 0f, radius, radius, radius, radius, radius, radius + ) + LightSource.BOTTOM_RIGHT -> floatArrayOf( + radius, radius, 0f, 0f, radius, radius, radius, radius + ) + } } } } } } - diff --git a/library/src/main/java/me/nikhilchaudhari/library/shapes/ShapeConfig.kt b/library/src/main/java/me/nikhilchaudhari/library/shapes/ShapeConfig.kt index ca282b41..13f92478 100644 --- a/library/src/main/java/me/nikhilchaudhari/library/shapes/ShapeConfig.kt +++ b/library/src/main/java/me/nikhilchaudhari/library/shapes/ShapeConfig.kt @@ -3,6 +3,7 @@ package me.nikhilchaudhari.library.shapes import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import me.nikhilchaudhari.library.LightSource import me.nikhilchaudhari.library.NeuInsets @@ -23,5 +24,6 @@ data class ShapeConfig( val lightShadowColor: Color, val darkShadowColor: Color, val strokeWidth: Dp, - var cornerType: CornerType = CornerType.Rounded() + var cornerType: CornerType = CornerType.Rounded(), + val lightSource: LightSource = LightSource.TOP_LEFT ) diff --git a/settings.gradle b/settings.gradle index f563cff3..9f6f9776 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,11 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { @@ -8,3 +16,4 @@ dependencyResolutionManagement { rootProject.name = "NeumorphismCompose" include ':app' include ':library' +include ':library-views' From 80a74e1f4461ae327c860aa4803501029a319bba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:27:28 +0000 Subject: [PATCH 03/36] Address code review feedback: improve constants, layer type, and remove unused code Co-authored-by: obieda-hussien <184546202+obieda-hussien@users.noreply.github.com> --- .../library/views/NeumorphicButton.kt | 11 +++++++++-- .../library/views/NeumorphicCardView.kt | 8 +++++++- .../library/views/NeumorphicView.kt | 8 +++++++- .../java/me/nikhilchaudhari/library/Neumorphic.kt | 15 +++++++++++++-- .../me/nikhilchaudhari/library/shapes/Drawing.kt | 10 +++------- 5 files changed, 39 insertions(+), 13 deletions(-) diff --git a/library-views/src/main/java/me/nikhilchaudhari/library/views/NeumorphicButton.kt b/library-views/src/main/java/me/nikhilchaudhari/library/views/NeumorphicButton.kt index 6501cea8..250a0399 100644 --- a/library-views/src/main/java/me/nikhilchaudhari/library/views/NeumorphicButton.kt +++ b/library-views/src/main/java/me/nikhilchaudhari/library/views/NeumorphicButton.kt @@ -7,6 +7,7 @@ import android.util.AttributeSet import android.view.MotionEvent import androidx.appcompat.widget.AppCompatButton import me.nikhilchaudhari.library.LightSource +import me.nikhilchaudhari.library.NeuConstants import me.nikhilchaudhari.library.internal.BlurMaker import me.nikhilchaudhari.library.internal.stackBlur import kotlin.math.min @@ -197,7 +198,13 @@ class NeumorphicButton @JvmOverloads constructor( stateListAnimator = null // Set layer type for proper shadow rendering - setLayerType(LAYER_TYPE_SOFTWARE, null) + // Use hardware layer on modern devices, fall back to software for complex operations + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { + setLayerType(LAYER_TYPE_HARDWARE, null) + } else { + // Software layer required for blur effects on older devices + setLayerType(LAYER_TYPE_SOFTWARE, null) + } } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { @@ -211,7 +218,7 @@ class NeumorphicButton @JvmOverloads constructor( when (event.action) { MotionEvent.ACTION_DOWN -> { isPressed = true - currentElevation = neuElevation * 0.5f + currentElevation = neuElevation * NeuConstants.PRESSED_ELEVATION_FACTOR needsRedraw = true invalidate() } diff --git a/library-views/src/main/java/me/nikhilchaudhari/library/views/NeumorphicCardView.kt b/library-views/src/main/java/me/nikhilchaudhari/library/views/NeumorphicCardView.kt index d82b4006..75232c34 100644 --- a/library-views/src/main/java/me/nikhilchaudhari/library/views/NeumorphicCardView.kt +++ b/library-views/src/main/java/me/nikhilchaudhari/library/views/NeumorphicCardView.kt @@ -191,7 +191,13 @@ class NeumorphicCardView @JvmOverloads constructor( setWillNotDraw(false) // Set layer type for proper shadow rendering - setLayerType(LAYER_TYPE_SOFTWARE, null) + // Use hardware layer on modern devices, fall back to software for complex operations + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { + setLayerType(LAYER_TYPE_HARDWARE, null) + } else { + // Software layer required for blur effects on older devices + setLayerType(LAYER_TYPE_SOFTWARE, null) + } // Add padding for shadows setPadding( diff --git a/library-views/src/main/java/me/nikhilchaudhari/library/views/NeumorphicView.kt b/library-views/src/main/java/me/nikhilchaudhari/library/views/NeumorphicView.kt index 701d3c32..dd51c76c 100644 --- a/library-views/src/main/java/me/nikhilchaudhari/library/views/NeumorphicView.kt +++ b/library-views/src/main/java/me/nikhilchaudhari/library/views/NeumorphicView.kt @@ -190,7 +190,13 @@ open class NeumorphicView @JvmOverloads constructor( setWillNotDraw(false) // Set layer type for proper shadow rendering - setLayerType(LAYER_TYPE_SOFTWARE, null) + // Use hardware layer on modern devices, fall back to software for complex operations + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { + setLayerType(LAYER_TYPE_HARDWARE, null) + } else { + // Software layer required for blur effects on older devices + setLayerType(LAYER_TYPE_SOFTWARE, null) + } } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { diff --git a/library/src/main/java/me/nikhilchaudhari/library/Neumorphic.kt b/library/src/main/java/me/nikhilchaudhari/library/Neumorphic.kt index d34d3571..b30f45ca 100644 --- a/library/src/main/java/me/nikhilchaudhari/library/Neumorphic.kt +++ b/library/src/main/java/me/nikhilchaudhari/library/Neumorphic.kt @@ -44,6 +44,17 @@ enum class LightSource { BOTTOM_RIGHT } +/** + * Constants for neumorphic effects + */ +object NeuConstants { + /** Factor by which elevation is reduced when pressed */ + const val PRESSED_ELEVATION_FACTOR = 0.5f + + /** Default animation duration in milliseconds */ + const val DEFAULT_ANIMATION_DURATION_MS = 150 +} + /** * Apply neumorphic effect to a composable * @@ -112,10 +123,10 @@ fun Modifier.animatedNeumorphic( elevation: Dp = 6.dp, lightSource: LightSource = LightSource.TOP_LEFT, pressed: Boolean = false, - animationDuration: Int = 150 + animationDuration: Int = NeuConstants.DEFAULT_ANIMATION_DURATION_MS ) = composed { val animatedElevation by animateDpAsState( - targetValue = if (pressed) elevation * 0.5f else elevation, + targetValue = if (pressed) elevation * NeuConstants.PRESSED_ELEVATION_FACTOR else elevation, animationSpec = tween(durationMillis = animationDuration), label = "elevationAnimation" ) diff --git a/library/src/main/java/me/nikhilchaudhari/library/shapes/Drawing.kt b/library/src/main/java/me/nikhilchaudhari/library/shapes/Drawing.kt index 1242461f..2160d626 100644 --- a/library/src/main/java/me/nikhilchaudhari/library/shapes/Drawing.kt +++ b/library/src/main/java/me/nikhilchaudhari/library/shapes/Drawing.kt @@ -99,7 +99,7 @@ private fun generateShadowBitmap( elevation: Float, blurMaker: BlurMaker, lightOffset: Pair, - darkOffset: Pair + @Suppress("UNUSED_PARAMETER") darkOffset: Pair ) = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888).blurred(blurMaker) { withTranslation( x = lightOffset.first, @@ -107,12 +107,8 @@ private fun generateShadowBitmap( ) { lightShadowDrawable.draw(this) } - withTranslation( - x = darkOffset.first * 0f, // Minimal offset for inner shadow - y = darkOffset.second * 0f - ) { - darkShadowDrawable.draw(this) - } + // Dark shadow is drawn at origin for inner shadow effect + darkShadowDrawable.draw(this) } From b424fd087e5695be94dbb7dc030da09a88ae7716 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:32:40 +0000 Subject: [PATCH 04/36] Add modern CI workflow with APK artifact and update Material 3 to 1.3.0 Co-authored-by: obieda-hussien <184546202+obieda-hussien@users.noreply.github.com> --- .github/workflows/ci-build.yml | 110 ++++++++++++++++++++++++++++++--- app/build.gradle | 2 +- build.gradle | 1 + library/build.gradle | 2 +- 4 files changed, 104 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 5b908eef..bd51048c 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -1,26 +1,118 @@ -# This workflow will build a Java project with Gradle +# Modern Android CI/CD workflow with APK artifact -name: Java/Kotlin CI with Gradle +name: Android CI Build on: push: branches: [ main ] pull_request: branches: [ main ] + workflow_dispatch: # Allows manual triggering jobs: build: - + name: Build & Generate APK runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 with: - java-version: '11' - distribution: 'adopt' + java-version: '17' + distribution: 'temurin' + cache: gradle + - name: Grant execute permission for gradlew run: chmod +x gradlew + + - name: Validate Gradle wrapper + uses: gradle/wrapper-validation-action@v2 + - name: Build with Gradle - run: ./gradlew build + run: ./gradlew build --no-daemon + + - name: Build Debug APK + run: ./gradlew assembleDebug --no-daemon + + - name: Build Release APK + run: ./gradlew assembleRelease --no-daemon + + - name: Upload Debug APK + uses: actions/upload-artifact@v4 + with: + name: app-debug + path: app/build/outputs/apk/debug/app-debug.apk + retention-days: 30 + + - name: Upload Release APK + uses: actions/upload-artifact@v4 + with: + name: app-release-unsigned + path: app/build/outputs/apk/release/app-release-unsigned.apk + retention-days: 30 + + test: + name: Run Unit Tests + runs-on: ubuntu-latest + needs: build + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Run Unit Tests + run: ./gradlew test --no-daemon + + - name: Upload Test Results + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-results + path: | + app/build/reports/tests/ + library/build/reports/tests/ + retention-days: 14 + + lint: + name: Run Lint + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Run Lint + run: ./gradlew lint --no-daemon + + - name: Upload Lint Results + uses: actions/upload-artifact@v4 + if: always() + with: + name: lint-results + path: | + app/build/reports/lint-results*.html + library/build/reports/lint-results*.html + retention-days: 14 diff --git a/app/build.gradle b/app/build.gradle index b36f33f3..a067b274 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -48,7 +48,7 @@ dependencies { implementation 'com.google.android.material:material:1.11.0' implementation "androidx.compose.ui:ui:$compose_version" implementation "androidx.compose.material:material:$compose_version" - implementation "androidx.compose.material3:material3:1.1.2" + implementation "androidx.compose.material3:material3:$material3_version" implementation "androidx.compose.ui:ui-tooling:$compose_version" implementation "androidx.compose.animation:animation:$compose_version" implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2' diff --git a/build.gradle b/build.gradle index 47c03126..f5d63acf 100644 --- a/build.gradle +++ b/build.gradle @@ -4,6 +4,7 @@ buildscript { compose_version = '1.5.4' kotlin_version = '1.9.20' compose_compiler_version = '1.5.4' + material3_version = '1.3.0' } repositories { google() diff --git a/library/build.gradle b/library/build.gradle index 7e1e1270..900f1d51 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -57,7 +57,7 @@ dependencies { implementation 'com.google.android.material:material:1.11.0' implementation "androidx.compose.ui:ui:$compose_version" implementation "androidx.compose.material:material:$compose_version" - implementation "androidx.compose.material3:material3:1.1.2" + implementation "androidx.compose.material3:material3:$material3_version" implementation "androidx.compose.ui:ui-tooling:$compose_version" implementation "androidx.compose.animation:animation:$compose_version" testImplementation 'junit:junit:4.13.2' From 5a351c8a1e2f3cd6785d534c55607a3360cb284b Mon Sep 17 00:00:00 2001 From: Obieda Date: Wed, 7 Jan 2026 14:38:11 +0200 Subject: [PATCH 05/36] Modify CI workflow to support all branches Updated CI workflow to trigger on all branches and pull requests. --- .github/workflows/ci-build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index bd51048c..8e0b9b77 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -4,10 +4,10 @@ name: Android CI Build on: push: - branches: [ main ] - pull_request: - branches: [ main ] - workflow_dispatch: # Allows manual triggering + branches: + - '**' # هذا يسمح للـ Workflow بالعمل عند أي Push على أي فرع + pull_request: # هذا يسمح للـ Workflow بالعمل مع كل الـ Pull Requests لأي فرع + workflow_dispatch: # يسمح بتشغيل الـ Workflow يدوياً jobs: build: From c9acee60e839c96242a16d4456e33d34ea4b8635 Mon Sep 17 00:00:00 2001 From: Obieda Date: Wed, 7 Jan 2026 14:43:21 +0200 Subject: [PATCH 06/36] Update CI workflow for better branch handling --- .github/workflows/ci-build.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 8e0b9b77..0fcc8dfb 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -5,9 +5,11 @@ name: Android CI Build on: push: branches: - - '**' # هذا يسمح للـ Workflow بالعمل عند أي Push على أي فرع - pull_request: # هذا يسمح للـ Workflow بالعمل مع كل الـ Pull Requests لأي فرع - workflow_dispatch: # يسمح بتشغيل الـ Workflow يدوياً + - '**' # يعمل عند أي Push على أي فرع في الـ Fork + pull_request: + branches: + - '**' # يعمل عند فتح PR لأي فرع داخل الـ Fork + workflow_dispatch: # يسمح بتشغيل الـ Workflow يدوياً من واجهة GitHub jobs: build: @@ -29,7 +31,7 @@ jobs: run: chmod +x gradlew - name: Validate Gradle wrapper - uses: gradle/wrapper-validation-action@v2 + uses: gradle/wrapper-validation-action@v3 # تحديث للإصدار الأحدث - name: Build with Gradle run: ./gradlew build --no-daemon @@ -82,8 +84,7 @@ jobs: with: name: test-results path: | - app/build/reports/tests/ - library/build/reports/tests/ + **/build/reports/tests/ retention-days: 14 lint: @@ -113,6 +114,5 @@ jobs: with: name: lint-results path: | - app/build/reports/lint-results*.html - library/build/reports/lint-results*.html + **/build/reports/lint-results*.html retention-days: 14 From 3e01403fa5bf207ba102610e14e3fc10becd3450 Mon Sep 17 00:00:00 2001 From: Obieda Date: Wed, 7 Jan 2026 14:45:51 +0200 Subject: [PATCH 07/36] Update ci-build.yml --- .github/workflows/ci-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 0fcc8dfb..36a5bb5c 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -1,4 +1,4 @@ -# Modern Android CI/CD workflow with APK artifact +# Modern Android CI/CD workflow with APK artifact name: Android CI Build From 71cd22a4eaf66d983958584184647a73dc923fa5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:56:21 +0000 Subject: [PATCH 08/36] Initial plan From 6c85e14409c774de94433a0ef6f8add749e22b42 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 13:17:25 +0000 Subject: [PATCH 09/36] Upgrade to Material 3 Expressive with modern neumorphic components Co-authored-by: obieda-hussien <184546202+obieda-hussien@users.noreply.github.com> --- app/build.gradle | 75 +- .../neumorphiccompose/MainActivity.kt | 876 ++++++++++++++---- .../neumorphiccompose/ui/theme/Color.kt | 28 + .../neumorphiccompose/ui/theme/Shape.kt | 11 +- .../neumorphiccompose/ui/theme/Theme.kt | 170 +++- .../neumorphiccompose/ui/theme/Type.kt | 112 ++- app/src/main/res/values-night/themes.xml | 7 +- app/src/main/res/values/themes.xml | 11 +- build.gradle | 13 +- gradle/wrapper/gradle-wrapper.properties | 2 +- .../aapt/AndroidManifest.xml | 7 + .../aapt/output-metadata.json | 18 + .../debug/syncDebugLibJars/classes.jar | Bin 0 -> 31239 bytes .../aar-metadata.properties | 6 + .../annotationProcessors.json | 1 + .../extractDebugAnnotations/typedefs.txt | 0 .../debug/generateDebugRFile/R.jar | Bin 0 -> 5214 bytes .../debug/generateDebugRFile/R.txt | 68 ++ .../debug-mergeJavaRes/merge-state | Bin 0 -> 880 bytes .../compile-file-map.properties | 1 + .../merged.dir/values/values.xml | 88 ++ .../debug/packageDebugResources/merger.xml | 164 ++++ .../mergeDebugJniLibFolders/merger.xml | 2 + .../incremental/mergeDebugShaders/merger.xml | 2 + .../incremental/packageDebugAssets/merger.xml | 2 + .../library-views_debug.kotlin_module | Bin 0 -> 24 bytes .../debug/parseDebugLocalResources/R-def.txt | 26 + .../manifest-merger-blame-debug-report.txt | 7 + .../proguard.txt | 2 + .../feature-library-views.jar | Bin 0 -> 205 bytes .../processDebugManifest/AndroidManifest.xml | 7 + .../nestedResourcesValidationReport.txt | 1 + .../packageDebugResources/values/values.xml | 88 ++ .../generateDebugRFile/package-aware-r.txt | 25 + .../caches-jvm/inputs/source-to-output.tab | Bin 0 -> 4096 bytes .../inputs/source-to-output.tab.keystream | Bin 0 -> 4096 bytes .../inputs/source-to-output.tab.keystream.len | Bin 0 -> 8 bytes .../inputs/source-to-output.tab.len | Bin 0 -> 8 bytes .../inputs/source-to-output.tab.values.at | Bin 0 -> 1848 bytes .../caches-jvm/inputs/source-to-output.tab_i | Bin 0 -> 32768 bytes .../inputs/source-to-output.tab_i.len | Bin 0 -> 8 bytes .../jvm/kotlin/class-attributes.tab | Bin 0 -> 4096 bytes .../jvm/kotlin/class-attributes.tab.keystream | Bin 0 -> 4096 bytes .../kotlin/class-attributes.tab.keystream.len | Bin 0 -> 8 bytes .../jvm/kotlin/class-attributes.tab.len | Bin 0 -> 8 bytes .../jvm/kotlin/class-attributes.tab.values.at | Bin 0 -> 64 bytes .../jvm/kotlin/class-attributes.tab_i | Bin 0 -> 32768 bytes .../jvm/kotlin/class-attributes.tab_i.len | Bin 0 -> 8 bytes .../jvm/kotlin/class-fq-name-to-source.tab | Bin 0 -> 4096 bytes .../class-fq-name-to-source.tab.keystream | Bin 0 -> 4096 bytes .../class-fq-name-to-source.tab.keystream.len | Bin 0 -> 8 bytes .../kotlin/class-fq-name-to-source.tab.len | Bin 0 -> 8 bytes .../class-fq-name-to-source.tab.values.at | Bin 0 -> 871 bytes .../jvm/kotlin/class-fq-name-to-source.tab_i | Bin 0 -> 32768 bytes .../kotlin/class-fq-name-to-source.tab_i.len | Bin 0 -> 8 bytes .../jvm/kotlin/internal-name-to-source.tab | Bin 0 -> 4096 bytes .../internal-name-to-source.tab.keystream | Bin 0 -> 4096 bytes .../internal-name-to-source.tab.keystream.len | Bin 0 -> 8 bytes .../kotlin/internal-name-to-source.tab.len | Bin 0 -> 8 bytes .../internal-name-to-source.tab.values.at | Bin 0 -> 1369 bytes .../jvm/kotlin/internal-name-to-source.tab_i | Bin 0 -> 32768 bytes .../kotlin/internal-name-to-source.tab_i.len | Bin 0 -> 8 bytes .../cacheable/caches-jvm/jvm/kotlin/proto.tab | Bin 0 -> 4096 bytes .../caches-jvm/jvm/kotlin/proto.tab.keystream | Bin 0 -> 4096 bytes .../jvm/kotlin/proto.tab.keystream.len | Bin 0 -> 8 bytes .../caches-jvm/jvm/kotlin/proto.tab.len | Bin 0 -> 8 bytes .../caches-jvm/jvm/kotlin/proto.tab.values.at | Bin 0 -> 9834 bytes .../caches-jvm/jvm/kotlin/proto.tab_i | Bin 0 -> 32768 bytes .../caches-jvm/jvm/kotlin/proto.tab_i.len | Bin 0 -> 8 bytes .../jvm/kotlin/source-to-classes.tab | Bin 0 -> 4096 bytes .../kotlin/source-to-classes.tab.keystream | Bin 0 -> 4096 bytes .../source-to-classes.tab.keystream.len | Bin 0 -> 8 bytes .../jvm/kotlin/source-to-classes.tab.len | Bin 0 -> 8 bytes .../kotlin/source-to-classes.tab.values.at | Bin 0 -> 1034 bytes .../jvm/kotlin/source-to-classes.tab_i | Bin 0 -> 32768 bytes .../jvm/kotlin/source-to-classes.tab_i.len | Bin 0 -> 8 bytes .../caches-jvm/jvm/kotlin/subtypes.tab | Bin 0 -> 4096 bytes .../jvm/kotlin/subtypes.tab.keystream | Bin 0 -> 4096 bytes .../jvm/kotlin/subtypes.tab.keystream.len | Bin 0 -> 8 bytes .../caches-jvm/jvm/kotlin/subtypes.tab.len | Bin 0 -> 8 bytes .../jvm/kotlin/subtypes.tab.values.at | Bin 0 -> 300 bytes .../caches-jvm/jvm/kotlin/subtypes.tab_i | Bin 0 -> 32768 bytes .../caches-jvm/jvm/kotlin/subtypes.tab_i.len | Bin 0 -> 8 bytes .../caches-jvm/jvm/kotlin/supertypes.tab | Bin 0 -> 4096 bytes .../jvm/kotlin/supertypes.tab.keystream | Bin 0 -> 4096 bytes .../jvm/kotlin/supertypes.tab.keystream.len | Bin 0 -> 8 bytes .../caches-jvm/jvm/kotlin/supertypes.tab.len | Bin 0 -> 8 bytes .../jvm/kotlin/supertypes.tab.values.at | Bin 0 -> 170 bytes .../caches-jvm/jvm/kotlin/supertypes.tab_i | Bin 0 -> 32768 bytes .../jvm/kotlin/supertypes.tab_i.len | Bin 0 -> 8 bytes .../cacheable/caches-jvm/lookups/counters.tab | 2 + .../caches-jvm/lookups/file-to-id.tab | Bin 0 -> 4096 bytes .../lookups/file-to-id.tab.keystream | Bin 0 -> 4096 bytes .../lookups/file-to-id.tab.keystream.len | Bin 0 -> 8 bytes .../caches-jvm/lookups/file-to-id.tab.len | Bin 0 -> 8 bytes .../lookups/file-to-id.tab.values.at | Bin 0 -> 67 bytes .../caches-jvm/lookups/file-to-id.tab_i | Bin 0 -> 32768 bytes .../caches-jvm/lookups/file-to-id.tab_i.len | Bin 0 -> 8 bytes .../caches-jvm/lookups/id-to-file.tab | Bin 0 -> 4096 bytes .../lookups/id-to-file.tab.keystream | Bin 0 -> 4096 bytes .../lookups/id-to-file.tab.keystream.len | Bin 0 -> 8 bytes .../caches-jvm/lookups/id-to-file.tab.len | Bin 0 -> 8 bytes .../lookups/id-to-file.tab.values.at | Bin 0 -> 298 bytes .../caches-jvm/lookups/id-to-file.tab_i | Bin 0 -> 32768 bytes .../caches-jvm/lookups/id-to-file.tab_i.len | Bin 0 -> 8 bytes .../cacheable/caches-jvm/lookups/lookups.tab | Bin 0 -> 8192 bytes .../caches-jvm/lookups/lookups.tab.keystream | Bin 0 -> 36864 bytes .../lookups/lookups.tab.keystream.len | Bin 0 -> 8 bytes .../caches-jvm/lookups/lookups.tab.len | Bin 0 -> 8 bytes .../caches-jvm/lookups/lookups.tab.values.at | Bin 0 -> 5135 bytes .../caches-jvm/lookups/lookups.tab_i | Bin 0 -> 32768 bytes .../caches-jvm/lookups/lookups.tab_i.len | Bin 0 -> 8 bytes .../cacheable/last-build.bin | Bin 0 -> 18 bytes .../shrunk-classpath-snapshot.bin | Bin 0 -> 31132 bytes .../local-state/build-history.bin | Bin 0 -> 48 bytes .../build/outputs/aar/library-views-debug.aar | Bin 0 -> 31688 bytes .../logs/manifest-merger-debug-report.txt | 16 + .../library-views_debug.kotlin_module | Bin 0 -> 24 bytes .../library/views/NeuCornerType.class | Bin 0 -> 1919 bytes .../library/views/NeuShapeType.class | Bin 0 -> 1972 bytes .../views/NeumorphicButton$WhenMappings.class | Bin 0 -> 1603 bytes .../library/views/NeumorphicButton.class | Bin 0 -> 18364 bytes .../NeumorphicCardView$WhenMappings.class | Bin 0 -> 1609 bytes .../library/views/NeumorphicCardView.class | Bin 0 -> 18037 bytes .../views/NeumorphicView$WhenMappings.class | Bin 0 -> 1597 bytes .../library/views/NeumorphicView.class | Bin 0 -> 17248 bytes library/build.gradle | 51 +- .../nikhilchaudhari/library/NeuExtensions.kt | 153 +++ .../me/nikhilchaudhari/library/NeuTheme.kt | 162 +++- .../me/nikhilchaudhari/library/Neumorphic.kt | 157 ++++ .../library/components/NeuComponents.kt | 667 +++++++++++++ .../library/internal/StackBlur.kt | 3 +- 132 files changed, 2717 insertions(+), 314 deletions(-) create mode 100644 library-views/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/AndroidManifest.xml create mode 100644 library-views/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/output-metadata.json create mode 100644 library-views/build/intermediates/aar_main_jar/debug/syncDebugLibJars/classes.jar create mode 100644 library-views/build/intermediates/aar_metadata/debug/writeDebugAarMetadata/aar-metadata.properties create mode 100644 library-views/build/intermediates/annotation_processor_list/debug/javaPreCompileDebug/annotationProcessors.json create mode 100644 library-views/build/intermediates/annotations_typedef_file/debug/extractDebugAnnotations/typedefs.txt create mode 100644 library-views/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar create mode 100644 library-views/build/intermediates/compile_symbol_list/debug/generateDebugRFile/R.txt create mode 100644 library-views/build/intermediates/incremental/debug-mergeJavaRes/merge-state create mode 100644 library-views/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties create mode 100644 library-views/build/intermediates/incremental/debug/packageDebugResources/merged.dir/values/values.xml create mode 100644 library-views/build/intermediates/incremental/debug/packageDebugResources/merger.xml create mode 100644 library-views/build/intermediates/incremental/mergeDebugJniLibFolders/merger.xml create mode 100644 library-views/build/intermediates/incremental/mergeDebugShaders/merger.xml create mode 100644 library-views/build/intermediates/incremental/packageDebugAssets/merger.xml create mode 100644 library-views/build/intermediates/java_res/debug/processDebugJavaRes/out/META-INF/library-views_debug.kotlin_module create mode 100644 library-views/build/intermediates/local_only_symbol_list/debug/parseDebugLocalResources/R-def.txt create mode 100644 library-views/build/intermediates/manifest_merge_blame_file/debug/processDebugManifest/manifest-merger-blame-debug-report.txt create mode 100644 library-views/build/intermediates/merged_consumer_proguard_file/debug/mergeDebugConsumerProguardFiles/proguard.txt create mode 100644 library-views/build/intermediates/merged_java_res/debug/mergeDebugJavaResource/feature-library-views.jar create mode 100644 library-views/build/intermediates/merged_manifest/debug/processDebugManifest/AndroidManifest.xml create mode 100644 library-views/build/intermediates/nested_resources_validation_report/debug/generateDebugResources/nestedResourcesValidationReport.txt create mode 100644 library-views/build/intermediates/packaged_res/debug/packageDebugResources/values/values.xml create mode 100644 library-views/build/intermediates/symbol_list_with_package_name/debug/generateDebugRFile/package-aware-r.txt create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream.len create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.len create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.values.at create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i.len create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream.len create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.len create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.values.at create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i.len create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream.len create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.len create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.values.at create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i.len create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream.len create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.len create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.values.at create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i.len create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream.len create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.len create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values.at create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i.len create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream.len create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.len create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.values.at create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i.len create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.keystream create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.keystream.len create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.len create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.values.at create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab_i create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab_i.len create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.keystream create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.keystream.len create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.len create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.values.at create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab_i create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab_i.len create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/counters.tab create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream.len create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.len create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.values.at create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i.len create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream.len create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.len create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.values.at create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i.len create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream.len create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.len create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.values.at create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab_i create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab_i.len create mode 100644 library-views/build/kotlin/compileDebugKotlin/cacheable/last-build.bin create mode 100644 library-views/build/kotlin/compileDebugKotlin/classpath-snapshot/shrunk-classpath-snapshot.bin create mode 100644 library-views/build/kotlin/compileDebugKotlin/local-state/build-history.bin create mode 100644 library-views/build/outputs/aar/library-views-debug.aar create mode 100644 library-views/build/outputs/logs/manifest-merger-debug-report.txt create mode 100644 library-views/build/tmp/kotlin-classes/debug/META-INF/library-views_debug.kotlin_module create mode 100644 library-views/build/tmp/kotlin-classes/debug/me/nikhilchaudhari/library/views/NeuCornerType.class create mode 100644 library-views/build/tmp/kotlin-classes/debug/me/nikhilchaudhari/library/views/NeuShapeType.class create mode 100644 library-views/build/tmp/kotlin-classes/debug/me/nikhilchaudhari/library/views/NeumorphicButton$WhenMappings.class create mode 100644 library-views/build/tmp/kotlin-classes/debug/me/nikhilchaudhari/library/views/NeumorphicButton.class create mode 100644 library-views/build/tmp/kotlin-classes/debug/me/nikhilchaudhari/library/views/NeumorphicCardView$WhenMappings.class create mode 100644 library-views/build/tmp/kotlin-classes/debug/me/nikhilchaudhari/library/views/NeumorphicCardView.class create mode 100644 library-views/build/tmp/kotlin-classes/debug/me/nikhilchaudhari/library/views/NeumorphicView$WhenMappings.class create mode 100644 library-views/build/tmp/kotlin-classes/debug/me/nikhilchaudhari/library/views/NeumorphicView.class create mode 100644 library/src/main/java/me/nikhilchaudhari/library/components/NeuComponents.kt diff --git a/app/build.gradle b/app/build.gradle index a067b274..955c2196 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,18 +1,19 @@ plugins { id 'com.android.application' id 'kotlin-android' + id 'org.jetbrains.kotlin.plugin.compose' version '2.0.21' } android { namespace 'me.nikhilchaudhari.neumorphiccompose' - compileSdk 34 + compileSdk 35 defaultConfig { applicationId "me.nikhilchaudhari.neumorphiccompose" - minSdk 21 - targetSdk 34 - versionCode 2 - versionName "2.0" + minSdk 24 + targetSdk 35 + versionCode 3 + versionName "3.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -36,27 +37,57 @@ android { buildFeatures { compose true } - composeOptions { - kotlinCompilerExtensionVersion compose_compiler_version - } } dependencies { - - implementation 'androidx.core:core-ktx:1.12.0' - implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'com.google.android.material:material:1.11.0' - implementation "androidx.compose.ui:ui:$compose_version" - implementation "androidx.compose.material:material:$compose_version" + // Compose BOM for version management + implementation platform("androidx.compose:compose-bom:$compose_bom_version") + + implementation 'androidx.core:core-ktx:1.15.0' + implementation 'androidx.appcompat:appcompat:1.7.0' + implementation 'com.google.android.material:material:1.12.0' + + // Compose UI + implementation "androidx.compose.ui:ui" + implementation "androidx.compose.ui:ui-graphics" + implementation "androidx.compose.ui:ui-tooling-preview" + implementation "androidx.compose.foundation:foundation" + + // Material Design 3 - Expressive with latest features implementation "androidx.compose.material3:material3:$material3_version" - implementation "androidx.compose.ui:ui-tooling:$compose_version" - implementation "androidx.compose.animation:animation:$compose_version" - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2' - implementation 'androidx.activity:activity-compose:1.8.2' + implementation "androidx.compose.material3.adaptive:adaptive:$material3_adaptive_version" + implementation "androidx.compose.material3.adaptive:adaptive-layout:$material3_adaptive_version" + implementation "androidx.compose.material3.adaptive:adaptive-navigation:$material3_adaptive_version" + + // Animation - including spring physics and shared element + implementation "androidx.compose.animation:animation" + implementation "androidx.compose.animation:animation-core" + implementation "androidx.compose.animation:animation-graphics" + + // Material icons extended + implementation "androidx.compose.material:material-icons-extended" + + // Lifecycle + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.7' + implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.8.7' + implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7' + + // Activity Compose + implementation 'androidx.activity:activity-compose:1.9.3' + + // Navigation + implementation 'androidx.navigation:navigation-compose:2.8.5' + + // Library module implementation project(path: ':library') -// implementation "me.nikhilchaudhari:composeNeumorphism:1.0.0-alpha" + + // Testing testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' - androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" + androidTestImplementation 'androidx.test.ext:junit:1.2.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' + androidTestImplementation "androidx.compose.ui:ui-test-junit4" + + // Debug tooling + debugImplementation "androidx.compose.ui:ui-tooling" + debugImplementation "androidx.compose.ui:ui-test-manifest" } \ No newline at end of file diff --git a/app/src/main/java/me/nikhilchaudhari/neumorphiccompose/MainActivity.kt b/app/src/main/java/me/nikhilchaudhari/neumorphiccompose/MainActivity.kt index 2c2a21a6..bbb17b55 100644 --- a/app/src/main/java/me/nikhilchaudhari/neumorphiccompose/MainActivity.kt +++ b/app/src/main/java/me/nikhilchaudhari/neumorphiccompose/MainActivity.kt @@ -1,47 +1,96 @@ package me.nikhilchaudhari.neumorphiccompose - import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.Image +import androidx.activity.enableEdgeToEdge +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.Notifications +import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.outlined.FavoriteBorder +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import me.nikhilchaudhari.library.NeuInsets +import me.nikhilchaudhari.library.NeuTheme +import me.nikhilchaudhari.library.components.NeuButton +import me.nikhilchaudhari.library.components.NeuCard +import me.nikhilchaudhari.library.components.NeuChip +import me.nikhilchaudhari.library.components.NeuIconButton +import me.nikhilchaudhari.library.components.NeuSwitch +import me.nikhilchaudhari.library.components.NeuTextField +import me.nikhilchaudhari.library.expressiveNeumorphic +import me.nikhilchaudhari.library.expressiveNeumorphicClickable import me.nikhilchaudhari.library.neumorphic -import me.nikhilchaudhari.library.shapes.NeuShape import me.nikhilchaudhari.library.shapes.Pot import me.nikhilchaudhari.library.shapes.Pressed import me.nikhilchaudhari.library.shapes.Punched +import me.nikhilchaudhari.library.softNeumorphic import me.nikhilchaudhari.neumorphiccompose.ui.theme.NeumorphismComposeTheme class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - - //TODO: Fix the colors acc to themes + enableEdgeToEdge() + setContent { NeumorphismComposeTheme { - // A surface container using the 'background' color from the theme - Surface(color = Color(236, 234, 235)) { - Greeting("Android") + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.surface + ) { + MainContent() } } } @@ -49,247 +98,670 @@ class MainActivity : ComponentActivity() { } @Composable -fun Greeting(name: String) { - Column(modifier = Modifier.fillMaxSize()) { - PunchedCards() - PunchedButtons() -// PotCard() - PressedRow() - SimpleDesignCard() +fun MainContent() { + val neuColorScheme = if (isSystemInDarkTheme()) { + NeuTheme.DarkColorScheme + } else { + NeuTheme.LightColorScheme + } + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(neuColorScheme.backgroundColor), + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + // Header Section + item { + HeaderSection(colorScheme = neuColorScheme) + } + + // Search Bar + item { + SearchBarSection(colorScheme = neuColorScheme) + } + + // Quick Actions + item { + QuickActionsSection(colorScheme = neuColorScheme) + } + + // Main Cards Section + item { + MainCardsSection(colorScheme = neuColorScheme) + } + + // Interactive Controls + item { + InteractiveControlsSection(colorScheme = neuColorScheme) + } + + // Chips Section + item { + ChipsSection(colorScheme = neuColorScheme) + } + + // Bottom Navigation Preview + item { + BottomNavigationSection(colorScheme = neuColorScheme) + } + + // Spacing at the bottom + item { + Spacer(modifier = Modifier.height(32.dp)) + } + } +} +@Composable +private fun HeaderSection(colorScheme: NeuTheme.NeuColorScheme) { + var notificationEnabled by remember { mutableStateOf(false) } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 32.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column { + Text( + text = "Welcome Back", + style = MaterialTheme.typography.titleMedium, + color = colorScheme.onBackgroundColor.copy(alpha = 0.7f) + ) + Text( + text = "Neumorphic UI", + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + color = colorScheme.onBackgroundColor + ) + } + + NeuIconButton( + onClick = { notificationEnabled = !notificationEnabled }, + colorScheme = colorScheme, + size = 52.dp + ) { + Icon( + imageVector = if (notificationEnabled) Icons.Filled.Notifications else Icons.Filled.Notifications, + contentDescription = "Notifications", + tint = if (notificationEnabled) colorScheme.accentColor else colorScheme.onBackgroundColor + ) + } } } +@Composable +private fun SearchBarSection(colorScheme: NeuTheme.NeuColorScheme) { + var searchText by remember { mutableStateOf("") } + + NeuTextField( + value = searchText, + onValueChange = { searchText = it }, + placeholder = "Search anything...", + colorScheme = colorScheme, + modifier = Modifier.fillMaxWidth(), + leadingIcon = { + Icon( + imageVector = Icons.Default.Search, + contentDescription = "Search", + tint = colorScheme.onBackgroundColor.copy(alpha = 0.6f) + ) + } + ) +} @Composable -fun SimpleDesignCard() { - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { - Card( - backgroundColor = Color(236, 234, 235), - elevation = 0.dp, - shape = RoundedCornerShape(6.dp), - modifier = Modifier - .padding(16.dp) - .size(400.dp, 350.dp) - .neumorphic( - neuShape = Pot.Rounded(6.dp), - strokeWidth = 7.dp, -// neuInsets = NeuInsets(8.dp, 8.dp) - ) +private fun QuickActionsSection(colorScheme: NeuTheme.NeuColorScheme) { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text( + text = "Quick Actions", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = colorScheme.onBackgroundColor + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly ) { - Column( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.Top, - horizontalAlignment = Alignment.CenterHorizontally - ) { + QuickActionItem( + icon = Icons.Default.Home, + label = "Home", + colorScheme = colorScheme + ) + QuickActionItem( + icon = Icons.Default.Person, + label = "Profile", + colorScheme = colorScheme + ) + QuickActionItem( + icon = Icons.Default.Settings, + label = "Settings", + colorScheme = colorScheme + ) + QuickActionItem( + icon = Icons.Default.Favorite, + label = "Favorites", + colorScheme = colorScheme + ) + } + } +} - SimpleOvalCard( - resource = R.drawable.ic_baseline_emoji_emotions_24, - size = 64.dp +@Composable +private fun QuickActionItem( + icon: ImageVector, + label: String, + colorScheme: NeuTheme.NeuColorScheme +) { + var isPressed by remember { mutableStateOf(false) } + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Box( + modifier = Modifier + .size(64.dp) + .clip(RoundedCornerShape(16.dp)) + .expressiveNeumorphicClickable( + onClick = { isPressed = !isPressed }, + colorScheme = colorScheme, + neuShape = Punched.Rounded(16.dp), + elevation = 8.dp ) + .background(colorScheme.backgroundColor, RoundedCornerShape(16.dp)), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = icon, + contentDescription = label, + tint = if (isPressed) colorScheme.accentColor else colorScheme.onBackgroundColor, + modifier = Modifier.size(28.dp) + ) + } + + Text( + text = label, + style = MaterialTheme.typography.labelMedium, + color = colorScheme.onBackgroundColor + ) + } +} +@Composable +private fun MainCardsSection(colorScheme: NeuTheme.NeuColorScheme) { + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + Text( + text = "Featured", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = colorScheme.onBackgroundColor + ) + + // Main Featured Card with Pot shape + NeuCard( + colorScheme = colorScheme, + neuShape = Pot.Rounded(20.dp), + modifier = Modifier.fillMaxWidth(), + contentPadding = PaddingValues(20.dp) + ) { + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { Row( - modifier = Modifier - .size(300.dp, 150.dp) - .clip(RoundedCornerShape(12.dp)) - .neumorphic(neuShape = Pressed.Rounded(12.dp)) - .padding(12.dp), - horizontalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - Text( - text = stringResource(id = R.string.lorem), - textAlign = TextAlign.Center, modifier = Modifier.padding(4.dp) - ) + Column { + Text( + text = "Material 3 Expressive", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = colorScheme.onBackgroundColor + ) + Text( + text = "Spring physics animations", + style = MaterialTheme.typography.bodyMedium, + color = colorScheme.onBackgroundColor.copy(alpha = 0.7f) + ) + } + + Box( + modifier = Modifier + .size(48.dp) + .clip(CircleShape) + .neumorphic( + neuShape = Punched.Oval(), + lightShadowColor = colorScheme.lightShadowColor, + darkShadowColor = colorScheme.darkShadowColor, + elevation = 6.dp + ) + .background(colorScheme.accentColor.copy(alpha = 0.2f), CircleShape), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = "Check", + tint = colorScheme.accentColor + ) + } } - - Row( + + // Pressed inner section + Box( modifier = Modifier .fillMaxWidth() - .padding(8.dp), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically + .height(100.dp) + .clip(RoundedCornerShape(16.dp)) + .neumorphic( + neuShape = Pressed.Rounded(16.dp), + lightShadowColor = colorScheme.lightShadowColor, + darkShadowColor = colorScheme.darkShadowColor, + strokeWidth = 6.dp + ) + .background(colorScheme.backgroundColor, RoundedCornerShape(16.dp)) + .padding(16.dp), + contentAlignment = Alignment.Center ) { - PunchedButton( - - btnShape = CircleShape, - text = "Always", - neuInsets = NeuInsets(6.dp), - neuShape = Punched.Oval() + Text( + text = "This card showcases the Pot shape with an inner Pressed section, demonstrating the depth variations possible with neumorphic design.", + style = MaterialTheme.typography.bodySmall, + color = colorScheme.onBackgroundColor.copy(alpha = 0.8f), + textAlign = TextAlign.Center ) - PunchedButton( - - btnShape = CircleShape, - text = "Never", - neuInsets = NeuInsets(6.dp), - neuShape = Punched.Oval() + } + } + } + + // Two smaller cards in a row + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + NeuCard( + colorScheme = colorScheme, + modifier = Modifier.weight(1f), + elevation = 8.dp + ) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = "Punched", + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold, + color = colorScheme.onBackgroundColor + ) + Text( + text = "Raised effect", + style = MaterialTheme.typography.bodySmall, + color = colorScheme.onBackgroundColor.copy(alpha = 0.6f) + ) + } + } + + Box( + modifier = Modifier + .weight(1f) + .height(80.dp) + .clip(RoundedCornerShape(20.dp)) + .neumorphic( + neuShape = Pressed.Rounded(20.dp), + lightShadowColor = colorScheme.lightShadowColor, + darkShadowColor = colorScheme.darkShadowColor, + strokeWidth = 5.dp + ) + .background(colorScheme.backgroundColor, RoundedCornerShape(20.dp)), + contentAlignment = Alignment.Center + ) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Pressed", + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold, + color = colorScheme.onBackgroundColor + ) + Text( + text = "Sunken effect", + style = MaterialTheme.typography.bodySmall, + color = colorScheme.onBackgroundColor.copy(alpha = 0.6f) ) } - } } } } @Composable -fun PressedRow() { - Row( - modifier = Modifier - .padding(18.dp) - .size(400.dp, 100.dp) - .background(Color(236, 234, 235)) - .clip(RoundedCornerShape(12.dp)) - .neumorphic(neuShape = Pressed.Rounded(12.dp)), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - Image( - painterResource(id = R.drawable.ic_baseline_shopping_cart_24), - contentDescription = "image", modifier = Modifier - .size(80.dp) - .padding(12.dp) - ) +private fun InteractiveControlsSection(colorScheme: NeuTheme.NeuColorScheme) { + var switchState by remember { mutableStateOf(false) } + var buttonCount by remember { mutableIntStateOf(0) } + var sliderValue by remember { mutableFloatStateOf(0.5f) } + + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { Text( - text = "Simple Row wih Pressed shape.", - textAlign = TextAlign.Center + text = "Interactive Controls", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = colorScheme.onBackgroundColor ) - } -} - - -@Composable -fun PotCard() { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - Card( - backgroundColor = Color(236, 234, 235), - elevation = 0.dp, - shape = RoundedCornerShape(12.dp), - modifier = Modifier - .padding(16.dp) - .size(300.dp, 100.dp) - .neumorphic( - neuShape = Pot.Rounded(12.dp), - elevation = 12.dp, - strokeWidth = 8.dp, - neuInsets = NeuInsets(10.dp, 10.dp) - ) + + NeuCard( + colorScheme = colorScheme, + modifier = Modifier.fillMaxWidth() ) { - Row( - modifier = Modifier.fillMaxSize(), - verticalAlignment = Alignment.CenterVertically, + Column( + verticalArrangement = Arrangement.spacedBy(24.dp), + modifier = Modifier.padding(8.dp) ) { - Image( - painterResource(id = R.drawable.ic_baseline_shopping_cart_24), - contentDescription = "image", modifier = Modifier - .size(80.dp) - .padding(12.dp) - ) - Text( - text = "Sample card wih Pot/Puddle/Basin shape.", - textAlign = TextAlign.Center - ) + // Switch Row + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column { + Text( + text = "Dark Mode", + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium, + color = colorScheme.onBackgroundColor + ) + Text( + text = "Spring animated toggle", + style = MaterialTheme.typography.bodySmall, + color = colorScheme.onBackgroundColor.copy(alpha = 0.6f) + ) + } + + NeuSwitch( + checked = switchState, + onCheckedChange = { switchState = it }, + colorScheme = colorScheme + ) + } + + // Button Row + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column { + Text( + text = "Counter: $buttonCount", + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium, + color = colorScheme.onBackgroundColor + ) + Text( + text = "Tap to increment", + style = MaterialTheme.typography.bodySmall, + color = colorScheme.onBackgroundColor.copy(alpha = 0.6f) + ) + } + + NeuButton( + onClick = { buttonCount++ }, + colorScheme = colorScheme, + contentPadding = PaddingValues(horizontal = 20.dp, vertical = 10.dp) + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = "Add", + modifier = Modifier.size(18.dp) + ) + Text("Add") + } + } + } + + // Slider-like progress + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "Progress", + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium, + color = colorScheme.onBackgroundColor + ) + Text( + text = "${(sliderValue * 100).toInt()}%", + style = MaterialTheme.typography.bodyMedium, + color = colorScheme.accentColor + ) + } + + // Progress track + Box( + modifier = Modifier + .fillMaxWidth() + .height(12.dp) + .clip(RoundedCornerShape(6.dp)) + .neumorphic( + neuShape = Pressed.Rounded(6.dp), + lightShadowColor = colorScheme.lightShadowColor, + darkShadowColor = colorScheme.darkShadowColor, + strokeWidth = 3.dp + ) + .background(colorScheme.backgroundColor, RoundedCornerShape(6.dp)) + ) { + val animatedProgress by animateFloatAsState( + targetValue = sliderValue, + animationSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessLow + ), + label = "progressAnimation" + ) + + Box( + modifier = Modifier + .fillMaxWidth(animatedProgress) + .height(12.dp) + .clip(RoundedCornerShape(6.dp)) + .background(colorScheme.accentColor.copy(alpha = 0.7f), RoundedCornerShape(6.dp)) + ) + } + + // Increase/Decrease buttons for slider + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + NeuButton( + onClick = { sliderValue = (sliderValue - 0.1f).coerceIn(0f, 1f) }, + colorScheme = colorScheme, + elevation = 6.dp, + contentPadding = PaddingValues(horizontal = 24.dp, vertical = 8.dp) + ) { + Text("-10%") + } + + NeuButton( + onClick = { sliderValue = (sliderValue + 0.1f).coerceIn(0f, 1f) }, + colorScheme = colorScheme, + elevation = 6.dp, + contentPadding = PaddingValues(horizontal = 24.dp, vertical = 8.dp) + ) { + Text("+10%") + } + } + } } } } } @Composable -fun PunchedButtons() { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - for (i in 1..2) { - PunchedButton( - btnShape = RoundedCornerShape(6.dp), - text = "Button $i", - neuShape = Punched.Rounded(6.dp), - ) +private fun ChipsSection(colorScheme: NeuTheme.NeuColorScheme) { + var selectedChip by remember { mutableIntStateOf(0) } + val chips = listOf("All", "Design", "Code", "Music", "Art", "Photo") + + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text( + text = "Categories", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = colorScheme.onBackgroundColor + ) + + LazyRow( + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + items(chips.size) { index -> + NeuChip( + onClick = { selectedChip = index }, + selected = selectedChip == index, + colorScheme = colorScheme, + leadingIcon = if (selectedChip == index) { + { + Icon( + imageVector = Icons.Default.Check, + contentDescription = "Selected", + modifier = Modifier.size(16.dp) + ) + } + } else null + ) { + Text(chips[index]) + } + } } } } - @Composable -fun PunchedCards() { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - - val resourceArray = arrayOf( - R.drawable.ic_baseline_android_24, - R.drawable.ic_baseline_warning_24, - R.drawable.ic_baseline_construction_24 +private fun BottomNavigationSection(colorScheme: NeuTheme.NeuColorScheme) { + var selectedNav by remember { mutableIntStateOf(0) } + val navItems = listOf( + Icons.Default.Home to "Home", + Icons.Default.Search to "Search", + Icons.Default.Add to "Add", + Icons.Outlined.FavoriteBorder to "Likes", + Icons.Default.Person to "Profile" + ) + + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text( + text = "Navigation Bar", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = colorScheme.onBackgroundColor ) - - for (i in 0..2) { - SimpleOvalCard(resource = resourceArray[i]) + + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(24.dp)) + .neumorphic( + neuShape = Punched.Rounded(24.dp), + lightShadowColor = colorScheme.lightShadowColor, + darkShadowColor = colorScheme.darkShadowColor, + elevation = 10.dp + ) + .background(colorScheme.backgroundColor, RoundedCornerShape(24.dp)) + .padding(8.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceAround, + verticalAlignment = Alignment.CenterVertically + ) { + navItems.forEachIndexed { index, (icon, label) -> + val isSelected = selectedNav == index + val scale by animateFloatAsState( + targetValue = if (isSelected) 1.1f else 1f, + animationSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessMedium + ), + label = "navScale" + ) + + Box( + modifier = Modifier + .size(52.dp) + .scale(scale) + .clip(RoundedCornerShape(16.dp)) + .then( + if (isSelected) { + Modifier.neumorphic( + neuShape = Pressed.Rounded(16.dp), + lightShadowColor = colorScheme.lightShadowColor, + darkShadowColor = colorScheme.darkShadowColor, + strokeWidth = 4.dp + ) + } else { + Modifier.softNeumorphic( + neuShape = Punched.Rounded(16.dp), + lightShadowColor = colorScheme.lightShadowColor, + darkShadowColor = colorScheme.darkShadowColor + ) + } + ) + .background( + if (isSelected) colorScheme.accentColor.copy(alpha = 0.15f) + else colorScheme.backgroundColor, + RoundedCornerShape(16.dp) + ) + .expressiveNeumorphicClickable( + onClick = { selectedNav = index }, + colorScheme = colorScheme, + elevation = 0.dp + ), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = icon, + contentDescription = label, + tint = if (isSelected) colorScheme.accentColor else colorScheme.onBackgroundColor.copy(alpha = 0.6f), + modifier = Modifier.size(24.dp) + ) + } + } + } } } } +@Preview(showBackground = true, showSystemUi = true) @Composable -fun PunchedButton( - btnShape: Shape, text: String, neuShape: NeuShape, - neuInsets: NeuInsets = NeuInsets(4.dp, 4.dp) -) { - Button( - onClick = { /*TODO*/ }, - contentPadding = PaddingValues(16.dp), - colors = ButtonDefaults.buttonColors(backgroundColor = Color(236, 234, 235)), - elevation = ButtonDefaults.elevation(0.dp), - shape = btnShape, - modifier = Modifier - .padding(12.dp) - .neumorphic( - neuShape = neuShape, - neuInsets = neuInsets - ) - ) { - Text(text = text) - } -} - - -@Composable -fun SimpleOvalCard(resource: Int, size: Dp = 32.dp) { - Card( - backgroundColor = Color(236, 234, 235), - elevation = 0.dp, - border = BorderStroke(2.dp, Color(236, 234, 235)), - shape = CircleShape, modifier = Modifier - .size(100.dp) - .padding(16.dp) - .neumorphic(neuShape = Punched.Oval()) - ) { - Row( +fun MainContentPreview() { + NeumorphismComposeTheme { + Surface( modifier = Modifier.fillMaxSize(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center + color = MaterialTheme.colorScheme.surface ) { - Image( - painterResource(id = resource), - contentDescription = "image", modifier = Modifier.size(size) - ) + MainContent() } } } -@Preview(showBackground = true) +@Preview(showBackground = true, showSystemUi = true, uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES) @Composable -fun DefaultPreview() { +fun MainContentDarkPreview() { NeumorphismComposeTheme { - Greeting("Android") + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.surface + ) { + MainContent() + } } } \ No newline at end of file diff --git a/app/src/main/java/me/nikhilchaudhari/neumorphiccompose/ui/theme/Color.kt b/app/src/main/java/me/nikhilchaudhari/neumorphiccompose/ui/theme/Color.kt index fe14dd40..08dedbf5 100644 --- a/app/src/main/java/me/nikhilchaudhari/neumorphiccompose/ui/theme/Color.kt +++ b/app/src/main/java/me/nikhilchaudhari/neumorphiccompose/ui/theme/Color.kt @@ -2,6 +2,34 @@ package me.nikhilchaudhari.neumorphiccompose.ui.theme import androidx.compose.ui.graphics.Color +// Material 3 Expressive - Primary +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6750A4) +val PurpleGrey40 = Color(0xFF625B71) +val Pink40 = Color(0xFF7D5260) + +// Neumorphic specific colors - Light +val NeuBackgroundLight = Color(0xFFECEAEB) +val NeuLightShadowLight = Color(0xFFFFFFFF) +val NeuDarkShadowLight = Color(0xFFC8C8C8) + +// Neumorphic specific colors - Dark +val NeuBackgroundDark = Color(0xFF2D2D2D) +val NeuLightShadowDark = Color(0xFF3D3D3D) +val NeuDarkShadowDark = Color(0xFF1D1D1D) + +// Additional expressive colors +val Lavender = Color(0xFFCBA6F7) +val Mauve = Color(0xFF8839EF) +val Peach = Color(0xFFFAB387) +val Teal = Color(0xFF94E2D5) +val Sky = Color(0xFF89DCEB) +val Sapphire = Color(0xFF74C7EC) + +// Legacy colors for backward compatibility val Purple200 = Color(0xFFBB86FC) val Purple500 = Color(0xFF6200EE) val Purple700 = Color(0xFF3700B3) diff --git a/app/src/main/java/me/nikhilchaudhari/neumorphiccompose/ui/theme/Shape.kt b/app/src/main/java/me/nikhilchaudhari/neumorphiccompose/ui/theme/Shape.kt index 2226c70d..07b3c5be 100644 --- a/app/src/main/java/me/nikhilchaudhari/neumorphiccompose/ui/theme/Shape.kt +++ b/app/src/main/java/me/nikhilchaudhari/neumorphiccompose/ui/theme/Shape.kt @@ -1,11 +1,14 @@ package me.nikhilchaudhari.neumorphiccompose.ui.theme import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Shapes +import androidx.compose.material3.Shapes import androidx.compose.ui.unit.dp +// Material 3 Expressive Shapes - larger corner radii for more expressive look val Shapes = Shapes( - small = RoundedCornerShape(4.dp), - medium = RoundedCornerShape(4.dp), - large = RoundedCornerShape(0.dp) + extraSmall = RoundedCornerShape(4.dp), + small = RoundedCornerShape(8.dp), + medium = RoundedCornerShape(16.dp), + large = RoundedCornerShape(24.dp), + extraLarge = RoundedCornerShape(32.dp) ) \ No newline at end of file diff --git a/app/src/main/java/me/nikhilchaudhari/neumorphiccompose/ui/theme/Theme.kt b/app/src/main/java/me/nikhilchaudhari/neumorphiccompose/ui/theme/Theme.kt index d12fc43d..28daf44e 100644 --- a/app/src/main/java/me/nikhilchaudhari/neumorphiccompose/ui/theme/Theme.kt +++ b/app/src/main/java/me/nikhilchaudhari/neumorphiccompose/ui/theme/Theme.kt @@ -1,45 +1,163 @@ package me.nikhilchaudhari.neumorphiccompose.ui.theme +import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material.MaterialTheme -import androidx.compose.material.darkColors -import androidx.compose.material.lightColors +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext -private val DarkColorPalette = darkColors( - primary = Purple200, - primaryVariant = Purple700, - secondary = Teal200 -) +// Material 3 Expressive Colors - Light +private val md_theme_light_primary = Color(0xFF6750A4) +private val md_theme_light_onPrimary = Color(0xFFFFFFFF) +private val md_theme_light_primaryContainer = Color(0xFFEADDFF) +private val md_theme_light_onPrimaryContainer = Color(0xFF21005D) +private val md_theme_light_secondary = Color(0xFF625B71) +private val md_theme_light_onSecondary = Color(0xFFFFFFFF) +private val md_theme_light_secondaryContainer = Color(0xFFE8DEF8) +private val md_theme_light_onSecondaryContainer = Color(0xFF1D192B) +private val md_theme_light_tertiary = Color(0xFF7D5260) +private val md_theme_light_onTertiary = Color(0xFFFFFFFF) +private val md_theme_light_tertiaryContainer = Color(0xFFFFD8E4) +private val md_theme_light_onTertiaryContainer = Color(0xFF31111D) +private val md_theme_light_error = Color(0xFFB3261E) +private val md_theme_light_errorContainer = Color(0xFFF9DEDC) +private val md_theme_light_onError = Color(0xFFFFFFFF) +private val md_theme_light_onErrorContainer = Color(0xFF410E0B) +private val md_theme_light_background = Color(0xFFFFFBFE) +private val md_theme_light_onBackground = Color(0xFF1C1B1F) +private val md_theme_light_surface = Color(0xFFECEAEB) // Neumorphic surface +private val md_theme_light_onSurface = Color(0xFF1C1B1F) +private val md_theme_light_surfaceVariant = Color(0xFFE7E0EC) +private val md_theme_light_onSurfaceVariant = Color(0xFF49454F) +private val md_theme_light_outline = Color(0xFF79747E) +private val md_theme_light_inverseOnSurface = Color(0xFFF4EFF4) +private val md_theme_light_inverseSurface = Color(0xFF313033) +private val md_theme_light_inversePrimary = Color(0xFFD0BCFF) +private val md_theme_light_surfaceTint = Color(0xFF6750A4) +private val md_theme_light_outlineVariant = Color(0xFFCAC4D0) +private val md_theme_light_scrim = Color(0xFF000000) + +// Material 3 Expressive Colors - Dark +private val md_theme_dark_primary = Color(0xFFD0BCFF) +private val md_theme_dark_onPrimary = Color(0xFF381E72) +private val md_theme_dark_primaryContainer = Color(0xFF4F378B) +private val md_theme_dark_onPrimaryContainer = Color(0xFFEADDFF) +private val md_theme_dark_secondary = Color(0xFFCCC2DC) +private val md_theme_dark_onSecondary = Color(0xFF332D41) +private val md_theme_dark_secondaryContainer = Color(0xFF4A4458) +private val md_theme_dark_onSecondaryContainer = Color(0xFFE8DEF8) +private val md_theme_dark_tertiary = Color(0xFFEFB8C8) +private val md_theme_dark_onTertiary = Color(0xFF492532) +private val md_theme_dark_tertiaryContainer = Color(0xFF633B48) +private val md_theme_dark_onTertiaryContainer = Color(0xFFFFD8E4) +private val md_theme_dark_error = Color(0xFFF2B8B5) +private val md_theme_dark_errorContainer = Color(0xFF8C1D18) +private val md_theme_dark_onError = Color(0xFF601410) +private val md_theme_dark_onErrorContainer = Color(0xFFF9DEDC) +private val md_theme_dark_background = Color(0xFF1C1B1F) +private val md_theme_dark_onBackground = Color(0xFFE6E1E5) +private val md_theme_dark_surface = Color(0xFF2D2D2D) // Neumorphic surface +private val md_theme_dark_onSurface = Color(0xFFE6E1E5) +private val md_theme_dark_surfaceVariant = Color(0xFF49454F) +private val md_theme_dark_onSurfaceVariant = Color(0xFFCAC4D0) +private val md_theme_dark_outline = Color(0xFF938F99) +private val md_theme_dark_inverseOnSurface = Color(0xFF1C1B1F) +private val md_theme_dark_inverseSurface = Color(0xFFE6E1E5) +private val md_theme_dark_inversePrimary = Color(0xFF6750A4) +private val md_theme_dark_surfaceTint = Color(0xFFD0BCFF) +private val md_theme_dark_outlineVariant = Color(0xFF49454F) +private val md_theme_dark_scrim = Color(0xFF000000) -private val LightColorPalette = lightColors( - primary = Purple500, - primaryVariant = Purple700, - secondary = Teal200 +private val LightColorScheme = lightColorScheme( + primary = md_theme_light_primary, + onPrimary = md_theme_light_onPrimary, + primaryContainer = md_theme_light_primaryContainer, + onPrimaryContainer = md_theme_light_onPrimaryContainer, + secondary = md_theme_light_secondary, + onSecondary = md_theme_light_onSecondary, + secondaryContainer = md_theme_light_secondaryContainer, + onSecondaryContainer = md_theme_light_onSecondaryContainer, + tertiary = md_theme_light_tertiary, + onTertiary = md_theme_light_onTertiary, + tertiaryContainer = md_theme_light_tertiaryContainer, + onTertiaryContainer = md_theme_light_onTertiaryContainer, + error = md_theme_light_error, + errorContainer = md_theme_light_errorContainer, + onError = md_theme_light_onError, + onErrorContainer = md_theme_light_onErrorContainer, + background = md_theme_light_background, + onBackground = md_theme_light_onBackground, + surface = md_theme_light_surface, + onSurface = md_theme_light_onSurface, + surfaceVariant = md_theme_light_surfaceVariant, + onSurfaceVariant = md_theme_light_onSurfaceVariant, + outline = md_theme_light_outline, + inverseOnSurface = md_theme_light_inverseOnSurface, + inverseSurface = md_theme_light_inverseSurface, + inversePrimary = md_theme_light_inversePrimary, + surfaceTint = md_theme_light_surfaceTint, + outlineVariant = md_theme_light_outlineVariant, + scrim = md_theme_light_scrim +) - /* Other default colors to override - background = Color.White, - surface = Color.White, - onPrimary = Color.White, - onSecondary = Color.Black, - onBackground = Color.Black, - onSurface = Color.Black, - */ +private val DarkColorScheme = darkColorScheme( + primary = md_theme_dark_primary, + onPrimary = md_theme_dark_onPrimary, + primaryContainer = md_theme_dark_primaryContainer, + onPrimaryContainer = md_theme_dark_onPrimaryContainer, + secondary = md_theme_dark_secondary, + onSecondary = md_theme_dark_onSecondary, + secondaryContainer = md_theme_dark_secondaryContainer, + onSecondaryContainer = md_theme_dark_onSecondaryContainer, + tertiary = md_theme_dark_tertiary, + onTertiary = md_theme_dark_onTertiary, + tertiaryContainer = md_theme_dark_tertiaryContainer, + onTertiaryContainer = md_theme_dark_onTertiaryContainer, + error = md_theme_dark_error, + errorContainer = md_theme_dark_errorContainer, + onError = md_theme_dark_onError, + onErrorContainer = md_theme_dark_onErrorContainer, + background = md_theme_dark_background, + onBackground = md_theme_dark_onBackground, + surface = md_theme_dark_surface, + onSurface = md_theme_dark_onSurface, + surfaceVariant = md_theme_dark_surfaceVariant, + onSurfaceVariant = md_theme_dark_onSurfaceVariant, + outline = md_theme_dark_outline, + inverseOnSurface = md_theme_dark_inverseOnSurface, + inverseSurface = md_theme_dark_inverseSurface, + inversePrimary = md_theme_dark_inversePrimary, + surfaceTint = md_theme_dark_surfaceTint, + outlineVariant = md_theme_dark_outlineVariant, + scrim = md_theme_dark_scrim ) +/** + * Material 3 Expressive Theme for Neumorphism Compose + * Supports dynamic colors on Android 12+ and provides expressive animations + */ @Composable fun NeumorphismComposeTheme( darkTheme: Boolean = isSystemInDarkTheme(), - content: @Composable() () -> Unit + dynamicColor: Boolean = true, + content: @Composable () -> Unit ) { - val colors = if (darkTheme) { - DarkColorPalette - } else { - LightColorPalette + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + darkTheme -> DarkColorScheme + else -> LightColorScheme } MaterialTheme( - colors = colors, + colorScheme = colorScheme, typography = Typography, shapes = Shapes, content = content diff --git a/app/src/main/java/me/nikhilchaudhari/neumorphiccompose/ui/theme/Type.kt b/app/src/main/java/me/nikhilchaudhari/neumorphiccompose/ui/theme/Type.kt index ac8a5544..55530f72 100644 --- a/app/src/main/java/me/nikhilchaudhari/neumorphiccompose/ui/theme/Type.kt +++ b/app/src/main/java/me/nikhilchaudhari/neumorphiccompose/ui/theme/Type.kt @@ -1,28 +1,116 @@ package me.nikhilchaudhari.neumorphiccompose.ui.theme -import androidx.compose.material.Typography +import androidx.compose.material3.Typography import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp -// Set of Material typography styles to start with +// Material 3 Expressive Typography val Typography = Typography( - body1 = TextStyle( + displayLarge = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, - fontSize = 16.sp - ) - /* Other default text styles to override - button = TextStyle( + fontSize = 57.sp, + lineHeight = 64.sp, + letterSpacing = (-0.25).sp + ), + displayMedium = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 45.sp, + lineHeight = 52.sp, + letterSpacing = 0.sp + ), + displaySmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 36.sp, + lineHeight = 44.sp, + letterSpacing = 0.sp + ), + headlineLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 32.sp, + lineHeight = 40.sp, + letterSpacing = 0.sp + ), + headlineMedium = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 28.sp, + lineHeight = 36.sp, + letterSpacing = 0.sp + ), + headlineSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 24.sp, + lineHeight = 32.sp, + letterSpacing = 0.sp + ), + titleLarge = TextStyle( fontFamily = FontFamily.Default, - fontWeight = FontWeight.W500, - fontSize = 14.sp + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + titleMedium = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.15.sp + ), + titleSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.1.sp ), - caption = TextStyle( + bodyLarge = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, - fontSize = 12.sp + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ), + bodyMedium = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.25.sp + ), + bodySmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.4.sp + ), + labelLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.1.sp + ), + labelMedium = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp ) - */ ) \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 0ef9b46b..e86cfef3 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index e0c95732..ab292583 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,6 +1,6 @@ - -