[MAUI]在.NET MAUI中复刻苹果Cover Flow

Posted 林晓lx

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[MAUI]在.NET MAUI中复刻苹果Cover Flow相关的知识,希望对你有一定的参考价值。

Cover Flow是iTunes和Finder中的一个视图选项,允许用户使用水平滚动的图像查看他们的音乐库或文件。Cover Flow的交互设计非常优秀:通过指尖滑动从堆叠的专辑库中翻动和挑选一张专辑的交互方式不仅有趣,而且在有限的屏幕空间内,展现了更多的专辑封面。那个是乔布斯时代的苹果——使事情变得简单和有趣。最近我很怀念这个功能,但由于我手头上已经没有任何一台设备能访问这个功能了。于是在.NET MAUI中复刻了Cover Flow。

@


Cover Flow是iTunes和Finder中的一个视图选项,允许用户使用水平滚动的图像查看他们的音乐库或文件。

2007年9月5日iPod classic/nano3/touch在同一场发布会上发布,苹果首次向我们展示了Cover Flow

在iOS7之前的“音乐”App中,旋转设备90度,或在iTunes中的“查看”下,选择“Cover Flow”都可以进入到Cover Flow视图。

Cover Flow的交互设计非常优秀:通过指尖滑动从堆叠的专辑库中翻动和挑选一张专辑的交互方式不仅有趣,而且在有限的屏幕空间内,展现了更多的专辑封面。

但由于流媒体时代弱化了专辑的概念,拟物化设计退潮以及设备性能/续航等方面的考虑,苹果逐步放弃了Cover Flow。

在2012年新发布的iTunes 11,2013年新发布的iOS 7,以及2018年发布的MacOS Mojave中删除了Cover Flow界面,Gallery View取而代之

那个是乔布斯时代的苹果——使事情变得简单和有趣。最近我很怀念这个功能,但由于我手头上已经没有任何一台设备能访问这个功能了。于是在
.NET MAUI 中复刻了Cover Flow。

使用.NET MAUI实现跨平台支持,本项目可运行于Android、iOS平台。

原理

实际上,Cover flow的原理非常简单,核心算法是对专辑图片进行3D变换(3DTransform)。

.NET MAUI 并没有直接提供3D变换,但我们可以通过SkiaSharp来实现。

PS: Skia 本身是一个开源图形库,它提供适用于各种语言和硬件平台的通用 API,(如 C++/Qt、Chrome、Android、iOS等 ),根据本博文提到的算法,你可以用Skia尝试在你擅长的平台上实现相同的效果。

3D旋转

视图元素的3D变换(3DTransform)中,有一类是以视图元素的Y或X轴作为旋转中心做旋转,称之为3D旋转,除了专业的程序设计领域外,经常使用图形处理工具,甚至是ppt的同学可能都熟悉这个概念。在ppt中插入图形,设置形状格式,可以看到“三维旋转”的选项,如下图:

这里涉及到一个透视的概念,透视是指在视觉上,远处的物体比近处的物体小,来思考一下,在现实世界中要看到同样大小的物体,可以离得很近,视野变大,物体的畸变会变大。也可以离得很远,用一个望远镜去看,视野变小,物体的畸变也会变小。透视参数就是在屏幕中模拟了现实世界中近大远小透视效果,我简单用ptt做一个演示:

三个图形沿Y轴方向旋转, 从左到右透视距离依次减小,透视角度依次增大,换句话说是离得更近,视野变大,物体的畸变变大。

在大多数支持3D旋转的图形系统中都会包含透视这个参数变量,如css中的perspective亦或是ppt中的“透视”格式。

在Skia中,3D变换是通过矩阵乘法实现的,这里需要大致了解数字图像处理的基本知识,可以参考这里

矩阵乘法就是把原始图像矩阵的横排和变换矩阵的竖排相应位相乘,将结果相加。

在二维空间,原始图像中的每个像素点 (x,y) 所代表的单列矩阵,通过变换矩阵相乘,得到新的像素点 (x\',y\')。
例如缩小图像:

因为要考虑平移等非线性计算,常用3*3的矩阵来表示变换
在三维空间,用一个4*4的矩阵来表示变换,例如围绕Y轴旋转的变换矩阵如下:

|  cos(α)  0  –sin(α)  0  |
|    0     1     0     0  |
|  sin(α)  0   cos(α)  0  |
|    0     0     0     1  |

平行变换

另外涉及到的图像处理是平行变换(Skew),每一个平台上的值可能不同,但是原理都是通过增加或减少X轴或Y轴的值来实现平行变换。

在Skia中,根据参数值转换 x\' 后的值随着 y 增加而增加。 这就是导致倾斜的原因。

如有一个200*100的图形,其左上角位于 (0、0) 的点上,并且呈现 xSkew 值为 1.5,则以下并行影像结果如下:

底部边缘 y 的坐标值为 100,因此将 150 像素移向右侧。

接下来我们用代码实现3D变换

创建3D变换控件

我们还是以分治的思路实现,图片变换由控件内部实现,平移及动画由控件外部实现。

新建.NET MAUI项目,命名Coverflow。将界面图片资源文件拷贝到项目\\Resources\\Raw中并将他们包含在MauiImage资源清单中。

<ItemGroup>
	  <MauiImage Include="Resources\\Raw\\*.jpg" />
</ItemGroup>

在项目中添加SkiaSharp绘制功能的引用Microsoft.Maui.Graphics.Skia以及SkiaSharp.Views.Maui.Controls

<ItemGroup>
    <PackageReference Include="Microsoft.Maui.Graphics.Skia" Version="7.0.59" />
    <PackageReference Include="SkiaSharp.Views.Maui.Controls" Version="2.88.3" />
</ItemGroup>

创建3D变换的图片控件RotationImage.xaml,代码如下:

<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:forms="clr-namespace:SkiaSharp.Views.Maui.Controls;assembly=SkiaSharp.Views.Maui.Controls"
             x:Class="Coverflow.RotationImage">

        <forms:SKCanvasView x:Name="canvasView"
                           Grid.Row="8"
                           PaintSurface="OnCanvasViewPaintSurface" />

</ContentView>

绘制封面图片

在RotationImage.xaml.cs中添加代码:

SKBitmap对象

public SKBitmap bitmap  get; private set; 

初始化方法,以及在图形大小变化时应用初始化

private async void RotationImage_SizeChanged(object sender, EventArgs e)

    await InitBitmap();


private async Task InitBitmap()

    using (Stream stream = await FileSystem.OpenAppPackageFileAsync("./15.jpg"))
    
        if (stream!=null)
        
            var mainDisplayInfo = DeviceDisplay.Current.MainDisplayInfo;
            var pixcelHeight = mainDisplayInfo.Density*200;
            var pixcelWidth = mainDisplayInfo.Density*200;

            var bitmap = SKBitmap.Decode(stream);
            bitmap= bitmap.Resize(new SKImageInfo((int)pixcelHeight,
                (int)pixcelWidth),
                SKFilterQuality.Medium);
            this.bitmap=bitmap;
        

    

初始化时将读取图片资源文件,然后将图片缩放到200*200的大小。

注意此处使用mainDisplayInfo.Density将MAUI各平台的逻辑分辨率转为图片的真实分辨率

此时在画布中绘制了一个简单的200*200专辑封面图片

应用3D旋转

在Skia用SKMatrix44类来描述4*4的变换矩阵,同时提供了 CreateRotation 和 CreateRotationDegrees 方法,可用于指定旋转围绕的轴

RotationImage_SizeChanged中,添加代码如下:


SKMatrix matrix = SKMatrix.CreateTranslation(-xCenter, -yCenter);

SKMatrix44 matrix44 = SKMatrix44.CreateIdentity();
matrix44.PostConcat(SKMatrix44.CreateRotationDegrees(1, 0, 0, (float)0));
matrix44.PostConcat(SKMatrix44.CreateRotationDegrees(0, 1, 0, (float)25));
matrix44.PostConcat(SKMatrix44.CreateRotationDegrees(0, 0, 1, (float)0));

SKMatrix44 perspectiveMatrix = SKMatrix44.CreateIdentity();
perspectiveMatrix[3, 2] = -1 / 800;
matrix44.PostConcat(perspectiveMatrix);

matrix= matrix.PostConcat(matrix44.Matrix);

matrix= matrix.PostConcat(
    SKMatrix.CreateTranslation(xCenter, yCenter));

将变换矩阵应用到画布中

canvas.SetMatrix(matrix);

此时在画布中专辑封面图片以800的透视距离,绕Y轴旋转25度

应用平行变换

首先计算倾斜角度,如有一个200*100的图形,其左上角位于 (0、0) 的点上,图中的角度α:

150 像素到 100 像素垂直方向的比率是该角度的正切值,即 56.3 度。

RotationImage_SizeChanged中,对matrix对象应用平行变换

matrix.SkewY =  (float)Math.Tan(Math.PI * (float)15 / 180);

此时在画布中专辑封面图片以15度平行变换

绘制倒影

在cover flow中,封面图片包含倒影效果。

之前的绘制的封面图片,在控件中央(也是画布中央)的位置。为了放置倒影后仍然处于控件中心,画布应该一分为二:上半部分绘制封面图片,下半部分绘制倒影。

更改代码:

//float yBitmap = yCenter - bitmap.Height / 2;
float yBitmap = yCenter-bitmap.Height;

绘制倒影封面图片:

using (SKPaint paint = new SKPaint())

    paint.Color = SKColors.Black.WithAlpha((byte)(255 * 0.8));
    canvas.Scale(1, -1, 0, yCenter);
    canvas.DrawBitmap(bitmap, xBitmap, yBitmap, paint);
    SKRect rect = SKRect.Create(xBitmap, yBitmap, bitmap.Width, bitmap.Height);
    canvas.DrawRect(rect, paint);

倒影用一个黑色半透明的矩形覆盖在原始封面图片上,并且将画布沿Y轴翻转,使得倒影图片在封面图片的下方。

创建绑定属性

将图片源,旋转角度,平行角度等作为绑定属性,以便在XAML中绑定。代码忽略。

创建绑定数据

创建MainPageViewModel.cs,用于界面绑定数据源。

AlbumInfo描述专辑信息

public class AlbumInfo

    public AlbumInfo()  

    public string AlbumName  get; set; 
    public string AlbumArtSource  get; set; 

在MainPageViewModel构造函数中,初始化AlbumInfo列表,在控件中绑定此列表作为数据源

创建布局

在MainPage.xaml中,创建一个Grid作为专辑封面容器,我们将使用绑定集合的方式,将专辑封面添加到这个容器中。

代码如下:

<Grid Grid.Row="1"
    x:Name="BoxLayout"
    Background="black"
    BindableLayout.ItemsSource="Binding AlbumInfos">

它的DataTemplate代表一个专辑信息,使用Grid布局,专辑封面图片与专辑名称分别位于Grid的第一行和第二行。

<BindableLayout.ItemTemplate>
    <DataTemplate>
        <Grid Style="StaticResource BoxFrameStyle"
                Background="Transparent">
            <Grid.RowDefinitions>
                <RowDefinition></RowDefinition>
                <RowDefinition Height="auto"></RowDefinition>
            </Grid.RowDefinitions>
            <controls:RotationImage WidthRequest="200"
                                    HeightRequest="500"
                                    ImageWidth="200"
                                    ImageHeight="200"
                                    Source="Binding AlbumArtSource"></controls:RotationImage>


            <Label Margin="0,30,0,0"
                    Text="Binding AlbumName"
                    HorizontalTextAlignment="Center"
                    VerticalOptions="Center"></Label>


        </Grid>
    </DataTemplate>

</BindableLayout.ItemTemplate>

对专辑封面Grid的样式进行定义:

<ContentPage.Resources>
   <Style TargetType="Grid"
               x:Key="BoxFrameStyle">
            <Setter Property="HeightRequest"
                    Value="100"></Setter>
            <Setter Property="WidthRequest"
                    Value="100"></Setter>
            <Setter Property="HorizontalOptions"
                    Value="Center"></Setter>
            <Setter Property="VerticalOptions"
                    Value="Center"></Setter>
        </Style>
</ContentPage.Resources>

效果如下:

计算位置

Cover Flow的滑动交互由两种方式实现:1. 左右轻扫屏幕,切换到上一张或下一张专辑封面;2. 拨动底部Slider控件,切换到指定的专辑封面。

两种方式都会改变当前位置,我们将当前位置定义为一个整数,表示当前专辑在容器中的索引。

private int currentPos;

当手势触发时,根据手势方向,改变当前位置:

this.currentPos=e.Direction==SwipeDirection.Right
    ? Math.Max(0, this.currentPos-1)
    : Math.Min(this.BoxLayout.Children.Count-1, this.currentPos+1);

当Slider控件的值发生变化时,根据Slider的值,计算当前位置:


var currentPos = (int)Math.Floor(e.NewValue*  (this.BoxLayout.Children.Count-1));
if (this.currentPos!=currentPos)

    this.currentPos = currentPos;


当前位置索引的值始终在0到专辑封面数量减1之间。

当前封面是从专辑堆叠中挑选出来的,它的位置是固定的,左右两边的封面相对于当前封面,有一个固定的距离,step为当前封面和左右第一张封面之间的距离,slidePadding为其它封面和当前封面之间的距离。

其它封面的位置,分为两种情况:1. 在当前封面的左边;2. 在当前封面的右边。

封面叠层的顺序是当前封面最靠上,左右两边的封面随着距离由近及远,依次向下叠放。

创建RenderTransform方法,作为刷新的入口,当当前位置发生变化时,调用此方法,重新计算每个专辑封面的位置和叠放顺序。

private void RenderTransform(int currentPos)

    var step=40.0;
    var currentSlidePadding=100.0;
    foreach (var bitmapLayout in this.BoxLayout.Children)
    
        var pos = this.BoxLayout.Children.IndexOf(bitmapLayout);
        double xBitmap;
        int zIndex;
        if (pos < currentPos)
        
            zIndex=pos;
            xBitmap = (double)(-(currentPos * step) + (pos * step)  - currentSlidePadding);
        
        else if (pos > currentPos)
        
            zIndex=this.BoxLayout.Children.Count-pos;
            xBitmap = (double)(((pos - currentPos) * step)  + currentSlidePadding);
        
        else
        
            xBitmap =  0;
            zIndex=this.BoxLayout.Children.Count;
        

        (bitmapLayout as VisualElement).ZIndex = zIndex;
        (bitmapObj as RotationImage).TranslationX=xBitmap;
    

创建后,运行效果如下

计算3D旋转

我们对当前封面的左边的封面,以及当前封面的右边的封面,分别计算旋转角度,以实现3D效果。

var rotateY = 65;
foreach (var bitmapLayout in this.BoxLayout.Children)

    double targetRotateY;
    if (pos < currentPos)
     
        targetRotateY=rotateY;
    
    else if (pos > currentPos)
       
        targetTransY=transY;
    
    else
    
        targetTransY=0;
    
    (bitmapObj as RotationImage).RotateY=targetRotateY;

再对3D旋转的封面进行平行变换调整,并对封面位置作微调

var rotateY = 65;
var skewY = 0;
var transY = 0;
foreach (var bitmapLayout in this.BoxLayout.Children)

   
    double targetRotateY;
    double targetSkewY;
    double targetTransY;
    if (pos < currentPos)
    
      
        targetRotateY=rotateY;
        targetSkewY=skewY;
        targetTransY=-transY;

    
    else if (pos > currentPos)
    
       
        targetRotateY=-rotateY;
        targetSkewY=-skewY;
        targetTransY=transY;
    
    else
    
        targetRotateY=0;
        targetSkewY=0;
        targetTransY=0;

    

    (bitmapObj as RotationImage).RotateY=targetRotateY;
    (bitmapObj as RotationImage).TranslationX=xBitmap;
    (bitmapObj as RotationImage).SkewY=targetSkewY;
    (bitmapObj as RotationImage).TransY=targetTransY;

最后配置封面图片的缩放,以及封面标题显示、隐藏。

效果如下:

至此我们完成了静态的工作内容,下一步要让界面的过渡动画更加流畅,我们将使用MAUI的动画框架,实现平滑的过渡动画。

创建动效

我们通过创建Animation对象,添加子动画来实现。详情请参考Animation子动画

RotateY、SkewY、TranslationX、Scale直接赋值的方式将由动画代替。动画是一种缓动机制,通过属性的缓慢改变实现平滑的过渡动画。

在渲染中我们为每一个封面创建一个Animation对象,然后添加子动画,最后调用Animation对象的Commit方法,

在400ms内将各属性缓慢应用到界面上。各属性步调一致,所以动画的过程是平滑的。

foreach (var bitmapLayout in this.BoxLayout.Children)

    uint duration = 400;
    ...

    Animation albumAnimation = new Animation();


    var originTranslationX = (bitmapLayout as VisualElement).TranslationX;
    var originScale = (bitmapLayout as VisualElement).Scale;
    var animation1 = new Animation(v => (bitmapLayout as VisualElement).TranslationX = v, originTranslationX, xBitmap, Easing.CubicInOut);
    var animation2 = new Animation(v => (bitmapLayout as VisualElement).Scale = v, originScale, targetScale, Easing.CubicInOut);


    if (targetSkewY!=(bitmapObj as RotationImage).SkewY)
    
        var animation4 = new Animation(v => (bitmapObj as RotationImage).SkewY = v, (bitmapObj as RotationImage).SkewY, targetSkewY, Easing.CubicInOut);
        albumAnimation.Add(0, 1, animation4);

    

    if (targetRotateY!=(bitmapObj as RotationImage).RotateY)
    
        var animation3 = new Animation(v => (bitmapObj as RotationImage).RotateY = v, (bitmapObj as RotationImage).RotateY, targetRotateY, Easing.CubicInOut);
        albumAnimation.Add(0, 1, animation3);

    

    if (targetTransY!=(bitmapObj as RotationImage).TransY)
    
        var animation5 = new Animation(v => (bitmapObj as RotationImage).TransY = v, (bitmapObj as RotationImage).TransY, targetTransY, Easing.CubicInOut);
        albumAnimation.Add(0, 1, animation5);

    
    albumAnimation.Add(0, 1, animation1);
    albumAnimation.Add(0, 1, animation2);

    albumAnimation.Commit((bitmapLayout as VisualElement), "AlbumArtImageAnimation", 16, duration);

效果如下:

在页面大小变化时,重新渲染变换。

    private void MainPage_SizeChanged(object sender, EventArgs e)
    
        RenderTransform(currentPos);
    

step和currentSlidePadding值将由屏幕宽度计算得出,使得在不同屏幕大小设备,或者横竖屏切换时,效果保持一致。

var xCenter = this.BoxLayout.Width / 2;
var step = xCenter*0.12;
var currentSlidePadding = this.BoxLayout.Width * 0.15;

项目地址

Github:maui-samples

关注我,学习更多.NET MAUI开发知识!

MAUI自定义 .NET MAUI XAML 页面中的布局

介绍

.NET MAUI 布局面板可帮助您在各种设备上为应用程序创建一致的用户界面。

假设您正在构建一个提示计算器应用程序,您计划在许多计算机和设备上部署该应用程序。每个设备可以具有不同的屏幕尺寸和像素密度。您的目标是使应用程序在所有设备上看起来尽可能相似。您希望避免手动计算每个屏幕尺寸的视图大小和位置。.NET MAUI 包括一个布局管理系统,可为您执行这些计算。您可以将视图放在布局面板中,这些布局面板会自动管理其子视图的大小和位置。这些面板使在不同设备上创建一致的用户界面变得更加容易。

在本模块中,您将构建一个在不同设备上看起来相似的 .NET MAUI 应用程序。首先,您将设置视图的首选大小和位置。然后,您将使用 StackLayout 垂直排列视图。接下来,使用 将视图放入行和列中。在模块结束时,您将拥有一个 .NET MAUI 应用程序,该应用程序在每种设备类型和屏幕尺寸上看起来都一致。Grid

学习目标

在本模块中,您将:

排列应用中的用户界面元素并调整其大小
使用在垂直或水平列表中显示视图StackLayout
使用网格在行和列中显示视图

先决条件

安装了 .NET MAUI 工作负载的 Visual Studio 2022
熟悉 C# 和 .NET
指定视图的大小
设计跨多个设备一致的用户界面非常困难,因为设备的大小可能不同,像素密度也不同。考虑可用的不同设备:移动设备,平板电脑,台式机等。我们如何创建一个在每个界面上看起来相似的用户界面?

.NET MAUI 提供布局面板来帮助您构建一致的用户界面。布局面板负责调整其子视图的大小和位置。在本单元中,您将了解布局系统在 .NET MAUI 中的工作原理。您将了解默认情况下视图的大小,以及如何在运行时请求视图的特定大小和位置。

什么是布局面板?

布局面板是一个 .NET MAUI 容器,用于保存子视图的集合并确定其大小和位置。布局面板会在应用程序大小更改时自动重新计算;例如,当用户旋转设备时。

备注

术语“视图”或“子视图”是指放置在布局面板上的控件。视图可以是标签、按钮、输入字段或 .NET MAUI 支持的任何其他类型的可视元素。

.NET MAUI 具有多个布局面板供您选择。每个面板以不同的方式管理其子视图。下图显示了一些最常见选项的概念性概述。

  • StackLayout:将其子视图排列在单个行或列中。除了 之外,还有一个新的优化和当你不需要改变方向。StackLayoutVerticalStackLayoutHorizontalStackLayout
  • AbsoluteLayout:使用 x 和 y 坐标排列其子视图。
  • FlexLayout:像 a 一样排列其子视图,但如果它们不适合单个行或列,则可以将它们换行。StackLayout
  • Grid:将其子视图排列在由行和列创建的单元格中。

还有第五种类型的布局面板称为 a ,它使您能够指定如何相对于彼此排列子视图。您应该使用该控件,而不是因为它的性能更好。 包含在 .NET MAUI 中,以便与较旧的 Xamarin 应用向后兼容。RelativeLayoutFlexLayoutRelativeLayoutRelativeLayout

生成 .NET MAUI 页面的典型过程是创建布局面板,然后向其添加子视图。向布局添加视图时,可以影响其大小和位置。但是,该面板根据其内部布局算法拥有最终决定权。

在了解如何为视图请求特定大小之前,请先了解布局系统在默认情况下如何调整视图大小。

视图的默认大小

如果未指定视图的大小,它将自动增长到足够大以适合其内容。例如,请考虑以下 XAML:

<Label
    Text="Hello"
    BackgroundColor="Silver"
    VerticalOptions="Center"
    HorizontalOptions="Center" 
    FontSize="40"/>

本示例定义一个标签,用于在银色背景上显示单词。由于您没有指定标签的大小,因此它将自动调整大小,使其适合单词 。下图显示了在安卓设备上呈现的标签:HelloHello

备注

您可以设置标签的背景色,以帮助您确定标签在运行时的大小。这是一个很好的调试技术,在生成 UI 时要记住。

指定视图的大小

生成 UI 时,通常需要控制视图的大小。例如,假设你正在构建一个登录页,并且你希望登录按钮正好是屏幕宽度的一半。如果对视图使用了默认大小调整,则按钮将仅为文本“登录”的大小。该大小不够大,因此您需要自己指定大小。

基类定义了影响视图大小的两个属性:和 。 用于指定宽度,并允许您指定高度。这两个属性的类型都是 。ViewWidthRequestHeightRequestWidthRequestHeightRequestdouble

下面的示例演示如何在 XAML 中指定标签的宽度和高度:

<Label
    Text="Hello"
    BackgroundColor="Silver"
    VerticalOptions="Center"
    HorizontalOptions="Center"
    WidthRequest="100"
    HeightRequest="300"
    FontSize="40"/>

结果如下所示:

备注

标签仍居中,但标签的文本不在标签的中心。

值得注意的一件事是这些属性的名称。这两个属性都包含单词请求。这个词意味着布局面板在运行时可能不会遵循它们。布局面板在其大小调整计算期间读取这些值,并尝试在可能的情况下容纳这些请求。如果没有足够的空间,布局面板可以忽略这些值。

尺寸单位

设置 和 时,可以使用文本值,如 。在 .NET MAUI 级别,这些值没有单位。它们不是点或像素。它们只是 类型的值。.NET MAUI 在运行时将这些值传递给基础操作系统。操作系统提供了确定数字含义所需的上下文:WidthRequestHeightRequest100double

  • 在 iOS 上,这些值称为点。
  • 在Android上,它们是与密度无关的像素。

视图的呈现大小

由于由布局面板确定视图的大小,因此您无法在运行时使用和告诉您实际大小。例如,假设您为标签设置了 ,但面板没有足够的空间来满足请求。相反,该面板会为标签指定宽度。此时,如果检查属性的值,即使呈现的值为 。,它也会说。WidthRequestHeightRequestWidthRequest10080WidthRequest10080

为了解决此问题,基类定义了另外两个名为 和 的属性。这些属性是类型,表示视图的呈现宽度和高度。每当检索视图的大小时,请使用 和 属性。ViewWidthHeightdoubleWidthHeight

什么是布局选项类型?

LayoutOptions是封装了两个布局首选项的 C# 类型,以及 。这两个属性都与定位相关,但它们彼此不相关。以下是该类型的定义:AlignmentExpands

ublic struct LayoutOptions

    public LayoutAlignment Alignment  get; set; 
    public bool Expands  get; set; 
    ...

你会看这里,因为它是最常见和最直观的。Alignment

什么是布局对齐枚举?
LayoutAlignment是包含四个值的枚举:、、、和 。可以使用这些值来控制子视图在其布局面板提供给它的矩形中的定位方式。例如,请考虑以下代码和安卓屏幕截图:StartCenterEndFill

<StackLayout>
    <Label Text="Start" HorizontalOptions="Start" BackgroundColor="Silver" FontSize="40" />
    <Label Text="Center" HorizontalOptions="Center" BackgroundColor="Silver"  FontSize="40" />
    <Label Text="End" HorizontalOptions="End" BackgroundColor="Silver"  FontSize="40"/>
    <Label Text="Fill" HorizontalOptions="Fill" BackgroundColor="Silver"  FontSize="40"/>
</StackLayout>


该示例使用垂直视图,因此为每个子视图指定一行。 确定视图在其行中的位置。StackLayoutHorizontalOptions

什么是扩展?

结构的第二个属性是 。该属性允许 中的视图请求额外的空间(如果有)。您将在布局上的单元中更详细地浏览该属性。LayoutOptionsExpandsExpandsboolStackLayoutExpandsStackLayout

练习 - 探索对齐选项

在本练习中,您将使用 .NET MAUI 应用程序来查看四个主要布局选项在应用于 中包含的视图时的效果。您不会在练习中编写代码。相反,您将使用提供给您的解决方案并选择按钮来更改标签的布局选项。Grid

打开入门级解决方案

  1. 从 GitHub 克隆或下载锻炼存储库。 备注如果您计划从 Windows 在 Android 上运行和调试 .NET MAUI 应用程序,最好将练习内容克隆或下载到较短的文件夹路径(如 C:\\dev)中,以避免生成生成的文件超过最大路径长度。
  2. 使用 Visual Studio 从 exercise1/Alignment 文件夹中打开初学者解决方案。

测试应用程序行为

运行应用以测试并查看不同的布局选项如何更改标签的大小和位置。LayoutOptions

您将看到更改水平和垂直的按钮。选择它们并观察发生的情况。下图显示了如果选择“居中”的“水平”和“垂直对齐”选项会发生什么情况:LayoutOptions

请注意,对齐选项(、、 和 ) 可以更改视图的大小和对齐方式。StartCenterEndFill

使用堆栈布局排列视图

在垂直或水平列表中堆叠视图是用户界面的常见设计。想想应用程序中的一些常见页面。示例包括登录、注册和设置页面。所有这些页面通常都包含堆叠内容。在本单元中,您将学习如何使用和新的优化和 排列垂直或水平列表中的视图。StackLayoutVerticalStackLayoutHorizontalStackLayout

什么是 StackLayout、VerticalStackLayout 和 HorizontalStackLayout?

StackLayout是一个布局容器,用于从左到右或从上到下组织其子视图。方向基于其属性,默认值为自上而下。下图显示了垂直的概念视图。OrientationStackLayout

  • StackLayout具有它从其基类继承的列表。该列表存储视图,这很好,因为在 .NET MAUI 中使用的大多数 UI 元素都派生自 。布局面板也派生自 ,这意味着您可以根据需要嵌套面板。ChildrenLayoutViewView
  • VerticalStackLayout并且是当您知道方向不会更改时的首选布局,因为它们已针对性能进行了优化。HorizontalStackLayout

如何将视图添加到堆栈布局

在 .NET MAUI 中,可以向 C# 代码或 XAML 中的 视图添加视图。下面是使用代码添加的三个视图的示例:StackLayout

<StackLayout x:Name="stack">
</StackLayout>
public partial class MainPage : ContentPage

    public MainPage()
    
        InitializeComponent();
        var a = new BoxView  BackgroundColor = Colors.Silver, HeightRequest = 40 ;
        var b = new BoxView  BackgroundColor = Colors.Blue, HeightRequest = 40 ;
        var c = new BoxView  BackgroundColor = Colors.Gray, HeightRequest = 40 ;
        stack.Children.Add(a);
        stack.Children.Add(b);
        stack.Children.Add(c);
    

您可以将视图添加到集合中,并自动将视图定位在垂直列表中。以下是它在Android设备上的外观:ChildrenStackLayout

若要在 XAML 中执行相同的操作,请将子项嵌套在标记内。XAML 分析器会自动将嵌套视图添加到集合中,因为 是所有布局面板的。下面是将相同的三个视图添加到 XAML 中的示例:StackLayoutChildrenChildrenContentPropertyStackLayout

<StackLayout>
    <BoxView Color="Silver" />
    <BoxView Color="Blue" />
    <BoxView Color="Gray" />
</StackLayout>

如何在堆栈布局中对视图进行排序

集合中视图的顺序决定了它们在呈现时的布局顺序。对于在 XAML 中添加的视图,将使用文本顺序。对于在代码中添加的子级,布局顺序由调用方法的顺序确定。ChildrenAdd

如何更改堆栈布局中视图之间的间距

通常需要在 . 允许您使用属性控制每个子级之间的空间。默认值为零个单位,但您可以将其设置为对您来说合适的单位。下面是在 XAML 中将属性设置为的示例:StackLayoutStackLayoutSpacingSpacing30

<StackLayout Spacing="30">
    <BoxView Color="Silver" />
    <BoxView Color="Blue" />
    <BoxView Color="Gray" />
</StackLayout>

如何设置堆栈布局的方向

StackLayout允许您在列或行中排列子项。您可以通过设置其属性来控制此行为。到目前为止,我们只显示了垂直 。OrientationStackLayout

Vertical是默认值。是否显式设置 to 取决于您。一些程序员更喜欢显式设置,以使代码更具自我记录性。OrientationVertical

下面是在 XAML 中设置 to 的示例:OrientationHorizontal

<StackLayout x:Name="stack" Orientation="Horizontal">
    <BoxView Color="Silver" WidthRequest="40"/>
    <BoxView Color="Blue" WidthRequest="40"/>
    <BoxView Color="Gray" WidthRequest="40"/>
</StackLayout>

备注

如上一练习中所述,方向的更改会导致它忽略每个 的属性。相反,您可以设置 .以下屏幕截图显示了 UI 如何在 Android 设备上呈现:StackPanelHeightRequestBoxViewWidthRequest

在堆栈布局中设置视图的布局选项

每个景色都有一个和财产。可以使用这些属性在布局面板提供的矩形显示区域中设置视图的位置。VerticalOptionsHorizontalOptions

如上所述,使用 ,属性的行为取决于 . 在与 该属性相反的方向上使用该属性。默认情况下,不会在与堆栈布局相同的方向上为堆栈布局中的元素分配任何额外空间。在此默认情况下,为该方向分配位置不会更改元素的呈现。但是,当位置与扩展相结合时,渲染会发生变化。StackLayoutLayoutOptionsOrientationStackLayoutStackLayoutLayoutOptionsOrientationOrientation

什么是扩张?

从前面的单元中回想一下,该结构包含一个名为 的属性。此属性专为子视图设计,并允许子视图在有任何可用空间时请求额外的空间。下面是该属性的工作原理示例:LayoutOptionsboolExpandsStackLayoutExpands


请注意,中提供了额外的可用空间。额外的空间将在需要额外空间的所有视图之间平均分配。StackLayout

如何申请额外空间

每个视图都有两个名为 和 的属性。到目前为止,您已经看到了这些属性的四个值:、 、 和 。如果要请求额外的空间,请将该值替换为以下值之一:、 、 或 。LayoutOptionsVerticalOptionsHorizontalOptionsStartCenterEndFillLayoutOptionsStartAndExpandCenterAndExpandEndAndExpandFillAndExpand

以下是这些值中每个值的工作原理:

橙色框是视图,灰色矩形表示属性为其提供的额外空间。仅当使用值时,视图才会填充多余的空间。使用其他值时,多余的空间将保持为空,但不能由 中的其他视图使用。ExpandsFillAndExpandStackLayout

优化的堆栈布局

如前所述,和 是具有预定义方向的优化控件。建议尽可能使用这些控件,以获得最佳的布局性能。这些布局具有常规布局的功能。VerticalStackLayoutHorizontalStackLayoutStackLayoutLayoutOptionsSpacingStackLayout

<VerticalStackLayout Spacing="30">
    <BoxView Color="Silver" />
    <BoxView Color="Blue" />
    <BoxView Color="Gray" />
</VerticalStackLayout>
<HorizontalStackLayout Spacing="30">
    <BoxView Color="Silver" />
    <BoxView Color="Blue" />
    <BoxView Color="Gray" />
</HorizontalStackLayout>

练习 - 使用 StackLayout 构建用户界面

在本练习中,你将使用嵌套容器在用户界面 (UI) 中排列视图。第一个屏幕截图显示初学者项目实现的布局,第二个屏幕截图显示已完成项目的布局。您的工作是使用容器并将初学者项目转换为已完成的版本。StackLayoutStackLayoutLayoutOptions

探索入门级解决方案

入门解决方案包含一个功能齐全的提示计算器应用程序。你将首先浏览 UI 以了解应用的功能。

使用 Visual Studio,在上一练习开始时克隆的存储库中的 exercise2/TipCalculator 文件夹中打开初学者解决方案。

在你的首选操作系统上构建并运行应用。

在文本框中输入一个数字,然后使用应用查看其工作原理。

尝试使用吸头量按钮和滑块。

完成后,关闭应用程序。

打开 MainPage.xaml。请注意,所有视图都放在一个中,如以下 XAML 标记所示:VerticalStackLayout

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
         xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
         xmlns:local="clr-namespace:TipCalculator"
         x:Class="TipCalculator.MainPage">
    <VerticalStackLayout>
        <Label Text="Bill" />
        <Entry x:Name="billInput" Placeholder="Enter Amount" Keyboard="Numeric" />
        <Label Text="Tip"   />
        <Label x:Name="tipOutput" Text="0.00" />
        <Label Text="Total" />
        <Label x:Name="totalOutput" Text="0.00" />
        <Label Text="Tip Percentage" />
        <Label x:Name="tipPercent" Text="15%" />
        <Slider x:Name="tipPercentSlider" Minimum="0" Maximum="100" Value="15" />
        <Button Text="15%" Clicked="OnNormalTip" />
        <Button Text="20%" Clicked="OnGenerousTip" />
        <Button x:Name="roundDown" Text="Round Down" />
        <Button x:Name="roundUp"   Text="Round Up" />
    </VerticalStackLayout>
</ContentPage>

修复用户界面
现在,你已看到应用已运行,可以通过添加其他容器来使其外观更好。目标是使应用看起来像实验室开始时的屏幕截图。HorizontalStackLayout

打开 MainPage.xaml 文件。

将填充单位和间距单位添加到 :4010VerticalStackLayout

<VerticalStackLayout Padding="40" Spacing="10">

添加 一个以分组,该分组显示比尔,其下方的字段。将该属性设置为 。HorizontalStackLayoutLabelEntrySpacing10

将 帐单的 设置为 ,将属性设置为 。这将确保标签与字段垂直对齐。WidthRequestLabel100VerticalOptionsCenterEntry

<HorizontalStackLayout Spacing="10">
    <Label Text="Bill" WidthRequest="100" VerticalOptions="Center"/>
    <Entry ... />
</HorizontalStackLayout>

添加另一个以将显示 Tip 的与命名的 tipOutput 分组。将该属性设置为 ,将该属性设置为 。HorizontalStackLayoutLabelLabelSpacing10Margin0,20,0,0

将提示设置为WidthRequestLabel100

<HorizontalStackLayout Margin="0,20,0,0" Spacing="10">
    <Label Text="Tip" WidthRequest="100" />
    <Label .../>
</HorizontalStackLayout>

使用 a 将“总计”与命名的 totalOutput 分组。将该属性设置为 。HorizontalStackLayoutLabelLabelSpacing10

将 “总计” 设置为WidthRequestLabel100

<HorizontalStackLayout Spacing="10">
    <Label Text="Total" WidthRequest="100"  />
    <Label .../>
</HorizontalStackLayout>

添加另一个以将“提示百分比”与指定的 tipPercent 分组。HorizontalStackLayoutLabelLabel

将此属性设置为 ,并将该属性设置为 :VerticalOptionsHorizontalStackLayoutEndAndExpandSpacing10

将 提示百分比设置为WidthRequestLabel100

<HorizontalStackLayout VerticalOptions="EndAndExpand" Spacing="10">
    <Label Text="Tip Percentage" WidthRequest="100"/>
    <Label ... />
</HorizontalStackLayout>

使用 a 将 标题为 15% 和标题为 20% 分组。HorizontalStackLayoutButtonButton

将此属性设置为 ,并将属性设置为 :MarginStackLayout0,20,0,0Spacing10

<HorizontalStackLayout  Margin="0,20,0,0" Spacing="10">
    <Button Text="15%" ... />
    <Button Text="20%" ... />
</HorizontalStackLayout>

添加一个 final,将 带有标题的 Round Down 和带有标题的 Round Up 分组。将此属性设置为 ,并将属性设置为 :HorizontalStackLayoutButtonButtonMarginStackLayout0,20,0,0Spacing10

<HorizontalStackLayout Margin="0,20,0,0" Spacing="10">
        <Button ... Text="Round Down" />
        <Button ... Text="Round Up" />
</HorizontalStackLayout>

在所有四个按钮控件上,将属性设置为 ,将属性设置为 。例如:HorizontalOptionsCenterAndExpandWidthRequest150

<Button Text="15%" WidthRequest="150" HorizontalOptions="CenterAndExpand" ... />

内容页的完整 XAML 标记应如下所示:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:TipCalculator"
             x:Class="TipCalculator.MainPage">
    <VerticalStackLayout Padding="40" Spacing="10">
        <HorizontalStackLayout Spacing="10">
            <Label Text="Bill" WidthRequest="100" VerticalOptions="Center" />
            <Entry x:Name="billInput" Placeholder="Enter Amount" Keyboard="Numeric" />
        </HorizontalStackLayout>
        <HorizontalStackLayout Margin="0,20,0,0" Spacing="10">
            <Label Text="Tip"  WidthRequest="100" />
            <Label x:Name="tipOutput" Text="0.00" />
        </HorizontalStackLayout>
        <HorizontalStackLayout Spacing="10">
            <Label Text="Total" WidthRequest="100"/>
            <Label x:Name="totalOutput" Text="0.00" />
        </HorizontalStackLayout>
        <HorizontalStackLayout VerticalOptions="EndAndExpand" Spacing="10">
            <Label Text="Tip Percentage" WidthRequest="100"/>
            <Label x:Name="tipPercent" Text="15%" />
        </HorizontalStackLayout>
        
        <Slider x:Name="tipPercentSlider" Minimum="0" Maximum="100" Value="15" />
        <HorizontalStackLayout Margin="0,20,0,0" Spacing="10">
            <Button Text="15%" Clicked="OnNormalTip" WidthRequest="150" HorizontalOptions="CenterAndExpand"/>
            <Button Text="20%" Clicked="OnGenerousTip"  WidthRequest="150" HorizontalOptions="CenterAndExpand"/>
        </HorizontalStackLayout>
        
        <HorizontalStackLayout Margin="0,20,0,0" Spacing="10">
            <Button x:Name="roundDown" Text="Round Down" WidthRequest="150" HorizontalOptions="CenterAndExpand"/>
            <Button x:Name="roundUp"   Text="Round Up" WidthRequest="150" HorizontalOptions="CenterAndExpand"/>
        </HorizontalStackLayout>
    </VerticalStackLayout>
</ContentPage>

检查结果

再次运行应用并查看 UI 中的差异。验证控件是否正确对齐,以及大小和间距是否正确。

你使用和容器来改善现有 UI 的美观性。这些布局是最简单的布局面板,但功能强大到足以生成合理的 UI。VerticalStakLayoutHorizontalStackLayout

使用网格排列视图

假设您正在构建一个在 7x5 网格中显示图像的页面。可以使用多个水平和垂直容器创建此页面。但是,由于多个布局面板的内存和处理要求,编码起来会很繁琐,并且可能导致性能问题。布局面板是同时需要行和列的 UI 的更好选择。在本单元中,您将学习如何定义 a 和在其单元格内放置视图。StackLayoutGridGrid

什么是网格?

Grid是由行和列组成的布局面板。下图显示了网格的概念视图。

将视图放置在从行和列的交集创建的单元格中。例如,如果创建具有三列和两行的 a,则将有六个单元格可用于视图。行和列可以有不同的大小,也可以将它们设置为自动适应放置在其中的子项的大小。子视图可以占用单个单元格或跨多个单元格。这种灵活性是许多应用的根布局面板的不错选择。GridGrid

如何指定网格的行和列

创建 时,可以单独定义每一行和每一列。该系统使您可以完全控制每行的高度和每列的宽度。每个都有一个定义网格形状的集合 和 对象。使用 和 的实例填充这些集合,每个实例表示 UI 中的一行或一列。GridGridRowDefinitionColumnDefinitionRowDefinitionColumnDefinition

下面是两个代码片段,它们显示了 和 的类定义:RowDefinitionColumnDefinition
public sealed class RowDefinition : …


public GridLength Height get; set;

public sealed class ColumnDefinition : …


public GridLength Width get; set;

请注意,具有一个称为 的属性,并具有一个名为 的属性。您可以使用这些属性来设置行的高度和列的宽度,如以下各节所述。RowDefinitionHeightColumnDefinitionWidth

什么是 GridLength?
和 属性的数据类型为 。此类型包含两个属性:和 。下面是一段代码,其中显示了类型定义的一部分。WidthHeightGridLengthGridUnitTypeValue
public struct GridLength


public GridUnitType GridUnitType get;
public double Value get;

您可以将该属性设置为以下值之一:GridUnitType

  • Absolute
  • Auto
  • Star
    让我们仔细看看这些值中的每一个。

绝对网格单位类型

Absolute指定行或列的大小是固定的。您可以使用该属性来指示大小。下面的示例演示如何将行的高度设置为 C# 中设备单位的固定大小。请注意如何使用构造函数,它采用数值。此构造函数将自动设置为 为您。Value100GridLengthGridUnitTypeAbsolute

var row = new RowDefinition()  Height = new GridLength(100) ;

在 XAML 中,只需提供一个数值。XAML 分析器将调用类型转换器来创建实例。下面是一个示例,它在 XAML 中显示了相同的内容:GridLength

MAUI.NET MAUI – Calling RESTFUL API

MAUI.NET MAUI – Calling RESTFUL API

.NET MAUI实战 FilePicker

MAUI自定义 .NET MAUI XAML 页面中的布局

MAUI自定义 .NET MAUI XAML 页面中的布局

创建新的 .NET 6 MAUI 项目