这是我参与更文挑战的第 1 天,活动详情查看: 更文挑战
前言
笔者日常开发除了使用 Android 原生、Flutter 外,偶尔还会使用 ionic + cordova 的组合来完成部分跨平台应用开发。最近在使用过程中发现一个问题,ionic 组件 ion-datetime、ion-picker 不支持级联操作
,但是可以通过一些取巧的方式间接实现,一起看看吧。
无级联常规使用
布局
<ion-header translucent>
<ion-toolbar>
<ion-title>Picker</ion-title>
</ion-toolbar>
</ion-header>
<ion-content fullscreen class="ion-padding">
<ion-button expand="block" (click)="openPicker()">Show Single Column Picker</ion-button>
<ion-button expand="block" (click)="openPicker(2, 5, multiColumnOptions)">Show Multi Column Picker</ion-button>
</ion-content>
单列、多列
import {Component} from '@angular/core';
import {PickerController, ToastController} from "@ionic/angular";
@Component({
selector: 'app-home',
templateUrl: 'home.page.html',
styleUrls: ['home.page.scss'],
})
export class HomePage {
defaultColumnOptions = [
[
'Dog',
'Cat',
'Bird',
'Lizard',
'Chinchilla'
]
];
multiColumnOptions = [
[
'Minified',
'Responsive',
'Full Stack',
'Mobile First',
'Serverless'
],
[
'Tomato',
'Avocado',
'Onion',
'Potato',
'Artichoke'
]
];
constructor(private pickerController: PickerController,
private toastController: ToastController) {
}
async openPicker(numColumns = 1, numOptions = 5, columnOptions = this.defaultColumnOptions) {
const picker = await this.pickerController.create({
columns: this.getColumns(numColumns, numOptions, columnOptions),
mode: 'ios',
buttons: [
{
text: 'Cancel',
role: 'cancel'
},
{
text: 'Confirm',
handler: (value) => {
console.log(value)
if (numColumns == 1) {
this.presentToast(value.col0.text)
} else {
this.presentToast(value.col0.text + ',' + value.col1.text)
}
}
}
]
});
await picker.present();
}
async presentToast(message: string) {
const toast = await this.toastController.create({
message: message,
duration: 2000
});
await toast.present();
}
getColumns(numColumns, numOptions, columnOptions) {
let columns = [];
for (let i = 0; i < numColumns; i++) {
columns.push({
name: `col${i}`,
options: this.getColumnOptions(i, numOptions, columnOptions)
});
}
return columns;
}
getColumnOptions(columnIndex, numOptions, columnOptions) {
let options = [];
for (let i = 0; i < numOptions; i++) {
options.push({
text: columnOptions[columnIndex][i % numOptions],
value: i
})
}
return options;
}
}
效果
尝试级联使用
ion-picker
组件支持的事件只有如下四种:
若要实现级联操作,开发者需要监听到每一列的取值变化,但是从 ionic 暴露的 API 来看,无法直接获取。但是通过查阅 ionic 的源码我们可以发现在ion-picker-column
中存在ionPickerColChange
事件。
文件位置:core/src/components/picker-column/picker-column.tsx
该事件返回的数据结构为:
借助这个事件,我们可以尝试自己实现级联选择。
布局
<ion-button expand="block" (click)="openCascadePicker()">Show Cascade Column Picker</ion-button>
级联
// 级联数据,可以根据自己的需要调整
cascadeColumnOptions = [
[
'贵州省',
'湖北省',
], [
[
'贵阳市',
'遵义市',
'凯里市',
'六盘水市',
],
[
'武汉市',
'黄石市',
'十堰市',
'宜昌市'
]
]
];
// 级联数据单列选中值
cascadeColumnValue = [0, 0]
async openCascadePicker(numColumns = 2, columnOptions = this.cascadeColumnOptions) {
const pickerOptions = {
columns: this.getCascadeColumns(2, columnOptions),
mode: 'ios',
buttons: [
{
text: 'Cancel',
role: 'cancel'
},
{
text: 'Confirm',
handler: (value) => {
console.log(value)
if (numColumns == 1) {
this.presentToast(value.col0.text)
} else {
this.presentToast(value.col0.text + ',' + value.col1.text)
}
}
}
]
}
// @ts-ignore
this.cascadePicker = await this.pickerController.create(pickerOptions);
await this.cascadePicker.present().then(() => {
setTimeout(() => {
try {
// 通过元素选择器找到 Picker 的列
const pickerCols = document.querySelectorAll('ion-picker-column');
const provinceCol = pickerCols[0];
const cityCol = pickerCols[1];
// 监听 列值 变化事件
provinceCol.addEventListener('ionPickerColChange', (event: CustomEvent) => {
console.log(event)
this.cascadeColumnValue[0] = event.detail.selectedIndex
this.cascadePicker.columns = this.getCascadeColumns(2, this.cascadeColumnOptions)
})
cityCol.addEventListener('ionPickerColChange', (event: CustomEvent) => {
this.cascadeColumnValue[1] = event.detail.selectedIndex
console.log(event)
})
} catch (e) {
console.log(e);
}
}, 400);
});
}
效果
已知问题
整体效果看上去还 OK ,但是这里存在一个 BUG ,官方暂时没有解决。示例中,”贵州省“和”湖北省“对应的二级选项数目均为四个,所以使用正常。但是当二级选项数据不一致时,会出现选项文字重叠的问题,虽然说稍稍滚动就可恢复正常,但是用户体验依然不失那么舒适。
Github Issues:github.com/ionic-team/…
当然,针对这个问题,虽然官方没有给解决办法,但是 Github 大神在开源项目内提出了一种解决办法:github.com/zwlccc/ioni…。
修改方式: github.com/zwlccc/ioni…
- 打开
ion-datetime_3.entry.js
文件,目录:node_modules/@ionic/core/dist/esm/ion-datetime_3.entry.js
- 修改
update(y, duration, saveY)
方法
update(y, duration, saveY) {
if (!this.optsEl) {
return;
}
// ensure we've got a good round number :)
let translateY = 0;
let translateZ = 0;
const { col, rotateFactor } = this;
const selectedIndex = col.selectedIndex = this.indexForY(-y);
const durationStr = (duration === 0) ? '' : duration + 'ms';
const scaleStr = `scale(${this.scaleFactor})`;
const children = this.optsEl.children;
const children_length = this.optsEl.children.length;
const options_length = col.options.length;
const length = children_length < options_length ? options_length : children_length;
for (let i = 0; i < length; i++) {
const button = children[i];
const opt = col.options[i];
const optOffset = (i * this.optHeight) + y;
let transform = '';
if (rotateFactor !== 0) {
const rotateX = optOffset * rotateFactor;
if (Math.abs(rotateX) <= 90) {
translateY = 0;
translateZ = 90;
transform = `rotateX(${rotateX}deg) `;
}
else {
translateY = -9999;
}
}
else {
translateZ = 0;
translateY = optOffset;
}
const selected = selectedIndex === i;
transform += `translate3d(0px,${translateY}px,${translateZ}px) `;
if (this.scaleFactor !== 1 && !selected) {
transform += scaleStr;
}
// Update transition duration
if (this.noAnimate) {
if (opt) {
opt.duration = 0;
}
if (button) {
button.style.transitionDuration = '';
}
}
else if (opt) {
if (duration !== opt.duration) {
opt.duration = duration;
if (button) {
button.style.transitionDuration = durationStr;
}
}
}
// Update transform
if (opt) {
if (transform !== opt.transform) {
opt.transform = transform;
if (button) {
button.style.transform = transform;
}
}
}
// Update selected item
if (opt) {
if (selected !== opt.selected) {
opt.selected = selected;
if (selected && button) {
button.classList.add(PICKER_OPT_SELECTED);
}
else if (button) {
button.classList.remove(PICKER_OPT_SELECTED);
}
}
}
}
this.col.prevSelected = selectedIndex;
if (saveY) {
this.y = y;
}
if (this.lastIndex !== selectedIndex) {
// have not set a last index yet
hapticSelectionChanged();
this.lastIndex = selectedIndex;
}
}
- 修改
render()
方法
render() {
const col = this.col;
const Button = 'button';
const mode = getIonMode(this);
return (h(Host, {
class: {
[mode]: true,
'picker-col': true,
'picker-opts-left': this.col.align === 'left',
'picker-opts-right': this.col.align === 'right'
}, style: {
'max-width': this.col.columnWidth
}
}, col.prefix && (h("div", {
class: "picker-prefix",
style: {width: col.prefixWidth}
}, col.prefix)), h("div", {
class: "picker-opts",
style: {maxWidth: col.optionsWidth},
ref: el => this.optsEl = el
}, col.options.map((o, index) => h(Button, {
type: "button",
class: {'picker-opt': true, 'picker-opt-disabled': !!o.disabled, 'picker-opt-selected': o.selected},
style: {
transform: o.transform ? o.transform : 'translate3d(0px, -9999px, 90px)',
'transition-duration': o.duration ? o.duration : TRANSITION_DURATION + 'ms'
},
"opt-index": index
}, o.text))), col.suffix && (h("div", {class: "picker-suffix", style: {width: col.suffixWidth}}, col.suffix))));
}
虽然笔者的开发环境为 ionic V6.12.0
,但是采用如上方式修改后可以解决问题,效果如下: