metamonkey
metamonkey

Reputation: 447

Flutter Stack Widget - SingleChildScrollView Causing Issues

My stack widget (fancy_on_boarding) was overflowing and/or getting cut off in landscape mode based on the clipping behavior. Any way to resolve so that the widget just scrolls in landscape and I don't get an overflow error? Background below:

I tried to add a SingleChildScrollView as I've done in other widgets, but a lot was cut off and it no longer swipes. See here:

library fancy_on_boarding;

import 'dart:async';
import 'dart:ui' as ui;

import 'package:fancy_on_boarding/src/fancy_page.dart';
import 'package:fancy_on_boarding/src/page_dragger.dart';
import 'package:fancy_on_boarding/src/page_model.dart';
import 'package:fancy_on_boarding/src/page_reveal.dart';
import 'package:fancy_on_boarding/src/pager_indicator.dart';
import 'package:flutter/material.dart';

class FancyOnBoarding extends StatefulWidget {
  final List<PageModel> pageList;
  final VoidCallback onDoneButtonPressed;
  final VoidCallback onSkipButtonPressed;
  final String doneButtonText;
  final ShapeBorder doneButtonShape;
  final TextStyle doneButtonTextStyle;
  final Color doneButtonBackgroundColor;
  final String skipButtonText;
  final TextStyle skipButtonTextStyle;
  final Color skipButtonColor;
  final bool showSkipButton;
  final double bottomMargin;
  final Widget doneButton;
  final Widget skipButton;

  FancyOnBoarding({
    @required this.pageList,
    @required this.onDoneButtonPressed,
    this.onSkipButtonPressed,
    this.doneButtonText = "Done",
    this.doneButtonShape,
    this.doneButtonTextStyle,
    this.doneButtonBackgroundColor,
    this.skipButtonText = "Skip",
    this.skipButtonTextStyle,
    this.skipButtonColor,
    this.showSkipButton = true,
    this.bottomMargin = 8.0,
    this.doneButton,
    this.skipButton,
  }) : assert(pageList.length != 0 && onDoneButtonPressed != null);

  @override
  _FancyOnBoardingState createState() => _FancyOnBoardingState();
}

class _FancyOnBoardingState extends State<FancyOnBoarding>
    with TickerProviderStateMixin {
  StreamController<SlideUpdate> slideUpdateStream;
  AnimatedPageDragger animatedPageDragger;
  List<PageModel> pageList;
  int activeIndex = 0;
  int nextPageIndex = 0;
  SlideDirection slideDirection = SlideDirection.none;
  double slidePercent = 0.0;

  bool get isRTL => ui.window.locale.languageCode.toLowerCase() == "ar";

  @override
  void initState() {
    super.initState();
    this.pageList = widget.pageList;
    this.slideUpdateStream = StreamController<SlideUpdate>();
    _listenSlideUpdate();
  }

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
        child: Stack(
      overflow: Overflow.visible,
      children: [
        FancyPage(
          model: pageList[activeIndex],
          percentVisible: 1.0,
        ),
        PageReveal(
          revealPercent: slidePercent,
          child: FancyPage(
            model: pageList[nextPageIndex],
            percentVisible: slidePercent,
          ),
        ),
        Positioned(
          bottom: widget.bottomMargin,
          child: PagerIndicator(
            isRtl: isRTL,
            viewModel: PagerIndicatorViewModel(
              pageList,
              activeIndex,
              slideDirection,
              slidePercent,
            ),
          ),
        ),
        PageDragger(
          pageLength: pageList.length - 1,
          currentIndex: activeIndex,
          canDragLeftToRight: activeIndex > 0,
          canDragRightToLeft: activeIndex < pageList.length - 1,
          slideUpdateStream: this.slideUpdateStream,
        ),
        Padding(
          padding: EdgeInsets.only(top: 425.0),
          child: Align(
            alignment: Alignment.bottomCenter,
            child: Opacity(
              opacity: opacity,
              child: widget.doneButton ??
                  FlatButton(
                    height: 45,
                    shape: widget.doneButtonShape ??
                        RoundedRectangleBorder(
                            borderRadius: BorderRadius.circular(30.0)),
                    color: widget.doneButtonBackgroundColor ??
                        const Color(0xffE7C24A), //0x88FFFFFF),
                    child: Text(
                      widget.doneButtonText,
                      style: widget.doneButtonTextStyle ??
                          const TextStyle(
                            fontFamily: 'Title',
                            color: Color(0xff012A4C), //Colors.white,
                            fontSize: 22.0,
                          ),
                    ),
                    onPressed:
                        opacity == 1.0 ? widget.onDoneButtonPressed : () {},
                  ),
            ),
            // ),
          ),
        ),
        widget.showSkipButton
            ? Padding(
                padding: EdgeInsets.only(top: 425.0),
                child: Align(
                  alignment: Alignment.bottomCenter,
                  child: widget.skipButton ??
                      FlatButton(
                        height: 25,
                        shape: widget.doneButtonShape ??
                            RoundedRectangleBorder(
                                borderRadius: BorderRadius.circular(30.0)),
                        color: widget.skipButtonColor ??
                            const Color(0xffE7C24A), //0x88FFFFFF),
                        child: Text(
                          widget.skipButtonText,
                          style: widget.skipButtonTextStyle ??
                              const TextStyle(
                                fontFamily: 'Title',
                                color: Color(0xff012A4C), //0xffE7C24A
                                fontSize: 18.0,
                              ),
                        ),
                        onPressed: widget.onSkipButtonPressed,
                      ),
                ))
            : Offstage()
      ],
    ));
  }

  _listenSlideUpdate() {
    slideUpdateStream.stream.listen((SlideUpdate event) {
      setState(() {
        if (event.updateType == UpdateType.dragging) {
          slideDirection = event.direction;
          slidePercent = event.slidePercent;

          if (slideDirection == SlideDirection.leftToRight) {
            nextPageIndex = activeIndex - 1;
          } else if (slideDirection == SlideDirection.rightToLeft) {
            nextPageIndex = activeIndex + 1;
          } else {
            nextPageIndex = activeIndex;
          }
        } else if (event.updateType == UpdateType.doneDragging) {
          if (slidePercent > 0.5) {
            animatedPageDragger = AnimatedPageDragger(
              slideDirection: slideDirection,
              transitionGoal: TransitionGoal.open,
              slidePercent: slidePercent,
              slideUpdateStream: slideUpdateStream,
              vsync: this,
            );
          } else {
            animatedPageDragger = AnimatedPageDragger(
              slideDirection: slideDirection,
              transitionGoal: TransitionGoal.close,
              slidePercent: slidePercent,
              slideUpdateStream: slideUpdateStream,
              vsync: this,
            );
            nextPageIndex = activeIndex;
          }

          animatedPageDragger.run();
        } else if (event.updateType == UpdateType.animating) {
          slideDirection = event.direction;
          slidePercent = event.slidePercent;
        } else if (event.updateType == UpdateType.doneAnimating) {
          activeIndex = nextPageIndex;

          slideDirection = SlideDirection.none;
          slidePercent = 0.0;

          animatedPageDragger.dispose();
        }
      });
    });
  }

  double get opacity {
    if (pageList.length - 2 == activeIndex &&
        slideDirection == SlideDirection.rightToLeft) return slidePercent;
    if (pageList.length - 1 == activeIndex &&
        slideDirection == SlideDirection.leftToRight) return 1 - slidePercent;
    if (pageList.length - 1 == activeIndex) return 1.0;
    return 0.0;
  }

  @override
  void dispose() {
    slideUpdateStream?.close();
    super.dispose();
  }
}

Looks and works great in portrait mode with this code:

library fancy_on_boarding;

import 'dart:async';
import 'dart:ui' as ui;

import 'package:fancy_on_boarding/src/fancy_page.dart';
import 'package:fancy_on_boarding/src/page_dragger.dart';
import 'package:fancy_on_boarding/src/page_model.dart';
import 'package:fancy_on_boarding/src/page_reveal.dart';
import 'package:fancy_on_boarding/src/pager_indicator.dart';
import 'package:flutter/material.dart';

class FancyOnBoarding extends StatefulWidget {
  final List<PageModel> pageList;
  final VoidCallback onDoneButtonPressed;
  final VoidCallback onSkipButtonPressed;
  final String doneButtonText;
  final ShapeBorder doneButtonShape;
  final TextStyle doneButtonTextStyle;
  final Color doneButtonBackgroundColor;
  final String skipButtonText;
  final TextStyle skipButtonTextStyle;
  final Color skipButtonColor;
  final bool showSkipButton;
  final double bottomMargin;
  final Widget doneButton;
  final Widget skipButton;

  FancyOnBoarding({
    @required this.pageList,
    @required this.onDoneButtonPressed,
    this.onSkipButtonPressed,
    this.doneButtonText = "Done",
    this.doneButtonShape,
    this.doneButtonTextStyle,
    this.doneButtonBackgroundColor,
    this.skipButtonText = "Skip",
    this.skipButtonTextStyle,
    this.skipButtonColor,
    this.showSkipButton = true,
    this.bottomMargin = 8.0,
    this.doneButton,
    this.skipButton,
  }) : assert(pageList.length != 0 && onDoneButtonPressed != null);

  @override
  _FancyOnBoardingState createState() => _FancyOnBoardingState();
}

class _FancyOnBoardingState extends State<FancyOnBoarding>
    with TickerProviderStateMixin {
  StreamController<SlideUpdate> slideUpdateStream;
  AnimatedPageDragger animatedPageDragger;
  List<PageModel> pageList;
  int activeIndex = 0;
  int nextPageIndex = 0;
  SlideDirection slideDirection = SlideDirection.none;
  double slidePercent = 0.0;

  bool get isRTL => ui.window.locale.languageCode.toLowerCase() == "ar";

  @override
  void initState() {
    super.initState();
    this.pageList = widget.pageList;
    this.slideUpdateStream = StreamController<SlideUpdate>();
    _listenSlideUpdate();
  }

  @override
  Widget build(BuildContext context) {
    return 
    Stack(
      clipBehavior: Clip.hardEdge,
      children: [
        FancyPage(
          model: pageList[activeIndex],
          percentVisible: 1.0,
        ),
        PageReveal(
          revealPercent: slidePercent,
          child: FancyPage(
            model: pageList[nextPageIndex],
            percentVisible: slidePercent,
          ),
        ),
        Positioned(
          bottom: widget.bottomMargin,
          child: PagerIndicator(
            isRtl: isRTL,
            viewModel: PagerIndicatorViewModel(
              pageList,
              activeIndex,
              slideDirection,
              slidePercent,
            ),
          ),
        ),
        PageDragger(
          pageLength: pageList.length - 1,
          currentIndex: activeIndex,
          canDragLeftToRight: activeIndex > 0,
          canDragRightToLeft: activeIndex < pageList.length - 1,
          slideUpdateStream: this.slideUpdateStream,
        ),
        Padding(
          padding: EdgeInsets.only(top: 425.0),
          child: Align(
            alignment: Alignment.bottomCenter,
            child: Opacity(
              opacity: opacity,
              child: widget.doneButton ??
                  FlatButton(
                    height: 45,
                    shape: widget.doneButtonShape ??
                        RoundedRectangleBorder(
                            borderRadius: BorderRadius.circular(30.0)),
                    color: widget.doneButtonBackgroundColor ??
                        const Color(0xffE7C24A), //0x88FFFFFF),
                    child: Text(
                      widget.doneButtonText,
                      style: widget.doneButtonTextStyle ??
                          const TextStyle(
                            fontFamily: 'Title',
                            color: Color(0xff012A4C), 
                            fontSize: 22.0,
                          ),
                    ),
                    onPressed:
                        opacity == 1.0 ? widget.onDoneButtonPressed : () {},
                  ),
            ),

          ),
        ),
        widget.showSkipButton
            ? Padding(
                padding: EdgeInsets.only(top: 425.0),
                child: Align(
                  alignment: Alignment.bottomCenter,
                  child: widget.skipButton ??
                      FlatButton(
                        height: 25,
                        shape: widget.doneButtonShape ??
                            RoundedRectangleBorder(
                                borderRadius: BorderRadius.circular(30.0)),
                        color: widget.skipButtonColor ??
                            const Color(0xffE7C24A), 
                        child: Text(
                          widget.skipButtonText,
                          style: widget.skipButtonTextStyle ??
                              const TextStyle(
                                fontFamily: 'Title',
                                color: Color(0xff012A4C), 
                                fontSize: 18.0,
                              ),
                        ),
                        onPressed: widget.onSkipButtonPressed,
                      ),
                ))
            : Offstage()
      ],
    );
  }

  _listenSlideUpdate() {
    slideUpdateStream.stream.listen((SlideUpdate event) {
      setState(() {
        if (event.updateType == UpdateType.dragging) {
          slideDirection = event.direction;
          slidePercent = event.slidePercent;

          if (slideDirection == SlideDirection.leftToRight) {
            nextPageIndex = activeIndex - 1;
          } else if (slideDirection == SlideDirection.rightToLeft) {
            nextPageIndex = activeIndex + 1;
          } else {
            nextPageIndex = activeIndex;
          }
        } else if (event.updateType == UpdateType.doneDragging) {
          if (slidePercent > 0.5) {
            animatedPageDragger = AnimatedPageDragger(
              slideDirection: slideDirection,
              transitionGoal: TransitionGoal.open,
              slidePercent: slidePercent,
              slideUpdateStream: slideUpdateStream,
              vsync: this,
            );
          } else {
            animatedPageDragger = AnimatedPageDragger(
              slideDirection: slideDirection,
              transitionGoal: TransitionGoal.close,
              slidePercent: slidePercent,
              slideUpdateStream: slideUpdateStream,
              vsync: this,
            );
            nextPageIndex = activeIndex;
          }

          animatedPageDragger.run();
        } else if (event.updateType == UpdateType.animating) {
          slideDirection = event.direction;
          slidePercent = event.slidePercent;
        } else if (event.updateType == UpdateType.doneAnimating) {
          activeIndex = nextPageIndex;

          slideDirection = SlideDirection.none;
          slidePercent = 0.0;

          animatedPageDragger.dispose();
        }
      });
    });
  }

  double get opacity {
    if (pageList.length - 2 == activeIndex &&
        slideDirection == SlideDirection.rightToLeft) return slidePercent;
    if (pageList.length - 1 == activeIndex &&
        slideDirection == SlideDirection.leftToRight) return 1 - slidePercent;
    if (pageList.length - 1 == activeIndex) return 1.0;
    return 0.0;
  }

  @override
  void dispose() {
    slideUpdateStream?.close();
    super.dispose();
  }
}

I am calling this Widget from another class as shown below:

    return Scaffold(
    body: FancyOnBoarding(
  doneButton: TwinkleButton(
      buttonWidth: 230,
      buttonTitle: Text(
        'CREATE',
        style: TextStyle(
          fontFamily: 'Title',
          color: Color(0xff012A4C), //0xff012A4C
          fontSize: 18,
        ),
      ),
      buttonColor: Color(0xffE7C24A),
      onclickButtonFunction: () async {
        // go();
      }),
  skipButton: TwinkleButton(
      // buttonHeight: 35,
      buttonWidth: 230,
      buttonTitle: Text(
        'CREATE',
        style: TextStyle(
          fontFamily: 'Title',
          color: Color(0xff012A4C), //0xff012A4C
          fontSize: 18,
        ),
      ),
      buttonColor: Color(0xffE7C24A),
      onclickButtonFunction: () async {
        go();
      }),
  
  pageList: pageList,
  onDoneButtonPressed: () => go(),
  onSkipButtonPressed: () => go(),

));

Upvotes: 0

Views: 4077

Answers (3)

shorol
shorol

Reputation: 990

Have you tried?

    Expanded(child: FancyOnBoarding(
        doneButton: TwinkleButton()

& please share the error message or any screenshot of it. Thanks.

Upvotes: 0

Miro
Miro

Reputation: 449

You can wrap SingleChildScrollView(...) in a Container like this:

Container(
   child: SingleChildScrollView(
          //your code here
          )
)

Then, give this Container a bound height and width like this:

Container(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
   child: Stack(
   //your code here
   )
)

Note that you will have to give the stack a smaller height if there are other children in the tree taking up space. In the images below you can see the behavior when I flip the screen. :) portrait landscape

Upvotes: 0

Mehmet Yaz
Mehmet Yaz

Reputation: 305

There are too many codes, so I may have overlooked it. But you need the stack sizing. Wrap the Stack with a SizedBox if you want, or put a SizedBox inside the stack whose size will include all other children.

Editing:

Certainly. I've tried and confirmed. As you can see in the codes below, vertical scrolling or Positioned.bottom are only possible when the Stack has bounded height. Similarly horizontal scrolling or Positioned.left possible with Stack has bounded width.

How to give bounded dimension to Stack?

Among the children of the Stack, those whose size and position are determined are placed. The rectangle containing all of these children is the render size of the Stack.

If the right , bottom, left, right parameters of Positioned are to be used in order to determine the position of one of the clidren, they must not be in the scrolling direction.However, if there is even one child whose size and position are certain, all other children are placed as a reference from the child and their sizes/positions are determined.

If there are no children of such size and position, the Stack references the BoxConstraints of its render tree. And if those constraints don't have a bound in the scroll direction, you can't enter a value in that direction (if scroll direction is Axis.vertical you can't give bottom to Positioned).

In the example below it should be one of both SizedBox. Otherwise it will not scroll (even if we remove the bottom values) or we will get an error.

(Since the constraints from the top on the horizontal are bounded height and non-scrollable, it will use the screenSize.width value even if I have entered an infinity value.)

class _StackExampleState extends State<StackExample> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SingleChildScrollView(
        child: SizedBox(
          // height: 2000,
          // width: double.infinity,
          child: Stack(
            children: [
              const SizedBox(
                height: 2000,
                width: double.infinity,
              ),
              Positioned(
                  height: 200,
                  left: 0,
                  right: 0,
                  child: Container(
                    color: Colors.red,
                  )),
              Positioned(
                  height: 200,
                  bottom: 0,
                  left: 0,
                  right: 0,
                  child: Container(
                    color: Colors.green,
                  )),
              Positioned(
                  bottom: 300,
                  height: 200,
                  left: 0,
                  right: 0,
                  child: Container(
                    color: Colors.blue,
                  )),
              Positioned(
                  height: 200,
                  right: 0,
                  left: 0,
                  child: Container(
                    color: Colors.orange,
                  ))
            ],
          ),
        ),
      ),
    );
  }
}

Upvotes: 1

Related Questions