使用 FlexLayout 配合 BindableLayout 建立一個動態產生與自動配置的效果
在這裡,將要使用 Visual Studio 2019 建立一個 Xamarin.Forms 的專案,並且使用 FlexLayout 彈性版面配置 這個檢視來做出一個可以動態成長的布局設計;在這裡將會一開始顯示三個區塊,這三個區塊將會顯示在同一個 Row,接下來將會顯示一個文字,該文字將會獨佔一個 Row,在下一個 Row 將會顯示出一個比較大的區塊而且也是獨占一個 Row,最後,將會產生出 31 個區塊,用來模擬可以做到動態的產生出不同數量的區塊檢視,但是一樣可以顯示在螢幕上的效果,而最後的執行效果將會如下圖所示,左下圖為一開始執行的畫面,而右下圖為向上捲動後的結果,此時底下是還有很多的區塊尚未顯示出來的。
該文件的專案原始碼可以透過 GitHub 來取得
建立一個 使使用 FlexLayout 專案
- 開啟 Visual Studio 2019 程式
- 當 Visual Studio 2019 開始 視窗 出現之後,請點選左下角的 [建立新專案] 選項
- 當 [建立新專案] 對話窗出現之後,請在中間最上方的搜尋文字輸入盒中輸入 [prism] 關鍵字,搜尋所有與 Prism 有關的專案樣板
- 請選擇 [Prism Blank App (Xamarin.Forms)] 這個專案樣板
- 當出現 [設定新的專案] 對話窗,請在 [專案名稱] 輸入 [DynamicFlexLayout]
- 最後點選該對話窗右下方的 [建立] 按鈕
- 現在將會看到 [PRISM PROJECT WIZARD] 對話窗,請勾選 ANDROID, iOS, UWP 三個行動裝置平台,接著在底下 [Container] 下拉選單,選擇 Unity 項目
- 最後,點選 [CREATE PROJECT] 按鈕,以便產生 Xamarin.Forms 專案
安裝需要用到的 PropertyChanged.Fody NuGet 套件
- 當這個 Xamarin.Forms 專案建立成功之後,請在該方案中,找到 Xamarin.Forms 使用的專案(這是一個 .NET Standard 類別庫,簡稱為 SCL ),請在該專案中,使用滑鼠右擊 [相依性] 節點,選擇 [管理 NuGet 套件] 選項
- 在 [NuGet: XXX] 視窗中,點選 [瀏覽] 標籤頁次,並且在下方的搜尋文字輸入盒中,輸入 [propertychanged.fody] 關鍵字,搜尋出這個 NuGet 套件
- 當出現 [PropertyChanged.Fody] NuGet 套件,請點選該套件,並且點選右方的 [安裝] 按鈕,將這個套件安裝到 Xamarin.Forms 專案內
- 請查看 Xamarin.Forms 專案內,並沒有 [FodyWeavers.xml] 這個檔案,因此,使用滑鼠右擊 Xamarin.Forms 專案節點,選擇 [建置] 選項
- 當建置完成之後,在這個 Xamarin.Forms 專案內將會出現 [FodyWeavers.xml] 檔案
安裝需要用到的 Xamarin.Essentials NuGet 套件
- 在 [NuGet: XXX] 視窗中,搜尋文字輸入盒中,輸入 [Xamarin.Essentials] 關鍵字,搜尋出這個 NuGet 套件
- 當出現 [Xamarin.Essentials] NuGet 套件,請點選該套件,並且點選右方的 [安裝] 按鈕,將這個套件安裝到 Xamarin.Forms 專案內
修正因為安裝 Xamarin.Essentials 帶來的錯誤
現在,可以從 Visual Studio 2019 的錯誤視窗中,看到底下的錯誤訊息
NU1107 偵測到 Xamarin.Android.Support.Compat 有版本衝突。請將 Xamarin.Android.Support.Compat 28.0.0.1 直接安裝/參考到專案 DynamicFlexLayout.Android 來解決此問題。
DynamicFlexLayout.Android -> DynamicFlexLayout -> Xamarin.Essentials 1.1.0 -> Xamarin.Android.Support.Compat (>= 28.0.0.1)
DynamicFlexLayout.Android -> Xamarin.Android.Support.Design 27.0.2.1 -> Xamarin.Android.Support.Compat (= 27.0.2.1). DynamicFlexLayout.Android D:\Vulcan\GitHub\Xamarin2019\DynamicFlexLayout\DynamicFlexLayout\DynamicFlexLayout.Android\DynamicFlexLayout.Android.csproj 1
想要解決此一問題:
- 使用滑鼠右擊方案節點(方案總管最上方的那個節點),選擇 [管理方案的 NuGet 套件]
- 點選 [更新] 標籤頁次
- 勾選該標籤頁次內的所有項目
- 點選右上方的更新按鈕,就可以升級這些套件到最新版本了
建立資料模型
- 滑鼠右擊 Xamarin.Forms 專案,選擇 [加入] > [新增資料夾]
- 將新增資料夾的名稱設定為 [Models]
- 滑鼠右擊剛剛建立的 [Models] 資料夾,選擇 [加入] > [類別]
- 在 [新增項目] 對話窗下方的 [名稱] 欄位中,輸入 [ItemBlock]
- 點選右下方的 [新增] 按鈕
- 將底下程式碼填入到這個新建立的類別檔案內
public class ItemBlock
{
public double Width { get; set; }
public double Height { get; set; }
public Color Color { get; set; }
public bool ShowLabel { get; set; }
public bool ShowBoxView { get; set; } = true;
}
建立支援方法類別
- 滑鼠右擊 Xamarin.Forms 專案,選擇 [加入] > [新增資料夾]
- 將新增資料夾的名稱設定為 [Helpers]
- 滑鼠右擊剛剛建立的 [Helpers] 資料夾,選擇 [加入] > [類別]
- 在 [新增項目] 對話窗下方的 [名稱] 欄位中,輸入 [ScreenInfo]
- 點選右下方的 [新增] 按鈕
- 將底下程式碼填入到這個新建立的類別檔案內
這個類別將會儲存來自於 Xamarin.Essentials:裝置顯示資訊 Device Display Information 所讀取到的相關螢幕資訊,例如:當時螢幕的實際可用寬度與高度的畫素是多少?這個裝置的密度是多少?接著將會經過計算,得知該螢幕的寬度與高度的設計尺寸(單位為 dip) 是多少?而這個裝置當時寬度的設計尺寸將會與 360 dip (這是在設計這個 App 畫面時所使用的設計尺寸)進行計算,得到一個縮放比例,將其值儲存到 DesignScalar 靜態屬性中。最後,將會提供一個靜態方法,該方法會使用剛剛計算出來的設計尺寸所放比例 DesignScalar,計算出現在提供的設計尺寸,是否放大多少還是要縮小多少。
public class ScreenInfo
{
public static double ScreenPixelWidth { get; set; }
public static double ScreenPixelHeight { get; set; }
public static double DesignScreenWidth { get; set; }
public static double DesignScreenHeight { get; set; }
public static double DesignTimeScreenWidth { get; set; } = 360;
public static double DesignScalar { get; set; }
public static double Density { get; set; }
public static double GetNewDesingSize(double value)
{
return DesignScalar * value;
}
}
建立一個啟動頁面
- 在 SCL 專案中,滑鼠右擊 [Views] 這個節點,選擇 [加入] > [新增項目]
- 在 [新增項目] 對話窗的左方,分別點選 [已安裝] > [Visual C# 項目] > [Prism] > [Xamarin.Forms]
- 在 [新增項目] 對話窗的中間,點選 [Prism ContentPage (Xamarin.Forms)] 這個項目
- 在 [新增項目] 對話窗的最下方的名稱欄位,輸入 [SplashPage]
- 最後點選 [新增] 按鈕,完成建立這個新頁面 View 與 檢視模型 ViewModel
- 在 [ViewModels] 資料夾內,找到 [SplashPageViewModel.cs] 節點,並開啟這個節點
- 使用底下程式碼覆寫這個這個檔案內容
在這個檢視模型類別中,將會透過 OnNavigatedTo 事件,從 Xamarin.Essentials:裝置顯示資訊 Device Display Information 所讀取到的相關螢幕資訊,儲存到 ScreenInfo 靜態屬性內,最後將會
navigationService.NavigateAsync("/NavigationPage/MainPage");
敘述,導航到 MainPage 這個頁面using Prism.Commands;
using Prism.Mvvm;
using System;
using System.Collections.Generic;
using System.Linq;
namespace DynamicFlexLayout.ViewModels
{
using System.ComponentModel;
using DynamicFlexLayout.Helpers;
using Prism.Events;
using Prism.Navigation;
using Prism.Services;
using Xamarin.Essentials;
public class SplashPageViewModel : INotifyPropertyChanged, INavigationAware
{
public event PropertyChangedEventHandler PropertyChanged;
private readonly INavigationService navigationService;
public SplashPageViewModel(INavigationService navigationService)
{
this.navigationService = navigationService;
}
public void OnNavigatedFrom(INavigationParameters parameters)
{
}
public void OnNavigatedTo(INavigationParameters parameters)
{
var mainDisplayInfo = DeviceDisplay.MainDisplayInfo;
ScreenInfo.Density = mainDisplayInfo.Density;
ScreenInfo.ScreenPixelWidth = mainDisplayInfo.Width;
ScreenInfo.ScreenPixelHeight = mainDisplayInfo.Height;
ScreenInfo.DesignScreenWidth = ScreenInfo.ScreenPixelWidth/ScreenInfo.Density;
ScreenInfo.DesignScreenHeight = ScreenInfo.ScreenPixelHeight / ScreenInfo.Density;
ScreenInfo.DesignScalar = ScreenInfo.DesignScreenWidth / ScreenInfo.DesignTimeScreenWidth;
navigationService.NavigateAsync("/NavigationPage/MainPage");
}
public void OnNavigatingTo(INavigationParameters parameters)
{
}
}
}
修正該 Xamarin.Forms 的第一個顯示頁面
- 打開 [App.xaml.cs] 檔案
- 將第 26 行修正為
await NavigationService.NavigateAsync("SplashPage");
建立使用 FlexLayout 的頁面與商業邏輯
- 在 [Views] 資料夾內,打開 [MainPage.xaml] 檔案
- 修正使用底下的 XAML 語言宣告
在這裡的 FlexLayout 版面配置,將會透過 [BindableLayout.ItemsSource] 這個附加屬性,指定其子項目的來源,而這些子項目將會由該頁面的 檢視模型 ViewModel 來自動產生,因此 [BindableLayout.ItemsSource] 的屬性值將會是透過資料綁定的方式來取得。
而該 FlexLayout 的每個子項目將會使用 [BindableLayout.ItemTemplate] 這個附加屬性來指定,透過 [DataTemplate] 這個項目 Element,將會指定該子項目要顯示的 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="DynamicFlexLayout.Views.MainPage"
Title="FlexLayout 的動態調適">
<Grid>
<ScrollView>
<Grid
HorizontalOptions="Center">
<FlexLayout
Wrap="Wrap"
Direction="Row"
AlignItems="Start"
AlignContent="Start"
JustifyContent="Start"
BindableLayout.ItemsSource="{Binding myItemList}"
>
<BindableLayout.ItemTemplate>
<DataTemplate>
<Grid
WidthRequest="{Binding Width}" HeightRequest="{Binding Height}">
<Label
Text="Logo"
FontSize="30"
HorizontalTextAlignment="Center"
WidthRequest="{Binding Width}" HeightRequest="{Binding Height}"
IsVisible="{Binding ShowLabel}"/>
<BoxView
Color="{Binding Color}"
WidthRequest="{Binding Width}" HeightRequest="{Binding Height}"
IsVisible="{Binding ShowBoxView}"/>
</Grid>
</DataTemplate>
</BindableLayout.ItemTemplate>
</FlexLayout>
</Grid>
</ScrollView>
</Grid>
</ContentPage>
- 在 [ViewModels] 資料夾內,打開 [MainPageViewModel.xaml] 檔案
- 修正使用底下的 C# 程式碼
在 [OnNavigatedTo] 事件中,將會建立 [ItemBlock] 類別物件,並且加入到型別為
ObservableCollection<ItemBlock>
的 [myItemList] 屬性內。using Prism.Commands;
using Prism.Mvvm;
using Prism.Navigation;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace DynamicFlexLayout.ViewModels
{
using System.Collections.ObjectModel;
using System.ComponentModel;
using DynamicFlexLayout.Helpers;
using DynamicFlexLayout.Models;
using Prism.Events;
using Prism.Navigation;
using Prism.Services;
using Xamarin.Forms;
public class MainPageViewModel : INotifyPropertyChanged, INavigationAware
{
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<ItemBlock> myItemList { get; set; } = new ObservableCollection<ItemBlock>();
private readonly INavigationService navigationService;
public MainPageViewModel(INavigationService navigationService)
{
this.navigationService = navigationService;
}
public void OnNavigatedFrom(INavigationParameters parameters)
{
}
public void OnNavigatedTo(INavigationParameters parameters)
{
Random rnd = new Random();
ItemBlock fooItem;
for (int i = 0; i < 3; i++)
{
fooItem = new ItemBlock()
{
Width = ScreenInfo.GetNewDesingSize(100),
Height = ScreenInfo.GetNewDesingSize(100),
Color = Color.FromRgba(rnd.Next(256), rnd.Next(256), rnd.Next(256), rnd.Next(256)),
ShowLabel = false,
};
myItemList.Add(fooItem);
}
fooItem = new ItemBlock()
{
Width = ScreenInfo.GetNewDesingSize(300),
Height = ScreenInfo.GetNewDesingSize(50),
Color = Color.FromRgba(rnd.Next(256), rnd.Next(256), rnd.Next(256), rnd.Next(256)),
ShowLabel = true,
ShowBoxView = false
};
myItemList.Add(fooItem);
fooItem = new ItemBlock()
{
Width = ScreenInfo.GetNewDesingSize(301),
Height = ScreenInfo.GetNewDesingSize(200),
Color = Color.FromRgba(rnd.Next(256), rnd.Next(256), rnd.Next(256), rnd.Next(256)),
ShowLabel = false,
};
myItemList.Add(fooItem);
for (int i = 0; i < 31; i++)
{
fooItem = new ItemBlock()
{
Width = ScreenInfo.GetNewDesingSize(100),
Height = ScreenInfo.GetNewDesingSize(100),
Color = Color.FromRgba(rnd.Next(256), rnd.Next(256), rnd.Next(256), rnd.Next(256)),
ShowLabel = false,
};
myItemList.Add(fooItem);
}
}
public void OnNavigatingTo(INavigationParameters parameters)
{
}
}
}
執行結果
在這裡,將會開啟兩種模式的 Android 模擬器,在下圖左方的是 xxhdpi (螢幕密度為 2.65)、1080x1920 的裝置,而右下方的是 xhdpi (螢幕密度為 1.84)、720x1280 的裝置。透過上面的演算法之後,不論當時螢幕的設計尺寸是否大於 360 dip(device independent pixels 與裝置無關的畫素) 或者小於 360 dip 的裝置,都可以完美的顯示比例將這個內容顯示在裝置螢幕上;不過,對於不需要做到完美排版的需求,其實是不需要針對顯示縮放比例做這樣的獨特設計的。
在下方,將會使用 iOS 模擬器作為執行測試環境,在下圖左方(iPhone 6)的螢幕密度為2、750x1334 的裝置,而右下方(iPhone5s)的是螢幕密度為2、640x1136 的裝置。透過上面的演算法之後,不論左下方螢幕的設計尺寸(為375)是大於 360 dip(device independent pixels 與裝置無關的畫素) 而右下方螢幕的設計尺寸(為320)是小於 360 dip 的裝置,都可以完美的顯示比例將這個內容顯示在裝置螢幕上。
在下方(iPhone 8 Plus)的螢幕密度為3、1242x2208 的裝置,此時螢幕的設計尺寸為 414x736,是大於 360 dip(device independent pixels 與裝置無關的畫素) 可以完美的顯示比例將這個內容顯示在裝置螢幕上。