47

My Container having a description of movies.

Initially, I want to show only a few lines of description. And below that there should be a link (more...), After Tapping more... all content of description should be get displayed.

For example, check this JQuery plugin.

Paresh Mangukiya
  • 37,512
  • 17
  • 201
  • 182
Tushar Pol
  • 5,629
  • 11
  • 28
  • 35

11 Answers11

59

you can do that this way

example

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

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      home: new HomeScreen(),
    );
  }
}

class HomeScreen extends StatelessWidget {
  final String description =
      "Flutter is Google’s mobile UI framework for crafting high-quality native interfaces on iOS and Android in record time. Flutter works with existing code, is used by developers and organizations around the world, and is free and open source.";

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: const Text("Demo App"),
      ),
      body: new Container(
        child: new DescriptionTextWidget(text: description),
      ),
    );
  }
}

class DescriptionTextWidget extends StatefulWidget {
  final String text;

  DescriptionTextWidget({@required this.text});

  @override
  _DescriptionTextWidgetState createState() => new _DescriptionTextWidgetState();
}

class _DescriptionTextWidgetState extends State<DescriptionTextWidget> {
  String firstHalf;
  String secondHalf;

  bool flag = true;

  @override
  void initState() {
    super.initState();

    if (widget.text.length > 50) {
      firstHalf = widget.text.substring(0, 50);
      secondHalf = widget.text.substring(50, widget.text.length);
    } else {
      firstHalf = widget.text;
      secondHalf = "";
    }
  }

  @override
  Widget build(BuildContext context) {
    return new Container(
      padding: new EdgeInsets.symmetric(horizontal: 10.0, vertical: 10.0),
      child: secondHalf.isEmpty
          ? new Text(firstHalf)
          : new Column(
              children: <Widget>[
                new Text(flag ? (firstHalf + "...") : (firstHalf + secondHalf)),
                new InkWell(
                  child: new Row(
                    mainAxisAlignment: MainAxisAlignment.end,
                    children: <Widget>[
                      new Text(
                        flag ? "show more" : "show less",
                        style: new TextStyle(color: Colors.blue),
                      ),
                    ],
                  ),
                  onTap: () {
                    setState(() {
                      flag = !flag;
                    });
                  },
                ),
              ],
            ),
    );
  }
}
Ajay Kumar
  • 12,514
  • 11
  • 49
  • 51
  • I have a question , how can I put the "show more" behind the text , I mean it looks like this "Flutter is Google’s mobile UI framework for... show more",they are both in the same line. – JDChi Nov 05 '19 at 04:01
  • @JDNew Well, you could just simply add a row and wrap the `Text` & `InkWell` widget inside it. Or the most easiest way, is just rename the `Column` widget into `Row` widget. Give it a try. Let me know if you need another help. – Richie Permana Nov 05 '19 at 04:08
  • 1
    is there any way to put icons instead of `Show more` and `Show less` ? – dqureshiumar Sep 25 '20 at 19:34
  • @RichiePermana renaming ```Column``` widget to a ```Row``` widget gives an overflow error and even when replaced it with the ```Wrap``` widget it gives unexpected output – Viraj D Dec 14 '20 at 11:10
  • Perfect answer... Thanks buddy – Roopak Varghese Apr 03 '22 at 07:09
27

A simple example

class ExpandableText extends StatefulWidget {
  ExpandableText(this.text);

  final String text;
  bool isExpanded = false;

  @override
  _ExpandableTextState createState() => new _ExpandableTextState();
}

class _ExpandableTextState extends State<ExpandableText> {
  @override
  Widget build(BuildContext context) {
    return new Column(children: <Widget>[
      new ConstrainedBox(
          constraints: widget.isExpanded
              ? new BoxConstraints()
              : new BoxConstraints(maxHeight: 50.0),
          child: new Text(
            widget.text,
            softWrap: true,
            overflow: TextOverflow.fade,
          )),
      widget.isExpanded
          ? new Container()
          : new FlatButton(
              child: const Text('...'),
              onPressed: () => setState(() => widget.isExpanded = true))
    ]);
  }
}

with animation

class ExpandableText extends StatefulWidget {
  ExpandableText(this.text);

  final String text;
  bool isExpanded = false;

  @override
  _ExpandableTextState createState() => new _ExpandableTextState();
}

class _ExpandableTextState extends State<ExpandableText>
    with TickerProviderStateMixin<ExpandableText> {
  @override
  Widget build(BuildContext context) {
    return new Column(children: <Widget>[
      new AnimatedSize(
          vsync: this,
          duration: const Duration(milliseconds: 500),
          child: new ConstrainedBox(
              constraints: widget.isExpanded
                  ? new BoxConstraints()
                  : new BoxConstraints(maxHeight: 50.0),
              child: new Text(
                widget.text,
                softWrap: true,
                overflow: TextOverflow.fade,
              ))),
      widget.isExpanded
          ? new ConstrainedBox(constraints: new BoxConstraints())
          : new FlatButton(
          child: const Text('...'),
          onPressed: () => setState(() => widget.isExpanded = true))
    ]);
  }
}
Günter Zöchbauer
  • 558,509
  • 191
  • 1,911
  • 1,506
26

To get Exact Behavior like JQuery plugin. you need to use TextPainter and calculate the hight on given Width.

Expanded State

Normal State

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';

void main() => runApp(MaterialApp( home: DemoApp()));

class DemoApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
          title: Text(
            'Read More Text',
          )),
      body:  SingleChildScrollView(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Padding(
                padding: const EdgeInsets.all(16.0),
                child: ExpandableText(
                  'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque scelerisque efficitur posuere. Curabitur tincidunt placerat diam ac efficitur. Cras rutrum egestas nisl vitae pulvinar. Donec id mollis diam, id hendrerit neque. Donec accumsan efficitur libero, vitae feugiat odio fringilla ac. Aliquam a turpis bibendum, varius erat dictum, feugiat libero. Nam et dignissim nibh. Morbi elementum varius elit, at dignissim ex accumsan a',
                   trimLines: 2,
                ),
              ),
            ],
          ),
        ),
    );
  }
}
class ExpandableText extends StatefulWidget {
  const ExpandableText(
      this.text, {
        Key key,
        this.trimLines = 2,
      })  : assert(text != null),
        super(key: key);

  final String text;
  final int trimLines;

  @override
  ExpandableTextState createState() => ExpandableTextState();
}

class ExpandableTextState extends State<ExpandableText> {
  bool _readMore = true;
  void _onTapLink() {
    setState(() => _readMore = !_readMore);
  }

  @override
  Widget build(BuildContext context) {
    final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context);
    final colorClickableText = Colors.blue;
    final widgetColor = Colors.black;
    TextSpan link = TextSpan(
      text: _readMore ? "... read more" : " read less",
      style: TextStyle(
        color: colorClickableText,
      ),
      recognizer: TapGestureRecognizer()..onTap = _onTapLink
    );
    Widget result = LayoutBuilder(
      builder: (BuildContext context, BoxConstraints constraints) {
        assert(constraints.hasBoundedWidth);
        final double maxWidth = constraints.maxWidth;
        // Create a TextSpan with data
        final text = TextSpan(
          text: widget.text,
        );
        // Layout and measure link
        TextPainter textPainter = TextPainter(
          text: link,
          textDirection: TextDirection.rtl,//better to pass this from master widget if ltr and rtl both supported
          maxLines: widget.trimLines,
          ellipsis: '...',
        );
        textPainter.layout(minWidth: constraints.minWidth, maxWidth: maxWidth);
        final linkSize = textPainter.size;
        // Layout and measure text
        textPainter.text = text;
        textPainter.layout(minWidth: constraints.minWidth, maxWidth: maxWidth);
        final textSize = textPainter.size;
        // Get the endIndex of data
        int endIndex;
        final pos = textPainter.getPositionForOffset(Offset(
            textSize.width - linkSize.width,
            textSize.height,
        ));
        endIndex = textPainter.getOffsetBefore(pos.offset);
        var textSpan;
        if (textPainter.didExceedMaxLines) {
          textSpan = TextSpan(
            text: _readMore
                ? widget.text.substring(0, endIndex)
                : widget.text,
            style: TextStyle(
              color: widgetColor,
            ),
            children: <TextSpan>[link],
          );
        } else {
          textSpan = TextSpan(
            text: widget.text,
          );
        }
        return RichText(
          softWrap: true,
          overflow: TextOverflow.clip,
          text: textSpan,
        );
      },
    );
    return result;
  }
}
Prafulla
  • 603
  • 7
  • 11
8

Try this.

Watch the output from here ( Video )

or refer below images

enter image description here

import 'package:flutter/material.dart';

class DemoPage extends StatefulWidget {
  final Widget child;

  DemoPage({Key key, this.child}) : super(key: key);

  _DemoPageState createState() => _DemoPageState();
}

class _DemoPageState extends State<DemoPage> {

String descText = "Description Line 1\nDescription Line 2\nDescription Line 3\nDescription Line 4\nDescription Line 5\nDescription Line 6\nDescription Line 7\nDescription Line 8";
bool descTextShowFlag = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("DemoPage"),
      ),
      body: new Container(
        margin: EdgeInsets.all(16.0),
        child: new Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
              Text(descText,
                maxLines: descTextShowFlag ? 8 : 2,textAlign: TextAlign.start),
              InkWell(
                onTap: (){ setState(() {
                descTextShowFlag = !descTextShowFlag; 
                }); },
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.end,
                  children: <Widget>[
                    descTextShowFlag ? Text("Show Less",style: TextStyle(color: Colors.blue),) :  Text("Show More",style: TextStyle(color: Colors.blue))
                  ],
                ),
              ),
          ],
        ),
      ),
    );
  }
}
Vicky Salunkhe
  • 7,528
  • 4
  • 34
  • 51
7

If you want to just use a package to simply do it, you can use the Readmore package. its working just fine and so simple to use.

import 'package:readmore/readmore.dart';

ReadMoreText(
  'Flutter is Google’s mobile UI open source framework to build high-quality native (super fast) interfaces for iOS and Android apps with the unified codebase.',
  trimLines: 2,
  colorClickableText: Colors.pink,
  trimMode: TrimMode.Line,
  trimCollapsedText: 'Show more',
  trimExpandedText: 'Show less',
  moreStyle: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
);
Taba
  • 2,942
  • 3
  • 28
  • 43
3

Please use package flutter-expandable.
This package can produce an effect of expanding an image or text.
https://github.com/aryzhov/flutter-expandable

enter image description here

import 'package:expandable/expandable.dart';
...
ExpandablePanel(
        header: Text( content,
          maxLines: 2,
          style: Theme.of(context).textTheme.body2,
        ),
        expanded: Align(
            alignment: Alignment.centerLeft,
            child: Text(
              content,
              softWrap: true,
            )),
        tapHeaderToExpand: true,
        hasIcon: true,
      ),
chunhunghan
  • 43,865
  • 4
  • 67
  • 93
3

ExpandableText with regrex validation as well.

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

class ExpandableText extends StatefulWidget {
  ExpandableText(this.text);

  final String text;
  // bool isExpanded = false;

  @override
  _ExpandableTextState createState() => new _ExpandableTextState();
}

class _ExpandableTextState extends State<ExpandableText> {
  String text;
  bool canExpand = false;
  bool isExpand = false;

  @override
  Widget build(BuildContext context) {
    //
    canExpand = widget.text != null && widget.text.length >= 150;
    text = canExpand
        ? (isExpand ? widget.text : widget.text.substring(0, 150))
        : (widget.text);

    return canExpand
        ? Column(
            mainAxisAlignment: MainAxisAlignment.start,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              buildTextWithLinks(text.trim()),
              GestureDetector(
                onTap: () {
                  setState(() {
                    isExpand = !isExpand;
                  });
                },
                child: Padding(
                  padding: const EdgeInsets.symmetric(vertical: 4.0),
                  child: Text(isExpand ? ' ... show less' : ' ... show more'
                  ),
                ),
              ),
            ],
          )
        : Text(text != null ? text : "");
  }
}

Text buildTextWithLinks(String textToLink, {String text}) =>
    Text.rich(TextSpan(children: linkify(textToLink)));

Future<void> openUrl(String url) async {
  if (await canLaunch(url)) {
    await launch(url);
  } else {
    throw 'Could not launch $url';
  }
}

const String urlPattern =
    r"(https?|http)://([-A-Z0-9.]+)(/[-A-Z0-9+&@#/%=~_|!:,.;]*)?(\?[A-Z0-9+&@#/%=~_|!:‌​,.;]*)?";
const String emailPattern = r'\S+@\S+';
const String phonePattern = r'[\d-]{9,}';
final RegExp linkRegExp = RegExp(
    '($urlPattern)|($emailPattern)|($phonePattern)',
    caseSensitive: false);

WidgetSpan buildLinkComponent(String text, String linkToOpen) => WidgetSpan(
        child: InkWell(
      child: Text(
        text,
        style: TextStyle(
          color: Colors.red,
          decoration: TextDecoration.underline,
        ),
      ),
      onTap: () => openUrl(linkToOpen),
    ));

List<InlineSpan> linkify(String text) {
  final List<InlineSpan> list = <InlineSpan>[];
  final RegExpMatch match = linkRegExp.firstMatch(text);
  if (match == null) {
    list.add(TextSpan(text: text));
    return list;
  }

  if (match.start > 0) {
    list.add(TextSpan(text: text.substring(0, match.start)));
  }

  final String linkText = match.group(0);
  if (linkText.contains(RegExp(urlPattern, caseSensitive: false))) {
    list.add(buildLinkComponent(linkText, linkText));
  } else if (linkText.contains(RegExp(emailPattern, caseSensitive: false))) {
    list.add(buildLinkComponent(linkText, 'mailto:$linkText'));
  } else if (linkText.contains(RegExp(phonePattern, caseSensitive: false))) {
    list.add(buildLinkComponent(linkText, 'tel:$linkText'));
  } else {
    throw 'Unexpected match: $linkText';
  }

  list.addAll(linkify(text.substring(match.start + linkText.length)));

  return list;
}

sarin upreti
  • 306
  • 4
  • 9
1
Widget _text() {
var exceeded;
return LayoutBuilder(builder: (context, size) {
  // Build the textspan
  var span = TextSpan(
    text:
        "The red-tailed tropicbird is a seabird native to the tropical Indian and Pacific Oceans. One of three closely related species of tropicbird, it has four subspecies. Text wrapping is quite a pain for me too. I find that putting Text in a Container and then wrapping that container in a Expanded/Flexible works well.",
    style: Theme.of(context).textTheme.body1.copyWith(color: Colors.white),
  );

  // Use a textpainter to determine if it will exceed max lines
  var tp = TextPainter(
    maxLines: _maxLine.toInt(),
    textAlign: TextAlign.left,
    textDirection: TextDirection.ltr,
    text: span,
  );

  // trigger it to layout
  tp.layout(maxWidth: size.maxWidth);

  // whether the text overflowed or not
  exceeded = tp.didExceedMaxLines;

  // return Column(children: <Widget>[
  return Container(
    child: exceeded && seeMoreClicked
        ? _seeMoreLess(span, "See Less ")
        : exceeded && !seeMoreClicked
            ? _seeMoreLess(span, "See More", 3)
            : Text.rich(
                span,
                overflow: TextOverflow.visible,
              ),
  );
});
}

Widget _seeMoreLess(TextSpan span, String _text, [int maxLine = 0]) {
return Column(
  mainAxisAlignment: MainAxisAlignment.start,
  crossAxisAlignment: CrossAxisAlignment.end,
  children: <Widget>[
    maxLine > 0
        ? Text.rich(
            span,
            overflow: TextOverflow.ellipsis,
            maxLines: 3,
          )
        : Text.rich(
            span,
            overflow: TextOverflow.visible,
          ),
    InkWell(
        child: Text(
          _text,
          style: Theme.of(context)
              .textTheme
              .body1
              .copyWith(color: Colors.blue),
        ),
        onTap: () {
          setState(() {
            seeMoreClicked = !seeMoreClicked;
          });
        }),
  ],
);
Avnish Nishad
  • 1,224
  • 1
  • 12
  • 17
0

Option 1: if you are looking for something like facebook,Linkedin you just need one variable isOpen. If isOpen true return MoreText else LessText (use substring) . May not be a perfect solution but you can get what you need

Warning!!! you have to handle text length when using substring

import 'package:flutter/material.dart';
import 'package:linky/presentation/extension/utils.dart';

class ExpandableText extends StatefulWidget {
  final String text;
  final double max;
  const ExpandableText({Key? key, required this.text, required this.max})
      : super(key: key);

  @override
  _ExpandableTextState createState() => _ExpandableTextState();
}

class _ExpandableTextState extends State<ExpandableText> {
  TextPainter? textPainter;
  bool isOpen = false;

  @override
  Widget build(BuildContext context) {
    return isOpen
        ? SizedBox(
            child: Align(
                alignment: Alignment.centerLeft,
                child: RichText(
                  textAlign: TextAlign.start,
                  text: TextSpan(children: [
                    TextSpan(
                        text: widget.text, style: textTheme(context).bodyText1),
                    WidgetSpan(
                        child: InkWell(
                            onTap: () {
                              setState(() {
                                isOpen = !isOpen;
                              });
                            },
                            child: Text(
                              "Less more",
                              style: textTheme(context).bodyText1!.copyWith(
                                  fontWeight: FontWeight.bold,
                                  color: Colors.blueAccent),
                            )),
                        style: textTheme(context).bodyText1)
                  ]),
                )))
        : Align(
            alignment: Alignment.centerLeft,
            child: RichText(
              textAlign: TextAlign.start,
              maxLines: 2,
              text: TextSpan(children: [
                TextSpan(
                    text: widget.text.substring(
                            0,
                            int.parse(
                                "${(widget.text.length * widget.max).toInt()}")) +
                        "...",
                    style: textTheme(context).bodyText1),
                WidgetSpan(
                    child: InkWell(
                        mouseCursor: SystemMouseCursors.click,
                        onTap: () {
                          setState(() {
                            isOpen = !isOpen;
                          });
                        },
                        child: Text(
                          "more",
                          style: textTheme(context).bodyText1!.copyWith(
                              fontWeight: FontWeight.bold,
                              color: Colors.blueAccent),
                        )),
                    style: textTheme(context).bodyText1)
              ]),
            ),
          );
  }
}

Exemple :

ExpandableText(
          text: users[0].loremIpsum,
          max: 0.2,
        )

Option 2: If you need something like ExpansionPanel you juste use widget ExpansionTile .

ExpansionTile(title: title,children: [])

Inside Childress moreText & title your LessText . You can hide LessText when isOpen

enter image description here

0

This an old question, but hope this could help anybody who looks for simple fast solution with simple code: Just check if the text less than (for example) 125 chars, then show the whole text, if not show the first 125 chars then use '... show more' as textspan where when you click on it show the whole text and change 'show more' with 'show less'

I am using here getx for state mang, if you are not using it just use statefull screen with setstate to change its value

final RxBool showLongDes = false.obs;
final String shortDescription = 'This is great product ...'
...

shortDescription.length <= 125

? Text(shortDescription)      
:Obx(() {
   return RichText(
       text: TextSpan(
         text: showLongDes.value
               ?shortDescription
                : shortDescription.substring(0, 125) + ' ... '
          children: [
             TextSpan(
                 text: showLongDes.value
                        ? ' ' + 'Show less'
                        : 'Show more',
                 recognizer: TapGestureRecognizer()
                       ..onTap = () => showLongDes.value =!showLongDes.value,
                 style: GoogleFonts.rubik(
                          color: Colors.blue,
                            decoration: TextDecoration.underline),
             )
          ],
       ),
);
                            
Remoo
  • 364
  • 2
  • 9
-1

I examine all the answer and make a good solution. Just pass your text here and you can change number of lines just by changing maxLines in text widget..

class ExpandableText extends StatefulWidget {
  final String text;
  const ExpandableText(this.text, {Key? key}) : super(key: key);

  @override
  _ExpandableTextState createState() => _ExpandableTextState();
}

class _ExpandableTextState extends State<ExpandableText>
    with TickerProviderStateMixin<ExpandableText> {
  bool isExpanded = false;
  late int numLines;

 @override
 void initState() {
   numLines = '\n'.allMatches(widget.text).length + 1;
   super.initState();
 }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: double.infinity,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          AnimatedSize(
            duration: const Duration(milliseconds: 200),
            child: Text(
                widget.text,
                maxLines: isExpanded ? null : 3,
                softWrap: true,
                overflow: TextOverflow.fade,
              ),
          ),
         numLines > 2
          ? TextButton(
              child: const Text('View more'),
              onPressed: () => setState(() => 
                   isExpanded = !isExpanded),
            )
          : const SizedBox()
        ],
      ),
    );
  }
}