このカレンダーレイアウトは二つの部分に分けられています。一つは曜日を表示するLinearLayout、もう一つは縦方向にスクロールするRecyclerViewです。
ユーティリティクラス:
implementation 'com.blankj:utilcode:1.17.3'
activity_calendarのレイアウトコード:
<?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"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:padding="5dp"
android:text="日" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:padding="5dp"
android:text="月" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:padding="5dp"
android:text="火" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:padding="5dp"
android:text="水" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:padding="5dp"
android:text="木" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:padding="5dp"
android:text="金" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:padding="5dp"
android:text="土" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:overScrollMode="never" />
</LinearLayout>
続いてCalendarActivityの実装:
- データの計算処理
- カレンダーのクリックイベントや範囲選択イベントの拡張可能
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.blankj.utilcode.constant.TimeConstants;
import com.blankj.utilcode.util.TimeUtils;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Locale;
public class CalendarActivity extends AppCompatActivity {
private RecyclerView recyclerView;
private CalendarRangeAdapter calendarRangeAdapter;
private int maxMonths = 12;
private List<DateBean> dateList = new ArrayList<>();
private List<DateBean> fullDateList = new ArrayList<>();
private Handler messageHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
setupViews();
super.handleMessage(msg);
}
};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_calendar);
recyclerView = findViewById(R.id.recycler_view);
new Thread(() -> initData()).start();
}
private void setupViews() {
GridLayoutManager layoutManager = new GridLayoutManager(CalendarActivity.this, 7);
layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
return fullDateList.get(position).getType() == 0 ? 7 : 1;
}
});
recyclerView.setLayoutManager(layoutManager);
calendarRangeAdapter = new CalendarRangeAdapter(CalendarActivity.this, fullDateList);
recyclerView.setAdapter(calendarRangeAdapter);
calendarRangeAdapter.setOnItemSelect(new CalendarRangeAdapter.OnItemSelect() {
@Override
public void onItemClick(int position) {}
@Override
public void onItemRangeSelect(String startDate, String endDate) {
System.out.println(startDate + "~" + endDate);
}
});
}
private void initData() {
for (int i = 0; i < maxMonths; i++) {
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.MONTH, i);
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH) + 1;
int maxDays = calendar.getActualMaximum(Calendar.DAY_OF_MONTH);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.CHINA);
String dateString = dateFormat.format(calendar.getTime());
DateBean monthBean = new DateBean();
monthBean.setDate(dateString.substring(0, 7));
monthBean.setCanSelect(false);
monthBean.setType(0);
dateList.clear();
dateList.add(monthBean);
calendar.set(Calendar.DAY_OF_MONTH, 1);
int firstDayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
for (int j = 0; j < firstDayOfWeek - 1; j++) {
DateBean dateBean = new DateBean();
dateBean.setCanSelect(false);
dateBean.setType(1);
dateBean.setDate("");
dateList.add(dateBean);
}
for (int j = 0; j < maxDays; j++) {
DateBean dateBean = new DateBean();
dateBean.setType(1);
dateBean.setCenterText(String.valueOf(j + 1));
dateBean.setIsSelected(true);
dateBean.setDate(year + "-" + padZero(month) + "-" + padZero(j + 1));
if (TimeUtils.getTimeSpanByNow(dateBean.getDate(), dateFormat, TimeConstants.DAY) < 0) {
dateBean.setCenterText(String.valueOf(j + 1));
dateBean.setCanSelect(false);
} else {
if (TimeUtils.getTimeSpanByNow(dateBean.getDate(), dateFormat, TimeConstants.DAY) > 0) {
dateBean.setCenterText(String.valueOf(j + 1));
} else {
dateBean.setCenterText("今日");
}
dateBean.setCanSelect(true);
}
dateList.add(dateBean);
}
fullDateList.addAll(dateList);
}
Message msg = messageHandler.obtainMessage();
messageHandler.sendMessage(msg);
}
private String padZero(int value) {
return value < 10 ? "0" + value : String.valueOf(value);
}
}
CalendarRangeAdapterの実装(データモデルに基づく操作):
- 型によって月のレイアウトと日付のレイアウトを描画
- クリックおよび範囲選択機能を含む
public class CalendarRangeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private Context context;
private List<DateBean> dataList;
private int[] selectedRange = new int[2];
private static final int MONTH_TYPE = 0;
private static final int DAY_TYPE = 1;
public CalendarRangeAdapter(Context context, List<DateBean> dataList) {
this.context = context;
this.dataList = dataList;
resetSelection();
}
public void resetSelection() {
for (int i = 0; i < dataList.size(); i++) {
dataList.get(i).setIsSelected(false);
dataList.get(i).setIsInSelectedRange(false);
dataList.get(i).setBottomText("");
}
selectedRange[0] = -1;
selectedRange[1] = -1;
}
public void notifySelectionChange() {
notifyDataSetChanged();
}
public void updateData(List<DateBean> dataList) {
this.dataList = dataList;
notifyDataSetChanged();
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
RecyclerView.ViewHolder holder = null;
if (viewType == MONTH_TYPE) {
View view = LayoutInflater.from(context).inflate(R.layout.item_month, parent, false);
holder = new MonthViewHolder(view);
} else {
View view = LayoutInflater.from(context).inflate(R.layout.item_day, parent, false);
holder = new DayViewHolder(view);
}
return holder;
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
final int adapterPosition = holder.getAdapterPosition();
if (holder instanceof MonthViewHolder) {
((MonthViewHolder) holder).monthTextView.setText(dataList.get(position).getDate().replace("-", "年") + "月");
} else {
final DayViewHolder viewHolder = (DayViewHolder) holder;
viewHolder.centerTextView.setText(dataList.get(adapterPosition).getCenterText());
viewHolder.bottomTextView.setText(dataList.get(adapterPosition).getBottomText());
if (dataList.get(adapterPosition).isCanSelect()) {
if (TimeUtils.getTimeSpanByNow(dataList.get(position).getDate(), new SimpleDateFormat("yyyy-MM-dd", Locale.CHINA), TimeConstants.DAY) < 0) {
viewHolder.centerTextView.setTextColor(ContextCompat.getColor(context, R.color.color_calendar_can_not_select));
} else {
if (TimeUtils.getTimeSpanByNow(dataList.get(position).getDate(), new SimpleDateFormat("yyyy-MM-dd", Locale.CHINA), TimeConstants.DAY) > 0) {
viewHolder.centerTextView.setTextColor(ContextCompat.getColor(context, R.color.color_calendar_can_select));
} else {
viewHolder.centerTextView.setTextColor(ContextCompat.getColor(context, R.color.color_calendar_today));
}
if (dataList.get(adapterPosition).isIsSelected()) {
viewHolder.dayLayout.setBackgroundColor(ContextCompat.getColor(context, R.color.color_calendar_background_select));
viewHolder.centerTextView.setTextColor(ContextCompat.getColor(context, R.color.color_calendar_select));
viewHolder.bottomTextView.setTextColor(ContextCompat.getColor(context, R.color.color_calendar_select));
} else if (dataList.get(adapterPosition).isIsInSelectedRange()) {
viewHolder.dayLayout.setBackgroundColor(ContextCompat.getColor(context, R.color.color_calendar_background_select_range));
viewHolder.centerTextView.setTextColor(ContextCompat.getColor(context, R.color.color_calendar_select));
viewHolder.bottomTextView.setTextColor(ContextCompat.getColor(context, R.color.color_calendar_select));
} else {
viewHolder.dayLayout.setBackgroundColor(ContextCompat.getColor(context, R.color.color_calendar_background_normal));
}
if (onItemClickListener != null) {
View.OnClickListener clickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
if (selectedRange[0] == -1 && selectedRange[1] == -1) {
selectedRange[0] = adapterPosition;
dataList.get(adapterPosition).setIsSelected(true);
dataList.get(adapterPosition).setBottomText("開始");
onItemClickListener.onItemClick(adapterPosition);
notifyDataSetChanged();
} else if (selectedRange[0] != -1 && selectedRange[1] == -1) {
onItemClickListener.onItemClick(adapterPosition);
if (adapterPosition < selectedRange[0]) {
resetSelection();
selectedRange[0] = adapterPosition;
dataList.get(adapterPosition).setIsSelected(true);
dataList.get(adapterPosition).setBottomText("開始");
} else if (adapterPosition > selectedRange[0]) {
selectedRange[1] = adapterPosition;
dataList.get(adapterPosition).setIsSelected(true);
dataList.get(adapterPosition).setBottomText("終了");
for (int i = selectedRange[0] + 1; i < selectedRange[1]; i++) {
dataList.get(i).setIsInSelectedRange(true);
dataList.get(i).setBottomText("");
}
onItemClickListener.onItemRangeSelect(dataList.get(selectedRange[0]).getDate(), dataList.get(selectedRange[1]).getDate());
} else {
resetSelection();
}
notifyDataSetChanged();
} else {
resetSelection();
selectedRange[0] = adapterPosition;
dataList.get(adapterPosition).setIsSelected(true);
dataList.get(adapterPosition).setBottomText("開始");
onItemClickListener.onItemClick(adapterPosition);
notifyDataSetChanged();
}
}
};
viewHolder.dayLayout.setOnClickListener(clickListener);
viewHolder.dayLayout.setTag(clickListener);
}
}
} else {
viewHolder.dayLayout.setBackgroundColor(ContextCompat.getColor(context, R.color.color_calendar_background_normal));
viewHolder.centerTextView.setTextColor(ContextCompat.getColor(context, R.color.color_calendar_can_not_select));
}
}
}
public OnItemSelect onItemClickListener;
public void setOnItemSelect(OnItemSelect listener) {
this.onItemClickListener = listener;
}
public interface OnItemSelect {
void onItemClick(int position);
void onItemRangeSelect(String startDate, String endDate);
}
@Override
public int getItemViewType(int position) {
return dataList.get(position).getType() == 0 ? MONTH_TYPE : DAY_TYPE;
}
@Override
public int getItemCount() {
return dataList == null ? 0 : dataList.size();
}
class MonthViewHolder extends RecyclerView.ViewHolder {
TextView monthTextView;
public MonthViewHolder(View itemView) {
super(itemView);
monthTextView = itemView.findViewById(R.id.tv_month);
}
}
class DayViewHolder extends RecyclerView.ViewHolder {
LinearLayout dayLayout;
TextView centerTextView;
TextView bottomTextView;
public DayViewHolder(View itemView) {
super(itemView);
dayLayout = itemView.findViewById(R.id.ll_day);
centerTextView = itemView.findViewById(R.id.tv_center);
bottomTextView = itemView.findViewById(R.id.tv_bottom);
}
}
}
DateBeanの定義(中央テキスト、下部テキスト、選択可能フラグなど):
public class DateBean {
private String date;
private String bottomText;
private String centerText;
private boolean canSelect;
private boolean isSelected;
private boolean isInSelectedRange;
private int type;
public String getDate() {
return date == null ? "" : date;
}
public void setDate(String date) {
this.date = date;
}
public String getCenterText() {
return centerText == null ? "" : centerText;
}
public void setCenterText(String centerText) {
this.centerText = centerText;
}
public boolean isCanSelect() {
return canSelect;
}
public void setCanSelect(boolean canSelect) {
this.canSelect = canSelect;
}
public boolean isIsSelected() {
return isSelected;
}
public void setIsSelected(boolean selected) {
isSelected = selected;
}
public boolean isIsInSelectedRange() {
return isInSelectedRange;
}
public void setIsInSelectedRange(boolean inSelectedRange) {
isInSelectedRange = inSelectedRange;
}
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
public String getBottomText() {
return bottomText == null ? "" : bottomText;
}
public void setBottomText(String bottomText) {
this.bottomText = bottomText;
}
}
color.xml(カスタマイズ可能):
<resources>
<color name="color_calendar_can_not_select">#dedede</color>
<color name="color_calendar_can_select">#505050</color>
<color name="color_calendar_select">#ffffff</color>
<color name="color_calendar_today">#F67332</color>
<color name="color_calendar_background_select">#F3BE30</color>
<color name="color_calendar_background_select_range">#7DF3BE30</color>
<color name="color_calendar_background_normal">#00000000</color>
</resources>
以上で完了です!