c77fb700创建于 2025年1月16日历史提交
/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { router } from '@kit.ArkUI';
import { BreakpointTypeEnum, Logger } from '@ohos/utils';
import ZonesItem from '../model/ZonesItem';
import { ChallengeConstants as Const } from '../constants/ChallengeConstants';
import { ICON_SUBTITLE_ARRAY, IconSubtitle } from '../model/IconSubtitleData';
import BuildListItem from '../components/BuildListItem';
import StyleListItem from '../components/StyleListItem';
import SubTitleItem from '../components/SubTitleItem';
import { ChallengeItemBuilder } from '../components/ChallengeItem';
import LocationItem from '../components/LocationItem';

const TAG: string = '[ZoneDetailView]';
const MAX_IMG_RATIO: number = 0.15;

@Component
export struct ZoneDetailView {
  private scroller: Scroller = new Scroller();
  @StorageProp('deviceWidth') @Watch('handleBreakpointChange') deviceWidth: number =
    AppStorage.get('deviceWidth') as number;
  @StorageProp('deviceHeight') imageMaxHeight: number = AppStorage.get('deviceHeight') as number;
  @State imageHeight: number = this.imageMaxHeight;
  @State imageMinHeight: number = 0;
  detailSheetMinHeight: number = 0;
  detailSheetMaxHeight: number = 0;
  @Link @Watch('changeImageHeight') detailSheetHeight: number;
  @State arrowIconOpacity: number = 0;
  @State isReachStart: boolean = false;
  @State currentStyleIndex: number = 0;
  @Consume('introductionData') introductionData: ZonesItem;
  @State topImgOffset: number = 0;
  @State locationImgScale: number = 0;
  @StorageProp('currentBreakpoint') @Watch('handleBreakpointChange') currentBreakpoint: string = BreakpointTypeEnum.SM;

  getRatios(): number[] {
    let ratio: number = 0;
    let arr: number[] = [0];
    while (ratio < 1) {
      ratio += 0.01;
      arr.push(ratio);
    }
    return arr;
  }

  changeImageHeight() {
    this.imageHeight =
      this.detailSheetHeight > this.detailSheetMinHeight ? (this.detailSheetHeight - Const.MODAL_BOTTOM_HEIGHT) :
        (this.detailSheetMinHeight - Const.MODAL_BOTTOM_HEIGHT);
    this.currentStyleIndex = 0;
    this.arrowIconOpacity = this.detailSheetMinHeight === this.detailSheetHeight ? 0 : 1;
  }

  dynamicLoading(): void {
    try {
      import('./ChallengeDetailView');
    } catch (err) {
      Logger.error(TAG, 'dynamicLoading error:' + err);
    }
  }

  aboutToAppear() {
    this.dynamicLoading();
    this.handleBreakpointChange();
    this.arrowIconOpacity = 0;
  }

  handleBreakpointChange() {
    this.imageMaxHeight = this.detailSheetMaxHeight - 96;
    this.imageMinHeight = Math.floor((this.currentBreakpoint === BreakpointTypeEnum.SM ?
    this.deviceWidth : Const.MODAL_MAX_WIDTH) * Const.IMG_ASPECT_RATIO);
    this.imageHeight = this.imageMinHeight;
  }

  handleVisibleAreaChange(isVisible: boolean, currentRatio: number) {
    Logger.info('StyleListItem isVisible: ' + isVisible + ', currentRatio:' + currentRatio);
    if (isVisible) {
      this.currentStyleIndex = currentRatio > 0.8 ? 2 : 1;
    } else {
      this.currentStyleIndex = currentRatio > 0.2 ? 1 : 0;
    }
  }

  handleScrollFrame(offset: number): number {
    let tempImgHeight: number = Math.ceil(this.imageHeight - offset);
    let yOffset: number = this.scroller.currentOffset().yOffset;
    Logger.info(TAG, 'yOffset: ' + offset + '   Height: ' + (this.imageMaxHeight - this.imageMinHeight));
    if (yOffset > 0) {
      Logger.info(TAG, 'unHandle Image');
      this.imageHeight = this.imageMinHeight;
      return offset;
    }
    if (tempImgHeight >= this.imageMinHeight && tempImgHeight <= this.imageMaxHeight) {
      Logger.info(TAG, 'scale Image');
      this.imageHeight = tempImgHeight;
      this.arrowIconOpacity = (this.imageHeight - this.imageMinHeight) / (this.imageMaxHeight - this.imageMinHeight);
      return 0;
    }
    if (tempImgHeight < this.imageMinHeight) {
      Logger.info(TAG, 'min Image');
      this.imageHeight = this.imageMinHeight;
      return (this.imageMinHeight - tempImgHeight);
    }
    if (tempImgHeight > this.imageMaxHeight) {
      Logger.info(TAG, 'max Image');
      this.imageHeight = this.imageMaxHeight;
      return (this.imageMaxHeight - tempImgHeight);
    }
    return offset;
  }

  /**
   * Click the up arrow scroll to the beginning.
   */
  scrollToTop() {
    this.scroller.scrollTo({
      xOffset: 0,
      yOffset: 0,
      animation: {
        duration: Const.IMAGE_ANIMATION_DURATION,
        curve: Curve.Linear,
      }
    });
    animateTo({
      duration: Const.IMAGE_ANIMATION_DURATION,
    }, () => {
      this.arrowIconOpacity = 1;
      this.imageHeight = this.imageMaxHeight;
      this.currentStyleIndex = 0;
    });
  }

  @Builder
  StyleTitle(item: IconSubtitle, index: number) {
    Column() {
      Image(item.icon)
        .width($r('app.float.title_icon_width'))
        .aspectRatio(1)
        .objectFit(ImageFit.Contain)
      Text(item.title)
        .fontSize($r('app.float.small_font_size'))
        .fontColor($r('sys.color.ohos_id_color_titlebar_subtitle_text'))
        .margin({ top: $r('app.float.title_style_margin'), bottom: $r('app.float.title_style_margin') })
        .fontWeight(FontWeight.Medium)
      Text(item.enTitle)
        .fontSize($r('app.float.smaller_font_size'))
        .fontColor($r('sys.color.ohos_id_color_titlebar_subtitle_text'))
        .margin({ bottom: $r('app.float.icon_title_margin_bottom') })
        .fontWeight(FontWeight.Bold)
    }
    .margin({ top: $r('app.float.style_margin_top') })
    .scale({ x: this.currentStyleIndex === index ? 1 : 0 })
    .opacity(this.currentStyleIndex === index ? 1 : 0)
    .animation({
      duration: Const.TITLE_ICON_ANIMATION_DURATION,
      curve: Curve.EaseOut
    })
  }

  @Builder
  StickyHeader() {
    Column() {
      Column() {
        Image(this.introductionData.subPicBottom)
          .width(Const.FULL_PERCENT)
          .aspectRatio(Const.HEAD_IMG_ASPECT)
          .objectFit(ImageFit.Fill)
      }
      .width(Const.FULL_PERCENT)
      .backgroundColor(this.introductionData.backgroundColor)
      .margin({ top: $r('app.float.intro_margin_top') })

      Image(this.introductionData.titleIcon)
        .margin({ top: $r('app.float.xxl_padding_margin') })
        .width($r('app.float.introduction_title_icon_width'))
        .height($r('app.float.introduction_title_icon_height'))
      Stack() {
        ForEach(ICON_SUBTITLE_ARRAY, (item: IconSubtitle, index?: number) => {
          if (index !== undefined) {
            this.StyleTitle(item, index)
          }
        })
      }
    }
    .backgroundColor($r('sys.color.ohos_id_color_sub_background'))
    .alignItems(HorizontalAlign.Center)
  }

  build() {
    Stack({ alignContent: Alignment.Bottom }) {
      Column() {
        List({ scroller: this.scroller }) {
          ListItem() {
            Stack({ alignContent: Alignment.Bottom }) {
              Image(this.introductionData.swiperPic)
                .width(Const.FULL_PERCENT)
                .height(this.imageHeight)
                .objectFit(ImageFit.Cover)
              Column() {
                Text(this.introductionData.title)
                  .fontSize($r('app.float.zone_title_size'))
                  .fontWeight(FontWeight.Bold)
                  .fontColor($r('app.color.white'))
                  .margin({
                    top: $r('app.float.sm_padding_margin'),
                    bottom: $r('app.float.arrow_down_margin')
                  })
                Image($r('app.media.ic_arrow_down'))
                  .height($r('app.float.smaller_font_size'))
                  .width($r('app.float.arrow_down_width'))
              }
              .width(Const.FULL_PERCENT)
              .margin({ bottom: $r('app.float.arrow_down_margin') })
              .opacity(this.detailSheetHeight >= this.detailSheetMaxHeight ? this.arrowIconOpacity : 0)
              .animation({
                duration: Const.ANIMATION_DURATION_NORMAL,
                curve: Curve.EaseOut,
              })
            }
          }

          ListItem() {
            SubTitleItem({ topImgOffset: this.topImgOffset })
          }

          ListItemGroup({ header: this.StickyHeader() }) {
            ListItem() {
              StyleListItem()
            }
            .onVisibleAreaChange(this.getRatios(), (isVisible: boolean, currentRatio: number) => {
              Logger.info(TAG, 'StyleListItem isVisible: ' + isVisible + ', currentRatio:' + currentRatio);
              if (currentRatio <= Const.HALF_NUMBER) {
                this.topImgOffset = Math.ceil(currentRatio * Const.IMG_COEFFICIENT);
              }
            })

            ListItem() {
              BuildListItem({ buildingImgRatio: Math.min(this.locationImgScale, MAX_IMG_RATIO) })
            }

            ListItem() {
              Column() {
                LocationItem({ imgScale: this.locationImgScale })
                ChallengeItemBuilder(BreakpointTypeEnum.SM, () => {
                  router.pushNamedRoute({
                    name: 'ChallengeDetailView',
                    params: undefined
                  })
                })
              }
              .onVisibleAreaChange(this.getRatios(), (isVisible: boolean, currentRatio: number) => {
                this.locationImgScale = currentRatio;
                this.handleVisibleAreaChange(isVisible, currentRatio);
              })
            }
          }
        }
        .width(Const.FULL_PERCENT)
        .height(Const.FULL_PERCENT)
        .edgeEffect(EdgeEffect.None)
        .enableScrollInteraction(this.detailSheetHeight >= this.detailSheetMaxHeight)
        .scrollBar(BarState.Off)
        .sticky(StickyStyle.Header)
        .onScrollFrameBegin((offset: number) => {
          return { offsetRemain: this.handleScrollFrame(offset) };
        })
      }
      .height(Const.FULL_PERCENT)

      Image($r('app.media.ic_up_icon'))
        .height($r('app.float.page_icon_size'))
        .aspectRatio(1)
        .opacity(this.detailSheetHeight === this.detailSheetMaxHeight ? (1 - this.arrowIconOpacity) : 0)
        .position({
          x: Const.PERCENT_87,
          y: Const.PERCENT_90
        })
        .onClick(() => {
          this.scrollToTop();
        })
    }
    .width(Const.FULL_PERCENT)
    .backgroundColor($r('sys.color.ohos_id_color_sub_background'))
    .height(Const.FULL_PERCENT)
  }
}