I want to create the Netflix home page in flutter, and I'm stuck while creating this hover state.
I have created two base widgets. One for the number plus the thumbnail and the other one for the expanded view when the widget is hovered. Then I put them in a stack
with an Inkwell
where the onHover
changes the state to show the expanded widget.
When I hover on the widget, it does switch between the normal state an expanded state, the problem comes when I try to put a list of these widgets together.
- When using
row
(or ListView) to put them together, after hovering, the expanded widget makes the other widgets move. (which is not the wanted behaviour, I want them to overlap)
- When I use it with
stack
, the widgets do overlap but now it isn't scrollable anymore.
I have added the link to the repo for anyone that wants to clone it and try running it themselves, I'm running it on flutter web.https://github.com/Advait1306/netflix-flutter
Widget with thumbnail and number:
class TopListItem extends StatelessWidget { final int index; const TopListItem({Key? key, required this.index}) : super(key: key); @override Widget build(BuildContext context) { const double height = 250; return SizedBox( height: height, child: Row( mainAxisSize: MainAxisSize.min, children: [ SvgPicture.asset("assets/numbers/$index.svg", fit: BoxFit.fitHeight, height: height), Transform.translate( offset: const Offset(-30, 0), child: Image.asset("assets/thumbnails/thumb1.jpg")) ], ), ); }}
Expanded view widget:
import 'package:flutter/material.dart';class HoverMovieTrailer extends StatelessWidget { const HoverMovieTrailer({Key? key}) : super(key: key); @override Widget build(BuildContext context) { const textTheme = TextStyle(color: Colors.white); return SizedBox( width: 400, height: 400, child: Container( clipBehavior: Clip.hardEdge, decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), color: const Color(0xFF242424)), child: Column( children: [ Image.asset("assets/backgrounds/background1.jpg"), const SizedBox( height: 20, ), Padding( padding: const EdgeInsets.symmetric(horizontal: 18), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: const [ RoundIconButton(icon: Icons.play_arrow_outlined), SizedBox(width: 5), RoundIconButton(icon: Icons.add_outlined), SizedBox(width: 5), RoundIconButton(icon: Icons.thumb_up_alt_outlined), SizedBox(width: 5), ], ), Row( children: const [ RoundIconButton(icon: Icons.keyboard_arrow_down_outlined), ], ), ], ), ), const SizedBox( height: 20, ), Padding( padding: const EdgeInsets.symmetric(horizontal: 18), child: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ const Text("98% Match", style: TextStyle( color: Colors.green, fontWeight: FontWeight.bold ), ), const SizedBox(width: 5), Container( padding: const EdgeInsets.all(1), decoration: BoxDecoration( border: Border.all(color: Colors.white, width: 1) ), child: const Text("18+", style: textTheme, ), ), const SizedBox(width: 5), const Text("4 Seasons", style: textTheme, ), const SizedBox(width: 5), Container( decoration: BoxDecoration( border: Border.all(color: Colors.white, width: 1) ), child: const Text("HD", style: textTheme, ), ) ], ), ), const SizedBox( height: 5, ), Padding( padding: const EdgeInsets.all(18.0), child: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ const Text("Captivating", style: textTheme, ), const SizedBox(width: 5), Container( width: 5, height: 5, decoration: const BoxDecoration( shape: BoxShape.circle, color: Colors.white54 ), ), const SizedBox(width: 5), const Text("Exciting", style: textTheme, ), const SizedBox(width: 5), Container( width: 5, height: 5, decoration: const BoxDecoration( shape: BoxShape.circle, color: Colors.white54 ), ), const SizedBox(width: 5), const Text("Docuseries", style: textTheme, ), ], ), ), ], ), ), ); }}class RoundIconButton extends StatelessWidget { final IconData icon; const RoundIconButton({Key? key, required this.icon}) : super(key: key); @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.transparent, border: Border.all(width: 2, color: Colors.white)), margin: const EdgeInsets.all(1), child: IconButton( onPressed: () {}, icon: Icon(icon), color: Colors.white, ), ); }}
Combining the widgets in the single widget:
import 'dart:developer';import 'package:flutter/material.dart';import 'package:netflix_flutter/widgets/hover_movie_trailer.dart';import 'package:netflix_flutter/widgets/top_list_item.dart';class TopListItemWithHover extends StatefulWidget { const TopListItemWithHover({Key? key}) : super(key: key); @override State<TopListItemWithHover> createState() => _TopListItemWithHoverState();}class _TopListItemWithHoverState extends State<TopListItemWithHover> { bool hover = false; @override Widget build(BuildContext context) { return InkWell( onTap: (){}, onHover: (value){ log("Hover value: $value"); setState(() { hover = value; }); }, child: Stack( clipBehavior: Clip.none, children: [ TopListItem(index: 1), if(hover) HoverMovieTrailer(), ], ), ); }}
Lists:
import 'package:flutter/material.dart';import 'package:netflix_flutter/widgets/hover_movie_trailer.dart';import 'package:netflix_flutter/widgets/top_list_item.dart';import 'package:netflix_flutter/widgets/top_list_item_with_hover.dart';void main() { runApp(const MyApp());}class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(title: 'Flutter Demo Home Page'), ); }}class MyHomePage extends StatefulWidget { const MyHomePage({Key? key, required this.title}) : super(key: key); final String title; @override State<MyHomePage> createState() => _MyHomePageState();}class _MyHomePageState extends State<MyHomePage> { @override Widget build(BuildContext context) { return Scaffold( body: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ SingleChildScrollView( scrollDirection: Axis.horizontal, child: SizedBox( height: 400, child: ListView.builder( shrinkWrap: true, scrollDirection: Axis.horizontal, clipBehavior: Clip.none, itemCount: 8, itemBuilder: (context, index) { return TopListItemWithHover(); }, ), ), ), const SizedBox(height: 50), SingleChildScrollView( child: SizedBox( height: 400, child: Stack( fit: StackFit.passthrough, children: [ for (var i = 10; i >= 0; i--) Positioned( left: (i) * 300, child: TopListItemWithHover(), ) ], ), ), ) ], ), ); }}