滿足你各種姿勢的最美Android開源日曆

黃海彬發表於2018-09-25

日曆控制元件定製是移動開發平臺上比較常見的而且比較難的需求,一般會遇到以下問題:

  • 效能差,載入速度慢,原因是各種基於GridView或RecyclerView等ViewGroup實現的日曆,控制元件數太多,假設一個月檢視介面有42個item,每個item裡面分別就有2個子TextView:天數、農曆數和本身3個控制元件,這樣一個月檢視就有42 * 3+1(RecyclerView or GridView),清楚ViewPager特性的開發者就會明白,一般ViewPager持有3個item,那麼一個日曆控制元件持有的View控制元件數的數量將達到 1(ViewPager)+ 3(RecyclerView or GridView) + 3 * 42 * 3 = 382,如果用1個View來代替RecyclerView等,用Canvas來代替各種TextView,那View的數量瞬間將下降360+,記憶體和效能優勢將相當明顯了
  • 難定製 一般日曆框架釋出的同時也將UI風格確定下來了,假如人人都使用這個日曆框架,那麼將會千篇一律,難以突出自己的風格,要麼就得改原始碼,成本太大,不太實際
  • 功能性不足 例如無法自定義周起始、無法更改選擇模式、動態設定UI等等
  • 無法滿足產品經理提出的變態需求 今天產品經歷說我們要這樣的實現、明天跟你說這裡得改、後天說我們得限制一些日期...

但現在有了全新的 CalendarView 控制元件,它解鎖了各種姿勢,而且你可以不斷調教它,直到你滿足為止...

國際慣例:先放專案github地址

https://github.com/huanghaibin-dev/CalendarView

國內慣例:無圖言吊

滿足你各種姿勢的最美Android開源日曆     滿足你各種姿勢的最美Android開源日曆

滿足你各種姿勢的最美Android開源日曆     滿足你各種姿勢的最美Android開源日曆

CalendarView的騷特性

  • 基於Canvas繪製,極速效能
  • 熱插拔思想,任意定製周檢視、月檢視,即插即用!
  • 支援單選、多選、國內手機日曆預設自動選擇等選擇模式
  • 支援靜態、動態設定周起始,一行程式碼搞定
  • 支援靜態、動態設定日曆項高度、日曆填充模式
  • 支援設定任意日期範圍、任意攔截日期
  • 支援多點觸控、手指平滑切換過渡,拒絕介面抖動
  • 既然這麼多支援,那一定支援英語、繁體、簡體,任意定製實現

接下來請看CalendarView騷操作,看看它是可以怎樣調教的

  • 你這樣繼承自己的月檢視和周檢視,只需要依次實現繪製選中:onDrawSelected、繪製事務:onDrawScheme、繪製文字:onDrawText這三個回撥即可,引數和座標都已經在回撥函式上實現好,周檢視也是一樣的邏輯,只是不需要y引數
/**
 * 定製高仿魅族日曆介面,按你的想象力繪製出各種各樣的介面
 * Created by huanghaibin on 2017/11/15.
 */

public class MeiZuMonthView extends MonthView {

    /**
     * 繪製選中的日子
     * 
     * @param canvas    canvas
     * @param calendar  日曆日曆calendar
     * @param x         日曆Card x起點座標
     * @param y         日曆Card y起點座標
     * @param hasScheme hasScheme 非標記的日期
     * @return 返回true 則繪製onDrawScheme,因為這裡背景色不是是互斥的,所以返回true
     */
    @Override
    protected boolean onDrawSelected(Canvas canvas, Calendar calendar, int x, int y, boolean hasScheme) {
        canvas.drawRect(x + mPadding, y + mPadding, x + mItemWidth - mPadding, y + mItemHeight - mPadding, mSelectedPaint);
        return true;
    }

    /**
     * 繪製標記的事件日子
     *
     * @param canvas   canvas
     * @param calendar 日曆calendar
     * @param x        日曆Card x起點座標
     * @param y        日曆Card y起點座標
     */
    @Override
    protected void onDrawScheme(Canvas canvas, Calendar calendar, int x, int y) {
        canvas.drawCircle(x + mItemWidth - mPadding - mRadio / 2, y + mPadding + mRadio, mRadio, mSchemeBasicPaint);
        canvas.drawText(calendar.getScheme(),
                x + mItemWidth - mPadding - mRadio / 2 - getTextWidth(calendar.getScheme()) / 2,
                y + mPadding + mSchemeBaseLine, mTextPaint);
    }

    /**
     * 繪製文字
     *
     * @param canvas     canvas
     * @param calendar   日曆calendar
     * @param x          日曆Card x起點座標
     * @param y          日曆Card y起點座標
     * @param hasScheme  是否是標記的日期
     * @param isSelected 是否選中
     */
    @Override
    protected void onDrawText(Canvas canvas, Calendar calendar, int x, int y, boolean hasScheme, boolean isSelected) {
        int cx = x + mItemWidth / 2;
        int top = y - mItemHeight / 6;

        boolean isInRange = isInRange(calendar);

        if (isSelected) {
            canvas.drawText(String.valueOf(calendar.getDay()), cx, mTextBaseLine + top,
                    mSelectTextPaint);
            canvas.drawText(calendar.getLunar(), cx, mTextBaseLine + y + mItemHeight / 10, mSelectedLunarTextPaint);
        } else if (hasScheme) {
            canvas.drawText(String.valueOf(calendar.getDay()), cx, mTextBaseLine + top,
                    calendar.isCurrentMonth() && isInRange ? mSchemeTextPaint : mOtherMonthTextPaint);

            canvas.drawText(calendar.getLunar(), cx, mTextBaseLine + y + mItemHeight / 10, mCurMonthLunarTextPaint);
        } else {
            canvas.drawText(String.valueOf(calendar.getDay()), cx, mTextBaseLine + top,
                    calendar.isCurrentDay() ? mCurDayTextPaint :
                            calendar.isCurrentMonth() && isInRange ? mCurMonthTextPaint : mOtherMonthTextPaint);
            canvas.drawText(calendar.getLunar(), cx, mTextBaseLine + y + mItemHeight / 10,
                    calendar.isCurrentDay() && isInRange ? mCurDayLunarTextPaint :
                            calendar.isCurrentMonth() ? mCurMonthLunarTextPaint : mOtherMonthLunarTextPaint);
        }
    }
}
複製程式碼
  • 當你實現好之後,直接在xml介面上新增特性,可以即時預覽效果:

<attr name="month_view" format="string" /><!--自定義月檢視路徑-->
<attr name="week_view" format="string" /> <!--自定義周檢視路徑-->

app:month_view="com.haibin.calendarviewproject.MeiZuCalendarCardView"
app:week_view="com.haibin.calendarviewproject.MeiZuWeekView"

複製程式碼
  • 但這種靜態模式可能無法滿足你的需求,你可能需要動態變換定製的檢視介面,於是你可以使用熱插拔特性,即插即用,不爽就換:

mCalendarView.setWeekView(MeizuWeekView.class);

mCalendarView.setMonthView(MeizuMonthView.class);
 
複製程式碼
  • CalendarView也提供了高效便利的年檢視,可以快速切換年份、月份,十分便利
滿足你各種姿勢的最美Android開源日曆
  • 但年檢視也不一定就適合產品經理的胃口,產品經理希望像小米日曆一樣,彈出DatePickerView,通過它來跳轉日期,於是你可以使用以下的API來讓日曆與其它控制元件聯動

CalendarView.scrollToCalendar();

CalendarView.scrollToNext();

CalendarView.scrollToPre();

CalendarView.scrollToXXX();

複製程式碼
  • 你也許需要像魅族日曆一樣,可以靜態、動態更換周起始

app:week_start_with="mon、sun、sat"

CalendarView.setWeekStarWithSun();

CalendarView.setWeekStarWithMon();

CalendarView.setWeekStarWithSat();

複製程式碼
  • 假如你是做酒店、旅遊等應用場景的APP的,那麼需要可選範圍的日曆,你可以這樣繼承,和普通檢視實現完全一樣
public class CustomRangeMonthView extends RangeMonthView{
    
}

public class CustomRangeWeekView extends RangeWeekView{
    
}

複製程式碼
  • 然後你需要設定選擇模式為範圍模式:select_mode="range_mode"

  • 酒店式日曆場景當然是不能從昨天開始訂房的,也不能無限期訂房,所以你需要靜態或動態設定日曆範圍、精確到具體某一天!!!

<attr name="min_year" format="integer" />
<attr name="max_year" format="integer" />
<attr name="min_year_month" format="integer" />
<attr name="max_year_month" format="integer" />
<attr name="min_year_day" format="integer" />
<attr name="max_year_day" format="integer" />

CalendarView.setRange(int minYear, int minYearMonth, int minYearDay,
         int maxYear, int maxYearMonth, int maxYearDay)

複製程式碼
  • 當然還有更特殊的日子也是不能選擇的,例如:某月某號起這N天時間內因為超強颱風來襲,酒店需停止營業N天,這段期間不可訂房,這時日期攔截器就排上用場了
//設定日期攔截事件
mCalendarView.setOnCalendarInterceptListener(new CalendarView.OnCalendarInterceptListener() {
     @Override
     public boolean onCalendarIntercept(Calendar calendar) {
         //這裡寫攔截條件,返回true代表攔截
         return calendar.isWeekend();
     }

     @Override
     public void onCalendarInterceptClick(Calendar calendar, boolean isClick) {
         //todo 點選攔截的日期回撥
     }
});
複製程式碼
  • 新增日期攔截器和範圍設定後,你可以在周月檢視按需求獲得他們的結果

boolean isInRange = isInRange(calendar);//日期是否在範圍內,超出範圍的可以置灰

boolean isEnable = !onCalendarIntercept(calendar);//日期是否可用,沒有被攔截,被攔截的可以置灰

複製程式碼
  • 假如你是做清單類、任務類APP的,可能會有這樣的需求:標記某天事務的進度,這也很簡單,因為:日曆介面長什麼樣,你自己說了算!!!
滿足你各種姿勢的最美Android開源日曆
  • 也許你只需要像原生日曆那樣就夠了,但原生日曆那奇怪且十分不友好的style,受到theme的影響,各種頭疼,使用此控制元件,你只需要簡簡單單定製月檢視就夠了,CalendarView 能非常簡單就高仿各種日曆UI

  • CalendarView 提供了 setSchemeDate(Map<String, Calendar> mSchemeDates) 這個十分高效的API用來動態標記事務,即時你的資料量達到數千、數萬、數十萬,都不會對UI渲染造成影響

  • 日曆類 Calendar 提供了許多十分有用的API

boolean isWeekend();//判斷是不是週末,可以用不同的畫筆繪製週末的樣式

int getWeek();//獲取星期

String getSolarTerm();//獲取24節氣,可以用不同顏色標記不同節日

String getGregorianFestival();//獲取公曆節日,自由判斷,把節日換上喜歡的顏色

String getTraditionFestival();//獲取傳統節日

boolean isLeapYear();//是否是閏年

int getLeapMonth();//獲取閏月

boolean isSameMonth(Calendar calendar);//是否相同月

int compareTo(Calendar calendar);//畢竟日期大小 -1 0 1

long getTimeInMillis();//獲取時間戳

int differ(Calendar calendar);//日期運算,相差多少天
複製程式碼

其它各種場景姿勢就不多說了,你得自己去解鎖,一起看Demo以及各種APP的風騷實現

滿足你各種姿勢的最美Android開源日曆     滿足你各種姿勢的最美Android開源日曆

滿足你各種姿勢的最美Android開源日曆     滿足你各種姿勢的最美Android開源日曆

滿足你各種姿勢的最美Android開源日曆     滿足你各種姿勢的最美Android開源日曆

寫在最後,框架本身是為了解決各種各樣的場景而設計的,UI本身是靠自己繪製的,非常簡單,不懂的請優先看Demo,你可以自由發揮想象力定製最喜歡的日曆,只有你想不到,Demo基本給出了各種場景的實現思路。覺得可以的請給個star或者留下你寶貴的意見。

部落格慣例:結尾再放github地址,不然你就不願意翻到最上面點選了

https://github.com/huanghaibin-dev/CalendarView

相關文章