local_hero_transform 1.0.6 copy "local_hero_transform: ^1.0.6" to clipboard
local_hero_transform: ^1.0.6 copied to clipboard

local hero transform is a Flutter package that simplifies seamless transitions between items in grid and list views using local hero animations, enhancing your app's visual appeal

example/lib/main.dart

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

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

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

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final _themeNotifier = ValueNotifier(ThemeMode.light);

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

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder<ThemeMode>(
      valueListenable: _themeNotifier,
      builder: (_, themeMode, __) => MaterialApp(
        title: 'Flutter Demo',
        debugShowCheckedModeBanner: false,
        theme: ThemeData.light().copyWith(
          scaffoldBackgroundColor: AppColors.backgroundColor,
        ),
        darkTheme: ThemeData.dark().copyWith(
          scaffoldBackgroundColor: AppColors.darkBackgroundColor,
        ),
        themeMode: themeMode,
        home: MyHomePage(themeNotifier: _themeNotifier),
      ),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.themeNotifier});
  final ValueNotifier<ThemeMode> themeNotifier;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
  late final TabController _tabController;
  late final ValueNotifier<FavoriteShape> _viewModeNotifier;
  late final ValueNotifier<TextDirection> _languageNotifier;
  late final ValueNotifier<bool> _loadingNotifier;

  @override
  void initState() {
    super.initState();
    _viewModeNotifier = ValueNotifier(FavoriteShape.grid);
    _languageNotifier = ValueNotifier(TextDirection.ltr);
    _loadingNotifier = ValueNotifier(false);
    _tabController = TabController(length: 2, vsync: this)..addListener(_handleTabChange);
  }

  void _handleTabChange() {
    if (!_tabController.indexIsChanging) {
      _viewModeNotifier.value = _tabController.index == 0 ? FavoriteShape.grid : FavoriteShape.list;
    }
  }

  @override
  void dispose() {
    _tabController
      ..removeListener(_handleTabChange)
      ..dispose();
    _viewModeNotifier.dispose();
    _languageNotifier.dispose();
    _loadingNotifier.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: _buildAppBar(context),
      body: _buildBody(),
    );
  }

  Widget _buildBody() {
    return ValueListenableBuilder<TextDirection>(
      valueListenable: _languageNotifier,
      builder: (context, textDirection, _) {
        return Directionality(
          textDirection: textDirection,
          child: ValueListenableBuilder<bool>(
              valueListenable: _loadingNotifier,
              builder: (context, _, child) {
                return LocalHeroViews(
                  tabController: _tabController,
                  onPressedCard: _handleCardPressed,
                  textDirection: textDirection,
                  itemCount: locations.length,
                  itemsModel: _buildItemsModel,
                );
              }),
        );
      },
    );
  }

  void _handleCardPressed(int index) {
    final location = locations[index];
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (_) => DetailsScreen(name: location.name, imageUrl: location.imageUrl),
      ),
    );
  }

  ItemsModel _buildItemsModel(int index) {
    final isDarkMode = widget.themeNotifier.value == ThemeMode.dark;
    final location = locations[index];
    final textTheme = _buildTextTheme();

    return ItemsModel(
      cardStyleMode: CardStyleMode(
        isDarkMode: isDarkMode,
        isLoading: _loadingNotifier.value,

        ///Todo: Uncomment the following lines if you want to use custom colors
        // cardColor: isDarkMode ? AppColors.darkCardColor : Colors.white,
        // itemColor: isDarkMode ? AppColors.darkItemColor : AppColors.lightItemColor,
        // animationShimmerColor: isDarkMode ? AppColors.darkShimmerColor : Colors.grey[300]!,
      ),
      loadingImageBuilder: (context, child, loadingProgress) {
        return CustomShimmer(isDark: widget.themeNotifier.value == ThemeMode.dark);
      },
      image: DecorationImage(image: NetworkImage(location.imageUrl), fit: BoxFit.cover),
      name: Text(location.name, style: textTheme.name),
      title: Text(location.place, style: textTheme.title),
      subTitle: Text(location.subtitle, style: textTheme.subTitle),
      subTitleIcon: Icon(
        Icons.location_on_outlined,
        color: AppColors.subtitleColor,
        size: MediaQuery.sizeOf(context).width * 0.03,
      ),
      favoriteIconButton: _buildFavoriteButton(),
    );
  }

  _TextTheme _buildTextTheme() {
    return _TextTheme(
      name: TextStyle(
        color: widget.themeNotifier.value == ThemeMode.dark ? Colors.white : Colors.black,
        fontSize: 16,
        fontWeight: FontWeight.w500,
      ),
      title: TextStyle(
        color: Colors.blue,
        fontSize: 14,
        fontWeight: FontWeight.w500,
      ),
      subTitle: TextStyle(
        color: AppColors.subtitleColor,
        fontSize: 13,
        fontWeight: FontWeight.w400,
      ),
    );
  }

  Widget _buildFavoriteButton() {
    bool isFavored = true;
    return StatefulBuilder(
      builder: (context, setState) {
        return IconButton(
          style: ButtonStyle(
            backgroundColor: WidgetStateProperty.resolveWith<Color>(
              (states) => isFavored ? Colors.redAccent : Colors.grey,
            ),
          ),
          icon: Icon(
            Icons.favorite,
            color: Colors.white,
            size: MediaQuery.sizeOf(context).width * 0.06,
          ),
          onPressed: () => setState(() => isFavored = !isFavored),
        );
      },
    );
  }

  AppBar _buildAppBar(BuildContext context) {
    return AppBar(
      backgroundColor: Theme.of(context).scaffoldBackgroundColor,
      surfaceTintColor: Theme.of(context).scaffoldBackgroundColor,
      actions: [
        _buildLanguageButtons(),
        const Spacer(),
        Padding(
          padding: const EdgeInsets.symmetric(horizontal: 20),
          child: _buildViewToggleButton(),
        ),
      ],
    );
  }

  Widget _buildLanguageButtons() {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 20),
      child: Row(
        children: [
          _buildLanguageButton(TextDirection.rtl, 'πŸ‡ΈπŸ‡¦'),
          const SizedBox(width: 8),
          _buildLanguageButton(TextDirection.ltr, 'πŸ‡ΊπŸ‡Έ'),
          const SizedBox(width: 8),
          _buildThemeToggleButton(),
          const SizedBox(width: 8),
          _buildLoadingButton()
        ],
      ),
    );
  }

  IconButton _buildLanguageButton(TextDirection direction, String emoji) {
    return IconButton(
      style: _iconButtonStyle,
      onPressed: () => _languageNotifier.value = direction,
      icon: Text(emoji),
    );
  }

  final _iconButtonStyle = ButtonStyle(
    backgroundColor: WidgetStatePropertyAll(Colors.grey.withValues(alpha: 0.3)),
  );
  Widget _buildThemeToggleButton() {
    return ValueListenableBuilder<ThemeMode>(
      valueListenable: widget.themeNotifier,
      builder: (context, themeMode, _) {
        bool isDarkMode = themeMode == ThemeMode.dark;
        return IconButton(
          style: _iconButtonStyle,
          icon: Icon(
            isDarkMode ? Icons.light_mode : Icons.dark_mode_outlined,
            color: isDarkMode ? Colors.white : Colors.grey,
          ),
          onPressed: () => widget.themeNotifier.value =
              themeMode == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark,
        );
      },
    );
  }

  Widget _buildLoadingButton() {
    return ValueListenableBuilder<bool>(
      valueListenable: _loadingNotifier,
      builder: (context, isLoading, _) {
        return IconButton(
            style: _iconButtonStyle,
            icon: Icon(
              isLoading ? Icons.hourglass_bottom : Icons.hourglass_empty,
              color: isLoading ? Colors.black38 : Colors.grey,
            ),
            onPressed: () => _loadingNotifier.value = !_loadingNotifier.value);
      },
    );
  }

  Widget _buildViewToggleButton() {
    return ValueListenableBuilder<FavoriteShape>(
      valueListenable: _viewModeNotifier,
      builder: (context, value, _) => ConstrainedBox(
        constraints: BoxConstraints.tight(Size(35, 35)),
        child: AspectRatio(
          aspectRatio: 1.9 / 2,
          child: RawMaterialButton(
            onPressed: _toggleView,
            elevation: 0,
            visualDensity: const VisualDensity(vertical: -4, horizontal: -4),
            shape: _buttonShape,
            fillColor: Colors.blue,
            child: Icon(
              value == FavoriteShape.grid ? Icons.grid_view_rounded : Icons.view_agenda_outlined,
              size: 16,
              color: Colors.white,
            ),
          ),
        ),
      ),
    );
  }

  final _buttonShape = RoundedRectangleBorder(
    side: const BorderSide(color: Colors.black, width: 0.2),
    borderRadius: BorderRadius.circular(5),
  );

  void _toggleView() {
    final newIndex = _tabController.index == 0 ? 1 : 0;
    _tabController.animateTo(newIndex);
  }
}

class _TextTheme {
  final TextStyle name;
  final TextStyle title;
  final TextStyle subTitle;

  const _TextTheme({
    required this.name,
    required this.title,
    required this.subTitle,
  });
}

class AppColors {
  static const backgroundColor = Color(0xFFF2F3F8);
  static const darkBackgroundColor = Color(0xff10122C);
  static const darkCardColor = Color.fromARGB(255, 36, 39, 92);
  static const darkItemColor = Color.fromARGB(255, 13, 13, 29);
  static const darkShimmerColor = Color.fromARGB(255, 36, 39, 92);
  static const lightItemColor = Color(0xFFEEEEEE);
  static const subtitleColor = Color(0xFF95979A);
}

enum FavoriteShape { grid, list }

class DetailsScreen extends StatelessWidget {
  const DetailsScreen({
    super.key,
    required this.name,
    required this.imageUrl,
  });

  final String name;
  final String imageUrl;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.indigoAccent,
        iconTheme: IconThemeData(color: Colors.white),
        title: Text(name, style: TextStyle(color: Colors.white)),
      ),
      body: Center(
        child: AspectRatio(
          aspectRatio: 16 / 14,
          child: Image.network(imageUrl, fit: BoxFit.cover),
        ),
      ),
    );
  }
}

class Location {
  final String name;
  final String place;
  final String imageUrl;
  final String subtitle;

  const Location({
    required this.name,
    required this.place,
    required this.imageUrl,
    required this.subtitle,
  });
}

const urlPrefix = 'https://docs.flutter.dev/cookbook/img-files/effects/parallax';

const locations = [
  Location(
    name: 'Mount ',
    place: 'U.S.A',
    imageUrl: '$urlPrefix/01-mount-rushmore.jpg',
    subtitle: 'Presidential monument',
  ),
  Location(
    name: 'Gardens ',
    place: 'Singapore',
    imageUrl: '$urlPrefix/02-singapore.jpg',
    subtitle: 'Futuristic gardens',
  ),
  Location(
    name: 'Machu Picchu',
    place: 'Peru',
    imageUrl: '$urlPrefix/03-machu-picchu.jpg',
    subtitle: 'Ancient Inca city',
  ),
  Location(
    name: 'Vitznau',
    place: 'Switzerland',
    imageUrl: '$urlPrefix/04-vitznau.jpg',
    subtitle: 'Lakeside village',
  ),
  Location(
    name: 'Bali',
    place: 'Indonesia',
    imageUrl: '$urlPrefix/05-bali.jpg',
    subtitle: 'Tropical paradise',
  ),
  Location(
    name: 'Mexico City',
    place: 'Mexico',
    imageUrl: '$urlPrefix/06-mexico-city.jpg',
    subtitle: 'Vibrant capital',
  ),
  Location(
    name: 'Cairo',
    place: 'Egypt',
    imageUrl: '$urlPrefix/07-cairo.jpg',
    subtitle: 'Pyramids city',
  ),
  Location(
    name: 'Yemen',
    place: "Sana'a",
    imageUrl: '$urlPrefix/07-cairo.jpg',
    subtitle: 'Ancient architecture',
  ),
];
183
likes
150
points
277
downloads

Publisher

unverified uploader

Weekly Downloads

local hero transform is a Flutter package that simplifies seamless transitions between items in grid and list views using local hero animations, enhancing your app's visual appeal

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

flutter, flutter_screenutil

More

Packages that depend on local_hero_transform