1

I would like to create a widget that behaves same way as the horizontal scrolling lists (for example for recommended apps) in google play app.

An example of what I mean:

enter image description here

I have tried to use the Page Viewer widget but it does not quite do same thing. Some noticiable facts:

1) 3 perfectly squared images are fully seen and another one is partially shown. The second one (the greenish one) is right in the center.

2) The left most image (the blue one) is perfectly aligned with the Caption "Recommended for you"

3) All 4 squared images have rounded corners.

4) It supports item snapping

This is the best I have been able to mimic this behaviour with the PageView widget:

enter image description here

  • Red is the background color of the container containing the pageview.
  • Green is the color for even items and Blue for odd items
  • Inside every item an image of 100x100 is added.

This is the code:

  return Container(
      width: double.infinity,
      color: Colors.red,
      height: 100,
      child:
          PageView(
              pageSnapping: true,
              controller:
                  PageController(initialPage: 1, viewportFraction: 0.315),
              children: List<Widget>.generate(10, (index) {
                return Container(
                  color: index%2 ==0 ? Colors.green : Colors.blue,
                  child:Image.network("http://via.placeholder.com/100x100",
                      fit: BoxFit.fitHeight)
                );
              })));

Width my code there are 2 things that won't work:

1) I can't make appear a partially seen item from the right side keeping the 3 fully visible items and the second perfectly centered as in the original image form google play, just by using viewportFraction.

2) I can't apply rounded corners to the item images because as you see the container is not squared but the image is. So when I apply ClipRRect it is not applied correctly. I have tried to add another container forcing its size to 100x100 and apply ClipRRect on the image inside this container but when it is inside a PageView it seems the width/height given have no effect. They seem controlled internally by PageView.

So, Could anybody help on this problems to get something with the same behaviour as the google play horizontal scrollable list?

Cheers!

Notbad
  • 5,936
  • 12
  • 54
  • 100

2 Answers2

1
class SO extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    double edge = 120.0;
    double padding = edge / 10.0;
    return Scaffold(
      appBar: AppBar(),
      body: Container(
        color: Colors.red,
        padding: const EdgeInsets.symmetric(vertical: 8),
        child: SingleChildScrollView(
          scrollDirection: Axis.horizontal,
          child: Row(
            children: [
              for (int i = 0; i < 10; i++)
                Column(
                  mainAxisSize: MainAxisSize.min,
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Padding(
                      padding: EdgeInsets.all(padding),
                      child: ClipRRect(
                        borderRadius: BorderRadius.all(Radius.circular(edge * .2)),
                        child: Container(
                          width: edge,
                          height: edge,
                          color: Colors.blue,
                          child: Image.network("http://via.placeholder.com/${edge.round()}x${edge.round()}", fit: BoxFit.fitHeight),
                        ),
                      ),
                    ),
                    Container(
                      width: edge + padding,
                      padding: EdgeInsets.only(left: padding),
                      child: Text(
                        'foo app bar baz app apk',
                        maxLines: 2,
                        overflow: TextOverflow.ellipsis,
                      ),
                    ),
                    Padding(
                      padding: EdgeInsets.only(left: padding),
                      child: Row(
                        mainAxisSize: MainAxisSize.min,
                        children: <Widget>[
                          Text('4.2'),
                          Icon(
                            Icons.star,
                            size: 16,
                          )
                        ],
                      ),
                    ),
                  ],
                )
            ],
          ),
        ),
      ),
    );
  }
}

which gives

screenshot

Doc
  • 10,831
  • 3
  • 39
  • 63
  • Yes, but... This doesn't support snapping for example. One of my first thoughts when I was implementing this was that I chose the wrong widget and was going to use a horizontal list for example but snapping does not work for this kind of controls. – Notbad Dec 21 '19 at 23:04
1

You can modify PageScrollPhysics to create the snapping effect.

This what I did,

import 'package:flutter/material.dart';

Future<void> main() async {
  runApp(
    MaterialApp(
      home: new Main(),
    ),
  );
}

class Main extends StatefulWidget {
  @override
  _MainState createState() => _MainState();
}

class _MainState extends State<Main> {
  static const _scrollPhysics =
      const ExtentScrollPhysics(itemExtent: 80, separatorSpacing: 10);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Demo"),
      ),
      body: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 20.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: <Widget>[
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: <Widget>[
                Text(
                  "Recommended for you",
                  style: const TextStyle(fontWeight: FontWeight.bold),
                ),
                IconButton(
                  icon: Icon(Icons.arrow_forward),
                  onPressed: () {},
                )
              ],
            ),
            SizedBox.fromSize(
              size: Size.fromHeight(130),
              child: ListView.separated(
                scrollDirection: Axis.horizontal,
                itemCount: 30,
                physics: _scrollPhysics,
                separatorBuilder: (context, _) =>
                    SizedBox(width: _scrollPhysics.dividerSpacing),
                itemBuilder: (context, index) {
                  return SizedBox(
                    width: _scrollPhysics.itemExtent, // set height for vertical
                    child: CardItem(),
                  );
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class CardItem extends StatefulWidget {
  @override
  _CardItemState createState() => _CardItemState();
}

class _CardItemState extends State<CardItem> {
  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        AspectRatio(
          aspectRatio: 1,
          child: Container(
            decoration: BoxDecoration(
              color: Colors.blue,
              borderRadius: BorderRadius.circular(20.0),
            ),
          ),
        ),
        const SizedBox(height: 8.0),
        Text(
          "Tile Name",
          overflow: TextOverflow.ellipsis,
          maxLines: 2,
          style: const TextStyle(fontSize: 12.0),
        ),
        Text(
          "1 MB",
          style: const TextStyle(
            fontSize: 12.0,
            color: Colors.black54,
          ),
        ),
      ],
    );
  }
}

class ExtentScrollPhysics extends ScrollPhysics {
  final double itemExtent;
  final double dividerSpacing;

  const ExtentScrollPhysics(
      {ScrollPhysics parent, this.itemExtent, double separatorSpacing})
      : assert(itemExtent != null && itemExtent > 0),
        dividerSpacing = separatorSpacing ?? 0,
        super(parent: parent);

  @override
  ExtentScrollPhysics applyTo(ScrollPhysics ancestor) {
    return ExtentScrollPhysics(
      parent: buildParent(ancestor),
      itemExtent: itemExtent,
      separatorSpacing: dividerSpacing,
    );
  }

  double _getItem(ScrollPosition position) {
    return position.pixels / (itemExtent + dividerSpacing);
  }

  double _getPixels(ScrollPosition position, double item) {
    return item * (itemExtent + dividerSpacing);
  }

  double _getTargetPixels(
      ScrollPosition position, Tolerance tolerance, double velocity) {
    double page = _getItem(position);
    if (velocity < -tolerance.velocity)
      page -= 0.5;
    else if (velocity > tolerance.velocity) page += 0.5;
    return _getPixels(position, page.roundToDouble());
  }

  @override
  Simulation createBallisticSimulation(
      ScrollMetrics position, double velocity) {
    // If we're out of range and not headed back in range, defer to the parent
    // ballistics, which should put us back in range at a page boundary.
    if ((velocity <= 0.0 && position.pixels <= position.minScrollExtent) ||
        (velocity >= 0.0 && position.pixels >= position.maxScrollExtent))
      return super.createBallisticSimulation(position, velocity);

    final Tolerance tolerance = this.tolerance;
    final double target = _getTargetPixels(position, tolerance, velocity);
    if (target != position.pixels)
      return ScrollSpringSimulation(spring, position.pixels, target, velocity,
          tolerance: tolerance);
    return null;
  }

  @override
  bool get allowImplicitScrolling => false;
}
Crazy Lazy Cat
  • 13,595
  • 4
  • 30
  • 54