9433cfb9创建于 2025年12月31日历史提交
<template>
  <view ref="page" class="page">
    <text class="tip">半屏弹窗,演示了弹出层内scroll-view滚动到顶时由滚变拖。本效果是通过监听TouchEvent实现,当半屏窗口移动时禁用scroll-view的滚动,避免两者的冲突。</text>
    <button class="bottomButton" @click="switchHalfScreen(true)">打开弹窗</button>
    <view ref="halfScreen" class="halfScreen" @touchstart="onHalfTouchStart" @touchmove="onHalfTouchMove"
      @touchend="onHalfTouchEnd">
      <view class="halfTitle">半屏弹窗标题</view>
      <scroll-view ref="halfScroll" class="halfScroll" bounces="true" :direction="scrollDirection">
        <view v-for="(item,index) in list" :key="index" class="item">
          half screen content-{{item}}
        </view>
      </scroll-view>
    </view>
  </view>
</template>

<script>
  export default {
    data() {
      return {
        list: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15'],
        totalHeight: 0,		//总高度
        halfMove: false,	//是否Move,响应TouchMove
        halfScreenY: 0,		//响应TouchMove的起始点Y坐标
        halfOffset: 0,		//偏移的位置,translateY
        halfHeight: 0,		//高度
        lastY: 0,			//上次
        lastY2: 0,			//
        bAnimation: false,	//是否动画
        halfNode: null as UniElement | null,
        scrollNode: null as UniElement | null,
        scrollDirection: 'vertical'
      }
    },
    methods: {
      onHalfTouchStart(_ : TouchEvent) {
        this.halfNode?.style?.setProperty('transition-duration', 0);
      },
      onHalfTouchMove(e : TouchEvent) {
        if (this.bAnimation) {//容错处理
          return;
        }
        let top : number = this.scrollNode?.scrollTop ?? 0;
        let p = e.touches[0];
        this.lastY2 = this.lastY;
        this.lastY = p.screenY;
        if (top <= 0.01 || this.halfMove) {
          if (this.halfScreenY == 0) {
            this.halfScreenY = p.screenY;
          }
          let offset = p.screenY - this.halfScreenY;
          if (offset > 0) {//向下滚动
            this.halfMove = true;
            // #ifdef APP
            //this.scrollNode?.setAttribute('scroll-y', 'false');
            this.scrollNode?.setAttribute('direction', 'none');
            // #endif
            // #ifdef WEB || MP
            this.scrollDirection = 'none';
            // #endif
            this.halfNode?.style?.setProperty('transform', 'translateY(' + offset.toFixed(2) + 'px)');
            this.halfOffset = offset;
          } else if (this.halfOffset > 0) {//向上滚动
            offset = this.halfScreenY - p.screenY;
            if (offset > this.halfOffset) {
              offset = 0;
              this.halfMove = false;
            }
            // #ifdef APP
            //this.scrollNode?.setAttribute('scroll-y', 'true');
            this.scrollNode?.setAttribute('direction', 'vertical');
            // #endif
            // #ifdef WEB || MP
            this.scrollDirection = 'vertical';
            // #endif
            this.halfNode?.style?.setProperty('transform', 'translateY(' + offset.toFixed(2) + 'px)');
            this.halfOffset = offset;
          }
        }
        // #ifdef WEB
        e.preventDefault();
        // #endif
      },
      onHalfTouchEnd(_ : TouchEvent) {
        this.halfScreenY = 0;
        if (this.bAnimation) {//容错处理
          return;
        }
        let top : number = this.scrollNode?.scrollTop ?? 0;
        let bHide = (this.halfHeight - this.halfOffset) < this.halfHeight / 4;
        if (bHide) {
          bHide = this.lastY2 > 0 && this.lastY2 <= this.lastY;
        } else if (top <= 0.01) {
          bHide = (this.lastY - this.lastY2) > 3;		//向下滑动计算加速度判断是否关闭,简单处理未考虑时间
        }
        if (bHide) {
          this.switchHalfScreen(false);
        } else if (this.halfOffset > 0) {
          this.resumeHalfScreen();
        }
      },
      switchHalfScreen(show : boolean) {
        if (show && ('visible' == this.halfNode?.style?.getPropertyValue('visibility'))) {//容错处理
          console.log('quick click button!!!');
          return;
        }
        this.halfMove = false;
        // #ifdef APP
        //this.scrollNode?.setAttribute('scroll-y', 'true');
        this.scrollNode?.setAttribute('direction', 'vertical');
        // #endif
        // #ifdef WEB || MP
        this.scrollDirection = 'vertical';
        // #endif
        this.halfScreenY = 0;
        this.halfOffset = 0;
        let top = this.totalHeight;
        let time = 300;
        if (show) {
          top = this.totalHeight * 30 / 100;	//计算显示的位置
          this.halfNode?.style?.setProperty('visibility', 'visible');
          this.halfNode?.style?.setProperty('transition-timing-function', 'ease-in-out');
        } else {
          this.halfNode?.style?.setProperty('transition-timing-function', 'linear');
          time *= (this.halfHeight / this.totalHeight);		//计算关闭动画时间
        }
        this.halfNode?.style?.setProperty('transition-duration', time.toFixed(0)+'ms');
        this.halfNode?.style?.setProperty('transition-property', 'top');
        this.halfNode?.style?.setProperty('top', top.toFixed(2)+'px');
        setTimeout(() => {
          if (!show) {
            this.halfNode?.style?.setProperty('visibility', 'hidden');
            this.halfNode?.style?.setProperty('transition-duration', 0);
            this.halfNode?.style?.setProperty('transform', '');
          }
          this.halfNode?.style?.setProperty('transition-property', 'none');
          this.bAnimation = false;
        }, time)
        this.bAnimation = true;
      },
      resumeHalfScreen() {
        let time = 300;//(500*this.halfOffset/this.halfHeight).toFixed(0); //回弹动画时间
        this.halfNode?.style?.setProperty('transition-duration', time.toFixed(0)+'ms');
        this.halfNode?.style?.setProperty('transition-timing-function', 'ease-in-out');
        this.halfNode?.style?.setProperty('transition-property', 'transform');
        this.halfNode?.style?.setProperty('transform', 'translateY(0px)');
        this.halfMove = false;
        // #ifdef APP
        //this.scrollNode?.setAttribute('scroll-y', 'true');
        this.scrollNode?.setAttribute('direction', 'vertical');
        // #endif
        // #ifdef WEB || MP
        this.scrollDirection = 'vertical';
        // #endif
        this.halfScreenY = 0;
        this.halfOffset = 0;
        setTimeout(() => {
          this.bAnimation = false;
          this.halfNode?.style?.setProperty('transition-property', 'none');
        }, time)
        this.bAnimation = true;
      }
    },
    onReady() {
      this.halfNode = this.$refs['halfScreen'] as UniElement;//uni.getElementById('halfScreen');
      this.scrollNode = this.$refs['halfScroll'] as UniElement;//uni.getElementById('halfScroll');
      this.halfNode!.getBoundingClientRectAsync()!.then((rect: DOMRect) => {
          this.halfHeight = rect.height
      });
      (this.$refs['page'] as UniElement).getBoundingClientRectAsync()!.then((rect: DOMRect) => {
          this.totalHeight = rect.height
          this.halfNode?.style?.setProperty('top', this.totalHeight.toFixed(2)+'px');
      });
    },
    onResize() {
      this.halfNode?.getBoundingClientRectAsync()!.then((rect: DOMRect) => {
          this.halfHeight = rect.height
      });
      this.totalHeight = uni.getWindowInfo().windowHeight;
      this.halfNode?.style?.setProperty('top', this.totalHeight.toFixed(2)+'px');
      this.halfNode?.style?.setProperty('visibility', 'hidden');
    },
    onBackPress(): boolean {
      // #ifndef MP
      if('visible' == this.halfNode?.style?.getPropertyValue('visibility')){
        this.switchHalfScreen(false);
        return true;
      }
      return false;
      // #endif
    }
  }
</script>

<style>
  /* #ifdef MP */
  page {
    overflow: hidden
  }
  /* #endif */

  .page {
    flex: 1;
    background-color: darkgrey;
  }

  .tip {
    margin: 10px
  }

  .bottomButton {
    position: absolute;
    width: 100%;
    bottom: 0px;
    /* #ifdef APP */
    padding-bottom: env(safe-area-inset-bottom, 0px);
    /* #endif */
  }

  .halfScreen {
    position: absolute;
    top: 100%;
    width: 100%;
    height: 70%;
    transition-timing-function: ease-in-out;
    /*ease ease-in ease-out ease-in-out linear step-start step-end*/
    transition-property: top;
    transition-duration: 0ms;
    visibility: hidden;
  }

  .halfTitle {
    align-items: center;
    justify-content: center;
    height: 48px;
    background-color: ghostwhite;
    border-radius: 10px 10px 0 0;
  }

  .halfScroll {
    background-color: white;
    flex: 1;
  }

  .item {
    height: 100px;
  }
</style>