/*
 * Copyright (c) 2024 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 { HomePage } from '../model/HomePage'; // 首页
import { DetailsPage } from '../model/DetailsPage'; // 全屏播放页
import { AnimationInfo } from '../model/AnimationInfo'; // 动画相关参数类
import Constants from '../model/Constants'; // 常量类
import display from '@ohos.display'; // 屏幕属性模块。本例用于获取屏幕宽高信息。
import window from '@ohos.window'; // 窗口模块。本例用于获取屏幕中顶部和底部非安全区域高度。
import common from '@ohos.app.ability.common';
import animator, { AnimatorResult } from '@ohos.animator'; // 动画模块
import { emitter } from '@kit.BasicServicesKit';

/**
 * 实现步骤
 * 本例中一镜到底动画分两块:1.Mini条展开和收起的一镜到底动画 2.全屏播放页上下拖动的手势动画和松手后的回弹动画
 * 1.Mini条展开和收起的一镜到底动画。本例中展开和收起动画(expandCollapseAnimation())大部分动画相同,且共用同一个动画对象animatorObject,
 * 主要有三部分动画组成。以Mini条展开动画为例,分为:
 * (1)Mini条歌曲封面缩放和X,Y轴偏移动画
 * (2)Mini条向上平移,高度拉伸,同时透明度降低动画
 * (3)全屏播放页向上平移,同时透明度增加动画
 * 本例中使用@ohos.animator动画模块的AnimatorResult定义动画对象,通过创建AnimatorOptions动画选项,并传入create来创建Animator对象
 * animatorObject。通过play()启动动画,在动画帧回调onframe中通过参数value获取动画进度,然后根据动画进度实时改
 * 变自定义动画相关属性AnimationInfo的值来实现Mini条展开和收起的一镜到底动画。
 * 2.全屏播放页上下拖动的手势动画和松手后的回弹动画。
 * (1)本例中全屏播放页上下拖动的手势动画和Mini条收起动画实现方式类似。Mini条收起动画是在动画帧回调onframe中通过参数value获取动画进度,而拖动
 * 手势动画是在PanGesture拖动手势的onActionUpdate移动回调中,通过滑动偏移量event.offsetY,计算动画进度,然后根据动画进度实时改变自定义动画
 * 相关属性AnimationInfo的值来实现全屏播放页上下拖动的手势动画。
 * (2)本例中全屏播放页拖动松手后的回弹动画使用显示动画animateTo,在PanGesture拖动手势的onActionEnd手指抬起回调中,通过前面拖动手势动画中计
 * 算的动画过程中全屏播放页Y轴位置detailsPagePositionY。判断抬手时,当全屏播放页Y轴位置小于等于1/2屏幕高度,全屏播放页做向上回弹动画。当全屏
 * 播放页Y轴位置大于1/2屏幕高度时,做向下回弹动画。
 */

const context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;

@Component
export struct MiniPlayerAnimation {
  // 动画相关参数类
  @Provide @Watch('animationInfoChange') animationData: AnimationInfo = new AnimationInfo();
  // 动画对象
  @State animatorObject: AnimatorResult | undefined = undefined;
  // 依据cases工程Navigation的mode属性说明,如使用Auto,窗口宽度>=600vp时,采用Split模式显示;窗口宽度<600vp时,采用Stack模式显示。
  private readonly DEVICESIZE: number = 600;
  // 检查设备是否可折叠
  private isFoldable: boolean = false;
  // 折叠设备屏幕显示模式回调
  private callback: Callback<display.FoldDisplayMode> = (mode: display.FoldDisplayMode) => {
    // 可折叠设备的显示模式改变时(如展开或者折叠),重新获取屏幕宽度
    this.animationData.screenWidth = this.getCurrentScreenWidth();
  };

  /**
   * 动画信息变化,用于自动化用例
   */
  animationInfoChange() {
    // 全屏播放页Y轴位置
    let detailsPageYPosition = this.animationData?.screenHeight - this.animationData?.detailsPageOffsetY -
      this.animationData?.miniDistanceToBottom;
    // Mini条透明度
    let miniPlayerOpacity = this.animationData?.miniPlayerOpacity;
    // 全屏播放页透明度
    let detailsPageOpacity = this.animationData?.detailsPageOpacity;
    // 歌曲封面图偏移量
    let miniImgOffsetSize = this.animationData?.miniImgOffsetSize;
    // 歌曲封面图X轴偏移距离
    let miniImgOffsetX = this.animationData?.miniImgOffsetX;
    // 设备屏幕宽度
    let screenWidth = this.animationData?.screenWidth;
    // 设备屏幕高度
    let screenHeight = this.animationData?.screenHeight;
    // 全屏播放页左上角收起按钮和右上角分享按钮图标透明度
    let detailsPageTopOpacity = this.animationData?.detailsPageTopOpacity;
    emitter.emit({ eventId: 0, priority: 0 }, {
      data: {
        detailsPageYPosition: detailsPageYPosition,
        miniPlayerOpacity: miniPlayerOpacity,
        detailsPageOpacity: detailsPageOpacity,
        miniImgOffsetSize: miniImgOffsetSize,
        miniImgOffsetX: miniImgOffsetX,
        screenWidth: screenWidth,
        screenHeight: screenHeight,
        detailsPageTopOpacity: detailsPageTopOpacity
      }
    });
  }

  /**
   * 获取当前屏幕宽度
   */
  getCurrentScreenWidth(): number {
    let screenW: number = px2vp(display.getDefaultDisplaySync().width);
    // 适配cases中Navigation在不同mode时,计算相对需要使用的屏幕宽度。当屏幕宽度大于600vp时,cases工程Navigation的mode采用Split模式显示,需要重新计算实际页面所需的屏幕宽度。
    if (screenW >= this.DEVICESIZE) {
      return screenW / 2;
    } else {
      return screenW;
    }
  }

  /**
   * 获取屏幕宽高,顶部和底部非安全区域高度
   */
  async aboutToAppear() {
    // 检查设备是否可折叠。false表示不可折叠,true表示可折叠。
    this.isFoldable = display.isFoldable();
    if (this.isFoldable) {
      // 如果是可折叠设备,注册折叠设备屏幕显示模式变化监听
      display.on('foldDisplayModeChange', this.callback);
    }
    // 获取屏幕宽高
    this.animationData.screenHeight = px2vp(display.getDefaultDisplaySync().height);
    this.animationData.screenWidth = this.getCurrentScreenWidth();
    const windowHight: window.Window = await window.getLastWindow(context);
    // 获取顶部非安全区域高度(状态栏高度)
    const TOP_UNSAFE_HEIGHT: number =
      px2vp(windowHight.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM).topRect.height);
    // 获取底部非安全区域高度(导航栏高度)
    const BOTTOM_UNSAFE_HEIGHT: number =
      px2vp(windowHight.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR).bottomRect.height);
    this.animationData.topUnsafeHeight = TOP_UNSAFE_HEIGHT;
    // 计算全屏播放页歌曲封面Y轴位置。全屏播放页歌曲封面Y轴位置=顶部非安全区域高度(状态栏高度)+全屏播放页左上角收起按钮父容器Row高度+全屏播放页左上角收起按钮父容器Row的底部外边距
    this.animationData.detailsPageImgPositionY =
      this.animationData.topUnsafeHeight + Constants.DOWN_ARROW_ROW_HEIGHT + Constants.ROW_MARGIN_BOTTOM;
    this.animationData.bottomUnsafeHeight = BOTTOM_UNSAFE_HEIGHT;
    // 创建动画
    this.createAnimation();
    //定义事件ID
    let innerEvent: emitter.InnerEvent = { eventId: 1 }
    emitter.on(innerEvent, data => {
      if (data?.data?.backPressed) {
        if (this.animatorObject) {
          // 启动动画。这里为收起动画
          if (this.animationData.isExpand) {
            this.animatorObject.play();
            this.animationData.isAnimating = true;
          } else {
            emitter.emit({ eventId: 100, priority: 0 }, {});
          }
        }
      }
    })
  }

  aboutToDisappear() {
    // TODO 知识点:由于animatorObject在onframe中引用了this, this中保存了animatorObject,在自定义组件消失时应该将保存在组件中的animatorObject置空,避免内存泄漏。
    this.animatorObject = undefined;
    if (this.isFoldable) {
      // 关闭显示设备变化的监听
      display.off('foldDisplayModeChange', this.callback);
    }
  }

  /**
   * 展开收起动画公共部分
   */
  expandCollapseAnimation(progress: number) {
    // Mini条歌曲封面动画过程中尺寸变化量
    this.animationData.miniImgOffsetSize = (Constants.DETAILS_PAGE_IMG_SIZE - Constants.MINI_IMG_SIZE) * progress;
    // Mini条歌曲封面动画过程中X轴偏移量。Mini条歌曲封面X轴偏移=((屏幕宽度-Mini条歌曲封面尺寸-Mini条歌曲封面动画过程中尺寸变化量)/2-Mini条X轴位置-Mini条歌曲封面距离左侧的外边距)*动画进度
    this.animationData.miniImgOffsetX = ((this.animationData.screenWidth - Constants.MINI_IMG_SIZE -
    this.animationData.miniImgOffsetSize) / 2 - Constants.MINI_POSITION_X - Constants.MINI_IMG_MARGIN_LEFT) * progress;
    if (progress <= Constants.ANIMATION_PROGRESS) {
      // 为了达到更好的动画效果。动画进度0%-30%时,全屏播放页Y轴偏移距离和Mini条,Mini条歌曲封面保持相同的偏移距离
      this.animationData.detailsPageOffsetY = this.animationData.miniImgOffsetY;
      // Mini条透明度。动画进度0%-30%时,Mini条透明度从1降低到0。
      this.animationData.miniPlayerOpacity = 1 - progress / Constants.ANIMATION_PROGRESS;
    } else {
      // 由于动画进度0%-30%时改变了原全屏播放页的偏移距离。所以需要在动画进度30%-100%时重新计算全屏播放页Y轴偏移距离,以达到在动画进度100%时全屏播放页能偏移到屏幕顶部位置。
      this.animationData.detailsPageOffsetY = this.animationData.miniDistanceToTop * progress -
        (this.animationData.miniDistanceToTop - this.animationData.miniImgToDetailsPageImgDistance) * Constants.ANIMATION_PROGRESS *
          ((1 - Constants.ANIMATION_PROGRESS) - (progress - Constants.ANIMATION_PROGRESS)) / (1 - Constants.ANIMATION_PROGRESS);
      // 动画进度30%-100%时,Mini条透明度为0。
      this.animationData.miniPlayerOpacity = 0;
    }
    // Mini条动画过程中高度拉伸大小
    this.animationData.miniChangeHeight = this.animationData.miniImgOffsetY;
    /**
     * 动画进度0%-30%时,全屏播放页透明度从0上升到1。和前面Mini条透明度变化相反。
     * 为了达到更好的动画效果。在一开始全屏播放页出现时透明度快速变大。这里在动画进度0%-5%时,全屏播放页透明度从0上升到0.5,在动画进度5%-30%时
     * ,全屏播放页透明度从0.5上升到1.
     */
    if (progress <= Constants.PROGRESS_PERCENTAGE_FIVE) {
      this.animationData.detailsPageOpacity = progress * (1 - Constants.DETAILS_PAGE_INTERIM_OPACITY) / Constants.PROGRESS_PERCENTAGE_FIVE;
    } else if (progress < Constants.ANIMATION_PROGRESS) {
      this.animationData.detailsPageOpacity = (progress - Constants.PROGRESS_PERCENTAGE_FIVE) *
        (1 - Constants.DETAILS_PAGE_INTERIM_OPACITY) / (Constants.ANIMATION_PROGRESS - Constants.PROGRESS_PERCENTAGE_FIVE) +
      Constants.DETAILS_PAGE_INTERIM_OPACITY;
    } else {
      // 动画进度30%-100%时,全屏播放页透明度为1。
      this.animationData.detailsPageOpacity = 1;
    }
    // 动画过程中全屏播放页Y轴位置。全屏播放页Y轴位置=屏幕高度-全屏播放页Y轴偏移距离-Mini条距离屏幕底部的高度(Mini条高度+TabBar高度+底部非安全区域高度(导航栏高度))
    this.animationData.detailsPagePositionY = this.animationData.screenHeight - this.animationData.detailsPageOffsetY - this.animationData.miniDistanceToBottom;
    // 动画过程中全屏播放页Y轴位置如果在0-1/2屏幕高度,全屏播放页收起按钮父容器Row的透明度从0上升到1。动画过程中全屏播放页Y轴位置如果大于1/2屏幕高度,则全屏播放页收起按钮父容器Row的透明度为0。
    if (this.animationData.detailsPagePositionY <= this.animationData.screenHeight / 2) {
      this.animationData.detailsPageTopOpacity = 1 - this.animationData.detailsPagePositionY / (this.animationData.screenHeight / 2);
    } else {
      this.animationData.detailsPageTopOpacity = 0;
    }
  }

  /**
   * 创建动画
   */
  createAnimation() {
    // 计算Mini条距离屏幕顶部的高度(包含顶部非安全区域高度)。Mini条距离屏幕顶部的高度=屏幕高度-Mini条高度-TabBar高度-底部非安全区域高度(导航栏高度)
    this.animationData.miniDistanceToTop = this.animationData.screenHeight - Constants.MINI_HEIGHT -
    Constants.BAR_HEIGHT - this.animationData.bottomUnsafeHeight;
    // 计算Mini条歌曲封面Y轴位置。Mini条距离屏幕顶部的高度(包含顶部状态栏高度)+Mini条歌曲封面距离Mini条顶部距离
    this.animationData.miniImgPositionY = this.animationData.miniDistanceToTop + Constants.MINI_SPACE;
    // 计算Mini条歌曲封面Y轴位置到全屏播放页歌曲封面Y轴位置的距离
    this.animationData.miniImgToDetailsPageImgDistance = this.animationData.miniImgPositionY - this.animationData.detailsPageImgPositionY;
    // TODO 知识点:本例中使用@ohos.animator动画模块的AnimatorResult定义动画对象,通过创建AnimatorOptions动画选项,并传入create来创建Animator对象animatorObject。在动画帧回调onframe中通过参数value获取动画进度,然后根据动画进度实时改变自定义动画相关属性AnimationInfo的值来实现Mini条展开和收起的一镜到底动画。
    // 设置动画选项,定义Animator类
    this.animatorObject = animator.create({
      duration: Constants.MINI_ANIMATION_DURATION, // 动画时长
      /**
       * easing动画插值曲线。这里使用三次贝塞尔曲线,通过设置cubicBezierCurve的4个参数控制曲线动画速度,(0.22,0.17,0.07,1)效果为先慢后快
       * 最后再缓慢减速的曲线动画。具体请参考https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-curve-0
       * 000001774121126#ZH-CN_TOPIC_0000001857917121__curvescubicbeziercurve9。
       */
      easing: "cubic-bezier(0.22, 0.17, 0.07, 1)",
      delay: 0, // 动画延时播放时长。0表示不延时。
      fill: "forwards", // forwards表示在动画结束后,目标将保留动画结束时的状态(在最后一个关键帧中定义)。
      direction: "normal", // 动画播放模式。normal表示设置动画正向循环播放。
      iterations: 1, // 动画播放次数
      begin: 0, // 动画插值起点
      /**
       * end动画插值终点。这里设置Mini条歌曲封面Y轴位置到全屏播放页歌曲封面Y轴位置的距离作为动画插值终点。
       */
      end: this.animationData.miniImgToDetailsPageImgDistance
    });
    // onfinish动画完成时回调
    this.animatorObject.onFinish = () => {
      // 重置正在动画标志位
      this.animationData.isAnimating = false;
      if (!this.animationData.isExpand) {
        // 重置Mini条是否展开标志位
        this.animationData.isExpand = true;
      } else {
        this.animationData.isExpand = false;
        // 重置Mini条距离屏幕底部的高度
        this.animationData.miniDistanceToBottom = 0;
        // 重置Mini条实际歌曲封面的透明度
        this.animationData.miniImgOpacity = 1;
        // 重置用于动画的Mini条歌曲封面的透明度
        this.animationData.miniImgAnimateOpacity = 0;
      }
    }
    // onframe接收到动画帧时回调,value返回当前的动画进度。value的取值范围就是前面animatorOption中设定的动画插值起点begin到动画插值终点end。
    this.animatorObject.onFrame = (value: number) => {
      // 展开动画
      if (!this.animationData.isExpand) {
        // 计算当前动画进度占比。
        const progress: number = value / this.animationData.miniImgToDetailsPageImgDistance;
        // Mini条歌曲封面一镜到底动画过程中偏移的距离
        this.animationData.miniImgOffsetY = value;
        // 展开收起动画公共部分
        this.expandCollapseAnimation(progress);
      } else { // 收起动画
        // 展开动画过程和收起动画过程相反,所以这里用1-当前动画进度占比
        const progress: number = 1 - value / this.animationData.miniImgToDetailsPageImgDistance;
        // Mini条歌曲封面一镜到底动画过程中偏移的距离
        this.animationData.miniImgOffsetY = this.animationData.miniImgToDetailsPageImgDistance - value;
        // 展开收起动画公共部分
        this.expandCollapseAnimation(progress);
      }
    }
  }

  build() {
    Stack() {
      // 首页
      HomePage({ animatorObject: this.animatorObject, animationInfo: this.animationData })

      // 全屏播放页
      DetailsPage({ animatorObject: this.animatorObject, animationInfo: this.animationData })

      // Mini条歌曲封面
      Image($r('app.media.miniplayeranimation_music_cover'))
        .borderRadius(Constants.MINI_IMG_RADIUS)
        .margin({ left: Constants.MINI_IMG_MARGIN_LEFT })
        .size({
          width: Constants.MINI_IMG_SIZE + this.animationData.miniImgOffsetSize,
          height: Constants.MINI_IMG_SIZE + this.animationData.miniImgOffsetSize
        })
        .position({
          x: Constants.MINI_POSITION_X + this.animationData.miniImgOffsetX,
          y: this.animationData.miniImgPositionY - this.animationData.miniImgOffsetY
        })
        .opacity(this.animationData.miniImgAnimateOpacity)
        .responseRegion({
          // 设置无点击热区。避免点击图片,没法触发底部的mini条点击事件
          width: $r('app.string.mini_player_animation_response_region'),
          height: $r('app.string.mini_player_animation_response_region')
        })
    }
    .size({
      width: $r('app.string.mini_player_animation_full_size'),
      height: $r('app.string.mini_player_animation_full_size')
    })
  }
}