-
2024年8月3日,全国高校计算机类课程能力提升高级研修班第六期在华为上海研究所举行。来自西安交通大学的谢涛老师作了题为“基于OpenHarmony的Web编程技术及应用开发基础”的课程改革经验分享。谢涛老师分别从OpenHarmony概述及开发环境部署、两个OpenHarmony应用开发案例(查查词典、辞艺)等方面阐述了基于OpenHarmony的Web编程技术及应用开发的课程改革情况及实践效果,最后带领全体老师一起体验了AppGallery Connect 云数据库的连接实例。
-
HarmonyOS 项目 API 使用注意事项总结1.手动导包的重要性:* 自动导包(Alt + 回车)并不总是有效,某些包需要手动输入。例如:import fs from '@ohos.file.fs'; // 需手动输入2.使用 @Kit:* 从 API 12 开始,使用 @Kit 替代 @ohos。许多论坛资料仍使用 @ohos,可能导致功能不显示。例如:要把import hilog from '@ohos.hilog'; import deviceInfo from '@ohos.deviceInfo';改为import { hilog } from '@kit.PerformanceAnalysisKit'; import { deviceInfo } from '@kit.BasicServicesKit'; * 新建 API 12 项目时,查找资料时遇到 @ohos,请参考官方文档确认对应的 @Kit 版本。3.注意手机版本:* Beta 1 和 Beta 3 之间差异显著,尽管代码在 API 12 下不报错,但在真机上可能会出现错误。确保真机版本升级到 Beta 3 以上,官方文档可能未提供相关提示。常用 API 导入示例// 日志系统import { hilog } from '@kit.PerformanceAnalysisKit';// Web 控制能力import { webview } from '@kit.ArkWeb';// 图片处理import { image } from '@kit.ImageKit';// 相册管理import { photoAccessHelper } from '@kit.MediaLibraryKit';// 呼叫管理import { call } from '@kit.TelephonyKit';// 资源管理import { resourceManager } from '@kit.LocalizationKit';// 分享功能import { systemShare } from '@kit.ShareKit';// 用户首选项import { preferences } from '@kit.ArkData';// 事件处理import { emitter } from '@kit.BasicServicesKit';// 文件操作import { fileIo as fs } from '@kit.CoreFileKit';// 网络连接管理import { connection } from '@kit.NetworkKit';// 扫码功能import { scanBarcode } from '@kit.ScanKit';// 设备信息import { deviceInfo } from '@kit.BasicServicesKit';// 剪贴板管理import { pasteboard } from '@kit.BasicServicesKit';// 窗口管理import { window } from '@kit.ArkUI';// 动画插值曲线import { curves } from '@kit.ArkUI';// 组件内容封装import { ComponentContent } from '@kit.ArkUI';// 提示框import { promptAction } from '@kit.ArkUI';// 路由管理import { router } from '@kit.ArkUI';// 页签型标题栏import { TabTitleBar } from '@kit.ArkUI';示例代码// @ohos全面替换 @kit //hilog日志系统,使应用/服务可以按照指定级别、标识和格式字符串输出日志内容,帮助开发者了解应用/服务的运行状态,更好地调试程序。import { hilog } from '@kit.PerformanceAnalysisKit';//@ohos.web.webview提供web控制能力,Web组件提供网页显示的能力。访问在线网页时需添加网络权限:ohos.permission.INTERNET,具体申请方式请参考声明权限。import { webview } from '@kit.ArkWeb';//本模块提供图片处理效果,包括通过属性创建PixelMap、读取图像像素数据、读取区域内的图片数据等。import { image } from '@kit.ImageKit';//该模块提供相册管理模块能力,包括创建相册以及访问、修改相册中的媒体数据信息等。import { photoAccessHelper } from '@kit.MediaLibraryKit';//该模块提供呼叫管理功能,包括拨打电话、跳转到拨号界面、获取通话状态、格式化电话号码等。import { call } from '@kit.TelephonyKit';//资源管理模块,根据当前configuration:语言、区域、横竖屏、Mcc(移动国家码)和Mnc(移动网络码)、Device capability(设备类型)、Density(分辨率)提供获取应用资源信息读取接口。import { resourceManager } from '@kit.LocalizationKit'//本模块提供分享数据创建及分享面板拉起的功能,提供多种系统标准分享服务,例如分享数据给其他应用、复制、打印等。//// 分享接入应用需要配置、呈现和关闭分享面板。// 分享面板的配置包括数据对象、呈现视图的方式、预览方式等。import { systemShare } from '@kit.ShareKit';//本模块对标准化数据类型进行了抽象定义与描述。import { uniformTypeDescriptor as utd } from '@kit.ArkData';//@ohos.data.preferences (用户首选项)用户首选项为应用提供Key-Value键值型的数据处理能力,支持应用持久化轻量级数据,并对其修改和查询。//// 数据存储形式为键值对,键的类型为字符串型,值的存储数据类型包括数字型、字符型、布尔型以及这3种类型的数组类型。import { preferences } from '@kit.ArkData';//错误信息打印import { BusinessError } from '@kit.BasicServicesKit';//开发者可以通过该模块引用Ability公共模块类。import { common } from '@kit.AbilityKit';//需要校验的权限名称,合法的权限名取值可在应用权限列表中查询。import { Permissions } from '@kit.AbilityKit';//UIAbility是包含UI界面的应用组件,继承自Ability,提供组件创建、销毁、前后台切换等生命周期回调,同时也具备组件协同的能力,组件协同主要提供如下常用功能:import { UIAbility } from '@kit.AbilityKit';//本模块提供应用信息查询能力,支持BundleInfo、ApplicationInfo、AbilityInfo、ExtensionAbilityInfo等信息的查询。import { bundleManager } from '@kit.AbilityKit';//Want是对象间信息传递的载体, 可以用于应用组件间的信息传递。 Want的使用场景之一是作为startAbility的参数, 其包含了指定的启动目标, 以及启动时需携带的相关数据, 如bundleName和abilityName字段分别指明目标Ability所在应用的Bundle名称以及对应包内的Ability名称。当Ability A需要启动Ability B并传入一些数据时, 可使用Want作为载体将这些数据传递给Ability B。import { Want } from '@kit.AbilityKit';//本模块提供HTTP数据请求能力。应用可以通过HTTP发起一个数据请求,支持常见的GET、POST、OPTIONS、HEAD、PUT、DELETE、TRACE、CONNECT方法。import { http } from '@kit.NetworkKit';//网络连接管理提供管理网络一些基础能力,包括获取默认激活的数据网络、获取所有激活数据网络列表、开启关闭飞行模式、获取网络能力信息等功能。import { connection } from '@kit.NetworkKit';//本模块提供默认界面扫码能力。import { scanBarcode } from '@kit.ScanKit'//本模块提供扫码公共信息。import { scanCore } from '@kit.ScanKit'//本模块提供终端设备信息查询,开发者不可配置。其中deviceInfo.marketNames可以获取设备名称,比如mate60import { deviceInfo } from '@kit.BasicServicesKit';//本模块提供了在同一进程不同线程间,或同一进程同一线程内,发送和处理事件的能力,包括持续订阅事件、单次订阅事件、取消订阅事件,以及发送事件到事件队列的能力。import { emitter } from '@kit.BasicServicesKit';//本模块主要提供管理系统剪贴板的能力,为系统复制、粘贴功能提供支持。系统剪贴板支持对文本、HTML、URI、Want、PixelMap等内容的操作。import { pasteboard } from '@kit.BasicServicesKit';//该模块为基础文件操作API,提供基础文件操作能力,包括文件基本管理、文件目录管理、文件信息统计、文件流式读写等常用功能。import { fileIo as fs } from '@kit.CoreFileKit';//该模块提供空间查询相关的常用功能:包括对内外卡的空间查询,对应用分类数据统计的查询,对应用数据的查询等。import { storageStatistics } from '@kit.CoreFileKit';//选择器(Picker)是一个封装PhotoViewPicker、DocumentViewPicker、AudioViewPicker等API模块,具有选择与保存的能力。应用可以自行选择使用哪种API实现文件选择和文件保存的功能。该类接口,需要应用在界面UIAbility中调用,否则无法拉起photoPicker应用或FilePicker应用。import { picker } from '@kit.CoreFileKit';//本模块提供设置动画插值曲线功能,用于构造阶梯曲线对象、构造三阶贝塞尔曲线对象和构造弹簧曲线对象。import { curves } from '@kit.ArkUI';//ComponentContent表示组件内容的实体封装,其对象支持在非UI组件中创建与传递,便于开发者对弹窗类组件进行解耦封装。ComponentContent底层使用了BuilderNode,相关使用规格参考BuilderNode。import { ComponentContent } from '@kit.ArkUI';//用于设置长度属性,当长度单位为PERCENT时,值为1表示100%。import { LengthMetrics } from '@kit.ArkUI';//创建并显示文本提示框、对话框和操作菜单。import { promptAction } from '@kit.ArkUI';//窗口提供管理窗口的一些基础能力,包括对当前窗口的创建、销毁、各属性设置,以及对各窗口间的管理调度。//// 该模块提供以下窗口相关的常用功能://// Window:当前窗口实例,窗口管理器管理的基本单元。// WindowStage:窗口管理器。管理各个基本窗口单元。import { window } from '@kit.ArkUI';//一种普通标题栏,支持设置标题、头像(可选)和副标题(可选),可用于一级页面、二级及其以上界面配置返回键。import { ComposeTitleBar } from '@kit.ArkUI';//本模块提供通过不同的url访问不同的页面,包括跳转到应用内的指定页面、同应用内的某个页面替换当前页面、返回上一页面或指定的页面等。//// 推荐使用Navigation组件作为应用路由框架。import { router } from '@kit.ArkUI';//页签型标题栏,用于页面之间的切换。仅一级页面适用。import { TabTitleBar } from '@kit.ArkUI'; class MyUIAbility extends UIAbility {} @Builderfunction buildText(params: object) { Column() { }.backgroundColor('#FFF0F0F0')} @Entry@Componentstruct Page14 { controller: webview.WebviewController = new webview.WebviewController(); test() { router.pushUrl({ url: 'pages/routerpage2' }) let config: window.Configuration = { name: "test", windowType: window.WindowType.TYPE_DIALOG, ctx: getContext() }; let promise = window.createWindow(config); try { promptAction.showToast({ message: 'Hello World', duration: 2000 }); } catch (error) { let message = (error as BusinessError).message let code = (error as BusinessError).code console.error(`showToast args error code is ${code}, message is ${message}`); } ; } aboutToAppear(): void { LengthMetrics.vp(3) let uiContext = this.getUIContext(); let promptAction = uiContext.getPromptAction(); let contentNode = new ComponentContent(uiContext, wrapBuilder(buildText), Object); curves.initCurve(Curve.EaseIn) // 创建一个默认先慢后快插值曲线 let documentPicker = new picker.DocumentViewPicker(getContext()); storageStatistics.getCurrentBundleStats().then((BundleStats: storageStatistics.BundleStats) => { console.info("getCurrentBundleStats successfully:" + JSON.stringify(BundleStats)); }).catch((err: BusinessError) => { console.error("getCurrentBundleStats failed with error:" + JSON.stringify(err)); }); let filePath = "pathDir/test.txt"; fs.stat(filePath).then((stat: fs.Stat) => { console.info("get file info succeed, the size of file is " + stat.size); }).catch((err: BusinessError) => { console.error("get file info failed with error message: " + err.message + ", error code: " + err.code); }); let dataXml = new ArrayBuffer(256); let pasteData: pasteboard.PasteData = pasteboard.createData('app/xml', dataXml); let innerEvent: emitter.InnerEvent = { eventId: 1 }; console.info(`marketName:${deviceInfo.marketName}`) let netConnection = connection.createNetConnection(); http.RequestMethod.POST hilog.isLoggable(0x0001, "testTag", hilog.LogLevel.INFO); const color: ArrayBuffer = new ArrayBuffer(96); // 96为需要创建的像素buffer大小,取值为:height * width *4 let opts: image.InitializationOptions = { editable: true, pixelFormat: 3, size: { height: 4, width: 6 } } image.createPixelMap(color, opts).then((pixelMap: image.PixelMap) => { console.info('Succeeded in creating pixelmap.'); }).catch((error: BusinessError) => { console.error(`Failed to create pixelmap. code is ${error.code}, message is ${error.message}`); }) //此处获取的phAccessHelper实例为全局对象,后续使用到phAccessHelper的地方默认为使用此处获取的对象,如未添加此段代码报phAccessHelper未定义的错误请自行添加 let context = getContext(this); let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context); call.makeCall("138xxxxxxxx", (err: BusinessError) => { if (err) { console.error(`makeCall fail, err->${JSON.stringify(err)}`); } else { console.log(`makeCall success`); } }); let systemResourceManager = resourceManager.getSystemResourceManager(); let data: systemShare.SharedData = new systemShare.SharedData({ utd: utd.UniformDataType.PLAIN_TEXT, content: 'Hello HarmonyOS' }); preferences.getPreferencesSync(getContext(), { name: 'myStore' }) let uiAbilityContext: common.UIAbilityContext; let permissionName: Permissions = 'ohos.permission.GRANT_SENSITIVE_PERMISSIONS'; let bundleFlags = bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION | bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_METADATA; let want: Want = { deviceId: '', // deviceId为空表示本设备 bundleName: 'com.example.myapplication', abilityName: 'EntryAbility', moduleName: 'entry' // moduleName非必选 }; // 定义扫码参数options let options: scanBarcode.ScanOptions = { scanTypes: [scanCore.ScanType.ALL], enableMultiMode: true, enableAlbum: true }; // 可调用getContext接口获取当前页面关联的UIAbilityContext scanBarcode.startScanForResult(context, options, (error: BusinessError, result: scanBarcode.ScanResult) => { }) } build() { Column() { ComposeTitleBar({ title: "标题", subtitle: "副标题", }) TabTitleBar({ // swiperContent: this.componentBuilder, // tabItems: this.tabItems, // menuItems: this.menuItems, }) } .height('100%') .width('100%') }}转载自https://www.cnblogs.com/zhongcx/articles/18440601
-
在项目中使用的系统组件和属性总结如下:使用的系统组件:1.Column:纵向布局容器,用于垂直排列子组件。2.Row:横向布局容器,用于水平排列子组件。3.Stack:层叠布局容器,用于将子组件层叠显示。4.Flex:弹性布局容器,用于创建灵活的布局。5.Scroll:滚动布局容器,用于实现滚动效果。6.Image:图片组件,用于显示图片内容。7.Text:文本组件,用于显示文本内容。8.LoadingProgress:加载指示器组件,用于展示加载过程中的指示器。9.SaveButton:保存按钮组件,用于保存图片到相册。10.Swiper:轮播图组件,用于创建轮播图效果。11.TextInput:输入框组件,用于接收用户输入。12.Search:搜索条组件,用于实现搜索功能。13.Web:网页加载组件,用于加载网页内容。使用的属性记录:* width:设置容器或组件的宽度。* height:设置容器或组件的高度。* backgroundColor:设置容器或组件的背景色。* padding:设置容器或组件的内边距。* margin:设置容器或组件的外边距。* visibility:根据属性值控制容器或组件的显示或隐藏。* onClick:设置点击事件处理器。* onAreaChange:监听容器尺寸变化。* linearGradient:设置容器或组件的背景为线性渐变色。* shadow:设置容器的阴影效果。* align:设置内部组件的位置显示方式。* constraintSize:设置容器或组件的最大宽度或高度。* transition:为容器或组件添加过渡动画效果。* maxLines:设置文本组件最多显示的行数。* onChange:监听输入内容的变化。* indicator:设置是否显示轮播指示点。* loop:设置是否开启轮播图的循环播放。* src:设置网页加载组件的源地址。* javaScriptProxy:注册JS Bridge,用于与H5页面进行数据通信。* onLoadIntercept:拦截内部跳转、重定向等链接。以上是项目中使用的系统组件和属性记录,可以作为学习和开发项目时的参考。【代码示例】import { LengthMetrics } from '@kit.ArkUI'import web_webview from '@ohos.web.webview'; @Entry@Componentexport struct Page13 { controller: web_webview.WebviewController = new web_webview.WebviewController(); @State inviteQrCodeID: string = "inviteQrCodeID" @State isShow: boolean = true @State searchValue: string = "" scripts: Array<ScriptItem> = [ // { script: this.localStorage, scriptRules: ["*"] } ]; build() { Column() { Column({ space: 10 }) { // 纵向布局容器 // ForEach() 可配合使用以动态渲染子组件 } .width('90%') // 设置容器宽度为父容器的90% .height('100%') // 设置容器高度为父容器的100% .backgroundColor("#ffffff") // 设置容器背景色为白色 .layoutWeight(1) // 占据父容器的剩余空间 .alignItems(HorizontalAlign.Center) // 水平居中对齐 .justifyContent(FlexAlign.Center) // 垂直居中对齐 .align(Alignment.TopStart) // 内容对齐方式为顶部左端 .borderWidth({ bottom: 1 }) // 设置底部边框宽度为1 .borderColor("#E3E3E3") // 设置边框颜色 .padding({ left: '20lpx', right: '20lpx' }) // 设置左右内边距为20逻辑像素 .margin({ top: '44lpx' }) // 设置顶部外边距为44逻辑像素 .visibility(this.isShow ? Visibility.Visible : Visibility.None) // 根据isShow属性控制容器的显示或隐藏 .id(this.inviteQrCodeID) // 设置组件ID,方便后续截图保存到相册 .flexGrow(1) // 当父布局为Flex时,占据父容器的剩余空间 .gesture(LongPressGesture({ repeat: false })) // 长按手势监听,设置repeat为false表示长按时不会重复触发 .onClick(() => { // 点击事件处理器 // 处理点击逻辑 }) Row() { // 横向布局容器 // ForEach() 可用于动态渲染子组件列表 } .width('100%') // 设置容器宽度为父容器的100% .height('192lpx') // 设置容器高度为192逻辑像素 .justifyContent(FlexAlign.End) // 子元素在主轴方向上靠右对齐 .backgroundColor(Color.White) // 设置容器背景色为白色 .padding({ left: '28lpx', right: '28lpx' }) // 设置左右内边距为28逻辑像素 .margin({ top: '39lpx' }) // 设置顶部外边距为39逻辑像素 .transition(TransitionEffect.OPACITY.animation({ duration: 200 })) // 添加透明度过渡动画,持续时间为200毫秒 .visibility(this.isShow ? Visibility.None : Visibility.Visible) // 根据 `this.isShow` 属性来决定是否显示该容器 .onClick(() => { // 点击事件处理器 // 执行点击操作的逻辑 }) .onAreaChange((previousArea: Area, currentArea: Area) => { // 监听容器尺寸变化 // 在容器尺寸发生变化时执行相关逻辑 }) Stack({ alignContent: Alignment.Bottom }) { // 创建一个层叠布局容器,并设置内容对其方式为底部对齐 } .width(180) // 设置容器宽度为180逻辑像素 .height(180) // 设置容器高度为180逻辑像素 .backgroundColor(0x88000000) // 设置容器背景色为半透明黑色(RGB: #000000,Alpha: 0.53) .borderRadius(10) // 设置容器的圆角半径为10逻辑像素 .margin({ left: '25lpx', top: '6lpx', right: '25lpx' }) // 设置容器的左、顶和右外边距分别为25和6逻辑像素 .align(Alignment.Top) // 设置内部组件的位置显示方式为顶部对齐 .visibility(this.isShow ? Visibility.Visible : Visibility.None) // 根据 `this.isShow` 属性值来控制容器的显示或隐藏 .onClick(() => { }) // 定义点击事件处理函数 .linearGradient({ // 设置容器背景色为线性渐变色,注意与 `.backgroundColor` 互斥 angle: 90, // 渐变角度为90度 colors: [// 渐变颜色配置 [0xFF0000, 0.0], // 红色,位置0% [0xFFFF00, 0.2], // 黄色,位置20% [0x00FF00, 0.4], // 绿色,位置40% [0x00FFFF, 0.6], // 青绿色,位置60% [0x0000FF, 0.8], // 蓝色,位置80% [0xFF00FF, 1.0]// 紫色,位置100% ] }) .shadow({ // 设置容器的阴影效果 radius: 20, // 阴影模糊半径为20逻辑像素 offsetY: 25, // 阴影垂直偏移量为25逻辑像素 offsetX: 0, // 阴影水平偏移量为0逻辑像素 color: "#bfbfbf" // 阴影颜色为浅灰色 }) Flex({ wrap: FlexWrap.Wrap }) { // 创建一个允许换行的弹性布局容器 // ForEach 通常用于内部动态渲染子组件列表 } .width('100%') // 设置容器宽度为父容器的100% .height('88lpx') // 设置容器高度为88逻辑像素 .margin({ top: '20lpx', bottom: '30lpx' }) // 设置容器的顶部和底部外边距分别为20和30逻辑像素 .borderWidth({ top: 1 }) // 设置容器顶部边框宽度为1逻辑像素 .borderColor("#cccccc") // 设置容器边框颜色为浅灰色 .visibility(this.isShow ? Visibility.Visible : Visibility.None) // 根据 `this.isShow` 属性值来控制容器的显示或隐藏 Scroll() { // 创建一个滚动布局容器 // 通常内部配合 Row() 或 Column() 使用以实现滚动效果 } .width('100%') // 设置容器宽度为父容器的100% .layoutWeight(1) // 占据父容器的剩余空间 .scrollable(ScrollDirection.Horizontal) // 设置滚动方向为水平 .scrollBar(BarState.Off) // 关闭滚动条显示 .borderWidth({ bottom: 1 }) // 设置底部边框宽度为1逻辑像素 .borderColor("#e3e3e3") // 设置边框颜色 .align(Alignment.Start) // 设置内部组件的位置显示方式为起始位置对齐 .visibility(this.isShow ? Visibility.Visible : Visibility.None) // 根据 `this.isShow` 属性值来控制容器的显示或隐藏 Image($r('app.media.app_icon'))// 图片组件 .width('120lpx')// 设置图片宽度为120逻辑像素 .height('120lpx')// 设置图片高度为120逻辑像素 .padding('10lpx')// 设置图片的内边距为10逻辑像素 .margin({ top: '25lpx' })// 设置图片的顶部外边距为25逻辑像素 .draggable(false)// 设置图片不可拖动,默认情况下不设置时可能会受外层长按手势影响 .objectFit(ImageFit.Fill)// 设置图片显示方式为填充,可能会导致图片变形 .visibility(this.isShow ? Visibility.Visible : Visibility.Hidden)// 根据 `this.isShow` 属性值来控制图片的显示或隐藏 .alt($r('app.media.app_icon'))// 设置图片加载失败时的占位图 .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])// 使图片在系统安全区域内扩展,避免被状态栏遮挡 .onClick(() => { // 点击事件处理器 // 处理点击逻辑 }) Text() { // 文本组件,可结合 ForEach 和 Span 使用 ForEach(["a", "b"], (item: string, index: number) => { // 使用 ForEach 循环遍历数组 ["a", "b"] Span(item)// 创建一个 Span 子组件,并传入当前项作为文本内容 .fontColor("#FF1919")// 设置 Span 组件的文字颜色为红色 .fontSize('24lpx')// 设置 Span 组件的字体大小为24逻辑像素 .onClick(() => { }) // 设置 Span 组件的点击事件处理器 }) } .width('100%') // 设置 Text 组件的宽度为100%,即填充其父容器的宽度 .height('120lpx') // 设置 Text 组件的高度为120逻辑像素 .textAlign(TextAlign.Center) // 设置 Text 组件内的文本水平居中对齐 .fontColor("#2E2E2E") // 设置 Text 组件内的文本颜色为深灰色 .fontSize('36lpx') // 设置 Text 组件内的字体大小为36逻辑像素 .backgroundColor("#F9F9F9") // 设置 Text 组件的背景色为浅灰色 .padding(20) // 设置 Text 组件的内边距为20逻辑像素 .margin({ bottom: '44lpx' }) // 设置 Text 组件的底部外边距为44逻辑像素 .lineHeight('60lpx') // 设置 Text 组件内文本的行间距(行高)为60逻辑像素 .lineSpacing(LengthMetrics.lpx(15)) // 设置 Text 组件内文本行与行之间的实际空白距离为15逻辑像素 .borderRadius(8) // 设置 Text 组件的圆角半径为8逻辑像素 .borderWidth('1lpx') // 设置 Text 组件的边框宽度为1逻辑像素 .borderColor("#bbbbbb") // 设置 Text 组件的边框颜色为浅灰色 .borderStyle(BorderStyle.Solid) // 设置 Text 组件的边框样式为实线 .visibility(this.isShow ? Visibility.Visible : Visibility.None) // 根据 `this.isShow` 的值来决定 Text 组件是否可见 .constraintSize({ maxWidth: '80%' }) // 设置 Text 组件的最大宽度为其父容器宽度的80% .layoutWeight(1) // 设置 Text 组件在弹性布局中的权重为1,意味着它会占据剩余的空间 .transition(TransitionEffect.OPACITY.animation({ duration: 200 })) // 为 Text 组件添加一个持续200毫秒的透明度过渡动画 .maxLines(2) // 设置 Text 组件最多显示两行文本 .onClick(() => { // 设置 Text 组件的点击事件处理器 // 在此处添加点击事件的具体逻辑 }) .onAreaChange((previousArea: Area, currentArea: Area) => { // 监听 Text 组件的尺寸变化 // 在此处添加尺寸变化时的具体逻辑 }) .linearGradient({ // 设置 Text 组件的背景为线性渐变色,注意与 `.backgroundColor` 不能同时使用 angle: 90, // 设置渐变的角度为90度,从左到右 colors: [// 设置渐变的颜色配置数组 [0xFF0000, 0.0], // 红色,位于渐变起始点(0%) [0xFFFF00, 0.2], // 黄色,位于渐变的20%位置 [0x00FF00, 0.4], // 绿色,位于渐变的40%位置 [0x00FFFF, 0.6], // 青绿色,位于渐变的60%位置 [0x0000FF, 0.8], // 蓝色,位于渐变的80%位置 [0xFF00FF, 1.0]// 紫色,位于渐变终点(100%) ] }) LoadingProgress()// 创建一个加载指示器组件,通常用于页面加载过程中展示 .color(Color.White)// 设置加载指示器的颜色为白色 .width(100)// 设置加载指示器的宽度为100单位 .height(100) // 设置加载指示器的高度为100单位 SaveButton()// 用户点击此按钮可保存图片到相册 .onClick(() => { }) // 设置点击事件处理器 Swiper() { // 创建一个轮播图组件,通常内部结合 ForEach 使用 } .indicator(false) // 设置是否显示轮播指示点 .loop(false) // 设置是否开启轮播图的循环播放 TextInput({ placeholder: '请输入邀请码' })// 创建一个输入框,带有提示信息 .width('540lpx')// 设置输入框宽度为540逻辑像素 .height('76lpx')// 设置输入框高度为76逻辑像素 .placeholderColor("#CBCBCB")// 设置提示信息文字颜色为浅灰色 .maxLength(6)// 设置输入框最多可输入的字符长度为6 .fontColor("#2E2E2E")// 设置输入内容的颜色为深灰色 .fontSize('36lpx')// 设置输入框内字体大小为36逻辑像素 .padding({ left: 0 })// 设置输入框左侧内边距为0逻辑像素 .margin({ left: '105lpx', top: '28lpx' })// 设置输入框左侧和顶部的外边距分别为105和28逻辑像素 .borderRadius(0)// 设置输入框圆角为0,即直角 .backgroundColor("#ffffff")// 设置输入框背景色为白色 .inputFilter('^[0-9a-zA-Z]*$')// 设置输入过滤规则,仅允许数字和字母输入 .borderWidth({ bottom: 1 })// 设置输入框底部边框宽度为1逻辑像素 .borderColor("#CBCBCB")// 设置输入框边框颜色为浅灰色 .type(InputType.PhoneNumber)// 设置输入类型为电话号码 .caretColor('#FF1919')// 设置输入框光标颜色为红色 .onChange((value: string) => { }) // 监听输入内容的变化 Search({ value: $$this.searchValue, placeholder: '搜索当前列表显示的单位' })// 创建一个搜索条组件,并设置初始值和提示信息 .layoutWeight(1)// 在弹性布局中占据剩余空间 .backgroundColor(Color.Transparent)// 设置搜索条背景为透明 .fontColor("#2E2E2E")// 设置搜索内容的颜色为深灰色 .placeholderColor("#CBCBCB")// 设置提示信息文字颜色为浅灰色 .borderRadius('32lpx')// 设置搜索条的圆角半径为32逻辑像素 .textFont({ size: '28lpx' })// 设置搜索条内字体大小为28逻辑像素 .onChange((value: string) => { }) // 监听输入内容的变化 Web({ src: "https://xxx", controller: this.controller })// 创建一个加载网页的组件,并设置源地址和控制器 .width('100%')// 设置组件宽度为100%,即填充其父容器宽度 .height('100%')// 设置组件高度为100%,即填充其父容器高度 .domStorageAccess(true)// 开启DOM Storage API权限,允许本地存储功能 .javaScriptAccess(true)// 允许执行JavaScript脚本,默认允许执行 .databaseAccess(true)// 开启数据库存储API权限,默认不开启 .mixedMode(MixedMode.All)// 允许HTTP和HTTPS混合模式 .fileAccess(true)// 开启应用文件系统的访问权限,默认已开启 .imageAccess(true)// 允许自动加载图片资源,默认允许 .geolocationAccess(true)// 开启地理位置权限,默认开启 .onlineImageAccess(true)// 允许从网络加载图片资源,默认允许 .mediaPlayGestureAccess(true)// 允许有声视频播放无需用户手动点击,默认需要用户点击 .backgroundColor('#ffffff')// 设置组件背景色为白色 .javaScriptOnDocumentStart(this.scripts)// 在文档开始加载前注入JavaScript脚本 .javaScriptProxy({ // 注册JS Bridge,用于与H5页面进行数据通信 name: "xx", // 注册名称,注意避免使用关键字 object: Object(), methodList: Object(), controller: this.controller, }) .onLoadIntercept((event) => { return true })// 拦截内部跳转、重定向等链接 .onControllerAttached(() => { })// 在网页加载前设置自定义UA .onPageEnd((e) => { })// 页面加载完成后,可以设置隐藏loading布局 .onShowFileSelector((event) => { return true })// 网页请求拉起相机时,由系统调用返回用户选择的图片 .onVisibleAreaChange([0.0, 1.0], () => { }) // 监听网页显示区域的变化 } .height('100%') .width('100%') }}转载自https://www.cnblogs.com/zhongcx/articles/18440599
-
【HarmonyOS】高仿华为阅读app翻页demosrc/main/ets/entryability/EntryAbility.etsimport { window } from '@kit.ArkUI';import { UIAbility } from '@kit.AbilityKit';export default class EntryAbility extends UIAbility { onWindowStageCreate(windowStage: window.WindowStage): void { let windowClass = windowStage.getMainWindowSync() let statusBarHeight = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM).topRect.height let navigationIndicatorHeight = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR).bottomRect.height AppStorage.setOrCreate('statusBarHeight', statusBarHeight) //保存状态栏高度,单位px AppStorage.setOrCreate('navigationIndicatorHeight', navigationIndicatorHeight) //保存底部导航条的高度,单位px windowClass.setWindowSystemBarEnable([]); //'status' | 'navigation' windowStage.loadContent('pages/Page40'); }}src/main/ets/pages/Page40.etsimport { promptAction } from '@kit.ArkUI'import { batteryInfo, systemDateTime } from '@kit.BasicServicesKit'@Entry@Componentstruct Page40 { // 页面信息 @Provide info: string = '设计理念\n在万物互联的时代,我们每天都会接触到很多不同形态的设备,每种设备在特定的场景下能够为我们解决一些特定的问题,表面看起来我们能够做到的事情更多了,但每种设备在使用时都是孤立的,提供的服务也都局限于特定的设备,我们的生活并没有变得更好更便捷,反而变得非常复杂。HarmonyOS 的诞生旨在解决这些问题,在纷繁复杂的世界中回归本源,建立平衡,连接万物。\n混沌初开,一生二、二生三、三生万物,我们希望通过 HarmonyOS 为用户打造一个和谐的数字世界——One Harmonious Universe。\nOne\n万物归一,回归本源。我们强调以人为本的设计,通过严谨的实验探究体验背后的人因,并将其结论融入到我们的设计当中。\nHarmonyOS 系统的表现应该符合人的本质需求。结合充分的人因研究,为保障全场景多设备的舒适体验,在整个系统中,各种大小的文字都清晰易读,图标精确而清晰、色彩舒适而协调、动效流畅而生动。同时,界面元素层次清晰,能巧妙地突出界面的重要内容,并能传达元素可交互的感觉。另外,系统的表现应该是直觉的,用户在使用过程中无需思考。因此系统的操作需要符合人的本能,并且使用智能化的技术能力主动适应用户的习惯。\nHarmonious\n一生为二,平衡共生。万物皆有两面,虚与实、阴与阳、正与反... 二者有所不同却可以很好地融合,达至平衡。\n在 HarmonyOS 中,我们希望给用户带来和谐的视觉体验。我们在物理世界中找到在数字世界中的映射,通过光影、材质等设计转化到界面设计中,给用户带来高品质的视觉享受。同时,物理世界中的体验记忆转化到虚拟世界中,熟悉的印象有助于帮助用户快速理解界面元素并完成相应的操作。\nUniverse\n三生万物,演化自如。HarmonyOS 是面向多设备体验的操作系统,因此,给用户提供舒适便捷的多设备操作体验是 HarmonyOS 区别于其他操作系统的核心要点。\n一方面,界面设计/组件设计需要拥有良好的自适应能力,可快速进行不同尺寸屏幕的开发。\n另一方面,我们希望多设备的体验能在一致性与差异性中取得良好的平衡。\n● 一致性:界面中的元素设计以及交互方式尽量保持一致,以便减少用户的学习成本。\n● 差异性:不同类型的设备在屏幕尺寸、交互方式、使用场景、用户人群等方面都会存在一定的差异性,为了给用户提供合适的操作体验,我们需要针对不同类型的设备进行差异化的设计。\n同时,HarmonyOS 作为面向全球用户的操作系统,为了让更多的用户享受便利的科技与愉悦的体验,我们将在数字健康、全球化、无障碍等方面进行积极的探索与思考。' @Provide lineHeight: number = 0 // 单行文本的高度 @Provide pageHeight: number = 0 // 每页的最大高度 @Provide totalContentHeight: number = 0 // 整个文本内容的高度 @Provide textContent: string = " " // 文本内容,默认一个空格是为了计算单行文本的高度 @Provide @Watch('totalPagesChanged') totalPages: number = 1 // 总页数 //=====页面切换动画===== @State currentPage: number = 0 // 当前页数 private DISPLAY_COUNT: number = 1 private MIN_SCALE: number = 0.75 @State pages: string[] = [] @State opacityList: number[] = [] @State scaleList: number[] = [] @State translateList: number[] = [] @State zIndexList: number[] = [] //=====定时器===== timeIntervalId: number = 0 @Provide timeStr: string = "" @Provide batterySOC: string = "" //======左右滑动判断====== @State screenStartX: number = 0 totalPagesChanged() { // 总页数变化时更新 this.pages = new Array(this.totalPages).fill(''); } aboutToDisappear(): void { clearInterval(this.timeIntervalId) } aboutToAppear(): void { this.timeIntervalId = setInterval(() => { let timestamp = systemDateTime.getTime(true) / 1000000 //因为获取的是纳秒 所以要 / 1000000 // console.info(`timestamp:${timestamp}`) const date = new Date(timestamp); const hours = ('0' + date.getHours()).slice(-2); const minutes = ('0' + date.getMinutes()).slice(-2); this.timeStr = `${hours}:${minutes}` this.batterySOC = `电量${batteryInfo.batterySOC}%` }, 1000, 0) for (let i = 0; i < this.pages.length; i++) { this.opacityList.push(1.0) this.scaleList.push(1.0) this.translateList.push(0.0) this.zIndexList.push(0) } } build() { Stack() { Page40Child()// 自定义动画变化透明度、缩放页面、抵消系统默认位移、渲染层级等 .width('100%').height('100%').visibility(Visibility.Hidden) Swiper() { ForEach(this.pages, (item: string, index: number) => { Page40Child({ index: index })// 自定义动画变化透明度、缩放页面、抵消系统默认位移、渲染层级等 .opacity(this.opacityList[index]) .scale({ x: this.scaleList[index], y: this.scaleList[index] }) .translate({ x: this.translateList[index] }) .zIndex(this.zIndexList[index]) }) } .onTouch((e) => { if (e.type === TouchType.Down && e.touches.length > 0) { // 触摸开始,记录初始位置 this.screenStartX = e.touches[0].x; } else if (e.type === TouchType.Up && e.changedTouches.length > 0) { // 当手指抬起时,更新最后的位置 let lastScreenX = e.changedTouches[0].x; if (this.screenStartX < lastScreenX && this.currentPage === 0) { promptAction.showToast({ message: "没有上一页了" }); } else if (this.screenStartX > lastScreenX && this.currentPage === this.totalPages - 1) { promptAction.showToast({ message: "没有下一页了" }); } } }) .onChange((index: number) => { console.info(index.toString()) this.currentPage = index }) .loop(false) // .height(300) .layoutWeight(1) .indicator(false) .displayCount(this.DISPLAY_COUNT, true) .customContentTransition({ // 页面移除视窗时超时1000ms下渲染树 // timeout: 1000, // 对视窗内所有页面逐帧回调transition,在回调中修改opacity、scale、translate、zIndex等属性值,实现自定义动画 transition: (proxy: SwiperContentTransitionProxy) => { if (proxy.position <= proxy.index % this.DISPLAY_COUNT || proxy.position >= this.DISPLAY_COUNT + proxy.index % this.DISPLAY_COUNT) { // 同组页面往左滑或往右完全滑出视窗外时,重置属性值 this.opacityList[proxy.index] = 1.0 this.scaleList[proxy.index] = 1.0 this.translateList[proxy.index] = 0.0 this.zIndexList[proxy.index] = 0 } else { // 同组页面往右滑且未滑出视窗外时,对同组中左右两个页面,逐帧根据position修改属性值,实现两个页面往Swiper中间靠拢并透明缩放的自定义切换动画 if (proxy.index % this.DISPLAY_COUNT === 0) { this.opacityList[proxy.index] = 1 - proxy.position / this.DISPLAY_COUNT this.scaleList[proxy.index] = this.MIN_SCALE + (1 - this.MIN_SCALE) * (1 - proxy.position / this.DISPLAY_COUNT) this.translateList[proxy.index] = -proxy.position * proxy.mainAxisLength + (1 - this.scaleList[proxy.index]) * proxy.mainAxisLength / 2.0 } else { this.opacityList[proxy.index] = 1 - (proxy.position - 1) / this.DISPLAY_COUNT this.scaleList[proxy.index] = this.MIN_SCALE + (1 - this.MIN_SCALE) * (1 - (proxy.position - 1) / this.DISPLAY_COUNT) this.translateList[proxy.index] = -(proxy.position - 1) * proxy.mainAxisLength - (1 - this.scaleList[proxy.index]) * proxy.mainAxisLength / 2.0 } this.zIndexList[proxy.index] = -1 } } }) .width('100%') .height('100%') }.width('100%').height('100%') }}@Componentstruct Page40Child { @Consume info: string @Consume lineHeight: number // 单行文本的高度 @Consume pageHeight: number // 每页的最大高度 @Consume totalContentHeight: number // 整个文本内容的高度 @Consume textContent: string // 文本内容,默认一个空格是为了计算单行文本的高度 @Consume totalPages: number // 总页数 @Consume timeStr: string @Consume batterySOC: string @State scrollOffset: number = 0 // 当前滚动偏移量 @Prop index: number = 0 scroller: Scroller = new Scroller() // 滚动条实例 resetMaxLineHeight() { if (this.lineHeight > 0 && this.pageHeight > 0 && this.totalContentHeight > 0) { this.pageHeight = (Math.floor(this.pageHeight / this.lineHeight)) * this.lineHeight this.totalPages = Math.ceil(this.totalContentHeight / this.pageHeight) //向上取整得到总页数 } } aboutToAppear(): void { this.scrollOffset = -(this.pageHeight * this.index) } build() { Column() { Text().width('100%').height(`${AppStorage.get('statusBarHeight')}px`) //顶部状态栏高度 Text('通用设计基础') .fontColor("#7a7a7a") .fontSize(10) .padding({ left: 30, top: 10, bottom: 10 }) .width('100%') Column() { Scroll(this.scroller) { Column() { Text(this.textContent) .fontSize(18) .lineHeight(36) .fontColor(Color.Black) .margin({ top: this.scrollOffset }) .onAreaChange((oldArea: Area, newArea: Area) => { if (this.lineHeight == 0 && newArea.height > 0) { this.lineHeight = newArea.height as number this.resetMaxLineHeight() //添加数据测试 this.textContent = this.info return } if (this.totalContentHeight != newArea.height) { console.info(`newArea.height:${newArea.height}`) this.totalContentHeight = newArea.height as number this.resetMaxLineHeight() } }) } .padding({ left: 25, right: 25 }) }.scrollBar(BarState.Off) .constraintSize({ maxHeight: this.pageHeight == 0 ? 1000 : this.pageHeight }) } .width('100%') .layoutWeight(1) .onAreaChange((oldArea: Area, newArea: Area) => { if (this.pageHeight == 0 && newArea.height > 0) { this.pageHeight = newArea.height as number this.resetMaxLineHeight() } }) Row() { Row() { Text(this.timeStr) .fontColor("#7a7a7a") .fontSize(10) Text(this.batterySOC) .fontColor("#7a7a7a") .fontSize(10) .margin({ left: 5 }) } Text(`${this.index + 1}/${this.totalPages}`) .fontColor("#7a7a7a") .fontSize(10) }.width('100%').padding({ left: 30, right: 30, top: 30 }).justifyContent(FlexAlign.SpaceBetween) Text().width('100%').height(`${AppStorage.get('navigationIndicatorHeight')}px`) //底部导航栏高度 } .width('100%') .height('100%') .backgroundColor("#CFE6D6") }}原理参考:cid:link_0
-
【HarmonyOS】仿照IOS中可以通过输入start=(0,0),end=(1,1)获取角度到.linearGradient,从而实现左上到右下渐变class Point { x: number = 0 y: number = 0}@Entry@Componentstruct Page57 { @State message: string = 'Hello World'; //输入start=(0,0),end=(1,1)实现左上到右下渐变 private calculateGradientAngle(start: Point, end: Point): number { // 计算两点之间的向量 const dx = end.x - start.x; const dy = end.y - start.y; // 使用 Math.atan2(dy, dx) 计算角度 // Math.atan2 返回的是弧度值,需要转换为角度 const radian = Math.atan2(dy, dx); const degree = radian * (180 / Math.PI); console.info(`degree:${degree}`) // 根据实际情况调整角度 // 从左上角到右下角的角度通常是 45 度 return (90 + degree) % 360; } build() { Column() { Text('背景渐变') Row() { Text(this.message) .fontSize(50) .fontWeight(FontWeight.Bold) //.blendMode(BlendMode.DST_IN, BlendApplyType.OFFSCREEN) }.linearGradient({ angle: this.calculateGradientAngle({ x: 0, y: 0 }, { x: 1, y: 1 }), colors: [[0xff0000, 0.0], [0x0000ff, 1.0]] }) //.blendMode(BlendMode.SRC_OVER, BlendApplyType.OFFSCREEN) Text('文字渐变') Row() { Text(this.message) .fontSize(50) .fontWeight(FontWeight.Bold) .blendMode(BlendMode.DST_IN, BlendApplyType.OFFSCREEN) }.linearGradient({ angle: this.calculateGradientAngle({ x: 0, y: 0 }, { x: 1, y: 1 }), colors: [[0xff0000, 0.0], [0x0000ff, 1.0]] }).blendMode(BlendMode.SRC_OVER, BlendApplyType.OFFSCREEN) } .width('100%') .height('100%') }}转载自https://www.cnblogs.com/zhongcx/articles/18433360
-
利用emitter封装工具类,使得父组件与子组件实现事件监听。注意:只能用真机调试,在预览器会提示The emitter.emit interface in the Previewer is a mocked implementation and may behave differently than on a real device.调用示例import { MyEmitterUtil } from '../MyEmitterUtil'@Componentstruct MyView { @Prop controller: MyEmitterUtil @State info: string = "" aboutToAppear(): void { this.controller.onChild((eventData) => { console.info('====eventData', JSON.stringify(eventData)) if (eventData.flag == "ABC") { this.info = eventData.data } }) } build() { Column() { Text('子组件接收到的数据:' + this.info) Button('向父组件发送数据').onClick(() => { this.controller.emitFather("CCC", "EEE") }) } }}@Entry@Componentstruct Page81 { controller: MyEmitterUtil = new MyEmitterUtil() @State info: string = "" aboutToAppear(): void { this.controller.onFather((eventData) => { console.info('====eventData', JSON.stringify(eventData)) if (eventData.flag == "CCC") { this.info = eventData.data } }) } build() { Row() { Column() { Text('父组件接收到的数据:' + this.info) Button('向子组件发送数据').onClick(() => { this.controller.emitChild("ABC", "conter") }) MyView({ controller: this.controller }) } .width('100%') } .height('100%') }}工具类import Emitter from '@ohos.events.emitter';/** * `MyEmitterUtil` 是一个针对 HarmonyOS 的事件驱动编程封装类,主要用于组件间的通信和数据传递。 * * 使用要求: * - API 版本:api 11 * 示例用法: * 1. 父组件绑定、解绑、向子组件发送事件: * ```typescript * aboutToAppear() { * this.myEmitterUtil.onFather((eventData: EmitterData) => { * console.info('父组件监听结果: ', JSON.stringify(eventData)); * // 判断事件类型并执行相应操作... * }); * } * * aboutToDisappear() { * this.myEmitterUtil.offFather(); * } * * // 向子组件发送事件 * this.myEmitterUtil.emitChild(MyEmitterUtil.UPDATE_DETAIL, "携带的测试数据"); * ``` * * 2. 子组件绑定、解绑、向父组件发送事件: * ```typescript * aboutToAppear() { * this.myEmitterUtil.onChild((eventData: EmitterData) => { * console.info('子组件监听结果: ', JSON.stringify(eventData)); * // 判断事件类型并执行相应操作... * }); * } * * aboutToDisappear() { * this.myEmitterUtil.offChild(); * } * * // 向父组件发送事件 * this.myEmitterUtil.emitFather(MyEmitterUtil.UPDATE_LIST, "测试"); * this.myEmitterUtil.emitFather(MyEmitterUtil.UPDATE_LIST_2, "测试2"); * ``` * * 参考文档: * - 请查阅 HarmonyOS 开发文档了解详细信息。 */export class MyEmitterUtil { private static EVENT_ID_COUNTER: number = 0; // 自动递增,生成唯一的事件ID private readonly eventIdFather: number; private readonly eventIdChild: number; constructor() { this.eventIdFather = MyEmitterUtil.EVENT_ID_COUNTER++; this.eventIdChild = MyEmitterUtil.EVENT_ID_COUNTER++; console.info(`事件ID(父组件): ${this.eventIdFather}`); console.info(`事件ID(子组件): ${this.eventIdChild}`); } // 定义业务状态标识常量 static readonly UPDATE_LIST = "UPDATE_LIST"; static readonly UPDATE_LIST_2 = "UPDATE_LIST_2"; static readonly UPDATE_DETAIL = "UPDATE_DETAIL"; /** * 在组件的`aboutToAppear`生命周期钩子中调用,监听父组件事件 * @param callback 事件回调函数,接受一个`EmitterData`对象作为参数 */ onFather(callback: (eventData: EmitterData) => void) { Emitter.on({ eventId: this.eventIdFather }, (event) => { if (event.data) { callback(new EmitterData(event.data.flag, event.data.data)); } }); } /** * 在组件的`aboutToDisappear`生命周期钩子中调用,解除父组件事件监听 */ offFather() { Emitter.off(this.eventIdFather); } /** * 在组件的`aboutToAppear`生命周期钩子中调用,监听子组件事件 * @param callback 事件回调函数,接受一个`EmitterData`对象作为参数 */ onChild(callback: (eventData: EmitterData) => void) { Emitter.on({ eventId: this.eventIdChild }, (event) => { if (event.data) { callback(new EmitterData(event.data.flag, event.data.data)); } }); } /** * 在组件的`aboutToDisappear`生命周期钩子中调用,解除子组件事件监听 */ offChild() { Emitter.off(this.eventIdChild); } /** * 向父组件发送事件 * @param flag 事件类型标识 * @param data 事件携带的数据 */ emitFather(flag: string, data: string) { Emitter.emit( { eventId: this.eventIdFather, priority: Emitter.EventPriority.IMMEDIATE }, { data: { flag, data } } ); } /** * 向子组件发送事件 * @param flag 事件类型标识 * @param data 事件携带的数据 */ emitChild(flag: string, data: string) { Emitter.emit( { eventId: this.eventIdChild, priority: Emitter.EventPriority.IMMEDIATE }, { data: { flag, data } } ); }}/** * 用于封装事件数据的类 */export class EmitterData { flag: string = ""; data: string = ""; constructor(flag: string, data: string) { this.flag = flag; this.data = data; }}转载自https://www.cnblogs.com/zhongcx/articles/18433359
-
【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
上滑加载中
推荐直播
-
GaussDB数据库介绍
2025/01/07 周二 16:00-18:00
Steven 华为云学堂技术讲师
本期直播将介绍GaussDB数据库的发展历程、优势、架构、关键特性和部署模式等,旨在帮助开发者了解GaussDB数据库,并通过手把手实验教大家如何在华为云部署GaussDB数据库和使用gsql连接GaussDB数据库。
去报名 -
DTT年度收官盛典:华为开发者空间大咖汇,共探云端开发创新
2025/01/08 周三 16:30-18:00
Yawei 华为云开发工具和效率首席专家 Edwin 华为开发者空间产品总监
数字化转型进程持续加速,驱动着技术革新发展,华为开发者空间如何巧妙整合鸿蒙、昇腾、鲲鹏等核心资源,打破平台间的壁垒,实现跨平台协同?在科技迅猛发展的今天,开发者们如何迅速把握机遇,实现高效、创新的技术突破?DTT 年度收官盛典,将与大家共同探索华为开发者空间的创新奥秘。
去报名 -
GaussDB应用实战:手把手带你写SQL
2025/01/09 周四 16:00-18:00
Steven 华为云学堂技术讲师
本期直播将围绕数据库中常用的数据类型、数据库对象、系统函数及操作符等内容展开介绍,帮助初学者掌握SQL入门级的基础语法。同时在线手把手教你写好SQL。
去报名
热门标签