Android入門-第二章-探究活動

  • 2020 年 2 月 19 日
  • 筆記

一、引出

 通過上一章的學習,你已經成功創建了你的第一個Android項目。不過僅僅滿足於此顯然是不夠的,是時候學點新的東西了。作為你的導師,我有義務幫你制定好後面的學習路線,那麼今天我們應該從哪兒入手呢?現在你可以想像一下,假如你已經寫出了一個非常優秀的應用程式,然後推薦給你的第一個用戶,你會從哪裡開始介紹呢?毫無疑問,當然是從介面開始介紹了!因為即使你的程式演算法再高效,架構再出色,用戶根本不會在乎這些,他們一開始只會對看得到的東西感興趣,那麼我們今天的主題自然也要從看得到的入手了。

二、活動的定義

 活動(Activity)是最容易吸引用戶的地方,它是一種可以包含用戶介面的組件,主要用於和用戶進行交互。一個應用程式中可以包含零個或多個活動,但不包含任何活動的應用程式很少見,誰也不想讓自己的應用永遠無法被用戶看到吧?

 活動代表了一個具有用戶介面的單一螢幕,如 Java 的窗口或者幀。Android 的活動是ContextThemeWrapper 類的子類。

 如果你曾經用 C,C++ 或者 Java 語言編程,你應該知道這些程式從 main() 函數開始。很類似的,Android 系統初始化它的程式是通過活動中的 onCreate()回調的調用開始的。存在有一序列的回調方法來啟動一個活動,同時有一序列的方法來關閉活動,如下面的活動聲明周期圖所示:

2.2.1 手動創建活動

  1. 右擊com.example.activitytest包→New→Activity→Empty Activity,會彈出一個創建活動的對話框
  2. 勾選Generate Layout File表示會自動為FirstActivity創建一個對應的布局文件
  3. 勾選Launcher Activity表示會自動將FirstActivity設置為當前項目的主活動

 這裡由於你是第一次手動創建活動,這些自動生成的東西暫時都不要勾選,下面我們將會一個個手動來完成。勾選Backwards Compatibility表示會為項目啟用向下兼容的模式,這個選項要勾上。點擊Finish完成創建。

​ 你需要知道,項目中的任何活動都應該重寫Activity的onCreate()方法,而目前我們的FirstActivity中已經重寫了這個方法,這是由Android Studio自動幫我們完成的,程式碼如下所示:

package com.example.activitytest;    import androidx.appcompat.app.AppCompatActivity;    import android.os.Bundle;    public class FirstActivity extends AppCompatActivity {        @Override      protected void onCreate(Bundle savedInstanceState) {          super.onCreate(savedInstanceState);      }  }

 可以看到,onCreate()方法非常簡單,就是調用了父類的onCreate()方法。當然這只是默認的實現,後面我們還需要在裡面加入很多自己的邏輯。

2.2.2 創建和載入布局

 前面我們說過,Android程式的設計講究邏輯和視圖分離,最好每一個活動都能對應一個布局,布局就是用來顯示介面內容的,因此我們現在就來手動創建一個布局文件。創建的步驟依次為:

右擊app/src/main/res目錄→New→Directory,然後會出現下面的空白文件夾:

再對此文件夾右擊:new—>XML—>Layout XML File:

接著就會下圖所示的布局編輯器:

 這是Android Studio為我們提供的可視化布局編輯器,你可以在螢幕的中央區域預覽當前的布局。在窗口的最下方有兩個切換卡,左邊是Design,右邊是Text。

 Design:當前的可視化布局編輯器,在這裡你不僅可以預覽當前的布局,還可以通過拖放的方式編輯布局。

 Text:通過XML文件的方式來編輯布局的,現在點擊一下Text切換卡,可以看到如下程式碼:

<?xml version="1.0" encoding="utf-8"?>  <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"      android:layout_width="match_parent"      android:layout_height="match_parent">    </LinearLayout>

 由於我們剛才在創建布局文件時選擇了LinearLayout作為根元素,因此現在布局文件中已經有一個LinearLayout元素了。那我們現在對這個布局稍做編輯,添加一個按鈕,如下所示:

<?xml version="1.0" encoding="utf-8"?>  <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"      android:layout_width="match_parent"      android:layout_height="match_parent">        <Button          android:id="@+id/button_1"//定義一個android:id,是當前元素的唯一標識符          android:layout_width="match_parent"//指定了當前元素的寬度          android:layout_height="wrap_content"//指定了當前元素的高度,          android:text="Button 1" /> //指定了元素中顯示的文字內容  </LinearLayout>

 當然,再可視化布局編輯器中對應也會多出來按鈕。具體的按鈕的屬性既可以再XML文本文件中讀出,也可以通過可視化布局編輯器中選中按鈕後點擊Attributes來進行屬性的查看:

 這裡添加了一個Button元素,並在Button元素的內部增加了幾個屬性。android:id 是給當前的元素定義一個唯一標識符,之後可以在程式碼中對這個元素進行操作。

 在XML文檔中定義一個id所使用的語法:

@+id/id_name

 在XML文檔中引用一個id所使用的語法:

@id/id_name

寬度與高度的值說明:

match_parent:表示讓當前元素和父元素一樣寬或長;

wrap_content:表示當前元素的高度或寬度只要能剛好包含裡面的內容就行;

在布局文件的XML文本編輯模式下,點擊Preview可以預覽一下當前布局,如下圖所示:

 可以看到,按鈕已經成功顯示出來了,這樣一個簡單的布局就編寫完成了。那麼接下來我們要做的,就是在活動中載入這個布局。

 重新回到FirstActivity,在onCreate()方法中加入如下程式碼:

package com.example.activitytest;    import androidx.appcompat.app.AppCompatActivity;    import android.os.Bundle;    public class FirstActivity extends AppCompatActivity {        @Override      protected void onCreate(Bundle savedInstanceState) {          super.onCreate(savedInstanceState);          setContentView(R.layout.my_firstlayout);      }  }

 可以看到,這裡調用了setContentView()方法來給當前的活動載入一個布局,而在**setContentView()方法中,我們一般都會傳入一個布局文件的id** 。在第1章介紹項目資源的時候我曾提到過,項目中添加的任何資源都會在R文件中生成一個相應的資源id,因此我們剛才創建的first_layout.xml 布局的id現在應該是已經添加到R文件中了。在程式碼中去引用布局文件的方法你也已經學過了,只需要調用R.layout.my_firstlayout就可以得到first_layout.xml 布局的id ,然後將這個值傳入setContentView() 方法即可。

創建布局的步驟小結:

 新建布局文件夾layout -> 文件夾內新建布局文件XML -> 增加元素(比如說按鈕)-> 活動中載入布局文件(即:在活動的onCreate方法中載入布局文件,調用setContentView方法)

2.2.3 活動在AndroidManifest文件中註冊

註冊活動到活動能夠運行有兩個步驟:

  1. 註冊活動
  2. 為程式配置主活動(如果沒有主活動,其作用是為第三方調用)

 別忘了在上一章我們學過,所有的活動都要在AndroidManifest.xml中進行註冊才能生效,而實際上FirstActivity已經在AndroidManifest.xml中註冊過了,我們打開app/src/main/AndroidManifest.xml文件瞧一瞧,程式碼如下所示:

<?xml version="1.0" encoding="utf-8"?>  <manifest xmlns:android="http://schemas.android.com/apk/res/android"      package="com.example.activitytest">        <application          android:allowBackup="true"          android:icon="@mipmap/ic_launcher"          android:label="@string/app_name"          android:roundIcon="@mipmap/ic_launcher_round"          android:supportsRtl="true"          android:theme="@style/AppTheme">          <activity android:name=".FirstActivity"></activity>//註冊指定名稱的活動      </application>    </manifest>

 可以看到,活動的註冊聲明要放在 標籤內,這裡是通過 標籤來對活動進行註冊的。那麼又是誰幫我們自動完成了對FirstActivity的註冊呢?當然是Android Studio了,之前在使用Eclipse創建活動或其他系統組件時,很多人都會忘記要去Android Manifest.xml中註冊一下,從而導致程式運行崩潰,很顯然Android Studio在這方面做得更加人性化。

 在 標籤中我們使用了android:name來指定具體註冊哪一個活動,那麼這裡填入的.FirstActivity是什麼意思呢?其實這不過就是com.example.activitytest.FirstActivity 的縮寫而已。由於在最外層的 標籤中已經通過package 屬性指定了程式的包名是com.example.activitytest ,因此在註冊活動時這一部分就可以省略了,直接使用.FirstActivity 就足夠了。

 不過,僅僅是這樣註冊了活動,我們的程式仍然是不能運行的,因為還沒有為程式配置主活動,也就是說,當程式運行起來的時候,不知道要首先啟動哪個活動。

配置主活動的方法:

  1. 在 標籤的內部加入 標籤
  2. 並在 標籤內添加 和 這兩句聲明即可。

即加入如下語句:

            <intent-filter>                  <action android:name="android.intent.action.MAIN" />                  <category android:name="android.intent.category.LAUNCHER" />              </intent-filter>

 除此之外,我們還可以使用android:label 指定活動中標題欄的內容,標題欄是顯示在活動最頂部的,待會兒運行的時候你就會看到。需要注意的是,給主活動指定的label不僅會成為標題欄中的內容,還會成為啟動器(Launcher)中應用程式顯示的名稱。

 修改後的AndroidManifest.xml文件,程式碼如下所示:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"      package="com.example.activitytest">      <application          ... >          <activity android:name=".FirstActivity"              android:label="This is FirstActivity">              <intent-filter>                  <action android:name="android.intent.action.MAIN" />                  <category android:name="android.intent.category.LAUNCHER" />              </intent-filter>          </activity>      </application>  </manifest>

 這樣的話,FirstActivity就成為我們這個程式的主活動了,即點擊桌面應用程式圖標時首先打開的就是這個活動。另外需要注意,如果你的應用程式中沒有聲明任何一個活動作為主活動,這個程式仍然是可以正常安裝的,只是你無法在啟動器中看到或者打開這個程式。這種程式一般都是作為第三方服務供其他應用在內部進行調用的,如支付寶快捷支付服務。

 好了,現在一切都已準備就緒,讓我們來運行一下程式吧,結果如圖2.7所示。

 在介面的最頂部是一個標題欄,裡面顯示著我們剛才在註冊活動時指定的內容。標題欄的下面就是在布局文件first_layout.xml中編寫的介面,可以看到我們剛剛定義的按鈕。現在你已經成功掌握了手動創建活動的方法,下面讓我們繼續看一看你在活動中還能做哪些事情吧。

活動創建的步驟小結:

我們以手動創建活動為例:

創建一個空的活動 -> 創建布局 -> 活動中載入布局 -> 註冊活動 -> 配置程式配置主活動

2.2.4 在活動中使用Toast

 Toast是Android系統提供的一種非常好的提醒方式,在程式中可以使用它將一些短小的資訊通知給用戶,這些資訊會在一段時間後自動消失,並且不會佔用任何螢幕空間,我們現在就嘗試一下如何在活動中使用Toast。

 首先需要定義一個彈出Toast的觸發點,正好介面上有個按鈕,那我們就讓點擊這個按鈕的時候彈出一個Toast吧。在onCreate() 方法中添加如下程式碼:

protected void onCreate(Bundle savedInstanceState) {      super.onCreate(savedInstanceState);      setContentView(R.layout.first_layout);      Button button1 = (Button) findViewById(R.id.button_1);//通過id得到按鈕實例對象      button1.setOnClickListener(new View.OnClickListener() {//匿名內部類。          @Override          public void onClick(View v) {              Toast.makeText(FirstActivity.this, "You clicked Button 1",                  Toast.LENGTH_SHORT).show();          }      });  }

 在活動中,可以通過findViewById()方法獲取到在布局文件中定義的元素,這裡我們傳入R.id.button_1 ,來得到按鈕的實例,這個值是剛才在first_layout.xml中通過android:id屬性指定的。findViewById()方法返回的是一個View 對象,我們需要向下轉型將它轉成Button對象。得到按鈕的實例之後,我們通過調用setOnClickListener()方法為按鈕註冊一個監聽器,點擊按鈕時就會執行監聽器中的onClick()方法。因此,彈出Toast的功能當然是要在onClick()方法中編寫了。

 Toast的用法非常簡單,通過靜態方法makeText()創建出一個Toast 對象,然後調用show()將Toast顯示出來就可以了。makeText()方法需要傳入3個參數:

  1. 第一個參數是Context ,也就是Toast要求的上下文,由於活動本身就是一個Context 對象,因此這裡直接傳入FirstActivity.this 即可。
  2. 第二個參數是Toast顯示的文本內容
  3. 第三個參數是Toast顯示的時長,有兩個內置常量可以選擇Toast.LENGTH_SHORT 和Toast.LENGTH_LONG 。

Toast機制的步驟小結:

Toast的靜態方法,比如說:makeText顯示於螢幕上相關問題提示,實際上很多地方都能調用此方法,所以說具體要說一個調用步驟實際上難說的。其代表的是一個行為,而且一般是將其放在做出響應的方式程式碼塊中的。

2.2.5 在活動中使用Menu

 手機畢竟和電腦不同,它的螢幕空間非常有限,因此充分地利用螢幕空間在手機介面設計中就顯得非常重要了。如果你的活動中有大量的菜單需要顯示,這個時候介面設計就會比較尷尬,因為僅這些菜單就可能佔用螢幕將近三分之一的空間,這該怎麼辦呢?不用擔心,Android給我們提供了一種方式,可以讓菜單都能得到展示的同時,還能不佔用任何螢幕空間。

 首先在res目錄下新建一個menu文件夾,右擊res目錄→New→Directory,輸入文件夾名menu,點擊OK。接著在這個文件夾下再新建一個名叫main的菜單文件,右擊menu文件夾→New→Menu resource file

<menu xmlns:android="http://schemas.android.com/apk/res/android">      <item          android:id="@+id/add_item"//指定唯一標識符          android:title="Add"/>     //給菜單項指定一個名稱      <item          android:id="@+id/remove_item"          android:title="Remove"/>  </menu>

 這裡我們創建了兩個菜單項,其中 標籤就是用來創建具體的某一個菜單項,然後通過android:id給這個菜單項指定一個唯一的標識符,通過android:title給這個菜單項指定一個名稱。

 接著重新回到FirstActivity中來重寫onCreateOptionsMenu()方法,重寫方法可以使用Ctrl + O快捷鍵。重寫的程式碼如下:

    @Override      public boolean onCreateOptionsMenu(Menu menu) {          getMenuInflater().inflate(R.menu.main, menu);          return true;      }

 通過getMenuInflater()方法能夠得到MenuInflater對象,再調用它的inflate()方法就可以給當前活動創建菜單了。inflate()方法接收兩個參數:

  1. 第一個參數用於指定我們通過哪一個資源文件來創建菜單,這裡當然傳入R.menu.main
  2. 第二個參數用於指定我們的菜單項將添加到哪一個Menu 對象當中,這裡直接使用onCreateOptionsMenu()方法中傳入的menu參數。

方法返回值的含義:

  1. 返回true,表示允許創建的菜單顯示出來
  2. 返回了false ,創建的菜單將無法顯示。

 當然,僅僅讓菜單顯示出來是不夠的,我們定義菜單不僅是為了看的,關鍵是要菜單真正可用才行,因此還要再定義菜單響應事件。在FirstActivity中重寫onOptionsItemSelected()方法:

public boolean onOptionsItemSelected(MenuItem item) {      switch (item.getItemId()) {      case R.id.add_item:          Toast.makeText(this, "You clicked Add", Toast.LENGTH_SHORT).show();          break;      case R.id.remove_item:          Toast.makeText(this, "You clicked Remove", Toast.LENGTH_SHORT).show();          break;      default:      }      return true;  }

 在onOptionsItemSelected()方法中,通過調用item.getItemId()來判斷我們點擊的是哪一個菜單項,然後給每個菜單項加入自己的邏輯處理,這裡我們就活學活用,彈出一個剛剛學會的Toast。

 重新運行程式,你會發現在標題欄的右側多了一個三點的符號,這個就是菜單按鈕了,如下圖所示。

 可以看到,菜單里的菜單項默認是不會顯示出來的,只有點擊一下菜單按鈕才會彈出裡面具體的內容,因此它不會佔用任何活動的空間。然後如果你點擊了Add菜單項就會彈出You clicked Add提示,如果點擊了Remove菜單項就會彈出You clicked Remove提示。

創建一個菜單的步驟小結:

在res中創建一個menu文件夾 -> 在文件夾中新建一個Menu resource file XML文件-> 在XML文件中創建菜單的相關元素 -> 活動中重寫顯示菜單的方法(onCreateOptionsMenu,其獨立於onCreate方法) -> 活動中重寫菜單響應時間的方法(onOptionsItemSelected),其仍然是獨立於onCreate方法。

菜單創建和按鈕創建的不同:

  1. 菜單的創建不放置於布局文件中,而是獨立於布局文件;按鈕的創建則是反之。
  2. 菜單的響應方法不寫於onCreate方法中,而是獨立於onCreate方法;按鈕的創建則是反之。

2.2.6 銷毀一個活動

 通過上一節的學習,你已經掌握了手動創建活動的方法,並學會了如何在活動中創建Toast和創建菜單。或許你現在心中會有個疑惑,如何銷毀一個活動呢?

 其實答案非常簡單,只要按一下Back鍵就可以銷毀當前的活動了。不過如果你不想通過按鍵的方式,而是希望在程式中通過程式碼來銷毀活動,當然也可以,Activity類提供了一個finish() 方法,我們在活動中調用一下這個方法就可以銷毀當前活動了。

 修改按鈕監聽器中的程式碼,如下所示:

button1.setOnClickListener(new View.OnClickListener() {      @Override      public void onClick(View v) {          finish();      }  });

 重新運行程式,這時點擊一下按鈕,當前的活動就被成功銷毀了,效果和按下Back鍵是一樣的。

三、使用Intent在活動之間穿梭

 只有一個活動的應用也太簡單了吧?沒錯,你的追求應該更高一點。不管你想創建多少個活動,方法都和上一節中介紹的是一樣的。唯一的問題在於,你在啟動器中點擊應用的圖標只會進入到該應用的主活動,那麼怎樣才能由主活動跳轉到其他活動呢?我們現在就來一起看一看。

2.3.1 使用顯式Intent

 你應該已經對創建活動的流程比較熟悉了,那我們現在快速地在ActivityTest項目中再創建一個活動。

 仍然還是右擊com.example.activitytest包→New→Activity→Empty Activity,會彈出一個創建活動的對話框,我們這次將活動命名為SecondActivity,並勾選Generate Layout File,給布局文件起名為second_layout,但不要勾選Launcher Activity選項。點擊Finish完成創建,Android Studio會為我們自動生成SecondActivity.java和second_layout.xml這兩個文件。不過自動生成的布局程式碼目前對你來說可能有些複雜,這裡我們仍然還是使用最熟悉的LinearLayout,編輯second_layout.xml,將裡面的程式碼替換成如下內容:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"      android:orientation="vertical"      android:layout_width="match_parent"      android:layout_height="match_parent">        <Button          android:id="@+id/button_2"          android:layout_width="match_parent"          android:layout_height="wrap_content"          android:text="Button 2"          />    </LinearLayout>

我們還是定義了一個按鈕,按鈕上顯示Button 2。

然後SecondActivity中的程式碼已經自動生成了一部分,我們保持默認不變就好,如下所示:

package com.example.myfirstactivity;    import androidx.appcompat.app.AppCompatActivity;    import android.os.Bundle;    public class SecondActivity extends AppCompatActivity {        @Override      protected void onCreate(Bundle savedInstanceState) {          super.onCreate(savedInstanceState);          setContentView(R.layout.second_layout);      }  }

另外不要忘記,任何一個活動都是需要在AndroidManifest.xml中註冊的,不過幸運的是,Android Studio已經幫我們自動完成了,你可以打開AndroidManifest.xml瞧一瞧:

<?xml version="1.0" encoding="utf-8"?>  <manifest xmlns:android="http://schemas.android.com/apk/res/android"      package="com.example.myfirstactivity">        <application          android:allowBackup="true"          android:icon="@mipmap/ic_launcher"          android:label="@string/app_name"          android:roundIcon="@mipmap/ic_launcher_round"          android:supportsRtl="true"          android:theme="@style/AppTheme">          <activity android:name=".SecondActivity"></activity>          <activity android:name=".FirstActivity">              <intent-filter>                  <action android:name="android.intent.action.MAIN" />                    <category android:name="android.intent.category.LAUNCHER" />              </intent-filter>          </activity>      </application>    </manifest>

由於SecondActivity不是主活動,因此不需要配置 標籤里的內容,註冊活動的程式碼也簡單了許多。現在第二個活動已經創建完成,剩下的問題就是如何去啟動這第二個活動了,這裡我們需要引入一個新的概念:Intent。

Intent的相關定義和概念:

  1. Android程式中各組件之間進行交互的一種重要方式
  2. 它不僅可以指明當前組件想要執行的動作
  3. 還可以在不同組件之間傳遞數據。

 Intent一般可被用於啟動活動、啟動服務以及發送廣播等場景,由於服務、廣播等概念你暫時還未涉及,那麼本章我們的目光無疑就鎖定在了啟動活動上面

 Intent大致可以分為兩種:顯式Intent 和隱式Intent ,我們先來看一下顯式Intent如何使用。

 Intent有多個構造函數的重載,其中一個是Intent(Context packageContext, Class<?> cls) 。這個構造函數接收兩個參數:

  1. 第一個參數Context 要求提供一個啟動活動的上下文
  2. 第二個參數Class 則是指定想要啟動的目標活動,通過這個構造函數就可以構建出Intent 的「意圖」。

 然後我們應該怎麼使用這個Intent呢?Activity類中提供了一個startActivity() 方法,這個方法是專門用於啟動活動的,它接收一個Intent 參數,這裡我們將構建好的Intent傳入startActivity() 方法就可以啟動目標活動了。修改FirstActivity中按鈕的點擊事件,程式碼如下所示:

button1.setOnClickListener(new View.OnClickListener() {      @Override      public void onClick(View v) {          Intent intent = new Intent(FirstActivity.this, SecondActivity.class);          startActivity(intent);      }  });

 我們首先構建出了一個Intent,傳入FirstActivity.this 作為上下文,傳入SecondActivity.class 作為目標活動,這樣我們的「意圖」就非常明顯了,即在FirstActivity這個活動的基礎上打開SecondActivity這個活動。然後通過startActivity() 方法來執行這個Intent。

 重新運行程式,在FirstActivity的介面點擊一下按鈕。可以看到,我們已經成功啟動SecondActivity這個活動了。如果你想要回到上一個活動怎麼辦呢?很簡單,按下Back鍵就可以銷毀當前活動,從而回到上一個活動了。使用這種方式來啟動活動,Intent的「意圖」非常明顯,因此我們稱之為顯式Intent 。

2.3.2 使用隱式Intent

 相比於顯式Intent,隱式Intent則含蓄了許多,它並不明確指出我們想要啟動哪一個活動,而是指定了一系列更為抽象的action 和category 等資訊,然後交由系統去分析這個Intent,並幫我們找出合適的活動去啟動。

 什麼叫作合適的活動呢?簡單來說就是可以響應我們這個隱式Intent的活動,那麼目前SecondActivity可以響應什麼樣的隱式Intent呢?額,現在好像還什麼都響應不了,不過很快就會有了。

 通過在 標籤下配置 的內容,可以指定當前活動能夠響應的action 和category ,打開AndroidManifest.xml,添加如下程式碼:

<activity android:name=".SecondActivity" >      <intent-filter>          <action android:name="com.example.activitytest.ACTION_START" />          <category android:name="android.intent.category.DEFAULT" />      </intent-filter>  </activity>

 在action標籤中我們指明了當前活動可以響應com.example.activitytest.ACTION_START這個action ,而category標籤則包含了一些附加資訊,更精確地指明了當前的活動能夠響應的Intent中還可能帶有的category 。只有action 和category中的內容同時能夠匹配上Intent中指定的action 和category 時,這個活動才能響應該Intent。

修改FirstActivity中按鈕的點擊事件,程式碼如下所示:

button1.setOnClickListener(new View.OnClickListener() {      @Override      public void onClick(View v) {          Intent intent = new Intent("com.example.activitytest.ACTION_START");          startActivity(intent);      }  });

 可以看到,我們使用了Intent的另一個構造函數,直接將action 的字元串傳了進去,表明我們想要啟動能夠響應com.example.activitytest.ACTION_START 這個action 的活動。那前面不是說要action 和category 同時匹配上才能響應的嗎?怎麼沒看到哪裡有指定category 呢?這是因為android.intent.category.DEFAULT 是一種默認的category ,在調用startActivity()方法的時候會自動將這個category 添加到Intent中。

 重新運行程式,在FirstActivity的介面點擊一下按鈕,你同樣成功啟動SecondActivity了。不同的是,這次你是使用了隱式Intent的方式來啟動的,說明我們在activity標籤下配置的action 和category 的內容已經生效了!

 每個Intent中只能指定一個action ,但卻能指定多個category 。目前我們的Intent中只有一個默認的category ,那麼現在再來增加一個吧。

 修改FirstActivity中按鈕的點擊事件,程式碼如下所示:

button1.setOnClickListener(new View.OnClickListener() {      @Override      public void onClick(View v) {          Intent intent = new Intent("com.example.activitytest.ACTION_START");          intent.addCategory("com.example.activitytest.MY_CATEGORY");          startActivity(intent);      }  });

 可以調用Intent中的addCategory() 方法來添加一個category ,這裡我們指定了一個自定義的category ,值為com.example.activitytest.MY_CATEGORY 。

 現在重新運行程式,在FirstActivity的介面點擊一下按鈕,你會發現,程式崩潰了!這是你第一次遇到程式崩潰,可能會有些束手無策。別緊張,其實大多數的崩潰問題都是很好解決的,只要你善於分析。在logcat介面查看錯誤日誌,你會看到如圖2.16所示的錯誤資訊。

 錯誤資訊中提醒我們,沒有任何一個活動可以響應我們的Intent,為什麼呢?這是因為我們剛剛在Intent中新增了一個category ,而SecondActivity的intent-filter標籤中並沒有聲明可以響應這個category ,所以就出現了沒有任何活動可以響應該Intent的情況。現在我們在intent-filter 中再添加一個category 的聲明,如下所示:

<activity android:name=".SecondActivity" >      <intent-filter>          <action android:name="com.example.activitytest.ACTION_START" />          <category android:name="android.intent.category.DEFAULT" />          <category android:name="com.example.activitytest.MY_CATEGORY"/>      </intent-filter>  </activity>

再次重新運行程式,你就會發現一切都正常了。

2.3.3 更多隱式Intent的用法

 上一節中,你掌握了通過隱式Intent來啟動活動的方法,但實際上隱式Intent還有更多的內容需要你去了解,本節我們就來展開介紹一下。

 使用隱式Intent,我們不僅可以啟動自己程式內的活動,還可以啟動其他程式的活動,這使得Android多個應用程式之間的功能共享成為了可能。比如說你的應用程式中需要展示一個網頁,這時你沒有必要自己去實現一個瀏覽器(事實上也不太可能),而是只需要調用系統的瀏覽器來打開這個網頁就行了。

 修改FirstActivity中按鈕點擊事件的程式碼,如下所示:

button1.setOnClickListener(new View.OnClickListener() {      @Override      public void onClick(View v) {          Intent intent = new Intent(Intent.ACTION_VIEW);          intent.setData(Uri.parse("http://www.baidu.com"));          startActivity(intent);      }  });

 這裡我們首先指定了Intent的action 是Intent.ACTION_VIEW,這是一個Android系統內置的動作,其常量值為android.intent.action.VIEW。然後通過Uri.parse()方法,將一個網址字元串解析成一個Uri 對象,再調用Intent的setData()方法將這個Uri 對象傳遞進去。

 重新運行程式,在FirstActivity介面點擊按鈕就可以看到打開了系統瀏覽器,如下圖示。

 在上述程式碼中,可能你會對setData()部分感覺到陌生,這是我們前面沒有講到的。這個方法其實並不複雜,它接收一個Uri 對象,主要用於指定當前Intent正在操作的數據,而這些數據通常都是以字元串的形式傳入到Uri.parse()方法中解析產生的。

 與此對應,我們還可以在intent-filter標籤中再配置一個data標籤,用於更精確地指定當前活動能夠響應什麼類型的數據。data標籤中主要可以配置以下內容:其含義是當前別的軟體點擊一個按鈕之類觸發第三方軟體響應時,如果當前活動有這個標籤,那麼就能夠響應其他軟體的觸發,這樣一來就做到軟體的切換了。比如說我在自己定義的活動中加入:<data android:scheme="http" />,那麼當其他軟體點擊了一個網站觸發,那麼就會詢問是否使用自己之前所定義的活動。

android:scheme 。用於指定數據的協議部分,如上例中的http部分。

android:host 。用於指定數據的主機名部分,如上例中的www.baidu.com部分。

android:port 。用於指定數據的埠部分,一般緊隨在主機名之後。

android:path 。用於指定主機名和埠之後的部分,如一段網址中跟在域名之後的內容。

android:mimeType 。用於指定可以處理的數據類型,允許使用通配符的方式進行指定。

 只有data標籤中指定的內容和Intent中攜帶的Data完全一致時,當前活動才能夠響應該Intent。不過一般在data標籤中都不會指定過多的內容,如上面瀏覽器示例中,其實只需要指定android:scheme 為http,就可以響應所有的http協議的Intent了。

 為了讓你能夠更加直觀地理解,我們來自己建立一個活動,讓它也能響應打開網頁的Intent。右擊com.example.activitytest包→New→Activity→Empty Activity,新建ThirdActivity,並勾選Generate Layout File,給布局文件起名為third_layout,點擊Finish完成創建。然後編輯third_layout.xml,將裡面的程式碼替換成如下內容:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"      android:orientation="vertical"      android:layout_width="match_parent"      android:layout_height="match_parent">        <Button          android:id="@+id/button_3"          android:layout_width="match_parent"          android:layout_height="wrap_content"          android:text="Button 3"          />    </LinearLayout>

ThirdActivity中的程式碼保持不變就可以了,最後在AndroidManifest.xml中修改ThirdActivity的註冊資訊:

<activity android:name=".ThirdActivity">      <intent-filter>          <action android:name="android.intent.action.VIEW" />          <category android:name="android.intent.category.DEFAULT" />          <category android:name="android.intent.category.BROWSABLE"/>//注意書上程式碼沒有此行,不增加此句會出錯;            <data android:scheme="http" />      </intent-filter>  </activity>

 我們在ThirdActivity的intent-filter中配置了當前活動能夠響應的action 是Intent.ACTION_VIEW 的常量值,而category 則毫無疑問指定了默認的category 值,另外在data標籤中我們通過android:scheme 指定了數據的協議必須是http協議,這樣ThirdActivity應該就和瀏覽器一樣,能夠響應一個打開網頁的Intent了。讓我們運行一下程式試試吧,在FirstActivity的介面點擊一下按鈕,結果如圖2.18所示。

 可以看到,系統自動彈出了一個列表,顯示了目前能夠響應這個Intent的所有程式。選擇Browser還會像之前一樣打開瀏覽器,並顯示百度的主頁,而如果選擇了ActivityTest,則會啟動ThirdActivity。需要注意的是,雖然我們聲明了ThirdActivity是可以響應打開網頁的Intent的,但實際上這個活動並沒有載入並顯示網頁的功能,所以在真正的項目中盡量不要出現這種有可能誤導用戶的行為,不然會讓用戶對我們的應用產生負面的印象。

 除了http協議外,我們還可以指定很多其他協議,比如geo表示顯示地理位置、tel表示撥打電話。下面的程式碼展示了如何在我們的程式中調用系統撥號介面。

button1.setOnClickListener(new View.OnClickListener() {      @Override      public void onClick(View v) {          Intent intent = new Intent(Intent.ACTION_DIAL);          intent.setData(Uri.parse("tel:10086"));          startActivity(intent);      }  });

 首先指定了Intent的action 是Intent.ACTION_DIAL ,這又是一個Android系統的內置動作。然後在data部分指定了協議是tel,號碼是10086。重新運行一下程式,在FirstActivity的介面點擊一下按鈕,結果如圖2.19所示。

2.3.4 向下一個活動傳遞數據

 經過前面幾節的學習,你已經對Intent有了一定的了解。不過到目前為止,我們都只是簡單地使用Intent來啟動一個活動,其實Intent還可以在啟動活動的時候傳遞數據,下面我們來一起看一下。

 在啟動活動時傳遞數據的思路很簡單,Intent中提供了一系列putExtra()方法的重載,可以把我們想要傳遞的數據暫存在Intent中,啟動了另一個活動後,只需要把這些數據再從Intent中取出就可以了。比如說FirstActivity中有一個字元串,現在想把這個字元串傳遞到SecondActivity中,你就可以這樣編寫:

button1.setOnClickListener(new View.OnClickListener() {      @Override      public void onClick(View v) {          String data = "Hello SecondActivity";//創建一個字元串          Intent intent = new Intent(FirstActivity.this, SecondActivity.class);//創建一個Intent對象,並且說明了源與目的地          intent.putExtra("extra_data", data);//輸入鍵值對          startActivity(intent);//使這個Intent對象運行      }  });

 這裡我們還是使用顯式Intent的方式來啟動SecondActivity,並通過putExtra()方法傳遞了一個字元串。注意這裡putExtra()方法接收兩個參數:

  1. 第一個參數是鍵,用於後面從Intent中取值
  2. 第二個參數才是真正要傳遞的數據。

 然後我們在SecondActivity中將傳遞的數據取出,並列印出來,程式碼如下所示:

public class SecondActivity extends AppCompatActivity {        @Override      protected void onCreate(Bundle savedInstanceState) {          super.onCreate(savedInstanceState);          setContentView(R.layout.second_layout);          Intent intent = getIntent();//獲取到用於啟動SecondActivity的Intent          String data = intent.getStringExtra("extra_data");//利用key得到value          Log.d("SecondActivity", data);//打出日誌      }    }

 首先可以通過getIntent()方法獲取到用於啟動SecondActivity的Intent,然後調用getStringExtra()方法,傳入相應的鍵值,就可以得到傳遞的數據了。這裡由於我們傳遞的是字元串,所以使用getStringExtra()方法來獲取傳遞的數據。如果傳遞的是整型數據,則使用getIntExtra()方法;如果傳遞的是布爾型數據,則使用getBooleanExtra()方法,以此類推。

 重新運行程式,在FirstActivity的介面點擊一下按鈕會跳轉到SecondActivity,查看logcat列印資訊,如圖2.20所示。

當然我們也可以選擇使用Toast的方式打出提示條,比如我們在上面的程式碼中加入:

Toast.makeText(SecondActivity.this, data, Toast.LENGTH_SHORT).show();語句,就可以得到以下程式運行情況:

2.3.5 返回數據給上一個活動

 既然可以傳遞數據給下一個活動,那麼能不能夠返回數據給上一個活動呢?答案是肯定的。不過不同的是,返回上一個活動只需要按一下Back鍵就可以了,並沒有一個用於啟動活動的Intent來傳遞數據。通過查閱文檔你會發現,Activity中還有一個startActivityForResult()方法也是用於啟動活動的,但這個方法期望在活動銷毀的時候能夠返回一個結果給上一個活動。毫無疑問,這就是我們所需要的。

startActivityForResult()方法接收兩個參數:

  1. 第一個參數還是Intent
  2. 第二個參數是請求碼,用於在之後的回調中判斷數據的來源。

我們還是來實戰一下,修改FirstActivity中按鈕的點擊事件,程式碼如下所示:

button1.setOnClickListener(new View.OnClickListener() {      @Override      public void onClick(View v) {          Intent intent = new Intent(FirstActivity.this, SecondActivity.class);          startActivityForResult(intent, 1);      }  });

 這裡我們使用了startActivityForResult()方法來啟動SecondActivity,請求碼只要是一個唯一值就可以了,這裡傳入了1。接下來我們在SecondActivity中給按鈕註冊點擊事件,並在點擊事件中添加返回數據的邏輯,程式碼如下所示:

public class SecondActivity extends AppCompatActivity {        @Override      protected void onCreate(Bundle savedInstanceState) {          super.onCreate(savedInstanceState);          setContentView(R.layout.second_layout);          Button button2 = (Button) findViewById(R.id.button_2);          button2.setOnClickListener(new View.OnClickListener() {              @Override              public void onClick(View v) {                  Intent intent = new Intent();                  intent.putExtra("data_return", "Hello FirstActivity");//存放進鍵值對                  setResult(RESULT_OK, intent);                  finish();              }          });      }    }

 可以看到,我們還是構建了一個Intent,只不過這個Intent僅僅是用於傳遞數據而已,它沒有指定任何的「意圖」。緊接著把要傳遞的數據存放在Intent中,然後調用了setResult()方法。這個方法非常重要,是專門用於向上一個活動返回數據的。setResult()方法接收兩個參數,第一個參數用於向上一個活動返回處理結果,一般只使用RESULT_OK 或RESULT_CANCELED 這兩個值,第二個參數則把帶有數據的Intent傳遞迴去,然後調用了finish()方法來銷毀當前活動。

 由於我們是使用startActivityForResult()方法來啟動SecondActivity的,在SecondActivity被銷毀之後會回調上一個活動的onActivityResult()方法,因此我們需要在FirstActivity中重寫這個方法來得到返回的數據,如下所示:

@Override  protected void onActivityResult(int requestCode, int resultCode, Intent data) {      switch (requestCode) {          case 1:              if (resultCode == RESULT_OK) {                  String returnedData = data.getStringExtra("data_return");                  Log.d("FirstActivity", returnedData);              }              break;          default:      }  }

onActivityResult()方法帶有三個參數,第一個參數requestCode ,即我們在啟動活動時傳入的請求碼。第二個參數resultCode ,即我們在返回數據時傳入的處理結果。第三個參數data ,即攜帶著返回數據的Intent。由於在一個活動中有可能調用startActivityForResult() 方法去啟動很多不同的活動,每一個活動返回的數據都會回調到onActivityResult()這個方法中,因此我們首先要做的就是通過檢查requestCode 的值來判斷數據來源。確定數據是從SecondActivity返回的之後,我們再通過resultCode 的值來判斷處理結果是否成功。最後從data 中取值並列印出來,這樣就完成了向上一個活動返回數據的工作。

 重新運行程式,在FirstActivity的介面點擊按鈕會打開SecondActivity,然後在SecondActivity介面點擊Button 2按鈕會回到FirstActivity,這時查看logcat的列印資訊,如圖2.21所示。

 可以看到,SecondActivity已經成功返回數據給FirstActivity了。

 這時候你可能會問,如果用戶在SecondActivity中並不是通過點擊按鈕,而是通過按下Back鍵回到FirstActivity,這樣數據不就沒法返回了嗎?沒錯,不過這種情況還是很好處理的,我們可以通過在SecondActivity中重寫onBackPressed()方法來解決這個問題,程式碼如下所示:

@Override  public void onBackPressed() {      Intent intent = new Intent();      intent.putExtra("data_return", "Hello FirstActivity");      setResult(RESULT_OK, intent);      finish();  }

 這樣的話,當用戶按下Back鍵,就會去執行onBackPressed()方法中的程式碼,我們在這裡添加返回數據的邏輯就行了。


四、活動的聲明周期

4.1 返回棧

 Android中的活動是可以層疊的。我們每啟動一個新的活動,就會覆蓋在原活動之上,然後點擊Back鍵會銷毀最上面的活動,下面的一個活動就會重新顯示出來。

 其實Android是使用任務(Task)來管理活動的,一個任務就是一組存放在棧里的活動的集合,這個棧也被稱作返回棧(Back Stack)。我們可以淺顯地認為一個任務的數據結構體現就是一個棧,不同的任務由不同的棧。棧是一種後進先出的數據結構,在默認情況下,每當我們啟動了一個新的活動,它會在返回棧中入棧,並處於棧頂的位置。而每當我們按下Back鍵或調用finish() 方法去銷毀一個活動時,處於棧頂的活動會出棧,這時前一個入棧的活動就會重新處於棧頂的位置。系統總是會顯示處於棧頂的活動給用戶。

 下圖展示了返回棧是如何管理活動入棧出棧操作的:

4.2 活動的狀態

每個活動在其生命周期中最多可能會有4種狀態:

  1. 運行狀態
  2. 暫停狀態
  3. 停止狀態
  4. 銷毀狀態

以下是活動的四個狀態的詳細解釋:

狀態名稱

詳細含義

運行狀態

當一個活動位於返回棧的棧頂時,這時活動就處於運行狀態。系統最不願意回收的就是處於運行狀態的活動,因為這會帶來非常差的用戶體驗。

暫停狀態

當一個活動不再處於棧頂位置,但仍然可見時,這時活動就進入了暫停狀態。你可能會覺得既然活動已經不在棧頂了,還怎麼會可見呢?這是因為並不是每一個活動都會佔滿整個螢幕的,比如對話框形式的活動只會佔用螢幕中間的部分區域,你很快就會在後面看到這種活動。處於暫停狀態的活動仍然是完全存活著的,系統也不願意去回收這種活動(因為它還是可見的,回收可見的東西都會在用戶體驗方面有不好的影響),只有在記憶體極低的情況下,系統才會去考慮回收這種活動。

停止狀態

當一個活動不再處於棧頂位置,並且完全不可見的時候,就進入了停止狀態。系統仍然會為這種活動保存相應的狀態和成員變數,但是這並不是完全可靠的,當其他地方需要記憶體時,處於停止狀態的活動有可能會被系統回收。

銷毀狀態

當一個活動從返回棧中移除後就變成了銷毀狀態。系統會最傾向於回收處於這種狀態的活動,從而保證手機的記憶體充足。

4.3 活動的生存期

 Activity類中定義了7個回調方法,覆蓋了活動生命周期的每一個環節,下面就來一一介紹這7個方法:

  1. onCreate():這個方法你已經看到過很多次了,每個活動中我們都重寫了這個方法,它會在活動第一次被創建的時候調用。你應該在這個方法中完成活動的初始化操作,比如說載入布局、綁定事件等。
  2. onStart():這個方法在活動由不可見變為可見的時候調用。
  3. onResume():這個方法在活動準備好和用戶進行交互的時候調用。此時的活動一定位於返回棧的棧頂,並且處於運行狀態。
  4. onPause():這個方法在系統準備去啟動或者恢復另一個活動的時候調用。我們通常會在這個方法中將一些消耗CPU的資源釋放掉,以及保存一些關鍵數據,但這個方法的執行速度一定要快,不然會影響到新的棧頂活動的使用。
  5. onStop():這個方法在活動完全不可見的時候調用。它和onPause() 方法的主要區別在於,如果啟動的新活動是一個對話框式的活動,那麼onPause() 方法會得到執行,而onStop() 方法並不會執行。
  6. onDestroy():這個方法在活動被銷毀之前調用,之後活動的狀態將變為銷毀狀態。
  7. onRestart():這個方法在活動由停止狀態變為運行狀態之前調用,也就是活動被重新啟動了。

活動的完整周期:

活動的初始化,比如布局、綁定事件:onCreate() -> 活動的轉為可見:onStart() -> 活動轉為可以與用戶進行交互:onResume() -> 活動轉為不可見並釋放相關資源:onPause() -> 活動釋放資源:onStop() -> 活動銷毀:onDestory()

體驗活動的生命周期:

 詳情還是看第一行程式碼比較好,但是主要思想是我們通過創建多個活動,通過按鈕進行任務之間的切換,查看日誌的內容進行觀察上圖中的方法調用情況:

 而最為關鍵的一步不是說我們認為的調用這些方法,而是重寫這些方法,重寫的原則是調用父類的方法,但是補充一個日誌輸出,代表此方法被執行了:

    @Override      protected void onStart() {          super.onStart();//調用父類          Log.d(TAG, "onStart");//輸出日誌內容      }

 經過我個人嘗試,發現由系統強制關閉掉應用會調用onDestory()方法,而如果讓一個按鈕觸發時執行:finish()方法,那麼就會調用onDestory方法。

五、活動的啟動模式

 活動的啟動模式對你來說應該是個全新的概念,在實際項目中我們應該根據特定的需求為每個活動指定恰當的啟動模式。啟動模式一共有4種,分別是:

  1. standard
  2. singleTop
  3. singleTask
  4. singleInstance

 可以在AndroidManifest.xml中通過給 標籤指定android:launchMode 屬性來選擇啟動模式。下面我們來逐個進行學習。

5.1 standard

 standard是活動默認的啟動模式,在不進行顯式指定的情況下,所有活動都會自動使用這種啟動模式。因此,到目前為止我們寫過的所有活動都是使用的standard模式。經過上一節的學習,你已經知道了Android是使用返回棧來管理活動的,在standard模式(即默認情況)下,每當啟動一個新的活動,它就會在返回棧中入棧,並處於棧頂的位置。對於使用standard模式的活動,系統不會在乎這個活動是否已經在返回棧中存在,每次啟動都會創建該活動的一個新的實例。

 我們現在通過實踐來體會一下standard模式,這次還是準備在ActivityTest項目的基礎上修改,首先關閉ActivityLifeCycleTest項目,打開ActivityTest項目。

修改FirstActivity中onCreate() 方法的程式碼,如下所示:

@Override  protected void onCreate(Bundle savedInstanceState) {      super.onCreate(savedInstanceState);      Log.d("FirstActivity", this.toString());      setContentView(R.layout.first_layout);      Button button1 = (Button) findViewById(R.id.button_1);      button1.setOnClickListener(new View.OnClickListener() {          @Override          public void onClick(View v) {              Intent intent = new Intent(FirstActivity.this, FirstActivity.class);              startActivity(intent);          }      });  }

5.2 singleTop

 可能在有些情況下,你會覺得standard模式不太合理。活動明明已經在棧頂了,為什麼再次啟動的時候還要創建一個新的活動實例呢?別著急,這只是系統默認的一種啟動模式而已,你完全可以根據自己的需要進行修改,比如說使用singleTop模式。當活動的啟動模式指定為singleTop,在啟動活動時如果發現返回棧的棧頂已經是該活動,則認為可以直接使用它,不會再創建新的活動實例。

 我們還是通過實踐來體會一下,修改AndroidManifest.xml中FirstActivity的啟動模式,如下所示:

<activity      android:name=".FirstActivity"      android:launchMode="singleTop"      android:label="This is FirstActivity">      <intent-filter>          <action android:name="android.intent.action.MAIN" />          <category android:name="android.intent.category.LAUNCHER" />      </intent-filter>  </activity>

 注意在重寫的onCreate方法中加入intent.setFlags(intent.FLAG_ACTIVITY_SINGLE_TOP);語句,確保其活動切換時的調用方式。

5.3singleTask

 使用singleTop模式可以很好地解決重複創建棧頂活動的問題,但是正如你在上一節所看到的,如果該活動並沒有處於棧頂的位置,還是可能會創建多個活動實例的。那麼有沒有什麼辦法可以讓某個活動在整個應用程式的上下文中只存在一個實例呢?這就要藉助singleTask模式來實現了。當活動的啟動模式指定為singleTask,每次啟動該活動時系統首先會在返回棧中檢查是否存在該活動的實例,如果發現已經存在則直接使用該實例,並把在這個活動之上的所有活動統統出棧,如果沒有發現就會創建一個新的活動實例。

 我們還是通過程式碼來更加直觀地理解一下。修改AndroidManifest.xml中FirstActivity的啟動模式:

<activity      android:name=".FirstActivity"      android:launchMode="singleTask"      android:label="This is FirstActivity">      <intent-filter>          <action android:name="android.intent.action.MAIN" />          <category android:name="android.intent.category.LAUNCHER" />      </intent-filter>  </activity>

5.4 singleInstance

 singleInstance模式應該算是4種啟動模式中最特殊也最複雜的一個了,你也需要多花點功夫來理解這個模式。不同於以上3種啟動模式,指定為singleInstance模式的活動會啟用一個新的返回棧來管理這個活動(其實如果singleTask模式指定了不同的taskAffinity,也會啟動一個新的返回棧)。那麼這樣做有什麼意義呢?想像以下場景,假設我們的程式中有一個活動是允許其他程式調用的,如果我們想實現其他程式和我們的程式可以共享這個活動的實例,應該如何實現呢?使用前面3種啟動模式肯定是做不到的,因為每個應用程式都會有自己的返回棧,同一個活動在不同的返回棧中入棧時必然是創建了新的實例。而使用singleInstance模式就可以解決這個問題,在這種模式下會有一個單獨的返回棧來管理這個活動,不管是哪個應用程式來訪問這個活動,都共用的同一個返回棧,也就解決了共享活動實例的問題。

 如果講第二個活動設置為單例啟動模式,就是放入單獨的一個棧中,然後使活動一按鈕指向活動二,活動二按鈕指向活動三,這樣就會發現:

 可以看到,SecondActivity的Task id 不同於FirstActivity和ThirdActivity,這說明SecondActivity確實是存放在一個單獨的返回棧里的,而且這個棧中只有SecondActivity這一個活動。然後我們按下Back鍵進行返回,你會發現ThirdActivity竟然直接返回到了FirstActivity,再按下Back鍵又會返回到SecondActivity,再按下Back鍵才會退出程式,這是為什麼呢?其實原理很簡單,由於FirstActivity和ThirdActivity是存放在同一個返回棧里的,當在ThirdActivity的介面按下Back鍵,ThirdActivity會從返回棧中出棧,那麼FirstActivity就成為了棧頂活動顯示在介面上,因此也就出現了從ThirdActivity直接返回到FirstActivity的情況。然後在FirstActivity介面再次按下Back鍵,這時當前的返回棧已經空了,於是就顯示了另一個返回棧的棧頂活動,即SecondActivity。最後再次按下Back鍵,這時所有返回棧都已經空了,也就自然退出了程式。

六、活動的最佳實踐

6.1 知曉當前是在哪一個活動

 思路就是創建一個新類,而不是活動文件,並且使其繼承於AppCompatActivity類,然後重寫其onCreatre方法,再讓所有之前寫的活動繼承於此類。重寫的方法為:

    @Override      protected void onCreate(@Nullable Bundle savedInstanceState) {          super.onCreate(savedInstanceState);          Log.d("BaseActivity",getClass().getSimpleName());      }

 由於BaseActivity 又是繼承自AppCompatActivity 的,所以項目中所有活動的現有功能並不受影響,它們仍然完全繼承了Activity中的所有特性。

 現在重新運行程式,然後通過點擊按鈕分別進入到FirstActivity、SecondActivity和ThirdActivity的介面,這時觀察logcat中的列印資訊,如下圖所示:

 現在每當我們進入到一個活動的介面,該活動的類名就會被列印出來,這樣我們就可以時時刻刻知曉當前介面對應的是哪一個活動了。

 換種情況,如果活動本來就是繼承於某個類,而不是父類AppCompatActivity,那麼直接就使其最終類繼承於我們新寫的BaseActivity類即可。

6.2 隨時隨地退出程式

 如果目前你手機的介面還停留在ThirdActivity,你會發現當前想退出程式是非常不方便的,需要連按3次Back鍵才行。按Home鍵只是把程式掛起,並沒有退出程式。其實這個問題就足以引起你的思考,如果我們的程式需要一個註銷或者退出的功能該怎麼辦呢?必須要有一個隨時隨地都能退出程式的方案才行。

 其實解決思路也很簡單,只需要用一個專門的集合類對所有的活動進行管理就可以了,下面我們就來實現一下。

 新建一個ActivityCollector 類作為活動管理器,程式碼如下所示:

public class BaseActivity extends AppCompatActivity {      @Override      protected void onCreate(Bundle savedInstanceState) {          super.onCreate(savedInstanceState);          Log.d("BaseActivity", getClass().getSimpleName());          ActivityCollector.addActivity(this);      }        @Override      protected void onDestroy() {          super.onDestroy();          ActivityCollector.removeActivity(this);      }  }    class ActivityCollector {        public static List<Activity> activities = new ArrayList<>();        public static void addActivity(Activity activity) {          activities.add(activity);      }        public static void removeActivity(Activity activity) {          activities.remove(activity);      }        public static void finishAll() {          for (Activity activity : activities) {              if (!activity.isFinishing()) {                  activity.finish();              }          }          activities.clear();          android.os.Process.killProcess(android.os.Process.myPid());      }    }

通過以上的基活動類被各個子類活動類繼承,我們確保了:

  1. 子類活動對象在創建過程中調用父類的onCreate方法時,會將子類對象加入到此activities鏈表中
  2. 通過鏈表對象的引用,可以通過finishAll方法提供一個增強for循環來進行活動的結束finish()
  3. 我們確保了活動調用了onDestroy方法後活動已經關閉,故沒必要將其放置於鏈表中,所以進行移除鏈表操作,並且雖然即使不移除,可能也不會有空指針報錯,但是在某個活動中調用ActivityCollector.finishAll()方法時遍歷對象個數更多,效率上顯得差了一點。

6.3啟動活動的最佳寫法

啟動活動的方法為:

  1. 首先通過Intent構建出當前的「意圖」,
  2. 然後調用startActivity()startActivityForResult()方法將活動啟動起來,如果有數據需要從一個活動傳遞到另一個活動,也可以藉助Intent來完成。

以下就是一個啟動活動的典型程式碼:

button1.setOnClickListener(new View.OnClickListener() {      @Override      public void onClick(View v) {          Intent intent = new Intent(FirstActivity.this, SecondActivity.class);          startActivity(intent);      }  });

 再說一個案例:假設SecondActivity中需要用到兩個非常重要的字元串參數,在啟動SecondActivity的時候必須要傳遞過來,那麼我們很容易會寫出如下程式碼:

Intent intent = new Intent(FirstActivity.this, SecondActivity.class);  intent.putExtra("param1", "data1");  intent.putExtra("param2", "data2");  startActivity(intent);

 這樣寫是完全正確的,不管是從語法上還是規範上,只是在**真正的項目開發中經常會有對接的問題出現。**比如SecondActivity並不是由你開發的,但現在你負責的部分需要有啟動SecondActivity這個功能,而你卻不清楚啟動這個活動需要傳遞哪些數據。這時無非就有兩種辦法,一個是你自己去閱讀SecondActivity中的程式碼,二是詢問負責編寫SecondActivity的同事。你會不會覺得很麻煩呢?其實只需要換一種寫法,就可以輕鬆解決掉上面的窘境。

修改SecondActivity中的程式碼,如下所示:

public class SecondActivity extends BaseActivity {        public static void actionStart(Context context, String data1, String data2) {          Intent intent = new Intent(context, SecondActivity.class);          intent.putExtra("param1", data1);          intent.putExtra("param2", data2);          context.startActivity(intent);      }      ...  }

 我們在SecondActivity中添加了一個actionStart()方法,在這個方法中完成了Intent的構建,另外所有SecondActivity中需要的數據都是通過actionStart()方法的參數傳遞過來的,然後把它們存儲到Intent中,最後調用startActivity()方法啟動SecondActivity。

 這樣一來,SecondActivity所需要的數據在方法參數中全部體現出來了,這樣即使不用閱讀SecondActivity中的程式碼,不去詢問負責編寫SecondActivity的同事,你也可以非常清晰地知道啟動SecondActivity需要傳遞哪些數據。另外,這樣寫還簡化了啟動活動的程式碼,現在只需要一行程式碼就可以啟動SecondActivity,如下所示:

button1.setOnClickListener(new OnClickListener() {      @Override      public void onClick(View v) {          SecondActivity.actionStart(FirstActivity.this, "data1", "data2");      }  });

 這裡就是我們應當再編寫的每個活動都添加類似的啟動方法,這樣不僅可以讓啟動活動變得非常簡單,還可以節省不少你同事過來詢問你的時間。