ionic 不支持级联选择 ?

1,171 阅读1分钟

这是我参与更文挑战的第 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 组件支持的事件只有如下四种:

image-20210530165656461

若要实现级联操作,开发者需要监听到每一列的取值变化,但是从 ionic 暴露的 API 来看,无法直接获取。但是通过查阅 ionic 的源码我们可以发现在ion-picker-column中存在ionPickerColChange事件。

文件位置:core/src/components/picker-column/picker-column.tsx

ionPickerColChange事件

该事件返回的数据结构为:

事件对象

借助这个事件,我们可以尝试自己实现级联选择。

布局

<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,但是采用如上方式修改后可以解决问题,效果如下:

解决文字重叠问题

源码

github.com/onlyloveyd/…