Marvin's Blog【程式人生】

Ability will never catch up with the demand for it

27 Nov 2020

UWP Design Layout文档笔记

Uwp Design Layout学习笔记。

Page layout

一般来说一个页面包含导航,命令以及内容等元素。

尽管包含很多不同风格的导航,但是看不到对传统菜单的采用。

一些应用示例: https://developer.microsoft.com/en-us/windows/samples/

Screen sizes and breakpoints

屏幕大小区分标准。

Responsive design techniques

优化不同屏幕尺寸下的界面设计。对于内容,怎么处理静止的部分以及动态变化的部分。对于内容以外的部分,决定哪些显示,哪些不显示。

Responsive layouts with XAML

跟布局相关的辖属

  • Height定义高度,取值可以是具体的像素值,或者auto,或者按比例
  • Width定义宽度,取值同上

一个元素的大小可以从下到上按自身内容决定,也可以从上到下塞满给定的空间。具体的大小可以聪明ActualHeight和AcutualWidth得到。

可以使用MinWidth/MinHeight以及MaxWidth/MaxHeight来对内容范围做出更多限制。

对于Grid,MinWidth/MaxWidth可以被用于Column定义,而MinHeight/MaxHeigh可以被用于Row定义。

如果需要同时兼顾从上到下,以及从下到上,可以使用HorizontalAlignment 和VerticalAlignment 这两个辖属,可以帮助摆放当前元素在容器中的位置。这两个辖属都可以被设成Stretch。但是Stretch 的含义在不同的容器下有不同的含义,在Grid之下,Stretch会元素大小设置成和Grid的容器一样大小,而在Canvans下面则保持元素自身的大小。也就是说,Stremtch的呈现取决于容器自身的需求。

Visibility辖属可以用来控制元素的显示,他有Visible和Collapsed两个可能值。如果是Collapsed,可以通过把x:DeferLoadStrategy设置成Lazy来对元素进行延迟加载从而提高页面的首次显示速度。

通过Style,可以将公共属性设置到对象元素上。

对于可视化对象,必须将其放置在面板(panel)或者其他容器对象之中。XAML框架提供了一些预制的面板,比如Canvas、Grid、Relativepanel以及StackPanel。其中一些面板支持Fluid UI,另一些则不支持。

章节中包含更多对面板的描述

VisualStateManager可以用来指定页面的显示状态。另外可以指定AdaptiveTrigger 之类的触发器来触发显示状态的变化。在代码中则可以调用VisualStateManager.GoToState来改变显示状态。

在Windows 10以前,VisualState的定义需要使用到Storyboard。从Windows 10开始,可以使用Setter语法来设置目标对象的辖属,并且可以使用StateTrigger来定义简单的状态变化触发器。触发器的使用可以避免定义默认的VisualState,以及避免在代码中对GoToState的调用。

要确保 VisualStateManager.VisualStateGroups这个附庸辖属是定义在root元素的第一个子元素上的。否则StateTriggers可能不工作。

在XAML中设置附庸辖属的语法如下:

<Setter Target="myTextBox.(RelativePanel.AlignHorizontalCenterWithPanel)" Value="True"/>

可以自定义StateTrigger从而获得更多状态变化的可能性,参考https://github.com/Microsoft/Windows-universal-samples/tree/master/Samples/XamlStateTriggers

针对不同的设备类型,可以提供不同的XAML文件,或者提供不同的XAMl页面。

Show multiple views for an app

可以为同一个应用创建多个窗口。第一个窗口叫做MainView。每个窗口拥有各自的线程。MainView一定是在ApplicationView管理。其他窗口可以在ApplicationView或者AppWindow管理。

AppWindow可以和它的创建者在同一个线程上。AppWindow 本身以及相关的API(在WindowManagement内)只在1903以后的Win10才可以获取。更多参考https://docs.microsoft.com/en-us/windows/uwp/design/layout/app-window 。AppWindow目前还有很多限制https://docs.microsoft.com/en-us/uwp/api/windows.ui.windowmanagement.appwindow#limitations。

可以把XAML放在Win32线程上管理(使用HWND)。这中方式叫做XAML Island,XAML的宿主为DesktopWindowXamlSource。更多参考https://docs.microsoft.com/en-us/windows/apps/desktop/modernize/using-the-xaml-hosting-api

当XAML内容在CoreWindows显示时,一定有随之关联的ApplicationView以及XAML Window。这些类型都有相应的静态方法可以帮助获取其实例,比如CoreWindow.GetForCurrentThread, ApplicationView.GetForCurrentView或者Window.Current辖属。GetForCurrentView这个方法蛮常见的,比如DisplayInformation.GetForCurrentView。这些API之所以能工作,是因为在一个CoreWindow/Application上只会有一颗XAML节点树,有一一对应的关系。

但是在AppWindow或者DesktopWindowXamlSource的情况下,可能一个线程上会有多颗XAML节点树。上述的API就无法给出正确的信息了。在这些场景下,需要使用XamlRoot提供的API。XamlRoot表征一颗XAML节点树以及相关的上下文信息。下面是这两套API的对比和替换关系:

  • CoreWindow.GetForCurrentThread().Bounds, uiElement.XamlRoot.Size
  • CoreWindow.GetForCurrentThread().SizeChanged, uiElement.XamlRoot.Changed
  • CoreWindow.Visible, uiElement.XamlRoot.IsHostVisible
  • CoreWindow.VisibilityChanged, uiElement.XamlRoot.Changed
  • CoreWindow.GetForCurrentThread().GetKeyState, Unchanged. This is supported in AppWindow and DesktopWindowXamlSource.
  • CoreWindow.GetForCurrentThread().GetAsyncKeyState, Unchanged. This is supported in AppWindow and DesktopWindowXamlSource.
  • Window.Current, Returns the main XAML Window object which is closely bound to the current CoreWindow. See Note after this table.
  • Window.Current.Bounds, uiElement.XamlRoot.Size
  • Window.Current.Content, UIElement root = uiElement.XamlRoot.Content
  • Window.Current.Compositor, Unchanged. This is supported in AppWindow and DesktopWindowXamlSource.
  • VisualTreeHelper.FindElementsInHostCoordinates(Although the UIElement param is optional, the method raises an exception if a UIElement isn’t supplied when hosted on an Island.), Specify the uiElement.XamlRoot as UIElement instead of leaving it blank.
  • VisualTreeHelper.GetOpenPopups (In XAML Islands apps this will throw an error. In AppWindow apps this will return open popups on the main window), VisualTreeHelper.GetOpenPopupsForXamlRoot(uiElement.XamlRoot)
  • FocusManager.GetFocusedElement, FocusManager.GetFocusedElement(uiElement.XamlRoot)
  • contentDialog.ShowAsync(), contentDialog.XamlRoot = uiElement.XamlRoot; contentDialog.ShowAsync();
  • menuFlyout.ShowAt(null, new Point(10, 10));, menuFlyout.XamlRoot = uiElement.XamlRoot; menuFlyout.ShowAt(null, new Point(10, 10));

在DesktopWindowXamlSource情况下,CoreWindow/Window 依然存在,但是只有1x1大小,所以获取它的bounds或者visibility是没有意义的。

对于XAML content,每个线程上还是只有一个CoreWindow。所以GetForCurrentView 或者GetForCurrentThread 只是返回这个CoreWindow,而不是想要的AppWindow。

要和不要

  • 要提供一个清晰的入口点给secondary view,可以使用open new window图形
  • 要让用户知晓secondary view的含义
  • 要保证应用在单个view下功能完整。secondary view只是辅助用的
  • 不要依赖于secondary view来提供通知或者临时性的视觉通告

Alignment, margin, padding

像尺寸,对齐,间隔,边距这些影响具体界面呈现的辖属都在FrameworkElement 上定义。

尺寸

尺寸包括高度和宽度,默认值是数学上的NaN。它们可以被设置成具体的有效像素大小,亦或是Auto,抑或是f流动的按比例大小。

ActualHeight 和ActualWidth这两只读辖属则反映了元素被赋予的实际大小。当流动布局改变时,SizeChanged事件会告知这两个辖属的改变。需要注意的时RenderTransform不会改变这两个辖属的值。

MinWidth/MaxWidth和MinHeight/MaxHeight 可以添加额外限制,避免流动布局的时候过度放大或者缩小。

对于文本,虽然没有上述属性,但是可以使用FontSize来控制大小,它们也有具体的ActualHeight和ActualWidth。

对齐

HorizontalAlignment和VerticalAlignment可以指定元素在其容器中的布局。这两个辖属的默认值是Stretch。如果Height,Width被设置了有理数值,那么Stretch也就相应失效了,从而变成Center。有一些控件比如Button,会在默认样式中将默认的Stretch改为其他。

HorizontalContentAlignment 和VerticalContentAlignment指定子元素如何在容器中放置。对齐属性会影响裁切,HorizontalAlignment="Left"的例子,如果元素的大小超过ActualWidth,元素的右部会被裁切。

文本对象使用的是TextAlignment辖属,通常使用默认的左对齐就好了。

间隔和边距

Margin在元素的边框之外设置空余。这些空余不计入ActualHeight和ActualWdith,从而不参与HitTest。相邻的元素的Margin会叠加。

Margin的指定形式有三种,一是一值定四边;二是一值定左右,另一值定上下;三是左上右下各一值。

Margin可以是负值,但不常用。另外Margin可能导致一个元素无法在容器中使用,因为Margin的存在,元素的大小被限制成0。

Padding用于边框和内容之间设置空余。Padding不在FrameworkElement中定义,而是在需要使用Padding的元素中出现:

  • Control.Padding。不是所有的Control都有内容。没有内容设置Padding没有意义。
  • Border.Padding,在BorderThickness/BorderBrush和Child元素之间创造空余
  • ItemsPresenter.Padding,对于每个元素应用Padding
  • TextBlock.Padding 和RichTextBlock.Padding,为文本元素增加padding。由于这些文本元素没有Background辖属,一个迂回的办法是在Block容器上设置Margin。

通用建议

  • 为了支持响应式设计,尽量不要使用太具体的值(measurement values)
  • 如果使用具体值得话,以4eps为间隔
  • 对于640epx以下得界面,推荐使用12epx的gutter,否则使用24epx的gutter

Layout panels

panel用来囊括一组UI元素,其需要考虑的问题是:

  • 怎么决定其子元素的位置
  • 怎么决定其子元素的大小
  • 怎么解决子元素的层叠(z-order)

为了达到上述目的,panel经常需要把自己的一些辖属寄放在子元素上面。比如Grid.Row,Canvas.Left等等。

一些Panel带有border相关的辖属(BorderBrush,BorderThickness,CornerRadius,以及Padding),比如RelativePanel, StackPanel, Grid等等。使用这些Panel的时候不需要额外再套一层Border元素了。可以减少一些XAML元素的使用。参考Optimize your XAML layout

下面是个例子:

<Grid BorderBrush="Blue" BorderThickness="12" CornerRadius="12" Padding="12">
    <TextBlock Text="Hello World!"/>
</Grid>

RelativePanel

RelativePanel允许元素根据邻里或者与RelativePanel的关系来布局。默认情况下元素排布再RelativePanel的左上部分。

下面的辖属定义与RelativePanel的对齐关系:

  • Align{Top/Bottom/Left/Right}WithPanel
  • Align{Horizontal/Vertical}CenterWithPanel

下面的辖属定义与邻里的对齐关系:

  • Align{Top/Bottom/Left/Right}Width
  • Align{Horizontal/Vertical}CenterWith

下面的辖属定义与邻里的对齐关系:

  • Above
  • Below
  • LeftOf
  • RightOf

StackPanel

StackPanel把子元素组织成单行或者单列。通过其Orientation辖属,可以设置横纵。

这个经常用,不需要详细介绍

Grid

Grid可以以表格的方式来设置内容布局。

这个经常用,不需要详细介绍

VariableSizedWrapGrid

跟Grid类似,但是支持自动换行或者换列(由Orientation决定,默认是Vertical,也就是说当一个列满的时候,产生新的一列)。

表格单元的尺寸由ItemHeight和 ItemWidth决定。每个单元同等大小。如果ItemHeight和ItemWidth没有指定,那么则根据第一个单元的大小来决定后续单元的大小。

VariableSizedWrapGrid.ColumnSpan和VariableSizedWrapGrid.RowSpan这两个寄放辖属可以用来让一个子元素跨多个单元。

Canvas

Canvas使用非常具体的Left, Top等辖属来决定子元素的排布。再Canvas执行Arrange过程的时候,会读取这些寄放在子元素身上的辖属。

Canvas中的元素可以重合,使用Canvas.ZIndex可以指定重合时候的上下关系。Canvas.ZIndex越大的位置其绘制越后,所以重合的时候越在上头。

重合的时候会考虑alpha透明度。Canvas不会对子元素的大小做任何改变。每个元素必须指定自己的大小。

Panels for ItemsControl

有一些专门的Panel是给ItemsControl使用的,比如:ItemsStackPanel, ItemsWrapGrid, VirtualizaingStackPanel,以及WrapGride。不能将这些Panel用于通用的UI。

Attached Layouts

Attached Layout,意味着一个panel可以把子元素布局部分外包给另外一个类。

布局其实就是决定子元素的位置和大小。Custom panels对此有更深入一点的介绍。

概念上,XAML的面板扮演了下面角色:

  • 归纳子元素,从而在XAML节点树上产生一个分支。
  • 不同类型的Pan/el可以采用不同的布局策略。

所以在XAML中,Panel可以说是Layout的代名词。但实际上,Panel做的事情又比布局多。

ItemsRepeater有点像Panel,但是用户无法往其中添加或删除子元素。反之,ItemsRepeater的子元素的由框架根据数据集合来自动管理。所以即便ItemsRepeater不是Panel,也被当做Panel处理。

概念上,Panel是一个元素容器,并且有绘制Background像素的任务。而Attached layout则把容器和布局这两个概念分离开来。

下面是使用LayoutPanel的一个例子:

<LayoutPanel>
    <LayoutPanel.Layout>
        <UniformGridLayout/>
    </LayoutPanel.Layout>
    <Button Content="1"/>
    <Button Content="2"/>
    <Button Content="3"/>
</LayoutPanel>

一个Attached Layout可以被不同的页面共享:

<!-- ... --->
<Page.Resources>
    <ExampleLayout x:Name="exampleLayout"/>
<Page.Resources>

<LayoutPanel x:Name="example1" Layout="{StaticResource exampleLayout}"/>
<LayoutPanel x:Name="example2" Layout="{StaticResource exampleLayout}"/>
<!-- ... --->

LayoutContext用来帮助Attached Layout跟宿主沟通,完成诸如获取子元素以及布局相关辖属的功能。并且能够为不同的宿主保持独立的状态。

非虚布局基本上不需要保存状态。但是也有例外,像Grid这种,可能会在measure和arrange之间保存中间状态来避免重复计算。 而虚化布局更需要在measure和arrange之间保存状态。

InitializeForContextCore和UninitializeForContextCore 可以用来初始化和解除状态。LayoutContext的LayoutState可以获取这些初始化的状态。

UI虚拟化意味着延迟创建UI对象。这是一种性能上的优化,因为创建对象会被推迟到真正被需要的时候。使用 x:Load可以让App决定什么时候需要一个元素。但是在滚动的场景下,问题就变成什么时候需要将UI呈现出来。这也是本文讨论的主要场景。

用于滚动的虚拟化技术也可以用于非滚动的场景。

剩余略。

Transforms overview

未学习。

Z-depth and shadow

本文讨论如何通过elevation如何让界面元素突显出来。主要涉及的技术是z-depth以及shadow。

ThemeShadow可以应用于任何XAML元素来绘制阴影。

ThemeShadow有一些环境自适应特性:

  • 可以自适应光线,主题,应用环境以及扇面(Shell)的改变。
  • 可以根据元素的z-depth自动生成阴影
  • 在被突显的元素改变的时候保持同步
  • 在不同的应用之间保持阴影的协调

下面的元素会自动使用32px深度的ThemeShadow:

  • ContextMenu, Command bar, Command bar flyout, MenuBar
  • Dialogs and flyouts (Dialog使用64px)
  • NavigationView
  • ComboBox, DropDownButton, SplitBUtton, ToggleSplitButton
  • TeachingTip
  • AugoSuggestBox
  • Calendar/Date/Time pickers
  • Tooltip (16px)
  • Media transport control, InkToolbar
  • Connected animation

Fyouts在1903及以上版本的SDK中才会自动引用阴影

当应用在Popup中的XAML元素之上时,ThemeShadow自动在Popup之后的窗口上形成阴影:

<Popup>
    <Rectangle x:Name="PopupRectangle" Fill="Lavender" Height="48" Width="96">
        <Rectangle.Shadow>
            <ThemeShadow />
        </Rectangle.Shadow>
    </Rectangle>
</Popup>
// Elevate the rectangle by 32px
PopupRectangle.Translation += new Vector3(0, 0, 32);

从Flyout, DatePickerFlyout, MenuFlyout或者TimePickerFlyout派生出来的应用会自动使用ThemeShadow来绘制阴影。如果默认的阴影不适合,那么可以设置IsDefaultShadowEnabled未false来取消:

<Flyout>
    <Flyout.FlyoutPresenterStyle>
        <Style TargetType="FlyoutPresenter">
            <Setter Property="IsDefaultShadowEnabled" Value="False" />
        </Style>
    </Flyout.FlyoutPresenterStyle>
</Flyout>

可以在Popup以外的XAML元素中使用阴影,但是必须显示指定ThemeShadow.Receivers。ThemeShadow.Receivers不能未先序节点。

<Grid>
    <Grid.Resources>
        <ThemeShadow x:Name="SharedShadow" />
    </Grid.Resources>

    <Grid x:Name="BackgroundGrid" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" />

    <Rectangle x:Name="Rectangle1" Height="100" Width="100" Fill="Turquoise" Shadow="{StaticResource SharedShadow}" />

    <Rectangle x:Name="Rectangle2" Height="100" Width="100" Fill="Turquoise" Shadow="{StaticResource SharedShadow}" />
</Grid>
/// Add BackgroundGrid as a shadow receiver and elevate the casting buttons above it
SharedShadow.Receivers.Add(BackgroundGrid);

Rectangle1.Translation += new Vector3(0, 0, 16);
Rectangle2.Translation += new Vector3(120, 0, 32);

关于ThemeShadow的最佳实践:

  • 系统默认最大的caster-receiver对数为5,超过之后就无效了
  • 将自定义的接收元素数目设置为可能的最小值
  • 如果多个receiver元素在同一个突显层次,可以将其合并起来面向同一个父元素
  • 如果多个元素把相同的阴影打到同一个元素,那么可以把这个阴影作为共享资源

除了ThemeShadow之外,还有DropShadow。但是后者不具备环境的自动适应性。参考Which shadow should I use?

(下面没了)

Categories