學習目標
- 列和欄的配置方式有三種
- 物件指定於 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.Row 和 Grid.Column 值,就能決定在哪個 Cell。
( 這邊 Xamarin 用的技術是 14 章 提過的 bindable properties 概念,將屬性綁入物件內)。
除了設定位置,還能設定此物件是否跨列或跨欄:
- Grid.RowSpan
- Grid.ColumnSpan
<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!!
但,
作者出外取材,本周休刊...
罗大神,我又遇到一个问题。如何自定义ContentPage 中 Title 的文字大小呢?我在网上找资源都没找到合适的解决办法。希望大神能解答一下,谢谢了
回覆刪除今天研究了這篇介紹的Grid排版功能
回覆刪除在實驗以C#調整gird板塊時發現C#中是以 new GridLength(整數數值,種類(Auto/Star/Absolute))來達到XAML直接用字串表示的"auto"、"*"、"數字"
謝謝補充喔~ 因為一般來說比較常用XAML做畫面的排版,但如果要全部用 C# 寫也是可行的
刪除老師您好,我再引入Xamarin.FormsBook.Toolkit時發生問題,錯誤訊息如下:
回覆刪除Failed to resolve assembly: 'Xamarin.FormsBook.Toolkit, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null'
請問老師知道是什麼問題嗎!!?? 感謝老師!
哎呀,Xamarin.FormsBook.Toolkit 是書上的 namespace,namespace 會跟妳開的專案有關係
刪除新增一個類別將 public class DoubleToIntConverter 以下的程式碼貼上就好