Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@ All notable changes to this project are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.1.0] - 2026-05-26

### Added

- `Modifier.animatedGlassEffect()` — entrance animation that transitions blur, alpha, and border from zero to target values
- `Modifier.glassShimmer()` — infinite diagonal light-streak overlay for ambient surface motion
- `Modifier.glassBorderGlow()` — pulsing border opacity animation for breathing edge highlight
- `GlassButton` press animation with configurable `pressScale` and `pressAlpha` parameters
- "Animate" tab in sample app showcasing all new animation modifiers
- "Config" tab in sample app separating visual editor controls from main content
- Combined effects demo showing all three modifiers stacked together

### Changed

- `GlassButton` now uses `graphicsLayer` for disabled state alpha (was `Modifier.alpha`)
- `GlassButton` uses `collectIsPressedAsState` for press detection
- Sample app restructured into tabbed navigation (Home, Animate, Config)

## [1.0.0] - 2026-05-17

### Added
Expand All @@ -20,4 +38,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Apache 2.0 license
- Maven Central release [`io.github.saadkhalidkhan:compose-glasskit:1.0.0`](https://central.sonatype.com/artifact/io.github.saadkhalidkhan/compose-glasskit)

[1.1.0]: https://github.com/saadkhalidkhan/ComposeGlassKitTheme/compare/v1.0.0...v1.1.0
[1.0.0]: https://github.com/saadkhalidkhan/ComposeGlassKitTheme/releases/tag/v1.0.0
30 changes: 29 additions & 1 deletion docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,34 @@ Provides `GlassConfig` to descendants. Read current values with `GlassTheme.conf

Required: `shape`. Optional overrides for blur, colors, alpha, and border width. Unspecified numeric values inherit from `GlassTheme`.

### `Modifier.animatedGlassEffect(...)`

Same parameters as `glassEffect` plus `animationSpec` (default `tween(600ms)`). Animates blur, alpha, and border from zero to target on first composition — the glass "forms" into view.

### `Modifier.glassShimmer(...)`

| Parameter | Default | Description |
|-----------|---------|-------------|
| `color` | `White @ 0.15f` | Shimmer highlight color |
| `durationMillis` | `2000` | Full sweep cycle duration |
| `shimmerWidth` | `0.4f` | Band width as fraction of composable width |
| `angle` | `20f` | Diagonal angle in degrees |

Draws an infinite diagonal light-streak overlay. Apply after `glassEffect` or `animatedGlassEffect`.

### `Modifier.glassBorderGlow(...)`

| Parameter | Default | Description |
|-----------|---------|-------------|
| `shape` | (required) | Must match the glass surface shape |
| `borderColor` | theme default | Base gradient color |
| `borderWidth` | theme default | Stroke width |
| `minAlpha` | `0.1f` | Minimum pulse opacity |
| `maxAlpha` | `0.5f` | Maximum pulse opacity |
| `durationMillis` | `2000` | Full pulse cycle (min → max → min) |

Adds a breathing border glow that oscillates independently of the glass fill.

## Components

All components accept the same glass parameters as `glassEffect` unless noted.
Expand All @@ -35,7 +63,7 @@ All components accept the same glass parameters as `glassEffect` unless noted.

### `GlassButton`

`onClick`, `enabled`, `shape` (default `CircleShape`), `contentPadding`, `content` row scope.
`onClick`, `enabled`, `shape` (default `CircleShape`), `contentPadding`, `pressScale` (default `0.96f`), `pressAlpha` (default `0.85f`), `content` row scope. Includes animated scale and alpha feedback on press.

### `GlassNavBar`

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.saadkhan.composeglasskit.components

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
Expand All @@ -11,22 +14,29 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.saadkhan.composeglasskit.modifiers.glassEffect

/**
* A clickable button with a glassmorphism surface.
* A clickable button with a glassmorphism surface and press animation feedback.
*
* When pressed, the button scales down slightly and becomes more translucent,
* providing tactile visual feedback without a ripple effect.
*
* @param pressScale Scale factor when button is pressed (0f–1f). Default 0.96f.
* @param pressAlpha Opacity when button is pressed. Default 0.85f.
*/
@Composable
fun GlassButton(
Expand All @@ -41,12 +51,32 @@ fun GlassButton(
borderAlpha: Float = Float.NaN,
borderWidth: Dp = Dp.Unspecified,
contentPadding: PaddingValues = PaddingValues(horizontal = 24.dp, vertical = 12.dp),
pressScale: Float = 0.96f,
pressAlpha: Float = 0.85f,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
content: @Composable RowScope.() -> Unit,
) {
val isPressed by interactionSource.collectIsPressedAsState()

val scale by animateFloatAsState(
targetValue = if (isPressed && enabled) pressScale else 1f,
animationSpec = tween(durationMillis = 100),
label = "glassButtonScale",
)

val pressedAlpha by animateFloatAsState(
targetValue = if (isPressed && enabled) pressAlpha else 1f,
animationSpec = tween(durationMillis = 100),
label = "glassButtonAlpha",
)

Box(
modifier = modifier
.alpha(if (enabled) 1f else 0.5f)
.graphicsLayer {
scaleX = scale
scaleY = scale
alpha = pressedAlpha * (if (enabled) 1f else 0.5f)
}
.glassEffect(
blurRadius = blurRadius,
containerColor = containerColor,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package com.saadkhan.composeglasskit.modifiers

import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import com.saadkhan.composeglasskit.theme.GlassTheme

/**
* Applies a glassmorphism surface with an animated entrance transition.
*
* The glass "forms" by animating blur radius from 0 to the target value, fading in
* the container fill, and revealing the border gradient — creating the illusion of
* the glass surface materializing.
*
* @param blurRadius Target blur radius after the animation completes.
* @param containerColor Base fill color.
* @param containerAlpha Target fill opacity after animation.
* @param borderColor Border gradient base color.
* @param borderAlpha Target border opacity after animation.
* @param shape Clip and border shape.
* @param borderWidth Border stroke width.
* @param animationSpec The [AnimationSpec] controlling entrance timing. Defaults to a 600ms tween.
*/
fun Modifier.animatedGlassEffect(
blurRadius: Dp = Dp.Unspecified,
containerColor: Color? = null,
containerAlpha: Float = Float.NaN,
borderColor: Color? = null,
borderAlpha: Float = Float.NaN,
shape: Shape,
borderWidth: Dp = Dp.Unspecified,
animationSpec: AnimationSpec<Float> = tween(durationMillis = 600),
): Modifier = composed {
val config = GlassTheme.config

val targetBlurRadius = if (blurRadius != Dp.Unspecified) blurRadius else config.blurRadius
val targetContainerAlpha = if (!containerAlpha.isNaN()) containerAlpha else config.containerAlpha
val targetBorderAlpha = if (!borderAlpha.isNaN()) borderAlpha else config.borderAlpha
val finalBorderWidth = if (borderWidth != Dp.Unspecified) borderWidth else config.borderWidth

val baseContainerColor = containerColor
?: if (config.glassColor != Color.Unspecified) config.glassColor else MaterialTheme.colorScheme.surface

val baseBorderColor = borderColor
?: if (config.borderColor != Color.Unspecified) config.borderColor else MaterialTheme.colorScheme.outlineVariant

var animationStarted by remember { mutableStateOf(false) }
LaunchedEffect(Unit) { animationStarted = true }

val progress by animateFloatAsState(
targetValue = if (animationStarted) 1f else 0f,
animationSpec = animationSpec,
label = "glassEntrance",
)

val currentBlur = targetBlurRadius * progress
val currentContainerAlpha = targetContainerAlpha * progress
val currentBorderAlpha = targetBorderAlpha * progress

this
.graphicsLayer {
clip = true
this.shape = shape
alpha = progress
}
.then(
if (currentBlur > 0.dp) {
Modifier.blur(currentBlur)
} else {
Modifier
},
)
.background(
color = baseContainerColor.copy(alpha = currentContainerAlpha),
shape = shape,
)
.border(
width = finalBorderWidth,
brush = Brush.verticalGradient(
colors = listOf(
baseBorderColor.copy(alpha = currentBorderAlpha),
baseBorderColor.copy(alpha = currentBorderAlpha * 0.4f),
baseBorderColor.copy(alpha = currentBorderAlpha * 0.1f),
),
),
shape = shape,
)
.clip(shape)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.saadkhan.composeglasskit.modifiers

import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.border
import androidx.compose.material3.MaterialTheme
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Dp
import com.saadkhan.composeglasskit.theme.GlassTheme

/**
* Applies an animated pulsing glow to the glass border.
*
* The border gradient alpha oscillates between [minAlpha] and [maxAlpha], creating an
* ambient "breathing" light effect on the glass edge. Pair with [glassEffect] for the
* full glass appearance — this modifier only draws the animated border.
*
* @param shape Border shape. Should match the shape used for [glassEffect].
* @param borderColor Base color for the gradient border.
* @param borderWidth Border stroke width.
* @param minAlpha Minimum opacity during the pulse cycle.
* @param maxAlpha Maximum opacity during the pulse cycle.
* @param durationMillis Full pulse cycle duration (min to max and back).
*/
fun Modifier.glassBorderGlow(
shape: Shape,
borderColor: Color? = null,
borderWidth: Dp = Dp.Unspecified,
minAlpha: Float = 0.1f,
maxAlpha: Float = 0.5f,
durationMillis: Int = 2000,
): Modifier = composed {
val config = GlassTheme.config
val finalBorderWidth = if (borderWidth != Dp.Unspecified) borderWidth else config.borderWidth

val baseBorderColor = borderColor
?: if (config.borderColor != Color.Unspecified) config.borderColor else MaterialTheme.colorScheme.outlineVariant

val transition = rememberInfiniteTransition(label = "glassBorderGlow")

val alpha by transition.animateFloat(
initialValue = minAlpha,
targetValue = maxAlpha,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = durationMillis / 2),
repeatMode = RepeatMode.Reverse,
),
label = "borderGlowAlpha",
)

border(
width = finalBorderWidth,
brush = Brush.verticalGradient(
colors = listOf(
baseBorderColor.copy(alpha = alpha),
baseBorderColor.copy(alpha = alpha * 0.5f),
baseBorderColor.copy(alpha = alpha * 0.15f),
),
),
shape = shape,
)
}
Loading
Loading