'ListView widget not entirely visible when scaled

When I scale a horizontal ListView widget, I observe that only a portion of the list items are visible when the widget is scrolled all the way to the right:

import 'dart:ui';

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final sideLength = 50.0;
    final scale = 2.0;

    return MaterialApp(
        scrollBehavior: MyCustomScrollBehavior(),
        home: Scaffold(
            body: Transform.scale(
          scale: scale,
          alignment: Alignment(-1, -1),
          child: Container(
              height: sideLength * scale,
              child: ListView.builder(
                itemCount: 20,
                scrollDirection: Axis.horizontal,
                itemBuilder: (context, index) => Container(
                    width: sideLength,
                    height: sideLength,
                    child: Text(index.toString()),
                    decoration: BoxDecoration(
                        border: Border.all(width: 3, color: Colors.red))),
              )),
        )));
  }
}

class MyCustomScrollBehavior extends MaterialScrollBehavior {
  // Override behavior methods and getters like dragDevices
  @override
  Set<PointerDeviceKind> get dragDevices => {
        PointerDeviceKind.touch,
        PointerDeviceKind.mouse,
      };
}

On the Pixel 2 emulator, only the first 16 items are visible when I scroll to extreme right. For example:

enter image description here

When the scale is 1 or if the Transform.scale widget is not there, all 20 items are visible.

I observe the following behavior:

Total item count Last item scrollable to
8 4
10 6
20 16
30 26
50 46

So it seems like the last 4 items are always left out.

Ultimately my goal is to create a responsive widget that scales according to the dimensions of screen, so I'm looking for a generic solution.

The custom scroll behavior is only there so that horizontal scrolling works on dartpad.dev, as per this answer.



Solution 1:[1]

I worked around this issue by:

  • setting itemCount to a higher value than the desired item count. This allows you to scroll to the last desired item in the ListView
  • having a scroll controller that checks whether you're past the last visible desired item within the viewport. If so, jump to the last possible scroll offset within the viewport
import 'dart:ui';

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  final ScrollController _scrollController = ScrollController();
  static const actualTotalItems = 20;
  static const sideLength = 50.0;
  static const scale = 2.0;

  MyApp() {
    _scrollController.addListener(() {
      if (_scrollController.offset > sideLength * actualTotalItems - _scrollController.position.viewportDimension / scale) {
        _scrollController.jumpTo(sideLength * actualTotalItems - _scrollController.position.viewportDimension / scale);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        scrollBehavior: MyCustomScrollBehavior(),
        home: Scaffold(
            body: Container(
                transform: Matrix4.diagonal3Values(scale, scale, 1),
                height: sideLength * scale,
                child: ListView.builder(
                    itemCount: actualTotalItems * 2,
                    scrollDirection: Axis.horizontal,
                    controller: _scrollController,
                    itemBuilder: (context, index) => Container(
                        width: sideLength,
                        height: sideLength,
                        child: Text(index.toString()),
                        decoration: BoxDecoration(border: Border.all(width: 3, color: Colors.red)))))));
  }
}

class MyCustomScrollBehavior extends MaterialScrollBehavior {
  // Override behavior methods and getters like dragDevices
  @override
  Set<PointerDeviceKind> get dragDevices => {
        PointerDeviceKind.touch,
        PointerDeviceKind.mouse,
      };
}

I scaled the widget using the transform property in the Container to have a smaller widget tree, but that has no impact on the solution. The Transform widget could have been used as in the OP.

Solution 2:[2]

Transform only affects how the child should be painted.

On the other hand, The scale argument multiplies the x and y axes.

For example: Imagine a photographer who is taking a landscape photo that contains a tree, now if the photographer gets closer to the tree, the upper part of the tree will slightly be out of the photo.

Try adding padding or margin to the Container and observe how the widget is affected by the scale. Then you will know how to manipulate it.

body: Transform.scale(
              scale: scale,
              alignment: Alignment(0, -5),
              child: Container(
                  height: sideLength * scale,
                  margin: EdgeInsets.only(left: 100, right: 100),
                  child: ListView.builder(

Solution 3:[3]

Give here width and height by MediaQuery

Widget build(BuildContext context) {

return MaterialApp(
    scrollBehavior: MyCustomScrollBehavior(),
    home: Scaffold(
        body: Transform.scale(
      scale: scale,
      alignment: Alignment(-1, -1),
      child: Container(
          height: sideLength * scale,
          child: ListView.builder(
            itemCount: 20,
            scrollDirection: Axis.horizontal,
            itemBuilder: (context, index) => Container(
                width: MediaQuery.of(context).size.width*0.90 ,
                height: MediaQuery.of(context).size.height*0.80 ,
                child: Text(index.toString()),
                decoration: BoxDecoration(
                    border: Border.all(width: 3, color: Colors.red))),
          )),
    )));

}

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
Solution 2 Mhmd Zawi
Solution 3 Dharman