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

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

image

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

为什么需要这个

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

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

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

接下来要做什么

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

  • 需要继承 UIView
  • 需要实现 @IBDesignable 功能,这样的话就可以通过storyboard直接调试并显示出效果
  • 需要兼容纯代码或者XIB两种创建方式

获取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() 
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容