I've re-created the threads app splash logo animation in Flutter.
A Flutter splash screen that hand-draws the Threads logo stroke by stroke, then fades away to reveal your app.
Everything lives in two files: lib/splash.dart (the animation) and lib/main.dart (the entry point). No packages, no assets, just Flutter’s canvas.
A single AnimationController runs for 2 seconds. Two TweenSequence animations are derived from it:
_draw = TweenSequence<double>([
TweenSequenceItem(
tween: Tween(begin: 0.0, end: 1.0)
.chain(CurveTween(curve: Curves.easeInOut)),
weight: 80, // first 80% of duration: draw the logo
),
TweenSequenceItem(tween: ConstantTween(1.0), weight: 20),
]).animate(_ctrl);
_overlayFade = TweenSequence<double>([
TweenSequenceItem(tween: ConstantTween(1.0), weight: 85),
TweenSequenceItem(
tween: Tween(begin: 1.0, end: 0.0)
.chain(CurveTween(curve: Curves.easeIn)),
weight: 15, // last 15% of duration: fade the overlay out
),
]).animate(_ctrl);
The drawing finishes before the fade starts, the logo is always fully visible for a moment before disappearing.
The logo is split into an outer path (the spiral body) and a inner path (the dot). They’re drawn sequentially — the inner dot only starts once the outer stroke is 100% complete.
final drawn = draw * total; // total length in logical pixels
final drawOuter = drawn.clamp(0.0, lenOuter);
// ... draw outer stroke up to `drawOuter`
if (drawn > lenOuter) {
final drawInner = (drawn - lenOuter).clamp(0.0, lenInner);
// ... draw inner dot
}
Flutter’s PathMetrics API lets you extract a sub-segment of any path by length. This is the core trick that makes the “drawing” effect work:
for (final metric in _outerMetrics!) {
final take = remaining.clamp(0.0, metric.length);
if (take > 0) canvas.drawPath(metric.extractPath(0, take), paint);
remaining -= take;
if (remaining <= 0) break;
}
Path metrics are computed once and cached as static fields, so there’s no per-frame reallocation.
The stroke width is intentionally wide (30 logical units). To keep it inside the logo silhouette rather than bleeding outside, the canvas is clipped to _logoFillPath before any drawing happens:
canvas.save();
canvas.scale(scaleX, scaleY);
canvas.clipPath(_logoFillPath); // mask to logo shape
// ... draw strokes
canvas.restore();
The fill path uses PathFillType.evenOdd, which correctly handles the hole in the center of the logo.
SplashView wraps any widget. Once the animation completes it replaces itself with child directly — no routes, no Navigator:
if (_done) return widget.child;
return Stack(
children: [
widget.child, // your app renders underneath
AnimatedBuilder(
animation: _ctrl,
builder: (_, __) => Opacity(
opacity: _overlayFade.value,
child: ColoredBox(
color: Colors.black,
child: Center(child: CustomPaint(...)),
),
),
),
],
);
git clone https://github.com/junaidjamel/threads_logo_animation.git
cd threads_logo_animation
flutter pub get
flutter run
This project is licensed under the MIT License.
Junaid Jamel