Native Android GIF creator — pick photos, reorder frames, add styled text overlays, and export an animated GIF entirely on-device with a custom pure-Kotlin encoder
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.
Modern Android stack with a custom-built encoding layer — no third-party GIF library.
Four distinct systems working together to produce a polished on-device GIF creation experience.
LzwEncoder.ktQuantizer interfacedetectTransformGesturesMediaStore (scoped storage, Android 10+)ShareCompat system share sheetReal device screenshots from a Pixel 9. Click any to view full size.
The engineering decisions that made GIFit a substantive Android project.
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.
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.
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.
The text overlay responds to drag, pinch-to-scale, and twist-to-rotate simultaneously on a single touch area. This is implemented with Compose's detectTransformGestures modifier, which provides a single callback with pan, zoom, and rotation deltas per gesture event. The three transforms are accumulated into a ViewModel-held state object (offset, scale, rotation) that drives both the live preview rendering and the Canvas draw calls that bake the text into each frame at export time. The same transform math is applied identically in both paths so the exported GIF matches exactly what the user saw in preview.
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.
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.
detectTransformGesturesFrom custom encoding algorithms to polished Compose UIs — we build native Android apps that go beyond the standard toolkit.