jonataslaw / readmore

A Flutter plugin than allow expand and collapse text dynamically
MIT License
257 stars 79 forks source link

Add a feature to allow WidgetSpan in annotation #70

Open chan150 opened 2 months ago

chan150 commented 2 months ago
  1. To add a support of WidgetSpan, I replace Annotation's input type from TextSpan to InlineSpan.
  2. Remove a TextSpan checker as called as _isTextSpan.
  3. When performing Layout and measure text, WidgetSpan may be skipped .
chan150 commented 2 months ago

It should be related with #25

maRci002 commented 2 months ago

What's the point of removing WidgetSpan during layout? In this scenario, the package won't function as expected:

  1. When trimMode is set to TrimMode.Line and trimLines is set to 3, if there are 3 lines of text followed by a WidgetSpan on the 4th line, the measurement will not detect any text to trim.
  2. When trimMode is set to TrimMode.Line and trimLines is set to 3, if there are 3 lines of WidgetSpan followed by 2 lines of text, the measurement will not detect any text to trim.
chan150 commented 2 months ago

What's the point of removing WidgetSpan during layout? In this scenario, the package won't function as expected:

  1. When trimMode is set to TrimMode.Line and trimLines is set to 3, if there are 3 lines of text followed by a WidgetSpan on the 4th line, the measurement will not detect any text to trim.
  2. When trimMode is set to TrimMode.Line and trimLines is set to 3, if there are 3 lines of WidgetSpan followed by 2 lines of text, the measurement will not detect any text to trim.

Hi @maRci002,

It is a scenario to be useful of the added feature. We can attach overlayable widget inside readmore widget.

I understand that removing just WidgetSpan is too naive and there is no way to estimate the size of widget before rendering.

ReadMoreText(
  '@[{"key": {"inner":"test"}}] and @[{"key":"value"}]',
  annotations: [
    Annotation(
      regExp: RegExp('@\\[(.+?)\\]'),
      spanBuilder: ({required text, required textStyle}) {
        return WidgetSpan(
          child: Tooltip(
            message: 'It is just tooltip',
            child: Text(
              'It is a widget',
              style: TextStyle(height: 0),
            ),
          ),
        );
      },
    ),
  ],
  style: TextStyle(color: Colors.red),
)
maRci002 commented 2 months ago

I understand that removing just WidgetSpan is too naive and there is no way to estimate the size of widget before rendering.

I think instead of removing WidgetSpan during the layout phase, we should provide or calculate the WidgetSpan size.

Note: The effectiveTextStyle is already provided to Annotation.spanBuilder, and we could offer a simple function that takes a TextSpan (without any WidgetSpan children) and returns its size. For example, this works when WidgetSpan includes a ToolTip widget, but users can also return a hardcoded size. Therefore, the spanBuilder could return both InlineSpan and possible PlaceholderDimensions.

Here is an example of how PlaceholderDimensions can be used in the case of WidgetSpan. However, the trimming logic should also be updated, which results in a very difficult API.

import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('WidgetSpan Test'),
        ),
        body: const Center(
          child: TextPainterButton(),
        ),
      ),
    );
  }
}

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

  void _printTextLayout(BuildContext context) {
    final defaultTextStyle = DefaultTextStyle.of(context);
    var effectiveTextStyle = defaultTextStyle.style;
    if (MediaQuery.boldTextOf(context)) {
      effectiveTextStyle = effectiveTextStyle.merge(const TextStyle(fontWeight: FontWeight.bold));
    }

    final textScaler = MediaQuery.textScalerOf(context);
    final textAlign = defaultTextStyle.textAlign ?? TextAlign.start;
    final textDirection = Directionality.of(context);
    final locale = Localizations.maybeLocaleOf(context);
    final textWidthBasis = defaultTextStyle.textWidthBasis;
    final textHeightBehavior = defaultTextStyle.textHeightBehavior ?? DefaultTextHeightBehavior.maybeOf(context);

    final textPainter = TextPainter(
      textAlign: textAlign,
      textDirection: textDirection,
      locale: locale,
      textScaler: textScaler,
      maxLines: 1,
      textWidthBasis: textWidthBasis,
      textHeightBehavior: textHeightBehavior,
    );

    const txt = 'It is a widget';
    const widgetSpan = WidgetSpan(
      child: Tooltip(
        message: 'It is just tooltip',
        child: Text(txt),
      ),
    );

    final widgetSpanText = TextSpan(
      text: txt,
      style: effectiveTextStyle,
    );

    textPainter.text = widgetSpanText;
    textPainter.layout();

    final widgetSpanSize = textPainter.size;

    final widgetSpanPlaceHolderDimension = PlaceholderDimensions(
      size: widgetSpanSize,
      alignment: widgetSpan.alignment,
      baseline: widgetSpan.baseline,
    );

    final span = TextSpan(
      style: effectiveTextStyle.merge(const TextStyle(color: Colors.black, fontSize: 18)),
      text: 'Hello, TextPainter!',
      children: const [widgetSpan],
    );

    textPainter.text = span;
    textPainter.setPlaceholderDimensions([widgetSpanPlaceHolderDimension]);
    textPainter.layout();

    print('Text size: ${textPainter.size}');
  }

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () => _printTextLayout(context),
      child: const Text('Layout Text'),
    );
  }
}
chan150 commented 1 month ago

I think instead of removing WidgetSpan during the layout phase, we should provide or calculate the WidgetSpan size.

I provide a widget size estimator. It will be helpful for above idea.

Size? _calculateWidgetSize(Widget widget) {
    final child = InheritedTheme.captureAll(
      context,
      MediaQuery(
        data: MediaQuery.of(context),
        child: Material(
          color: Colors.transparent,
          child: widget,
        ),
      ),
    );

    final RenderRepaintBoundary repaintBoundary = RenderRepaintBoundary();
    final platformDispatcher = WidgetsBinding.instance.platformDispatcher;
    final fallBackView = platformDispatcher.views.first;
    final view = View.maybeOf(context) ?? fallBackView;
    Size logicalSize = view.physicalSize / view.devicePixelRatio; // Adapted

    final RenderView renderView = RenderView(
      view: view,
      child: RenderPositionedBox(
          alignment: Alignment.center, child: repaintBoundary),
      configuration: ViewConfiguration(
        // size: logicalSize,
        logicalConstraints: BoxConstraints(
          maxWidth: logicalSize.width,
          maxHeight: logicalSize.height,
        ),
        devicePixelRatio: 1.0,
      ),
    );

    final PipelineOwner pipelineOwner = PipelineOwner();
    final BuildOwner buildOwner =
        BuildOwner(focusManager: FocusManager(), onBuildScheduled: () {});

    pipelineOwner.rootNode = renderView;
    renderView.prepareInitialFrame();

    final RenderObjectToWidgetElement<RenderBox> rootElement =
        RenderObjectToWidgetAdapter<RenderBox>(
            container: repaintBoundary,
            child: Directionality(
              textDirection: TextDirection.ltr,
              child: child,
            )).attachToRenderTree(
      buildOwner,
    );

    buildOwner.buildScope(
      rootElement,
    );
    buildOwner.finalizeTree();

    pipelineOwner.flushLayout();
    pipelineOwner.flushCompositingBits();
    pipelineOwner.flushPaint();

    return rootElement.size;
  }