Android自定义滚动式时间选择器(在他人基础上修改)

尽管Android给我们提供了时间选择控件DatePicker和TimePicker(它们的使用方法可以参考我的这篇文章Android之日期时间选择控件DatePicker和TimePicker),但无奈我的项目主色调是土豪金和高级黑,原生的控件用在里面显得格格不入,特别是为了兼容低版本的系统之后显示的是2.x年代的风格,不但是简陋,简直是丑陋了。要解决这种问题,就只有走自定义控件这条道。但我目前还是不太熟悉自定义控件的写法,所以只好发挥拿来主义了。查看了一通之后,发现liuwan1992这位博主写的非常漂亮,我在他的基础上做了一些改动,使得整个控件更符合我的项目。

这是他的文章链接:Android 好看的自定义滚动式日期选择控件 ,关于控件的使用大家直接阅读他的文章即可。在此,感谢他的付出,本人只是巨人肩膀上的小白而已。

1、创建工程

你可以下载博主的源码,用Android Studio打开之后就直接动手修改,也可以像我这样新建一个工程,然后将需要用到的代码和文件从源码复制过来即可。

2、修改对话框外观

由于我需要用到我自己的颜色,所以在colors.xml中做了一些改动:

<resources>
    <color name="colorPrimary">#3F51B5</color>
    <color name="colorPrimaryDark">#303F9F</color>
    <color name="colorAccent">#FF4081</color>
    
    <color name="color_unit">#F0F0F0</color> //年、月、日等单位的字体颜色
    <color name="color_item_bg">#FFFFFF</color>
    <color name="color_text_unselected">#B0B0B0</color> //未被选中的数字颜色
    <color name="color_desc">#666666</color>
    <color name="color_result">#57C5E8</color>
    <color name="color_bg">#b0000000</color> //作为背景的高级黑
    <color name="color_gold">#ffda53</color> //作为字体的土豪金
</resources>

其中color_bg和color_gold分别是背景高级黑和字体土豪金。

资源文件准备好之后,就可以到对话框的布局custom_date_picker.xml中修改背景和字体颜色了。滚轮中选中和未选中的字体颜色则需要到DatePickerView中修改:

    private void init() {
        timer = new Timer();
        mDataList = new ArrayList<>();
        //第一个paint
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Style.FILL);
        mPaint.setTextAlign(Align.CENTER);
        //被选中的数字颜色
        mPaint.setColor(ContextCompat.getColor(context, R.color.color_gold));
        //第二个paint
        nPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        nPaint.setStyle(Style.FILL);
        nPaint.setTextAlign(Align.CENTER);
        //未选中的数字颜色
        nPaint.setColor(ContextCompat.getColor(context, R.color.color_text_unselected));
    }

修改完的效果如下,项目需求如此,如果觉得辣眼睛的请多多谅解。

请选择日期

请选择时间

3、给控件增加标题

虽然选择的都是时间,但是如果我们在控件的标题中告诉用户当前要选择的是什么时间的话就更好一点。比如用户要确定一个时间段的话,我们就可以在弹出的控件上显示要选择的是“起始时间”还是“结束时间”。因此,我们可以在CustomDatePicker的构造函数中再增加一个参数:

   public CustomDatePicker(Context context,String title, ResultHandler resultHandler, String startDate, String endDate) {
    }

这样在创建控件时我们就可以把控件的标题传进去了。

4、点击控件以外的区域让控件消失

这个修改比较简单,我们的时间选择控件其实就是一个Dialog,只要将setCancelable方法设置为true就可以了。

5、修改选择顺序

这个控件的时间选择顺序默认是“年-月-日-时-分”,也就是选好了年才能选择月,选好了月才能选择日,从左到右。但是如果用户先确定了日,再去选择月的话,之后月份改变,日数就会重新从1开始。用户体验无疑是十分糟糕的,所以我们要把用户的每一步选择都保存下来,使控件可以从右到左选择时间。毫无疑问,这就需要到CustomDatePicker中改动了,因此,这一步的修改是最复杂也是最关键的。

5.1 保存用户选好的时间数据

打开CustomDatePicker文件,创建以下几个String变量:

    private String currentMon, currentDay, currentHour, currentMin; //当前选中的月、日、时、分

年份位于最左端,“权限”是最高的,不会受其它数据的影响,可以不必保存。仔细想想,我们需要在两种情况下保存选中的日期数据:

  • 用户滑动滚轮之后,保存选择的数据;

  • 用户没有滑动滚轮,但是点击了对话框上的“确认”按钮

源码中,对于每个滚轮的滚动都写了监听方法,我们可以在每个监听方法中赋值:

   private void addListener() {
        year_pv.setOnSelectListener(new DatePickerView.onSelectListener() {
            @Override
            public void onSelect(String text) {
                selectedCalender.set(Calendar.YEAR, Integer.parseInt(text));
                monthChange();
            }
        });

        month_pv.setOnSelectListener(new DatePickerView.onSelectListener() {
            @Override
            public void onSelect(String text) {
                selectedCalender.set(Calendar.DAY_OF_MONTH, 1);
                selectedCalender.set(Calendar.MONTH, Integer.parseInt(text) - 1);
                currentMon = text; //保存选择的月份
                dayChange();
            }
        });

        day_pv.setOnSelectListener(new DatePickerView.onSelectListener() {
            @Override
            public void onSelect(String text) {
                selectedCalender.set(Calendar.DAY_OF_MONTH, Integer.parseInt(text));
                currentDay = text;//保存选择的日期
                hourChange();
            }
        });

        hour_pv.setOnSelectListener(new DatePickerView.onSelectListener() {
            @Override
            public void onSelect(String text) {
                selectedCalender.set(Calendar.HOUR_OF_DAY, Integer.parseInt(text));
                currentHour = text; //保存选择的小时
                minuteChange();
            }
        });

        minute_pv.setOnSelectListener(new DatePickerView.onSelectListener() {
            @Override
            public void onSelect(String text) {
                selectedCalender.set(Calendar.MINUTE, Integer.parseInt(text));
                currentMin = text; //保存选择的分钟
            }
        });
    }

如果用户没有滑动滚轮,那么滚动监听事件就不会触发,这时就要到设置默认选中时间的方法setSelectedTime方法中去赋值了,比如分钟的赋值:

currentMin = timeStr[1]; //保存选择的分钟

其它的时间数据同理,代码太长,这里就不贴了,大家直接看Demo吧。

5.2 时间数据之间的联动逻辑

保存好数据之后还没有完,要知道,各个时间数据之间并不是可以任意选择的,它们之间是相互关联,相互制约的,特别是当你规定了最大时间不超过当前时间时。比如假设现在是2017年05月24日15:30,当我滑到2017年5月23日16:00,此时再将日改回24日时,时间显然是不能超过15:30的。这时就要做一些判断了。

以分钟的数值为例,如果当前的分钟数值小于60,而且之前选择的分钟数值比当前的分钟数值大,那么就要将分钟数值修改为当前的分钟时间,否则继续沿用之前的数值。
阅读源码可以看到,修改分钟数值的方法为minuteChange,将复位处理的代码注释之后,然后加上我们的逻辑判断。

//            selectedCalender.set(Calendar.MINUTE, Integer.parseInt(minute.get(0)));
//            minute_pv.setSelected(0);
            if (minute.size() < 60 && minute.size() < Integer.valueOf(currentMin)) {
                minute_pv.setSelected(minute.size() - 1);
                selectedCalender.set(Calendar.MINUTE, minute.size());
                //改变当前选择的分钟
                currentMin = formatTimeUnit(minute.size());
            } else {
                minute_pv.setSelected(currentMin);
                selectedCalender.set(Calendar.MINUTE, Integer.parseInt(currentMin));
            }

小时的判断跟分钟的差不多,这里就贴代码了,但是月份的就有点特殊了,因为无论哪一天都是24小时,无论哪一小时都有60分钟,而每个月的天数却不尽相同,比如二月份就只有28或者29天。我们需要再创建一个int值变量lastMonthDays来记录上一个被选中的月份的天数。当前选中的月份天数比之前选中的月份天数少,而且之前选中的日的数值比当前选中的月份天数还有大时,那么日的数值就必须改为这个月的最后一天了。举个例子,比如将2017年3月31日中月份改为2月份,由于二月份只有28天,那么日的数值就不能停留在31了,而是跳转到28。

dayChange方法中的代码修改如下:

//        selectedCalender.set(Calendar.DAY_OF_MONTH, Integer.parseInt(day.get(0)));
//        day_pv.setSelected(0);
        if (day.size() < lastMonthDays && Integer.valueOf(currentDay) > day.size()) {
            day_pv.setSelected(day.size() - 1);
            currentDay = formatTimeUnit(day.size());
        } else {
            day_pv.setSelected(currentDay);
        }
        selectedCalender.set(Calendar.DAY_OF_MONTH, Integer.parseInt(currentDay));
        //重新赋值
        lastMonthDays = day.size();

最后,当然也别忘了到setSelectedTime中给lastMonthDays设置默认值。

            lastMonthDays = day.size();

6、将某一列的数值设为单独循环滚动

尽管源码中已经有setIsLoop方法可以设置为循环滚动,但是这个是将所有列都同时设置为循环滚动的,如果我们想单独将某一列设为循环滚动的话可以再增加几个方法:

    public void setYearIsLoop(boolean isLoop) {
        if (canAccess) {
            this.year_pv.setIsLoop(isLoop);
        }
    }

    public void setMonIsLoop(boolean isLoop) {
        if (canAccess) {
            this.month_pv.setIsLoop(isLoop);
        }
    }

    public void setDayIsLoop(boolean isLoop) {
        if (canAccess) {
            this.day_pv.setIsLoop(isLoop);
        }
    }

    public void setHourIsLoop(boolean isLoop) {
        if (canAccess) {
            this.hour_pv.setIsLoop(isLoop);
        }
    }

    public void setMinIsLoop(boolean isLoop) {
        if (canAccess) {
            this.minute_pv.setIsLoop(isLoop);
        }
    }

7、去除对话框与屏幕之间的间距

这个问题我在Demo中没有发现,整合到工程中才遇到。弹出对话框时,左右和底部有一定的间距始终消除不了,但是在初始化对话框的方法initDialog方法中加了下面的代码就可以了:

      window.setBackgroundDrawableResource(R.color.color_bg);

颜色的值可以是透明的,但个人觉得最后还是设置一个背景色。这个问题,目前我也没有找到原因……

8、总结

经过上面一番改动,我可以愉快地整合到我自己的项目中了,个人觉得这个控件挺美观实用的,欢迎大家支持原博主。如果大家有什么好的改动,也欢迎给我留言。下面附上源码:

TimePickerDemo

阅读 5.3k

推荐阅读