# 图片混合案例 **Repository Path**: shiwawaye/image-mixing-case ## Basic Information - **Project Name**: 图片混合案例 - **Description**: 以学习为组 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-07-04 - **Last Updated**: 2024-07-04 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 图片混合案例 #### 介绍 本实例主要通过BlendMode属性来实现挂件和图片的混合,通过更改不同的混合参数,能够展示不同的混合效果。 此文档为“以学习为组”的解说文档。 #### 效果图预览 ![输入图片说明](imageblend_mode.gif) #### 使用说明 进入页面,点击挂件区域,进行挂件和图片的混合,点击不同的挂件,展示不同的挂件和混合效果,再次点击取消混合效果以及挂件。 #### 重要知识点 数据通过LazyForEach进行遍历 #### 模块类型 ``` blendmode // har类型 |---model | |---DataSource.ets // 数据资源 | |---DataType.ets // 数据类型 | |---MockData.ets // 模拟数据 |---view | |---BlendModeView.ets // 视图层-应用主页面 ``` #### 模块依赖 本实例依赖common模块来实现日志的打印、资源 的调用、依赖动态路由模块来实现页面的动态加载。 #### 实现思路 首先将挂件Image组件绑定BlendMode属性,currentBlendMode控制混合模式更改的变量,初始化为BlendMode.NONE,不进行任何混合操作, 点击挂件区域,切换不同的混合模式达到不同的混合效果。 1、挂件Image组件绑定BlendMode属性,属性值初始化为BlendMode.NONE。源码参考BlendModeView.ets。 ``` Image(this.currentUserPendant) .width($r('app.integer.blend_mode_image_size')) .height($r('app.integer.blend_mode_image_size')) .borderRadius($r('app.integer.blend_mode_image_border_radius')) .blendMode(this.currentBlendMode, BlendApplyType.OFFSCREEN) ``` 2、点击挂件区域,通过currentBlendMode变量来改变混合模式。源码参考BlendModeView.ets。 ``` if (this.currentBlendMode === item.blendMode) { this.currentBlendMode = BlendMode.DST; this.currentUserPendant = ''; this.currentIndex = -1; return; } // TODO:知识点:点击切换混合模式 this.currentIndex = index; this.currentUserPendant = item.pendantImage; this.currentBlendMode = item.blendMode; ``` #### 详细说明 ###### DataType.ets ``` /** * 数据类型 */ //用于存储与挂坠(pendant)相关的信息。 export class PendantType { //用于存储挂坠的资源字符串 pendantImage: ResourceStr; //用于指定挂坠的混合模式 blendMode: BlendMode; //`PendantType` 类的构造函数 constructor(pendantImage: ResourceStr, blendMode: BlendMode = BlendMode.NONE) { // 将构造函数传入的 `pendantImage` 参数赋值给当前实例的 `pendantImage` 属性。 this.pendantImage = pendantImage; // 将构造函数传入的 `blendMode` 参数赋值给当前实例的 `blendMode` 属性,若未传入则使用默认值 `BlendMode.NONE`。 this.blendMode = blendMode; } } ``` 这段代码定义了一个名为PendantData的常量数组,数组中的每个元素都是PendantType类型的实例。PendantType是一个自定义类型,它有两个属性:imageUrl和blendMode。在数组的初始化过程中,通过构造函数new PendantType创建了每个元素,并给它们分别指定了对应的imageUrl和blendMode值。这些值通过调用$r函数获取,并将返回的字符串作为参数传递给PendantType构造函数。BlendMode是一个枚举类型,提供了多种混合模式的选择。 ###### MockData.ets ``` import { PendantType } from './DataType'; //引用了我们自己创建的一个对象,每一行创建了一个对象,存入数组中 export const PendantData: PendantType[] = [ new PendantType($r("app.media.blend_mode_image_1"), BlendMode.DST), new PendantType($r("app.media.blend_mode_image_2"), BlendMode.MULTIPLY), new PendantType($r("app.media.blend_mode_image_3"), BlendMode.SRC_OVER), new PendantType($r("app.media.blend_mode_image_4"), BlendMode.SCREEN), new PendantType($r("app.media.blend_mode_image_5"), BlendMode.SRC_ATOP), new PendantType($r("app.media.blend_mode_image_7"), BlendMode.EXCLUSION), ]; ``` PendantType是一个类,表示挂件类型,包含两个属性:pendantImage表示挂件图片资源,blendMode表示混合模式。该类有一个构造函数,用于初始化挂件图片资源和混合模式,其中混合模式可选,默认为BlendMode.NONE。 ###### BlendModeView.ets ``` @Component export struct BlendModeView { @Builder // 这个是标题栏,Text获取的是字符串资源,.fontSize).width().textAlign()是对标题的样式设置 titleBar() { Text($r('app.string.blend_mode_avatar_pendant')) .fontSize($r('app.integer.blend_mode_title_font_size')) .width($r('app.string.blend_mode_container_size')) .textAlign(TextAlign.Center) } build() { Column() { // 标题 this.titleBar() // 组件 PendantDisplay() }.width($r('app.string.blend_mode_container_size')) .height($r('app.string.blend_mode_container_size')) .backgroundColor(Color.White) .padding($r('app.string.ohos_id_card_padding_start')) } } @Entry @Component struct PendantDisplay { // pendantData是一个PendantDataSource类型的变量,用于存储挂件数据源。 // currentUserPendant是一个ResourceStr类型的变量,用于存储当前用户的挂件资源字符串。 // currentBlendMode是一个BlendMode类型的变量,用于存储当前的混合模式,初始值为BlendMode.NONE。 // currentIndex是一个number类型的变量,用于存储当前挂件的索引值,初始值为0。 // Context是获取当前上下文对象。 @State pendantData: PendantDataSource = new PendantDataSource(); @State currentUserPendant: ResourceStr = ''; @State currentBlendMode: BlendMode = BlendMode.NONE; @State currentIndex: number = 0; private context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext; // 用于在组件即将出现时向挂件数据中添加新数据。它通过调用this.pendantData.pushData(PendantData)来实现, // 其中pendantData是组件实例中的挂件数据对象,pushData是向挂件数据中添加数据的方法, // PendantData是要添加的六个挂件图片。 aboutToAppear(): void { this.pendantData.pushData(PendantData); } //提供了快速同步访问应用资源中的字符串,在后面调用读取挂件资源。 getResourceString(resourceMsg: Resource): string { return this.context.resourceManager.getStringSync(resourceMsg.id); } ``` BlendModeView是一个组件结构体,包含titleBar()和build()。标题栏通过titleBar方法构建,使用了Text组件读取string.blend_mode_avatar_pendant中的内容,并设置了字体大小、宽度和居中对齐。方法 build():构建整个组件的布局和样式。布局结构:使用Column垂直布局容器,包含两部分内容——上面是定义的标题栏titleBar(),下面是PendantDisplay组件,对整个组件进行样式设置。 PendantDisplay是一个结构体,其中定义了四个状态变量和一个私有变量,pendantData是一个PendantDataSource类型的变量,用于存储挂件数据源。 currentUserPendant是一个ResourceStr类型的变量,用于存储当前用户的挂件资源字符串。 currentBlendMode是一个BlendMode类型的变量,用于存储当前的混合模式,初始值为BlendMode.NONE。 currentIndex是一个number类型的变量,用于存储当前挂件的索引值,初始值为0。 Context是获取当前上下文对象。 aboutToAppear是arkts库中的一个方法,用于在组件即将出现时向挂件数据中添加新数据。它通过调用this.pendantData.pushData(PendantData)来实现,其中pendantData是组件实例中的挂件数据对象,pushData是向挂件数据中添加数据的方法,PendantData是要添加的六个挂件图片。 getResourceString函数提供了快速同步访问应用资源中的字符串。 ``` /** * 自定义组件构建器,用于创建挂件展示区域。 * * 该构建器旨在通过提供的挂件数据和索引,构建一个包含挂件图像和索引编号的展示单元。 * 展示单元的背景颜色和文本样式会根据当前选中的挂件索引进行动态调整。 * * @param item 挂件数据对象,包含挂件的图像信息。 * @param index 挂件的索引编号,用于区分不同的挂件。 */ @Builder pendantBuilder(item: PendantType, index: number) { // 主要展示区域,包含挂件图像和文本。 Column() { // 挂件图像展示区域。 Column() { Image(item.pendantImage) .width($r('app.integer.blend_mode_image_size')) .height($r('app.integer.blend_mode_image_size')) .borderRadius($r('app.integer.blend_mode_image_border_radius')) } .justifyContent(FlexAlign.Center) // 根据当前选中状态,设置背景颜色。 .backgroundColor(this.currentIndex === index ? $r('app.color.blend_mode_pendant_area_selected_color') : $r('app.color.blend_mode_pendant_area_default_color')) .height($r('app.integer.blend_mode_pendant_area_height_size')) .width($r('app.string.blend_mode_container_size')) // 挂件索引编号展示区域。 Text(`${this.getResourceString($r('app.string.blend_mode_pendant'))}${index}`) .width($r('app.string.blend_mode_container_size')) .fontSize($r('app.integer.blend_mode_pendant_name_font_size')) .textAlign(TextAlign.Center) // 设置与上部元素的间距。 .margin({ top: $r('app.string.ohos_id_elements_margin_vertical_m') }) } } ``` 该函数pendantBuilder是用于构建挂件用户界面组件的方法,接收两个参数:item表示挂件类型和index表示当前挂件的索引。 嵌套布局: 首先创建了一个外层的Column垂直布局容器。 在这个外层容器内,又嵌套了一个内层的Column容器,用于进一步组织内容。  图片展示: 在内层Column中添加了一个Image组件来显示挂件图片,其尺寸由应用资源定义的blend_mode_image_size决定,并设置了圆角大小为blend_mode_image_border_radius,使得图片呈现圆形或圆角矩形样式。  背景与选中效果: 外层Column设置了背景颜色,该颜色会根据当前挂件索引index与组件维护的当前选中索引currentIndex进行比较来动态改变。如果匹配,则背景色为选中颜色blend_mode_pendant_area_selected_color;否则,使用默认颜色blend_mode_pendant_area_default_color。这提供了视觉上的选中反馈。 同时,外层Column的高度和宽度也由应用资源定义,确保了挂件区域的一致性。  文字标签: 在图片下方,添加了一个Text组件来显示挂件的索引信息(如"挂件1"、"挂件2"等),字符串前缀通过getResourceString方法获取国际化字符串资源。 文字的宽度、字体大小、对齐方式以及与上层元素的间距都根据应用资源设定,确保良好的阅读体验和布局一致性。 综上所述,pendantBuilder函数是用于生成具有统一风格、可响应选中状态变化的挂件UI组件,适用于列表或网格中展示多个挂件时的界面构建。 ``` build() { Column() { Column() { Image(this.currentUserPendant) .width($r('app.integer.blend_mode_avatar_image_size')) .height($r('app.integer.blend_mode_avatar_image_size')) .borderRadius($r('app.integer.blend_mode_avatar_image_border_radius')) .blendMode(this.currentBlendMode, BlendApplyType.OFFSCREEN) // TODO:知识点:将当前控件的内容(包含子节点内容)与下方画布(可能为离屏画布)已有内容进行混合 } .width($r('app.integer.blend_mode_avatar_image_size')) .height($r('app.integer.blend_mode_avatar_image_size')) .borderRadius($r('app.integer.blend_mode_avatar_image_border_radius')) .margin($r('app.integer.blend_mode_avatar_area_margin_size')) .backgroundImage($r("app.media.blend_mode_image_6")) .backgroundImageSize({ width: $r('app.integer.blend_mode_avatar_image_size'), height: $r('app.integer.blend_mode_avatar_image_size') }) .justifyContent(FlexAlign.Center) GridRow({ columns: 9, // 将GridRow所占区域宽度平分为9份 gutter: { x: $r('app.integer.blend_mode_grid_space'), y: $r('app.integer.blend_mode_grid_space') } }) { // 性能知识点:动态加载数据或者数据量比较多的情况下,建议使用LazyForEach,参考链接https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V1/arkts-rendering-control-lazyforeach-0000001580345086-V1 LazyForEach(this.pendantData, (item: PendantType, index: number) => { // columns/span计算列数 GridCol({ span: { xs: 3, sm: 3, md: 2, lg: 2 } }) { this.pendantBuilder(item, index) } .onClick(() => { this.currentIndex = index; if (this.currentIndex === 0) { this.currentBlendMode = BlendMode.DST; this.currentUserPendant = ''; return; } // TODO:知识点:点击切换混合模式 this.currentUserPendant = item.pendantImage; this.currentBlendMode = item.blendMode; }) }, (item: PendantType) => JSON.stringify(item)) } } } } ``` 该build函数是一个UI构建方法,主要用于构造一个复合界面元素,主要由两大部分组成:一个用户头像展示区和一个挂件选择的九宫格布局。下面是详细的分点描述: 1、用户头像展示区: 使用两个嵌套的Column组件来布局,外层用于整体布局,内层专门用于头像。 头像通过Image组件展示,其大小、边框半径通过资源配置动态设定。 应用了blendMode属性来设置图像的混合模式(通过currentBlendMode变量控制),这是一种图形处理技术,允许图像内容与其下方内容按指定模式混合,增加视觉效果。 头像周围包裹的Column还设置了背景图、内外边距以及垂直居中对齐。 2、九宫格挂件选择区: 通过GridRow组件创建一个九宫格布局,用于展示可选的挂件。列数固定为9,每格之间的间隔通过配置定义。 数据渲染采用了LazyForEach,这是一种性能优化手段,特别适用于大量数据或动态数据的场景。它会根据提供的数据源(pendantData)懒加载渲染内容。 对于每个挂件项,使用GridCol定义其在不同屏幕尺寸下的列跨度,以实现响应式布局。 每个挂件上附加了点击事件处理器,当点击某个挂件时,会更新currentIndex、currentUserPendant和currentBlendMode状态。如果点击的是第一个项(索引为0),则重置头像为默认无挂件状态;否则,应用所选挂件的图像和混合模式。 ###### DataSource.ets ``` import { PendantType } from './DataType'; class BasicDataSource implements IDataSource { // 创建一个私有属性,用来存储数据变化监听器的数组,listeners命名,DataChangeListener类型 private listeners: DataChangeListener[] = []; // 创建一个私有属性,用来存储原始数据的数组,需要进行数据迭代的数据源,originDataArray命名,PendantType类型 private originDataArray: PendantType[] = []; // 获取数据的长度 // 该函数是一个空实现的函数,返回固定的数值0。 // 它的作用可能是为了在继承该类时,提供一个默认的实现,或者是为了占位。 // 该函数没有实际的逻辑, public totalCount(): number { return 0; } // 获取指定数据项 //根据给定的索引从源数据数组中获取对应元素,并返回该元素的类型为PendantType //返回的是originDataArray数组中指定索引index的元素 // 总结:根据给定的索引从源数据数组中获取对应元素。 public getData(index: number): PendantType { return this.originDataArray[index]; } // 该方法为框架侧调用,为LazyForEach组件向其数据源处添加listener监听 //输入类型为DataChangeListener的参数listener,不返回任何值 // 总结:用于向数据源添加监听器。 registerDataChangeListener(listener: DataChangeListener): void { //检查传入的监听器是否已经在当前对象的listeners数组中 if (this.listeners.indexOf(listener) < 0) { console.info('add listener');// 在控制台打印信息,表示将要添加一个新的监听器 this.listeners.push(listener);// 将新的监听器添加到listeners数组中 } } // 该方法为框架侧调用,为对应的LazyForEach组件在数据源处去除listener监听 // 总结:用于从数据源移除监听器。 unregisterDataChangeListener(listener: DataChangeListener): void { //查找传入的监听器在listeners数组中的位置 const pos = this.listeners.indexOf(listener); //如果此监听器存在,就从监听器数组中删除他 if (pos >= 0) { console.info('remove listener'); this.listeners.splice(pos, 1); } } // 通知LazyForEach组件需要重载所有子组件 //当数据发生重新加载时,系统会通过它来触发所有注册在此处的监听器的onDataReloaded回调方法, // 从而实现数据变化的自动响应和处理机制。 // 总结:提供了notifyDataReload函数,用于通知所有监听器数据重新加载。 notifyDataReload(): void { this.listeners.forEach(listener => { listener.onDataReloaded(); }) } // 通知LazyForEach组件需要在index对应索引处添加子组件 // 这样做可以让所有监听器都知道在某个特定索引处有新数据添加 // 总结:提供了notifyDataAdd函数,用于通知所有监听器在指定索引处添加了新数据。 notifyDataAdd(index: number): void { this.listeners.forEach(listener => { listener.onDataAdd(index); }) } // 通知LazyForEach组件在index对应索引处数据有变化,需要重建该子组件 //该函数用于通知所有监听器数据发生了变化,变化的索引为参数index。 // 函数遍历监听器数组,调用每个监听器的onDataChange方法,并将index作为参数传递给它。 // 总结:用于通知所有监听器在指定索引处的数据发生了变化。 notifyDataChange(index: number): void { this.listeners.forEach(listener => { listener.onDataChange(index); }) } // 通知LazyForEach组件需要在index对应索引处删除该子组件 //当需要从LazyForEach组件中移除一个子组件时,通过调用此函数并提供子组件对应的数组索引, // 可以确保所有相关的监听器都被恰当地通知到这一变化,从而同步更新UI或内部状态。 // 总结用于通知所有监听器在指定索引处的数据被删除。 notifyDataDelete(index: number): void { this.listeners.forEach(listener => { listener.onDataDelete(index); }) } } ``` 这是一个BasicDataSource类,这个类主要作用是用于管理和操作数据源,他构建了一个基础但强大的数据管理框架,不仅存储数据,还通过一套完善的监听和通知机制,实现了数据变化与UI更新的解耦。 为了对数据进行操作,类中定义了许多方法,使其能获取、添加、修改和删除监听器数组的数据。当数据发生变化时,它还能够精确地通知所有监听器,使得整个系统能够及时更新和响应变更,做到数据变化与UI更新的解耦。 ``` //挂件数据继承了基本数据 export class PendantDataSource extends BasicDataSource { // 挂件数据 private pendantData: Array = []; // 定义一个方法 `totalCount`,返回 `pendantData` 数组的长度,即数据的总数。 totalCount(): number { return this.pendantData.length; } //定义一个方法 `getData`,根据提供的 `index` 下标,返回对应位置的数据。 getData(index: number): PendantType { // 获取当前下标的数据 return this.pendantData[index]; } //定义一个方法 `pushData`,用于向 `pendantData` 数组中添加数据。 pushData(data: Array): void { //遍历传入的 `data` 数组。 data.forEach((item: PendantType) => { //将当前遍历到的 `item` 添加到 `pendantData` 数组的末尾。 this.pendantData.push(item); // 调用 `notifyDataAdd` 方法,通知数据已添加,并传递新数据在数组中的索引位置。 this.notifyDataAdd(this.pendantData.length - 1); }) } } ``` 这段代码展示的是一个名为`PendantDataSource`的类,它继承自`BasicDataSource`类,主要用于管理挂件数据。`PendantDataSource`类内部维护了一个私有的数组`pendantData`,用于存储挂件数据项,类型为`Array`。 `PendantDataSource`提供了三个关键的方法: 1. `totalCount()`:这是一个用于获取当前挂件数据总数的方法,它返回`pendantData`数组的长度。 2. `getData(index: number)`:这是一个用于根据指定的索引`index`获取挂件数据的方法,返回对应索引处的`PendantType`数据。 3. `pushData(data: Array)`:这是一个用于批量添加挂件数据的方法,接收一个`PendantType`类型的数组`data`,遍历这个数组并将每一个数据项`item`添加到`pendantData`数组的末尾。在每次添加完数据后,会调用`notifyDataAdd`方法,通知数据源有新数据添加,同时传入新数据在数组中的索引位置,这可能用于触发视图更新或其他依赖数据变化的逻辑。