如以前所见的大量可视化元素均有本身的尺寸大小:html
Padding
值,留出这个高度。BoxView
设置它的默认宽度和高度为40。Frame
的默认Padding
为20。StackLayout
的默认Spacing
属性值为6。还有Device.GetNamedSize
方法,该方法将Label
或Button
等控件中使用的NamedSize
枚举值转换为不一样平台对应的数值,即不一样控件中不一样NamedSize
枚举对应的FontSize
值。git
而后上面那些数值表明什么?它们的单位是什么?而且怎样精确的设置这些值得到指定的大小?github
好问题。尺寸大小一样会影响文本的显示效果,正如咱们所看到,不一样的平台显示的文本的数量也会不同,那么能够在Forms程序中控制显示的文本数量吗?即便能够控制,那会是一种好的编程实践吗?程序应该经过调整尺寸大小来适应屏幕的显示密度吗?算法
一般,当编写Xamarin.Forms
应用程序时不要过于接近那些可视化元素的实际尺寸数值。最好的方式是充分信任Xamarin.Forms
在三个不一样平台下都会作出最好的默认选择。编程
而后,有时一个开发者仍是须要知道部分可视化元素的尺寸大小以及它们所附着的屏幕的尺寸大小。小程序
如你平时所知的同样,视频是由一大堆像素所组成的一个矩形。任何能够显示在屏幕上的可视化元素都有一个像素尺寸。在早期的我的电脑中,开发者都用像素来定位和布局那些可视化元素。可是,随着拥有更多元素的大小尺寸和像素密度的显示设备出现,在编写程序时直接使用像素的方式变得过期和不受开发者欢迎了,必须寻求另外一种新的解决方案。ide
这种控制像素的方式始于桌面电脑时代的操做系统,因而这种解决方案也天然而然的被用于移动设备。所以,咱们将从桌面设备开始探讨这个问题。函数
桌面视频有大量不一样的像素尺寸,从几乎要过期的640x480到上千像素。跟电影和电视同样,4:3的纵宽比也曾经是电脑显示的标准,不过如今更经常使用高清晰纵宽比,如16:9或者16:10。oop
桌面视频也有一个物理尺寸,这个物理尺寸一般是测量显示器对角线的英寸和厘米长度。经过像素尺寸和物理尺寸能够计算出这个视频的显示分辨率或者像素密度,像素密度使用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开发者一般使用设备无关的方式来处理手机显示,可是在具体三个平台上也有一些不同:
若是将相同物理大小的可视化元素放在三个平台,那么Windows Phone平台上看见的大小会比iOS和Android大1.5倍。
VisualElement
类定义了两个属性,Width
和Height
,这两个元素用设备无关的单位来描述views,layouts和pages。这两个属性的初始值被设置为伪值-1。只有当page上的全部元素都已经定位和调整大小完毕这两个属性的值才有效。一样,须要注意HorizontalOptions
或VerticalOptions
的默认值是Fill
,这个设置将会让视图尽量的占据更多的空白地方。Width
和Height
的值也能够用来反映一些额外空间值,好比Padding
,设置后的区域会被view的BackgroundColor
属性指定的颜色填充。
VisualElement
定义了一个SizeChanged
事件,当一个可视化元素的Width
或Height
属性发生变化时触发。当page对内部的大量元素进行定位和调整大小时会触发一系列事件,SizeChanged
事件就是其中一个。这个构造的过程会在第一次定义这个page时出现(一般是在page的构造中),而任何一个对布局内容的影响都会使这一过程再次发生,例如将视图添加到ContentPage
或者StackLayout
中,或从它们中移除,或者改变可视化元素的大小。
当屏幕尺寸发生改变时一样也会触发新的布局过程,这种状况一般发生在设备在竖屏和横屏之间进行切换的时候。
熟悉Xamarin.Forms的布局系统能够帮助咱们写出更好的Layout<View>
继承类。具体怎样写将在之后的章节中介绍到,到时,你就会明白清楚地知道Width
和Height
属性什么时候改变有助于咱们更好地改变可视化元素的大小。你能够经过处理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
方法会在元素大小并无真正改变时触发。
下面是程序运行在各个平台下的样子:
下面是这三张图的具体信息:
须要注意程序的垂直高度尺寸,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
的值为Portrait
或Landscape
。
这里再一次强调一下三个平台上的英寸和设备无关单位之间的关系:
下面是尺寸以厘米为单位的状况:
那么意味着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 }; } }
若是你使用直尺在手机屏幕上测量,你会发现结果跟咱们但愿的尺寸很接近。
Label
和Button
控件上的FontSize
属性的类型是double
。FontSize
指的是文本字符从最下面到最上面到高度,也包括该字体对应的标点符号。在大多数状况下,你须要经过Device.GetNamedSize
方法设置这个属性。该方法容许你使用一系列NamedSize
相关到枚举值:Default
,Micro
,Small
,Medium
,Large
。
你也可使用字体大小的实际数字,可是这么作会引发一个小问题(稍后会谈到这个细节)。在大多数状况下,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项,能够看到,有Small,Normal(默认),Large,Huge这几个字号选择。这项设置能够给用户提供更广的字号选择,对于那些以为字体过小感受眼睛不舒服的用户能够将字号调大,对于那些眼睛很好想一次多看一些字的用户能够将字号设小。
在设置中修改字号,选择除Normal外的其余选项,而后从新运行FontSizes程序,能够看到程序里的全部文本都不同里,根据你的设置,文本比以前都更大或更小了。你能够看到在水平线的上面部分,也就是Device.GetNamedSize
方法返回的数值根据系统字号的不一样发生了变化。对于NamedSize.Default
,Normal的默认设置返回的字号是14(就如上面的截图所展现的同样),若是设置为Small则返回12,Large返回16,Huge返回18.33。
除了Device.GetNamedSize
返回的值不同之外,根据字号设置的不一样,底层文本绘制的逻辑也不同。继续看程序的下面部分,程序计算出的字体的点位值依然相同,虽然它们的文本大小已经发生了改变。这是用枚举值设置Android的Label
的结果,Android在内部会使用ComplexUnitType.Sp
(COMPLEX_UNIT_SP
)计算字体大小,SP
表明缩放像素scaled pixel,当文本超过使用的设备无关像素时会产生一个缩放。
也许你须要调整一堆文本到必定大小的矩形区域,你可使用两个数值来计算,一个是矩形区域的实际尺寸,另外一个是装载这些文本的Label
控件的FontSize
属性值(可是Andorid须要将Font size设置为Normal)。
第一个须要的数值是行距,即Label
视图里每一行文本间的垂直高度。下面展现了三个平台下的具体行高值:
第二个有帮助的数值是字符宽度,无论在哪一个平台,一段混合了大小写的默认字体的字符宽度大约是font size的一半:
例如,假设你想在宽度为320的长度内容纳80个文本字符,而且你想让字体尽可能的大。那么320除以40(宽度大约占高度一半)获得字号为8,这个数值就是咱们能够给Label
的FontSize
属性赋的值。对于文原本说在真正测试以前还有一些不肯定性,但愿不要对你的计算结果产生太多惊喜。
下面这个程序展现了如何让行距以及字符宽更适合页面中的一段文本,固然这个页面是不包括iPhone的状态栏的。为了让iPhone排除状态栏更容易一些,这个程序使用了ContentView
。
ContentView
继承自Layout
,只添加了一个Content
属性。ContentView
是Frame
的基类,可是Frame
没有添加过多的额外功能。然而,当你想在自定义页面中定义一组视图,并轻松的模拟它们间的外边距,它将变得颇有用。
也许你注意到了,Xamarin.Forms没有一个margin的概念,跟padding很类似,padding定义了视图里的内边距,而margin定义了视图外面的外边距。ContentView
可让咱们模拟这个,若是你发现一个视图须要一个外边距,那么你能够将这个视图放在ContentView
中,而且设置这个ContentView
的Padding
属性。ContentView
的Padding
属性继承自Layout
。
这个EstimatedFontSize程序使用ContentView
的方式略有不一样:它经过设置整个页面的padding来避开iOS的状态栏,而不是将页面中的某一项内容设置到ContentView
中。所以,此处的ContentView
除了iOS的状态栏之外与页面有相同的尺寸。经过附加ContentView
的SizeChanged
事件来获取内容区的尺寸,经过这个尺寸来计算文本的字号。
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
值也相等,但有时候它也确实能够作到:
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的状况。
在一个特定的矩形框大小范围内填充合适的文本的另外一个解决方法是:先凭经验设置文本的字号,而后在此基础上再调大或调小。该方法的优势是在Android设备上不管用户系统设置中的Font size是什么,均可以很好的工做。
但这个过程可能比较棘手:第一个问题是在字体大小和渲染文本的高度上没有一个清晰的线性关系。当文本在它的容器中宽度越大时,它在单词间就越容易出现分行,这种状况会形成更多的空间浪费。因此为了找到最佳字号每每会重复屡次计算。
第二个问题涉及到Label
渲染一个指定大小字号的文本时,获取Label
尺寸的一个机制。你能够处理Label
的SizeChanged
事件,可是在处理函数里你不能作任何改变(如设置一个新的FontSize
属性),由于这样作会引发这个事件处理函数的递归调用。
一个更好的方式是调用GetSizeRequest
方法,这个方法定义在VisualElement
类中,Label
和其余全部视图元素都继承自这个类。GetSizeRequest
方法须要两个参数,一个是宽度的限制,另外一个是高度的限制。这两个值能够表示一个矩形范围,以此来限制你想让这个元素填充的一个范围,而且这两个值能够部分或所有都定义为无穷大。当调用Label
的GetSizeRequest
方法时,一般能够将宽度限制为Label
元素容器的宽度,高度设置为Double.PositiveInfinity
。
GetSizeRequest
方法返回一个类型为SizeRequest
的值,该类型为一个结构体,定义了两个属性Minimum
和Request
,两个属性的类型都为Size
。Request
属性指出了这段渲染文本的尺寸大小(关于此类容更多的内容会在后面的章节讲到)。
下面的程序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
的承载容器ContentView
的SizeChanged
事件处理函数中有使用FontCalc
值的实例。(这里的事件处理函数与EstimatedFontSize程序类似)。每一个FontCalc
的构造函数对Label
调用了GetSizeRequest
方法并将结果存放在TextHeight
中。SizeChanged
的处理函数在10和100的上下限字号之间尝试最佳值。所以变量的名称是lowerFontCalc
和upperFontCalc
:
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