Friday 22 May 2015

RelativePanel. Nice. Meh.

Windows 10 introduces a new layout control called RelativePanel, which sounds a lot like the Android relative layout system, of which dequehead approves. He checks it out...

As Laurent demonstrates this thing can be used to persuade certain types of UI to behave in a more portrait-y or more landscape-y fashion by using AdaptiveTrigger to change a .LeftOf to a .Below, for example, and allowing the layout system to re-flow the UI accordingly. This is all very nice.

However, what I would really like is something that works for larger-scale structural changes to the UI. As things stand this generally requires manipulating Grid's attached properties such as Row, ColumnSpan etc. This works very well for small mobile UI designs because the column and row definitions can take proportional dimensions such as * and 2*, so they can arrange nicely across a range of window sizes and everything remains visible on the screen. Combined with AdaptiveTrigger we can make a nice UI that adjusts fluidly to window size and orientation changes, without anything hard-wired. Here is an example of how you might do it (to try it out, just create a blank UWP app and replace the empty Grid with this):

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <!-- dq: here we set the layout defaults for PortraitState -->
        <Grid.ColumnDefinitions> 
            <ColumnDefinition x:Name="LeftColumn" Width="*"/>
            <ColumnDefinition x:Name="RightColumn" Width="0"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition x:Name="TopRow" Height="*"/>
            <RowDefinition x:Name="BottomRow" Height="2*"/>
        </Grid.RowDefinitions>
        
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="LayoutStateGroup">
                <VisualState x:Name="LandscapeState">
                    <VisualState.StateTriggers>
                        <!-- dq: landscape applies until width <= 500 effective pixels -->
                        <AdaptiveTrigger MinWindowWidth="501"/> 
                    </VisualState.StateTriggers>
                    <VisualState.Setters>
                        <Setter Target="BigContent.(Grid.Column)" Value="1"/>
                        <Setter Target="BigContent.(Grid.Row)" Value="0"/>
                        <Setter Target="RightColumn.Width" Value="2*"/>
                        <Setter Target="BottomRow.Height" Value="0"/>
                    </VisualState.Setters>
                </VisualState>
                <VisualState x:Name="PortraitState"/>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>

        <Grid x:Name="SmallContent" Grid.Column="0" Grid.Row="0" Background="Red"/>
        <Grid x:Name="BigContent" Grid.Column="0" Grid.Row="1" Background="Blue"/>
    </Grid>

The big problem with this approach is that the layout is tightly coupled to the contents - I have to know all about the Rows and Columns that are available (not too Bad..) and also manipulate their dimensions (..Bad). It becomes very fragile once your UI gets a bit more complex, and before you know it you are giving up with the declarative approach and it all moves into C# code in the code-behind (woe is me) or ViewModel (oh not, not again!).

No good, dequehead wants to do it all in the lovely new Blend that he has been blessed with.

So at first sight it seemed that RelativePanel was going to solve this problem. Perhaps like this:

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="LayoutStateGroup">
                <VisualState x:Name="LandscapeState">
                    <VisualState.StateTriggers>
                        <AdaptiveTrigger MinWindowWidth="501"/>
                    </VisualState.StateTriggers>
                    <VisualState.Setters>
                        <!-- dq: move to the right in landscape -->
                        <Setter Target="BigContent.(RelativePanel.RightOf)" Value="SmallContent"/>
                        <Setter Target="BigContent.(RelativePanel.Below)" Value=""/>
                    </VisualState.Setters>
                </VisualState>
                <VisualState x:Name="PortraitState"/>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>

        <RelativePanel>

            <!-- dq: dock this one to the top-left -->
            <Grid x:Name="SmallContent" 
                  RelativePanel.AlignTopWithPanel="True" 
                  RelativePanel.AlignLeftWithPanel="True" 
                  Background="Red" Width="300" Height="300"/>

            <!-- dq: park this below in portrait -->
            <Grid x:Name="BigContent" 
                  RelativePanel.Below="SmallContent" 
                  Background="Blue" Width="300" Height="300"/>

        </RelativePanel>
    </Grid>

This sort of works in the sense that the panels change their relative positions, but I have had to hard-wire the dimensions. Without further mechanics (read complexity) it is rubbish (try it). The Grid approach, for all its faults, provides a much better solution for this use-case. What I'm after is the proportional layout capability of Grid combined with the intuitive dynamic nature of what RelativePanel pretends to be (I know, that's a bit unfair..). Perhaps a couple more attached properties WidthWeighting and HeightWeighting, like this:

    <RelativePanel>
        <Grid x:Name="SmallContent" 
              RelativePanel.WidthWeighting="*"
              RelativePanel.HeightWeighting="*"
              RelativePanel.AlignTopWithPanel="True" 
              RelativePanel.AlignLeftWithPanel="True" 
              Background="Red"/>
        <Grid x:Name="BigContent" 
              RelativePanel.WidthWeighting="2*"
              RelativePanel.HeightWeighting="2*"
              RelativePanel.Below="SmallContent" 
              Background="Blue"/>
    </RelativePanel>

This might be interpreted as "if BigContent is involved in a Width calculation, give it a weighting of 2*", and similar for Height (...along similar lines to android:layout_weight).

Rather more thought required, but maybe it could be made to work. Any ideas, please leave a comment below.

No comments:

Post a Comment