SnappyRulerSet is an Android drawing application focused on precise and intuitive geometric constructions. It includes core virtual geometry tools such as a draggable, rotatable ruler with snapping to common angles and existing line points; set squares with 45° and 30°–60° variants; a protractor that measures and snaps angles with high accuracy; and optionally a compass for drawing arcs and circles.
The app is implemented using Kotlin and Jetpack Compose, with modular architecture separating rendering, interaction, and data persistence layers. Snapping behavior adapts dynamically with zoom level and prioritizes nearest snap targets with clear visual cues. The app is offline-first, requiring no network connectivity.
- Major Highlights
- Features Implemented
- Documentation References
- Core Components
- How to Run the Project
- Complete Project Structure
- Working Demo
- MVVM Architecture
- Offline First
- Kotlin
- Flows
- Stateflow
- Compose UI
- Grid Snap Toggle - On/Off
- Common Angle Snapping
- Free Hand Drawing
- Compass Tool
- Protractor Tool
- Set Square Tool
- Export PNG/JPG
- Grid Resizing
- Undo and Redo Support
- Precision Hud for selected Tool
- Jetpack Compose Graphics Overview
- Drawing Shapes
- Graphic Modifiers
- Multi Touch Input
- Intrinsic Measurements
- Dragging, Touch Inputs
Location: ui/DrawingCanvas.kt
The main composable that handles all drawing operations and user interactions.
@Composable
fun DrawingCanvas(
state: DrawingState, // Current drawing state
snapEngine: SnapEngine, // Magnetic snapping engine
onAction: (DrawingAction) -> Unit, // Action dispatcher
modifier: Modifier = Modifier
)Key Responsibilities:
- Handle touch gestures (tap, drag)
- Render all drawing elements
- Manage tool-specific interactions
- Coordinate with SnapEngine for magnetic snapping
Location: data/DrawingState.kt
Centralized state that holds all drawing information.
data class DrawingState(
val currentTool: DrawingTool = DrawingTool.Freehand,
val elements: List<DrawingElement> = emptyList(),
val isDrawing: Boolean = false,
val canvasOffset: Offset = Offset.Zero,
val canvasScale: Float = 1f,
val strokeColor: Color = Color.Black,
val strokeWidth: Float = 2f,
val snapEnabled: Boolean = true,
val rulerTool: RulerTool = RulerTool(),
val compassTool: CompassTool = CompassTool(),
val protractorTool: ProtractorTool = ProtractorTool(),
val setSquareTool: SetSquareTool = SetSquareTool(),
val gridSpacing: Float = 20f
)Location: viewmodel/DrawingViewModel.kt
Manages state changes and business logic.
class DrawingViewModel : ViewModel() {
private val _drawingState = mutableStateOf(DrawingState())
val drawingState: State<DrawingState> = _drawingState
fun handleAction(action: DrawingAction) {
// Process actions and update state
}
}-
Clone the Repository:
git clone https://github.com/gunishjain/SnappyRulerSet.git cd SnappyRulerSet -
Check AGP Version: Make sure to check AGP Version to avoid compose compiler sync issues. This project uses AGP version 8.9.1.
-
Build and run the Project:
./gradlew assembleDebug
SnappyRulerSet/
├── app/
│ ├── src/main/java/com/gunishjain/myapplication/
│ │ ├── data/
│ │ │ ├── DrawingState.kt
│ │ │ └── UndoRedoManager.kt
│ │ ├── drawing/
│ │ │ ├── tool/
│ │ │ │ ├── CompassTool.kt
│ │ │ │ ├── ProtractorTool.kt
│ │ │ │ ├── RulerTool.kt
│ │ │ │ └── SetSquareTool.kt
│ │ │ ├── PrecisionHUD.kt
│ │ │ └── SnapEngine.kt
│ │ ├── export/
│ │ │ ├── BitmapExporter.kt
│ │ │ └── ImageExporter.kt
│ │ ├── model/
│ │ │ ├── DrawingTool.kt
│ │ │ ├── GeometryTypes.kt
│ │ │ └── SnapTarget.kt
│ │ ├── ui/
│ │ │ ├── theme/
│ │ │ │ ├── Color.kt
│ │ │ │ ├── Theme.kt
│ │ │ │ └── Type.kt
│ │ │ ├── DrawingCanvas.kt
│ │ │ ├── ExportDialog.kt
│ │ │ └── ToolOverlay.kt
│ │ ├── utils/
│ │ │ ├── HapticFeedbackUtil.kt
│ │ │ └── PermissionHandler.kt
│ │ ├── viewmodel/
│ │ │ └── DrawingViewModel.kt
│ │ └── MainActivity.kt
| Demo 1 | Demo 2 |
|---|---|
![]() |
![]() |

