Skip to content

Commit 1fd032e

Browse files
committed
feat: add Jetpack Compose support (Roadmap #2)
- Implemented ShineButtonCompose using native Compose Animation and Canvas APIs - Supports ImageVector for flexible shape rendering - Updated README.md with Compose usage examples - Bumped minSdkVersion to 21 - Bumped version to 0.4.0 - Updated CHANGELOG.md and Roadmap status
1 parent 0e82e56 commit 1fd032e

5 files changed

Lines changed: 239 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [0.4.0] - 2026-06-13
6+
7+
### Added
8+
- **Jetpack Compose Support**: Introduced `ShineButtonCompose`, a native Composable implementation of the ShineButton.
9+
- Added Compose animation (`Transition`, `Animatable`) and rendering logic.
10+
- Supported custom shapes using `ImageVector` in Compose.
11+
12+
### Changed
13+
- Increased `minSdkVersion` to 21 to support Jetpack Compose.
14+
- Updated library version to 0.4.0.
15+
516
## [0.3.0] - 2026-06-13
617

718
### Added

README.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ allprojects {
3838

3939
```gradle
4040
dependencies {
41-
implementation 'com.github.ChadCSong:ShineButton:v0.3.0'
41+
implementation 'com.github.ChadCSong:ShineButton:v0.4.0'
4242
}
4343
```
4444

@@ -89,6 +89,25 @@ ShineButton shineButton = (ShineButton) findViewById(R.id.shine_button);
8989
shineButton.init(activity);
9090
```
9191

92+
### Jetpack Compose Implementation
93+
94+
ShineButton now supports Jetpack Compose natively!
95+
96+
```kotlin
97+
var isChecked by remember { mutableStateOf(false) }
98+
99+
ShineButtonCompose(
100+
isChecked = isChecked,
101+
onCheckedChange = { isChecked = it },
102+
shape = Icons.Default.Favorite, // Use any ImageVector
103+
btnColor = Color.LightGray,
104+
btnFillColor = Color.Red,
105+
shineColor = Color.Red,
106+
shineSize = 50.dp,
107+
allowRandomColor = true
108+
)
109+
```
110+
92111
Or create it dynamically:
93112

94113
```java
@@ -140,7 +159,7 @@ shineButton.setFixDialog(dialog);
140159
We are continuously working to improve ShineButton. Here is what we have planned:
141160

142161
- [x] **Kotlin Migration**: Fully convert the library to Kotlin for better safety and modern features.
143-
- [ ] **Jetpack Compose**: Provide a native Composable version of ShineButton.
162+
- [x] **Jetpack Compose**: Provide a native Composable version of ShineButton.
144163
- [ ] **Vector Support**: Allow using `VectorDrawable` as shape masks.
145164
- [ ] **Custom Animators**: Support for custom easing and path-based animations.
146165
- [ ] **Material 3**: Update the demo app with Material 3 design and dynamic colors.

gradle.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
1818
# org.gradle.parallel=true
1919
org.gradle.jvmargs=-Xmx1536M
20-
VERSION_NAME=0.3.0
21-
VERSION_CODE=12
20+
VERSION_NAME=0.4.0
21+
VERSION_CODE=13
2222
POM_GROUP_ID=com.sackcentury
2323
POM_DESCRIPTION=This is a UI lib for Android. Effects like shining.
2424
POM_URL=https://github.com/ChadCSong/ShineButton

shinebuttonlib/build.gradle

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,30 @@ android {
66
compileSdkVersion 34
77

88
defaultConfig {
9-
minSdkVersion 14
9+
minSdkVersion 21
1010
targetSdkVersion 34
11-
versionCode 12
12-
versionName "0.3.0"
11+
versionCode 13
12+
versionName "0.4.0"
1313

1414
}
15+
16+
buildFeatures {
17+
compose true
18+
}
19+
20+
composeOptions {
21+
kotlinCompilerExtensionVersion '1.3.2'
22+
}
23+
1524
compileOptions {
1625
sourceCompatibility JavaVersion.VERSION_1_8
1726
targetCompatibility JavaVersion.VERSION_1_8
1827
}
28+
29+
kotlinOptions {
30+
jvmTarget = '1.8'
31+
}
32+
1933
buildTypes {
2034
release {
2135
minifyEnabled false
@@ -30,6 +44,14 @@ dependencies {
3044
testImplementation 'junit:junit:4.13.2'
3145
implementation 'androidx.appcompat:appcompat:1.6.1'
3246
implementation 'com.github.MasayukiSuda:EasingInterpolator:v1.3.2'
47+
48+
// Compose dependencies
49+
def compose_version = '1.3.3'
50+
implementation "androidx.compose.ui:compose-ui:$compose_version"
51+
implementation "androidx.compose.material:compose-material:$compose_version"
52+
implementation "androidx.compose.ui:compose-ui-tooling-preview:$compose_version"
53+
implementation "androidx.compose.runtime:compose-runtime:$compose_version"
54+
implementation "androidx.compose.animation:compose-animation:$compose_version"
3355
}
3456

3557
//apply from: '../gradle-mvn-push.gradle'
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package com.sackcentury.shinebuttonlib
2+
3+
import androidx.compose.animation.core.*
4+
import androidx.compose.foundation.Canvas
5+
import androidx.compose.foundation.clickable
6+
import androidx.compose.foundation.interaction.MutableInteractionSource
7+
import androidx.compose.foundation.layout.Box
8+
import androidx.compose.foundation.layout.size
9+
import androidx.compose.runtime.*
10+
import androidx.compose.ui.Alignment
11+
import androidx.compose.ui.Modifier
12+
import androidx.compose.ui.geometry.Offset
13+
import androidx.compose.ui.geometry.Size
14+
import androidx.compose.ui.graphics.Color
15+
import androidx.compose.ui.graphics.ImageBitmap
16+
import androidx.compose.ui.graphics.Paint
17+
import androidx.compose.ui.graphics.PaintingStyle
18+
import androidx.compose.ui.graphics.drawscope.Stroke
19+
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
20+
import androidx.compose.ui.graphics.nativeCanvas
21+
import androidx.compose.ui.graphics.painter.Painter
22+
import androidx.compose.ui.res.imageResource
23+
import androidx.compose.ui.unit.Dp
24+
import androidx.compose.ui.unit.dp
25+
import kotlin.math.PI
26+
import kotlin.math.cos
27+
import kotlin.math.sin
28+
29+
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
30+
import androidx.compose.ui.graphics.nativeCanvas
31+
import androidx.compose.ui.graphics.vector.ImageVector
32+
import androidx.compose.ui.graphics.vector.rememberVectorPainter
33+
import androidx.compose.ui.graphics.graphicsLayer
34+
import androidx.compose.ui.graphics.drawscope.translate
35+
36+
/**
37+
* A native Jetpack Compose implementation of the ShineButton.
38+
*
39+
* @param isChecked The current checked state.
40+
* @param onCheckedChange Callback when the state changes.
41+
* @param modifier Modifier for the button.
42+
* @param shape The Vector image to use for the button icon (e.g., Icons.Default.Favorite).
43+
* @param btnColor Base color of the button (unchecked).
44+
* @param btnFillColor Color when the button is checked.
45+
* @param shineColor Primary color for shine particles.
46+
* @param shineSize Size of the button.
47+
* @param shineCount Number of shine particles.
48+
*/
49+
@Composable
50+
fun ShineButtonCompose(
51+
isChecked: Boolean,
52+
onCheckedChange: (Boolean) -> Unit,
53+
shape: ImageVector,
54+
modifier: Modifier = Modifier,
55+
btnColor: Color = Color.Gray,
56+
btnFillColor: Color = Color.Red,
57+
shineColor: Color = Color.Red,
58+
shineSize: Dp = 50.dp,
59+
shineCount: Int = 8,
60+
animDuration: Int = 1000,
61+
allowRandomColor: Boolean = false
62+
) {
63+
val interactionSource = remember { MutableInteractionSource() }
64+
val painter = rememberVectorPainter(image = shape)
65+
66+
// Animation states
67+
val transition = updateTransition(targetState = isChecked, label = "ShineTransition")
68+
69+
val shineProgress by transition.animateFloat(
70+
transitionSpec = {
71+
if (targetState) {
72+
tween(durationMillis = animDuration, easing = LinearOutSlowInEasing)
73+
} else {
74+
tween(durationMillis = 0)
75+
}
76+
},
77+
label = "ShineProgress"
78+
) { checked ->
79+
if (checked) 1f else 0f
80+
}
81+
82+
val scale by transition.animateFloat(
83+
transitionSpec = {
84+
if (targetState) {
85+
keyframes {
86+
durationMillis = 500
87+
0.4f at 0
88+
1.1f at 250
89+
1.0f at 500
90+
}
91+
} else {
92+
tween(durationMillis = 200)
93+
}
94+
},
95+
label = "Scale"
96+
) { checked ->
97+
1.0f
98+
}
99+
100+
val particleColors = remember(allowRandomColor) {
101+
if (allowRandomColor) {
102+
listOf(
103+
Color(0xFFFFFF99), Color(0xFFFFCCCC), Color(0xFF996699),
104+
Color(0xFFFF6666), Color(0xFFFFFF66), Color(0xFFF44336),
105+
Color(0xFF666666), Color(0xFFCCCC00)
106+
)
107+
} else {
108+
listOf(shineColor)
109+
}
110+
}
111+
112+
Box(
113+
modifier = modifier
114+
.size(shineSize)
115+
.clickable(
116+
interactionSource = interactionSource,
117+
indication = null
118+
) {
119+
onCheckedChange(!isChecked)
120+
},
121+
contentAlignment = Alignment.Center
122+
) {
123+
// Shine Particles Layer
124+
if (shineProgress > 0f && shineProgress < 1f) {
125+
Canvas(modifier = Modifier.size(shineSize * 2.5f)) {
126+
val center = Offset(size.width / 2, size.height / 2)
127+
val maxRadius = (shineSize.toPx() * 1.2f)
128+
val currentRadius = maxRadius * shineProgress
129+
130+
for (i in 0 until shineCount) {
131+
val angle = (360f / shineCount) * i
132+
val angleRad = angle * (PI / 180f).toFloat()
133+
134+
val x = center.x + cos(angleRad) * currentRadius
135+
val y = center.y + sin(angleRad) * currentRadius
136+
137+
val color = if (allowRandomColor) particleColors[i % particleColors.size] else shineColor
138+
139+
// Main particle
140+
drawCircle(
141+
color = color,
142+
radius = 3.dp.toPx() * (1f - shineProgress),
143+
center = Offset(x, y)
144+
)
145+
146+
// Secondary small particle
147+
val offsetAngleRad = (angle + 20f) * (PI / 180f).toFloat()
148+
val x2 = center.x + cos(offsetAngleRad) * (currentRadius * 0.85f)
149+
val y2 = center.y + sin(offsetAngleRad) * (currentRadius * 0.85f)
150+
151+
drawCircle(
152+
color = color.copy(alpha = 0.6f),
153+
radius = 1.5.dp.toPx() * (1f - shineProgress),
154+
center = Offset(x2, y2)
155+
)
156+
}
157+
}
158+
}
159+
160+
// Center Icon Layer
161+
val iconColor = if (isChecked) btnFillColor else btnColor
162+
Box(
163+
modifier = Modifier
164+
.size(shineSize)
165+
.graphicsLayer(
166+
scaleX = scale,
167+
scaleY = scale
168+
)
169+
) {
170+
Canvas(modifier = Modifier.matchParentSize()) {
171+
with(painter) {
172+
draw(
173+
size = size,
174+
colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(iconColor)
175+
)
176+
}
177+
}
178+
}
179+
}
180+
}

0 commit comments

Comments
 (0)