Browse Source

Merge pull request #2066 from donandren/prs/viewbox

Viewbox implementation
pull/2123/head
Steven Kirk 8 years ago
committed by GitHub
parent
commit
88424b4a2a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      samples/ControlCatalog/MainView.xaml
  2. 65
      samples/ControlCatalog/Pages/ViewboxPage.xaml
  3. 18
      samples/ControlCatalog/Pages/ViewboxPage.xaml.cs
  4. 41
      src/Avalonia.Controls/LayoutTransformControl.cs
  5. 124
      src/Avalonia.Controls/Viewbox.cs
  6. 1
      src/Avalonia.Themes.Default/DefaultTheme.xaml
  7. 13
      src/Avalonia.Themes.Default/LayoutTransformControl.xaml
  8. 18
      tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs
  9. 105
      tests/Avalonia.Controls.UnitTests/ViewboxTests.cs

1
samples/ControlCatalog/MainView.xaml

@ -27,5 +27,6 @@
<TabItem Header="ToolTip"><pages:ToolTipPage/></TabItem> <TabItem Header="ToolTip"><pages:ToolTipPage/></TabItem>
<TabItem Header="TreeView"><pages:TreeViewPage/></TabItem> <TabItem Header="TreeView"><pages:TreeViewPage/></TabItem>
<TabItem Header="TabControl"><pages:TabControlPage/></TabItem> <TabItem Header="TabControl"><pages:TabControlPage/></TabItem>
<TabItem Header="Viewbox"><pages:ViewboxPage/></TabItem>
</TabControl> </TabControl>
</UserControl> </UserControl>

65
samples/ControlCatalog/Pages/ViewboxPage.xaml

@ -0,0 +1,65 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<UserControl.Resources>
<StreamGeometry x:Key="Acorn">
F1 M 16.6309,18.6563C 17.1309,
8.15625 29.8809,14.1563 29.8809,
14.1563C 30.8809,11.1563 34.1308,
11.4063 34.1308,11.4063C 33.5,12
34.6309,13.1563 34.6309,13.1563C
32.1309,13.1562 31.1309,14.9062
31.1309,14.9062C 41.1309,23.9062
32.6309,27.9063 32.6309,27.9062C
24.6309,24.9063 21.1309,22.1562
16.6309,18.6563 Z M 16.6309,19.9063C
21.6309,24.1563 25.1309,26.1562
31.6309,28.6562C 31.6309,28.6562
26.3809,39.1562 18.3809,36.1563C
18.3809,36.1563 18,38 16.3809,36.9063C
15,36 16.3809,34.9063 16.3809,34.9063C
16.3809,34.9063 10.1309,30.9062 16.6309,19.9063 Z
</StreamGeometry>
</UserControl.Resources>
<Grid RowDefinitions="Auto,*">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">Viewbox</TextBlock>
<TextBlock Classes="h2">A control used to scale single child.</TextBlock>
</StackPanel>
<Grid ColumnDefinitions="Auto,*,*"
RowDefinitions="*,*,*,*"
Grid.Row="1" Margin="48"
MaxWidth="400">
<TextBlock Grid.Row="0" VerticalAlignment="Center">None</TextBlock>
<TextBlock Grid.Row="1" VerticalAlignment="Center">Fill</TextBlock>
<TextBlock Grid.Row="2" VerticalAlignment="Center">Uniform</TextBlock>
<TextBlock Grid.Row="3" VerticalAlignment="Center">UniformToFill</TextBlock>
<Viewbox Grid.Row="0" Grid.Column="1" Stretch="None">
<TextBlock>Hello World!</TextBlock>
</Viewbox>
<Viewbox Grid.Row="1" Grid.Column="1" Stretch="Fill">
<TextBlock>Hello World!</TextBlock>
</Viewbox>
<Viewbox Grid.Row="2" Grid.Column="1" Stretch="Uniform">
<TextBlock>Hello World!</TextBlock>
</Viewbox>
<Viewbox Grid.Row="3" Grid.Column="1" Stretch="UniformToFill">
<TextBlock>Hello World!</TextBlock>
</Viewbox>
<Viewbox Grid.Row="0" Grid.Column="2" Stretch="None">
<Path Fill="Blue" Data="{StaticResource Acorn}"/>
</Viewbox>
<Viewbox Grid.Row="1" Grid.Column="2" Stretch="Fill">
<Path Fill="Blue" Data="{StaticResource Acorn}"/>
</Viewbox>
<Viewbox Grid.Row="2" Grid.Column="2" Stretch="Uniform">
<Path Fill="Blue" Data="{StaticResource Acorn}"/>
</Viewbox>
<Viewbox Grid.Row="3" Grid.Column="2" Stretch="UniformToFill">
<Path Fill="Blue" Data="{StaticResource Acorn}"/>
</Viewbox>
</Grid>
</Grid>
</UserControl>

18
samples/ControlCatalog/Pages/ViewboxPage.xaml.cs

@ -0,0 +1,18 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace ControlCatalog.Pages
{
public class ViewboxPage : UserControl
{
public ViewboxPage()
{
this.InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

41
src/Avalonia.Controls/LayoutTransformControl.cs

@ -5,28 +5,30 @@
// http://silverlight.codeplex.com/SourceControl/changeset/view/74775#Release/Silverlight4/Source/Controls.Layout.Toolkit/LayoutTransformer/LayoutTransformer.cs // http://silverlight.codeplex.com/SourceControl/changeset/view/74775#Release/Silverlight4/Source/Controls.Layout.Toolkit/LayoutTransformer/LayoutTransformer.cs
// //
using Avalonia.Controls.Primitives;
using Avalonia.Media;
using Avalonia.VisualTree;
using System; using System;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reactive.Linq; using System.Reactive.Linq;
using Avalonia.Media;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
/// <summary> /// <summary>
/// Control that implements support for transformations as if applied by LayoutTransform. /// Control that implements support for transformations as if applied by LayoutTransform.
/// </summary> /// </summary>
public class LayoutTransformControl : ContentControl public class LayoutTransformControl : Decorator
{ {
public static readonly AvaloniaProperty<Transform> LayoutTransformProperty = public static readonly AvaloniaProperty<Transform> LayoutTransformProperty =
AvaloniaProperty.Register<LayoutTransformControl, Transform>(nameof(LayoutTransform)); AvaloniaProperty.Register<LayoutTransformControl, Transform>(nameof(LayoutTransform));
static LayoutTransformControl() static LayoutTransformControl()
{ {
ClipToBoundsProperty.OverrideDefaultValue<LayoutTransformControl>(true);
LayoutTransformProperty.Changed LayoutTransformProperty.Changed
.AddClassHandler<LayoutTransformControl>(x => x.OnLayoutTransformChanged); .AddClassHandler<LayoutTransformControl>(x => x.OnLayoutTransformChanged);
ChildProperty.Changed
.AddClassHandler<LayoutTransformControl>(x => x.OnChildChanged);
} }
/// <summary> /// <summary>
@ -38,8 +40,7 @@ namespace Avalonia.Controls
set { SetValue(LayoutTransformProperty, value); } set { SetValue(LayoutTransformProperty, value); }
} }
public Control TransformRoot => _transformRoot ?? public IControl TransformRoot => Child;
(_transformRoot = this.GetVisualChildren().OfType<Control>().FirstOrDefault());
/// <summary> /// <summary>
/// Provides the behavior for the "Arrange" pass of layout. /// Provides the behavior for the "Arrange" pass of layout.
@ -132,16 +133,8 @@ namespace Avalonia.Controls
return transformedDesiredSize; return transformedDesiredSize;
} }
/// <summary> private void OnChildChanged(AvaloniaPropertyChangedEventArgs e)
/// Builds the visual tree for the LayoutTransformerControl when a new
/// template is applied.
/// </summary>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
{ {
base.OnTemplateApplied(e);
_matrixTransform = new MatrixTransform();
if (null != TransformRoot) if (null != TransformRoot)
{ {
TransformRoot.RenderTransform = _matrixTransform; TransformRoot.RenderTransform = _matrixTransform;
@ -169,14 +162,14 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// RenderTransform/MatrixTransform applied to TransformRoot. /// RenderTransform/MatrixTransform applied to TransformRoot.
/// </summary> /// </summary>
private MatrixTransform _matrixTransform; private MatrixTransform _matrixTransform = new MatrixTransform();
/// <summary> /// <summary>
/// Transformation matrix corresponding to _matrixTransform. /// Transformation matrix corresponding to _matrixTransform.
/// </summary> /// </summary>
private Matrix _transformation; private Matrix _transformation;
private IDisposable _transformChangedEvent = null; private IDisposable _transformChangedEvent = null;
private Control _transformRoot;
/// <summary> /// <summary>
/// Returns true if Size a is smaller than Size b in either dimension. /// Returns true if Size a is smaller than Size b in either dimension.
/// </summary> /// </summary>
@ -215,7 +208,8 @@ namespace Avalonia.Controls
/// </remarks> /// </remarks>
private void ApplyLayoutTransform() private void ApplyLayoutTransform()
{ {
if (LayoutTransform == null) return; if (LayoutTransform == null)
return;
// Get the transform matrix and apply it // Get the transform matrix and apply it
_transformation = RoundMatrix(LayoutTransform.Value, DecimalsAfterRound); _transformation = RoundMatrix(LayoutTransform.Value, DecimalsAfterRound);
@ -376,11 +370,8 @@ namespace Avalonia.Controls
{ {
var newTransform = e.NewValue as Transform; var newTransform = e.NewValue as Transform;
if (_transformChangedEvent != null) _transformChangedEvent?.Dispose();
{ _transformChangedEvent = null;
_transformChangedEvent.Dispose();
_transformChangedEvent = null;
}
if (newTransform != null) if (newTransform != null)
{ {
@ -392,4 +383,4 @@ namespace Avalonia.Controls
ApplyLayoutTransform(); ApplyLayoutTransform();
} }
} }
} }

124
src/Avalonia.Controls/Viewbox.cs

@ -0,0 +1,124 @@
using System;
using Avalonia.Media;
namespace Avalonia.Controls
{
/// <summary>
/// Viewbox is used to scale single child.
/// </summary>
/// <seealso cref="Avalonia.Controls.Decorator" />
public class Viewbox : Decorator
{
/// <summary>
/// The stretch property
/// </summary>
public static AvaloniaProperty<Stretch> StretchProperty =
AvaloniaProperty.RegisterDirect<Viewbox, Stretch>(nameof(Stretch),
v => v.Stretch, (c, v) => c.Stretch = v, Stretch.Uniform);
private Stretch _stretch = Stretch.Uniform;
/// <summary>
/// Gets or sets the stretch mode,
/// which determines how child fits into the available space.
/// </summary>
/// <value>
/// The stretch.
/// </value>
public Stretch Stretch
{
get => _stretch;
set => SetAndRaise(StretchProperty, ref _stretch, value);
}
static Viewbox()
{
ClipToBoundsProperty.OverrideDefaultValue<Viewbox>(true);
AffectsMeasure<Viewbox>(StretchProperty);
}
protected override Size MeasureOverride(Size availableSize)
{
var child = Child;
if (child != null)
{
child.Measure(Size.Infinity);
var childSize = child.DesiredSize;
var scale = GetScale(availableSize, childSize, Stretch);
return childSize * scale;
}
return new Size();
}
protected override Size ArrangeOverride(Size finalSize)
{
var child = Child;
if (child != null)
{
var childSize = child.DesiredSize;
var scale = GetScale(finalSize, childSize, Stretch);
var scaleTransform = child.RenderTransform as ScaleTransform;
if (scaleTransform == null)
{
child.RenderTransform = scaleTransform = new ScaleTransform(scale.X, scale.Y);
child.RenderTransformOrigin = RelativePoint.TopLeft;
}
scaleTransform.ScaleX = scale.X;
scaleTransform.ScaleY = scale.Y;
child.Arrange(new Rect(childSize));
return childSize * scale;
}
return new Size();
}
private static Vector GetScale(Size availableSize, Size childSize, Stretch stretch)
{
double scaleX = 1.0;
double scaleY = 1.0;
bool validWidth = !double.IsPositiveInfinity(availableSize.Width);
bool validHeight = !double.IsPositiveInfinity(availableSize.Height);
if (stretch != Stretch.None && (validWidth || validHeight))
{
scaleX = childSize.Width <= 0.0 ? 0.0 : availableSize.Width / childSize.Width;
scaleY = childSize.Height <= 0.0 ? 0.0 : availableSize.Height / childSize.Height;
if (!validWidth)
{
scaleX = scaleY;
}
else if (!validHeight)
{
scaleY = scaleX;
}
else
{
switch (stretch)
{
case Stretch.Uniform:
scaleX = scaleY = Math.Min(scaleX, scaleY);
break;
case Stretch.UniformToFill:
scaleX = scaleY = Math.Max(scaleX, scaleY);
break;
}
}
}
return new Vector(scaleX, scaleY);
}
}
}

1
src/Avalonia.Themes.Default/DefaultTheme.xaml

@ -12,7 +12,6 @@
<StyleInclude Source="resm:Avalonia.Themes.Default.DropDownItem.xaml?assembly=Avalonia.Themes.Default"/> <StyleInclude Source="resm:Avalonia.Themes.Default.DropDownItem.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.GridSplitter.xaml?assembly=Avalonia.Themes.Default"/> <StyleInclude Source="resm:Avalonia.Themes.Default.GridSplitter.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.ItemsControl.xaml?assembly=Avalonia.Themes.Default"/> <StyleInclude Source="resm:Avalonia.Themes.Default.ItemsControl.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.LayoutTransformControl.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.ListBox.xaml?assembly=Avalonia.Themes.Default"/> <StyleInclude Source="resm:Avalonia.Themes.Default.ListBox.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.ListBoxItem.xaml?assembly=Avalonia.Themes.Default"/> <StyleInclude Source="resm:Avalonia.Themes.Default.ListBoxItem.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.Menu.xaml?assembly=Avalonia.Themes.Default"/> <StyleInclude Source="resm:Avalonia.Themes.Default.Menu.xaml?assembly=Avalonia.Themes.Default"/>

13
src/Avalonia.Themes.Default/LayoutTransformControl.xaml

@ -1,13 +0,0 @@
<Style xmlns="https://github.com/avaloniaui" Selector="LayoutTransformControl">
<Setter Property="Template">
<ControlTemplate>
<ContentPresenter Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"/>
</ControlTemplate>
</Setter>
</Style>

18
tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs

@ -1,6 +1,4 @@
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Shapes; using Avalonia.Controls.Shapes;
using Avalonia.Controls.Templates;
using Avalonia.Media; using Avalonia.Media;
using Xunit; using Xunit;
@ -311,20 +309,10 @@ namespace Avalonia.Controls.UnitTests
{ {
var lt = new LayoutTransformControl() var lt = new LayoutTransformControl()
{ {
LayoutTransform = transform, LayoutTransform = transform
Template = new FuncControlTemplate<LayoutTransformControl>(
p => new ContentPresenter() { Content = p.Content })
}; };
lt.Content = new Rectangle() { Width = width, Height = height }; lt.Child = new Rectangle() { Width = width, Height = height };
lt.ApplyTemplate();
//we need to force create visual child
//so the measure after is correct
(lt.Presenter as ContentPresenter).UpdateChild();
Assert.NotNull(lt.Presenter?.Child);
lt.Measure(Size.Infinity); lt.Measure(Size.Infinity);
lt.Arrange(new Rect(lt.DesiredSize)); lt.Arrange(new Rect(lt.DesiredSize));
@ -332,4 +320,4 @@ namespace Avalonia.Controls.UnitTests
return lt; return lt;
} }
} }
} }

105
tests/Avalonia.Controls.UnitTests/ViewboxTests.cs

@ -0,0 +1,105 @@
using Avalonia.Controls.Shapes;
using Avalonia.Media;
using Xunit;
namespace Avalonia.Controls.UnitTests
{
public class ViewboxTests
{
[Fact]
public void Viewbox_Stretch_Uniform_Child()
{
var target = new Viewbox() { Child = new Rectangle() { Width = 100, Height = 50 } };
target.Measure(new Size(200, 200));
target.Arrange(new Rect(new Point(0, 0), target.DesiredSize));
Assert.Equal(new Size(200, 100), target.DesiredSize);
var scaleTransform = target.Child.RenderTransform as ScaleTransform;
Assert.NotNull(scaleTransform);
Assert.Equal(2.0, scaleTransform.ScaleX);
Assert.Equal(2.0, scaleTransform.ScaleY);
}
[Fact]
public void Viewbox_Stretch_None_Child()
{
var target = new Viewbox() { Stretch = Stretch.None, Child = new Rectangle() { Width = 100, Height = 50 } };
target.Measure(new Size(200, 200));
target.Arrange(new Rect(new Point(0, 0), target.DesiredSize));
Assert.Equal(new Size(100, 50), target.DesiredSize);
var scaleTransform = target.Child.RenderTransform as ScaleTransform;
Assert.NotNull(scaleTransform);
Assert.Equal(1.0, scaleTransform.ScaleX);
Assert.Equal(1.0, scaleTransform.ScaleY);
}
[Fact]
public void Viewbox_Stretch_Fill_Child()
{
var target = new Viewbox() { Stretch = Stretch.Fill, Child = new Rectangle() { Width = 100, Height = 50 } };
target.Measure(new Size(200, 200));
target.Arrange(new Rect(new Point(0, 0), target.DesiredSize));
Assert.Equal(new Size(200, 200), target.DesiredSize);
var scaleTransform = target.Child.RenderTransform as ScaleTransform;
Assert.NotNull(scaleTransform);
Assert.Equal(2.0, scaleTransform.ScaleX);
Assert.Equal(4.0, scaleTransform.ScaleY);
}
[Fact]
public void Viewbox_Stretch_UniformToFill_Child()
{
var target = new Viewbox() { Stretch = Stretch.UniformToFill, Child = new Rectangle() { Width = 100, Height = 50 } };
target.Measure(new Size(200, 200));
target.Arrange(new Rect(new Point(0, 0), target.DesiredSize));
Assert.Equal(new Size(200, 200), target.DesiredSize);
var scaleTransform = target.Child.RenderTransform as ScaleTransform;
Assert.NotNull(scaleTransform);
Assert.Equal(4.0, scaleTransform.ScaleX);
Assert.Equal(4.0, scaleTransform.ScaleY);
}
[Fact]
public void Viewbox_Stretch_Uniform_Child_With_Unrestricted_Width()
{
var target = new Viewbox() { Child = new Rectangle() { Width = 100, Height = 50 } };
target.Measure(new Size(double.PositiveInfinity, 200));
target.Arrange(new Rect(new Point(0, 0), target.DesiredSize));
Assert.Equal(new Size(400, 200), target.DesiredSize);
var scaleTransform = target.Child.RenderTransform as ScaleTransform;
Assert.NotNull(scaleTransform);
Assert.Equal(4.0, scaleTransform.ScaleX);
Assert.Equal(4.0, scaleTransform.ScaleY);
}
[Fact]
public void Viewbox_Stretch_Uniform_Child_With_Unrestricted_Height()
{
var target = new Viewbox() { Child = new Rectangle() { Width = 100, Height = 50 } };
target.Measure(new Size(200, double.PositiveInfinity));
target.Arrange(new Rect(new Point(0, 0), target.DesiredSize));
Assert.Equal(new Size(200, 100), target.DesiredSize);
var scaleTransform = target.Child.RenderTransform as ScaleTransform;
Assert.NotNull(scaleTransform);
Assert.Equal(2.0, scaleTransform.ScaleX);
Assert.Equal(2.0, scaleTransform.ScaleY);
}
}
}
Loading…
Cancel
Save