Files
FocusGram-Android/lib/features/loading/skeleton_screen.dart
Ujwal 7bb472d212 What's new
- Reordered Settings Page.
- Added "Click to Unblur" for posts.
- Added Persistent Notification
- Improved Grayscale Scheduling.
and more.
2026-03-04 10:48:14 +05:45

533 lines
17 KiB
Dart

import 'package:flutter/material.dart';
enum SkeletonType { feed, reels, explore, messages, profile, generic }
class SkeletonScreen extends StatefulWidget {
final SkeletonType skeletonType;
const SkeletonScreen({super.key, this.skeletonType = SkeletonType.generic});
@override
State<SkeletonScreen> createState() => _SkeletonScreenState();
}
class _SkeletonScreenState extends State<SkeletonScreen>
with SingleTickerProviderStateMixin {
late AnimationController _shimmerController;
late Animation<double> _shimmerAnimation;
@override
void initState() {
super.initState();
_shimmerController = AnimationController(
duration: const Duration(milliseconds: 1200),
vsync: this,
)..repeat();
_shimmerAnimation = Tween<double>(
begin: -1.0,
end: 2.0,
).animate(_shimmerController);
}
@override
void dispose() {
_shimmerController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final baseColor = theme.colorScheme.surfaceContainerHighest.withValues(
alpha: theme.brightness == Brightness.dark ? 0.25 : 0.4,
);
final highlightColor = theme.colorScheme.onSurface.withValues(alpha: 0.08);
return Container(
color: theme.scaffoldBackgroundColor,
width: double.infinity,
height: double.infinity,
child: AnimatedBuilder(
animation: _shimmerAnimation,
builder: (context, child) {
return ShaderMask(
shaderCallback: (rect) {
return LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [baseColor, highlightColor, baseColor],
stops: const [0.1, 0.3, 0.6],
transform: _SlidingGradientTransform(
slidePercent: _shimmerAnimation.value,
),
).createShader(rect);
},
blendMode: BlendMode.srcATop,
child: _buildSkeletonContent(context),
);
},
),
);
}
Widget _buildSkeletonContent(BuildContext context) {
switch (widget.skeletonType) {
case SkeletonType.feed:
return _buildFeedSkeleton(context);
case SkeletonType.reels:
return _buildReelsSkeleton(context);
case SkeletonType.explore:
return _buildExploreSkeleton(context);
case SkeletonType.messages:
return _buildMessagesSkeleton(context);
case SkeletonType.profile:
return _buildProfileSkeleton(context);
case SkeletonType.generic:
return _buildGenericSkeleton(context);
}
}
Widget _buildFeedSkeleton(BuildContext context) {
final width = MediaQuery.of(context).size.width;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 80,
padding: const EdgeInsets.symmetric(vertical: 8),
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 12),
itemCount: 6,
itemBuilder: (context, index) => Padding(
padding: const EdgeInsets.only(right: 12),
child: Column(
children: [
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(28),
),
),
const SizedBox(height: 4),
Container(
width: 32,
height: 8,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
],
),
),
),
),
Expanded(
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 0),
itemCount: 3,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
child: Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
),
const SizedBox(width: 8),
Container(
width: 80,
height: 12,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
],
),
),
Container(
width: double.infinity,
height: width,
color: Colors.white,
),
Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: List.generate(
3,
(i) => Padding(
padding: const EdgeInsets.only(right: 16),
child: Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
),
),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Container(
width: width * 0.7,
height: 10,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
),
],
),
);
},
),
),
],
);
}
Widget _buildReelsSkeleton(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 120,
height: 200,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
),
const SizedBox(height: 24),
Container(
width: 150,
height: 16,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(height: 12),
Container(
width: 100,
height: 12,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
],
),
);
}
Widget _buildExploreSkeleton(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(2),
child: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 2,
mainAxisSpacing: 2,
),
itemCount: 15,
itemBuilder: (context, index) {
return Container(color: Colors.white);
},
),
);
}
Widget _buildMessagesSkeleton(BuildContext context) {
return ListView.builder(
itemCount: 8,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
children: [
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(28),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 100,
height: 14,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(height: 6),
Container(
width: 150,
height: 12,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
],
),
),
],
),
);
},
);
}
Widget _buildProfileSkeleton(BuildContext context) {
return SingleChildScrollView(
child: Column(
children: [
const SizedBox(height: 24),
Container(
width: 96,
height: 96,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(48),
),
),
const SizedBox(height: 16),
Container(
width: 120,
height: 16,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(
3,
(index) => Column(
children: [
Container(
width: 40,
height: 20,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(height: 4),
Container(
width: 50,
height: 10,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
],
),
),
),
const SizedBox(height: 24),
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 2,
mainAxisSpacing: 2,
),
itemCount: 9,
itemBuilder: (context, index) {
return Container(color: Colors.white);
},
),
],
),
);
}
Widget _buildGenericSkeleton(BuildContext context) {
final width = MediaQuery.of(context).size.width;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 16),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
children: List.generate(
6,
(index) => Padding(
padding: const EdgeInsets.only(right: 12),
child: Column(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(30),
),
),
const SizedBox(height: 8),
Container(
width: 40,
height: 8,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
],
),
),
),
),
),
const SizedBox(height: 16),
Expanded(
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 12),
itemCount: 3,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.only(bottom: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(18),
),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: width * 0.4,
height: 10,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(height: 6),
Container(
width: width * 0.25,
height: 8,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
],
),
],
),
const SizedBox(height: 12),
Container(
width: double.infinity,
height: width * 1.1,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(18),
),
),
const SizedBox(height: 12),
Container(
width: width * 0.7,
height: 10,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(height: 6),
Container(
width: width * 0.5,
height: 8,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
],
),
);
},
),
),
],
);
}
}
class _SlidingGradientTransform extends GradientTransform {
final double slidePercent;
const _SlidingGradientTransform({required this.slidePercent});
@override
Matrix4 transform(Rect bounds, {TextDirection? textDirection}) {
return Matrix4.translationValues(bounds.width * slidePercent, 0.0, 0.0);
}
}
SkeletonType getSkeletonTypeFromUrl(String url) {
final parsed = Uri.tryParse(url);
if (parsed == null) return SkeletonType.generic;
final path = parsed.path.toLowerCase();
if (path.startsWith('/reels') || path.startsWith('/reel/')) {
return SkeletonType.reels;
} else if (path.startsWith('/explore')) {
return SkeletonType.explore;
} else if (path.startsWith('/messages') || path.startsWith('/inbox')) {
return SkeletonType.messages;
} else if (path.startsWith('/') && !path.startsWith('/accounts')) {
if (path.split('/').length <= 2) {
return SkeletonType.feed;
}
return SkeletonType.profile;
}
return SkeletonType.generic;
}