Xamarin.Forms——尺寸大小(五 Dealing with sizes)

如之前所见的大量可视化元素均有自己的尺寸大小:

  • iOS的状态栏高度为20,所以我们需要调整iOS的页面的Padding值,留出这个高度。
  • BoxView设置它的默认宽度和高度为40。
  • Frame的默认Padding为20。
  • StackLayout的默认Spacing属性值为6。

还有Device.GetNamedSize方法,该方法将LabelButton等控件中使用的NamedSize枚举值转换为不同平台对应的数值,即不同控件中不同NamedSize枚举对应的FontSize值。

然后上面那些数值代表什么?它们的单位是什么?并且怎样精确的设置这些值获得指定的大小?

好问题。尺寸大小同样会影响文本的显示效果,正如我们所看到,不同的平台显示的文本的数量也会不一样,那么可以在Forms程序中控制显示的文本数量吗?即使可以控制,那会是一种好的编程实践吗?程序应该通过调整尺寸大小来适应屏幕的显示密度吗?

通常,当编写Xamarin.Forms应用程序时不要过于接近那些可视化元素的实际尺寸数值。最好的方式是充分信任Xamarin.Forms在三个不同平台下都会做出最好的默认选择。

然后,有时一个开发者还是需要知道部分可视化元素的尺寸大小以及它们所附着的屏幕的尺寸大小。

如你平时所知的一样,视频是由一大堆像素所组成的一个矩形。任何可以显示在屏幕上的可视化元素都有一个像素尺寸。在早期的个人电脑中,开发者都用像素来定位和布局那些可视化元素。但是,随着拥有更多元素的大小尺寸和像素密度的显示设备出现,在编写程序时直接使用像素的方式变得过时和不受开发者欢迎了,必须寻求另一种新的解决方案。

Pixels,points,dps,DIPs,DIUs


这种控制像素的方式始于桌面电脑时代的操作系统,于是这种解决方案也自然而然的被用于移动设备。因此,我们将从桌面设备开始探讨这个问题。

桌面视频有大量不同的像素尺寸,从几乎要过时的640x480到上千像素。跟电影和电视一样,4:3的纵宽比也曾经是电脑显示的标准,不过现在更常用高清晰纵宽比,如16:9或者16:10。

桌面视频也有一个物理尺寸,这个物理尺寸通常是测量显示器对角线的英寸和厘米长度。通过像素尺寸和物理尺寸可以计算出这个视频的显示分辨率或者像素密度,像素密度使用DPI(dots per inch 打印分辨率——即每英寸所打印点数)来描述,有时也可以使用PPI(pixels per inch 图像的采样率——即每英寸的像素数量)。显示分辨率还可以通过点距(dot pitch——即相邻像素间的距离,毫米为单位)来描述。

例如,使用毕达哥拉斯定律可以计算出一个800x600分辨率的对角线长度上可以容纳1000像素点,如果是13英寸的显示器,那么像素密度是77DPI,或者0.33毫米的点距。然后,如果现代笔记本上的13英寸显示器可能拥有2560x1600的像素尺寸,230DPI的像素密度,或者0.11毫米的点距。那么同样的一个100像素的正方形元素在高精度显示器上的大小可能只有老式显示器的三分之一大。

当开发者试图调整可视化元素到正确的大小就像一场战役一样。因此,Apple和Microsoft计划为桌面电脑建立一套机制来允许开发者用一些设备无关的单位来描述视频显示的尺寸而不是直接使用像素。开发者遇到的大多数尺寸规格都能用这一系列的设备独立单位来描述,而操作系统就负责在这些设备独立单位和像素之间进行转换。

在Apple的世界里,桌面视频都假设每英寸拥有72单位元素。这一数字来源于印刷排版界,在传统的印刷排版里,每英寸大约有72个点,但是在数字排版印刷方面,这个点位的精度已经标准化为1/72英寸。使用点的数量来描述比直接使用像素更好,开发者能更直观的感受到屏幕上可视化元素和这个大小包括的尺寸点数之间的关系。

在Microsoft世界里,一个相似的技术已经成熟,被称为设备无关像素(device-independent pixels DIPs),或者设备无关单位(device-independent units DIUs)。作为一个Windows开发者,需要知道该平台下的桌面视频假定拥有一个96DIUs的分辨率,比72DPI高三分之一。

然而,移动设备拥有不同的规则:一个特点就是现代手机的像素密度比桌面设备高出很多。高像素密度意味着文本和其他可视化元素会收缩在一个很小的尺寸空间中。

手机的另一个特点就是比桌面设备或笔记本更贴近人的面部。这也意味着相同的可视化元素如果呈现在手机上,尺寸可以比桌面设备更小。因为手机的物理尺寸比桌面设备更小,所以缩小可视化元素来适应屏幕就变得十分可取。

Apple继续在iPhone上使用DIUs来描述点数,直到最近,所有的苹果设备都采用来一种被叫做Retina的高清屏解决方案,该方案使单点的像素密度变成原来的两倍。这个规则适用于苹果的几乎所有设备,MacBook Pro,iPad和iPhone。直到iPhone 6 Plus的出现,将单点的像素密度变成了原来的三倍。

例如,iPhone 4拥有3.5英寸屏幕,640x960像素显示分辨率,320 DPI的像素密度。由于单点有两倍的像素密度,所以当应用程序运行在iPhone4上当时候,将会在屏幕上呈现320x480个点。iPhone 3有320x480的像素显示分辨率,点的数量等于像素的数量,所以,对于一个程序来说,呈现在iPhone 3和iPhone 4上的大小相同。尽管大小尺寸相同,但是iPhone 4上的文本和可视化元素将会显示在一个更高的分辨率之上。

对于iPhone 3和iPhone 4,从屏幕尺寸和点数尺寸的关系上来说,它们拥有比桌面设备每英寸72点更大的一个密度,每英寸160点。

iPhone 5拥有一个4英寸屏幕,但是它点像素尺寸达到了640x1136。像素密度和iPhone 4一样,对于程序来说,屏幕上点数尺寸为320x768。

iPhone 6拥有4.7英寸屏幕,像素尺寸为750x1334。像素密度同样也是320DPI,每单位点有两个像素,所以对于程序来说,屏幕上能呈现的点数尺寸为375x667。

然而,iPhone 6 Plus拥有5.5英寸屏幕,像素尺寸为1080x1920,像素密度为400DPI,更高的像素密度意味着一个点上有更多的像素,对于iPhone 6 Plus,Apple设定一个点等于三个像素点。给我们的感觉是屏幕的点数尺寸应该是360x640,但是实际对于程序来说,iPhone 6 Plus点屏幕点数尺寸是414x736,每英寸150个点。
以上信息总结起来就如下面这个表:

型号 iPhone 2,3 iPhone 4 iPhone 5 iPhone 6 iPhone 6 Plus
像素尺寸 320x480 640x960 640x1136 750x1134 1080x1920
屏幕尺寸 3.5英寸 3.5英寸 4英寸 4.7英寸 5.5英寸
像素密度 165 DPI 330 DPI 326 DPI 326 DPI 401 DPI
单位点包含像素数量 1 2 2 2 3
点数尺寸 320x480 320x480 320x568 375x667 414x736
每英寸包含点数量 165 165 163 163 154

Android也十分相似,只是Andorid设备拥有更多的设备尺寸和显示尺寸,但是Andorid开发者在工作中通常不关心具体设备,而是关心密度无关像素这个单位(density-independent pixel dps)。像素密度和dps之间的关系是,每英寸呈现160dps,即Andorid和Apple的单位很相似。

然而Mircosoft通过Windows Phone带来了一种不同的方式。Windows Phone 7设备无论它的屏幕分辨率是320x480(这种分辨率很稀有,可不做讨论)或者是480x800(通常叫做WVGA Wide Video Graphics Array),都拥有统一的像素尺寸。Windows Phone 7程序工作在这种像素单位的基础上。假设一台最平常的4英寸480x800的Windows Phone 7设备,这意味着该设备的像素密度大约是240DPI。而这是iPhone和Android设备的1.5倍。

当Windows Phone 8来临时,出现了很多更大屏幕的设备,768x1280(WXGA Wide Extended Graphics Array),720x1280(720P),1080x1920(1080P)。

对于这三种额外的尺寸,开发者同样使用设备无关的单位。此时,一个内部的缩放机制将会使所有设备在竖屏情况下宽度都呈现480像素。对应的比例因子如下表:

屏幕类型 WVGA WXGA 720P 1080P
像素尺寸 480x800 768x1280 720x1280 1080x1920
缩放比例 1 1.6 1.5 2.25
DIUs尺寸 480x800 480x800 480x853 480x853

Xamarin.Forms开发者通常使用设备无关的方式来处理手机显示,但是在具体三个平台上也有一些不一样:

  • iOS:每英寸160单位
  • Android:每英寸160单位
  • Windows Phone:每英寸240单位

如果将相同物理大小的可视化元素放在三个平台,那么Windows Phone平台上看见的大小会比iOS和Android大1.5倍。

VisualElement类定义了两个属性,WidthHeight,这两个元素用设备无关的单位来描述views,layouts和pages。这两个属性的初始值被设置为伪值-1。只有当page上的所有元素都已经定位和调整大小完毕这两个属性的值才有效。同样,需要注意HorizontalOptionsVerticalOptions的默认值是Fill,这个设置将会让视图尽可能的占据更多的空白地方。WidthHeight的值也可以用来反映一些额外空间值,比如Padding,设置后的区域会被view的BackgroundColor属性指定的颜色填充。

VisualElement定义了一个SizeChanged事件,当一个可视化元素的WidthHeight属性发生变化时触发。当page对内部的大量元素进行定位和调整大小时会触发一系列事件,SizeChanged事件就是其中一个。这个构造的过程会在第一次定义这个page时出现(通常是在page的构造中),而任何一个对布局内容的影响都会使这一过程再次发生,例如将视图添加到ContentPage或者StackLayout中,或从它们中移除,或者改变可视化元素的大小。

当屏幕尺寸发生改变时同样也会触发新的布局过程,这种情况通常发生在设备在竖屏和横屏之间进行切换的时候。

熟悉Xamarin.Forms的布局系统可以帮助我们写出更好的Layout<View>继承类。具体怎样写将在以后的章节中介绍到,到时,你就会明白清楚地知道WidthHeight属性何时改变有助于我们更好地改变可视化元素的大小。你可以通过处理SizeChanged事件来处理page中任意可视化元素的大小,甚至包括page自身。这个WhatSize程序将会向你展示如何获page的大小并展示出来:

public class WhatSizePage : ContentPage
{
    Label label;
    public WhatSizePage()
    {
        label = new Label
        {
            FontSize = Device.GetNamedSize(NamedSize.Large, typeof(Label)),
            HorizontalOptions = LayoutOptions.Center,
            VerticalOptions = LayoutOptions.Center
        };
        Content = label;
        SizeChanged += OnPageSizeChanged;
    }
    void OnPageSizeChanged(object sender, EventArgs args)
    {
        label.Text = String.Format("{0} \u00D7 {1}", Width, Height);
    }
}

这是本书当中的第一个事件处理的例子,事件处理跟其他C#程序差不多,事件处理者有两个参数,第一个代表引发该事件的对象,第二个参数提供额外的关于这个事件的信息。

SizeChanged不是唯一的监控元素尺寸改变的事件,VisualElement还定义了一个受保护的虚方法——OnSizeAllocated,该方法也能知道可视化元素何时改变大小。你可以在ContentPage重写该方法而不处理SizeChanged事件,但是有时OnSizeAllocated方法会在元素大小并没有真正改变时触发。

下面是程序运行在各个平台下的样子:

下面是这三张图的具体信息:

  1. iPhone 6模拟器,屏幕像素尺寸为750x1334。
  2. LG Nexus 5,屏幕像素尺寸为1080x1920。
  3. Nokia Lumia 925,屏幕像素尺寸为768x1280。

需要注意程序的垂直高度尺寸,Android的垂直高度不包括顶部状态栏和底部按钮区域;Windows Phone的垂直高度不包括顶部状态栏。

默认情况下,三个平台都会在设备翻转时做出响应。如果将设备逆时针旋转90度,将呈现下面这种情况:

为了方便排版,手机还是竖着显示,重点看状态栏来区分。可以看到,Android度宽度为598,这个宽度不包括按钮区域,高度为335,这个高度包括了状态栏度高度。Windows Phone的宽度为728,这个宽度包括了侧边状态栏,可以看到,状态栏的图标还在相同位置,只是旋转了图标的方向。

这个WhatSize程序在构造函数中创建了一个Label控件并且在事件处理中设置Label的文本。这种方式不是写这个程序的唯一方式,程序也可以在SizeChanged事件的处理方法中创建一个新的Label控件,然后设置好文本再将它添加到page中,在这种情况下之前的那个Label就变得没有用处了。但是可以看到在这个程序中创建新的可视化元素是没有必要的,最好的方式是创建一个唯一的Label,通过设置它的Text属性来展示page的尺寸。

如果不使用平台相关的API,那么监控尺寸的改变是Xamarin.Forms程序唯一知道设备是横屏还是竖屏的方式。如果宽度大于高度,那么此时设备就是横屏的状态,否则就是竖屏。

默认情况下,使用Visual Studio和Xamarin Studio的模版创建的Xamarin.Forms工程在三个平台下都允许改变设备的屏幕方向。如果你想禁止屏幕改变方向,那么需要按如下操作。

对于iOS,首先在Visual Studio和Xamarin Studio中打开Info.plist文件,在iPhone Deployment Info节点下,使用Supported Device Orientations来标明设备支持哪些屏幕方向。

对于Android,在MainActivity类的Activity特性上添加:

ScreenOrientation = ScreenOrientation.Landscape

或者

ScreenOrientation = ScreenOrientation.Portrait

Activity的特性是被解决方案的模版所生成,其中包含的ConfigurationChanges参数也涉及到了屏幕朝向,但是ConfigurationChanges参数的目的是禁止手机的屏幕方向或尺寸改变导致的activity重启。

对于Windows Phone,在MainPage.xaml.cs文件中,改变SupportedPageOrientation的值为PortraitLandscape

可测量尺寸(Metrical sizes)


这里再一次强调一下三个平台上的英寸和设备无关单位之间的关系:

  • iOS:每英寸160单位
  • Android:每英寸160单位
  • Windows Phone:每英寸240单位

下面是尺寸以厘米为单位的情况:

  • iOS:每厘米64单位
  • Android:每厘米64单位
  • Windows Phone:每厘米96单位

那么意味着Xamarin.Forms程序可以使用以上可测量尺寸来更改可视化元素大小,使用熟悉的英寸或厘米为单位。下面给出一个名叫MetricalBoxView的程序来展示这个问题,该程序在屏幕上显示了一个宽大约1厘米高大约1英寸的BoxView

public class MetricalBoxViewPage : ContentPage
{
    public MetricalBoxViewPage()
    {
        Content = new BoxView
        {
            Color = Color.Accent,
            WidthRequest = Device.OnPlatform(64, 64, 96),
            HeightRequest = Device.OnPlatform(160, 160, 240),
            HorizontalOptions = LayoutOptions.Center,
            VerticalOptions = LayoutOptions.Center
        };
    }
}

如果你使用直尺在手机屏幕上测量,你会发现结果跟我们希望的尺寸很接近。

估计字体大小(Estimated font sizes)


LabelButton控件上的FontSize属性的类型是doubleFontSize指的是文本字符从最下面到最上面到高度,也包括该字体对应的标点符号。在大多数情况下,你需要通过Device.GetNamedSize方法设置这个属性。该方法允许你使用一系列NamedSize相关到枚举值:DefaultMicroSmallMediumLarge

你也可以使用字体大小的实际数字,但是这么做会引起一个小问题(稍后会谈到这个细节)。在大多数情况下,Xamarin.Forms通过相同的设备无关单位来表示字体的大小,这意味着你可以基于不同的平台分辨率计算设备无关的字体大小。

例如,假设你想在程序中使用12号字体。首先,你必须要知道12号字体用于印刷材料或是桌面显示器的效果很好,但是如果用于手机就太大了。

如果移动设备上一英寸有72个点,那么12号字体大约是六分之一英寸,乘以分辨率的DPI。结果是iOS和Android设备大约是27设备无关单位,Windows Phone大约是40设备无关单位。

我们写一个名叫FontSizes的小程序,开头部分与第三章中的NamedFontSizes程序很相似,后面还列出了不同字体的点数大小,使用设备点分辨率转换为设备无关单位。

public class FontSizesPage : ContentPage
{
    public FontSizesPage()
    {
        BackgroundColor = Color.White;
        StackLayout stackLayout = new StackLayout
        {
            HorizontalOptions = LayoutOptions.Center,
            VerticalOptions = LayoutOptions.Center
        };
        
        // Do the NamedSize values.
        NamedSize[] namedSizes = 
        {
            NamedSize.Default, NamedSize.Micro, NamedSize.Small,
            NamedSize.Medium, NamedSize.Large
        };
        
        foreach (NamedSize namedSize in namedSizes)
        {
            double fontSize = Device.GetNamedSize(namedSize, typeof(Label));
            
            stackLayout.Children.Add(new Label
                {
                    Text = String.Format("Named Size = {0} ({1:F2})",
                                         namedSize, fontSize),
                    FontSize = fontSize,
                    TextColor = Color.Black
                });
        }
        
        // Resolution in device-independent units per inch.
        double resolution = Device.OnPlatform(160, 160, 240);
        
        // Draw horizontal separator line.
        stackLayout.Children.Add(
            new BoxView
            {
                Color = Color.Accent,
                HeightRequest = resolution / 80
            });
        
        // Do some numeric point sizes.
        int[] ptSizes = { 4, 6, 8, 10, 12 };
        
        foreach (double ptSize in ptSizes)
        {
            double fontSize = resolution * ptSize / 72;
            
            stackLayout.Children.Add(new Label
                {
                    Text = String.Format("Point Size = {0} ({1:F2})",
                                         ptSize, fontSize),
                    FontSize = fontSize,
                    TextColor = Color.Black
                });
        }
        
        Content = stackLayout;
    }
}

为便于在三个平台上面比较,背景已被统一设置为白色,文字设置为黑色。在StackLayout中间用一个高1/8英尺的BoxView将两部分分隔开。

这个程序提供了一个粗略的思路让你能够在三个平台上产生视觉上差不多大小的元素。括号中的数字是特定平台下的设备无关的FontSize数值。

然而在Android平台下有一个问题,运行Android的Settings,进入Display页面,选择Font size项,可以看到,有SmallNormal(默认),LargeHuge这几个字号选择。这项设置可以给用户提供更广的字号选择,对于那些觉得字体太小感觉眼睛不舒服的用户可以将字号调大,对于那些眼睛很好想一次多看一些字的用户可以将字号设小。

在设置中修改字号,选择除Normal外的其他选项,然后重新运行FontSizes程序,可以看到程序里的所有文本都不一样里,根据你的设置,文本比之前都更大或更小了。你可以看到在水平线的上面部分,也就是Device.GetNamedSize方法返回的数值根据系统字号的不同发生了变化。对于NamedSize.DefaultNormal的默认设置返回的字号是14(就如上面的截图所展示的一样),如果设置为Small则返回12,Large返回16,Huge返回18.33。

除了Device.GetNamedSize返回的值不一样以外,根据字号设置的不同,底层文本绘制的逻辑也不一样。继续看程序的下面部分,程序计算出的字体的点位值依然相同,虽然它们的文本大小已经发生了改变。这是用枚举值设置Android的Label的结果,Android在内部会使用ComplexUnitType.SpCOMPLEX_UNIT_SP)计算字体大小,SP代表缩放像素scaled pixel,当文本超过使用的设备无关像素时会产生一个缩放。

调整文本到合适的尺寸(Fitting text to available size)


也许你需要调整一堆文本到一定大小的矩形区域,你可以使用两个数值来计算,一个是矩形区域的实际尺寸,另一个是装载这些文本的Label控件的FontSize属性值(但是Andorid需要将Font size设置为Normal)。

第一个需要的数值是行距,即Label视图里每一行文本间的垂直高度。下面展示了三个平台下的具体行高值:

  • iOS:行距 = 1.2 * label.FontSize
  • Android:行距 = 1.2 * label.FontSize
  • Windows Phone:行距 = 1.3 * label.FontSize

第二个有帮助的数值是字符宽度,不管在哪个平台,一段混合了大小写的默认字体的字符宽度大约是font size的一半:

  • 平均字符宽度 = 0.5 * label.FontSize

例如,假设你想在宽度为320的长度内容纳80个文本字符,并且你想让字体尽量的大。那么320除以40(宽度大约占高度一半)得到字号为8,这个数值就是我们可以给LabelFontSize属性赋的值。对于文本来说在真正测试之前还有一些不确定性,希望不要对你的计算结果产生太多惊喜。

下面这个程序展示了如何让行距以及字符宽更适合页面中的一段文本,当然这个页面是不包括iPhone的状态栏的。为了让iPhone排除状态栏更容易一些,这个程序使用了ContentView

ContentView继承自Layout,只添加了一个Content属性。ContentViewFrame的基类,但是Frame没有添加过多的额外功能。然而,当你想在自定义页面中定义一组视图,并轻松的模拟它们间的外边距,它将变得很有用。

也许你注意到了,Xamarin.Forms没有一个margin的概念,跟padding很相似,padding定义了视图里的内边距,而margin定义了视图外面的外边距。ContentView可以让我们模拟这个,如果你发现一个视图需要一个外边距,那么你可以将这个视图放在ContentView中,并且设置这个ContentViewPadding属性。ContentViewPadding属性继承自Layout

这个EstimatedFontSize程序使用ContentView的方式略有不同:它通过设置整个页面的padding来避开iOS的状态栏,而不是将页面中的某一项内容设置到ContentView中。因此,此处的ContentView除了iOS的状态栏以外与页面有相同的尺寸。通过附加ContentViewSizeChanged事件来获取内容区的尺寸,通过这个尺寸来计算文本的字号。

SizeChanged事件的处理方法中使用了第一个参数,这个参数通常是引发这次事件的对象(在这个程序里就是包含那个文本填充的ContentView),代码如下:

public class EstimatedFontSizePage : ContentPage
{
    Label label;
    
    public EstimatedFontSizePage()
    {
        label = new Label();
        
        Padding = new Thickness(0, Device.OnPlatform(20, 0, 0), 0, 0);
        ContentView contentView = new ContentView
        {
            Content = label
        };
        contentView.SizeChanged += OnContentViewSizeChanged;
        Content = contentView;
    }
    
    void OnContentViewSizeChanged(object sender, EventArgs args)
    {
        string text =
        "A default system font with a font size of S " +
        "has a line height of about ({0:F1} * S) and an " +
        "average character width of about ({1:F1} * S). " +
        "On this page, which has a width of {2:F0} and a " +
        "height of {3:F0}, a font size of ?1 should " +
        "comfortably render the ??2 characters in this " +
        "paragraph with ?3 lines and about ?4 characters " +
        "per line. Does it work?";
        
        // Get View whose size is changing.
        View view = (View)sender;
        
        // Define two values as multiples of font size.
        double lineHeight = Device.OnPlatform(1.2, 1.2, 1.3);
        double charWidth = 0.5;
        
        // Format the text and get its character length.
        text = String.Format(text, lineHeight, charWidth, view.Width, view.Height);
        int charCount = text.Length;
        
        // Because:
        //   lineCount = view.Height / (lineHeight * fontSize)
        //   charsPerLine = view.Width / (charWidth * fontSize)
        //   charCount = lineCount * charsPerLine
        // Hence, solving for fontSize:
        int fontSize = (int)Math.Sqrt(view.Width * view.Height /
                    (charCount * lineHeight * charWidth));
        
        // Now these values can be calculated.
        int lineCount = (int)(view.Height / (lineHeight * fontSize));
        int charsPerLine = (int)(view.Width / (charWidth * fontSize));
        
        // Replace the placeholders with the values.
        text = text.Replace("?1", fontSize.ToString());
        text = text.Replace("??2", charCount.ToString());
        text = text.Replace("?3", lineCount.ToString());
        text = text.Replace("?4", charsPerLine.ToString());
        
        // Set the Label properties.
        label.Text = text;
        label.FontSize = fontSize;
    }
}

这段文本中可以看到唯一名称为“?1”,“??2”,“?3”和“?4”的占位符,程序运行中会用文本的信息替换掉这些占位符。

如果我们的目标是让文本尽量的大但是又不会溢出一屏的范围,那么结果会跟下面的图很接近:

效果不错,虽然iPhone和Android实际上只显示了14行文本,但技术看起来还是可靠的。我们没必要让横屏模式计算出的FontSize值也相等,但有时候它也确实可以做到:

一个大小合适的计时器(A fit-to-size clock)


Class类中包含一个静态StartTimer方法让你能够设置一个计时器定期触发事件。这个可用的周期性事件可以保证这个计时器应用可行,虽然这个应用只是简单的展示一个时间文本。

此处Device.StartTimer方法的第一个参数使用一个TimeSpan类型的值表示一个时间间隔,这个时间间隔直接影响计时器的触发周期(你的设置可以低到15或16毫秒,大概等于每秒60帧的显示器的帧速率周期),计时器的事件处理函数没有参数,但是需要返回true让计时器继续。

程序FitToSizeClock创建了一个Label用于显示时间然后设置了两个事件:页面的SizeChanged事件用于改变字号,Device.StartTimer事件用于每秒钟改变时间文本值。两个事件的处理代码都是只需要简单的改变Label的一个属性,所以可以使用lambda表达式来简化写法,就不需要将Label存成字段,直接在lambda表达式里就直接访问。

public class FitToSizeClockPage : ContentPage
{
    public FitToSizeClockPage()
    {
        Label clockLabel = new Label
        {
            HorizontalOptions = LayoutOptions.Center,    
            VerticalOptions = LayoutOptions.Center
        };
    
        Content = clockLabel;
        
        // Handle the SizeChanged event for the page.
        SizeChanged += (object sender, EventArgs args) =>
        {
            // Scale the font size to the page width
            //      (based on 11 characters in the displayed string).
            if (this.Width > 0)
                clockLabel.FontSize = this.Width / 6;
        };
        
        // Start the timer going.
        Device.StartTimer(TimeSpan.FromSeconds(1), () =>
        {
            // Set the Text property of the Label.
            clockLabel.Text = DateTime.Now.ToString("h:mm:ss tt");
            return true;
        });
    }
}

StartTimer的方法中指定了一个DateTime的自定义格式化字符串将文本格式化为一段10个或11个的文本字符,文本都是大写字符,并且宽度比平均宽度更宽。在SizeChanged处理函数中隐藏了一个逻辑,即假设要显示的文本字符数为12个,那么设置它的字号应该是页面宽度的1/6:

当然,在横屏模式下文本会变得更大:

再次提醒,该技术在Android平台下只能用于系统设置中Font size的值设置为Normal的情况。

凭经验使用恰当的文本(Empirically fitting text)


在一个特定的矩形框大小范围内填充合适的文本的另一个解决方法是:先凭经验设置文本的字号,然后在此基础上再调大或调小。该方法的优点是在Android设备上无论用户系统设置中的Font size是什么,都可以很好的工作。

但这个过程可能比较棘手:第一个问题是在字体大小和渲染文本的高度上没有一个清晰的线性关系。当文本在它的容器中宽度越大时,它在单词间就越容易出现分行,这种情况会造成更多的空间浪费。所以为了找到最佳字号往往会重复多次计算。

第二个问题涉及到Label渲染一个指定大小字号的文本时,获取Label尺寸的一个机制。你可以处理LabelSizeChanged事件,但是在处理函数里你不能做任何改变(如设置一个新的FontSize属性),因为这样做会引起这个事件处理函数的递归调用。

一个更好的方式是调用GetSizeRequest方法,这个方法定义在VisualElement类中,Label和其他所有视图元素都继承自这个类。GetSizeRequest方法需要两个参数,一个是宽度的限制,另一个是高度的限制。这两个值可以表示一个矩形范围,以此来限制你想让这个元素填充的一个范围,并且这两个值可以部分或全部都定义为无穷大。当调用LabelGetSizeRequest方法时,通常可以将宽度限制为Label元素容器的宽度,高度设置为Double.PositiveInfinity

GetSizeRequest方法返回一个类型为SizeRequest的值,该类型为一个结构体,定义了两个属性MinimumRequest,两个属性的类型都为SizeRequest属性指出了这段渲染文本的尺寸大小(关于此类容更多的内容会在后面的章节讲到)。

下面的程序EmpiricalFontSize证明了这项技术。为了方便,定义了一个名叫FontCalc的结构体来专门针对特定的Label(已初始化文本)、字号和文本宽度调用GetSizeRequest方法:

struct FontCalc
{
    public FontCalc(Label label, double fontSize, double containerWidth)
    : this()
    {
        // Save the font size.
        FontSize = fontSize;
        
        // Recalculate the Label height.
        label.FontSize = fontSize;
        SizeRequest sizeRequest =
        label.GetSizeRequest(containerWidth, Double.PositiveInfinity);
        
        // Save that height.
        TextHeight = sizeRequest.Request.Height;
    }
    
    public double FontSize { private set; get; }
    
    public double TextHeight { private set; get; }
}

这段代码将渲染后的Label元素的高度存储在一个TextHeight属性中。

当你对一个page或是layout调用GetSizeRequest方法时,它们必须要获得所有包含在可视化树中的元素的尺寸大小。当然,这是有性能损失的,所以,除非有特别的必要,你应该尽量避免这样做。但是Label元素没有子元素,所以对Label调用GetSizeRequest方法的影响并不大。然而,你依然应该尽量尝试优化这个调用。尽量避免通过循环一列字号来找出那个不会导致文本溢出容器的最大字号值,能通过算法来找出合适的值那才更好。

GetSizeRequest方法需要被调用的元素是可视化树的一部分,并且布局过程至少应该部分开始了。不要在page类的构造函数中调用GetSizeRequest方法,你不会从中获得任何信息。第一个可能获取到返回信息的时机是OnAppearing的重载方法。当然,此时你可能没有足够的信息给GetSizeRequest方法提供参数。

EmpiricalFontSizePage类中,Label的承载容器ContentViewSizeChanged事件处理函数中有使用FontCalc值的实例。(这里的事件处理函数与EstimatedFontSize程序相似)。每个FontCalc的构造函数对Label调用了GetSizeRequest方法并将结果存放在TextHeight中。SizeChanged的处理函数在10和100的上下限字号之间尝试最佳值。因此变量的名称是lowerFontCalcupperFontCalc

public class EmpiricalFontSizePage : ContentPage
{
    Label label;
    
    public EmpiricalFontSizePage()
    {
        label = new Label();
        
        Padding = new Thickness(0, Device.OnPlatform(20, 0, 0), 0, 0);
        ContentView contentView = new ContentView
        {
            Content = label
        };
        contentView.SizeChanged += OnContentViewSizeChanged;
        Content = contentView;
    }
    
    void OnContentViewSizeChanged(object sender, EventArgs args)
    {
        // Get View whose size is changing.
        View view = (View)sender;
        
        if (view.Width <= 0 || view.Height <= 0)
        return;
        
        label.Text =
        "This is a paragraph of text displayed with " +
        "a FontSize value of ?? that is empirically " +
        "calculated in a loop within the SizeChanged " +
        "handler of the Label's container. This technique " +
        "can be tricky: You don't want to get into " +
        "an infinite loop by triggering a layout pass " +
        "with every calculation. Does it work?";
        
        // Calculate the height of the rendered text.
        FontCalc lowerFontCalc = new FontCalc(label, 10, view.Width);
        FontCalc upperFontCalc = new FontCalc(label, 100, view.Width);
        
        while (upperFontCalc.FontSize - lowerFontCalc.FontSize > 1)
        {
            // Get the average font size of the upper and lower bounds.
            double fontSize = (lowerFontCalc.FontSize + upperFontCalc.FontSize) / 2;
            
            // Check the new text height against the container height.
            FontCalc newFontCalc = new FontCalc(label, fontSize, view.Width);
            
            if (newFontCalc.TextHeight > view.Height)
            {
                upperFontCalc = newFontCalc;
            }
            else
            {
                lowerFontCalc = newFontCalc;
            }
        }
        
        // Set the final font size and the text with the embedded value.
        label.FontSize = lowerFontCalc.FontSize;
        label.Text = label.Text.Replace("??", label.FontSize.ToString("F0"));
    }
}

while循环的每一次迭代中,根据两个FontCalc值的平均值获取Fontsize的值并且获取一个新的FontCalc对象。依据渲染文本的高度用这个新对象来设置lowerFontCalc或者upperFontCalc。当字体大小计算出最佳值时,循环结束。

大约七次循环之后,就能得到一个比之前那个程序估算出的值更合适的值:

旋转手机就能触发另一次重算,计算出的字号跟刚才相似(虽然没必要一样):

似乎该算法通过FontCalc作为上下限能计算出更大平均值的字号。但是字号和渲染文本之间的高度过于复杂,有时最简单的方式得到的结果也一样的好。

原文链接:
https://download.xamarin.com/developer/xamarin-forms-book/BookPreview2-Ch05-Rel0203.pdf




感谢您阅读这份文稿。转载请注明原文地址

Attribution作者 qinjin

posted @ 2016-03-06 21:23  fengrui  阅读(5273)  评论(0编辑  收藏  举报