1

I have a very long ListView with long text in the ListView item and I don't want the text to wrap automatically. How to modify the code to scroll the entire page horizontally to show the text on the right.

I think this is a very common requirement. For example, a text editor needs to display many long lines of text, and can also scroll to the right when the text is long.

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

void main(List<String> args) {
  runApp(const MyApp());
}

class MyCustomScrollBehavior extends MaterialScrollBehavior {
  @override
  Set<PointerDeviceKind> get dragDevices => {
        PointerDeviceKind.touch,
        PointerDeviceKind.mouse,
      };
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        scrollBehavior: MyCustomScrollBehavior(),
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: const HomePage());
  }
}

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

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ListView example'),
      ),
      body: ListView.builder(
        itemExtent: 50,
        itemCount: 1000 * 1000 * 1000,
        itemBuilder: (context, index) {
          return Container(
            padding: const EdgeInsets.all(10),
            color: index % 2 == 0 ? Colors.white : const Color(0xFFEFEFEF),
            child: Text(
              "$index very long long long long long long long long long"
              "long long long long long long long long long long long"
              "long long long long long long long long long long long"
              "long long long long long long long long long long long"
              "long long long long long long long long long long text",
              softWrap: false,
            ),
          );
        },
      ),
    );
  }
}

enter image description here

Although I can add a SingleChildScrollView to the outer layer, it seems that children in SingleChildScrollView must specify the width, it cannot automatically adjust based on the current text length, and the scroll bar will run to the far right. How to better achieve horizontal scrolling of ListView pages.

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ListView example'),
      ),
      body: SingleChildScrollView(
        scrollDirection: Axis.horizontal,
        child: SizedBox(
          width: 2000,
          child: ListView.builder(
            itemExtent: 50,
            itemCount: 1000 * 1000 * 1000,
            itemBuilder: (context, index) {
              return Container(
                padding: const EdgeInsets.all(10),
                color: index % 2 == 0 ? Colors.white : const Color(0xFFEFEFEF),
                child: Text(
                  "$index very long long long long long long long long long"
                  "long long long long long long long long long long long"
                  "long long long long long long long long long long long"
                  "long long long long long long long long long long long"
                  "long long long long long long long long long long text",
                  softWrap: false,
                ),
              );
            },
          ),
        ),
      ),
    );
  }
}

Or add SingleChildScrollView to each item, but only a single line can be scrolled, which does not achieve the desired effect.

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ListView example'),
      ),
      body: ListView.builder(
        itemExtent: 50,
        itemCount: 1000 * 1000 * 1000,
        itemBuilder: (context, index) {
          return SingleChildScrollView(
            scrollDirection: Axis.horizontal,
            child: Container(
              padding: const EdgeInsets.all(10),
              color: index % 2 == 0 ? Colors.white : const Color(0xFFEFEFEF),
              child: Text(
                "$index very long long long long long long long long long"
                "long long long long long long long long long long long"
                "long long long long long long long long long long long"
                "long long long long long long long long long long long"
                "long long long long long long long long long long text",
                softWrap: false,
              ),
            ),
          );
        },
      ),
    );
  }
}
3
  • 1
    try two_dimensional_scrollables Commented May 29 at 5:14
  • For two_dimensional_scrollables, It not work when rowCount is lareg more than 1000 * 1000 * 1000, if I set rowCount to be infinite, scroll bar will not show. Commented May 29 at 7:21
  • 1
    Haven't tried for text, you can use CustomPaint or RenderWidget this is the lowest level ik, but two_dimensional_scrollables already handles rendering mechanism and only render visible items. Once I played with game of life,might give you some idea Commented May 29 at 8:14

2 Answers 2

1

I think this is what you're looking for:

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

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ListView example'),
      ),
      body: ListView.builder(
        itemExtent: 50,
        itemCount: 1000 * 1000 * 1000,
        itemBuilder: (context, index) {
          return Container(
            padding: const EdgeInsets.all(10),
            color: index % 2 == 0 ? Colors.white : const Color(0xFFEFEFEF),
            child: SingleChildScrollView(
              scrollDirection: Axis.horizontal,
              child: Text(
                "$index very long long long long long long long long long"
                "long long long long long long long long long long long"
                "long long long long long long long long long long long"
                "long long long long long long long long long long long"
                "long long long long long long long long long long text",
                style: TextStyle(color: Colors.black),
                softWrap: false,
              ),
            ),
          );
        },
      ),
    );
  }
}

In a real application, the Text widgets generated inside ListView.builder might have varying horizontal lengths in each instance. In such cases, requiring the user (if it overlaps the available screen width) to scroll the entire list of Text horizontally isn't ideal. Therefore, the code snippet provided above should be what you're looking for, as it makes the Text horizontally scrollable if its content is too long to fit the available screen width.

Update: Using InteractiveViewer class

import 'package:flutter/material.dart';

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

  @override
  State<XYAxisScrollTest> createState() => _XYAxisScrollTestState();
}

class _XYAxisScrollTestState extends State<XYAxisScrollTest> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ListView example'),
      ),
      body: InteractiveViewer(
        constrained: false, // Allows child to be larger than viewport
        boundaryMargin: EdgeInsets.zero,
        //const EdgeInsets.all(double.infinity), // Allows panning far out
        minScale: 1.0, // Optional: allow zooming
        maxScale: 1.0, // Optional: allow zooming
        child: Column(
          crossAxisAlignment:
              CrossAxisAlignment.start, // For Column, to align text left
          children: List.generate(100, (index) {
            return Container(
              padding: const EdgeInsets.all(10),
              color: index % 2 == 0 ? Colors.white : const Color(0xFFEFEFEF),
              // No specific width needed here, Column will size to widest child
              // if InteractiveViewer.constrained is false.
              child: Text(
                "$index very long long long long long long long long long "
                "long long long long long long long long long long long "
                "long long long long long long long long long long long "
                "long long long long long long long long long long long "
                "long long long long long long long long long long text",
                style: const TextStyle(color: Colors.black),
                softWrap: false,
              ),
            );
          }),
        ),
      ),
    );
  }
}

It behaves differently because you can scroll along both the X and Y axes simultaneously.

UPDATE: For a memory-efficient approach

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

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

  @override
  State<XYAxisScrollTest> createState() => _XYAxisScrollTestState();
}

class _XYAxisScrollTestState extends State<XYAxisScrollTest> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ListView example'),
      ),
      body: SingleChildTwoDimensionalScrollView(
        child: ConstrainedBox(
          constraints: BoxConstraints(
            maxWidth: 1800,
            maxHeight: MediaQuery.of(context).size.height,
          ),
          child: ListView.builder(
            itemExtent: 50,
            itemCount: 1000 * 1000 * 1000,
            itemBuilder: (context, index) {
              return Container(
                padding: const EdgeInsets.all(10),
                color: index % 2 == 0 ? Colors.white : const Color(0xFFEFEFEF),
                child: Text(
                  "$index very long long long long long long long long long"
                  "long long long long long long long long long long long"
                  "long long long long long long long long long long long"
                  "long long long long long long long long long long long"
                  "long long long long long long long long long long text",
                  style: TextStyle(color: Colors.black),
                  softWrap: false,
                ),
              );
            },
          ),
        ),
      ),
    );
  }
}

I find it more efficient to combine the SingleChildTwoDimensionalScrollView (using the single_child_two_dimensional_scroll_view package) and ListView.builder.

But you need to wrap ListView.builder with ConstrainedBox to avoid:

  • Vertical viewport was given unbounded height.

or

  • Vertical viewport was given unbounded width.

The challenge is that you need to specify the longest possible maxWidth that your Text widget might generate. Based on your example, I found the possible maxWidth to be 1800. I then set the maxHeight as MediaQuery.of(context).size.height to occupy the available screen height, since the ListView.builder is already vertically scrollable by default.

Another issue is that I can't compute the size of each Text widget using GlobalKey by assigning these keys to each Text widget generated by ListView.builder.

This approach is helpful if you're providing predefined text data or if you've set a length limit for the table field (from your database or data repository) for your text. In these scenarios, you can provide the maxWidth constraint for the ConstrainedBox, because you can reliably predict the expected maxWidth.

P.S. Credit to Csaba Mihaly for the SingleChildTwoDimensionalScrollView idea.

Sign up to request clarification or add additional context in comments.

Comments

1

I like the InteractiveViewer solution by @DevQt it's perfect if you don't need to load in elements dynamically. It doesn't use a package which I like and seems performant.

I'm just not sure how would you detect if you need to load in more elements. Maybe it's possible with it's controller.

EDIT: I have found an answer that describes implementing 2d scroll with scrollbars, using DataTable and two SingleChildScrollViews https://stackoverflow.com/a/72734055/9682338

I have found a package that has a scrollcontroller which lets you detect if the user scrolled to the end.

import 'dart:ui';

import 'package:flutter/material.dart';

import 'package:single_child_two_dimensional_scroll_view/single_child_two_dimensional_scroll_view.dart';

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

class MyCustomScrollBehavior extends MaterialScrollBehavior {
  @override
  Set<PointerDeviceKind> get dragDevices => {
    PointerDeviceKind.touch,
    PointerDeviceKind.mouse,
  };
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      scrollBehavior: MyCustomScrollBehavior(),
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const HomePage(),
    );
  }
}

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

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('ListView example')),
      body: SingleChildTwoDimensionalScrollView(
        child: Column(
          children: List.generate(
            1000,
            (index) => Container(
              padding: const EdgeInsets.all(10),
              color: index % 2 == 0 ? Colors.white : const Color(0xFFEFEFEF),
              child: Text(
                "$index very long long long long long long long long long"
                "long long long long long long long long long long long"
                "long long long long long long long long long long long"
                "long long long long long long long long long long long"
                "long long long long long long long long long long text",
                softWrap: false,
              ),
            ),
          ),
        ),
      ),
    );
  }
}

You can also do it with two_dimensional_scrollables, this can load infinite elements dynamically, I just don't know how to get the SpanExtent to be the size of the Cell's content.

import 'dart:ui';

import 'package:flutter/material.dart';

import 'package:two_dimensional_scrollables/two_dimensional_scrollables.dart';

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

class MyCustomScrollBehavior extends MaterialScrollBehavior {
  @override
  Set<PointerDeviceKind> get dragDevices => {
    PointerDeviceKind.touch,
    PointerDeviceKind.mouse,
  };
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      scrollBehavior: MyCustomScrollBehavior(),
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const HomePage(),
    );
  }
}

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

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: TableView.builder(
        cellBuilder: _buildCell,
        columnCount: 1,
        columnBuilder: _columnSpan,
        rowCount: null,
        rowBuilder: _rowSpan,
        diagonalDragBehavior: DiagonalDragBehavior.free,
      ),
    );
  }

  TableViewCell _buildCell(BuildContext context, TableVicinity vicinity) {
    final Color boxColor = vicinity.row.isEven
        ? Colors.white
        : Colors.indigo[100]!;

    return TableViewCell(
      child: ColoredBox(
        color: boxColor,
        child: Center(
          child: Align(
            alignment: Alignment.centerLeft,
            child: Text(
              "${vicinity.row} very long long long long long long long long long"
              "long long long long long long long long long long long"
              "long long long long long long long long long long long"
              "long long long long long long long long long long long"
              "long long long long long long long long long long text",
            ),
          ),
        ),
      ),
    );
  }

  TableSpan _columnSpan(int index) {
    return const TableSpan(extent: FixedTableSpanExtent(2000));
  }

  TableSpan _rowSpan(int index) {
    return const TableSpan(extent: FixedTableSpanExtent(50));
  }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.