XAML in Xamarin.Forms 基礎篇 電子書

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

Xamarin.Forms 快速入門 電子書

Xamarin.Forms 快速入門 電子書
Xamarin.Forms 快速入門 電子書
顯示具有 Xamarin.iOS 標籤的文章。 顯示所有文章
顯示具有 Xamarin.iOS 標籤的文章。 顯示所有文章

2019/06/26

如何在 Xamarin.iOS 專案內建立一個啟動畫面,並且可以自動在不同裝置下,橫向與執行模式下,自動調整 直向 Portrait / 橫向 Landscape

如何在 Xamarin.iOS 專案內建立一個啟動畫面,並且可以自動在不同裝置下,橫向與執行模式下,自動調整 直向 Portrait / 橫向 Landscape

想要讓 Xamarin.Forms 開發出來的專案,不會有 iOS 應用程式一起動,就會看到一片藍色的畫面,就需要根據這篇文章來建立一個啟動螢幕 Splash Screen
這篇文章的專案原始碼,可以從 Github 取得

準備工作與建立 Storeboard

  • 請先複製一張圖片檔案到 Resources 目錄下,這裡使用的片為 MyIcon.png
  • 使用滑鼠右鍵點選 Resources 目錄,選擇 [加入] > [新增項目]
  • 在 [新增項目] 對話窗中,選擇 [已安裝] > [Visual C#] > [Apple] 類別
  • 在該對話窗的中間選擇 [分鏡腳本空白] 這個選項
  • 在名稱欄位輸入 MySplash.storyboard
  • 最後點選 [新增] 按鈕
  • 此時該 Storyboard 檔案將會自動開啟,若此時該專案尚未連線到 Mac 電腦,則會產生錯誤訊息,此時,需要先連線到 Mac 電腦,再度重新打開這個 Storyboard 檔案

設計該 Storyboard 分鏡腳本

  • 若看不到如下圖左方的 [工具箱] 與 [文件大綱] 這兩個視窗,請從 Visual Studio 的檢視視窗中找到這兩個項目,打開這兩個視窗
  • 在 [工具箱] 視窗的搜尋文字輸入盒內,輸入 view 關鍵字,將會看到 View Controller 這個項目
  • 請點選這個選項,並且拖拉到 Visual Studio 的中間地方,並且放開滑鼠,完成建立一個 View Controller 的操作
  • 底下是完成後的操作螢幕截圖
  • 現在,請在工具箱的文字輸入盒內輸入 Im 文字,將會看到出現 Image View 控制項,請將這個圖片控制項拖拉到剛剛建立的 View Controller 上
  • 請在工具箱的文字輸入盒內輸入 lab 文字,將會看到出現 Label 控制項,請將這個文字控制項拖拉到剛剛建立的 View Controller 上
  • 剛剛這兩個控制項可以拖拉到該 View Controller 的任何地方,不過,這裡準備要設計的構想是,要將圖片放到螢幕的最上方,而該文字將會永遠出現在螢幕的下方,因此,這兩個控制項將會拖拉成為如下圖的樣貌。
  • 接下來要來設定圖片控制項
  • 請先點選該圖片控制項,在右方的 [屬性] 視窗中,將會看到 [Image] 屬性項目,請在此選擇剛剛托拉進來的圖片名稱
  • 在 [Content Mode] 屬性欄位中,選擇 [Center] 項目
  • 最後,回到原先的圖片按鈕,透過上下左右的四個點,來調整整個 圖片控制項的大小,並且將該圖片拖拉到儘量離螢幕上方一點,並且水平置中
    • 請點選文字控制項,設定他的文字內容為 [我的 Xamarin.Forms] 與字體大小和文字置中等設定,最後將該文字拖移到螢幕的下方
    • 現在,點選螢幕空白的地方,設定 View 控制項的背景為 綠色,最後的結果將會如下圖所示
    • 現在來看看這樣的設計在不同裝置與螢幕方向下,有沒有問題存在
    • 在 Storyboard 視窗的左下方,將會看到一個 [正在檢視 : iPhone 8 Plus - 橫向 - 寬 R 高壓縮] 這個文字,請點選這個項目。
    • 這裡將會看到不同的裝置,與最右方可以選擇直向或者橫向的顯示方式,在這裡先點選 橫向 選項,將會看到如下圖畫面,整個原先螢幕的下方被切割掉了,只剩下螢幕上方的圖片,現在請再度點選 直向 選項
  • 在該 Storyboard 視窗的右上方將會看到有三個圖示,請點選中間的 [限制式編輯模式] 按鈕
  • 再度點選圖片控制項,將會出現不同的設定節點項目
  • 請點選右方的 T 字型的節點圖示,拖拉該圖示到螢幕的左方,直到出現如下圖的藍色虛線才放開,這將會這點該圖片的限制約束條件;同樣的請設定左方的 T 字型節點圖示,拖拉到螢幕右方,,直到出現如下圖的藍色虛線才放開
  • 同樣的,請也設定上方的 T 字型的節點圖示,拖拉該圖示到螢幕的上方,直到出現如下圖的藍色虛線與有上版面配置輔助線區域才放開
  • 請點選右方的 I 字型的節點圖示,將會彈跳出一個小視窗,請選擇 [高度] 選項,同樣的操作,點選該圖片下方的 I 字型圖示,選擇 [寬度] 選項
  • 此時,可以來查看這樣的設計是否可以正常在 橫向 模式下來顯示,結果是沒有問題的,如下圖所示。
  • 現在,請將 Label 控制項也依據這樣的方式來設定
  • 請滑鼠雙擊 info.plist 這個檔案,點選該視窗的 [視覺資產] 標籤頁次,在下方的 [啟動畫面] 下拉選單中,選擇 [MySplash] 項目
  • 請執行這個專案,分別將裝置轉向成為 直向與橫向 模式,這樣的啟動畫面就不會跑版囉
     



2019/06/10

如何在Xamarin.Forms 的 iOS App,需要即時變更回上一頁按鈕的文字

如何在 Xamarin.Forms 的 iOS App,需要即時變更回上一頁按鈕的文字

最近遇到上課學員提出一個問題,學員要設計一個頁面,這個頁面可以設定這個 App 要使用的多國語言選單功能,當從某個語系切換到另外一個語系的時候,這個頁面上的相關內容也可以同步的切換成為新的語言,可是,當這個 App 在 Android 平台下的時候,相關的設計都可以正常運作,不過,當在 iOS 平台下的時候,卻發生了問題;問題在於這個頁面的最上方有個導航工具列,在 Android 平台下這個導航工具列上僅會顯示這個頁面的名稱,此時,這個頁面的名稱是可以透過資料綁定的方式,在 ViewModel 程式碼中,即時變更成為任何文字,頁面的工具列上也就會即時變更與顯示出最新的頁面名稱,然而,在 iOS 平台下,導航工具列的左上方出現的卻是回上頁的頁面名稱或者是自訂的回上頁文字,若在設定頁面的 ViewModel 來變更回上頁的按鈕的文字,是無法正常更新的,這是因為回上頁的按鈕名稱是要在上一頁的頁面中來設定的。
這篇文章的範例專案原始碼,可以從 GitHub 取得
下面螢幕截圖為這個範例專案在 Android 平台上的執行結果左下圖為一開始啟動 App 的頁面,右下圖為按下 切換可動態變換按鈕文字 的按鈕,就會切換到右下角的頁面。
 
下面螢幕截圖為這個範例專案在 iOS 平台上的執行結果左下圖為一開始啟動 App 的頁面,右下圖為按下 切換可動態變換按鈕文字 的按鈕,就會切換到右下角的頁面。
 
而在首頁上,其宣告的 XAML 內容可以從底下看的出來,這裡並沒有使用 NavigationPage.BackButtonTitle="自訂回上頁按鈕文字" 這樣的宣告,所以,當從首頁切換到下一個頁面的時候,在 iOS 平台下預設將會顯示首頁頁面標題的文字在下一頁的回上頁按鈕文字上。不過,因為這裡的頁面已經過修正,可以自動更新回上頁按鈕文字,與平常看到的不太一樣;若在 Xamarin.Forms 開發的程式,在 iOS 平台下跑起來,其實將會看到如下圖的畫面,若在前一頁面中,沒有使用 NavigationPage.BackButtonTitle 來指定客製按鈕文字,原則上都會出現上一頁面的標題文字在這個頁面的回上頁按鈕上。
xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="ChangeiOSBackButtonText.Views.MainPage"
             Title="上頁文字變換">

    <StackLayout HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand">
        <Label Text="Welcome to Xamarin Forms and Prism!" />
        <Button Text="切換可動態變換按鈕文字" Command="{Binding SwithChangePageCommand}"/>
        <Button Text="切換一般頁面" Command="{Binding SwithNextPageCommand}"/>
        <Button Text="連續兩個頁面" Command="{Binding SwithTwoPageCommand}"/>
    </StackLayout>

</ContentPage>
那麼,要如何做到這樣的客製化的回上頁按鈕功能,並且可以結合資料綁定,做到動態可以在 ViewModel 中,來設定要顯示的文字,例如,回上頁按鈕文字、頁面主題名稱,達到如下的效果。
在 Android 系統下,若點選 切換中文 按鈕,會出現左下圖畫面,若點選 Switch English 按鈕,則會出現右下圖畫面。
 
在 iOS 系統下,若點選 切換中文 按鈕,會出現左下圖畫面,若點選 Switch English 按鈕,則會出現右下圖畫面。
 
現在來看看這個頁面 XAML 宣告內容,在這裡若想要變更回上頁按鈕的文字,需要在這裡使用這裡自訂 附加屬性 DynamicBackButtonTextAttached.SetBackButtonText ,使用它來指定要顯示的回上頁按鈕文字內容,由於這個附加屬性值可以做到資料綁定的更新通知,所以,想要變更該頁面回上頁按鈕文字內容,就可以從 ViewModel 來修改 ThisBackText 這個屬性值即可。
xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="ChangeiOSBackButtonText.Views.CanChangePage"
             xmlns:DynBtn="clr-namespace:ChangeiOSBackButtonText.AttachedProperties"
             DynBtn:DynamicBackButtonTextAttached.SetBackButtonText="{Binding ThisBackText}"
             xmlns:localViews="clr-namespace:ChangeiOSBackButtonText.Views"
             Title="{Binding Title}">

    <StackLayout
        >
        <Entry Text="{Binding Message}"/>
        <Button Text="設定" Command="{Binding SetBackButtonTextCommand}"/>
        <Button Text="切換中文" Command="{Binding SetChineseCommand}"/>
        <Button Text="Switch English" Command="{Binding SetEnglishCommand}"/>
    </StackLayout>

</ContentPage>

設計理念說明

為了要做到這樣的效果,這個時候需要建立一個名為 NaviCustomPage 的 NavigationPage 型別的頁面,並且整個 App 的頁面導航功能,需要透過這個新建立的導航頁面來切換頁面,這樣剛剛所使用的附加屬性 DynamicBackButtonTextAttached.SetBackButtonText 才會正常的運作。
而這個附加屬性的主要目的是要能夠指定當時這個頁面的回上頁按鈕文字內容,不過,這需要透過導航頁面中的 可綁定屬性 DynamicBackButtonText 來指定該導航工具列上的回上頁按鈕文字,該 DynamicBackButtonText 屬性將會透過 附加屬性 DynamicBackButtonTextAttached.SetBackButtonText 來變更,因為在這個附加屬性上,有訂閱 OnSetBackButtonTextChanged 這個屬性變更事件,所以,當該附加屬性有變動就會觸發這個事件,接著將會在這個事件中執行一個輔助支援方法 ChangeBackButtonTextHelper.ChangeBackButtonText,透過這個方法來取得當前導航工具列上的 可綁定屬性 ,也就 DynamicBackButtonText 屬性值。
最後,需要在 iOS 平台下來實作出 NaviCustomPage 的 NavigationRenderer,這裡將會在 iOS 平台下建立一個 NaviCustomPageRenderer 類別來做到這件事情,在這個類別中,將會覆寫方方法 OnElementPropertyChanged ,若該 NaviCustomPage 內的任何一個可綁定屬性有異動的時候,就將會觸發這個事件;在該事件中將會檢查是否是 DynamicBackButtonText 這個屬性有異動,若有變更的話,將會呼叫 UpdateBackButtonTitleText 方法來變更回上頁按鈕。
因此,就可以完成這樣的需求,現在來逐一檢視這樣的設計過程。
建立一個新的 NavigationPage,並且指定該頁面的名稱為 NaviCustomPage
xaml
<?xml version="1.0" encoding="utf-8" ?>
<NavigationPage xmlns="http://xamarin.com/schemas/2014/forms"
                xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                xmlns:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
                prism:ViewModelLocator.AutowireViewModel="True"
                x:Class="ChangeiOSBackButtonText.Views.NaviCustomPage">

</NavigationPage>
打開 NaviCustomPage 的 Code Behind ,加入一個可綁定屬性 DynamicBackButtonText,在這裡並不需要做其他的設計,因為,這個屬性存在的目的僅僅是要能夠讓 iOS 下的 NaviCustomPageRenderer 類別,可以知道這個屬性值有異動,也就是需要更新回上頁按鈕了,而真正要變更回上頁按鈕的處理動作,需要 iOS 平台下的 NaviCustomPageRenderer 類別中來處理。
public partial class NaviCustomPage : NavigationPage
{
    #region DynamicBackButtonText 可綁定屬性 BindableProperty
    public static readonly BindableProperty DynamicBackButtonTextProperty =
        BindableProperty.Create("DynamicBackButtonText", // 屬性名稱 
            typeof(string), // 回傳類型 
            typeof(NaviCustomPage), // 宣告類型 
            "", // 預設值 
            propertyChanged: OnDynamicBackButtonTextChanged  // 屬性值異動時,要執行的事件委派方法
        );

    public string DynamicBackButtonText
    {
        set
        {
            SetValue(DynamicBackButtonTextProperty, value);
        }
        get
        {
            return (string)GetValue(DynamicBackButtonTextProperty);
        }
    }

    private static void OnDynamicBackButtonTextChanged(BindableObject bindable, object oldValue, object newValue)
    {
    }

    #endregion

    public NaviCustomPage()
    {
        InitializeComponent();
    }
}

指定使用來做頁面導航

在這個測試專案,將會在 App.xaml.cs 內,指定使用 NaviCustomPage 作為導航工具列
protected override async void OnInitialized()
{
    InitializeComponent();

    await NavigationService.NavigateAsync("MDPage/NaviCustomPage/MainPage");
}

設計附加屬性 DynamicBackButtonTextAttached

在 Xamarin.Forms 專案內,建立這個類別,在此建裡一個附加屬性。在建立這個 附加屬性 類別的時候,有指定這個 propertyChanged 事件,在此綁定到這個 OnSetBackButtonTextChanged 事件上。
當特定 ContentPage 頁面上有指定 DynamicBackButtonTextAttached.SetBackButtonText 屬性且屬性值有變動的時候,這個 OnSetBackButtonTextChanged 就會被觸發,不過,當時的頁面必須要有實作 IDynamicChangeBackText 介面,這樣,才能夠接下來做異動,這裡將會呼叫 ChangeBackButtonTextHelper.ChangeBackButtonText(newString); 方法,這裡將會變更導航頁面上的 DynamicBackButtonText 屬性
public class DynamicBackButtonTextAttached
{
    #region SetBackButtonText 附加屬性 Attached Property
    public static readonly BindableProperty SetBackButtonTextProperty =
           BindableProperty.CreateAttached(
               propertyName: "SetBackButtonText",   // 屬性名稱 
               returnType: typeof(string), // 回傳類型 
               declaringType: typeof(ContentPage), // 宣告類型 
               defaultValue: null, // 預設值 
               propertyChanged: OnSetBackButtonTextChanged  // 屬性值異動時,要執行的事件委派方法
           );

    public static void SetSetBackButtonText(BindableObject bindable, string entryType)
    {
        bindable.SetValue(SetBackButtonTextProperty, entryType);
    }
    public static string GetSetBackButtonText(BindableObject bindable)
    {
        return (string)bindable.GetValue(SetBackButtonTextProperty);
    }

    private static void OnSetBackButtonTextChanged(BindableObject bindable, object oldValue, object newValue)
    {
        ContentPage page = bindable as ContentPage;
        if (page == null) return;
        string oldString = oldValue as string;
        string newString = newValue as string;

        if (newString == null) return;

        if(page is IDynamicChangeBackText)
        {
            ChangeBackButtonTextHelper.ChangeBackButtonText(newString);
        }
    }
    #endregion
}

建立輔助支援方法,可以修改當前導航頁面上的 DynamicBackButtonText 屬性

在這裡將會使用 App.Current.MainPage 屬性來檢查當前的頁面是 MasterDetailPage 還是 NaviCustomPage ;若為 MasterDetailPage 頁面,對於 NaviCustomPage 可以透過 MasterDetailPage.Detail 取得;若當前頁面是 NaviCustomPage,則就可以直接取得這個物件。
有了 naviCustomPage 這個物件,就可以透過該物件來存取該導航頁面上剛剛建立的新可綁定屬性 naviCustomPage.DynamicBackButtonText,一旦變更這個屬性值,在 iOS 平台下的 NaviCustomPageRenderer 事件也會被觸發。
public class ChangeBackButtonTextHelper
{
    public static void ChangeBackButtonText(string newBackButtonText)
    {
        NaviCustomPage naviCustomPage = GetNaviCustomPage();
        if (naviCustomPage != null)
        {
            naviCustomPage.DynamicBackButtonText = newBackButtonText;
        };
    }
    public static string GetBackButtonText()
    {
        string result = "";
        NaviCustomPage naviCustomPage = GetNaviCustomPage();
        if (naviCustomPage != null)
        {
            result = naviCustomPage.DynamicBackButtonText;
        }
        return result;
    }
    public static NaviCustomPage GetNaviCustomPage()
    {
        NaviCustomPage naviCustomPage = null;
        if (App.Current.MainPage is MasterDetailPage)
        {
            MasterDetailPage masterDetailPage = App.Current.MainPage as MasterDetailPage;
            if (masterDetailPage.Detail is NaviCustomPage)
            {
                naviCustomPage = masterDetailPage.Detail as NaviCustomPage;
            }
        }
        else if (App.Current.MainPage is NaviCustomPage)
        {
            naviCustomPage = App.Current.MainPage as NaviCustomPage;
        }
        return naviCustomPage;
    }
}

對於要能夠變更回上頁按鈕文字的 ContentPage,需要實作 IDynamicChangeBackText

首先,對於 IDynamicChangeBackText 這個介面內,並沒有宣告什麼內容,只是為了要能夠分辨出該頁面是否可以透過剛剛設計的可綁定屬性來取得回上頁按鈕文字。
public interface IDynamicChangeBackText
{
}
對於要自動更新回上頁按鈕的 ContentPage,需要打開該頁面的 Code Behind C# 程式碼,實作這個 IDynamicChangeBackText 介面。
public partial class CanChangePage : ContentPage, IDynamicChangeBackText
{
    public CanChangePage()
    {
        InitializeComponent();
    }
}

在 iOS 平台下實作 NaviCustomPageRenderer

為了要能夠做出可變動回上頁按鈕,這裡在 iOS 平台下,建立 NaviCustomPageRenderer 類別,該類別需要實作 NavigationRenderer
在這裡需要覆寫 OnPushAsync 與 OnPopViewAsync 這兩個方法,當要透過導航工具列切換到不同頁面的時候,這個 OnPushAsync 方法就會被執行,在此方法內,若該新頁面有實作 IDynamicChangeBackText 這個介面,將會呼叫 SetBackButtonOnPage(page) 方法,該方法將會呼叫 ChangeBackButtonTextHelper.GetBackButtonText() 方法,取得當時設定到的回上頁按鈕自訂文字內容。
[assembly: ExportRenderer(typeof(NaviCustomPage), typeof(NaviCustomPageRenderer))]
namespace ChangeiOSBackButtonText.iOS.Renderers
{
    public class NaviCustomPageRenderer : NavigationRenderer
    {
        UIBarButtonItem barButtonItem;
        NaviCustomPage oldMyNaviPage;
        NaviCustomPage newMyNaviPage;

        protected override Task<bool> OnPushAsync(Page page, bool animated)
        {
            var retVal = base.OnPushAsync(page, animated);

            if (page is IDynamicChangeBackText)
            {
                SetBackButtonOnPage(page);
            }

            return retVal;
        }
        protected override Task<bool> OnPopViewAsync(Page page, bool animated)
        {
            var retVal = base.OnPopViewAsync(page, animated);

            if (page is IDynamicChangeBackText)
            {
                var stack = page.Navigation.NavigationStack;

                var returnPage = stack[stack.Count - 2];

                if (returnPage != null)
                {
                    SetBackButtonOnPage(returnPage);
                }
            }
            else
            {
                //SetDefaultBackButton();
            }

            return retVal;
        }

        void SetBackButtonOnPage(Page page)
        {
            //var stack = page.Navigation.NavigationStack;

            //if(stack.Count == 1)
            //{
            //    //SetDefaultBackButton();
            //}

            if (page is IDynamicChangeBackText)
            {
                string backButtonText = ChangeBackButtonTextHelper.GetBackButtonText();
                SetImageTitleBackButton("Left2", backButtonText, -15);
            }
            else
            {
                //SetDefaultBackButton();
            }

        }

        void SetImageTitleBackButton(string imageBundleName, string buttonTitle, int horizontalOffset)
        {
            var topVC = this.TopViewController;

            // Create the image back button
            var backButtonImage = new UIBarButtonItem(
                    UIImage.FromBundle(imageBundleName),
                    UIBarButtonItemStyle.Plain,
                    (sender, args) =>
                    {
                        topVC.NavigationController.PopViewController(true);
                    });

            // Create the Text Back Button
            //var backLeftButtonText = new UIBarButtonItem(
            //    "<",
            //    UIBarButtonItemStyle.Plain,
            //    (sender, args) =>
            //    {
            //        topVC.NavigationController.PopViewController(true);
            //    });

            // Create the Text Back Button
            var backButtonText = new UIBarButtonItem(
                buttonTitle,
                UIBarButtonItemStyle.Plain,
                (sender, args) =>
                {
                    topVC.NavigationController.PopViewController(true);
                });

            backButtonText.SetTitlePositionAdjustment(new UIOffset(horizontalOffset, 0), UIBarMetrics.Default);

            // Add buttons to the Top Bar
            UIBarButtonItem[] buttons = new UIBarButtonItem[2];
            buttons[0] = backButtonImage;
            buttons[1] = backButtonText;

            topVC.NavigationItem.LeftBarButtonItems = buttons;
        }

        void SetDefaultBackButton()
        {
            this.TopViewController.NavigationItem.LeftBarButtonItems = null;
        }

        protected override void OnElementChanged(VisualElementChangedEventArgs e)
        {
            base.OnElementChanged(e);

            oldMyNaviPage = (NaviCustomPage)e.OldElement;
            newMyNaviPage = (NaviCustomPage)e.NewElement;
            if (oldMyNaviPage != null)
            {
                oldMyNaviPage.PropertyChanged -= OnElementPropertyChanged;
            }
            if (newMyNaviPage != null)
            {
                newMyNaviPage.PropertyChanged += OnElementPropertyChanged;
            }
        }

        private void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "DynamicBackButtonText")
            {
                UpdateBackButtonTitleText();
            }
        }

        private void UpdateBackButtonTitleText()
        {
            string backTitle = "";
            backTitle= ChangeBackButtonTextHelper.GetBackButtonText();
            if (this.NavigationBar.Items.Count() > 1)
            {
                this.TopViewController.NavigationItem.LeftBarButtonItems[1].Title = backTitle;
            }
        }
    }
}