android日历选择按月分组并用recyclerview展示

    技术2025-04-01  20

    今天分享一个日历选择控件,可以定义日期可选、选择范围、按月分组展示。这个日历无非就是把每个日期的数据通过系统的日历查询出来,然后用recyclerview展示即可,数据模型里可以定义哪些可选以及选定状态等等。思路就是这样了,先看看效果:

    首先定义好数据来源,即从系统的calendar获取日期列表,这里因为是要按月分组,所以我选择用一个key为月份的时间戳value为对应月份的所有日期list的map来接收查询出来的日历,在展示的时候把map转为分组的list即可:

    import android.annotation.SuppressLint; import androidx.annotation.NonNull; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.LinkedHashMap; /** * 日历工具类 * @author ly * date 2020/4/24 9:52 */ public class CalendarUtils { public final static String YEAR = "yyyy"; public final static String YEAR_MONTH = "yyyy年MM月"; public final static String MONTH_DAY = "MM-dd"; public final static String DATE = "yyyy-MM-dd"; public final static String TIME = "HH:mm"; public final static String MONTH_DAY_TIME = "MM月dd日 hh:mm"; public final static String DATE_TIME_SECOND = "yyyy-MM-dd HH:mm:ss"; public final static String DATE_TIME = "yyyy-MM-dd HH:mm"; public final static String DATE1_TIME = "yyyy/MM/dd HH:mm"; public static @NonNull LinkedHashMap<Long, ArrayList<DateInfo>> getMonthMap(Calendar initialCalendar, int monthNum) { return getMonthMap(initialCalendar, monthNum, Integer.MAX_VALUE); } public static @NonNull LinkedHashMap<Long, ArrayList<DateInfo>> getMonthByLimit(Calendar initialCalendar, int validSelNumLimit) { return getMonthMap(initialCalendar, Integer.MAX_VALUE, validSelNumLimit); } /** * 以月为单位,获取每个月的日期列表 * * @param initialCalendar 初始选中的日期 * @param validSelNumLimit 最大有效选中数量 * @return 返回一个key为当前loop月的时间戳,value为当前月的所有日期list的有序map * @author ly on 2020/4/25 11:37 */ public static @NonNull LinkedHashMap<Long, ArrayList<DateInfo>> getMonthMap(Calendar initialCalendar, int monthNum, int validSelNumLimit) { //key:当前loop月的时间戳 value:当前月的所有日期list LinkedHashMap<Long, ArrayList<DateInfo>> dateMap = new LinkedHashMap<>(); ArrayList<DateInfo> dateInMonthList = new ArrayList<>(); if (initialCalendar == null) initialCalendar = Calendar.getInstance(); Calendar c = Calendar.getInstance(); //设置开始遍历的date //也可以不使用下面的set,直接c.setTime(initialCalendar.getTime()),只是第一个月的数据可能不足那个月的最大天数(ui不美观) //告诉calendar从指定月的第一天开始,而不是精确到某一天 c.set(initialCalendar.get(Calendar.YEAR), initialCalendar.get(Calendar.MONTH), 1); int loopMonthCount = 0; int validDateNum = 0;//有效的可选日期数量 while (loopMonthCount < monthNum) { // 获取当前月最大天数 int maxDayNum = c.getActualMaximum(Calendar.DATE); int month = c.get(Calendar.MONTH); DateInfo dateInfo = new DateInfo(); dateInfo.year = c.get(Calendar.YEAR); dateInfo.month = month + 1; dateInfo.day = c.get(Calendar.DAY_OF_MONTH); dateInfo.timestamp = c.getTimeInMillis(); dateInfo.week = c.get(Calendar.DAY_OF_WEEK); dateInfo.isSelected = initialCalendar.get(Calendar.MONTH) == c.get(Calendar.MONTH) && initialCalendar.get(Calendar.DATE) == c.get(Calendar.DATE); //是否为可选的日期 boolean isAvailableDate = (dateInfo.isSelected || c.after(initialCalendar)) && dateInfo.week != Calendar.SUNDAY && dateInfo.week != Calendar.SATURDAY; if (isAvailableDate) validDateNum++; dateInfo.canSelect = isAvailableDate && validDateNum <= validSelNumLimit; if (dateInMonthList.isEmpty()) {//在每一月的第一周前面补空data for (int j = 0; j < dateInfo.week - 1; j++) { DateInfo d = new DateInfo(); dateInMonthList.add(0, d); } } dateInMonthList.add(dateInfo); if (maxDayNum == dateInfo.day) {//每loop到月底put一次 dateMap.put(dateInfo.timestamp, new ArrayList<>(dateInMonthList)); dateInMonthList.clear(); if (validDateNum < validSelNumLimit) { loopMonthCount++; } else { break;//达到最大可选数后自动跳出looper } } //条件满足时一直add天数即可 c.add(Calendar.DATE, 1); } return dateMap; } public static @NonNull LinkedHashMap<Long, ArrayList<DateInfo>> getMonthMap(String startDate, String endDate) { //key:当前loop月的时间戳 value:当前月的所有日期list LinkedHashMap<Long, ArrayList<DateInfo>> dateMap = new LinkedHashMap<>(); ArrayList<DateInfo> dateInMonthList = new ArrayList<>(); long startTimestamp = getMillisecondByFormat(startDate, DATE); long endTimestamp = getMillisecondByFormat(endDate, DATE); if (endTimestamp < startTimestamp || startTimestamp <= 0) return dateMap; Calendar start = Calendar.getInstance(); start.setTimeInMillis(startTimestamp); Calendar end = Calendar.getInstance(); end.setTimeInMillis(endTimestamp); Calendar c = Calendar.getInstance(); //设置开始遍历的date //也可以不使用下面的set,直接c.setTime(initialCalendar.getTime()),只是第一个月的数据可能不足那个月的最大天数(ui不美观) //告诉calendar从指定月的第一天开始,而不是精确到某一天 c.set(start.get(Calendar.YEAR), start.get(Calendar.MONTH), 1); boolean needLoop = true; while (needLoop) { // 获取当前月最大天数 int maxDayNum = c.getActualMaximum(Calendar.DATE); int month = c.get(Calendar.MONTH); DateInfo dateInfo = new DateInfo(); dateInfo.year = c.get(Calendar.YEAR); dateInfo.month = month; dateInfo.day = c.get(Calendar.DAY_OF_MONTH); dateInfo.timestamp = c.getTimeInMillis(); dateInfo.week = c.get(Calendar.DAY_OF_WEEK); //此处理想条件应为 start.getTimeInMillis()==c.getTimeInMillis(),但开始时间戳和查询出来的时间戳不相等(就算是同一天) //先用这个判断着 dateInfo.isSelected = start.get(Calendar.YEAR) == c.get(Calendar.YEAR) && start.get(Calendar.MONTH) == c.get(Calendar.MONTH) && start.get(Calendar.DATE) == c.get(Calendar.DATE); boolean isEnd = end.get(Calendar.MONTH) == c.get(Calendar.MONTH) && end.get(Calendar.DATE) == c.get(Calendar.DATE); dateInfo.canSelect = (dateInfo.isSelected || c.after(start)) && (c.before(end) || isEnd) && dateInfo.week != Calendar.SUNDAY && dateInfo.week != Calendar.SATURDAY; if (dateInMonthList.isEmpty()) {//在每一月的第一周前面补空data for (int j = 0; j < dateInfo.week - 1; j++) { DateInfo d = new DateInfo(); dateInMonthList.add(0, d); } } dateInMonthList.add(dateInfo); if (maxDayNum == dateInfo.day) {//每loop到月底put一次 dateMap.put(dateInfo.timestamp, new ArrayList<>(dateInMonthList)); dateInMonthList.clear(); needLoop = c.before(end);//每到月底判断一下,是否还需继续loop } //条件满足时一直add天数即可 c.add(Calendar.DATE, 1); } return dateMap; } /** * 获取当前日期一周的日期 */ @SuppressLint("SimpleDateFormat") public static ArrayList<DateInfo> getWeek(String date) { ArrayList<DateInfo> result = new ArrayList<>(); Calendar c = Calendar.getInstance(); try { c.setTime(new SimpleDateFormat("yyyy-MM-dd").parse(date)); } catch (ParseException e) { e.printStackTrace(); } c.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY); //获取本周一的日期 for (int i = 0; i < 7; i++) { DateInfo entity = new DateInfo(); entity.timestamp = c.getTimeInMillis(); entity.day = c.get(Calendar.DATE); entity.week = c.get(Calendar.DAY_OF_WEEK); c.add(Calendar.DATE, 1); result.add(entity); } return result; } /** * 获取系统当前日期 */ public static String getCurrDate(String format) { @SuppressLint("SimpleDateFormat") SimpleDateFormat formatter = new SimpleDateFormat(format); Date curDate = new Date(System.currentTimeMillis());//获取当前时间 return formatter.format(curDate); } /** * String类型的日期时间转化为毫秒(1970-)类型. * * @param strDate String形式的日期时间 * @param format 格式化字符串,如:"yyyy-MM-dd HH:mm:ss" * @author LY 2015-9-16 上午11:40:26 */ public static long getMillisecondByFormat(String strDate, String format) { @SuppressLint("SimpleDateFormat") SimpleDateFormat mSimpleDateFormat = new SimpleDateFormat(format); Date date = null; try { date = mSimpleDateFormat.parse(strDate); } catch (ParseException e) { e.printStackTrace(); } if (date == null) return 0; return date.getTime(); } /** * 毫秒----> format格式的日期 * * @author LY 2015-9-18 下午3:41:32 */ public static String getDateByMillisecond(long milliseconds, String format) { Date d = new Date(milliseconds); @SuppressLint("SimpleDateFormat") SimpleDateFormat f = new SimpleDateFormat(format); return f.format(d); } public static String getCurDateByFormat(String format) { return getDateByMillisecond(System.currentTimeMillis(), format); } public static String getNextMonthByFormat(String format) { Calendar c = Calendar.getInstance(); c.setTimeInMillis(System.currentTimeMillis()); c.add(Calendar.MONTH, 1); Date d = c.getTime(); @SuppressLint("SimpleDateFormat") SimpleDateFormat f = new SimpleDateFormat(format); return f.format(d); } }

    日历的数据entity,注意一下canSelect我这里的业务需求是周末不可选,其他在范围内的日期可选,可以根据自己的需求来定制。DateInfo:

    import com.robot.common.utils.CalendarUtils; import java.util.Calendar; /** * @author ly * date 2020/4/24 9:52 */ public class DateInfo { public long timestamp; //时间戳 public int year; public int month; public int day; public int week;//一周中第几天,非中式 //是否选中 public boolean isSelected; //日期是否可选 public boolean canSelect; /** * 根据美式周末到周一 返回 */ public String getWeekName() { String name = ""; switch (week) { case Calendar.SUNDAY: name = "周日"; break; case Calendar.MONDAY: name = "周一"; break; case Calendar.TUESDAY: name = "周二"; break; case Calendar.WEDNESDAY: name = "周三"; break; case Calendar.THURSDAY: name = "周四"; break; case Calendar.FRIDAY: name = "周五"; break; case Calendar.SATURDAY: name = "周六"; break; default: break; } return name; } public String getDay() { if (day > 0) { return String.valueOf(day); } else { return ""; } } public int getMonth() { return month + 1; } public String getDate() { return CalendarUtils.getDateByMillisecond(timestamp, CalendarUtils.DATE); } }

    最后就是负责展示的类了,很简单。这里是dialog来实现的:

    import android.app.Activity; import android.view.Gravity; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.chad.library.adapter.base.BaseSectionQuickAdapter; import com.chad.library.adapter.base.BaseViewHolder; import com.chad.library.adapter.base.entity.SectionEntity; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; /** * @author ly * date 2019/8/1 17:36 */ public class SelDateDialog extends BaseDialog { private SectionAdapter mAdapter; private OnDateSelectedListener onDateSelectedListener; private List<DateSection> dateList = new ArrayList<>(); private LinkedHashMap<Long, ArrayList<DateInfo>> monthMap; private RecyclerView rv; private int selPosition; private DateInfo selDateInfo; public SelDateDialog(@NonNull Activity activity, OnDateSelectedListener onDateSelectedListener) { super(activity); this.onDateSelectedListener = onDateSelectedListener; mAdapter = new SectionAdapter(dateList); mAdapter.setOnItemClickListener((adapter, view, position) -> { SectionEntity<DateInfo> item = mAdapter.getItem(position); if (item == null || item.t == null)//item.t=null可能是点到了header 不做处理 return; selPosition = position; mAdapter.selectOne(position); selDateInfo = item.t; if (onDateSelectedListener != null) { onDateSelectedListener.onDateSelect(selDateInfo); } // dismiss(); }); } @Override public int getLayoutResId() { return R.layout.dialog_sel_date; } @Override public void initViews() { if (getWindow() != null) { getWindow().getAttributes().width = ScreenUtil.getScreenWidth(); getWindow().getAttributes().height = (int) (ScreenUtil.getScreenHeight() * 0.7); getWindow().setGravity(Gravity.BOTTOM); getWindow().setWindowAnimations(com.robot.common.R.style.bottom_enter_anim); } setCanceledOnTouchOutside(true); setCancelable(true); findViewById(R.id.m_tv_dialog_sel_date_cancel).setOnClickListener(view -> { dismiss(); }); rv = findViewById(R.id.m_rv_dialog_calendar); rv.setLayoutManager(new GridLayoutManager(getContext(), 7)); mAdapter.bindToRecyclerView(rv); rv.smoothScrollToPosition(selPosition); } public void setData(String startDate, String endDate) { if (!dateList.isEmpty()) { dateList.clear(); mAdapter.notifyDataSetChanged(); } // LinkedHashMap<Long, ArrayList<DateInfo>> month = CalendarUtils.getMonthByLimit(selCalendar, 30); monthMap = CalendarUtils.getMonthMap(startDate, endDate); for (Long aLong : monthMap.keySet()) { DateSection sectionTitle = new DateSection(true, CalendarUtils.getDateByMillisecond(aLong, CalendarUtils.YEAR_MONTH)); dateList.add(sectionTitle); ArrayList<DateInfo> monthDateList = monthMap.get(aLong); if (monthDateList != null) for (DateInfo dateInfo : monthDateList) { dateList.add(new DateSection(dateInfo)); if (dateInfo.isSelected) { selDateInfo = dateInfo; if (onDateSelectedListener != null) onDateSelectedListener.onDateSelect(selDateInfo); } } } DateSection sectionFooter = new DateSection(true, "后续日期暂不可订"); dateList.add(sectionFooter); } public LinkedHashMap<Long, ArrayList<DateInfo>> getMonthMap() { return monthMap; } public DateInfo getSelDateInfo() { return selDateInfo; } public void selectOne(DateInfo dateInfo) { if (dateInfo != null && dateList != null) { for (int i = 0; i < dateList.size(); i++) { DateSection dateSection = dateList.get(i); DateInfo info = dateSection.t; if (info != null) { info.isSelected = dateInfo.timestamp == info.timestamp; if (info.isSelected) selPosition = i; } } mAdapter.notifyDataSetChanged(); if (rv != null) rv.smoothScrollToPosition(selPosition); } } static class SectionAdapter extends BaseSectionQuickAdapter<DateSection, BaseViewHolder> { private int itemW; SectionAdapter(List<DateSection> data) { super(R.layout.dialog_sel_date_item, R.layout.dialog_sel_date_title, data); itemW = ScreenUtil.getScreenWidth() / 7; } void selectOne(int position) { for (int i = 0; i < mData.size(); i++) { SectionEntity<DateInfo> entity = mData.get(i); DateInfo dateInfo = entity.t; if (dateInfo != null) dateInfo.isSelected = position == i; } notifyDataSetChanged(); } @Override protected void convertHead(BaseViewHolder helper, final DateSection item) { helper.setText(R.id.m_tv_dialog_sel_date_title, item.header); } @Override protected void convert(BaseViewHolder helper, DateSection item) { DateInfo dateInfo = item.t; TextView tv = helper.getView(R.id.m_tv_dialog_sel_date); tv.setText(dateInfo.getDay()); helper.itemView.setEnabled(dateInfo.canSelect); tv.setBackgroundResource(dateInfo.isSelected ? R.mipmap.ic_date_item_sel : R.color.transparent); if (dateInfo.canSelect) { if (dateInfo.isSelected) { tv.setTextColor(0xffffffff); } else { tv.setTextColor(mContext.getResources().getColor(R.color.black_text1)); } } else { tv.setTextColor(0xffC3C3DC); } ViewGroup.LayoutParams layoutParams = helper.itemView.getLayoutParams(); layoutParams.width = itemW; layoutParams.height = itemW; } } private static class DateSection extends SectionEntity<DateInfo> { DateSection(boolean isHeader, String header) { super(isHeader, header); } DateSection(DateInfo dateInfo) { super(dateInfo); } } public interface OnDateSelectedListener { void onDateSelect(@NonNull DateInfo dateInfo); } }

    注:recyclerview的adapter使用了BaseRecyclerViewAdapterHelper

    布局也贴一下,dialog_sel_date:

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/ll_dialog_share_confirm" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom" android:background="@drawable/shape_white_tr16b0" android:gravity="bottom" android:orientation="vertical"> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <ImageView android:id="@+id/m_tv_dialog_sel_date_cancel" android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="@dimen/list_lr_margin" android:src="@mipmap/ic_close" android:text="取消" /> <com.robot.common.view.BoldTextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:gravity="center" android:text="选择日期" android:textColor="#ff282832" android:textSize="17sp" /> </RelativeLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:paddingTop="8dp" android:paddingBottom="15dp"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="日" android:textColor="#fffa496a" android:textSize="12sp" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="一" android:textColor="@color/black_text1" android:textSize="12sp" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="二" android:textColor="@color/black_text1" android:textSize="12sp" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="三" android:textColor="@color/black_text1" android:textSize="12sp" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="四" android:textColor="@color/black_text1" android:textSize="12sp" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="五" android:textColor="@color/black_text1" android:textSize="12sp" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="六" android:textColor="#fffa496a" android:textSize="12sp" /> </LinearLayout> <androidx.recyclerview.widget.RecyclerView android:id="@+id/m_rv_dialog_calendar" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:overScrollMode="never" tools:listitem="@layout/m_dialog_item_sel_scenic" /> </LinearLayout>

    dialog_sel_date_item:

    <?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/m_tv_dialog_sel_date" style="@style/text_black" android:layout_gravity="center" android:gravity="center" android:textSize="16sp" tools:text="10" />

    m_tv_dialog_sel_date_title:

    <?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/m_tv_dialog_sel_date_title" style="@style/text_black" android:layout_width="match_parent" android:background="#F3F3FA" android:gravity="center" android:paddingTop="6dp" android:paddingBottom="6dp" android:textSize="14sp" tools:text="2020.3"/>

    接着在需要用这个日历的地方创建上面的Dialog,传入开始及结束日期show即可:

    SelDateDialog selDateDialog = new SelDateDialog(this, dateInfo -> showToast(dateInfo.getDate())); selDateDialog.setData("2020-07-06", "2020-08-06"); selDateDialog.show();

    最后,我想说一下,github上有很多的开源日历库,那些库大多功能繁多,如果app对日历依赖性强,需求稍微复杂那我还是建议用开源库的,毕竟不用花时间造轮子。但是简单的需求(就如本例)就没必要用那些库了,而且还要看文档学他的使用方式,还不如自己动手写了。所以视情况而定,不要动不动就接第三方库,不利于自己能力的提升。

    Processed: 0.009, SQL: 9