XAML in Xamarin.Forms 基礎篇 電子書

XAML in Xamarin.Forms 基礎篇 電子書
XAML in Xamarin.Forms 基礎篇 電子書

Xamarin.Forms 快速入門 電子書

Xamarin.Forms 快速入門 電子書
Xamarin.Forms 快速入門 電子書

2015/10/04

Async Await , C# 編譯器做了些甚麼事情呢–2?

在上一篇文章中 Async Await , C# 編譯器做了些甚麼事情呢– 1,我們將 WPF 內的非同步程式碼,透過編譯器的處理,產生了相對應的狀態機程式碼,並且在產生後的程式碼中,加入了詳盡的註解說明,並且說明了編譯器的處理過程。

在這篇文章中,我們將要補足上篇文章的一些不足點,那就是,在我們呼叫非同步程式之前,也就是 await 關鍵字之前,若還有些 C# 程式碼的話,而且,在這 await 關鍵字之後,繼續有使用到這些變數,那麼,在編譯器中所產生的狀態機物件會如何呈現?

(至於,同一個非同步方法中,若有多個 await 關鍵字,則狀態機內會使用 goto 來切換到不同的部分還處理,有興趣的人,可以自行針對編譯器產生結果程式碼來觀察)

底下為我在 WPF 的按鈕事件內呼叫一個非同步方法,在呼叫非同步方法之前,我們會計算 x * y * z 的數學運算結果到本地變數 total 內;並且在呼叫完成非同步方法之後,再將 total 變數的值,輸出到 Console上。

        private async void btnDownload2_Click(object sender, RoutedEventArgs e)
{
int
x = 3, y = 1, z = 2, total = 0;
total = x * y * z;

string
fooStr = await GetString2Async();
Console.WriteLine(
string.Format("{0} {1}", total,fooStr));
}

接著我們來看看,經過了編譯器的轉換,狀態機的類別,會如何定義呢?


我們特別將呼叫 await 非同步方法前的程式碼使用黃底色標示出來,而呼叫完成非同步方法之後的程式碼使用橘底色標示出來,您可以參考上一篇文章 Async Await , C# 編譯器做了些甚麼事情呢– 1 ,試著找出其中異同點。

        [CompilerGenerated]
private sealed class
<btnDownload2_Click>d__3 : IAsyncStateMachine
{
public int
<>1__state;

public
AsyncVoidMethodBuilder <>t__builder;

public object
sender;

public
RoutedEventArgs e;

public
MainWindow <>4__this;

private int
<x>5__1;

private int
<y>5__2;

private int
<z>5__3;

private int
<total>5__4;

private string
<fooStr>5__5;

private string
<>s__6;

private TaskAwaiter<string
> <>u__1;

void
IAsyncStateMachine.MoveNext()
{
int num = this
.<>1__state;
try
{
TaskAwaiter<
string
> taskAwaiter;
if
(num != 0)
{
this
.<x>5__1 = 3;
this
.<y>5__2 = 1;
this
.<z>5__3 = 2;
this
.<total>5__4 = 0;
this.<total>5__4 = this.<x>5__1 * this.<y>5__2 * this
.<z>5__3;
taskAwaiter =
this
.<>4__this.GetString2Async().GetAwaiter();
if
(!taskAwaiter.IsCompleted)
{
this
.<>1__state = 0;
this
.<>u__1 = taskAwaiter;
MainWindow.<btnDownload2_Click>d__3 <btnDownload2_Click>d__ =
this
;
this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<string>, MainWindow.<btnDownload2_Click>d__3>(ref taskAwaiter, ref
<btnDownload2_Click>d__);
return
;
}
}
else
{
taskAwaiter =
this
.<>u__1;
this.<>u__1 = default(TaskAwaiter<string
>);
this
.<>1__state = -1;
}
string
result = taskAwaiter.GetResult();
taskAwaiter =
default(TaskAwaiter<string
>);
this
.<>s__6 = result;
this.<fooStr>5__5 = this
.<>s__6;
this.<>s__6 = null
;
Console.WriteLine(
string.Format("{0} {1}", this.<total>5__4, this
.<fooStr>5__5));
}
catch
(Exception exception)
{
this
.<>1__state = -2;
this
.<>t__builder.SetException(exception);
return
;
}
this
.<>1__state = -2;
this
.<>t__builder.SetResult();
}

[DebuggerHidden]
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
}
}

Async Await , C# 編譯器做了些甚麼事情呢–1 ?

在這篇文章,我將會解析 Async/Await 的非同步方法的神秘面紗,當我們使用 Async / Await 來進行非同步方法定義的時候,此時您所寫出來的程式碼,並不是最後程式要執行的程式碼,編譯器會自動幫您重寫您所定義的非同步方法,並且加入了 State Machine 狀態機物件到您的程式內,透過自動產生的 State Machine,讓您的非同步呼叫完成之後,可以回到原先的呼叫的地方,以便繼續執行下去。
要了解這篇文章,首先,您需要了解甚麼是 狀態機
底下為該WPF上 MainWindows 的類別定義,在這個類別定義中,我們定義了一個按鈕的事件,當使用者按下了這個按鈕,會呼叫我們自訂的方法,透過 Async/Await 的非同步方式,取得特定網址的網頁,該非同步方法會回傳所取得網頁的字串到按鈕事件內,接著,在按鈕事件內會將這個字串從 Console內列印出來。
在非同步方法中 GetStringAsync中,我們使用了 HttpClient 物件,呼叫了 GetStringAsync 非同步方法,取得了微軟首頁的內容。
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private async void btnDownload_Click(object sender, RoutedEventArgs e)
        {
            string fooStr = await GetStringAsync();
            Console.WriteLine(fooStr);
        }

        public async Task<string> GetStringAsync()
        {
            HttpClient client = new HttpClient();
            var result = await client.GetStringAsync("http://www.microsoft.com");
            return result;
        }
    }

接著,我們來看看,對於我們經常使用的 Async / Await 非同步呼叫,編譯器究竟做了甚麼處理,可以讓我們在寫非同步方法的時候,既然可以像使用同步方式來寫程式碼,變得這麼方便、好用。

我們先來看看按鈕事件的方法:
        private async void btnDownload_Click(object sender, RoutedEventArgs e)
        {
            string fooStr = await GetStringAsync();
            Console.WriteLine(fooStr);
        }


底下為編譯器處理過的上述方法,我們從底下程式碼中,看不到原來事件方法的任何程式碼了,例如: Console.WriteLine 。

我已經將編譯器產生的按鈕事件方法程式碼加上許多註解,您可以參考這些程式碼說明;其實,編譯器把原先該按鈕內的所有程式碼,都搬到另外一個新產生的類別中,這個類別其實就是一個狀態機的運作狀態。

底下的方法內容其實相當的簡單,它產生了一個狀態機物件,接著,對此狀態機進行初始化,並且設定這個狀態機的初始狀態值為 -1 (狀態機的型別為 int 整數),最後,呼叫 Start 方法,開始進行狀態機的運作。

狀態機的將會在第一行內產生  MainWindow.<btnDownload_Click>d__1 <btnDownload_Click>d__ = new MainWindow.<btnDownload_Click>d__1();

接著,針對這個狀態機變數 <btnDownload_Click>d__  進行初始化。
// 這裡重新改寫了 btnDownload_Click 方法
// AsyncStateMachine 請參考 https://msdn.microsoft.com/zh-tw/library/system.runtime.compilerservices.asyncstatemachineattribute(v=vs.110).aspx
[DebuggerStepThrough, AsyncStateMachine(typeof(MainWindow.<btnDownload_Click>d__1))]private void btnDownload_Click(object sender, RoutedEventArgs e)
{
    // 類別 <GetStringAsync>d__1 為編譯器產生的一個類別,其中,是運用狀態機 State Machine 觀念來處理非同步呼叫運作
    MainWindow.<btnDownload_Click>d__1 <btnDownload_Click>d__ = new MainWindow.<btnDownload_Click>d__1();
    // 底下的程式碼為進行該非同步呼叫的 State Machine的各種預設值初始化    // 
    // 呼叫非同步呼叫時候的物件本身
    < btnDownload_Click >d__.<>4__this = this;
    // 設定該事件的 sender 物件    <btnDownload_Click>d__.sender = sender;
    // 設定該事件的 RoutedEventArgs 物件    <btnDownload_Click>d__.e = e;
    // 建立 AsyncTaskMethodBuilder 類別的執行個體    < btnDownload_Click >d__.<>t__builder = AsyncVoidMethodBuilder.Create();
    // 該 State Machine 最初狀態值為 -1    <btnDownload_Click>d__.<>1__state = -1;
    // 表示非同步方法產生器,不會傳回值。
    // https://msdn.microsoft.com/zh-tw/library/system.runtime.compilerservices.asyncvoidmethodbuilder(v=vs.110).aspx    AsyncVoidMethodBuilder<>t__builder = <btnDownload_Click>d__.<>t__builder;
    // 開始執行具有相關聯狀態機器的產生器
    <>t__builder.Start<MainWindow.<btnDownload_Click>d__1>(ref <btnDownload_Click>d__);
}

原先按鈕事件內的方法都搬到了編譯器產生的狀態機類別內,也就是這個 <btnDownload_Click>d__1 類別,這個類別是繼承了 IAsyncStateMachine 這個類別,表示針對非同步方法所產生的狀態機器。 這個型別僅供編譯器使用;該類別程式碼如下所示:

這個編譯器產生的狀態機類別相當的長,不過,我也儘可能的在程式碼內加入註解說明。

這個類別所做的事情,我們簡單說明一下:狀態機最初的狀態值為 -1,一旦狀態機開始運作的時候(也就上面程式碼呼叫了 <>t__builder.Start 方法),就會開始進入狀態機的處理週期,也就是 MoveNext 這個方法;每次狀態機的狀態值有變更的時候,就會再次呼叫這個方法,進入到不同處理階段。

因為最初狀態值為 -1,所以,就會執行底下黃底內的 if 內程式碼,而這段程式碼也只會執行一次,之後就不會再進來了。在 if 內的程式碼,會呼叫我們自己寫的非同步方法 GetStringAsync(),而後會取得 等候非同步工作完成的物件 taskAwaiter,因為該非同步工作尚未完成,所以,就會將狀態機的狀態值變更為 0,接著透過 AwaitUnsafeOnCompleted 方法設定當工作完成後,繼續回到狀態機內繼續持行 MoveNext 方法。

當非同步工作完成之後,就會將非同步執行結果取出來,並且列應到 Console 內,也就是下面橘色底的程式碼,會執行這段程式法,是因為狀態機的值已經成為 0了。
        // 這是編譯器產生出來的類別,用來處理非同步需求的狀態機        [CompilerGenerated]
        private sealed class <btnDownload_Click>d__1 : IAsyncStateMachine
        {
            // 該 State Machine 狀態值
            public int <>1__state;

            // 建立 AsyncTaskMethodBuilder 類別的執行個體
            public AsyncVoidMethodBuilder<> t__builder;

            // 該事件的 sender 物件
            public object sender;

            // 該事件的 RoutedEventArgs 物件
            public RoutedEventArgs e;

            // 呼叫非同步呼叫時候的物件本身
            public MainWindow<>4__this;

            // 原先事件方法中,定義的本地變數 fooStr
            private string <fooStr>5__1;

            // 用來暫時儲存呼叫非同步方法的時候的字串值
            private string <>s__2;

            // 用來暫時儲存 提供等候非同步工作完成的物件
            // https://msdn.microsoft.com/zh-tw/library/system.runtime.compilerservices.taskawaiter(v=vs.110).aspx
            private TaskAwaiter<string> <>u__1;

            // 每當狀態機的狀態值有變動的時候,呼叫 MoveNext來執行下一個狀態機要執行的動作
            void IAsyncStateMachine.MoveNext()
            {
            // 暫時儲存現在狀態機內的狀態值
            int num = this.<>1__state;
            // 當在原先方法內用了 await 關鍵字,編譯器,會加入異常事件捕捉
            try            {
                TaskAwaiter<string> taskAwaiter;
                if (num != 0)
                {
                    // 一開始進入狀態機,狀態機值為-1,所以,會先進入到這裡,若狀態機值為 0 ,表示此非同步工作已經完成

                    // 執行 GetStringAsync 方法&取得用來等候這個 Task 的 awaiter,回傳值為 提供等候非同步工作完成的物件
                    // https://msdn.microsoft.com/zh-tw/library/system.threading.tasks.task.getawaiter(v=vs.110).aspx
                    taskAwaiter = this.<>4__this.GetStringAsync().GetAwaiter();
                    // 指出非同步工作是否已經完成
                    // https://msdn.microsoft.com/zh-tw/library/system.runtime.compilerservices.taskawaiter.iscompleted(v=vs.110).aspx
                    if (!taskAwaiter.IsCompleted)
                    {
                        // 非同步工作尚未完成
                        this.<>1__state = 0; // 設定狀態機狀態值,標明狀態值為 0,下一個週期,就不會進入到這段程式碼了
                        this.<>u__1 = taskAwaiter;  // 用來暫時儲存 提供等候非同步工作完成的物件
                        MainWindow.<btnDownload_Click>d__1 <btnDownload_Click>d__ = this;
                        // 排程狀態機器以在指定的 awaiter 完成時繼續下一個動作
                        // 也就是說,當非同步呼叫完成後,會再度回到 MoveNext() 方法重頭執行一次,不過,因為狀態值有變動了,所以,執行結果會不同
                        this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<string>, MainWindow.<btnDownload_Click>d__1> (ref taskAwaiter, ref <btnDownload_Click>d__);
                        return;
                    }
                }
                else                {
                    // 因為 狀態機值為 0 ,表示此非同步工作已經完成
                    // 取得等候非同步工作完成的物件
                    taskAwaiter = this.<>u__1;
                    // 設定等候非同步工作完成的物件的預設值
                    this.<>u__1 = default(TaskAwaiter<string>);
                    // 因為非同步工作已經完成,所以,再將狀態機值設為 -1 
                    this.<>1__state = -1;
                }
                // --------------------------------------------------------
                // 底下的程式碼,為該事件內 await 呼叫後的相關程式碼,也就是說,當完成非同步呼叫之後,會繼續回到原先地方繼續執行
                // --------------------------------------------------------
                // 取得 等候非同步工作完成的物件 的執行結果
                string result = taskAwaiter.GetResult();
                taskAwaiter = default(TaskAwaiter<string>);
                this.<>s__2 = result;
                this.<fooStr>5__1 = this.<>s__2;
                this.<>s__2 = null;
                // 這段程式法為原先非同步呼叫方法內的,並沒做任何改變
                Console.WriteLine(this.<fooStr>5__1);
            }
            catch (Exception exception)
            {
                // await 的非同步工作,可以捕捉異常事件,並且回報給呼叫者
                this.<>1__state = -2; 
                this.<>t__builder.SetException(exception);
                return;
            }
            this.<>1__state = -2; // 變更狀態值,表示已經完成所有的非同步工作
            // 將方法產生器標記為成功完成。
            this.<>t__builder.SetResult();
        }

最後,我們來看看我們自己定義的非同步方法,編譯器會把它變成怎麼樣,首先,底下是原先我們在 WPF 內寫的非同步方法程式碼:

這個非同步方法相當的簡單,我們先定義了一個 HttpClient 物件,接著,透過該物件,取得微軟官網首頁內,並且傳回原先呼叫者。
        public async Task<string> GetStringAsync()
        {
            HttpClient client = new HttpClient();
            var result = await client.GetStringAsync("http://www.microsoft.com");
            return result;
        }

底下為編譯器重新將 GetStringAsync 非同步方法進行包裝,並且產生了另外一個狀態機類別,以便處理非同步呼叫的相關動作,如同上面按鈕事件一樣,編譯器一樣會把我們寫的非同步方法的所有程式碼都包裝在新產生的狀態機類別內,將GetStringAsync這個方法改寫成只有進行狀態機初始化與啟動狀態機的動作。

由於這兩個非同步呼叫(一個是由按鈕事件來呼叫,一個是由自訂非同步方法來呼叫)程式碼很類似,因此,這兩個狀態機的內容很類似,您可以參考底下的程式碼註解說明,來了解到這些功能。

在此,特別說明一下,狀態機內的 if (num != 0){ ... } 程式碼 (底下黃底程式碼),也就是當 if 條件成立的時候,要執行該 if 內的程式碼內容,您可以當作,在原先寫的非同步程式碼中,所有在 await ... 這個等候非同步呼救前的所有程式碼,都會搬移到這個地方,這個 if 內的程式碼,透過狀態機的狀態值控制,就僅僅提供狀態機第一次啟動的時候才會執行,而(這個時候狀態機內的狀態值為 -1)後,就不會再執行到這段程式碼(因為,狀態機的值就會為 0了)。因此,我們就看到了,第一次進入到狀態機內,會先建立一個 HttClient 物件。

由於這個非同步方法需要回傳一個字串,因此,底下橘底程式碼,就是在整個非同步工作處理完成之後,通知最上層呼叫這個非同步方法,您可以取得這個非同步方法的傳回結果了。

    // 這是編譯器產生出來的類別,用來處理非同步需求的狀態機    [CompilerGenerated]
    private sealed class <GetStringAsync>d__2 : IAsyncStateMachine
        {
            // State Machine 的狀態值,表示處理到哪裡了
            public int <>1__state;

            // 表示非同步方法產生器,會傳回工作
            // https://msdn.microsoft.com/zh-tw/library/system.runtime.compilerservices.asynctaskmethodbuilder(v=vs.110).aspx
            public AsyncTaskMethodBuilder<string> <>t__builder;

            // 當時呼叫非同步方法的物件
            public MainWindow <>4__this;

            // 這個非同步方法內用到的 HttpClient 物件
            private HttpClient <client>5__1;

            // 原先事件方法中,定義的本地變數 result
            private string <result>5__2;

            // 用來暫時儲存呼叫非同步方法的時候的字串值
            private string <>s__3;

            // 用來暫時儲存 提供等候非同步工作完成的物件
            // https://msdn.microsoft.com/zh-tw/library/system.runtime.compilerservices.taskawaiter(v=vs.110).aspx
            private TaskAwaiter<string> <>u__1;

            // 每當狀態機的狀態值有變動的時候,呼叫 MoveNext來執行下一個狀態機要執行的動作
            void IAsyncStateMachine.MoveNext()
            {
                // 暫時儲存現在狀態機內的狀態值
                int num = this.<>1__state;
                string result2;
                // 當在原先方法內用了 await 關鍵字,編譯器,會加入異常事件捕捉
               try               {
                    TaskAwaiter<string> taskAwaiter;
                    if (num != 0)
                    {
                        // 一開始進入狀態機,狀態機值為-1,所以,會先進入到這裡,若狀態機值為 0 ,表示此非同步工作已經完成
                        this.<client>5__1 = new HttpClient();
                        // 執行 GetStringAsync 方法&取得用來等候這個 Task 的 awaiter,回傳值為 提供等候非同步工作完成的物件
                        taskAwaiter = this.<client>5__1.GetStringAsync("http://www.microsoft.com").GetAwaiter();
                        // 指出非同步工作是否已經完成
                        if (!taskAwaiter.IsCompleted)
                        {
                           // 非同步工作尚未完成
                            this.<>1__state = 0;  // 設定狀態機狀態值,標明狀態值為 0,下一個週期,就不會進入到這段程式碼了
                            this.<>u__1 = taskAwaiter; // 用來暫時儲存 提供等候非同步工作完成的物件
                            MainWindow.<GetStringAsync>d__2 <GetStringAsync>d__ = this;
                            // 排程狀態機器以在指定的 awaiter 完成時繼續下一個動作
                            // 也就是說,當非同步呼叫完成後,會再度回到 MoveNext() 方法重頭執行一次,不過,因為狀態值有變動了,所以,執行結果會不同
                            this.<>t__builder.AwaitUnsafeOnCompleted <TaskAwaiter<string>, MainWindow.<GetStringAsync>d__2> (ref taskAwaiter, ref <GetStringAsync>d__);
                            return;
                        }
                    }
                    else                    {
                        // 因為 狀態機值為 0 ,表示此非同步工作已經完成
                        // 取得等候非同步工作完成的物件
                        taskAwaiter = this.<>u__1;
                        // 設定等候非同步工作完成的物件的預設值
                        this.<>u__1 = default(TaskAwaiter<string>);
                        // 因為非同步工作已經完成,所以,再將狀態機值設為 -1 
                        this.<>1__state = -1;
                    }
                    // --------------------------------------------------------
                    // 底下的程式碼,為該事件內 await 呼叫後的相關程式碼,也就是說,當完成非同步呼叫之後,會繼續回到原先地方繼續執行
                    // --------------------------------------------------------
                    // 取得 等候非同步工作完成的物件 的執行結果
                    string result = taskAwaiter.GetResult();
                    taskAwaiter = default(TaskAwaiter<string>);
                    this.<>s__3 = result;
                    this.<result>5__2 = this.<>s__3;
                    this.<>s__3 = null;
                    result2 = this.<result>5__2;
                }
                catch (Exception exception)
                {
                    this.<>1__state = -2;
                    this.<>t__builder.SetException(exception);
                    return;
                }
                this.<>1__state = -2;
                this.<>t__builder.SetResult(result2);
         }

    [DebuggerHidden]
    void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
    {
    }
}

2015/10/01

使用Material Theme來變更App主題與風格 Theme / Style

在Android 5.0之後,提出了 Material Theme的設計指引,提供了每個開發者有個統一規範可以遵循來設計每個App,透過這個指引,我們可以透過 Theme / Style 的XML定義檔案,不需要修正任何C#程式碼,就可以直接將您的App風格樣貌做個更換。

了解更多關於 [Xamarin.Android] 的使用方式
了解更多關於 [Xamarin.iOS] 的使用方式
了解更多關於 [Xamarin.Forms] 的使用方式
了解更多關於 [Hello, Android:快速入門] 的使用方式
了解更多關於 [Hello, iOS – 快速入門] 的使用方式
了解更多關於 [Xamarin.Forms 快速入門] 的使用方式

不過,Material Theme這樣的設計方式,只存在於 Android 5.0 (API 21)或者更新版本的作業系統環境上才能夠運行,若您想要使用這樣的方式來設計您的App,並且希望讓您的App可以在比Android 5.0更舊的版本上來運行,並且享受到同樣的好處,您需要透過 v7 Support Libraries的支援,它提供了 material 設計樣式的主題 theme,可以用來配置不同的UI控制項,並且可以讓您客製化不同的顏色配置。
在看到如何使用之前,我們先看看有哪些項目是我們可以透過 Theme / Style的方式來進行替換不同的風格,在下圖中,我們可以看到不同的屬性,例如:colorPrimary, colorPrimaryDark等等,透過定義這些屬性到不同的顏色色碼,我們就可以立即將您的App更換到不同的風格樣貌。

首先,我們需要在 Resources/values目錄內styles.xml,加入一些定義,如下所示。
在 styles.xml檔案內定義我們App要用的基礎主題 MyTheme.Base,這個主題內容是從 Theme.AppCompat.Light.DarkActionBar繼承而來;  Theme.AppCompat.Light.DarkActionBar 這個主題,是在 Meterial Theme內預設定義的。  我們在自行定義的基礎主題 MyTheme.Base 中,修改了許多顏色配置,這些顏色配置將會存在於 Resources/values/colors.xml內,  也就是說,我們要變更App的顏色風格配置,只需要修改Resources/values/colors.xml內的顏色色碼即可。
想要參考更詳盡的 系統提供的 Theme / Style ,可以參考 Android API開發指南的Styles / Themes部分內容
R.Style的參考文件中,其實並不是一個很好與方便查詢各 Style和屬性的地方,因為在其官方文件上,這部分並沒有寫得相當清楚;如果您想要更進一步、完整的了解與參考這些Android內的 Style 和 Theme,可以參考底下兩個連結,在這兩個連結中,完整的列出了系統內提供的各個 Style & Theme 與其屬性定義名稱,透過這樣的查詢,您可以廓中您的App成為更多采多姿的程式。
Android Styles (styles.xml)
Android Themes (themes.xml)
在上圖中所所標示出來的 colorPrimary, colorPrimaryDark, textColorPrimary, windowbackground, navigationbarcolor 這些屬性,就可以透過地下的方式來進行擴充,若想要自訂其他的項目,可以參考與查詢上述兩個 Android Style / Android Themes 這兩份文件來做到。
  <style name="MyTheme" parent="MyTheme.Base">
  </style>

  <!--
  定義我們App要用的基礎主題 MyTheme.Base,這個主題內容是從 Theme.AppCompat.Light.DarkActionBar繼承而來
  Theme.AppCompat.Light.DarkActionBar 這個主題,是在 Meterial Theme內預設定義的
  我們在自行定義的基礎主題 MyTheme.Base 中,修改了許多顏色配置,這些顏色配置將會存在於 Resources/values/colors.xml內
  也就是說,我們要變更App的顏色風格配置,只需要修改Resources/values/colors.xml內的顏色色碼即可
  -->
  <!-- Base theme applied no matter what API -->
  <style name="MyTheme.Base" parent="Theme.AppCompat.Light.DarkActionBar">
    <item name="windowNoTitle">true</item>
    <!--We will be using the toolbar so no need to show ActionBar-->
    <item name="windowActionBar">false</item>
    <!-- Set theme colors from http://www.google.com/design/spec/style/color.html#color-color-palette-->
    <!-- colorPrimary is used for the default action bar background -->
    <item name="colorPrimary">@color/primary</item>
    <!-- colorPrimaryDark is used for the status bar -->
    <item name="colorPrimaryDark">@color/primaryDark</item>
    <!-- colorAccent is used as the default value for colorControlActivated which is used to tint widgets -->
    <item name="colorAccent">@color/accent</item>    
    <!-- You can also set colorControlNormal, colorControlActivated colorControlHighlight and colorSwitchThumbNormal. -->
    <!-- Option to hide the drop shadow here    
      <item name="actionBarStyle">@style/MyTheme.ActionBar</item>
      <item name="android:windowContentOverlay">@null</item>
    -->
  </style>

  <style name="MyTheme.ActionBar" parent="style/Widget.AppCompat.Light.ActionBar.Solid">
    <!-- remove shadow below action bar -->
    <!-- <item name="android:elevation">0dp</item> -->
    <!-- Support library compatibility -->
    <item name="elevation">0dp</item>
  </style>
</resources>

底下為自行定義擴充的顏色定義,上面的Xml定義中,標示著黃底紫色的文字的屬性,其顏色代碼,就是參考底下這個XML定義。
<?xml version="1.0" encoding="utf-8"?>
<!--專案範本預設的樣式-->
<!--Get colors from: http://www.materialpalette.com/-->
<!--<resources>
  <color name="primary">#03A9F4</color>
  <color name="primaryDark">#0288D1</color>
  <color name="accent">#FFC107</color>
  <color name="lightPrimary">#B3E5FC</color>
  <color name="textIcon">#FFFFFF</color>
  <color name="primaryText">#212121</color>
  <color name="secondaryText">#727272</color>
  <color name="divider">#B6B6B6</color>
  <color name="activated_color">#E8E8E8</color>
</resources>-->

<!--自訂顏色-->
<resources>
  <!--Get colors from: http://www.materialpalette.com/-->
  <color name="primary">#4CAF50</color>
  <color name="primaryDark">#388E3C</color>
  <color name="accent">#FFEB3B</color>
  <color name="lightPrimary">#C8E6C9</color>
  <color name="textIcon">#FFFFFF</color>
  <color name="primaryText">#212121</color>
  <color name="secondaryText">#727272</color>
  <color name="divider">#B6B6B6</color>
  <color name="activated_color">#E8E8E8</color>
</resources>

上面的 Colors.xml 檔案,我們定義了兩種情境顏色配置,下面螢幕截圖則是執行結果;下圖左邊是 [專案範本預設的樣式]定義,下圖右邊則是[自訂顏色]的執行結果

image image

image image

當然,我們也有方便的視覺快速設定方式,您可以透過 material palette 網頁,得到底下的設定

image





image

Xamarin.Android Templates Pack 範本解密3

在上一篇 [Xamarin.Android Templates Pack 範本解密2]文章中,我們剖析了專案內的 Activity & 相關的 Layout 配置,不過,在 Resources\layout 目錄下,還有兩個檔案沒有說明到
fragment1.axml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <TextView
        android:text="@string/fragment1"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/textView1"
        android:gravity="center" />
</LinearLayout>

Fragment1.cs 這個檔案定義這個 Fragment 的控制相關的行為,程式碼內容如下,我們可以看到,這個類別內並沒有複雜的定義,而是在 OnCreateView 方法內,使用 LayoutInflater.Inflate方法,指定這個 Fragment要顯示的視覺配置內容。

    public class Fragment1 : Fragment
    {
        public override void OnCreate(Bundle savedInstanceState)
        {
            base.OnCreate(savedInstanceState);

            // Create your fragment here        }

        public static Fragment1 NewInstance()
        {
            var frag1 = new Fragment1 { Arguments = new Bundle() };
            return frag1;
        }


        public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
        {
            var ignored = base.OnCreateView(inflater, container, savedInstanceState);
            return inflater.Inflate(Resource.Layout.fragment1, null);
        }
    }


fragment2.axml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <TextView
        android:text="@string/fragment2"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/textView1"
        android:gravity="center" />
</LinearLayout>

Fragment2.cs 這個檔案定義這個 Fragment 的控制相關的行為,程式碼內容如下,我們可以看到,這個類別內並沒有複雜的定義,而是在 OnCreateView 方法內,使用 LayoutInflater.Inflate方法,指定這個 Fragment要顯示的視覺配置內容。

    public class Fragment2 : Fragment
    {
        public override void OnCreate(Bundle savedInstanceState)
        {
            base.OnCreate(savedInstanceState);

            // Create your fragment here        }

        public static Fragment2 NewInstance()
        {
            var frag2 = new Fragment2 { Arguments = new Bundle() };
            return frag2;
        }


        public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
        {
            var ignored = base.OnCreateView(inflater, container, savedInstanceState);
            return inflater.Inflate(Resource.Layout.fragment2, null);
        }
    }


這兩個 .axml檔案,定義了兩個 Fragment,而當我們在 MainActivity內,有定義了當點選了導覽列中的功能表選項的話,會進行切換到不同的 Fragment

            //定義事件,當 navigation view 內的功能表選單有被選取的時候,要做何相對應的處置            navigationView.NavigationItemSelected += (sender, e) =>
            {
                e.MenuItem.SetChecked(true);

                switch (e.MenuItem.ItemId)
                {
                    case Resource.Id.nav_home_1:
                        ListItemClicked(0);
                        break;
                    case Resource.Id.nav_home_2:
                        ListItemClicked(1);
                        break;
                }

                // 這是 Google 新推出的 Metrial Design
                // 詳細說明,請參考 https://www.google.com/design/spec/material-design/introduction.html
                // 底下為產生類似 Toast 的簡短提示訊息通知效果
                Snackbar.Make(drawerLayout, "You selected: " + e.MenuItem.TitleFormatted, Snackbar.LengthLong)
                    .Show();

                // 使用動畫撥放方式,關閉所有正在開啟的 Drawer views
                drawerLayout.CloseDrawers();
            };


而 ListItemClicked方法定義如下,我們可以看到,這個方法會根據所選擇的項目,產生相對應的Fragment物件,並且透過 SuppotFragmentManager的 BeginTranscation 來進行切換到所指定的 Fragment。

Fragement是我當初學習 Android UI時候,一開始不太了解的東東,不過,把 Fragment當作是在 WPF中的 User Control,這樣的使用者控制項,是可以反覆使用的,也就是說,一旦我們定義好了之後,這些 Fragment是可以用於不同的 Activity內。
        private void ListItemClicked(int position)
        {
            //this way we don't load twice, but you might want to modify this a bit.
            if (position == oldPosition)
                return;

            oldPosition = position;

            Android.Support.V4.App.Fragment fragment = null;
            switch (position)
            {
                case 0:
                    fragment = Fragment1.NewInstance();
                    break;
                case 1:
                    fragment = Fragment2.NewInstance();
                    break;
            }

            SupportFragmentManager.BeginTransaction()
                .Replace(Resource.Id.content_frame, fragment)
                .Commit();
        }


最後,我們要來看 Resources\values 內的一些常數定義內容,首先看到的是 colors.xml 檔案,這個檔案定義了 Activity 的要顯示的顏色配色定義。

這些配色定義,將會在樣式定義檔案 styles.xml 內使用到
<?xml version="1.0" encoding="utf-8"?>
<resources>
  <!--Get colors from: http://www.materialpalette.com/-->
  <color name="primary">#03A9F4</color>
  <color name="primaryDark">#0288D1</color>
  <color name="accent">#FFC107</color>
  <color name="lightPrimary">#B3E5FC</color>
  <color name="textIcon">#FFFFFF</color>
  <color name="primaryText">#212121</color>
  <color name="secondaryText">#727272</color>
  <color name="divider">#B6B6B6</color>
  <color name="activated_color">#E8E8E8</color>
</resources>

接下來看看格式定義檔案的內容 styles.xml,在這個檔案內,我們定義一個 MyTheme樣式,他繼承了 MyTheme.Base;而 MyTheme.Base則是繼承從 Theme.AppCompat.Light.DarkActionBar,並且做了些修正來取代系統中的定義,另外還有兩種 Theme可以來選擇,分別是 Theme.AppCompat / Theme.AppCompat.Light

例如:我們定義了Theme.AppCompat.Light.DarkActionBar內用到的 colorPrimary 顏色,將會使用 Resources\values\color.xml 內的 primary 來取代,也就是這個色碼 #03A9F4

而有些資源資料,可以從這裡取得 http://developer.android.com/intl/zh-tw/design/downloads/index.html#action-bar-icon-pack
<?xml version="1.0" encoding="utf-8" ?>
<resources>
  <style name="MyTheme" parent="MyTheme.Base">
  </style>
  <!-- Base theme applied no matter what API -->
  <style name="MyTheme.Base" parent="Theme.AppCompat.Light.DarkActionBar">
    <item name="windowNoTitle">true</item>
    <!--We will be using the toolbar so no need to show ActionBar-->
    <item name="windowActionBar">false</item>
    <!-- Set theme colors from http://www.google.com/design/spec/style/color.html#color-color-palette-->
    <!-- colorPrimary is used for the default action bar background -->
    <item name="colorPrimary">@color/primary</item>
    <!-- colorPrimaryDark is used for the status bar -->
    <item name="colorPrimaryDark">@color/primaryDark</item>
    <!-- colorAccent is used as the default value for colorControlActivated
         which is used to tint widgets -->
    <item name="colorAccent">@color/accent</item>
    <!-- You can also set colorControlNormal, colorControlActivated
         colorControlHighlight and colorSwitchThumbNormal. -->    
    <!-- Option to hide the drop shadow here
    
      <item name="actionBarStyle">@style/MyTheme.ActionBar</item>
      <item name="android:windowContentOverlay">@null</item>
    -->
  </style>

  <style name="MyTheme.ActionBar" parent="style/Widget.AppCompat.Light.ActionBar.Solid">
    <!-- remove shadow below action bar -->
    <!-- <item name="android:elevation">0dp</item> -->
    <!-- Support library compatibility -->
    <item name="elevation">0dp</item>
  </style>
</resources>

而在 Resources/values-v21 目錄下也有個 styles.xml 檔案,內容如下;這裡定義了 API 21以上用到的Theme會用到的內容。
<?xml version="1.0" encoding="utf-8" ?>
<resources>
  <!--
        Base application theme for API 21+. This theme completely replaces
        MyTheme from BOTH res/values/styles.xml on API 21+ devices.
    -->
  <style name="MyTheme" parent="MyTheme.Base">
   <item name="android:windowContentTransitions">true</item>
    <item name="android:windowAllowEnterTransitionOverlap">true</item>
    <item name="android:windowAllowReturnTransitionOverlap">true</item>
    <item name="android:windowSharedElementEnterTransition">@android:transition/move</item>
    <item name="android:windowSharedElementExitTransition">@android:transition/move</item>
  </style>
</resources>