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。因此這就產生了兼容性問題。android

我要解決的問題

  1. UI的一致性。
    • 在4.x上,DatePicker沒有Mode的概念,默認就是滾輪和日曆並排顯示,但可經過xml或者代碼,控制只顯示滾輪或者只顯示日曆。可是日曆模式下,存在上述bug1的問題,因此與老大商量了一下,考慮到4.x系統佔比過小,可使用滾輪模式。api


       
      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)的屬性來作到的。app

  2. 解決上述發現的bug。
    要解決兼容性問題,也要解決bug,因此在代碼中必須分狀況處理。ide

代碼

代碼量不多,註釋也寫的很清楚,相信看完就懂了。源碼分析

  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,能夠直接以對話框的形式使用,可是這樣就不夠靈活了。ui



做者:華枯榮
轉自:https://www.jianshu.com/p/6700e0422e6ethis

參考文章

  1. Change Datepicker dialog color for Android 5.0
  2. 【Android開源庫合集】日曆效果 - 若是須要更強大的效果,仍是第三方開源庫靠譜
  3. 修改DatePicker、 NumberPicker 默認屬性(間距、分割線顏色和高度) - 這篇文章提出了用反射和getIdentifier方法獲取並修改隱藏屬性,頗有啓發性。
相關文章
相關標籤/搜索