Text widget with read more or expand feature - Flutter

Published August 13, 2020


In most of the mobile applications, there is a scenario that will come, to show a few texts with Read more option. 

In this post, we are going to learn how to add Read more options for long text in a flutter application.

The read more or expandable feature in a Text and other widgets will help you to show large sentences or paragraphs.  It shows complete paragraphs when a user clicks on Read more button

 

Creating a new Project

Step 1: Create a new project.

Step 2: Create a widget which will handle all text to expand/collapse mode.

read_more_text.dart

 

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

enum TrimMode {
  Length,
  Line,
}

class ReadMoreText extends StatefulWidget {
  const ReadMoreText(
      this.data, {
        Key key,
        this.trimExpandedText = ' Read Less',
        this.trimCollapsedText = ' ...Read More',
        this.colorClickableText,
        this.trimLength = 240,
        this.trimLines = 2,
        this.trimMode = TrimMode.Length,
        this.style,
        this.textAlign,
        this.textDirection,
        this.locale,
        this.textScaleFactor,
        this.semanticsLabel,
      })  : assert(data != null),
        super(key: key);

  final String data;
  final String trimExpandedText;
  final String trimCollapsedText;
  final Color colorClickableText;
  final int trimLength;
  final int trimLines;
  final TrimMode trimMode;
  final TextStyle style;
  final TextAlign textAlign;
  final TextDirection textDirection;
  final Locale locale;
  final double textScaleFactor;
  final String semanticsLabel;

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

const String _kEllipsis = '\u2026';

const String _kLineSeparator = '\u2028';

class ReadMoreTextState extends State<ReadMoreText> {
  bool _readMore = true;

  void _onTapLink() {
    setState(() => _readMore = !_readMore);
  }

  @override
  Widget build(BuildContext context) {
    final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context);
    TextStyle effectiveTextStyle = widget.style;
    if (widget.style == null || widget.style.inherit) {
      effectiveTextStyle = defaultTextStyle.style.merge(widget.style);
    }

    final textAlign =
        widget.textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start;
    final textDirection = widget.textDirection ?? Directionality.of(context);
    final textScaleFactor =
        widget.textScaleFactor ?? MediaQuery.textScaleFactorOf(context);
    final overflow = defaultTextStyle.overflow;
    final locale =
        widget.locale ?? Localizations.localeOf(context, nullOk: true);

    final colorClickableText =
        widget.colorClickableText ?? Theme.of(context).accentColor;

    TextSpan link = TextSpan(
      text: _readMore ? widget.trimCollapsedText : widget.trimExpandedText,
      style: effectiveTextStyle.copyWith(
        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(
          style: effectiveTextStyle,
          text: widget.data,
        );

        // Layout and measure link
        TextPainter textPainter = TextPainter(
          text: link,
          textAlign: textAlign,
          textDirection: textDirection,
          textScaleFactor: textScaleFactor,
          maxLines: widget.trimLines,
          ellipsis: overflow == TextOverflow.ellipsis ? _kEllipsis : null,
          locale: locale,
        );
        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;

        print('linkSize $linkSize textSize $textSize');

        // Get the endIndex of data
        bool linkLongerThanLine = false;
        int endIndex;

        if (linkSize.width < maxWidth) {
          final pos = textPainter.getPositionForOffset(Offset(
            textSize.width - linkSize.width,
            textSize.height,
          ));
          endIndex = textPainter.getOffsetBefore(pos.offset);
        }
        else {
          var pos = textPainter.getPositionForOffset(
            textSize.bottomLeft(Offset.zero),
          );
          endIndex = pos.offset;
          linkLongerThanLine = true;
        }

        var textSpan;
        switch (widget.trimMode) {
          case TrimMode.Length:
            if (widget.trimLength < widget.data.length) {
              textSpan = TextSpan(
                style: effectiveTextStyle,
                text: _readMore
                    ? widget.data.substring(0, widget.trimLength)
                    : widget.data,
                children: <TextSpan>[link],
              );
            } else {
              textSpan = TextSpan(
                style: effectiveTextStyle,
                text: widget.data,
              );
            }
            break;
          case TrimMode.Line:
            if (textPainter.didExceedMaxLines) {
              textSpan = TextSpan(
                style: effectiveTextStyle,
                text: _readMore
                    ? widget.data.substring(0, endIndex) +
                    (linkLongerThanLine ? _kLineSeparator : '')
                    : widget.data,
                children: <TextSpan>[link],
              );
            } else {
              textSpan = TextSpan(
                style: effectiveTextStyle,
                text: widget.data,
              );
            }
            break;
          default:
            throw Exception(
                'TrimMode type: ${widget.trimMode} is not supported');
        }

        return RichText(
          textAlign: textAlign,
          textDirection: textDirection,
          softWrap: true,
          //softWrap,
          overflow: TextOverflow.clip,
          //overflow,
          textScaleFactor: textScaleFactor,
          text: textSpan,
        );
      },
    );
    if (widget.semanticsLabel != null) {
      result = Semantics(
        textDirection: widget.textDirection,
        label: widget.semanticsLabel,
        child: ExcludeSemantics(
          child: result,
        ),
      );
    }
    return result;
  }
}

 

 

Step 3: Update main.dart file 

 

import 'package:flutter/material.dart';
import 'package:flutter_readmore_text/readmore_text.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(

        primarySwatch: Colors.blue,

        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyExample(),
    );
  }
}
class MyExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
          title: Text(
            'Read More Text',
            style: TextStyle(color: Colors.white),
          )),
      body: DefaultTextStyle.merge(
        style: const TextStyle(
          fontSize: 16.0,
          //fontFamily: 'monospace',
        ),
        child: SingleChildScrollView(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Padding(
                padding: const EdgeInsets.all(16.0),
                child: Card(
                  child: Column(
                    children: <Widget>[
                      Image.network( "https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcQO2u8W5qVZNrIoftM59MYbafwVqTF6DgSsjw&usqp=CAU",height:200,width: double.infinity,fit: BoxFit.cover,),
                      SizedBox(height: 10,),
                      ReadMoreText(
                        'The virus that causes COVID-19 is mainly transmitted through droplets generated when an infected person coughs, sneezes, or exhales. These droplets are too heavy to hang in the air, and quickly fall on floors or surfaces. You can be infected by breathing in the virus if you are within close proximity of someone who has COVID-19, or by touching a contaminated surface and then your eyes, nose or mouth',
                        trimLines: 2,
                        colorClickableText: Colors.purple,
                        trimMode: TrimMode.Line,
                        trimCollapsedText: '...Read more',
                        trimExpandedText: ' Hide',
                      ),
                      SizedBox(height: 10,),
                    ],
                  ),
                ),
              ),
              Divider(
                color: const Color(0xFF167F67),
              ),
              Padding(
                padding: const EdgeInsets.all(16.0),
                child: Card(
                  child: Column(
                    children: <Widget>[
                      Image.network("https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcRvOQke8ZNpIejOz8qVcUOziATtqrqmnFtnXQ&usqp=CAU",height:200,width: double.infinity,fit: BoxFit.cover,),
                      SizedBox(height: 10,),
                      ReadMoreText(
                        'National Expert Group on Vaccine Administration for COVID-19, met for the first time on 12th August. The meeting was chaired by Dr V K Paul, Member Niti Aayog along with Secretary (Ministry of Health and Family Welfare) as co-Chair.\nThe expert group deliberated on conceptualization and implementation mechanisms for creation of a digital infrastructure for inventory management and delivery mechanism of the vaccine including tracking of vaccination process with particular focus on last mile delivery. They discussed on broad parameters guiding the selection of COVID-19 vaccine candidates for the country and sought inputs from Standing Technical Sub-Committee of National Technical Advisory Group on Immunization (NTAGI). The group delved on the procurement mechanisms for COVID-19 vaccine, including both indigenous and international manufacturing along with guiding principles for prioritization of population groups for vaccination',
                        trimLines: 3,

                        colorClickableText: Colors.purple,
                        trimMode: TrimMode.Line,
                        trimCollapsedText: '...Show More',
                        trimExpandedText: ' Show Less ',
                      ),SizedBox(height: 10,),
                    ],
                  ),
                ),
              ),

              Divider(
                color: const Color(0xFF167F67),
              ),

              Padding(
                padding: const EdgeInsets.all(16.0),
                child: Card(
                  child: Column(
                    children: <Widget>[
                      Image.network("https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcSl7FaXwasURMmb2TVVQPB2C52GX1cHS4JmHQ&usqp=CAU",height:200,width: double.infinity,fit: BoxFit.cover,),
                      SizedBox(height: 10,),
                      ReadMoreText(
                        'National Expert Group on Vaccine Administration for COVID-19, met for the first time on 12th August. The meeting was chaired by Dr V K Paul, Member Niti Aayog along with Secretary (Ministry of Health and Family Welfare) as co-Chair.\nThe expert group deliberated on conceptualization and implementation mechanisms for creation of a digital infrastructure for inventory management and delivery mechanism of the vaccine including tracking of vaccination process with particular focus on last mile delivery. They discussed on broad parameters guiding the selection of COVID-19 vaccine candidates for the country and sought inputs from Standing Technical Sub-Committee of National Technical Advisory Group on Immunization (NTAGI). The group delved on the procurement mechanisms for COVID-19 vaccine, including both indigenous and international manufacturing along with guiding principles for prioritization of population groups for vaccination',
                        trimLines: 3,

                        colorClickableText: Colors.purple,
                        trimMode: TrimMode.Line,
                        trimCollapsedText: '...Expand',
                        trimExpandedText: ' Collapse ',
                      ),
                      SizedBox(height: 10,),
                    ],
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

 

 

 

Tags: ReadMore widget, Flutter, Expand Widget


Article Contributed By :
https://www.rrtutors.com/site_assets/profile/assets/img/avataaars.svg

740 Views

Subscribe For Daily Updates

Flutter Questions
Android Questions