Суть календаря в том что у нас в шапке находится список дней, с понедельника по воскресенье и даты этих дней отсортированные по дням недели, с боку от этих дней левее распологаются кнопки листающие дни на 7 дней назад или вперед, а по центру отображается месяц относящийся к этим дням.
Под верхним меню с датами у нас будет находится собственно сам календарь, в нем у нас будет название задачи, сколько она длится и кружки которые показывают уровень выполнения заданий каждый день. То есть у нас есть задача например каждый день отжиматься по 10 подходов, в календаре будет стоять 10 подходов, сколько выполнено и сколько надо выполнить и по мере выполнения мы отмечаем по нажатию на кружок, и он делится на кусочки. Ну а еще у нас будет экран создания задания в календарь, он у нас будет вызываться по нажатию на плюс в статус баре. В нем мы будет просто устанавливать нужные параметры для календаря.
Наверно начнем все с создания проекта, у нас создастся проект с одним файлом активити и разметкой. Давайте сперва наверно мы создадим экран создания таска, так будет как по мне логичней. В проекте мы будем использовать несколько библиотек:
- Realm
- Joda time
- Butter knife
Давайте их добавим в наш app/build.gradle. Вот так у нас он будет выглядеть:
app/build.gradle
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
android {
compileSdkVersion 25
buildToolsVersion "25.0.2"
defaultConfig {
applicationId "com.project.tele-plus"
minSdkVersion 15
targetSdkVersion 25
versionCode 1
versionName "1.0"
multiDexEnabled true
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
dexOptions {
javaMaxHeapSize "4g"
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:25.3.1'
compile 'com.jakewharton:butterknife:8.0.1'
apt 'com.jakewharton:butterknife-compiler:8.0.1'
compile 'io.realm:realm-android:0.87.+'
compile 'joda-time:joda-time:2.4'
}
В этом коде нас интересует вот этот блок dependencies в котором у нас подключаются библиотеки, и в самом верху вот эта строчка apply plugin: 'android-apt', ее мы добавляем для подключения butter knife. Ну и далее нам нужно в файл build.gradle добавить еще одну строчку, весь код файла ниже:
build.gradle
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.1'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
allprojects {
repositories {
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
В этом куске исходника мы добавили в dependencies вот эту строчку classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' которая позволит нам подключить butter knife к проекту.
Так, с библиотеками мы закончили, теперь мы готовы писать божественный код который будет творить магию. Начнем мы с экрана который будет создавать цели, для этого мы создадим новую активити и фрагмент в котором у нас будет находится соответственно все элементы экрана.
Для начала создадим базовую активити и базовый фрагменты, они нам нужны для упрощения кода и для того что бы поменьше было повторяющегося кода.
BaseActivity.java
public abstract class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(getViewId());
}
public abstract int getViewId();
}
Тут мы сделали метод в котором задаем леяут для каждой активити которая будет наследовать этот класс. getViewId() — метод который будет создаваться в каждом унаследованном классе, и в нем мы будем указывать леяут который мы хотим что бы отображался.
BaseFragment.java
public abstract class BaseFragment extends Fragment {
public Activity context;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(getViewId(), container, false);
ButterKnife.bind(this, view);
setHasOptionsMenu(true);
return view;
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
context = activity;
}
public abstract int getViewId();
}
Здесь делаем тоже самое что мы делали в BaseActivity, но только мы еще добавили инициализацию контекста для того что бы его можно было использовать вместо метода getActivity().
NewTaskActivity.java
public class NewTaskActivity extends BaseActivity {
@Override
public int getViewId() {
return R.layout.activity_new_task;
}
}
Теперь создаем класс NewTaskActivity и дальше наследуем его от BaseActivity, и дальше имплементим метод getViewId() и задаем в нем наш леяут.
activity_new_task.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools">
<fragment
android:id="@+id/add"
android:name="com.project.piechartcallendarexample.ui.add.NewTaskFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:layout="@layout/fragment_add_task"
android:layout_weight="0.1" />
</LinearLayout>
Здесь мы просто задали какой фрагмент будем отображать в активити. Теперь нам нужно создать фрагмент в котором мы опишем функционал добавления целей в БД. Давайте опишем работу с реалмом для текущей задачи.
Нам нужно создать модели для БД, у нас будет 3 таблицы, главная таблица в которой у нас будет вся важная информация по цели, таблица о днях которые у нас будут активными и таблица в которой у нас будет сохраняться количество дней на протяжении которых мы будем выполнять наши цели.
Главной таблице у нас будет RealmTaskModel. Таблицей для хранения активных дней RealmBooleanModel и таблица для хранения дней называться RealmTaskHistoryModel.
RealmTaskModel.java
import io.realm.RealmList;
import io.realm.RealmObject;
public class RealmTaskModel extends RealmObject {
private int id;
private String title;
private int countDays;
private int countRepeats;
private String dateStart;
private String dateFinish;
private RealmList<RealmBooleanModel> fixDaysList;
private RealmList<RealmTaskHistoryModel> realmTaskHistoryModels = new RealmList<>();
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDateStart() {
return dateStart;
}
public void setDateStart(String dateStart) {
this.dateStart = dateStart;
}
public String getDateFinish() {
return dateFinish;
}
public void setDateFinish(String dateFinish) {
this.dateFinish = dateFinish;
}
public RealmList<RealmTaskHistoryModel> getRealmTaskHistoryModels() {
return realmTaskHistoryModels;
}
public void setRealmTaskHistoryModels(RealmList<RealmTaskHistoryModel> realmTaskHistoryModels) {
this.realmTaskHistoryModels = realmTaskHistoryModels;
}
public int getCountDays() {
return countDays;
}
public void setCountDays(int countDays) {
this.countDays = countDays;
}
public int getCountRepeats() {
return countRepeats;
}
public void setCountRepeats(int countRepeats) {
this.countRepeats = countRepeats;
}
public RealmList<RealmBooleanModel> getFixDaysList() {
return fixDaysList;
}
public void setFixDaysList(RealmList<RealmBooleanModel> fixDaysList) {
this.fixDaysList = fixDaysList;
}
}
RealmBooleanModel.java
import io.realm.RealmObject;
public class RealmBooleanModel extends RealmObject {
private int id;
private boolean fixDay;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public boolean isFixDay() {
return fixDay;
}
public void setFixDay(boolean fixDay) {
this.fixDay = fixDay;
}
}
RealmTaskHistoryModel.java
import io.realm.RealmObject;
public class RealmTaskHistoryModel extends RealmObject {
private int id = 0;
private String date = "";
private int progress = 0;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
public int getProgress() {
return progress;
}
public void setProgress(int progress) {
this.progress = progress;
}
}
По сути просто создали сеттеры и геттеры и поля в БД с помощью родителя RealmObject. Теперь нам нужно все это закодить что бы оно умело записывать, обновлять и доставать данные из БД.
TaskController.java
import android.content.Context;
import com.project.piechartcallendarexample.ui.calendar.db.model.RealmBooleanModel;
import com.project.piechartcallendarexample.ui.calendar.db.model.RealmTaskHistoryModel;
import com.project.piechartcallendarexample.ui.calendar.db.model.RealmTaskModel;
import java.util.ArrayList;
import io.realm.Realm;
import io.realm.RealmConfiguration;
import io.realm.RealmResults;
public class TaskController {
private Context context;
private Realm realm;
public TaskController(Context context) {
this.context = context;
RealmConfiguration config = new RealmConfiguration.Builder(context).build();
realm.setDefaultConfiguration(config);
realm = Realm.getInstance(context);
}
public void addTask(RealmTaskModel model) {
realm.beginTransaction();
RealmTaskModel realmTaskModel = realm.createObject(RealmTaskModel.class);
realmTaskModel.setId(getNextKey());
realmTaskModel.setTitle(model.getTitle());
realmTaskModel.setDateStart(model.getDateStart());
realmTaskModel.setDateFinish(model.getDateFinish());
realmTaskModel.setCountDays(model.getCountDays());
realmTaskModel.setCountRepeats(model.getCountRepeats());
for (RealmBooleanModel booleanModel : model.getFixDaysList())
realmTaskModel.getFixDaysList().add(booleanModel);
for (RealmTaskHistoryModel taskHistoryModel : model.getRealmTaskHistoryModels())
realmTaskModel.getRealmTaskHistoryModels().add(taskHistoryModel);
realm.commitTransaction();
}
private int getNextKey() {
return realm.where(RealmTaskModel.class).max("id").intValue() + 1;
}
public ArrayList<RealmTaskModel> getAllTasks() {
ArrayList<RealmTaskModel> realmTaskModels = new ArrayList<>();
RealmResults<RealmTaskModel> results = realm.where(RealmTaskModel.class).findAll();
for (int i = 0; i < results.size(); i++) {
RealmTaskModel u = results.get(i);
realmTaskModels.add(u);
}
return realmTaskModels;
}
public void updateTaskProgress(int id, int progress) {
RealmTaskHistoryModel realmTargetModel = realm.where(RealmTaskHistoryModel.class).equalTo("id", id).findFirst();
realm.beginTransaction();
realmTargetModel.setProgress(progress);
realm.commitTransaction();
}
public void clearDatabase() {
realm.beginTransaction();
realm.clear(RealmTaskModel.class);
realm.commitTransaction();
}
}
В конструкторе мы создаем объект Realm и дальше мы создаем методы для записи, чтения и т.д.
addTask() — тут мы начинаем транзакцию и сетим данные в таблицу, и коммитим ее.
getNextKey() — метод который определяет какой id является последним. Возвращает id который идет следующим по логике.
getAllTasks() — возвращает список всех записей из БД.
updateTaskProgress() — метод который обновляет прогресс в определенной цели, определенного кружочка.
clearDatabase() — чистит БД целиком и полностью.
Теперь нам нужно создать фрагмент. В нем у нас используется два кастомных элемента которые я вынес в отдельные вьюхи. Создадим сперва их. В первом у нас как я описывал выше будет поля и кнопки для задания определенных настроек, вторая вьюха у нас будет входить в первую, она будет чисто списком кнопок которые мы по нажатию будем выбирать или развыбирать, в зависимости от надобности. В общем увидите, сперва создадим ScheduleView.
ScheduleView.java
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.TextView;
import com.project.piechartcallendarexample.R;
import java.util.List;
import butterknife.BindColor;
import butterknife.BindView;
import butterknife.ButterKnife;
public class ScheduleView extends LinearLayout implements View.OnClickListener {
@BindView(R.id.weeksView)
WeeksView weeksView;
@BindView(R.id.repeatSpinner)
Spinner repeatSpinner;
@BindView(R.id.deadLine)
TextView deadLine;
@BindView(R.id.withoutDeadLine)
TextView withoutDeadLine;
@BindView(R.id.schletudeQuestion)
ImageView schletudeQuestion;
@BindColor(R.color.colorPrimary)
int blueColor;
@BindColor(R.color.dark_gray_text)
int grayColor;
private boolean withoutDeadline;
private int repeatValue;
private Context context;
public ScheduleView(Context context) {
super(context);
init(context);
}
public ScheduleView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public ScheduleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
this.context = context;
inflate(context, R.layout.view_new_target_shletude, this);
setOrientation(VERTICAL);
ButterKnife.bind(this);
setupViews();
}
private void setupViews() {
schletudeQuestion.setOnClickListener(this);
withoutDeadLine.setOnClickListener(this);
repeatValue = Integer.valueOf(getResources().getStringArray(R.array.task_repeats)[0]);
setRepeatSpinnerAdapter();
}
public void setRepeatSpinnerAdapter() {
ArrayAdapter<String> adapter = new ArrayAdapter<>(context, android.R.layout.simple_spinner_item, getResources().getStringArray(R.array.task_repeats));
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
repeatSpinner.setAdapter(adapter);
repeatSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
repeatValue = Integer.valueOf(getResources().getStringArray(R.array.task_repeats)[position]);
}
@Override
public void onNothingSelected(AdapterView<?> arg0) { }
});
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.withoutDeadLine:
onWithoutDeadlineChecked(view);
break;
}
}
public void onWithoutDeadlineChecked(View v) {
if(v.getId() == R.id.withoutDeadLine) {
if(!withoutDeadline) {
((TextView) v).setTextColor(blueColor);
deadLine.setEnabled(false);
withoutDeadline = true;
} else {
((TextView) v).setTextColor(grayColor);
deadLine.setEnabled(true);
withoutDeadline = false;
}
}
}
public String getDeadline() {
return deadLine.getText().toString();
}
public int getRepeatValue() {
return repeatValue;
}
public List<Boolean> getFixDaysList() {
return weeksView.getFixDaysList();
}
public boolean isWithoutDeadline() {
return withoutDeadline;
}
}
В этом классе у нас есть спиннер, по клику на который по клику мы выбираем нужные данные из массива который у нас указан в string.xml. Да и вообще я забыл указать весь этот файл, так что я его приведу ниже.
string.xml
<resources>
<string name="app_name">Horizontal Goal Calendar</string>
<string name="calendar">Calendar</string>
<string name="add">Add goal</string>
<string name="task.new.target.mon">MON</string>
<string name="task.new.target.thu">TUE</string>
<string name="task.new.target.wen">WED</string>
<string name="task.new.target.sut">THU</string>
<string name="task.new.target.fr">FRI</string>
<string name="task.new.target.surth">SAT</string>
<string name="task.new.target.sun">SUN</string>
<string name="task.new.target.shletude">Schedule</string>
<string name="task.new.target.repeat">Repeats</string>
<string name="task.new.target.each.dat">a day</string>
<string name="task.new.target.deadline">Terms</string>
<string name="task.new.target.days">days</string>
<string name="task.new.target.without.deadline">Without terms</string>
<string name="task.new.target.toast.error">Chose please any day from list above.</string>
<string name="task.details.times.day" formatted="false">%s times a day, during %s days</string>
<string-array name="task_repeats">
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
<item>5</item>
<item>6</item>
<item>7</item>
<item>8</item>
<item>9</item>
<item>10</item>
</string-array>
</resources>
Вот он. Собственно тут мы мы сетим адаптеры в этой вьюхе, для спиннера, он клик лисенеры для вьюх и т. д. По клику на кнопку without deadlines у нас делается или активным или не активным поле для ввода количества дней, ну и подствечиваем эту кнопку. И куча сеттеров и геттеров для получения данных из этой вьюхи.
XML будет иметь такой вид:
view_new_target_shletude.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/border_with_shadow"
android:layout_marginTop="10dp">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/task.new.target.shletude"
android:textAllCaps="true"
android:textAppearance="?android:attr/textAppearanceMedium" />
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="right" >
<ImageView
android:layout_width="25dp"
android:layout_height="25dp"
android:id="@+id/schletudeQuestion"
android:src="@mipmap/question_grey" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:padding="10dp">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.project.piechartcallendarexample.ui.add.view.WeeksView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/weeksView">
</com.project.piechartcallendarexample.ui.add.view.WeeksView>
</LinearLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:layout_marginTop="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/task.new.target.repeat"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@android:color/black"
android:textStyle="bold" />
<Spinner
android:layout_width="75dp"
android:layout_height="40dp"
android:id="@+id/repeatSpinner"
android:layout_marginLeft="10dp"
android:gravity="center_horizontal" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:text="@string/task.new.target.each.dat"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@android:color/black"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:layout_marginTop="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/task.new.target.deadline"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@android:color/black"
android:textStyle="bold" />
<EditText
android:layout_width="50dp"
android:layout_height="wrap_content"
android:id="@+id/deadLine"
android:inputType="numberDecimal"
android:maxLength="4"
android:gravity="center_horizontal" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:text="@string/task.new.target.days"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@android:color/black"
android:textStyle="bold" />
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="right">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
android:text="@string/task.new.target.without.deadline"
android:id="@+id/withoutDeadLine"
android:layout_marginLeft="10dp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
А еще у нас есть WeeksView в котором у нас список кнопок дней недели. Сама вьюха представляет из себя просто список текствьюх которые по клику делаются активными или нет и добавляем их в массив с которого потом читаем какие у нас выбранны и записываем в БД.
WeeksView.java
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.project.piechartcallendarexample.R;
import java.util.ArrayList;
import java.util.List;
import butterknife.BindColor;
import butterknife.BindView;
import butterknife.ButterKnife;
public class WeeksView extends LinearLayout implements View.OnClickListener {
@BindView(R.id.monBtn)
TextView monBtn;
@BindView(R.id.thuBtn)
TextView thuBtn;
@BindView(R.id.wedBtn)
TextView wedBtn;
@BindView(R.id.thurBtn)
TextView thurBtn;
@BindView(R.id.frBtn)
TextView frBtn;
@BindView(R.id.surthBtn)
TextView sutBtn;
@BindView(R.id.sunBtn)
TextView sunBtn;
@BindColor(R.color.colorPrimary)
int blueColor;
@BindColor(R.color.dark_gray_text)
int grayColor;
private boolean monday;
private boolean tuesday;
private boolean wednesday;
private boolean thursday;
private boolean friday;
private boolean saturday;
private boolean sunday;
private List<Boolean> fixDaysList;
public WeeksView(Context context) {
super(context);
init(context);
}
public WeeksView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public WeeksView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
inflate(context, R.layout.view_tasks_weeks, this);
setOrientation(VERTICAL);
ButterKnife.bind(this);
setupViews();
}
private void setupViews() {
monBtn.setOnClickListener(this);
thuBtn.setOnClickListener(this);
wedBtn.setOnClickListener(this);
thurBtn.setOnClickListener(this);
frBtn.setOnClickListener(this);
sutBtn.setOnClickListener(this);
sunBtn.setOnClickListener(this);
fixDaysList = new ArrayList<>(6);
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.monBtn:
case R.id.thuBtn:
case R.id.wedBtn:
case R.id.thurBtn:
case R.id.frBtn:
case R.id.surthBtn:
case R.id.sunBtn:
onWeekChecked(view);
break;
}
}
public void onWeekChecked(View v) {
switch(v.getId()) {
case R.id.monBtn:
monday = selectDayOfWeek(v, monday);
fixDaysList.add(monday);
break;
case R.id.thuBtn:
tuesday = selectDayOfWeek(v, tuesday);
fixDaysList.add(tuesday);
break;
case R.id.wedBtn:
wednesday = selectDayOfWeek(v, wednesday);
fixDaysList.add(wednesday);
break;
case R.id.thurBtn:
thursday = selectDayOfWeek(v, thursday);
fixDaysList.add(thursday);
break;
case R.id.frBtn:
friday = selectDayOfWeek(v, friday);
fixDaysList.add(friday);
break;
case R.id.surthBtn:
saturday = selectDayOfWeek(v, saturday);
fixDaysList.add(saturday);
break;
case R.id.sunBtn:
sunday = selectDayOfWeek(v, sunday);
fixDaysList.add(sunday);
break;
}
}
private boolean selectDayOfWeek(View v, boolean day) {
if(!day) {
((TextView) v).setTextColor(blueColor);
day = true;
} else {
((TextView) v).setTextColor(grayColor);
day = false;
}
return day;
}
public List<Boolean> getFixDaysList() {
fixDaysList.add(0 ,monday);
fixDaysList.add(1, tuesday);
fixDaysList.add(2, wednesday);
fixDaysList.add(3, thursday);
fixDaysList.add(4, friday);
fixDaysList.add(5, saturday);
fixDaysList.add(6, sunday);
return fixDaysList;
}
}
Вьюха состоит всего из 7 текствьюх запиханные в LinearLayout.
view_tasks_weeks.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/task.new.target.mon"
android:id="@+id/monBtn"
android:padding="10dp"
android:textStyle="bold"
android:textColor="@color/dark.gray.text" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/task.new.target.thu"
android:id="@+id/thuBtn"
android:padding="10dp"
android:textStyle="bold"
android:textColor="@color/dark.gray.text" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/task.new.target.wen"
android:id="@+id/wedBtn"
android:padding="10dp"
android:textStyle="bold"
android:textColor="@color/dark.gray.text" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/task.new.target.sut"
android:id="@+id/thurBtn"
android:padding="10dp"
android:textStyle="bold"
android:textColor="@color/dark.gray.text" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/task.new.target.fr"
android:id="@+id/frBtn"
android:padding="10dp"
android:textStyle="bold"
android:textColor="@color/dark.gray.text" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/task.new.target.surth"
android:id="@+id/surthBtn"
android:padding="10dp"
android:textStyle="bold"
android:textColor="@color/dark.gray.text" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/task.new.target.sun"
android:id="@+id/sunBtn"
android:padding="10dp"
android:textStyle="bold"
android:textColor="@color/dark.gray.text" />
</LinearLayout>
</LinearLayout>
Ну, а дальше мы уже можем создавать фрагмент и использовать все наши компоненты которые мы создали.
NewTaskFragment.java
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.TextUtils;
import android.text.format.Time;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;
import com.project.piechartcallendarexample.R;
import com.project.piechartcallendarexample.etc.RandomUtils;
import com.project.piechartcallendarexample.ui.BaseFragment;
import com.project.piechartcallendarexample.ui.add.view.ScheduleView;
import com.project.piechartcallendarexample.ui.calendar.db.TaskController;
import com.project.piechartcallendarexample.ui.calendar.db.model.RealmBooleanModel;
import com.project.piechartcallendarexample.ui.calendar.db.model.RealmTaskHistoryModel;
import com.project.piechartcallendarexample.ui.calendar.db.model.RealmTaskModel;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import java.util.Calendar;
import java.util.List;
import butterknife.BindColor;
import butterknife.BindView;
import io.realm.RealmList;
/**
* Created by gleb on 6/16/17.
*/
public class NewTaskFragment extends BaseFragment {
public static final int WITHOUD_DEADLINE = 5475;
public static final int START_DATE = 0;
@BindView(R.id.schletudeView)
public ScheduleView scheduleView;
@BindView(R.id.target)
public EditText target;
@BindColor(R.color.colorPrimary)
int blueColor;
@BindColor(R.color.dark_gray_text)
int grayColor;
private RealmTaskModel realmTaskModel;
private RealmList<RealmTaskHistoryModel> taskHistoryModels;
public TaskController taskController;
@Override
public int getViewId() {
return R.layout.fragment_add_task;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
taskController = new TaskController(context);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
taskHistoryModels = new RealmList<>();
realmTaskModel = new RealmTaskModel();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.add:
int deadlineValue = WITHOUD_DEADLINE;
if(!TextUtils.isEmpty(scheduleView.getDeadline()))
deadlineValue = Integer.valueOf(scheduleView.getDeadline());
realmTaskModel.setTitle(target.getText().toString());
realmTaskModel.setDateStart(getDate(START_DATE));
realmTaskModel.setDateFinish(getDate(daysBetweenDates(deadlineValue)));
realmTaskModel.setCountDays(scheduleView.isWithoutDeadline() ? WITHOUD_DEADLINE : deadlineValue);
realmTaskModel.setCountRepeats(scheduleView.getRepeatValue());
List<Boolean> boo = scheduleView.getFixDaysList();
RealmList<RealmBooleanModel> realmListBooleans = new RealmList<>();
for(int k = 0; k < boo.size(); k++) {
RealmBooleanModel booleanModel = new RealmBooleanModel();
booleanModel.setId(RandomUtils.getRandomValue());
booleanModel.setFixDay(boo.get(k));
realmListBooleans.add(booleanModel);
}
realmTaskModel.setFixDaysList(realmListBooleans);
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DATE, -1);
for(int i = 0; i < daysBetweenDates(deadlineValue); i++) {
RealmTaskHistoryModel realmTaskHistoryModel = new RealmTaskHistoryModel();
calendar.add(Calendar.DATE, 1);
realmTaskHistoryModel.setId(RandomUtils.getRandomValue());
realmTaskHistoryModel.setDate(calendar.get(Calendar.YEAR) + " " + calendar.get(Calendar.MONTH) + " " + calendar.get(Calendar.DATE));
taskHistoryModels.add(realmTaskHistoryModel);
}
realmTaskModel.setRealmTaskHistoryModels(taskHistoryModels);
if (realmTaskModel.getFixDaysList().size() > 0)
new InsertDataAboutTask().execute();
else
Toast.makeText(context, getString(R.string.task_new_target_toast_error), Toast.LENGTH_LONG).show();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
public String getDate(int daysCount) {
Time time = new Time();
Calendar now = Calendar.getInstance();
if(daysCount == START_DATE) {
time.set(now.get(Calendar.DATE), now.get(Calendar.MONTH), now.get(Calendar.YEAR));
} else {
now.add(Calendar.DATE, daysCount);
time.set(now.get(Calendar.DATE), now.get(Calendar.MONTH), now.get(Calendar.YEAR));
}
return time.format("%d %m %Y");
}
public int daysBetweenDates(int daysCount) {
DateTimeFormatter formatter = DateTimeFormat.forPattern("dd MM yyyy");
DateTime d1 = formatter.parseDateTime(getDate(START_DATE));
DateTime d2 = formatter.parseDateTime(getDate(daysCount));
Duration duration = new Duration(d1, d2);
int days = (int)duration.getStandardDays();
return days;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.menu_add, menu);
super.onCreateOptionsMenu(menu, inflater);
}
private class InsertDataAboutTask extends AsyncTask<Void, Void, Void> {
private TaskController taskController;
@Override
protected Void doInBackground(Void... params) {
taskController = new TaskController(context);
taskController.addTask(realmTaskModel);
return null;
}
@Override
protected void onPreExecute() { }
@Override
protected void onPostExecute(Void sum) {
Intent intent = new Intent();
context.setResult(context.RESULT_OK, intent);
context.finish();
}
}
}
getViewId() — мы указываем какую вьюху гам использовать.
onCreate() — инициализируем наш контроллер по записи в БД.
onViewCreated() — инициализируем нудные списки и модели.
onOptionsItemSelected() — по клику на кнопку add мы сохраняем данные в БД.
getDate() — возвращает стартовую дату в виде строки.
daysBetweenDates() — возвращает количество дней между днями указаными от начальной даты и конечной.
В классе InsertDataAboutTask у нас идет запись в БД, это мы делаем для разделения потоков записи, так как если без этого таска у нас приложение зависнет просто, хоть разработчики реалма и говорят что оно создает отдельные потоки каждый раз когда выполняется какое-то действие, но почему-то не всегда это срабатывает.
По окончанию выполнения таска в onPostExecute() у нас вызывается закрытие активити и setResult() метод для возвращения того что все ок.
А еще у нас есть такая штука как RandomUtils которая создаем рандомное число для записи id для таблицы количества дней и таблицы для хранения активных дней.
RandomUtils.java
import java.util.Random;
public class RandomUtils {
public static int getRandomValue() {
Random rand = new Random();
return Math.abs(rand.nextInt());
}
}
Теперь мы умеем писать в БД, и оно умеет создавать цели для календаря, далее нам нужно начать писать календарь. У нас есть класс MainActivity, я его переименовал себе в CalendarActivity что бы было понятней, да и поэстетичней впринципе выглядит. В нем у нас как и в активити новой цели так же всего один метод getViewId().
CalendarActivity.java
import com.project.piechartcallendarexample.R;
import com.project.piechartcallendarexample.ui.BaseActivity;
public class CalendarActivity extends BaseActivity {
@Override
public int getViewId() {
return R.layout.activity_main;
}
}
В activity_main все так же как и в new task activity мы прописали фрагмент который у нас будет отображаться в активитии леяут который мы хотим отображать по дефолту.
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools">
<fragment
android:id="@+id/main"
android:name="com.project.piechartcallendarexample.ui.calendar.CalendarFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:layout="@layout/fragment_calendar"
android:layout_weight="0.1" />
</LinearLayout>
А дальше давайте создадим фрагмент, в нем у нас будет использоваться один список который мы в адаптере будем разбивать на нужные айтемы.
CalendarFragment.java
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.ListView;
import com.project.piechartcallendarexample.R;
import com.project.piechartcallendarexample.ui.BaseFragment;
import com.project.piechartcallendarexample.ui.add.NewTaskActivity;
import com.project.piechartcallendarexample.ui.calendar.adapter.CalendarAdapter;
import com.project.piechartcallendarexample.ui.calendar.adapter.model.CalendarModel;
import com.project.piechartcallendarexample.ui.calendar.db.TaskController;
import com.project.piechartcallendarexample.ui.calendar.db.model.RealmTaskModel;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import butterknife.BindView;
public class CalendarFragment extends BaseFragment implements CalendarAdapter.OnClickCallback {
public static final int REQUEST_RESULT = 256;
public static final String[] dayOfWeeksArray = { "MON", "THU", "WED", "THU", "FRI", "SAT", "SUN" };
@BindView(R.id.listView)
ListView listView;
public TaskController taskController;
public CalendarAdapter calendarAdapter;
public Calendar current;
@Override
public int getViewId() {
return R.layout.fragment_calendar;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
taskController = new TaskController(context);
current = Calendar.getInstance();
current.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
calendarAdapter = new CalendarAdapter(context);
calendarAdapter.setItem(taskController.getAllTasks().size() > 0 ? taskController.getAllTasks() : new ArrayList<RealmTaskModel>());
calendarAdapter.setCalendarDated(showDatesInView());
calendarAdapter.setCalendar(current);
calendarAdapter.setOnClickListener(this);
listView.setAdapter(calendarAdapter);
}
public String getDayOfWeek(Calendar calendar) {
return new SimpleDateFormat("EE").format(calendar.getTime());
}
public void refreshAdapter() {
calendarAdapter.setCalendar(current);
calendarAdapter.setCalendarDated(showDatesInView());
listView.setAdapter(calendarAdapter);
}
public ArrayList<CalendarModel> showDatesInView() {
ArrayList<CalendarModel> dates = new ArrayList<>();
for(int i = 0; i < dayOfWeeksArray.length; i++) {
CalendarModel model = new CalendarModel();
model.setDay(current.get(Calendar.DATE));
model.setMonth(current.get(Calendar.MONTH));
model.setYear(current.get(Calendar.YEAR));
model.setDayOfWeek(getDayOfWeek(current));
current.add(Calendar.DATE, 1);
dates.add(model);
}
return dates;
}
@Override
public void onNextClick() {
current.add(Calendar.DATE, 0);
refreshAdapter();
}
@Override
public void onPrevClick() {
current.add(Calendar.DATE, -14);
refreshAdapter();
}
@Override
public void onDateClick(int dayCount) { }
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.menu_calendar, menu);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.add:
startActivityForResult(new Intent(context, NewTaskActivity.class), REQUEST_RESULT);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
calendarAdapter.setCalendar(current);
calendarAdapter.setItem(taskController.getAllTasks());
listView.setAdapter(calendarAdapter);
}
}
Здесь у нас есть несколько моментов. У нас есть dayOfWeeksArray в котором у нас прописаны дни недели, у нас их 7, можно впринципе просто сделать константу которая будет равна семи, но я подумал что так сделать будет получше.
onCreate() — у нас создается объект нашего таск контроллера который является контроллером для работы с БД. Проинициализировали календарь и сказали ему что у нас понедельник является первым днем недели, а не воскресенье.
onViewCreated() — создали в нем адаптер, засетили туда все нужные данные что бы в адаптере были данные для отображения и отправили все это в листвью.
getDayOfWeek() — возвращает текущий день недели.
refreshAdapter() — обновляет адаптер с новыми данными каждый раз когда мы его вызываем.
showDatesInView() — в этом методе мы создаем даты для шапки которая у нас отображает дни недели и даты. Тут мы записываем это все в список предварительно прогнав календарь в цикле и ретурном возвращаем в метод CalnedarModel приведу его ниже.
onClick() — методы которые по клику что-то делают, думаю они не требуют особого внимания.
onActivityResult() — обновляем адаптер по возвращению на этот фрагмент.
CalendarModel.java
public class CalendarModel implements Comparable<CalendarModel> {
private String dayOfWeek;
private Integer day;
private Integer month;
private Integer year;
public String getDayOfWeek() {
return dayOfWeek;
}
public void setDayOfWeek(String dayOfWeek) {
this.dayOfWeek = dayOfWeek;
}
public int getDay() {
return day;
}
public void setDay(int date) {
this.day = date;
}
@Override
public int compareTo(CalendarModel another) {
return day.compareTo(another.day);
}
public Integer getYear() {
return year;
}
public void setYear(Integer year) {
this.year = year;
}
public Integer getMonth() {
return month;
}
public void setMonth(Integer month) {
this.month = month;
}
}
А теперь у нас начинается екшн — будем писать адаптер для календаря, он у нас довольно сложно выглядит со стороны, но на деле там все очень просто, давайте посмотрим на код с начала, а потом уже буду разъяснять что к чему там.
CalendarAdapter.java
import android.app.Activity;
import android.content.Context;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TableLayout;
import android.widget.TableRow;
import android.widget.TextView;
import com.project.piechartcallendarexample.R;
import com.project.piechartcallendarexample.ui.calendar.adapter.holder.HeaderHolder;
import com.project.piechartcallendarexample.ui.calendar.adapter.holder.ItemHolder;
import com.project.piechartcallendarexample.ui.calendar.adapter.model.CalendarModel;
import com.project.piechartcallendarexample.ui.calendar.db.TaskController;
import com.project.piechartcallendarexample.ui.calendar.db.model.RealmTaskModel;
import com.project.piechartcallendarexample.ui.calendar.view.CalendarDaysView;
import com.project.piechartcallendarexample.ui.calendar.view.CalendarProgressView;
import com.project.piechartcallendarexample.ui.calendar.view.ProgressView;
import java.util.ArrayList;
import java.util.Calendar;
public class CalendarAdapter extends BaseAdapter {
private static final int TYPE_HEAD = 0;
private static final int TYPE_ITEM = 1;
private static final String DATE_TEMPLATE = "MMMM \nyyyy";
private HeaderHolder headHolder;
private ItemHolder itemHolder;
private TaskController taskController;
private OnClickCallback onClickCallback;
private DateFormat dateFormatter = new DateFormat();
private int datePosition = 0;
private Context context;
private Calendar calendar;
private ArrayList<RealmTaskModel> listData = new ArrayList<>();
private ArrayList<CalendarModel> getDatesInView;
public CalendarAdapter(Context context) {
this.context = context;
taskController = new TaskController(context);
}
public void setItem(ArrayList<RealmTaskModel> articleResultModels) {
this.listData = articleResultModels;
notifyDataSetChanged();
}
public void setCalendarDated(ArrayList<CalendarModel> calendarDated) {
this.getDatesInView = calendarDated;
notifyDataSetChanged();
}
public void setCalendar(Calendar calendar) {
this.calendar = calendar;
notifyDataSetChanged();
}
@Override
public int getItemViewType(int position) {
if(position == 0)
return TYPE_HEAD;
else
return TYPE_ITEM;
}
@Override
public int getViewTypeCount() {
return super.getViewTypeCount() + 1;
}
@Override
public int getCount() {
return listData.size() + 1;
}
@Override
public RealmTaskModel getItem(int position) {
if(position == 0) return null;
return listData.size() > 0 ? listData.get(position - 1) : new RealmTaskModel();
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
LayoutInflater inflater = ((Activity) context).getLayoutInflater();
TableRow.LayoutParams rowParams = new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT, 0.1f);
TableRow tableRow = new TableRow(context);
tableRow.setOrientation(TableLayout.HORIZONTAL);
int type = getItemViewType(position);
switch(type) {
case TYPE_HEAD:
convertView = inflater.inflate(R.layout.item_calendar_days, parent, false);
headHolder = new HeaderHolder(convertView);
headHolder.currentMonth.setFirstCupText(dateFormatter.format(DATE_TEMPLATE, calendar.getTime()));
for(int i = 0; i < 7; i++) {
CalendarDaysView daysView = new CalendarDaysView(context);
daysView.setDate(String.valueOf(getDatesInView.get(i).getDay()));
daysView.setDayOfWeek(getDatesInView.get(i).getDayOfWeek());
tableRow.addView(daysView, rowParams);
}
headHolder.daysRow.addView(tableRow);
headHolder.nextMonth.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
datePosition += 7;
onClickCallback.onNextClick();
}
});
headHolder.prevMonth.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (datePosition > 0)
datePosition -= 7;
onClickCallback.onPrevClick();
}
});
headHolder.currentMonth.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onClickCallback.onDateClick(datePosition);
}
});
break;
case TYPE_ITEM:
convertView = inflater.inflate(R.layout.item_calendar_progress, parent, false);
itemHolder = new ItemHolder(convertView);
final RealmTaskModel model = getItem(position);
itemHolder.title.setText(model.getTitle());
itemHolder.days.setText(String.format(context.getString(R.string.task_details_times_day), model.getCountRepeats(), model.getCountDays()));
for(int numOfDaysInc = 0; numOfDaysInc < 7; numOfDaysInc++) {
CalendarProgressView progressView = new CalendarProgressView(context);
for (int daysInc = 0; daysInc < (7 + datePosition); daysInc++) {
if (compareDates(getFullDate(numOfDaysInc), getRealmDate(position, daysInc))) {
setRepeatsInfo(progressView.getText(), model, daysInc, model.getFixDaysList().get(numOfDaysInc).isFixDay());
onProgressClick(progressView.getProgressView(), model, daysInc, model.getFixDaysList().get(numOfDaysInc).isFixDay(), position);
}
}
tableRow.addView(progressView, rowParams);
}
itemHolder.progressRow.addView(tableRow);
break;
}
return convertView;
}
private void onProgressClick(ProgressView progressView, final RealmTaskModel model, final int daysInc, boolean isFixed, final int position) {
if(isFixed) {
progressView.setValue(model.getRealmTaskHistoryModels().get(daysInc).getProgress());
progressView.setProgressMaximum(model.getCountRepeats());
progressView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int progress = model.getRealmTaskHistoryModels().get(daysInc).getProgress() + 1;
int id = model.getRealmTaskHistoryModels().get(daysInc).getId();
if (progress <= model.getCountRepeats())
setProgress(progress, position, daysInc, id);
else
setProgress(0, position, daysInc, id);
notifyDataSetChanged();
}
});
}
}
private void setProgress(int progress, int position, int daysInc, int id) {
if (compareDates(getRealmDate(position, daysInc), getCurrentDate()))
taskController.updateTaskProgress(id, progress);
}
private void setRepeatsInfo(TextView repeatText, RealmTaskModel model, int i, boolean isFixed) {
if(isFixed) {
if (!TextUtils.isEmpty(model.getRealmTaskHistoryModels().get(i).getDate())) {
repeatText.setVisibility(View.VISIBLE);
repeatText.setText(model.getCountRepeats() + "/" + model.getRealmTaskHistoryModels().get(i).getProgress());
}
}
}
public boolean compareDates(String date1, String date2) {
if (date1.trim().equals(date2.trim())) {
return true;
} else {
return false;
}
}
private String getRealmDate(int position, int id) {
String value = "1900 01 01";
if (id < getItem(position).getRealmTaskHistoryModels().size())
value = getItem(position).getRealmTaskHistoryModels().get(id).getDate();
return value;
}
private String getFullDate(int position) {
int year = getDatesInView.get(position).getYear();
int month = getDatesInView.get(position).getMonth();
int day = getDatesInView.get(position).getDay();
return year + " " + month + " " + day;
}
private String getCurrentDate() {
Calendar cal = Calendar.getInstance();
return cal.get(Calendar.YEAR) + " " + cal.get(Calendar.MONTH) + " " + cal.get(Calendar.DATE);
}
public void setOnClickListener(OnClickCallback onNextMonthClickListener) {
this.onClickCallback = onNextMonthClickListener;
}
public interface OnClickCallback {
void onNextClick();
void onPrevClick();
void onDateClick(int dayCount);
}
}
Вот такой у нас адаптер получается, в нем у нас солянка сборная. Тут мы и сетим данные, и обрабатываем их, и выплевываем обратно в список, в общем щас будем все по порядку разбирать. В этом адаптере у нас есть два вида 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
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:slanting_progress="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimaryDark">
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical">
<LinearLayout
android:orientation="horizontal"
android:layout_width="125dp"
android:layout_height="60dp"
android:gravity="center_vertical">
<ImageView
android:id="@+id/prevMonth"
android:src="@mipmap/arrow_white_left"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:background="@color/colorPrimaryDark">
</ImageView>
<com.project.tele-plus.ui.calendar.view.CapFirstLetterTextView
android:id="@+id/currentMonth"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="0.1"
android:gravity="center_horizontal"
android:text="date"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@color/white" />
<ImageView
android:id="@+id/nextMonth"
android:src="@mipmap/arrow_white_write"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:background="@color/colorPrimaryDark">
</ImageView>
</LinearLayout>
<TableLayout
android:id="@+id/daysRow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" />
</LinearLayout>
</LinearLayout>
Тут интересный момент есть, вот этот класс CapFirstLetterTextView, мы его используем что бы сделать месяц отображаемый с большой буквы, так как по умолчанию в классе Calendar он идет с маленькой буквы… Так что пришлось повыпендриваться. Его код я приведу чуть попозже.
item_calendar_progress.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:slanting_progress="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/enter.button.color">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="70dp"
android:gravity="center_vertical">
<LinearLayout
android:orientation="horizontal"
android:layout_width="125dp"
android:layout_height="60dp"
android:padding="5dp"
android:id="@+id/nameLayout"
android:gravity="center_vertical">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:text="Large Text"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="@+id/days"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:text="Small Text"
android:textAppearance="?android:attr/textAppearanceSmall" />
</LinearLayout>
</LinearLayout>
<TableLayout
android:id="@+id/progressRow"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal" />
</LinearLayout>
</LinearLayout>
И не забываем про холдеры, они у нас тоже в отдельных классах. Если кто не знает, холдеры у нас для того что бы можно было обращаться к вьюхам напрямую из леяута.
HeaderHolder.java
import android.view.View;
import android.widget.ImageView;
import android.widget.TableLayout;
import com.project.piechartcallendarexample.R;
import com.project.tele-plus.ui.calendar.view.CapFirstLetterTextView;
import butterknife.BindColor;
import butterknife.BindView;
import butterknife.ButterKnife;
public class HeaderHolder {
@BindView(R.id.currentMonth)
public CapFirstLetterTextView currentMonth;
@BindView(R.id.prevMonth)
public ImageView prevMonth;
@BindView(R.id.nextMonth)
public ImageView nextMonth;
@BindView(R.id.daysRow)
public TableLayout daysRow;
@BindColor(R.color.colorPrimary)
public int primaryColor;
@BindColor(R.color.colorPrimaryDark)
public int primaryDarcColor;
@BindColor(R.color.colorGrayLight)
public int colorGrayLight;
@BindColor(R.color.enter_button_color)
public int backgroundColor;
public HeaderHolder(View view){
ButterKnife.bind(this, view);
}
}
ItemHolder.java
import android.view.View;
import android.widget.TableLayout;
import android.widget.TextView;
import com.project.tele-plus.R;
import butterknife.BindView;
import butterknife.ButterKnife;
public class ItemHolder {
@BindView(R.id.title)
public TextView title;
@BindView(R.id.days)
public TextView days;
@BindView(R.id.progressRow)
public TableLayout progressRow;
public ItemHolder(View view){
ButterKnife.bind(this, view);
}
}
Вы вот щас смотрите на код и такие, а что же мы сетим, ведь в леяутах пусто, и там нету ничего что мы отображаем в адаптере, а я вам скажу что да, у нас есть кастомные две вьюхи еще которые у нас в циклах сетятся, я думаю у тех кто скопировал просто код они как раз подкрашиваются красным. Первая вьюха у нас CalendarDaysView, ее мы используем для шапки и для дней недели, в ней у нас две текствьюхи, день недели и дата.
CalendarDaysView.java
import android.content.Context;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.project.piechartcallendarexample.R;
import butterknife.BindView;
import butterknife.ButterKnife;
public class CalendarDaysView extends LinearLayout {
@BindView(R.id.date)
TextView date;
@BindView(R.id.dayOfWeek)
TextView dayOfWeek;
public CalendarDaysView(Context context) {
super(context);
init(context);
}
public CalendarDaysView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
public CalendarDaysView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
inflate(context, R.layout.view_calendar_days_item, this);
setOrientation(HORIZONTAL);
ButterKnife.bind(this);
}
public void setDate(String date) {
this.date.setText(date);
}
public void setDayOfWeek(String dayOfWeek) {
this.dayOfWeek.setText(dayOfWeek);
}
}
В конструкторе у нас идет сетап метода init() — в нем мы задаем леяут который мы используем для вьюхи, задаем ориентацию горизонтальную для того что бы просто так. И инициализируем баттер найф. А дальше у нас просто два сеттера которые получают и сетят данные в текствьюхи.
view_calendar_days_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="60dp"
android:layout_weight="1"
android:background="@color/colorPrimaryDark"
android:gravity="center_vertical|center_horizontal"
android:orientation="vertical"
android:padding="5dp">
<TextView
android:id="@+id/date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="false"
android:text="0"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@color/white" />
<TextView
android:id="@+id/dayOfWeek"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="false"
android:text="@string/task.new.target.mon"
android:textAllCaps="true"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@color/white" />
</LinearLayout>
А вторая вьюха у нас CalendarProgressView, мы ее используем для отображения кружочков прогресса. В ней у нас собственно кружочек прогресса и текствьюха которая отображает количество повторений.
CalendarProgressView.java
import android.content.Context;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.project.tele-plus.R;
import butterknife.BindView;
import butterknife.ButterKnife;
public class CalendarProgressView extends LinearLayout {
@BindView(R.id.progresBar)
ProgressView progresBar;
@BindView(R.id.text)
TextView text;
public CalendarProgressView(Context context) {
super(context);
init(context);
}
public CalendarProgressView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
public CalendarProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
inflate(context, R.layout.view_calendar_progress_item, this);
setOrientation(HORIZONTAL);
ButterKnife.bind(this);
}
public ProgressView getProgressView() {
return progresBar;
}
public TextView getText() {
return text;
}
}
Собственно тоже самое что и в предыдущей вьюхе, только тут мы возвращаем текст и прогресс вью вместо того что бы в них сетить что-то, это мы делаем непосредственно в адаптере. Единственное что еще оставляет вопросы, что такое ProgressView, это опять же кастомная вьюха которая умеет очень многое, ее код опять же приведу ниже. А пока посмотрите на этот великлепный xml.
view_calendar_progress_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="70dp"
android:layout_weight="0.1"
android:gravity="center_vertical|center_horizontal"
android:orientation="vertical">
<com.project.tele-plus.ui.calendar.view.ProgressView
android:id="@+id/progresBar"
android:layout_width="35dp"
android:layout_height="35dp" />
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="5/5"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@color/circle.fifth.color"
android:textSize="11sp"
android:visibility="invisible" />
</LinearLayout>
Ну вот собственно почти все готово, осталось добавить пару вьюх которые у нас остались, а это CapFirstLetterTextView и ProgressView. Начнем с простого, создатим кастомную текствьюху для того что бы наш текст всегда стартовал с заглавной буквы.
CapFirstLetterTextView.java
import android.content.Context;
import android.util.AttributeSet;
import android.widget.TextView;
public class CapFirstLetterTextView extends TextView {
public CapFirstLetterTextView(Context context) {
super(context);
}
public CapFirstLetterTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CapFirstLetterTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setFirstCupText(CharSequence text) {
String upperString = text.toString().substring(0, 1).toUpperCase() + text.toString().substring(1);
setText(upperString);
}
}
setFirstCupText() — метод который работает так же как и с setText, только увеличивает нашу первую букву автоматом.
А дальше у нас еще есть наш ProgressView который красивенько отображает прогресс, умеет его увеличивать и уменьшать. Давайте же посмотрим что это такое и как это работает.
ProgressView.java
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.graphics.Xfermode;
import android.util.AttributeSet;
import android.view.View;
import com.project.tele-plus.R;
import com.project.tele-plus.etc.CircleRadiusHelper;
public class ProgressView extends View {
private final RectF mRect = new RectF();
private final RectF mRectInner = new RectF();
private final Paint mPaintForeground = new Paint();
private final Paint mPaintStroke = new Paint();
private final Paint mPaintBackground = new Paint();
private final Paint mPaintErase = new Paint();
private static final Xfermode PORTER_DUFF_CLEAR = new PorterDuffXfermode(PorterDuff.Mode.CLEAR);
private int mColorForeground = Color.WHITE;
private int mColorStroke = Color.WHITE;
private int mColorBackground = Color.BLACK;
private float mValue = -1f;
private static final float PADDING = 4;
private float mPadding;
private Bitmap mBitmap;
private Paint emptyData = new Paint(Paint.ANTI_ALIAS_FLAG);
private int progressMax = 1;
private static final float INNER_RADIUS_RATIO = 0.84f;
public ProgressView(Context context) {
this(context, null);
}
public ProgressView(Context context, AttributeSet attrs) {
super(context, attrs);
Resources r = context.getResources();
float scale = r.getDisplayMetrics().density;
mPadding = scale * PADDING ;
mPaintForeground.setColor(mColorForeground);
mPaintForeground.setAntiAlias(true);
mPaintBackground.setColor(mColorBackground);
mPaintBackground.setAntiAlias(true);
mPaintErase.setXfermode(PORTER_DUFF_CLEAR);
mPaintErase.setAntiAlias(true);
emptyData.setColor(Color.GRAY);
setForegroundColor(getResources().getColor(R.color.progressbar_foreground_color));
setBackgroundColor(getResources().getColor(R.color.progressbar_background_color));
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(mBitmap, getWidth() / 2 - mBitmap.getWidth() / 2, getHeight() / 2 - mBitmap.getHeight() / 2, null);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
float bitmapWidth = w - 2 * mPadding;
float bitmapHeight = h - 2 * mPadding;
float radius = Math.min(bitmapWidth / 2, bitmapHeight / 2);
mRect.set(0, 0, bitmapWidth, bitmapHeight);
radius *= INNER_RADIUS_RATIO;
mRectInner.set(bitmapWidth / 2f - radius, bitmapHeight / 2f - radius, bitmapWidth / 2f + radius, bitmapHeight / 2f + radius);
updateBitmap();
}
private void updateBitmap() {
if (mRect == null || mRect.width() == 0) return;
mBitmap = Bitmap.createBitmap((int) mRect.width(), (int) mRect.height(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(mBitmap);
float angle = mValue * (3600 / progressMax);
if (mValue < 0) {
setBackgroundColor(getResources().getColor(R.color.progressbar_empty_color));
setBackgroundStrokeColor(getResources().getColor(R.color.round_color_center_circle_text));
setForegroundColor(getResources().getColor(R.color.progressbar_empty_color));
canvas.drawCircle(canvas.getWidth() / 2, canvas.getHeight() / 2, CircleRadiusHelper.getRadius(canvas) / 2, emptyData);
} else {
if(mValue == 0) {
emptyData.setStyle(Paint.Style.STROKE);
canvas.drawCircle(canvas.getWidth() / 2, canvas.getHeight() / 2, CircleRadiusHelper.getRadius(canvas), emptyData);
} else {
canvas.drawArc(mRect, -90, 360, true, mPaintBackground);
canvas.drawArc(mRect, -90, angle, true, mPaintForeground);
}
}
postInvalidate();
}
public void setForegroundColor(int color) {
this.mColorForeground = color;
mPaintForeground.setColor(color);
invalidate();
}
public void setBackgroundColor(int color) {
this.mColorBackground = color;
mPaintBackground.setColor(color);
invalidate();
}
public void setBackgroundStrokeColor(int color) {
this.mColorStroke = color;
mPaintStroke.setColor(color);
mPaintStroke.setStrokeWidth(3);
mPaintStroke.setStyle(Paint.Style.STROKE);
invalidate();
}
public synchronized void setValue(float value) {
mValue = value / 10f;
if(Float.isInfinite(mValue))
mValue = 0;
updateBitmap();
}
public void setProgressMaximum(int value) {
this.progressMax = value;
}
}
По сути это вьюха, которую мы рисуем на канвасе и перерисовываем каждый раз когда выполняем какое действие над ней. У нас в этой вьюхе есть несколько состояний, первое это не активное, второе это активное но пустое, третье активное заполненное на какую-то часть, часть зеленым часть красным, и активная но заполненная полностью зеленым цветом.
В конструкторе мы инициализируем все нужные нам параметры Paint, у нас их там порядка 4 штук, и они имеют разные состояния которые я описал выше.
onDraw() — рисует «битмап» по центру выделенной области и заполняет ее стилем, в данном случае null, а это значит что он у нас просто пустой.
onSizeChanged() — метод который по изменению размера экрана будет подстраивать вьюху под нужные размеры, что бы она была всегда одинаковой.
updateBitmap() — метод который по обновлении вьюхи проверяет какой стиль к ней применить, если например у нас mValue меньше нуля — то у нас кружочек пустой, если mValue больше нуля то — делаем его заполненным на нужный угол и нужные цвет. А если mValue равно нулю то просто делаем его пустым. А по умолчанию у нас создается серый маленький кружок который не нажимается и не имеет вообще никаких действий. В конце вызываем метод обновления вьюхи.
setForegroundColor(), setBackgroundColor(), setBackgroundStrokeColor() — методы по задаванию нужного стиля кружочку.
setValue() — метод который мы вызываем когда кликаем на кружочек что бы увеличить его диаметер занимаемой поверхности на ту или иную mValue.
setProgressMaximum() — метод в котором мы задаем на сколько кусочков разбиваем наш кружочек, пусть то будет или 1 или 10 или 15, сколько укажите на столько оно и будет раз разбивать.
Тут еще один метод у нас используется, но он был вынесен в отдельный класс, так как я его использовал в других вьюхах в этом же проекте. Этот метод расчитывает радиус холста на котором у нас рисуется вьюха.
CircleRadiusHelper.java
import android.graphics.Canvas;
public class CircleRadiusHelper {
public static float getRadius(Canvas canvas) {
float width = canvas.getWidth();
float height = canvas.getHeight();
float minSize = width > height ? height : width;
float radius = minSize / 2;
return radius;
}
}
Вроде бы все. Весь код который используется для создания такого календаря я вынес в статью. Возможно кому-то эта статья поможет в написании чего-то похожего.