使用 Xamarin 的 CALayer 自定义属性动画

Posted

技术标签:

【中文标题】使用 Xamarin 的 CALayer 自定义属性动画【英文标题】:CALayer Custom Property Animation with Xamarin 【发布时间】:2016-10-02 09:16:05 【问题描述】:

我真的很沮丧,因为自上周以来,我一直在尝试使用 CoreAnimation 在 ios 上为饼图(带有透明孔的弧段)制作动画。

目前,我正在使用带有路径属性的 CAShapeLayer 绘制 ArcSegment。它看起来很棒,但我无法为这个属性设置动画。 我想使用 CABasicAnimation 为半径、段等图层属性设置动画。

这里有没有人可以告诉我如何解决这个问题? 谢谢。

问候 罗尼

    public class ArcSegmentLayer : CAShapeLayer 
    private const string StartAngleProperty = "StartAngle";
    private const string EndAngleProperty = "EndAngle";

    public static void RegisterProperties() 
        ObjCProperties.RegisterDynamicProperty(typeof(ArcSegmentLayer), StartAngleProperty, typeof(float));
        ObjCProperties.RegisterDynamicProperty(typeof(ArcSegmentLayer), EndAngleProperty, typeof(float));
    

    public ArcSegmentLayer()  

    [Export("initWithLayer:")]
    public ArcSegmentLayer(ArcSegmentLayer layer) 
        this.LineWidth = layer.LineWidth;
        this.Frame = layer.Frame;
        this.FillColor = layer.FillColor;
        this.StrokeColor = layer.StrokeColor;
        this.Segments = layer.Segments;
        this.Margin = layer.Margin;
    

    #region Properties

    public float StartAngle 
        get  return ObjCProperties.GetFloatProperty(Handle, StartAngleProperty); 
        set 
            ObjCProperties.SetFloatProperty(Handle, StartAngleProperty, value);
        
    

    public float EndAngle 
        get  return ObjCProperties.GetFloatProperty(Handle, EndAngleProperty); 
        set 
            ObjCProperties.SetFloatProperty(Handle, EndAngleProperty, value);
        
    

    public nint Segments 
        get  return segments; 
        set 
            if (segments != value) 
                segments = value;
                this.SetNeedsDisplay();
            
        
    

    public nfloat Margin 
        get 
            return margin;
        
        set 
            if (margin != value) 
                margin = value;
                this.SetNeedsDisplay();
            
        
    

    #endregion

    [Export("needsDisplayForKey:")]
    public static bool NeedsDisplayForKey(NSString key) 
        return key == StartAngleProperty
            || key == EndAngleProperty
            || key == "Margin"
            || key == "Segments"
            || key == "LineWidth"
            || key == "StrokeColor"
            || CALayer.NeedsDisplayForKey(key);
    

    [Export("display")]
    public override void Display() 
        base.Display();

        Console.WriteLine(this.EndAngle);

        this.Path = CreateSegments().CGPath;
    

    [Export("actionForKey:")]
    public override NSObject ActionForKey(string eventKey) 
        /*
        if (eventKey == EndAngleProperty) 
            CABasicAnimation animation = CABasicAnimation.FromKeyPath(eventKey);
            animation.TimingFunction = CAMediaTimingFunction.FromName(CAMediaTimingFunction.Linear);
            animation.From = new NSNumber(this.EndAngle); //PresentationLayer.ValueForKey(new NSString(eventKey));
                                                          //animation.Duration = CATransition. 1;   
            animation.Duration = 0;
            return animation;
         else if (eventKey == StartAngleProperty) 
            CABasicAnimation animation = CABasicAnimation.FromKeyPath(eventKey);
            animation.TimingFunction = CAMediaTimingFunction.FromName(CAMediaTimingFunction.Linear);
            animation.From = new NSNumber(this.StartAngle);
            animation.Duration = 0;
            return animation;
        */
        return base.ActionForKey(eventKey);
    

    private UIBezierPath CreateSegments() 
        var path = new UIBezierPath();

        nfloat segmentSize = (nfloat)(360.0 / (nfloat)this.Segments);
        nfloat startSegAngle = 0;
        nfloat endSegAngle = startSegAngle + segmentSize;

        if (this.Segments > 1) 
            var fromSeg = (nint)((((double)this.Segments) * this.StartAngle) / 360.0);
            var toSeg = (nint)((((double)this.Segments) * this.EndAngle) / 360.0);
            for (var seg = 0; seg < this.Segments; seg++) 
                var hiddenLayer = !(seg >= fromSeg && seg < toSeg);
                if (!hiddenLayer) 
                    path.AppendPath(
                        this.CreateSegmentPath(
                            startSegAngle, endSegAngle - this.Margin));
                
                startSegAngle += segmentSize;
                endSegAngle += segmentSize;
            
         else if (this.Segments == 1) 
            path.AppendPath(this.CreateSegmentPath(this.StartAngle, this.EndAngle));
        
        return path;
    

    private UIBezierPath CreateSegmentPath(nfloat startSegAngle, nfloat endSegAngle) 
        var center = new CGPoint(x: this.Bounds.Width / 2f, y: this.Bounds.Height / 2f);
        var radius = (nfloat)Math.Max(this.Bounds.Width, this.Bounds.Height) / 2f - this.LineWidth / 2f;

        var path = UIBezierPath.FromArc(
            center,
            radius,
            Deg2Rad(startSegAngle - 90f),
            Deg2Rad(endSegAngle - 90f),
            true);

        path.MoveTo(center);
        path.ClosePath();
        path.Stroke();

        return path;
    

    private static nfloat Deg2Rad(nfloat value) 
        return (nfloat)(floatPI / 180.0 * value);
    

    private static readonly nfloat floatPI = (nfloat)Math.PI;

    private nint segments;
    private nfloat margin;


    [DesignTimeVisible(true)]
public partial class ArcSegmentView : UIView 
    public ArcSegmentView(IntPtr handle) : base(handle) 
        this.strokeColor = UIColor.Black.CGColor;
    

    #region Properties

    [Export("StartAngle"), Browsable(true)]
    public nfloat StartAngle 
        get  return startAngle; 
        set 
            if (startAngle != value) 
                startAngle = value;
                ((ArcSegmentLayer)this.Layer).StartAngle = (float)value;
                this.SetNeedsDisplay();
            
        
    

    [Export("EndAngle"), Browsable(true)]
    public nfloat EndAngle 
        get  return endAngle; 
        set 
            if (endAngle != value) 
                endAngle = value;
                ((ArcSegmentLayer)this.Layer).EndAngle = (float)value;
                this.SetNeedsDisplay();
            
        
    

    [Export("Segments"), Browsable(true)]
    public nint Segments 
        get  return segments; 
        set 
            if (segments != value) 
                segments = value;
                ((ArcSegmentLayer)this.Layer).Segments = value;
                this.SetNeedsDisplay();
            
        
    

    [Export("Margin"), Browsable(true)]
    public nfloat Margin 
        get  return margin; 
        set 
            if (margin != value) 
                margin = value;
                ((ArcSegmentLayer)this.Layer).Margin = value;
                this.SetNeedsDisplay();
            
        
    

    [Export("LineWidth"), Browsable(true)]
    public nfloat LineWidth 
        get  return lineWidth; 
        set 
            if (lineWidth != value) 
                lineWidth = value;
                ((ArcSegmentLayer)this.Layer).LineWidth = value;
                this.SetNeedsDisplay();
            
        
    

    [Export("StrokeColor"), Browsable(true)]
    public CGColor StrokeColor 
        get  return strokeColor; 
        set 
            if (StrokeColor != value) 
                strokeColor = value;
                ((ArcSegmentLayer)this.Layer).StrokeColor = value;
                //this.SetNeedsDisplay();
            
        
    

    #endregion

    [Export("layerClass")]
    static Class LayerClass() 
        return new Class(typeof(ArcSegmentLayer));
    

    private nfloat lineWidth;
    private nfloat margin;
    private nint segments;
    private nfloat startAngle;
    private nfloat endAngle;
    private CGColor strokeColor;


public partial class ViewController : UIViewController 
    protected ViewController(IntPtr handle) : base(handle)  

    public override void ViewDidLoad() 
        base.ViewDidLoad();

        arcSegment.StartAngle = 45;
        arcSegment.EndAngle = 90;
        arcSegment.Margin = 2;
        arcSegment.StrokeColor = UIColor.Red.CGColor;
        arcSegment.Segments = 70;
        arcSegment.LineWidth = 10;

        CABasicAnimation animation = CABasicAnimation.FromKeyPath("EndAngle");
        animation.TimingFunction = CAMediaTimingFunction.FromName(CAMediaTimingFunction.Linear);
        animation.From = new NSNumber(45);
        animation.To = new NSNumber(360);
        animation.Duration = 10;

        arcSegment.Layer.AddAnimation(animation, "EndAngle");
    

【问题讨论】:

这不适用于Xamarin.iOS,因为不支持@dynamic 属性:bugzilla.xamarin.com/show_bug.cgi?id=38823 【参考方案1】:

我们有一个示例说明如何执行此操作:

https://github.com/xamarin/ios-samples/tree/master/CustomPropertyAnimation

特别是:

https://github.com/xamarin/ios-samples/blob/master/CustomPropertyAnimation/AppDelegate.cs

【讨论】:

以上是关于使用 Xamarin 的 CALayer 自定义属性动画的主要内容,如果未能解决你的问题,请参考以下文章

动画边界更改时具有 CALayer 有线效果的自定义视图

动画边界更改时具有 CALayer 有线效果的自定义视图

如何让我的 UIView 使用我的自定义 CALayer 作为其支持层?

自定义 CALayer 子类未显示

在自定义属性更改时重绘自定义 CALayer 子类

为啥动画自定义 CALayer 属性会导致其他属性在动画期间为零?