zoukankan      html  css  js  c++  java
  • android 日期控件 DatePicker

    DatePicker的缺陷

    1. 提供的API太少,没办法个性化定制。比如,不能指定某部分的颜色,不能控制显示的部分等。
    2. xml中提供的属性太少,同样影响定制化。
    3. 兼容性问题太多,在4.x,5.x和6.0+上UI不同
    4. 同样是兼容性问题,同一个属性设置在不同的系统版本上有不同的效果
    5. bug太多,暂且发现下面这6个
      • bug1:日历模式,在5.0以下设置的可选时间区间如果与当前日期在同一栏会crash
      • bug2:LOLLIPOP上OnDateChangedListener回调无效(5.0上存在,5.1修复)
      • bug3:5.0上超过可选区间的日期依然能选中,所以要手动校验.5.1上已解决.
      • bug4:LOLLIPOP和Marshmallow上,使用spinner模式,然后隐藏滚轮,显示日历,日历最底部一排日期被截去部分
      • bug5:5.1上,maxdate不可选。由于5.0有bug3,所以可能bug5被掩盖了。4.x和6.0+版本没有这个问题。
      • bug6:bug5在6.0+上有另一个表现形式,currentDate如果与MaxDate一样,初始化时会触发一个onDateChanged回调。其内部原因都是一样的。

    DatePicker的使用

    由于Google在Android4.x上采用的是holo风格,在5.x及以上采用的Material Design风格,所以从4.x到5.x,Google重构了DatePicker,包括代码和UI。所以这就产生了兼容性问题。

    我要解决的问题

    1. UI的一致性。
      • 在4.x上,DatePicker没有Mode的概念,默认就是滚轮和日历并排显示,但可通过xml或者代码,控制只显示滚轮或者只显示日历。但是日历模式下,存在上述bug1的问题,所以与老大商量了一下,考虑到4.x系统占比太小,可以使用滚轮模式。


         
        DatePicker在4.x
      • 在5.x及以上,DatePicker引入了Mode的概念,spinner和calendar只能显示其中一个,所以可以在xml直接指定calendar模式。但是5.x和6.0+的日历都多了一个头部,而且5.x和6.0+的头部还不一样,又没有API可以隐藏头部。所以,需要自己想办法隐藏头部。
     
    DatePicker在5.x
     
    DatePicker在6.0+
    1. 定制DatePicker,符合射鸡师的要求。
      DatePicker的能用来做个性化的API和属性值太少了,正常途径我要改变选中日期的圆圈颜色都做到。其实,系统提供的控件多半是从系统提供的style中读取配置,我们可以自己配置一个style给DatePicker。
      如果在Activity中使用DatePicker,DatePicker会读取Activity的Theme;如果在Dialog中使用DatePicker,会读取Dialog的Theme(如果Dialog没有指定Theme,默认使用Activity的Theme)。我们要在Dialog中使用DatePicker,所以自定义一个DatePicker的style,传给自定义的Dialog的Theme,再使用自定义的Theme创建Dialog就好了。
      其实系统提供了几个默认Theme,通过它们可以简单改变DatePicker的风格,参考这个答案。但其实这些Theme内部也是通过改变DatePicker(通过datepickerstyle)的属性来做到的。

    2. 解决上述发现的bug。
      要解决兼容性问题,也要解决bug,所以在代码中必须分情况处理。

    代码

    代码量很少,注释也写的很清楚,相信看完就懂了。

    1. 内部封装DatePicker的DialogFragment
    public class CustomDatePickerDialogFragment extends DialogFragment  implements DatePicker.OnDateChangedListener, View.OnClickListener{
        public static final String CURRENT_DATE = "datepicker_current_date";
        public static final String START_DATE = "datepicker_start_date";
        public static final String END_DATE = "datepicker_end_date";
        Calendar currentDate;
        Calendar startDate;
        Calendar endDate;
    
        DatePicker datePicker;
        TextView backButton;
        TextView ensureButton;
        View splitLineV;
    
        @Override
        public void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setCancelable(false);
            Bundle bundle = getArguments();
            currentDate = (Calendar) bundle.getSerializable(CURRENT_DATE);
            startDate = (Calendar) bundle.getSerializable(START_DATE);
            endDate = (Calendar) bundle.getSerializable(END_DATE);
        }
    
        @Nullable
        @Override
        public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
            if (inflater == null) {
                return super.onCreateView(inflater, container, savedInstanceState);
            }
            getDialog().requestWindowFeature(Window.FEATURE_NO_TITLE);
            getDialog().getWindow().setDimAmount(0.8f);
            View view  = inflater.inflate(R.layout.dialog_date_picker_layout,container,false);
            return view;
        }
    
        @NonNull
        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            int style;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                style = R.style.ZZBDatePickerDialogLStyle;
            } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                style = R.style.ZZBDatePickerDialogLStyle;
            } else {
                style = getTheme();
            }
            return new Dialog(getActivity(), style);
        }
    
        @Override
        public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
            super.onViewCreated(view, savedInstanceState);
            if (view != null) {
                datePicker = view.findViewById(R.id.datePickerView);
                backButton = view.findViewById(R.id.back);
                backButton.setOnClickListener(this);
                ensureButton = view.findViewById(R.id.ensure);
                ensureButton.setOnClickListener(this);
                splitLineV = view.findViewById(R.id.splitLineV);
                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
                    //bug1:日历模式,在5.0以下设置的可选时间区间如果与当前日期在同一栏会crash,所以只能用滚轮模式
                    datePicker.setCalendarViewShown(false);
                    datePicker.setSpinnersShown(true);
                    //滚轮模式必须使用确定菜单
                    ensureButton.setVisibility(View.VISIBLE);
                    splitLineV.setVisibility(View.VISIBLE);
                } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
                        && Build.VERSION.SDK_INT < Build.VERSION_CODES.M){
                    //bug2:LOLLIPOP上OnDateChangedListener回调无效(5.0存在,5.1修复),必须使用确定菜单回传选定日期
                    ensureButton.setVisibility(View.VISIBLE);
                    splitLineV.setVisibility(View.VISIBLE);
                    //如果只要日历部分,隐藏header
                    ViewGroup mContainer = (ViewGroup) datePicker.getChildAt(0);
                    View header = mContainer.getChildAt(0);
                    header.setVisibility(View.GONE);
                } else {
                    //bug4:LOLLIPOP和Marshmallow上,使用spinner模式,然后隐藏滚轮,显示日历(spinner模式下的日历没有头部),日历最底部一排日期被截去部分。所以只能使用calender模式,然后手动隐藏header(系统没有提供隐藏header的api)。
                    //如果只要日历部分,隐藏header
                    ViewGroup mContainer = (ViewGroup) datePicker.getChildAt(0);
                    View header = mContainer.getChildAt(0);
                    header.setVisibility(View.GONE);
                    //Marshmallow上底部留白太多,减小间距
                    LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) datePicker.getLayoutParams();
                    layoutParams.bottomMargin = 10;
                    datePicker.setLayoutParams(layoutParams);
                }
                initDatePicker();
            }
        }
    
        private void initDatePicker() {
            if (datePicker == null) {
                return;
            }
            if (currentDate == null) {
                currentDate = Calendar.getInstance();
                currentDate.setTimeInMillis(System.currentTimeMillis());
            }
            datePicker.init(currentDate.get(Calendar.YEAR),currentDate.get(Calendar.MONTH),currentDate.get(Calendar.DAY_OF_MONTH),this);
            if (startDate != null) {
                datePicker.setMinDate(startDate.getTimeInMillis());
            }
            if (endDate != null) {  
                //bug5:5.1上,maxdate不可选。由于5.0有bug3,所以可能bug5被掩盖了。4.x和6.0+版本没有这个问题。
                //bug5在6.0+上有另一个表现形式:初始化时会触发一次onDateChanged回调。通过源码分析一下原因:init方法只会设置控件当前日期的
                //年月日,而时分秒默认使用现在时间的时分秒,所以当前日期大于>最大日期,执行setMaxDate方法时,就会触发一次onDateChanged回调。
                //同理,setMinDate方法也面临同样的方法。所以设置范围时,MinDate取0时0分0秒,MaxDate取23时59分59秒。
                endDate.set(Calendar.HOUR_OF_DAY,23);
                endDate.set(Calendar.MINUTE,59);
                endDate.set(Calendar.SECOND,59);
                datePicker.setMaxDate(endDate.getTimeInMillis());
            }
        }
    
        @Override
        public void onClick(View v) {
            switch (v.getId()) {
                case R.id.back:
                    dismiss();
                    break;
                case R.id.ensure:
                    returnSelectedDateUnderLOLLIPOP();
                    break;
                default:
                    break;
            }
        }
    
        private void returnSelectedDateUnderLOLLIPOP() {
            //bug3:5.0上超过可选区间的日期依然能选中,所以要手动校验.5.1上已解决,但是为了与5.0保持一致,也采用确定菜单返回日期
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
                    && Build.VERSION.SDK_INT < Build.VERSION_CODES.M){
                Calendar selectedDate = Calendar.getInstance();
                selectedDate.set(datePicker.getYear(), datePicker.getMonth(), datePicker.getDayOfMonth(),0,0,0);
                selectedDate.set(Calendar.MILLISECOND,0);
                if (selectedDate.before(startDate) || selectedDate.after(endDate)) {
                    Toast.makeText(getActivity(), "日期超出有效范围", Toast.LENGTH_SHORT).show();
                    return;
                }
            }
            if (onSelectedDateListener != null) {
                onSelectedDateListener.onSelectedDate(datePicker.getYear(), datePicker.getMonth(), datePicker.getDayOfMonth());
            }
            dismiss();
        }
    
        @Override
        public void onDestroyView() {
            super.onDestroyView();
            onSelectedDateListener = null;
        }
    
        public interface OnSelectedDateListener {
            void onSelectedDate(int year, int monthOfYear, int dayOfMonth);
        }
    
        OnSelectedDateListener onSelectedDateListener;
    
        public void setOnSelectedDateListener(OnSelectedDateListener onSelectedDateListener) {
            this.onSelectedDateListener = onSelectedDateListener;
        }
    
        @Override
        public void onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
                    && Build.VERSION.SDK_INT < Build.VERSION_CODES.M){ //LOLLIPOP上,这个回调无效,排除将来可能的干扰
                return;
            }
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { //5.0以下,必须采用滚轮模式,所以需借助确定菜单回传选定值
                return;
            }
            if (onSelectedDateListener != null) {
                onSelectedDateListener.onSelectedDate(year, monthOfYear, dayOfMonth);
            }
            dismiss();
        }
    }
    1. CustomDatePickerDialogFragment的layout文件 - R.layout.dialog_date_picker_layout
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
    
        <DatePicker
            android:id="@+id/datePickerView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:spinnersShown="false"
            android:calendarViewShown="true"
            android:datePickerMode="calendar"
            android:layout_gravity="center_horizontal"
            android:layout_marginTop="20dp"
            android:layout_marginBottom="20dp"
            android:layout_marginLeft="10dp"
            android:layout_marginRight="10dp"/>
    
        <View
            android:layout_width="match_parent"
            android:layout_height="1px"
            android:background="@android:color/black" />
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">
    
            <TextView
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:paddingTop="10dp"
                android:paddingBottom="10dp"
                android:text="返回"
                android:gravity="center"
                android:textColor="@android:color/black"
                android:id="@+id/back"/>
    
            <View
                android:layout_width="1px"
                android:layout_height="match_parent"
                android:background="@android:color/black"
                android:id="@+id/splitLineV"
                android:visibility="gone"/>
    
            <TextView
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:paddingTop="10dp"
                android:paddingBottom="10dp"
                android:text="确认"
                android:gravity="center"
                android:textColor="@android:color/black"
                android:id="@+id/ensure"
                android:visibility="gone"/>
    
        </LinearLayout>
    
    </LinearLayout>
    1. 自定义的DatePicker的style
    <style name="ZZBDatePickerDialogLStyle" parent="android:Theme.DeviceDefault.Light.Dialog">
            <item name="android:datePickerStyle">@style/ZZBDatePickerLStyle</item>
            <!-- 初始化的那一天和选中时的圆圈的颜色-->
            <item name="android:colorControlActivated">@android:color/holo_blue_dark</item>
            <!-- LOLLIPOP,整个日历字体的颜色。Marshmallow,日历中星期字体颜色-->
            <item name="android:textColorSecondary">@android:color/holo_blue_dark</item>
            <!-- Marshmallow,日历字体的颜色,不可选的日期依然有置灰效果。LOLLIPOP,无效-->
            <item name="android:textColorPrimary">@android:color/holo_purple</item>
        </style>
    
        <style name="ZZBDatePickerLStyle" parent="android:Widget.Material.Light.DatePicker">
            <!-- LOLLIPOP,最顶部,星期标题的背景色。Marshmallow星期标题被合并到header,所以字段无效-->
            <item name="android:dayOfWeekBackground">@android:color/holo_blue_light</item>
            <!-- LOLLIPOP,最顶部,星期字体的颜色、大小等。Marshmallow星期标题被合并到header,所以字段无效-->
            <item name="android:dayOfWeekTextAppearance">@style/ZZBTitleDayOfWeekTextAppearance</item>
            <!-- 中间部分,header的背景色 -->
            <item name="android:headerBackground" >@android:color/holo_orange_dark</item>
            <!-- 中间部分,header的字体大小和颜色-->
            <!-- 对LOLLIPOP有效,对Marshmallow无效-->
            <item name="android:headerYearTextAppearance">@style/ZZBHeaderYearTextAppearance</item>
            <!-- 对LOLLIPOP和Marshmallow都是部分有效-->
            <item name="android:headerMonthTextAppearance">@style/ZZBHeaderMonthTextAppearance</item>
            <!-- 对LOLLIPOP有效,对Marshmallow无效-->
            <item name="android:headerDayOfMonthTextAppearance">@style/ZZBHeaderDayOfMonthTextAppearance</item>
            <!-- LOLLIPOP,控制整个日历字体颜色的最终字段,优先级最高,但是一旦使用了这个字段,不可选的日期就失去了置灰效果。对Marshmallow无效-->
            <item name="android:calendarTextColor">@android:color/holo_green_dark</item>
        </style>
    
        <style name="ZZBTitleDayOfWeekTextAppearance" parent="android:TextAppearance.Material">
            <item name="android:textColor">@android:color/black</item>
            <item name="android:textSize">12sp</item>
        </style>
        <style name="ZZBHeaderYearTextAppearance" parent="android:TextAppearance.Material">
            <item name="android:textColor">@android:color/holo_blue_light</item>
            <item name="android:textSize">50sp</item>
        </style>
        <style name="ZZBHeaderMonthTextAppearance" parent="android:TextAppearance.Material">
            <!-- LOLLIPOP无效,Marshmallow有效。控制Marshmallow中header部分所有的字体颜色。LOLLIPOP没有找到控制字体颜色的字段-->
            <item name="android:textColor">@android:color/holo_blue_light</item>
            <!-- LOLLIPOP有效,Marshmallow无效。Marshmallow没有找到控制header字体大小的字段-->
            <item name="android:textSize">50sp</item>
        </style>
        <style name="ZZBHeaderDayOfMonthTextAppearance" parent="android:TextAppearance.Material">
            <!-- 只可以控制字体的大小,没有找到控制字体颜色的字段-->
            <item name="android:textSize">50sp</item>
        </style>
    1. MainActivity的代码
    public class MainActivity extends AppCompatActivity implements View.OnClickListener,CustomDatePickerDialogFragment.OnSelectedDateListener{
        Button button;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            button = (Button) findViewById(R.id.datepicker);
            button.setOnClickListener(this);
        }
    
        @Override
        public void onClick(View v) {
            switch (v.getId()) {
                case R.id.datepicker:
                    showDatePickDialog();
                    break;
                default:
                    break;
            }
        }
    
        long day = 24 * 60 * 60 * 1000;
    
        private void showDatePickDialog() {
            CustomDatePickerDialogFragment fragment = new CustomDatePickerDialogFragment();
            fragment.setOnSelectedDateListener(this);
            Bundle bundle = new Bundle();
            Calendar currentDate = Calendar.getInstance();
            currentDate.setTimeInMillis(System.currentTimeMillis());
            currentDate.set(Calendar.HOUR_OF_DAY,0);
            currentDate.set(Calendar.MINUTE,0);
            currentDate.set(Calendar.SECOND,0);
            currentDate.set(Calendar.MILLISECOND,0);
            bundle.putSerializable(CustomDatePickerDialogFragment.CURRENT_DATE,currentDate);
    
    
            long start = currentDate.getTimeInMillis() - day * 2;
            long end = currentDate.getTimeInMillis() - day;
            Calendar startDate = Calendar.getInstance();
            startDate.setTimeInMillis(start);
            Calendar endDate = Calendar.getInstance();
            endDate.setTimeInMillis(end);
            bundle.putSerializable(CustomDatePickerDialogFragment.START_DATE,startDate);
            bundle.putSerializable(CustomDatePickerDialogFragment.END_DATE,currentDate);
    
            fragment.setArguments(bundle);
            fragment.show(getSupportFragmentManager(),CustomDatePickerDialogFragment.class.getSimpleName());
        }
    
        @Override
        public void onSelectedDate(int year, int monthOfYear, int dayOfMonth) {
            Toast.makeText(MainActivity.this,year+""+(monthOfYear+1)+""+dayOfMonth+"",Toast.LENGTH_SHORT).show();
        }
    1. MainActivity的Layout文件
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="10dp" >
    
        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="DatePickerDialog"
            android:id="@+id/datepicker"/>
    
    </LinearLayout>
    

      

    效果截图

    1. Jelly Bean
     
    DatePicker效果在4.x
    1. Lollipop
     
    DatePicker效果在5.x
    1. Marshmallow
     
    DatePicker效果在6.0
    1. 最后再贴一张符合设计稿的效果图
      我隐藏了头部,并且把ZZBDatePickerDialogLStyle中的颜色值都改成了设计稿中的颜色。
     
    datepickerfordesign在6.0+
     
    datepickerfordesign在5.x
     
    datepickerfordesign在4.x

    可见在6.0+上效果最好。

    最后

    除了自己用DialogFragment封装,系统还直接给提供了DatePickerDialog,可以直接以对话框的形式使用,但是这样就不够灵活了。



    作者:华枯荣
    转自:https://www.jianshu.com/p/6700e0422e6e

    参考文章

    1. Change Datepicker dialog color for Android 5.0
    2. 【Android开源库合集】日历效果 - 如果需要更强大的效果,还是第三方开源库靠谱
    3. 修改DatePicker、 NumberPicker 默认属性(间距、分割线颜色和高度) - 这篇文章提出了用反射和getIdentifier方法获取并修改隐藏属性,很有启发性。
  • 相关阅读:
    PHP:__get()、__set()、__isset()、__unset()、__call()、__callStatic()六个魔术方法
    概念:RPG游戏中两个兵种互相攻击的逻辑
    php怎么获取checkbox复选框的内容?
    20150724之问题
    Uploadify 之使用
    oneThink后台添加插件步骤详解
    针对各种浏览器,前端解决方案(持续更新...)
    解决IE8中select下拉列表文字上下不居中的问题
    针对IE6 7 8当独写样式
    document对象详解
  • 原文地址:https://www.cnblogs.com/xuqp/p/10121979.html
Copyright © 2011-2022 走看看