-
【HarmonyOS】封装可以同时显示多个toast的工具类src/main/ets/common/MyPromptActionUtil.etsimport { ComponentContent, PromptAction, window } from '@kit.ArkUI';import { BusinessError } from '@kit.BasicServicesKit';// MyPromptInfo 类用于生成唯一的 dialogIDexport class MyPromptInfo { public dialogID: string constructor() { this.dialogID = this.generateRandomString(10) } // 生成指定长度的随机字符串 generateRandomString(length: number): string { const characters = 'abcdefghijklmnopqrstuvwxyz0123456789'; let result = ''; for (let i = 0; i < length; i++) { const randomIndex = Math.floor(Math.random() * characters.length); result += characters.charAt(randomIndex); } return result; }}// MyPromptActionUtil 类用于封装弹窗操作export class MyPromptActionUtil<T extends MyPromptInfo> { static myDialogPromptActionUtil: MyPromptActionUtil<MyToastInfo> | undefined = undefined public static showToast(message: string) { if (!MyPromptActionUtil.myDialogPromptActionUtil) { //当前页面没显示toast // getContext().eventHub.off(MyPromptActionUtil.myDialogPromptActionUtil?.dialogID) // MyPromptActionUtil.myDialogPromptActionUtil?.closeCustomDialog() //如果之前有的toast对话框,并且正在显示,则先关闭toast提示 window.getLastWindow(getContext()).then((windowClass) => { const uiContext = windowClass.getUIContext() MyPromptActionUtil.myDialogPromptActionUtil = new MyPromptActionUtil<MyToastInfo>(uiContext, wrapBuilder(myToastView), new MyToastInfo(message)) .setModal(false)//true:存在黑色半透明蒙层,false:没有蒙层 .setSwipeBackEnabled(false)//true:侧滑允许关闭弹窗 .setMaskTapToCloseEnabled(true)//true:点击半透明蒙层可关闭弹窗【注:如果setModal(false),那么就没有蒙层,所以点击对话框外也没有响应事件,也就是这里设置了也没效果,并且事件会穿透】 .setAlignment(DialogAlignment.Center) .onWillAppear(() => { console.info('在对话框的打开动画开始之前调用的回调函数') getContext().eventHub.on(MyPromptActionUtil.myDialogPromptActionUtil?.dialogID, (data: string) => { //监听结果 if (data == '关闭弹窗') { MyPromptActionUtil.myDialogPromptActionUtil?.closeCustomDialog() } }) }) .onWillDisappear(() => { console.info('在对话框的关闭动画开始之前调用的回调函数') getContext().eventHub.off(MyPromptActionUtil.myDialogPromptActionUtil?.dialogID) MyPromptActionUtil.myDialogPromptActionUtil = undefined }) .showCustomDialog() }) } else { //当前正在显示toast getContext().eventHub.emit(MyPromptActionUtil.myDialogPromptActionUtil.dialogID, { msg: message }) } } private uiContext: UIContext; private promptAction: PromptAction; private contentNode: ComponentContent<T> | undefined; private wrapBuilder: WrappedBuilder<[T]>; private t: T; private isModal: boolean = true; private alignment: DialogAlignment = DialogAlignment.Center; private isSwipeBackEnabled: boolean = true; private isMaskTapToCloseEnabled: boolean = true; public dialogID: string constructor(uiContext: UIContext, wrapBuilder: WrappedBuilder<[T]>, t: T) { this.uiContext = uiContext; this.promptAction = uiContext.getPromptAction(); this.wrapBuilder = wrapBuilder; this.t = t; this.dialogID = t.dialogID } setSwipeBackEnabled(isSwipeBackEnabled: boolean) { this.isSwipeBackEnabled = isSwipeBackEnabled; return this; } setMaskTapToCloseEnabled(isMaskTapToCloseEnabled: boolean) { this.isMaskTapToCloseEnabled = isMaskTapToCloseEnabled return this; } setAlignment(alignment: DialogAlignment) { this.alignment = alignment; return this; } setModal(isModal: boolean) { this.isModal = isModal; return this; } onDidAppear(callback: () => void) { this.onDidAppearCallback = callback; return this; } onDidDisappear(callback: () => void) { this.onDidDisappearCallback = callback; return this; } onWillAppear(callback: () => void) { this.onWillAppearCallback = callback; return this; } onWillDisappear(callback: () => void) { this.onWillDisappearCallback = callback; return this; } private onDidAppearCallback?: () => void; private onDidDisappearCallback?: () => void; private onWillAppearCallback?: () => void; private onWillDisappearCallback?: () => void; closeCustomDialog() { if (this.contentNode) { this.promptAction.closeCustomDialog(this.contentNode); } return this; } // 显示自定义弹窗 showCustomDialog() { try { if (!this.contentNode) { this.contentNode = new ComponentContent(this.uiContext, this.wrapBuilder, this.t); } this.promptAction.openCustomDialog(this.contentNode, { // 打开自定义弹窗 alignment: this.alignment, isModal: this.isModal, showInSubWindow: false, maskRect: { x: 0, y: 0, width: '100%', height: '100%' }, onWillDismiss: (dismissDialogAction: DismissDialogAction) => { //弹窗响应 console.info("reason" + JSON.stringify(dismissDialogAction.reason)) console.log("dialog onWillDismiss") if (dismissDialogAction.reason == 0 && this.isSwipeBackEnabled) { //手势返回时,关闭弹窗。 this.promptAction.closeCustomDialog(this.contentNode) } if (dismissDialogAction.reason == 1 && this.isMaskTapToCloseEnabled) { this.promptAction.closeCustomDialog(this.contentNode) } }, onDidAppear: this.onDidAppearCallback ? this.onDidAppearCallback : () => { }, onDidDisappear: this.onDidDisappearCallback ? this.onDidDisappearCallback : () => { }, onWillAppear: this.onWillAppearCallback ? this.onWillAppearCallback : () => { }, onWillDisappear: this.onWillDisappearCallback ? this.onWillDisappearCallback : () => { }, }); } catch (error) { // 错误处理 let message = (error as BusinessError).message; let code = (error as BusinessError).code; console.error(`OpenCustomDialog args error code is ${code}, message is ${message}`); } return this; }}class MyToastInfo extends MyPromptInfo { public message: string = "" constructor(message: string) { super() this.message = message }}@Builderfunction myToastView(data: MyToastInfo) { MyToastView({ dialogID: data.dialogID, message: data.message })}@ObservedV2class ToastBean { message: string = "" @Trace isShow: boolean = true constructor(message: string) { this.message = message }}@Componentstruct MyToastView { @State toast_info_list: ToastBean[] = [] @Prop dialogID: string @Prop message: string aboutToAppear(): void { this.toast_info_list.push(new ToastBean(this.message)) getContext().eventHub.on(this.dialogID, (data: object) => { if (data['msg']) { this.toast_info_list.push(new ToastBean(data['msg'])) } }) } build() { Column() { ForEach(this.toast_info_list, (item: ToastBean) => { Text(item.message) .fontSize('36lpx') .fontColor(Color.White) .backgroundColor("#B2ff0000") .borderRadius(8) .constraintSize({ maxWidth: '80%' }) .padding({ bottom: '28lpx', left: '60lpx', right: '60lpx', top: '28lpx' }) .margin(5) .visibility(item.isShow ? Visibility.Visible : Visibility.None) .onVisibleAreaChange([0.0, 1.0], (isVisible: boolean, currentRatio: number) => { console.info('Test Text isVisible: ' + isVisible + ', currentRatio:' + currentRatio) if (isVisible && currentRatio >= 1.0) { setTimeout(() => { item.isShow = false }, 2000) } }) .animation({ duration: 200, onFinish: () => { console.info('==== onFinish') //动画结束后,判断数组是否已全部为隐藏状态,是的话证明所有toast内容都展示完成,可以释放全局弹窗了 let isAnimAll = true for (let i = 0; i < this.toast_info_list.length; i++) { if (this.toast_info_list[i].isShow == true) { //至少有一个正在显示 isAnimAll = false break; } } if (isAnimAll) { console.info('已展示完全部toast,为了性能,关闭弹窗释放view') getContext(this).eventHub.emit(this.dialogID, "关闭弹窗") } } }) .transition(TransitionEffect.OPACITY.animation({ duration: 200 })) }) } }}src/main/ets/pages/Page01.etsimport { MyPromptActionUtil } from '../common/MyPromptActionUtil'@Entry@Componentstruct Page01 { build() { Column() { Button('显示Toast').onClick(() => { MyPromptActionUtil.showToast(`随机数:${this.getRandomInt(1, 100)}`) }) } .width('100%') .height('100%') } getRandomInt(min: number, max: number): number { min = Math.ceil(min); max = Math.floor(max); return Math.floor(Math.random() * (max - min + 1)) + min; }}转载自https://www.cnblogs.com/zhongcx/articles/18433356
-
【起因】需要实现头像剪裁的圆形遮罩。看到一个第三方库oh-crop是方形遮罩的cid:link_0@xinyansoft%2Foh-crop【经过】查看源码,修改.onReady改为圆形遮罩【完整示例】import { image } from '@kit.ImageKit';import { picker } from '@kit.CoreFileKit';import Matrix4 from '@ohos.matrix4'import fs from '@ohos.file.fs';import { hilog } from '@kit.PerformanceAnalysisKit';@Componentexport struct CropView { @State private model: CropModel = new CropModel(); private settings: RenderingContextSettings = new RenderingContextSettings(true); private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings); @State private matrix: object = Matrix4.identity() .translate({ x: 0, y: 0 }) .scale({ x: this.model.scale, y: this.model.scale}); /** 临时变量,无需手动赋值 **/ private tempScale = 1; /** 临时变量,无需手动赋值 **/ private startOffsetX: number = 0; /** 临时变量,无需手动赋值 **/ private startOffsetY: number = 0; build() { Stack() { Image(this.model.src) .width('100%') .height('100%') .alt(this.model.previewSource) .objectFit(ImageFit.Contain) .transform(this.matrix) .onComplete((msg) => { if (msg) { // 图片加载成功 this.model.imageWidth = msg.width; this.model.imageHeight = msg.height; this.model.componentWidth = msg.componentWidth; this.model.componentHeight = msg.componentHeight; this.checkImageAdapt(); if (this.model.imageLoadEventListener != null && msg.loadingStatus == 1) { this.model.imageLoadEventListener.onImageLoaded(msg); } } }) .onError((error) => { if (this.model.imageLoadEventListener != null) { this.model.imageLoadEventListener.onImageLoadError(error); } }) Canvas(this.context) .width('100%') .height('100%') .backgroundColor(Color.Transparent) .onReady(() => { if (this.context == null) { return } let height = this.context.height let width = this.context.width this.context.fillStyle= this.model.maskColor; this.context.fillRect(0, 0, width, height) // 计算圆形的中心点和半径 let centerX = width / 2; let centerY = height / 2; let minDimension = Math.min(width, height); let frameRadiusInVp = (minDimension - px2vp(this.model.frameWidth)) / 2; // 减去边框宽度 // 把中间的取景框透出来 this.context.globalCompositeOperation = 'destination-out' this.context.fillStyle = 'white' let frameWidthInVp = px2vp(this.model.frameWidth); let frameHeightInVp = px2vp(this.model.getFrameHeight()); let x = (width - px2vp(this.model.frameWidth)) / 2; let y = (height - px2vp(this.model.getFrameHeight())) / 2; // this.context.fillRect(x, y, frameWidthInVp, frameHeightInVp) console.info(`width:${width}`) console.info(`height:${height}`) console.info(`x:${x}`) console.info(`y:${y}`) console.info(`this.model.frameWidth:${this.model.frameWidth}`) console.info(`this.model.getFrameHeight():${this.model.getFrameHeight()}`) console.info(`frameWidthInVp:${frameWidthInVp}`) console.info(`frameHeightInVp:${frameHeightInVp}`) console.info(`frameRadiusInVp:${frameRadiusInVp}`) this.context.beginPath(); this.context.arc(centerX, centerY, px2vp(this.model.frameWidth/2), 0, 2 * Math.PI); this.context.fill(); // 设置综合操作模式为源覆盖,以便在现有图形上添加新的图形 this.context.globalCompositeOperation = 'source-over'; // 设置描边颜色 this.context.strokeStyle = this.model.strokeColor; // 计算圆形的半径,这里我们取正方形边框的较短边的一半作为半径 let radius = Math.min(frameWidthInVp, frameHeightInVp) / 2; // 开始绘制路径 this.context.beginPath(); // 使用 arc 方法绘制圆形 this.context.arc(centerX, centerY, radius, 0, 2 * Math.PI); // 关闭路径 this.context.closePath(); // 描绘圆形边框 this.context.lineWidth = 1; // 边框宽度 this.context.stroke(); }) .enabled(false) } .clip(true) .width('100%') .height('100%') .backgroundColor("#00000080") .priorityGesture( TapGesture({ count: 2, fingers: 1 }) .onAction((event:GestureEvent) => { if(!event){ return } if (this.model.zoomEnabled) { if (this.model.scale != 1) { this.model.scale = 1; this.model.reset(); this.updateMatrix(); } else { this.zoomTo(2); } } this.checkImageAdapt(); }) ) .gesture( GestureGroup(GestureMode.Parallel, // 拖动手势 PanGesture({}) .onActionStart(() => { hilog.info(0, "CropView", "Pan gesture start"); this.startOffsetX = this.model.offsetX; this.startOffsetY = this.model.offsetY; }) .onActionUpdate((event:GestureEvent) => { hilog.info(0, "CropView", `Pan gesture update: ${JSON.stringify(event)}`); if (event) { if (this.model.panEnabled) { let distanceX: number = this.startOffsetX + vp2px(event.offsetX) / this.model.scale; let distanceY: number = this.startOffsetY + vp2px(event.offsetY) / this.model.scale; this.model.offsetX = distanceX; this.model.offsetY = distanceY; this.updateMatrix() } } }) .onActionEnd(() => { hilog.info(0, "CropView", "Pan gesture end"); this.checkImageAdapt(); }), // 缩放手势处理 PinchGesture({ fingers: 2 }) .onActionStart(() => { this.tempScale = this.model.scale }) .onActionUpdate((event) => { if (event) { if (!this.model.zoomEnabled) return; this.zoomTo(this.tempScale * event.scale); } }) .onActionEnd(() => { this.checkImageAdapt(); }) ) ) } /** * 检查手势操作后,图片是否填满取景框,没填满则进行调整 */ private checkImageAdapt() { let offsetX = this.model.offsetX; let offsetY = this.model.offsetY; let scale = this.model.scale; hilog.info(0, "CropView", `offsetX: ${offsetX}, offsetY: ${offsetY}, scale: ${scale}`) // 图片适配控件的时候也进行了缩放,计算出这个缩放比例 let widthScale = this.model.componentWidth / this.model.imageWidth; let heightScale = this.model.componentHeight / this.model.imageHeight; let adaptScale = Math.min(widthScale, heightScale); hilog.info(0, "CropView", `Image scale ${adaptScale} while attaching the component[${this.model.componentWidth}, ${this.model.componentHeight}]`) // 经过两次缩放(适配控件、手势)后,图片的实际显示大小 let showWidth = this.model.imageWidth * adaptScale * this.model.scale; let showHeight = this.model.imageHeight * adaptScale * this.model.scale; let imageX = (this.model.componentWidth - showWidth) / 2; let imageY = (this.model.componentHeight - showHeight) / 2; hilog.info(0, "CropView", `Image left top is (${imageX}, ${imageY})`) // 取景框的左上角坐标 let frameX = (this.model.componentWidth - this.model.frameWidth) / 2; let frameY = (this.model.componentHeight - this.model.getFrameHeight()) / 2; // 图片左上角坐标 let showX = imageX + offsetX * scale; let showY = imageY + offsetY * scale; hilog.info(0, "CropView", `Image show at (${showX}, ${showY})`) if(this.model.frameWidth > showWidth || this.model.getFrameHeight() > showHeight) { // 图片缩放后,大小不足以填满取景框 let xScale = this.model.frameWidth / showWidth; let yScale = this.model.getFrameHeight() / showHeight; let newScale = Math.max(xScale, yScale); this.model.scale = this.model.scale * newScale; showX *= newScale; showY *= newScale; } // 调整x轴方向位置,使图像填满取景框 if(showX > frameX) { showX = frameX; } else if(showX + showWidth < frameX + this.model.frameWidth) { showX = frameX + this.model.frameWidth - showWidth; } // 调整y轴方向位置,使图像填满取景框 if(showY > frameY) { showY = frameY; } else if(showY + showHeight < frameY + this.model.getFrameHeight()) { showY = frameY + this.model.getFrameHeight() - showHeight; } this.model.offsetX = (showX - imageX) / scale; this.model.offsetY = (showY - imageY) / scale; this.updateMatrix(); } public zoomTo(scale: number): void { this.model.scale = scale; this.updateMatrix(); } public updateMatrix(): void { this.matrix = Matrix4.identity() .translate({ x: this.model.offsetX, y: this.model.offsetY }) .scale({ x: this.model.scale, y: this.model.scale }) }}interface ImageLoadedEvent { width: number; height: number; componentWidth: number; componentHeight: number; loadingStatus: number; contentWidth: number; contentHeight: number; contentOffsetX: number; contentOffsetY: number;}export interface ImageLoadEventListener { onImageLoaded(msg: ImageLoadedEvent): void; onImageLoadError(error: ImageError): void;}export class CropModel { /** * 图片uri * 类型判断太麻烦了,先只支持string,其他形式的需要先转换成路径 */ src: string = ''; /** * 图片预览 */ previewSource: string | Resource = ''; /** * 是否可以拖动 */ panEnabled: boolean = true; /** * 是否可以缩放 */ zoomEnabled: boolean = true; /** * 取景框宽度 */ frameWidth = 1000; /** * 取景框宽高比 */ frameRatio = 1; /** * 遮罩颜色 */ maskColor: string = '#AA000000'; /** * 取景框边框颜色 */ strokeColor: string = '#FFFFFF'; /** * 图片加载监听 */ imageLoadEventListener: ImageLoadEventListener | null = null; /// 以下变量不要手动赋值 /// /** * 图片宽度,加载完成之后才会赋值 */ imageWidth: number = 0; /** * 图片高度,加载完成之后才会赋值 */ imageHeight: number = 0; /** * 控件宽度 */ componentWidth: number = 0; /** * 控件高度 */ componentHeight: number = 0; /** * 手势缩放比例 * 图片经过了两重缩放,一是适配控件的时候进行了缩放,二是手势操作的时候进行了缩放 */ scale: number = 1; /** * x轴方向偏移量 */ offsetX: number = 0; /** * y轴方向偏移量 */ offsetY: number = 0; ///////////////////////////////////////////// public setImage(src: string, previewSource?: string | Resource): CropModel { this.src = src; if (!!previewSource) { this.previewSource = previewSource; } return this; } public setScale(scale: number): CropModel { this.scale = scale; return this; } public isPanEnabled(): boolean { return this.panEnabled; } public setPanEnabled(panEnabled: boolean): CropModel { this.panEnabled = panEnabled; return this; } public setZoomEnabled(zoomEnabled: boolean): CropModel { this.zoomEnabled = zoomEnabled; return this; } public setFrameWidth(frameWidth: number) : CropModel { this.frameWidth = frameWidth; return this; } public setFrameRatio(frameRatio: number) : CropModel { this.frameRatio = frameRatio; return this; } public setMaskColor(color: string) : CropModel { this.maskColor = color; return this; } public setStrokeColor(color: string) : CropModel { this.strokeColor = color; return this; } public setImageLoadEventListener(listener: ImageLoadEventListener) : CropModel { this.imageLoadEventListener = listener; return this; } public getScale(): number { return this.scale; } public isZoomEnabled(): boolean { return this.zoomEnabled; } public getImageWidth(): number { return this.imageWidth; } public getImageHeight(): number { return this.imageHeight; } public getFrameHeight() { return this.frameWidth / this.frameRatio; } public reset(): void { this.scale = 1; this.offsetX = 0; this.offsetY = 0; } public async crop(format: image.PixelMapFormat) : Promise<image.PixelMap> { if(!this.src || this.src == '') { throw new Error('Please set src first'); } if(this.imageWidth == 0 || this.imageHeight == 0) { throw new Error('The image is not loaded'); } // 图片适配控件的时候也进行了缩放,计算出这个缩放比例 let widthScale = this.componentWidth / this.imageWidth; let heightScale = this.componentHeight / this.imageHeight; let adaptScale = Math.min(widthScale, heightScale); // 经过两次缩放(适配控件、手势)后,图片的实际显示大小 let totalScale = adaptScale * this.scale; let showWidth = this.imageWidth * totalScale; let showHeight = this.imageHeight * totalScale; let imageX = (this.componentWidth - showWidth) / 2; let imageY = (this.componentHeight - showHeight) / 2; // 取景框的左上角坐标 let frameX = (this.componentWidth - this.frameWidth) / 2; let frameY = (this.componentHeight - this.getFrameHeight()) / 2; // 图片左上角坐标 let showX = imageX + this.offsetX * this.scale; let showY = imageY + this.offsetY * this.scale; let x = (frameX - showX) / totalScale; let y = (frameY - showY) / totalScale; let file = fs.openSync(this.src, fs.OpenMode.READ_ONLY) let imageSource : image.ImageSource = image.createImageSource(file.fd); let decodingOptions : image.DecodingOptions = { editable: true, desiredPixelFormat: image.PixelMapFormat.BGRA_8888, } // 创建pixelMap let pm = await imageSource.createPixelMap(decodingOptions); let cp = await this.copyPixelMap(pm); pm.release(); let region: image.Region = { x: x, y: y, size: { width: this.frameWidth / totalScale, height: this.getFrameHeight() / totalScale } }; cp.cropSync(region); return cp; } async copyPixelMap(pm: PixelMap): Promise<PixelMap> { const imageInfo: image.ImageInfo = await pm.getImageInfo(); const buffer: ArrayBuffer = new ArrayBuffer(pm.getPixelBytesNumber()); // TODO 知识点:通过readPixelsToBuffer实现PixelMap的深拷贝,其中readPixelsToBuffer输出为BGRA_8888 await pm.readPixelsToBuffer(buffer); // TODO 知识点:readPixelsToBuffer输出为BGRA_8888,此处createPixelMap需转为RGBA_8888 const opts: image.InitializationOptions = { editable: true, pixelFormat: image.PixelMapFormat.RGBA_8888, size: { height: imageInfo.size.height, width: imageInfo.size.width } }; return image.createPixelMap(buffer, opts); }}interface MyEvent { result: FileSelectorResult, fileSelector: FileSelectorParam}@Entry@Componentstruct Page23 { @State pm: PixelMap | undefined = undefined; @State private model: CropModel = new CropModel(); build() { Column() { CropView({ model: this.model, }) .layoutWeight(1) .width('100%') Button('打开相册').onClick(()=>{ this.openPicker() }) Button('测试剪裁').onClick(async () => { try { this.pm = await this.model.crop(image.PixelMapFormat.RGBA_8888); } catch (e) { console.info(`e:${JSON.stringify(e)}`) } }) Text('剪裁结果') Image(this.pm).width('300lpx').height('300lpx').borderRadius('150lpx') } .height('100%') .width('100%') } // 弹出图片选择器方法 async openPicker() { try { // 设置图片选择器选项 const photoSelectOptions = new picker.PhotoSelectOptions(); // 限制只能选择一张图片 photoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE; photoSelectOptions.maxSelectNumber = 1; // 创建并实例化图片选择器 const photoViewPicker = new picker.PhotoViewPicker(); // 选择图片并获取图片URI let uris: picker.PhotoSelectResult = await photoViewPicker.select(photoSelectOptions); if (!uris || uris.photoUris.length === 0) return; // 获取选中图片的第一张URI let uri: string = uris.photoUris[0]; this.model.setImage(uri) .setFrameWidth(1000) .setFrameRatio(1); } catch (e) { console.error('openPicker', JSON.stringify(e)); } } handleFileSelection(event: MyEvent) { const PhotoSelectOptions = new picker.PhotoSelectOptions(); PhotoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE; PhotoSelectOptions.maxSelectNumber = 1; const photoPicker = new picker.PhotoViewPicker(); photoPicker.select(PhotoSelectOptions) .then((PhotoSelectResult) => { if (PhotoSelectResult.photoUris.length === 0) { console.warn('No image selected.'); return; } const srcUri = PhotoSelectResult.photoUris[0]; this.model.setImage(srcUri) .setFrameWidth(1000) .setFrameRatio(1); }) .catch((selectError: object) => { console.error('Failed to invoke photo picker:', JSON.stringify(selectError)); }); return true; }}转载自https://www.cnblogs.com/zhongcx/articles/18433355
-
import { promptAction } from '@kit.ArkUI'class MyDataItem { id: number = 0 provinceid: string = "" provincename: string = "" cityid: string = "" cityname: string = "" countyid: string = "" countyname: string = ""}// 示例数据const transformedData: MyDataItem[] = [ { "id": 1, "provinceid": "110000", "provincename": "北京市", "cityid": "110100000000", "cityname": "北京市", "countyid": "110101000000", "countyname": "东城区" }, { "id": 2, "provinceid": "110000", "provincename": "北京市", "cityid": "110100000000", "cityname": "北京市", "countyid": "110102000000", "countyname": "西城区" }, { "id": 3, "provinceid": "110000", "provincename": "北京市", "cityid": "110100000000", "cityname": "北京市", "countyid": "110103000000", "countyname": "朝阳区" }, { "id": 4, "provinceid": "120000", "provincename": "天津市", "cityid": "120100000000", "cityname": "天津市", "countyid": "120101000000", "countyname": "和平区" }, { "id": 5, "provinceid": "120000", "provincename": "天津市", "cityid": "120100000000", "cityname": "天津市", "countyid": "120102000000", "countyname": "河东区" }, { "id": 6, "provinceid": "120000", "provincename": "天津市", "cityid": "120100000000", "cityname": "天津市", "countyid": "120103000000", "countyname": "河西区" }, { "id": 7, "provinceid": "130000", "provincename": "河北省", "cityid": "130100000000", "cityname": "石家庄市", "countyid": "130102000000", "countyname": "桥西区" }, { "id": 8, "provinceid": "130000", "provincename": "河北省", "cityid": "130100000000", "cityname": "石家庄市", "countyid": "130103000000", "countyname": "新华区" }, { "id": 9, "provinceid": "130000", "provincename": "河北省", "cityid": "130100000000", "cityname": "石家庄市", "countyid": "130104000000", "countyname": "长安区" } ]export class MyTabBean { name: string = "" idType: string | null = null // 'province' | 'city' | null = null idContent: string | null = null x: number = -1 //当前位置 width: number = -1 //当前宽度 constructor(name: string) { this.name = name }}@Entry@Componentstruct Page11 { scroller: Scroller = new Scroller() @State titles: MyTabBean[] = [] scrollWidthOld: number = 0 @State searchValue: string = "" //搜索内容 @State errorInfo: string = "" //错误信息 @State contentList: contentItem[] = [] //要显示的列表 aboutToAppear(): void { this.getDataByIdType("全部", null, null) } build() { Column() { //tab栏 Scroll(this.scroller) { Row() { ForEach(this.titles, (item: MyTabBean, index: number) => { if (index == 0) { Text(item.name) .padding({ left: '30lpx', right: '16lpx' ,top:'5lpx',bottom:'5lpx'}) .fontColor(index == this.titles.length - 1 ? "#FF1919" : "#2E2E2E") .fontSize('30lpx') .transition(TransitionEffect.OPACITY.animation({ duration: 200 })) .onAreaChange((previousArea: Area, currentArea: Area) => { if (item.x == -1) { item.x = currentArea.position.x as number } if (item.width == -1) { item.width = currentArea.width as number } }) .onClick(() => { this.getDataByIdType("全部", null, null) }) } else { Row() { Text(">").width('44lpx').height('44lpx') Text(item.name) .fontColor(index == this.titles.length - 1 ? "#FF1919" : "#2E2E2E") .fontSize('30lpx') }.padding({ right: '16lpx' ,top:'5lpx',bottom:'5lpx'}) .transition(TransitionEffect.OPACITY.animation({ duration: 200 })) .onAreaChange((previousArea: Area, currentArea: Area) => { if (item.x == -1) { item.x = currentArea.position.x as number } if (item.width == -1) { item.width = currentArea.width as number } }) .onClick(() => { this.getDataByIdType(item.name, item.idType, item.idContent, index) }) } }) }.height('95lpx') .onAreaChange((previousArea: Area, currentArea: Area) => { let scrollWidth = currentArea.width as number console.info(`scrollWidth:${scrollWidth}`) if (this.titles.length > 0 && this.scrollWidthOld != scrollWidth) { this.scrollWidthOld = scrollWidth let item = this.titles[this.titles.length-1] this.scroller.scrollTo({ xOffset: (item.x - scrollWidth / 2 + item.width / 2), yOffset: 0, animation: true }) } }) } .scrollable(ScrollDirection.Horizontal) .scrollBar(BarState.Off) .borderWidth({ bottom: 1 }) .borderColor("#e3e3e3") .align(Alignment.Start) .width('100%') //内容 Scroll() { Column() { ForEach(this.contentList, (item: contentItem, index: number) => { if (index != 0) { Text() .height(1) .width('720lpx') .margin({ left: '30lpx' }) .backgroundColor("#e3e3e3") } Row() { Column() { Text(item.name).fontSize('30lpx').fontColor("#4B4B4B") }.layoutWeight(1).alignItems(HorizontalAlign.Start).justifyContent(FlexAlign.Center) } .width('100%') .backgroundColor(Color.White) .padding({ top: '20lpx', bottom: '20lpx' }) .onClick(() => { this.getDataByIdType(item.name, item.idType, item.idContent) // else{//是叶子节点了,点击需要调用加入部门接口 // // for(let i = 0 ;i<this.contentList.length;i++){ // // this.contentList[i].isSelect = false // // } // // } }) }) } .width('100%') .padding({ left: '30lpx', right: '30lpx' }) .margin({ top: '20lpx', bottom: '20lpx' }) .borderWidth({ top: 1, bottom: 1 }) .borderColor("#e3e3e3") .backgroundColor(Color.White) } .align(Alignment.Top) .width('100%') .layoutWeight(1) .scrollBar(BarState.Off) .backgroundColor("#fafafa") .visibility(this.contentList.length != 0 ? Visibility.Visible : Visibility.None) } .height('100%') .width('100%'); } // 封装一个方法来根据ID类型和具体内容返回相应的数据 //idType: 'province' | 'city' | null getDataByIdType(name: string, idType: string | null, idContent: string | null, index?: number) { console.info(`内容填充`) let contentItems: contentItem[] = []; if (idType === null) { // 如果idType为空,返回所有省份的不重复数据 const seenProvinces: object = Object(); for (let i = 0; i < transformedData.length; i++) { const item = transformedData[i]; const provinceKey = item.provincename + item.provinceid; if (!seenProvinces[provinceKey]) { seenProvinces[provinceKey] = true; contentItems.push({ name: item.provincename, idType: 'province', idContent: item.provinceid }); } } } else { // 如果idType为省份或城市,筛选出相应级别的所有数据 for (let i = 0; i < transformedData.length; i++) { const item = transformedData[i]; if (idType === 'province') { // 如果idType为省份,筛选出指定省份下的所有数据 if (item.provinceid === idContent) { contentItems.push({ name: item.cityname, idType: 'city', idContent: item.cityid }); } } else if (idType === 'city') { // 如果idType为城市,筛选出指定城市下的所有数据 if (item.cityid === idContent) { contentItems.push({ name: item.countyname, idType: 'county', idContent: item.countyid }); } } } } if(contentItems.length == 0){ let str = "" for(let i = 0 ;i<transformedData.length;i++){ if(idContent ==transformedData[i].countyid ){ str = `省:${transformedData[i].provincename},市:${transformedData[i].cityname},区:${transformedData[i].countyname}` break; } } promptAction.showDialog({ title: '当前选中的是', message: `${str}`, buttons: [ { text: '我知道了', color: '#000000' } ], }) return } this.contentList = [...contentItems] //标题栏填充 console.info(`标题栏填充`) if (!idType) { console.info(`用户点了根,或者是首次启动的`) this.titles = [new MyTabBean(name)] } else { //清空index之后的 console.info(`index:${index}`) console.info(`this.titles:${JSON.stringify(this.titles)}`) if (index) { this.titles.splice(index) } let myTabBean = new MyTabBean(name) myTabBean.idType = idType myTabBean.idContent = idContent this.titles.push(myTabBean) console.info(`this.titles:${JSON.stringify(this.titles)}`) } }}class contentItem { name: string = ""; idType: string | null = null; //'province' | 'city' | 'county' | null idContent: string | null = null;}转载自https://www.cnblogs.com/zhongcx/articles/18433352
-
【HarmonyOS】TextInput设置最多两位小数且整数部分不超过6位方法【使用自定义键盘】@Entry@Componentstruct Page47 { controller: TextInputController = new TextInputController(); @State inputValue: string = ''; // 自定义键盘组件 @Builder CustomKeyboardBuilder() { Column() { Button('x') .onClick(() => { // 关闭自定义键盘 this.controller.stopEditing(); }) Grid() { ForEach([1, 2, 3, 4, 5, 6, 7, 8, 9, '.', 0, '删除'], (item: number | string) => { GridItem() { Button(item + '') .width(110) .onClick(() => { if (item == '删除') { this.inputValue = this.inputValue.substring(0, this.inputValue.length - 1); return; } this.inputValue += item; let regex = /^-?\d{0,6}(?:\.\d{0,2})?$/ let test = regex.test(this.inputValue) console.info(`onChange regex.test(this.inputValue) :${test}`) if (!test) { this.inputValue = this.inputValue.substring(0, this.inputValue.length - 1); } }) } }) } .maxCount(3) .columnsGap(10) .rowsGap(10) .padding(5) } .backgroundColor(Color.Gray) } build() { Column() { TextInput({ text: $$this.inputValue, placeholder: '请输入内容', controller: this.controller }) .customKeyboard(this.CustomKeyboardBuilder()) } .height('100%') .width('100%') }}参考:cid:link_0【使用原生键盘】打印0.1 --> true.1 --> true1. --> true1.0 --> true0.1 --> 0.1.1 --> 0.11. --> 11.0 --> 1代码@Entry@Componentstruct Page47 { @State inputValue: string = ''; regex = /^-?\d{0,6}(?:\.\d{0,2})?$/ build() { Column() { Button('正则测试').onClick(() => { console.info(`0.1 --> ${this.regex.test('0.1')}`) console.info(`.1 --> ${this.regex.test('.1')}`) console.info(`1. --> ${this.regex.test('1.')}`) console.info(`1.0 --> ${this.regex.test('1.0')}`) console.info(`0.1 --> ${0.1}`) console.info(`.1 --> ${.1}`) console.info(`1. --> ${1.}`) console.info(`1.0 --> ${1.0}`) }) TextInput({ text: $$this.inputValue, placeholder: '请输入内容' }) .onChange((value: string) => { console.info(`onChange inputValue start :${this.inputValue}`) let test = this.regex.test(this.inputValue) console.info(`onChange regex.test(this.inputValue) :${test}`) if (!test) { this.inputValue = this.inputValue.substring(0, this.inputValue.length - 1); } }) } .height('100%') .width('100%') }}转载自https://www.cnblogs.com/zhongcx/articles/18433351
-
【HarmonyOS】仿前端css中的背景色渐变两层叠加/* * 前端css中的背景色渐变两层叠加效果 *<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Gradient Background Test</title> <style> .gradient-background { width: 100%; height: 100vh; background-image: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, #f3f5f7 100%), linear-gradient(58deg, #4966ff 1%, #3489ff 50%, #28b2ff 100%); } </style></head><body> <div class="gradient-background"> <h1>测试</h1> </div></body></html> * */@Entry@Componentstruct Page64 { build() { Column() { Text('仿前端css中的背景色渐变两层叠加') Stack(){ Text().width('100%').height('100%').linearGradient({ angle: 58, colors: [[0x4966ff, 0.0], [0x3489ff, 0.5], [0x28b2ff, 1.0]] }) Text().width('100%').height('100%').linearGradient({ angle: 180, colors: [['rgba(255, 255, 255, 0)', 0.0], [0xf3f5f7, 1.0]] }) Text('测试') }.width('200lpx').height('200lpx') } .height('100%') .width('100%') }}转载自https://www.cnblogs.com/zhongcx/articles/18433348
-
【方案一】使用@Link@Componentstruct LoginEditCompoments { @Link inputValue :string build() { TextInput({text:$$this.inputValue}) }}@Entry@Componentstruct Page04 { @State inputValue_1: string = "" @State inputValue_2: string = "" build() { Column({ space: 10 }) { LoginEditCompoments({ inputValue:this.inputValue_1 }) LoginEditCompoments({ inputValue:this.inputValue_2 }) Button('登录').onClick(() => { console.info(`inputValue_1:${this.inputValue_1}`) console.info(`inputValue_2:${this.inputValue_2}`) }) } .height('100%') .width('100%') }}打印inputValue_1:555inputValue_2:7【2】使用onChange回调@Componentstruct LoginEditCompoments { onTextChange?: (value: string) => void build() { TextInput().onChange((value: string) => { if(this.onTextChange){ this.onTextChange(value) } }) }}@Entry@Componentstruct Page04 { @State inputValue_1: string = "" @State inputValue_2: string = "" build() { Column({ space: 10 }) { LoginEditCompoments({ onTextChange: (value) => { this.inputValue_1 = value } }) LoginEditCompoments({ onTextChange: (value) => { this.inputValue_2 = value } }) Button('登录').onClick(() => { console.info(`inputValue_1:${this.inputValue_1}`) console.info(`inputValue_2:${this.inputValue_2}`) }) } .height('100%') .width('100%') }}打印inputValue_1:777inputValue_2:5转载自https://www.cnblogs.com/zhongcx/articles/18433345
-
【HarmonyOS】记录淡入淡出切换控件内图片的方法.transition(TransitionEffect.OPACITY)注意只有用 if 时才会生效,用.visibility()只有显示的时候生效,隐藏不生效。@Entry@Componentstruct Page09 { @State isShowImage1: boolean = false; base64Str: string = 'data:image/png;base64,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' @State idAnim: string = "idAnim" build() { Column() { Button('切换图片 淡入淡出').onClick(() => { this.isShowImage1 = !this.isShowImage1 }) Stack() { if (this.isShowImage1) { Image($r("app.media.app_icon")) .width('100%') .height('100%') .transition(TransitionEffect.OPACITY.animation({ duration: 600, curve: Curve.Sharp })) } else { Image(this.base64Str) .width('100%') .height('100%') .transition(TransitionEffect.OPACITY.animation({ duration: 600, curve: Curve.Sharp })) } }.width('200lpx').height('200lpx') } .height('100%') .width('100%') }}转载自https://www.cnblogs.com/zhongcx/articles/18433341
-
【HarmonyOS】自定义TabLayout代码示例,通过 Scroll 锚点 Tab 布局,滚动条会自动滚动使选中的标签居中显示。class MyTabItem { label: string = ""; positionX: number = -1; // 当前位置 width: number = -1; // 当前宽度 constructor(label: string) { this.label = label; }}@Componentstruct MyTabLayout { onSelected?: (selectedIndex: number) => void; scroller: Scroller = new Scroller(); @Prop tabItems: MyTabItem[]; @State @Watch('selectIndexChanged') selectedIndex: number = 0; @State tabBarWidth: number = 0; // Tab 栏宽度 selectIndexChanged() { if (this.onSelected) { this.onSelected(this.selectedIndex); } } build() { Column() { Scroll(this.scroller) { Row() { ForEach(this.tabItems, (item: MyTabItem, index: number) => { Row() { Image($r('app.media.app_icon')).width('44lpx').height('44lpx'); Text(item.label) .margin({ left: '16lpx' }) .fontColor(index === this.selectedIndex ? "#FF1919" : "#2E2E2E") .fontSize('30lpx'); }.padding({ right: '16lpx' }) .onAreaChange((previousArea: Area, currentArea: Area) => { if (item.positionX === -1) { item.positionX = currentArea.position.x as number; } if (item.width === -1) { item.width = currentArea.width as number; } }) .onClick(() => { this.selectedIndex = index; this.scroller.scrollTo({ xOffset: (item.positionX - this.tabBarWidth / 2 + item.width / 2), yOffset: 0, animation: true }); }); }); }.height('95lpx'); } .scrollable(ScrollDirection.Horizontal) .scrollBar(BarState.Off) .borderWidth({ bottom: 1 }) .borderColor("#e3e3e3") .align(Alignment.Start) .width('100%') .onAreaChange((previousArea: Area, currentArea: Area) => { this.tabBarWidth = currentArea.width as number; }); } }}@Entry@Componentstruct Page11 { scroller: Scroller = new Scroller(); @State tabItems: MyTabItem[] = []; @State selectedIndex: number = 0 getRandomInt(min: number, max: number): number { min = Math.ceil(min); max = Math.floor(max); return Math.floor(Math.random() * (max - min + 1)) + min; } aboutToAppear(): void { for (let i = 0; i < 20; i++) { this.tabItems.push(new MyTabItem(`项目:${this.getRandomInt(1, 10000)}`)); } } build() { Column() { MyTabLayout({ tabItems: this.tabItems, onSelected: (selectedIndex: number) => { console.info(`当前选择的位置: ${selectedIndex}`); this.selectedIndex = selectedIndex } }); Stack() { Text(`当前选择的位置:${this.selectedIndex}`) }.width('100%') .layoutWeight(1) .backgroundColor(Color.Orange) } .height('100%') .width('100%'); }}转载自https://www.cnblogs.com/zhongcx/articles/18433337
-
【HarmonyOS】borderRadius百分比不生效,可以根据onAreaChange自己封装个方法计算百分比。@Entry@Componentstruct Page32 { @State private _viewWidth: number = 0; private getPercentOfWidth(percent: number): number { return this._viewWidth / 100 * percent; } build() { Column() { Text('哈哈') .width(200) .height(200) .textAlign(TextAlign.Center) .backgroundColor(Color.Pink) .borderRadius(this.getPercentOfWidth(15))//15% .onAreaChange((oldValue, newValue) => { this._viewWidth = newValue.width as number; }) } .height('100%') .width('100%') }}转载自https://www.cnblogs.com/zhongcx/articles/18433336
-
// 使用 DevEco Studio 3.1.1 Release 及以上版本,API 版本为 api 9 及以上。// 主要功能及注意事项:// 该组件展示了一个乘客选择列表。列表中的每个项目包含一个复选框和对应的乘客姓名,// 用户点击任意一项即可切换其选中状态。组件通过限制最多只能选择5名乘客,// 并在超过限制时通过promptAction模块弹出 toast 提示用户。// 注意,代码中的Checkbox组件目前设置为不可更改(enabled(false)),// 在实际应用中可以根据需求决定是否允许用户手动改变复选框状态。// 导入提示操作模块import promptAction from '@ohos.promptAction';// 定义数据模型类ItemDataclass ItemData { // 名字属性 name: string; // 是否选中属性 isSelect: boolean; // 构造函数初始化数据 constructor(name: string, isSelect: boolean) { this.name = name; this.isSelect = isSelect; }}// 标记为入口文件并创建组件@Entry@Componentstruct test { // 状态变量arr用于存储ItemData对象数组 @State arr: Array<ItemData> = [ new ItemData('赵大', false), new ItemData('钱二', false), new ItemData('张三', false), new ItemData('李四', false), new ItemData('王五', false), new ItemData('周六', false), new ItemData('李七', false), new ItemData('朱八', false) ]; // 构建UI组件的方法 build() { // 创建垂直方向布局 Column() { // 显示提示文本 Text('请选择乘客,最多限五人') .margin({ top: '60lpx', left: '50lpx', bottom: '10lpx' }); // 遍历存储乘客信息的数据数组 ForEach(this.arr, (item: ItemData, index: number) => { // 创建水平方向布局 Row() { // 创建复选框组件,禁用修改(此处可能是样式演示,实际应用中可去除.enabled(false)) Checkbox() .enabled(false) .select(item.isSelect) .width('41lpx') .height('41lpx') .selectedColor("#FF53B175"); // 显示乘客姓名文本 Text(item.name) .fontSize('27lpx') .margin({ left: '10lpx' }) .fontWeight(400) .fontColor(item.isSelect ? "#FF53B175" : "#FF181725") // 当行组件点击事件处理 } .onClick(() => { // 反转当前项的选中状态 item.isSelect = !item.isSelect; // 计算已选中乘客数量 let isSelectCount = 0; for (let i = 0; i < this.arr.length; i++) { if (this.arr[i].isSelect) { isSelectCount++; } } // 如果已选中超过5人,则恢复当前项未选中状态并弹出提示 if (isSelectCount > 5) { item.isSelect = !item.isSelect; try { // 使用promptAction模块显示toast消息 promptAction.showToast({ message: '最多限五人', duration: 2000, bottom: '375lpx' }); } catch (error) { // 忽略错误 } return; } // 更新数组中对应项的状态 this.arr[index] = new ItemData(item.name, item.isSelect); }) // 设置行组件的边距 .margin({ left: '40lpx', top: '10lpx' }) }) } // 设置Column组件的整体样式 .width('100%') .height('100%') .backgroundColor("#FFF2F3F2") .justifyContent(FlexAlign.Start) .alignItems(HorizontalAlign.Start); }}转载自https://www.cnblogs.com/zhongcx/articles/18433333
-
// 注:当前代码基于宽度为720的设计稿进行适配,使用lpx单位。// 整段代码描述的功能:// 该代码实现了一个简单的搜索功能组件,其中包括:// 1. 输入框:用户可以在此输入要搜索的内容;// 2. 搜索按钮:点击后,将当前输入内容添加到搜索历史记录的首位,若有重复则移除重复项,并保持历史记录不超过10条;// 3. 搜索历史标题和清空记录按钮:展示搜索历史记录列表,并提供清空全部历史记录的功能;// 4. 搜索历史记录列表:按照时间最近的顺序显示搜索历史记录,最多显示10条。@Entry@Componentstruct test { // 定义状态变量,用于存储输入框的当前值 @State inputValue: string = '' // 定义状态变量,用于存储搜索历史记录的数组 @State historyValueArr: Array<string> = [ '张三', '李四', '举头望明月', '低头思故乡', 'HarmonyOs', '不可能,绝对不可能' ] // 构建UI组件 build() { // 主体内容使用Column布局,垂直堆叠组件 Column() { // 输入框和搜索按钮组合,使用Row布局,水平排列 Row() { // 创建一个TextInput输入框 TextInput({ placeholder: '请输入内容', text: this.inputValue }) .width('524.31lpx') // 设置宽度 .height('70lpx') // 设置高度 .fontSize('27lpx') // 设置字体大小 .backgroundColor("#ffffff") // 设置背景颜色 // 输入框内容改变时,同步更新状态变量inputValue .onChange((value) => { this.inputValue = value }) // 创建一个搜索按钮 Button('搜索') // 按钮点击事件,处理搜索逻辑 .onClick(() => { // 遍历历史记录数组,若找到与输入框内容相同的记录,从数组中移除 for (let i = 0; i < this.historyValueArr.length; i++) { if (this.historyValueArr[i] === this.inputValue) { this.historyValueArr.splice(i, 1); break; } } // 将输入框内容添加到历史记录数组的首位 this.historyValueArr.unshift(this.inputValue); // 若历史记录超过10条,则移除最后一项 if (this.historyValueArr.length > 10) { this.historyValueArr.splice(this.historyValueArr.length - 1); } }) } // 设置Row组件的宽度、对齐方式和内外边距 .width('100%') .justifyContent(FlexAlign.SpaceBetween) .padding({ left: '37lpx', top: '11lpx', bottom: '11lpx', right: '15lpx' }) // 搜索历史标题和清除记录按钮组合,同样使用Row布局 Row() { // 搜索历史标题 Text('搜索历史').fontSize('31lpx').fontColor("#000000") // 清空记录按钮 Text('清空记录') .fontSize('27lpx').fontColor("#828385") // 清空记录按钮点击事件,清空历史记录数组 .onClick(() => { this.historyValueArr.length = 0; }) } // 设置Row组件的宽度、对齐方式和内外边距 .width('100%') .justifyContent(FlexAlign.SpaceBetween) .padding({ left: '37lpx', top: '11lpx', bottom: '11lpx', right: '37lpx' }) // 使用Flex布局,按行(FlexDirection.Row)包裹搜索历史记录 Flex({ direction: FlexDirection.Row, wrap: FlexWrap.Wrap, }) { // 遍历历史记录数组,创建Text组件展示每一条历史记录 ForEach(this.historyValueArr, (item: string, value: number) => { Text(item) .padding({ left: '15lpx', right: '15lpx', top: '7lpx', bottom: '7lpx' }) // 设置背景颜色、圆角和间距 .backgroundColor("#EFEFEF") .borderRadius(10) .margin('11lpx') }) } // 设置Flex容器的宽度和内外边距 .width('100%') .padding({ left: '26lpx', top: '11lpx', bottom: '11lpx', right: '26lpx' }) } // 设置Column容器的宽度、高度和背景颜色 .width('100%') .height('100%') .backgroundColor("#F8F8F8") }}转载自https://www.cnblogs.com/zhongcx/articles/18433330
-
星星之火,可以燎原——从鸿蒙小白到校园布道师的故事亲爱的开发者朋友们:大家好!今天想和大家分享一段关于梦想、挑战和传承的故事——也就是我从鸿蒙小白到校园布道师的旅程。接过火种今年年初,我从前辈的手中,接过了鸿蒙的火种。那时,我只是一个对科技充满好奇的普通学生,但在华为校园公开课上,李林峰老师的生动讲解点燃了我心中的火苗。HarmonyOS NEXT的全新架构、分布式软总线、超级终端...这些前沿技术让我看到了一个全场景的万物互联的智慧世界。我决定投身于鸿蒙开发,开始了我的技术探险。微光成炬这条从0到1的开发之路却并不简单。当时正处于鸿蒙NEXT生态建设初期,开发文档还不够完善,网络传输、滑动卡顿、卡片数据同步等问题分分钟让我想要放弃。但是,就像火苗在风中摇曳,却始终不灭,我在广大开发者朋友们的帮助和自身努力下,逐渐克服了这些困难。今年4月份,仅用一个月的时间,我和小伙伴们不仅开发上架了应用,还获了奖。我们的项目,采用HarmonyOS NEXT的最新技术特性,实现了跨设备接续,支持多设备协同和同步的全新体验。如此丰富和全面的鸿蒙开放能力,如此简单的开发语言和配套工具,帮助我们迅速将脑海中的想象变为现实。在校园调查中,逸校园的满意度高达95%,同学们对鸿蒙原生应用带来的原生精致、易用、流畅、安全、智能、互联的使用体验一致地好评。造炬成阳在开发过程中,我们受到了徐彬老师的极大帮助。徐老师是一位华为开发者布道师,他通过深入浅出的课堂帮助我们快速入门。这让我意识到,作为开发者,我们不仅要做技术的探索者,也要做技术的传播者。今年8月份,我也成为了一名华为开发者布道师,立志将鸿蒙技术的力量带给更多同学。我在校内筹办了华为创新俱乐部,开始推广鸿蒙技术。我通过实际操作和案例演示,展示鸿蒙技术的独特魅力。半年的时间里,我已经将手中的火苗传递给了200多名同学。星星之火,可以燎原。星星之火,可以燎原作为一名校园开发者,我深知技术创新的力量。我相信,在华为技术的支持下,未来会有更多像我一样的开发者涌现出来。作为华为开发者布道师的一员,无论是老师、学生,还是企业布道师,我们在逐渐成长为照亮全国乃至全球的火炬。朋友们,让我们一起,用千万火苗,照亮鸿蒙新世界的每一个角落。谢谢大家!
-
2024年11月22日,华为开发者布道师于人民大学站开展布道师华为ICT创新赛和编程赛技术案例分享技术布道案例分享,并在分享中对相关疑问分析解答。首先介绍了基于鸿蒙生态的低光照增强安全辅助驾驶系统:低光照增强安全辅助驾驶系统以华为云作为训练的平台,以MindSpore作为框架,通过整合红外相机与可见光相机的特征,利用特征互补模型在软件平台控制下进行数据处理。系统采用Hispark IPC通讯协议实现各设备间的通信,包括红外相机、可见光相机、深度相机以及车辆显示屏等,这些设备采集的数据会上传到华为云进行进一步的处理和存储。在华为云的平台上,智能决策单元和传感器单元协同工作,收集并分析环境信息,同时应用云端算力对算法模型进行部署以优化系统性能。系统还具备低光照工作能力,能够实时显示感知数据,并在必要时进行决策预警,确保驾驶安全。此外,系统还展示了与Harmony OS生态的兼容,如手表等设备的联动提醒,以及红外点阵投影等高级功能,为驾驶者提供全面的安全辅助。并介绍了技术方案所涉及的关键技术:(1)传感器标定与图像配准技术:应用传感器标定与图像配准技术,对传感器采集到的图像数据进行进一步的配准以及标定。其中,本技术包括了双目视觉系统、交通目标识别方法、深度迭代配准算法来实现图像标定以及配准。其中,双目视觉系统基于深度迭代的配准方法主要使用CNN进行特征的提取或者代替传统配准算法中相似性度量的计算函数。使用深度学习网络对输入的图像对进行特征提取与相似性测度,能够实现配准图像的生成与图像配准精度的判别。在实际检测中,大大降低了系统的计算时间,提高了图像配准以及标定的精度。(2)图像特征融合感知技术在图像处理过程中还应用到了图像特征融合感知技术。在对标定以及配准好的图像进行特征融合的过程中,项目团队通过Mindspore训练的AI神经运算专用芯片,运用多源数据融合感知技术与交通安全隐患的智能检测从而识别不同模型,实现多源数据的精确融合。最终,在这些技术的应用下,通过不同类型相机采集到的图像特征进行融合感知,在低光照等不同恶劣环境(3)辅助决策预警与风险评估技术在辅助决策以及预警层面应用HiSpark-WiFi-IoT套件为主的智能决策模块来进行实现。通过紧急避障系统中搭建的感知层、规划层、决策层实现安全隐患智能评定与决策预警的相关功能,为显示交互单元提供安全数据信息。在辅助决策预警与风险评估的功能中,终端显示给用户也是十分关键的。所以在显示与交互的单元中开发了搭载Harmony OS的显示终端、多频报警音响等显示报警装置,运行配套开发的鸿蒙APP,实时显示感知数据与决策预警判定结果,可以在低光照、雨雪雾霾等全天候全场景工况下辅助驾驶人员进行判断,提升驾驶安全性与可靠性。接着介绍了基于华为云的制造业咨询服务微调问答助手案例:构建了私有的工业中文知识库问答系统,旨在为用户提供自定义友好、离线可运行的全面解决方案。我们的系统的亮点包括:多模态交互。支持文本输入、语音输入和文件上传等多种输入方式,实现了多模态交互。用户可以根据实际需求选择最方便的交互方式,提高了系统的灵活性和适用性。同时,多模态交互也考虑到了用户的个体差异,满足了不同用户的使用习惯和需求。多功能对话界面。对话界面不仅支持基本的文本对话,还提供了多会话管理、对话模式切换和语音输入等功能。用户可以方便地切换不同对话会话,选择不同对话模式,以及通过语音输入进行自然交互。这种多功能对话界面提升了用户体验,使得系统更易于使用和操作。权限管理与安全性。系统引入了权限管理功能,通过登录注册和权限认证,确保只有经过授权的用户才能访问和操作知识库。这一功能提高了系统的安全性,防止未经授权的访问和操作,保护敏感信息免受泄露和篡改。知识库管理。系统提供了完整的知识库管理功能,包括知识库创建、文件上传、向量数据库构建、文件检索等。用户可以方便地管理知识库中的信息,通过文件对话和知识库问答等方式进行信息检索,满足不同场景和需求。检索增强的大模型。系统整合了检索增强的大模型,包括文件对话、知识库问答、搜索引擎问答和自定义Agent问答等功能。通过这些功能,系统可以更全面、准确地回答用户的问题,提供更丰富的信息服务。同时,用户可以选择不同的检索方式,根据实际需求获取最合适的答案。基于华为云ModelArts、企业级华为云主机的详细解决方案如下所示:首先介绍使用智能问答助手连接企业知识库实时获取信息。在权限管理方面,智能问答助手上运行着基于用户认证机制,利用安全框架,对用户进行身份验证,确保只有授权用户才能访问知识库;使用加密协议上传用户信息到云端服务中;使用专用SDK与API能够方便地对用户权限进行控制与管理。在系统实现方面,我们使用了微服务架构通过streamlit-authenticator模块对用户进行身份验证;并将认证结果通过后端服务传输到前端界面进行展示;前端界面使用HTML、CSS和JavaScript技术,通过发送请求获得认证结果,使用WebSocket技术实时更新用户界面。其次,介绍整合多模态对话输入技术,实现了与用户的高效交互。它支持传统的文本输入方式,同时引入了先进的语音输入识别技术,能够准确捕捉并实时转换用户的语音指令为文字,以便进行后续处理。这一语音输入功能的实现,依托于Streamlit和Bokeh库,通过在用户界面上设置一个专门的按钮,用户点击后即可激活语音识别。系统内部,js_on_event事件监听器与webkitSpeechRecognition对象协同工作,确保语音识别的准确性和实时性。此外,Omniind还提供了多种对话模板供用户选择,以适应不同咨询场景的需求,从而提升企业咨询服务的质量和效率。最后说明问答助手在知识库构建方面,预先构建了一个事实对应的知识库,用于存储和组织信息。当用户提出问题时,系统会进行问题解析,然后通过知识检索在知识库中寻找相关信息,最终生成答案。在实现过程中,助手采用了知识库构建、问题解析、知识检索和答案生成等一系列步骤,避免推理跳跃问题,确保了回答的准确性和相关性。为了验证系统的有效性,进行了功能测试。测试中,基于自定义数据集创建了Digital Twin领域的知识库,并提出了一系列问题,观察系统是否能够正确检索知识库并给出相关答案。此外,系统还提供了一个用户界面,用户可以通过这个界面与知识库进行交互,提出问题并获取答案。这个界面设计直观,易于使用,使得用户能够方便地获取所需的信息。通过这种方式,Omniind智能问答助手不仅提高了信息检索的效率,也提升了用户获取知识的体验。本次项目展示是华为开发者布道师首次对ICT实战技术案例及华为云企业级边云部署行业解决方案进行高校技术布道,希望后续能够带给大家更多具有行业价值和实践意义的布道案例 。欢迎大家加入华为开发者布道师的大家庭,成为优秀的华为云开发者!
-
直播主题:手把手教你玩转鸿蒙短视频应用案例直播时间:2024.12.09 16:00-18:00直播链接:cid:link_1直播老师:Cindy 华为云学堂技术讲师 直播简介:本期直播带你了解鸿蒙应用的基础组件和容器组件,通过短视频应用案例进行UI界面的搭建,熟悉组件的核心属性和事件,助力开发者高效开发!活动信息:鸿蒙应用入门级开发者认证训练营旨在助力开发者从零基础学习到实现鸿蒙应用构建的快速入门!我们将由华为云专家团队打造的免费精品认证课程,并安排7场专家直播授课,以及超过2000份总价值50万的认证代金券与云资源代金券,600份认证激励奖品考证就送!活动报名链接:云学堂鸿蒙应用入门级开发者认证训练营_开发者学堂-华为云往期直播回顾:【第一期】深度解析鸿蒙应用入门级开发者认证cid:link_2【第二期】鸿蒙应用入门:轻松掌握ArkTS开发语言cid:link_3
-
【活动简介】鸿蒙应用入门级开发者认证训练营旨在助力开发者从零基础学习到实现鸿蒙应用构建的快速入门!我们将由华为云专家团队打造的免费精品认证课程,并安排7场专家直播授课,以及超过2000份总价值50万的认证代金券与云资源代金券,600份认证激励奖品考证就送!【活动报名链接】cid:link_3【邀请好友奖励】云资源代金券申请链接:cid:link_2【邀请好友】进度公示(统计数据截止到2024/12/31 8:43)-具体名单可看附件查看链接:https://doc.weixin.qq.com/sheet/e3_AX0AjQaEALkSZt249ZISNKeLGAzNm?scode=ANEAbQegAA8Ei7NYfJAX0AjQaEALk&tab=BB08J2【考证通过】进度公示(统计数据截止到2024/12/31 11:00)-具体名单可看附件查看链接: https://doc.weixin.qq.com/sheet/e3_AX0AjQaEALk7sWNpVt1R0OPROU7XO?scode=ANEAbQegAA8l6ml7CwAX0AjQaEALk&tab=BB08J2【关于邀请有礼】 点击活动页面右上角“分享有礼”按钮生成邀请链接,邀请好友即可获得云资源代金券。(可用于免费购买云服务器资源)邀请人数云资源代金券350元5100元15300元25500元501000元
上滑加载中
推荐直播
-
HDC深度解读系列 - Serverless与MCP融合创新,构建AI应用全新智能中枢2025/08/20 周三 16:30-18:00
张昆鹏 HCDG北京核心组代表
HDC2025期间,华为云展示了Serverless与MCP融合创新的解决方案,本期访谈直播,由华为云开发者专家(HCDE)兼华为云开发者社区组织HCDG北京核心组代表张鹏先生主持,华为云PaaS服务产品部 Serverless总监Ewen为大家深度解读华为云Serverless与MCP如何融合构建AI应用全新智能中枢
回顾中 -
关于RISC-V生态发展的思考2025/09/02 周二 17:00-18:00
中国科学院计算技术研究所副所长包云岗教授
中科院包云岗老师将在本次直播中,探讨处理器生态的关键要素及其联系,分享过去几年推动RISC-V生态建设实践过程中的经验与教训。
回顾中 -
一键搞定华为云万级资源,3步轻松管理企业成本2025/09/09 周二 15:00-16:00
阿言 华为云交易产品经理
本直播重点介绍如何一键续费万级资源,3步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签