readmore 3.0.0 copy "readmore: ^3.0.0" to clipboard
readmore: ^3.0.0 copied to clipboard

A Flutter package that allows for dynamic expansion and collapse of text, as well as interactions with text patterns such as hashtags, URLs, and mentions.

example/lib/main.dart

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

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primaryColor: const Color(0xFF02BB9F),
        primaryColorDark: const Color(0xFF167F67),
      ),
      title: 'Read More Text',
      home: const DemoApp(),
    );
  }
}

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

  @override
  State<DemoApp> createState() => _DemoAppState();
}

class _DemoAppState extends State<DemoApp> {
  final isCollapsed = ValueNotifier<bool>(false);

  // K: UID, V: Username
  final userMap = {
    123: 'Android',
    456: 'iOS',
  };

  var _trimMode = TrimMode.Line;
  int _trimLines = 3;
  int _trimLength = 240;

  void _incrementTrimLines() => setState(() => _trimLines++);

  void _decrementTrimLines() =>
      setState(() => _trimLines = _trimLines > 1 ? _trimLines - 1 : 1);

  void _incrementTrimLength() => setState(() => _trimLength++);

  void _decrementTrimLength() =>
      setState(() => _trimLength = _trimLength > 1 ? _trimLength - 1 : 1);

  void _showMessage(String message) {
    ScaffoldMessenger.of(context)
      ..hideCurrentSnackBar()
      ..showSnackBar(SnackBar(content: Text(message)));
  }

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

    isCollapsed.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Read More Text'),
      ),
      body: DefaultTextStyle.merge(
        style: const TextStyle(fontSize: 14),
        child: DraggableDivider(
          child: SingleChildScrollView(
            child: _buildContent(),
          ),
        ),
      ),
    );
  }

  Column _buildContent() {
    return Column(
      children: <Widget>[
        _buildSettings(),
        Padding(
          key: const Key('showMore'),
          padding: const EdgeInsets.all(16),
          child: 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.',
            trimMode: _trimMode,
            trimLines: _trimLines,
            trimLength: _trimLength,
            preDataText: 'AMANDA',
            preDataTextStyle: const TextStyle(fontWeight: FontWeight.w500),
            style: const TextStyle(color: Colors.black),
            colorClickableText: Colors.pink,
            trimCollapsedText: '...Show more',
            trimExpandedText: ' show less',
          ),
        ),
        const Divider(
          color: Color(0xFF167F67),
        ),
        Padding(
          padding: const EdgeInsets.all(16),
          child: ReadMoreText(
            'Flutter(https://flutter.dev/) has its own UI components, along with an engine to render them on both the <@123> and <@456> platforms <@999> http://google.com #read_more. Most of those UI components, right out of the box, conform to the guidelines of #Material Design.',
            trimMode: _trimMode,
            trimLines: _trimLines,
            trimLength: _trimLength,
            style: const TextStyle(color: Colors.black),
            colorClickableText: Colors.pink,
            trimCollapsedText: '...Expand',
            trimExpandedText: ' Collapse ',
            annotations: [
              // URL
              Annotation(
                regExp: RegExp(
                  r'(?:(?:https?|ftp)://)?[\w/\-?=%.]+\.[\w/\-?=%.]+',
                ),
                spanBuilder: ({
                  required String text,
                  TextStyle? textStyle,
                }) {
                  return TextSpan(
                    text: text,
                    style: (textStyle ?? const TextStyle()).copyWith(
                      decoration: TextDecoration.underline,
                      color: Colors.green,
                    ),
                    recognizer: TapGestureRecognizer()
                      ..onTap = () => _showMessage(text),
                  );
                },
              ),
              // Mention
              Annotation(
                regExp: RegExp(r'<@(\d+)>'),
                spanBuilder: ({
                  required String text,
                  TextStyle? textStyle,
                }) {
                  final user = userMap[int.tryParse(
                    text.substring(2, text.length - 1),
                  )];

                  if (user == null) {
                    return TextSpan(
                      text: '@unknown user',
                      style: (textStyle ?? const TextStyle()).copyWith(
                        fontWeight: FontWeight.bold,
                      ),
                      recognizer: TapGestureRecognizer()
                        ..onTap = () => _showMessage('User not found'),
                    );
                  }

                  return TextSpan(
                    text: '@$user',
                    style: (textStyle ?? const TextStyle()).copyWith(
                      decoration: TextDecoration.underline,
                      color: Colors.redAccent,
                    ),
                    recognizer: TapGestureRecognizer()
                      ..onTap = () => _showMessage('@$user'),
                    children: [
                      if (user == 'iOS') const TextSpan(text: 'Extra'),
                    ],
                  );
                },
              ),
              // Hashtag
              Annotation(
                // Test: non capturing group should work also
                regExp: RegExp('#(?:[a-zA-Z0-9_]+)'),
                spanBuilder: ({
                  required String text,
                  TextStyle? textStyle,
                }) {
                  return TextSpan(
                    text: text,
                    style: (textStyle ?? const TextStyle()).copyWith(
                      color: Colors.blueAccent,
                      height: 1.5,
                      letterSpacing: 5,
                    ),
                    recognizer: TapGestureRecognizer()
                      ..onTap = () => _showMessage(text),
                  );
                },
              ),
            ],
          ),
        ),
        const Divider(
          color: Color(0xFF167F67),
        ),
        Padding(
          padding: const EdgeInsets.all(16),
          child: ReadMoreText(
            'The Flutter framework builds its layout via the composition of widgets, everything that you construct programmatically is a widget and these are compiled together to create the user interface. ',
            trimMode: _trimMode,
            trimLines: _trimLines,
            trimLength: _trimLength,
            isCollapsed: isCollapsed,
            style: const TextStyle(color: Colors.black),
            colorClickableText: Colors.pink,
            trimCollapsedText: '...Read more',
            trimExpandedText: ' Less',
          ),
        ),
        ValueListenableBuilder(
          valueListenable: isCollapsed,
          builder: (context, value, child) {
            return Center(
              child: ElevatedButton(
                onPressed: () => isCollapsed.value = !isCollapsed.value,
                child: Text('is collapsed: $value'),
              ),
            );
          },
        ),
      ],
    );
  }

  Column _buildSettings() {
    return Column(
      mainAxisSize: MainAxisSize.min,
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        const Text('Trim Mode'),
        Padding(
          padding: const EdgeInsets.all(8),
          child: SegmentedButton<TrimMode>(
            segments: const [
              ButtonSegment<TrimMode>(
                value: TrimMode.Length,
                label: Text('Length'),
              ),
              ButtonSegment<TrimMode>(
                value: TrimMode.Line,
                label: Text('Line'),
              ),
            ],
            selected: <TrimMode>{_trimMode},
            onSelectionChanged: (Set<TrimMode> newSelection) {
              setState(() {
                _trimMode = newSelection.first;
              });
            },
          ),
        ),
        if (_trimMode == TrimMode.Length)
          Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              IconButton(
                icon: const Icon(Icons.remove),
                onPressed: _decrementTrimLength,
              ),
              Text('$_trimLength'),
              IconButton(
                icon: const Icon(Icons.add),
                onPressed: _incrementTrimLength,
              ),
            ],
          ),
        if (_trimMode == TrimMode.Line)
          Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              IconButton(
                icon: const Icon(Icons.remove),
                onPressed: _decrementTrimLines,
              ),
              Text('$_trimLines'),
              IconButton(
                icon: const Icon(Icons.add),
                onPressed: _incrementTrimLines,
              ),
            ],
          ),
      ],
    );
  }
}

class DraggableDivider extends StatefulWidget {
  const DraggableDivider({super.key, required this.child});

  final Widget child;

  @override
  State<DraggableDivider> createState() => _DraggableDividerState();
}

class _DraggableDividerState extends State<DraggableDivider> {
  final double dividerWidth = 10;
  late double _leftWidth;
  final double _minWidth = 20;

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

    final screenWidth = MediaQuery.sizeOf(context).width;
    setState(() {
      _leftWidth = screenWidth - _minWidth - dividerWidth;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        SizedBox(
          width: _leftWidth,
          child: widget.child,
        ),
        GestureDetector(
          onHorizontalDragUpdate: (details) {
            setState(() {
              final newWidth = _leftWidth + details.delta.dx;
              final screenWidth = MediaQuery.of(context).size.width;

              if (newWidth >= _minWidth &&
                  (screenWidth - newWidth - dividerWidth) >= _minWidth) {
                _leftWidth = newWidth;
              }
            });
          },
          child: Container(
            color: Colors.grey,
            width: 10,
          ),
        ),
        const Expanded(
          child: Center(child: VerticalText('Drag Test')),
        ),
      ],
    );
  }
}

class VerticalText extends StatelessWidget {
  const VerticalText(this.text, {super.key});

  final String text;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: text
          .split('')
          .map(
            (letter) => Text(
              letter,
              textAlign: TextAlign.center,
            ),
          )
          .toList(),
    );
  }
}
1.02k
likes
160
points
80.8k
downloads

Publisher

verified publishergetx.site

Weekly Downloads

A Flutter package that allows for dynamic expansion and collapse of text, as well as interactions with text patterns such as hashtags, URLs, and mentions.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

flutter

More

Packages that depend on readmore