Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple Initial Dates #153

Closed
LenvZyl opened this issue Nov 13, 2019 · 21 comments
Closed

Multiple Initial Dates #153

LenvZyl opened this issue Nov 13, 2019 · 21 comments

Comments

@LenvZyl
Copy link

LenvZyl commented Nov 13, 2019

Are there any work arounds for Multiple Initial dates

@aleksanderwozniak
Copy link
Owner

Do you mean selection of multiple dates at once?

@trankhacvy
Copy link

@aleksanderwozniak yes. Do you have any way to achieve this image below ?
Screen Shot 2019-11-17 at 21 28 01

@aleksanderwozniak
Copy link
Owner

Ah I see... Unfortunately there is no built-in support for multiple date selection at this moment. I will be adding it in the future.

@LenvZyl
Copy link
Author

LenvZyl commented Nov 22, 2019

So What I did show multiple selected dates was to add the selected dates to events, and then modify the markersBuilder to display as if the date was selected.
And then when selecting a date, I am just adding it to events.
Works perfectly!

@andressilvac
Copy link

@LenvZyl that sounds great. Could you please share with us the code of your implementation?
Thank you!

@aleksanderwozniak
Copy link
Owner

I have added multiple selection in 3.0.0-beta branch 🚀

Here is how it works. You can use either multi-selection by tapping on individual dates, or you can select a continuous range of dates by tapping on a startDate and endDate.

Multi-selection Range selection

To use multi-selection:

  • Use onDaySelected callback to get the selected day. Add it to a collection (I'd recommend a Set*).
  • Use selectedDayPredicate to tell the TableCalendar widget which days are selected. You will be given a DateTime object, so just check if it is present in your day collection - and return true/false.
  • To clean the selection, you just need to reset the data inside collection (use .clear() on it). Rebuild the widget afterwards.

* Actually, a LinkedHashSet with equality override would be even better - to compare just the date-part of DateTime object, as their time-part is redundant for us.

Here is a complete example containing all of this.

To use range selection:

  • Use rangeSelectionMode property. Set it to either RangeSelectionMode.toggledOn* or RangeSelectionMode.enforced.
  • Create rangeStart and rangeEnd DateTime fields. Pass them to TableCalendar widget.
  • Use onRangeSelected callback. Update both fields inside it. Rebuild the widget afterwards.

* Range selection can be toggled on/off by longpressing a day cell in TableCalendar. When range selection mode is on, tapping will return ranges (onRangeSelected). When range selection is off, tapping will return single days (onDaySelected). To disable toggling on longpress, use RangeSelectionMode.enforced (always on), or RangeSelectionMode.disabled (always off).

Here is a simple example. Here is a bit more complex example, starting in RangeSelectionMode.toggledOff state.


To use 3.0.0-beta branch, add this to pubspec.yaml:

  table_calendar:
    git:
      url: git://github.com/aleksanderwozniak/table_calendar.git
      ref: 3.0.0-beta

@mees-brenzie
Copy link

I have added multiple selection in 3.0.0-beta branch 🚀

Here is how it works. You can use either multi-selection by tapping on individual dates, or you can select a continuous range of dates by tapping on a startDate and endDate.

Multi-selection Range selection

To use multi-selection:

  • Use onDaySelected callback to get the selected day. Add it to a collection (I'd recommend a Set*).
  • Use selectedDayPredicate to tell the TableCalendar widget which days are selected. You will be given a DateTime object, so just check if it is present in your day collection - and return true/false.
  • To clean the selection, you just need to reset the data inside collection (use .clear() on it). Rebuild the widget afterwards.

* Actually, a LinkedHashSet with equality override would be even better - to compare just the date-part of DateTime object, as their time-part is redundant for us.

Here is a complete example containing all of this.

To use range selection:

  • Use rangeSelectionMode property. Set it to either RangeSelectionMode.toggledOn* or RangeSelectionMode.enforced.
  • Create rangeStart and rangeEnd DateTime fields. Pass them to TableCalendar widget.
  • Use onRangeSelected callback. Update both fields inside it. Rebuild the widget afterwards.

* Range selection can be toggled on/off by longpressing a day cell in TableCalendar. When range selection mode is on, tapping will return ranges (onRangeSelected). When range selection is off, tapping will return single days (onDaySelected). To disable toggling on longpress, use RangeSelectionMode.enforced (always on), or RangeSelectionMode.disabled (always off).

Here is a simple example. Here is a bit more complex example, starting in RangeSelectionMode.toggledOff state.

To use 3.0.0-beta branch, add this to pubspec.yaml:

  table_calendar:
    git:
      url: git://github.com/aleksanderwozniak/table_calendar.git
      ref: 3.0.0-beta

@aleksanderwozniak Thank you very much for this feature! I really needed this.

I was looking into the beta branch and I noticed a lot of breaking changes. For example the CalendarController is now missing, the events list has been replaced with the eventLoader, the styling has been moved to one single object. I was wondering if there is a changelog available for this update, so I can see what the entirety of the update looks like before updating to the new version.

Thank you!

@aleksanderwozniak
Copy link
Owner

@mees-brenzie
v3.0.0 update is definitely a big one. I will be writing a proper guide for it, to explain some stuff. Examples are also a great way to have a look at new API.

Although it may seem like migrating would require a lot of work, it actually should be a rather straightforward and quick process. Many concepts from previous versions still apply, but are used in more flexible and performant ways.

Here is a list of some key features I thought were worth mentioning. You will find a list of most important API modifications underneath it.


Key features:

  • Improved horizontal scrolling animation (no 'blank page' in between two pages).
  • Horizontal swipe boundaries (firstDay and lastDay).
  • Date range selection + background highlight.
  • Multiple date selection.
  • Selective builders - you can now return null in a builder, which will in turn make the widget use a default style. This allows you to selectively override day cell UI - more on that coming soon in examples. This also regards any builder - be it a day cell, a dow cell or a marker.
  • Improved widget performance.
  • Proper state handling on widget rebuilds - CalendarController posed some problems in previous versions.
  • In general, more concise API.

Key modifications:

  • CalendarController is gone. This results in a couple of changes:
    • initialSelectedDay became selectedDayPredicate. This allows to select multiple dates at once. Note that you also can have no selected date now. Basic usage is to store a _selectedDay field and update it via onDaySelected callback.
      selectedDayPredicate: (day) {
      // Use `selectedDayPredicate` to determine which day is currently selected.
      // If this returns true, then `day` will be marked as selected.
      // Using `isSameDay` is recommended to disregard
      // the time-part of compared DateTime objects.
      return isSameDay(_selectedDay, day);
      },
      selectedDayPredicate: (day) {
      // Use values from Set to mark multiple days as selected
      return _selectedDays.contains(day);
      },
    • There is a new required property called focusedDay - it is used to determine which year/month should be currently in view.
    • initialCalendarFormat became calendarFormat. To set the current format manually, just rebuild the widget with updated value.
    • Methods like calendarController.previousPage() and calendarController.nextPage() are replaced by a PageController. You can obtain it from the widget by using onCalendarCreated callback. Its disposing is handled by the widget, so no need to do it manually.
    • You can no longer use properties like visibleDays, visibleEvents or visibleHolidays - this is something that you have to handle yourself if needed.
  • startDay and endDay are renamed to firstDay and lastDay respectively, and are both required. This allows to setup a range of available days; users will not be able to scroll past them.
  • events map is replaced by eventLoader, which gives you a DateTime object and expects you to return a list of events that you want to assign to that date. You can still use the original map for that - but you have much more freedom now. Note that LinkedHashMap is the recommended map type to use.
    eventLoader: _getEventsForDay,
    List<Event> _getEventsForDay(DateTime day) {
    // Implementation example
    return kEvents[day] ?? [];
    }
  • holidays map is replaced by holidayPredicate, which operates in the same manner that selectedDayPredicate does.
  • Callbacks don't return lists of events and holidays. If you need a list of events in onDaySelected, you can call the same thing you are using in eventLoader for single day selection, or you can adapt it for multiple day selection.
    void _onDaySelected(DateTime selectedDay, DateTime focusedDay) {
    // Update values in a Set
    setState(() {
    if (_selectedDays.contains(selectedDay)) {
    _selectedDays.remove(selectedDay);
    } else {
    _selectedDays.add(selectedDay);
    }
    });
    _selectedEvents.value = _getEventsForDays(_selectedDays);
    _focusedDay = focusedDay;
    }

@aleksanderwozniak
Copy link
Owner

Null safe 3.0.0 prerelease has been uploaded to pub.dev: https://pub.dev/packages/table_calendar/versions/3.0.0-nullsafety.0

@chitgoks
Copy link

chitgoks commented Apr 8, 2021

Before, onCalendarCreated returns the first day, last day. But now it returns a PageController. but there is no function or property in the page controller to get the first day and last day. Is this possible?

And ... the readme is fairly lacking. it does not mention about onVisibleDaysChanged and how to migrate to the new one.

@aleksanderwozniak
Copy link
Owner

Before, onCalendarCreated returns the first day, last day. But now it returns a PageController. but there is no function or property in the page controller to get the first day and last day. Is this possible?

Your question was already asked in #441.

And ... the readme is fairly lacking. it does not mention about onVisibleDaysChanged and how to migrate to the new one.

As mentioned in #265 (comment), onVisibleDaysChanged is now split into onPageChanged and onFormatChanged. Both of them belong to the basic setup of TableCalendar, and as such are present in every example.

@MrJai
Copy link

MrJai commented Apr 26, 2021

  • CalendarController is gone. This results in a couple of changes:

    • initialSelectedDay became selectedDayPredicate. This allows to select multiple dates at once. Note that you also can have no selected date now. Basic usage is to store a _selectedDay field and update it via onDaySelected callback.
      selectedDayPredicate: (day) {
      // Use `selectedDayPredicate` to determine which day is currently selected.
      // If this returns true, then `day` will be marked as selected.
      // Using `isSameDay` is recommended to disregard
      // the time-part of compared DateTime objects.
      return isSameDay(_selectedDay, day);
      },

      selectedDayPredicate: (day) {
      // Use values from Set to mark multiple days as selected
      return _selectedDays.contains(day);
      },
    • There is a new required property called focusedDay - it is used to determine which year/month should be currently in view.
    • initialCalendarFormat became calendarFormat. To set the current format manually, just rebuild the widget with updated value.
    • Methods like calendarController.previousPage() and calendarController.nextPage() are replaced by a PageController. You can obtain it from the widget by using onCalendarCreated callback. Its disposing is handled by the widget, so no need to do it manually.
    • You can no longer use properties like visibleDays, visibleEvents or visibleHolidays - this is something that you have to handle yourself if needed.

Thank you @aleksanderwozniak for such a detailed comment, explaining almost everything. One function that was very important to my app was calendarController.setSelectedDate. That helped me to manually trigger the date update. How can I update the selected date now without the controller, should I be using selectedDayPredicate?

Thanks,

@aleksanderwozniak
Copy link
Owner

@MrJai
Correct, same concept applies as here.

@vytautas-pranskunas-
Copy link

vytautas-pranskunas- commented May 3, 2021

I have few a questions

  1. onVisibleDaysChanged used to return from / to dates which i used to laod range of events. no onPageChanged returns just focusedDate - what is it? first daty of the month or? How can i get last day of that page - shoudl i calculate it my self?

  2. Previously there was CalendarController It is gone now... I used to use: setCalendarFormat before. How can is et calendar format dynmically now?

  3. style migration:

  • is weekdayStyle == defaultTextStyle?
  • These are missing - how should i repleace them?
    • outsideStyle - i see outsideDecoration but it misses fontSize because it is boxDecoration
    • unavailableStyle
    • outsideWeekendStyle
    • contentPadding

@MrJai
Copy link

MrJai commented May 3, 2021

I have 2 a questions

  1. onVisibleDaysChanged used to return from / to dates which i used to laod range of events. no onPageChanged returns just focusedDate - what is it? first daty of the month or? How can i get last day of that page - shoudl i calculate it my self?
  2. Previously there was CalendarController It is gone now... I used to use: setCalendarFormat before. How can is et calendar format dynmically now?

@vytautas-pranskunas- Let me try and reply to both questions, @aleksanderwozniak can correct me.

  1. onVisibleDaysChanged now returns focusedDate that is actually the same date in previous or next view. For example if you are looking at week view and now Monday, May 3, 2021 is focused, if you swipe right you will have Monday, April 26, 2021 as focused day, and for future you will have Monday, May 10, 2021 as focusedDay. Ofcourse you can use this info to get start and end of the week/month depending on your view. I used to use onVisibleDaysChanged to get last Sunday when going in past and next Monday when going to future. Now I am doing it like this.
DateTime getLastSunday(DateTime focusedDay) {
  return focusedDay.add(Duration(days: (7 - focusedDay.weekday)));
}

DateTime nextMonday(DateTime focusedDay) {
  return focusedDay.subtract(Duration(days: (focusedDay.weekday - 1)));
}

  1. You can see the updated setCalendarFormat, or CalendarFormat flow in general, you can check this doc.
calendarFormat: _calendarFormat,
onFormatChanged: (format) {
  setState(() {
    _calendarFormat = format;
  });
},

@desmeit
Copy link

desmeit commented May 4, 2021

Is it also possible to set multiple Range selection with this plugin?
I want to select a range from January 1 to February 12 and March 14 to March 30 for example.
Is there an example for this purpose?

@aleksanderwozniak
Copy link
Owner

@vytautas-pranskunas-

I have few a questions

1. onVisibleDaysChanged used to return from / to dates which i used to laod range of events. no onPageChanged returns just focusedDate - what is it? first daty of the month or? How can i get last day of that page - shoudl i calculate it my self?

2. Previously there was CalendarController It is gone now... I used to use: setCalendarFormat before. How can is et calendar format dynmically now?

3. style migration:


* is weekdayStyle == defaultTextStyle?

* These are missing - how should i repleace them?
  
  * outsideStyle - i see outsideDecoration but it misses fontSize because it is boxDecoration
  * unavailableStyle
  * outsideWeekendStyle
  * contentPadding

Points 1. and 2. have been pretty much covered by @MrJai, you can additionally check #441 (comment). For point 3. refer to your original issue: #492 (comment)

@desmeit

Is it also possible to set multiple Range selection with this plugin?
I want to select a range from January 1 to February 12 and March 14 to March 30 for example.
Is there an example for this purpose?

Yes, but you will need to write your custom logic using onDaySelected and CalendarBuilders. All available examples are here.

@desmeit
Copy link

desmeit commented May 5, 2021

Thanks but I don't want to select ranges through onDaySelected. The ranges should already be selected from the beginning.
Should I work with rangeStartBuilder from calenderBuilders? But as I understand correctly, with the Builder you can only customize the ranges that were already defined with rangeStartDay and rangeEndDay. But this is only one range. How can I add a second and a third range? This is not clear to me from the examples. It is no problem to develop my own logic, but I don't understand how I can address different ranges.

@aleksanderwozniak
Copy link
Owner

@desmeit
Correct, you will need to skip rangeStartDay and instead do everything manually inside the builders. I'd suggest using prioritizedBuilder in your case.

The basic concept is as follows: builder will give you a DateTime object called day. Check if this day belongs to any of your custom date ranges - if yes, then return a custom widget to highlights range selection. If no - return null to use the default UI.

@reju1021
Copy link

reju1021 commented Jul 22, 2021

@desmeit Did you eventually succeed implementing it? If yes, would you mind sharing your code? I'm currently trying to implement the same.

I eventually implemented it myself. You can now select multiple date ranges in the same calendar. For anyone interested a working code snippet below (@aleksanderwozniak I don't know if many people try to implement his, if so, maybe you can link this somewhere, might be useful for others.):

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:planner/const/constants.dart';
import 'package:table_calendar/table_calendar.dart';

class Event {

  DateTime startDate;
  DateTime endDate;

  Event({this.startDate, this.endDate});

}



class AddEvent2 extends StatefulWidget {
  AddEventState2 createState() {return new AddEventState2();}
}

class AddEventState2 extends State<AddEvent2> {

  Event event = new Event();
  CalendarFormat calendarFormat = CalendarFormat.month;
  RangeSelectionMode rangeSelectionMode = RangeSelectionMode.enforced;
  DateTime focusedDay = DateTime.now();
  DateTime selectedDay;
  List<DateTimeRange> dateTimeRanges = [];
  CalendarStyle style = const CalendarStyle();

  @override
  void initState() {
    super.initState();
  }



  bool isSameDay(DateTime a, DateTime b) {
    if (a == null || b == null) {
      return false;
    }
    return a.year == b.year && a.month == b.month && a.day == b.day;
  }

  //checks if the list of DateTimeRanges (variable name dateTimeRanges) contains a DateTimeRange in which the day lies
  DateTimeRange dayInRange(DateTime day) {
    List<DateTimeRange> list = dateTimeRanges.where((element) => element.start.isBefore(day) && element.end.isAfter(day) || element.start.year == day.year && element.start.day == day.day  && element.start.month == day.month || element.end.year == day.year && element.end.day == day.day && element.end.month == day.month).toList();
    return list.length > 0 ? list[0] : null;
  }

  //checks if a day is between two days
  bool isInRange(DateTime day, DateTime start, DateTime end) {
    if (isSameDay(day, start) || isSameDay(day, end)) {
      return true;
    }

    if (day.isAfter(start) && day.isBefore(end)) {
      return true;
    }

    return false;
  }

  Widget getCalendar() {
    return TableCalendar(
      locale: context.locale.toString(),
      availableCalendarFormats: {CalendarFormat.month: 'Month'},
      firstDay: DateTime.now(),
      lastDay: DateTime.now().add(Duration(days: 365)),
      focusedDay: focusedDay,
      selectedDayPredicate: (day) =>
          isSameDay(selectedDay, day),
      rangeStartDay: event.startDate,
      rangeEndDay: event.endDate,
      calendarFormat: calendarFormat,
      rangeSelectionMode: rangeSelectionMode,
      calendarBuilders: CalendarBuilders(prioritizedBuilder: (context, day, focusedMonth) {
        DateTimeRange dateTimeRange = dayInRange(day);
        //if day is in any saved DateTimeRange -> show a highlighted cell
        if(dateTimeRange != null) {
          return LayoutBuilder(
              builder: (context, constraints) {
                final shorterSide = constraints.maxHeight > constraints.maxWidth
                    ? constraints.maxWidth
                    : constraints.maxHeight;

                final children = <Widget>[];

                final isWithinRange = dateTimeRange.start != null &&
                    dateTimeRange.end != null &&
                    isInRange(day, dateTimeRange.start, dateTimeRange.end);

                final isRangeStart = isSameDay(day, dateTimeRange.start);
                final isRangeEnd = isSameDay(day, dateTimeRange.end);

                if (isWithinRange) {
                  Widget rangeHighlight = Center(
                    child: Container(
                      margin: EdgeInsetsDirectional.only(
                        start: isRangeStart ? constraints.maxWidth * 0.5 : 0.0,
                        end: isRangeEnd ? constraints.maxWidth * 0.5 : 0.0,
                      ),
                      height:
                      (shorterSide - style.cellMargin.vertical) *
                          style.rangeHighlightScale,
                      color: style.rangeHighlightColor,
                    ),
                  );
                  children.add(rangeHighlight);
                }

                Widget content;

                if (isRangeStart) {
                  content = AnimatedContainer(
                    duration: Duration(milliseconds: 250),
                    margin: style.cellMargin,
                    decoration: style.rangeStartDecoration,
                    alignment: Alignment.center,
                    child: Text('${day.day}', style: style.rangeStartTextStyle),
                  );
                } else if (isRangeEnd) {
                  content = AnimatedContainer(
                      duration: Duration(milliseconds: 250),
                      margin: style.cellMargin,
                      decoration: style.rangeEndDecoration,
                      alignment: Alignment.center,
                      child: Text('${day.day}', style: style.rangeEndTextStyle)
                  );
                } else if (isWithinRange) {
                  content = AnimatedContainer(
                      duration: Duration(milliseconds: 250),
                      margin: style.cellMargin,
                      decoration: style.withinRangeDecoration,
                      alignment: Alignment.center,
                      child: Text(
                          '${day.day}', style: style.withinRangeTextStyle)
                  );
                }

                if (content != null)
                  children.add(content);

                return Stack(
                  alignment: style.markersAlignment,
                  children: children,
                  clipBehavior: style.canMarkersOverflow
                      ? Clip.none
                      : Clip.hardEdge,
                );
              });
        }
        return null;
      },),
      onDaySelected: (selDay, focDay) {
        if (!isSameDay(selectedDay, selDay)) {
          setState(() {
            selectedDay = selectedDay;
            focusedDay = focDay;
            event.startDate =
            null; // Important to clean those
            event.endDate = null;
            rangeSelectionMode =
                RangeSelectionMode.toggledOff;
          });
        }
      },
      onRangeSelected: (start, end, focDay) {
        setState(() {
          selectedDay = null;
          focusedDay = focDay;
          event.startDate = start;
          event.endDate = end;

          bool startDateInRange = false;
          bool endDateInRange = false;

          DateTimeRange range = dayInRange(event.startDate);
          if(range == null && event.endDate != null) {
            range = dayInRange(event.endDate);
            if(range != null)
              endDateInRange = true;
          } else if(range != null) {
            startDateInRange = true;
            if(event.endDate != null && dayInRange(event.endDate) != null)
              endDateInRange = true;
          }

          bool insertNewRange = true;

          if(startDateInRange) {
            if(isInRange(event.startDate, range.start, range.end)) {
              int index = dateTimeRanges.indexOf(range);
              if(!endDateInRange && event.endDate != null)
                dateTimeRanges[index] = DateTimeRange(start: event.startDate, end: event.endDate);
              else
                dateTimeRanges[index] = DateTimeRange(start: event.startDate, end: range.end);
              insertNewRange = false;
            }
          }

          if(endDateInRange) {
            if(isInRange(event.endDate, range.start, range.end)) {
              print("enddate is not null and is in range");
              int index = dateTimeRanges.indexOf(range);
              dateTimeRanges[index] = DateTimeRange(start: event.startDate, end: event.endDate);
              insertNewRange = false;
            }
          }

          if(event.startDate != null && event.endDate != null && insertNewRange) {
            dateTimeRanges.add(
                DateTimeRange(start: event.startDate, end: event.endDate));
          }

        });
      },
      onPageChanged: (focDay) {
        focusedDay = focDay;
      },
    );
  }



  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(
          "Calendar",
          style: Theme.of(context).textTheme.headline4,
        ).tr(),
      ),
      body: Padding(
        padding: EdgeInsets.all(tenPaddingW),
        child: ListView(
          children: [
            Card(
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(15.0),
              ),
              elevation: 2.0,
              child: Padding(
                padding: EdgeInsets.all(tenPaddingW),
                child: Container(
                    height: 400,
                    child:
                    getCalendar()
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

@MuhammadSufyanMalik
Copy link

Hi,
I have a list of seven(can me month) days tasks, İ would like those days to be colored, differently.
I could not find, can any one help, thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests