Был у меня как-то проект, который я так и не смог довести до ума, причин было много, из-за этого осталось много наработок которые лежат без дела мертвым грузом, и вот я решил поделиться своим опытом создания самопального календаря. Я очень долго не мог понять как мне его нужно сделать, да и до написания статьи я потратил прилично времени что бы причесать код и дизайн календаря до нормального вида, это можно увидеть в коммитах на гитхабе.
Проект я делал еще года два назад, код не совсем идеальный, я его конечно по максимуму отрефакторил, но все равно остались куски от которых хочется плакать. Но я думаю со временем я доведу все до ума на столько что буду гордиться этим кодом :) Так сказать поживем увидим.
Суть календаря в том что у нас в шапке находится список дней, с понедельника по воскресенье и даты этих дней отсортированные по дням недели, с боку от этих дней левее распологается кнопки листающие дни на 7 дней назад или вперед, а по центру отображается месяц относящийся к этим дням.
Под верхним меню с датами у нас будет находится собственно сам календарь, в нем у нас будет название задачи, сколько она длится и кружки которые показывают уровень выполнения заданий каждый день. То есть у нас есть задача например каждый день отжиматься по 10 подходов, в календаре будет стоять 10 подходов, сколько выполнено и сколько надо выполнить и по мере выполнения мы отмечаем по нажатию на кружок, и он делится на кусочки. В общем чувствую что по описанию я сам запутаюсь, просто приведу скриншот, надеюсь так будет понятней. Ну а еще у нас будет экран создания задания в календарь, он у нас будет вызываться по нажатию на плюсик в статус баре. В нем мы будет просто устанавливать нужные параметры для календаря.
Ну окей, давайте начнем писать код, тут его много, по этому предстоит много писать. Оформлять код буду без комментариев, буду описывать методы ниже под кодом.
Наверно начнем все с создания проекта, у нас создастся проект с одним файлом активити и разметкой. Давайте сперва наверно мы создадим экран создания таска, так будет как по мне логичней. В проекте мы будем использовать несколько библиотек:
Про работу с Realm я расказывал в этой статье, там я расказал в деталях как работать с ним
Давайте их добавим в наш app/build.gradle. Вот так у нас он будет выглядеть:
app/build.gradle
В этом коде нас интересует вот этот блок dependencies в котором у нас подключаются библиотеки, и в самом верху вот эта строчка apply plugin: 'android-apt', ее мы добавляем для подключения butter knife. Ну и далее нам нужно в файл build.gradle добавить еще одну строчку, весь код файла ниже:
build.gradle
В этом куске исходника мы добавили в dependencies вот эту строчку classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' которая позволит нам подключить butter knife к проекту.
Так, с библиотеками мы закончили, теперь мы готовы писать божественный код который будет творить магию. Начнем мы с экрана который будет создавать цели, для этого мы создадим новую активити и фрагмент в котором у нас будет находится соответственно все элементы экрана.
Для начала создадим базовую активити и базовый фрагменты, они нам нужны для упрощения кода и для того что бы поменьше было повторяющегося кода.
BaseActivity.java
Тут мы сделали метод в котором задаем леяут для каждой активити которая будет наследовать этот класс. getViewId() — метод который будет создаваться в каждом унаследованном классе, и в нем мы будем указывать леяут который мы хотим что бы отображался.
BaseFragment.java
Здесь делаем тоже самое что мы делали в BaseActivity, но только мы еще добавили инициализацию контекста для того что бы его можно было использовать вместо метода getActivity().
NewTaskActivity.java
Теперь создаем класс NewTaskActivity и дальше наследуем его от BaseActivity, и дальше имплементим метод getViewId() и задаем в нем наш леяут.
activity_new_task.xml
Здесь мы просто задали какой фрагмент будем отображать в активити. Теперь нам нужно создать фрагмент в котором мы опишем функционал добавления целей в БД. Давайте опишем работу с реалмом для текущей задачи.
Нам нужно создать модели для БД, у нас будет 3 таблицы, главная таблица в которой у нас будет вся важная информация по цели, таблица о днях которые у нас будут активными и таблица в которой у нас будет сохраняться количество дней на протяжении которых мы будем выполнять наши цели.
Главной таблице у нас будет RealmTaskModel. Таблицей для хранения активных дней RealmBooleanModel и таблица для хранения дней называться RealmTaskHistoryModel.
RealmTaskModel.java
RealmBooleanModel.java
RealmTaskHistoryModel.java
По сути просто создали сеттеры и геттеры и поля в БД с помощью родителя RealmObject. Теперь нам нужно все это закодить что бы оно умело записывать, обновлять и доставать данные из БД.
TaskController.java
В конструкторе мы создаем объект Realm и дальше мы создаем методы для записи, чтения и т.д.
addTask() — тут мы начинаем транзакцию и сетим данные в таблицу, и коммитим ее.
getNextKey() — метод который определяет какой id является последним. Возвращает id который идет следующим по логике.
getAllTasks() — возвращает список всех записей из БД.
updateTaskProgress() — метод который обновляет прогресс в определенной цели, определенного кружочка.
clearDatabase() — чистит БД целиком и полностью.
Теперь нам нужно создать фрагмент. В нем у нас используется два кастомных элемента которые я вынес в отдельные вьюхи. Создадим сперва их. В первом у нас как я описывал выше будет поля и кнопки для задания определенных настроек, вторая вьюха у нас будет входить в первую, она будет чисто списком кнопок которые мы по нажатию будем выбирать или развыбирать, в зависимости от надобности. В общем увидите, сперва создадим ScheduleView.
ScheduleView.java
В этом классе у нас есть спиннер, по клику на который по клику мы выбираем нужные данные из массива который у нас указан в string.xml. Да и вообще я забыл указать весь этот файл, так что я его приведу ниже.
string.xml
Вот он. Собственно тут мы мы сетим адаптеры в этой вьюхе, для спиннера, он клик лисенеры для вьюх и т. д. По клику на кнопку without deadlines у нас делается или активным или не активным поле для ввода количества дней, ну и подствечиваем эту кнопку. И куча сеттеров и геттеров для получения данных из этой вьюхи.
XML будет иметь такой вид:
view_new_target_shletude.xml
А еще у нас есть WeeksView в котором у нас список кнопок дней недели. Сама вьюха представляет из себя просто список текствьюх которые по клику делаются активными или нет и добавляем их в массив с которого потом читаем какие у нас выбранны и записываем в БД.
WeeksView.java
Вьюха состоит всего из 7 текствьюх запиханные в LinearLayout.
view_tasks_weeks.xml
Ну, а дальше мы уже можем создавать фрагмент и использовать все наши компоненты которые мы создали.
NewTaskFragment.java
getViewId() — мы указываем какую вьюху гам использовать.
onCreate() — инициализируем наш контроллер по записи в БД.
onViewCreated() — инициализируем нудные списки и модели.
onOptionsItemSelected() — по клику на кнопку add мы сохраняем данные в БД.
getDate() — возвращает стартовую дату в виде строки.
daysBetweenDates() — возвращает количество дней между днями указаными от начальной даты и конечной.
В классе InsertDataAboutTask у нас идет запись в БД, это мы делаем для разделения потоков записи, так как если без этого таска у нас приложение зависнет просто, хоть разработчики реалма и говорят что оно создает отдельные потоки каждый раз когда выполняется какое-то действие, но почему-то не всегда это срабатывает.
По окончанию выполнения таска в onPostExecute() у нас вызывается закрытие активити и setResult() метод для возвращения того что все ок.
А еще у нас есть такая штука как RandomUtils которая создаем рандомное число для записи id для таблицы количества дней и таблицы для хранения активных дней.
RandomUtils.java
Теперь мы умеем писать в БД, и оно умеет создавать цели для календаря, далее нам нужно начать писать календарь. У нас есть класс MainActivity, я его переименовал себе в CalendarActivity что бы было понятней, да и поэстетичней впринципе выглядит. В нем у нас как и в активити новой цели так же всего один метод getViewId().
CalendarActivity.java
В activity_main все так же как и в new task activity мы прописали фрагмент который у нас будет отображаться в активитии леяут который мы хотим отображать по дефолту.
activity_main.xml
А дальше давайте создадим фрагмент, в нем у нас будет использоваться один список который мы в адаптере будем разбивать на нужные айтемы.
CalendarFragment.java
Здесь у нас есть несколько моментов. У нас есть dayOfWeeksArray в котором у нас прописаны дни недели, у нас их 7, можно впринципе просто сделать константу которая будет равна семи, но я подумал что так сделать будет получше.
onCreate() — у нас создается объект нашего таск контроллера который является контроллером для работы с БД. Проинициализировали календарь и сказали ему что у нас понедельник является первым днем недели, а не воскресенье.
onViewCreated() — создали в нем адаптер, засетили туда все нужные данные что бы в адаптере были данные для отображения и отправили все это в листвью.
getDayOfWeek() — возвращает текущий день недели.
refreshAdapter() — обновляет адаптер с новыми данными каждый раз когда мы его вызываем.
showDatesInView() — в этом методе мы создаем даты для шапки которая у нас отображает дни недели и даты. Тут мы записываем это все в список предварительно прогнав календарь в цикле и ретурном возвращаем в метод CalnedarModel приведу его ниже.
onClick() — методы которые по клику что-то делают, думаю они не требуют особого внимания.
onActivityResult() — обновляем адаптер по возвращению на этот фрагмент.
CalendarModel.java
А теперь у нас начинается екшн — будем писать адаптер для календаря, он у нас довольно сложно выглядит со стороны, но на деле там все очень просто, давайте посмотрим на код с начала, а потом уже буду разъяснять что к чему там.
CalendarAdapter.java
Вот такой у нас адаптер получается, в нем у нас солянка сборная. Тут мы и сетим данные, и обрабатываем их, и выплевываем обратно в список, в общем щас будем все по порядку разбирать. В этом адаптере у нас есть два вида View. Первый у нас это шапка, он у нас обозначен константой TYPE_HEAD, а второй у нас обычный айтем с тайтлом, дескрипшеном и кружочками которые будем заполнять, он же обозначен как TYPE_ITEM. Еще у нас есть непонятная константа DATE_TEMPLATE, она нам нужна для того что бы дата отображалась у нас сверху месяц и ниже год, так ничего не вылазит за пределы ограничивающих полей.
Дальше у нас там идет пачка переменных которые мы сетим в сеттерах вместе с инициализацией адаптера, там по сути оч простой код, просто в одну переменную передаем другу. А вот некоторые методы нас интенересуют особенно.
getItemViewType() — метод который разбивает наш адаптер на две части. В нем мы говорим что если позиция равна 0 то мы обозначаем ее как TYPE_HEAD и в ней мы отображаем вьюху шапки, а остальное мы отображаем как TYPE_ITEM. По сути так можно разбивать на сколько угодно частей адаптер, особой магии тут нет.
getViewTypeCount() — возвращает количество элементов в адаптере. + 1 — обозначает что у нас плюс одна вьюха которой у нас является наша шапка, если хотите добавить третью вьюху какую-то, то надо написать + 2 и т.д.
getCount() — возвращает количество элементов в списке. Так же, + 1 обозначает что отображать мы будем на с 0 позиции, так как у нас там шапка, а со второй.
getItem() — возвращает айтем со списка. Та проверка которая возвращает нулл нужна для того что бы у нас было смещение на 0 позицию, так как у нас там шапка и там используется другой список.
getView() — главный метод адаптера, в нем происходит сетап данных в адаптер и тут же обновление и вся остальная магия. В самом начале мы инициализируем LayoutInflater, тут он нужен для захвата вьюх, на нем особо не будем заострять внимания. Дальше у нас идет TableRow, его мы создали для того что бы отцентровать вьюхи по центру, сделать им горизонтальную орниентацию и вообще сделать их красивыми.
Далее у нас идет свитч который у нас разбивает адаптер на шапку и айтемы.
TYPE_HEAD же значит что мы будем работать с шапкой. В нем мы задали леяут с которым будем работать, задали шаблон для отображения месяца и года, и дальше в цикле прогоняем дни недели и отображаем их в TableRow который мы создали выше и потом сетим его во вьюху на леяуте. А ниже просто добавляем лисенеры на клик на кнопки даты, и кнопки вперед и назад. Тут у нас используются колбеки которые будут возвращать события в фрагмент.
TYPE_ITEM же значит что мы будем работать с обычными айтемами которые будут идти ниже шапки. В нем мы так же как и в шапке задали леяут и дальше засетили данные в поля тайтла, дескрипшена и кружочков прогресса и добавили это все в TableRow и потом его же во вьюху на леяуте.
onProgressClick() — метод который отслеживает клик по кружочку прогресса, в нем мы проверяем активен ли наш кружочек для нажатия, и сетим в него данные если да, а дальше по клику если он возможен мы задаем прогресс с помощью метода setProgress().
setProgress() — метод который апдейтит прогресс конкретного кружочка, если дата текущего дня совпадает с датой нажатого кружочка, ведь мы можем отмечать прогресс только в тот день когда мы его выполняем, то есть в текущий.
setRepeatsInfo() — если у нас кружочек прогресса активен то отображаем количество повторений ниже него.
compareDates() — сравнивает даты и если они равны возвращает тру если не — то фолс.
getRealmDate() — возвращает дату которая у нас хранится в БД по нужному айди и позиции.
getFullDate() — так же возаращет дату но уже из другого списка, по позиции.
getCurrentDate() — очевидно что возвращает текущую дату.
setOnClickListener() — сеттер для интерфейса клика.
OnClickCallback — сам интерфейс в котором у нас обозначены он клик лисенеры.
Вот как то так выглядит у нас наш календарь, надо еще привести наши леяуты которые мы используем. Приведу их ниже.
item_calendar_days.xml
Тут интересный момент есть, вот этот класс CapFirstLetterTextView, мы его используем что бы сделать месяц отображаемый с большой буквы, так как по умолчанию в классе Calendar он идет с маленькой буквы… Так что пришлось повыпендриваться. Его код я приведу чуть попозже.
item_calendar_progress.xml
И не забываем про холдеры, они у нас тоже в отдельных классах. Если кто не знает, холдеры у нас для того что бы можно было обращаться к вьюхам напрямую из леяута.
HeaderHolder.java
ItemHolder.java
Вы вот щас смотрите на код и такие, а что же мы сетим, ведь в леяутах пусто, и там нету ничего что мы отображаем в адаптере, а я вам скажу что да, у нас есть кастомные две вьюхи еще которые у нас в циклах сетятся, я думаю у тех кто скопировал просто код они как раз подкрашиваются красным. Первая вьюха у нас CalendarDaysView, ее мы используем для шапки и для дней недели, в ней у нас две текствьюхи, день недели и дата.
CalendarDaysView.java
В конструкторе у нас идет сетап метода init() — в нем мы задаем леяут который мы используем для вьюхи, задаем ориентацию горизонтальную для того что бы просто так. И инициализируем баттер найф. А дальше у нас просто два сеттера которые получают и сетят данные в текствьюхи.
view_calendar_days_item.xml
А вторая вьюха у нас CalendarProgressView, мы ее используем для отображения кружочков прогресса. В ней у нас собственно кружочек прогресса и текствьюха которая отображает количество повторений.
CalendarProgressView.java
Собственно тоже самое что и в предыдущей вьюхе, только тут мы возвращаем текст и прогресс вью вместо того что бы в них сетить что-то, это мы делаем непосредственно в адаптере. Единственное что еще оставляет вопросы, что такое ProgressView, это опять же кастомная вьюха которая умеет очень многое, ее код опять же приведу ниже. А пока посмотрите на этот великлепный xml.
view_calendar_progress_item.xml
Ну вот собственно почти все готово, осталось добавить пару вьюх которые у нас остались, а это CapFirstLetterTextView и ProgressView. Начнем с простого, создатим кастомную текствьюху для того что бы наш текст всегда стартовал с заглавной буквы.
CapFirstLetterTextView.java
setFirstCupText() — метод который работает так же как и с setText, только увеличивает нашу первую букву автоматом.
А дальше у нас еще есть наш ProgressView который красивенько отображает прогресс, умеет его увеличивать и уменьшать. Давайте же посмотрим что это такое и как это работает.
ProgressView.java
По сути это вьюха, которую мы рисуем на канвасе и перерисовываем каждый раз когда выполняем какое действие над ней. У нас в этой вьюхе есть несколько состояний, первое это не активное, второе это активное но пустое, третье активное заполненное на какую-то часть, часть зеленым часть красным, и активная но заполненная полностью зеленым цветом.
В конструкторе мы инициализируем все нужные нам параметры Paint, у нас их там порядка 4 штук, и они имеют разные состояния которые я описал выше.
onDraw() — рисует «битмап» по центру выделенной области и заполняет ее стилем, в данном случае null, а это значит что он у нас просто пустой.
onSizeChanged() — метод который по изменению размера экрана будет подстраивать вьюху под нужные размеры, что бы она была всегда одинаковой.
updateBitmap() — метод который по обновлении вьюхи проверяет какой стиль к ней применить, если например у нас mValue меньше нуля — то у нас кружочек пустой, если mValue больше нуля то — делаем его заполненным на нужный угол и нужные цвет. А если mValue равно нулю то просто делаем его пустым. А по умолчанию у нас создается серый маленький кружок который не нажимается и не имеет вообще никаких действий. В конце вызываем метод обновления вьюхи.
setForegroundColor(), setBackgroundColor(), setBackgroundStrokeColor() — методы по задаванию нужного стиля кружочку.
setValue() — метод который мы вызываем когда кликаем на кружочек что бы увеличить его диаметер занимаемой поверхности на ту или иную mValue.
setProgressMaximum() — метод в котором мы задаем на сколько кусочков разбиваем наш кружочек, пусть то будет или 1 или 10 или 15, сколько укажите на столько оно и будет раз разбивать.
Тут еще один метод у нас используется, но он был вынесен в отдельный класс, так как я его использовал в других вьюхах в этом же проекте. Вы его уже видели в статье про создание настраиваемых PieChartView. Этот метод расчитывает радиус холста на котором у нас рисуется вьюха.
CircleRadiusHelper.java
Вроде бы все. Весь код который используется для создания такого календаря я вынес в статью. Возможно кому-то эта статья поможет в написании чего-то похожего. В любом случае я хотел поделиться этим опытом, так как он довольно таки много времени у меня отнял :) В будущем надеюсь еще подрефакторить код, возможно опишу в отдельной статье что я отрефакторил и как. Может даже красивше будет, и побыстрей. Поживем увидим.
Исходники:
GitHub
Проект я делал еще года два назад, код не совсем идеальный, я его конечно по максимуму отрефакторил, но все равно остались куски от которых хочется плакать. Но я думаю со временем я доведу все до ума на столько что буду гордиться этим кодом :) Так сказать поживем увидим.
Суть календаря в том что у нас в шапке находится список дней, с понедельника по воскресенье и даты этих дней отсортированные по дням недели, с боку от этих дней левее распологается кнопки листающие дни на 7 дней назад или вперед, а по центру отображается месяц относящийся к этим дням.
Под верхним меню с датами у нас будет находится собственно сам календарь, в нем у нас будет название задачи, сколько она длится и кружки которые показывают уровень выполнения заданий каждый день. То есть у нас есть задача например каждый день отжиматься по 10 подходов, в календаре будет стоять 10 подходов, сколько выполнено и сколько надо выполнить и по мере выполнения мы отмечаем по нажатию на кружок, и он делится на кусочки. В общем чувствую что по описанию я сам запутаюсь, просто приведу скриншот, надеюсь так будет понятней. Ну а еще у нас будет экран создания задания в календарь, он у нас будет вызываться по нажатию на плюсик в статус баре. В нем мы будет просто устанавливать нужные параметры для календаря.
Ну окей, давайте начнем писать код, тут его много, по этому предстоит много писать. Оформлять код буду без комментариев, буду описывать методы ниже под кодом.
Наверно начнем все с создания проекта, у нас создастся проект с одним файлом активити и разметкой. Давайте сперва наверно мы создадим экран создания таска, так будет как по мне логичней. В проекте мы будем использовать несколько библиотек:
- Realm
- Joda time
- Butter knife
Про работу с Realm я расказывал в этой статье, там я расказал в деталях как работать с ним
Давайте их добавим в наш app/build.gradle. Вот так у нас он будет выглядеть:
app/build.gradle
В этом коде нас интересует вот этот блок dependencies в котором у нас подключаются библиотеки, и в самом верху вот эта строчка apply plugin: 'android-apt', ее мы добавляем для подключения butter knife. Ну и далее нам нужно в файл build.gradle добавить еще одну строчку, весь код файла ниже:
build.gradle
В этом куске исходника мы добавили в dependencies вот эту строчку classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' которая позволит нам подключить butter knife к проекту.
Так, с библиотеками мы закончили, теперь мы готовы писать божественный код который будет творить магию. Начнем мы с экрана который будет создавать цели, для этого мы создадим новую активити и фрагмент в котором у нас будет находится соответственно все элементы экрана.
Для начала создадим базовую активити и базовый фрагменты, они нам нужны для упрощения кода и для того что бы поменьше было повторяющегося кода.
BaseActivity.java
Тут мы сделали метод в котором задаем леяут для каждой активити которая будет наследовать этот класс. getViewId() — метод который будет создаваться в каждом унаследованном классе, и в нем мы будем указывать леяут который мы хотим что бы отображался.
BaseFragment.java
Здесь делаем тоже самое что мы делали в BaseActivity, но только мы еще добавили инициализацию контекста для того что бы его можно было использовать вместо метода getActivity().
NewTaskActivity.java
Теперь создаем класс NewTaskActivity и дальше наследуем его от BaseActivity, и дальше имплементим метод getViewId() и задаем в нем наш леяут.
activity_new_task.xml
Здесь мы просто задали какой фрагмент будем отображать в активити. Теперь нам нужно создать фрагмент в котором мы опишем функционал добавления целей в БД. Давайте опишем работу с реалмом для текущей задачи.
Нам нужно создать модели для БД, у нас будет 3 таблицы, главная таблица в которой у нас будет вся важная информация по цели, таблица о днях которые у нас будут активными и таблица в которой у нас будет сохраняться количество дней на протяжении которых мы будем выполнять наши цели.
Главной таблице у нас будет RealmTaskModel. Таблицей для хранения активных дней RealmBooleanModel и таблица для хранения дней называться RealmTaskHistoryModel.
RealmTaskModel.java
RealmBooleanModel.java
RealmTaskHistoryModel.java
По сути просто создали сеттеры и геттеры и поля в БД с помощью родителя RealmObject. Теперь нам нужно все это закодить что бы оно умело записывать, обновлять и доставать данные из БД.
TaskController.java
В конструкторе мы создаем объект Realm и дальше мы создаем методы для записи, чтения и т.д.
addTask() — тут мы начинаем транзакцию и сетим данные в таблицу, и коммитим ее.
getNextKey() — метод который определяет какой id является последним. Возвращает id который идет следующим по логике.
getAllTasks() — возвращает список всех записей из БД.
updateTaskProgress() — метод который обновляет прогресс в определенной цели, определенного кружочка.
clearDatabase() — чистит БД целиком и полностью.
Теперь нам нужно создать фрагмент. В нем у нас используется два кастомных элемента которые я вынес в отдельные вьюхи. Создадим сперва их. В первом у нас как я описывал выше будет поля и кнопки для задания определенных настроек, вторая вьюха у нас будет входить в первую, она будет чисто списком кнопок которые мы по нажатию будем выбирать или развыбирать, в зависимости от надобности. В общем увидите, сперва создадим ScheduleView.
ScheduleView.java
В этом классе у нас есть спиннер, по клику на который по клику мы выбираем нужные данные из массива который у нас указан в string.xml. Да и вообще я забыл указать весь этот файл, так что я его приведу ниже.
string.xml
Вот он. Собственно тут мы мы сетим адаптеры в этой вьюхе, для спиннера, он клик лисенеры для вьюх и т. д. По клику на кнопку without deadlines у нас делается или активным или не активным поле для ввода количества дней, ну и подствечиваем эту кнопку. И куча сеттеров и геттеров для получения данных из этой вьюхи.
XML будет иметь такой вид:
view_new_target_shletude.xml
А еще у нас есть WeeksView в котором у нас список кнопок дней недели. Сама вьюха представляет из себя просто список текствьюх которые по клику делаются активными или нет и добавляем их в массив с которого потом читаем какие у нас выбранны и записываем в БД.
WeeksView.java
Вьюха состоит всего из 7 текствьюх запиханные в LinearLayout.
view_tasks_weeks.xml
Ну, а дальше мы уже можем создавать фрагмент и использовать все наши компоненты которые мы создали.
NewTaskFragment.java
getViewId() — мы указываем какую вьюху гам использовать.
onCreate() — инициализируем наш контроллер по записи в БД.
onViewCreated() — инициализируем нудные списки и модели.
onOptionsItemSelected() — по клику на кнопку add мы сохраняем данные в БД.
getDate() — возвращает стартовую дату в виде строки.
daysBetweenDates() — возвращает количество дней между днями указаными от начальной даты и конечной.
В классе InsertDataAboutTask у нас идет запись в БД, это мы делаем для разделения потоков записи, так как если без этого таска у нас приложение зависнет просто, хоть разработчики реалма и говорят что оно создает отдельные потоки каждый раз когда выполняется какое-то действие, но почему-то не всегда это срабатывает.
По окончанию выполнения таска в onPostExecute() у нас вызывается закрытие активити и setResult() метод для возвращения того что все ок.
А еще у нас есть такая штука как RandomUtils которая создаем рандомное число для записи id для таблицы количества дней и таблицы для хранения активных дней.
RandomUtils.java
Теперь мы умеем писать в БД, и оно умеет создавать цели для календаря, далее нам нужно начать писать календарь. У нас есть класс MainActivity, я его переименовал себе в CalendarActivity что бы было понятней, да и поэстетичней впринципе выглядит. В нем у нас как и в активити новой цели так же всего один метод getViewId().
CalendarActivity.java
В activity_main все так же как и в new task activity мы прописали фрагмент который у нас будет отображаться в активитии леяут который мы хотим отображать по дефолту.
activity_main.xml
А дальше давайте создадим фрагмент, в нем у нас будет использоваться один список который мы в адаптере будем разбивать на нужные айтемы.
CalendarFragment.java
Здесь у нас есть несколько моментов. У нас есть dayOfWeeksArray в котором у нас прописаны дни недели, у нас их 7, можно впринципе просто сделать константу которая будет равна семи, но я подумал что так сделать будет получше.
onCreate() — у нас создается объект нашего таск контроллера который является контроллером для работы с БД. Проинициализировали календарь и сказали ему что у нас понедельник является первым днем недели, а не воскресенье.
onViewCreated() — создали в нем адаптер, засетили туда все нужные данные что бы в адаптере были данные для отображения и отправили все это в листвью.
getDayOfWeek() — возвращает текущий день недели.
refreshAdapter() — обновляет адаптер с новыми данными каждый раз когда мы его вызываем.
showDatesInView() — в этом методе мы создаем даты для шапки которая у нас отображает дни недели и даты. Тут мы записываем это все в список предварительно прогнав календарь в цикле и ретурном возвращаем в метод CalnedarModel приведу его ниже.
onClick() — методы которые по клику что-то делают, думаю они не требуют особого внимания.
onActivityResult() — обновляем адаптер по возвращению на этот фрагмент.
CalendarModel.java
А теперь у нас начинается екшн — будем писать адаптер для календаря, он у нас довольно сложно выглядит со стороны, но на деле там все очень просто, давайте посмотрим на код с начала, а потом уже буду разъяснять что к чему там.
CalendarAdapter.java
Вот такой у нас адаптер получается, в нем у нас солянка сборная. Тут мы и сетим данные, и обрабатываем их, и выплевываем обратно в список, в общем щас будем все по порядку разбирать. В этом адаптере у нас есть два вида View. Первый у нас это шапка, он у нас обозначен константой TYPE_HEAD, а второй у нас обычный айтем с тайтлом, дескрипшеном и кружочками которые будем заполнять, он же обозначен как TYPE_ITEM. Еще у нас есть непонятная константа DATE_TEMPLATE, она нам нужна для того что бы дата отображалась у нас сверху месяц и ниже год, так ничего не вылазит за пределы ограничивающих полей.
Дальше у нас там идет пачка переменных которые мы сетим в сеттерах вместе с инициализацией адаптера, там по сути оч простой код, просто в одну переменную передаем другу. А вот некоторые методы нас интенересуют особенно.
getItemViewType() — метод который разбивает наш адаптер на две части. В нем мы говорим что если позиция равна 0 то мы обозначаем ее как TYPE_HEAD и в ней мы отображаем вьюху шапки, а остальное мы отображаем как TYPE_ITEM. По сути так можно разбивать на сколько угодно частей адаптер, особой магии тут нет.
getViewTypeCount() — возвращает количество элементов в адаптере. + 1 — обозначает что у нас плюс одна вьюха которой у нас является наша шапка, если хотите добавить третью вьюху какую-то, то надо написать + 2 и т.д.
getCount() — возвращает количество элементов в списке. Так же, + 1 обозначает что отображать мы будем на с 0 позиции, так как у нас там шапка, а со второй.
getItem() — возвращает айтем со списка. Та проверка которая возвращает нулл нужна для того что бы у нас было смещение на 0 позицию, так как у нас там шапка и там используется другой список.
getView() — главный метод адаптера, в нем происходит сетап данных в адаптер и тут же обновление и вся остальная магия. В самом начале мы инициализируем LayoutInflater, тут он нужен для захвата вьюх, на нем особо не будем заострять внимания. Дальше у нас идет TableRow, его мы создали для того что бы отцентровать вьюхи по центру, сделать им горизонтальную орниентацию и вообще сделать их красивыми.
Далее у нас идет свитч который у нас разбивает адаптер на шапку и айтемы.
TYPE_HEAD же значит что мы будем работать с шапкой. В нем мы задали леяут с которым будем работать, задали шаблон для отображения месяца и года, и дальше в цикле прогоняем дни недели и отображаем их в TableRow который мы создали выше и потом сетим его во вьюху на леяуте. А ниже просто добавляем лисенеры на клик на кнопки даты, и кнопки вперед и назад. Тут у нас используются колбеки которые будут возвращать события в фрагмент.
TYPE_ITEM же значит что мы будем работать с обычными айтемами которые будут идти ниже шапки. В нем мы так же как и в шапке задали леяут и дальше засетили данные в поля тайтла, дескрипшена и кружочков прогресса и добавили это все в TableRow и потом его же во вьюху на леяуте.
onProgressClick() — метод который отслеживает клик по кружочку прогресса, в нем мы проверяем активен ли наш кружочек для нажатия, и сетим в него данные если да, а дальше по клику если он возможен мы задаем прогресс с помощью метода setProgress().
setProgress() — метод который апдейтит прогресс конкретного кружочка, если дата текущего дня совпадает с датой нажатого кружочка, ведь мы можем отмечать прогресс только в тот день когда мы его выполняем, то есть в текущий.
setRepeatsInfo() — если у нас кружочек прогресса активен то отображаем количество повторений ниже него.
compareDates() — сравнивает даты и если они равны возвращает тру если не — то фолс.
getRealmDate() — возвращает дату которая у нас хранится в БД по нужному айди и позиции.
getFullDate() — так же возаращет дату но уже из другого списка, по позиции.
getCurrentDate() — очевидно что возвращает текущую дату.
setOnClickListener() — сеттер для интерфейса клика.
OnClickCallback — сам интерфейс в котором у нас обозначены он клик лисенеры.
Вот как то так выглядит у нас наш календарь, надо еще привести наши леяуты которые мы используем. Приведу их ниже.
item_calendar_days.xml
Тут интересный момент есть, вот этот класс CapFirstLetterTextView, мы его используем что бы сделать месяц отображаемый с большой буквы, так как по умолчанию в классе Calendar он идет с маленькой буквы… Так что пришлось повыпендриваться. Его код я приведу чуть попозже.
item_calendar_progress.xml
И не забываем про холдеры, они у нас тоже в отдельных классах. Если кто не знает, холдеры у нас для того что бы можно было обращаться к вьюхам напрямую из леяута.
HeaderHolder.java
ItemHolder.java
Вы вот щас смотрите на код и такие, а что же мы сетим, ведь в леяутах пусто, и там нету ничего что мы отображаем в адаптере, а я вам скажу что да, у нас есть кастомные две вьюхи еще которые у нас в циклах сетятся, я думаю у тех кто скопировал просто код они как раз подкрашиваются красным. Первая вьюха у нас CalendarDaysView, ее мы используем для шапки и для дней недели, в ней у нас две текствьюхи, день недели и дата.
CalendarDaysView.java
В конструкторе у нас идет сетап метода init() — в нем мы задаем леяут который мы используем для вьюхи, задаем ориентацию горизонтальную для того что бы просто так. И инициализируем баттер найф. А дальше у нас просто два сеттера которые получают и сетят данные в текствьюхи.
view_calendar_days_item.xml
А вторая вьюха у нас CalendarProgressView, мы ее используем для отображения кружочков прогресса. В ней у нас собственно кружочек прогресса и текствьюха которая отображает количество повторений.
CalendarProgressView.java
Собственно тоже самое что и в предыдущей вьюхе, только тут мы возвращаем текст и прогресс вью вместо того что бы в них сетить что-то, это мы делаем непосредственно в адаптере. Единственное что еще оставляет вопросы, что такое ProgressView, это опять же кастомная вьюха которая умеет очень многое, ее код опять же приведу ниже. А пока посмотрите на этот великлепный xml.
view_calendar_progress_item.xml
Ну вот собственно почти все готово, осталось добавить пару вьюх которые у нас остались, а это CapFirstLetterTextView и ProgressView. Начнем с простого, создатим кастомную текствьюху для того что бы наш текст всегда стартовал с заглавной буквы.
CapFirstLetterTextView.java
setFirstCupText() — метод который работает так же как и с setText, только увеличивает нашу первую букву автоматом.
А дальше у нас еще есть наш ProgressView который красивенько отображает прогресс, умеет его увеличивать и уменьшать. Давайте же посмотрим что это такое и как это работает.
ProgressView.java
По сути это вьюха, которую мы рисуем на канвасе и перерисовываем каждый раз когда выполняем какое действие над ней. У нас в этой вьюхе есть несколько состояний, первое это не активное, второе это активное но пустое, третье активное заполненное на какую-то часть, часть зеленым часть красным, и активная но заполненная полностью зеленым цветом.
В конструкторе мы инициализируем все нужные нам параметры Paint, у нас их там порядка 4 штук, и они имеют разные состояния которые я описал выше.
onDraw() — рисует «битмап» по центру выделенной области и заполняет ее стилем, в данном случае null, а это значит что он у нас просто пустой.
onSizeChanged() — метод который по изменению размера экрана будет подстраивать вьюху под нужные размеры, что бы она была всегда одинаковой.
updateBitmap() — метод который по обновлении вьюхи проверяет какой стиль к ней применить, если например у нас mValue меньше нуля — то у нас кружочек пустой, если mValue больше нуля то — делаем его заполненным на нужный угол и нужные цвет. А если mValue равно нулю то просто делаем его пустым. А по умолчанию у нас создается серый маленький кружок который не нажимается и не имеет вообще никаких действий. В конце вызываем метод обновления вьюхи.
setForegroundColor(), setBackgroundColor(), setBackgroundStrokeColor() — методы по задаванию нужного стиля кружочку.
setValue() — метод который мы вызываем когда кликаем на кружочек что бы увеличить его диаметер занимаемой поверхности на ту или иную mValue.
setProgressMaximum() — метод в котором мы задаем на сколько кусочков разбиваем наш кружочек, пусть то будет или 1 или 10 или 15, сколько укажите на столько оно и будет раз разбивать.
Тут еще один метод у нас используется, но он был вынесен в отдельный класс, так как я его использовал в других вьюхах в этом же проекте. Вы его уже видели в статье про создание настраиваемых PieChartView. Этот метод расчитывает радиус холста на котором у нас рисуется вьюха.
CircleRadiusHelper.java
Вроде бы все. Весь код который используется для создания такого календаря я вынес в статью. Возможно кому-то эта статья поможет в написании чего-то похожего. В любом случае я хотел поделиться этим опытом, так как он довольно таки много времени у меня отнял :) В будущем надеюсь еще подрефакторить код, возможно опишу в отдельной статье что я отрефакторил и как. Может даже красивше будет, и побыстрей. Поживем увидим.
Исходники:
GitHub
Комментариев нет:
Отправить комментарий