Creating a Dynamic Alphabet Slider for Your Android Launcher with Flutter

Creating a Dynamic Alphabet Slider for Your Android Launcher with Flutter

FlutterPulse

This article was translated specially for the channel FlutterPulseYou'll find lots of interesting things related to Flutter on this channel. Don't hesitate to subscribe!๐Ÿš€

โœจ Build an amazing alphabet slider with Flutter! Stunning animations, responsive gestures, and dynamic positioningโ€Šโ€”โ€Šlike Niagara Launcher.

Hello Flutter enthusiasts! ๐Ÿ‘‹ I'm excited to kick off the Samagra development series, where I'll share my journey of building an open-source Android launcher that combines completeness with customization.

In this first article, I'll guide you through creating a dynamic alphabet slider component โ€” a handy UI element found in many launchers (like Niagara) that lets users quickly navigate through their app list. By the end of this tutorial, you'll understand how to implement a feature-rich alphabet slider with beautiful animations and dynamic positioning.

(Demo of our Samagra launcher's alphabet slider in action)

๐Ÿ” What We're Building

Our alphabet slider will have these key features:

  • Vertical letter index that responds to user touch
  • Visual feedback when dragging (with a circular letter indicator)
  • Dynamic positioning that follows your finger
  • Adaptive bell curve animation for nearby letters
  • Support for both left and right alignment
  • Clean, customizable design

๐Ÿงฉ Setting Up the Project

Let's start by creating a new Flutter project and implementing our AlphabetSlider widget. For this tutorial, we'll focus solely on the slider component, which you can later integrate into your launcher's app drawer.

First, create a new Flutter project and replace the content of your main.dart file with the following code:

import 'dart:math' as math;
import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Alphabet Slider',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: Scaffold(
body: Center(
child: AlphabetSlider(
onLetterSelected: (val) {
// Handle custom logic after letter selection
},
alignment: Alignment.centerRight,
letterHeight: 20,
),
),
),
);
}
}

Here we've set up a basic Flutter app with a simple layout containing our AlphabetSlider widget. I've placed it on the right side of the screen with a letter height of 20 pixels.

๐Ÿ“ Building the AlphabetSlider Widget

Now let's implement the core functionality of our slider. We'll create a stateful widget that tracks user interactions and animates accordingly.

class AlphabetSlider extends StatefulWidget {
final Function(String) onLetterSelected;
final AlignmentGeometry alignment;
final double letterHeight;

const AlphabetSlider({
super.key,
required this.onLetterSelected,
this.alignment = Alignment.centerLeft,
this.letterHeight = 20,
});

@override
State<AlphabetSlider> createState() => _AlphabetSliderState();
}

Our widget accepts three parameters:

  • onLetterSelected: A callback function that will be triggered when a letter is selected
  • alignment: Determines if the slider appears on the left or right side of the screen
  • letterHeight: Controls the height of each letter in the slider

๐ŸŽฎ Handling State and Drag Gestures

Next, let's implement the state class with all the necessary variables and methods to handle user interactions:

class _AlphabetSliderState extends State<AlphabetSlider>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
String _selectedLetter = '';
bool _isDragging = false;
Offset _dragPosition = Offset.zero;
Offset _startDragPosition = Offset.zero;
final _columnKey = GlobalKey();
int _selectedIndex = -1;
double _initialTopY = 0;
double _initialBottomY = 0;
double _topY = -999999;
double _bottomY = -999999;
double _itemsHeight = 0;

final List<String> _alphabet = List.generate(
26,
(index) => String.fromCharCode(65 + index),
);

@override
void initState() {
super.initState();
_itemsHeight = widget.letterHeight * _alphabet.length;
_controller = AnimationController(
duration: const Duration(milliseconds: 50),
vsync: this,
);
CurvedAnimation(parent: _controller, curve: Curves.easeOutBack);
WidgetsBinding.instance.addPostFrameCallback((_) {
_getColumnYCoordinates();
});
}

void _getColumnYCoordinates() {
final RenderBox? columnBox =
_columnKey.currentContext?.findRenderObject() as RenderBox?;
if (columnBox != null) {
final Offset columnOffset = columnBox.localToGlobal(Offset.zero);
_initialTopY = columnOffset.dy;
_initialBottomY = _initialTopY + columnBox.size.height;
}
}

In the state initialization, we're:

  1. Generating the alphabet list (A-Z)
  2. Setting up an animation controller for smooth transitions
  3. Calculating the total height of all letters
  4. Getting the coordinates of our column after the first frame is rendered

This is crucial for the dynamic positioning feature that allows our alphabet slider to follow the user's finger when they drag outside the initial boundaries.

๐Ÿ‘† Handling Drag Events

Now let's implement the drag event handlers that make our slider interactive:

  void _onDragStart(DragStartDetails details) {
setState(() {
_isDragging = true;
_startDragPosition = details.globalPosition;
_updateSelectionFromPosition(details.globalPosition);
});
}

void _onDragUpdate(DragUpdateDetails details) {
_updateSelectionFromPosition(details.globalPosition);
}

void _resetAlphabetPositionY() {
_topY = -999999;
_bottomY = -999999;
}

void _updateSelectionFromPosition(Offset globalPosition) {
if (_topY == -999999) _topY = _initialTopY;
if (_bottomY == -999999) _bottomY = _initialBottomY;
final RenderBox? columnBox =
_columnKey.currentContext?.findRenderObject() as RenderBox?;
if (columnBox == null) return;
final Offset localPosition = columnBox.globalToLocal(globalPosition);
double totalHeight = widget.letterHeight * _alphabet.length;
double startY = (columnBox.size.height - totalHeight) / 2;
int index = ((localPosition.dy - startY) / widget.letterHeight).floor();
index = index.clamp(0, _alphabet.length - 1);
// Check if the current thumb position is inside or outside of the column
bool isInsideColumn =
globalPosition.dy >= _topY && globalPosition.dy <= _bottomY;
if (!isInsideColumn) {
if (globalPosition.dy < _topY) {
double safeTop = MediaQuery.paddingOf(context).top;
if (globalPosition.dy >= safeTop) {
setState(() {
_topY = globalPosition.dy;
_bottomY = _topY + _itemsHeight;
});
}
}
if (globalPosition.dy > _bottomY) {
double safeBottom =
MediaQuery.of(context).size.height -
MediaQuery.paddingOf(context).bottom;
if (globalPosition.dy <= safeBottom) {
setState(() {
_bottomY = globalPosition.dy;
_topY = _bottomY - _itemsHeight;
});
}
}
}
setState(() {
_dragPosition = globalPosition;
_selectedLetter = _alphabet[index];
_selectedIndex = index;
});
widget.onLetterSelected(_selectedLetter);
}

void _onDragEnd(DragEndDetails details) {
_resetAlphabetPositionY();
setState(() {
_isDragging = false;
_startDragPosition = Offset.zero;
_selectedIndex = -1;
});
_controller.forward(from: 0.0);
}

The magic happens in _updateSelectionFromPosition(), which:

  1. Finds which letter the user is pointing at based on their finger position
  2. Updates the alphabet list position if the user drags outside the initial boundaries
  3. Makes sure everything stays within the safe areas of the screen
  4. Calls our callback function so parent widgets know which letter was selected

This dynamic positioning is a standout feature! When users drag their finger beyond the initial alphabet list, the list follows them โ€” similar to how Niagara Launcher's alphabet slider works. It creates a natural, fluid interaction that feels intuitive.

๐Ÿ”„ Creating the Bell Curve Animation

Now for one of the coolest parts โ€” the bell curve animation that creates a wave-like effect when you drag:

double _calculateOffset(int index) {
double screenWidth = MediaQuery.sizeOf(context).width;
bool startedFromSecondHalf = _startDragPosition.dx > (screenWidth / 2);
if (!_isDragging || _selectedIndex == -1) return 0.0;
int distance = (index - _selectedIndex).abs();
double maxOffset;
if (widget.alignment == Alignment.centerLeft) {
if (startedFromSecondHalf) {
maxOffset = screenWidth * .30;
} else {
if (_dragPosition.dx + 60 <= screenWidth - 100) {
maxOffset = _dragPosition.dx + 60;
} else {
maxOffset = screenWidth - 100;
}
}
// Create a single smooth bell curve using gaussian function
double divisor = (_dragPosition.dx / 4.0) + 12.0;
double gaussian = math.exp(-(math.pow(distance, 2) / divisor));
double offset = maxOffset * gaussian;
return offset.clamp(0, screenWidth - 85);
} else {
double maxOffset;
if (!startedFromSecondHalf) {
maxOffset = screenWidth * .30;
} else {
maxOffset = (screenWidth - _dragPosition.dx + 60).clamp(
0,
screenWidth - 100,
);
}
double divisor = ((screenWidth - _dragPosition.dx) / 4.0) + 12.0;
double gaussian = math.exp(-(math.pow(distance, 2) / divisor));
double offset = maxOffset * gaussian;
return -offset;
}
}

This method calculates how much each letter should move based on:

  1. Its distance from the selected letter
  2. The position of your finger on the screen
  3. Whether the slider is aligned to the left or right

The Gaussian function creates that beautiful bell curve โ€” letters close to the selected one move more, while distant letters move less. The divisor adjusts the curve's width based on your finger's horizontal position, creating a responsive and dynamic feel.

๐ŸŽจ Building the UI

Finally, let's implement the build method to render our alphabet slider:

@override
Widget build(BuildContext context) {
return GestureDetector(
onVerticalDragStart: _onDragStart,
onVerticalDragUpdate: _onDragUpdate,
onVerticalDragEnd: _onDragEnd,
child: Container(
height: double.infinity,
decoration: BoxDecoration(
border: Border.all(color: Colors.transparent),
),
child: Stack(
alignment: widget.alignment,
children: [
AnimatedPositioned(
duration: Duration(milliseconds: 50),
top:
_topY == -999999
? null
: _topY.clamp(
MediaQuery.paddingOf(context).top,
MediaQuery.of(context).size.height -
_itemsHeight -
MediaQuery.paddingOf(context).bottom,
),
child: Column(
key: _columnKey,
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(_alphabet.length, (index) {
bool isSelected = _alphabet[index] == _selectedLetter;
bool isRightAligned =
widget.alignment == Alignment.centerRight;
return Container(
width: MediaQuery.sizeOf(context).width,
height: widget.letterHeight,
padding: EdgeInsets.fromLTRB(
isRightAligned ? 0 : 10,
0,
isRightAligned ? 10 : 0,
0,
),
child: AnimatedContainer(
duration: Duration(milliseconds: 50),
transform: Matrix4.translationValues(
_calculateOffset(index),
0,
0,
),
child: Text(
_alphabet[index],
textAlign:
widget.alignment == Alignment.centerLeft
? TextAlign.left
: TextAlign.right,
style: TextStyle(
fontSize: 16,
fontWeight:
isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected ? Colors.blue : Colors.black,
),
),
),
);
}),
),
),
if (_isDragging && _selectedIndex != -1)
Positioned(
left:
widget.alignment == Alignment.centerLeft
? 30 + _calculateOffset(_selectedIndex)
: null,
right:
widget.alignment == Alignment.centerRight
? 30 + _calculateOffset(_selectedIndex).abs()
: null,
top: (_dragPosition.dy - 25).clamp(
MediaQuery.paddingOf(context).top,
MediaQuery.of(context).size.height -
50 -
MediaQuery.paddingOf(context).bottom,
),
child: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: Center(
child: Text(
_selectedLetter,
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
),
),
);
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}
}

The build method creates:

  1. The vertically arranged alphabet letters that animate using our bell curve
  2. A floating circular indicator showing the selected letter during drag
  3. Support for both left and right alignment with proper text alignment

๐Ÿš€ Putting It All Together

That's it! We've created a fully functional alphabet slider with some impressive features:

  1. Dynamic Positioning: The alphabet slider follows your finger if you drag beyond its boundaries
  2. Adaptive Bell Curve: Letters near the selected one animate outward in a fluid, wave-like motion
  3. Visual Feedback: A circular indicator shows the currently selected letter
  4. Customizable Alignment: The slider works on both the left and right sides of the screen
  5. Safe Area Handling: Everything stays within the visible screen area

This component is just the beginning of our Samagra launcher journey. In future articles, we'll explore how to integrate this with an app drawer, create custom animations for app icons, and much more.

๐Ÿ” Next Steps

You can already customize this component further with your own styling:

  • Change the colors and font styles
  • Adjust the animation timing and curves
  • Modify the bell curve behavior
  • Add haptic feedback
  • Include additional indicators or labels

๐Ÿ”— Connect With Us

Thank you for joining us on this journey to build Samagra! We believe in the power of community and would love to connect with fellow developers, designers, and launcher enthusiasts.

Stay Updated

๐ŸŒŸ Join the Samagra Journey

We're excited to build Samagra as an open-source alternative to premium launchers, bringing powerful customization without the complexity or paywalls. If you want to contribute or follow our development journey, connect with us and stay tuned for the next articles in this series!

In the next article, we'll dive into fetching the list of launchable apps, listening for app changes, launching apps, and even uninstalling them โ€” all while ensuring a smooth Flutter-Kotlin integration. Stay tuned! ๐Ÿš€ See you then! ๐Ÿ‘‹

Did you enjoy this tutorial? Have questions or want to contribute to Samagra? Leave a comment below or reach out to us directly. Let's build something amazing together!

Samagra โ€” Where Completeness Meets Customization

Report Page