《第一行代碼:Android篇》學習筆記(二)

来源:https://www.cnblogs.com/1693977889zz/archive/2022/05/11/16256331.html
-Advertisement-
Play Games

本文和接下來的幾篇文章為閱讀郭霖先生所著《第一行代碼:Android(篇第2版)》的學習筆記,按照書中的內容順序進行記錄,書中的Demo本人全部都做過了。 每一章節本人都做了詳細的記錄,以下是我學習記錄(包含大量書中內容的整理和自己在學習中遇到的各種bug及解決方案),方便以後閱讀和查閱。最後,感激 ...


本文和接下來的幾篇文章為閱讀郭霖先生所著《第一行代碼:Android(篇第2版)》的學習筆記,按照書中的內容順序進行記錄,書中的Demo本人全部都做過了。
每一章節本人都做了詳細的記錄,以下是我學習記錄(包含大量書中內容的整理和自己在學習中遇到的各種bug及解決方案),方便以後閱讀和查閱。最後,感激感激郭霖先生提供這麼好的書籍。

第2章 先從看得到的入手——探究活動

2.1 活動是什麼

活動(Activity)是最容易吸引用戶的地方,它是一種可以包含用戶界面的組件,主要用於和用戶進行交互。

2.2 活動的基本用法

到現在為止,還沒有手動創建過活動呢,上一章中的HelloWorldActivity是Android Studio幫我們自動創建的。手動創建活動可以加深我們的理解,因此現在是時候應該自己動手了。

由於Android Studio在一個工作區間內只允許打開一個項目,因此首先需要將當前的項目關閉,點擊導航欄File→Close Project。

  • 項目名可以叫作ActivityTest
  • 包名,com.zhouzhou.activitytest
  • 選擇Add No Activity(不再選擇Empty Activity)準備手動創建活動
  • 點擊Finish,等待Gradle構建完成後,項目創建成功

2.2.1 手動創建活動

項目創建成功後,仍然會預設使用Android模式的項目結構,這裡手動改成Project模式。此時,app/src/main/java/com.zhouzhou.activitytest目錄應該是空的了,初始項目結構:

image

現在右擊com.zhouzhou.activitytest包→New→Activity→Empty Activity,會彈出一個創建活動的對話框。

image

將活動命名為FirstActivity,並且不要勾選Generate Layout File和LauncherActivity這兩個選項。

image

  • 勾選Generate Layout File表示會自動為FirstActivity創建一個對應的佈局文件
  • 勾選Launcher Activity表示會自動將FirstActivity設置為當前項目的主活動
  • 勾選Backwards Compatibility表示會為項目啟用向下相容的模式(上圖沒有此選項,教科書里有)

項目中的任何活動都應該重寫Activity的onCreate()方法,而目前我們的FirstActivity中已經重寫了這個方法,這是由Android Studio自動幫我們完成的,代碼如下:

package com.zhouzhou.activitytest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;

public class FirstActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }
}

2.2.2 創建和載入佈局

佈局就是用來顯示界面內容的,因此我們現在就來手動創建一個佈局文件。

  • 右擊app/src/main/res目錄→New→Directory,創建一個名為layout的目錄

image

image

  • 對著layout目錄右鍵→New→Layout resource file,又會彈出一個新建佈局資源文件的視窗,將這個佈局文件命名為first_layout,根元素選擇為LinearLayout(現在預設是androidx.constraintlayout.widget.ConstraintLayout,書中所寫預設選擇為LinearLayout)

image

image

  • 點擊OK完成佈局的創建,會看到如圖所示的佈局編輯器

image

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

​ Code(書中是Test):是通過XML文件的方式來編輯佈局的:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    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:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:id="@+id/button_1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Button1"/>

</LinearLayout>
  • 添加了一個Button元素

  • 併在Button元素的內部增加了幾個屬性

  • android:id是給當前的元素定義一個唯一標識符,之後可以在代碼中對這個元素進行操作

  • 如果需要在XML中定義一個id,則要使用@+id/id_name這種語法,而如果你需要在XML中引用一個id,就使用@id/id_name這種語法

  • android:layout_width指定了當前元素的寬度,match_parent表示讓當前元素和父元素一樣寬

  • android:layout_height指定了當前元素的高度,使用wrap_content表示當前元素的高度只要能剛好包含裡面的內容就行

  • android:text指定了元素中顯示的文字內容

    預覽當前佈局:

image

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

package com.zhouzhou.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.first_layout);
    }
}

這裡調用了setContentView()方法來給當前的活動載入一個佈局,而在setContentView()方法中,我們一般都會傳入一個佈局文件的id。(在代碼中去引用佈局文件的方法:項目中添加的任何資源都會在R文件中生成一個相應的資源id,因此我們剛纔創建的first_layout.xml佈局的id現在應該是已經添加到R文件中了。

2.2.3 在AndroidManifest文件中註冊

所有的活動都要在AndroidManifest.xml中進行註冊才能生效,而實際上FirstActivity已經在AndroidManifest.xml中註冊過了,我們打開app/src/main/Android-Manifest.xml文件,代碼如下所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.zhouzhou.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/Theme.ActivityTest">
        <activity
            android:name=".FirstActivity"
            android:exported="false" />
    </application>

</manifest>
  • 活動的註冊聲明要放在<application>標簽內,這裡是通過<activity>標簽來對活動進行註冊的。

    之前在使用Eclipse創建活動或其他系統組件時,很多人都會忘記要去Android Manifest.xml中註冊一下,從而導致程式運行崩潰,AndroidStudio在這方面做得更加人性化(上面準備,右擊com.zhouzhou.activitytest包→New→Activity→Empty Activity,創建名為FirstActivity的活動)。

  • <activity>標簽中使用了android:name來指定具體註冊哪一個活動(.FirstActivity是com.zhouzhou.activitytest.FirstActivity的縮寫,因為在最外層的<manifest>標簽中已經通過package屬性指定了程式的包名package="com.zhouzhou.activitytest",因此在註冊活動時這一部分就可以省略了。

僅僅是這樣註冊了活動,我們的程式仍然是不能運行的,因為還沒有為程式配置主活動,也就是說,當程式運行起來的時候,不知道要首先啟動哪個活動。在<activity>標簽的內部加入<intent-filter>標簽,併在這個標簽里添加:

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

還可以使用android:label指定活動中標題欄的內容,標題欄是顯示在活動最頂部的。需要註意的是,給主活動指定的label不僅會成為標題欄中的內容,還會成為啟動器(Launcher)中應用程式顯示的名稱。修改後的AndroidManifest.xml文件,代碼如下:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.zhouzhou.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/Theme.ActivityTest">
        <activity
            android:name=".FirstActivity"
            android:label="This is FirstActivity"
            android:exported="true" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

image

這樣,FirstActivity就成為我們這個程式的主活動了,即點擊桌面應用程式圖標時首先打開的就是這個活動。

註意,如果你的應用程式中沒有聲明任何一個活動作為主活動,這個程式仍然是可以正常安裝的,只是你無法在啟動器中看到或者打開這個程式。這種程式一般都是作為第三方服務供其他應用在內部進行調用的,如支付寶快捷支付服務。

2.2.4 在活動中使用Toast

Toast是Android系統提供的一種非常好的提醒方式,在程式中可以使用它將一些短小的信息通知給用戶,這些信息會在一段時間後自動消失,並且不會占用任何屏幕空間。

  • 首先需要定義一個彈出Toast的觸發點
  • 正好界面上有個按鈕,就讓點擊這個按鈕的時候彈出一個Toast吧
  • onCreate()方法中添加如下代碼:
package com.zhouzhou.activitytest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class FirstActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.first_layout);
        Button button1=(Button) findViewById(R.id.button_1);
        button1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(FirstActivity.this,"You clicked Button 1",Toast.LENGTH_SHORT).show();
            }
        });
    }
}
  1. 通過findViewById()方法獲取到在佈局文件中定義的元素,這裡傳入R.id.button_1,來得到按鈕的實例(這個值是在first_layout.xml中通過android:id屬性指定的)
  2. findViewById()方法返回的是一個View對象,需要向下轉型將它轉成Button對象
  3. 通過調用setOnClickListener()方法為按鈕註冊一個監聽器,點擊按鈕時就會執行監聽器中的onClick()方法
  4. Toast的用法非常簡單,通過靜態方法makeText()創建出一個Toast對象,然後調用show()將Toast顯示出來就可以了
  5. makeText()方法需要傳入3個參數。
    • 第一個參數是Context,是Toast要求的上下文,由於活動本身就是一個Context對象,因此這裡直接傳入FirstActivity.this即可。
    • 第二個參數是Toast顯示的文本內容。
    • 第三個參數是Toast顯示的時長,有兩個內置常量可以選擇Toast.LENGTH_SHORT和Toast.LENGTH_LONG。

重新運行程式,並點擊一下按鈕,效果如圖:

image

2.2.5 在活動中使用Menu

Android給我們提供了一種方式,可以讓菜單都能得到展示的同時,還能不占用任何屏幕空間。

  1. 在res目錄下新建一個menu文件夾,右擊res目錄→New→Directory,輸入文件夾名menu,點擊OK
  2. 在這個文件夾(menu)下再新建一個名叫main的菜單文件,右擊menu文件夾→New→Menu resource file
  3. 然後在main.xml中添加如下代碼:
<?xml version="1.0" encoding="utf-8"?>
<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>

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

  1. 重新回到FirstActivity中來重寫onCreateOptionsMenu()方法,重寫方法可以使用Ctrl+ O快捷鍵(Mac系統是control +O)

image

  1. 然後在onCreateOptionsMenu()方法中編寫如下代碼:
@Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main,menu);
        return true;
    }

通過getMenuInflater()方法能夠得到MenuInflater對象,再調用它的inflate()方法就可以給當前活動創建菜單。inflate()方法接收兩個參數,第一個參數用於指定通過哪一個資源文件來創建菜單,這裡傳入R.menu.main。第二個參數用於指定我們的菜單項將添加到哪一個Menu對象當中,這裡直接使用onCreateOptionsMenu()方法中傳入的menu參數。然後給這個方法返回true,表示允許創建的菜單顯示出來,如果返回了false,創建的菜單將無法顯示。

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

    @Override
    public boolean onOptionsItemSelected(@NonNull 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。

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

image

菜單里的菜單項預設是不會顯示出來的,只有點擊一下菜單按鈕才會彈出裡面具體的內容,因此它不會占用任何活動的空間,然後如果你點擊了Add菜單項就會彈出Youclicked Add提示,如果點擊了Remove菜單項就會彈出You clickedRemove提示。

2.2.6 銷毀一個活動

只要按一下Back鍵就可以銷毀當前的活動了。不過如果不想通過按鍵的方式,而是希望在程式中通過代碼來銷毀活動,Activity類提供了一個finish()方法,在活動中調用一下這個方法就可以銷毀當前活動了。

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

button1.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
             //Toast.makeText(FirstActivity.this,"You clicked Button 1",Toast.LENGTH_SHORT).show();
            finish();
        }
  });

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

2.3 使用Intent在活動之間穿梭

在啟動器中點擊應用的圖標只會進入到該應用的主活動,那麼怎樣才能由主活動跳轉到其他活動呢?

2.3.1 使用顯式Intent

在快速地在ActivityTest項目中再創建一個活動。右擊com.zhouzhou.activitytest包→New→Activity→Empty Activity,會彈出一個創建活動的對話框,這次將活動命名為SecondActivity,並勾選Generate LayoutFile,給佈局文件起名為second_layout,但不要勾選Launcher Activity選項,如圖:

image

Android Studio會為我們自動生成SecondActivity.java和second_layout. xml這兩個文件。不過自動生成的佈局代碼目前對你來說可能有些複雜,這裡我們仍然還是使用最熟悉的LinearLayout,編輯second_layout.xml,將裡面的代碼替換成如下內容:

<?xml version="1.0" encoding="utf-8"?>
<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="Button2"/>

</LinearLayout>

定義了一個按鈕,按鈕上顯示Button2。SecondActivity中的代碼已經自動生成了一部分,保持預設不變就好,如下所示:

package com.zhouzhou.activitytest;

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.zhouzhou.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/Theme.ActivityTest">
        <activity
            android:name=".FirstActivity"
            android:exported="true"
            android:label="This is FirstActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name=".SecondActivity"
            android:exported="false" />
    </application>

</manifest>

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

Intent是Android程式中各組件之間進行交互的一種重要方式,它不僅可以指明當前組件想要執行的動作,還可以在不同組件之間傳遞數據。Intent一般可被用於啟動活動、啟動服務以及發送廣播等場景。

Intent大致可以分為兩種:顯式Intent和隱式Intent

顯式Intent如何使用:

Intent有多個構造函數的重載,其中一個是Intent(Context packageContext, Class<?>cls)。第一個參數Context要求提供一個啟動活動的上下文,第二個參數Class則是指定想要啟動的目標活動,通過這個構造函數就可以構建出Intent的“意圖”。

然後應該怎麼使用這個Intent呢?

Activity類中提供了一個startActivity()方法,這個方法是專門用於啟動活動的,它接收一個Intent參數,這裡我們將構建好的Intent傳入startActivity()方法就可以啟動目標活動了。

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

button1.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        //Toast.makeText(FirstActivity.this,"You clicked Button 1",Toast.LENGTH_SHORT).show();
        Intent intent=new Intent(FirstActivity.this,SecondActivity.class);
        startActivity(intent);
    }
});

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

重新運行程式,在FirstActivity的界面點擊一下按鈕,結果如圖:

image

已經成功啟動SecondActivity這個活動。如果想要回到上一個活動怎麼辦呢?很簡單,按下Back鍵就可以銷毀當前活動,從而回到上一個活動了。

使用這種方式來啟動活動,Intent的“意圖”非常明顯,因此我們稱之為顯式Intent

2.3.2 使用隱式Intent

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

通過在<activity>標簽下配置<intent-filter>的內容,可以指定當前活動能夠響應的action和category,打開AndroidManifest.xml,添加如下代碼:

<activity
            android:name=".SecondActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="com.zhouzhou.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 view) {
                //Toast.makeText(FirstActivity.this,"You clicked Button 1",Toast.LENGTH_SHORT).show();
                //Intent intent=new Intent(FirstActivity.this,SecondActivity.class);
                Intent intent=new Intent("com.zhouzhou.activitytest.ACTION_START");
                startActivity(intent);
            }
        });

使用了Intent的另一個構造函數,直接將action的字元串傳了進去,表明我們想要啟動能夠響應com.zhouzhou.activitytest.ACTION_START這個action的活動。

因為android.intent.category.DEFAULT是一種預設的category,在調用startActivity()方法的時候會自動將這個category添加到Intent中。

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

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

button1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                //Toast.makeText(FirstActivity.this,"You clicked Button 1",Toast.LENGTH_SHORT).show();
                //Intent intent=new Intent(FirstActivity.this,SecondActivity.class);
                Intent intent=new Intent("com.zhouzhou.activitytest.ACTION_START");
                intent.addCategory("com.zhouzhou.activitytest.MY_CATEGORY");
                startActivity(intent);
            }
        });

可以調用Intent中的addCategory()方法來添加一個category,這裡我們指定了一個自定義的category,值為com.zhouzhou.activitytest.MY_CATEGORY。現在重新運行程式,在FirstActivity的界面點擊一下按鈕,你會發現,程式崩潰了!

其實大多數的崩潰問題都是很好解決的,只要你善於分析。在logcat界面查看錯誤日誌,你會看到如圖:

image

錯誤信息中提醒,沒有任何一個活動可以響應我們的Intent,為什麼呢?

這是因為剛剛在Intent中新增了一個category,而SecondActivity的<intent-filter>標簽中並沒有聲明可以響應這個category,所以就出現了沒有任何活動可以響應該Intent的情況。現在在<intent-filter>中再添加一個category的聲明,如下所示:

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

再次重新運行程式,一切都正常了。

2.3.3 更多隱式Intent的用法

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

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

 button1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                //Toast.makeText(FirstActivity.this,"You clicked Button 1",Toast.LENGTH_SHORT).show();
                //Intent intent=new Intent(FirstActivity.this,SecondActivity.class);
                //Intent intent=new Intent("com.zhouzhou.activitytest.ACTION_START");
                //intent.addCategory("com.zhouzhou.activitytest.MY_CATEGORY");
                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界面點擊按鈕就可以看到打開了系統瀏覽器,如圖:

image

setData()方法,它接收一個Uri對象,主要用於指定當前Intent正在操作的數據,而這些數據通常都是以字元串的形式傳入到Uri.parse()方法中解析產生的。

與此對應,還可以在<intent-filter>標簽中再配置一個<data>標簽,用於更精確地指定當前活動能夠響應什麼類型的數據。<data>標簽中主要可以配置以下內容:

  • 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。

  1. 右擊com.zhouzhou.activitytest包→New→Activity→Empty Activity,新建ThirdActivity;
  2. 勾選Generate LayoutFile,給佈局文件起名為third_layout,點擊Finish完成創建;
  3. 編輯third_layout.xml,將裡面的代碼替換成如下內容:
<?xml version="1.0" encoding="utf-8"?>
<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_1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Button1"/>
</LinearLayout>

ThirdActivity中的代碼保持不變就可以了,最後在AndroidManifest.xml中修改ThirdActivity的註冊信息:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.zhouzhou.activitytest"
    tools:ignore="GoogleAppIndexingWarning">

    ......

        <activity
            android:name=".ThirdActivity"
            android:exported="true">
            <intent-filter tools:ignore ="AppLinkUrlError">
                <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>
    </application>

</manifest>

在ThirdActivity的<intent-filter>中配置了當前活動能夠響應的action是Intent.ACTION_VIEW的常量值,而category則毫無疑問指定了預設的category值,另外在<data>標簽中通過android:scheme指定了數據的協議必須是http協議,這樣ThirdActivity應該就和瀏覽器一樣,能夠響應一個打開網頁的Intent了。運行一下程式試試吧,在FirstActivity的界面點擊一下按鈕:(遇到問題,我的博客:https://www.cnblogs.com/1693977889zz/p/15937645.html)

image

系統自動彈出了一個列表,顯示了目前能夠響應這個Intent的所有程式。選擇Browser還會像之前一樣打開瀏覽器,並顯示百度的主頁,而如果選擇了ActivityTest,則會啟動ThirdActivity。JUST ONCE表示只是這次使用選擇的程式打開,ALWAYS則表示以後一直都使用這次選擇的程式打開。

需要註意的是,雖然我們聲明瞭ThirdActivity是可以響應打開網頁的Intent的,但實際上這個活動並沒有載入並顯示網頁的功能,所以在真正的項目中儘量不要出現這種有可能誤導用戶的行為,不然會讓用戶對我們的應用產生負面的印象。

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

button1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                //Toast.makeText(FirstActivity.this,"You clicked Button 1",Toast.LENGTH_SHORT).show();
                //Intent intent=new Intent(FirstActivity.this,SecondActivity.class);
                //Intent intent=new Intent("com.zhouzhou.activitytest.ACTION_START");
                //intent.addCategory("com.zhouzhou.activitytest.MY_CATEGORY");
                //Intent intent=new Intent(Intent.ACTION_VIEW);
                //intent.setData(Uri.parse("http://www.baidu.com"));
                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的界面點擊一下按鈕,結果如圖:

image

2.3.4 向下一個活動傳遞數據

到目前為止,都只是簡單地使用Intent來啟動一個活動,其實Intent還可以在啟動活動的時候傳遞數據,下麵我們來一起看一下。

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

button1.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        String data="Hello SecondActivity";
        Intent intent= new Intent(FirstActivity.this,SecondActivity.class);
        intent.putExtra("extra data",data);
        startActivity(intent);
    }
});

使用顯式Intent的方式來啟動SecondActivity,並通過putExtra()方法傳遞了一個字元串。註意這裡putExtra()方法接收兩個參數,第一個參數是鍵,用於後面從Intent中取值,第二個參數才是真正要傳遞的數據。然後在SecondActivity中將傳遞的數據取出,並列印出來,代碼如下所示:

package com.zhouzhou.activitytest;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.util.Log;

public class SecondActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.second_layout);
        Intent intent = getIntent();
        String data = intent.getStringExtra("extra data");
        Log.d("SecondActivity",data);
    }
}

通過getIntent()方法獲取到用於啟動SecondActivity的Intent,然後調用getStringExtra()方法,傳入相應的鍵值,就可以得到傳遞的數據了。

這裡由於傳遞的是字元串,所以使用getStringExtra()方法來獲取傳遞的數據。如果傳遞的是整型數據,則使用getIntExtra()方法;如果傳遞的是布爾型數據,則使用getBooleanExtra()方法,以此類推。

重新運行程式,在FirstActivity的界面點擊一下按鈕會跳轉到SecondActivity,查看logcat列印信息,如圖:

image

2.3.5 返回數據給上一個活動

既然可以傳遞數據給下一個活動,那麼能不能夠返回數據給上一個活動呢?答案是肯定的。

不同的是,返回上一個活動只需要按一下Back鍵就可以了,並沒有一個用於啟動活動的Intent來傳遞數據。通過查閱文檔發現,Activity中還有一個startActivityForResult()方法也是用於啟動活動的,但這個方法期望在活動銷毀的時候能夠返回一個結果給上一個活動。

毫無疑問,這就是我們所需要的。startActivityForResult()方法接收兩個參數,第一個參數還是Intent,第二個參數是請求碼,用於在之後的回調中判斷數據的來源。我們還是來實戰一下,修改FirstActivity中按鈕的點擊事件,代碼如下所示:

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

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

package com.zhouzhou.activitytest;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

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 view) {
                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, @Nullable Intent data) {
    super.onActivityResult(requestCode, resultCode, 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的列印信息,如圖:

image

可以看到,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();
}

image

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

2.4 活動的生命周期

掌握活動的生命周期對任何Android開發者來說都非常重要,當你深入理解活動的生命周期之後,就可以寫出更加連貫流暢的程式,併在如何合理管理應用資源方面發揮得游刃有餘。你的應用程式將會擁有更好的用戶體驗。

2.4.1 返回棧

經過前面幾節的學習發現,Android中的活動是可以層疊的。每啟動一個新的活動,就會覆蓋在原活動之上,然後點擊Back鍵會銷毀最上面的活動,下麵的一個活動就會重新顯示出來。

其實Android是使用任務(Task)來管理活動的,一個任務就是一組存放在棧里的活動的集合,這個棧也被稱作返回棧(Back Stack)。

棧是一種後進先出的數據結構,在預設情況下,每當我們啟動了一個新的活動,它會在返回棧中入棧,並處於棧頂的位置。而每當按下Back鍵或調用finish()方法去銷毀一個活動時,處於棧頂的活動會出棧,這時前一個入棧的活動就會重新處於棧頂的位置。系統總是會顯示處於棧頂的活動給用戶。

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

image

2.4.2 活動狀態

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

  1. 運行狀態

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

  1. 暫停狀態

當一個活動不再處於棧頂位置,但仍然可見時,這時活動就進入了暫停狀態。

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

  1. 停止狀態

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

  1. 銷毀狀態

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

2.4.3 活動的生存期

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

  1. onCreate()

    它會在活動第一次被創建的時候調用。你應該在這個方法中完成活動的初始化操作,比如說載入佈局、綁定事件等。

  2. onStart()

    在活動由不可見變為可見的時候調用。

  3. onResume()

    在活動準備好和用戶進行交互的時候調用。此時的活動一定位於返回棧的棧頂,並且處於運行狀態。

  4. onPause()

    在系統準備去啟動或者恢復另一個活動的時候調用。我們通常會在這個方法中將一些消耗CPU的資源釋放掉,以及保存一些關鍵數據,但這個方法的執行速度一定要快,不然會影響到新的棧頂活動的使用。

  5. onStop()

    在活動完全不可見的時候調用。它和onPause()方法的主要區別在於,如果啟動的新活動是一個對話框式的活動,那麼onPause()方法會得到執行,而onStop()方法並不會執行。

  6. onDestroy()

    在活動被銷毀之前調用,之後活動的狀態將變為銷毀狀態。

  7. onRestart()

    在活動由停止狀態變為運行狀態之前調用,也就是活動被重新啟動了。

以上7個方法中除了onRestart()方法,其他都是兩兩相對的,從而又可以將活動分為3種生存期。

  1. 完整生存期

    活動在onCreate()方法和onDestroy()方法之間所經歷的,就是完整生存期。一般情況下,一個活動會在onCreate()方法中完成各種初始化操作,而在onDestroy()方法中完成釋放記憶體的操作。

  2. 可見生存期

    活動在onStart()方法和onStop()方法之間所經歷的,就是可見生存期。在可見生存期內,活動對於用戶總是可見的,即便有可能無法和用戶進行交互。我們可以通過這兩個方法,合理地管理那些對用戶可見的資源。比如在onStart()方法中對資源進行載入,而在onStop()方法中對資源進行釋放,從而保證處於停止狀態的活動不會占用過多記憶體。

  3. 前臺生存期

    活動在onResume()方法和onPause()方法之間所經歷的就是前臺生存期。在前臺生存期內,活動總是處於運行狀態的,此時的活動是可以和用戶進行交互的,我們平時看到和接觸最多的也就是這個狀態下的活動。

Android官方提供了一張活動生命周期的示意圖,如圖:

image

2.4.4 體驗活動的生命周期

實戰一下了,更加直觀地體驗活動的生命周期。

  1. 首先關閉ActivityTest項目,點擊導航欄File→Close Project。
  2. 再新建一個ActivityLifeCycleTest項目,這次允許Android Studio自動創建活動和佈局,這樣可以省去不少工作,創建的活動名和佈局名都使用預設值。
  3. 再創建兩個子活動——NormalActivity和DialogActivity。右擊com.zhouzhou.activitylifecycletest包→New→Activity→EmptyActivity,新建NormalActivity,佈局起名為normal_layout。然後使用同樣的方式創建DialogActivity,佈局起名為dialog_layout。
  4. 編輯normal_layout.xml文件,將裡面的代碼替換成如下內容:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="This is a normal activity"/>
</LinearLayout>

這個佈局中我們就非常簡單地使用了一個TextView,用於顯示一行文字。

  1. 再編輯dialog_layout.xml文件,將裡面的代碼替換成如下內容:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="This is a dialog activity"/>
</LinearLayout>

兩個佈局文件的代碼幾乎沒有區別,只是顯示的文字不同而已。NormalActivity和DialogActivity中的代碼我們保持預設就好,不需要改動。這兩個活動一個是普通的活動,一個是對話框式的活動。

可是我們並沒有修改活動的任何代碼,兩個活動的代碼應該幾乎是一模一樣的,在哪裡有體現出將活動設成對話框式的呢?

  1. 修改AndroidManifest.xml的<activity>標簽的配置,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.zhouzhou.activitylifecycletes">

    <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/Theme.ActivityLifeCycleTes">
        <activity
            android:name=".DialogActivity"
            android:theme="@style/Theme.AppCompat.Dialog"/>
        <activity
            android:name=".NormalActivity"/>
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

這裡是兩個活動的註冊代碼,但是DialogActivity的代碼有些不同,給它使用了一個android:theme屬性,這是用於給當前活動指定主題的,Android系統內置有很多主題可以選擇,當然我們也可以定製自己的主題,而這裡@style/Theme.AppCompat.Dialog則毫無疑問是讓DialogActivity使用對話框式的主題。

  1. 修改activity_main.xml,重新定製主活動的佈局,將裡面的代碼替換成如下內容:
<?xml version="1.0" encoding="utf-8"?>
<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/start_normal_activity"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Start NormalActivity"/>
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/start_dialog_activity"
        android:text="Start DialogActivity"/>
</LinearLayout>

在LinearLayout中加入了兩個按鈕,一個用於啟動NormalActivity,一個用於啟動DialogActivity。

  1. 最後修改MainActivity中的代碼,如下所示:
package com.zhouzhou.activitylifecycletes;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {
    public static final String TAG = "MainActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG,"onCreate");
        setContentView(R.layout.activity_main);
        Button startNormalActivity = (Button) findViewById(R.id.start_normal_activity);
        Button startDialogActivity = (Button) findViewById(R.id.start_dialog_activity);
        startNormalActivity.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this,NormalActivity.class);
                startActivity(intent);
            }
        });
        startDialogActivity.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this,DialogActivity.class);
                startActivity(intent);
            }
        });
    }

    @Override
    protected void onStart() {
        super.onStart();
        Log.d(TAG,"onStart");
    }
    @Override
    protected void onResume() {
        super.onResume();
        Log.d(TAG,"onResume");
    }
    @Override
    protected void onPause() {
        super.onPause();
        Log.d(TAG,"onPause");
    }
    @Override
    protected void onStop() {
        super.onStop();
        Log.d(TAG,"onStop");
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.d(TAG,"onDestroy");
    }
    @Override
    protected void onRestart() {
        super.onRestart();
        Log.d(TAG,"onRestart");
    }
}

在onCreate()方法中,分別為兩個按鈕註冊了點擊事件,點擊第一個按鈕會啟動NormalActivity,點擊第二個按鈕會啟動DialogActivity。然後在Activity的7個回調方法中分別列印了一句話,這樣就可以通過觀察日誌的方式來更直觀地理解活動的生命周期。現在運行程式,效果如圖:

image

當MainActivity第一次被創建時會依次執行onCreate()、onStart()和onResume()方法。然後點擊第一個按鈕,啟動NormalActivity。由於NormalActivity已經把MainActivity完全遮擋住,因此onPause()和onStop()方法都會得到執行。

然後按下Back鍵返回MainActivity,由於之前MainActivity已經進入了停止狀態,所以onRestart()方法會得到執行,之後又會依次執行onStart()和onResume()方法。註意此時onCreate()方法不會執行,因為MainActivity並沒有重新創建。

然後再點擊第二個按鈕,啟動DialogActivity,可以看到,只有onPause()方法得到了執行,onStop()方法並沒有執行,這是因為DialogActivity並沒有完全遮擋住MainActivity,此時MainActivity只是進入了暫停狀態,並沒有進入停止狀態。

相應地,按下Back鍵返回MainActivity也應該只有onResume()方法會得到執行。最後在MainActivity按下Back鍵退出程式,依次會執行onPause()、onStop()和onDestroy()方法,最終銷毀MainActivity。

這樣活動完整的生命周期你已經體驗了一遍。

2.4.5 活動被回收了怎麼辦

前面已經說過,當一個活動進入到了停止狀態,是有可能被系統回收的。

那麼想象以下場景:應用中有一個活動A,用戶在活動A的基礎上啟動了活動B,活動A就進入了停止狀態,這個時候由於系統記憶體不足,將活動A回收掉了,然後用戶按下Back鍵返回活動A,會出現什麼情況呢?

其實還是會正常顯示活動A的,只不過這時並不會執行onRestart()方法,而是會執行活動A的onCreate()方法,因為活動A在這種情況下會被重新創建一次。這樣看上去好像一切正常,可是別忽略了一個重要問題,活動A中是可能存在臨時數據和狀態的。打個比方,MainActivity中有一個文本輸入框,現在你輸入了一段文字,然後啟動NormalActivity,這時MainActivity由於系統記憶體不足被回收掉,過了一會你又點擊了Back鍵回到MainActivity,你會發現剛剛輸入的文字全部都沒了,因為MainActivity被重新創建了。

如果我們的應用出現了這種情況,是會嚴重影響用戶體驗的,所以必須要想想辦法解決這個問題。

查閱文檔可以看出,Activity中還提供了一個onSaveInstanceState()回調方法,這個方法可以保證在活動被回收之前一定會被調用,因此我們可以通過這個方法來解決活動被回收時臨時數據得不到保存的問題。onSaveInstanceState()方法會攜帶一個Bundle類型的參數,Bundle提供了一系列的方法用於保存數據,比如可以使用putString()方法保存字元串,使用putInt()方法保存整型數據,以此類推。每個保存方法需要傳入兩個參數,第一個參數是鍵,用於後面從Bundle中取值,第二個參數是真正要保存的內容。在MainActivity中添加如下代碼就可以將臨時數據進行保存:

@Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        String tempData = "Something you just typed";
        outState.putString("data_key",tempData);
    }

數據是已經保存下來了,那麼我們應該在哪裡進行恢復呢?

我們一直使用的onCreate()方法其實也有一個Bundle類型的參數。這個參數在一般情況下都是null,但是如果在活動被系統回收之前有通過onSaveInstanceState()方法來保存數據的話,這個參數就會帶有之前所保存的全部數據,我們只需要再通過相應的取值方法將數據取出即可。修改MainActivity的onCreate()方法,如下所示:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG,"onCreate");
        setContentView(R.layout.activity_main);
        if (savedInstanceState!=null) {
            String tempData = savedInstanceState.getString("data_key");
            Log.d(TAG,tempData);
        }
        ......

取出值之後再做相應的恢復操作就可以了,比如說將文本內容重新賦值到文本輸入框上,這裡我們只是簡單地列印一下。

一下午的測試,嗚嗚嗚~上面例子,咋也列印不出來“Something you just typed”,代碼沒問題,出發機制沒整明白。onSaveInstanceState()方法了,它不是生命周期方法,它不同於生命周期方法,它並不會一定會被觸發,它只有具備以下條件的時候才會觸發:

  1. 當按下HOME鍵的時
  2. 長按HOME鍵,選擇運行程式的時
  3. 按下電源(關閉屏幕顯示)時
  4. 從Activity中啟動其他Activity時
  5. 屏幕方向切換時(例如從豎屏切換到橫屏時)

我是利用,屏幕方向切換時(例如從豎屏切換到橫屏時)達到效果的,如下圖顯示:
image

image

Intent還可以結合Bundle一起用於傳遞數據,首先可以把需要傳遞的數據都保存在Bundle對象中,然後再將Bundle對象存放在Intent里。到了目標活動之後先從Intent中取出Bundle,再從Bund

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文力求以簡單易懂的語言描述出資料庫發展史,儘量避免出現複雜的概念介紹。資料庫演進史如圖1所示: 圖1 資料庫演進 一、穿孔紙帶和文件系統 在現代意義的資料庫出現之前(20世紀60年代),人們通過人工和文件系統的方式來存儲、管理數據。在人工管理時期,人們常使用穿孔紙帶來管理數據(圖2),雖然穿孔紙帶 ...
  • 19c單實例配置GoldenGate 併進行用戶數據同步測試 當前數據使用的是經典模式配置GoldenGate 進行數據同步,關於容器資料庫配置GoldenGate 請參閱: Configuring Oracle GoldenGate in a Multitenant Container Datab ...
  • 離線CDH集群自動化部署工具 離線CDH集群安裝與部署的自動化腳本工具,簡單支持「離線一鍵裝機」。 腳本將對系統配置做出一定修改,使用前請務必確認當前伺服器無其他人員、任務使用,以免造成不必要的麻煩,建議提前使用測試伺服器或虛擬機測試體驗。 一、Features 已實現的自動化功能(僅支持Redha ...
  • 一、引言 需求描述:現實工作中,有一些很特別的需求:在一個彙總表中,需要顯示明細數據。因為是在彙總表中,所以明細數據只能顯示在某一列中,這個列,就是多行數據合併為一行之後的結果。 案例描述:比如,在物料凈需求表中,需求量扣減庫存量、在途量等等之後,結果為剩餘量,剩餘量為負則需要採購。此時,不管剩餘量 ...
  • 5月中國資料庫排行榜已出爐!各大資料庫廠商表現如何?排名有何變化?一起來解讀吧! ...
  • 本文和接下來的幾篇文章為閱讀郭霖先生所著《第一行代碼:Android(篇第2版)》的學習筆記,按照書中的內容順序進行記錄,書中的Demo本人全部都做過了。 每一章節本人都做了詳細的記錄,以下是我學習記錄(包含大量書中內容的整理和自己在學習中遇到的各種bug及解決方案),方便以後閱讀和查閱。最後,感激 ...
  • 本文和接下來的幾篇文章為閱讀郭霖先生所著《第一行代碼:Android(篇第2版)》的學習筆記,按照書中的內容順序進行記錄,書中的Demo本人全部都做過了。 每一章節本人都做了詳細的記錄,以下是我學習記錄(包含大量書中內容的整理和自己在學習中遇到的各種bug及解決方案),方便以後閱讀和查閱。最後,感激 ...
  • 本文和接下來的幾篇文章為閱讀郭霖先生所著《第一行代碼:Android(篇第2版)》的學習筆記,按照書中的內容順序進行記錄,書中的Demo本人全部都做過了。 每一章節本人都做了詳細的記錄,以下是我學習記錄(包含大量書中內容的整理和自己在學習中遇到的各種bug及解決方案),方便以後閱讀和查閱。最後,感激 ...
一周排行
    -Advertisement-
    Play Games
  • Timer是什麼 Timer 是一種用於創建定期粒度行為的機制。 與標準的 .NET System.Threading.Timer 類相似,Orleans 的 Timer 允許在一段時間後執行特定的操作,或者在特定的時間間隔內重覆執行操作。 它在分散式系統中具有重要作用,特別是在處理需要周期性執行的 ...
  • 前言 相信很多做WPF開發的小伙伴都遇到過表格類的需求,雖然現有的Grid控制項也能實現,但是使用起來的體驗感並不好,比如要實現一個Excel中的表格效果,估計你能想到的第一個方法就是套Border控制項,用這種方法你需要控制每個Border的邊框,並且在一堆Bordr中找到Grid.Row,Grid. ...
  • .NET C#程式啟動閃退,目錄導致的問題 這是第2次踩這個坑了,很小的編程細節,容易忽略,所以寫個博客,分享給大家。 1.第一次坑:是windows 系統把程式運行成服務,找不到配置文件,原因是以服務運行它的工作目錄是在C:\Windows\System32 2.本次坑:WPF桌面程式通過註冊表設 ...
  • 在分散式系統中,數據的持久化是至關重要的一環。 Orleans 7 引入了強大的持久化功能,使得在分散式環境下管理數據變得更加輕鬆和可靠。 本文將介紹什麼是 Orleans 7 的持久化,如何設置它以及相應的代碼示例。 什麼是 Orleans 7 的持久化? Orleans 7 的持久化是指將 Or ...
  • 前言 .NET Feature Management 是一個用於管理應用程式功能的庫,它可以幫助開發人員在應用程式中輕鬆地添加、移除和管理功能。使用 Feature Management,開發人員可以根據不同用戶、環境或其他條件來動態地控制應用程式中的功能。這使得開發人員可以更靈活地管理應用程式的功 ...
  • 在 WPF 應用程式中,拖放操作是實現用戶交互的重要組成部分。通過拖放操作,用戶可以輕鬆地將數據從一個位置移動到另一個位置,或者將控制項從一個容器移動到另一個容器。然而,WPF 中預設的拖放操作可能並不是那麼好用。為瞭解決這個問題,我們可以自定義一個 Panel 來實現更簡單的拖拽操作。 自定義 Pa ...
  • 在實際使用中,由於涉及到不同編程語言之間互相調用,導致C++ 中的OpenCV與C#中的OpenCvSharp 圖像數據在不同編程語言之間難以有效傳遞。在本文中我們將結合OpenCvSharp源碼實現原理,探究兩種數據之間的通信方式。 ...
  • 一、前言 這是一篇搭建許可權管理系統的系列文章。 隨著網路的發展,信息安全對應任何企業來說都越發的重要,而本系列文章將和大家一起一步一步搭建一個全新的許可權管理系統。 說明:由於搭建一個全新的項目過於繁瑣,所有作者將挑選核心代碼和核心思路進行分享。 二、技術選擇 三、開始設計 1、自主搭建vue前端和. ...
  • Csharper中的表達式樹 這節課來瞭解一下表示式樹是什麼? 在C#中,表達式樹是一種數據結構,它可以表示一些代碼塊,如Lambda表達式或查詢表達式。表達式樹使你能夠查看和操作數據,就像你可以查看和操作代碼一樣。它們通常用於創建動態查詢和解析表達式。 一、認識表達式樹 為什麼要這樣說?它和委托有 ...
  • 在使用Django等框架來操作MySQL時,實際上底層還是通過Python來操作的,首先需要安裝一個驅動程式,在Python3中,驅動程式有多種選擇,比如有pymysql以及mysqlclient等。使用pip命令安裝mysqlclient失敗應如何解決? 安裝的python版本說明 機器同時安裝了 ...