Scratchify is a powerful and highly customizable scratch card SDK built using Jetpack Compose Multiplatform. It enables you to create engaging interactive scratch surfaces where users can scratch off an overlay to reveal hidden content underneath. Perfect for reward systems, discount reveals, surprise elements, and gamification in your modern apps!
Perfect for:
โ
Reward reveals & lottery systems
โ
Discount coupons & promotional codes
โ
Interactive surprise elements
โ
Gamification features & mini-games
โ
Educational apps & learning activities
โ
User engagement & retention strategies
โ๏ธ Multiplatform support (Android & iOS)
โ๏ธ Two-layer scratch surface (Overlay & Revealed Content)
โ๏ธ Customizable brush configuration (size, color, opacity)
โ๏ธ Auto-reveal after threshold (configurable percentage)
โ๏ธ Scratch event callbacks (onScratchStarted
, onScratchProgress
, onScratchCompleted
)
โ๏ธ Instant reveal & reset functionality
โ๏ธ Tap-to-scratch detection alongside drag gestures
โ๏ธ Configurable grid resolution for performance optimization
โ๏ธ Save & restore scratch state for persistent experiences
โ๏ธ Cross-platform haptic feedback (iOS & Android)
โ๏ธ Animated reveal effects (fade, scale, slide, bounce, zoom)
โ๏ธ Custom brush shapes (circle, square, star, heart, diamond, custom paths)
โ๏ธ Transparent & colored brush modes (traditional scratch or paint effects)
Since Scratchify is a Compose Multiplatform (CMP) library, you should add it to your commonMain
source set to use it across both iOS and Android.
Add the dependency in your shared
module's build.gradle.kts
:
dependencies {
implementation("io.github.gsrathoreniks:scratchify:<latest_version>")
}
import com.gsrathoreniks.scratchify.api.Scratchify
import com.gsrathoreniks.scratchify.api.ScratchifyController
import com.gsrathoreniks.scratchify.api.config.ScratchifyConfig
@Composable
fun BasicScratchCard() {
val controller = remember { ScratchifyController() }
Scratchify(
modifier = Modifier.size(300.dp, 200.dp),
config = ScratchifyConfig(),
controller = controller,
contentToReveal = {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFFFFD700)),
contentAlignment = Alignment.Center
) {
Text("๐ You Won! ๐", style = MaterialTheme.typography.headlineMedium)
}
},
overlayContent = {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFF8E24AA)),
contentAlignment = Alignment.Center
) {
Text("Scratch Here!", color = Color.White)
}
}
)
}
@Composable
fun AdvancedScratchCard() {
val controller = remember {
ScratchifyController(
ScratchifyConfig(
// Core settings
revealFullAtPercent = 0.6f,
gridResolution = 100,
enableTapToScratch = true,
// Brush configuration
brushConfig = ScratchifyBrushConfig(
brushSize = 30.dp,
brushColor = Color.Red,
opacity = 0.8f,
brushShape = BrushShape.Star(points = 5)
),
// Haptic feedback
hapticConfig = ScratchifyHapticConfig(
isEnabled = true,
onScratchStarted = HapticFeedbackType.LIGHT,
onScratchProgress = HapticFeedbackType.LIGHT,
onScratchCompleted = HapticFeedbackType.SUCCESS,
progressHapticInterval = 0.25f
),
// Animation effects
animationConfig = ScratchifyAnimationConfig(
revealAnimationType = RevealAnimationType.BOUNCE,
animationDurationMs = 800,
enableProgressAnimation = true
)
)
)
}
// Controller API usage
Row {
Button(onClick = { controller.revealInstantly() }) {
Text("Reveal")
}
Button(onClick = { controller.resetScratch() }) {
Text("Reset")
}
}
Scratchify(
modifier = Modifier.size(300.dp, 200.dp),
config = controller.config,
controller = controller,
contentToReveal = { /* Your content */ },
overlayContent = { /* Your overlay */ }
)
}
Parameter | Type | Default | Description |
---|---|---|---|
revealFullAtPercent |
Float |
0.75f |
Auto-reveal threshold (0.0 to 1.0) |
isScratchingEnabled |
Boolean |
true |
Enable/disable scratching |
gridResolution |
Int |
150 |
Grid resolution for performance tuning |
enableTapToScratch |
Boolean |
true |
Allow single taps to create scratches |
Parameter | Type | Default | Description |
---|---|---|---|
brushSize |
Dp |
4.dp |
Size of the brush stroke |
brushColor |
Color |
Color.Cyan |
Brush color (Color.Transparent for traditional scratch) |
opacity |
Float |
1f |
Brush opacity (0.0 to 1.0) |
brushShape |
BrushShape |
BrushShape.Circle |
Shape of the brush stroke |
BrushShape.Circle
- Classic circular brushBrushShape.Square
- Square brush strokesBrushShape.Star(points: Int = 5)
- Star-shaped brushBrushShape.Heart
- Heart-shaped brushBrushShape.Diamond
- Diamond-shaped brushBrushShape.Custom(path: Path, size: Dp)
- Custom path shapes
Parameter | Type | Default | Description |
---|---|---|---|
isEnabled |
Boolean |
true |
Enable haptic feedback |
onScratchStarted |
HapticFeedbackType |
LIGHT |
Feedback when scratching begins |
onScratchProgress |
HapticFeedbackType |
NONE |
Feedback during scratching |
onScratchCompleted |
HapticFeedbackType |
SUCCESS |
Feedback when completed |
progressHapticInterval |
Float |
0.25f |
Progress interval for haptic feedback |
Parameter | Type | Default | Description |
---|---|---|---|
revealAnimationType |
RevealAnimationType |
FADE |
Type of reveal animation |
animationDurationMs |
Int |
500 |
Animation duration in milliseconds |
enableProgressAnimation |
Boolean |
true |
Enable progress animations |
NONE
- No animationFADE
- Fade out effectSCALE
- Scale down effectSLIDE_UP
- Slide up and disappearSLIDE_DOWN
- Slide down and disappearSLIDE_LEFT
- Slide left and disappearSLIDE_RIGHT
- Slide right and disappearBOUNCE
- Bouncy scale effectZOOM_OUT
- Zoom out effect
The ScratchifyController
provides programmatic control over the scratch card:
val controller = remember { ScratchifyController() }
// Instant actions
controller.revealInstantly() // Reveal content immediately
controller.resetScratch() // Reset to initial state
// State management
val state = controller.saveState() // Save current scratch state
controller.restoreState(state) // Restore saved state
// Progress monitoring
val progress = controller.scratchProgress // Current scratch progress (0.0 to 1.0)
- Use
Color.Transparent
for traditional scratch-off behavior - Use any other color for paint/overlay effects
- Combine with
opacity
for semi-transparent effects
- Lower
gridResolution
(75-100) for better performance on complex layouts - Higher
gridResolution
(150-200) for more precise scratch detection
Scratchify(
config = ScratchifyConfig(
revealFullAtPercent = 0.8f,
brushConfig = ScratchifyBrushConfig(brushSize = 25.dp),
hapticConfig = ScratchifyHapticConfig(
onScratchCompleted = HapticFeedbackType.SUCCESS
),
animationConfig = ScratchifyAnimationConfig(
revealAnimationType = RevealAnimationType.BOUNCE
)
),
contentToReveal = { LotteryPrizeContent() },
overlayContent = { LotteryTicketOverlay() }
)
Scratchify(
config = ScratchifyConfig(
brushConfig = ScratchifyBrushConfig(
brushSize = 35.dp,
brushColor = Color.Magenta,
opacity = 0.7f,
brushShape = BrushShape.Star(points = 6)
)
),
contentToReveal = { CanvasBackground() },
overlayContent = { PaintSurface() }
)
Scratchify(
config = ScratchifyConfig(
enableTapToScratch = true,
gridResolution = 100, // Optimized for mobile
hapticConfig = ScratchifyHapticConfig(
progressHapticInterval = 0.3f
)
),
contentToReveal = { RewardContent() },
overlayContent = { EngagementOverlay() }
)
Platform | Haptic Feedback | Performance | Status |
---|---|---|---|
Android | โ Full Support | โก Optimized | โ Stable |
iOS | โ Full Support | โก Optimized | โ Stable |
Android:
- Vibrator API integration
- Pattern-based haptic feedback
- Performance optimizations for various device types
iOS:
- UIImpactFeedbackGenerator support
- UINotificationFeedbackGenerator for success/error
- Native iOS haptic patterns
- Grid Resolution: Lower values (75-100) for better performance
- Brush Size: Larger brushes require fewer touch points
- Haptic Intervals: Increase interval (0.3f+) to reduce battery usage
- Animation Duration: Shorter animations (300-500ms) feel more responsive
// Save current scratch progress
val scratchState = controller.saveState()
// Later restore the exact same state
controller.restoreState(scratchState)
val customPath = Path().apply {
// Define your custom shape
moveTo(-10f, -10f)
lineTo(10f, -10f)
lineTo(0f, 10f)
close()
}
ScratchifyBrushConfig(
brushShape = BrushShape.Custom(customPath, 30.dp)
)
val controller = remember { ScratchifyController() }
// Monitor progress in real-time
LaunchedEffect(controller.scratchProgress) {
println("Scratch progress: ${(controller.scratchProgress * 100).toInt()}%")
}
We welcome contributions from the community! ๐
- Found a bug? Open an Issue
- Have a feature request? Suggest a Feature
- Clone the repository
- Open in Android Studio
- Run the sample app to see all features in action
We appreciate your help in improving Scratchify! ๐
This project is licensed under the MIT License.
๐ Read the full license: MIT License
- Built with โค๏ธ using Jetpack Compose Multiplatform
- Haptic feedback implementation inspired by platform best practices
- Animation system leveraging Compose's powerful animation APIs
- Performance optimizations based on real-world usage patterns
Made with โค๏ธ for the Compose Multiplatform community