ionic3自定义日期滑动选择组件(兼容性有问题,仅作为思路参考
最终效果:
代码书写过程中难点在于:
1、根据年月,动态计算日,并且切换到当前日的显示。
2、组件export输出数据的值时,picker.js系统组件中默认输出内部selectindex指向的value,但是实际可能是不正确的,需要输出显示的日的值。
3、MutationObserver 观察者模式的使用:【学习地址】
4、ion-multi-picker的使用:【官方说明】
1、下载ion-multi-picker
npm i ion-multi-picker --save
2、创建组件
ionic g component datetime
执行以上命令后,在component目录中新建了datetime组件目录,如图:
image.png
3、代码详细
datetime.html代码
<ion-multi-picker item-content [multiPickerColumns]="dataColumns" (ionOpen)="listenDo()" (ionChange)="ionChange($event)" [separator]="'/'" [(ngModel)]="option.Value" cancelText="取消" doneText="确定"></ion-multi-picker>
datetime.ts代码
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { PubFunction } from '../../pubfunction';
import 'rxjs/Rx';
declare var window: {
MutationObserver: any,
WebKitMutationObserver: any,
MozMutationObserver: any
}
@Component({
selector: 'datetime',
templateUrl: 'datetime.html'
})
export class DatetimeComponent {
public dataColumns: Array<any> = [];
private _def = new importModel(2015, 2039);//min:1999 max:2039
public option: importModel;
@Input("Options")
importValue: any; //获取从父组件传递过来的数据
@Output('Export')
EmitData: EventEmitter<string> = new EventEmitter();
constructor() {
this.init();
}
listenDo() {
setTimeout(() => {
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
var ColumnYear = document.getElementsByClassName("picker-opts")[0];
var ColumnMonth = document.getElementsByClassName("picker-opts")[1];
var observerY = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type == "attributes") {
this.setDateItem();
}
});
});
observerY.observe(ColumnYear, {
// childList:子节点的变动(指新增,删除或者更改)。
// attributes:属性的变动。
// characterData:节点内容或节点文本的变动。
// subtree:布尔值,表示是否将该观察器应用于该节点的所有后代节点。
// attributeOldValue:布尔值,表示观察attributes变动时,是否需要记录变动前的属性值。
// characterDataOldValue:布尔值,表示观察characterData变动时,是否需要记录变动前的值。
// attributeFilter:数组,表示需要观察的特定属性(比如['class','src'])。
attributeFilter: ['moved'],
// subtree: true
});
var observerM = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type == "attributes") {
this.setDateItem();
}
});
});
observerM.observe(ColumnMonth, {
attributeFilter: ['moved'],
// subtree: true
});
this.setDateItem();
}, 500);
}
Bu10(num: Number) {
return num < 10 ? '0' + num : num.toString();
}
setDateItem() {
let y = document.getElementsByClassName("picker-opt picker-opt-selected")[0];
let m = document.getElementsByClassName("picker-opt picker-opt-selected")[1];
let d = document.getElementsByClassName("picker-opt picker-opt-selected")[2];
let ds = document.getElementsByClassName("picker-opts")[2].children;
let year = Number(y.innerHTML);
let month = Number(m.innerHTML);
let date = Number(d.innerHTML);
let currentDate = new Date(year.toString() + "-" + month.toString());
currentDate.setMonth(currentDate.getMonth() + 1);
currentDate.setDate(0);
//29以上日期控制显示/隐藏
for (let i = 29; i <= 31; i++) {
if (i <= currentDate.getDate()) {
ds[i - 1].classList.remove("picker-opt-disabled");
} else if (i > currentDate.getDate()) {
ds[i - 1].classList.add("picker-opt-disabled");
}
}
//日期大于上限,重置最大日期
if (date > currentDate.getDate()) {
document.getElementsByClassName("picker-opt picker-opt-selected")[2].classList.remove("picker-opt-selected");
ds[currentDate.getDate() - 1].classList.add("picker-opt-selected");
}
//刷新3D显示效果
date = Number(document.getElementsByClassName("picker-opt picker-opt-selected")[2].innerHTML);
for (let i = 1; i <= 31; i++) {
(ds[i - 1] as HTMLElement).style.transform = " translate3d(-9999px, 0px, 0px)";//清空全部
}
let nestestItem = (y.previousElementSibling || y.nextElementSibling) as HTMLElement;
let rotateX = Number(/rotateX\((.+)deg\)/.exec(nestestItem.style.transform)[1]);
for (let i = date - 3; i <= date + 3; i++) {
if (i >= 1 && i <= 31) {
(ds[i - 1] as HTMLElement).style.transform = "rotateX(" + rotateX * (date - i) + "deg) translate3d(0px, 0px, 90px)";
}
}
}
init() {
this.option = Object.assign(this._def, this.importValue);
//年
this.dataColumns.push({
name: "year",
columnWidth: "18%",
options: this.generateYear()
});
//月
this.dataColumns.push({
name: "month",
// parentCol: 'year',
columnWidth: "10%",
options: this.generateMonth()
});
//日
this.dataColumns.push({
name: "day",
// parentCol: 'month',
columnWidth: "10%",
options: this.generateDay()
});
//时间
this.dataColumns.push({
name: "time",
columnWidth: "25%",
options: this.generateTime()
});
setTimeout(() => {
this.EmitData.emit(this.option.Value);
}, 300);
}
ionChange(data) {
let d = document.getElementsByClassName("picker-opt picker-opt-selected")[2];
let values = this.option.Value.split("/");
values[2] = d.innerHTML;
this.option.Value = values.join("/");
this.EmitData.emit(this.option.Value);
}
//生成年
generateYear(): Array<optionModel> {
let items: Array<optionModel> = [];
for (let i = this.option.minYear; i <= this.option.maxYear; i++) {
let item = new optionModel(i.toString(), i.toString());
items.push(item);
}
return items;
}
//生成月
generateMonth(): Array<optionModel> {
let items: Array<optionModel> = [];
for (let i = 1; i <= 12; i++) {
let item = new optionModel((i < 10 ? '0' + i : i).toString(), (i < 10 ? '0' + i : i).toString());
items.push(item);
}
return items;
}
//生成日
generateDay(): Array<optionModel> {
let items: Array<optionModel> = [];
for (let i = 1; i <= 31; i++) {
let item = new optionModel((i < 10 ? '0' + i : i).toString(), (i < 10 ? '0' + i : i).toString());
items.push(item);
}
return items;
}
//生成时间
generateTime(): Array<optionModel> {
let items: Array<optionModel> = [];
for (let i = 0; i <= 23; i++) {
for (let k = 0; k <= 3; k++) {
let hour = (i < 10 ? '0' + i : i).toString();
let minite = (15 * k < 10 ? '0' + 15 * k : 15 * k).toString();
let item = new optionModel(hour + ":" + minite, hour + ":" + minite);
items.push(item);
}
}
return items;
}
}
class importModel {
constructor(
public minYear: number = 1999,
public maxYear: number = 2039,
public Value: string = PubFunction.Format(new Date(), 'yyyy/MM/dd/hh') + ":00"
) { }
}
class optionModel {
constructor(
public text: string = "",
public value: string = "",
public disabled: Boolean = false,
public parentVal: string = ""
) {
}
}
1、滑动停止后,修改当前列上moved属性值
文件路径:node_modules\ionic-angular\components\picker\picker-column.js
修改pointerStart、pointerEnd、decelerate方法中代码,
//pointerStart
PickerColumnCmp.prototype.pointerStart = function (ev) {
......
for (var i = 0; i < options.length; i++) {
if (!options[i].disabled) {
minY = Math.min(minY, i);
maxY = Math.max(maxY, i);
}
}
var currentText = document.getElementsByClassName("picker-opt-selected")[2].innerHTML;
if (this.col.name == "day" && this.col.selectedIndex != Number(currentText) - 1) {
// this.lastIndex = this.col.prevSelected = this.col.selectedIndex = Number(currentText) - 1
this.y = (Number(currentText) - 1) * this.optHeight * -1;
}
// console.log(Number(currentText) - 1, this.col.selectedIndex, this.col.prevSelected, this.lastIndex, this.lastTempIndex);
this.minY = (minY * this.optHeight * -1);
this.maxY = ((maxY - this.colEle.nativeElement.getElementsByClassName("picker-opt-disabled").length) * this.optHeight * -1);
// console.log("有效:" + (maxY - this.colEle.nativeElement.getElementsByClassName("picker-opt-disabled").length).toString());
// this.maxY = (maxY * this.optHeight * -1);
return true;
};
//pointerEnd
PickerColumnCmp.prototype.pointerEnd = function (ev) {
......
if (this.bounceFrom > 0) {
// bounce back up
this.update(this.minY, 100, true, true);
//顶部拖拽结束,标记时间戳,用于监听dom attribute变化。
this.colEle.nativeElement.setAttribute("moved", +new Date());
return;
}
else if (this.bounceFrom < 0) {
// bounce back down
this.update(this.maxY, 100, true, true);
//底部拖拽结束,标记时间戳,用于监听dom attribute变化。
this.colEle.nativeElement.setAttribute("moved", +new Date());
return;
}
......
};
//decelerate
PickerColumnCmp.prototype.decelerate = function () {
......
var notLockedIn = (y % this.optHeight !== 0 || Math.abs(this.velocity) > 1);
this.update(y, 0, true, !notLockedIn);
if (notLockedIn) {
// isn't locked in yet, keep decelerating until it is
this.rafId = this._plt.raf(this.decelerateFunc);
} else {
//滑动结束,标记时间戳,用于监听dom attribute变化。
this.colEle.nativeElement.setAttribute("moved", +new Date());
}
......
};
修改maxY的计算方式,每次点击滑动时,重新计算有多少有效的button。
maxY:有效button个数
this.maxY:有效button垂直方向坐标
this.optHeight:每个button的高度
var currentText = document...
注意务必使用var定义变量,如使用let定义,在android7+中打包后,会卡死在启动页面,一直处于加载状态。
2、添加ionOpen事件
multi-picker.js中添加ionOpen事件,打开时触发,需要修改3处(aot编译后真机运行时,ionOpen事件不触发,原因不详。暂且用click代替)
image.png文件路径:node_modules\ion-multi-picker\dist\components\multi-picker\multi-picker.js
image.png
image.png
3、禁止点击背景后关闭
MultiPicker.prototype.open = function () {
var _this = this;
if (this._disabled) {
return;
}
var pickerOptions = { enableBackdropDismiss: false};//add
var picker = this._pickerCtrl.create(pickerOptions);
picker.setCssClass("exx-datetime");//add,添加外围的css类
var cancel = { text: this.cancelText, role: 'multi-picker-cancel', handler: function () { _this.ionCancel.emit(null); } };
4、css样式exx-datetime代码
//选择日期组件
.exx-datetime{}
.exx-datetime .picker-wrapper{ height: 300px; padding: 20px 0; bottom: 50%; margin-bottom:-150px;}
.exx-datetime .picker-wrapper .picker-toolbar{ position: absolute;bottom: 20px; border-bottom: none;}
.exx-datetime .picker-wrapper .picker-toolbar .picker-toolbar-button{text-align: center}
.exx-datetime .picker-wrapper .picker-toolbar .picker-toolbar-button .picker-button{background-color: #387ef7; color: #fff; min-width: 50%}
.exx-datetime .picker-wrapper .picker-toolbar .picker-toolbar-button.picker-toolbar-multi-picker-cancel .picker-button{ background-color: #f8f8f8; color: #000; border:1px solid #eee;}
.exx-datetime .picker-wrapper .picker-columns{ background-color: #e2e6e9;}
.exx-datetime .picker-wrapper .picker-columns .picker-above-highlight{}
.exx-datetime .picker-wrapper .picker-columns .picker-below-highlight{}
5、在弹出页面头部添加蓝色标题栏
node_modules\ionic-angular\components\picker\picker-component.js
代码中搜索ion-backdrop,在后面插入如下代码:
<div class=\"picker-header\"><span>时间选择</span></div>
在aot编译后,这个标题不生效。还需要把picker-component.metadata.json中对应的html也加上这一段。
6、修改PickerSlideIn、PickerSlideOut动画效果
node_modules\ionic-angular\components\picker\picker-transitions.js
PickerSlideIn
var ele = this.enteringView.pageRef().nativeElement;
var backdrop = new Animation(this.plt, ele.querySelector('ion-backdrop'));
var wrapper = new Animation(this.plt, ele.querySelector('.picker-wrapper'));
backdrop.fromTo('opacity', 0.01, 1);
wrapper.fromTo('translateY', '100%', '0%');
this.easing('cubic-bezier(.36,.66,.04,1)').duration(400).add(backdrop).add(wrapper);
PickerSlideOut
var ele = this.leavingView.pageRef().nativeElement;
var pickerFrame = new Animation(this.plt, ele);
pickerFrame.fromTo('opacity', 1, 0);
this.easing('cubic-bezier(.36,.66,.04,1)').duration(300).add(pickerFrame);
调用
<datetime item-content (Export)="qjtime=$event"></datetime>