【HarmonyOS开发实战】电子相册开发随笔(2)图片详情页面,查看大图&通过手势控制图片

图片详情页面——DetailListPage

该页面由两个横向滚动List组件构成,两个组件之间相互联动。

import display from '@ohos.display';    //用于获取设备的显示信息
import router from '@ohos.router';
import Constants from '../common/constants/Constants';
import Logger from '../common/utils/Logger';

//定义一个枚举量,用于区分滚动状态
enum scrollTypeEnum {
  STOP = 'onScrollStop',
  SCROLL = 'onScroll'
};

@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> = [
    ...Constants.CACHE_IMG_LIST,
    ...(router.getParams() as Record<string, Array<Resource | string>>)[`${Constants.PARAM_PHOTO_ARR_KEY}`],
    ...Constants.CACHE_IMG_LIST
  ];
  //selectedIndex与存储链接,以便在不同页面访问和保存状态。
  @StorageLink('selectedIndex') selectedIndex: number = 0;

  @Builder
  SmallImgItemBuilder(img: Resource, index: number) {
    if (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() {
    router.pushUrl({
      url: Constants.URL_DETAIL_PAGE,
      params: { photoArr: this.photoArr }
    }).catch((error: Error) => {
      Logger.error(Constants.URL_DETAIL_LIST_PAGE, JSON.stringify(error));
    });
  }

//以下方法用来处理两个List组件之间的联动
  smallImgClickAction(index: number) {
  //点击小图片时更新选中索引并滚动到相应位置
    this.selectedIndex = index - Constants.CACHE_IMG_SIZE;
    this.smallScroller.scrollToIndex(this.selectedIndex);
    this.bigScroller.scrollToIndex(this.selectedIndex);
  }

//根据小滚动器的滚动处理选中索引和大滚动器的滚动
  smallScrollAction(type: scrollTypeEnum) {
    this.selectedIndex = Math.round((this.smallScroller.currentOffset().xOffset +
      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) {
    let smallWidth = this.smallImgWidth + Constants.LIST_ITEM_SPACE;
    this.selectedIndex = Math.round((this.bigScroller.currentOffset().xOffset +
      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);
            //大滚动器联动索引和小滚动器
          }
        })
        .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, index: number) => JSON.stringify(item) + index)
        }
        .listDirection(Axis.Horizontal)
        .onScroll((scrollOffset, scrollState) => {
          if (scrollState === ScrollState.Fling) {
            this.smallScrollAction(scrollTypeEnum.SCROLL);
          }
        })
        .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)
  }
}

该页面的构建比较复杂,这里就选几个值的注意的地方说说

两个List组件间和两个大小滚动器的联动

该页面实现了滑动其中任意一个组件,另一个也会跟着移动。诀窍就在设定了一个图片选择的索引。

//根据大滚动器的滚动处理选中索引和小滚动器的滚动
  bigScrollAction(type: scrollTypeEnum) {
    let smallWidth = this.smallImgWidth + Constants.LIST_ITEM_SPACE;
    this.selectedIndex = Math.round((this.bigScroller.currentOffset().xOffset +
      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 });
    }
  }

就以这个方法举例。在大滚动器滚动时获取其滚动偏移量,然后再经过计算推导出所在图片的索引值。在获取到索引值后,通过索引值*宽度锁定小滚动器所在位置并滚动到该位置。滚动结束后,大滚动器自动锁定当前索引对应位置进行滚动校准。

空占位实现首位末位图片居中显示

空占位即我们在Constants.ets中设立的CACHE_IMG_LIST

static readonly CACHE_IMG_LIST: string[] = ['', '', '', '']

//在定义小图片数组时这样定义
private smallPhotoArr: Array<Resource | string> = [
    ...Constants.CACHE_IMG_LIST,
    ...(router.getParams() as Record<string, Array<Resource | string>>)[`${Constants.PARAM_PHOTO_ARR_KEY}`],
    ...Constants.CACHE_IMG_LIST
  ];

效果就是令首位末位图片在中间显示

在本页面中通过selectedIndex这一索引量来实现大图和小图之间的同步,虽然中间涉及到的滚动条定位有些麻烦,但是理解了就可以了()

查看大图&通过手势控制图片——DetailPage

在该页面我们可以左右滑动来切换图片,也可以进行手势操作实现图片的移动和缩放放大

import display from '@ohos.display'
import router from '@ohos.router';
import Constants from '../common/constants/Constants'

@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
  //是否进行缩放操作
  isScalling: boolean = true
  @State
  imgScale: number = 1      //缩放比例
  @State
  imgOffSetX: number = 0     //偏移量
  @State
  imgOffSetY: number = 0
  @State
  bgOpacity: number = 0    //控制背景透明度

  aboutToAppear(): void {
    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() {
    this.imgScale = 1
    this.currentScale = 1
    this.preOffsetX = 0
    this.preOffsetY = 0
  }
  
//根据图片的偏移量来判断是否切换图片
  handlePanEnd() {
  //计算初始偏移量
    let initOffsetX = (this.imgScale - 1) * this.imageWidth + this.smallImgWidth
    //拖动水平距离的绝对值是否超过了initOffsetX
    if(Math.abs(this.imgOffSetX) > initOffsetX)
    {
    //不是第一张图片且向右拖动了足够长,切换上一张图片
      if(this.imgOffSetX > initOffsetX && this.selectedIndex > 0)
      {
        this.selectedIndex = this.selectedIndex - 1
      }
      //不是最后一张图片且向左拖动了足够长的距离,切换下一张图片
      else if(this.imgOffSetX < -initOffsetX && this.selectedIndex < (this.photoArr.length - 1))
      {
        this.selectedIndex = this.selectedIndex + 1
      }
      //重置图片状态
      this.isScalling = 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(PinchGesture({fingers: Constants.DOUBLE_NUMBER})
            .onActionStart(()=>{
              this.resetImg()
              this.isScalling = true  //表示正在缩放图片
              this.imgOffSetX = 0     //重置偏移量
              this.imgOffSetY = 0
            })
            .onActionUpdate((event: GestureEvent) => {
            //更新图片缩放比例
              this.imgScale = this.currentScale * event.scale
            })
            .onActionEnd(() => {
            //判断图片缩放比例是否小于1
              if(this.imgScale < 1){
                this.resetImg
                this.imgOffSetX = 0
                this.imgOffSetY = 0
              }else {
              //更新当前比例为图片最终的缩放比例
                this.currentScale = this.imgScale
              }
            })
          )
          .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 + (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.isScalling ? 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)=>{
            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)=>{
            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.isScalling ? Visibility.Visible : Visibility.Hidden)
    }
    .offset({
      x: -(this.smallImgWidth / Constants.DOUBLE_NUMBER)
    })
    .width(this.imageWidth)
    .height(Constants.FULL_PERCENT)
    .backgroundColor($r('app.color.detail_background'))
  }
}

在手势缩放事件中经历了三个阶段

.onActionStart(() => { ... })://当捏合手势开始时,会触发这个回调函数。
.onActionUpdate((event: GestureEvent) => { ... }): //当捏合手势发生变化时,会触发这个回调函数。
.onActionEnd(() => { ... }): //当捏合手势结束时,会触发这个回调函数。

三个阶段的回调事件各司其职,完成了图片的缩放及完成缩放后的图片比例

在一开始设定了是否缩放选项为true,保证了Row组件控制的图片可见,在滑动图片时又更新为false,此时Row组件不可见,List组件显示,进而可以实现图片的切换操作。而在进入缩放事件中,Row组件又再次可见,可以实现图片的缩放移动操作。通过使用一个boolean值实现了Row组件和List组件之间的切换显示。两个组件各自完成了自己的工作。

组合手势GestureGroup

//组合手势
.gesture(GestureGroup(GestureMode.Exclusive,
        PinchGesture({ fingers: Constants.DOUBLE_NUMBER})
          .onActionUpdate((event: GestureEvent)=>{
            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)=>{
            this.imgOffSetX = this.preOffsetX + event.offsetX
            this.imgOffSetY = this.preOffsetY + event.offsetY
          })
          .onActionEnd(()=>{
            this.handlePanEnd()
          })
      ))
//基本格式
.gesture(GestureGroup(GestureMode. ... , ... , ...)

其中GestureMode(组合手势类型)分为

  • 顺序识别(Sequence):按照手势的注册顺序识别手势,直到所有的手势识别成功,如果其中一个手势识别失败,则所有手势识别失败。

  • 并行识别(Parallel):将注册的手势同时进行识别,直到所有手势识别结束。该方法下的的手势进行识别时互不影响。允许系统同时检测和识别多个手势,提高了交互效率。

  • 互斥识别(Exclusive):同一时间只能有一个手势被识别。

在本项目中,捏合手势和平移手势为互斥识别组合手势。即同时只能识别二者之一。这样便可以防止缩放和平移图片两个动作同时进行。

简单总结一下:

在后边两个页面的构建中涉及到了坐标一类的数据,比如设定大小滚动器同步滚动,图片平移定位等。其中有些还要进行运算才能确定最终坐标的值。(比较麻烦

手势也是用户交互中重要的一步,在处理图片缩放移动命令时运用了单手势和组合手势,手势之间的逻辑关系在以后的开发中要多加注意。

下一次开发就要做自己的项目了()

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值