Seth Ladd
Seth Ladd

Reputation: 120539

How do I make a TextSpan ripple when I tap it?

Imagine I have a long piece of text like this: HELLO THIS IS MY LONG SENTENCE . I want the word LONG to ripple (ink splash) when I tap it.

Let's say I have this code:

new RichText(
  text: new TextSpan(
    text: 'HELLO THIS IS MY ',
    style: DefaultTextStyle.of(context).style,
    children: <TextSpan>[
      new TextSpan(text: 'LONG', style: new TextStyle(fontWeight: FontWeight.bold)),
      new TextSpan(text: ' SENTENCE'),
    ],
  ),
)

Thanks!

Upvotes: 9

Views: 7341

Answers (3)

Thea Choem
Thea Choem

Reputation: 775

By 2019, we can use this:

RichText(
  textAlign: TextAlign.center,
  text: TextSpan(
    style: textTheme.bodyText2,
    text: "HELLO THIS IS MY",
    children: [
      WidgetSpan(
        child: InkWell(
          onTap: () {},
          child: Text(
            "SENTENCE",
            style: TextStyle(color: colorScheme.primary),
          ),
        ),
      ),
    ],
  ),
),

Upvotes: 9

workingkills
workingkills

Reputation: 2716

example

If you want a generic solution to place widgets over portions of text, see this gist.

You can use the following code to have the ripple constrained to a specific section of the text:

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

import 'dart:ui' show TextBox;
import 'dart:math';

void main() {
  runApp(new MaterialApp(
    home: new Material(
      child: new Center(
        child: new Demo(),
      ),
    ),
  ));
}

class Demo extends StatelessWidget {
  final TextSelection textSelection =
      const TextSelection(baseOffset: 17, extentOffset: 21);

  final GlobalKey _textKey = new GlobalKey();

  @override
  Widget build(context) => new Stack(
        children: <Widget>[
          new RichText(
            key: _textKey,
            text: new TextSpan(
              text: 'HELLO THIS IS MY ',
              style: DefaultTextStyle.of(context).style,
              children: <TextSpan>[
                new TextSpan(
                    text: 'LONG',
                    style: new TextStyle(fontWeight: FontWeight.bold)),
                new TextSpan(text: ' SENTENCE'),
              ],
            ),
          ),
          new Positioned.fill(
            child: new LayoutBuilder(
              builder: (context, _) => new Stack(
                    children: <Widget>[
                      new Positioned.fromRect(
                        rect: _getSelectionRect(),
                        child: new InkWell(
                          onTap: () => {}, // needed to show the ripple
                        ),
                      ),
                    ],
                  ),
            ),
          ),
        ],
      );

  Rect _getSelectionRect() =>
      (_textKey.currentContext.findRenderObject() as RenderParagraph)
          .getBoxesForSelection(textSelection)
          .fold(
            null,
            (Rect previous, TextBox textBox) => new Rect.fromLTRB(
                  min(previous?.left ?? textBox.left, textBox.left),
                  min(previous?.top ?? textBox.top, textBox.top),
                  max(previous?.right ?? textBox.right, textBox.right),
                  max(previous?.bottom ?? textBox.bottom, textBox.bottom),
                ),
          ) ??
      Rect.zero;
}

Upvotes: 14

Collin Jackson
Collin Jackson

Reputation: 116728

You can accomplish this effect by adapting the code in ink_well.dart.

In this example I configured the rectCallback to expand to the containing card, but you could provide a smaller rectangle with the splash be centered around the point of the tap.

video

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

void main() {
  runApp(new MaterialApp(home: new DemoApp()));
}

class DemoText extends StatefulWidget {
  @override
  DemoTextState createState() => new DemoTextState();
}

class DemoTextState<T extends InkResponse> extends State<T>
    with AutomaticKeepAliveClientMixin {
  Set<InkSplash> _splashes;
  InkSplash _currentSplash;

  @override
  bool get wantKeepAlive => (_splashes != null && _splashes.isNotEmpty);

  void _handleTapDown(TapDownDetails details) {
    final RenderBox referenceBox = context.findRenderObject();
    InkSplash splash;
    splash = new InkSplash(
        controller: Material.of(context),
        referenceBox: referenceBox,
        containedInkWell: true,
        rectCallback: () => referenceBox.paintBounds,
        position: referenceBox.globalToLocal(details.globalPosition),
        color: Theme.of(context).splashColor,
        onRemoved: () {
          if (_splashes != null) {
            assert(_splashes.contains(splash));
            _splashes.remove(splash);
            if (_currentSplash == splash) _currentSplash = null;
            updateKeepAlive();
          } // else we're probably in deactivate()
        });
    _splashes ??= new HashSet<InkSplash>();
    _splashes.add(splash);
    _currentSplash = splash;
    updateKeepAlive();
  }

  void _handleTap(BuildContext context) {
    _currentSplash?.confirm();
    _currentSplash = null;
    Feedback.forTap(context);
  }

  void _handleTapCancel() {
    _currentSplash?.cancel();
    _currentSplash = null;
  }

  @override
  void deactivate() {
    if (_splashes != null) {
      final Set<InkSplash> splashes = _splashes;
      _splashes = null;
      for (InkSplash splash in splashes) splash.dispose();
      _currentSplash = null;
    }
    assert(_currentSplash == null);
    super.deactivate();
  }

  Widget build(BuildContext context) {
    return new Padding(
      padding: new EdgeInsets.all(20.0),
      child: new RichText(
        text: new TextSpan(
          text: 'HELLO THIS IS MY ',
          style: DefaultTextStyle.of(context).style,
          children: <TextSpan>[
            new TextSpan(
              recognizer: new TapGestureRecognizer()
                ..onTapCancel = _handleTapCancel
                ..onTapDown = _handleTapDown
                ..onTap = () => _handleTap(context),
              text: 'LONG',
              style: new TextStyle(fontWeight: FontWeight.bold),
            ),
            new TextSpan(text: ' SENTENCE'),
          ],
        ),
      ),
    );
  }
}

class DemoApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      body: new Center(
        child: new Container(
          height: 150.0,
          width: 150.0,
          child: new Card(
            child: new DemoText(),
          ),
        ),
      ),
    );
  }
}

Upvotes: 6

Related Questions