今天在某群刚好看到一群友,一需求:日期选择器TimePicker,博主花了点小时间抽出Demo分享出去了,因为一般博主比较喜欢笔记记录在本子上(翻阅起来纸张还是比较舒爽的,你懂得,哈哈),因此写此个博客记录下来,提供未来有需要开发到此功能的读者,本文仅供技术学习使用.
效果图:
实例源代码包含配套一个人信息页面
读者按着实例代码,就能实现上述功能到自己的项目,达到快速开发实现的功能.
MainActivity 页面设置为个人中心页面,点击调用changeBirthday()方法即可
/**
* 修改生日时间选择器
*/
private void changeBirthday() {
/**
* 注意事项:
* 2.因为系统Calendar的月份是从0-11的,所以如果是调用Calendar的set方法来设置时间,月份的范围也要是从0-11
*/
Calendar selectedDate = Calendar.getInstance();
selectedDate.set(2005, 5, 30); //弹窗默认时间
Calendar startDate = Calendar.getInstance();
startDate.set(1960, 0, 1);
Calendar endDate = Calendar.getInstance();
endDate.set(2020, 11, 31);
//时间选择器 ,自定义布局
mPvCustomTime = new TimePickerBuilder(this, new OnTimeSelectListener() {
@Override
public void onTimeSelect(Date date, View v) {//选中事件回调
//判断时间是否超过当前时间
Calendar currentTime = Calendar.getInstance();
Date currentDate = currentTime.getTime();
int dateResult = date.compareTo(currentDate);
if (dateResult == 1) {
ToastUtil.toast(mContext, "您选择的时间超过了当前时间");
} else {
//显示正在上传信息的Dialog,上报新的生日
mPvCustomTime.dismiss();
mCenterDialog.show();
// todo 服务器
// mUserMessagePI.uploadUserMessage(null, null, null, getTime(date), null);
}
}
}).setDate(selectedDate)
.setRangDate(startDate, endDate)
.setLayoutRes(R.layout.pickerview_custom_time, new CustomListener() {
@Override
public void customLayout(View v) {
final TextView tvSubmit = v.findViewById(R.id.tv_confirm);
TextView tvCancel = v.findViewById(R.id.tv_cancel);
tvSubmit.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mPvCustomTime.returnData();
}
});
tvCancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mPvCustomTime.dismiss();
}
});
}
})
.setContentTextSize(20)
.setType(new boolean[]{true, true, true, false, false, false})
.setLabel("", "", "", "", "", "")
.setLineSpacingMultiplier(1.6f)
.isCenterLabel(false) //是否只显示中间选中项的label文字,false则每项item全部都带有label。
.setDividerColor(mContext.getResources().getColor(R.color.transparent_00FFFFFF))
.setTextColorCenter(mContext.getResources().getColor(R.color.color_55ceac))
.build();
mPvCustomTime.show();
}
MainActivity 对应XML布局
/**
* 时间转轮
* @param timePickerView
*/
private void initWheelTime(LinearLayout timePickerView) {
wheelTime = new WheelTime(timePickerView, mPickerOptions.type, mPickerOptions.textGravity, mPickerOptions.textSizeContent);
if (mPickerOptions.timeSelectChangeListener != null) {
wheelTime.setSelectChangeCallback(new ISelectTimeCallback() {
@Override
public void onTimeSelectChanged() {
try {
Date date = WheelTime.dateFormat.parse(wheelTime.getTime());
mPickerOptions.timeSelectChangeListener.onTimeSelectChanged(date);
} catch (ParseException e) {
e.printStackTrace();
}
}
});
}
wheelTime.setLunarMode(mPickerOptions.isLunarCalendar);
if (mPickerOptions.startYear != 0 && mPickerOptions.endYear != 0
&& mPickerOptions.startYear <= mPickerOptions.endYear) {
setRange();
}
//若手动设置了时间范围限制
if (mPickerOptions.startDate != null && mPickerOptions.endDate != null) {
if (mPickerOptions.startDate.getTimeInMillis() > mPickerOptions.endDate.getTimeInMillis()) {
throw new IllegalArgumentException("startDate can't be later than endDate");
} else {
setRangDate();
}
} else if (mPickerOptions.startDate != null) {
if (mPickerOptions.startDate.get(Calendar.YEAR) < 1900) {
throw new IllegalArgumentException("The startDate can not as early as 1900");
} else {
setRangDate();
}
} else if (mPickerOptions.endDate != null) {
if (mPickerOptions.endDate.get(Calendar.YEAR) > 2100) {
throw new IllegalArgumentException("The endDate should not be later than 2100");
} else {
setRangDate();
}
} else {//没有设置时间范围限制,则会使用默认范围。
setRangDate();
}
setTime();
wheelTime.setLabels(mPickerOptions.label_year, mPickerOptions.label_month, mPickerOptions.label_day
, mPickerOptions.label_hours, mPickerOptions.label_minutes, mPickerOptions.label_seconds);
wheelTime.setTextXOffset(mPickerOptions.x_offset_year, mPickerOptions.x_offset_month, mPickerOptions.x_offset_day,
mPickerOptions.x_offset_hours, mPickerOptions.x_offset_minutes, mPickerOptions.x_offset_seconds);
setOutSideCancelable(mPickerOptions.cancelable);
wheelTime.setCyclic(mPickerOptions.cyclic);
wheelTime.setDividerColor(mPickerOptions.dividerColor);
wheelTime.setDividerType(mPickerOptions.dividerType);
wheelTime.setLineSpacingMultiplier(mPickerOptions.lineSpacingMultiplier);
wheelTime.setTextColorOut(mPickerOptions.textColorOut);
wheelTime.setTextColorCenter(mPickerOptions.textColorCenter);
wheelTime.isCenterLabel(mPickerOptions.isCenterLabel);
}
@Override
protected void onDraw(Canvas canvas) {
if (adapter == null) {
return;
}
//initPosition越界会造成preCurrentIndex的值不正确
initPosition = Math.min(Math.max(0, initPosition), adapter.getItemsCount() - 1);
//可见的item数组
@SuppressLint("DrawAllocation") Object visibles[] = new Object[itemsVisible];
//滚动的Y值高度除去每行Item的高度,得到滚动了多少个item,即change数
change = (int) (totalScrollY / itemHeight);
// Log.d("change", "" + change);
try {
//滚动中实际的预选中的item(即经过了中间位置的item) = 滑动前的位置 + 滑动相对位置
preCurrentIndex = initPosition + change % adapter.getItemsCount();
} catch (ArithmeticException e) {
Log.e("WheelView", "出错了!adapter.getItemsCount() == 0,联动数据不匹配");
}
if (!isLoop) {//不循环的情况
if (preCurrentIndex < 0) {
preCurrentIndex = 0;
}
if (preCurrentIndex > adapter.getItemsCount() - 1) {
preCurrentIndex = adapter.getItemsCount() - 1;
}
} else {//循环
if (preCurrentIndex < 0) {//举个例子:如果总数是5,preCurrentIndex = -1,那么preCurrentIndex按循环来说,其实是0的上面,也就是4的位置
preCurrentIndex = adapter.getItemsCount() + preCurrentIndex;
}
if (preCurrentIndex > adapter.getItemsCount() - 1) {//同理上面,自己脑补一下
preCurrentIndex = preCurrentIndex - adapter.getItemsCount();
}
}
//跟滚动流畅度有关,总滑动距离与每个item高度取余,即并不是一格格的滚动,每个item不一定滚到对应Rect里的,这个item对应格子的偏移值
float itemHeightOffset = (totalScrollY % itemHeight);
// 设置数组中每个元素的值
int counter = 0;
while (counter < itemsVisible) {
int index = preCurrentIndex - (itemsVisible / 2 - counter);//索引值,即当前在控件中间的item看作数据源的中间,计算出相对源数据源的index值
//判断是否循环,如果是循环数据源也使用相对循环的position获取对应的item值,如果不是循环则超出数据源范围使用""空白字符串填充,在界面上形成空白无数据的item项
if (isLoop) {
index = getLoopMappingIndex(index);
visibles[counter] = adapter.getItem(index);
} else if (index < 0) {
visibles[counter] = "";
} else if (index > adapter.getItemsCount() - 1) {
visibles[counter] = "";
} else {
visibles[counter] = adapter.getItem(index);
}
counter++;
}
//绘制中间两条横线
if (dividerType == DividerType.WRAP) {//横线长度仅包裹内容
float startX;
float endX;
if (TextUtils.isEmpty(label)) {//隐藏Label的情况
startX = (measuredWidth - maxTextWidth) / 2 - 12;
} else {
startX = (measuredWidth - maxTextWidth) / 4 - 12;
}
if (startX <= 0) {//如果超过了WheelView的边缘
startX = 10;
}
endX = measuredWidth - startX;
canvas.drawLine(startX, firstLineY, endX, firstLineY, paintIndicator);
canvas.drawLine(startX, secondLineY, endX, secondLineY, paintIndicator);
} else {
canvas.drawLine(0.0F, firstLineY, measuredWidth, firstLineY, paintIndicator);
canvas.drawLine(0.0F, secondLineY, measuredWidth, secondLineY, paintIndicator);
}
//只显示选中项Label文字的模式,并且Label文字不为空,则进行绘制
if (!TextUtils.isEmpty(label) && isCenterLabel) {
//绘制文字,靠右并留出空隙
int drawRightContentStart = measuredWidth - getTextWidth(paintCenterText, label);
canvas.drawText(label, drawRightContentStart - CENTER_CONTENT_OFFSET, centerY, paintCenterText);
}
counter = 0;
while (counter < itemsVisible) {
canvas.save();
// 弧长 L = itemHeight * counter - itemHeightOffset
// 求弧度 α = L / r (弧长/半径) [0,π]
double radian = ((itemHeight * counter - itemHeightOffset)) / radius;
// 弧度转换成角度(把半圆以Y轴为轴心向右转90度,使其处于第一象限及第四象限
// angle [-90°,90°]
float angle = (float) (90D - (radian / Math.PI) * 180D);//item第一项,从90度开始,逐渐递减到 -90度
// 计算取值可能有细微偏差,保证负90°到90°以外的不绘制
if (angle >= 90F || angle <= -90F) {
canvas.restore();
} else {
// 根据当前角度计算出偏差系数,用以在绘制时控制文字的 水平移动 透明度 倾斜程度
float offsetCoefficient = (float) Math.pow(Math.abs(angle) / 90f, 2.2);
//获取内容文字
String contentText;
//如果是label每项都显示的模式,并且item内容不为空、label 也不为空
if (!isCenterLabel && !TextUtils.isEmpty(label) && !TextUtils.isEmpty(getContentText(visibles[counter]))) {
contentText = getContentText(visibles[counter]) + label;
} else {
contentText = getContentText(visibles[counter]);
}
reMeasureTextSize(contentText);
//计算开始绘制的位置
measuredCenterContentStart(contentText);
measuredOutContentStart(contentText);
float translateY = (float) (radius - Math.cos(radian) * radius - (Math.sin(radian) * maxTextHeight) / 2D);
//根据Math.sin(radian)来更改canvas坐标系原点,然后缩放画布,使得文字高度进行缩放,形成弧形3d视觉差
canvas.translate(0.0F, translateY);
// canvas.scale(1.0F, (float) Math.sin(radian));
if (translateY <= firstLineY && maxTextHeight + translateY >= firstLineY) {
// 条目经过第一条线
canvas.save();
canvas.clipRect(0, 0, measuredWidth, firstLineY - translateY);
canvas.scale(1.0F, (float) Math.sin(radian) * SCALE_CONTENT);
canvas.drawText(contentText, drawOutContentStart, maxTextHeight, paintOuterText);
canvas.restore();
canvas.save();
canvas.clipRect(0, firstLineY - translateY, measuredWidth, (int) (itemHeight));
canvas.scale(1.0F, (float) Math.sin(radian) * 1.0F);
canvas.drawText(contentText, drawCenterContentStart, maxTextHeight - CENTER_CONTENT_OFFSET, paintCenterText);
canvas.restore();
} else if (translateY <= secondLineY && maxTextHeight + translateY >= secondLineY) {
// 条目经过第二条线
canvas.save();
canvas.clipRect(0, 0, measuredWidth, secondLineY - translateY);
canvas.scale(1.0F, (float) Math.sin(radian) * 1.0F);
canvas.drawText(contentText, drawCenterContentStart, maxTextHeight - CENTER_CONTENT_OFFSET, paintCenterText);
canvas.restore();
canvas.save();
canvas.clipRect(0, secondLineY - translateY, measuredWidth, (int) (itemHeight));
canvas.scale(1.0F, (float) Math.sin(radian) * SCALE_CONTENT);
canvas.drawText(contentText, drawOutContentStart, maxTextHeight, paintOuterText);
canvas.restore();
} else if (translateY >= firstLineY && maxTextHeight + translateY <= secondLineY) {
// 中间条目
canvas.clipRect(0, 0, measuredWidth, maxTextHeight);
//让文字居中
float Y = maxTextHeight - CENTER_CONTENT_OFFSET;//因为圆弧角换算的向下取值,导致角度稍微有点偏差,加上画笔的基线会偏上,因此需要偏移量修正一下
canvas.drawText(contentText, drawCenterContentStart, Y, paintCenterText);
//设置选中项
selectedItem = preCurrentIndex - (itemsVisible / 2 - counter);
} else {
// 其他条目
canvas.save();
canvas.clipRect(0, 0, measuredWidth, (int) (itemHeight));
canvas.scale(1.0F, (float) Math.sin(radian) * SCALE_CONTENT);
// 控制文字倾斜角度
paintOuterText.setTextSkewX((textXOffset == 0 ? 0 : (textXOffset > 0 ? 1 : -1)) * (angle > 0 ? -1 : 1) * DEFAULT_TEXT_TARGET_SKEWX * offsetCoefficient);
// 控制透明度
paintOuterText.setAlpha((int) ((1 - offsetCoefficient) * 255));
// 控制文字水平偏移距离
canvas.drawText(contentText, drawOutContentStart + textXOffset * offsetCoefficient, maxTextHeight, paintOuterText);
canvas.restore();
}
canvas.restore();
paintCenterText.setTextSize(textSize);
}
counter++;
}
}
祝大家国庆节快乐!