chapter 1: introductionepaper.gotop.com.tw/pdfsample/ael018400.pdf會不斷地更新,gradle...

37
25 Android Studio 是用 Gradle 套件來管理 App 專案。Gradle 的功能非 常強大,但是這也意謂著它的用法有一定的複雜度。建立 App 專案的時 候,Android Studio 會自動幫我們產生正確的 Gradle 設定。但是軟體版本 會不斷地更新, Gradle 也一樣。再加上 Android Build Tools 版本的更 新,以及 Android SDK 程式庫版本的更新。這些軟體版本的更動都會影響 Gradle 的設定。如果沒有正確搭配,建置 App 專案時就會出錯。這些錯誤 常常會讓初學者覺得困惑。因此這個單元我們就來介紹如何設定 Gradle 讓讀者知道如何排除專案建置的錯誤。不過在介紹 Gradle 以前,我們先從 專案的複製和刪除開始。 4-1 App 專案管理技巧 開發 App 專案的過程中,除了新增專案之外,有些時候也需要複製專 案或是刪除專案,以下我們針對專案的管理作一個綜合性的介紹。 1. 複製 App 專案 如果想要複製目前開啟的程式專案,可以在 Android Studio 左邊的 專案檢視視窗中完成,但是要先變更檢視模式。首先點選專案檢視視 窗上方的下拉式選單(參考圖 4-1 ),選擇其中的 Project ,就會在 專案檢視視窗中顯示完整的專案名稱。用滑鼠右鍵點選專案名稱,從 快顯功能表中選擇 Copy 。然後在專案檢視視窗的空白處按下滑鼠右 鍵,再選擇 Paste,就會出現圖 4-2 的對話盒。在 New name 欄位輸 入新專案的名稱,To directory 欄位輸入新專案的儲存路徑。複製後 會在這個路徑中建立一個新的資料夾,資料夾的名稱就是 New name 欄位中的專案名稱。另外記得取消 Open copy in editor 項目,稍後 我們可以利用 Open Project 的方式開啟這個複製的 App 專案。另一 App 專案管理和 Gradle 設定技巧 Lesson 004

Upload: others

Post on 29-May-2020

8 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

25

Android Studio 是用 Gradle 套件來管理 App 專案。Gradle 的功能非

常強大,但是這也意謂著它的用法有一定的複雜度。建立 App 專案的時

候,Android Studio 會自動幫我們產生正確的 Gradle 設定。但是軟體版本

會不斷地更新,Gradle 也一樣。再加上 Android Build Tools 版本的更

新,以及 Android SDK 程式庫版本的更新。這些軟體版本的更動都會影響

Gradle 的設定。如果沒有正確搭配,建置 App 專案時就會出錯。這些錯誤

常常會讓初學者覺得困惑。因此這個單元我們就來介紹如何設定 Gradle,

讓讀者知道如何排除專案建置的錯誤。不過在介紹 Gradle 以前,我們先從

專案的複製和刪除開始。

4-1 App 專案管理技巧

開發 App 專案的過程中,除了新增專案之外,有些時候也需要複製專

案或是刪除專案,以下我們針對專案的管理作一個綜合性的介紹。

1. 複製 App 專案

如果想要複製目前開啟的程式專案,可以在 Android Studio 左邊的

專案檢視視窗中完成,但是要先變更檢視模式。首先點選專案檢視視

窗上方的下拉式選單(參考圖 4-1),選擇其中的 Project,就會在

專案檢視視窗中顯示完整的專案名稱。用滑鼠右鍵點選專案名稱,從

快顯功能表中選擇 Copy。然後在專案檢視視窗的空白處按下滑鼠右

鍵,再選擇 Paste,就會出現圖 4-2 的對話盒。在 New name 欄位輸

入新專案的名稱,To directory 欄位輸入新專案的儲存路徑。複製後

會在這個路徑中建立一個新的資料夾,資料夾的名稱就是 New name

欄位中的專案名稱。另外記得取消 Open copy in editor 項目,稍後

我們可以利用 Open Project 的方式開啟這個複製的 App 專案。另一

App專案管理和Gradle設定技巧

Lesson

004

Page 2: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

26

004

Ap

p

專案管理和G

radle

設定技巧

個做法是利用 Windows 檔案總管複製整個 App 專案的資料夾。複製

後可以變更資料夾名稱,然後就可以用接下來介紹的 import 方式,

開啟複製的專案。

圖 4-1 變更專案檢視視窗的檢視模式

圖 4-2 複製 App 專案的對話盒

複製App專案的訣竅

要複製 App 專案,只要利用 Windows 檔案總管,把專案資料夾複製一份即

可。只是專案中除了程式檔和資源檔,還有很多編譯過程產生的暫存檔,以

及開發環境的暫存檔。這些暫存檔會佔據額外的磁碟空間,其實它們是不需

要複製的。或者如果要把程式專案複製給其他人,也不需要複製這些暫存

檔。為了減少程式專案佔據的磁碟空間。可以把下列的資料夾和檔案刪除:

Page 3: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

27

1. 程式專案資料夾中的「.gradle」、「.idea」和「build」子資料夾。

2. app 資料夾中的 build 子資料夾,請特別注意,app 資料夾不能夠刪除,

只能夠刪除其中的 build 子資料夾。

3. 程式專案資料夾中的「.iml」檔,這個檔案的主檔名和專案資料夾的名稱

相同。如果變更專案資料夾的名稱,會重新建立一個「.iml」檔。

刪除這個檔案和資料夾之後,可以利用 Android Studio 的 Import project 功

能,重新載入 App 專案。

2. 載入(import)App 專案

「開啟(open)專案」和「載入( import)專案」是不同的操作。

開啟專案只能夠用在「完整」的 App 專案,也就是用 Android

Studio 建立,或是之前已經用 Android Studio 開啟過,這種專案已

經有完整的專案設定檔。載入專案則會重新產生專案設定檔。它會依

照目前電腦 Android SDK 的路徑,以及 Gradle 的版本,重新建立專

案設定檔。因此如果用 Windows 檔案總管複製的 App 專案,或是從

另一部電腦複製過來,甚至是從網路上下載的專案,應該用「載入

(import)」的方式開啟。

載入 App 專案有二種方式。如果是在 Android Studio 操作首頁,可

以選擇 Import project (Eclipse ADT, Gradle, etc.)。如果已經開啟某

一個專案,可以選擇主選單 File > New > Import Project。

3. 刪除 App 專案

只要執行 Windows 檔案總管,刪除 App 專案的資料夾即可。下次啟

動 Android Stdio 的時候,就不會在 Recent Projects 清單中出現這個

專案。

Page 4: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

28

004

Ap

p

專案管理和G

radle

設定技巧

4-2 Gradle 設定技巧

圖 4-3 專案檢視視窗 Gradle Scripts 項目的內容

新增一個 App 專案的時候,會自動建立一個名字叫做 app 的模組。我

們可以在 Android Studio 的專案檢視視窗看到 app 這個項目(提示:請確

定專案檢視視窗是在 Android 模式,參考圖 4-1),它就是 app 模組。

app 模組下一項是 Gradle Scripts,將它展開,會看到圖 4-3 的結果。其

中的 build.gradle (Project: MyApplication)是整個專案使用的 Gradle 設定

檔,build.gradle (Module: app)則是 app 模組使用的 Gradle 設定檔。開

啟 build.gradle (Module: app)設定檔,會看到如下的結果。我們在某些項

目後面加上註解(這是 Gradle 設定檔正式的註解方式)。請讀者特別注意

粗體標示的部分,如果設定錯誤,在建置 App 專案時會發生錯誤。

compileSdkVersion 是指定要用哪一個版本的 Android SDK 來編譯。這和

前一個單元介紹的 Android SDK Manager 有關,請讀者參考圖 4-4。

compileSdkVersion 必須設定為已經安裝的 SDK Platform 版本,圖 4-4 中

圈起來的 SDK Platform 的 API 編號是 23,該項後面標示為 Installed,所

以 compileSdkVersion 才可以設定成 23。至於 buildToolsVersion 必須設

定為已經安裝的 Android SDK Build-tools 的版本,圖 4-4 中圈起來的

Build-tools 版本是 24,所以我們可以設定成 24.0.0(有小數點是因為這一

項可能會有次版本編號)。如果日後更新它們的版本,這二項設定必須跟

著調整。

Page 5: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

29

apply plugin: 'com.android.application' android { compileSdkVersion 23 // 指定要用哪一個版本的 Android SDK來編譯 buildToolsVersion "24.0.0" // 指定要用哪一個版本的 Android SDK Build-tools defaultConfig { applicationId "com.myapplication" // 指定這個 App使用的套件路徑 minSdkVersion 15 // 限制 App要在哪一個版本以上的 Android執行 targetSdkVersion 23 // 指定 App要使用哪一個 Android版本的功能 versionCode 1 // 設定專案版本的編號 versionName "1.0" // 設定專案版本的名稱 } buildTypes { …(省略) } } dependencies { …(省略) }

圖 4-4 Android SDK Manager 檢視安裝的 Android SDK 工具版本編號

Page 6: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

215

在設計手機或是平板電腦的 App 時,經常面臨的一個問題,就是不同

裝置的螢幕尺寸大小不一致。當 App 在手機上執行時,由於螢幕畫面比較

小,如果需要顯示比較多的介面元件,就必須將程式畫面適當地切割,再逐

一顯示。但是如果 App 是在螢幕比較大的平板電腦上執行,就可以一次顯

示全部的介面元件。為了適應不同的情況,我們需要一個具有高度彈性的介

面設計機制,讓程式能夠依照執行環境,自動調整操作介面的顯示方式。

針對以上的問題,從 Android 3.0 版開始,新增一個名為 Fragment 的

類別。利用這個新類別,我們可以將程式的畫面分割成數個區域。這些不同

區域的程式畫面可以各自隱藏或顯示,以適應不同螢幕尺寸的裝置。這種

Fragment 型態的程式介面具有以下特性:

1. 程式的執行畫面可以由多個 Fragment 組成;

2. 每一個 Fragment 都有各自獨立的執行狀態,並且接收各自的處理

事件;

3. 在程式執行的過程中,Fragment 可以動態加入和移除。

Fragment 其實是一個類別,它的架構和 Activity 很類似,二者都有自己

的專屬介面佈局檔和程式檔,而且 Fragment 在執行的過程中也會經歷不同

的狀態變化。手機模擬器本身提供一個 Fragment 範例,我們可以點選首頁

的 App 程式集,再選擇 API Demos,然後依序選擇 App > Fragment >

Layout,就會看到圖 27-1 的畫面。乍看之下好像沒什麼特別,可是如果按

下手機模擬器右邊操作列的旋轉按鈕,就會變成圖 27-2 的畫面。這個畫面

的左邊和右邊是二個不同的 Fragment。也就是說,這個範例程式會依照手

機的方向,改變 Fragment 的顯示模式,這就是使用 Fragment 的好處。

使用 Fragment 讓程式介面一分為多

Lesson

027

Page 7: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

216

027

使用Fragm

ent

讓程式介面一分為多

圖 27-1 手機模擬器提供的 Fragment 範例程式

圖 27-2 旋轉手機模擬器後變成左右二個 Fragment 的畫面

27-1 使用 Fragment 的步驟

要在程式中使用 Fragment 需要完成以下步驟:

在專案檢視視窗中,展開 app/java 資料夾,用滑鼠右鍵點選套件路

徑名稱資料夾,然後選擇 New > Fragment > Fragment (Blank),就

會顯示圖 27-3 的對話盒。在 Fragment Name 欄位輸入 Fragment 類

別名稱,Fragment Layout Name 欄位就會自動完成命名。這個欄位

就是 Fragment 介面佈局檔的名稱。它的下方有二個可以勾選的項

Page 8: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

217

目,如果勾選它們,會在 Fragment 的程式檔中加入額外的 callback

方法。現階段我們不會用到這些方法,因此可以取消勾選,最後按下

Finish 按鈕。

Fragment有二個版本?!

Fragment 是 Android 3.0 以後的版本才出現的功能,因此如果以 Android 系

統的支援能力來說,Android 3.0 以前的裝置並無法執行 Fragment 程式。為

了解決這個問題,Google 特別開發一個名為 android-support-v4.jar 的程式

庫,只要把它放到 Android App 專案中,就可以放心地讓程式使用

Fragment,不必擔心舊版的 Android 系統無法支援。但是這個程式庫中的

Fragment,和 Android 系統內建的 Fragment 不可以同時使用。我們在開發

App 專案時,必須從二者之中選擇一個。筆者的建議是使用 android-support-

v4.jar 程式庫的版本,一來是因為可以讓 App 相容 Android 3.0 以前的裝置,

再者是因為程式庫的更新比較快,如果有新的功能,程式庫會比較早更新。

要確定程式使用哪一個版本的 Fragment,只要在程式檔中,展開 import 區

塊,如果是「import android.support.v4.app.Fragment;」,就是使用 android-

support-v4.jar 程式庫的版本。如果是「import android.app.Fragment;」,就是

使用 Android 系統內建的版本。

圖 27-3 新增 Fragment 的對話盒

Page 9: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

218

027

使用Fragm

ent

讓程式介面一分為多

新增的 Fragment 介面佈局檔和程式檔會自動開啟,編輯 Fragment

介面佈局檔的方式和我們前面學過的完全相同,所以讀者可以運用所

有學過的技巧,來編輯這個介面佈局檔。

在 Fragment 的程式檔加入需要用到的狀態處理方法,例如:

1. onCreate()

這是當 Fragment 被建立時會執行的方法,我們可以在這個方法中

進行必要的初始設定。

2. onCreateView()

這是當 Fragment 將要顯示在螢幕上時會執行的方法,我們必須在

這個方法中設定好 Fragment 使用的介面佈局檔。

3. onActivityCreated()

這是當 Fragment 底層的 Activity 被建立時會執行的方法,我們必

須在這個方法中取得 Fragment 的介面元件,並設定給對應的介面

物件,就像是之前在 Activity 的 onCreate()中做的事情一樣。

4. onPause()

這是當 Fragment 要從螢幕上消失時會執行的方法,我們可以在這

個方法中儲存使用者的操作狀態和輸入的資料,以便下次

Fragment 重新顯示時,讓使用者繼續進行目前的工作。

Fragment 和 Activity 一樣,二者都有各式各樣的狀態處理方法,讀者

可以查詢 Android 官方網站的技術文件以取得更多相關資料。如果要

在 Fragment 程式檔中加入狀態處理方法,必須先將編輯游標設定到

要插入的位置,然後按下滑鼠右鍵,從快顯功能表中選擇 Generate

> Override Methods,在顯示的對話盒中點選想要加入的狀態處理方

法,如果要複選,可以按下鍵盤的 Ctrl 鍵。選好之後,按下 OK 按

鈕。有關 Fragment 程式檔的範例,請讀者參考本單元後續建立的程

式專案。

在程式中使用建立好的 Fragment 類別。我們可以利用<fragment>

標籤,在主程式的介面佈局檔中使用指定的 Fragment 。開啟

app/res/layout 資料夾中的主程式介面佈局檔,在 Design 編輯模式

下,從左邊的 Palette 工具箱最下方的 Custom 群組中,點選

<fragment>,就會顯示對話盒讓我們選擇要使用的 Fragment 類

Page 10: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

219

別,點選想要加入的 Fragment 類別,按下 OK 按鈕,就會出現一個

Fragment 元件。我們可以移動滑鼠游標,將它放到想要的位置。如

果切換到 Text 編輯模式,就會看到如下的 Fragment 標籤:

<fragment android:name="com.gameusingfragment.MyFragment" android:id="@+id/fragment" android:layout_width="match_parent" android:layout_height="wrap_content" />

使用<fragment>標籤時要注意以下幾點:

1. <fragment>標籤的開頭字母必須小寫。

2. 每一個<fragment>標籤都要設定 android:id 屬性。

3. <fragment> 標 籤 的 android:name 屬 性 是 指 定 所 使 用 的

Fragment 類別,而且必須加上完整的套件路徑名稱。

4. 在<fragment>標籤中可以使用 android:layout_weight 屬性,用

設定比例的方式控制每一個 Fragment 所佔的螢幕寬度,此時

android:layout_width 屬性必須設定為"0dp"。

以上就是在程式專案中使用 Fragment 的步驟。

27-2 範例程式

本單元的範例程式,是將之前的

「電腦猜拳遊戲」程式,加上局數統計

的功能,並且改成用 Fragment 顯

示。程式中將建立二個 Fragment,一

個用來當成遊戲的主畫面,一個則用

來顯示局數統計資料,程式的執行畫

面如圖 27-4,以下是完成此程式專案

的步驟。

圖 27-4 使用 Fragment 介面的「電腦猜拳遊戲」程式

Page 11: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

220

027

使用Fragm

ent

讓程式介面一分為多

執行 Android Studio 新增一個 App 專案,專案的設定依照慣例

即可。

執行前一小節的操作步驟,建立 Fragment 的介面佈局檔和程式檔。

這個 Fragment 是用來當成程式的主畫面,我們可以將它取名為

MainFragment。接著從單元 21 的「電腦猜拳遊戲」程式專案中,將

主程式的介面佈局檔 app/res/layout/activity_main.xml 的全部內容,

複製到這個 Fragment 的介面佈局檔中。由於遊戲畫面會用到許多字

串資源,因此也要把原來「電腦猜拳遊戲」程式專案中的字串資源檔

內容,複製到這個新專案的字串資源檔。還有剪刀、石頭、布的影像

檔也要複製過來(可以在專案檢視視窗中,用滑鼠右鍵點選檔案,再

選擇 Copy 進行檔案複製。如果要選擇多個檔案,可以按住鍵盤的

Ctrl 鍵,再用滑鼠點選)。

重複前一個步驟的操作,再建立一個顯示遊戲局數的 Fragment 類

別,我們將它取名為 GameResultFragment。以下是這個它的介面佈

局檔的完整內容,讀者可以利用 Design 模式或 Text 模式進行編輯。

<?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="@string/total_set" /> <EditText android:id="@+id/edtCountSet" android:layout_width="match_parent" android:layout_height="wrap_content" android:focusable="false" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/player_win_set" /> <EditText android:id="@+id/edtCountPlayerWin" a android:layout_width="match_parent" android:layout_height="wrap_content"

Page 12: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

221

android:focusable="false" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/comupter_win_set" /> <EditText android:id="@+id/edtCountComWin" android:layout_width="match_parent" android:layout_height="wrap_content" android:focusable="false" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/draw" /> <EditText android:id="@+id/edtCountDraw" android:layout_width="match_parent" android:layout_height="wrap_content" android:focusable="false" /> </LinearLayout>

以上的介面佈局檔會用到定義在字串資源檔 res/values/strings.xml 中

的字串如下:

<?xml version="1.0" encoding="utf-8"?> <resources> …(其它字串) <string name="total_set">全部局數:</string> <string name="player_win_set">玩家贏:</string> <string name="comupter_win_set">電腦贏:</string> <string name="draw">平手:</string> </resources>

利用前一小節介紹的操作方式,在 MainFragment 的程式檔加入

onCreateView() 和 onActivityCreated() 二 個 狀 態 處 理 方 法 。

MainFragment 是負責顯示遊戲的主畫面,所以它的功能和單元 20 的

遊戲主程式相同,因此讀者可以從該專案中複製程式碼再進行修改。

要提醒讀者的是在 Fragment 中必須先呼叫 getView()取得程式畫面物

件,然後才能呼叫它的 findViewById()取得介面物件。如果要取得

Page 13: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

222

027

使用Fragm

ent

讓程式介面一分為多

Fragment 底 層 的 Activity , 可 以 呼 叫 getActivity() 方 法 。 在

onCreateView()方法中,我們利用 inflater 物件的 inflate()方法取得

res/layout/fragment_main.xml 介面佈局檔,並將最後的結果傳回給

系統,這樣就完成了 Fragment 的介面設定。以下是 MainFragment

類別的程式檔:

package … import … public class MainFragment extends Fragment { private ImageButton mImgBtnScissors, mImgBtnStone, mImgBtnPaper; private ImageView mImgViewComPlay; private TextView mTxtResult; private TextView mEdtCountSet, mEdtCountPlayerWin, mEdtCountComWin, mEdtCountDraw; // 新增統計遊戲局數和輸贏的變數 private int miCountSet = 0, miCountPlayerWin = 0, miCountComWin = 0, miCountDraw = 0; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // 利用 inflater物件的inflate()方法取得介面佈局檔,並將最後的結果傳回給系統 return inflater.inflate(R.layout.fragment_main, container, false); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); // 必須先呼叫 getView()取得程式畫面物件,然後才能呼叫它的 // findViewById()取得介面物件 mTxtResult = (TextView) getView().findViewById(R.id.txtResult); mImgBtnScissors = (ImageButton) getView().findViewById(R.id.imgBtnScissors); mImgBtnStone = (ImageButton) getView().findViewById(R.id.imgBtnStone); mImgBtnPaper = (ImageButton) getView().findViewById(R.id.imgBtnPaper);

Page 14: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

223

mImgViewComPlay = (ImageView) getView().findViewById(R.id.imgViewComPlay); // 以下介面元件是在另一個 Fragment中,因此必須呼叫所屬的 Activity // 才能取得這些介面元件 mEdtCountSet = (EditText) getActivity().findViewById(R.id.edtCountSet); mEdtCountPlayerWin = (EditText) getActivity().findViewById(R.id.edtCountPlayerWin); mEdtCountComWin = (EditText) getActivity().findViewById(R.id.edtCountComWin); mEdtCountDraw = (EditText) getActivity().findViewById(R.id.edtCountDraw); mImgBtnScissors.setOnClickListener(imgBtnScissorsOnClick); mImgBtnStone.setOnClickListener(imgBtnStoneOnClick); mImgBtnPaper.setOnClickListener(imgBtnPaperOnClick); } private View.OnClickListener imgBtnScissorsOnClick = new View.OnClickListener() { public void onClick(View v) { // Decide computer play. int iComPlay = (int)(Math.random()*3 + 1); miCountSet++; mEdtCountSet.setText(String.valueOf(miCountSet)); // 1 - scissors, 2 - stone, 3 - net. if (iComPlay == 1) { mImgViewComPlay.setImageResource(R.drawable.scissors); mTxtResult.setText(getString(R.string.result) + getString(R.string.player_draw)); miCountDraw++; mEdtCountDraw.setText(String.valueOf(miCountDraw)); } else if (iComPlay == 2) { mImgViewComPlay.setImageResource(R.drawable.stone); mTxtResult.setText(getString(R.string.result) + getString(R.string.player_lose)); miCountComWin++; mEdtCountComWin.setText(String.valueOf(miCountComWin)); } else { mImgViewComPlay.setImageResource(R.drawable.paper); mTxtResult.setText(getString(R.string.result) + getString(R.string.player_win)); miCountPlayerWin++; mEdtCountPlayerWin.setText (String.valueOf(miCountPlayerWin));

Page 15: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

224

027

使用Fragm

ent

讓程式介面一分為多

} } }; private View.OnClickListener imgBtnStoneOnClick = new View.OnClickListener() { public void onClick(View v) { int iComPlay = (int)(Math.random()*3 + 1); miCountSet++; mEdtCountSet.setText(String.valueOf(miCountSet)); // 1 - scissors, 2 - stone, 3 - net. if (iComPlay == 1) { mImgViewComPlay.setImageResource(R.drawable.scissors); mTxtResult.setText(getString(R.string.result) + getString(R.string.player_win)); miCountPlayerWin++; mEdtCountPlayerWin.setText (String.valueOf(miCountPlayerWin)); } else if (iComPlay == 2) { mImgViewComPlay.setImageResource(R.drawable.stone); mTxtResult.setText(getString(R.string.result) + getString(R.string.player_draw)); miCountDraw++; mEdtCountDraw.setText(String.valueOf(miCountDraw)); } else { mImgViewComPlay.setImageResource(R.drawable.paper); mTxtResult.setText(getString(R.string.result) + getString(R.string.player_lose)); miCountComWin++; mEdtCountComWin.setText(String.valueOf(miCountComWin)); } } }; private View.OnClickListener imgBtnPaperOnClick = new View.OnClickListener() { public void onClick(View v) { int iComPlay = (int)(Math.random()*3 + 1); miCountSet++; mEdtCountSet.setText(String.valueOf(miCountSet)); // 1 - scissors, 2 - stone, 3 - net. if (iComPlay == 1) { mImgViewComPlay.setImageResource(R.drawable.scissors); mTxtResult.setText(getString(R.string.result) + getString(R.string.player_lose));

Page 16: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

225

miCountComWin++; mEdtCountComWin.setText(String.valueOf(miCountComWin)); } else if (iComPlay == 2) { mImgViewComPlay.setImageResource(R.drawable.stone); mTxtResult.setText(getString(R.string.result) + getString(R.string.player_win)); miCountPlayerWin++; mEdtCountPlayerWin.setText (String.valueOf(miCountPlayerWin)); } else { mImgViewComPlay.setImageResource(R.drawable.paper); mTxtResult.setText(getString(R.string.result) + getString(R.string.player_draw)); miCountDraw++; mEdtCountDraw.setText(String.valueOf(miCountDraw)); } } }; }

學習 Android App 開發的過程中,會遇到愈來愈多的 class。每一個 class 都

有許多內定的方法,我們必須視程式的需要 override 某些方法,像是這裡用

到的 onActivityCreated()和 onCreateView()。當要 override 類別內部的方法

時,先將文字輸入游標設定到要插入方法的位置,再按下滑鼠右鍵,選擇快

顯功能表中的 Generate > Override Methods,就會顯示方法清單對話盒讓我

們選擇。如果程式中有不同類別的程式碼,把編輯游標設定到不同類別內

部,可以叫出不同的方法清單對話盒。

切換到 GameResultFragment 的程式檔,把內容編輯如下:

package … import … public class GameResultFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // 利用 inflater物件的 inflate()方法取得介面佈局檔,

Page 17: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

226

027

使用Fragm

ent

讓程式介面一分為多

// 並將最後的結果傳回給系統 return inflater.inflate(R.layout.fragment_game_result, container, false); } }

開啟主程式的介面佈局檔 app/res/layout/activity_main.xml,將內容

編輯如下。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" … (自動產生的屬性設定) android:orientation="horizontal" android:gravity="center"> <fragment android:id="@+id/fragMain" android:name="com.gameusingfragment.MainFragment" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <fragment android:id="@+id/fragGameResult" android:name="com.gameusingfragment.GameResultFragment" android:layout_width="match_parent" android:layout_height="wrap_content" /> </LinearLayout>

這個單元是我們第一次學習 Fragment 的用法,為了不要讓問題太過複

雜,我們先以靜態的方式使用 Fragment。下一個單元我們將進一步學習,

如何在程式執行的過程中,動態加入和移除 Fragment。

Page 18: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

227

前一個單元我們介紹了 Fragment 的基本功能和用法,實作範例是以靜

態的方式使用 Fragment。也就是說,不會在程式執行的過程中讓 Fragment

隱藏或顯示。但是其實 Fragment 最大的優點是動態控制,也就是在程式執

行過程中,可以隨時加入或是移除 Fragment,或是用一個新的 Fragment

取代原來的 Fragment。利用這種方式,我們可以讓程式的操作介面,依照

使用者的喜好,或是執行環境的限制,做出各種變化。想要在程式執行的過

程中動態控制 Fragment,需要用到 FragmentManager,因此接下來就讓我

們來介紹本單元的主角- FragmentManager。

28-1 Fragment 的總管 - FragmentManager

要在程式中改變 Fragment,必須藉助 FragmentManager 物件,

FragmentManager 物件提供以下控制 Fragment 的方法:

1. add()

把 Fragment 加入程式的操作介面,我們可以指定要加入哪一個介面

元件中,例如指定一個 FrameLayout 元件,另外也可以設定這個

Fragment 的 tag 名稱。

2. remove()

從程式的操作介面中移除指定的 Fragment,並將它刪除。

3. replace()

用另一個 Fragment 取代目前程式畫面中的 Fragment,它的功能等

同於先呼叫 remove()移除目前的 Fragment,再呼叫 add()加入另一

個 Fragment。

動態 Fragment 讓程式成為變形金剛

Lesson

028

Page 19: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

228

028

動態Fragm

ent

讓程式成為變形金剛

4. attach()

將之前 detach 的 Fragment 重新顯示在程式畫面,Fragment 的畫面

會重新建立。

5. detach()

將指定的 Fragment 從程式畫面移開,這時候 Fragment 的畫面會被

刪除,等到下次 attach 的時候再重新建立。

6. hide()

隱藏指定的 Fragment。

7. show()

顯示之前隱藏的 Fragment。

8. addToBackStack()

把這個 Fragment 的交易加入系統的 Back Stack,這樣當使用者按下

手機或平板電腦上的「回上一頁」按鍵時,就會回復到之前的狀態。

9. setCustomAnimations()

設定變換 Fragment 時使用的動畫效果。但是要特別注意,如果使用

Android 3.0 以上的系統內建的 FragmentManager ,必須使用

Property Animation。但是如果是用 android.support.v4.app 套件的

FragmentManager,就必須使用 View Animation。

10. commit()

要求系統開始執行 Fragment 的切換,但是要注意,系統是用一個非

同步的 process 來進行,所以 Fragment 的切換不會立即完成。如果

程 式 需 要 立 刻 完 成 Fragment 的 切 換 , 可 以 隨 後 呼 叫

FragmentManager 物件的 executePendingTransactions()方法。

在程式中,改變 Fragment 的過程稱為一個 Transaction(中文的意思

是「交易」)。Transaction 的特性是整個操作必須從頭到尾全部完成才算

數,不能只執行其中一部份。如果不能從頭到尾全部完成,就要回到原來未

執行 Transaction 之前的狀態。在使用 FragmentManager 控制 Fragment 的

過程中,必須依序執行以下幾個步驟:

呼叫 getFragmentManager()方法取得 FragmentManager。

FragmentManager fragmentMgr = getFragmentManager();

Page 20: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

229

呼 叫 FragmentManager 的 beginTransaction() 方 法 建 立 一 個

FragmentTransaction 物件。

FragmentTransaction fragmentTrans = fragmentMgr.beginTransaction();

利用 FragmentTransaction 物件提供的方法,控制 Fragment。請讀

者參考以下程式碼範例,其中的 MyFragmentA 和 MyFragmentB 是

在程式專案中,已經建立好的二個 Fragment 類別。frameLay 是在主

程 式 介 面 佈 局 檔 中 的 FrameLayout 元 件 , 通 常 我 們 都 是 用

FrameLayout 當成 Fragment 的容器。也就是說,把 Fragment 物件

放到 FrameLayout 元件裡頭。

MyFragmentA myFragmentA = new MyFragmentA(); MyFragmentB myFragmentB = new MyFragmentB(); fragmentTrans.add(R.id.frameLay, myFragmentA, "My fragment A"); fragmentTrans.add(R.id.frameLay, myFragmentB, "My fragment B");

在呼叫 add()方法之前,我們必須先建立 Fragment 物件。呼叫 add()方

法時,必須指定要將 Fragment 加入哪一個介面元件中(這個介面元件就稱

為 Fragment 的容器)。另外我們也可以幫這個新加入的 Fragment 取一個

名稱(稱為 Tag,就是 add()方法的第三個參數),這個 Tag 可以讓我們找

到特定的 Fragment。

使用new運算子或是newInstance()靜態方法建立Fragment物件

Android Developers 官方網站的技術文件,建議使用 Fragment 類別的

newInstance()靜態方法來建立 Fragment 物件,避免使用 new 運算子,這是考

量 Fragment 類別的建構式含有引數的情形。要解釋其中的原因,必須先瞭解

Android 系統在運作的過程中,有時候會強制銷毀 Fragment 物件,再重新建

立它,例如當手機或是平板電腦從直立轉成水平時,App 會被強制結束,再

重新啟動。當 Android 系統要重新建立 Fragment 物件的時候,會執行空的建

構式(也就是不含任何引數的建構式),再將銷毀 Fragment 物件時儲存的

Bundle 物件,傳給 Fragment 的 onCreate()方法。這樣 Fragment 物件就可以

回復到原來銷毀時的狀態(我們的程式要自己從 Bundle 物件中取出資料來使

用),也就是說每一個 Fragment 類別都要提供一個空的建構式讓系統呼叫。

如果要在建立 Fragment 物件時傳入引數,正確的做法是定義一個

newInstance()靜態方法,用來接收傳入的引數,在這個 newInstance()方法

中,利用 new 運算子建立一個 Fragment 物件,並且把傳入的引數儲存在一

Page 21: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

230

028

動態Fragm

ent

讓程式成為變形金剛

個 Bundle 物件裡頭,再把這個 Bundle 物件設定給 Fragment 物件。然後在

Fragment 的 onCreate()狀態處理方法中,取得 Bundle 物件,再取出其中的資

料。如果建立 Fragment 物件時不需要傳入任何引數,就可以直接利用 new

運算子來建立,如同本單元的範例程式。

呼叫 FragmentTransaction 的 commit()方法,啟動 Fragment 處理流

程。但是要注意,Android 系統並不是立刻執行,而是以執行緒

( background thread)的方式來處理。如果我們希望立刻執行

Fragment 的處理流程,必須再執行下一個步驟。

fragmentTrans.commit();

如果程式需要立刻執行 Fragment 的處理,可以呼叫

FragmentManager 的 executePendingTransactions()方法。

fragmentMgr.executePendingTransactions();

我們可以利用之前介紹過的「匿名物件」語法,將上述操作 Fragment

的程式碼簡化如下: getFragmentManager().beginTransaction() .add(R.id.frameLay, myFragmentA, "My fragment A") .add(R.id.frameLay, myFragmentB, "My fragment B") .commit();

28-2 範例程式

上一個單元的「電腦猜拳遊戲」,是用二個 Fragment 組成操作介面。

其中一個 Fragment 用來顯示操作元件,另一個則顯示局數統計資料。這二

個 Fragment 都是固定出現在程式畫面中。這個單元我們將把局數統計資料

的 Fragment 改成可以動態顯示和隱藏,並且新增第二種局數統計資料的顯

示方式,讓使用者可以依喜好自由選擇。圖 28-1 是程式的執行畫面,一開

始並不會顯示局數統計資料,必須按下「顯示結果 1」或「顯示結果 2」按

鈕才會出現局數統計畫面。以下是完成這個程式專案的詳細步驟:

Page 22: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

231

圖 28-1 使用動態 Fragment 的「電腦猜拳遊戲」程式

利用 Windows 檔案總管複製前一個單元的程式專案,再將複製的資

料夾改成想要的名稱。然後利用 Android Studio 的 Import Project 功

能,載入複製的 App 專案。

依照前一個單元的操作方法,新增一個 Fragment,我們可以將它取

名為 GameResult2Fragment,它是用來當作第二種局數統計資料的

畫面。我們在這個新的介面佈局檔中,換成用 TableLayout 顯示局數

統計資料。請將介面佈局檔編輯如下,可以先用 Design 模式加入介

面元件,再切換到 Text 模式設定屬性。

<?xml version="1.0" encoding="utf-8"?> <TableLayout xmlns:android="http://schemas.android.com/apk/res/android" … (自動產生的屬性設定) android:layout_gravity="center_horizontal" > <TableRow> <TextView android:text="@string/total_set" android:textSize="20sp" android:textColor="#0FFFFF" /> <EditText android:id="@+id/edtCountSet" android:focusable="false" android:layout_weight="1" /> </TableRow>

Page 23: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

232

028

動態Fragm

ent

讓程式成為變形金剛

<TableRow> <TextView android:text="@string/player_win_set" android:textSize="20sp" android:textColor="#0FFFFF" /> <EditText android:id="@+id/edtCountPlayerWin" android:focusable="false" android:layout_weight="1" /> </TableRow> <TableRow> <TextView android:text="@string/comupter_win_set" android:textSize="20sp" android:textColor="#0FFFFF" /> <EditText android:id="@+id/edtCountComWin" android:focusable="false" android:layout_weight="1" /> </TableRow> <TableRow> <TextView android:text="@string/draw" android:textSize="20sp" android:textColor="#0FFFFF" /> <EditText android:id="@+id/edtCountDraw" android:focusable="false" android:layout_weight="1" /> </TableRow> </TableLayout>

我們要先將 MainFragment 中,用來顯示局數統計的 TextView 物件改成

public,這樣一來 GameResultFragment 和 GameResult2Fragment 的

程式碼就可以直接使用。

public class MainFragment extends Fragment { private ImageButton mImgBtnScissors, mImgBtnStone, mImgBtnPaper; private ImageView mImgViewComPlay; private TextView mTxtResult; public EditText mEdtCountSet, mEdtCountPlayerWin, mEdtCountComWin, mEdtCountDraw; … (其它程式碼) }

Page 24: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

233

將 GameResult2Fragment 的程式檔編輯如下:

package … import … public class GameResult2Fragment extends Fragment { public GameResult2Fragment() { // Required empty public constructor } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // 利用 inflater物件的 inflate()方法取得介面佈局檔, // 並將最後的結果傳回給系統 return inflater.inflate(R.layout.fragment_game_result2, container, false); } @Override public void onResume() { super.onResume(); MainFragment frag = (MainFragment) getFragmentManager() .findFragmentById(R.id.fragMain); frag.mEdtCountSet = (EditText) getActivity(). findViewById(R.id.edtCountSet); frag.mEdtCountPlayerWin = (EditText) getActivity() .findViewById(R.id.edtCountPlayerWin); frag.mEdtCountComWin = (EditText) getActivity() .findViewById(R.id.edtCountComWin); frag.mEdtCountDraw = (EditText)getActivity(). findViewById(R.id.edtCountDraw); } }

以 上 的 程 式 碼 除 了 onCreateView() 方 法 之 外 , 還 多 了 一 個

onResume()方法(操作提示:在程式碼中插入新方法的操作步驟可

以參考上一個單元的說明)。這個方法裡頭的程式碼是設定用來顯示

局數統計資料的介面物件,這些介面物件是宣告在 MainFragment 類

別中,而且必須等使用者開啟局數統計的 Fragment 之後才能使用,

Page 25: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

234

028

動態Fragm

ent

讓程式成為變形金剛

因此我們必須在 onResume()中執行(未開啟局數統計 Fragment 之

前這些介面物件並不存在,因此也無法使用)。

當使用者建立一個 GameResult2Fragment 物件,並將它加入程式畫面

時,onCreateView()方法會先執行。當 GameResult2Fragment 物件即

將顯示在螢幕上時,會執行 onResume()方法,因此我們在 onResume()

方法中設定好介面物件。這裡我們使用 FragmentManager 的

findFragmentById()方法取得程式中的 MainFragment 物件,再將其中

宣告的介面物件設定成對應的介面元件。

在 Android Studio 左邊的專案檢視視窗中,展開「app/java/(套件路

徑名稱)/GameResultFragment」程式檔,將它開啟,再依照同樣的

方法加入 onResume(),然後加入下列粗體標示的程式碼。這一段程

式 碼 和 上 一 個 步 驟 的 程 式 碼 完 全 相 同 , 也 就 是 說

GameResultFragment 和 GameResult2Fragment,除了元件排列方

式不同之外,內部的功能完全相同。

package … import … public class GameResultFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // 利用 inflater物件的 inflate()方法取得介面佈局檔, // 並將最後的結果傳回給系統 return inflater.inflate(R.layout.fragment_game_result, container, false); } @Override public void onResume() { super.onResume(); MainFragment frag = (MainFragment) getFragmentManager() .findFragmentById(R.id.fragMain); frag.mEdtCountSet = (EditText) getActivity().findViewById(R.id.edtCountSet); frag.mEdtCountPlayerWin = (EditText) getActivity() .findViewById(R.id.edtCountPlayerWin); frag.mEdtCountComWin = (EditText) getActivity()

Page 26: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

235

.findViewById(R.id.edtCountComWin); frag.mEdtCountDraw = (EditText)getActivity(). findViewById(R.id.edtCountDraw); } }

開啟主程式介面佈局檔 app/res/layout/activity_main.xml,刪除第二

個<fragment>標籤,然後在同樣的位置加入一個<FrameLayout>標

籤,並且設定屬性如下。這個 FrameLayout 元件將作為 Fragment 物

件的容器,我們會在程式碼中,依照使用者的操作,把不同的

Fragment 放進這個 FrameLayout 元件。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/LinearLayout1" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context=".MainActivity" android:gravity="center_horizontal" > <fragment android:id="@+id/fragMain" android:name="com.android.MainFragment" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <FrameLayout android:id="@+id/frameLay" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="50dp" android:padding="20dp" /> </LinearLayout>

開啟介面佈局檔 app/res/layout/fragment_main.xml,我們在最後加

入三個按鈕,使用者可以利用這些按鈕啟動二種局數統計資料畫面,

或是隱藏局數統計資料畫面。這裡我們用到許多控制介面元件位置和

外觀的屬性,以增加程式畫面的美觀。

Page 27: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

236

028

動態Fragm

ent

讓程式成為變形金剛

<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="400dp" android:layout_height="match_parent" android:layout_gravity="center_horizontal" > …(原來的程式碼) <Button android:id="@+id/btnShowResult1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/btn_show_result1" android:layout_below="@id/txtResult" android:layout_alignLeft="@id/txtCom" android:textSize="20sp" android:layout_marginTop="10dp" /> <Button android:id="@+id/btnShowResult2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/btn_show_result2" android:layout_below="@id/txtResult" android:layout_alignLeft="@id/imgBtnPaper" android:textSize="20sp" android:layout_marginTop="10dp" /> <Button android:id="@+id/btnHiddenResult" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/btn_hidden_result" android:layout_below="@id/btnShowResult1" android:layout_centerInParent="true" android:textSize="20sp" android:layout_marginTop="10dp" /> </RelativeLayout>

這三個新按鈕用到定義在字串資源檔中的字串:

<?xml version="1.0" encoding="utf-8"?> <resources> …(原來的字串) <string name="btn_show_result1">顯示結果 1</string> <string name="btn_show_result2">顯示結果 2</string> <string name="btn_hidden_result">隱藏結果</string> </resources>

Page 28: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

237

開啟負責顯示遊戲畫面的 Fragment 程式檔 src/(套件路徑名稱 )/

MainFragment.java,我們在裡頭加入前一個步驟新增的三個按鈕所

對應的物件,並設定好它們的 OnClickListener,如以下粗體字的部

分。此外為了記錄程式是否開啟局數統計的顯示畫面,我們新增一個

mbShowResult 變數,並在三個按鈕的 OnClickListener 中對它進行

設定。在這些按鈕的 OnClickListener 中,我們依照上一個小節介紹

的 Fragment 操作技巧,分別顯示或隱藏局數統計畫面。最後在「剪

刀」、「石頭」和「布」的按鈕的 OnClickListener 中,將顯示局數

統計資料的程式碼集中放到最後,只有當 mbShowResult 變數的值是

true 的時候才顯示。

package … import … public class MainFragment extends Fragment { private ImageButton mImgBtnScissors, mImgBtnStone, mImgBtnPaper; private ImageView mImgViewComPlay; private TextView mTxtResult; public EditText mEdtCountSet, mEdtCountPlayerWin, mEdtCountComWin, mEdtCountDraw; // 新增統計遊戲局數和輸贏的變數 private int miCountSet = 0, miCountPlayerWin = 0, miCountComWin = 0, miCountDraw = 0; private Button mBtnShowResult1, mBtnShowResult2, mBtnHiddenResult; private boolean mbShowResult = false; private final static String TAG_FRAGMENT_RESULT_1 = "Result 1", TAG_FRAGMENT_RESULT_2 = "Result 2"; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

Page 29: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

238

028

動態Fragm

ent

讓程式成為變形金剛

// TODO Auto-generated method stub // 利用 inflater物件的 inflate()方法取得介面佈局檔, //並將最後的結果傳回給系統 return inflater.inflate(R.layout.fragment_main, container, false); } @Override public void onActivityCreated(Bundle savedInstanceState) { // TODO Auto-generated method stub super.onActivityCreated(savedInstanceState); // 必須先呼叫 getView()取得程式畫面物件,然後才能呼叫它的 // findViewById()取得介面物件 mTxtResult = (TextView) getView().findViewById(R.id.txtResult); mImgBtnScissors = (ImageButton) getView().findViewById (R.id.imgBtnScissors); mImgBtnStone = (ImageButton) getView().findViewById (R.id.imgBtnStone); mImgBtnPaper = (ImageButton) getView().findViewById (R.id.imgBtnPaper); mImgViewComPlay = (ImageView) getView().findViewById (R.id.imgViewComPlay); // 以下介面元件是在另一個 Fragment中,因此必須呼叫所屬的 Activity // 才能取得這些介面元件 /* mEdtCountSet = (EditText) getActivity().findViewById (R.id.edtCountSet); mEdtCountPlayerWin = (EditText) getActivity().findViewById(R.id.edtCountPlayerWin); mEdtCountComWin = (EditText) getActivity().findViewById (R.id.edtCountComWin); mEdtCountDraw = (EditText) getActivity().findViewById (R.id.edtCountDraw); */ mImgBtnScissors.setOnClickListener(imgBtnScissorsOnClick); mImgBtnStone.setOnClickListener(imgBtnStoneOnClick); mImgBtnPaper.setOnClickListener(imgBtnPaperOnClick); mBtnShowResult1 = (Button) getView().findViewById (R.id.btnShowResult1); mBtnShowResult2 = (Button) getView().findViewById (R.id.btnShowResult2); mBtnHiddenResult = (Button) getView().findViewById (R.id.btnHiddenResult); mBtnShowResult1.setOnClickListener(btnShowResult1OnClick); mBtnShowResult2.setOnClickListener(btnShowResult2OnClick); mBtnHiddenResult.setOnClickListener(btnHiddenResultOnClick); }

Page 30: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

239

private View.OnClickListener imgBtnScissorsOnClick = new View.OnClickListener() { public void onClick(View v) { // Decide computer play. int iComPlay = (int)(Math.random()*3 + 1); miCountSet++; // mEdtCountSet.setText(String.valueOf(miCountSet)); // 1 - scissors, 2 - stone, 3 - net. if (iComPlay == 1) { mImgViewComPlay.setImageResource(R.drawable.scissors); mTxtResult.setText(getString(R.string.result) + getString(R.string.player_draw)); miCountDraw++; // mEdtCountDraw.setText(String.valueOf(miCountDraw)); } else if (iComPlay == 2) { mImgViewComPlay.setImageResource(R.drawable.stone); mTxtResult.setText(getString(R.string.result) + getString(R.string.player_lose)); miCountComWin++; // mEdtCountComWin.setText(String.valueOf(miCountComWin)); } else { mImgViewComPlay.setImageResource(R.drawable.paper); mTxtResult.setText(getString(R.string.result) + getString(R.string.player_win)); miCountPlayerWin++; // mEdtCountPlayerWin.setText(String.valueOf(miCountPlayerWin)); } if (mbShowResult) { mEdtCountSet.setText(String.valueOf(miCountSet)); mEdtCountDraw.setText(String.valueOf(miCountDraw)); mEdtCountComWin.setText(String.valueOf(miCountComWin)); mEdtCountPlayerWin.setText(String.valueOf(miCountPlayerWin)); } } }; private View.OnClickListener imgBtnStoneOnClick = new View.OnClickListener() { public void onClick(View v) { …(參考前面 btnScissorsLin物件的程式碼修改) } }; private View.OnClickListener imgBtnPaperOnClick =

Page 31: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

240

028

動態Fragm

ent

讓程式成為變形金剛

new View.OnClickListener() { public void onClick(View v) { …(參考前面 btnScissorsLin物件的程式碼修改) } }; private View.OnClickListener btnShowResult1OnClick = new View.OnClickListener() { public void onClick(View v) { GameResultFragment fragGameResult = new GameResultFragment(); FragmentTransaction fragTran = getFragmentManager().beginTransaction(); fragTran.replace(R.id.frameLay, fragGameResult, TAG_FRAGMENT_RESULT_1); fragTran.commit(); mbShowResult = true; } }; private View.OnClickListener btnShowResult2OnClick = new View.OnClickListener() { public void onClick(View v) { GameResult2Fragment fragGameResult2 = new GameResult2Fragment(); FragmentTransaction fragTran = getFragmentManager().beginTransaction(); fragTran.replace(R.id.frameLay, fragGameResult2, TAG_FRAGMENT_RESULT_2); fragTran.commit(); mbShowResult = true; } }; private View.OnClickListener btnHiddenResultOnClick = new View.OnClickListener() { public void onClick(View v) { mbShowResult = false; FragmentManager fragMgr = getFragmentManager(); GameResultFragment fragGameResult = (GameResultFragment) fragMgr.findFragmentByTag(TAG_ FRAGMENT_RESULT_1); if (null != fragGameResult) { FragmentTransaction fragTran = fragMgr.beginTransaction(); fragTran.remove(fragGameResult); fragTran.commit();

Page 32: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

241

return; } GameResult2Fragment fragGameResult2 = (GameResult2Fragment) fragMgr.findFragmentByTag (TAG_FRAGMENT_RESULT_2); if (null != fragGameResult2) { FragmentTransaction fragTran = fragMgr.beginTransaction(); fragTran.remove(fragGameResult2); fragTran.commit(); return; } } }; }

完成程式專案的修改之後,啟動執行。由於這個 App 的介面元件比較

多,如果模擬器的螢幕太小,會看不到完整的畫面。如果發生這種情況,可

以把每一個介面佈局檔中的 padding 和 margin 相關屬性全部刪除,或是將

文字大小設定成比較小的值。看到程式的執行畫面後,分別按下二個不同局

數統計畫面的按鈕,將會看到如圖 28-2 和 28-3 的結果。如果要取消局數統

計畫面,可以按下「隱藏結果」按鈕。這個單元示範動態控制 Fragment 的

方法,另外我們還可以針對 Fragment 的操作提供回復(back stack)的功

能,這些進階的 Fragment 使用技巧我們留待下一個單元再繼續介紹。

圖 28-2 第一種顯示局數統計資料的畫面 圖 28-3 第二種顯示局數統計資料的畫面

Page 33: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

289

SeekBar 介面元件的功能類似 MS Word 程式中用來瀏覽文件的捲軸,

捲軸上有一個可以拖曳的控制鈕,捲軸的長度代表文件全部的範圍,控制鈕

則代表目前在文件中的位置,圖 34-1 是一個 SeekBar 元件的範例。要建立

SeekBar 元件只要在程式專案的程式介面佈局檔 res/layout/activity_main.xml

中增加一個<SeekBar …/>標籤即可,圖 34-1 就是利用以下的介面元件程

式碼所產生: <SeekBar android:id="@+id/seekBar" android:layout_width="match_parent" android:layout_height="wrap_content" android:max="100" android:progress="80" />

圖 34-1 SeekBar 元件的範例

SeekBar 元件上的控制鈕可以讓使用者拖曳,程式會根據它的位置來調

整 顯 示 的 內 容 。 我 們 必 須 在 程 式 中 建 立 一 個 SeekBar 元 件 的

OnSeekBarChangeListener 物件,當使用者改變了 SeekBar 上的控制鈕的

位 置 時 , Android 系 統 就 會 執 行 該 物 件 中 的 程 式 碼 。 在 這 個

OnSeekBarChangeListener 物件中,我們需要建立以下 3 個方法: public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { // 使用者改變 SeekBar上的控制鈕位置時執行。 } public void onStartTrackingTouch(SeekBar seekBar) { // 使用者按下 SeekBar上的控制鈕,準備要開始拖曳時執行。 } public void onStopTrackingTouch(SeekBar seekBar) { // 使用者放開 SeekBar上的控制鈕時執行。 }

SeekBar 和RatingBar 介面元件

Lesson

034

Page 34: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

290

034

SeekBar

和RatingBar

介面元件

第一個方法 onProgressChanged()是當使用者改變了 SeekBar 上的控

制鈕位置時執行,後面二個方法是當控制鈕被按下準備拖曳時,和控制鈕被

放開時執行。在一般情況下,我們只需要處理 onProgressChanged()這個方

法即可,它輸入的引數包括所操作的 SeekBar 物件,和目前控制鈕的位置

(progress 引數的值)。

RatingBar 介面元件的外觀比較特別,它像是經常用來評比商品的一排

星星。在評比的時候,可能會用到一顆完整的星星或是半顆星星,

RatingBar 元件也是一樣,如圖 34-2。建立 RatingBar 元件的方法是在介面

佈局檔中加上<RatingBar …/>標籤如以下範例: <RatingBar android:id="@+id/ratingBar" android:layout_width="wrap_content" android:layout_height="wrap_content" style="?android:attr/ratingBarStyle" android:numStars="5" android:rating="3.5" />

圖 34-2 RatingBar 介面元件的範例

RatingBar 元件可以利用 style 屬性來設定型態,ratingBarStyle 是預設型

態,我們也可以設定為 ratingBarStyleSmall 或是 ratingBarStyleIndicator。

ratingBarStyleSmall 表 示 要 使 用 小 的 RatingBar , 如 果 設 定 為

ratingBarStyleIndicator,表示只用來顯示評分值,使用者不能夠對它進行變

更。android:numStars 屬性可以用來設定星星的數目,android:rating 屬性

則是設定目前的評分值。評分值可以有小數點(SeekBar 元件的 progress

屬性不可以有小數點)。其實 RatingBar 元件的評分值也可以用 progress

屬性來表示,progress 屬性和 rating 屬性之間的關係是,progress 屬性值

剛好是 rating 屬性值的兩倍,也就是說如果 rating 是 0.5 那麼 progress 就

是 1,如果 rating 是 1 那麼 progress 就是 2,依此類推。程式碼中可以呼叫

RatingBar 元件的 setRating()方法改變 rating 的值,使用者也可以用滑鼠按

下 RatingBar 元件上的星星來設定 rating 的值。如果程式要取得使用者的設

定值,就要建立一個 OnRatingBarChangeListener 物件,該物件中需要建立

以下方法。方法中的第一個引數是目前操作的 RatingBar 元件,第二個引數

Page 35: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

291

是使用者設定的評分值。如果需要取得 progress 的值,可以呼叫

getProgress()方法。 public void onRatingChanged(RatingBar ratingBar, float rating, boolean fromUser) { // 當 RatingBar上的評分值改變時執行。 }

接下來我們用一個範例程式來示範 SeekBar 和 RatingBar 的功能,程式

的執行畫面如圖 34-3。使用者可以拖曳 SeekBar 元件上的控制鈕,下方的

說明文字會即時更新 progress 的值。使用者也可以用滑鼠點選下方的

RatinBar 元件上的星星,下方的說明文字會顯示目前設定的 rating 值和

progress 值。這個程式的字串資源檔、介面佈局檔和程式檔如下,程式碼的

部分主要是 SeekBar 和 RatingBar 這二個介面元件的事件處理程序,請讀者

特別留意粗體字的部分。

圖 34-3 SeekBar 和 RatingBar 範例程式的執行畫面

字串資源檔 <resources> <string name="app_name">SeekBar和 RatingBar</string> <string name="seek_bar_progress">SeekBar的 Progress值:</string> <string name="rating_bar_value">RatingBar的 Rating值:</string> <string name="rating_bar_progress">RatingBar的 Progress值:</string> </resources>

介面佈局檔 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" …(自動產生的屬性設定) android:orientation="vertical" > <SeekBar

Page 36: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

292

034

SeekBar

和RatingBar

介面元件

android:id="@+id/seekBar" android:layout_width="match_parent" android:layout_height="wrap_content" android:max="100" android:layout_marginTop="20dp" /> <TextView android:id="@+id/txtSeekBarProgress" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/seek_bar_progress" android:textSize="20sp" /> <RatingBar android:id="@+id/ratingBar" android:layout_width="wrap_content" android:layout_height="wrap_content" style="?android:attr/ratingBarStyle" android:numStars="5" android:layout_marginTop="20dp" /> <TextView android:id="@+id/txtRatingBarValue" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/rating_bar_value" android:textSize="20sp" /> <TextView android:id="@+id/txtRatingBarProgress" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/seek_bar_progress" android:textSize="20sp" /> </LinearLayout>

程式檔 package … import … public class MainActivity extends AppCompatActivity { private RatingBar mRatingBar; private SeekBar mSeekBar; private TextView mTxtSeekBarProgress, mTxtRatingBarValue, mTxtRatingBarProgress;

Page 37: Chapter 1: Introductionepaper.gotop.com.tw/PDFSample/AEL018400.pdf會不斷地更新,Gradle 也一樣。再加上 Android Build Tools版本的更 新,以及Android SDK程式庫版本的更新。這些軟體版本的更動都會影響

293

@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mRatingBar = (RatingBar) findViewById(R.id.ratingBar); mSeekBar = (SeekBar) findViewById(R.id.seekBar); mTxtSeekBarProgress = (TextView) findViewById(R.id.txtSeekBarProgress); mTxtRatingBarValue = (TextView) findViewById(R.id.txtRatingBarValue); mTxtRatingBarProgress = (TextView) findViewById(R.id.txtRatingBarProgress); mSeekBar.setOnSeekBarChangeListener(seekBarOnChange); mRatingBar.setOnRatingBarChangeListener(ratingBarOnChange); } private SeekBar.OnSeekBarChangeListener seekBarOnChange = new SeekBar.OnSeekBarChangeListener() { public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { String s = getString(R.string.seek_bar_progress); mTxtSeekBarProgress.setText(s + String.valueOf(progress)); } public void onStartTrackingTouch(SeekBar seekBar) { } public void onStopTrackingTouch(SeekBar seekBar) { } }; private RatingBar.OnRatingBarChangeListener ratingBarOnChange = new RatingBar.OnRatingBarChangeListener() { @Override public void onRatingChanged(RatingBar ratingBar, float rating, boolean fromUser) { String s = getString(R.string.rating_bar_value); mTxtRatingBarValue.setText(s + String.valueOf(rating)); s = getString(R.string.rating_bar_progress); mTxtRatingBarProgress.setText(s + String.valueOf(mRatingBar.getProgress())); } }; }