第十四章:绝对布局(七)

一些乐趣

正如您现在可能看到的那样,AbsoluteLayout通常用于某些特殊目的,否则就不容易了。 其中一些可能实际上被归类为“有趣”。
DotMatrixClock使用模拟的5×7点阵显示器显示当前时间的数字。 每个点都是一个BoxView,单独调整尺寸并定位在屏幕上,并根据点是打开还是关闭而着色为红色或浅灰色。 可以想象,这个时钟的点可以用嵌套的StackLayout元素或Grid组织,但每个BoxView都需要给出一个大小。 这些视图的绝对数量和规律性表明程序员比布局类更了解如何在屏幕上排列它们,因为StackLayout和Grid需要以更通用的方式执行位置计算。 因此,这是AbsoluteLayout的理想工作。
XAML文件在页面上设置一个小填充,并准备AbsoluteLayout以按代码填充:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="DotMatrixClock.DotMatrixClockPage"
             Padding="10"
             SizeChanged="OnPageSizeChanged">
    <AbsoluteLayout x:Name="absoluteLayout"
                    VerticalOptions="Center" />
 
</ContentPage>

代码隐藏文件包含几个字段,包括两个名为numberPatterns和colonPattern的数组,它们定义10位数的点阵模式和冒号分隔符:

public partial class DotMatrixClockPage : ContentPage
{
    // Total dots horizontally and vertically.
    const int horzDots = 41;
    const int vertDots = 7;
    // 5 x 7 dot matrix patterns for 0 through 9.
    static readonly int[,,] numberPatterns = new int[10,7,5]
    {
        {
            { 0, 1, 1, 1, 0}, { 1, 0, 0, 0, 1}, { 1, 0, 0, 1, 1}, { 1, 0, 1, 0, 1}, 
            { 1, 1, 0, 0, 1}, { 1, 0, 0, 0, 1}, { 0, 1, 1, 1, 0}
        },
        {
            { 0, 0, 1, 0, 0}, { 0, 1, 1, 0, 0}, { 0, 0, 1, 0, 0}, { 0, 0, 1, 0, 0}, 
            { 0, 0, 1, 0, 0}, { 0, 0, 1, 0, 0}, { 0, 1, 1, 1, 0}
        },
        {
            { 0, 1, 1, 1, 0}, { 1, 0, 0, 0, 1}, { 0, 0, 0, 0, 1}, { 0, 0, 0, 1, 0}, 
            { 0, 0, 1, 0, 0}, { 0, 1, 0, 0, 0}, { 1, 1, 1, 1, 1}
        },
        {
            { 1, 1, 1, 1, 1}, { 0, 0, 0, 1, 0}, { 0, 0, 1, 0, 0}, { 0, 0, 0, 1, 0},
            { 0, 0, 0, 0, 1}, { 1, 0, 0, 0, 1}, { 0, 1, 1, 1, 0}
        },
        {
            { 0, 0, 0, 1, 0}, { 0, 0, 1, 1, 0}, { 0, 1, 0, 1, 0}, { 1, 0, 0, 1, 0}, 
            { 1, 1, 1, 1, 1}, { 0, 0, 0, 1, 0}, { 0, 0, 0, 1, 0}
        },
        {
            { 1, 1, 1, 1, 1}, { 1, 0, 0, 0, 0}, { 1, 1, 1, 1, 0}, { 0, 0, 0, 0, 1}, 
            { 0, 0, 0, 0, 1}, { 1, 0, 0, 0, 1}, { 0, 1, 1, 1, 0}
        },
        {
            { 0, 0, 1, 1, 0}, { 0, 1, 0, 0, 0}, { 1, 0, 0, 0, 0}, { 1, 1, 1, 1, 0}, 
            { 1, 0, 0, 0, 1}, { 1, 0, 0, 0, 1}, { 0, 1, 1, 1, 0}
        },
        {
            { 1, 1, 1, 1, 1}, { 0, 0, 0, 0, 1}, { 0, 0, 0, 1, 0}, { 0, 0, 1, 0, 0}, 
            { 0, 1, 0, 0, 0}, { 0, 1, 0, 0, 0}, { 0, 1, 0, 0, 0}
        },
        {
            { 0, 1, 1, 1, 0}, { 1, 0, 0, 0, 1}, { 1, 0, 0, 0, 1}, { 0, 1, 1, 1, 0}, 
            { 1, 0, 0, 0, 1}, { 1, 0, 0, 0, 1}, { 0, 1, 1, 1, 0}
        },
        {
            { 0, 1, 1, 1, 0}, { 1, 0, 0, 0, 1}, { 1, 0, 0, 0, 1}, { 0, 1, 1, 1, 1}, 
            { 0, 0, 0, 0, 1}, { 0, 0, 0, 1, 0}, { 0, 1, 1, 0, 0}
        },
    };
    // Dot matrix pattern for a colon.
    static readonly int[,] colonPattern = new int[7, 2]
    {
        { 0, 0 }, { 1, 1 }, { 1, 1 }, { 0, 0 }, { 1, 1 }, { 1, 1 }, { 0, 0 }
    };
    // BoxView colors for on and off.
    static readonly Color colorOn = Color.Red;
    static readonly Color colorOff = new Color(0.5, 0.5, 0.5, 0.25);
    // Box views for 6 digits, 7 rows, 5 columns.
    BoxView[,,] digitBoxViews = new BoxView[6, 7, 5];
    __
}

还为BoxView对象的数组定义字段,用于时间的六位数字 - 两位数字,分别用于小时,分钟和秒。水平点的总数(设置为horzDots)包括六个数字中的每一个的五个点,小时和分钟之间的冒号四个点,分钟和秒之间的冒号四个点,以及数字之间的一个点宽度除此以外。
程序的构造函数(如下所示)创建了总共238个BoxView对象并将它们添加到AbsoluteLayout,但它也为digitBoxViews数组中的数字保存了BoxView对象。 (理论上,稍后可以通过索引AbsoluteLayout的Children集合来引用BoxView对象。但是在该集合中,它们看起来只是一个线性列表。将它们存储在多维数组中可以更容易地识别和引用它们。 )所有定位和尺寸均基于AbsoluteLayout成比例,假设其长宽比为41到7,其中包含41个BoxView宽度和7个BoxView高度。

public partial class DotMatrixClockPage : ContentPage
{
    __
    public DotMatrixClockPage()
    {
        InitializeComponent();
        // BoxView dot dimensions.
        double height = 0.85 / vertDots;
        double width = 0.85 / horzDots;
        // Create and assemble the BoxViews.
        double xIncrement = 1.0 / (horzDots - 1);
        double yIncrement = 1.0 / (vertDots - 1);
        double x = 0;
        for (int digit = 0; digit < 6; digit++)
        {
            for (int col = 0; col < 5; col++)
            {
                double y = 0;
                for (int row = 0; row < 7; row++)
                {
                    // Create the digit BoxView and add to layout.
                    BoxView boxView = new BoxView();
                    digitBoxViews[digit, row, col] = boxView;
                    absoluteLayout.Children.Add(boxView, 
                                                new Rectangle(x, y, width, height), 
                                                AbsoluteLayoutFlags.All);
                     y += yIncrement;
                }
            x += xIncrement;
        }
        x += xIncrement;
        // Colons between the hour, minutes, and seconds.
        if (digit == 1 || digit == 3)
        {
            int colon = digit / 2;
            for (int col = 0; col < 2; col++)
            {
                double y = 0;
                for (int row = 0; row < 7; row++)
                {
                    // Create the BoxView and set the color.
                    BoxView boxView = new BoxView
                        {
                             Color = colonPattern[row, col] == 1 ?
                                        colorOn : colorOff
                        };
                        absoluteLayout.Children.Add(boxView,
                                                    new Rectangle(x, y, width, height), 
                                                    AbsoluteLayoutFlags.All);
                        y += yIncrement;
                    }
                    x += xIncrement;
                }
                x += xIncrement;
            }
        }
        // Set the timer and initialize with a manual call.
        Device.StartTimer(TimeSpan.FromSeconds(1), OnTimer);
        OnTimer();
    }
    __
}

你会记得,horzDots和vertDots常量分别设置为41和7。 要填充AbsoluteLayout,每个BoxView需要占用宽度的一小部分,等于1 / horzDots,高度的一小部分等于1 / vertDots。 设置到每个BoxView的高度和宽度是该值的85%,以便将点分开,以便它们不会相互碰撞:

double height = 0.85 / vertDots;
double width = 0.85 / horzDots;

要定位每个BoxView,构造函数计算比例xIncrement和yIncrement值,如下所示:

double xIncrement = 1.0 / (horzDots - 1);
double yIncrement = 1.0 / (vertDots - 1);

这里的分母是40和6,因此最终的X和Y位置坐标是1的值。
时间数字的BoxView对象在构造函数中根本没有着色,但是两个冒号的BoxView对象基于colonPattern数组被赋予Color属性。 DotMatrixClockPage构造函数以一秒钟的计时器结束。
页面的SizeChanged处理程序是从XAML文件设置的。 AbsoluteLayout会自动水平拉伸以填充页面的宽度(减去填充),因此HeightRequest实际上只设置了宽高比:

public partial class DotMatrixClockPage : ContentPage
{
    __
    void OnPageSizeChanged(object sender, EventArgs args)
    {
        // No chance a display will have an aspect ratio > 41:7
        absoluteLayout.HeightRequest = vertDots * Width / horzDots;
    }
 __
}

似乎Device.StartTimer事件处理程序应该相当复杂,因为它负责根据当前时间的数字设置每个BoxView的Color属性。 但是,numberPatterns数组和digitBoxViews数组的定义之间的相似性令人惊讶地直截了当:

public partial class DotMatrixClockPage : ContentPage
{
    __
 
    bool OnTimer()
    {
        DateTime dateTime = DateTime.Now;
        // Convert 24-hour clock to 12-hour clock.
        int hour = (dateTime.Hour + 11) % 12 + 1;
        // Set the dot colors for each digit separately.
        SetDotMatrix(0, hour / 10);
        SetDotMatrix(1, hour % 10);
        SetDotMatrix(2, dateTime.Minute / 10);
        SetDotMatrix(3, dateTime.Minute % 10);
        SetDotMatrix(4, dateTime.Second / 10);
        SetDotMatrix(5, dateTime.Second % 10);
        return true;
    }
    void SetDotMatrix(int index, int digit)
    {
        for (int row = 0; row < 7; row++)
        for (int col = 0; col < 5; col++)
        {
            bool isOn = numberPatterns[digit, row, col] == 1;
            Color color = isOn ? colorOn : colorOff;
            digitBoxViews[index, row, col].Color = color;
        }
    }
}

这是结果:
2018_09_03_144922
当然,越大越好,所以你可能想要将手机(或书)侧身转向足够大的东西,从房间的另一端读取:
2018_09_03_145007
适用于AbsoluteLayout的另一种特殊类型的应用是动画。 BouncingText程序使用其XAML文件来实例化两个Label元素:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="BouncingText.BouncingTextPage">
    <AbsoluteLayout>
        <Label x:Name="label1"
               Text="BOUNCE"
               FontSize="Large"
               AbsoluteLayout.LayoutFlags="PositionProportional" />
        <Label x:Name="label2"
               Text="BOUNCE"
               FontSize="Large"
               AbsoluteLayout.LayoutFlags="PositionProportional" />
 
    </AbsoluteLayout>
</ContentPage>

请注意,AbsoluteLayout.LayoutFlags属性设置为PositionProportional。 Label会计算自己的大小,但定位是成比例的。 介于0和1之间的值可以将两个Label元素放置在页面内的任何位置。
代码隐藏文件以15毫秒的持续时间启动计时器。 这相当于每秒约60个滴答,这通常是视频显示的刷新率。 15毫秒的计时器持续时间是执行动画的理想选择:

public partial class BouncingTextPage : ContentPage
{
    const double period = 2000; // in milliseconds
    readonly DateTime startTime = DateTime.Now;
    public BouncingTextPage()
    {
        InitializeComponent();
        Device.StartTimer(TimeSpan.FromMilliseconds(15), OnTimerTick);
    }
    bool OnTimerTick()
    {
        TimeSpan elapsed = DateTime.Now - startTime;
        double t = (elapsed.TotalMilliseconds % period) / period; // 0 to 1
        t = 2 * (t < 0.5 ? t : 1 - t); // 0 to 1 to 0
        AbsoluteLayout.SetLayoutBounds(label1,
            new Rectangle(t, 0.5, AbsoluteLayout.AutoSize, AbsoluteLayout.AutoSize));
        AbsoluteLayout.SetLayoutBounds(label2,
            new Rectangle(0.5, 1 - t, AbsoluteLayout.AutoSize, AbsoluteLayout.AutoSize));
        return true;
    }
}

OnTimerTick处理程序计算自程序启动以来经过的时间,并将其转换为每两秒从0到1的值t(对于时间)。 t的第二次计算使其从0增加到1,然后每两秒减少回0。 该值直接传递给两个AbsoluteLayout.SetLayoutBounds调用中的Rectangle构造函数。 结果是第一个标签水平移动穿过屏幕中心,似乎从左侧和右侧反弹。 第二个标签垂直上下移动到屏幕中心,似乎从顶部和底部反弹:
2018_09_03_145355
Windows 10 Mobile截图确认,两个Label视图每秒都会在中心短暂相遇。
从现在开始,我们的Xamarin.Forms应用程序的页面将变得更加活跃,动画和动态。 在下一章中,您将看到Xamarin.Forms的交互式视图如何在用户和应用程序之间建立通信方式。

猜你喜欢

转载自yq.aliyun.com/articles/632942