Androidの縦方向二重選択カレンダー

このカレンダーレイアウトは二つの部分に分けられています。一つは曜日を表示する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の実装:

  1. データの計算処理
  2. カレンダーのクリックイベントや範囲選択イベントの拡張可能
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の実装(データモデルに基づく操作):

  1. 型によって月のレイアウトと日付のレイアウトを描画
  2. クリックおよび範囲選択機能を含む
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>

以上で完了です!

タグ: Android カレンダー RecyclerView 日付選択 範囲選択

5月25日 10:27 投稿