chulwoo-park/timelines

Pressing an Individual Node

Opened this issue · 1 comments

Is there a way to change the colour of a node when it's pressed? I can't seem to see any onTap or onPress methods for a specific node.

I was able to add GestureDetector by essentially recreating a custom TimelineTileBuilder. I was specifically using the TimelineTileBuilder.connected() factory so I based it off of that.

In order to add the GestureDetector you need access to the individual TimelineTile, which is only created in a private constructor.

This is a brute force method but it may be helpful in a hack-y way:

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

/// Custom TimelineTileBuilder created to add GestureDetector to the original TimelineTileBuilder
/// Reuses code from the original with parameters based on TimelineTileBuilder.connected() factory
class CustomTimelineTileBuilder implements TimelineTileBuilder {
  CustomTimelineTileBuilder(
      {required this.itemCount,
      ContentsAlign contentsAlign = ContentsAlign.basic,
      ConnectionDirection connectionDirection = ConnectionDirection.after,
      NullableIndexedWidgetBuilder? contentsBuilder,
      NullableIndexedWidgetBuilder? oppositeContentsBuilder,
      NullableIndexedWidgetBuilder? indicatorBuilder,
      ConnectedConnectorBuilder? connectorBuilder,
      WidgetBuilder? firstConnectorBuilder,
      WidgetBuilder? lastConnectorBuilder,
      double? itemExtent,
      IndexedValueBuilder<double>? itemExtentBuilder,
      IndexedValueBuilder<double>? nodePositionBuilder,
      IndexedValueBuilder<double>? indicatorPositionBuilder,
      bool addAutomaticKeepAlives = true,
      bool addRepaintBoundaries = true,
      bool addSemanticIndexes = true,
      Function? onTap,
      Function? onLongPress}) {
    assert(
      itemExtent == null || itemExtentBuilder == null,
      'Cannot provide both a itemExtent and a itemExtentBuilder.',
    );

    final startConnectorBuilder = _createConnectedStartConnectorBuilder(
      connectionDirection: connectionDirection,
      firstConnectorBuilder: firstConnectorBuilder,
      connectorBuilder: connectorBuilder,
    );

    final endConnectorBuilder = _createConnectedEndConnectorBuilder(
      connectionDirection: connectionDirection,
      lastConnectorBuilder: lastConnectorBuilder,
      connectorBuilder: connectorBuilder,
      itemCount: itemCount,
    );

    final effectiveContentsBuilder = _createAlignedContentsBuilder(
      align: contentsAlign,
      contentsBuilder: contentsBuilder,
      oppositeContentsBuilder: oppositeContentsBuilder,
    );
    final effectiveOppositeContentsBuilder = _createAlignedContentsBuilder(
      align: contentsAlign,
      contentsBuilder: oppositeContentsBuilder,
      oppositeContentsBuilder: contentsBuilder,
    );

    _builder = (context, index) {
      final tile = GestureDetector(
        onTap: () => onTap?.call(index),
        onLongPress: () => onLongPress?.call(index),
        child: TimelineTile(
          mainAxisExtent: itemExtent ?? itemExtentBuilder?.call(context, index),
          node: TimelineNode(
            indicator: indicatorBuilder?.call(context, index) ?? Indicator.transparent(),
            startConnector: startConnectorBuilder?.call(context, index),
            endConnector: endConnectorBuilder?.call(context, index),
            position: nodePositionBuilder?.call(context, index),
            indicatorPosition: indicatorPositionBuilder?.call(context, index),
          ),
          contents: effectiveContentsBuilder(context, index),
          oppositeContents: effectiveOppositeContentsBuilder(context, index),
        ),
      );

      return tile;
    };
  }

  late IndexedWidgetBuilder _builder;

  @override
  int itemCount;

  @override
  Widget build(BuildContext context, int index) {
    return _builder(context, index);
  }

  static NullableIndexedWidgetBuilder _createConnectedStartConnectorBuilder({
    ConnectionDirection? connectionDirection,
    WidgetBuilder? firstConnectorBuilder,
    ConnectedConnectorBuilder? connectorBuilder,
  }) =>
      (context, index) {
        if (index == 0) {
          if (firstConnectorBuilder != null) {
            return firstConnectorBuilder.call(context);
          } else {
            return null;
          }
        }

        if (connectionDirection == ConnectionDirection.before) {
          return connectorBuilder?.call(context, index, ConnectorType.start);
        } else {
          return connectorBuilder?.call(context, index - 1, ConnectorType.start);
        }
      };

  static NullableIndexedWidgetBuilder _createConnectedEndConnectorBuilder({
    ConnectionDirection? connectionDirection,
    WidgetBuilder? lastConnectorBuilder,
    ConnectedConnectorBuilder? connectorBuilder,
    required int itemCount,
  }) =>
      (context, index) {
        if (index == itemCount - 1) {
          if (lastConnectorBuilder != null) {
            return lastConnectorBuilder.call(context);
          } else {
            return null;
          }
        }

        if (connectionDirection == ConnectionDirection.before) {
          return connectorBuilder?.call(context, index + 1, ConnectorType.end);
        } else {
          return connectorBuilder?.call(context, index, ConnectorType.end);
        }
      };

  static NullableIndexedWidgetBuilder _createAlignedContentsBuilder({
    required ContentsAlign align,
    NullableIndexedWidgetBuilder? contentsBuilder,
    NullableIndexedWidgetBuilder? oppositeContentsBuilder,
  }) {
    return (context, index) {
      switch (align) {
        case ContentsAlign.alternating:
          if (index.isOdd) {
            return oppositeContentsBuilder?.call(context, index);
          }

          return contentsBuilder?.call(context, index);
        case ContentsAlign.reverse:
          return oppositeContentsBuilder?.call(context, index);
        case ContentsAlign.basic:
        default:
          return contentsBuilder?.call(context, index);
      }
    };
  }
}

With this you'd get a callback to the index that was tapped or long pressed and use that information to change the theme details via changes in view state.

Please let me know if there's a better way to do this. Still getting used to Function being a first-class datatype.