2017/5/31

Xamarin.Forms 教學系列文(十七) Grid - 強大卻簡單的畫面排版工具



學習目標
  • 列和欄的配置方式有三種
  • 物件指定於 Grid 的位置
  • portrait & landscape 轉換的應用

開發 App 有先做 UI 設計的話,本章節必讀,因為要排版排版再排版!!!

排版上,Grid 是一個相當強力的物件,他有點像是 Html 的 Table

但 Grid 很單純的只用在畫面排版,並不像 Html 的 Table 有相當多屬性可以設定 (ex. border)。

Grid 兩個最重要的屬性,Row Column,可以依照比例分配寬度,來決定畫面物件的位置,而 Row 和 Column 中間放物件的地方叫 Cell。

Grid 二維的特性也相當適用於 portrait 和 landscape 的轉換配置,在本節最後會有範例程式。

本章著重於會用 Grid 就好,將跳過原文書很多內容
本節只講解 XAML 的用法

Basic Grid 

先決定 Row 和 Column 的數量

寫 Grid 時,一開始就定義好兩個最重要的屬性:
  • RowDefinitions - RowDefinition 的集合 (同時設定其 Height)
  • ColumnDefinitions - ColumnDefinition 的集合 (同時設定其 Width)

Height 和 Width 都是 GridLength 類別,有三種方式設定其大小:
  • Absolute - 輸入指定數字
  • Auto - 依照內容物件決定大小
  • Star - 依照 星星數 比例分配

星星數
舉例來說,假設 Grid 有兩個 Row,第一個 Height 設定為 2*,第二個 Height 設定為 1*,Grid 就會將整個畫面切成三等份,各別擁有 2 : 1 的畫面空間。

看一下範例:
*OnPlatform 寫法已更改,請參考此篇文章
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" 
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
             x:Class="SimpleGridDemo.SimpleGridDemoPage">
    
    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness" iOS="0, 20, 0, 0" />
    </ContentPage.Padding>
    
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="100" />
            <RowDefinition Height="2*" />
            <RowDefinition Height="1*" />
        </Grid.RowDefinitions>
        
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        
        ...
        
    </Grid>
</ContentPage>

ColumnDefinition 的 Width 只寫一個 *,意思等同於 1*,左右寬度會 1 : 1 分配。


決定物件在 Grid 內的位置

當我們設定好 Row 和 Column 的數量和空間後,再來要決定物件放置在哪個 Cell 裡。

方法:
只要設定物件的 Grid.RowGrid.Column 值,就能決定在哪個 Cell。
( 這邊 Xamarin 用的技術是 14 章 提過的 bindable properties 概念,將屬性綁入物件內)。

除了設定位置,還能設定此物件是否跨列或跨欄:
  • Grid.RowSpan
  • Grid.ColumnSpan

接續上面的範例,多了物件放置:
*OnPlatform 寫法已更改,請參考此篇文章
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" 
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="SimpleGridDemo.SimpleGridDemoPage">
    
    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness" 
                    iOS="0, 20, 0, 0" />
    </ContentPage.Padding>
    
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="100" />
            <RowDefinition Height="2*" />
            <RowDefinition Height="1*" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        
        <!--設定 Label 位置在 (0,0)-->
        <Label Text="Grid Demo" 
               Grid.Row="0"
               Grid.Column="0" 
               FontSize="Large" 
               HorizontalOptions="End" />
        
        <Label Text="Demo the Grid" 
               Grid.Row="0" 
               Grid.Column="1" 
               FontSize="Small" 
               HorizontalOptions="End"
               VerticalOptions="End" />
        
        <Image BackgroundColor="Gray" 
               Grid.Row="1" 
               Grid.Column="0"
               Grid.ColumnSpan="2">

            <Image.Source>
                <OnPlatform x:TypeArguments="ImageSource" 
                            iOS="Icon-60.png" 
                            Android="icon.png"
                            WinPhone="Assets/StoreLogo.png" />
            </Image.Source>
        </Image>
        
        <BoxView Color="Green" 
                 Grid.Row="2"
                 Grid.Column="0" />
        
        <!-- 跨兩列-->
        <BoxView Color="Red"
                 Grid.Row="2" 
                 Grid.Column="1"
                 Grid.RowSpan="2" />
        
        <!--跨兩欄-->
        <BoxView Color="Blue" 
                 Opacity="0.5" 
                 Grid.Row="3"
                 Grid.Column="0"
                 Grid.ColumnSpan="2" />
    </Grid>
</ContentPage>
                

執行結果 (會發現右下有塊紫色,因為紅藍色互跨產生):


這範例還能發現每個 Cell 之間都會有一條間隔 (Spacing),若你要讓每個 Cell 接近一點,可以設定 Grid 的兩個屬性值:
  • RowSpacing - 預設為 6
  • ColumnSpacing - 預設為 6

以上的觀念已經可以應付許多排版的窘境了~ 若還有興趣可以往下看看 Grid 的應用之一,手機轉向

手機方向轉換時的應用

手機轉換方向時,Grid 除了協助物件的定位,還能善用 SizeChanged 時去更改物件在 Grid 內的 Row 或 Column,完成更符合使用性的頁面。

舉個最簡單的例子,在直向 (Portrait) 時先把畫面切成上下兩個區域,接著轉成橫向 (Landscape ) 時,可以將下面的區域整塊移至右側,如圖。



這樣的轉移排版,透過 Grid 就能很簡單的控制。

直接來看範例:
畫面會切成兩塊,第一塊有個  BoxView 顯示顏色,第二塊有 3 個 Slider 控制要顯示的顏色。
*OnPlatform 寫法已更改,請參考此篇文章

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" 
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
             xmlns:toolkit= "clr-namespace:Xamarin.FormsBook.Toolkit;assembly=Xamarin.FormsBook.Toolkit"
             x:Class="GridRgbSliders.GridRgbSlidersPage" SizeChanged="OnPageSizeChanged">
   
    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness" 
                    iOS="0, 20, 0, 0" />
    </ContentPage.Padding>
    
    <ContentPage.Resources>
        <ResourceDictionary>
            <toolkit:DoubleToIntConverter x:Key="doubleToInt" />
            <Style TargetType="Label">
                <Setter Property="HorizontalTextAlignment" Value="Center" />
            </Style>
        </ResourceDictionary>
    </ContentPage.Resources>
    
    <Grid x:Name="mainGrid">
        
        <!-- Portrait 初始排版 -->
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="0" />
        </Grid.ColumnDefinitions>
        
        <!--第一區域-->
        <BoxView x:Name="boxView" 
                 Grid.Row="0" 
                 Grid.Column="0" />
        
        <!--第二區域-->
        <StackLayout x:Name="controlPanelStack" 
                     Grid.Row="1"
                     Grid.Column="0"
                     Padding="10, 5">
            
            <!--Red Slider-->
            <StackLayout VerticalOptions="CenterAndExpand">
                <Slider x:Name="redSlider" 
                        ValueChanged="OnSliderValueChanged" />
                
                <Label Text="{Binding Source={x:Reference redSlider}, 
                                      Path=Value,
                                      Converter={StaticResource doubleToInt}, 
                                      ConverterParameter=255, 
                                      StringFormat='Red = {0:X2}'}" />
            </StackLayout>

            <!--Green Slider-->
            <StackLayout VerticalOptions="CenterAndExpand">
                <Slider x:Name="greenSlider" 
                        ValueChanged="OnSliderValueChanged" />
                
                <Label Text="{Binding Source={x:Reference greenSlider}, 
                                      Path=Value, 
                                      Converter={StaticResource doubleToInt}, 
                                      ConverterParameter=255, 
                                      StringFormat='Green = {0:X2}'}" />
            </StackLayout>

            <!--Blue Slider-->
            <StackLayout VerticalOptions="CenterAndExpand">
                <Slider x:Name="blueSlider" 
                        ValueChanged="OnSliderValueChanged" />
                <Label Text="{Binding Source={x:Reference blueSlider},
                                      Path=Value, 
                                      Converter={StaticResource doubleToInt}, 
                                      ConverterParameter=255, 
                                      StringFormat='Blue = {0:X2}'}" />
            </StackLayout>
        </StackLayout>
    </Grid>
</ContentPage>

直向 (Portrait) 長這樣:


在 OnSizeChanged 用寬和高判斷目前是 Portrait 或 Landscape,再來決定第二區域的位置:
public partial class GridRgbSlidersPage : ContentPage
{
    public GridRgbSlidersPage()
    {
        InitializeComponent();
    }

    void OnPageSizeChanged(object sender, EventArgs args)
    {
        // Portrait mode. 
        if (Width < Height)
        {
            mainGrid.RowDefinitions[1].Height = GridLength.Auto;
            mainGrid.ColumnDefinitions[1].Width = new GridLength(0, GridUnitType.Absolute);

            Grid.SetRow(controlPanelStack, 1);
            Grid.SetColumn(controlPanelStack, 0);
        }
        // Landscape mode. 
        else
        {
            mainGrid.RowDefinitions[1].Height = new GridLength(0, GridUnitType.Absolute);
            mainGrid.ColumnDefinitions[1].Width = new GridLength(1, GridUnitType.Star);

            Grid.SetRow(controlPanelStack, 0);
            Grid.SetColumn(controlPanelStack, 1);
        }
    }

    void OnSliderValueChanged(object sender, ValueChangedEventArgs args)
    {
        boxView.Color = new Color(redSlider.Value, greenSlider.Value, blueSlider.Value);
    }
}


橫向 (Landscape) 長這樣:


最後補一下 XAML 有用到的 DoubleToIntConverter
namespace Xamarin.FormsBook.Toolkit
{
    public class DoubleToIntConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, 
                                object parameter, CultureInfo culture)
        {
            string strParam = parameter as string; double multiplier = 1;

            if (!String.IsNullOrEmpty(strParam))
            {
                Double.TryParse(strParam, out multiplier);
            }

            return (int)Math.Round((double)value * multiplier);
        }

        public object ConvertBack(object value, Type targetType,
                                    object parameter, CultureInfo culture)
        {
            string strParam = parameter as string;
            double divider = 1;

            if (!String.IsNullOrEmpty(strParam))
            {
                Double.TryParse(strParam, out divider);
            }

            return (int)value / divider;
        }
    }
}

由於 Slider 拖曳時的值是浮點數,而 RGB 需要的值是 0~255 之間的整數,所以才需要這個 Converter。


這範例不知道大家有沒有發現,拖曳 Slider 並去更改畫面上顯示的東西,跟我們 16 章 介紹的 Data Binding 好像有點類似

但這邊還是使用事件 OnSliderValueChanged 去處理頁面邏輯。

而且這邊有 3 個 Slider,是沒辦法直接物件對物件做 Data Binding,更況且要轉成 RGB 值...


那我們有沒有其他解決方案?

可以不要寫 OnSliderValueChanged 事件控制,卻還是達到我們要的功能 - 拖曳 Slider 並更改 Boxview 的顏色。


那就是我們下一章節要介紹的 MVVM!!





但,




作者出外取材,本周休刊...


5 則留言:

  1. 罗大神,我又遇到一个问题。如何自定义ContentPage 中 Title 的文字大小呢?我在网上找资源都没找到合适的解决办法。希望大神能解答一下,谢谢了

    回覆刪除
  2. 今天研究了這篇介紹的Grid排版功能
    在實驗以C#調整gird板塊時發現C#中是以 new GridLength(整數數值,種類(Auto/Star/Absolute))來達到XAML直接用字串表示的"auto"、"*"、"數字"

    回覆刪除
    回覆
    1. 謝謝補充喔~ 因為一般來說比較常用XAML做畫面的排版,但如果要全部用 C# 寫也是可行的

      刪除
  3. 老師您好,我再引入Xamarin.FormsBook.Toolkit時發生問題,錯誤訊息如下:
    Failed to resolve assembly: 'Xamarin.FormsBook.Toolkit, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null'
    請問老師知道是什麼問題嗎!!?? 感謝老師!

    回覆刪除
    回覆
    1. 哎呀,Xamarin.FormsBook.Toolkit 是書上的 namespace,namespace 會跟妳開的專案有關係
      新增一個類別將 public class DoubleToIntConverter 以下的程式碼貼上就好

      刪除

注意:只有此網誌的成員可以留言。