# Angular Fade In/Out Div ( 此為 Component 範本 )
###### tags: `Angular`
## 先上測試結果圖

## 步驟/簡介
1. 用一個變數存取區塊(div)狀態 ( Default(其實就是隱藏)、FadeIn、FadeOut )
2. 提供一個 function 判斷 window 和 element 之間的位置關係去更改此區塊(div)狀態
3. 註冊 window 的 OnScroll、OnResize 去呼叫第 2 點的 function
4. ngAfterViewInit ( 讓 url 更換/首次進入頁面 trigger ) 中使用 requestAnimationFrame 方法 ( 為了等待元素正確渲染完畢計算位置 ),讓其呼叫第 2 點的 function
5. 依照狀態、方向 在 css 中訂好對應的 class name
6. 使用 [ngClass] 依照狀態去更改套用對應的 class
## 訂定 Fade In Or Out Component
### Component.html
```
<div #fadeDiv class="fadeDiv-main"
[ngClass]="applyClass()">
<ng-content select="[div-main-content]"></ng-content>
</div>
```
### Component.ts
```
import { Component, HostListener, Input, OnInit, ViewChild, ViewContainerRef, AfterViewInit } from '@angular/core';
@Component({
selector: 'app-fade-in-or-out-div',
templateUrl: './fade-in-or-out-div.component.html',
styleUrls: ['./fade-in-or-out-div.component.css']
})
export class FadeInOrOutDivComponent implements OnInit, AfterViewInit {
@Input() isFromLeft = true;
@ViewChild('fadeDiv', { read: ViewContainerRef }) fadeDiv: ViewContainerRef;
// 註冊 window scroll 事件
// 不需要特別刪除事件,參考: https://stackoverflow.com/a/50015632
@HostListener('window:scroll', ['$event']) onScroll(e: Event): void {
this.fadeInOrOutInTheScreen();
}
// 註冊 window resize 事件
@HostListener('window:resize', ['$event']) onResize(e: Event): void {
this.fadeInOrOutInTheScreen();
}
// ※ 棄用原因: angular route change 的時候不會 trigger window onload
// 註冊 window onload 事件
// 如果 div 已經在畫面內直接顯示
// ※: 原本使用 ngAfterViewInit 一樣沒有渲染完畢,導致 element offsetTop 還是錯的位置
// 故使用 load 事件,等元素加載完畢後再檢測
// @HostListener('window:load', ['$event']) onLoad(e: Event): void {
// if (this.isFirstLoaded == false) {
// this.fadeInOrOutInTheScreen();
// this.isFirstLoaded = true;
// console.log("window load")
// }
// }
isFirstLoaded: boolean = false;
fadeStatus: E_FadeStatus = E_FadeStatus.Default;
constructor() { }
ngOnInit(): void {
}
ngAfterViewInit(): void {
// 嘗試等待下一個動畫幀等 html 元素渲染完畢
// 參考:
// 1. JavaScript學習日記 : Day30 - JavaScript動畫: https://ithelp.ithome.com.tw/articles/10280810
// 2. 計時與動畫: https://pjchender.dev/webapis/webapis-timer-and-scheduling/
// console.log(this.isFirstLoaded)
if (this.isFirstLoaded == false) {
requestAnimationFrame(() => {
this.fadeInOrOutInTheScreen();
})
this.isFirstLoaded = true;
}
}
applyClass() {
let baseClass = "";
if (this.fadeStatus == E_FadeStatus.Default) {
// console.log("hide")
return "hide";
}
else if (this.fadeStatus == E_FadeStatus.FadeIn) {
baseClass = "fadeIn";
}
else if (this.fadeStatus == E_FadeStatus.FadeOut) {
baseClass = "fadeOut";
}
if (this.isFromLeft) {
// console.log(baseClass + " left-to-right")
return baseClass + " left-to-right";
}
else {
// console.log(baseClass + " right-to-left")
return baseClass + " right-to-left";
}
}
fadeInOrOutInTheScreen() {
const element = this.fadeDiv.element.nativeElement;
// console.log("object offsetTop: " + element.offsetTop)
// console.log("object clientHeight: " + element.clientHeight)
// console.log("window scrollY: " + window.scrollY)
// console.log("window outerHeight: " + window.outerHeight)
// 棄用原因: 看到底部才跑出來,這樣區塊太長會看不到最上方的資訊,故改為看到區塊的最上方就開始 Fade In
// var bottom_of_object = element.offsetTop + element.clientHeight;
// var bottom_of_window = window.scrollY + window.outerHeight;
// console.log("object bottom: " + bottom_of_object)
// console.log("window bottom: " + bottom_of_window)
var top_of_object = element.offsetTop + 200; // +200 讓其再下滑一點才 FadeIn,這樣能更清楚看到 FadeIn 效果
var windowHeight = window.scrollY + window.outerHeight;
// console.log("object top: " + top_of_object)
// console.log("window height: " + windowHeight)
// console.log(this.fadeStatus)
if (windowHeight > top_of_object) {
this.fadeStatus = E_FadeStatus.FadeIn;
}
else {
// 如果是剛剛 Fade In 進入畫面的才需要 Fade Out
if (this.fadeStatus == E_FadeStatus.FadeIn) {
this.fadeStatus = E_FadeStatus.FadeOut;
}
}
// console.log(this.fadeStatus)
}
}
enum E_FadeStatus {
Default = "Default",
FadeIn = "FadeIn",
FadeOut = "FadeOut",
}
```
### 測試荒唐鏡板 Component.css
```
div.fadeDiv-main {
margin: 100px;
padding: 100px;
background-color: lightgreen;
}
div.fadeDiv-main.hide {
opacity: 0;
}
div.fadeIn.right-to-left {
background-image: url("https://pic1.xuehuaimg.com/proxy/baijia/https://f12.baidu.com/it/u=3910127792,1794832412&fm=173&app=25&f=JPEG?w=640&h=367&s=E0C296472A4108550ED12CA20300D002&access=215967316");
background-position: center;
background-repeat: no-repeat;
background-size: contain;
animation: fadeIn-right-to-left-animation 1.5s ease-out forwards;
}
@keyframes fadeIn-right-to-left-animation {
0% {
opacity: 0;
transform: translateX(100%);
}
100% {
opacity: 1;
transform: translateX(0%);
}
}
div.fadeIn.left-to-right {
background-image: url("https://pic1.xuehuaimg.com/proxy/baijia/https://f12.baidu.com/it/u=3910127792,1794832412&fm=173&app=25&f=JPEG?w=640&h=367&s=E0C296472A4108550ED12CA20300D002&access=215967316");
background-position: center;
background-repeat: no-repeat;
background-size: contain;
animation: fadeIn-left-to-right-animation 1.5s ease-out forwards;
}
@keyframes fadeIn-left-to-right-animation {
0% {
opacity: 0;
transform: translateX(-100%);
}
100% {
opacity: 1;
transform: translateX(0%);
}
}
div.fadeOut.right-to-left {
background-image: url("https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQV-1hvRnGwpIgG2RmB8eYtBv9nnVNtoNYuDHoOrKkQ5RmXaGDwMhZgrmsDC1qEr3BpT-c&usqp=CAU");
background-position: center;
background-repeat: no-repeat;
background-size: contain;
animation: fadeOut-right-to-left-animation 1.5s ease-out forwards;
}
@keyframes fadeOut-right-to-left-animation {
0% {
opacity: 1;
transform: translateX(0%);
}
100% {
opacity: 0;
transform: translateX(100%);
}
}
div.fadeOut.left-to-right {
background-image: url("https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQV-1hvRnGwpIgG2RmB8eYtBv9nnVNtoNYuDHoOrKkQ5RmXaGDwMhZgrmsDC1qEr3BpT-c&usqp=CAU");
background-position: center;
background-repeat: no-repeat;
background-size: contain;
animation: fadeOut-left-to-right-animation 1.5s ease-out forwards;
}
@keyframes fadeOut-left-to-right-animation {
0% {
opacity: 1;
transform: translateX(0%);
}
100% {
opacity: 0;
transform: translateX(-100%);
}
}
```
### 正常使用版 Component.css
```
div.fadeDiv-main {
width: 100%;
height: 100%;
padding: 5px;
}
div.fadeDiv-main.hide {
opacity: 0;
}
div.fadeIn.right-to-left {
animation: fadeIn-right-to-left-animation 1.5s ease-out forwards;
}
@keyframes fadeIn-right-to-left-animation {
0% {
opacity: 0;
transform: translateX(100%);
}
100% {
opacity: 1;
transform: translateX(0%);
}
}
div.fadeIn.left-to-right {
animation: fadeIn-left-to-right-animation 1.5s ease-out forwards;
}
@keyframes fadeIn-left-to-right-animation {
0% {
opacity: 0;
transform: translateX(-100%);
}
100% {
opacity: 1;
transform: translateX(0%);
}
}
div.fadeOut.right-to-left {
animation: fadeOut-right-to-left-animation 1.5s ease-out forwards;
}
@keyframes fadeOut-right-to-left-animation {
0% {
opacity: 1;
transform: translateX(0%);
}
100% {
opacity: 0;
transform: translateX(100%);
}
}
div.fadeOut.left-to-right {
animation: fadeOut-left-to-right-animation 1.5s ease-out forwards;
}
@keyframes fadeOut-left-to-right-animation {
0% {
opacity: 1;
transform: translateX(0%);
}
100% {
opacity: 0;
transform: translateX(-100%);
}
}
```
## 呼叫 Fade In Or Out Component
### 呼叫時特別注意
錯誤示範↓

今日頭條6以後,最下面兩個div不該出現的
img 沒有給 height/width attribute 的情況下
而是 style 設定 width/height 都 100% ( 自適應大小 )
這樣會因為 img 剛剛還沒 loading 出來 ( img 沒設定固定高度 => 沒讀到圖片的情況下高度為 0 )
導致下面的 div 位置在渲染階段的時候被認為該出現 ( 還沒被圖片推下去 )
但其實不應該
所以使用時,<span style="color:red;font-weight: bold">區塊最好先設定好該有的高度</span>
### html
```
<!-- class 可有可無,若要覆蓋基本 style 再給 class -->
<app-fade-in-or-out-div class="testDiv" [isFromLeft]="false">
<div div-main-content>
<h2>今日頭條</h2>
</div>
</app-fade-in-or-out-div>
```
### css
若要覆蓋 style 的寫法
```
::ng-deep .testDiv>div.fadeDiv-main {
margin: 100px;
padding: 100px;
background-color: rgb(255, 255, 255) !important;
}
```
### 進階使用: 呼叫方使用 float 進行兩側分割畫面 ( RWD 適應 )
#### 結果

#### css
```
.news-wrap {
/* 可以用來清除浮動,去除 margin 合併 */
display: flow-root;
}
.news-wrap>.left {
width: 50%;
line-height: 30px;
padding: 5px;
border: 2px blue solid;
float: left;
}
.news-wrap>.right {
width: 50%;
line-height: 30px;
padding: 5px;
border: 2px green solid;
float: left;
}
@media screen and (max-width: 600px) {
.news-wrap>.left {
width: 100%;
}
.news-wrap>.right {
width: 100%;
}
}
```
#### html
```
<div class="news-wrap">
<div class="left">
<app-fade-in-or-out-div [isFromLeft]="true">
<div div-main-content
class="rounded shadow bg-white">
<h2 class="m-0 py-2">今日頭條1</h2>
<h4><a href="https://ynews.page.link/EjrC">俄烏戰停火有解? 美媒推測普丁在等「完全解放」頓巴斯</a></h4>
<img class="w-75 rounded shadow"
style="max-width: 500px;"
onerror="this.src='assets/Images/404-error.png'"
[src]="'https://images.pexels.com/photos/3526022/pexels-photo-3526022.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1'" />
<p class="mt-3 text-start px-3">
俄烏戰爭開打迄今已經超過4個多月,而隨著全球經濟所受到的衝擊愈來愈深,何時才能畫下句點,成了各國最關注的焦點,而現在最新消息傳出,俄羅斯總統普丁有可能在完全佔領頓巴斯(Donbas)地區後,提出停火協議。
根據《CNBC》新聞網的報導,本周一普丁慶賀俄羅斯「解放」了盧甘斯克地區後,另一個宣布脫離烏克蘭的頓巴斯地區成了普丁下一個重點,據當地官員透露,俄羅斯的軍隊現正集中精力攻打頓巴斯,該地區週日遭到猛烈砲擊。
對此專為國防產業提供建議的奧斯托亞諮詢(Ostoya Consulting)負責人阿布拉莫維奇(Victor Abramowicz)表示,俄羅斯「幾乎可以肯定」能夠在頓巴斯實現其更有限的目標,而普丁提議停火的前提,只要給他自己一個機會,來鞏固他迄今為止所取得的成果。
阿布拉莫維奇表示,如果普丁在取得頓巴斯後提議停火,將會讓烏克蘭總統澤倫斯基陷入困境,如果他拒絕了停火協議,最嚴重的後果將會失去西方國家的支持。尤其美國今年11月將面臨期中選舉,在自身經濟持續低迷的情況下,拜登總統對於烏克蘭的堅定支持會不會出現鬆動,澤倫斯基必須慎重考量。
</p>
</div>
</app-fade-in-or-out-div>
<app-fade-in-or-out-div [isFromLeft]="true">
<div div-main-content
class="rounded shadow bg-white w-100">
<h2 class="m-0 py-2">今日頭條3</h2>
<h4><a href="https://ynews.page.link/BB3y">金曲33/小S當眾「襲胸」具俊曄 網虧:過氣主持風格</a></h4>
<img class="w-75 rounded shadow"
style="max-width: 500px;"
onerror="this.src='assets/Images/404-error.png'"
[src]="'https://images.pexels.com/photos/713149/pexels-photo-713149.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1'" />
<p class="mt-3 text-start px-3">
「第33屆金曲獎」於昨(2)日晚間7時在高雄巨蛋隆重登場,由資深藝人羅時豐獨挑大樑主持典禮,並請來藝人小S(徐熙娣)與陶晶瑩一同搭檔演出,更邀請到大S老公、人稱「國民姊夫」的具俊曄擔任神秘嘉賓,不僅具俊曄於台上熱情的展現自我,小S更當眾對姊夫「襲胸」,眾多搞笑互動把現場氣氛炒熱到最高點。
身為今年金曲獎神秘嘉賓具俊曄,在台上勇於表現自己,除了大秀一口流利的中文,更與小S大跳酷龍經典歌曲〈Bing Bing Bing〉,此外,小S還大膽伸手狂摸姊夫的胸部,具俊曄隨即撥開她的手並大喊「不可以這樣」、「安堆(韓文不行)」,有趣的互動笑翻台下眾人。
</p>
</div>
</app-fade-in-or-out-div>
</div>
<div class="right">
<app-fade-in-or-out-div [isFromLeft]="false">
<div div-main-content
class="rounded shadow bg-white w-100">
<h2 class="m-0 py-2">今日頭條2</h2>
<h4><a href="https://ynews.page.link/6gHv">本土34499例95死! 1歲男童發燒、嘔吐「一天就心跳突停」不治</a></h4>
<img class="w-75 rounded shadow"
style="max-width: 500px;"
onerror="this.src='assets/Images/404-error.png'"
[src]="'https://images.pexels.com/photos/3952231/pexels-photo-3952231.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1'" />
<p class="mt-3 text-start px-3">
中央流行疫情指揮中心今(6)日公布國內新增34577例COVID-19確定病例,分別為34499例本土個案及78例境外移入;另確診個案中新增95例死亡,中重症新增275例。今日新增死亡個案中,最小僅1歲男童,6月30日發病,出現嘔吐、呼吸喘、發燒等症狀,隔天送到急診時即突發心跳停止,急救不治。
今日新增本土個案,依縣市區分,以新北市新增5324例最多,其次為台中市新增4669例、桃園市新增3687例、高雄市新增3608例、台北市新增3110例、台南市新增2902例。
死亡個案95例當中,最年幼的1歲男童,本身有先天性神經系統疾病,長期使用呼吸器,6月30日發病,出現嘔吐、呼吸喘、發燒、活力及餵食量下降,7月1日送至醫院急診,一到急診後就突發心跳停止,經急救仍不治。死亡原因為新冠病毒感染跟本身疾病,歸類為共病死亡案例。
另外,今日中重症新增275例,中症患者新增99例、重症患者新增68例。當中一口氣新增3例兒童重症MIS-C(兒童多系統發炎症候群),分別為0歲女童、2歲男童、12歲女童(曾接種一劑疫苗),均在6月初確診,6月底又開始出現發燒合併相關的腹瀉、眼睛紅、頸部腫等症狀,於6月29日至7月2日間就醫,經醫師診斷疑似MIS-C住院,目前都住院治療中。
指揮中心統計,國內兒童(12歲以下)重症累計有88例,已有22例死亡;分別為腦炎22例、肺炎16例、MIS-C 30例、敗血症3例、哮吼症8症、家中死亡6例以及共病2例。
</p>
</div>
</app-fade-in-or-out-div>
<app-fade-in-or-out-div [isFromLeft]="false">
<div div-main-content
class="rounded shadow bg-white w-100">
<h2 class="m-0 py-2">今日頭條4</h2>
<h4><a href="https://udn.com/news/story/7266/6439509">高溫上看36度!午後大雷雨 吳德榮:周五起雨漸少但酷熱</a></h4>
<img class="w-75 rounded shadow"
style="max-width: 500px;"
onerror="this.src='assets/Images/404-error.png'"
[src]="'https://images.pexels.com/photos/1102915/pexels-photo-1102915.jpeg'" />
<p class="mt-3 text-start px-3">
中央大學大氣科學系兼任副教授吳德榮今天在「三立準氣象.老大洩天機」專欄表示,各國模式模擬顯示,熱帶低壓在日本本州南方近海,向東移動;未來有再恢復成輕颱的趨勢,並轉向沿本州東側北上。最新歐洲系集模式及美國模式模擬圖皆顯示,下周菲律賓東方至南海海面仍是有利熱帶擾動發展的海域,但模擬結果仍分歧,需持續密切觀察。
他說,今天各地大多為多雲時晴的天氣;大氣不穩定,迎風面的南台灣不定時及中部以北的午後,皆有明顯降雨的機率,應注意大雷雨、伴隨劇烈天氣如雷擊、強風、瞬間強降雨,也要注意氣象局的特報。天氣偏熱,北部23至36度,中部22至35度,南部23至35度,東部22至35度。
明天太平洋高壓仍偏弱,天氣晴熱,持續不穩定,午後易有強對流發生,仍應注意劇烈天氣。
吳德榮表示,周五至下周二太平洋高壓逐日增強,晴朗炎熱,高溫升至36度左右,要防曬、防中暑;午後對流逐日減弱、範圍逐日縮小,大多在山區及少部分鄰近平地。
</p>
</div>
</app-fade-in-or-out-div>
</div>
</div>
```