首頁 > 軟體

Android 超詳細SplashScreen入門教學

2022-03-26 19:01:02

這次的Android系統變化當中,UI的變化無疑是巨大的。Google在Android 12中採取了一種叫作Material You的介面設計,一切以你為中心,以你的喜好為風格。相信大家一旦上手Android 12之後應該能立刻察覺到這些視覺方面的變化。

關於這個SplashScreen,今天就值得好好講一講了。

什麼是SplashScreen

SplashScreen其實通俗點講就是指的閃屏介面。這個我們國內開發者一定不會陌生,因為絕大多數的國內App都會有閃屏介面這個功能,很多的App還會利用閃屏介面去打廣告。下圖是QQ的閃屏介面:

然而在海外,閃屏介面其實並不太常見,甚至Google之前都不推薦我們在App中加入閃屏介面,所以這次Android 12中官方推出了SplashScreen功能還是讓我有點意外的。

不過這次官方的SplashScreen和我們國內常見的閃屏介面還不一樣,它並不是為了讓你在這個介面打廣告的,而是為了在App啟動初始化的時候避免讓使用者在一個空白介面等待過長時間。

雖說Android一直是建議我們將重量級的操作延後執行,讓App的啟動時間越短越好,但是仍然無法完全避免一些App啟動時的短暫白屏情況。

因此,這次的SplashScreen就是為了解決這個問題而推出的,它將會在一定程度上提升使用者體驗,徹底告別過去的啟動白屏現象。

何時會顯示SplashScreen

注意,SplashScreen在Android 12上是強制的,即使你什麼都不做,你的App在Android 12上也會自動擁有SplashScreen介面。預設情況下,App的Launcher圖示會作為SplashScreen介面的中央圖示,windowBackground屬性指定的顏色會作為SplashScreen介面的背景顏色。不過這些都可以修改。

關於如何修改我們稍後再談,既然SplashScreen介面是強制顯示的,我們首先應該搞清楚,在什麼情況下會顯示SplashScreen?

根據官方檔案的說明,SplashScreen會在App冷啟動和溫啟動的時候顯示,永遠不會在App熱啟動的時候顯示。

那麼,什麼是冷啟動、溫啟動和熱啟動呢?

簡單概括一下的話,如果App被完全殺死了,這個時候去啟動它就是冷啟動。如果App的主Activity被銷燬或回收了,這個時候去啟動它就是溫啟動。如果App只是被掛起到了後臺,這個時候去啟動它就是熱啟動。

我這種概括方式在一些細節方面其實並不足夠準確,但如果只是為了大概瞭解SplashScreen的顯示時機,那麼簡單這樣理解就可以了。

而如果你想更加細緻地學習這幾種啟動模式的區別,可以參考以下官方檔案連結:

https://developer.android.google.cn/topic/performance/vitals/launch-time

何時會隱藏SplashScreen

SplashScreen是為了防止App在冷啟動或溫啟動的時候初始化時間過長,導致使用者看到白屏現象而引入的。那麼很顯然,只要App初始化完成,可以將內容展示給使用者的時候,SplashScreen就會自動隱藏。

如果用更加科學一點的定義來描述的話,那就是當App開始在介面上繪製第一幀的時候,SplashScreen就會消失。

那麼一個App什麼時候會在介面上繪製第一幀呢?我們可以不用知道它準確的時機,但是要知道它大致的時機範圍,因為這決定要我們如何更好地編寫程式碼。

假如我們在一個應用的主Activity中編寫如下程式碼:

class MainActivity : AppCompatActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        Thread.sleep(3000)
    }

}

或者也可以這樣寫:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    override fun onResume() {
        super.onResume()
        Thread.sleep(3000)
    }

}

可以看到,我們分別在onCreate()和onResume()方法中讓主執行緒沉睡了3秒鐘。然後執行一下程式:

你會發現,SplashScreen真的顯示了3秒鐘以上才消失。

同時這也說明了,不管是onCreate()還是onResume()方法,它們都還處於App的初始化階段,並沒有開始在介面上繪製第一幀。

接下來我們可以嘗試這樣改造一下程式碼:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val contentView: View = findViewById(android.R.id.content)
        contentView.post {
            Thread.sleep(3000)
        }
    }

}

這裡可以藉助任何一個View的範例呼叫一下它的post函數,並在post的回撥當中讓主執行緒沉睡3秒。然後再次執行程式:

你會發現,SplashScreen只是短暫顯示了一下就進入了App的主介面。但現在主介面其實還是不能響應任何事件的,而是要等待3秒鐘以後才能響應。

由此我們就可以大致得出一些結論,比如說onCreate()和onResume()方法都是在App開始繪製第一幀之前執行的,而View的post回撥則是在App繪製第一幀之後執行的。

當第一幀繪製出來以後,說明App的介面上已經可以有東西展示出來了,將不會再是一個空白介面,此時繼續展示SplashScreen就沒有意義了,所以SplashScreen理應在這個時候消失。但同時,如果在第一幀繪製出來之後我們再在主執行緒裡去執行耗時邏輯,那麼使用者將會實實在在感受到卡頓的體驗,SplashScreen已經無法再幫我們進行掩蓋。

實際上,不管是在第一幀繪製之前還是之後,我們都不應該在主執行緒執行長時間的耗時操作。最正確的做法是,只在主執行緒裡做最少的事情,讓App可以快速響應使用者的各種輸入事件,將所有耗時的邏輯都放到子執行緒當中去處理。

延長顯示SplashScreen

延長SplashScreen的顯示時間是一種我不太建議的做法,但我們確實可以這樣做。

先說為什麼不建議延長SplashScreen的顯示時間。

原則上我們應該讓App的啟動時間越短越好,即使有了SplashScreen,我們也不應該故意讓App的啟動時間變得更長。

要知道,在SplashScreen的顯示過程中,App是一直在主執行緒裡執行初始化操作的。這也就意味著,你的App主執行緒是一直被佔據著的,從而無法響應使用者的各種輸入,這也就導致了應用程式ANR的可能。不管有沒有SplashScreen,只要在主執行緒裡執行了過多耗時操作,都可能會導致ANR。

那麼為什麼還要延長顯示SplashScreen呢?

有一種說法是,他們App的內容都是從伺服器或者從本地磁碟讀取的,即使App初始化完成了,資料還沒有準備好,也就沒有內容可以展示,所以想要將SplashScreen延長到資料準備完成。

但我個人認為這並不是一種非常合適的做法,這種情況我們完全可以先在介面上顯示一個載入進度條,或者佔點陣圖之類的東西,然後等有了資料之後再更新介面上的內容。

還有一種說法是,他們希望SplashScreen不僅僅是用來載入等待的,還可以用來做一些品牌展示和推廣之類的工作。這樣如果SplashScreen過快地消失,可能使用者根本來不及看到SplashScreen上的內容。

當然,也有另一種說法是,他們在SplashScreen上顯示的並不是一個靜態的圖示,而是一個動畫,所以至少要等到動畫結束之後再隱藏SplashScreen。

不管你是屬於哪一種,Google都給我們提供了延長顯示SplashScreen的能力。

剛才說了,SplashScreen會在App開始在介面上繪製第一幀的時候自動消失,那麼如果我們阻止了App在介面上繪製第一幀,是不是SplashScreen就不會消失了?

沒錯,這就是延長顯示SplashScreen的工作原理。具體程式碼如下:

class MainActivity : AppCompatActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val contentView: View = findViewById(android.R.id.content)
        contentView.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {
            override fun onPreDraw(): Boolean {
                return false
            }
        })
    }

}

這裡我們在回撥函數onPreDraw()中返回了一個false,也就意味著,我們的PreDraw階段始終沒有準備好。既然PreDraw都還沒準備好,App肯定是不會開始繪製第一幀的,那麼SplashScreen自然也就不會消失了。

於是上述程式碼將會實現一個永久顯示SplashScreen的效果。

有了這個原理,那麼我們就可以根據自己的需求編寫一些邏輯了。比如剛才提到的從磁碟讀取資料的場景,我們可以一開始在onPreDraw()中函數中返回false,然後開啟子執行緒去讀取資料,等到資料讀取完成再將返回值改成true即可。範例程式碼如下:

class MainActivity : AppCompatActivity() {

    @Volatile
    private var isReady = false
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val contentView: View = findViewById(android.R.id.content)
        contentView.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {
            override fun onPreDraw(): Boolean {
                if (isReady) {
                    contentView.viewTreeObserver.removeOnPreDrawListener(this)
                }
                return isReady
            }
        })
        thread { 
            // Read data from disk
            ...
            isReady = true
        }
    }

}

注意,在SplashScreen的顯示過程中,onPreDraw()函數是以很高的頻率在持續重新整理的。所以它依然會將主執行緒阻塞住,導致應用程式無法響應使用者的輸入事件,直到我們在onPreDraw()函數返回true才會停止重新整理。

自定義SplashScreen樣式

接下來終於到了可能許多朋友最為關心的部分,自定義SplashScreen的樣式。

雖然預設的SplashScreen介面並不難看,對於大多數的App來說可能也已經完全足夠了,但是Google仍然給了我們比較高的控制權來自定義SplashScreen的樣式。

這裡我就將幾個比較重要的自定義樣式屬性來跟大家介紹一下。

剛才有提到過,SplashScreen預設會使用windowBackground屬性指定的顏色作為介面的背景顏色。但如果我想要單獨給SplashScreen介面指定一個背景色呢?可以在主題檔案中定義如下屬性:

<item name="android:windowSplashScreenBackground">#CCCCCC</item>

這裡我們單獨將SplashScreen的背景指定成了淺灰色,效果如下圖所示:

需要注意,這個屬性以及接下來要介紹的所有屬性都是在Android 12系統上新增的,所以你應該在一個values-v31的專屬目錄下使用它們。

既然能夠自定義SplashScreen的背景色,那麼我們是不是也可以自定義SplashScreen上的圖示呢?

很難想象為什麼要在SplashScreen介面上展示一個和Launcher Icon不同的圖示,但Google確實允許我們這麼做:

<item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_screen_icon</item>

這裡我們給SplashScreen介面指定了一個單獨的圖示,注意這個圖示可以是一張靜態的圖片,也可以是一個動畫資源。由於製作動畫比較複雜,不在本文的討論範圍內,所以我們只以靜態圖片來舉例。

我準備了這樣一張圖,並將它命名為splash_screen_icon.jpg。

然後執行程式,效果如下圖所示:

你會發現,雖然我提供的圖示是正方形的,但最終顯示在SplashScreen上的卻是一個圓形圖片。

由此我們可以得出結論,SplashScreen和Launcher Icon一樣,也是同樣會受到廠商mask的影響的。它的大致工作原理如下圖所示:

可以看到,這裡背景層是一張藍色的網格圖,前景層是一張Android機器人Logo圖,然後蓋上一層圓形的mask,最終就裁剪出了一張圓形的應用圖示。

如果對此還不夠了解的話,可以去參考我之前寫的一篇文章 Android 8.0系統中的應用圖示適配

上述例子中我使用的是一張不透明的圖片來作為圖示,其實我們也可以提供一張有透明度的圖片,然後再借助如下屬性來控制圖示的背景色:

<item name="android:windowSplashScreenIconBackgroundColor">#BB86FC</item>

這樣,只要前景圖示是有透明度的圖片,背景顏色就可以顯示出來了,如下圖所示:

最後,如果你希望在SplashScreen上再進行一些品牌方面的推廣,還可以通過以下屬性來顯示你的品牌資訊:

<item name="android:windowSplashScreenBrandingImage">@drawable/brand_logo</item>

這裡可以傳入一張品牌圖片,我沒能在官網找到Google對這張圖片尺寸比例的定義,但如果你隨便傳入一張圖片的話,可能會出現拉伸的情況。

為此,我通過自己做實驗,大概總結出了這裡應該使用一張2.4:1的圖片,最終的效果如下圖所示:

適配舊版SplashScreen

最後,我們再來了解一下,如何才能去適配舊版的SplashScreen。

準確來說,Android官方是沒有舊版SplashScreen這一說的,因為SplashScreen是在Android 12中才新增加的功能。

但是,有很多的App早在官方提供API之前,就已經自己實現了SplashScreen功能。正如前面所說,這個功能在國內很常見。

那麼接下來問題來了。過去通過自己的方式實現的SplashScreen,和現在官方提供的SplashScreen要如何相容呢?

這著實是一個問題,主要原因在於,SplashScreen在Android 12上是強制啟用的。所以,如果你的程式碼中還保留著過去自己實現的那一套SplashScreen,在Android 12中就會出現雙重SplashScreen的現象。

但如果我們從程式碼中移除了過去自己實現的SplashScreen,那麼在Android 12之前的系統版本就沒有SplashScreen功能了。

要如何解決這個問題呢?不要著急,Google在AndroidX中提供了一個向下相容的SplashScreen庫。根據官方的說法,我們只要使用這個庫就可以輕鬆解決舊版SplashScreen的適配問題。

用法很簡單,跟著如下步驟走即可。

第一步,修改build.gradle檔案,將targetSdkVersion指定到31,並新增如下依賴庫:

android {
compileSdkVersion 31
...
}
dependencies {
...
implementation 'androidx.core:core-splashscreen:1.0.0-alpha01'
}

第二步,修改主題檔案,如下所示:

<style name="MySplashTheme" parent="Theme.SplashScreen">
    <item name="windowSplashScreenBackground">#CCCCCC</item>
    <item name="windowSplashScreenAnimatedIcon">@drawable/splash_screen_icon</item>
    <item name="postSplashScreenTheme">@style/Theme.SplashTest</item>
</style>

注意這裡的變動至關重要。我們新定義了一個主題,這個主題的名字叫什麼都可以,但它一定要繼承自Theme.SplashScreen。

然後我們可以使用windowSplashScreenBackground和windowSplashScreenAnimatedIcon這兩個屬性來分別指定SplashScreen的背景色和中央圖示。

不過我比較疑惑的是,我們不能像剛才那樣在SplashScreen介面指定圖示的背景色和品牌圖片,因為這裡並沒有那兩個屬性。不知道是不是因為現在庫還屬性比較早期的階段,以後或許會加上這些屬性。

另外,我們還必須要指定postSplashScreenTheme這個屬性,將它的值指定成你的App原來的主題。這樣,當SplashScreen結束時,你的主題就能夠被複原,從而不會影響到你的App的主題外觀。

第三步,修改AndroidManifest.xml檔案,應用我們剛剛新定義的主題:

<manifest>
   <application android:theme="@style/MySplashTheme">
    <!-- or -->
        <activity android:theme="@style/MySplashTheme">
	...

這裡視你之前程式碼的寫法來決定是替換application標題裡的theme,還是activity標題裡的theme。

第四步,在你的啟動Activity中加入如下程式碼:

class MainActivity : AppCompatActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        installSplashScreen()
        setContentView(R.layout.activity_main)
        ...
    }

}

如果你還在使用Java語言的話,那麼需要改成如下寫法:

public class MainActivity extends AppCompatActivity {

    @Override
	protected void onCreate(Bundle savedInstanceState) {
	    super.onCreate(savedInstanceState);
	    SplashScreen.installSplashScreen(this);
	    setContentView(R.layout.activity_main);
	    ...
	}
    
}

注意,installSplashScreen()這句程式碼一定要加入到setContentView()的前面。

這樣,當我們剛剛進入App的時候,就會先顯示一個SplashScreen介面,然後當App初始化完成之後,SplashScreen會自動消失,並且主題也會變成原來App的主題樣式。

接下來我們只需要把過去自己實現的SplashScreen移除即可,不然的話仍然還是會產生雙重SplashScreen的現象。

以上步驟是官方提供的適配舊版SplashScreen的解決方案,但是我按照上述步驟進行了一下實現,最終的測試效果卻非常差。

主要問題集中在於舊版Android系統上中央圖示不會被mask,而在Android 12上中央圖示卻會被mask,從而導致新舊系統的SplashScreen介面差別很大,也很難看。

不過畢竟我們現在使用的SplashScreen庫還處於alpha階段,後面發生變動的可能性很大,或許這些問題在正式版出現之後都會被修復。

另外,即使官方的庫有問題,我們還是完全有辦法去規避它。比如說在程式碼中進行邏輯判斷,如果是Android 12系統就不顯示自己的SplashScreen介面,因為系統有預設的SplashScreen。而在Android 12以下的系統,就顯示自己的SplashScreen介面。

方法總比困難多,不是嗎?

那麼本篇文章的內容就到這裡,讓我們一起靜靜等待Android 12的到來吧。

到此這篇關於Android 超詳細SplashScreen入門教學的文章就介紹到這了,更多相關Android SplashScreen內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com