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

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

如以前所見的大量可視化元素均有本身的尺寸大小:html

  • iOS的狀態欄高度爲20,因此咱們須要調整iOS的頁面的Padding值,留出這個高度。
  • BoxView設置它的默認寬度和高度爲40。
  • Frame的默認Padding爲20。
  • StackLayout的默認Spacing屬性值爲6。

還有Device.GetNamedSize方法,該方法將LabelButton等控件中使用的NamedSize枚舉值轉換爲不一樣平臺對應的數值,即不一樣控件中不一樣NamedSize枚舉對應的FontSize值。git

而後上面那些數值表明什麼?它們的單位是什麼?而且怎樣精確的設置這些值得到指定的大小?github

好問題。尺寸大小一樣會影響文本的顯示效果,正如咱們所看到,不一樣的平臺顯示的文本的數量也會不同,那麼能夠在Forms程序中控制顯示的文本數量嗎?即便能夠控制,那會是一種好的編程實踐嗎?程序應該經過調整尺寸大小來適應屏幕的顯示密度嗎?算法

一般,當編寫Xamarin.Forms應用程序時不要過於接近那些可視化元素的實際尺寸數值。最好的方式是充分信任Xamarin.Forms在三個不一樣平臺下都會作出最好的默認選擇。編程

而後,有時一個開發者仍是須要知道部分可視化元素的尺寸大小以及它們所附着的屏幕的尺寸大小。小程序

如你平時所知的同樣,視頻是由一大堆像素所組成的一個矩形。任何能夠顯示在屏幕上的可視化元素都有一個像素尺寸。在早期的我的電腦中,開發者都用像素來定位和佈局那些可視化元素。可是,隨着擁有更多元素的大小尺寸和像素密度的顯示設備出現,在編寫程序時直接使用像素的方式變得過期和不受開發者歡迎了,必須尋求另外一種新的解決方案。ide

Pixels,points,dps,DIPs,DIUs


這種控制像素的方式始於桌面電腦時代的操做系統,因而這種解決方案也天然而然的被用於移動設備。所以,咱們將從桌面設備開始探討這個問題。函數

桌面視頻有大量不一樣的像素尺寸,從幾乎要過期的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開發者一般使用設備無關的方式來處理手機顯示,可是在具體三個平臺上也有一些不同:

  • 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

相關文章
相關標籤/搜索