【需求解決系列之二】回款日曆的實現

前言

有一段時間沒有寫東西了,是由於最近換工做了,忙着適應新的同事,新的環境和新的項目。得空的時候有個朋友給了我一個需求,讓我有時間幫他看看,他在忙別的,沒時間弄,因此我就作了一下。java

另外說點題外話,最近P2P暴雷特別多,我表示表面看上去冷靜,心裏其實慌的一匹,投資的標的又遇到了展期,非常擔憂。也勸誡各位,投資需謹慎,P2P更加如此。下面的這個需求也是服務P2P項目的。git


需求

1、需求說明

  • 左右滑動日曆區可切換月份,上面月份對應改變,年份實時切換
  • 月份區域能夠左右滑動,點擊實際月份,下面聯動跳轉到對應的月份表
  • 標識今日日期,點擊具體日期顯示具體樣式效果
  • 程序提供具體回款日期列表,在日曆中須要體現出來

需求效果圖


2、效果預覽

首先會顯示出當前日期,以及給定的回款的數據顯示;日曆區域能夠左右滑動,上面月份會對應切換;上面月份能夠左右滑動,點擊對應月份完成切換github

效果預覽


實現

實現思路

其實對於每個需求,不管大小,咱們都須要進行一個需求的分析,而後分塊來處理,將大需求劃分紅小的需求來實現。緩存

對於上面的需求,咱們能夠將其分解成如下幾點:
1、日曆View的實現
2、月份選擇區域的實現
3、效果聯動ide


具體實現

1、日曆View的實現

對於日曆View的實現,咱們大體須要考慮三點,第一個是顯示日期的View的建立和佈局,這裏咱們須要考慮到底須要多少個控件才能裝下全部數據;第二個是每月的具體日期的顯示,主要考慮的點是這個月的第一天是星期幾,方便繪製;第三點就是對特殊點的處理,主要考慮的是被點擊日期的展現效果和回款日期的展現效果;最後一點就是將View放進ViewPager實現左右滑動切換日曆。佈局

第一點:咱們須要初始化顯示星期的控件和初始化顯示具體日期的控件ui

//初始化頭部佈局
    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]);
        }
    }

第二點:獲取每個月第一天對應星期幾,這樣方便從對應的位置開始繪製日期數this

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

//獲取指定年份指定月份的第一天的位置
    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);
    }

獲取指定年份指定月份的最後一天.net

//獲取指定年份指定月份的最後一天
    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);
    }
}

2、月份選擇區域的實現

需求中有一個很重要的點就是當前月份須要居中展現在全部的可視月份中。好比你當前選擇了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);
}

3、效果聯動

這一部分就是將ViewPager的左右選擇做用到上面的月份選擇上,實現實時匹配;再將上面的月份選擇的點擊選擇某一個月份的事件做用到ViewPager上,也就是viewPager.setCurrentItem(position);這一部分的實現都在MainActivity.java中,感興趣能夠去看看。


項目地址和結語

其實相似日曆的成熟的解決方案有不少不少,爲何仍是選擇了本身寫呢?一是由於相似的解決方案很是多,卻不必定是百分百契合你的需求,別人的解決方案在功能上或多或少會與你產品不一樣,你須要去修改別人的方案了;再者,可能別人的效果更加炫酷,卻不必定是你產品經理真正須要的。二是本身寫的代碼會帶來什麼樣的坑本身內心是清楚的,第三方的卻不必定清楚,在使用的時候內心不免不踏實。因此,在有時間和精力的狀況下,能夠嘗試參考別人的實現本身去寫屬於本身的功能。

還有一個點須要解釋一下,爲何沒有將這個需求封裝成一個library讓別人引用而是直接將邏輯寫在MainActivity中呢?由於我以爲這個需求並無什麼特別多的公用元素在裏面,他跟實際的業務須要結合很緊(回款),若是包成library,開發者在調用的時候須要對很是多的地方進行修改,不夠通用,要封裝只能對自定義的那個CalendarView進行封裝,可是他的功能又很簡單(這徹底是基於需求),全部就索性不作特殊處理了。

最後獻上項目地址: SuperCalendarDemo
若是鏈接失效就直接點擊這個連接吧!https://github.com/MZCretin/SuperCalendarDemo


關於個人

我就是比較喜歡用代碼解決生活中的問題,感受很開心,哈哈哈。也但願你們關注個人簡書,掘金,Github和CSDN。

簡書首頁,連接是 https://www.jianshu.com/u/123...

掘金首頁,連接是 https://juejin.im/user/5838d5...

Github首頁,連接是 https://github.com/MZCretin

CSDN首頁,連接是 http://blog.csdn.net/u010998327

我是Cretin,一個可愛的小男孩。

相關文章
相關標籤/搜索