鸿蒙Next - 手把手教你实现一个属于自己的日期选择器最近刚好遇到了还挺不错的日期选择器,经过不断打磨,最终效果还不错
前言
hello 大家好 我是无言。最近在开发一个鸿蒙项目,刚好遇到了一个自定义比较强,样式还挺不错的日期选择器需求,经过不断调试打磨,最终效果还不错,所以打算分享出来,给大家一些创意灵感。
主要实现的功能
- 选中日期回传给父组件。
- 根据父组件默认选中时间打开子组件弹窗回显日期。
- 动态加载日期边滚动边加载后续数据提升性能。
实现效果先一睹为快。
实现过程
一、准备工作
二、先把整个弹窗逻辑实现
- 创建弹窗子组件文件
ets/components/TimeDateDialog.ets
下面代码建议着重看下添加注释的地方,是容易踩坑的地方,所以我都特别标注出来了。
//弹窗具体内容
@CustomDialog
struct CustomDialogDate {
controller?: CustomDialogController
@State dateList:string[]=['2024-01-11','2024-01-12','2024-01-13','2024-01-14','2024-01-15'] //只存储天
@State hourList:string[]=['01','02','03','04','05'] //只存储小时
@State minList:string[]=['05','10','15','20','25'] //只存分钟
cancel: () => void = () => {
}
confirm: (data:string) => void = () => {
}
aboutToAppear(): void {
}
build() {
Column() {
Row(){
Text('显示时间').fontSize(18)
}.padding({ bottom:25 }).width('100%').justifyContent(FlexAlign.Center)
// 滚动选中区域
Flex(){
// 时间
Flex(){
// 时间
TextPicker({ range: this.dateList})
.width('50%')
.canLoop(false)//不循环
.divider({
strokeWidth: 1,
startMargin: 0,
endMargin: 0,
color:'#ececec'
})
.textStyle({color:'#777777', font: {size: 15, weight: 400}})
.selectedTextStyle({color: '#ffd96868', font: {size: 15, weight: 600}})
TextPicker({ range: this.hourList }).width('25%')
.divider({
strokeWidth: 1,
startMargin: 0,
endMargin: 0,
color:'#ececec'
})
.textStyle({color:'#777777', font: {size: 15, weight: 400}})
.selectedTextStyle({color: '#ffd96868', font: {size: 15, weight: 600}})
//分钟
TextPicker({ range: this.minList}).width('25%')
.divider({
strokeWidth: 1,
startMargin: 0,
endMargin: 0,
color:'#ececec'
})
.textStyle({color:'#777777', font: {size:15, weight: 400}})
.selectedTextStyle({color: '#ffd96868', font: {size: 15, weight: 600}})
}
}
// 按钮
Row(){
Button('取消', { type: ButtonType.Normal, stateEffect: true })
.margin({right:6})
.borderRadius(6)
.backgroundColor('#fff')
.fontColor('#ffd96868')
.borderWidth(1)
.borderColor('#ffd96868')
.fontSize(15)
.width(156)
.onClick(()=>{
this.controller?.close()
})
Button('确定', { type: ButtonType.Normal, stateEffect: true })
.margin({left:6})
.borderRadius(6)
.fontSize(15)
.backgroundColor('#ffd96868')
.width(156)
.onClick(()=>{
this.confirm('回传信息给父组件')
this.controller?.close()
})
}.justifyContent(FlexAlign.Center)
.padding({top:15})
.margin({top:25})
}.width('100%')
.padding(16)
.backgroundColor('#fff')
.borderRadius({topLeft:15,topRight:15})
}
}
//定义controller对象 主要作用父子通信 父组件调用子组件方法 唤醒弹窗
export class DialogDateController {
ShowDialog = (value?: string) => {
}
}
//弹窗控制逻辑
@Component
export struct TimeDateDialog { //修改命名 注意前面加了 export 需要暴露组件
private controller: DialogDateController = new DialogDateController();
CustomDialogController: CustomDialogController | null =null ;
@Prop defaultTime: string;
cancel?: () => void
confirm?: (data:string) => void = () => {
}
// 打开显示弹窗
private async ShowDialog(value?: string){
this.CustomDialogController?.open()
}
aboutToAppear(): void {
if (this.controller) {
//给controller对应的方法赋值
this.controller.ShowDialog = this.ShowDialog.bind(this); //这里需要注意 用了 bind改变 this 指向
}
this.CustomDialogController= new CustomDialogController({
builder: CustomDialogDate({
cancel: this.cancel,
confirm: this.confirm,
}),
autoCancel: true,
onWillDismiss:(dismissDialogAction: DismissDialogAction)=> {
if (dismissDialogAction.reason == DismissReason.PRESS_BACK) {
dismissDialogAction.dismiss()
}
if (dismissDialogAction.reason == DismissReason.TOUCH_OUTSIDE) {
dismissDialogAction.dismiss()
}
},
alignment: DialogAlignment.Bottom,
offset: { dx: 0, dy: 0},
customStyle: true,//这里为true 样式才可以完全自定义
})
}
aboutToDisappear() {
this.CustomDialogController = null // 将dialogController置空
}
build() { //因为用的 自定义弹窗功能,所以这下面可以为空
}
}
- 在父组件
ets/page/Index.ets
文件中引入子组件弹窗并唤醒弹窗,确定后回传信息给父组件。
import {TimeDateDialog,DialogDateController} from "../components/TimeDateDialog"
@Entry
@Component
struct Index {
@State DateDialogRef: DialogDateController = new DialogDateController();//用于调用子组件的方法 唤醒弹窗
build() {
RelativeContainer() {
Column(){
Button('打开弹窗')
.onClick(()=>{
this.DateDialogRef.ShowDialog()
})
}.padding(20)
TimeDateDialog({
controller:this.DateDialogRef,
confirm:(data)=>{
console.log('data',data)
}
})
}
.height('100%')
.width('100%')
}
}
预览可以看到父组件能够成功唤醒弹窗,并且子组件点击确定后,成功回传信息到了父组件。
三、动态生成日期
这里我们先默认动态生成当前日期前后20天的数据,核心方法用到了 dayjs
的 add
增加方法 和 subtract
减少方法和数组的 push
方法 和 unshift
方法。
修改TimeDateDialog.ets
文件 下面只列出了修改部分其余的省略保持一致。
import dayjs from "dayjs"
import 'dayjs/locale/zh-cn'; // 引入中文本地化
import localeData from 'dayjs/plugin/localeData';
dayjs.extend(localeData);
dayjs.locale('zh-cn'); // 使用中文
@CustomDialog
struct CustomDialogDate {
controller?: CustomDialogController
dateMap:Map<string,string>= new Map();//键值对存储日期信息
@State dateList:string[]=[] //只存储天
@State hourList:string[]=[] //只存储小时
@State minList:string[]=[] //只存分钟
@State dayStr: string=''; //存储日期选中的信息
cancel: () => void = () => {
}
confirm: (data:string) => void = () => {
}
aboutToAppear(): void {
this.initDate(); //初始化日期
this.initHour(); //初始化小时
this.initMin(); //初始化分钟
}
//初始化日期
initDate(){
// 获取后续10天的时间
this.changeDateList('add')
this.changeDateList('subtract')
}
// 处理数据 type: add 新增 subtract减少
changeDateList(type:string,time?:string){
for (let i = 0; i < 20; i++) {
// 这里需要 注意 subtract 减少 是i+1否则数据有一天是重复的
const futureDay =type=='add'? dayjs(time).add(i, 'day'):dayjs(time).subtract(i+1, 'day');
const nowDay = dayjs().format("YYYY-MM-DD") //今天
const day = futureDay.format("YYYY-MM-DD")
let title = futureDay.format('M月D日 dddd')
if (nowDay == day) {
title = '今天'
this.dayStr = title
}
this.dateMap.set(title, day)
if(type=='add'){ //添加以后的时间
this.dateList.push(title)
}else{ //添加 以前的时间
this.dateList.unshift(title)
}
}
}
// 时间
initMin(){
for (let i = 0; i < 12; i++) {
const str = this.padZero(i*5)
this.minList.push(str)
}
}
// 小时
initHour(){
for (let i = 0; i < 24; i++) {
const str = this.padZero(i)
this.hourList.push(str)
}
}
//补充 0
padZero(num:number) {
return num.toString().padStart(2, '0');
}
...
重新允许打开弹窗可以看到日期已经是动态生成 而且已经汉化 并且有了星期信息。
四、滚动加载更多数据
核心逻辑当往下滚动到倒数第10条数据的时候利用push
从日期数组尾部添加20条以后的数据,当往上滚动到第10条数据的时候 利用 unshift
从日期数组头部
添加 20条以前的数据,这里需要注意从头部添加完数据之后,需要 修改日期选中索引selected
值+20。因为现在默认从今日开始前后20条数据所以this.DateSelect=20
默认选中当天。
修改TimeDateDialog.ets
文件 下面只列出了修改部分其余的省略保持一致。
...
//初始化日期
initDate(){
// 获取后续10天的时间
this.changeDateList('add')
this.changeDateList('subtract')
this.DateSelect=20//默认选中当天
}
...
getTopName(){
const date =this.dateMap.get(this.dayStr)
return dayjs(date).format('YYYY年M月D日 dddd')
}
build() {
Column() {
Row(){
Text(this.getTopName()).fontSize(18)
}.padding({ bottom:25 }).width('100%').justifyContent(FlexAlign.Center)
// 滚动选中区域
Flex(){
// 时间
TextPicker({ range: this.dateList, selected: this.DateSelect})
.width('50%')
.canLoop(false)//不循环
.divider({
strokeWidth: 1,
startMargin: 0,
endMargin: 0,
color:'#ececec'
})
.textStyle({color:'#777777', font: {size: 15, weight: 400}})
.selectedTextStyle({color: '#ffd96868', font: {size: 15, weight: 600}})
.onChange((value: string | string[], index: number | number[]) => {
this.dayStr=value as string
if(index==(this.dateList.length-10)){ //当滚动到倒数第10个时开始加载后续20天的数据
this.DateSelect=index as number
const dayStr = this.dateList[this.dateList.length-1]
const day= this.dateMap.get(dayStr) as string
this.changeDateList('add',day) //添加以后的时间
}
if(index==10){ //当滚动第10个时开始加载前20天的数据
const dayStr = this.dateList[0]
const day= this.dateMap.get(dayStr) as string
this.changeDateList('subtract',day) //添加以前的时间
this.DateSelect=index as number +20
}
})
//小时
TextPicker({ range: this.hourList, selected: this.HourSelect }).width('25%')
.divider({
strokeWidth: 1,
startMargin: 0,
endMargin: 0,
color:'#ececec'
})
.textStyle({color:'#777777', font: {size: 15, weight: 400}})
.selectedTextStyle({color: '#ffd96868', font: {size: 15, weight: 600}})
.onChange((value: string | string[], index: number | number[]) => {
this.HourSelect=index as number
})
//分钟
TextPicker({ range: this.minList, selected: this.minSelect }).width('25%')
.divider({
strokeWidth: 1,
startMargin: 0,
endMargin: 0,
color:'#ececec'
})
.textStyle({color:'#777777', font: {size:15, weight: 400}})
.selectedTextStyle({color: '#ffd96868', font: {size: 15, weight: 600}})
.onChange((value: string | string[], index: number | number[]) => {
this.minSelect=index as number
})
}
// 按钮
Row(){
Button('取消', { type: ButtonType.Normal, stateEffect: true })
.margin({right:6})
.borderRadius(6)
.backgroundColor('#fff')
.fontColor('#ffd96868')
.borderWidth(1)
.borderColor('#ffd96868')
.fontSize(15)
.width(156)
.onClick(()=>{
this.controller?.close()
})
Button('确定', { type: ButtonType.Normal, stateEffect: true })
.margin({left:6})
.borderRadius(6)
.fontSize(15)
.backgroundColor('#ffd96868')
.width(156)
.onClick(()=>{
this.confirm('回传信息')
this.controller?.close()
})
}.justifyContent(FlexAlign.Center)
.padding({top:15})
.margin({top:25})
}.width('100%')
.padding(16)
.backgroundColor('#fff')
.borderRadius({topLeft:15,topRight:15})
}
...
看看运行效果,可以看到日期可以无限加载滚动了。
五、回传信息给父元素
点击确定按钮
提交信息回传给父元素
...
// 提交数据
onSubmit(){
const day = this.dateMap.get(this.dayStr)
const hour = this.hourList[this.HourSelect]
const min = this.minList[this.minSelect]
const dateTime = `${day} ${hour}:${min}:00`
this.confirm(dateTime) //回传日期到父组件
this.controller?.close()
}
...
六、回显默认值
最后ets/components/TimeDateDialog.ets
完整代码如下
import dayjs from "dayjs"
import 'dayjs/locale/zh-cn'; // 引入中文本地化
import localeData from 'dayjs/plugin/localeData';
dayjs.extend(localeData);
dayjs.locale('zh-cn'); // 使用中文
@CustomDialog
struct CustomDialogDate {
controller?: CustomDialogController
dateMap:Map<string,string>= new Map();//键值对存储日期信息
@State dateList:string[]=[] //只存储天
@State hourList:string[]=[] //只存储小时
@State minList:string[]=[] //只存分钟
@Prop defaultTime: string; //默认值
@State dayStr: string=''; //存储日期选中的信息
@State DateSelect: number=0;
@State HourSelect: number=0;
@State minSelect: number=0;
cancel: () => void = () => {
}
confirm: (data:string) => void = () => {
}
// 提交数据
onSubmit(){
const day = this.dateMap.get(this.dayStr)
const hour = this.hourList[this.HourSelect]
const min = this.minList[this.minSelect]
const dateTime = `${day} ${hour}:${min}:00`
this.confirm(dateTime) //回传日期到父组件
this.controller?.close()
}
aboutToAppear(): void {
this.initDate(); //初始化日期
this.initHour(); //初始化小时
this.initMin(); //初始化分钟
this.initDefaultTime()
}
// 初始化默认选中
initDefaultTime(){
if(!this.defaultTime) {
this.DateSelect=20//如果没有默认时间可以默认选中当天
return
}
const nowDay = dayjs().format("YYYY-MM-DD") //今天
const day = dayjs(this.defaultTime).format("YYYY-MM-DD") //
const hour = dayjs(this.defaultTime).format("HH") //小时
const min = dayjs(this.defaultTime).format("mm") //分钟
let dayStr = dayjs(this.defaultTime).format("M月D日 dddd")
if(nowDay==day){
dayStr='今天'
}
const dayIndex =this.dateList.indexOf(dayStr)
if(dayIndex>-1 ){
this.dayStr=dayStr
this.DateSelect=dayIndex
}
this.HourSelect = this.hourList.indexOf(hour) //选中 小时
this.minSelect = this.minList.indexOf(min) //选中 小时
}
//初始化日期
initDate(){
// 获取后续10天的时间
this.changeDateList('add',this.defaultTime)
this.changeDateList('subtract',this.defaultTime)
}
// 处理数据 type: add 新增 subtract减少
changeDateList(type:string,time?:string){
for (let i = 0; i < 20; i++) {
// 这里需要 注意 subtract 减少 是i+1否则数据有一天是重复的
const futureDay =type=='add'? dayjs(time).add(i, 'day'):dayjs(time).subtract(i+1, 'day');
const nowDay = dayjs().format("YYYY-MM-DD") //今天
const day = futureDay.format("YYYY-MM-DD")
let title = futureDay.format('M月D日 dddd')
if (nowDay == day) {
title = '今天'
this.dayStr = title
}
this.dateMap.set(title, day)
if(type=='add'){ //添加以后的时间
this.dateList.push(title)
}else{ //添加 以前的时间
this.dateList.unshift(title)
}
}
}
// 时间
initMin(){
for (let i = 0; i < 12; i++) {
const str = this.padZero(i*5)
this.minList.push(str)
}
}
// 小时
initHour(){
for (let i = 0; i < 24; i++) {
const str = this.padZero(i)
this.hourList.push(str)
}
}
//补充 0
padZero(num:number) {
return num.toString().padStart(2, '0');
}
getTopName(){
const date =this.dateMap.get(this.dayStr)
return dayjs(date).format('YYYY年M月D日 dddd')
}
build() {
Column() {
Row(){
Text(this.getTopName()).fontSize(18)
}.padding({ bottom:25 }).width('100%').justifyContent(FlexAlign.Center)
// 滚动选中区域
Flex(){
// 时间
TextPicker({ range: this.dateList, selected: this.DateSelect})
.width('50%')
.canLoop(false)//不循环
.divider({
strokeWidth: 1,
startMargin: 0,
endMargin: 0,
color:'#ececec'
})
.textStyle({color:'#777777', font: {size: 15, weight: 400}})
.selectedTextStyle({color: '#ffd96868', font: {size: 15, weight: 600}})
.onChange((value: string | string[], index: number | number[]) => {
this.dayStr=value as string
if(index==(this.dateList.length-10)){ //当滚动到倒数第10个时开始加载后续20天的数据
this.DateSelect=index as number
const dayStr = this.dateList[this.dateList.length-1]
const day= this.dateMap.get(dayStr) as string
this.changeDateList('add',day) //添加以后的时间
}
if(index==10){ //当滚动第10个时开始加载前20天的数据
const dayStr = this.dateList[0]
const day= this.dateMap.get(dayStr) as string
this.changeDateList('subtract',day) //添加以前的时间
this.DateSelect=index as number +20
}
})
//小时
TextPicker({ range: this.hourList, selected: this.HourSelect }).width('25%')
.divider({
strokeWidth: 1,
startMargin: 0,
endMargin: 0,
color:'#ececec'
})
.textStyle({color:'#777777', font: {size: 15, weight: 400}})
.selectedTextStyle({color: '#ffd96868', font: {size: 15, weight: 600}})
.onChange((value: string | string[], index: number | number[]) => {
this.HourSelect=index as number
})
//分钟
TextPicker({ range: this.minList, selected: this.minSelect }).width('25%')
.divider({
strokeWidth: 1,
startMargin: 0,
endMargin: 0,
color:'#ececec'
})
.textStyle({color:'#777777', font: {size:15, weight: 400}})
.selectedTextStyle({color: '#ffd96868', font: {size: 15, weight: 600}})
.onChange((value: string | string[], index: number | number[]) => {
this.minSelect=index as number
})
}
// 按钮
Row(){
Button('取消', { type: ButtonType.Normal, stateEffect: true })
.margin({right:6})
.borderRadius(6)
.backgroundColor('#fff')
.fontColor('#ffd96868')
.borderWidth(1)
.borderColor('#ffd96868')
.fontSize(15)
.width(156)
.onClick(()=>{
this.controller?.close()
})
Button('确定', { type: ButtonType.Normal, stateEffect: true })
.margin({left:6})
.borderRadius(6)
.fontSize(15)
.backgroundColor('#ffd96868')
.width(156)
.onClick(()=>{
this.onSubmit()
this.controller?.close()
})
}.justifyContent(FlexAlign.Center)
.padding({top:15})
.margin({top:25})
}.width('100%')
.padding(16)
.backgroundColor('#fff')
.borderRadius({topLeft:15,topRight:15})
}
}
//定义controller对象 主要作用父子通信 父组件调用子组件方法 唤醒弹窗
export class DialogDateController {
ShowDialog = (value?: string) => {
}
}
// @Entry 去掉入口页面标志
@Component
export struct TimeDateDialog { //修改命名 注意前面加了 export 需要暴露组件
private controller: DialogDateController = new DialogDateController();
CustomDialogController: CustomDialogController | null =null ;
@Prop defaultTime: string;
cancel?: () => void
confirm?: (data:string) => void = () => {
}
// 打开显示弹窗
private async ShowDialog(value?: string){
this.CustomDialogController?.open()
}
aboutToAppear(): void {
if (this.controller) {
//给controller对应的方法赋值
this.controller.ShowDialog = this.ShowDialog.bind(this); //这里需要注意 用了 bind改变 this 指向
}
this.CustomDialogController= new CustomDialogController({
builder: CustomDialogDate({
cancel: this.cancel,
confirm: this.confirm,
defaultTime:this.defaultTime
}),
autoCancel: true,
onWillDismiss:(dismissDialogAction: DismissDialogAction)=> {
if (dismissDialogAction.reason == DismissReason.PRESS_BACK) {
dismissDialogAction.dismiss()
}
if (dismissDialogAction.reason == DismissReason.TOUCH_OUTSIDE) {
dismissDialogAction.dismiss()
}
},
alignment: DialogAlignment.Bottom,
offset: { dx: 0, dy: 0},
customStyle: true,
})
}
aboutToDisappear() {
this.CustomDialogController = null // 将dialogController置空
}
build() { //因为用的 自定义弹窗功能,所以这下面可以为空
}
}
修改 ets/page/Index.ets
传入默认值
import {TimeDateDialog,DialogDateController} from "../components/TimeDateDialog"
@Entry
@Component
struct Index {
@State DateDialogRef: DialogDateController = new DialogDateController();
@State dateTime:string ='2024-08-14 13:05:00'
build() {
RelativeContainer() {
Column(){
Text(){
Span('默认时间:')
Span(this.dateTime)
}.margin({bottom:20})
Button('打开弹窗')
.onClick(()=>{
this.DateDialogRef.ShowDialog()
})
}.padding(20)
TimeDateDialog({
controller:this.DateDialogRef,
defaultTime:this.dateTime,
confirm:(data)=>{
this.dateTime=data
console.log('data',data)
}
})
}
.height('100%')
.width('100%')
}
}
重新打开预览可以看到默认值已经成功回显
总结
本文详细介绍了关于在华为鸿蒙系统 去实现一个自定义日期选中弹窗的详细教程,其实关于日期选中的逻辑是相通的,不仅仅是用于鸿蒙中,其实在其他项目中也可以以相同的逻辑实现。
希望这篇文章能帮到你,最后我把完整代码放到了gitee上有需要的小伙伴可以自己拉下来去试一试。
最后如果觉得本篇文章还不错,欢迎点赞收藏。
转载自:https://juejin.cn/post/7403151883135107099