Back to Portfolio
GIFit app icon

GIFit

Native Android GIF creator — pick photos, edit per-frame, add gesture-driven text overlays with 16 typefaces, and export an animated GIF entirely on-device with a custom pure-Kotlin encoder

v1.1 (vc3) · Google Play Review Pending
Min SDK 21 • Target SDK 36 • Kotlin
Download APK
GIFit — home screen (empty state)
Home
GIFit — frame editor with per-frame controls
Frame Editor
GIFit — text overlay with gesture controls
Text Overlay
GIFit — generated GIF share sheet
Export & Share

Overview

GIFit is a native Android app that turns a selection of photos into an animated GIF entirely on-device — no server, no third-party encoder, no network call. Users pick frames from their photo library, drag to reorder them, set timing per frame or globally, add styled text overlays with full gesture control, and export directly to the gallery or share to any app.

The core technical achievement is a pure-Kotlin GIF encoder written from scratch — implementing the GIF89a spec end-to-end, including LZW compression and two swappable color quantization algorithms. Everything from pixel quantization to byte-packing the bitstream is done in Kotlin with no native dependencies.

GIF89a
Spec Implemented
LZW
Custom Compression
SDK 21+
Android 5.0+
0
Network Calls

Tech Stack

Modern Android stack with a custom-built encoding layer — no third-party GIF library.

Kotlin
Primary Language
Jetpack Compose
Declarative UI + Material 3
Hilt (Dagger)
Dependency Injection
MVVM + StateFlow
Architecture
Coil 3
Image Loading
Custom GIF Encoder
Pure Kotlin, GIF89a
MediaStore API
Scoped Storage Export
ShareCompat
System Share Sheet
Gradle KTS + KSP
Build System
ProGuard / R8
Code Shrinking
JUnit 4 + Espresso
Unit & UI Testing
Compose UI Tests
Compose Test Runner

Key Features

Six distinct systems working together to produce a polished on-device GIF creation experience.

Photo-to-GIF Pipeline

  • Multi-select via Android's privacy-preserving system photo picker
  • Drag-to-reorder frames with animated transitions; duplicate or remove individual frames
  • Undo/redo history for frame edits
  • Global interval slider plus per-frame delay overrides
  • EXIF orientation corrected automatically on import

Per-Frame Editor

  • Dedicated editor sheet per frame: rotate, flip, and crop controls
  • Per-frame delay override independent of the global interval
  • Per-frame text overlay overrides — different text content, position, or style per frame

Transitions & Quality Controls

  • Crossfade and slide frame transitions
  • Four resolution presets: 240p, 480p, 720p, 1080p
  • Two color-quantization modes: Median Cut and NeuQuant
  • Optional error-diffusion dithering to reduce banding
  • Live file-size estimate before encoding starts

Text Overlay System

  • Single-finger drag, pinch-to-scale, and twist-to-rotate via detectTransformGestures
  • 16 typefaces drawn from the device's system font families
  • 7 preset colors with live WYSIWYG preview matching the baked output pixel-for-pixel
  • Reset button to snap back to center at default size and rotation
  • Transform state baked identically into every exported frame via a shared Typeface + Canvas pass

Preview & Custom Encoder

  • Animated in-app playback of exact frames and timing before encoding
  • Playback of the actual generated GIF after export
  • GIF89a encoder written from scratch in Kotlin — LZW compression, palette quantization, Netscape loop block
  • Encoding progress streamed live into the Compose UI

Export & Privacy

  • Saves to device gallery via MediaStore scoped storage (Android 10+)
  • One-tap sharing via FileProvider + system share sheet
  • Share button always responsive — if tapped mid-encode, auto-shares on completion
  • Zero network access — the app does not request the INTERNET permission
  • No server, no account, no ads, no data collection

Screenshots

Taken on a Pixel 6a from the signed release build using generated sample images. Click any to view full size.

GIFit — home screen (empty state)
Home
GIFit — frame editor
Frame Editor
GIFit — text overlay
Text Overlay
GIFit — generated GIF share
Export & Share

Technical Highlights

The engineering decisions that made GIFit a substantive Android project.

GIF89a Encoder Written From Scratch

Rather than wrapping a native GIF library, the encoder implements the GIF89a specification directly in pure Kotlin. This covers every required block: the GIF header and version string, logical screen descriptor, global color table, graphic control extensions (for per-frame delay and disposal method), image descriptors, and compressed image data blocks. The output is a fully spec-compliant GIF readable by any browser, messaging app, or image viewer. Writing this from the spec meant handling every byte offset, block terminator, and bit-packing rule explicitly — there are no convenient abstractions to fall back on.

LZW Compression in Pure Kotlin

GIF uses LZW (Lempel-Ziv-Welch) compression on each frame's pixel data. LzwEncoder.kt implements the full algorithm: initializing the code table with the color table size plus clear and EOI codes, building the string table incrementally as pixel sequences are matched, packing variable-width codes into a bitstream, and flushing sub-blocks capped at 255 bytes per the GIF spec. The minimum code size, code width growth, and clear code insertion on table overflow are all handled according to the spec. The result is a lossless compressed bitstream that is decompressed correctly by any conforming GIF decoder.

Swappable Color Quantization

GIF is limited to 256 colors per frame. Reducing a full-color photo to 256 colors without obvious banding requires a color quantization algorithm. GIFit implements two behind a Quantizer interface: Median Cut, which recursively splits the color space along its longest axis to find representative colors (good quality, deterministic, fast); and NeuQuant, a neural network-based algorithm that produces better results for photos with complex gradients (higher quality, slower). The active quantizer is selectable at encode time, letting the user trade quality for speed.

True WYSIWYG Text — One Typeface Source

The text overlay responds to drag, pinch-to-scale, and twist-to-rotate simultaneously on a single touch area via Compose's detectTransformGestures. The three transforms accumulate into a ViewModel state object that drives both the live Compose preview and the Canvas draw calls that bake text into each frame at export time. Crucially, both paths share one Typeface instance and an identical outline-over-fill render pass — so on-screen text is exactly what lands in the file, pixel-for-pixel, across all 16 system typefaces and every supported color. The same transform math runs identically in both the preview and the encoder, with no approximation between what the user sees and what gets compressed.

Scoped Storage Export & Deferred Share

On Android 10+, apps cannot write directly to arbitrary filesystem paths — all gallery writes go through MediaStore. GIFit inserts a pending ContentValues entry, writes the encoded bytes to the returned URI, then clears the IS_PENDING flag to make the file visible in the gallery. The share flow handles the case where the user taps Share before encoding is complete: a flag is set in the ViewModel, and when the encoder posts its completion callback, the share intent is launched automatically. This makes the Share button always feel responsive regardless of encoding progress.

EXIF Orientation Correction on Import

Photos taken on Android devices often have rotation metadata stored in EXIF rather than physically rotating the pixel data. If the EXIF orientation is ignored, frames appear rotated or mirrored in the exported GIF even though they look correct in the gallery. GIFit reads the EXIF orientation tag on import using ExifInterface and applies the correct rotation matrix via Matrix.postRotate() before the bitmap is handed to the encoder. This runs before quantization so the corrected orientation is what gets compressed — the exported GIF renders correctly on any viewer regardless of which device or app originally captured the photo.

Mixed-Dimension Frame Normalization

When frames arrive from different capture sessions — rotated, cropped, or shot at odd resolutions — they cannot be fed directly to the GIF encoder: getPixels would either crash or produce corrupted output if source bitmaps differ in size. GIFit composites every frame onto a common canvas at the target resolution before quantization, handling rotation, crop offsets, and aspect-ratio differences in a single normalization step. A regression test covers this path specifically, guarding against the class of crash it was introduced to fix.

Memory-Conscious Encoding Pipeline

Encoding a multi-frame GIF at 1080p can push the limits of a mid-range Android device. GIFit applies EXIF-aware downsampling on import so oversized camera photos are scaled to a workable size before entering the pipeline. The encoder streams output to a cache file rather than accumulating the full GIF as a byte array in memory, keeping heap use flat regardless of frame count. Bitmaps are recycled explicitly after each frame is quantized, and any OutOfMemoryError during encoding is caught and surfaced as a user-readable error rather than a silent crash — giving the user a chance to lower the resolution preset and retry.

What This Project Demonstrates

Specification-Level Engineering
Reading a binary file format spec and implementing it correctly from byte offsets and bit fields — not wrapping a library
Algorithm Implementation
LZW compression, two color quantization algorithms (Median Cut, NeuQuant), and optional error-diffusion dithering implemented in pure Kotlin
Compose Gesture System
Simultaneous drag, pinch-to-scale, and twist-to-rotate on a single touch target using detectTransformGestures
Modern Android Storage
MediaStore scoped storage export with pending entry pattern for Android 10+ compatibility
True WYSIWYG Preview
Single Typeface source and identical Canvas render pass shared between the live Compose preview and the encoder — what you see is exactly what lands in the file
Privacy-First Design
Zero network calls — no INTERNET permission declared; all encoding, text overlay, and export happens on-device
End-to-End Play Release
Store listing, Data Safety form, privacy policy, R8 + resource shrinking, Play App Signing, and release management — the full pipeline from binary encoder to published app
Memory-Safe Media Pipeline
EXIF-aware downsampling on import, streaming encode to cache file (not in-memory byte array), explicit bitmap recycling, and user-readable OOM handling
Per-Frame Editing Depth
Rotate, flip, crop, per-frame delay, per-frame text overrides, crossfade/slide transitions, and four resolution presets — all non-destructive via undo/redo

Interested in Android Development?

From custom encoding algorithms to polished Compose UIs — we build native Android apps that go beyond the standard toolkit.

Download APK Get In Touch

Frequently asked questions

How was GIFit built?

Native Android (Kotlin, Jetpack Compose) with a custom GIF encoder and LZW compression written from scratch, multi-gesture text overlays, and on-device export.

Can you build a media or camera app?

Yes — on-device media-processing apps are part of our software development work.

Do you build fully offline, on-device apps?

Yes — GIFit runs entirely on-device with no server, and we build privacy-first offline apps regularly.

Related services & work

Want something like this built?

Tell us what you have in mind and we'll come back with an approach and a flat quote — no obligation.

Get a free quote See more work