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
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.
Six distinct systems working together to produce a polished on-device GIF creation experience.
detectTransformGesturesTypeface + Canvas passMediaStore scoped storage (Android 10+)FileProvider + system share sheetTaken on a Pixel 6a from the signed release build using generated sample images. 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 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.
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.
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.
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.
detectTransformGesturesTypeface source and identical Canvas render pass shared between the live Compose preview and the encoder — what you see is exactly what lands in the fileFrom custom encoding algorithms to polished Compose UIs — we build native Android apps that go beyond the standard toolkit.
Native Android (Kotlin, Jetpack Compose) with a custom GIF encoder and LZW compression written from scratch, multi-gesture text overlays, and on-device export.
Yes — on-device media-processing apps are part of our software development work.
Yes — GIFit runs entirely on-device with no server, and we build privacy-first offline apps regularly.
Tell us what you have in mind and we'll come back with an approach and a flat quote — no obligation.