From adcf29eeea830571f52389809a9786641fc603dc Mon Sep 17 00:00:00 2001 From: Ilia Kurtov Date: Tue, 18 Oct 2016 16:07:53 +0300 Subject: [PATCH] update calendar --- build.gradle | 1 + .../templates/calendar/CalendarAdapter.java | 309 ++++++++++++++++++ .../templates/calendar/CalendarDayItem.java | 87 +++++ .../templates/calendar/CalendarEmptyItem.java | 46 +++ .../calendar/CalendarHeaderItem.java | 57 ++++ .../templates/calendar/CalendarItem.java | 43 +++ .../calendar/CalendarRecyclerView.java | 73 +++++ .../templates/calendar/CalendarUtils.java | 272 +++++++++++++++ .../templates/calendar/ComparingToToday.java | 32 ++ 9 files changed, 920 insertions(+) create mode 100644 src/main/java/ru/touchin/templates/calendar/CalendarAdapter.java create mode 100644 src/main/java/ru/touchin/templates/calendar/CalendarDayItem.java create mode 100644 src/main/java/ru/touchin/templates/calendar/CalendarEmptyItem.java create mode 100644 src/main/java/ru/touchin/templates/calendar/CalendarHeaderItem.java create mode 100644 src/main/java/ru/touchin/templates/calendar/CalendarItem.java create mode 100644 src/main/java/ru/touchin/templates/calendar/CalendarRecyclerView.java create mode 100644 src/main/java/ru/touchin/templates/calendar/CalendarUtils.java create mode 100644 src/main/java/ru/touchin/templates/calendar/ComparingToToday.java diff --git a/build.gradle b/build.gradle index a92d0ed..614c165 100644 --- a/build.gradle +++ b/build.gradle @@ -28,6 +28,7 @@ dependencies { compile 'io.reactivex:rxandroid:1.2.1' provided 'com.android.support:appcompat-v7:24.2.1' + provided 'com.android.support:recyclerview-v7:24.2.1' provided 'com.squareup.retrofit2:retrofit:2.1.0' provided 'com.squareup.okhttp3:okhttp:3.4.1' diff --git a/src/main/java/ru/touchin/templates/calendar/CalendarAdapter.java b/src/main/java/ru/touchin/templates/calendar/CalendarAdapter.java new file mode 100644 index 0000000..295d6dd --- /dev/null +++ b/src/main/java/ru/touchin/templates/calendar/CalendarAdapter.java @@ -0,0 +1,309 @@ +/* + * Copyright (c) 2016 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package ru.touchin.templates.calendar; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.StaggeredGridLayoutManager; +import android.view.ViewGroup; + +import org.joda.time.DateTime; +import org.joda.time.format.DateTimeFormat; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import ru.touchin.roboswag.core.utils.ShouldNotHappenException; + +/** + * Created by Ilia Kurtov on 17/03/2016. + * Adapter for Calendar view. Use with {@link CalendarRecyclerView}. + * + * @param Type of ViewHolders of a day with a date; + * @param Type of ViewHolders of a months header; + * @param Type of ViewHolders of an empty cell. + */ +public abstract class CalendarAdapter extends RecyclerView.Adapter { + + public static final int HEADER_ITEM_TYPE = 0; + public static final int EMPTY_ITEM_TYPE = 1; + public static final int DAY_ITEM_TYPE = 2; + + public static final int MONTHS_IN_YEAR = 12; + public static final long ONE_DAY_LENGTH = TimeUnit.DAYS.toMillis(1); + + @NonNull + private final List calendarItems; + @Nullable + private Integer startSelectionPosition; + @Nullable + private Integer endSelectionPosition; + @Nullable + private String[] monthsNames; + + /** + * Constructor that takes all necessary data to initialize. + * + * @param startDate First date in the calendar range; + * @param endDate Last date (not inclusive) in the calendar range; + * @param monthsNames String array of months names where #0 is January and #11 is December. + */ + public CalendarAdapter(@NonNull final DateTime startDate, @NonNull final DateTime endDate, @Nullable final String... monthsNames) { + super(); + if (monthsNames != null && monthsNames.length == MONTHS_IN_YEAR) { + this.monthsNames = monthsNames; + } + calendarItems = CalendarUtils.fillRanges(startDate, endDate); + if (calendarItems.isEmpty()) { + throw new ShouldNotHappenException("There is no items in calendar with startDate: " + DateTimeFormat.fullDate().print(startDate) + + ", and endDate: " + DateTimeFormat.fullDate().print(endDate)); + } + } + + /** + * Set selected dates range in calendar. Call this method before attaching this adapter to {@link CalendarRecyclerView}. + * + * @param startSelectionDate First date that should be selected; + * @param endSelectionDate Last date that should be selected (inclusive). + */ + public void setSelectedRange(@Nullable final DateTime startSelectionDate, @Nullable final DateTime endSelectionDate) { + if (startSelectionDate != null) { + startSelectionPosition = CalendarUtils.findPositionByDate(calendarItems, startSelectionDate.withTimeAtStartOfDay().getMillis()); + } + if (endSelectionDate != null) { + endSelectionPosition = CalendarUtils.findPositionByDate(calendarItems, endSelectionDate.withTimeAtStartOfDay().getMillis()); + } + + notifySelectedDaysChanged(); + } + + /** + * Method finds the number of the first cell of selected range. + * + * @param departure Pass true if {@link CalendarRecyclerView} connected with this adapter should select departure (pass true) date + * or arrival (pass false). + * @return position of the cell to scroll to at the calendar view opening. + */ + @Nullable + public Integer getPositionToScroll(final boolean departure) { + if (departure && startSelectionPosition != null) { + return CalendarUtils.findPositionOfSelectedMonth(calendarItems, startSelectionPosition); + } + if (!departure && endSelectionPosition != null) { + return CalendarUtils.findPositionOfSelectedMonth(calendarItems, endSelectionPosition); + } + if (!departure && startSelectionPosition != null) { + return CalendarUtils.findPositionOfSelectedMonth(calendarItems, startSelectionPosition); + } + return null; + } + + private void notifySelectedDaysChanged() { + if (startSelectionPosition == null && endSelectionPosition == null) { + return; + } + if (startSelectionPosition == null) { + notifyItemRangeChanged(endSelectionPosition, 1); + return; + } + if (endSelectionPosition == null) { + notifyItemRangeChanged(startSelectionPosition, 1); + return; + } + notifyItemRangeChanged(startSelectionPosition, endSelectionPosition - startSelectionPosition); + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) { + switch (viewType) { + case HEADER_ITEM_TYPE: + return createHeaderViewHolder(parent); + case EMPTY_ITEM_TYPE: + return createEmptyViewHolder(parent); + case DAY_ITEM_TYPE: + return createDayViewHolder(parent); + default: + return null; + } + } + + /** + * Method that creates Header ViewHolder with type of THeaderViewHolder. + * + * @param parent {@link ViewGroup} for inflating ViewHolder; + * @return New THeaderViewHolder; + */ + protected abstract THeaderViewHolder createHeaderViewHolder(final ViewGroup parent); + + /** + * Method that creates Empty ViewHolder with type of TEmptyViewHolder. + * + * @param parent {@link ViewGroup} for inflating ViewHolder; + * @return New TEmptyViewHolder; + */ + protected abstract TEmptyViewHolder createEmptyViewHolder(final ViewGroup parent); + + /** + * Method that creates Day ViewHolder with type of TDayViewHolder. + * + * @param parent {@link ViewGroup} for inflating ViewHolder; + * @return New TDayViewHolder; + */ + protected abstract TDayViewHolder createDayViewHolder(final ViewGroup parent); + + /** + * Bind data to a Header ViewHolder. + * + * @param viewHolder ViewHolder for binding; + * @param monthName Name of month; + * @param firstMonth True if bind called for the first month in calendar. + */ + protected abstract void bindHeaderItem(@NonNull final THeaderViewHolder viewHolder, @NonNull final String monthName, final boolean firstMonth); + + /** + * Bind data to an Empty ViewHolder. + * + * @param viewHolder ViewHolder for binding; + * @param selectionMode Either {@link SelectionMode#SELECTED_MIDDLE} or {@link SelectionMode#NOT_SELECTED} can be here. + */ + protected abstract void bindEmptyItem(@NonNull final TEmptyViewHolder viewHolder, @NonNull final SelectionMode selectionMode); + + /** + * Bind data to a Day ViewHolder. + * + * @param viewHolder ViewHolder for binding; + * @param day Text with number of a day. Eg "1" or "29"; + * @param date Date of current day; + * @param selectionMode Selection mode for this item; + * @param dateState Shows calendar date state for this item. + */ + protected abstract void bindDayItem(@NonNull final TDayViewHolder viewHolder, + @NonNull final String day, + @NonNull final DateTime date, + @NonNull final SelectionMode selectionMode, + @NonNull final ComparingToToday dateState); + + @Override + @SuppressWarnings("unchecked") + public void onBindViewHolder(final RecyclerView.ViewHolder holder, final int position) { + final CalendarItem calendarItem = CalendarUtils.findItemByPosition(calendarItems, position); + + if (calendarItem instanceof CalendarHeaderItem) { + final StaggeredGridLayoutManager.LayoutParams layoutParams = + new StaggeredGridLayoutManager.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + layoutParams.setFullSpan(true); + holder.itemView.setLayoutParams(layoutParams); + final String monthName = monthsNames != null ? monthsNames[((CalendarHeaderItem) calendarItem).getMonth()] + : String.valueOf(((CalendarHeaderItem) calendarItem).getMonth()); + bindHeaderItem((THeaderViewHolder) holder, monthName, position == 0); + } else if (calendarItem instanceof CalendarEmptyItem) { + if (startSelectionPosition != null && endSelectionPosition != null + && position >= startSelectionPosition && position <= endSelectionPosition) { + bindEmptyItem((TEmptyViewHolder) holder, SelectionMode.SELECTED_MIDDLE); + } else { + bindEmptyItem((TEmptyViewHolder) holder, SelectionMode.NOT_SELECTED); + } + } else if (calendarItem instanceof CalendarDayItem) { + bindDay((TDayViewHolder) holder, position, calendarItem); + } + } + + //TODO fix suppress + @SuppressWarnings("PMD.CyclomaticComplexity") + private void bindDay(final TDayViewHolder holder, final int position, final CalendarItem calendarItem) { + final String currentDay = String.valueOf(((CalendarDayItem) calendarItem).getPositionOfFirstDay() + + position - calendarItem.getStartRange()); + final DateTime currentDate = new DateTime(((CalendarDayItem) calendarItem).getDateOfFirstDay() + + (position - calendarItem.getStartRange()) * ONE_DAY_LENGTH); + final ComparingToToday dateState = ((CalendarDayItem) calendarItem).getComparingToToday(); + if (startSelectionPosition != null && position == startSelectionPosition) { + if (endSelectionPosition == null || endSelectionPosition.equals(startSelectionPosition)) { + bindDayItem(holder, currentDay, currentDate, SelectionMode.SELECTED_ONE_ONLY, dateState); + return; + } + bindDayItem(holder, currentDay, currentDate, SelectionMode.SELECTED_FIRST, dateState); + return; + } + if (endSelectionPosition != null && position == endSelectionPosition) { + bindDayItem(holder, currentDay, currentDate, SelectionMode.SELECTED_LAST, dateState); + return; + } + if (startSelectionPosition != null && endSelectionPosition != null + && position >= startSelectionPosition && position <= endSelectionPosition) { + bindDayItem(holder, currentDay, currentDate, SelectionMode.SELECTED_MIDDLE, dateState); + return; + } + + bindDayItem(holder, currentDay, currentDate, SelectionMode.NOT_SELECTED, dateState); + } + + @Override + public int getItemViewType(final int position) { + final CalendarItem calendarItem = CalendarUtils.findItemByPosition(calendarItems, position); + + if (calendarItem instanceof CalendarHeaderItem) { + return HEADER_ITEM_TYPE; + } else if (calendarItem instanceof CalendarEmptyItem) { + return EMPTY_ITEM_TYPE; + } else if (calendarItem instanceof CalendarDayItem) { + return DAY_ITEM_TYPE; + } + + return super.getItemViewType(position); + } + + @Override + public int getItemCount() { + return calendarItems.isEmpty() ? 0 : calendarItems.get(calendarItems.size() - 1).getEndRange(); + } + + /** + * Selection mode that shows the type of selection of a calendar cell. + */ + public enum SelectionMode { + + /** + * Selection mode for the case when first date in the calendar range selected + * (not first and last simultaneously; for this purpose see {@link #SELECTED_ONE_ONLY}). + */ + SELECTED_FIRST, + /** + * Selection mode for the case when date in a middle of the calendar range selected. + */ + SELECTED_MIDDLE, + /** + * Selection mode for the case when last date in the calendar range selected + * (not last and first simultaneously; for this purpose see {@link #SELECTED_ONE_ONLY}). + */ + SELECTED_LAST, + /** + * Selection mode for the case when only one date selected. + */ + SELECTED_ONE_ONLY, + /** + * Selection mode for the case when nothing selected. + */ + NOT_SELECTED + + } + +} \ No newline at end of file diff --git a/src/main/java/ru/touchin/templates/calendar/CalendarDayItem.java b/src/main/java/ru/touchin/templates/calendar/CalendarDayItem.java new file mode 100644 index 0000000..3840e7e --- /dev/null +++ b/src/main/java/ru/touchin/templates/calendar/CalendarDayItem.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2016 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package ru.touchin.templates.calendar; + +import android.support.annotation.NonNull; + +/** + * Created by Ilia Kurtov on 17/03/2016. + * Calendar header item for showing headers for months in calendar. + */ +public class CalendarDayItem implements CalendarItem { + + private final long dateOfFirstDay; + private final int positionOfFirstDate; + private final int startRange; + private final int endRange; + @NonNull + private final ComparingToToday comparingToToday; + + public CalendarDayItem(final long dateOfFirstDay, + final int positionOfFirstDate, + final int startRange, + final int endRange, + @NonNull final ComparingToToday comparingToToday) { + this.dateOfFirstDay = dateOfFirstDay; + this.positionOfFirstDate = positionOfFirstDate; + this.startRange = startRange; + this.endRange = endRange; + this.comparingToToday = comparingToToday; + } + + /** + * Returns date of the first date in millis in this calendar range. + * + * @return Date of first date in this item in millis. + */ + public long getDateOfFirstDay() { + return dateOfFirstDay; + } + + /** + * Returns position of calendar cell for the first date. + * + * @return Position of calendar cell for the first date. + */ + public int getPositionOfFirstDay() { + return positionOfFirstDate; + } + + @Override + public int getStartRange() { + return startRange; + } + + @Override + public int getEndRange() { + return endRange; + } + + /** + * Returns comparison of current item to today. + * + * @return comparison of current item to today. + */ + @NonNull + public ComparingToToday getComparingToToday() { + return comparingToToday; + } + +} \ No newline at end of file diff --git a/src/main/java/ru/touchin/templates/calendar/CalendarEmptyItem.java b/src/main/java/ru/touchin/templates/calendar/CalendarEmptyItem.java new file mode 100644 index 0000000..2844b2d --- /dev/null +++ b/src/main/java/ru/touchin/templates/calendar/CalendarEmptyItem.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2016 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package ru.touchin.templates.calendar; + +/** + * Created by Ilia Kurtov on 17/03/2016. + * Calendar item for showing empty cells in calendar. + */ +public class CalendarEmptyItem implements CalendarItem { + + private final int startRange; + private final int endRange; + + public CalendarEmptyItem(final int startRange, final int endRange) { + this.startRange = startRange; + this.endRange = endRange; + } + + @Override + public int getStartRange() { + return startRange; + } + + @Override + public int getEndRange() { + return endRange; + } + +} \ No newline at end of file diff --git a/src/main/java/ru/touchin/templates/calendar/CalendarHeaderItem.java b/src/main/java/ru/touchin/templates/calendar/CalendarHeaderItem.java new file mode 100644 index 0000000..c7ac7f8 --- /dev/null +++ b/src/main/java/ru/touchin/templates/calendar/CalendarHeaderItem.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2016 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package ru.touchin.templates.calendar; + +/** + * Created by Ilia Kurtov on 17/03/2016. + * Calendar header item for showing headers for months in calendar. + */ +public class CalendarHeaderItem implements CalendarItem { + + private final int month; + private final int startRange; + private final int endRange; + + public CalendarHeaderItem(final int month, final int startRange, final int endRange) { + this.month = month; + this.startRange = startRange; + this.endRange = endRange; + } + + /** + * Returns number of month (where 0 is January and 11 is December). + * + * @return Number of month (where 0 is January and 11 is December). + */ + public int getMonth() { + return month; + } + + @Override + public int getStartRange() { + return startRange; + } + + @Override + public int getEndRange() { + return endRange; + } + +} \ No newline at end of file diff --git a/src/main/java/ru/touchin/templates/calendar/CalendarItem.java b/src/main/java/ru/touchin/templates/calendar/CalendarItem.java new file mode 100644 index 0000000..cc8638d --- /dev/null +++ b/src/main/java/ru/touchin/templates/calendar/CalendarItem.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2016 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package ru.touchin.templates.calendar; + +/** + * Created by Ilia Kurtov on 17/03/2016. + * Interface for items for {@link CalendarAdapter}. Instead of storing data about all calendar cells separately, + * it sores list of this items. CalendarItem represents range with the same calendar items. + */ +public interface CalendarItem { + + /** + * Returns number of starting cell of this range. + * + * @return number of starting cell of this range. + */ + int getStartRange(); + + /** + * Returns number of ending cell of this range. + * + * @return number of ending cell of this range. + */ + int getEndRange(); + +} \ No newline at end of file diff --git a/src/main/java/ru/touchin/templates/calendar/CalendarRecyclerView.java b/src/main/java/ru/touchin/templates/calendar/CalendarRecyclerView.java new file mode 100644 index 0000000..961dd6d --- /dev/null +++ b/src/main/java/ru/touchin/templates/calendar/CalendarRecyclerView.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2016 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package ru.touchin.templates.calendar; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; + +import ru.touchin.roboswag.core.log.Lc; + +/** + * Created by Ilia Kurtov on 17/03/2016. + * Specific {@link RecyclerView} that works with {@link CalendarAdapter}. It optimizes speed of the calendar. + */ +public class CalendarRecyclerView extends RecyclerView { + + private static final int HEADER_MAX_ELEMENTS_IN_A_ROW = 1; + private static final int EMPTY_MAX_ELEMENTS_IN_A_ROW = 6; + private static final int DAY_MAX_ELEMENTS_IN_A_ROW = 7; + + public CalendarRecyclerView(@NonNull final Context context) { + this(context, null); + } + + public CalendarRecyclerView(@NonNull final Context context, @Nullable final AttributeSet attrs) { + this(context, attrs, 0); + } + + public CalendarRecyclerView(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyle) { + super(context, attrs, defStyle); + getRecycledViewPool().setMaxRecycledViews(CalendarAdapter.HEADER_ITEM_TYPE, HEADER_MAX_ELEMENTS_IN_A_ROW * 3); + getRecycledViewPool().setMaxRecycledViews(CalendarAdapter.EMPTY_ITEM_TYPE, EMPTY_MAX_ELEMENTS_IN_A_ROW * 3); + getRecycledViewPool().setMaxRecycledViews(CalendarAdapter.DAY_ITEM_TYPE, DAY_MAX_ELEMENTS_IN_A_ROW * 3); + setItemViewCacheSize(0); + } + + /** + * Used to set adapter that extends from {@link CalendarAdapter}. + * + * @param calendarAdapter Adapter that extends from {@link CalendarAdapter}. + */ + // This suppress needed for using only specific CalendarAdapter} + @SuppressWarnings("PMD.UselessOverridingMethod") + public void setAdapter(@NonNull final CalendarAdapter calendarAdapter) { + super.setAdapter(calendarAdapter); + } + + @Override + @Deprecated + public void setAdapter(final Adapter adapter) { + Lc.assertion("Unsupported adapter class. Use CalendarAdapter instead."); + } + +} \ No newline at end of file diff --git a/src/main/java/ru/touchin/templates/calendar/CalendarUtils.java b/src/main/java/ru/touchin/templates/calendar/CalendarUtils.java new file mode 100644 index 0000000..99678a6 --- /dev/null +++ b/src/main/java/ru/touchin/templates/calendar/CalendarUtils.java @@ -0,0 +1,272 @@ +/* + * Copyright (c) 2016 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package ru.touchin.templates.calendar; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.joda.time.DateTime; +import org.joda.time.DateTimeFieldType; +import org.joda.time.Days; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import ru.touchin.roboswag.core.log.Lc; + +/** + * Created by Ilia Kurtov on 17/03/2016. + * Utility class to simplify working with {@link CalendarAdapter}. + */ +public final class CalendarUtils { + + private static final int DAYS_IN_WEEK = 7; + private static final long ONE_DAY = TimeUnit.DAYS.toMillis(1); + + /** + * Method finds CalendarItem for specified position. Find process is optimized and use binary search algorithm. + * + * @param calendarItems List of {@link CalendarItem} where need to find specific element; + * @param position Position of adapter; + * @return CalendarItem for specified position. + */ + @Nullable + public static CalendarItem findItemByPosition(@NonNull final List calendarItems, final long position) { + return find(calendarItems, position, false); + } + + /** + * Method finds position of Header that respond to requested position. + * + * @param calendarItems List of {@link CalendarItem} where need to find specific element; + * @param position Position of adapter; + * @return Position of Header that respond to requested position. + * Returns null if Header or related CalendarItem was not found for specified position. + */ + @Nullable + public static Integer findPositionOfSelectedMonth(@NonNull final List calendarItems, final long position) { + final CalendarItem calendarItem = find(calendarItems, position, true); + if (calendarItem != null) { + return calendarItem.getStartRange(); + } + return null; + } + + /** + * Method finds position of calendar cell that respond to specified date. + * + * @param calendarItems List of {@link CalendarItem} where need to find specific element; + * @param date Requested date in milliseconds. + * @return Position of Calendar cell that that has specific date. + * Returns null if CalendarItem was not found for specified position. + */ + @Nullable + public static Integer findPositionByDate(@NonNull final List calendarItems, final long date) { + int low = 0; + int high = calendarItems.size() - 1; + int addition = 0; + float count = 0; + while (true) { + final int mid = (low + high) / 2 + addition; + if (calendarItems.get(mid) instanceof CalendarDayItem) { + if (date < ((CalendarDayItem) calendarItems.get(mid)).getDateOfFirstDay()) { + if (mid == 0) { + Lc.assertion("Selected date smaller then min date in calendar"); + return null; + } + high = mid - 1; + } else { + final long endDate = ((CalendarDayItem) calendarItems.get(mid)).getDateOfFirstDay() + + (calendarItems.get(mid).getEndRange() - calendarItems.get(mid).getStartRange()) * ONE_DAY; + if (date > endDate) { + if (mid == calendarItems.size()) { + Lc.assertion("Selected date bigger then max date in calendar"); + return null; + } + low = mid + 1; + } else { + return (int) (calendarItems.get(mid).getStartRange() + + (date - (((CalendarDayItem) calendarItems.get(mid)).getDateOfFirstDay())) / ONE_DAY); + } + } + count = 0; + addition = 0; + } else { + count++; + addition = ((int) Math.ceil(count / 2)) * ((int) (StrictMath.pow(-1, (count - 1)))); + } + } + } + + /** + * Create list of {@link CalendarItem} according to start and end Dates. + * + * @param startDate Start date of the range; + * @param endDate End date of the range; + * @return List of CalendarItems that could be one of these: {@link CalendarHeaderItem}, {@link CalendarDayItem} or {@link CalendarEmptyItem}. + */ + @NonNull + @SuppressWarnings("checkstyle:MethodLength") + public static List fillRanges(@NonNull final DateTime startDate, @NonNull final DateTime endDate) { + final DateTime cleanStartDate = startDate.withTimeAtStartOfDay(); + final DateTime cleanEndDate = endDate.plusDays(1).withTimeAtStartOfDay(); + + DateTime tempTime = cleanStartDate; + + final List calendarItems = fillCalendarTillCurrentDate(cleanStartDate, tempTime); + + tempTime = tempTime.plusDays(Days.ONE.getDays()); + + final int totalDaysCount = Days.daysBetween(tempTime, cleanEndDate).getDays(); + int shift = calendarItems.get(calendarItems.size() - 1).getEndRange(); + int firstDate = tempTime.getDayOfMonth() - 1; + int daysEnded = 1; + + while (true) { + final int daysInCurrentMonth = tempTime.dayOfMonth().getMaximumValue(); + final long firstRangeDate = tempTime.getMillis(); + + if ((daysEnded + (daysInCurrentMonth - firstDate)) <= totalDaysCount) { + tempTime = tempTime.plusMonths(1).withDayOfMonth(1); + + calendarItems.add(new CalendarDayItem(firstRangeDate, firstDate + 1, shift + daysEnded, + shift + daysEnded + (daysInCurrentMonth - firstDate) - 1, ComparingToToday.AFTER_TODAY)); + daysEnded += daysInCurrentMonth - firstDate; + if (daysEnded == totalDaysCount) { + break; + } + firstDate = 0; + + final int firstDayInWeek = tempTime.getDayOfWeek() - 1; + + if (firstDayInWeek != 0) { + calendarItems.add(new CalendarEmptyItem(shift + daysEnded, shift + daysEnded + (DAYS_IN_WEEK - firstDayInWeek - 1))); + shift += (DAYS_IN_WEEK - firstDayInWeek); + } + + calendarItems.add(new CalendarHeaderItem(tempTime.getMonthOfYear() - 1, shift + daysEnded, shift + daysEnded)); + shift += 1; + + if (firstDayInWeek != 0) { + calendarItems.add(new CalendarEmptyItem(shift + daysEnded, shift + daysEnded + firstDayInWeek - 1)); + shift += firstDayInWeek; + } + + } else { + calendarItems.add(new CalendarDayItem(firstRangeDate, firstDate + 1, shift + daysEnded, shift + totalDaysCount, + ComparingToToday.AFTER_TODAY)); + break; + } + } + + return calendarItems; + } + + @Nullable + @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.ModifiedCyclomaticComplexity", "PMD.StdCyclomaticComplexity"}) + private static CalendarItem find(@NonNull final List calendarItems, final long position, final boolean getHeaderPosition) { + int low = 0; + int high = calendarItems.size() - 1; + while (true) { + final int mid = (low + high) / 2; + if (position < calendarItems.get(mid).getStartRange()) { + if (mid == 0 || position > calendarItems.get(mid - 1).getEndRange()) { + Lc.assertion("CalendarAdapter cannot find item with that position"); + return null; + } + high = mid - 1; + } else if (position > calendarItems.get(mid).getEndRange()) { + if (mid == calendarItems.size() || position < calendarItems.get(mid + 1).getStartRange()) { + Lc.assertion("CalendarAdapter cannot find item with that position"); + return null; + } + low = mid + 1; + } else { + if (getHeaderPosition) { + int calendarShift = mid; + while (true) { + calendarShift--; + if (calendarShift == -1) { + return null; + } + if (calendarItems.get(calendarShift) instanceof CalendarHeaderItem) { + return calendarItems.get(calendarShift); + } + } + } + return calendarItems.get(mid); + } + } + } + + @NonNull + private static List fillCalendarTillCurrentDate(@NonNull final DateTime cleanStartDate, @NonNull final DateTime startDate) { + DateTime temp = startDate; + final List calendarItems = new ArrayList<>(); + int shift = 0; + final int firstDate = temp.getDayOfMonth() - 1; //?? - 1 ? + + // add first month header + calendarItems.add(new CalendarHeaderItem(temp.get(DateTimeFieldType.monthOfYear()) - 1, shift, shift)); // is Month starts from 1 or 0 ? + temp = temp.withDayOfMonth(1); + shift += 1; + + final int firstDayInTheWeek = temp.getDayOfWeek() - 1; + + // check if first day is Monday. If not - add empty items. Otherwise do nothing + if (firstDayInTheWeek != 0) { + calendarItems.add(new CalendarEmptyItem(shift, shift + firstDayInTheWeek - 1)); + } + shift += firstDayInTheWeek; + + // add range with days before today + calendarItems.add(new CalendarDayItem(temp.getMillis(), 1, shift, shift + firstDate - 1, ComparingToToday.BEFORE_TODAY)); + shift += firstDate; + + // add today item + temp = cleanStartDate; + calendarItems.add(new CalendarDayItem(temp.getMillis(), firstDate + 1, shift, shift, ComparingToToday.TODAY)); + + //add empty items and header if current day the last day in the month + if (temp.getDayOfMonth() == temp.dayOfMonth().getMaximumValue()) { + addItemsIfCurrentDayTheLastDayInTheMonth(startDate, calendarItems); + } + + return calendarItems; + } + + private static void addItemsIfCurrentDayTheLastDayInTheMonth(@NonNull final DateTime dateTime, + @NonNull final List calendarItems) { + + int shift = calendarItems.get(calendarItems.size() - 1).getEndRange(); + final DateTime nextMonthFirstDay = dateTime.plusDays(1); + final int firstFayInNextMonth = nextMonthFirstDay.getDayOfWeek() - 1; + calendarItems.add(new CalendarEmptyItem(shift, shift + (7 - firstFayInNextMonth) - 1)); + shift += 7 - firstFayInNextMonth; + calendarItems.add(new CalendarHeaderItem(nextMonthFirstDay.getMonthOfYear() + 1, shift, shift)); + shift += 1; + calendarItems.add(new CalendarEmptyItem(shift, shift + firstFayInNextMonth - 1)); + } + + private CalendarUtils() { + } + +} \ No newline at end of file diff --git a/src/main/java/ru/touchin/templates/calendar/ComparingToToday.java b/src/main/java/ru/touchin/templates/calendar/ComparingToToday.java new file mode 100644 index 0000000..943bdb3 --- /dev/null +++ b/src/main/java/ru/touchin/templates/calendar/ComparingToToday.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2016 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package ru.touchin.templates.calendar; + +/** + * Created by Ilia Kurtov on 18/03/2016. + * Show the comparison between a date and today. + */ +public enum ComparingToToday { + + BEFORE_TODAY, + TODAY, + AFTER_TODAY + +}