走进爱哥的仓库:DatePicker

DatePicker前传

不知什么时候,AigeStudio侵入了我们这群android开发者的内心深处,有着一群不知姓什名谁的人xx(不想用小明指代,比较喜欢xx),不断地发出歇斯底里的叫声:

然而,在某天某时某刻,XX遇到需求如下:

商户可以通过app手动选择指定时间段/一些天数进行统计分析。

这一刻来临了,XX人蒙了,找不到头绪了,开始进各个群展开撒网战术,可怜兮兮的等待着群友的回复,总有那么些乐于助人又是Aige的死忠粉,会回复一句:爱哥开源的DatePicker可以实现,然而xx还想问”DatePicker在哪里啊,谁有下载链接啊“…找到了解决办法,XX终于可以安心睡觉了,此时此刻,我只想对xx说:你对Aige爱得不够深沉


DatePicker实战

效果图:

依赖导入

compile 'cn.aigestudio.datepicker:DatePicker:2.2.0'

DatePicker直接在xml布局使用,代码进行相关的初始化配置监听设置等。(setData方法必须调用进行初始化)

......
DatePicker datePicker = (DatePicker) findViewById(R.id.datePicker);
datePicker.setDate(mYear, mMonth);
datePicker.setOnDateSelectedListener(new DatePicker.OnDateSelectedListener() {
    @Override
    public void onDateSelected(List<String> date) {

    }
});
......

DatePicker支持多种模式:单选、多选、正常模式,具体定义如下

public enum DPMode {
    SINGLE, MULTIPLE, NONE
}

DatePicker不同的模式通过不同的监听回调事件,获取选中的时间值

setOnDateSelectedListener // 多选监听

setOnDatePickedListener // 单选监听

选择后的日期将会以列表(多选模式下)或字符串(单选模式下)的形式返回,日期字符串的格式为:

格式:yyyy-M-d 
示例:2015-3-28

DatePicker有一套自己的默认UI,默认的Color配置,如果你使用Module导入工程,可以直接修改默认配置,关联类如下

DatePicker默认天朝的节日背景色

当然你也可以自己修改定制,同时还可以为特殊节日添加浮标,首先添加指定需要修改号数集,添加到缓存到对应map集合

        List<String> tmpTR = new ArrayList<>();
        tmpTR.add("2015-10-10");
        tmpTR.add("2015-10-11");
        tmpTR.add("2015-10-12");
        tmpTR.add("2015-10-13");
        tmpTR.add("2015-10-14");
        tmpTR.add("2015-10-15");
        tmpTR.add("2015-10-16");
        DPCManager.getInstance().setDecorTR(tmpTR);

    private static final HashMap<String, Set<String>> DECOR_CACHE_BG = new HashMap<>();
    private static final HashMap<String, Set<String>> DECOR_CACHE_TL = new HashMap<>();
    private static final HashMap<String, Set<String>> DECOR_CACHE_T = new HashMap<>();
    private static final HashMap<String, Set<String>> DECOR_CACHE_TR = new HashMap<>();
    private static final HashMap<String, Set<String>> DECOR_CACHE_L = new HashMap<>();
    private static final HashMap<String, Set<String>> DECOR_CACHE_R = new HashMap<>();

完成上述步骤后,再通过setDPDecor方法绘制对应号数的浮标

  picker.setDPDecor(new DPDecor() {
            @Override
            public void drawDecorTL(Canvas canvas, Rect rect, Paint paint, String data) {
                super.drawDecorTL(canvas, rect, paint, data);
                switch (data) {
                    case "2015-10-5":
                    case "2015-10-7":
                    case "2015-10-9":
                    case "2015-10-11":
                        paint.setColor(Color.GREEN);
                        canvas.drawRect(rect, paint);
                        break;
                    default:
                        paint.setColor(Color.RED);
                        canvas.drawCircle(rect.centerX(), rect.centerY(), rect.width() / 2, paint);
                        break;
                }
            }

            @Override
            public void drawDecorTR(Canvas canvas, Rect rect, Paint paint, String data) {
                super.drawDecorTR(canvas, rect, paint, data);
                switch (data) {
                    case "2015-10-10":
                    case "2015-10-11":
                    case "2015-10-12":
                        paint.setColor(Color.BLUE);
                        canvas.drawCircle(rect.centerX(), rect.centerY(), rect.width() / 2, paint);
                        break;
                    default:
                        paint.setColor(Color.YELLOW);
                        canvas.drawRect(rect, paint);
                        break;
                }
            }
        });

绘制的浮标可以是不同形状不同颜色,具体怎么关联到月视图稍后再看,绘制的浮标可以是不同方向,具体使用参考DPDecor类的定义。(具体使用参考上面代码块实例)

绘制浮标效果如下图:

更为详尽的使用请参考Aige官方示例:https://github.com/AigeStudio/DatePicker

DataPicker源码浅析

calendars包内部提供方法都有中文注释,相信都能看懂,关于一些节日号数相关基础数据的获取判断,内部的一些算法,表示无能为力(我算法白痴),DPDecor定义方法上面有提到,具体使用不详解。

抽象类DPLManager是一个语言管理方面的,该库支持中英文的日历,根据CH标示采取不同的实现

theme模块关于UI配置方面的,上面有提到,具体代码设置color,请参考源码,头有中文注释一目了然。

日历数据实体,封装日历绘制时需要的数据(封装按照二维数组,每个月可能存在的行列进行封装)


public class DPInfo {
    public String strG, strF;
    public boolean isHoliday;
    public boolean isToday, isWeekend;
    public boolean isSolarTerms, isFestival, isDeferred;
    public boolean isDecorBG;
    public boolean isDecorTL, isDecorT, isDecorTR, isDecorL, isDecorR;
}

utils包里面就一个dp px的转换和一维数组转二维数组

整个库的UI核心是下面两个类

  • DatePicker

  • MonthView

DatePicker自定义的Layout容器,构造函数先添加标题栏显示控件和星期-到星期日的显示视图,最后添加月视图

 public DatePicker(Context context, AttributeSet attrs) {
        super(context, attrs);
        //..............略过些许方法................
        mTManager = DPTManager.getInstance();
        mLManager = DPLManager.getInstance();

        // 设置排列方向为竖向
        setOrientation(VERTICAL);
        // 标题栏根布局
        RelativeLayout rlTitle = new RelativeLayout(context);

        // 周视图根布局
        LinearLayout llWeek = new LinearLayout(context);

        // 标题栏子元素布局参数
        RelativeLayout.LayoutParams lpYear =
                new RelativeLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT);

        // --------------------------------------------------------------------------------标题栏
        // 年份显示
        tvYear = new TextView(context);

        // 月份显示
        tvMonth = new TextView(context);
        tvMonth.setText("六月");

        // 确定显示
        tvEnsure = new TextView(context);

        tvEnsure.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (null != onDateSelectedListener) {
                    onDateSelectedListener.onDateSelected(monthView.getDateSelected());
                }
            }
        });

        rlTitle.addView(tvYear, lpYear);
        rlTitle.addView(tvMonth, lpMonth);
        rlTitle.addView(tvEnsure, lpEnsure);

        addView(rlTitle, llParams);

        // --------------------------------------------------------------------------------周视图
        for (int i = 0; i < mLManager.titleWeek().length; i++) {
            TextView tvWeek = new TextView(context);
            tvWeek.setText(mLManager.titleWeek()[i]);
            tvWeek.setGravity(Gravity.CENTER);
            tvWeek.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14);
            tvWeek.setTextColor(mTManager.colorTitle());
            llWeek.addView(tvWeek, lpWeek);
        }
        addView(llWeek, llParams);

        // ------------------------------------------------------------------------------------月视图
        monthView = new MonthView(context);
        monthView.setOnDateChangeListener(new MonthView.OnDateChangeListener() {
            @Override
            public void onMonthChange(int month) {
                tvMonth.setText(mLManager.titleMonth()[month - 1]);
            }

            @Override
            public void onYearChange(int year) {
                String tmp = String.valueOf(year);
                if (tmp.startsWith("-")) {
                    tmp = tmp.replace("-", mLManager.titleBC());
                }
                tvYear.setText(tmp);
            }
        });
        addView(monthView, llParams);
    }

设置日历选择模式如果支持多选则显示确定按钮

  /**
     * 设置日期选择模式
     *
     * @param mode ...
     */
    public void setMode(DPMode mode) {
        if (mode != DPMode.MULTIPLE) {
            tvEnsure.setVisibility(GONE);
        }
        monthView.setDPMode(mode);
    }

提供多个set方法,本质是调用了月视图View进行set,特别是对于DatePicker的监听绑定做了判断。Aige的使用文档是这样说的

MonthView默认多选模式,在没有设置模式的情况下直接设置单选监听会抛出异常,反之同理

  /**
     * 设置单选监听器
     *
     * @param onDatePickedListener ...
     */
    public void setOnDatePickedListener(OnDatePickedListener onDatePickedListener) {
        if (monthView.getDPMode() != DPMode.SINGLE) {
            throw new RuntimeException(
                    "Current DPMode does not SINGLE! Please call setMode set DPMode to SINGLE!");
        }
        monthView.setOnDatePickedListener(onDatePickedListener);
    }

    /**
     * 设置多选监听器
     *
     * @param onDateSelectedListener ...
     */
    public void setOnDateSelectedListener(OnDateSelectedListener onDateSelectedListener) {
        if (monthView.getDPMode() != DPMode.MULTIPLE) {
            throw new RuntimeException(
                    "Current DPMode does not MULTIPLE! Please call setMode set DPMode to MULTIPLE!");
        }
        this.onDateSelectedListener = onDateSelectedListener;
    }

MonthView月视图自定义控件,内部构造函数初始化一个Scroller实例用于辅助滑动,一个缩放动画监听(单选或者多选模式下,选中某一项会有个缩放动画的圆圈背景)

   public MonthView(Context context) {
        super(context);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            scaleAnimationListener = new ScaleAnimationListener();
        }
        mScroller = new Scroller(context);
        mPaint.setTextAlign(Paint.Align.CENTER);
    }

computeScroll:主要功能是计算拖动的位移量更新UI,重写computeScroll()的原因,调用startScroll()是不会有滚动效果的,只有在computeScroll()获取滚动情况,参考Scroller计算,做出滚动的响应,computeScroll在父控件执行drawChild时,会调用这个方法.要知道计算有没有终止,需要通过mScroller.computeScrollOffset()

 @Override
 public void computeScroll() {
     if (mScroller.computeScrollOffset()) {
         scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
         invalidate();
       } else {
            requestLayout()
         }
 }

onTouch方法内判断滑动事件,如果还在滑动,down时结束滑动,并记录位置,MOVE时判断滑动距离,设置滑动方向mSlideMode (内部定义枚举类型),根据不同的Mode和计算出的距离,调用Scroller.startScroll方法配合computescroll实现滑动视图。而Touch的up事件如果造成月视图切换,可能月份年份的变化,这时候会调用到computeDate方法,该方法判断后根据条件执行回调,而回调函数在DatePicker内部实现了局部更新UI


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mScroller.forceFinished(true);
                mSlideMode = null;
                isNewEvent = true;
                lastPointX = (int) event.getX();
                lastPointY = (int) event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                if (isNewEvent) {
                    if (Math.abs(lastPointX - event.getX()) > 100) {
                        mSlideMode = SlideMode.HOR;
                        isNewEvent = false;
                    } else if (Math.abs(lastPointY - event.getY()) > 50) {
                        mSlideMode = SlideMode.VER;
                        isNewEvent = false;
                    }
                }
                if (mSlideMode == SlideMode.HOR) {
                    int totalMoveX = (int) (lastPointX - event.getX()) + lastMoveX;
                    smoothScrollTo(totalMoveX, indexYear * height);
                } else if (mSlideMode == SlideMode.VER) {
                    int totalMoveY = (int) (lastPointY - event.getY()) + lastMoveY;
                    smoothScrollTo(width * indexMonth, totalMoveY);
                }
                break;
            case MotionEvent.ACTION_UP:
                if (mSlideMode == SlideMode.VER) {
                    if (Math.abs(lastPointY - event.getY()) > 25) {
                        if (lastPointY < event.getY()) {
                            if (Math.abs(lastPointY - event.getY()) >= criticalHeight) {
                                indexYear--;
                                centerYear = centerYear - 1;
                            }
                        } else if (lastPointY > event.getY()) {
                            if (Math.abs(lastPointY - event.getY()) >= criticalHeight) {
                                indexYear++;
                                centerYear = centerYear + 1;
                            }
                        }
                        buildRegion();
                        computeDate();
                        smoothScrollTo(width * indexMonth, height * indexYear);
                        lastMoveY = height * indexYear;
                    } else {
                        defineRegion((int) event.getX(), (int) event.getY());
                    }
                } else if (mSlideMode == SlideMode.HOR) {
                    if (Math.abs(lastPointX - event.getX()) > 25) {
                        if (lastPointX > event.getX() &&
                                Math.abs(lastPointX - event.getX()) >= criticalWidth) {
                            indexMonth++;
                            centerMonth = (centerMonth + 1) % 13;
                            if (centerMonth == 0) {
                                centerMonth = 1;
                                centerYear++;
                            }
                        } else if (lastPointX < event.getX() &&
                                Math.abs(lastPointX - event.getX()) >= criticalWidth) {
                            indexMonth--;
                            centerMonth = (centerMonth - 1) % 12;
                            if (centerMonth == 0) {
                                centerMonth = 12;
                                centerYear--;
                            }
                        }
                        buildRegion();
                        computeDate();
                        smoothScrollTo(width * indexMonth, indexYear * height);
                        lastMoveX = width * indexMonth;
                    } else {
                        defineRegion((int) event.getX(), (int) event.getY());
                    }
                } else {
                    defineRegion((int) event.getX(), (int) event.getY());
                }
                break;
        }
        return true;
    }

至于上面的defineRegion方法我也只了解一点(计算真心看不懂),基于v11版本区分单选模式和多选模式,执行缩放动画,加减速差值器效果(圆圈背景缩放),动画结束执行对应的回调函数,至于多选的值缓存方式使用的是List,个人认为还有更好的方案,具体参考AbsListView(看了别打我~~(>_<)~~)

  private List<String> dateSelected = new ArrayList<>();

至于圆形背景怎么来,以前博客提过很多次,不再逼逼叨叨了

小结

Aige就是Aige,看完这个库收获良多!今后要加强算法方面的学习了,闰年判断都差不多忘了,时光飞逝,遥想当年学java,一个闰年判断的java的demo分分钟的事,现在还得百度一下!!!


DatePicker番外

一天,群里来了新人,漂亮的MM,在IT行业mm就是国宝啊,更何况是PL妹纸,xx纷纷跳出来要求爆照
妹纸羞答答的说:“你们先告诉我DatePicker怎么选择多个日期,我就发”
xx就开始逼逼叨叨先聊着,都没说到正点,此时一个非常活跃的oo告诉mm:”爱哥知道,你问他吧“
……….
………
………逼逼叨叨结束

妹子来了,Aige献身了:“setMode(DPMode.MULTIPLE);”
而后不再言语,事了拂衣去,深藏身与名,PL妹纸都还不知道Aige就是爱哥!!


关于点击切换月份的解决方案:

MonthView添加这两个方法,调用通过DatePicker get到MonthView就可以调用了。(之前考虑用setData经过仔细测试发现有问题的,最后参照onTouchEvent修改完成这两个方法)

  public void showNectMonth(int distance) {
        mSlideMode = SlideMode.HOR;
        int totalMoveX = distance + lastMoveX;
        smoothScrollTo(totalMoveX, indexYear * height);
        indexMonth++;
        centerMonth = (centerMonth + 1) % 13;
        if (centerMonth == 0) {
            centerMonth = 1;
            centerYear++;
        }

        buildRegion();
        computeDate();
        smoothScrollTo(width * indexMonth, indexYear * height);
        lastMoveX = width * indexMonth;
    }

    public void showPreviousMonth(int distance) {
        mSlideMode = SlideMode.HOR;
        int totalMoveX = distance + lastMoveX;
        smoothScrollTo(totalMoveX, indexYear * height);

        indexMonth--;
        centerMonth = (centerMonth - 1) % 12;
        if (centerMonth == 0) {
            centerMonth = 12;
            centerYear--;
        }

        buildRegion();
        computeDate();
        smoothScrollTo(width * indexMonth, indexYear * height);
        lastMoveX = width * indexMonth;
    }
评论 22
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值