自定义Shape

1. 前言

这篇文章介绍了继承并自定义Shape的方法,不过,恐怕,事实上,100个xaml的程序员99个都不会用到。写出来是因为反正都学了,当作写个笔记。

通过这篇文章,你可以学到如下知识点:

  • 自定义Shape。
  • DeferRefresh模式。
  • InvalidateArrange的应用。

2. 从Path派生

UWP中的Shape大部分都是密封类--除了Path。所以要自定义Shape只能从Path派生。Template10给出了这个例子:RingSegment 。

从这个类中可以看到,自定义Shape只需要简单地在每个自定义属性的属性值改变时或SizeChanged时调用private void UpdatePath()为Path.Data赋值就完成了,很简单吧。

RingSegment.StartAngle = 30;
RingSegment.EndAngle = 330;
RingSegment.Radius = 50;
RingSegment.InnerRadius = 30;

3. BeginUpdate、EndUpdate与DeferRefresh

这段代码会产生一个问题:每更改一个属性的值后都会调用UpdatePath(),那不就会重复调用四次?

事实上真的会,显然这个类的作者也考虑过这个问题,所以提供了public void BeginUpdate()public void EndUpdate()函数。

/// <summary>/// Suspends path updates until EndUpdate is called;/// </summary>public void BeginUpdate(){
    _isUpdating = true;
}/// <summary>/// Resumes immediate path updates every time a component property value changes. Updates the path./// </summary>public void EndUpdate(){
    _isUpdating = false;    UpdatePath();
}

使用这两个方法重新写上面那段代码,就是这样:

try{
    RingSegment.BeginUpdate();
    RingSegment.StartAngle = 30;
    RingSegment.EndAngle = 330;
    RingSegment.Radius = 100;
    RingSegment.InnerRadius = 80;
}finally{
    RingSegment.EndUpdate();
}

这样就保证了只有在调用EndUpdate()时才执行UpdatePath(),而且只执行一次。

在WPF中,DeferRefresh是一种更成熟的方案。相信很多开发者在用DataGrid时多多少少有用过(主要是通过CollectionView或CollectionViewSource)。典型的实现方式可以参考DataSourceProvider。在UWPCommunityToolkit中也通过AdvancedCollectionView实现了这种方式。

在RingSegment中添加实现如下:

private int _deferLevel;public virtual IDisposable DeferRefresh(){
    ++_deferLevel;    return new DeferHelper(this);
}private void EndDefer(){
    Debug.Assert(_deferLevel > 0);
    --_deferLevel;    if (_deferLevel == 0)
    {        UpdatePath();
    }
}private class DeferHelper : IDisposable{    public DeferHelper(RingSegment source)    {
        _source = source;
    }    private RingSegment _source;    public void Dispose()    {
        GC.SuppressFinalize(this);        if (_source != null)
        {
            _source.EndDefer();
            _source = null;
        }
    }
}

使用如下:

using (RingSegment.DeferRefresh())
{
    RingSegment.StartAngle = 30;
    RingSegment.EndAngle = 330;
    RingSegment.Radius = 100;
    RingSegment.InnerRadius = 80;
}

使用DeferRefresh模式有两个好处:

  • 调用代码比较简单
  • 通过_deferLevel判断是否需要UpdatePath(),这样即使多次调用DeferRefresh()也只会执行一次UpdatePath()。譬如以下的调用方式:
using (RingSegment.DeferRefresh())
{
    RingSegment.StartAngle = 30;
    RingSegment.EndAngle = 330;
    RingSegment.Radius = 50;
    RingSegment.InnerRadius = 30;    using (RingSegment.DeferRefresh())
    {
        RingSegment.Radius = 51;
        RingSegment.InnerRadius = 31;
    }
}

也许你会觉得一般人不会写得这么复杂,但在复杂的场景DeferRefresh模式是有存在意义的。假设现在要更新一个复杂的UI,这个UI由很多个代码模块驱动,但不清楚其它地方有没有对需要更新的UI调用过DeferRefresh(),而创建一个DeferHelper 的消耗比起更新一次复杂UI的消耗低太多,所以执行一次DeferRefresh()是个很合理的选择。

看到++_deferLevel这句代码条件反射就会考虑到线程安全问题,但其实是过虑了。UWP要求操作UI的代码都只能在UI线程中执行,所以理论上来说所有UIElement及它的所有操作都是线程安全的。

4. InvalidateArrange

每次更改属性都要调用DeferRefresh显然不是一个聪明的做法,而且在XAML中也不可能做到。另一种延迟执行的机制是利用CoreDispatcher的public IAsyncAction RunAsync(CoreDispatcherPriority priority, DispatchedHandler agileCallback)函数异步地执行工作项。要详细解释RunAsync可能需要一整篇文章的篇幅,简单来说RunAsync的作用就是将工作项发送到一个队列,UI线程有空的时候会从这个队列获取工作项并执行。InvalidateArrange就是利用这种机制的典型例子。MSDN上对InvalidateArrange的解释是:

使 UIElement 的排列状态(布局)无效。失效后,UIElement 将以异步方式更新其布局。

将InvalidateArrange的逻辑简化后大概如下:

protected bool ArrangeDirty { get; set; }public void InvalidateArrange(){    if (ArrangeDirty == true)        return;

    ArrangeDirty = true;
    Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
    {
        ArrangeDirty = false;        lock (this)
        {            //Measure
            //Arrange
        }
    });
}

调用InvalidateArrange后将ArrangeDirty标记为True,然后异步执行Measure及Arrange代码进行布局。多次调用InvalidateArrange会检查ArrangeDirty的状态以免重复执行。利用InvalidateArrange,我们可以在RingSegment的自定义属性值改变事件中调用InvalidateArrange,异步地触发LayoutUpdated并在其中改变Path.Data。
修改后的代码如下:

private bool _realizeGeometryScheduled;private Size _orginalSize;private Direction _orginalDirection;private void OnStartAngleChanged(double oldStartAngle, double newStartAngle){    InvalidateGeometry();
}private void OnEndAngleChanged(double oldEndAngle, double newEndAngle){    InvalidateGeometry();
}private void OnRadiusChanged(double oldRadius, double newRadius){    this.Width = this.Height = 2 * Radius;    InvalidateGeometry();
}private void OnInnerRadiusChanged(double oldInnerRadius, double newInnerRadius){    if (newInnerRadius < 0)
    {        throw new ArgumentException("InnerRadius can‘t be a negative value.", "InnerRadius");
    }    InvalidateGeometry();
}private void OnCenterChanged(Point? oldCenter, Point? newCenter){    InvalidateGeometry();
}protected override Size ArrangeOverride(Size finalSize){    if (_realizeGeometryScheduled == false && _orginalSize != finalSize)
    {
        _realizeGeometryScheduled = true;
        LayoutUpdated += OnTriangleLayoutUpdated;
        _orginalSize = finalSize;
    }    base.ArrangeOverride(finalSize);    return finalSize;
}protected override Size MeasureOverride(Size availableSize){     return new Size(base.StrokeThickness, base.StrokeThickness);
}public void InvalidateGeometry(){    InvalidateArrange();    if (_realizeGeometryScheduled == false )
    {
        _realizeGeometryScheduled = true;
        LayoutUpdated += OnTriangleLayoutUpdated;
    }
}private void OnTriangleLayoutUpdated(object sender, object e){
    _realizeGeometryScheduled = false;
    LayoutUpdated -= OnTriangleLayoutUpdated;    RealizeGeometry();
}private void RealizeGeometry(){    //other code here

    Data = pathGeometry;
}

这些代码参考了ExpressionSDK的Silverlight版本。ExpressionSDK提供了一些Shape可以用作参考。(安装Blend后通常可以在这个位置找到它:C:\Program Files (x86)\Microsoft SDKs\Expression\Blend\Silverlight\v5.0\Libraries\Microsoft.Expression.Drawing.dll)由于比起WPF,Silverlight更接近UWP,所以Silverlight的很多代码及经验更有参考价值,遇到难题不妨找些Silverlight代码来作参考。

InvalidateArrange属于比较核心的API,文档中也充斥着“通常不建议“、”通常是不必要的”、“慎重地使用它”等字句,所以平时使用最好要谨慎。如果不是性能十分敏感的场合还是建议使用Template10的方式实现。

5. 使用TemplatedControl实现

除了从Path派生,自定义Shape的功能也可以用TemplatedControl实现,一般来说这种方式应该是最简单最通用的方式。下面的代码使用TemplatedControl实现了一个三角形:

[TemplatePart(Name = PathElementName,Type =typeof(Path))]
[StyleTypedProperty(Property = nameof(PathElementStyle), StyleTargetType =typeof(Path))]public class TriangleControl : Control
    {        private const string PathElementName = "PathElement";    

        public TriangleControl()        {            this.DefaultStyleKey = typeof(TriangleControl);            this.SizeChanged += OnTriangleControlSizeChanged;
        }     

        /// <summary>
        ///     标识 Direction 依赖属性。
        /// </summary>
        public static readonly DependencyProperty DirectionProperty =
            DependencyProperty.Register("Direction", typeof(Direction), typeof(TriangleControl), new PropertyMetadata(Direction.Up, OnDirectionChanged));        /// <summary>
        ///     获取或设置Direction的值
        /// </summary>
        public Direction Direction
        {            get { return (Direction)GetValue(DirectionProperty); }            set { SetValue(DirectionProperty, value); }
        }        private static void OnDirectionChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)        {            var target = obj as TriangleControl;            var oldValue = (Direction)args.OldValue;            var newValue = (Direction)args.NewValue;            if (oldValue != newValue)
                target.OnDirectionChanged(oldValue, newValue);
        }        protected virtual void OnDirectionChanged(Direction oldValue, Direction newValue)        {            UpdateShape();
        }        /// <summary>
        /// 获取或设置PathElementStyle的值
        /// </summary>  
        public Style PathElementStyle
        {            get { return (Style)GetValue(PathElementStyleProperty); }            set { SetValue(PathElementStyleProperty, value); }
        }        /// <summary>
        /// 标识 PathElementStyle 依赖属性。
        /// </summary>
        public static readonly DependencyProperty PathElementStyleProperty =
            DependencyProperty.Register("PathElementStyle", typeof(Style), typeof(TriangleControl), new PropertyMetadata(null));        private Path _pathElement;        public override void OnApplyTemplate()        {            base.OnApplyTemplate();
            _pathElement = GetTemplateChild("PathElement") as Path;
        }        private void OnTriangleControlSizeChanged(object sender, SizeChangedEventArgs e)        {            UpdateShape();
        }        private void UpdateShape()        {            var geometry = new PathGeometry();            var figure = new PathFigure { IsClosed = true };
            geometry.Figures.Add(figure);            switch (Direction)
            {                case Direction.Left:
                    figure.StartPoint = new Point(ActualWidth, 0);                    var segment = new LineSegment { Point = new Point(ActualWidth, ActualHeight) };
                    figure.Segments.Add(segment);
                    segment = new LineSegment { Point = new Point(0, ActualHeight / 2) };
                    figure.Segments.Add(segment);                    break;                case Direction.Up:
                    figure.StartPoint = new Point(0, ActualHeight);
                    segment = new LineSegment { Point = new Point(ActualWidth / 2, 0) };
                    figure.Segments.Add(segment);
                    segment = new LineSegment { Point = new Point(ActualWidth, ActualHeight) };
                    figure.Segments.Add(segment);                    break;                case Direction.Right:
                    figure.StartPoint = new Point(0, 0);
                    segment = new LineSegment { Point = new Point(ActualWidth, ActualHeight / 2) };
                    figure.Segments.Add(segment);
                    segment = new LineSegment { Point = new Point(0, ActualHeight) };
                    figure.Segments.Add(segment);                    break;                case Direction.Down:
                    figure.StartPoint = new Point(0, 0);
                    segment = new LineSegment { Point = new Point(ActualWidth, 0) };
                    figure.Segments.Add(segment);
                    segment = new LineSegment { Point = new Point(ActualWidth / 2, ActualHeight) };
                    figure.Segments.Add(segment);                    break;
            }
            _pathElement.Data = geometry;
        }
    }
<Style TargetType="Path"       x:Key="PathElementStyle">
    <Setter Property="Stroke"            Value="RoyalBlue" />
    <Setter Property="StrokeThickness"            Value="10" />
    <Setter Property="Stretch"            Value="Fill" /></Style><Style TargetType="local:TriangleControl">
    <Setter Property="PathElementStyle"            Value="{StaticResource PathElementStyle}" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:TriangleControl">
                <Border Background="{TemplateBinding Background}"                        BorderBrush="{TemplateBinding BorderBrush}"                        BorderThickness="{TemplateBinding BorderThickness}">
                    <Path x:Name="PathElement"                          Style="{TemplateBinding PathElementStyle}" />
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter></Style>

这种方式的好处是容易实现,而且兼容WPF和UWP。缺点是只能通过PathElementStyle修改Path的外观,毕竟它不是Shape,而且增加了VisualTree的层次,不适合于性能敏感的场合。

时间: 2024-10-05 12:27:37

自定义Shape的相关文章

Android自定义Shape的属性

Android xml资源文件中Shape的属性: solid 描述:内部填充 属性:android:color 填充颜色 size 描述:大小 属性: android:width 宽 android:height 高 gradient 描述:渐变色 属性: android:startColor渐变起始颜色 android:endColor渐变结束颜色 android:centerColor渐变中间颜色 android:angle 渐变的角度,angle=0时,渐变色是从左向右,然后逆时针方向转

Android 自定义shape圆形按钮

Shape的属性: solid 描述:内部填充 属性:android:color 填充颜色 size 描述:大小 属性: android:width 宽 android:height 高 gradient 描述:渐变色 属性: android:startColor渐变起始颜色 android:endColor渐变结束颜色 android:centerColor渐变中间颜色 android:angle 渐变的角度,angle=0时,渐变色是从左向右,然后逆时针方向转:当angle=90时,渐变色从

自定义shape文件

1.shape文件 btn_bg.xml文件内容 <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android"> <!--背景颜色--> <solid android:color="#00CCFF"/> <!--corners

Button 自定义(一)-shape

需求:自定义Button,使用系统自定义Shape: 效果图: 1.默认状态 2.选中状态 实现分析: 1.目录结构: 代码实现: 1.button_normal.xml <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" > <solid android:co

Progress 自定义(一)-shape

需求:自定义ProgressBar,使用系统自定义shape; 效果图: 1.默认底色: 2.第一进度颜色: 3.第二进度颜色: 实现分析: 1.目录结构: 代码实现: 1.progress_style.xml <?xml version="1.0" encoding= "utf-8"?> <layer-list xmlns:android="http://schemas.android.com/apk/res/android"

android中图型的阴影效果(shadow-effect-with-custom-shapes)

思路: 在自定义shape中增加一层或多层,并错开,即可显示阴影效果.为增加立体感,按钮按下的时候,只设置一层.我们可以通过top, bottom, right 和 left 四个参数来控制阴影的方向和大小. 关系图 以下自定义两种阴影效果: res/drawable-hdpi/shadow1.xml <?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http:

G2 2.0 更灵活、更强大、更完备的可视化引擎!

概述 G2作为一款技术产品,自诞生以来,服务于广大的Web工程师群体和一部分数据分析师.一直来,G2 因其易用的语法和扎实的可视化理论基础,广受使用者好评.G2 1.x 的可视化能力已经非常强大,使用者已经能够在掌握图形语法的基础上结合自己对数据的理解,从而绘制出各种各样的可视化图表.然而,随着DT时代的更加深化,随着G2的发展,我们还是遇到了各种各样的,以往G2无法满足的可视化需求.经总结发现,大体上有以下几点: 数据导向,同一张图表中,绘制异构数据图形的需求 设计导向,对图形高度订制的需求

ProgressBar 基本介绍

简介 ProgressBar 继承自View,用于在界面上显示一个进度指示的界面. 1.ProgressBar有两个进度,一个是android:progress,另一个是android:secondaryProgress.后者主要是为缓存需要所涉及的,比如在看网络视频时候都会有一个缓存的进度条以及还要一个播放的进度,在这里缓存的进度就可以是android:secondaryProgress,而播放进度就是android:progress. 2.ProgressBar分为确定的和不确定的,确定的是

高逼格UI-ASD(Android Support Design)

绪 今年的Google IO给我们android开发着带来了三样很屌很屌的library: ASD(Android Support Design) APL(Android Percent Layout) DBL(Data Binding Library) 这三个库都是很屌很屌的库,第一个可以让我们在低版本的Android上使用Material Design,第二个是为了更好的适配,提供了基于百分比的Layout;至于第三个,能让Activity更好负责MVC中C的职责,让我们开发者更少的去fin