diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 5b908eef..36a5bb5c 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 ] + branches: + - '**' # يعمل عند أي Push على أي فرع في الـ Fork pull_request: - branches: [ main ] + branches: + - '**' # يعمل عند فتح PR لأي فرع داخل الـ Fork + workflow_dispatch: # يسمح بتشغيل الـ Workflow يدوياً من واجهة GitHub 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@v3 # تحديث للإصدار الأحدث + - 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: | + **/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: | + **/build/reports/lint-results*.html + retention-days: 14 diff --git a/.gitignore b/.gitignore index 10cfdbfa..3324b6ac 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /.idea .DS_Store /build +**/build/ /captures .externalNativeBuild .cxx diff --git a/README.md b/README.md index c13e7cff..53d88e35 100644 --- a/README.md +++ b/README.md @@ -1,128 +1,516 @@ -# 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** - optimized in this fork to render shadows with far less CPU/battery cost than the upstream implementation, with no change to visual quality. +[![](https://jitpack.io/v/obieda-hussien/neumorphic-compose-pro.svg)](https://jitpack.io/#obieda-hussien/neumorphic-compose-pro) ![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 ✨ + +- 🎨 **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 +- 🔋 **Low-power rendering** - shadow bitmaps are cached and shared instead of being regenerated on every frame; see [Performance](#performance) +- ✨ **Smooth crossfade transitions** - selection changes (chips, nav bar) fade between states instead of cutting abruptly; see [What's New in 4.0.0](#whats-new-in-400) + +## What's New in 4.0.0 + +Both modules (`library` and `library-views`) are now versioned together - +previously `library` was on `3.1.0` while `library-views` trailed behind at +`2.1.0`, which was more confusing than useful since they're released +together from the same repo. `4.0.0` is a substantial release; the public +API is unchanged (nothing here requires code changes on your end), but a +lot changed underneath it: + +**Fixed:** +- **App freezing on launch** and **battery/CPU drain during animation** - + see [Performance](#performance) for the full breakdown (shared + `RenderScript` context, shadow bitmap caching, downsampled blur). +- **A real crash**: `NeuSwitch`'s and `NeuSlider`'s thumb used + `Modifier.padding()` with a spring-animated offset. Bouncy springs + overshoot past their target before settling, which briefly pushed the + padding value negative - and `Modifier.padding()` throws on any negative + value. Switched to `Modifier.offset()`, which is both the semantically + correct modifier for animated positioning and one that allows negative + values without crashing. +- **Raised (`Punched`) shapes losing their shadow when clipped**: several + components (and demo screens) applied `Modifier.clip()` *before* + `Modifier.neumorphic()`, which cut away the soft shadow's overflow + outside the shape's bounds, leaving a flat-looking box. Fixed throughout + the library and demo; see the updated [Best Practices](#best-practices) + entry on this. The `Pot` shape (raised + recessed combined) needed a + deeper fix, since a single external clip can't apply to only one of its + two shadow passes - it now clips its recessed pass internally instead. +- **Press feedback invisible on quick/light taps**: every component's tap + animation was driven directly by the raw pressed state, which a fast tap + can complete faster than the animation has time to visibly show - so it + only looked like anything happened on a firm, held press. Fixed by + holding the visual "pressed" state for a guaranteed minimum ~100ms + regardless of how quick the actual tap was. +- A cache-key collision risk in `NeuShadowCache` (different colors could + hash to the same key), and a couple of smaller validation/test issues + caught in review. + +**Changed:** +- `NeuFloatingActionButton` and `NeuRadioButton` restyled to match the rest + of the library's soft, dual-shadow look - both previously mixed in a hard + solid-fill circle / colored border, which is a Material-standard look, + not a neumorphic one. +- `NeuChip` and `NeuIconButton` (used by the demo's "Categories" chips and + bottom navigation bar) now crossfade between their raised/recessed + treatments on selection changes instead of cutting between them in a + single frame, and `NeuChip`'s leading icon fades in/out instead of + popping. + +**Added:** +- `NeuPerformanceConfig` - runtime-tunable blur downsampling and shadow + cache budget; see [Performance](#performance). +- Hand-authored baseline profiles for all three modules; see + [Performance](#performance). +- A Gradle version catalog (`gradle/libs.versions.toml`) as the single + source of truth for dependency versions across all three modules, and a + fix for two `implementation`-vs-`api` visibility bugs that could break + external JitPack consumers who don't separately declare `material3`/ + `compose-ui`. +- Real unit/instrumented tests for the caching and shared-`BlurMaker` + logic (previously only placeholder tests existed anywhere in the repo). + +## Installation + +This fork is published via [JitPack](https://jitpack.io/#obieda-hussien/neumorphic-compose-pro). + +Add JitPack to your **project-level** `settings.gradle` (or `build.gradle` for older projects): -### 1. Add the dependency +```kotlin +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + maven { url = uri("https://jitpack.io") } + } +} +``` + +### Jetpack Compose Library + +```kotlin +implementation("com.github.obieda-hussien.neumorphic-compose-pro:library:4.0.0") +``` -And your app level `build.gradle`. (Make sure you have compose dependencies as well) +### XML/Views Library ```kotlin - implementation("me.nikhilchaudhari:composeNeumorphism:1.0.0-alpha02") +implementation("com.github.obieda-hussien.neumorphic-compose-pro:library-views:4.0.0") ``` -### 2. Use +> Coming from the upstream `me.nikhilchaudhari:composeNeumorphism` artifact? The public API is unchanged - swap the dependency coordinates above and everything else keeps working as-is. + +## Quick Start + +### Jetpack Compose -Just add `modifier = Modifier.neumorphic()` to any of your UI element of Jetpack-Compose (just like you do for other modifiers). +```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") +} +``` -![image](https://user-images.githubusercontent.com/16976114/120097042-12adfe00-c14c-11eb-861c-a826108194d6.png) +### XML Layout + +```xml + + + + + + + +``` +### Java Usage -## How do I configure shape, color, etc.? +```java +NeumorphicButton button = new NeumorphicButton(context); +button.setNeuShapeType(NeuShapeType.PUNCHED); +button.setNeuCornerRadius(dpToPx(12)); +button.setNeuElevation(dpToPx(8)); +button.setNeuLightSource(LightSource.TOP_LEFT); +``` -### 1. Shapes -You've three basic **Neu** shapes available - -1. `Punched`, 2. `Pressed`, 3. `Pot` +## Shapes -These are not standard names for Neumorphism UI, I just named them based on how they look (at least to me). +Three neumorphic shapes are available: -And you can also configure the corner families of those shapes as **`Oval`** or **`Rounded`** +| 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 */} -``` - -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) - ) - ) -``` - -The overall configutation -![image](https://user-images.githubusercontent.com/16976114/120100461-780aea80-c15e-11eb-9626-10b684f9c982.png) - - -## 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 - -## 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. - -### 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. - -### 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. - -## 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). +// 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 + } +} +``` + +### Custom Color Scheme + +```kotlin +val customScheme = NeuTheme.customColorScheme( + backgroundColor = Color(0xFFE0E5EC) +) +``` + +## Animation + +### Animated Press Effect (Compose) + +```kotlin +Modifier.animatedNeumorphic( + neuShape = Punched.Rounded(), + elevation = 8.dp, + pressed = isPressed, // from interaction state + animationDuration = 150 +) +``` + +### Clickable with Animation + +```kotlin +Modifier.neumorphicClickable( + onClick = { /* action */ }, + elevation = 8.dp, + neuShape = Punched.Rounded() +) +``` + +### XML Views Animation + +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 only**: Use `Modifier.clip()` when using `Pressed` shape - its inner shadow needs to stay within bounds. **Never clip a `Punched` (raised) or `Pot` shape** - their shadow is *meant* to extend past the shape's own bounds, and `Modifier.clip()` placed before `Modifier.neumorphic()` cuts that overflow away, leaving a flat-looking box instead of a soft shadow. All of this library's own `NeuXxx` components already follow this rule internally. + +## Performance + +Neumorphism is expensive by nature - every soft shadow is a blurred bitmap, and +the upstream implementation regenerated that bitmap **from scratch on every +single draw call**, including every frame of a press animation. This fork +(as of v4.0.0) keeps the exact same visual output but changes *how often* +and *how expensively* that work happens. + +### What was actually draining the battery + +1. **A new `RenderScript` context was created on every recomposition.** The + modifier that owns the blur pipeline was rebuilt every time an animated + property (like elevation during a press) changed - so on API < 31, a brand + new `RenderScript` context (one of the heaviest objects on Android) was spun + up and torn down up to 60 times per second, per neumorphic component. +2. **No caching at all.** Two identical cards in a `LazyColumn`, or a button + returning to the same resting elevation after a press, each regenerated + their own `GradientDrawable` + `Bitmap` + full-resolution CPU blur, even + though the output was pixel-for-pixel identical. +3. **Blur ran at full bitmap resolution**, even though a Gaussian/stack blur + inherently destroys fine detail - so all that extra resolution was being + thrown away by the algorithm itself. +4. **Every neumorphic component owned its own `RenderScript` context.** A + typical screen (header, search bar, a row of quick actions, a couple of + cards, some switches) easily has 15-20+ neumorphic components. Even with + fix #1 in place, that's still 15-20+ separate `RenderScript` contexts all + being created synchronously during the very first frame - which is heavy + enough on the main thread to show up as the app freezing/hanging for a + moment on launch. + +### What changed + +| Fix | Effect | +|---|---| +| `BlurMaker` (and its `RenderScript` context) is now a single app-wide singleton (`NeuBlurMakerHolder`) shared by every `neumorphic()` call and every XML view | At most **one** `RenderScript` context is ever created for the app's entire lifetime, no matter how many neumorphic components exist - this is what fixes the launch freeze | +| Persistent `ScriptIntrinsicBlur` reused across calls | Cuts remaining RenderScript-path overhead further | +| Process-wide LRU shadow-bitmap cache (`NeuShadowCache`), keyed by size/elevation/colors/shape/light source | Identical components (list items, buttons at rest) share one bitmap instead of each generating their own | +| Elevation/stroke quantized to 0.5dp buckets for cache keys | A ~200-frame spring animation collapses into a handful of reusable shadow bitmaps instead of 200 unique ones - imperceptible visually | +| Blur now runs on a 2x-downsampled bitmap, then upscales | ~4x fewer pixels touched by the CPU blur loop, with no visible quality loss since blur already discards that detail | + +None of this changes the public API - `neumorphic()`, `animatedNeumorphic()`, +`springNeumorphic()`, `expressiveNeumorphic()`, and the XML views all work +exactly as before. + +### Optional: warm up before the first frame + +The shared `RenderScript` context is still created lazily on first use - by +default that's the first frame that draws a neumorphic component. For an +app whose very first screen is heavy with neumorphic components, you can +shave that off entirely by warming it up on a background thread before the +UI needs it: + +```kotlin +class MyApplication : Application() { + override fun onCreate() { + super.onCreate() + // Off the main thread - creates the shared RenderScript context + // ahead of time so the first neumorphic frame never has to wait for it. + Thread { NeuBlurMakerHolder.warmUp(this) }.start() + } +} +``` + +This is a pure optimization - safe to skip, call multiple times, or call +from anywhere; the singleton only does the expensive work once. + +### Demo app: smaller, correct APK + +The `app` module (the demo/sample app in this repo, not the library itself) +also had some unrelated issues that made it bigger and slower to install +than it needed to be: + +- Removed `material-icons-extended` (~5000 icons, several MB) - the demo + only uses about 10 common icons, all of which live in the much smaller + `material-icons-core`. +- Removed `navigation-compose` and the three `material3-adaptive` artifacts - + none of them were used anywhere in the demo's source. +- Enabled `minifyEnabled` + `shrinkResources` for the release build type + (previously `minifyEnabled false`, so the release APK shipped completely + unshrunk). +- Fixed a dead conditional in the header's notification icon that picked the + same icon in both the on/off branches regardless of state. + +### Tuning the cache/blur cost with `NeuPerformanceConfig` + +The defaults (2x downsampling, 6MB shadow cache) are chosen to be safe for +typical UI. If your app targets low-end devices, or has an unusually large +number of *distinct* neumorphic shapes on screen (so the shared cache gets +little reuse), you can tune these: + +```kotlin +class MyApplication : Application() { + override fun onCreate() { + super.onCreate() + NeuPerformanceConfig.blurDownsampling = 3 // lower-end devices: less CPU, slightly softer shadows + NeuPerformanceConfig.shadowCacheBudgetKB = 12 * 1024 // large/varied UI: fewer cache evictions + } +} +``` + +Set these once, early, before any neumorphic composable draws. They're safe +to change later too - a change only affects shadows generated *after* it, it +doesn't retroactively re-render what's already on screen. + +### Baseline profiles + +This library ships a bundled `baseline-prof.txt` (in `library/src/main/`) +listing its always-hot internal classes (the blur pipeline, the shadow +cache, the shape hierarchy). AGP automatically merges this into any +consuming app's own baseline profile at build time - no setup needed on your +end beyond depending on the library normally. + +To be transparent about what this is: it's a hand-authored, class-level-only +profile, not one generated from an actual device run. It tells ART "expect +these classes early, preload/verify them ahead of time" - a real but modest +win. It deliberately does not include method-level rules for the +`neumorphic()`/`animatedNeumorphic()`/`springNeumorphic()` composable +functions, since Compose's compiler generates synthetic default-argument and +mangled internal names for those that are impractical to guess correctly by +hand. For a more complete profile (including those hot composable entry +points), generate one from a real device using the +[Macrobenchmark + Baseline Profile Gradle plugin](https://developer.android.com/topic/performance/baselineprofiles/create-baselineprofile) +against the demo app, and merge the relevant lines into the bundled file. + +### Roadmap + +- Migrate to AGP 9.1.0+ / Compose 1.12+ / `compileSdk 37` once AGP 9's + migration surface (it changes how the Kotlin Gradle plugin is applied) has + been verified against a real build - this project deliberately deferred + that jump for now (see [Requirements](#requirements) above). +- Investigate `RenderEffect`-based GPU compositor blur on API 31+ as a + zero-CPU-cost alternative to the downsampled StackBlur path (higher + implementation risk - needs on-device verification before shipping). +- Generate a real Macrobenchmark-based baseline profile (the current one is + hand-authored and class-level-only, see [Baseline profiles](#baseline-profiles) above) once a + device/emulator is available to run it against the demo app. +- Extend `NeuPerformanceConfig` further if real-world usage shows a need + (e.g. per-shape overrides, disabling downsampling for very large elevated + surfaces where the softness difference could be visible). + +## 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**: 24 (Android 7.0) for both `library` (Compose) and `library-views` (XML/Views) +- **Compile/Target SDK**: 36 (the maximum recommended by AGP 8.13.0 - see note below) +- **Compose BOM**: 2026.04.01 (Compose 1.11) +- **Kotlin**: 2.3.0 +- **AGP**: 8.13.0 (build-time requirement for contributors; does not constrain consumers) +- **Java**: 17 source/target compatibility + +> **Why not the newest Compose BOM?** Compose 1.12.0 (BOM 2026.08.00 and later) +> requires `compileSdk 37`, which in turn requires AGP 9.1.0+. This project +> deliberately stays on AGP 8.13.x for now (see [Roadmap](#roadmap)), so it's +> pinned to Compose 1.11 (BOM `2026.04.01`), the last line before that jump - +> confirmed compatible with `compileSdk 36` + AGP 8.13 by CI. `material3` is +> no longer pinned to an independent alpha version either, for the same +> reason: an independently-pinned material3 alpha silently requiring +> `compileSdk 37` on its own (regardless of what the rest of the BOM needs) +> is exactly what broke this before - letting the BOM manage `material3`'s +> version keeps everything on one consistent, tested line. ## License -Licensed under Apache License, Version 2.0 [here](https://github.com/CuriousNikhil/neumorphic-compose/blob/main/LICENSE) + +Licensed under Apache License, Version 2.0 [here](https://github.com/obieda-hussien/neumorphic-compose-pro/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..0847b869 --- /dev/null +++ b/README_AR.md @@ -0,0 +1,408 @@ +
+ +# مكتبة Neumorphism UI لأندرويد + +مكتبة حديثة ومرنة لواجهة Neumorphism لأندرويد تدعم كل من **Jetpack Compose** و**Views/XML** التقليدية - وفي هذا الفرع (fork) تم تحسينها عشان ترسم نفس الظلال بمعالجة CPU/بطارية أقل بكتير من النسخة الأصلية، من غير أي فرق في الجودة البصرية. + +[![](https://jitpack.io/v/obieda-hussien/neumorphic-compose-pro.svg)](https://jitpack.io/#obieda-hussien/neumorphic-compose-pro) ![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) + +

+ +

+ +## المميزات ✨ + +- 🎨 **دعم Jetpack Compose** - استخدم الـ `neumorphic()` modifier مع أي composable +- 📱 **دعم XML/Java Views** - Views تقليدية (NeumorphicView, NeumorphicButton, NeumorphicCardView) +- 🌓 **دعم الوضع الداكن** - مخططات ألوان مدمجة للوضع الفاتح والداكن +- 🎭 **دعم Material You** - ألوان ديناميكية على أندرويد 12+ +- 💫 **دعم الأنيميشن** - تأثيرات ضغط سلسة +- 🔆 **مصدر الإضاءة قابل للتخصيص** - TOP_LEFT، TOP_RIGHT، BOTTOM_LEFT، BOTTOM_RIGHT +- 🔋 **رسم موفّر للطاقة** - الظلال بقت متخزنة (cached) ومشتركة بدل ما تتحسب من الصفر كل فريم - التفاصيل في قسم [الأداء](#الأداء) +- ✨ **انتقالات crossfade سلسة** - تغيير الاختيار (الـ chips، الـ navigation bar) بيدوب بين الحالتين بدل ما يقفز فجأة - شوف [الجديد في 4.0.0](#الجديد-في-400) + +## الجديد في 4.0.0 + +الموديولين (`library` و`library-views`) بقوا بيتاخدوا نفس رقم الإصدار مع بعض - قبل كده كانت `library` على `3.1.0` و`library-views` متأخرة عند `2.1.0`، وده كان مربك أكتر ما هو مفيد طالما الاتنين بيتنشروا مع بعض من نفس الريبو. `4.0.0` إصدار كبير؛ الـ API العام متغيرش (مفيش حاجة هنا محتاجة تعديل في كودك)، لكن حاجات كتير اتغيرت تحت السطح: + +**اتصلح:** +- **تجميد التطبيق عند الفتح** و**استنزاف البطارية/المعالج أثناء الأنيميشن** - شوف [الأداء](#الأداء) للتفاصيل الكاملة (RenderScript context مشترك، كاش لبيتماب الظلال، blur بدقة مخفّضة). +- **كراش حقيقي**: الـ thumb بتاع `NeuSwitch` و`NeuSlider` كان بيستخدم `Modifier.padding()` مع قيمة متحركة بسبرنج. السبرنجات المرتدة بتتخطى هدفها شوية قبل ما تستقر، وده كان بيخلي قيمة الـ padding سالبة لحظيًا - و`Modifier.padding()` بيعمل throw لأي قيمة سالبة. اتحول لـ `Modifier.offset()`، وهو الأنسب أصلاً للحركة المتحركة وبيقبل قيم سالبة من غير كراش. +- **الأشكال البارزة (`Punched`) بتفقد ظلها لما تتقص**: كذا كومبوننت (وشاشات ديمو) كانوا بيطبّقوا `Modifier.clip()` **قبل** `Modifier.neumorphic()`، وده كان بيقص امتداد الظل الناعم بره حدود الشكل، ويسيب صندوق شكله مسطّح. اتصلح في كل المكتبة والديمو؛ شوف تحديث قسم [أفضل الممارسات](#أفضل-الممارسات). شكل `Pot` (بارز+غائر مع بعض) احتاج إصلاح أعمق، لأن clip خارجي واحد مايقدرش يتطبق على واحد بس من تمريرتين الظل بتاعته - دلوقتي بيقص التمريرة الغائرة داخليًا بس. +- **استجابة اللمس مش بتبان في اللمسات السريعة/الخفيفة**: أنيميشن الضغط في كل كومبوننت كان بيتحرك مباشرة بحالة الضغط الخام، واللمسة السريعة ممكن تخلص قبل ما الأنيميشن يلحق يبان - فكان شكله إنه بيستجيب بس للضغطة الثابتة الطويلة. اتصلح بإننا نخلي حالة "مضغوط" البصرية تفضل ظاهرة لمدة ~100ms مضمونة، مهما كانت اللمسة سريعة. +- خطر تصادم في مفتاح كاش `NeuShadowCache` (ألوان مختلفة ممكن تنتج نفس المفتاح)، وكذا مشكلة تحقق/تيست أصغر اتلقطوا في المراجعة. + +**اتغيّر:** +- `NeuFloatingActionButton` و`NeuRadioButton` اتعملهم إعادة تصميم عشان يطابقوا شكل باقي المكتبة الناعم ذو الظل المزدوج - الاتنين كان فيهم دايرة صلبة اللون/حد ملون، وده أسلوب Material قياسي مش نيومورفيزم. +- `NeuChip` و`NeuIconButton` (المستخدمين في "Categories" chips وnavigation bar في الديمو) دلوقتي بيعملوا crossfade بين حالتهم البارزة/الغائرة عند تغيير الاختيار بدل ما يتقلبوا في فريم واحد، وأيقونة `NeuChip` بقت تظهر/تختفي بـ fade بدل ما تقفز. + +**اتضاف:** +- `NeuPerformanceConfig` - إعدادات downsampling وميزانية كاش الظلال قابلة للتعديل وقت التشغيل؛ شوف [الأداء](#الأداء). +- Baseline profiles مكتوبة يدويًا للموديولات التلاتة؛ شوف [الأداء](#الأداء). +- Gradle version catalog (`gradle/libs.versions.toml`) كمصدر وحيد لإصدارات الاعتماديات في كل الموديولات، وإصلاح لمشكلتين `implementation` مقابل `api` كانوا ممكن يكسروا أي حد بيستخدم المكتبة من JitPack من غير ما يضيف `material3`/`compose-ui` بنفسه. +- تيستات حقيقية (unit/instrumented) لمنطق الكاش ومشاركة الـ `BlurMaker` (كان فيه تيستات placeholder بس قبل كده في المشروع كله). + +## التثبيت + +المكتبة منشورة عبر [JitPack](https://jitpack.io/#obieda-hussien/neumorphic-compose-pro). + +ضيف مستودع JitPack في `settings.gradle` بتاع المشروع (على مستوى المشروع، أو `build.gradle` لو مشروعك أقدم): + +```kotlin +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + maven { url = uri("https://jitpack.io") } + } +} +``` + +### مكتبة Jetpack Compose + +```kotlin +implementation("com.github.obieda-hussien.neumorphic-compose-pro:library:4.0.0") +``` + +### مكتبة XML/Views + +```kotlin +implementation("com.github.obieda-hussien.neumorphic-compose-pro:library-views:4.0.0") +``` + +> جاي من الـ artifact الأصلي `me.nikhilchaudhari:composeNeumorphism`؟ الـ API العام متغيرش نهائي - نفس الدوال ونفس الاستخدام، بس غيّر إحداثيات الاعتمادية (dependency coordinates) فوق وكل حاجة هتشتغل زي ما هي. + +## البداية السريعة + +### 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)); +button.setNeuLightSource(LightSource.TOP_LEFT); +``` + +## الأشكال + +فيه 3 أشكال نيومورفيزم متاحة: + +| الشكل | الوصف | الشكل البصري | +|-------|-------------|--------| +| `Punched` | تأثير بارز/مرتفع | `__/‾‾‾‾‾‾‾\__` | +| `Pressed` | تأثير مضغوط/غائر | `‾‾\________/‾‾` | +| `Pot` | مزيج من البارز والغائر | `_/\‾‾‾‾‾/\_` | + +كل شكل بيدعم نوعين من الحواف: +- **Rounded** - نصف قطر حافة قابل للتخصيص +- **Oval** - شكل دائري/بيضاوي + +```kotlin +// Compose +Punched.Rounded(radius = 12.dp) +Punched.Oval() +Pressed.Rounded(radius = 8.dp) +Pressed.Oval() +Pot.Rounded(radius = 16.dp) +Pot.Oval() +``` + +## خيارات التخصيص + +| المعامل | القيمة الافتراضية | الوصف | +|-----------|---------|-------------| +| `neuShape` | `Punched.Rounded(12.dp)` | نوع الشكل وإعدادات الحواف | +| `lightShadowColor` | `Color.White` | لون الظل الفاتح | +| `darkShadowColor` | `Color.LightGray` | لون الظل الداكن | +| `elevation` | `6.dp` | عمق/ارتفاع الظل | +| `strokeWidth` | `6.dp` | عرض الظل الداخلي | +| `neuInsets` | `NeuInsets(6.dp, 6.dp)` | هوامش الظل (أفقي، رأسي) | +| `lightSource` | `LightSource.TOP_LEFT` | اتجاه مصدر الإضاءة | + +## مصدر الإضاءة + +اضبط اتجاه مصدر الإضاءة عشان تغيّر مكان الظل: + +```kotlin +// Compose +Modifier.neumorphic( + lightSource = LightSource.TOP_LEFT // الافتراضي + // أو + lightSource = LightSource.TOP_RIGHT + lightSource = LightSource.BOTTOM_LEFT + lightSource = LightSource.BOTTOM_RIGHT +) +``` + +## دعم الثيمات + +### استخدام ألوان الثيم + +```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) + ) { + // المحتوى + } +} +``` + +### مخطط ألوان مخصص + +```kotlin +val customScheme = NeuTheme.customColorScheme( + backgroundColor = Color(0xFFE0E5EC) +) +``` + +## الأنيميشن + +### تأثير ضغط متحرك (Compose) + +```kotlin +Modifier.animatedNeumorphic( + neuShape = Punched.Rounded(), + elevation = 8.dp, + pressed = isPressed, // من حالة التفاعل (interaction state) + animationDuration = 150 +) +``` + +### Clickable مع أنيميشن + +```kotlin +Modifier.neumorphicClickable( + onClick = { /* الحدث */ }, + elevation = 8.dp, + neuShape = Punched.Rounded() +) +``` + +### أنيميشن XML Views + +الأزرار عندها أنيميشن ضغط مدمج. فعّله/عطّله بـ: + +```kotlin +button.enablePressAnimation = true +``` + +## دوال مساعدة (Utility Extensions) + +```kotlin +// نيومورفيزم خفيف (elevation أقل) +Modifier.softNeumorphic() + +// نيومورفيزم عميق (elevation أكتر) +Modifier.deepNeumorphic() + +// توليد ألوان الظل من لون الخلفية +val (lightShadow, darkShadow) = backgroundColor.toNeuColors() + +// تفتيح/تغميق الألوان +val lighter = color.lighten(0.2f) +val darker = color.darken(0.2f) +``` + +## أفضل الممارسات + +1. **استخدم ألوان متقاربة**: لون الخلفية والظل لازم يكونوا متشابهين للحصول على أفضل تأثير +2. **تجنب الأبيض/الأسود النقي**: استخدم درجات قريبة من الأبيض/الرمادي الغامق عشان ظلال واقعية +3. **حافظ على اتساق مصدر الإضاءة**: خلّي مصدر الإضاءة ثابت في كل واجهتك +4. **استخدم elevation مناسب**: من 4 لـ 12dp بيناسب معظم الحالات +5. **استخدم Clip مع شكل Pressed بس**: استخدم `Modifier.clip()` لما تستخدم شكل `Pressed` - الظل الداخلي بتاعه لازم يفضل جوه الحدود. **متعملش clip خالص لشكل `Punched` (بارز) أو `Pot`** - ظلهم مفروض يمتد بره حدود الشكل نفسه، ولو حطيت `Modifier.clip()` قبل `Modifier.neumorphic()` هيقص الامتداد ده ويسيب شكل مسطّح بدل ظل ناعم. كل الكومبوننتس الجاهزة (`NeuXxx`) في المكتبة بتتبع القاعدة دي داخليًا بالفعل. + +## الأداء + +كل ظل نيومورفيزم عبارة عن bitmap متعمّلها blur، والنسخة الأصلية كانت بتعيد توليد الـ bitmap **من الصفر في كل استدعاء رسم**، بما فيها كل فريم في أنيميشن الضغط. النسخة دي (`4.0.0`) بتحافظ على نفس الشكل البصري بالظبط بس بتغيّر عدد مرات الشغل ده وتكلفته. + +### السبب الحقيقي لاستنزاف البطارية والتهنيج عند الفتح + +1. **كان بيتعمل `RenderScript` context جديد كل recomposition** - الـ modifier اللي بيمتلك مسار الـ blur كان بيتبني من جديد كل مرة خاصية متحركة (زي الـ elevation وقت الضغط) بتتغيّر - يعني على API أقل من 31، كان بيتعمل `RenderScript` context جديد (من أتقل الكائنات في أندرويد) وبيتقفل لحد 60 مرة/ثانية، لكل كومبوننت نيومورفيزم. +2. **صفر caching خالص.** كارتين متطابقين في `LazyColumn`، أو زرار بيرجع لنفس الـ elevation بعد ما يتضغط، كل واحد فيهم كان بيولّد `GradientDrawable` + `Bitmap` + blur كامل على CPU بمفرده، رغم إن الناتج متطابق بكسل ببكسل. +3. **الـ blur كان بيشتغل على الدقة الكاملة للـ bitmap**، رغم إن الـ blur نفسه (Gaussian/stack blur) بيفقد التفاصيل الدقيقة أصلاً - يعني الدقة الزيادة دي كانت بتترمي من غير فايدة. +4. **كل كومبوننت نيومورفيزم كان عنده RenderScript context خاص بيه.** شاشة عادية (هيدر، شريط بحث، صف من الأزرار السريعة، كذا كارت، سويتشات) بسهولة فيها 15-20+ كومبوننت نيومورفيزم. حتى مع إصلاح رقم 1، كان لسه فيه 15-20+ context منفصل بيتعملوا كلهم في نفس الوقت أثناء أول فريم - وده تقيل بما يكفي على الـ main thread إنه يبان كتهنيج/تجميد للتطبيق لحظة الفتح. + +### إيه اللي اتغيّر + +| الإصلاح | الأثر | +|---|---| +| `BlurMaker` (والـ `RenderScript` بتاعه) بقى **singleton واحد على مستوى التطبيق كله** (`NeuBlurMakerHolder`) مشترك بين كل استدعاء `neumorphic()` وكل XML view | أقصى حاجة، context واحد بس يتعمل طول عمر التطبيق مهما كان عدد الكومبوننتس - ده اللي بيحل مشكلة التهنيج عند الفتح | +| إعادة استخدام نفس `ScriptIntrinsicBlur` بدل إنشاءه كل مرة | يقلل تكلفة مسار RenderScript المتبقية أكتر | +| كاش LRU مشترك على مستوى التطبيق للظلال (`NeuShadowCache`)، بمفتاح مبني على الحجم/الـ elevation/الألوان/الشكل/مصدر الإضاءة | الكومبوننتس المتطابقة (عناصر list، أزرار في وضع الراحة) بتشارك نفس الـ bitmap بدل ما كل واحد يولّد بتاعه | +| تقريب الـ elevation/الـ stroke لأقرب 0.5dp في مفتاح الكاش | أنيميشن ضغط بـ ~200 فريم بيتحول لعدد بسيط من الـ bitmaps المُعاد استخدامها بدل واحد لكل فريم - الفرق مش محسوس بصريًا | +| الـ blur بقى بيشتغل على دقة مخفّضة (÷2) وبعدين يكبّر تاني | تقريبًا 4 أضعاف بكسلات أقل بيلمسها الـ CPU blur loop، من غير أي فرق ملحوظ في الجودة لأن الـ blur أصلاً بيفقد التفاصيل دي | + +الـ API العام متغيرش خالص - `neumorphic()`، `animatedNeumorphic()`، `springNeumorphic()`، `expressiveNeumorphic()`، وكل الـ XML views شغالة زي ما هي بالظبط. + +### اختياري: تسخين مسبق (warm-up) قبل أول فريم + +الـ `RenderScript` context المشترك لسه بيتعمل lazy عند أول استخدام - افتراضيًا ده أول فريم بيرسم فيه كومبوننت نيومورفيزم. لو أول شاشة في تطبيقك مليانة كومبوننتس نيومورفيزم، تقدر تلغي التأخير ده تمامًا بعمل warm-up في الخلفية قبل ما الواجهة تحتاجه: + +```kotlin +class MyApplication : Application() { + override fun onCreate() { + super.onCreate() + // في الخلفية - بيعمل الـ RenderScript context المشترك بدري + // عشان أول فريم نيومورفيزم متستناش عليه. + Thread { NeuBlurMakerHolder.warmUp(this) }.start() + } +} +``` + +ده تحسين اختياري بالكامل - آمن تتخطاه، أو تناديه كذا مرة، أو تناديه من أي مكان؛ الـ singleton بيعمل الشغل التقيل مرة واحدة بس. + +### تطبيق الديمو: حجم أصغر وكود مظبوط + +موديول الـ `app` (تطبيق الديمو في الريبو، مش المكتبة نفسها) كان فيه كمان شوية مشاكل مش متعلقة بالأداء خلته أكبر وأبطأ في التثبيت من غير داعي: + +- شلت `material-icons-extended` (~5000 أيقونة، كذا ميجا) - الديمو بيستخدم حوالي 10 أيقونات بس، وكلها موجودة في `material-icons-core` الأصغر بكتير. +- شلت `navigation-compose` والـ 3 مكتبات `material3-adaptive` - مش مستخدمين نهائي في كود الديمو. +- فعّلت `minifyEnabled` + `shrinkResources` لنسخة الـ release (كانت `minifyEnabled false`، يعني الـ APK بينشحن من غير أي تقليص). +- صلّحت شرط ميت (dead conditional) في أيقونة الإشعارات بالهيدر كان بيرجع نفس الأيقونة في الحالتين (مفعّل/معطّل) بغض النظر عن الحالة. + +### ضبط تكلفة الكاش/البلور عن طريق `NeuPerformanceConfig` + +القيم الافتراضية (downsampling ÷2، كاش 6MB) مختارة عشان تكون آمنة لمعظم الواجهات. لو تطبيقك مستهدف أجهزة ضعيفة، أو عنده عدد كبير وغير عادي من الأشكال النيومورفيزم *المختلفة* في نفس الوقت (يعني الكاش المشترك بيستفاد منه أقل): + +```kotlin +class MyApplication : Application() { + override fun onCreate() { + super.onCreate() + NeuPerformanceConfig.blurDownsampling = 3 // أجهزة ضعيفة: CPU أقل، ظل أنعم شوية + NeuPerformanceConfig.shadowCacheBudgetKB = 12 * 1024 // واجهة كبيرة/متنوعة: تفريغ كاش أقل + } +} +``` + +اضبطها مرة واحدة، بدري، قبل أي رسم لكومبوننت نيومورفيزم. آمن تغيّرها بعدين كمان - التغيير بيأثر بس على الظلال اللي هتتولّد بعد كده، مش الحاجات المرسومة بالفعل. + +### Baseline Profiles + +المكتبة بتشحن ملف `baseline-prof.txt` مدمج (في `library/src/main/`) فيه أهم الكلاسات اللي بتشتغل باستمرار (مسار الـ blur، كاش الظلال، شجرة الأشكال). AGP بيدمجه تلقائيًا في baseline profile أي تطبيق بيستخدم المكتبة وقت البناء - من غير أي إعداد إضافي منك غير إنك تعتمد على المكتبة عادي. + +عشان أكون صريح: ده ملف مكتوب يدويًا على مستوى الكلاس بس (مش method-level)، مش متولّد من قياس فعلي على جهاز. هو بيقول لـ ART "استنى الكلاسات دي بدري، اعمل preload/verify ليها قبل ما تتحتاج" - فايدة حقيقية بس متواضعة. متعمدين مافيهوش قواعد على مستوى الدوال لـ composable functions زي `neumorphic()`/`animatedNeumorphic()`/`springNeumorphic()`، لأن الـ Compose compiler بيولّد أسماء داخلية معقدة (mangled) ودوال default-argument صناعية، صعب تتخمّن صح من غير أدوات فعلية. لو عايز بروفايل أدق (يشمل نقاط الدخول دي كمان)، تقدر تولّده من جهاز حقيقي عن طريق [Macrobenchmark + Baseline Profile Gradle plugin](https://developer.android.com/topic/performance/baselineprofiles/create-baselineprofile) على تطبيق الديمو، وتدمج السطور المهمة في الملف المدمج. + +### خطط مستقبلية (Roadmap) + +- الترقية لـ AGP 9.1.0+ / Compose 1.12+ / `compileSdk 37` لما نتأكد من مساحة التغييرات اللي AGP 9 بيعملها (بتغيّر طريقة تطبيق Kotlin Gradle plugin) على بناء فعلي - المشروع ده متعمد يأجل القفزة دي دلوقتي (شوف [المتطلبات](#المتطلبات) فوق). +- دراسة الـ blur عن طريق `RenderEffect`/GPU compositor على API 31+ كبديل بدون أي تكلفة CPU لمسار StackBlur المخفّض الدقة الحالي (مخاطرة تنفيذ أعلى - محتاج تأكيد على جهاز حقيقي قبل النشر). +- توليد baseline profile حقيقي عن طريق Macrobenchmark (الحالي مكتوب يدويًا وعلى مستوى الكلاس بس، شوف قسم [Baseline Profiles](#baseline-profiles) فوق) لما يبقى متاح جهاز/إيميوليتر أشغّله عليه ضد تطبيق الديمو. +- توسيع `NeuPerformanceConfig` أكتر لو الاستخدام الفعلي وضّح احتياج ليه (زي إعدادات مخصصة لكل شكل، أو تعطيل الـ downsampling للأسطح المرتفعة جدًا اللي ممكن يبان فيها الفرق). + +## الترقية من v1.x + +```kotlin +// v1.x +Modifier.neumorphic( + neuShape = Punched.Rounded(), + elevation = 6.dp +) + +// v2.0 - نفس الـ API، مميزات جديدة +Modifier.neumorphic( + neuShape = Punched.Rounded(), + elevation = 6.dp, + lightSource = LightSource.TOP_LEFT // جديد +) +``` + +## المتطلبات + +- **الحد الأدنى للـ SDK**: 24 (أندرويد 7.0) لكل من `library` (Compose) و`library-views` (XML/Views) +- **Compile/Target SDK**: 36 (أقصى حد موصى بيه من AGP 8.13.0 - التفاصيل تحت) +- **Compose BOM**: 2026.04.01 (Compose 1.11) +- **Kotlin**: 2.3.0 +- **AGP**: 8.13.0 (متطلب وقت البناء للمساهمين في المكتبة فقط؛ مش قيد على مستخدمي المكتبة) +- **Java**: توافقية 17 (source/target) + +> **ليه مش أحدث BOM؟** Compose 1.12.0 (BOM 2026.08.00 وبعده) محتاج `compileSdk 37`، واللي بدوره محتاج AGP 9.1.0+. المشروع ده متعمد يفضل على AGP 8.13.x دلوقتي (شوف [خطط مستقبلية](#خطط-مستقبلية-roadmap))، فمثبّت على Compose 1.11 (BOM `2026.04.01`) - آخر نسخة قبل القفزة دي، وأتأكد من توافقها مع `compileSdk 36` + AGP 8.13 فعليًا عن طريق CI. كمان `material3` بقى مش مثبّت على alpha مستقل زي الأول لنفس السبب: تثبيت alpha لوحده محتاج `compileSdk 37` بمفرده (بغض النظر عن باقي الـ BOM) بالظبط ده اللي كسّر البناء قبل كده - سيبان الـ BOM يدير إصدار `material3` بيخلي كل حاجة على خط واحد متسق ومُختبر. + +## الرخصة + +مرخّص تحت Apache License, Version 2.0 [هنا](https://github.com/obieda-hussien/neumorphic-compose-pro/blob/main/LICENSE) + +## المساهمة + +المساهمات مرحب بها! لا تتردد في تقديم issues و pull requests. + +## شكر وتقدير + +- خوارزمية Stack Blur بواسطة Mario Klingemann +- فكرة تصميم Neumorphism الأصلية من Alexander Plyuto + +
diff --git a/app/build.gradle b/app/build.gradle index e8415852..566d524c 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.3.0' } android { - compileSdk 30 - buildToolsVersion "30.0.3" + namespace 'me.nikhilchaudhari.neumorphiccompose' + compileSdk 36 defaultConfig { applicationId "me.nikhilchaudhari.neumorphiccompose" - minSdk 21 - targetSdk 30 - versionCode 1 - versionName "1.0" + minSdk 24 + targetSdk 36 + versionCode 3 + versionName "3.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -22,41 +23,71 @@ android { buildTypes { release { - minifyEnabled false + minifyEnabled true + shrinkResources true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } 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 - } } dependencies { + // Compose BOM for version management + implementation platform(libs.compose.bom) + androidTestImplementation platform(libs.compose.bom) + + implementation libs.androidx.core.ktx + implementation libs.androidx.appcompat + implementation libs.material + + // Compose UI + implementation libs.androidx.compose.ui + implementation libs.androidx.compose.ui.graphics + implementation libs.androidx.compose.ui.tooling.preview + implementation libs.androidx.compose.foundation + + // Material Design 3 - Expressive with latest features + implementation libs.androidx.compose.material3 + + // Animation - including spring physics and shared element + implementation libs.androidx.compose.animation + implementation libs.androidx.compose.animation.core + implementation libs.androidx.compose.animation.graphics - 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.compose.ui:ui:$compose_version" - implementation "androidx.compose.material:material:$compose_version" - 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' + // Material icons - core set only. The demo only uses ~10 common icons + // (Add, Check, Favorite, Home, Notifications, Person, Search, Settings, + // FavoriteBorder), all of which live in material-icons-core. + // material-icons-extended ships 5000+ icons and was previously pulled in + // unused - it alone adds several MB to a debug/unshrunk APK. + implementation libs.androidx.compose.material.icons.core + + // Lifecycle + implementation libs.androidx.lifecycle.runtime.ktx + implementation libs.androidx.lifecycle.runtime.compose + implementation libs.androidx.lifecycle.viewmodel.compose + + // Activity Compose + implementation libs.androidx.activity.compose + + // Library module 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' - androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" + + // Testing + testImplementation libs.junit + androidTestImplementation libs.androidx.test.ext.junit + androidTestImplementation libs.androidx.test.espresso.core + androidTestImplementation libs.androidx.compose.ui.test.junit4 + + // Debug tooling + debugImplementation libs.androidx.compose.ui.tooling + debugImplementation libs.androidx.compose.ui.test.manifest } \ 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 @@ - + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier.fillMaxWidth() + ) { + NeuRadioButton( + selected = selectedRadio == index, + onClick = { selectedRadio = index }, + colorScheme = colorScheme + ) + Text( + text = option, + style = MaterialTheme.typography.bodyMedium, + color = colorScheme.onBackgroundColor + ) + } + } + } + + // Checkboxes Section + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text( + text = "Checkboxes", + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium, + color = colorScheme.onBackgroundColor + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + NeuCheckbox( + checked = checkbox1, + onCheckedChange = { checkbox1 = it }, + colorScheme = colorScheme + ) + Text( + text = "Enable notifications", + style = MaterialTheme.typography.bodyMedium, + color = colorScheme.onBackgroundColor + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + NeuCheckbox( + checked = checkbox2, + onCheckedChange = { checkbox2 = it }, + colorScheme = colorScheme + ) + Text( + text = "Dark mode", + style = MaterialTheme.typography.bodyMedium, + color = colorScheme.onBackgroundColor + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + NeuCheckbox( + checked = checkbox3, + onCheckedChange = { checkbox3 = it }, + colorScheme = colorScheme + ) + Text( + text = "Auto-sync", + style = MaterialTheme.typography.bodyMedium, + color = colorScheme.onBackgroundColor + ) + } + } + + // FAB Preview + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text( + text = "Floating Action Button", + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium, + color = colorScheme.onBackgroundColor + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically + ) { + NeuFloatingActionButton( + onClick = { }, + colorScheme = colorScheme, + size = 48.dp + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = "Add", + modifier = Modifier.size(20.dp) + ) + } + + NeuFloatingActionButton( + onClick = { }, + colorScheme = colorScheme, + size = 56.dp + ) { + Icon( + imageVector = Icons.Default.Favorite, + contentDescription = "Favorite", + modifier = Modifier.size(24.dp) + ) + } + + NeuFloatingActionButton( + onClick = { }, + colorScheme = colorScheme, + size = 64.dp + ) { + Icon( + imageVector = Icons.Default.Settings, + contentDescription = "Settings", + modifier = Modifier.size(28.dp) + ) + } + } + } + } } } } - @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 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 ) - - for (i in 0..2) { - SimpleOvalCard(resource = resourceArray[i]) + + 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 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) +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 + ) + + // Clean navigation bar container + Box( + modifier = Modifier + .fillMaxWidth() + .neumorphic( + neuShape = Punched.Rounded(28.dp), + lightShadowColor = colorScheme.lightShadowColor, + darkShadowColor = colorScheme.darkShadowColor, + elevation = 8.dp + ) + .background(colorScheme.backgroundColor, RoundedCornerShape(28.dp)) + .padding(12.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically + ) { + navItems.forEachIndexed { index, (icon, label) -> + val isSelected = selectedNav == index + + NeuIconButton( + onClick = { selectedNav = index }, + colorScheme = colorScheme, + size = 48.dp, + selected = isSelected + ) { + Icon( + imageVector = icon, + contentDescription = label, + modifier = Modifier.size(22.dp) + ) + } + } + } + } } } - +@Preview(showBackground = true, showSystemUi = true) @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 @@ - -