马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
x
目次
简介
相干概念
相干权限
使用阐明
项目环境
运行截图
代码示例
DetailPage
方法
aboutToAppear
resetImg
handlePanEnd
DetailListPage
交互逻辑
UI 结构
IndexPage
标题栏 (Row)
轮播图 (Swiper)
ListPage
网格结构 (Grid)
简介
本学期学习了鸿蒙开辟课程,在课程学习中收获良多,用一个小项目分享一些学习成果。
本项目是基于ArkTS实现的一个电子相册案例,通过捏合和拖拽手势控制图片的放大、缩小、左右拖动检察细节等效果。
相干概念
- Swiper:滑块视图容器,提供子组件滑动轮播表现的能力。
- Grid:网格容器,由“行”和“列”分割的单元格所组成,通过指定“项目”所在的单元格做出各种各样的结构。
- Navigation:Navigation组件一般作为Page页面的根容器,通过属性设置来展示页面的标题、工具栏、菜单。
- List:列表包罗一系列雷同宽度的列表项。得当连续、多行出现同类数据,例如图片和文本。
- 组合手势:手势识别组,多种手势组合为复合手势,支持连续识别、并行识别和互斥识别。
相干权限
不涉及
使用阐明
- 用户可以使用捏合手势实现图片的放大缩小。
- 用户可以通过滑动手势实现图片的左右切换。
- 用户可以在图片放大的底子上,可以通过拖拽手势对图片进行拖拽检察细节。
项目环境
- HarmonyOS系统:HarmonyOS NEXT Developer Beta1及以上。
- DevEco Studio版本:DevEco Studio NEXT Developer Beta1及以上。
- HarmonyOS SDK版本:HarmonyOS NEXT Developer Beta1 SDK及以上。
运行截图
代码示例
DetailPage

- @Entry
- @Component
- struct DetailPage {
- scroller: Scroller = new Scroller();
- photoArr: Array<Resource> = (router.getParams() as Record<string, Array<Resource>>)[`${Constants.PARAM_PHOTO_ARR_KEY}`];
- preOffsetX: number = 0;
- preOffsetY: number = 0;
- currentScale: number = 1;
- @State deviceWidth: number = Constants.DEFAULT_WIDTH;
- @State smallImgWidth: number = (Constants.DEFAULT_WIDTH - Constants.LIST_ITEM_SPACE * (Constants.SHOW_COUNT - 1)) /
- Constants.SHOW_COUNT;
- @State imageWidth: number = this.deviceWidth + this.smallImgWidth;
- @StorageLink('selectedIndex') selectedIndex: number = 0;
- @State isScaling: boolean = true;
- @State imgScale: number = 1;
- @State imgOffSetX: number = 0;
- @State imgOffSetY: number = 0;
- @State bgOpacity: number = 0;
- aboutToAppear() {
- let displayClass: display.Display = display.getDefaultDisplaySync();
- let width = displayClass?.width / displayClass.densityPixels ?? Constants.DEFAULT_WIDTH;
- this.deviceWidth = width;
- this.smallImgWidth = (width - Constants.LIST_ITEM_SPACE * (Constants.SHOW_COUNT - 1)) / Constants.SHOW_COUNT;
- this.imageWidth = this.deviceWidth + this.smallImgWidth;
- }
- resetImg(): void {
- this.imgScale = 1;
- this.currentScale = 1;
- this.preOffsetX = 0;
- this.preOffsetY = 0;
- }
- handlePanEnd(): void {
- let initOffsetX = (this.imgScale - 1) * this.imageWidth + this.smallImgWidth;
- if (Math.abs(this.imgOffSetX) > initOffsetX) {
- if (this.imgOffSetX > initOffsetX && this.selectedIndex > 0) {
- this.selectedIndex -= 1;
- } else if (this.imgOffSetX < -initOffsetX && this.selectedIndex < (this.photoArr.length - 1)) {
- this.selectedIndex += 1;
- }
- this.isScaling = false;
- this.resetImg();
- this.scroller.scrollTo({ xOffset: this.selectedIndex * this.imageWidth, yOffset: 0 });
- }
- }
- build() {
- Stack() {
- List({ scroller: this.scroller, initialIndex: this.selectedIndex }) {
- ForEach(this.photoArr, (img: Resource) => {
- ListItem() {
- Image(img)
- .objectFit(ImageFit.Contain)
- .onClick(() => router.back())
- }
- .gesture(GestureGroup(GestureMode.Exclusive,
- PinchGesture({ fingers: Constants.DOUBLE_NUMBER })
- .onActionStart(() => {
- this.resetImg();
- this.isScaling = true;
- this.imgOffSetX = 0;
- this.imgOffSetY = 0;
- })
- .onActionUpdate((event?: GestureEvent) => {
- if (event) {
- this.imgScale = this.currentScale * event.scale;
- }
- })
- .onActionEnd(() => {
- if (this.imgScale < 1) {
- this.resetImg();
- this.imgOffSetX = 0;
- this.imgOffSetY = 0;
- } else {
- this.currentScale = this.imgScale;
- }
- }), PanGesture()
- .onActionStart(() => {
- this.resetImg();
- this.isScaling = true;
- })
- .onActionUpdate((event?: GestureEvent) => {
- if (event) {
- this.imgOffSetX = this.preOffsetX + event.offsetX;
- this.imgOffSetY = this.preOffsetY + event.offsetY;
- }
- })
- ))
- .padding({
- left: this.smallImgWidth / Constants.DOUBLE_NUMBER,
- right: this.smallImgWidth / Constants.DOUBLE_NUMBER
- })
- .width(this.imageWidth)
- }, (item: Resource) => JSON.stringify(item))
- }
- .onScrollStop(() => {
- let currentIndex = Math.round(((this.scroller.currentOffset().xOffset as number) +
- (this.imageWidth / Constants.DOUBLE_NUMBER)) / this.imageWidth);
- this.selectedIndex = currentIndex;
- this.scroller.scrollTo({ xOffset: currentIndex * this.imageWidth, yOffset: 0 });
- })
- .width(Constants.FULL_PERCENT)
- .height(Constants.FULL_PERCENT)
- .listDirection(Axis.Horizontal)
- .visibility(this.isScaling ? Visibility.Hidden : Visibility.Visible)
- Row() {
- Image(this.photoArr[this.selectedIndex])
- .position({ x: this.imgOffSetX, y: this.imgOffSetY })
- .scale({ x: this.imgScale, y: this.imgScale })
- .objectFit(ImageFit.Contain)
- .onClick(() => router.back())
- }
- .gesture(GestureGroup(GestureMode.Exclusive,
- PinchGesture({ fingers: Constants.DOUBLE_NUMBER })
- .onActionUpdate((event?: GestureEvent) => {
- if (event) {
- this.imgScale = this.currentScale * event.scale;
- }
- })
- .onActionEnd(() => {
- if (this.imgScale < 1) {
- this.resetImg();
- this.imgOffSetX = 0;
- this.imgOffSetY = 0;
- } else {
- this.currentScale = this.imgScale;
- }
- }),
- PanGesture()
- .onActionStart(() => {
- this.preOffsetX = this.imgOffSetX;
- this.preOffsetY = this.imgOffSetY;
- })
- .onActionUpdate((event?: GestureEvent) => {
- if (event) {
- this.imgOffSetX = this.preOffsetX + event.offsetX;
- this.imgOffSetY = this.preOffsetY + event.offsetY;
- }
- })
- .onActionEnd(() => this.handlePanEnd())
- ))
- .padding({
- left: this.smallImgWidth / Constants.DOUBLE_NUMBER,
- right: this.smallImgWidth / Constants.DOUBLE_NUMBER
- })
- .width(this.imageWidth)
- .height(Constants.FULL_PERCENT)
- .visibility(this.isScaling ? Visibility.Visible : Visibility.Hidden)
- }
- .offset({ x: -(this.smallImgWidth / Constants.DOUBLE_NUMBER) })
- .width(this.imageWidth)
- .height(Constants.FULL_PERCENT)
- .backgroundColor($r('app.color.detail_background'))
- }
- }
复制代码 这段代码定义了用于表现一组图片,并允许用户通过手势进行缩放和平移。该组件使用了鸿蒙操作系统(HarmonyOS)的声明式UI框架ArkUI来构建。
方法
aboutToAppear
这个方法会在组件即将出如今屏幕上时调用,用来初始化一些依赖于屏幕尺寸的状态变量。
resetImg
重置图像的缩放和平移值到默认状态。
handlePanEnd
当用户结束拖动手势时调用此方法。根据用户的拖动距离和方向调整 selectedIndex 并重置图像状态,同时滚动到新的选定图像位置。
DetailListPage

- @Entry
- @Component
- struct DetailListPage {
- private smallScroller: Scroller = new Scroller();
- private bigScroller: Scroller = new Scroller();
- @State deviceWidth: number = Constants.DEFAULT_WIDTH;
- @State smallImgWidth: number = (this.deviceWidth - Constants.LIST_ITEM_SPACE * (Constants.SHOW_COUNT - 1)) /
- Constants.SHOW_COUNT;
- @State imageWidth: number = this.deviceWidth + this.smallImgWidth;
- private photoArr: Array<Resource | string> = (router.getParams() as Record<string, Array<Resource | string>>)[`${Constants.PARAM_PHOTO_ARR_KEY}`];
- private smallPhotoArr: Array<Resource | string> = new Array<Resource | string>().concat(Constants.CACHE_IMG_LIST,
- (router.getParams() as Record<string, Array<Resource | string>>)[`${Constants.PARAM_PHOTO_ARR_KEY}`],
- Constants.CACHE_IMG_LIST)
- @StorageLink('selectedIndex') selectedIndex: number = 0;
- @Builder SmallImgItemBuilder(img: Resource, index?: number) {
- if (index && index > (Constants.CACHE_IMG_SIZE - 1) && index < (this.smallPhotoArr.length - Constants.CACHE_IMG_SIZE)) {
- Image(img)
- .onClick(() => this.smallImgClickAction(index))
- }
- }
- aboutToAppear() {
- let displayClass: display.Display = display.getDefaultDisplaySync();
- let width = displayClass?.width / displayClass.densityPixels ?? Constants.DEFAULT_WIDTH;
- this.deviceWidth = width;
- this.smallImgWidth = (width - Constants.LIST_ITEM_SPACE * (Constants.SHOW_COUNT - 1)) / Constants.SHOW_COUNT;
- this.imageWidth = this.deviceWidth + this.smallImgWidth;
- }
- onPageShow() {
- this.smallScroller.scrollToIndex(this.selectedIndex);
- this.bigScroller.scrollToIndex(this.selectedIndex);
- }
- goDetailPage(): void {
- router.pushUrl({
- url: Constants.URL_DETAIL_PAGE,
- params: { photoArr: this.photoArr }
- });
- }
- smallImgClickAction(index: number): void {
- this.selectedIndex = index - Constants.CACHE_IMG_SIZE;
- this.smallScroller.scrollToIndex(this.selectedIndex);
- this.bigScroller.scrollToIndex(this.selectedIndex);
- }
- smallScrollAction(type: scrollTypeEnum): void {
- this.selectedIndex = Math.round(((this.smallScroller.currentOffset().xOffset as number) +
- this.smallImgWidth / Constants.DOUBLE_NUMBER) / (this.smallImgWidth + Constants.LIST_ITEM_SPACE));
- if (type === scrollTypeEnum.SCROLL) {
- this.bigScroller.scrollTo({ xOffset: this.selectedIndex * this.imageWidth, yOffset: 0 });
- } else {
- this.smallScroller.scrollTo({ xOffset: this.selectedIndex * this.smallImgWidth, yOffset: 0 });
- }
- }
- bigScrollAction(type: scrollTypeEnum): void {
- let smallWidth = this.smallImgWidth + Constants.LIST_ITEM_SPACE;
- this.selectedIndex = Math.round(((this.bigScroller.currentOffset().xOffset as number) +
- smallWidth / Constants.DOUBLE_NUMBER) / this.imageWidth);
- if (type === scrollTypeEnum.SCROLL) {
- this.smallScroller.scrollTo({ xOffset: this.selectedIndex * smallWidth, yOffset: 0 });
- } else {
- this.bigScroller.scrollTo({ xOffset: this.selectedIndex * this.imageWidth, yOffset: 0 });
- }
- }
- build() {
- Navigation() {
- Stack({ alignContent: Alignment.Bottom }) {
- List({ scroller: this.bigScroller, initialIndex: this.selectedIndex }) {
- ForEach(this.photoArr, (img: Resource) => {
- ListItem() {
- Image(img)
- .height(Constants.FULL_PERCENT)
- .width(Constants.FULL_PERCENT)
- .objectFit(ImageFit.Contain)
- .gesture(PinchGesture({ fingers: Constants.DOUBLE_NUMBER })
- .onActionStart(() => this.goDetailPage()))
- .onClick(() => this.goDetailPage())
- }
- .padding({
- left: this.smallImgWidth / Constants.DOUBLE_NUMBER,
- right: this.smallImgWidth / Constants.DOUBLE_NUMBER
- })
- .width(this.imageWidth)
- }, (item: Resource) => JSON.stringify(item))
- }
- .onScroll((scrollOffset, scrollState) => {
- if (scrollState === ScrollState.Fling) {
- this.bigScrollAction(scrollTypeEnum.SCROLL);
- }
- })
- .scrollBar(BarState.Off)
- .onScrollStop(() => this.bigScrollAction(scrollTypeEnum.STOP))
- .width(Constants.FULL_PERCENT)
- .height(Constants.FULL_PERCENT)
- .padding({ bottom: this.smallImgWidth * Constants.DOUBLE_NUMBER })
- .listDirection(Axis.Horizontal)
- List({
- scroller: this.smallScroller,
- space: Constants.LIST_ITEM_SPACE,
- initialIndex: this.selectedIndex
- }) {
- ForEach(this.smallPhotoArr, (img: Resource, index?: number) => {
- ListItem() {
- this.SmallImgItemBuilder(img, index)
- }
- .width(this.smallImgWidth)
- .aspectRatio(1)
- }, (item: Resource) => JSON.stringify(item))
- }
- .listDirection(Axis.Horizontal)
- .onScroll((scrollOffset, scrollState) => {
- if (scrollState === ScrollState.Fling) {
- this.smallScrollAction(scrollTypeEnum.SCROLL);
- }
- })
- .scrollBar(BarState.Off)
- .onScrollStop(() => this.smallScrollAction(scrollTypeEnum.STOP))
- .margin({ top: $r('app.float.detail_list_margin'), bottom: $r('app.float.detail_list_margin') })
- .height(this.smallImgWidth)
- .width(Constants.FULL_PERCENT)
- }
- .width(this.imageWidth)
- .height(Constants.FULL_PERCENT)
- }
- .title(Constants.PAGE_TITLE)
- .hideBackButton(false)
- .titleMode(NavigationTitleMode.Mini)
- }
- }
复制代码 这段代码是一个包罗两个程度滚动列表的页面:一个用于表现较大的图片(bigScroller),另一个用于表现较小的缩略图(smallScroller)。用户可以通过点击缩略图来切换大图,或者直接在大图上进行交互。
交互逻辑
- goDetailPage(): void {
- // 导航到详情页
- }
- smallImgClickAction(index: number): void {
- // 更新选中索引并滚动到对应位置
- }
- smallScrollAction(type: scrollTypeEnum): void {
- // 根据滚动类型更新索引和滚动位置
- }
- bigScrollAction(type: scrollTypeEnum): void {
- // 根据滚动类型更新索引和滚动位置
- }
复制代码 UI 结构
- build() {
- Navigation() {
- Stack({ alignContent: Alignment.Bottom }) {
- // 大图列表
- List({ scroller: this.bigScroller, initialIndex: this.selectedIndex }) {
- // ...
- }
- // 小图列表
- List({
- scroller: this.smallScroller,
- space: Constants.LIST_ITEM_SPACE,
- initialIndex: this.selectedIndex
- }) {
- // ...
- }
- }
- }
- .title(Constants.PAGE_TITLE)
- .hideBackButton(false)
- .titleMode(NavigationTitleMode.Mini)
- }
复制代码
- Navigation 包装整个结构,并设置了标题栏配置。
- Stack 用于堆叠大图列表和小图列表。
- 每个 List 都有自己的滚动控制器 (scroller) 并绑定了滚动变乱监听器,以同步两个列表之间的滚动状态。
- ForEach 循环生成列表项,每个项目都绑定有手势识别或点击变乱处理步调。
IndexPage

- @Entry
- @Component
- struct IndexPage {
- swiperController: SwiperController = new SwiperController();
- scroller: Scroller = new Scroller();
- @State currentIndex: number = 0;
- @State angle: number = 0;
- build() {
- Column() {
- Row() {
- Text($r('app.string.EntryAbility_label'))
- .fontSize($r('app.float.title_font_size'))
- .fontWeight(Constants.TITLE_FONT_WEIGHT)
- }
- .height($r('app.float.navi_bar_height'))
- .alignItems(VerticalAlign.Center)
- .justifyContent(FlexAlign.Start)
- .margin({ top: $r('app.float.grid_padding') })
- .padding({ left: $r('app.float.title_padding') })
- .width(Constants.FULL_PERCENT)
- Swiper(this.swiperController) {
- ForEach(Constants.BANNER_IMG_LIST, (item: Resource) => {
- Row() {
- Image(item)
- .width(Constants.FULL_PERCENT)
- .height(Constants.FULL_PERCENT)
- }
- .width(Constants.FULL_PERCENT)
- .aspectRatio(Constants.BANNER_ASPECT_RATIO)
- }, (item: Resource, index?: number) => JSON.stringify(item) + index)
- }
- .autoPlay(true)
- .loop(true)
- .margin($r('app.float.grid_padding'))
- .borderRadius($r('app.float.img_border_radius'))
- .clip(true)
- .duration(Constants.BANNER_ANIMATE_DURATION)
- .indicator(false)
- Grid() {
- ForEach(Constants.IMG_ARR, (photoArr: Array<Resource>) => {
- GridItem() {
- PhotoItem({ photoArr })
- }
- .width(Constants.FULL_PERCENT)
- .aspectRatio(Constants.STACK_IMG_RATIO)
- .onClick(() => {
- router.pushUrl({
- url: Constants.URL_LIST_PAGE,
- params: { photoArr: photoArr }
- });
- })
- }, (item: Resource, index?: number) => JSON.stringify(item) + index)
- }
- .scrollBar(BarState.Off)
- .columnsTemplate(Constants.INDEX_COLUMNS_TEMPLATE)
- .columnsGap($r('app.float.grid_padding'))
- .rowsGap($r('app.float.grid_padding'))
- .padding({ left: $r('app.float.grid_padding'), right: $r('app.float.grid_padding') })
- .width(Constants.FULL_PERCENT)
- .layoutWeight(1)
- }
- .width(Constants.FULL_PERCENT)
- .height(Constants.FULL_PERCENT)
- }
- }
复制代码 这段代码定义了应用的首页或入口页面,包罗顶部的文字标题、一个轮播图(Swiper)和一个网格结构(Grid)。
标题栏 (Row)
- Row() {
- Text($r('app.string.EntryAbility_label'))
- .fontSize($r('app.float.title_font_size'))
- .fontWeight(Constants.TITLE_FONT_WEIGHT)
- }
- .height($r('app.float.navi_bar_height'))
- .alignItems(VerticalAlign.Center)
- .justifyContent(FlexAlign.Start)
- .margin({ top: $r('app.float.grid_padding') })
- .padding({ left: $r('app.float.title_padding') })
- .width(Constants.FULL_PERCENT)
复制代码
- 创建一个行容器 Row 来放置文本 Text,用作页面标题。
- 设置了字体大小、权重,并调整了结构属性如高度、对齐方式、边距和内边距。
轮播图 (Swiper)
- Swiper(this.swiperController) {
- ForEach(Constants.BANNER_IMG_LIST, (item: Resource) => {
- Row() {
- Image(item)
- .width(Constants.FULL_PERCENT)
- .height(Constants.FULL_PERCENT)
- }
- .width(Constants.FULL_PERCENT)
- .aspectRatio(Constants.BANNER_ASPECT_RATIO)
- }, (item: Resource, index?: number) => JSON.stringify(item) + index)
- }
- .autoPlay(true)
- .loop(true)
- .margin($r('app.float.grid_padding'))
- .borderRadius($r('app.float.img_border_radius'))
- .clip(true)
- .duration(Constants.BANNER_ANIMATE_DURATION)
- .indicator(false)
复制代码
- 使用 Swiper 组件创建一个自动播放且循环的轮播图。
- ForEach 循环遍历 Constants.BANNER_IMG_LIST 数组来生成轮播项。
ListPage
- @Entry
- @Component
- struct ListPage {
- photoArr: Array<Resource> = (router.getParams() as Record<string, Array<Resource>>)[`${Constants.PARAM_PHOTO_ARR_KEY}`];
- @StorageLink('selectedIndex') selectedIndex: number = 0;
- build() {
- Navigation() {
- Grid() {
- ForEach(this.photoArr, (img: Resource, index?: number) => {
- GridItem() {
- Image(img)
- .height(Constants.FULL_PERCENT)
- .width(Constants.FULL_PERCENT)
- .objectFit(ImageFit.Cover)
- .onClick(() => {
- if (!index) {
- index = 0;
- }
- this.selectedIndex = index;
- router.pushUrl({
- url: Constants.URL_DETAIL_LIST_PAGE,
- params: {
- photoArr: this.photoArr,
- }
- });
- })
- }
- .width(Constants.FULL_PERCENT)
- .aspectRatio(1)
- }, (item: Resource) => JSON.stringify(item))
- }
- .scrollBar(BarState.Off)
- .columnsTemplate(Constants.GRID_COLUMNS_TEMPLATE)
- .rowsGap(Constants.LIST_ITEM_SPACE)
- .columnsGap(Constants.LIST_ITEM_SPACE)
- .layoutWeight(1)
- }
- .title(Constants.PAGE_TITLE)
- .hideBackButton(false)
- .titleMode(NavigationTitleMode.Mini)
- }
- }
复制代码 这段代码定义了一个列表页面,用于展示一组图片。这个页面从上一页接收参数,并根据这些参数来加载和表现对应的图片列表。
网格结构 (Grid)
- Grid() {
- ForEach(this.photoArr, (img: Resource, index?: number) => {
- GridItem() {
- Image(img)
- .height(Constants.FULL_PERCENT)
- .width(Constants.FULL_PERCENT)
- .objectFit(ImageFit.Cover)
- .onClick(() => {
- if (!index) {
- index = 0;
- }
- this.selectedIndex = index;
- router.pushUrl({
- url: Constants.URL_DETAIL_LIST_PAGE,
- params: {
- photoArr: this.photoArr,
- }
- });
- })
- }
- .width(Constants.FULL_PERCENT)
- .aspectRatio(1)
- }, (item: Resource) => JSON.stringify(item))
- }
- .scrollBar(BarState.Off)
- .columnsTemplate(Constants.GRID_COLUMNS_TEMPLATE)
- .rowsGap(Constants.LIST_ITEM_SPACE)
- .columnsGap(Constants.LIST_ITEM_SPACE)
- .layoutWeight(1)
复制代码
- 创建一个网格结构 Grid 来展示 photoArr 中的每一张图片。
- 使用 ForEach 循环遍历 photoArr 数组,为每个图片生成一个 GridItem。
- 每个 GridItem 包罗一个 Image 组件,用来展示单张图片。设置了图片的高度和宽度为全屏百分比,并使用 objectFit 属性确保图片覆盖整个容器。
- 当点击某个 GridItem 时,触发 onClick 变乱处理器,更新 selectedIndex 为当前项的索引,并通过路由跳转到详情页 URL_DETAIL_LIST_PAGE,同时传递图片数组作为参数。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |