HarmonyOS 鸿蒙Next 自定义轮播封装分享

发布于 1周前 作者 wuwangju 最后一次编辑是 5天前 来自 鸿蒙OS

HarmonyOS 鸿蒙Next 自定义轮播封装分享

自定义轮播封装分享

在数字化浪潮汹涌澎湃的当下,鸿蒙技术以其创新性与前瞻性脱颖而出,不仅在技术层面日臻成熟,更在应用拓展的广度与深度上不断突破,持续引领着行业变革的新方向。而与之相伴的,是市场需求的更迭加速,其变化的节奏宛如时代脉搏,跳动得愈发急促且强劲,深刻影响着鸿蒙生态的发展轨迹与前行方向。对展示效果要求越来越高,系统自带的功能无法满足需求,所以需要自定义。

二、功能需求

  1. 支持左右滑动,中间图片最大,2边的图片较小
  2. 滑动的时候有动画效果
  3. 点击中间的图片支持预览效果

三、轮播图的展示效果:

cke_4623.gif

四、实现思路:

1. 根据onGestureSwiper回调中,根据手指滑动实时计算卡片偏移量

2. 在onAnimationStart回调中,计算手指离开屏幕时卡片的偏移量与缩放系数,避免产生突变的偏移量与缩放系数

3. 在onChange回调中提前计算Swiper滑动后卡片的位置与缩放系数

4. 在onAnimationEnd回调中,让startSwiperOffset归0

5. 通过geometryTransition属性绑定两个需要“一镜到底”的组件,结合模态窗口转场即可

核心代码:

@Component
  export struct CardSwiperAnimationView {
    // 卡片数据源
    private data: CardsSource = new CardsSource([]);
    // 卡片数据列表
    @State private cardsList: CardInfo[] = [];
    // 卡片偏移度列表
    @State private cardsOffset: number[] = [];
    // 屏幕宽度
    private displayWidth: number = 0;
    // Swiper 两侧的偏移量
    private swiperMargin: number = Constants.SWIPER_MARGIN;
    // Swiper 当前索引值
    @State private currentSwiperIndex: number = 0;
    private readonly DEVICESIZE: number =
    600; // 依据Navigation的mode属性说明,如使用Auto,窗口宽度>=600vp时,采用Split模式显示;窗口宽度<600vp时,采用Stack模式显示。
    // 数据源对应的缩放数组
    @State scaleList: number[] = [];
    // 最大缩放
    private MAX_SCALE: number = 1;
    // 最小缩放
    private MIN_SCALE: number = 0.75;
    // 手势触发时的offset
    @State startSwiperOffset: number = 0;
    // 图片缩放的比例
    @State proportion: number = 0;
    @Prop
    images: string[] = []
    onChangeAction?:(index:number)=>void

    aboutToAppear(): void {
      // 获取屏幕大小,用于后续计算卡片的偏移量
      const displayData: display.Display = display.getDefaultDisplaySync();
      this.displayWidth = px2vp(displayData.width);
      if ((display.isFoldable() && display.getFoldStatus() === display.FoldStatus.FOLD_STATUS_EXPANDED) ||
          this.displayWidth >= this.DEVICESIZE) {
        this.displayWidth = px2vp(displayData.width) / 2;
      }

      logger.info(TAG, `Display width ${this.displayWidth}`);
      // 添加卡片数据,来自预置用例,可根据实际情况自行修改。同时初始化偏移量列表。
      this.images.forEach((item: string, index: number) => {
        const model = {
          src: item,
          width: 120,
        } as CardInfo
        this.cardsList.push(model)
        this.cardsOffset.push(0)
        this.scaleList.push(index === 0 ? this.MAX_SCALE : this.MIN_SCALE);

      })
      // 初始化懒加载列表
      this.data = new CardsSource(this.cardsList);
      // 计算当前卡片及关联卡片的偏移量
      this.calculateOffset(0);
    }

    build() {
      Column() {
        Swiper() {
          LazyForEach(this.data, (item: CardInfo, index: number) => {
            CardComponent({
              cardInfo: item,
              cardOffset: this.cardsOffset[index],
              cardIndex: index,
              showingCard: this.currentSwiperIndex,
              scaleList: this.scaleList
            })
              .border({
                width: 1,
                color: Color.Transparent
              })
          })
        }
        .index($$this.currentSwiperIndex)
          .loop(false)
          .prevMargin(this.swiperMargin)
          .nextMargin(this.swiperMargin)
          .duration(Constants.DURATION)
          .curve(Curve.Friction)
          .indicator(false)
          .onChange((index) => {
            this.calculateOffset(index);
            // index发生变化时,修改数组中对应的缩放系数
            this.currentSwiperIndex = index;
            this.scaleList[this.currentSwiperIndex] = this.MAX_SCALE;
            // 若index为第一张图时,最后一张图片缩放系数为MIN_SCALE,否则index-1缩放系数为MIN_SCALE
            if (this.currentSwiperIndex === 0) {
              this.scaleList[this.scaleList.length - 1] = this.MIN_SCALE;
            } else {
              this.scaleList[this.currentSwiperIndex - 1] = this.MIN_SCALE;
            }
            // 若index为最后一张图时,第一张图缩放系数为MIN_SCALE,否则index+1缩放系数为MIN_SCALE
        if (this.currentSwiperIndex === this.scaleList.length - 1) {
          this.scaleList[0] = this.MIN_SCALE;
        } else {
          this.scaleList[this.currentSwiperIndex + 1] = this.MIN_SCALE;
        }
        this.onChangeAction && this.onChangeAction(index)
      })
      .onGestureSwipe((index, event) => {
        const currentOffset = event.currentOffset;
        // 获取当前卡片(居中)的原始偏移量
        const maxOffset = this.getMaxOffset(index) * this.proportion / 2;
        // 实时维护卡片的偏移量列表,做到跟手效果
        if (currentOffset < 0) {
          // 向左偏移
          /*
           * 此处计算原理为:按照比例设置卡片的偏移量。
           * 当前卡片居中,向左滑动后将在左边,此时卡片偏移量即为 maxOffset * 2(因为向右对齐)。
           * 所以手指能够滑动的最大距离(this.displayWidth)所带来的偏移量即为 maxOffset。
           * 易得公式:卡片实时偏移量 = (手指滑动长度 / 屏幕宽度) * 卡片最大可偏移量 + 当前偏移量。
           * 之后的计算原理相同,将不再赘述。
           */
          this.cardsOffset[index] = (-currentOffset / this.displayWidth) * maxOffset + maxOffset;
          if (this.isIndexValid(index + 1)) {
            // 下一个卡片的偏移量
            const maxOffset = this.getMaxOffset(index + 1) / 2 * this.proportion;
            this.cardsOffset[index + 1] = (-currentOffset / this.displayWidth) * maxOffset;
          }
          if (this.isIndexValid(index - 1)) {
            // 上一个卡片的偏移量
            const maxOffset = this.getMaxOffset(index - 1) / 2 * this.proportion;
            this.cardsOffset[index - 1] = (currentOffset / this.displayWidth) * maxOffset + 2 * maxOffset;
          }
        } else if (currentOffset > 0) {
          // 向右滑动
          this.cardsOffset[index] = maxOffset - (currentOffset / this.displayWidth) * maxOffset;
          if (this.isIndexValid(index + 1)) {
            const maxOffset = this.getMaxOffset(index + 1) / 2 * this.proportion;
            this.cardsOffset[index + 1] = (currentOffset / this.displayWidth) * maxOffset;
          }
          if (this.isIndexValid(index - 1)) {
            const maxOffset = this.getMaxOffset(index - 1) / 2 * this.proportion;
            this.cardsOffset[index - 1] = 2 * maxOffset - (currentOffset / this.displayWidth) * maxOffset;
          }
        }
        // 页面跟手滑动过程中触发回调,动态计算卡片滑动距离实时计算缩放系数。
        this.calculateScaling(index, currentOffset);
      })
      .onAnimationStart((index, targetIndex) => {
        this.calculateOffset(targetIndex);
        // 计算手指离开屏幕时卡片的缩放系数
        if (index === targetIndex) {
          let nextIndex: number = (index === this.scaleList.length - 1 ? 0 : index + 1);
          let preIndex: number = (index === 0 ? this.scaleList.length - 1 : index - 1);
          this.scaleList[index] = this.MAX_SCALE;
          this.scaleList[nextIndex] = this.MIN_SCALE;
          this.scaleList[preIndex] = this.MIN_SCALE;
        } else {
          let nextIndex: number = (targetIndex === this.scaleList.length - 1 ? 0 : targetIndex + 1);
          let preIndex: number = (targetIndex === 0 ? this.scaleList.length - 1 : targetIndex - 1);
          this.scaleList[targetIndex] = this.MAX_SCALE;
          this.scaleList[nextIndex] = this.MIN_SCALE;
          this.scaleList[preIndex] = this.MIN_SCALE;
        }
      })
      .onAnimationEnd(() => {
        this.startSwiperOffset = 0;
      })
      .height(160)
      .backgroundColor(Color.Grey)

    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }

  /**
   * 计算卡片偏移量,并维护偏移量列表。
   * @param targetIndex { number } swiper target card's index.
   */
  calculateOffset(target: number) {
    let left = target - 1;
    let right = target + 1;

    // 计算上一张卡片的偏移值
    if (this.isIndexValid(left)) {
      this.cardsOffset[left] = this.getMaxOffset(left) - this.cardsList[left].width * (1 - this.MIN_SCALE) / 2;
    }
    // 计算当前卡片的偏移值
    if (this.isIndexValid(target)) {
      this.cardsOffset[target] = this.getMaxOffset(target) * this.proportion / 2;
    }
    // 下一张片的偏移值
    if (this.isIndexValid(right)) {
      this.cardsOffset[right] = -this.cardsList[right].width * (1 - this.MIN_SCALE) / 2;
    }
  }

  /**
   * 检查卡片索引值的合法性。
   * @param index {number} input card's index.
   * @returns true or false.
   */
  isIndexValid(index: number): boolean {
    return index >= 0 && index < this.cardsList.length;
  }

  /**
   * 计算指定卡片的最大偏移量。
   * @param index {number} target card's index.
   * @returns offset value.
   */
  getMaxOffset(index: number): number {
    /*
     * 这里的偏移量指相对容器左侧的值。
     * 计算公式为:屏幕宽度 - Swiper两侧突出的偏移量 - 卡片自身的宽度。
     * 此值即为卡片可偏移的最大值,也就是卡片右对齐的状态值。
     * 如果居中,则将最大偏移量 / 2。
     */
    // 原图时最大偏移量
    let maxOffset: number = this.displayWidth - this.cardsList[index].width - 2 * this.swiperMargin;
    // 缩放时最大偏移量
    let maxOffsetScale: number =
      this.displayWidth - this.cardsList[index].width * this.MIN_SCALE - 2 * this.swiperMargin;
    this.proportion = maxOffset / maxOffsetScale;
    return maxOffsetScale;
  }

  /**
   * 根据卡片滑动距离实时计算卡片缩放系数。
   * @param index {number} target card's index.
   * @param offset {number} current Offset distance.
   */
  calculateScaling(index: number, offset: number) {
    let currentScale: number = this.scaleList[index];
    let nextIndex: number = (index === this.scaleList.length - 1 ? 0 : index + 1);
    let preIndex: number = (index === 0 ? this.scaleList.length - 1 : index - 1);
    let nextScale: number = this.scaleList[nextIndex];
    let preScale: number = this.scaleList[preIndex];
    if (this.startSwiperOffset === 0) {
      this.startSwiperOffset = offset;
    }
    // 滑动距离
    let distance: number = Math.abs(this.startSwiperOffset - offset);
    currentScale = this.MAX_SCALE - Math.min(distance / this.displayWidth, this.MAX_SCALE - this.MIN_SCALE);
    // 滑动时实时缩放的比例
    if (this.startSwiperOffset > offset) {
      nextScale = this.MIN_SCALE + Math.min(distance / this.displayWidth, this.MAX_SCALE - this.MIN_SCALE);
      preScale = this.MIN_SCALE;
    } else {
      preScale = this.MIN_SCALE + Math.min(distance / this.displayWidth, this.MAX_SCALE - this.MIN_SCALE);
      nextScale = this.MIN_SCALE;
    }
    this.scaleList[this.currentSwiperIndex] = currentScale;
    this.scaleList[nextIndex] = nextScale;
    this.scaleList[preIndex] = preScale;
  }
}

五、总结

以上便是达成自定义轮播功能的完整实现思路。其中,最为关键的环节在于精准计算滑动过程中实时缩放的比例,并借助动画这一表现形式将其流畅地展示出来,从而契合预期的视觉效果和交互需求,为用户带来更为出色的体验。


更多关于HarmonyOS 鸿蒙Next 自定义轮播封装分享的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html

1 回复

更多关于HarmonyOS 鸿蒙Next 自定义轮播封装分享的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


针对您分享的HarmonyOS 鸿蒙Next自定义轮播封装,以下是一个简洁的专业回答:

在HarmonyOS鸿蒙Next系统中,自定义轮播封装可以通过使用ArkUI框架的组件和动画功能来实现。首先,需要定义轮播项的布局和样式,这可以通过XML或ETS(Enhanced TypeScript)来完成。每个轮播项可以包含图片、文本或其他自定义组件。

为了实现轮播效果,可以利用动画API来控制轮播项的切换。具体来说,可以使用动画插值器(Interpolator)和定时器(Timer)来定义轮播的平滑度和切换速度。通过监听动画的完成事件,可以实现自动循环播放。

在轮播逻辑中,需要维护一个当前轮播项的索引,并在动画结束时更新该索引以切换到下一个轮播项。同时,需要处理边界情况,如从最后一个轮播项切换到第一个轮播项,或从第一个轮播项切换到最后一个轮播项。

此外,为了提高用户体验,可以添加用户交互功能,如手势滑动切换轮播项或点击停止/继续轮播。这些交互可以通过监听器(Listener)来实现,并根据用户操作来更新轮播状态。

如果问题依旧没法解决请联系官网客服,官网地址是:https://www.itying.com/category-93-b0.html

回到顶部