fujidaiti / smooth_sheets

Sheet widgets with smooth motion and great flexibility.
https://pub.dev/packages/smooth_sheets
MIT License
174 stars 13 forks source link

Create NavigationSheet tutorial code using auto_router #17

Open fujidaiti opened 5 months ago

fikretsengul commented 5 months ago

Thank you for creating this great library and prioritizing the iOS stack effect.

I'm trying to get cupertino effect working with my auto route setup. I was using other custom routes other then smooth_sheets like this:

router.dart

CustomRoute(
  page: MaterialModalWrapperRoute.page,
  customRouteBuilder: materialModalRouteBuilder,
),

material_modal_builder.dart

// ignore_for_file: avoid_unused_parameters

import 'package:deps/packages/auto_route.dart';
import 'package:flutter/material.dart';

import 'material_modal_wrapper.dart';

Route<T> materialModalRouteBuilder<T>(BuildContext context, Widget child, AutoRoutePage<T> page) {
  if (page.child is! MaterialModalWrapperRoute) {
    throw ArgumentError('Child page must be of type MaterialModalWrapperRoute to use materialModalRouteBuilder.');
  }

  final dialogWrapperRoute = page.child as MaterialModalWrapperRoute;
  final config = dialogWrapperRoute.modalConfig;

  final isDismissible = config.isDismissible;
  final enableDrag = isDismissible && config.enableDrag;

  return ModalBottomSheetRoute<T>(
    settings: page,
    builder: (_) => child,
    capturedThemes: config.capturedThemes,
    barrierLabel: config.barrierLabel,
    barrierOnTapHint: config.barrierOnTapHint,
    backgroundColor: config.backgroundColor,
    elevation: config.elevation,
    shape: config.shape,
    clipBehavior: config.clipBehavior,
    constraints: config.constraints,
    modalBarrierColor: config.modalBarrierColor,
    isDismissible: isDismissible,
    enableDrag: enableDrag,
    showDragHandle: enableDrag,
    isScrollControlled: config.isScrollControlled,
    scrollControlDisabledMaxHeightRatio: config.scrollControlDisabledMaxHeightRatio,
    transitionAnimationController: config.transitionAnimationController,
    anchorPoint: config.anchorPoint,
    useSafeArea: config.useSafeArea,
  );
}

material_modal_config.dart

import 'package:flutter/material.dart';

class MaterialModalConfig {
  const MaterialModalConfig({
    this.isScrollControlled = true,
    this.modalBarrierColor = Colors.black54,
    this.showDragHandle = true,
    this.capturedThemes,
    this.barrierLabel,
    this.barrierOnTapHint,
    this.backgroundColor,
    this.elevation,
    this.shape,
    this.clipBehavior,
    this.constraints,
    this.isDismissible = true,
    this.enableDrag = true,
    this.scrollControlDisabledMaxHeightRatio = 0.7,
    this.settings,
    this.transitionAnimationController,
    this.anchorPoint,
    this.useSafeArea = true,
  });

  final bool isScrollControlled;
  final Color? modalBarrierColor;
  final bool showDragHandle;
  final CapturedThemes? capturedThemes;
  final String? barrierLabel;
  final String? barrierOnTapHint;
  final Color? backgroundColor;
  final double? elevation;
  final ShapeBorder? shape;
  final Clip? clipBehavior;
  final BoxConstraints? constraints;
  final bool isDismissible;
  final bool enableDrag;
  final double scrollControlDisabledMaxHeightRatio;
  final RouteSettings? settings;
  final AnimationController? transitionAnimationController;
  final Offset? anchorPoint;
  final bool useSafeArea;
}

material_modal_wrapper.dart

import 'package:deps/packages/auto_route.dart';
import 'package:flutter/material.dart';

import 'material_modal_config.dart';

@RoutePage()
class MaterialModalWrapperRoute extends StatelessWidget {
  const MaterialModalWrapperRoute({
    required this.builder,
    this.modalConfig = const MaterialModalConfig(),
    super.key,
  });

  final Widget Function(BuildContext context) builder;
  final MaterialModalConfig modalConfig;

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      top: false,
      right: false,
      left: false,
      child: builder(context),
    );
  }
}

using it by calling:

AutoRouter.of(context).push<T>(
  CupertinoModalWrapperRoute(
    builder: (_) => Placeholder(),
    modalConfig: config,
  ),
)

I reviewed the last commit that you implemented the iOS stack effect. However, there is no usable route support that can be used with auto route. That's why I made a correction myself as follows. I can open Modal but I can't get the iOS stack effect. (By the way, I would be very grateful if you could add auto_route support.)

router.dart

CustomRoute(
  page: CupertinoModalWrapperRoute.page,
  customRouteBuilder: cupertinoModalRouteBuilder,
),

cupertino_modal_builder.dart

// ignore_for_file: avoid_unused_parameters

import 'package:deps/packages/auto_route.dart';
import 'package:flutter/material.dart';

import 'cupertino_modal_sheet_route.dart';
import 'cupertino_modal_wrapper.dart';

Route<T> cupertinoModalRouteBuilder<T>(BuildContext context, Widget child, AutoRoutePage<T> page) {
  if (page.child is! CupertinoModalWrapperRoute) {
    throw ArgumentError('Child page must be of type CupertinoModalWrapperRoute to use cupertinoModalRouteBuilder.');
  }

  final dialogWrapperRoute = page.child as CupertinoModalWrapperRoute;
  final config = dialogWrapperRoute.modalConfig;

  return CupertinoModalSheetRoute<T>(
    settings: page,
    enablePullToDismiss: config.enablePullToDismiss,
    maintainState: config.maintainState,
    barrierDismissible: config.barrierDismissible,
    barrierLabel: config.barrierLabel,
    barrierColor: config.barrierColor,
    transitionDuration: config.transitionDuration,
    transitionCurve: config.transitionCurve,
    builder: (context) => child,
  );
}

cupertino_modal_config.dart

import 'package:flutter/material.dart';

const _cupertinoBarrierColor = Color(0x18000000);
const _cupertinoTransitionDuration = Duration(milliseconds: 300);
const _cupertinoTransitionCurve = Curves.fastEaseInToSlowEaseOut;

class CupertinoModalConfig {
  const CupertinoModalConfig({
    this.enablePullToDismiss = true,
    this.maintainState = true,
    this.barrierDismissible = true,
    this.barrierLabel,
    this.barrierColor = _cupertinoBarrierColor,
    this.transitionDuration = _cupertinoTransitionDuration,
    this.transitionCurve = _cupertinoTransitionCurve,
  });

  final Color? barrierColor;

  final bool barrierDismissible;

  final String? barrierLabel;

  final bool enablePullToDismiss;

  final bool maintainState;

  final Duration transitionDuration;

  final Curve transitionCurve;
}

cupertino_modal_wrapper.dart

import 'package:deps/packages/auto_route.dart';
import 'package:flutter/material.dart';

import 'cupertino_modal_config.dart';

@RoutePage()
class CupertinoModalWrapperRoute extends StatelessWidget {
  const CupertinoModalWrapperRoute({
    required this.builder,
    this.modalConfig = const CupertinoModalConfig(),
    super.key,
  });

  final Widget Function(BuildContext context) builder;
  final CupertinoModalConfig modalConfig;

  @override
  Widget build(BuildContext context) {
    return builder(context);
  }
}

cupertino_modal_sheet_route.dart (my refactor to use it with auto_route)

import 'dart:math' as math;

import 'package:deps/packages/smooth_sheets.dart';
import 'package:flutter/material.dart';

class _TransitionController extends ValueNotifier<double> {
  _TransitionController(super._value);

  @override
  set value(double newValue) {
    super.value = newValue.clamp(0, 1);
  }
}

double inverseLerp(double min, double max, double value) {
  return min == max ? 1.0 : (value - min) / (max - min);
}

/// A mapping of [PageRoute] to its associated [_TransitionController].
///
/// This is used to modify the transition progress of the previous route
/// from the current [_BaseCupertinoModalSheetRoute].
final _cupertinoTransitionControllerOf = <PageRoute<dynamic>, _TransitionController>{};

abstract class _CupertinoModalSheetRoute<T> extends PageRoute<T> with ModalSheetRouteMixin<T> {
  _CupertinoModalSheetRoute({super.settings});

  PageRoute<dynamic>? _previousRoute;

  @override
  void didChangePrevious(Route<dynamic>? previousRoute) {
    super.didChangePrevious(previousRoute);
    _previousRoute = previousRoute as PageRoute?;
  }

  @override
  void install() {
    super.install();
    controller!.addListener(_invalidateTransitionProgress);
    sheetController.addListener(
      _invalidateTransitionProgress,
      fireImmediately: true,
    );
  }

  @override
  void dispose() {
    sheetController.removeListener(_invalidateTransitionProgress);
    controller!.removeListener(_invalidateTransitionProgress);
    super.dispose();
  }

  void _invalidateTransitionProgress() {
    switch (controller!.status) {
      case AnimationStatus.forward:
      case AnimationStatus.completed:
        if (sheetController.metrics case final metrics?) {
          _cupertinoTransitionControllerOf[_previousRoute]?.value = math.min(
            controller!.value,
            inverseLerp(
              metrics.viewportDimensions.height / 2,
              metrics.viewportDimensions.height,
              metrics.viewPixels,
            ),
          );
        }

      case AnimationStatus.reverse:
      case AnimationStatus.dismissed:
        _cupertinoTransitionControllerOf[_previousRoute]?.value = math.min(
          controller!.value,
          _cupertinoTransitionControllerOf[_previousRoute]!.value,
        );
    }
  }

  @override
  Widget buildPage(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
  ) {
    return CupertinoModalStackedTransition(
      child: super.buildPage(context, animation, secondaryAnimation),
    );
  }
}

class CupertinoModalSheetRoute<T> extends _CupertinoModalSheetRoute<T> {
  CupertinoModalSheetRoute({
    required this.enablePullToDismiss,
    required this.maintainState,
    required this.barrierDismissible,
    required this.barrierLabel,
    required this.barrierColor,
    required this.transitionDuration,
    required this.transitionCurve,
    required this.builder,
    super.settings, // refactored this line to pass my AutoRoutePage to its super.
  });

  final WidgetBuilder builder;

  @override
  final Color? barrierColor;
  @override
  final bool barrierDismissible;
  @override
  final String? barrierLabel;
  @override
  final bool enablePullToDismiss;
  @override
  final bool maintainState;
  @override
  final Duration transitionDuration;
  @override
  final Curve transitionCurve;

  @override
  Widget buildContent(BuildContext context) => builder(context);
}

Lastly, I wrapped my MaterialApp's body which is a material Scaffold with CupertinoStackedTransition but no luck. Here is the video how it looks:

https://github.com/fujidaiti/smooth_sheets/assets/22684086/b16c4246-304b-4d3a-afdc-8038dbecf142

Can you help me @fujidaiti ?

Thank you and have a great day.

fujidaiti commented 5 months ago

I haven't read through your code entirely, but I guess that you may need to copy the entire modal/cupertino.dart file for your CupertinoModalSheetRoute to work properly. The global private variable _cupertinoTransitionControllerOf is used by several classes defined in cupertino.dart, but some of these classes are not included in your copied file. This makes it impossible to share _TransitionControllers between these components, which is crucial for the cupertino-style transition animation.

By the way, is

    super.settings, // refactored this line to pass my AutoRoutePage to its super.

the only part you have to refactor to make it work with auto_router?

fikretsengul commented 4 months ago

Fixed it. Here is the final code to make it work with auto_route:

router.dart

import 'package:deps/packages/auto_route.dart';
import 'package:flutter/material.dart';
import 'router.gr.dart';
...

// USING THIS TO OVERRIDE TRANSITION DURATION.
class CustomPageRoute<T> extends MaterialPageRoute<T> {
  CustomPageRoute({required super.builder, required super.settings});

  @override
  Duration get transitionDuration => const Duration(milliseconds: 500);
}

@AutoRouterConfig()
class FeaturesRouter extends $FeaturesRouter {
  FeaturesRouter();

  @override
  RouteType get defaultRouteType => RouteType.custom(
        customRouteBuilder: <T>(
          _,
          child,
          page,
        ) {
          return CustomPageRoute<T>(
            settings: page,
            builder: (_) {
              // ADDED THIS TO MAKE STACKING ANIMATION WORK.
              return CupertinoStackedTransition(
                cornerRadius: Tween(begin: 0, end: 16),
                child: child,
              );
            },
          );
        },
        durationInMilliseconds: 2500,
        reverseDurationInMilliseconds: 2500,
      );

  @override
  List<AutoRoute> get routes => [
        AutoRoute(
          page: SuperHandler.page,
          initial: true,
          children: [
            // ADDED THIS CUSTOM ROUTE BUILDER FOR SMOOTH_SHEETS
            CustomRoute(
              page: CupertinoModalWrapperRoute.page,
              customRouteBuilder: cupertinoModalRouteBuilder,
            ),
          ],
        ),
      ];
}

cupertino_modal_builder.dart

import 'package:deps/packages/auto_route.dart';
import 'package:flutter/material.dart';
import 'cupertino_modal_sheet_route.dart';
import 'cupertino_modal_wrapper.dart';

Route<T> cupertinoModalRouteBuilder<T>(BuildContext context, Widget child, AutoRoutePage<T> page) {
  if (page.child is! CupertinoModalWrapperRoute) {
    throw ArgumentError('Child page must be of type CupertinoModalWrapperRoute to use cupertinoModalRouteBuilder.');
  }

  final dialogWrapperRoute = page.child as CupertinoModalWrapperRoute;
  final config = dialogWrapperRoute.modalConfig;

  return CupertinoModalSheetRoute<T>(
    settings: page,
    enablePullToDismiss: config.enablePullToDismiss,
    maintainState: config.maintainState,
    barrierDismissible: config.barrierDismissible,
    barrierLabel: config.barrierLabel,
    barrierColor: config.barrierColor,
    transitionDuration: config.transitionDuration,
    transitionCurve: config.transitionCurve,
    builder: (context) => child,
  );
}

cupertino_modal_config.dart

import 'package:flutter/material.dart';

const _cupertinoBarrierColor = Color(0x18000000);
const _cupertinoTransitionDuration = Duration(milliseconds: 300);
const _cupertinoTransitionCurve = Curves.fastEaseInToSlowEaseOut;

class CupertinoModalConfig {
  const CupertinoModalConfig({
    this.enablePullToDismiss = true,
    this.maintainState = true,
    this.barrierDismissible = true,
    this.barrierLabel,
    this.barrierColor = _cupertinoBarrierColor,
    this.transitionDuration = _cupertinoTransitionDuration,
    this.transitionCurve = _cupertinoTransitionCurve,
  });

  final Color? barrierColor;
  final bool barrierDismissible;
  final String? barrierLabel;
  final bool enablePullToDismiss;
  final bool maintainState;
  final Duration transitionDuration;
  final Curve transitionCurve;
}

cupertino_modal_wrapper.dart

import 'package:deps/packages/auto_route.dart';
import 'package:flutter/material.dart';
import 'cupertino_modal_config.dart';

@RoutePage()
class CupertinoModalWrapperRoute extends StatelessWidget {
  const CupertinoModalWrapperRoute({
    required this.builder,
    this.modalConfig = const CupertinoModalConfig(),
    super.key,
  });

  final Widget Function(BuildContext context) builder;
  final CupertinoModalConfig modalConfig;

  @override
  Widget build(BuildContext context) {
    return builder(context);
  }
}

cupertino_modal_sheet_route.dart (copied from library (lib/src/modal/cupertino.dart) just to change single line)

...
class CupertinoModalSheetRoute<T> extends _CupertinoModalSheetRoute<T> {
  CupertinoModalSheetRoute({
    required this.enablePullToDismiss,
    required this.maintainState,
    required this.barrierDismissible,
    required this.barrierLabel,
    required this.barrierColor,
    required this.transitionDuration,
    required this.transitionCurve,
    required this.builder,
    super.settings, // THIS NEEDS TO BE ADDED IN ORDER TO MAKE IT COMPATIBLE WITH AUTO ROUTE.
  });

  final WidgetBuilder builder;

  @override
  final Color? barrierColor;
  @override
  final bool barrierDismissible;
  @override
  final String? barrierLabel;
  @override
  final bool enablePullToDismiss;
  @override
  final bool maintainState;
  @override
  final Duration transitionDuration;
  @override
  final Curve transitionCurve;

  @override
  Widget buildContent(BuildContext context) => builder(context);
}
...

usage:

Future<T?> showCupertinoModal<T>({
  required Widget Function(BuildContext context) builder,
  CupertinoModalConfig config = const CupertinoModalConfig(),
}) async {
  final dialog = builder(_navigator.context!);
  _addDialogVisible(dialog);

  return _navigator
      .push<T>(
        CupertinoModalWrapperRoute(
          builder: (_) => dialog,
          modalConfig: config,
        ),
      )
      .whenComplete(
        () => _removeDialogVisible(widget: dialog),
      );
}

I added comments to files where needs to be configured. Thank you for this great library.

fujidaiti commented 4 months ago

I will add the settings parameter to the constructor of CupertinoModalSheetRoute since there is no reason not to do so (#24). Thank you for posting an auto_router example!