Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does the W10 News app stretch the items in the gridview?

Tags:

c#

xaml

gridview

I'm trying to create a gridview like in the default News app in Windows 10. As far as I know I have to set the ItemHeight an ItemWidth for the VariableSizedWrapGrid. But then it does not stretch the items to fit the full grid width, while the News app does do that as you can see in the pictures below. How do they do that? Is it a special custom control? Widescreen, 4 columns and items are quite wide

Narrowscreen, 3 small columns

like image 301
Arn Vanhoutte Avatar asked Sep 07 '15 10:09

Arn Vanhoutte


2 Answers

UWP

As an addition to my previous answer where I show the basic concept here a solution for UWP platform using the VariableSizedWrapPanel as mentioned in the question:

The main job is done by

<local:MyGridView 
    ItemsSource="{Binding}" 
    ItemTemplateSelector="{StaticResource MyGridTemplateSelector}" 
    MinItemWidth="300" MaxItemWidth="600" 
    ScrollViewer.VerticalScrollBarVisibility="Hidden">
    <GridView.ItemsPanel>
        <ItemsPanelTemplate>
            <VariableSizedWrapGrid ItemHeight="180" Orientation="Horizontal"/>
        </ItemsPanelTemplate>
    </GridView.ItemsPanel>
</local:MyGridView>

along with

MyGridView.cs

using System;
using System.Collections.Generic;
using System.Linq;
using Windows.Foundation;
using Windows.UI.Core;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace App1
{
    public class MyGridView : GridView
    {
        private int _columnCount = 1;
        private double _itemWidth = 100;

        public double MinItemWidth
        {
            get { return (double) GetValue( MinItemWidthProperty ); }
            set { SetValue( MinItemWidthProperty, value ); }
        }

        // Using a DependencyProperty as the backing store for MinItemWidth.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty MinItemWidthProperty =
            DependencyProperty.Register( "MinItemWidth", typeof( double ), typeof( MyGridView ), new PropertyMetadata( 100.0 ) );


        public double MaxItemWidth
        {
            get { return (double) GetValue( MaxItemWidthProperty ); }
            set { SetValue( MaxItemWidthProperty, value ); }
        }

        // Using a DependencyProperty as the backing store for MaxItemWidth.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty MaxItemWidthProperty =
            DependencyProperty.Register( "MaxItemWidth", typeof( double ), typeof( MyGridView ), new PropertyMetadata( 200.0 ) );


        private long _itemsPanelPropertyChangedToken;

        public MyGridView()
        {
            _itemsPanelPropertyChangedToken = RegisterPropertyChangedCallback( ItemsPanelProperty, ItemsPanelChangedAsync );
        }

        private async void ItemsPanelChangedAsync( DependencyObject sender, DependencyProperty dp )
        {
            UnregisterPropertyChangedCallback( ItemsPanelProperty, _itemsPanelPropertyChangedToken );
            await this.Dispatcher.RunIdleAsync( ItemsPanelChangedCallback );
        }

        private void ItemsPanelChangedCallback( IdleDispatchedHandlerArgs e )
        {
            var wg = ItemsPanelRoot as VariableSizedWrapGrid;
            if (wg != null)
            {
                wg.ItemWidth = _itemWidth;
            }
        }

        protected override void PrepareContainerForItemOverride( DependencyObject element, object item )
        {
            var itemIndex = this.Items.IndexOf( item );

            element.SetValue( VariableSizedWrapGrid.RowSpanProperty, GetRowSpanByColumnCountAndIndex( _columnCount, itemIndex ) );
            element.SetValue( VerticalContentAlignmentProperty, VerticalAlignment.Stretch );
            element.SetValue( HorizontalContentAlignmentProperty, HorizontalAlignment.Stretch );
            base.PrepareContainerForItemOverride( element, item );
        }

        private static readonly Dictionary<int, int[]> _rowSpanLayout = new Dictionary<int, int[]>
        {
            [ 1 ] = new int[] { /* 5 */ 2, 2, 2, 2, 2, /* 6 */ 2, 2, 2, 2, 2, 2, /* 7 */ 2, 2, 2, 2, 2, 2, 2, /* 8 */ 2, 2, 2, 2, 2, 2, 2, 2, /* 9 */ 2, 2, 2, 2, 2, 2, 2, 2, 2 },
            [ 2 ] = new int[] { /* 5 */ 2, 1, 2, 2, 1, /* 6 */ 3, 3, 3, 2, 2, 2, /* 7 */ 3, 3, 1, 2, 3, 1, 1, /* 8 */ 2, 3, 2, 3, 3, 3, 3, 1, /* 9 */ 3, 2, 1, 3, 2, 2, 3, 1, 1 },
            [ 3 ] = new int[] { /* 5 */ 3, 2, 2, 1, 1, /* 6 */ 2, 3, 2, 3, 3, 2, /* 7 */ 3, 3, 3, 2, 1, 2, 1, /* 8 */ 2, 3, 3, 1, 2, 1, 2, 1, /* 9 */ 3, 3, 3, 1, 2, 1, 3, 3, 2 },
            [ 4 ] = new int[] { /* 5 */ 2, 2, 1, 2, 1, /* 6 */ 3, 3, 2, 2, 1, 1, /* 7 */ 3, 2, 2, 2, 1, 1, 1, /* 8 */ 3, 3, 3, 3, 2, 2, 2, 2, /* 9 */ 3, 3, 3, 2, 2, 2, 2, 2, 1 },
            [ 5 ] = new int[] { /* 5 */ 2, 2, 2, 2, 2, /* 6 */ 2, 2, 2, 1, 2, 1, /* 7 */ 3, 3, 3, 2, 2, 1, 1, /* 8 */ 3, 3, 2, 2, 2, 1, 1, 1, /* 9 */ 3, 2, 2, 2, 2, 1, 1, 1, 1 },
        };

        private int GetRowSpanByColumnCountAndIndex( int columnCount, int itemIndex )
        {
            return _rowSpanLayout[ columnCount ][ itemIndex % 35 ];
        }

        protected override Size MeasureOverride( Size availableSize )
        {
            System.Diagnostics.Debug.WriteLine( availableSize );

            int columnCount = _columnCount;
            double availableWidth = availableSize.Width;

            double itemWidth = availableWidth / columnCount;

            while ( columnCount > 1 && itemWidth < Math.Min( MinItemWidth, MaxItemWidth ) )
            {
                columnCount--;
                itemWidth = availableWidth / columnCount;
            }

            while ( columnCount < 5 && itemWidth > Math.Max( MinItemWidth, MaxItemWidth ) )
            {
                columnCount++;
                itemWidth = availableWidth / columnCount;
            }

            var wg = this.ItemsPanelRoot as VariableSizedWrapGrid;

            _itemWidth = itemWidth;
            if ( _columnCount != columnCount )
            {
                _columnCount = columnCount;
                if ( wg != null )
                {
                    Update( );
                }
            }

            if ( wg != null )
            {
                wg.ItemWidth = itemWidth;
            }

            return base.MeasureOverride( availableSize );
        }

        // refresh the variablesizedwrapgrid layout
        private void Update()
        {
            if ( !( this.ItemsPanelRoot is VariableSizedWrapGrid ) )
                throw new ArgumentException( "ItemsPanel is not VariableSizedWrapGrid" );

            int itemIndex = 0;
            foreach ( var container in this.ItemsPanelRoot.Children.Cast<GridViewItem>( ) )
            {
                int rowSpan = GetRowSpanByColumnCountAndIndex( _columnCount, itemIndex );
                VariableSizedWrapGrid.SetRowSpan( container, rowSpan );
                itemIndex++;
            }

            this.ItemsPanelRoot.InvalidateMeasure( );
        }
    }
}

and

MyGridViewTemplateSelector.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace App1
{
    public class MyGridViewTemplateSelector : DataTemplateSelector
    {
        public DataTemplate Small { get; set; }
        public DataTemplate Medium { get; set; }
        public DataTemplate Large { get; set; }

        protected override DataTemplate SelectTemplateCore( object item, DependencyObject container )
        {
            var rowSpan = container.GetValue( VariableSizedWrapGrid.RowSpanProperty );

            int index;
            try
            {
                dynamic model = item;
                index = model.Index;
            }
            catch ( Exception )
            {
                index = -1;
            }
            long token = 0;

            DependencyPropertyChangedCallback lambda = ( sender, dp ) =>
            {
                container.UnregisterPropertyChangedCallback( VariableSizedWrapGrid.RowSpanProperty, token );

                var cp = (ContentControl) container;
                cp.ContentTemplateSelector = null;
                cp.ContentTemplateSelector = this;
            };

            token = container.RegisterPropertyChangedCallback( VariableSizedWrapGrid.RowSpanProperty, lambda );

            switch ( rowSpan )
            {
                case 1:
                    return Small;
                case 2:
                    return Medium;
                case 3:
                    return Large;
                default:
                    throw new InvalidOperationException( );
            }
        }

        private void Foo( DependencyObject sender, DependencyProperty dp )
        {
            throw new NotImplementedException( );
        }
    }
}

To complete here the other files

MainPage.xaml

<Page
    x:Class="App1.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App1"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">
    <Page.Resources>

        <DataTemplate x:Key="Small">
            <Grid Margin="5">
                <Grid.Background>
                    <SolidColorBrush Color="{Binding Path=Color}"/>
                </Grid.Background>
                <StackPanel VerticalAlignment="Top">
                    <StackPanel.Background>
                        <SolidColorBrush Color="White" Opacity="0.75"/>
                    </StackPanel.Background>

                    <TextBlock FontSize="15" Margin="10">
                        <Run Text="{Binding Path=Index}"/>. <Run Text="{Binding Path=Name}"/>
                    </TextBlock>
                    <TextBlock Text="Small" TextAlignment="Center"/>
                </StackPanel>
            </Grid>
        </DataTemplate>

        <DataTemplate x:Key="Medium">
            <Grid Margin="5">
                <Grid.Background>
                    <SolidColorBrush Color="{Binding Path=Color}"/>
                </Grid.Background>
                <StackPanel VerticalAlignment="Top">
                    <StackPanel.Background>
                        <SolidColorBrush Color="White" Opacity="0.75"/>
                    </StackPanel.Background>

                    <TextBlock FontSize="15" Margin="10">
                        <Run Text="{Binding Path=Index}"/>. <Run Text="{Binding Path=Name}"/>
                    </TextBlock>
                    <TextBlock Text="Medium" TextAlignment="Center"/>
                </StackPanel>
            </Grid>
        </DataTemplate>

        <DataTemplate x:Key="Large">
            <Grid Margin="5">
                <Grid.Background>
                    <SolidColorBrush Color="{Binding Path=Color}"/>
                </Grid.Background>
                <StackPanel VerticalAlignment="Top">
                    <StackPanel.Background>
                        <SolidColorBrush Color="White" Opacity="0.75"/>
                    </StackPanel.Background>

                    <TextBlock FontSize="15" Margin="10">
                        <Run Text="{Binding Path=Index}"/>. <Run Text="{Binding Path=Name}"/>
                    </TextBlock>
                    <TextBlock Text="Large" TextAlignment="Center"/>
                </StackPanel>
            </Grid>

        </DataTemplate>
        <local:MyGridViewTemplateSelector x:Key="MyGridTemplateSelector"
                                          Small="{StaticResource Small}"
                                          Medium="{StaticResource Medium}"
                                          Large="{StaticResource Large}"/>
    </Page.Resources>

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <Grid.RowDefinitions>
            <RowDefinition Height="48"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="48"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>

        <!-- top left section -->

        <Border Background="#D13438">

        </Border>

        <!-- top bar -->

        <Border Grid.Column="1" Grid.Row="0" Padding="5" Background="#F2F2F2">
            <TextBlock Text="MenuBar" VerticalAlignment="Center"/>
        </Border>

        <!-- left bar -->

        <Border Grid.Column="0" Grid.Row="1" Width="48" Background="#2B2B2B">

        </Border>

        <!-- content -->

        <Border Grid.Column="1" Grid.Row="1" Background="#E6E6E6">
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="48"/>
                    <RowDefinition Height="*"/>
                </Grid.RowDefinitions>
                <Border Grid.Row="0" Padding="5" Background="#F2F2F2">
                    <TextBlock Text="SectionBar" VerticalAlignment="Center"/>
                </Border>
                <ScrollViewer Grid.Row="1">
                    <Border Margin="7,7,10,7">

                        <!-- the wrapped news items -->

                        <local:MyGridView ItemsSource="{Binding}" ItemTemplateSelector="{StaticResource MyGridTemplateSelector}" MinItemWidth="300" MaxItemWidth="600" ScrollViewer.VerticalScrollBarVisibility="Hidden">
                            <GridView.ItemsPanel>
                                <ItemsPanelTemplate>
                                    <VariableSizedWrapGrid ItemHeight="180" Orientation="Horizontal"/>
                                </ItemsPanelTemplate>
                            </GridView.ItemsPanel>
                        </local:MyGridView>

                    </Border>
                </ScrollViewer>
            </Grid>
        </Border>
    </Grid>
</Page>

MainPage.xaml.cs

using System.Linq;
using Windows.UI;
using Windows.UI.Xaml.Controls;
using System.Reflection;

namespace App1
{
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent( );

            // just some sample data

            var colors = typeof( Colors )
                .GetRuntimeProperties( )
                .Take( 140 )
                .Select( ( x, index ) => new
                {
                    Color = (Color) x.GetValue( null ),
                    Name = x.Name,
                    Index = index,
                } );
            this.DataContext = colors;
        }

    }
}

If you will ever think "I know that from somewhere" you should have a look at Jerry Nixon's blog :o)

like image 185
Sir Rufo Avatar answered Nov 14 '22 13:11

Sir Rufo


According to MSDN the ItemWidth can be set to Auto.

The default value of ItemHeight and ItemWidth is not 0, it is Double.NaN. ItemHeight and ItemWidth support the ability to be an unset "Auto" value. Because ItemHeight and ItemWidth are Double values, Double.NaN is used as a special value to represent this "Auto" behavior. The layout system interprets the "Auto" value to generally mean that the object should be sized to the available size in layout, instead of to a specific pixel value.

I don't know if this will result in the behavior you want though. If it doesn't then you might be able to get it by binding the ItemWidth to a property where you calculate the item width based on the width of the grid. It would look something like this:

float DynamicItemWidth {
  get {
    int ItemMinimumWidth = 300, margin = 16; //just some guesses
    var gridWidth = ...;

    var numberOfColumns = gridWidth % ItemMinimumWidth;
    var itemWidth = (gridWidth - margin * (numberOfColumns - 1)) / numberOfColumns;

    return itemWidth;
  }
}
like image 31
Herman Avatar answered Nov 14 '22 15:11

Herman