模仿,就是学习的一种思路,近几天发现了js的Animation,所以尝试复刻了苹果商城的卡片动画。
模仿的模板
当然实现方法有很多很多,因为是移动平台,这里我用uniapp来做。
我这里呢,就使用JS,AnimationAPI,来实现它,
先看看大概的效果吧:
动画分为4部分
- 打开动画,是根据原始的宽高,和位置。来占满全屏。
- 下拉动画,在内容区域中如果无法下拉则是响应下拉,把窗口变小,
- 下来关闭动画,在下拉到一定阈值时,根据当前宽高,和位置播放关闭动画
- 正常关闭动画,直接播放关闭动画,回到原始位置
实现的思路大概:
打开动画,可以直接使fixed布局来完成,从flex换成fixed,会出现一个问题
那就是点击他,之后他会浮动起来,我们需要保持他原来的位置,和边距,这个时候就需要先记住他的距离左边的像素,和列表的顶部和这个元素的距离。
因为他是个列表,我们就需要拿列表滑动条滑动的距离加上这个元素的top值,最后是我们需要的他的位置。
获得了基本位置信息后就可以为他创建打开动画了
const openAnimationEffect = new KeyframeEffect(
boxRef.value.$el, // element to animate
[{
position: "fixed",
width: w + "px",
height: h + "px",
left: uni.upx2px(24) + 'px',
top: top + 'px',
easing: 'cubic-bezier(.61, -0.2, .51, 1.4)',
}, // keyframe
{
position: "fixed",
zIndex: 99,
offset: 0.001
},
{
position: "fixed",
left: '0px',
width: "100%",
height: scH + "px",
top: props.scrollNum + "px",
zIndex: 99,
easing: 'cubic-bezier(.61, -0.2, .51, 1.4)',
borderRadius: '0px',
overflow: 'scroll',
flexDirection: "column",
}, // keyframe
], {
duration: 710,
fill: "both"
} // keyframe options
);
openAnimation = new Animation(
openAnimationEffect,
document.timeline
);
openAnimation.play();
2.下拉关闭可以通过滑动事件来完成,但是这个设计到一个滑动穿透的问题(让人头大)
可以了解一下这个移动端今典问题参考滑动穿透
就是我滑动我的内容时,内容已经无法再下滑,这个时候就会把事件传到上一级也就是列表。也就是说我可以在内容里面滑动到外面的列表,这个会让关闭动画时最后的位置不对,因为列表发生了改变
element.removeEventListener("touchstart", touchStartFunc);
element.removeEventListener("touchmove", touchMoveFunc);
element.removeEventListener("touchend", touchEndFunc);
根据具体需求完成,touchstart需要记录当前的元素信息,(方便他恢复,可能拉到一半不拉了)
还有就是过滤掉一些无效的滑动,只有内容无法滑动了,才会触发下拉关闭。
const touchStartFunc = (event) => {
element.className = "essay-main"
openAnimation.cancel()
Y2 = 0;
startX = event.touches[0].pageX;
startY = event.touches[0].pageY;
startH = scH
startW = scW
console.log(startH, startW);
element.style.width = startW + "px"
element.style.height = startH + "px"
console.log("action:start", startX, startY);
}
const touchMoveFunc = (event) => {
let X = event.touches[0].pageX;
let Y = event.touches[0].pageY;
// event.preventDefault();
console.log("scrollTop", element.scrollTop);
let delta = Y - startY;
if ((element.scrollTop == 0 && delta > 0) || (element.scrollTop + element.clientHeight == element
.scrollHeight && delta < 0)) {
console.log("无效滑动", );
if (Y2) {
} else {
Y2 = event.touches[0].pageY;
console.log("Y2:", Y2);
}
event.preventDefault()
if (parseInt(element.clientWidth) > 300) {
if (Y - Y2 > 10) {
event.preventDefault();
var changer = parseInt((Y - Y2))
element.style.width = startW - changer + "px"
element.style.height = startH - changer * 2 + "px"
}
} else {
console.log("move到达阈值");
OnBackAnimation()
}
}
}
const touchEndFunc = (event) => {
let X = event.changedTouches[0].pageX;
let Y = event.changedTouches[0].pageY;
if (Y - startY > 100) {
} else {
console.log("未到达阈值", Y - startY);
element.style.width = startW + "px"
element.style.height = startH + "px"
isContent.value = true
}
}
当然还需要一点点的css和js辅助
完整的essay.vue如下,可以参考参考,代码复用不强,只能针对项目来做修改
<essay title="哇哇哇哇" :scrollNum="boxScrollTop" class="item"></essay>
<essay title="哇哇哇哇" :scrollNum="boxScrollTop" class="item"></essay>
<template>
<view class="box">
<transition>
<view v-if="disabled"
style="position: fixed;top: 0%;left: 0%; height: 100vh;width: 100%; backdrop-filter: blur(7px);">
</view>
</transition>
<!-- <view ref="boxRef" class="essay " :class="{ 'essay-animation': disabled ,'essay-back-animation': !disabled}" -->
<view ref="boxRef" class="essay" @click="tosta" :style="'--scrollNum:'+props.scrollNum+'px'">
<view class="img" :class="{ 'essay-image-animation': disabled,'essay-image-back-animation': !disabled}">
</view>
<view class="essay-title">
<text class="">{{props.title}}</text>
<view class="essay-title-content">
<text>Vue花开代码,无限增长</text>
<text>Vue花开代码,无限增长</text>
</view>
</view>
<transition name="content">
<view v-if="isContent"
style="color: gray;text-align: left; flex: 1; height: 100rpx; padding: 12rpx;text-indent: 2rem; ">
{{props.content}}
</view>
</transition>
<transition>
<button
style="position:fixed; font-size: 20rpx; right: 0%; top: 0%; border-radius: 20%; height: 40rpx; width: 40rpx; background-color: rgba(97, 253, 222, 0.1); "
v-if="disabled" @click="close">x</button>
</transition>
</view>
</view>
</template>
<script setup>
import {
ref,
watch,
onMounted,
toRef
} from "vue";
import {
onLoad,
onShow
} from '@dcloudio/uni-app'
//父元素的滑动距离
const props = defineProps({
scrollNum: {
type: Number,
default: 0
},
title: {
type: String,
default: "没有标题"
},
content: {
type: String,
default: "没有内容"
}
})
const disabled = ref(false)
const isContent = ref(false)
const boxRef = ref(null)
const boxEl = ref(null)
const scrollNum = ref(0)
// const boxAc = ref({
// w: "",
// h: ""
// })
let w = ""
let h = ""
let scH = ""
let scW = ""
let top = ""
let element = null
let openAnimation = null
let backAnimation = null
var startX;
var startY;
var startH;
var startW;
let Y2;
onMounted((option) => {
//主元素 记录当前元素宽高,屏幕高度
element = boxRef.value.$el
w = element.clientWidth
h = element.clientHeight
//初始化
init()
})
const init = () => {
// boxEl.value= defineProps({
// boxEl:Element
// })
}
const close = (event) => {
event.stopPropagation();
disabled.value = false
isContent.value = false
element.removeEventListener("touchstart", touchStartFunc);
element.removeEventListener("touchmove", touchMoveFunc);
element.removeEventListener("touchend", touchEndFunc);
element.className = "essay"
openAnimation.reverse();
openAnimation.onfinish = (e) => {
console.log("Open animation reverse finish");
backAnimation.cancel()
element.style = ""
}
}
const OnBackAnimation = () => {
let animationW = element.clientWidth
let animationH = element.clientHeight
let animationTop = element.getBoundingClientRect().top + props.scrollNum
let animationLeft = element.getBoundingClientRect().left
console.log("关闭动画:top" + animationTop);
const backAnimationEffect = new KeyframeEffect(
element, // element to animate
[{
position: "fixed",
width: animationW + "px",
height: animationH + "px",
top: animationTop + 'px',
left: animationLeft + 'px',
flexDirection: "column",
easing: 'cubic-bezier(.61, -0.2, .51, 1.4)',
}, // keyframe
{
position: "fixed",
left: '0px',
width: w + "px",
top: top + "px",
left: uni.upx2px(24) + 'px',
height: h + "px",
easing: 'cubic-bezier(.61, -0.2, .51, 1.4)',
flexDirection: "row",
}, // keyframe
], {
duration: 710,
fill: "both"
} // keyframe options
);
backAnimation = new Animation(
backAnimationEffect,
document.timeline
);
element.removeEventListener("touchstart", touchStartFunc);
element.removeEventListener("touchmove", touchMoveFunc);
element.removeEventListener("touchend", touchEndFunc);
element.className = "essay"
isContent.value = false
backAnimation.play();
backAnimation.onfinish = (e) => {
console.log("Back animation finish");
backAnimation.cancel()
element.style = ""
}
disabled.value = false
}
const touchStartFunc = (event) => {
element.className = "essay-main"
openAnimation.cancel()
Y2 = 0;
startX = event.touches[0].pageX;
startY = event.touches[0].pageY;
startH = scH
startW = scW
console.log(startH, startW);
element.style.width = startW + "px"
element.style.height = startH + "px"
console.log("action:start", startX, startY);
}
const touchMoveFunc = (event) => {
let X = event.touches[0].pageX;
let Y = event.touches[0].pageY;
// event.preventDefault();
console.log("scrollTop", element.scrollTop);
let delta = Y - startY;
if ((element.scrollTop == 0 && delta > 0) || (element.scrollTop + element.clientHeight == element
.scrollHeight && delta < 0)) {
console.log("无效滑动", );
if (Y2) {
} else {
Y2 = event.touches[0].pageY;
console.log("Y2:", Y2);
}
event.preventDefault()
if (parseInt(element.clientWidth) > 300) {
if (Y - Y2 > 10) {
event.preventDefault();
var changer = parseInt((Y - Y2))
element.style.width = startW - changer + "px"
element.style.height = startH - changer * 2 + "px"
}
} else {
console.log("move到达阈值");
OnBackAnimation()
}
}
}
const touchEndFunc = (event) => {
let X = event.changedTouches[0].pageX;
let Y = event.changedTouches[0].pageY;
if (Y - startY > 100) {
} else {
console.log("未到达阈值", Y - startY);
element.style.width = startW + "px"
element.style.height = startH + "px"
isContent.value = true
}
}
const tosta = () => {
console.log("获取父元素滑动条:", props.scrollNum)
if (disabled.value) {
return
}
disabled.value = !disabled.value
if (disabled.value) {
scH = uni.getSystemInfoSync().windowHeight
scW = uni.getSystemInfoSync().windowWidth
// top = boxRef.value.$el.getBoundingClientRect().top
top = element.offsetTop
console.log("boxTop:", element.offsetTop);
console.log("boxscroll", props.scrollNum)
element.addEventListener("touchstart", touchStartFunc, false);
element.addEventListener("touchmove", touchMoveFunc, false);
element.addEventListener("touchend", touchEndFunc, false);
}
const openAnimationEffect = new KeyframeEffect(
boxRef.value.$el, // element to animate
[{
position: "fixed",
width: w + "px",
height: h + "px",
left: uni.upx2px(24) + 'px',
top: top + 'px',
easing: 'cubic-bezier(.61, -0.2, .51, 1.4)',
}, // keyframe
{
position: "fixed",
zIndex: 99,
offset: 0.001
},
{
position: "fixed",
left: '0px',
width: "100%",
height: scH + "px",
top: props.scrollNum + "px",
zIndex: 99,
easing: 'cubic-bezier(.61, -0.2, .51, 1.4)',
borderRadius: '0px',
overflow: 'scroll',
flexDirection: "column",
}, // keyframe
], {
duration: 710,
fill: "both"
} // keyframe options
);
openAnimation = new Animation(
openAnimationEffect,
document.timeline
);
openAnimation.play();
openAnimation.onfinish = (e) => {
console.log("Open animation finish");
isContent.value = true
}
}
</script>
<style scoped>
.essay-animation {
animation: essay .72s cubic-bezier(.61, -0.2, .51, 1.4) both;
}
.essay-back-animation {
animation: essay-back .72s cubic-bezier(.61, -0.2, .51, 1.2) both;
}
.essay-image-animation {
animation: essay-image .65s cubic-bezier(.61, -0.4, .51, 1.5) both;
}
.essay-image-back-animation {
animation: essay-back-image .65s cubic-bezier(.61, -0.4, .51, 1.5) both;
}
@keyframes essay-image {
0% {
/* height: 240rpx;
width: 240rpx;
border-radius: 24rpx 0rpx 0rpx 24rpx; */
border-radius: 24rpx 0rpx 0rpx 24rpx;
}
100% {
width: 100%;
height: 300rpx;
border-radius: 0rpx 0rpx 0rpx 0rpx;
}
}
@keyframes essay-back-image {
0% {
width: 100%;
height: 300rpx;
border-radius: 0;
}
100% {
height: 240rpx;
width: 240rpx;
border-radius: 24rpx 0rpx 0rpx 24rpx;
}
}
.box {
min-height: 240rpx;
width: 100%;
box-sizing: border-box;
}
.essay {
overflow: hidden;
height: 240rpx;
width: 100%;
background-color: white;
box-sizing: border-box;
border-radius: 24rpx;
display: flex;
flex-direction: row;
position: unset;
/* transition: 3s all cubic-bezier(.61, -0.2, .51, 1.4); */
}
.essay-main {
overflow: scroll;
left: 0px;
width: 100%;
top: calc(50% + var(--scrollNum));
background-color: white;
box-sizing: border-box;
position: fixed;
transform: translate(-50%, -50%);
z-index: 99;
left: 50%;
}
.essay .img {
height: 240rpx;
width: 240rpx;
border-radius: 24rpx 0rpx 0rpx 24rpx;
background-color: black;
background-image: url('/static/IMG_5812.jpg');
background-repeat: no-repeat;
background-position: center;
background-size: cover;
}
.essay-main .img {
height: 240rpx;
width: 240rpx;
border-radius: 24rpx 0rpx 0rpx 24rpx;
background-color: black;
background-image: url('/static/IMG_5812.jpg');
background-repeat: no-repeat;
background-position: center;
background-size: cover;
}
.essay-title {
font-size: 26rpx;
padding: 4rpx;
text-align: center;
}
.essay-title-content {
font-size: 20rpx;
color: gray;
margin-top: 14rpx;
}
.v-enter-active,
.v-leave-active {
transition: opacity 0.5s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
.content-enter-active {
transition: all .4s cubic-bezier(.61, -0.2, .51, 1.4);
}
.content-leave-active {
transition: all 0.1s cubic-bezier(.61, -0.2, .51, 1.4);
}
.content-enter-from,
.content-leave-to {
opacity: 0;
transform: translateX(-80rpx);
}
</style>
最后
这边是混合开发,性能和丝滑还是有差距的,
完成了,这个效果细节还是很多的,只能说是做像,当然还有其他的实现方案,
如果使用原生才能真正的完美复刻。我这是强行使用JS的Animation去实现的,
html css 简单版可以参考 打开动画
andorid 可以参考 安卓复刻