popup组件的实现,着重于过渡动画

<template>
    <div class="drawer" v-show="isVisible"
        :class="[
            props.overlayClass,
            { 'drawer-active': isActive }
        ]"
        @click.self="handleClickOverlay"
    >
        <div class="drawer-content"
            :class="[
                'drawer-content-'+props.position,
                { 'content-active': isActive }
            ]"
            :style="{
                ...contentStyle,
                ...props.style
            }"
        >
            <slot></slot>
        </div>
    </div>
</template>

<script setup lang="ts">
import { computed, ref, watch} from 'vue';

interface PropType {
    show: boolean,
    position: 'top' | 'bottom' | 'left' | 'right',
    round: boolean | number,
    height: number | string,
    overlayClass: string,
    closeOnClickOverlay: boolean,
    style: object
}

const props = withDefaults(defineProps<PropType>(), {
    show: false,
    position: 'bottom',
    round: true,
    height: '60vh',
    overlayClass: 'drawer-overlay',
    closeOnClickOverlay: true,
    style: {}
});
const emit = defineEmits(['update:show', 'close']);

// 控制整个抽屉的显示隐藏
const isVisible = ref(false);
// 控制动画状态
const isActive = ref(false);
// 监听 show 的变化
watch(() => props.show, (newVal) => {
    if (newVal) {
        // 显示抽屉
        isVisible.value = true;
        // 等待 DOM 更新后添加动画类
        requestAnimationFrame(() => {
            isActive.value = true;
        });
    } else {
        // 先移除动画类
        isActive.value = false;
        // 等待动画结束后隐藏抽屉
        let timer: any = setTimeout(() => {
            isVisible.value = false;
            clearTimeout(timer);
            timer = null;
        }, 300); // 这个时间需要和 CSS 动画时间一致
    }
}, { immediate: true });

// 圆角
const contentRound = computed(() => {
    const stringRound = props.round ? (typeof props.round === 'number' ? props.round + 'px' : '16px') : '0';
    let styleString = '';
    switch(props.position){
        case 'top':
            styleString = `0 0 ${stringRound} ${stringRound}`;
            break;
        case 'bottom':
            styleString = `${stringRound} ${stringRound} 0 0`;
            break;
        case 'right':
            styleString = `${stringRound} 0 0 ${stringRound}`;
            break;
        case 'left':
            styleString = `0 ${stringRound} ${stringRound} 0`;
            break;
    }
    return styleString;
});
// 高度
const contentHeight = computed(() => {
    return props.height ? (typeof props.height === 'number' ? props.height + 'px' : props.height) : '60vh';
});
// 内容区域的样式
const contentStyle = computed(() => {
    const style = {
        height: contentHeight.value,
        borderRadius: contentRound.value,
        width: 'auto',
    }
    if (props.position === 'top') {
        style.width = '100%';
    }
    if (props.position === 'bottom') {
        style.width = '100%';
    }
    if (props.position === 'left') {
        style.width = 'auto';
        style.height = '100%';
    }
    if (props.position === 'right') {
        style.width = 'auto';
        style.height = '100%';
    }
    return style;
});
// 点击遮罩层
const handleClickOverlay = () => {
    if (props.closeOnClickOverlay) {
        emit('update:show', false);
        emit('close');
    }
}
</script>

<style lang="scss" scoped>
.drawer {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    z-index: 1000;
    &-content{
        width: 100%;
        // height: 60vh;
        background-color: #fff;
        position: absolute;
        bottom: 0;
        transition: transform 0.3s ease;
    }
}
.drawer-overlay{
    background-color: rgba(0, 0, 0, 0.5);
}

.drawer-content {
    position: fixed;
    background: #fff;
    z-index: 1000;
    transition: all 0.3s ease;
    overflow: scroll;
    &::-webkit-scrollbar{
        display: none;
    }
}

// 底部弹出
.drawer-content-bottom {
    bottom: 0;
    left: 0;
    width: 100%;
    transform: translateY(100%);
    box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.1);
    
    &.content-active {
        transform: translateY(0);
    }
}

// 顶部弹出
.drawer-content-top {
    top: 0;
    left: 0;
    width: 100%;
    transform: translateY(-100%);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
    
    &.content-active {
        transform: translateY(0);
    }
}

// 左侧弹出
.drawer-content-left {
    top: 0;
    left: 0;
    height: 100%;
    transform: translateX(-100%);
    box-shadow: 4px 0 12px rgba(0, 0, 0, 0.1);
    
    &.content-active {
        transform: translateX(0);
    }
}

// 右侧弹出
.drawer-content-right {
    top: 0;
    right: 0;
    height: 100%;
    transform: translateX(100%);
    box-shadow: -4px 0 12px rgba(0, 0, 0, 0.1);
    
    &.content-active {
        transform: translateX(0);
    }
}

// 中心弹出
.drawer-content-center {
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%) scale(0.5);
    opacity: 0;
    box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
    
    &.content-active {
        transform: translate(-50%, -50%) scale(1);
        opacity: 1;
    }
}
</style>

### Jetpack Comate Compose 中 Popup 组件实现过渡动画的方法 在 Jetpack Compose 的 `Popup` 组件中,可以通过组合其他动画 API 来创建自定义的进入和退出动画效果。尽管当前 `navigation-compose` 提供的部分动画功能仍处于实验阶段[^1],但可以利用现有的 `AnimatedVisibility` 或者手动控制属性变化来完成弹窗动画。 以下是具体方法: #### 使用 `Modifier.graphicsLayer()` 实现动画 通过调整 `alpha` 和 `translationY` 属性,可以在显示或隐藏 `Popup` 时应用淡入/滑动等视觉效果。 ```kotlin import androidx.compose.animation.core.* import androidx.compose.foundation.layout.Box import androidx.compose.material.Button import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.draw.graphicsLayer import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Popup @Composable fun AnimatedPopupExample() { var isPopupVisible by remember { mutableStateOf(false) } val transition = updateTransition(targetState = isPopupVisible, label = "") val alpha by transition.animateFloat( transitionSpec = { tween(durationMillis = 300) }, label = "" ) { visible -> if (visible) 1f else 0f } val offsetY by transition.animateDp( transitionSpec = { spring(stiffness = Spring.StiffnessMediumLow) }, label = "" ) { visible -> if (visible) 0.dp else (-50).dp } Box { Button(onClick = { isPopupVisible = !isPopupVisible }) { Text(if (isPopupVisible) "Hide Popup" else "Show Popup") } if (isPopupVisible) { Popup( onDismissRequest = { isPopupVisible = false }, modifier = Modifier.graphicsLayer(alpha = alpha, translationY = offsetY.value) ) { Surface(shape = RoundedCornerShape(8.dp), color = Color.LightGray) { Column(modifier = Modifier.padding(16.dp)) { Text("This is an animated popup!") } } } } } } ``` 上述代码展示了如何使用 `updateTransition` 创建平滑的透明度和位置偏移动画[^2]。 --- #### 利用 `AnimatedVisibility` 如果只需要简单的可见性切换动画,则可以直接采用 `AnimatedVisibility` 包裹 `Popup` 内部的内容。 ```kotlin import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.Column import androidx.compose.material.Text import androidx.compose.runtime.* @Composable fun AnimatedVisibilityPopupExample() { var isVisible by remember { mutableStateOf(false) } Column { Button(onClick = { isVisible = true }) { Text("Show Popup with Animation") } if (isVisible) { Popup(onDismissRequest = { isVisible = false }) { AnimatedVisibility( visible = isVisible, enter = fadeIn(), exit = fadeOut() ) { Surface(color = Color.White, shape = RoundedCornerShape(8.dp)) { Column(modifier = Modifier.padding(16.dp)) { Text("Fade-in/Fade-out animation applied.") } } } } } } } ``` 此方式适用于更简洁的需求场景,并且能够快速集成标准动画行为。 --- ### 总结 虽然官方尚未完全稳定化所有的动画支持模块,但在实际开发过程中已经具备足够的灵活性去构建丰富的用户体验设计。以上两种方案分别针对不同程度复杂性的需求提供了可行路径。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值