【问题标题】:How can I animate the properties of my custom UIView?如何为自定义 UIView 的属性设置动画?
【发布时间】:2016-06-05 04:36:47
【问题描述】:

我经常需要在视图中只对两个角进行圆角处理,有时还需要使用渐变。我发现使用 CALayerMask 的常见解决方案不利于性能,因此我设计了自己的解决方案来覆盖 drawRect(rect: CGRect)。它运行良好,提供了一种简单的方法来圆化部分或所有角、绘制边框、使用线性和径向渐变填充,甚至可以为渐变设置色标。

不幸的是,当我尝试使用 UIView.animateWithDuration 为这些属性设置动画时,我的角、渐变和边框没有动画。相反,它们在初始状态下看起来“拉伸”,然后动画到最终状态。我读过这可以用 CALayer 动画解决,但我不太清楚问题的性质。有没有办法像现在这样解决这个问题?如果不是,什么时候drawRect(rect: CGRect)drawLayer(layer: CALayer, inContext ctx: CGContext) 更可取?

我也愿意接受有关改进本课程的一般性建议。

AppocalypseUI.swift(提供UI操作的支持函数)

//
//  AppocalypseUI.swift
//  Soapbox
//
//  Created by Joseph Falcone on 6/2/16.
//  Copyright © 2016 Joseph Falcone. All rights reserved.
//

import UIKit

class AppocalypseUI: NSObject
{
    /// Generates an array of CGFloat values ranging from 0.0-1.0 which represent the color stops in a gradient
    class func makeLinearColorStops(numStops:Int) -> [CGFloat]
    {
        assert(numStops >= 2, "Must have at least two color stops.")

        let stepIncrement = 1.0/Double(numStops-1)
        var returnArr : [CGFloat] = []

        // The first stop is always 0
        returnArr += [0.0]

        for i in 1 ..< numStops-1
        {
            let stepVal = stepIncrement*Double(i)
            let stepFactor = CGFloat(fmod(stepVal, 1.0))
            returnArr += [stepFactor]
        }

        // The last stop is always 1
        returnArr += [1.0]

        // Fini
        return returnArr
    }

    /// Returns the stop colors in an array
    class func colorsAlongArray(colorArr:[UIColor], steps:Int) -> [UIColor]
    {
        let arrCount = colorArr.count
        let stepIncrement = Double(arrCount)/Double(steps)
        var returnArr : [UIColor] = []

        for i in 0..<steps
        {
            let stepVal = stepIncrement*Double(i)
            let stepFactor = CGFloat(fmod(stepVal, 1.0))
            let stepIndex1 = Int(floor(stepVal/1.0))
            var stepIndex2 = Int(ceil(stepVal/1.0))

            if(stepIndex2 > arrCount-1)
                {stepIndex2 = arrCount-1}

            let color1 = colorArr[stepIndex1]
            let color2 = colorArr[stepIndex2]

            let color = colorByInterpolatingColors(color1, color2: color2, factor: stepFactor)
            returnArr += [color]
        }

        return returnArr
    }

    /// Returns a color between two colors on a gradient
    class func colorByInterpolatingColors(color1:UIColor, color2:UIColor, factor:CGFloat) -> UIColor
    {
        let startComponent = CGColorGetComponents(color1.CGColor)
        let endComponent = CGColorGetComponents(color2.CGColor)

        let startAlpha = CGColorGetAlpha(color1.CGColor)
        let endAlpha = CGColorGetAlpha(color2.CGColor)

        let r = startComponent[0] + (endComponent[0] - startComponent[0]) * factor
        let g = startComponent[1] + (endComponent[1] - startComponent[1]) * factor
        let b = startComponent[2] + (endComponent[2] - startComponent[2]) * factor
        let a = startAlpha + (endAlpha - startAlpha) * factor

        return UIColor(red: r, green: g, blue: b, alpha: a)
    }

    /* No longer needed
    class func getFloatArrayFromNSNumbers(numbers:[NSNumber]) -> [CGFloat]
    {
        var returnArr : [CGFloat] = []

        for number in numbers
        {
            returnArr += [CGFloat(number.floatValue)]
        }

        return returnArr
    }
    */

    /// Returns an array containing the RGBA components of an array of colors
    class func getFloatArrayFromUIColors(colors:[UIColor]) -> [CGFloat]
    {
        var returnArr : [CGFloat] = []
        for color : UIColor in colors
        {
            var red   : CGFloat = 0.0
            var green : CGFloat = 0.0
            var blue  : CGFloat = 0.0
            var alpha : CGFloat = 0.0

            color.getRed(&red, green: &green, blue: &blue, alpha: &alpha)

            /*
            // This check and backup should probably be implemented later, but it seems to fail when it shouldn't...probably improper use of optionals
            if(color?.getRed(&red, green: &green, blue: &blue , alpha: &alpha) == nil)
            {
                // If for some reason the above function call fails, try this method of getting RGBA instead
                let components = CGColorGetComponents(color?.CGColor)
                red     = components[0]
                green   = components[1]
                blue    = components[2]
                alpha   = components[3]
            }
            */

            returnArr += [red, green, blue, alpha]
        }
        return returnArr
    }

    /// Returns a path for a rectangle with rounded corners
    class func newPathForRoundedRect(rect:CGRect, radiusTL radTL:CGFloat, radiusTR radTR:CGFloat, radiusBL radBL:CGFloat, radiusBR radBR:CGFloat, edges:UIRectEdge = .All) -> CGPathRef
    {
        let retPath = CGPathCreateMutable()

        // Convenience
        let rectL = rect.origin.x
        let rectR = rect.origin.x+rect.size.width
        let rectT = rect.origin.y
        let rectB = rect.origin.y+rect.size.height

        // Starting from the top left arc, move clockwise
        let p1 = CGPointMake(rectL      , rectT+radTL)
        let p2 = CGPointMake(rectL+radTL, rectT)
        let p3 = CGPointMake(rectR-radTR, rectT)
        let p4 = CGPointMake(rectR      , rectT+radTR)
        let p5 = CGPointMake(rectR      , rectB-radBR)
        let p6 = CGPointMake(rectR-radBR, rectB)
        let p7 = CGPointMake(rectL+radBL, rectB)
        let p8 = CGPointMake(rectL      , rectB-radBL)

        let c1 = CGPointMake(rect.origin.x                  , rect.origin.y)
        let c2 = CGPointMake(rect.origin.x+rect.size.width  , rect.origin.y)
        let c3 = CGPointMake(rect.origin.x+rect.size.width  , rect.origin.y+rect.size.height)
        let c4 = CGPointMake(rect.origin.x                  , rect.origin.y+rect.size.height)

        if(edges.contains(.All) || (edges.contains(.Left) && edges.contains(.Right) && edges.contains(.Top) && edges.contains(.Bottom)))
        {
            CGPathMoveToPoint(retPath, nil, p1.x, p1.y)

            CGPathAddArcToPoint (retPath, nil, c1.x, c1.y, p2.x, p2.y, radTL)
            CGPathAddLineToPoint(retPath, nil, p3.x, p3.y)

            CGPathAddArcToPoint (retPath, nil, c2.x, c2.y, p4.x, p4.y, radTR)
            CGPathAddLineToPoint(retPath, nil, p5.x, p5.y)

            CGPathAddArcToPoint (retPath, nil, c3.x, c3.y, p6.x, p6.y, radBR)
            CGPathAddLineToPoint(retPath, nil, p7.x, p7.y)

            CGPathAddArcToPoint (retPath, nil, c4.x, c4.y, p8.x, p8.y, radBL)
            CGPathAddLineToPoint(retPath, nil, p1.x, p1.y)

            CGPathCloseSubpath(retPath)

            return retPath
        }

        if(edges.contains(.Top))
        {
            CGPathMoveToPoint(retPath, nil, p1.x, p1.y)
            CGPathAddArcToPoint (retPath, nil, c1.x, c1.y, p2.x, p2.y, radTL)
            CGPathAddLineToPoint(retPath, nil, p3.x, p3.y)
            CGPathAddArcToPoint (retPath, nil, c2.x, c2.y, p4.x, p4.y, radTR)
        }

        if(edges.contains(.Right))
        {
            CGPathMoveToPoint(retPath, nil, p3.x, p3.y)
            CGPathAddArcToPoint (retPath, nil, c2.x, c2.y, p4.x, p4.y, radTR)
            CGPathAddLineToPoint(retPath, nil, p5.x, p5.y)
            CGPathAddArcToPoint (retPath, nil, c3.x, c3.y, p6.x, p6.y, radBR)
        }

        if(edges.contains(.Bottom))
        {
            CGPathMoveToPoint(retPath, nil, p5.x, p5.y)
            CGPathAddArcToPoint (retPath, nil, c3.x, c3.y, p6.x, p6.y, radBR)
            CGPathAddLineToPoint(retPath, nil, p7.x, p7.y)
            CGPathAddArcToPoint (retPath, nil, c4.x, c4.y, p8.x, p8.y, radBL)
        }

        if(edges.contains(.Left))
        {
            CGPathMoveToPoint(retPath, nil, p7.x, p7.y)
            CGPathAddArcToPoint (retPath, nil, c4.x, c4.y, p8.x, p8.y, radBL)
            CGPathAddLineToPoint(retPath, nil, p1.x, p1.y)
            CGPathAddArcToPoint (retPath, nil, c1.x, c1.y, p2.x, p2.y, radTL)
        }

        return retPath
    }
}

JFStylishView.swift

//
//  JFStylishView.swift
//  Soapbox
//
//  Created by Joseph Falcone on 6/2/16.
//  Copyright © 2016 Joseph Falcone. All rights reserved.
//

import UIKit

enum GradientType
{
    case Linear
    case Radial
}

private enum BackgroundFillType
{
    case Solid
    case Gradient
}

class JFStylishView : UIView
{
    // Rounded Corners
    var cornerTL : CGFloat = 0.0
    var cornerTR : CGFloat = 0.0
    var cornerBR : CGFloat = 0.0
    var cornerBL : CGFloat = 0.0

    // Border
    var borderWidth : CGFloat = 4.0
    var borderColor = UIColor.greenColor()

    // Colors
    private var trueBackgroundColor = UIColor.clearColor() // The backgroundColor property has to be clear so that the layer doesn't draw behind the clipping area, so we use this to track what the user wants
    private var bgColors : [CGFloat] = [] // array of colors used in drawrect

    // Gradient points
    private var gradientStart   = CGPointMake(0.5, 0.0)
    private var gradientEnd     = CGPointMake(0.5, 1.0)
    private var gradientColorStops : [CGFloat] = []

    // Gradient type
    private var gradientType : GradientType = .Linear

    // Background Mode
    private var backgroundFillType : BackgroundFillType = .Solid

//    var shadowLayer: CAShapeLayer! // Not ready for this yet

    // MARK: Initialization

    override init(frame: CGRect)
    {
        super.init(frame:frame)
    }

    required init?(coder aDecoder: NSCoder)
    {
        super.init(coder:aDecoder)
    }

    override func awakeFromNib()
    {
        super.awakeFromNib()
    }

    func initStylishStuff()
    {
        cornerTL = 0.0

    }

    // MARK: Color

    private func getFillType() -> BackgroundFillType
    {
        // Rather than keeping a variable for this that gets set everywhere, we'll just use this getter to figure out what type we are using.
        // Of course, if I get sloppy and don't make the unused elements empty when setting another fill parameter, this could produce a bug.

        // RULES
        // If using a gradient, trueBackgroundColor will be clear
        // If using solid, bgColors will be empty
        // If patterns are ever added, the above will be empty

        if(bgColors.count == 0)
        {return .Solid}

        if(trueBackgroundColor == UIColor.clearColor())
        {return .Gradient}

        // Default
        return .Solid
    }

    override var backgroundColor: UIColor?
    {
        get
        {
            return trueBackgroundColor
        }
        set
        {
            trueBackgroundColor = backgroundColor!
            super.backgroundColor = UIColor.clearColor()
            //bgColorArr = []
            bgColors = []
            backgroundFillType = .Solid
        }

        /*
        // Property observer - whenever the background color is
        didSet
        {
            bgColorArr = []
            bgColors = []
//            bgColorArr = [backgroundColor!]
//            bgColors = AppocalypseUI.getFloatArrayFromUIColors([backgroundColor!, backgroundColor!])
        }
        */
    }

//    // Convenient...maybe we shouldn't include this?
//    func setBackgroundGradient(topColor:UIColor, bottomColor:UIColor)
//    {
//        bgColorArr = [topColor, bottomColor]
//        bgColors = AppocalypseUI.getFloatArrayFromUIColors([topColor, bottomColor])
//    }

    // Default is linear, top to bottom
    // startPoint, endPoint should be coordinates of 0.0-1.0
    func setBackgroundGradient(colors:[UIColor], stops:[CGFloat]? = nil, startPoint:CGPoint?=nil, endPoint:CGPoint?=nil, type:GradientType = .Linear)
    {
        assert(colors.count > 1, "At least two colors must be specified.")

        // We won't be using the backgroundColor property when drawing a gradient
        trueBackgroundColor = UIColor.clearColor()

        // Calculate the stops if they were not specified
        var stops = stops // arguments are immutable, but we can declare a variable with the same name
        if(stops == nil)
        {
            stops = AppocalypseUI.makeLinearColorStops(colors.count)
        }

        // Provide default start and end points if necessary
        gradientType = type
        switch type
        {
            case .Linear: // top to bottom
                gradientStart  = startPoint == nil ? CGPointZero : startPoint!
                gradientEnd    = endPoint == nil ? CGPointMake(0, 1.0) : endPoint!
            case .Radial: // center to top
                gradientStart  = startPoint == nil ? CGPointMake(0.5, 0.5) : startPoint!
                gradientEnd    = endPoint == nil ? CGPointMake(0.5, 0) : endPoint!
        }

        assert(colors.count == stops?.count, "The number of colors and stops must be equal.")

        //bgColorArr = colors
        bgColors = AppocalypseUI.getFloatArrayFromUIColors(colors)
        gradientColorStops = stops!
    }


    /*
    override func layoutSubviews()
    {
        super.layoutSubviews()

        if shadowLayer == nil
        {
            shadowLayer = CAShapeLayer()
            shadowLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: 12).CGPath
            //shadowLayer.fillColor = UIColor.whiteColor().CGColor
            shadowLayer.fillColor = UIColor.clearColor().CGColor

            shadowLayer.shadowColor = UIColor.darkGrayColor().CGColor
            shadowLayer.shadowPath = shadowLayer.path
            shadowLayer.shadowOffset = CGSize(width: 2.0, height: 2.0)
            shadowLayer.shadowOpacity = 0.8
            shadowLayer.shadowRadius = 2

            //layer.insertSublayer(shadowLayer, atIndex: 0)
            layer.insertSublayer(shadowLayer, below: nil) // also works
        }        
    }
    */

    // MARK: Drawing

//    override func drawLayer(layer: CALayer, inContext ctx: CGContext) {
//        
//    }

    override func drawRect(rect: CGRect)
    {
        // Get the current context
        let context = UIGraphicsGetCurrentContext()

        // Make the background gradient
        let baseSpace = CGColorSpaceCreateDeviceRGB();
        let gradient = CGGradientCreateWithColorComponents(baseSpace, bgColors, gradientColorStops, gradientColorStops.count);

        // Set the border color and stroke
        CGContextSetLineWidth(context, borderWidth);
        CGContextSetStrokeColorWithColor(context, borderColor.CGColor);

        // Fill in the background, inset by the border

        let bgRect      = CGRectMake(bounds.origin.x+borderWidth  , bounds.origin.y+borderWidth  , bounds.size.width-borderWidth*2, bounds.size.height-borderWidth*2);
        let borderRect  = CGRectMake(bounds.origin.x+borderWidth/2, bounds.origin.y+borderWidth/2, bounds.size.width-borderWidth  , bounds.size.height-borderWidth);

        let bgPath      = AppocalypseUI.newPathForRoundedRect(bgRect, radiusTL: cornerTL, radiusTR: cornerTR, radiusBL: cornerBL, radiusBR: cornerBR)
        let borderPath  = AppocalypseUI.newPathForRoundedRect(borderRect, radiusTL: cornerTL, radiusTR: cornerTR, radiusBL: cornerBL, radiusBR: cornerBR)

        CGContextStrokePath(context)

        // Background
        CGContextSaveGState(context); // Saves the state from before we clipped to the path
        CGContextAddPath(context, bgPath);
        CGContextClip(context); // Makes the background fill only the path

        switch getFillType()
        {
        case .Gradient:
            let gradientStartInPoints   = CGPointMake(gradientStart.x*bounds.size.width, gradientStart.y*bounds.size.height);
            let gradientEndInPoints     = CGPointMake(gradientEnd.x*bounds.size.width, gradientEnd.y*bounds.size.height);
            switch(gradientType)
            {
            case .Linear:
                CGContextDrawLinearGradient(context, gradient, gradientStartInPoints, gradientEndInPoints, []); // Draw a vertical gradient

            case .Radial:
                // A radial gradient might not fill the layer...first, fill it with the end color
                UIColor(red: bgColors[bgColors.count-4], green: bgColors[bgColors.count-3], blue: bgColors[bgColors.count-2], alpha: bgColors[bgColors.count-1]).setFill()
                CGContextAddPath(context, bgPath); // Not sure why I need this...TODO: Investigate
                CGContextFillPath(context)

                let endRadius = hypot(gradientStartInPoints.x-gradientEndInPoints.x, gradientStartInPoints.y-gradientEndInPoints.y)
                CGContextDrawRadialGradient(context, gradient, gradientStartInPoints, 0, gradientStartInPoints, endRadius, [])
            }
        case .Solid:
            trueBackgroundColor.setFill()
            CGContextFillPath(context)
        }

        CGContextRestoreGState(context); // Now we are no longer clipped to the path

        // Border
        CGContextAddPath(context, borderPath);
        CGContextStrokePath(context);
    }

    // MARK: Convenience

    func removeAllSubviews()
    {
        for view in subviews
            {view.removeFromSuperview()}
    }
}

【问题讨论】:

    标签: swift calayer drawrect


    【解决方案1】:

    您可以创建一个 CAGradientLayer,然后添加只有两个角半径的遮罩。然后您可以使用 Core Animation 为变换设置动画,或者如果您将图层放在 UIView 中,UIView 变换动画也应该可以工作。

    CAGradientLayer * rectangleGradient = [CAGradientLayer layer];
    rectangleGradient.colors             = @[(id)[UIColor greenColor].CGColor, (id)[UIColor orangeColor].CGColor];
    rectangleGradient.startPoint         = CGPointMake(0.5, 0);
    rectangleGradient.endPoint           = CGPointMake(0.5, 1);
    rectangleMask.path                   = maskPath;
    
    ////The animation
        CABasicAnimation * theTransformAnim = [CABasicAnimation animationWithKeyPath:@"transform"];
        theTransformAnim.fromValue          = [NSValue valueWithCATransform3D:CATransform3DIdentity];;
        theTransformAnim.toValue            = [NSValue valueWithCATransform3D:CATransform3DMakeScale(2, 2, 1)];;
        theTransformAnim.duration           = 1;
    
    [rectangleGradient addAnimation:theTransformAnim];
    

    【讨论】:

    • 如果可能的话,我宁愿使用不那么冗长的 UIView.animateWithDuration。
    • 我认为如果你对 UIView 进行动画变换也应该可以工作,你为什么不试试呢?
    猜你喜欢
    • 2014-04-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多