苹果很丝滑,使用JS模仿的APP Store的卡片动画

模仿,就是学习的一种思路,近几天发现了js的Animation,所以尝试复刻了苹果商城的卡片动画。

模仿的模板

当然实现方法有很多很多,因为是移动平台,这里我用uniapp来做。

 

我这里呢,就使用JS,AnimationAPI,来实现它,

先看看大概的效果吧:

 

 动画分为4部分

  1. 打开动画,是根据原始的宽高,和位置。来占满全屏。
  2. 下拉动画,在内容区域中如果无法下拉则是响应下拉,把窗口变小,
  3. 下来关闭动画,在下拉到一定阈值时,根据当前宽高,和位置播放关闭动画
  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 可以参考 安卓复刻

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值