鸿蒙学习笔记十五:大图缩放+轮播图+惯性滑动

最近要做大图预览功能,这就绕不开大图缩放,为此花了点时间做这个功能
具体功能点如下:

1:双指捏合图片放大和缩小
2:双击放大和缩小
3:放大后的单指拖动和松开手指后的惯性滑动
4:可与轮播图共用

遇到的主要问题点如下:

1:图片怎么缩放
2:图片怎么滑动
3:缩放后滑动时的边界怎么处理
4:与轮播图的滑动事件冲突
5:怎么在手指松开之后判断是否进行惯性滑动
6:怎么进行惯性滑动
7:放大后松开手指时怎么不让图片滑动
8:怎么监听双击事件 ?

问题解答:
1:图片缩放

使用Image的 transform() 进行缩放,具体捏合手势通过PinchGesture管理

2:图片滑动

使用Image的 offset() 进行滑动,具体滑动手势通过PanGesture管理

3:边界处理

在图片缩放的过程中实时计算出X轴和Y轴的最大滑动距离,大概思路如下:

// PinchGesture的onActionUpdate()方法
    this.maxScrollX = (this.imageWidth * this.scaleInfo.scaleValue - this.screenWidth) / 2
    this.maxScrollY = (this.imageHeight * this.scaleInfo.scaleValue - this.screenHeight) / 2
    if (this.maxScrollX < 0){
      this.maxScrollX = 0
    }
    if (this.maxScrollY < 0){
      this.maxScrollY = 0
    }

图片放大后滑动到上下左右的某个边界,然后进行缩小,缩小时会滑出边界并且显露出父组件的背景色,这时就需要边缩小边对图片进行移动——移动到边界处,具体思路如下:

// PinchGesture的onActionUpdate()方法
if (event.scale < 1) { //缩小
      if (this.offsetInfo.currentX > this.maxScrollX) {
        this.offsetInfo.currentX = this.maxScrollX
      }
      if (this.offsetInfo.currentX < -this.maxScrollX) {
        this.offsetInfo.currentX = -this.maxScrollX
      }
      if (this.offsetInfo.currentY > this.maxScrollY) {
        this.offsetInfo.currentY = this.maxScrollY
      }
      if (this.offsetInfo.currentY < -this.maxScrollY) {
        this.offsetInfo.currentY = -this.maxScrollY
      }
    }
4:与轮播图的滑动事件冲突

图片在轮播图中放大之后,本来只是想滑动图片的,但是却进行了轮播图的换页,从这里看是图片的滑动事件和轮播图的滑动事件冲突了,解决方法如下:
首先通过Swiper的disableSwipe()方法,禁用轮播图自带的切换功能,然后在图片滑动时进行控制,在图片滑动到左右边界的时候,通过SwiperController来进行上一页、下一页的操作,这样会有切换的动画,切记不要直接操作Swiper的index()方法,这样会导致没有切换动画

swiperController.showNext()
swiperController.showPrevious()

图片滑动到边界时直接切换轮播图的下一页或者上一页,就会导致滑动的时候很容易直接切换了轮播图,但是我们此时还是想看当前图片,因此需要滑动到边界之后进行二次滑动,再进行轮播图的翻页,通过增加isSliding这个boolean值进行判断,图片滑动时设置为true,在PanGesture的onActionEnd()中设置为false——注意这里需要延迟设置为false,只有为false的时候才允许切换轮播图

  //滑动图片时进行轮播图翻页
  onSlidPic(event: GestureEvent){
    if (this.isSliding == false) { //一次滑动只允许翻一页
      if (Math.abs(event.offsetX) > this.mMinimumSlideSwiper){ //必须大于最小滑动距离,才能翻页
        if (this.onSlideSwiper != null){
          if (event.offsetX > 0) { //右滑
            this.onSlideSwiper(false)
          }
          if (event.offsetX < 0) { //左滑
            this.onSlideSwiper(true)
          }
        }
        this.isSliding = true
      }
    }
  }
5:怎么在手指松开之后判断是否进行惯性滑动

首先需要明确进行惯性滑动的时机为PanGesture的onActionEnd()方法,这里是手指滑动操作结束的标志;如果直接在这里进行惯性滑动话,在手指滑动很慢然后抬起的情况下也会进行惯性滑动,这不符合操作习惯,所以就需要判断手指抬起时在屏幕上的滑动速度,通过event.velocityX 和 event.velocityY我们可以获取到手指抬起时Image在X轴和Y轴的滑动速度,当着两个值中的一个大于最小滑动速度时,我们就进行惯性滑动

if (Math.max(Math.abs(event.velocityX), Math.abs(event.velocityY)) > this.mMinimumVelocity){
  //进行惯性滑动             
}
6:怎么进行惯性滑动

鸿蒙的官方开发文档中有惯性滑动的方法,但是效果不甚理想,首先是只能滑动event.offsetX的距离,这就导致滑动距离很短;其次我建议使用EaseOut作为滑动效果,因为我们是全方向滑动;滑动的动画时长建议设置成300毫秒;滑动距离我们使用event.offsetX * 15 和 event.offsetY * 15,这样滑动距离长,效果更好;非惯性滑动时还是滑动event.offsetX和event.offsetY,这里注意区分。
具体滑动代码示例如下:

//PanGesture的onActionEnd()方法中
//惯性滑动动画:当手指抬起时执行,手指滑动过程中不执行
            if (Math.max(Math.abs(event.velocityX), Math.abs(event.velocityY)) > this.mMinimumVelocity){
              animateTo({
                duration: 300,
                curve: Curve.EaseOut, //表示动画以低速结束
                iterations: 1 ,
                playMode: PlayMode.Normal,
                onFinish: () => {
                  //动画结束100毫秒之后再保存移动距离,立即保存的话会产生动画卡顿
                  setTimeout(() => {
                    this.offsetInfo.stash()
                    this.isSliding = false
                  }, 100)
                }
              }, () => {
                this.setOffSet(event,true)
              })
            }else {
              this.offsetInfo.stash()  //动画结束
            }
7:放大后松开手指时怎么不让图片滑动

双指对图片进行缩放完松开手指时,因为手指离开屏幕时有先后顺序可能会触发滑动事件,导致图片的突然移动,因此我在PinchGesture的onActionEnd()方法中加了一个延迟以控制是否正在捏合操作的变量,示例代码如下:

setTimeout(() => {
              this.isScaling = false //缩放完成150毫秒后再重置,防止刚缩放完手指离开屏幕时有先后顺序导致的滑动
            }, 150)
8:怎么监听双击事件 ?

这个很简单,直接上代码:

//双击
        TapGesture({ count: 2 })
          .onAction((event: GestureEvent) => {
            this.onDoubleClick()
          })
完整代码如下:
import { CustomImage } from './customImage/CustomImage';
import { CustomSwiperDataSourceObject, CustomSwiper } from './customImage/CustomSwiper';

/**
 * 图片预览组件
 */
@Component
export struct CustomImagePreview {
  @Link show: boolean;
  @Require @Prop imageList: string[]; //预览图片列表
  @Require @Prop currentIndex: number; //当前预览第几张图
  @State private swiperList: CustomSwiperDataSourceObject<string>[] = [];
  private swiperController: SwiperController = new SwiperController() //轮播图翻页控制器

  aboutToAppear(): void {
    this.swiperList = this.imageList.map((item: string, _index: number) => {
      let data: CustomSwiperDataSourceObject<string> = {
        swiperItem: item
      }
      return data
    })
  }

  @Builder
  imageBuilder(item: CustomSwiperDataSourceObject<string>, index: number) {
    Column() {
      CustomImage({
        imgUrl: item.swiperItem,
        imgWidth: `100%`,
        imgHeight: `100%`,
        download: true,
        imgBg: '#00000000',
        canScal: true, //是否可以缩放,需要缩放时配置
        onSlideSwiper: (isNextPage: boolean) => {
          if (isNextPage) {
            this.swiperController.showNext()
          } else {
            this.swiperController.showPrevious()
          }
        }
      })
        .onClick(() => {
          this.show = false
        })
    }
  }

  build() {
    Stack() {
      Row() {
        Text(`${this.currentIndex + 1}/${this.imageList.length}`)
          .fontSize(vp2px(8))
          .fontColor('#FFFFFF')
          .textAlign(TextAlign.Center)
      }
      .width(`100%`)
      .height(vp2px(10))
      .position({
        y: vp2px(0)
      })
      .zIndex(10)
      .justifyContent(FlexAlign.Center)


      Column() {
        CustomSwiper({
          swiperList: this.swiperList,
          index: this.currentIndex,
          // showIndicator: false,
          autoPlay: false,
          radius: 0,
          swiperController: this.swiperController,
          disableSwipe: true, //有图片缩放功能时,必须禁用组件滑动功能,  传值  true:禁用  false:启用
          itemBuilder: (item: CustomSwiperDataSourceObject<string>, index: number) => {
            this.imageBuilder(item, index);
          },
          onSwiperChange: (item: CustomSwiperDataSourceObject<ESObject>, index: number) => {
            this.currentIndex = index;
          }
        })
      }.width(`100%`)
      .height(`100%`)
      .justifyContent(FlexAlign.Center)
      .zIndex(9)
    }.width(`100%`)
    .height(`100%`)
    .backgroundColor('#000000')
  }
}
import { CommonDataSource } from '../common/CommonDataSource';

/**
 * 通用轮播组件
 * @param swiperList 轮播列表
 * @param swiperHeight 轮播区域高度
 * @param radius 圆角
 */
@Preview
@Component
export struct CustomSwiper {
  // swiperList规定了 swiper 对象
  @Prop swiperList: CustomSwiperDataSourceObject<ESObject>[];
  // 当前在容器中显示的子组件的索引值。开始
  @Prop index: number = 0;
  // 是否自动播放。
  @Prop autoPlay: boolean = true;
  // 自动播放时播放的时间间隔
  @Prop interval: number = 3000;
  // 是否开启循环。
  @Prop loop: boolean = true;
  // 子组件切换的动画时长,单位为毫秒
  @Prop duration: number = 400;
  // 纵向滑动。
  @Prop vertical: boolean = false;
  // 子组件与子组件之间间隙。
  @Prop itemSpace: number = 0;
  //设置Swiper的动画曲线
  @Prop curve: Curve | string | ICurve = Curve.Linear;
  // 预加载子组件个数。
  @Prop cachedCount: number = 0;
  // 后边距,用于露出后一项的一小部分。
  @Prop nextMargin: number = 0;
  // 前边距,用于露出前一项的一小部分
  @Prop prevMargin: number = 0;
  // 整个 swiper圆角
  @Prop radius: BorderRadiuses | Length = vp2px(8)
  // 是否禁用组件滑动切换功能
  @Prop disableSwipe: boolean = false
  // 内容 builder
  @Require @BuilderParam itemBuilder: (item: CustomSwiperDataSourceObject<ESObject>, index: number) => void
  @State currentIndex: number = 0
  // swiper 控制器
  private swiperController: SwiperController = new SwiperController()
  // swiper 数据
  private data: CommonDataSource<CustomSwiperDataSourceObject<ESObject>> = new CommonDataSource([])
  onSwiperChange?: (item: CustomSwiperDataSourceObject<ESObject>, index: number) => void

  aboutToAppear(): void {
    this.data = new CommonDataSource(this.swiperList)
  }

  build() {
    Stack({
      alignContent: Alignment.BottomEnd
    }) {
      Swiper(this.swiperController) {
        LazyForEach(this.data, (item: CustomSwiperDataSourceObject<ESObject>, index: number) => {
          this.itemBuilder(item, index);
        }, (item: string) => item)
      }
      .index(this.index)
      .autoPlay(this.autoPlay)
      .interval(this.interval)
      .loop(this.loop)
      .duration(this.duration)
      .vertical(this.vertical)
      .itemSpace(this.itemSpace)
      .cachedCount(this.cachedCount)
      .curve(this.curve)
      .nextMargin(this.nextMargin)
      .prevMargin(this.prevMargin)
      .borderRadius(this.radius)
      .disableSwipe(this.disableSwipe)
      .indicator(false) //不展示指示器
      .onChange((_index: number) => {
        this.currentIndex = _index;
        if (this.onSwiperChange) {
          this.onSwiperChange(this.swiperList[_index], _index)
        }
      })
    }
    .width('100%')
  }
}

export interface CustomSwiperDataSourceObject<T> {
  swiperItem: T;
}
import { display, matrix4 } from '@kit.ArkUI';
import { CustomScaleModel } from './CustomScaleModel';
import { CustomOffsetModel } from './CustomOffsetModel';

// 图片信息
interface imgInfo {
  width: number;
  height: number;
  componentWidth: number;
  componentHeight: number;
  loadingStatus: number;
  contentWidth: number;
  contentHeight: number;
  contentOffsetX: number;
  contentOffsetY: number;
}

/**
 * KImage 图片组件
 * @param imgUrl: PixelMap | ResourceStr | DrawableDescriptor
 * @param imgWidth
 * @param imgHeight
 * @param imgAlt
 * @param imgBorderRadius
 * @param imgFit
 * @param imgBg
 * @callback onCompleteCall
 * @callback onErrorCall
 * */
@Component
export struct CustomImage {
  // url
  @Require @Prop imgUrl: PixelMap | ResourceStr | DrawableDescriptor
  // 宽
  @Prop imgWidth: Length
  // 高
  @Prop imgHeight: Length
  //加载时的占位图
  @Prop imgAlt: string | Resource = $r('app.media.imgError')
  @Prop hasImgAlt: boolean = true
  // 是否支持长按下载,默认不支持
  @Prop download: boolean = true
  // 圆角
  @Prop imgBorderRadius: Length | BorderRadiuses
  // ImageFit
  @Prop imgFit: ImageFit = ImageFit.Contain
  // 正常加载图片时的背景色,默认为 bg_lightgray4
  @Prop imgBg: ResourceColor = "#F0F0F0"
  // 加载图片异常时的背景色,按需配置
  @Prop imgBgError: ResourceColor = "#F0F0F0"
  // 图片宽高比
  @Prop imgAspectRatio: number
  @Prop imgBorder: BorderOptions | null
  // 图片加载成功
  onCompleteCall?: (imgInfo?: imgInfo) => void
  // 图片加载失败
  onErrorCall?: (error: ImageError) => void
  // 加载失败
  @State isError: Boolean = false
  // 加载中
  @State isLoading: Boolean = true
  //============== 图片缩放相关 ==============
  private screenWidth: number = 0
  private screenHeight: number = 0
  private imageWidth: number = 0
  private imageHeight: number = 0
  private maxScrollX: number = 0 //x轴最大移动距离
  private maxScrollY: number = 0 //y轴最大移动距离
  private onSlideSwiper?: (isNextPage: boolean) => void //轮播图切换
  private mMinimumVelocity: number = 50 //手指抬起时可以进行惯性滑动的最小速度
  private mMinimumSlideSwiper: number = 20 //手指抬起时可以进行惯性滑动的最小速度
  private canScal: boolean = false //是否可以缩放
  @State scaleInfo: CustomScaleModel = new CustomScaleModel(1.0, 1.0, 5.0)
  @State offsetInfo: CustomOffsetModel = new CustomOffsetModel(0, 0)
  @State matrix: matrix4.Matrix4Transit = matrix4.identity().copy()
  @State isScaling: boolean = false //是否正在缩放,正在缩放时不允许滑动图片
  @State isSliding: boolean = false //是否正在滑动

  aboutToAppear(): void {
    this.screenWidth = px2vp(display.getDefaultDisplaySync().width)
    this.screenHeight = px2vp(display.getDefaultDisplaySync().height)
  }

  build() {
    Stack() {
      this.showImage()
      if (this.isLoading || this.isError) {
        if (this.hasImgAlt) {
          this.showKfzLogo()
        }
      }
    }
    .width(this.imgWidth)
    .height(this.imgHeight)
    .clip(true)
    .borderRadius(this.imgBorderRadius)
    .backgroundColor(this.isError ? this.imgBgError : this.imgBg)
    .alignContent(Alignment.Center)
  }

  // 占位图
  @Builder
  showKfzLogo() {
    Image(this.imgAlt)
      .fillColor("#FFFFFF")
      .objectFit(ImageFit.Contain)
      .width(typeof this.imgWidth === 'number' ? (this.imgWidth * 0.6) : '60%')
      .height(typeof this.imgHeight === 'number' ? (this.imgHeight * 0.6) : '60%')
      .aspectRatio(this.imgAspectRatio ?? null)
  }

  // 图片
  @Builder
  showImage() {
    Image(this.imgUrl)
      .width(this.imgWidth)
      .height(this.imgHeight)
      .objectFit(this.imgFit)
      .aspectRatio(this.imgAspectRatio)
      .border(this.imgBorder)
      .borderRadius(this.imgBorderRadius)
      .onComplete((imgInfo) => {
        this.isLoading = false
        this.isError = false
        if (imgInfo) {
          this.imageWidth = px2vp(imgInfo.contentWidth)
          //得到图片绘制的高度 单位 vp的
          this.imageHeight = px2vp(imgInfo.contentHeight)
        }
        if (this.onCompleteCall) {
          this.onCompleteCall(imgInfo)
        }
      })
      .onError((error) => {
        this.isLoading = false
        this.isError = true
        if (this.onErrorCall) {
          this.onErrorCall(error)
        }
      })
      .transform(this.canScal ? this.matrix : null)// 通过matrix控制图片的缩放
      .offset({
        //通过offset控制图片的偏移
        x: this.canScal ? this.offsetInfo.currentX : 0,
        y: this.canScal ? this.offsetInfo.currentY : 0
      })
      .onImageGesture()
      .onGestureJudgeBegin((gestureInfo: GestureInfo, event: BaseGestureEvent) => {
        //捏合手势、滑动手势在非看大图模式下不响应
        if ((gestureInfo.type == GestureControl.GestureType.PAN_GESTURE ||
          gestureInfo.type == GestureControl.GestureType.PINCH_GESTURE) && !this.canScal) {
          return GestureJudgeResult.REJECT
        }
        return GestureJudgeResult.CONTINUE
      })

  }

  @Styles
  onImageGesture(){
    .gesture(
      GestureGroup(GestureMode.Parallel,
        //手指缩放
        PinchGesture({ fingers: 2, distance: 1 })
          .onActionUpdate((event: GestureEvent) => {
            this.onPinchGestureUpDate(event)
          })
          .onActionEnd((event: GestureEvent) => {
            this.scaleInfo.stash()
            setTimeout(() => {
              this.isScaling = false //缩放完成150毫秒后再重置,防止刚缩放完手指离开屏幕时有先后顺序导致的滑动
            }, 150)
          }),

        //滑动图片
        PanGesture({ fingers: 1, direction: PanDirection.All })
          .onActionUpdate((event: GestureEvent) => {
            this.onPanGestureUpDate(event)
          })
          .onActionEnd((event: GestureEvent) => {
            if (this.isScaling) {
              return
            }
            //惯性滑动动画:当手指抬起时执行,手指滑动过程中不执行
            if (Math.max(Math.abs(event.velocityX), Math.abs(event.velocityY)) > this.mMinimumVelocity) {
              animateTo({
                duration: 300,
                curve: Curve.EaseOut, //表示动画以低速结束
                iterations: 1,
                playMode: PlayMode.Normal,
                onFinish: () => {
                  //动画结束100毫秒之后再保存移动距离,立即保存的话会产生动画卡顿
                  setTimeout(() => {
                    this.offsetInfo.stash()
                    this.isSliding = false
                  }, 100)
                }
              }, () => {
                this.setOffSet(event, true)
              })
            } else {
              this.offsetInfo.stash() //动画结束
            }
          }),

        //双击
        TapGesture({ count: 2 })
          .onAction((event: GestureEvent) => {
            this.onDoubleClick()
          })
      )
    )
  }

  onDoubleClick() {
    if (this.scaleInfo.scaleValue > this.scaleInfo.minScaleValue) {
      //双击时已经放大了
      this.scaleInfo.scaleValue = this.scaleInfo.minScaleValue
    } else {
      this.scaleInfo.scaleValue = this.scaleInfo.mediumScaleValue
    }
    this.scaleInfo.lastValue = this.scaleInfo.scaleValue

    //matrix默认缩放中心为组件中心
    this.matrix = matrix4.identity().scale({
      x: this.scaleInfo.scaleValue,
      y: this.scaleInfo.scaleValue,
    }).copy()
  }

  //手势缩放
  onPinchGestureUpDate(event: GestureEvent) {
    this.isScaling = true

    this.scaleInfo.scaleValue = this.scaleInfo.lastValue * event.scale
    // 缩放时不允许大于最大缩放,不允许小于默认大小
    if (this.scaleInfo.scaleValue > this.scaleInfo.maxScaleValue) {
      this.scaleInfo.scaleValue = this.scaleInfo.maxScaleValue
    }
    if (this.scaleInfo.scaleValue < this.scaleInfo.minScaleValue) {
      this.scaleInfo.scaleValue = this.scaleInfo.minScaleValue
    }
    //matrix默认缩放中心为组件中心
    this.matrix = matrix4.identity().scale({
      x: this.scaleInfo.scaleValue,
      y: this.scaleInfo.scaleValue,
    }).copy()

    //计算X轴/Y轴最大移动距离
    this.maxScrollX = (this.imageWidth * this.scaleInfo.scaleValue - this.screenWidth) / 2
    this.maxScrollY = (this.imageHeight * this.scaleInfo.scaleValue - this.screenHeight) / 2
    if (this.maxScrollX < 0) {
      this.maxScrollX = 0
    }
    if (this.maxScrollY < 0) {
      this.maxScrollY = 0
    }

    if (event.scale < 1) { //缩小
      if (this.offsetInfo.currentX > this.maxScrollX) {
        this.offsetInfo.currentX = this.maxScrollX
      }
      if (this.offsetInfo.currentX < -this.maxScrollX) {
        this.offsetInfo.currentX = -this.maxScrollX
      }
      if (this.offsetInfo.currentY > this.maxScrollY) {
        this.offsetInfo.currentY = this.maxScrollY
      }
      if (this.offsetInfo.currentY < -this.maxScrollY) {
        this.offsetInfo.currentY = -this.maxScrollY
      }
    }
  }

  //手势滑动
  onPanGestureUpDate(event: GestureEvent) {
    //正在缩放,不允许移动
    if (this.isScaling) {
      return
    }

    if (this.scaleInfo.scaleValue === this.scaleInfo.minScaleValue) {
      this.onSlidPic(event) //还未缩放就进行滑动,则轮播图翻页
    } else {
      this.setOffSet(event, false)
    }
  }

  //设置图片偏移量
  setOffSet(event: GestureEvent, isFly: boolean) {
    //滑动到边界之后进行二次滑动,则轮播图翻页:注意这里需要区分左右两个边界的单独滑动
    if (this.offsetInfo.currentX == this.maxScrollX && event.offsetX > 0) {
      this.onSlidPic(event)
      return
    }
    if (this.offsetInfo.currentX == -this.maxScrollX && event.offsetX < 0) {
      this.onSlidPic(event)
      return
    }

    if (this.maxScrollX >= 0) { //缩放后图片宽度大于屏幕宽度
      //X轴移动
      let preScrollX = 0
      if (isFly) {
        preScrollX = this.offsetInfo.lastX + event.offsetX * 15 //预计X轴要移动的距离
      } else {
        preScrollX = this.offsetInfo.lastX + event.offsetX //预计X轴要移动的距离
      }
      if (Math.abs(preScrollX) < this.maxScrollX) {
        this.offsetInfo.currentX = preScrollX
      } else {
        if (preScrollX < 0) {
          this.offsetInfo.currentX = -this.maxScrollX
        } else {
          this.offsetInfo.currentX = this.maxScrollX
        }
      }
      this.isSliding = true
    }

    //Y轴移动
    if (this.maxScrollY >= 0) { //缩放后图片高度大于屏幕高度
      let preScrollY = 0
      if (isFly) {
        preScrollY = this.offsetInfo.lastY + event.offsetY * 15 //预计Y轴要移动的距离
      } else {
        preScrollY = this.offsetInfo.lastY + event.offsetY //预计Y轴要移动的距离
      }
      if (Math.abs(preScrollY) < this.maxScrollY) {
        this.offsetInfo.currentY = preScrollY
      } else {
        if (preScrollY < 0) {
          this.offsetInfo.currentY = -this.maxScrollY
        } else {
          this.offsetInfo.currentY = this.maxScrollY
        }
      }
      this.isSliding = true
    }
  }

  //滑动图片时进行轮播图翻页
  onSlidPic(event: GestureEvent) {
    if (this.isSliding == false) { //一次滑动只允许翻一页
      if (Math.abs(event.offsetX) > this.mMinimumSlideSwiper) { //必须大于最小滑动距离,才能翻页
        if (this.onSlideSwiper != null) {
          if (event.offsetX > 0) { //右滑
            this.onSlideSwiper(false)
          }
          if (event.offsetX < 0) { //左滑
            this.onSlideSwiper(true)
          }
        }
        this.isSliding = true
      }
    }
  }
}

@Observed
export class CustomOffsetModel {
  public currentX: number
  public currentY: number
  public lastX: number = 0
  public lastY: number = 0

  constructor(currentX: number = 0, currentY: number = 0) {
    this.currentX = currentX;
    this.currentY = currentY;
  }

  reset(): void {
    this.currentX = 0
    this.currentY = 0
    this.lastX = 0
    this.lastY = 0
  }

  stash(): void {
    this.lastX = this.currentX
    this.lastY = this.currentY
  }
}
@Observed
export class CustomScaleModel {
  public scaleValue: number //本次缩放值
  public lastValue: number //记录上次缩放完后的缩放值
  public maxScaleValue: number //最大默认缩放值
  public minScaleValue: number = 1 //最小缩放值
  public mediumScaleValue: number = 2.5 //双击的放大值

  constructor(scaleValue: number = 1.0, lastValue: number = 1.0, maxScaleValue: number = 5,
    mediumScaleValue: number = 2.5) {
    this.scaleValue = scaleValue
    this.lastValue = lastValue
    this.maxScaleValue = maxScaleValue
    this.mediumScaleValue = mediumScaleValue
  }

  reset(): void {
    this.scaleValue = this.minScaleValue
    this.lastValue = this.scaleValue
  }

  stash(): void {
    this.lastValue = this.scaleValue
  }
}
@Observed
export class ObservedArray<T> extends Array<T> {
  constructor(args?: T[]) {
    if (args instanceof Array) {
      super(...args);
    } else {
      super();
    }
  }
}

@Observed
export class CommonDataSource<T> implements IDataSource {
  private dataArray: T[] = [];
  private listeners: DataChangeListener[] = [];

  constructor(element: T[]) {
    this.dataArray = element;
  }

  public getData(index: number) {
    return this.dataArray[index]
  }

  public totalCount(): number {
    return this.dataArray.length;
  }

  public addData(index: number, data: T): void {
    this.dataArray.splice(index, 0, data);
    this.notifyDataAdd(index);
  }

  public addDatas(index: number, datas: T[]): void {
    this.dataArray = this.dataArray.concat(datas);
    this.notifyDataAdd(index);
  }

  public pushAllData(newData: ObservedArray<T>): void {
    this.clear();
    this.dataArray.push(...newData);
    this.notifyDataReload();
  }

  //分页加载时不要使用,会重新加载所有数据导致卡顿
  public appendAllData(addData: ObservedArray<T>): void {
    this.dataArray.push(...addData);
    this.notifyDataReload()
  }

  //分页加载时不要使用,会重新加载所有数据导致卡顿
  public addAllDatas(datas: T[]): void {
    datas.forEach(data => {
      this.dataArray.push(data);
    })
    this.notifyDataReload();
  }

  public setData(datas: T[]): void {
    this.clear();
    this.addAllDatas(datas);
  }

  public pushData(data: T): void {
    this.dataArray.push(data);
    this.notifyDataAdd(this.dataArray.length - 1);
  }

  public deleteData(index: number): void {
    this.dataArray.splice(index, 1);
    this.notifyDataDelete(index)
  }

  public moveData(from: number, to: number): void {
    let temp: T = this.dataArray[from];
    this.dataArray[from] = this.dataArray[to];
    this.dataArray[to] = temp;
    this.notifyDataMove(from, to);
  }

  public changeData(index: number, data: T): void {
    this.dataArray.splice(index, 1, data);
    this.notifyDataChange(index);
  }

  public reloadData(): void {
    this.notifyDataReload();
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener);
    if (pos >= 0) {
      this.listeners.splice(pos, 1);
    }
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      this.listeners.push(listener);
    }
  }

  notifyDataReload(): void {
    this.listeners.forEach((listener: DataChangeListener) => {
      listener.onDataReloaded();
    })
  }

  notifyDataAdd(index: number): void {
    this.listeners.forEach((listener: DataChangeListener) => {
      listener.onDataAdd(index);
    })
  }

  notifyDataChange(index: number): void {
    this.listeners.forEach((listener: DataChangeListener) => {
      listener.onDataChange(index);
    })
  }

  notifyDataDelete(index: number): void {
    this.listeners.forEach((listener: DataChangeListener) => {
      listener.onDataDelete(index);
    })
  }

  notifyDataMove(from: number, to: number): void {
    this.listeners.forEach((listener: DataChangeListener) => {
      listener.onDataMove(from, to);
    })
  }

  public notifyDatasetChange(operations: DataOperation[]): void {
    this.listeners.forEach((listener: DataChangeListener) => {
      listener.onDatasetChange(operations);
    })
  }

  getDataArray() {
    return this.dataArray
  }

  getItemIndex(t: T): number {
    return this.dataArray.indexOf(t)
  }

  public clear(): void {
    this.dataArray.splice(0, this.dataArray?.length)
  }

  public deleteAll(): void {
    while (this.dataArray.length > 0) {
      this.deleteData(0)
    }
  }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,313评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,369评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,916评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,333评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,425评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,481评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,491评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,268评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,719评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,004评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,179评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,832评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,510评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,153评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,402评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,045评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,071评论 2 352

推荐阅读更多精彩内容