卡片放大播放效果實(shí)現(xiàn)總結(jié),不同元素間的放大過渡效果如何實(shí)現(xiàn)?
背景
最近一段時(shí)間做了幾個(gè)需求,其中涉及的素材列表展示,需要實(shí)現(xiàn)類似下方圖片中的動(dòng)效,暫且稱之為【卡片放大播放動(dòng)效】,具體細(xì)節(jié)如下:
初始展示的是封面圖片,鼠標(biāo)經(jīng)過時(shí)是視頻放大的效果; 下方文字內(nèi)容區(qū)域,在放大前后展示的內(nèi)容不同,而且兩者的字體大小是一致的,不是簡單的放大實(shí)現(xiàn); 四五個(gè)頁面都需要實(shí)現(xiàn)相同列表效果,列表的行數(shù)和列數(shù)是不一致的;
遇到的四個(gè)問題
在整個(gè)實(shí)現(xiàn)過程中,遇到以下四個(gè)問題,后面的解析中會(huì)有對應(yīng)解答。
不同元素間的放大過渡效果如何實(shí)現(xiàn)? 抽離為通用性組件時(shí),如何實(shí)現(xiàn)類vue中的具名插槽效果,來替換下方文字信息區(qū)域? 不同頁面中的列表區(qū)域?qū)挾炔煌械捻撁媪斜磉€是彈性寬度,常用的flex布局無法滿足要求,如何實(shí)現(xiàn)呢? 邊界卡片放大后,如何避免被父級列表容器 overflow:hidden`隱藏掉?
實(shí)現(xiàn)解析
這個(gè)動(dòng)效,在愛奇藝官網(wǎng)也有類似效果,愛奇藝官網(wǎng)是通過生成初始狀態(tài)卡片列表和鼠標(biāo)放大卡片列表兩套列表,然后通過動(dòng)態(tài)計(jì)算放大卡片位置,相對于body進(jìn)行絕對定位展示的。
本實(shí)現(xiàn)方案通過將卡片初始狀態(tài)和放大狀態(tài),封裝到一個(gè)組件中,通過兩者間的相對關(guān)系,利用css自動(dòng)完成對應(yīng)關(guān)系,避免了大量的JS計(jì)算。
1. 放大效果實(shí)現(xiàn)
UI對該動(dòng)效的要求實(shí)際就是鼠標(biāo)視頻放大播放,如果卡片初始狀態(tài)也放置視頻video,通過transition對同一元素進(jìn)行scale放大也可以實(shí)現(xiàn),但是這是一個(gè)列表,用戶進(jìn)入頁面,就會(huì)同時(shí)加載多個(gè)視頻,用戶體驗(yàn)不是很好。
所以,實(shí)現(xiàn)方案就是卡片初始狀態(tài)放置poster圖片,鼠標(biāo)經(jīng)過時(shí),在poster上方展示視頻層(絕對定位),然后對視頻執(zhí)行animation動(dòng)畫來模擬放大效果。
卡片底部文字區(qū)域如何處理?
由于卡片初始狀態(tài)下,底部文字區(qū)域在列表布局中是占位的,所以在卡片初始狀態(tài)下,底部文字區(qū)域使用正常文檔流。
卡片鼠標(biāo)經(jīng)過狀態(tài)下,視頻層的放大效果是以poster中心點(diǎn)為放大原點(diǎn)的,所以底部文字區(qū)域使用absolute定位,相對于player進(jìn)行定位處理。
interface IItemData {
src?: string;
poster?: string;
industry?: string;
}
interface IProps {
posterClass: string; // poster區(qū)域?qū)捀?br> playerClass: string; // 放大后視頻寬高樣式
itemData: IItemData;
children: { [key: string]: any };
}
let t: NodeJS.Timeout;
const VideoItem: FC<IProps> = ({
posterClass,
playerClass,
itemData,
children,
}) => {
const [isHover, setIsHover] = useState(false);
const onMouseEnter = () => {
t = setTimeout(() => {
// 避免快速移動(dòng)鼠標(biāo),造成視頻無法隱藏問題
setIsHover(true);
}, 50);
};
const onMouseLeave = () => {
clearTimeout(t);
setIsHover(false);
};
return (
<div className={styles.container} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
<div
className={cls(styles.poster, posterClass)}
style={{
backgroundImage: `url(${itemData.poster})`,
}}
>
<div className={styles.topIcon}>Top 1</div>
{isHover ? (
<div className={cls(styles.player, playerClass)}>
<VideoPlayerCommon
src={itemData.src || ''}
poster={itemData.poster || ''}
triggerEvent={'hover'}
controls={true}
muted={false}
showFullScreen={true}
/>
<!--卡片放大狀態(tài)下,底部文字區(qū)域-->
<div className={styles.playerBottom}>{children?.playerBottom}</div>
</div>
) : null}
<div className={styles.bg}></div>
</div>
<!--卡片初始狀態(tài)底部文字區(qū)域-->
<div className={styles.posterBottom}>{children?.posterBottom}</div>
</div>
);
};
export default VideoItem;
.container {
position: relative;
.poster {
position: relative;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
.player {
z-index: 10;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.929);
animation: showPlayer 0.2s ease;
animation-fill-mode: forwards;
.playerBottom {
width: 100%;
position: absolute;
top: 100%;
left: 0;
box-shadow: 0px 6px 16px -8px rgba(0, 0, 0, 0.08), 0px 12px 48px 16px rgba(0, 0, 0, 0.03);
filter: drop-shadow(0px 9px 28px rgba(0, 0, 0, 0.05));
}
}
}
.posterBottom {
width: 100%;
}
}
@keyframes showPlayer {
0% {
transform: translate(-50%, -50%) scale(0.929);
}
100% {
transform: translate(-50%, -50%) scale(1);
}
}
2. 抽離為通用組件時(shí),卡片底部文字區(qū)域如何動(dòng)態(tài)替換
整個(gè)卡片組件,底部的文字區(qū)域在不同的場景下可能是不同的,所以作為通用性組件,需要將這部分抽離,支持動(dòng)態(tài)替換。
vue中最簡單的方式,就是插槽,通過插槽從外部動(dòng)態(tài)傳入。但是react框架是不支持具名插槽的。所以,這個(gè)問題就轉(zhuǎn)換成了,react如何實(shí)現(xiàn)具名插槽?
網(wǎng)上搜到了一種實(shí)現(xiàn)方式,通過傳入一個(gè)object來實(shí)現(xiàn),具體效果如下:
// cardList
<VideoItem
key={index}
posterClass={styles.posterClass}
playerClass={styles.playerClass}
itemData={{
src: item.sampleUrl,
poster: item.coverUrl,
industry: item.industryName,
}}
>
{{
posterBottom: bottomInfo,
playerBottom: bottomInfo,
onClickPlayer: () => {
const url = `${window.location.origin}/#/market/service/${item.itemId}`;
window.open(url, '_blank');
},
}}
</VideoItem>
// VideoItem
<!--卡片放大狀態(tài)下,底部文字區(qū)域-->
<div className={styles.playerBottom}>{props.children?.playerBottom}</div>
<!--卡片初始狀態(tài)底部文字區(qū)域-->
<div className={styles.posterBottom}>{props.children?.posterBottom}</div>
當(dāng)然,具名插槽還有其它的實(shí)現(xiàn)方式,后面會(huì)專門寫一篇文章總結(jié)學(xué)習(xí)下。
3. 列表容器如果是彈性布局時(shí),每行的列數(shù)無法固定,flex布局無法滿足
這個(gè)問題是一個(gè)通用性問題,在容器寬度不固定時(shí),flex布局,每行最后一個(gè)元素?zé)o法選中設(shè)置樣式,同時(shí)子元素個(gè)數(shù)不固定時(shí),最后一行元素的間距會(huì)變大。
這種情況下,就需要grid布局大顯身手了,以前很少用grid布局,這次也是學(xué)習(xí)到了,具體效果如下圖:
在示例中,調(diào)整瀏覽器窗口大小,來實(shí)驗(yàn)彈性布局觀看效果代碼片段
4. 卡片放大后,可能會(huì)被容器設(shè)置的overflow:hidden給遮蓋隱藏掉
目前放大效果的實(shí)現(xiàn)方式,放大的視頻層是絕對定位的,參照物是每個(gè)卡片本身。所以在四周邊界處的卡片,放大后,很容易被容器給遮蓋。
處理方式也很簡單,給容器多設(shè)置一些padding,讓放大部分足夠展示,然后用margin設(shè)置負(fù)值來調(diào)整布局
.container {
margin-left: -20px;
margin-right: -20px;
padding-left: 20px;
padding-right: 20px;
}
總結(jié)
每個(gè)產(chǎn)品需求里,可能都會(huì)隱藏著自己的盲點(diǎn),將效果做到極致,就能獲得技術(shù)成長。在重復(fù)的需求里,多反思總結(jié),尋找自己的提升點(diǎn),這就是進(jìn)步吧啊。
相關(guān)推薦
1.CSS 實(shí)現(xiàn)按鈕點(diǎn)擊動(dòng)效的套路
3.跑馬燈簡單版


