2016-06-05 4 views
1

보기에서 두 개의 모서리 만 둥글게 만들고 그라디언트를 사용해야하는 경우가 있습니다. 나는 CALayerMask를 사용하는 일반적인 해결책이 성능에 해롭다는 것을 발견했다. 그래서 나는 drawRect(rect: CGRect)을 무시하는 내 자신의 솔루션을 고안했다. 이 도구는 잘 작동하여 일부 또는 모든 모서리를 둥글게하고 테두리를 그리고 그라디언트의 색상 정지 점을 설정할 수있는 경우에도 선형 및 방사형 그래디언트 채우기를 모두 사용하는 쉬운 방법을 제공합니다.내 사용자 정의 UIView의 속성을 어떻게 애니메이트 할 수 있습니까?

불행히도 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()} 
    } 
} 

답변

0

당신은 다음에 두 개의 코너 반경 마스크를 추가합니다 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]; 
+0

가능한 경우 덜 자세한 UIView.animateWithDuration을 사용하고 싶습니다. – GoldenJoe

+0

UIView 변형도 작동해야한다고 생각합니다. 왜 시도하지 않습니까? –

관련 문제