【需求解决系列之二】回款日历的实现

1,645 阅读10分钟

前言

有一段时间没有写东西了,是因为最近换工作了,忙着适应新的同事,新的环境和新的项目。得空的时候有个朋友给了我一个需求,让我有时间帮他看看,他在忙别的,没时间弄,所以我就做了一下。

另外说点题外话,最近P2P暴雷特别多,我表示表面看上去冷静,内心其实慌的一匹,投资的标的又遇到了展期,很是担心。也劝诫各位,投资需谨慎,P2P更加如此。下面的这个需求也是服务P2P项目的。


需求

一、需求说明

  • 左右滑动日历区可切换月份,上面月份对应改变,年份实时切换
  • 月份区域可以左右滑动,点击实际月份,下面联动跳转到对应的月份表
  • 标识今日日期,点击具体日期显示具体样式效果
  • 程序提供具体回款日期列表,在日历中需要体现出来
需求效果图

二、效果预览

首先会显示出当前日期,以及给定的回款的数据显示;日历区域可以左右滑动,上面月份会对应切换;上面月份可以左右滑动,点击对应月份完成切换

效果预览

实现

实现思路

其实对于每一个需求,无论大小,我们都需要进行一个需求的分析,然后分块来处理,将大需求划分成小的需求来实现。

对于上面的需求,我们可以将其分解成以下几点:
一、日历View的实现
二、月份选择区域的实现
三、效果联动


具体实现

一、日历View的实现

对于日历View的实现,我们大致需要考虑三点,第一个是显示日期的View的创建和布局,这里我们需要考虑到底需要多少个控件才能装下所有数据;第二个是每个月的具体日期的显示,主要考虑的点是这个月的第一天是星期几,方便绘制;第三点就是对特殊点的处理,主要考虑的是被点击日期的展示效果和回款日期的展示效果;最后一点就是将View放进ViewPager实现左右滑动切换日历。

第一点:我们需要初始化显示星期的控件和初始化显示具体日期的控件

    //初始化头部布局
    private void initHeadView(Context context) {
        //获取当前时间 记录当前年月日保存下来
        Date dt = new Date();
        SimpleDateFormat matter = new SimpleDateFormat("yyyy MM dd");
        if (currYear == 0)
            tYear = currYear = Integer.parseInt(matter.format(dt).split(" ")[0]);
        if (currMonth == 0)
            tMonth = currMonth = Integer.parseInt(matter.format(dt).split(" ")[1]);
        tDay = currDay = Integer.parseInt(matter.format(dt).split(" ")[2]);

        //添加头部的显示星期的布局
        LinearLayout llWeek = new LinearLayout(context);
        llWeek.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
        llWeek.setOrientation(HORIZONTAL);
        for (int i = 0; i < 7; i++) {
            //把最终显示星期的TextView添加到LinearLayout里面去
            TextView week = new TextView(context);
            LayoutParams lpWeek = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
            lpWeek.weight = 1;
            week.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
            week.setText(weeks[i]);
            week.setTextColor(mWeekColor);
            week.setTextSize(14);
            week.setGravity(Gravity.CENTER);
            llWeek.addView(week, lpWeek);
        }
        //将星期添加到视图中
        addView(llWeek);
    }

    //初始化整体布局
    private void initBodyView(Context context) {
        int margin = (int) (10 * scaleSize);
        setPadding(0, 20, 0, 50 - margin);
        //添加日期的数据 6行
        for (int i = 0; i < 6; i++) {
            layouts[i] = new LinearLayout(context);
            layouts[i].setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, 1));
            layouts[i].setOrientation(HORIZONTAL);

            for (int j = 0; j < 7; j++) {
                containers[i][j] = new LinearLayout(context);
                containers[i][j].setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, 1));
                containers[i][j].setOrientation(LinearLayout.VERTICAL);
                containers[i][j].setGravity(Gravity.CENTER);

                days[i][j] = new TextView(context);

                LayoutParams lpWeek = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
                lpWeek.weight = 1;
                lpWeek.setMargins(margin, margin, margin, margin);

                days[i][j].setLayoutParams(new LayoutParams(defaultWidth, defaultWidth));
                days[i][j].setTextColor(mDaysColor);
                days[i][j].setTextSize(14);
                days[i][j].setGravity(Gravity.CENTER);

                containers[i][j].addView(days[i][j]);

                LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
                layoutParams.setMargins(0, (int) (2 * scaleSize), 0, 0);
                huikuan[i][j] = new TextView(context);
                huikuan[i][j].setLayoutParams(layoutParams);
                huikuan[i][j].setTextColor(Color.parseColor("#F8E71C"));
                huikuan[i][j].setTextSize(10);
                huikuan[i][j].setGravity(Gravity.CENTER);

-----------------------------------------
                containers[i][j].addView(huikuan[i][j]);

                containers[i][j].setOnClickListener(this);
                layouts[i].addView(containers[i][j]);
            }
            addView(layouts[i]);
        }
    }

第二点:获取每月第一天对应星期几,这样方便从对应的位置开始绘制日期数

获取指定年份指定月份的第一天的位置

    //获取指定年份指定月份的第一天的位置
    private int getDaysOfMonth(int year, int month) {
        Calendar cal = Calendar.getInstance();
        cal.set(Calendar.YEAR, year);
        cal.set(Calendar.MONTH, month - 1);
        return cal.getActualMaximum(Calendar.DATE);
    }

获取指定年份指定月份的最后一天

    //获取指定年份指定月份的最后一天
    public int getLastDayOfMonth(int year, int month) {
        month = month - 1;
        if (month == 0) {
            month = 12;
            year = year - 1;
        }
        month = month - 1;
        if (month == 0) {
            month = 12;
            year = year - 1;
        }
        Calendar cal = Calendar.getInstance();
        cal.set(Calendar.YEAR, year);
        cal.set(Calendar.MONTH, month);
        cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DATE));
        return Integer.parseInt(new SimpleDateFormat("dd").format(cal.getTime()));
    }

第三点:根据上面获取的数据进行日期的设置
我们获取到指定年份指定月份的第一天的位置之后,开始往后遍历累加设置日期,直到累加到指定年份指定月份的最后一天。剩下的就是设置设置上个月的日期,以及下个月的日期。上个月的日期直接首先获取上个月的天数,然后从这个月第一天的位置向前依次累减即可,下个月的直接从0直接累计设置即可。最后就是处理点击事件,给每个TextView设置一个事件监听,然后统一做回调处理并设置相关样式即可,为了清晰简单,这里就不再赘述了。

区域数据显示

具体代码实现

    //填充数据
    private void putData(List<String> dates) {
        //获取上个月的最后一天
        int lastMonthLastDay = getLastDayOfMonth(currYear, currMonth);
        //获取这个月第一天对应的星期
        int firstDayOfMonth = getFisrtDayOfMonth(currYear, currMonth);
        //获取这个月的天数
        int daysOfMonth = getDaysOfMonth(currYear, currMonth);
        int index = 1;
        int temp = 0;
        for (int i = 0; i < 6; i++) {
            for (int j = 0; j < 7; j++) {
                //第一行数据 可能包含上个月日期
                if (i == 0) {
                    if (j >= firstDayOfMonth - 1) {
                        days[i][j].setText((firstDay++) + "");
                        days[i][j].setTextColor(Color.parseColor("#FFFFFF"));
                        temp++;
                    } else {
                        //上个月的日期
                        days[i][j].setText((lastMonthLastDay - firstDayOfMonth + 2 + j) + "");
                        days[i][j].setTextColor(Color.parseColor("#AEEC8B"));
                    }
                } else {
                    if (firstDay <= daysOfMonth) {
                        days[i][j].setText((firstDay++) + "");
                        days[i][j].setTextColor(Color.parseColor("#FFFFFF"));
                        if (i < 5) {
                            temp++;
                        }
                    } else {
                        //下个月的日期
                        days[i][j].setText((index++) + "");
                        days[i][j].setTextColor(Color.parseColor("#AEEC8B"));
                    }
                }
            }
        }
        if (temp < daysOfMonth) {
            //说明5行并没有显示完 需要显示第6行
            layouts[5].setVisibility(VISIBLE);
        } else {
            //说明5行显示完了 隐藏第6行
            layouts[5].setVisibility(GONE);
        }
    }

第四点:结合ViewPager实现左右滑动效果
要实现需求中左右滑动的效果,不二选择就是ViewPager了。不过我们需要注意一下View的复用,所以我们要对视图进行缓存,这一块不用多说,看下具体适配器的实现,注释很清楚就不多说了。然后通过setPrimaryItem获取到当前操作的视图,对此视图进行数据的绑定操作。

public class CalendarPagerAdapter extends PagerAdapter {

    //缓存上一次回收的CalendarView
    private LinkedList<CalendarView> cache = new LinkedList<>();

    //记录当前展示的View
    private CalendarView currView;

    //记录需要展示的数量
    private int count;

    //初始化事件监听
    private CalendarView.OnItemClickListener listener;

    public CalendarPagerAdapter(int count, CalendarView.OnItemClickListener listener) {
        this.count = count;
        this.listener = listener;
    }

    public void setHuiKuan(List<String> dates) {
        if (currView != null)
            currView.setHuiKuan(dates);
    }

    @Override
    public void setPrimaryItem(ViewGroup container, int position, Object object) {
        currView = (CalendarView) object;
    }

    @Override
    public int getCount() {
        return count;
    }

    @Override
    public boolean isViewFromObject(View view, Object object) {
        return view == object;
    }

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        //尽可能复用View
        final CalendarView view;
        if (!cache.isEmpty()) {
            view = cache.removeFirst();
            //根据position获取当前页面需要展示的年份和月份
            int[] yearAndMonth = getYearAndMonth(position);
            view.setData(yearAndMonth[0], yearAndMonth[1]);
        } else {
            view = new CalendarView(container.getContext());
            //根据position获取当前页面需要展示的年份和月份
            int[] yearAndMonth = getYearAndMonth(position);
            view.setData(yearAndMonth[0], yearAndMonth[1]);
        }
        if (listener != null)
            view.setListener(listener);
        container.addView(view);
        return view;
    }

    //记录开始的年份 我们这里是从2014-5 到 这个月后面的36个月
    int startYear = 2014;
    int startMonth = 5;
    int afterMonth = 36;

    //根据position获取到此页面需要展示年月份的数据
    public int[] getYearAndMonth(int position) {
        int cMonth = startMonth + position;
        int cYear = startYear + cMonth / 12;

        if (cMonth % 12 == 1) {
            //增加一年
            cMonth = 1;
        } else if (cMonth % 12 == 0) {
            //正好12月
            cMonth = 12;
            cYear--;
        } else {
            cMonth = cMonth % 12;
        }
        return new int[]{cYear, cMonth};
    }

    //根据年月反推position
    public int getPosition(int[] yearAndMonth) {
        int year = yearAndMonth[0];
        int month = yearAndMonth[1];
        //计算需要展示的所有月数
        if (year == startYear) {
            if (month > startMonth) {
                return month - startMonth;
            } else {
                return 0;
            }
        }
        return (year - startYear - 1) * 12 + (12 - startMonth) + month;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        container.removeView((CalendarView) object);
        cache.addLast((CalendarView) object);
    }
}

二、月份选择区域的实现

需求中有一个很重要的点就是当前月份需要居中展示在所有的可视月份中。比如你当前选择了8月,那么上面的区域会展示6,7,8,9,10月,而且8月是在正中间的。首先,可以在86个月中进行滑动选择(86个月指的是我们的需求,从2014-5到本月之后的36个月),我们考虑使用RecyclerView来实现。为了保证居中,我们需要RecyclerView的item的宽度是整个RecyclerView的五分之一,那么每次显示5个,5个正好可以充满整个容器布局;其次,在每次选定月份之后,我们需要自动的获取需要滑动的距离,然后自动滚动到对应的月份,并保证该月份处于正中间状态。

其中最难的部分可能就是怎么保证被选中的月份如何居中显示在RecyclerView中吧。核心方法就是mRecycleView.smoothScrollBy(distance,duration);

  //自动滚动到指定position
  private void autoSmooth(int position){
                //记录第一个可视Item距离最左边的距离
                int top = 0;
                //获取可视的第一个item下标
                int pFirst = linearLayoutManager.findFirstVisibleItemPosition();
                //获取可视的第一个View
                View viewByPosition = linearLayoutManager.findViewByPosition(pFirst);
                //获取可视的第一个View的left 作为滑动的依据
                if (viewByPosition != null)
                    top = viewByPosition.getLeft();
                //获取每一个Item的宽度 我们默认一屏显示5条 所以除以5
                float itemViewHeight = recyclerViewWidth / 5;
                //计算需要滑动的Item的数量 这个自己比划算一算
                int needScrollPostion = position - pFirst - 3;
                //计算最终需要滑动的距离 为负数就是向相反方向滑动
                int distance = (int) (needScrollPostion * itemViewHeight + (itemViewHeight - Math.abs(top)));
                //开始滑动
                recyclerview.smoothScrollBy(distance, 10);
}

三、效果联动

这一部分就是将ViewPager的左右选择作用到上面的月份选择上,实现实时匹配;再将上面的月份选择的点击选择某一个月份的事件作用到ViewPager上,也就是viewPager.setCurrentItem(position);这一部分的实现都在MainActivity.java中,感兴趣可以去看看。


项目地址和结语

其实类似日历的成熟的解决方案有很多很多,为什么还是选择了自己写呢?一是因为类似的解决方案非常多,却不一定是百分百契合你的需求,别人的解决方案在功能上或多或少会与你产品不同,你需要去修改别人的方案了;再者,可能别人的效果更加炫酷,却不一定是你产品经理真正需要的。二是自己写的代码会带来什么样的坑自己心里是清楚的,第三方的却不一定清楚,在使用的时候心里难免不踏实。所以,在有时间和精力的情况下,可以尝试参考别人的实现自己去写属于自己的功能。

还有一个点需要解释一下,为什么没有将这个需求封装成一个library让别人引用而是直接将逻辑写在MainActivity中呢?因为我觉得这个需求并没有什么特别多的公用元素在里面,他跟实际的业务需要结合很紧(回款),如果包成library,开发者在调用的时候需要对非常多的地方进行修改,不够通用,要封装只能对自定义的那个CalendarView进行封装,但是他的功能又很简单(这完全是基于需求),所有就索性不做特殊处理了。

最后献上项目地址: SuperCalendarDemo
如果连接失效就直接点击这个链接吧!github.com/MZCretin/Su…


关于我的

我就是比较喜欢用代码解决生活中的问题,感觉很开心,哈哈哈。也希望大家关注我的简书,掘金,Github和CSDN。

简书首页,链接是 www.jianshu.com/u/123f97613…

掘金首页,链接是 juejin.cn/user/109916…

Github首页,链接是 github.com/MZCretin

CSDN首页,链接是 blog.csdn.net/u010998327

我是Cretin,一个可爱的小男孩。