iOS

如何利用Swift中的IBDesignable创建一个漂亮的可复

2018-11-26  本文已影响21人  Uncle丶shuai

如何利用Swift中的IBDesignable创建一个漂亮的可复用的渐变View

image

接下来讲述如何用swift4创建一个通用的并且带有@IBDesignable的gradient View类, 你可以在stroyboard中直接拖拽CAGradientView并且能及时看到效果,或者你也可以通过代码的形式添加。然后设置开始和结束两个点对应的色值已经颜色渐变的方向,当然,这些属性可以在XIB中直接控制。

为什么需要这个

不可否认的是设计师喜欢渐变图层,类似的还有毛玻璃背景以及阴影,时尚流行的风格不断变化,现如今,他们对这些更加敏感,所以他们需要大量的调试来使效果变得更好。

创建一个渐变图层需要一定量的工作,不断的调试,直到设计师满意为止。这是一个很费时间的过程,所以,本文将告诉你如何通过storyboard直接拖拽一个渐变图层,并且调试完直接显示出效果。

这样的话,你将省掉很多重复编译运行的时间。

接下来要做什么

我们先列举需要注意的事情

获取Demo工程

运行工程,在storyboard中找到ViewController对应的XIB,你将可以Inspector中编辑对应的属性值,效果就像上面放了一张图片一样。

image

关于渐变图层

注意: 这不是CAGradientLayer的文档,如果你想了解更多基础的介绍,可以参考这篇文章 Mastering CAGradientLayer in Swift

iOS中实现渐变效果的方式有很多,本文主要使用CALayer的子类CAGradientLayer,这也是核心动画中视图层级结构中的一个重要对象。在iOS中,UIView的内容显示主要是layer层来控制,每一个UIView对象都一个对应的layer,同时,就像每一个UIView对象都可以有多个subView对象一样,每一个layer对象也可以有多个sublayer对象。

而实际应用过程中,我们就是将这些错综复杂的sublayers添加到响应的View上。在深入了解Core Animation的时候,开发者同样需要知道怎样在layer层实现与View层同样的效果。通常,Views和layers的区别主要在事件响应上,而一般app都需要这些具备交互功能性的控件,比如说 UILabel, UIButton。

当我们创建一些精致的图形时,layer的层级关系就会变得很复杂,我们最好是避免让这些图层变得很复杂,毕竟这些layers不能用故事版来操作,只能通过纯代码,所以理论上来讲,处理图层将会变得很复杂。。这里将引导你将一个简单 CAGradientLayer对象作为sublayer添加到view的layer层上,这里只是一对一的关系,所以,也可以在storyboard里将layer添加到view里的效果展示出来。

定义View的子类

这里将创建LDGradientView作为UIView的子类,定义方式如下:


@IBDesignable class LDGradientView: UIView { 
  // ... 
}


@IBDesignable标记, 表明可以直接在storyboard里编辑

而渐变层本身将作为类的私有属性

// the gradient layer 
private var gradient: CAGradientLayer?

这个成员变量将通过下面的函数创建。这里设置了gradient的frame为对应View的bounds,让其填充整个View,这样就可以保证图层与视图界面的一对一关系。

// create gradient layer 
private func createGradient() -> CAGradientLayer { 
  let gradient = CAGradientLayer() 
  gradient.frame = self.bounds
  return gradient 
}

然后将创建的gradient layer作为子视图添加到对应View的layer层上。

// Create a gradient and install it on the layer 
private func installGradient() { 
  // if there's already a gradient installed on the layer, remove it
  if let gradient = self.gradient {
    gradient.removeFromSuperlayer()
  } 
  let gradient = createGradient()
  self.layer.addSublayer(gradient)
  self.gradient = gradient
}

这些都是私有函数,因为view的layer层级应当由它自己来处理。

如果你在一个很复杂的图层上创建gradient或者父视图使用了约束,那么每次在设置view的frame的时候都需要刷新一下它里面的子图层,代码如下:

override var frame: CGRect {
  didSet {
    updateGradient()
  }
}
override func layoutSubviews() {
  super.layoutSubviews()
  // this is crucial when constraints are used in superviews
  updateGradient()
}
// Update an existing gradient
    private func updateGradient() {
        if let gradient = self.gradient {
            let startColor = self.startColor ?? UIColor.clear
            let endColor = self.endColor ?? UIColor.clear
            gradient.colors = [startColor.cgColor, endColor.cgColor]
            let (start, end) = gradientPointsForAngle(self.angle)
            gradient.startPoint = start
            gradient.endPoint = end
            gradient.frame = self.bounds
        }
    }

最后,我们也需要初始化方法来创建渐变图层,而实现初始化方法主要通过代码和故事版两种创建方式。

// initializers 
required init?(coder aDecoder: NSCoder) {
  super.init(coder: aDecoder)
  installGradient() 
} 
override init(frame: CGRect) {
  super.init(frame: frame)
  installGradient() 
}

定义一个渐变层 Gradient

现在只是有了一个能创建CAGradientLayer的UIView的子类,这还不能满足我们所有的需求,所以就需要定义一下gradient来满足需求。

CAGradientLayer主要有两个重要的属性需要外界控制。

定义颜色

先给CAGradientLayer添加一个colors属性

// An array of CGColorRef objects defining the color of each gradient stop. Animatable.
var colors: [Any]?

渐变层的控制点

控制颜色变化的点称为颜色站点(gradient stops)。 渐变层可以有很多个颜色站点来支持复杂的图形变化,
而颜色站点添加的过程会十分的繁琐,而站点的数量又不定,所以解决起来很困难。在代码里直接一点点的重复调试作用效果不明显。也正是因为这样的原因,这里将创建一个开始为一种颜色结束为另一种颜色的"简单"图层。当然你也可以自己添加其它的颜色站点。

所以 colors属性的实现很简单:

// the gradient start colour 
@IBInspectable var startColor: UIColor? 
// the gradient end colour 
@IBInspectable var endColor: UIColor?

这些属性也可以再XIB上控制

定义方向

渐变层的变化方向主要由CAGradientLayer的两个属性控制

// 结束点
var endPoint: CGPoint
// 开始点
var startPoint: CGPoint

gradient的开始结束点定义在渐变空间单元(unit gradient space)

这就意味着无论给定的CAGradientLayer对象的大小是多少,我们都可以用下面的草图来表示。我们假定左上角的坐标为(0, 0), 右下角的坐标为(1, 1)

<div align=center> image

<div align=left>

用XIB来控制graident方向是一件很费劲的事情,因为 @IBInspectable attibuetes不支持CGPoint类型,但也不意味着完全没有数据来支持UI,只是我们的选择会有限制。最终在XIB上我们使用弧度来代替CGPoint。

//逆时针方向 从0开始
@IBInspectable var angle: CGFloat = 270

这里是270作为CAGradientLayer的默认渐变方向,方向为从下往上。如果想要设置为水平方向,那么angle的值可以设置成0或180

将angle转换成CGPoint

private func gradientPointsForAngle(_ angle: CGFloat) -> (CGPoint, CGPoint) {
// 获取方向的开始点
  let end = pointForAngle(angle)
  let start = oppositePoint(end)
  // convert to gradient space
  let p0 = transformToGradientSpace(start)
  let p1 = transformToGradientSpace(end)
  return (p0, p1)
  }

弧度指定了颜色渐变的方向从0度开始,0度为向右的方向,按逆时针方向递增,如图所示

<div align=center> image
<div align=left>

获取颜色渐变方向的结束点

private func pointForAngle(_ angle: CGFloat) -> CGPoint {
  // 将度数转换成弧度
  let radians = angle * .pi / 180.0
  var x = cos(radians)
  var y = sin(radians)
  // (x,y) 在单位圆内. 
  if (fabs(x) > fabs(y)) {
    // 假设x为单位长度
    x = x > 0 ? 1 : -1 y = x * tan(radians)
  } else {
    // 假设y为单位长度
    y = y > 0 ? 1 : -1
    x = y / tan(radians)
  } 
  return CGPoint(x: x, y: y) 
}

这么做看起来很复杂,主要通过sine和cosine函数,swift中的三角函数和其它语言中一样,需要的是弧度而不是角度,假设我们知道弧度,那么x = cos(radians)y = sin(radians)。接下来只要知道我们关心的点是否在我们的单位圆圈里,而如图所示,无论是0,90,180,270,点都在圈内

<div align=center> image

<div align=left>

在我们指定单位矩形内拿到结束点以后,找到开始点就很容易了,只需要把结束的点沿着y=x翻转一下就可以了

private func oppositePoint(_ point: CGPoint) -> CGPoint {
  return CGPoint(x: -point.x, y: -point.y) 
}

现在有了开始和结束的点,剩下的就是将它们转换到渐变空间里,单位渐变空间里有它自己的Y轴,这根Core Animation 里的维度一样。所以,上面所说的坐标点(0, 0)在新的坐标系里就是(0.5, 0.5)

private func transformToGradientSpace(_ point: CGPoint) -> CGPoint {
  // 输入的点从: (-1,-1) 到 (1,1)
  return CGPoint(x: (point.x + 1) * 0.5, y: 1.0 - (point.y + 1) * 0.5) 
}

支持界面调试

剩下的就是实现prepareForInterfaceBuilder()函数,这个方法仅仅通过XIB调试需要重新渲染界面的时候执行

override func prepareForInterfaceBuilder() { 
  super.prepareForInterfaceBuilder()
  installGradient()
  updateGradient() 
}

上一篇 下一篇

猜你喜欢

热点阅读