'How to expand list items without affecting other items. (flutter)

I want to create the Netflix home page in flutter, and I'm stuck while creating this hover state.

enter image description here

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.

  1. 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)

enter image description here

  1. When I use it with stack, the widgets do overlap but now it isn't scrollable anymore.

enter image description here

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(),
                    )
                ],
              ),
            ),
          )
        ],
      ),
    );
  }
}


Solution 1:[1]

So finally found the solution to this problem, the way to move forward is to use a stack in SingleChildScrollView.

A mistake that I made is, I did not set the SingleChildScrollView's direction to horizontal. So I added that.

And then one more addition that's needed is -- A empty sized box which has the width of sum of all the items in the stack.

          Stack(
            clipBehavior: Clip.none,
            children: [
              const SizedBox(
                width: 300*10,
              ),
              for (var i = 10; i >= 0; i--)
                Positioned(
                  left: (i) * 300,
                  child: TopListItemWithHover(),
                )
            ],
          )

This finally expanded the stack to the required width and then made is scrollable also.

enter image description here

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Advait