系統中的活動是被返回棧管理的。 啓動新活動時,通常將其放置在當前堆棧的頂部併成爲正在運行的活動,先前的活動始終在堆棧中保持在其下方,並且在新活動退出之前不會再次成爲前臺。 屏幕上可以看到一個或多個活動堆棧。
在這些活動,或者簡單點來說這些頁面不停的切換之間,它們的狀態發生了一次又一次的改變,這些狀態的改變我們就可以看作是Activity的生命週期變化。
研究生命週期變化的常見方法是通過log來進行觀察,這也是我們能查到的資料絕大部分都是用打印log的方法演示其生命週期的轉變。我們也這麼做,畢竟我也不會其他辦法。
基於課程2的代碼,我們爲MainActivity添加上圖所示的重載方法。
在MainActivity內部,使用ctrl+o的快捷方式:
在上面的界面選擇生命週期圖中出現的方法,當然了,小夥伴們onCreate方法,我們就不用在添加了。
/**
* Activity被創建時該回調方法執行,一般用來做初始化操作
*
* @param savedInstanceState
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
/**
* Activity正在重新啓動,當Activity由不可見變爲可見狀態時,該回調方法執行
*
* 一般在該Activity處於可見狀態,也就是當前顯示在屏幕上時,切換應用後,再次
* 打開該應用的時候會執行該回調方法
*/
@Override
protected void onRestart() {
super.onRestart();
}
/**
* Activity正在啓動,此時Activity已處於可見狀態,
* 此時,Activity並未在屏幕顯示,所以無法和用戶進行交互
*/
@Override
protected void onStart() {
super.onStart();
}
/**
* Activity已在前臺可見,可與用戶交互
*/
@Override
protected void onResume() {
super.onResume();
}
/**
* Activity正在停止(Paused形態)
*/
@Override
protected void onPause() {
super.onPause();
}
/**
* Activity即將停止或者完全被覆蓋(Stopped形態),此時Activity不可見,僅在後臺運行
*/
@Override
protected void onStop() {
super.onStop();
}
/**
* Activity正在被銷燬,一般在這個回調方法中釋放資源
*/
@Override
protected void onDestroy() {
super.onDestroy();
}
接下來我們就需要使用Logcat來跟蹤理解Activity的生命週期變化了。
在Logcat中輸出我們想要的日誌內容需要藉助Log類。
android.util.Log
通過 Log 類,我們可以創建日誌消息,這些消息會顯示在 logcat 中。哦我們呢可以使用以下日誌方法:
Log.e(String, String)(錯誤)
Log.w(String, String)(警告)
Log.i(String, String)(信息)
Log.d(String, String)(調試)
Log.v(String, String)(詳細)
除開發期間外,其他任何時候都絕不應將詳細日誌編譯到應用中。雖然會編譯調試日誌,但會在運行時將其去掉,而錯誤、警告和信息日誌會始終保留。
對於每種日誌方法,第一個參數都應是唯一標記,第二個參數都應是消息。系統日誌消息的標記是一個簡短的字符串,指示消息所源自的系統組件(例如 ActivityManager)。標記可以是您認爲有用的任何字符串,例如當前類的名稱。
最常見的方法就是在我們的Activity中定義如下格式的靜態變量:
private static final String TAG = "MainActivity";
好像還有一種更方便的寫法,我忘記了,不知道是不是下面的樣子:
private static final String TAG = MainActivity.class.getSimpleName();
其中TAG的值爲當前Activity的名稱。
我們在代碼中使用:
@Override
protected void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroy: ");
}
其他的方法同樣添加對應的log語句。
Logcat使用:
這裏我們簡單看下常見的幾種場景中生命週期的變化:
啓動應用
logcat輸出:
用戶按下BACK鍵
用戶按下BACK鍵後,再次返回應用
用戶按下HOME鍵
用戶按下HOME鍵後,再次返回應用
打開其他應用
另外我們需要主要一點,當用戶旋轉屏幕的時候,系統會銷燬MainActivity實例,然後重新創建一個實例。
那麼我們接下來看看怎麼在我們的程序中怎麼適配橫屏。
新建資源目錄layout-land:
在Project視圖下我們會看到該目錄已經被創建
那麼layout-land目錄有什麼用呢?
這裏的-land後綴名是配置修飾符的另一個使用例子。 Android依靠res子目錄的配置修飾符定位最佳資源以匹配當前設備配置。
我們可以將layout-land理解爲我們應用程序的備用資源,如果我們沒有提供這兩個目錄,那麼系統會使用默認的layout目錄下的佈局文件。
也就說當我們創建了layout-land目錄,並且在下面創建了佈局文件,那麼系統在橫屏模式下回優先從這裏查找佈局文件。
注意:兩個佈局文件的文件名必須相同,這樣它們才能以同一個資源ID被引用
我們可以將layout目錄下的activity_main.xml直接拷貝到layout-land中,然後修改下佈局文件:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/tv_question"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_answerA"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="@id/tv_question"
app:layout_constraintTop_toBottomOf="@id/tv_question" />
<TextView
android:id="@+id/tv_answerB"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="50dp"
android:textSize="18sp"
app:layout_constraintStart_toEndOf="@id/tv_answerA"
app:layout_constraintTop_toTopOf="@id/tv_answerA" />
<TextView
android:id="@+id/tv_answerC"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="@id/tv_question"
app:layout_constraintTop_toBottomOf="@id/tv_answerA" />
<TextView
android:id="@+id/tv_answerD"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="50dp"
android:textSize="18sp"
app:layout_constraintStart_toEndOf="@id/tv_answerC"
app:layout_constraintTop_toTopOf="@id/tv_answerC" />
<TextView
android:id="@+id/tv_confirm_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="25dp"
android:text="@string/question_confirm_title"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="@id/tv_question"
app:layout_constraintTop_toBottomOf="@id/tv_answerD" />
<EditText
android:id="@+id/et_confirm_answer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/question_select"
android:maxLength="4"
android:singleLine="true"
android:text=""
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/tv_confirm_title"
app:layout_constraintTop_toTopOf="@id/tv_confirm_title" />
<Button
android:id="@+id/bt_previous"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:text="@string/question_previous"
app:layout_constraintEnd_toStartOf="@id/bt_confirm"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/et_confirm_answer" />
<Button
android:id="@+id/bt_confirm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/question_ok"
app:layout_constraintEnd_toStartOf="@id/bt_next"
app:layout_constraintStart_toEndOf="@id/bt_previous"
app:layout_constraintTop_toTopOf="@id/bt_previous" />
<Button
android:id="@+id/bt_next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/question_next"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/bt_confirm"
app:layout_constraintTop_toTopOf="@id/bt_previous" />
</androidx.constraintlayout.widget.ConstraintLayout>
接下來就是驗證下是否按照我們的預期正確顯示,不過在模擬器上實現橫屏好像比較麻煩,我這裏在我的手機上試試。
確保你的手機的usb調試模式已經開啓,使用數據線連接,設置成功後,Andorid Studio能夠檢測到活動的設備。
運行程序,沒問題。。
寫在測試之後,記得把自動旋轉打開,我這看了半天沒反應結果卻是這個問題。。。。
上面提到過我們在橫屏和豎屏之間切換的時候,系統會銷燬原來的Activity重新創建Activity的實例,也就意味着我們的應用在切換後會返回到第一個頁面,而且如果用戶有什麼輸入之類的,這些數據也不會被系統保存。那麼有什麼辦法能夠在Activity銷燬之前保存我們需要的數據呢?
onSaveInstanceState() 保存簡單輕量的界面狀態
Activity 開始停止時,系統會調用 onSaveInstanceState() 方法,以便 Activity 可以將狀態信息保存到實例狀態 Bundle 中。此方法的默認實現保存有關 Activity 視圖層次結構狀態的瞬態信息。
要保存 Activity 的其他實例狀態信息,需要重寫 onSaveInstanceState()方法,並將鍵值對形式的數據添加到Activity 意外銷燬時所保存的 Bundle 對象中。重寫onSaveInstanceState() 時,如果要默認實現保存視圖層次結構的狀態,則必須調用父類實現。
對於我們的程序來說,我們需要保存:mCurrentQuestion即試題索引
首先重寫該方法:
/**
* 保存用戶輸入以及試題索引
*
* @param outState
*/
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(QUESTION_INDEX, mCurrentQuestion);
}
常量定義如下:
private static final String QUESTION_INDEX = "QUESTION_INDEX";
無論系統是新建 Activity 實例還是重新創建之前的實例,都會調用 onCreate() 方法,所以在嘗試讀取之前,您必須檢查狀態 Bundle 是否爲 null。如果爲 null,系統將新建 Activity 實例,而不會恢復之前銷燬的實例。
那麼我們需要做的就是在onCreate() 方法中恢復這些數據:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 恢復數據
if(savedInstanceState != null){
mCurrentQuestion = savedInstanceState.getInt(QUESTION_INDEX,0);
}
........
解釋下:int getInt (String key, int defaultValue)
,第一個參數爲key,第二個參數意味着沒有對應的key的值時候返回的默認值。
那麼接下來就該驗證下是否能夠正確恢復數據了:
當用戶顯式關閉
Activity 時,或者在其他情況下調用 finish() 時,系統不會調用 onSaveInstanceState()。
如要保存持久化數據(例如用戶首選項或數據庫中的數據),您應在 Activity 位於前臺時抓住合適機會。如果沒有這樣的時機,應在執行 onStop()
方法期間保存此類數據。
下一篇我們對該程序的界面進行升級。