撸一个 Android 高性能日历控件,高仿魅族

3,635 阅读12分钟

Android原生的CalendarView根本无法满足我们日常开发的需要,在开发吾记APP的过程中,我觉得需要来一款高性能且美观简洁的日历控件,觉得魅族的日历风格十分适合,于是打算撸一款。

github地址:github.com/huanghaibin…

compile 'com.haibin:calendarview:1.0.2'

先上效果图:


动手之前我们需要分析一下魅族是怎么设计如此高性能的日历的,我们打开开发者选项中的显示布局边界:


好吧,一开始我以为日历界面是ViewPager+RecyclerView的,但是这么一看明显就不是了,如果是RecyclerView,那么我们假设每个月的卡片都有5*7=35个item,每个item根布局是RelativeLayout+3个TextView,我们大概估算一下日历初始化时要加载的控件:

3个ViewPager的item * 35个RecyclerView的Item * 4(每个item的控件数) + 8 (星期栏)= 420+ 

我的天,这可不能这么干,明显性能大打折扣,我们再来看看月份控件:

好吧,这里看上去就是ViewPager+RecyclerView来做的,每个RecyclerView的item都只是一个控件,里面绘制了文本 ,这里大概就分析清楚了。

我们采取折中的方式,日历界面和月份卡界面均采用ViewPager+RecyclerView的方式,不同的是所有的item我们都采用自定义ViewCanvas绘制的方式来做,这样性能虽然比不上魅族,但速度体验基本差不多,下面先看日历界面的item代码:只需要绘制3个文本即可

  1. public class CellView extends View {  
  2.   
  3.     private int mDay = 20;  
  4.     private String mLunar;  
  5.     private String mScheme;  
  6.     private Paint mDayPaint = new Paint();  
  7.     private Paint mLunarPaint = new Paint();  
  8.     private Paint mSchemePaint = new Paint();  
  9.     private Paint mCirclePaint = new Paint();  
  10.     private int mRadius;  
  11.     private int mCirclePadding;  
  12.     private int mCircleColor;  
  13.   
  14.     public CellView(Context context) {  
  15.         this(context, null);  
  16.     }  
  17.   
  18.     public CellView(Context context, @Nullable AttributeSet attrs) {  
  19.         super(context, attrs);  
  20.   
  21.         mDayPaint.setAntiAlias(true);  
  22.         mDayPaint.setColor(Color.BLACK);  
  23.         mDayPaint.setFakeBoldText(true);  
  24.         mDayPaint.setTextAlign(Paint.Align.CENTER);  
  25.   
  26.         mLunarPaint.setAntiAlias(true);  
  27.         mLunarPaint.setColor(Color.GRAY);  
  28.         mLunarPaint.setTextAlign(Paint.Align.CENTER);  
  29.   
  30.         mSchemePaint.setAntiAlias(true);  
  31.         mSchemePaint.setColor(Color.WHITE);  
  32.         mSchemePaint.setFakeBoldText(true);  
  33.         mSchemePaint.setTextAlign(Paint.Align.CENTER);  
  34.   
  35.         mCirclePaint.setAntiAlias(true);  
  36.         mCirclePaint.setStyle(Paint.Style.FILL);  
  37.   
  38.         TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CellView);  
  39.         mDayPaint.setTextSize(array.getDimensionPixelSize(R.styleable.CellView_cell_day_text_size, 18));  
  40.         mLunarPaint.setTextSize(array.getDimensionPixelSize(R.styleable.CellView_cell_lunar_text_size, 12));  
  41.         mRadius = (int) array.getDimension(R.styleable.CellView_cell_scheme_radius, 8);  
  42.         mSchemePaint.setTextSize(array.getDimensionPixelSize(R.styleable.CellView_cell_scheme_text_size, 6));  
  43.         mCirclePadding = array.getDimensionPixelSize(R.styleable.CellView_cell_circle_padding, 4);  
  44.         mCirclePaint.setColor(array.getColor(R.styleable.CellView_cell_circle_color, 0xff16BB7F));  
  45.         array.recycle();  
  46.     }  
  47.   
  48.     @Override  
  49.     protected void onDraw(Canvas canvas) {  
  50.         super.onDraw(canvas);  
  51.         int width = getWidth();  
  52.         int height = getHeight();  
  53.         int w = (width - getPaddingLeft() - getPaddingRight());  
  54.         int h = (height - getPaddingTop() - getPaddingBottom()) / 4;  
  55.         canvas.drawText(String.valueOf(mDay), w / 22 * h + getPaddingTop(), mDayPaint);  
  56.         canvas.drawText(mLunar, w / 24 * h + getPaddingTop(), mLunarPaint);  
  57.         if (!TextUtils.isEmpty(mScheme)) {  
  58.             canvas.drawCircle(w / 2 + mCirclePadding + mDayPaint.getTextSize(), getPaddingTop() + h, mRadius, mCirclePaint);  
  59.             canvas.drawText(mScheme, w / 2 + mCirclePadding + mDayPaint.getTextSize(), getPaddingTop() + mRadius /  2 + h, mSchemePaint);  
  60.         }  
  61.     }  
  62.   
  63.     /** 
  64.      * 初始化日历 
  65.      * @param day 天 
  66.      * @param lunar 农历 
  67.      * @param scheme 事件标记 
  68.      */  
  69.     void init(int day, String lunar, String scheme) {  
  70.         this.mDay = day;  
  71.         this.mLunar = lunar;  
  72.         this.mScheme = scheme;  
  73.     }  
  74.   
  75.     void setTextColor(int textColor) {  
  76.         mDayPaint.setColor(textColor);  
  77.         mLunarPaint.setColor(textColor);  
  78.     }  
  79.   
  80.     void setCircleColor(int circleColor) {  
  81.         mCirclePaint.setColor(circleColor);  
  82.         invalidate();  
  83.     }  
  84. }  
public class CellView extends View {

    private int mDay = 20;
    private String mLunar;
    private String mScheme;
    private Paint mDayPaint = new Paint();
    private Paint mLunarPaint = new Paint();
    private Paint mSchemePaint = new Paint();
    private Paint mCirclePaint = new Paint();
    private int mRadius;
    private int mCirclePadding;
    private int mCircleColor;

    public CellView(Context context) {
        this(context, null);
    }

    public CellView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);

        mDayPaint.setAntiAlias(true);
        mDayPaint.setColor(Color.BLACK);
        mDayPaint.setFakeBoldText(true);
        mDayPaint.setTextAlign(Paint.Align.CENTER);

        mLunarPaint.setAntiAlias(true);
        mLunarPaint.setColor(Color.GRAY);
        mLunarPaint.setTextAlign(Paint.Align.CENTER);

        mSchemePaint.setAntiAlias(true);
        mSchemePaint.setColor(Color.WHITE);
        mSchemePaint.setFakeBoldText(true);
        mSchemePaint.setTextAlign(Paint.Align.CENTER);

        mCirclePaint.setAntiAlias(true);
        mCirclePaint.setStyle(Paint.Style.FILL);

        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CellView);
        mDayPaint.setTextSize(array.getDimensionPixelSize(R.styleable.CellView_cell_day_text_size, 18));
        mLunarPaint.setTextSize(array.getDimensionPixelSize(R.styleable.CellView_cell_lunar_text_size, 12));
        mRadius = (int) array.getDimension(R.styleable.CellView_cell_scheme_radius, 8);
        mSchemePaint.setTextSize(array.getDimensionPixelSize(R.styleable.CellView_cell_scheme_text_size, 6));
        mCirclePadding = array.getDimensionPixelSize(R.styleable.CellView_cell_circle_padding, 4);
        mCirclePaint.setColor(array.getColor(R.styleable.CellView_cell_circle_color, 0xff16BB7F));
        array.recycle();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width = getWidth();
        int height = getHeight();
        int w = (width - getPaddingLeft() - getPaddingRight());
        int h = (height - getPaddingTop() - getPaddingBottom()) / 4;
        canvas.drawText(String.valueOf(mDay), w / 2, 2 * h + getPaddingTop(), mDayPaint);
        canvas.drawText(mLunar, w / 2, 4 * h + getPaddingTop(), mLunarPaint);
        if (!TextUtils.isEmpty(mScheme)) {
            canvas.drawCircle(w / 2 + mCirclePadding + mDayPaint.getTextSize(), getPaddingTop() + h, mRadius, mCirclePaint);
            canvas.drawText(mScheme, w / 2 + mCirclePadding + mDayPaint.getTextSize(), getPaddingTop() + mRadius / 2 + h, mSchemePaint);
        }
    }

    /**
     * 初始化日历
     * @param day 天
     * @param lunar 农历
     * @param scheme 事件标记
     */
    void init(int day, String lunar, String scheme) {
        this.mDay = day;
        this.mLunar = lunar;
        this.mScheme = scheme;
    }

    void setTextColor(int textColor) {
        mDayPaint.setColor(textColor);
        mLunarPaint.setColor(textColor);
    }

    void setCircleColor(int circleColor) {
        mCirclePaint.setColor(circleColor);
        invalidate();
    }
}

月份卡自定义View

  1. public class MonthView extends View {  
  2.     private int mDiff;//第一天偏离周日多少天  
  3.     private int mCount;//总数  
  4.     private int mLastCount;//最后一行的天数  
  5.     private int mLine;//多少行  
  6.     private Paint mPaint = new Paint();  
  7.     private Paint mSchemePaint = new Paint();  
  8.     private List<Calendar> mSchemes;  
  9.     private Calendar mCalendar;  
  10.   
  11.     public MonthView(Context context) {  
  12.         this(context, null);  
  13.     }  
  14.   
  15.     public MonthView(Context context, @Nullable AttributeSet attrs) {  
  16.         super(context, attrs);  
  17.         mPaint.setAntiAlias(true);  
  18.         mPaint.setTextAlign(Paint.Align.CENTER);  
  19.         mSchemePaint.setAntiAlias(true);  
  20.         mSchemePaint.setTextAlign(Paint.Align.CENTER);  
  21.         TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.MonthView);  
  22.         mPaint.setTextSize(array.getDimensionPixelSize(R.styleable.MonthView_month_view_text_size, 12));  
  23.         mSchemePaint.setTextSize(array.getDimensionPixelSize(R.styleable.MonthView_month_view_text_size, 12));  
  24.         mPaint.setColor(array.getColor(R.styleable.MonthView_month_view_text_color, Color.BLACK));  
  25.         mSchemePaint.setColor(array.getColor(R.styleable.MonthView_month_view_remark_color, Color.RED));  
  26.         array.recycle();  
  27.         measureLine();  
  28.     }  
  29.   
  30.     @Override  
  31.     protected void onDraw(Canvas canvas) {  
  32.         super.onDraw(canvas);  
  33.         int width = getWidth();  
  34.         int height = getHeight();  
  35.         int pLeft = getPaddingLeft();  
  36.         int w = (width - getPaddingLeft() - getPaddingRight()) / 7;  
  37.         int h = (height - getPaddingTop() - getPaddingBottom()) / 6;  
  38.         int d = 0;  
  39.         for (int i = 0; i < mLine; i++) {  
  40.             if (i == 0) {//第一行  
  41.                 for (int j =  0; j < (7 - mDiff); j++) {  
  42.                     ++d;  
  43.                     canvas.drawText(String.valueOf(j + 1), mDiff * w + j * w + pLeft + w /  2, h, isScheme(d) ? mSchemePaint : mPaint);  
  44.                 }  
  45.             } else if (i == mLine -  1 && mLastCount != 0) {  
  46.                 int first = mCount - mLastCount +  1;  
  47.                 for (int j =  0; j < mLastCount; j++) {  
  48.                     ++d;  
  49.                     canvas.drawText(String.valueOf(first), j * w + pLeft + w / 2, (i +  1) * h, isScheme(d) ? mSchemePaint : mPaint);  
  50.                     ++first;  
  51.                 }  
  52.             } else {  
  53.                 int first = i * 7 - mDiff +  1;  
  54.                 for (int j =  0; j < 7; j++) {  
  55.                     ++d;  
  56.                     canvas.drawText(String.valueOf(first), j * w + pLeft + w / 2, (i +  1) * h, isScheme(d) ? mSchemePaint : mPaint);  
  57.                     ++first;  
  58.                 }  
  59.             }  
  60.         }  
  61.     }  
  62.   
  63.     /** 
  64.      * 计算行数 
  65.      */  
  66.     private void measureLine() {  
  67.         int offset = mCount - (7 - mDiff);  
  68.         mLine = 1 + (offset % 7 == 0 ?  0 : 1) + offset / 7;  
  69.         mLastCount = offset % 7;  
  70.     }  
  71.   
  72.     /** 
  73.      * 初始化月份卡 
  74.      * @param mDiff 偏离天数 
  75.      * @param mCount 当月总天数 
  76.      * @param mYear 哪一年 
  77.      * @param mMonth 哪一月 
  78.      */  
  79.      void init(int mDiff, int mCount,  int mYear, int mMonth) {  
  80.         this.mDiff = mDiff;  
  81.         this.mCount = mCount;  
  82.         mCalendar = new Calendar();  
  83.         mCalendar.setYear(mYear);  
  84.         mCalendar.setMonth(mMonth);  
  85.         measureLine();  
  86.         invalidate();  
  87.     }  
  88.   
  89.     void setSchemes(List<Calendar> mSchemes) {  
  90.         this.mSchemes = mSchemes;  
  91.     }  
  92.   
  93.     void setSchemeColor(int schemeColor) {  
  94.         if (schemeColor != 0)  
  95.             mSchemePaint.setColor(schemeColor);  
  96.         if(schemeColor == 0xff30393E)  
  97.             mSchemePaint.setColor(Color.RED);  
  98.     }  
  99.   
  100.     private boolean isScheme(int day) {  
  101.         if (mSchemes == null || mSchemes.size() ==  0)  
  102.             return false;  
  103.         mCalendar.setDay(day);  
  104.         return mSchemes.contains(mCalendar);  
  105.     }  
  106. }  
public class MonthView extends View {
    private int mDiff;//第一天偏离周日多少天
    private int mCount;//总数
    private int mLastCount;//最后一行的天数
    private int mLine;//多少行
    private Paint mPaint = new Paint();
    private Paint mSchemePaint = new Paint();
    private List<Calendar> mSchemes;
    private Calendar mCalendar;

    public MonthView(Context context) {
        this(context, null);
    }

    public MonthView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mPaint.setAntiAlias(true);
        mPaint.setTextAlign(Paint.Align.CENTER);
        mSchemePaint.setAntiAlias(true);
        mSchemePaint.setTextAlign(Paint.Align.CENTER);
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.MonthView);
        mPaint.setTextSize(array.getDimensionPixelSize(R.styleable.MonthView_month_view_text_size, 12));
        mSchemePaint.setTextSize(array.getDimensionPixelSize(R.styleable.MonthView_month_view_text_size, 12));
        mPaint.setColor(array.getColor(R.styleable.MonthView_month_view_text_color, Color.BLACK));
        mSchemePaint.setColor(array.getColor(R.styleable.MonthView_month_view_remark_color, Color.RED));
        array.recycle();
        measureLine();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width = getWidth();
        int height = getHeight();
        int pLeft = getPaddingLeft();
        int w = (width - getPaddingLeft() - getPaddingRight()) / 7;
        int h = (height - getPaddingTop() - getPaddingBottom()) / 6;
        int d = 0;
        for (int i = 0; i < mLine; i++) {
            if (i == 0) {//第一行
                for (int j = 0; j < (7 - mDiff); j++) {
                    ++d;
                    canvas.drawText(String.valueOf(j + 1), mDiff * w + j * w + pLeft + w / 2, h, isScheme(d) ? mSchemePaint : mPaint);
                }
            } else if (i == mLine - 1 && mLastCount != 0) {
                int first = mCount - mLastCount + 1;
                for (int j = 0; j < mLastCount; j++) {
                    ++d;
                    canvas.drawText(String.valueOf(first), j * w + pLeft + w / 2, (i + 1) * h, isScheme(d) ? mSchemePaint : mPaint);
                    ++first;
                }
            } else {
                int first = i * 7 - mDiff + 1;
                for (int j = 0; j < 7; j++) {
                    ++d;
                    canvas.drawText(String.valueOf(first), j * w + pLeft + w / 2, (i + 1) * h, isScheme(d) ? mSchemePaint : mPaint);
                    ++first;
                }
            }
        }
    }

    /**
     * 计算行数
     */
    private void measureLine() {
        int offset = mCount - (7 - mDiff);
        mLine = 1 + (offset % 7 == 0 ? 0 : 1) + offset / 7;
        mLastCount = offset % 7;
    }

    /**
     * 初始化月份卡
     * @param mDiff 偏离天数
     * @param mCount 当月总天数
     * @param mYear 哪一年
     * @param mMonth 哪一月
     */
     void init(int mDiff, int mCount, int mYear, int mMonth) {
        this.mDiff = mDiff;
        this.mCount = mCount;
        mCalendar = new Calendar();
        mCalendar.setYear(mYear);
        mCalendar.setMonth(mMonth);
        measureLine();
        invalidate();
    }

    void setSchemes(List<Calendar> mSchemes) {
        this.mSchemes = mSchemes;
    }

    void setSchemeColor(int schemeColor) {
        if (schemeColor != 0)
            mSchemePaint.setColor(schemeColor);
        if(schemeColor == 0xff30393E)
            mSchemePaint.setColor(Color.RED);
    }

    private boolean isScheme(int day) {
        if (mSchemes == null || mSchemes.size() == 0)
            return false;
        mCalendar.setDay(day);
        return mSchemes.contains(mCalendar);
    }
}
其它代码没有什么难度,日历算法是github上找的,更多详情请看仓库地址:github.com/huanghaibin…