HarmonyOS Next应用开发实战——商品详情页构建(part2)

126 阅读1分钟
3. 弹出框构建

bindSheetBuilder方法用于构建弹出框内容,包含商品图片、描述、价格、颜色选择、数量选择以及购物操作按钮。

@Builder bindSheetBuilder(){
  Column({ space: CommonConstants.MAIN_PADDING * 2  }){
    Column({ space: CommonConstants.MAIN_PADDING * 2  }){
      Row(){
        Text('×').fontSize(24)
          .onClick(()=>{
            this.isShow = false;
          })
      }
      .justifyContent(FlexAlign.End) .width(CommonConstants.COLUMN_WIDTH)

      Row({ space: CommonConstants.PUBLIC_SPACE }){
        Image(this.commodityData?.picture)
          .width(100).height(100)
        Column({ space: CommonConstants.PUBLIC_SPACE }){
          Text(this.commodityData?.description)
            .fontWeight(FontWeight.Bold)
          Blank()
          Text(){
            Span('¥')
            Span(`${this.commodityData?.price}`)
              .fontSize(CommonConstants.M_FONT_SIZE)
              .fontWeight(FontWeight.Bold)
          }
          .fontSize(CommonConstants.S_FONT_SIZE)
          Text(`已选:${this.checked ? '图片色' : ''}`).fontSize(CommonConstants.S_FONT_SIZE)
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Start)
      }

      Column({ space: CommonConstants.PUBLIC_SPACE }){
        Text('图片色')
          .fontSize(CommonConstants.M_FONT_SIZE)
          .fontColor(ShoppingMallConstants.NOT_CHECKED_COLOR)
        Text('图片色')
          .fontColor(this.checked ? ShoppingMallConstants.CHECKED_COLOR : ShoppingMallConstants.NOT_CHECKED_COLOR)
          .fontSize(CommonConstants.M_FONT_SIZE)
          .padding({
            top: CommonConstants.MAIN_PADDING / 2,
            bottom: CommonConstants.MAIN_PADDING / 2,
            left: CommonConstants.MAIN_PADDING,
            right: CommonConstants.MAIN_PADDING
          })
          .border({ width : 1 , color: this.checked ? ShoppingMallConstants.CHECKED_COLOR : ShoppingMallConstants.NOT_CHECKED_COLOR })
          .onClick(()=>{ this.checked = !this.checked; })
      }
      .width(CommonConstants.COLUMN_WIDTH)
      .alignItems(HorizontalAlign.Start)

      Column({ space: CommonConstants.PUBLIC_SPACE }){
        Text('数量')
          .fontSize(CommonConstants.M_FONT_SIZE)
          .fontColor(ShoppingMallConstants.NOT_CHECKED_COLOR)
        Counter(){
          Text(this.addCount.toString())
            .fontSize(CommonConstants.M_FONT_SIZE)
        }
        .onInc(() => { this.addCount++ })
        .onDec(() => {  this.addCount > 1 ? (this.addCount--) : '' })
      }
      .width(CommonConstants.COLUMN_WIDTH)
      .alignItems(HorizontalAlign.Start)
    }
    .padding({
      top: CommonConstants.MAIN_PADDING * 2,
      left: CommonConstants.MAIN_PADDING * 2,
      right: CommonConstants.MAIN_PADDING * 2,
    })
    Row(){
      this.footer()
    }.width(CommonConstants.COLUMN_WIDTH)
    .padding({
      right: CommonConstants.MAIN_PADDING,
      left: CommonConstants.MAIN_PADDING,
    })
  }
}
4. 页面构建与状态管理

build方法中,构建了整个商品详情页的布局,包括顶部导航、商品信息展示、滚动区域和页脚。同时,通过@State装饰器管理页面状态,如isShow控制弹出框的显示与隐藏,scrollHeight用于处理滚动交互。

build() {### 标题:HarmonyOS Next应用开发实战——探索页多标签滑动联动实现

#### 文章概要
本文聚焦于HarmonyOS Next应用开发中探索页面的实现,详细介绍了多标签滑动联动的功能。包括获取屏幕信息、处理组件属性、实现标签下划线动画效果,以及标签与滑动页面的联动,为开发者在HarmonyOS Next应用中实现类似交互效果提供参考。

#### 核心功能介绍

##### 1. 初始化屏幕信息
在页面即将显示时,获取屏幕的实例信息,为后续的布局和动画计算提供基础。
```typescript
aboutToAppear(): void {
  this.displayInfo = display.getDefaultDisplaySync(); //获取屏幕实例
}
2. 获取屏幕和组件信息

通过getDisplayWidth方法获取屏幕宽度,getTextInfo方法获取组件的位置和宽度信息。

// 获取屏幕宽度,单位vp
private getDisplayWidth(): number {
  return this.displayInfo != null ? px2vp(this.displayInfo.width) : 0;
}

// 获取组件大小、位置、平移缩放旋转及仿射矩阵属性信息。
private getTextInfo(index: number): Record<string, number> {
  let modePosition: componentUtils.ComponentInfo = componentUtils.getRectangleById(index.toString());
  try {
    return { 'left': px2vp(modePosition.windowOffset.x), 'width': px2vp(modePosition.size.width) }
  } catch (error) {
    return { 'left': 0, 'width': 0 }
  }
}
3. 下划线动画效果

getCurrentIndicatorInfo方法根据滑动事件计算当前下划线的位置和宽度,实现下划线跟随页面滑动和宽度渐变的效果。

// 当前下划线动画
private getCurrentIndicatorInfo(index: number, event: SwiperAnimationEvent): Record<string, number> {
  let nextIndex = index;
  // 滑动范围限制,Swiper不可循环,Scroll保持不可循环
  if (index > 0 && event.currentOffset > 0) {
    nextIndex--; // 左滑
  } else if (index < this.arr.length - 1 && event.currentOffset < 0) {
    nextIndex++; // 右滑
  }
  // 获取当前tabbar的属性信息
  let indexInfo = this.getTextInfo(index);
  // 获取目标tabbar的属性信息
  let nextIndexInfo = this.getTextInfo(nextIndex);
  // 滑动页面超过一半时页面切换
  this.swipeRatio = Math.abs(event.currentOffset / this.swiperWidth);
  let currentIndex = this.swipeRatio > 0.5 ? nextIndex : index; // 页面滑动超过一半,tabBar切换到下一页。
  let currentLeft = indexInfo.left + (nextIndexInfo.left - indexInfo.left) * this.swipeRatio;
  let currentWidth = indexInfo.width + (nextIndexInfo.width - indexInfo.width) * this.swipeRatio;
  this.indicatorIndex = currentIndex;
  return { 'index': currentIndex, 'left': currentLeft, 'width': currentWidth };
}
4. 标签与滑动页面联动

Swiper组件的事件回调中,如onAnimationStartonAnimationEndonGestureSwipe,实现标签与滑动页面的联动,确保标签和页面状态一致。

Swiper(this.swiperController) {
  // ...
}
.onChange((index: number) => {
  this.swiperIndex = index;
})
// 跟自定义tabbar进行联动效果
.index(this.swiperIndex)
.onAnimationStart((index: number, targetIndex: number, event: SwiperAnimationEvent) => {
  // 切换动画开始时触发该回调。下划线跟着页面一起滑动,同时宽度渐变。
  this.indicatorIndex = targetIndex;
  this.underlineScrollAuto(this.animationDuration, targetIndex);
})
.onAnimationEnd((index: number, event: SwiperAnimationEvent) => {
  // 切换动画结束时触发该回调。下划线动画停止。
  this.scrollIntoView(index);
})
.onGestureSwipe((index: number, event: SwiperAnimationEvent) => {
  // 在页面跟手滑动过程中,逐帧触发该回调。
  let currentIndicatorInfo = this.getCurrentIndicatorInfo(index, event);
  this.indicatorIndex = currentIndicatorInfo.index; //当前页签index
  let positionXOther = (this.getCurrTextWidth(index) - this.indicatorWidth) / 2;
  this.indicatorMarginLeft = currentIndicatorInfo.left + positionXOther; //当前页签距离左边margin
})

通过以上核心功能的实现,在HarmonyOS Next应用中可以实现一个具有流畅交互效果的探索页面,提升用户体验。

NavDestination(){ Column() { Row(){ Image(r(app.media.back)).width(ShoppingMallConstants.ICONWIDTH).fillColor(this.scrollHeight>ShoppingMallConstants.SCROLLhEIGHT?Color.Black:Color.White).onClick(()=>this.pageInfos.pop())Blank()Image(r('app.media.back')) .width(ShoppingMallConstants.ICON_WIDTH) .fillColor(this.scrollHeight > ShoppingMallConstants.SCROLL_hEIGHT ? Color.Black : Color.White) .onClick(()=>{ this.pageInfos.pop() }) Blank() Image(r('app.media.share_windows')) .width(ShoppingMallConstants.ICON_WIDTH) .fillColor(this.scrollHeight > ShoppingMallConstants.SCROLL_hEIGHT ? Color.Black : Color.White) } .width(CommonConstants.COLUMN_WIDTH) .padding({ top: CommonConstants.MAIN_PADDING + this.topHeight, bottom: CommonConstants.MAIN_PADDING, left: CommonConstants.MAIN_PADDING, right: CommonConstants.MAIN_PADDING }) .position({ y: 0 }).zIndex(1) .backgroundColor(rgba(255, 255, 255, ${this.scrollHeight > ShoppingMallConstants.SCROLL_hEIGHT ? 0.9 : 0})) Flex({ direction : FlexDirection.Column }){ Scroll(){ Column({ space: CommonConstants.PUBLIC_SPACE }){ Image(this.commodityData?.picture) .width(CommonConstants.COLUMN_WIDTH).height(ShoppingMallConstants.SWIPER_HEIGHT) Column({ space: CommonConstants.PUBLIC_SPACE * 2 }){ Text(this.commodityData?.description) .fontSize(CommonConstants.L_FONT_SIZE) .fontWeight(FontWeight.Bold) Text(){ Span('¥').fontSize(CommonConstants.M_FONT_SIZE) Span(${this.commodityData?.price}) .fontSize(CommonConstants.L_FONT_SIZE) .fontWeight(FontWeight.Bold) } Column({ space: CommonConstants.PUBLIC_SPACE }){ Text(){ Span('当前库存:') .fontSize(CommonConstants.M_FONT_SIZE) Span(${this.commodityData?.inventory}) .fontSize(CommonConstants.M_FONT_SIZE) .fontWeight(FontWeight.Bold) }.fontColor(ShoppingMallConstants.NOT_CHECKED_COLOR) Divider().backgroundColor(ShoppingMallConstants.LINE_COLOR).strokeWidth(1) Row(){ Text('购买须知').fontSize(CommonConstants.M_FONT_SIZE) Blank() Image($r('app.media.mi_ic_right_arrow')) .width(ShoppingMallConstants.ICON_WIDTH / 2) }.width(CommonConstants.COLUMN_WIDTH) Divider().backgroundColor(ShoppingMallConstants.LINE_COLOR).strokeWidth(1) } .width(CommonConstants.COLUMN_WIDTH) .alignItems(HorizontalAlign.Start)

          Column(){
            Image(this.commodityData?.picture)
          }
          .width(CommonConstants.COLUMN_WIDTH)
          .height(800)
        }
        .padding(CommonConstants.MAIN_PADDING)
        .alignItems(HorizontalAlign.Start)
        .width(CommonConstants.COLUMN_WIDTH)
      }
    }
    .layoutWeight(1)
    .onScroll((xOffset: number, yOffset: number)=>{
      this.scrollHeight += yOffset;
    })

    Divider().backgroundColor(Color.Red).strokeWidth(0.5)
    this.footer(
      ()=>{ this.isShow = true; },
      ()=>{ this.isShow = true; }
    )
  }
  .width(CommonConstants.COLUMN_WIDTH)
  .height(CommonConstants.COLUMN_HEIGHT)
}
.width(CommonConstants.COLUMN_WIDTH)
.height(CommonConstants.COLUMN_HEIGHT)
.bindSheet($$this.isShow, this.bindSheetBuilder(), {
  height: SheetSize.FIT_CONTENT,
  showClose: false
})

} .hideTitleBar(true) .onReady((ctx: NavDestinationContext) => { try { const id = ctx?.pathInfo?.param as string; this.commodityData = this.commodityModel.getCommodityDetail(id); } catch (e) { // ... } }) }


在HarmonyOS Next应用开发中,通过合理运用组件、状态管理和事件处理,我们可以构建出功能丰富、交互性强的商品详情页。