profile_image_viewer 1.0.2 copy "profile_image_viewer: ^1.0.2" to clipboard
profile_image_viewer: ^1.0.2 copied to clipboard

A customizable, WhatsApp-style profile image viewer with pinch-to-zoom, swipe-to-dismiss, Hero animations, gallery mode, screenshot protection, and 6+ theme presets.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:profile_image_viewer/profile_image_viewer.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Profile Image Viewer',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF6366F1),
          brightness: Brightness.light,
        ),
        useMaterial3: true,
        appBarTheme: const AppBarTheme(
          centerTitle: true,
          elevation: 0,
        ),
      ),
      darkTheme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF6366F1),
          brightness: Brightness.dark,
        ),
        useMaterial3: true,
        appBarTheme: const AppBarTheme(
          centerTitle: true,
          elevation: 0,
        ),
      ),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  static const List<DemoUser> users = [
    DemoUser(
      name: 'Sarah Wilson',
      role: 'Product Designer',
      image: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=400',
    ),
    DemoUser(
      name: 'James Chen',
      role: 'Software Engineer',
      image: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400',
    ),
    DemoUser(
      name: 'Emily Rodriguez',
      role: 'Marketing Lead',
      image: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=400',
    ),
    DemoUser(
      name: 'Michael Park',
      role: 'Data Scientist',
      image: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=400',
    ),
    DemoUser(
      name: 'Lisa Thompson',
      role: 'UX Researcher',
      image: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=400',
    ),
  ];

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final isDark = theme.brightness == Brightness.dark;

    return Scaffold(
      body: CustomScrollView(
        slivers: [
          // App Bar
          SliverAppBar.large(
            title: const Text('Profile Viewer'),
            actions: [
              IconButton(
                icon: const Icon(Icons.info_outline),
                onPressed: () => _showAboutDialog(context),
              ),
            ],
          ),

          // Header
          SliverToBoxAdapter(
            child: Padding(
              padding: const EdgeInsets.fromLTRB(20, 8, 20, 24),
              child: Text(
                'Tap any profile to preview. Try pinch-to-zoom, double-tap, and swipe down to dismiss.',
                style: theme.textTheme.bodyMedium?.copyWith(
                  color: theme.colorScheme.onSurfaceVariant,
                ),
              ),
            ),
          ),

          // Gallery Section
          SliverToBoxAdapter(
            child: _buildSection(
              context,
              title: 'Gallery Mode',
              subtitle: 'View multiple images with swipe navigation',
              child: _GalleryCard(users: users),
            ),
          ),

          // Theme Presets Section
          SliverToBoxAdapter(
            child: _buildSection(
              context,
              title: 'Theme Presets',
              subtitle: 'Pre-configured styles for popular apps',
              child: _ThemePresetsGrid(users: users),
            ),
          ),

          // Features Section
          SliverToBoxAdapter(
            child: _buildSection(
              context,
              title: 'Features',
              subtitle: 'Explore individual capabilities',
              child: _FeaturesList(users: users, isDark: isDark),
            ),
          ),

          // Bottom Padding
          const SliverToBoxAdapter(
            child: SizedBox(height: 32),
          ),
        ],
      ),
    );
  }

  Widget _buildSection(
    BuildContext context, {
    required String title,
    required String subtitle,
    required Widget child,
  }) {
    final theme = Theme.of(context);
    return Padding(
      padding: const EdgeInsets.fromLTRB(20, 0, 20, 24),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(title, style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)),
          const SizedBox(height: 4),
          Text(subtitle, style: theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.onSurfaceVariant)),
          const SizedBox(height: 16),
          child,
        ],
      ),
    );
  }

  void _showAboutDialog(BuildContext context) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Profile Image Viewer'),
        content: const Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('A customizable, WhatsApp-style profile image viewer for Flutter.'),
            SizedBox(height: 16),
            Text('Features:', style: TextStyle(fontWeight: FontWeight.bold)),
            SizedBox(height: 8),
            Text('• Pinch-to-zoom & double-tap zoom'),
            Text('• Swipe-to-dismiss gesture'),
            Text('• Hero animations'),
            Text('• Gallery with slideshow'),
            Text('• Multiple theme presets'),
            Text('• Keyboard shortcuts'),
            Text('• And much more...'),
          ],
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('Close'),
          ),
        ],
      ),
    );
  }
}

// Data Model
class DemoUser {
  final String name;
  final String role;
  final String image;

  const DemoUser({required this.name, required this.role, required this.image});
}

// Gallery Card
class _GalleryCard extends StatelessWidget {
  final List<DemoUser> users;

  const _GalleryCard({required this.users});

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);

    return Card(
      clipBehavior: Clip.antiAlias,
      child: InkWell(
        onTap: () => _openGallery(context),
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Row(
            children: [
              // Stacked Avatars
              SizedBox(
                width: 100,
                height: 56,
                child: Stack(
                  children: [
                    for (int i = 0; i < 4; i++)
                      Positioned(
                        left: i * 22.0,
                        child: Container(
                          decoration: BoxDecoration(
                            shape: BoxShape.circle,
                            border: Border.all(color: theme.colorScheme.surface, width: 3),
                          ),
                          child: CircleAvatar(
                            radius: 25,
                            backgroundImage: NetworkImage(users[i].image),
                          ),
                        ),
                      ),
                  ],
                ),
              ),
              const SizedBox(width: 16),
              // Info
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      '${users.length} Photos',
                      style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600),
                    ),
                    const SizedBox(height: 4),
                    Text(
                      'Thumbnails • Slideshow • Animated dots',
                      style: theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.onSurfaceVariant),
                    ),
                  ],
                ),
              ),
              Icon(Icons.arrow_forward_ios, size: 16, color: theme.colorScheme.onSurfaceVariant),
            ],
          ),
        ),
      ),
    );
  }

  void _openGallery(BuildContext context) {
    ProfileImageGallery.showWithFade(
      context,
      images: users
          .map((u) => GalleryImage.network(u.image, title: u.name, subtitle: u.role))
          .toList(),
      config: const ProfileImageViewerConfig(
        useAnimatedDots: true,
        showThumbnailStrip: true,
        enableSlideshow: true,
        slideshowInterval: Duration(seconds: 4),
        showAppBarGradient: true,
        useBlurBackground: true,
        precacheImages: true,
      ),
      onSaveTap: (i) => _showSnackBar(context, 'Save ${users[i].name}\'s photo'),
      onShareTap: (i) => _showSnackBar(context, 'Share ${users[i].name}\'s photo'),
    );
  }

  void _showSnackBar(BuildContext context, String message) {
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message), behavior: SnackBarBehavior.floating));
  }
}

// Theme Presets Grid
class _ThemePresetsGrid extends StatelessWidget {
  final List<DemoUser> users;

  const _ThemePresetsGrid({required this.users});

  @override
  Widget build(BuildContext context) {
    final presets = [
      _ThemePreset('WhatsApp', const Color(0xFF25D366), ProfileImageViewerConfig.whatsApp, users[0]),
      _ThemePreset('Instagram', const Color(0xFFE1306C), ProfileImageViewerConfig.instagram, users[1]),
      _ThemePreset('Telegram', const Color(0xFF5EBBEA), ProfileImageViewerConfig.telegram, users[2]),
      _ThemePreset('Twitter', const Color(0xFF1DA1F2), ProfileImageViewerConfig.twitter, users[3]),
    ];

    return GridView.builder(
      shrinkWrap: true,
      physics: const NeverScrollableScrollPhysics(),
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
        mainAxisSpacing: 12,
        crossAxisSpacing: 12,
        childAspectRatio: 1.6,
      ),
      itemCount: presets.length,
      itemBuilder: (context, index) => _ThemePresetCard(preset: presets[index]),
    );
  }
}

class _ThemePreset {
  final String name;
  final Color color;
  final ProfileImageViewerConfig config;
  final DemoUser user;

  _ThemePreset(this.name, this.color, this.config, this.user);
}

class _ThemePresetCard extends StatelessWidget {
  final _ThemePreset preset;

  const _ThemePresetCard({required this.preset});

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);

    return Card(
      clipBehavior: Clip.antiAlias,
      child: InkWell(
        onTap: () => _open(context),
        child: Padding(
          padding: const EdgeInsets.all(12),
          child: Row(
            children: [
              Hero(
                tag: 'preset-${preset.name}',
                child: CircleAvatar(
                  radius: 28,
                  backgroundImage: NetworkImage(preset.user.image),
                ),
              ),
              const SizedBox(width: 12),
              Expanded(
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      preset.name,
                      style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600),
                    ),
                    const SizedBox(height: 4),
                    Container(
                      width: 40,
                      height: 4,
                      decoration: BoxDecoration(
                        color: preset.color,
                        borderRadius: BorderRadius.circular(2),
                      ),
                    ),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  void _open(BuildContext context) {
    ProfileImageViewer.showWithFade(
      context,
      imageUrl: preset.user.image,
      title: preset.user.name,
      subtitle: '${preset.name} Theme',
      heroTag: 'preset-${preset.name}',
      config: preset.config,
    );
  }
}

// Features List
class _FeaturesList extends StatelessWidget {
  final List<DemoUser> users;
  final bool isDark;

  const _FeaturesList({required this.users, required this.isDark});

  @override
  Widget build(BuildContext context) {
    final features = [
      _Feature(
        icon: Icons.zoom_in,
        title: 'Zoom Indicator',
        subtitle: 'Shows zoom percentage',
        user: users[0],
        config: const ProfileImageViewerConfig(
          showZoomIndicator: true,
          enableDoubleTapZoom: true,
          doubleTapZoomToPoint: true,
        ),
      ),
      _Feature(
        icon: Icons.rotate_right,
        title: 'Rotation',
        subtitle: 'Rotate with button or gesture',
        user: users[1],
        config: const ProfileImageViewerConfig(
          enableRotation: true,
          showRotationResetButton: true,
        ),
      ),
      _Feature(
        icon: Icons.blur_on,
        title: 'Blur Background',
        subtitle: 'Blurred image as background',
        user: users[2],
        config: const ProfileImageViewerConfig(
          useBlurBackground: true,
          showAppBarGradient: true,
        ),
      ),
      _Feature(
        icon: Icons.menu,
        title: 'Context Menu',
        subtitle: 'Long-press for options',
        user: users[3],
        config: const ProfileImageViewerConfig(
          enableContextMenu: true,
          showImageInfo: true,
        ),
        hasActions: true,
      ),
      _Feature(
        icon: Icons.auto_awesome,
        title: 'Shimmer Loading',
        subtitle: 'Animated loading effect',
        user: users[4],
        config: const ProfileImageViewerConfig(
          useShimmerLoading: true,
        ),
      ),
      _Feature(
        icon: Icons.analytics_outlined,
        title: 'Analytics',
        subtitle: 'Track user interactions',
        user: users[0],
        config: null, // Special handling
        isAnalytics: true,
      ),
      _Feature(
        icon: Icons.person_off_outlined,
        title: 'Empty Placeholder',
        subtitle: 'No image available state',
        user: users[0],
        config: null, // Special handling
        isEmpty: true,
      ),
    ];

    return Column(
      children: features.map((f) => _FeatureCard(feature: f, isDark: isDark)).toList(),
    );
  }
}

class _Feature {
  final IconData icon;
  final String title;
  final String subtitle;
  final DemoUser user;
  final ProfileImageViewerConfig? config;
  final bool hasActions;
  final bool isAnalytics;
  final bool isEmpty;

  _Feature({
    required this.icon,
    required this.title,
    required this.subtitle,
    required this.user,
    required this.config,
    this.hasActions = false,
    this.isAnalytics = false,
    this.isEmpty = false,
  });
}

class _FeatureCard extends StatelessWidget {
  final _Feature feature;
  final bool isDark;

  const _FeatureCard({required this.feature, required this.isDark});

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);

    return Card(
      margin: const EdgeInsets.only(bottom: 8),
      child: ListTile(
        leading: Container(
          width: 44,
          height: 44,
          decoration: BoxDecoration(
            color: theme.colorScheme.primaryContainer,
            borderRadius: BorderRadius.circular(12),
          ),
          child: Icon(feature.icon, color: theme.colorScheme.primary),
        ),
        title: Text(feature.title, style: const TextStyle(fontWeight: FontWeight.w500)),
        subtitle: Text(feature.subtitle),
        trailing: Hero(
          tag: 'feature-${feature.title}',
          child: feature.isEmpty
              ? CircleAvatar(
                  radius: 22,
                  backgroundColor: theme.colorScheme.surfaceContainerHighest,
                  child: Icon(Icons.person, color: theme.colorScheme.onSurfaceVariant),
                )
              : CircleAvatar(
                  radius: 22,
                  backgroundImage: NetworkImage(feature.user.image),
                ),
        ),
        onTap: () => _open(context),
      ),
    );
  }

  void _open(BuildContext context) {
    if (feature.isAnalytics) {
      _openWithAnalytics(context);
      return;
    }

    if (feature.isEmpty) {
      _openEmpty(context);
      return;
    }

    ProfileImageViewer.showWithFade(
      context,
      imageUrl: feature.user.image,
      title: feature.user.name,
      subtitle: feature.title,
      heroTag: 'feature-${feature.title}',
      config: feature.config ?? const ProfileImageViewerConfig(),
      onSaveTap: feature.hasActions ? () => _showSnackBar(context, 'Save tapped') : null,
      onEditTap: feature.hasActions ? () => _showSnackBar(context, 'Edit tapped') : null,
      onShareTap: feature.hasActions ? () => _showSnackBar(context, 'Share tapped') : null,
    );
  }

  void _openEmpty(BuildContext context) {
    ProfileImageViewer.showWithFade(
      context,
      imageUrl: null,
      title: 'No Photo',
      subtitle: 'Placeholder Demo',
      heroTag: 'feature-${feature.title}',
      config: const ProfileImageViewerConfig(
        placeholderIcon: Icons.person,
        placeholderSizeRatio: 0.5,
      ),
    );
  }

  void _openWithAnalytics(BuildContext context) async {
    final result = await ProfileImageViewer.showWithFade(
      context,
      imageUrl: feature.user.image,
      title: feature.user.name,
      subtitle: 'Analytics Demo',
      heroTag: 'feature-${feature.title}',
      config: ProfileImageViewerConfig(
        showZoomIndicator: true,
        enableDoubleTapZoom: true,
        onAnalyticsEvent: (event, data) {
          debugPrint('📊 $event: $data');
        },
      ),
    );

    if (result != null && context.mounted) {
      final duration = (result.viewDurationMs ?? 0) / 1000;
      final zoom = result.maxZoomReached?.toStringAsFixed(1) ?? '1.0';
      _showSnackBar(context, 'Viewed ${duration.toStringAsFixed(1)}s • Max zoom: ${zoom}x');
    }
  }

  void _showSnackBar(BuildContext context, String message) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text(message), behavior: SnackBarBehavior.floating),
    );
  }
}
1
likes
160
points
177
downloads

Publisher

unverified uploader

Weekly Downloads

A customizable, WhatsApp-style profile image viewer with pinch-to-zoom, swipe-to-dismiss, Hero animations, gallery mode, screenshot protection, and 6+ theme presets.

Repository (GitHub)
View/report issues

Topics

#image-viewer #gallery #photo-view #profile #zoom

Documentation

API reference

License

MIT (license)

Dependencies

cached_network_image, flutter, photo_view, screen_protector

More

Packages that depend on profile_image_viewer