(请先认真读一下前两段,谢谢)
最近做了一个电商的Android原生项目,其中有一个酒店预订的功能,要实现一个日期控件,基本就是入住时间,离店时间,日期控件需要连续展示一年或者几年的按月份显示的连续视图。这样当然是为了让用户能在日期控件上选择一段连续的时间,相信大家不难理解这个需求。
有关日期控件,网上一抓一大把,但是我发现,无论是什么日期控件,博主们都是割裂的来介绍,无论日期控件多么炫酷,如果结合实际的项目环境来说,我们在实操的过程中,又会遇到很多的取舍和无可奈何,所以今天我就专门说一说如何根据一个具体的需求,作出一个在真实项目环境中的一个通用性强的日期控件,来解决具体的这个问题。
放出需求设计图:
我们看上面这张设计图,看似简单,实际上还是有很多细节的东西要做,简单分析一下:
1. 日期的显示,也就是调用日期控件后,回调回来的日期要特殊处理;
2. 星期的中文特殊处理;
3. 两个日期之间横跨几晚;
延伸:
1. 我点击入住时间,弹出的日期控件要既能选择一天,也能选择一段连续的时间;
2. 我点击离店时间,弹出的日期控件就只能选择单个的一天;(记住:无限制的灵活是对你的工作的不尊重哈);
3. 选择时间段,应该入职时间和离店时间刚好是这段时间的首位两个日期;
4. 入住时间选择时,超出最大时间限制或者逆向选择时间段,应该有错误提示;
再说说技术上的设计需求:
通用的控件写作原则,即控件的相关功能在控件中折腾,调用者只负责“new”控件,然后接收返回值后渲染调用页面的值,也就是上面的设计页面中文处理啊等等的效果。
插播一下我们可以使用的日期控件,Android提供的android-times-square日历控件,正好符合我的要求:
可以看到,日期控件可以选择单个日期也可以选择一个日期段,然后返回首尾日期到我们的调用页。
之所以插播这个日期控件的出处,是因为要在设计上注意,这个日期控件继承DialogFragment实现,所以我们要通用的写出这个日期控件需要额外写一个Activity包裹这个Fragment,这有一个好处,调用页面访问这个日期控件的Activity,然后日期控件回传值的时候可以随意关闭它的依托Activity而不用考虑主页面其他控件值的保留问题,也就是说我们调用页面可能还有查询条件的其他条件,比如城市、房间数等等,所以如果我们的日期控件加载在主调用页面上,那么我们选择日期后关闭当前页面的时候就只能关闭主调用页面了,也可以单独关闭这个碎片,但是碎片上的值不得不通过跳转Activity返回到主页面中,这样就得新打开这个主页面了,其他的值当然就需要传递保存下来,这就太麻烦了,也不符合低耦合的理念。另外频繁关闭主调用页面也会拖慢系统速度。
所以目测一下,我们要写三部分内容:
1. 调用日期控件的代码编写;
2. 日期控件的Activity;
3. 日期控件;
前期准备工作就说到这里,下面开始上代码:
步骤一:日期控件的引用,一句话:
compile 'com.squareup:android-times-square:1.4.1@aar'
步骤二:Layout引用:
<com.squareup.timessquare.CalendarPickerView
android:id="@+id/calendar_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="#FFFFFF"
android:clipToPadding="false"
android:paddingBottom="16dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:scrollbarStyle="outsideOverlay" />
<Button
android:id="@+id/date_done_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/hotel_date_confirm_btn" />
日期控件的引入加上一个确认按钮,点击以后将日期控件上的值带回到主调用页面。
步骤三:编写日期控件:
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.squareup.timessquare.CalendarPickerView;
import com.ta.utdid2.android.utils.StringUtils;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// 获取日期控件的视图
View view = inflater.inflate(R.layout.activity_date_picker, container, false);
// 初始化日期控件的形式,以及是否需要设置成可以选择范围
final CalendarPickerView calendarPickerView = initCalendarPickerView(view);
// 无效日期选择错误信息监听
calendarPickerView.setOnInvalidDateSelectedListener(new CalendarPickerView.OnInvalidDateSelectedListener() {
@Override
public void onInvalidDateSelected(Date date) {
String errMessage =
getResources().getString(R.string.hotel_invalid_date, currentDateStr, limitEndDateStr);
DialogUtil.showToastCust(errMessage);
}
});
view.findViewById(R.id.date_done_button).setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View view) {
List<Date> rangeDateList = calendarPickerView.getSelectedDates(); //选择的所有的日期
int size = calendarPickerView.getSelectedDates().size(); //一共几晚
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
boolean isEndDate = isEndDateComponent();
if (!isEndDate) {
// 如果触发时间控件的是开始日期控件
if (size >= 2) {
/*
* 选择的日期是一个区间
*/
Date startDate = rangeDateList.get(0);
String startDateStr = format.format(startDate);
Date endDate = rangeDateList.get(size - 1);
String endDateStr = format.format(endDate);
Intent intent = setBundleValue(size, startDateStr, endDateStr);
getActivity().setResult(Activity.RESULT_OK, intent);
getActivity().finish();
} else {
/*
* 选择的日期是一天
*/
Date startDate = rangeDateList.get(0);
String startDateStr = format.format(startDate);
Bundle bundle = new Bundle();
bundle.putString(IConst.Bundle.HOTEL_START_DATE, startDateStr);
Intent intent = new Intent();
intent.putExtras(bundle);
getActivity().setResult(Activity.RESULT_OK, intent);
getActivity().finish();
}
} else {
// 如果触发时间控件的是结束日期控件
Date endDate = rangeDateList.get(0);
String endDateStr = format.format(endDate);
Date date = new Date();
String startDateValue = getStartDateValue();
int hotelTotalN = 0;
if(!StringUtils.isEmpty(startDateValue)) {
date = DateUtil.str2Date(startDateValue, "yyyy-MM-dd");
hotelTotalN = DateUtil.daysBetween(date, endDate);
}
Intent intent = setBundleValue(hotelTotalN, startDateValue, endDateStr);
getActivity().setResult(Activity.RESULT_OK, intent);
getActivity().finish();
}
}
@NonNull
protected Intent setBundleValue(int size, String startDateStr, String endDateStr) {
Bundle bundle = new Bundle();
bundle.putString(IConst.Bundle.HOTEL_START_DATE, startDateStr);
bundle.putString(IConst.Bundle.HOTEL_END_DATE, endDateStr);
bundle.putString(IConst.Bundle.HOTEL_TOTAL_NIGHT, String.valueOf((size - 1) >= 0 ? (size - 1) : 0));
bundle.putString(IConst.Bundle.WIN_RESULT, IConst.Bundle.WIN_SELECT_DATE);
Intent intent = new Intent();
intent.putExtras(bundle);
return intent;
}
});
view.findViewById(R.id.date_picker_rl_back).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
getActivity().finish();
}
});
return view;
}
/**
* 初始化日期控件
* @param view
* @return
*/
@NonNull
protected CalendarPickerView initCalendarPickerView(View view) {
Calendar nextYear = Calendar.getInstance();
nextYear.add(Calendar.YEAR, 1);
final CalendarPickerView calendar = (CalendarPickerView) view.findViewById(R.id.calendar_view);
Date today = new Date();
currentDateStr = DateUtil.getCurrentDateForSDF();
// 最大能选择的日期
limitEndDateStr = DateUtil.getCalendarStr(nextYear);
//设置日期控件可以选择一段时间还是只能选择单个日期
if (isEndDateComponent()) {
calendar.init(today, nextYear.getTime())
.inMode(CalendarPickerView.SelectionMode.SINGLE)
.withSelectedDate(today);
} else {
calendar.init(today, nextYear.getTime())
.inMode(CalendarPickerView.SelectionMode.RANGE)
.withSelectedDate(today);
}
return calendar;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStyle(DialogFragment.STYLE_NORMAL, android.R.style.Theme_Black_NoTitleBar_Fullscreen);
}
/**
* setter getter method
* by shikeyue
**/
public String getStartDateValue() {
return startDateValue;
}
public void setStartDateValue(String startDateValue) {
this.startDateValue = startDateValue;
}
public boolean isEndDateComponent() {
return isEndDateComponent;
}
public void setEndDateComponent(boolean endDateComponent) {
isEndDateComponent = endDateComponent;
}
- 初始化控件的时候要标明日历显示的范围,比如一年的日历对于我的需求来讲就足够了。
- 先执行的onCreate方法中我设置了日期控件占满全屏。
- onCreateView是我的主要业务逻辑方法,因为要处理的事情是点击开始日期是可以选择一个范围,也可以选择一个日期,点击结束日期只能选择一个日期,返回的日期一个是list,一个是单个的时间,这个是通过日期控件的Activity向日期控件传值来判断的,我可以取一个日期还是取首尾两个。
- 构建的set get方法方便一些外部访问传值来控制我日期控件的行为。
- 此时不用销毁日期碎片,而是直接 getActivity().finish();来关闭整体的日期碎片以及下面要讲的依托的日期Activity
- 最重要的一点是,我利用了如下代码:
Intent intent = setBundleValue(hotelTotalN, startDateValue, endDateStr);
getActivity().setResult(Activity.RESULT_OK, intent);
这是Activity底层的方法,直接与主调用页面的Activity通信,主调用页面如何接到返回值,下面会提到。
步骤四:编写日期控件的Activity,记住Fragment依托于Activity,其实它就是一个Activity上的独立的控件。
/**
* 日历控件
* Created by shikeyue on 17/4/25.
*/
public class DatePickerActivity extends TbiAppActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle bundle = getIntent().getExtras();
if (bundle != null) {
// HotelQueryActivity to DatePickerActivity
boolean isEndDateInput = bundle.getBoolean(IConst.Bundle.IS_HOTEL_END_DATE_INPUT);
if (!isEndDateInput) {
// 不是由结束时间输入框触发的日期控件
DatePickerFragment datePickerFragment = new DatePickerFragment();
datePickerFragment.setEndDateComponent(false);
datePickerFragment.show(getSupportFragmentManager(), "DatePickerFragment");
} else {
String hotelStartDateStr = bundle.getString(IConst.Bundle.HOTEL_START_DATE);
DatePickerFragment datePickerFragment = new DatePickerFragment();
datePickerFragment.setEndDateComponent(true);
datePickerFragment.setStartDateValue(hotelStartDateStr);
datePickerFragment.show(getSupportFragmentManager(), "DatePickerFragment");
}
}
}
}
只有这么多,就是new一下日期碎片这个控件。可以看到我区分了开始日期输入框的点击还是结束日期输入框的点击,这个标志位是通过主调用页面来传递的。
步骤五:主调用页面日期控件的调用:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
hotelStartDate.setText(DateUtil.getMMDDSpecial(DateUtil.getCurrentDateForSDF()));
hotelStartDateWeek.setText(R.string.today_chinese);
hotelStartDateStr = DateUtil.getCurrentDateForSDF();
String nextDay = DateUtil.date2Str(DateUtil.addDay(new Date(), 1), "yyyy-MM-dd");
hotelEndDate.setText(DateUtil.getMMDDSpecial(nextDay));
hotelEndDateWeek.setText(DateUtil.calculateWeek(nextDay));
hotelEndDateStr = nextDay;
hotelQueryStartLayout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(ctx, DatePickerActivity.class);
Bundle bundle = new Bundle();
// 是否从结束日期控件触发日期控件
bundle.putBoolean(IConst.Bundle.IS_HOTEL_END_DATE_INPUT, false);
intent.putExtras(bundle);
ctx.startActivityForResult(intent, Activity.RESULT_FIRST_USER);
}
});
hotelQueryEndLayout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(ctx, DatePickerActivity.class);
Bundle bundle = new Bundle();
// 是否从结束日期控件触发日期控件
bundle.putBoolean(IConst.Bundle.IS_HOTEL_END_DATE_INPUT, true);
bundle.putString(IConst.Bundle.HOTEL_START_DATE, hotelStartDateStr);
intent.putExtras(bundle);
ctx.startActivityForResult(intent, Activity.RESULT_FIRST_USER);
}
});
}
我设置了两个日期初始化的显示,他们横跨一晚,这样比较友好。也可以看到对于两个日期输入框都加入了监听,访问的是日期Activity,注意一定是使用ctx.startActivityForResult(intent, Activity.RESULT_FIRST_USER);调用,否则后面的getActivity().setResult(Activity.RESULT_OK, intent);是不能成功返回的。
同时下面的这段是接受回传值的方法,也是这种形式的一部分(前面提到要在这里说的部分)
/**
* 接收返回值
* @param resultCode
* @param data
*/
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if(resultCode==RESULT_OK){
String winResult=data.getStringExtra(IConst.Bundle.WIN_RESULT);
if(IConst.Bundle.WIN_CP_COMMON_SELECT_CITY.equals(winResult)) {
//城市回调
}else if(IConst.Bundle.WIN_SELECT_DATE.equals(winResult)) {
// 设置页面控件的显示值
setViewTextValue(data);
}else {
}
}
}
/**
* 设置页面控件的值
*/
public void setViewTextValue(Intent intent) {
Bundle bundle = intent.getExtras();
if (bundle != null) {
hotelStartDateStr = bundle.getString(IConst.Bundle.HOTEL_START_DATE);
if (!StringUtils.isEmpty(hotelStartDateStr)) {
hotelStartDate.setText(DateUtil.getMMDDSpecial(hotelStartDateStr));
// 如果是当天不现实星期几显示今天
if (DateUtil.getCurrentDateForSDF().equals(hotelStartDateStr)) {
hotelStartDateWeek.setText(R.string.today_chinese);
} else {
hotelStartDateWeek.setText(DateUtil.calculateWeek(hotelStartDateStr));
}
}
hotelEndDateStr = bundle.getString(IConst.Bundle.HOTEL_END_DATE);
if (!StringUtils.isEmpty(hotelEndDateStr)) {
hotelEndDate.setText(DateUtil.getMMDDSpecial(hotelEndDateStr));
hotelEndDateWeek.setText(DateUtil.calculateWeek(hotelEndDateStr));
}
// 日期区间包含几晚
String hotelTotalN = bundle.getString(IConst.Bundle.HOTEL_TOTAL_NIGHT);
if (!StringUtils.isEmpty(hotelTotalN)) {
hotelTotalNight.setText(hotelTotalN);
}
}
}
这个方法中接受返回值,可以看到也是继承底层实现来的,可以通用的写,将不同的回调控件都写在这里,然后对返回值处理。
至此,一个完整的结合项目环境的通用控件就完成了。OK,写到这里,匆忙完成,希望大家能够指正,也欢迎给我留言,完整代码我会更新到我的GitHub上,欢迎大家查阅。