動(dòng)畫(huà)示例(十) —— 一種彈性動(dòng)畫(huà)的實(shí)現(xiàn) (一)

版本記錄

版本號(hào) 時(shí)間
V1.0 2018.08.26

前言

如果你細(xì)看了我前面寫(xiě)的有關(guān)動(dòng)畫(huà)的部分,就知道前面介紹了CoreAnimation、序列幀以及LOTAnimation等很多動(dòng)畫(huà)方式,接下來(lái)幾篇我們就以動(dòng)畫(huà)示例為線索,進(jìn)行動(dòng)畫(huà)的講解。相關(guān)代碼已經(jīng)上傳至GitHub - 刀客傳奇。感興趣的可以看我寫(xiě)的前面幾篇。
1. 動(dòng)畫(huà)示例(一) —— 一種外擴(kuò)的簡(jiǎn)單動(dòng)畫(huà)
2. 動(dòng)畫(huà)示例(二) —— 一種抖動(dòng)的簡(jiǎn)單動(dòng)畫(huà)
3. 動(dòng)畫(huà)示例(三) —— 仿頭條一種LOTAnimation動(dòng)畫(huà)
4. 動(dòng)畫(huà)示例(四) —— QuartzCore之CAEmitterLayer下雪??動(dòng)畫(huà)
5. 動(dòng)畫(huà)示例(五) —— QuartzCore之CAEmitterLayer煙花動(dòng)畫(huà)
6. 動(dòng)畫(huà)示例(六) —— QuartzCore之CAEmitterLayer、CAReplicatorLayer和CAGradientLayer簡(jiǎn)單動(dòng)畫(huà)
7. 動(dòng)畫(huà)示例(七) —— 基于CAShapeLayer圖像加載過(guò)程的簡(jiǎn)單動(dòng)畫(huà)(一)
8. 動(dòng)畫(huà)示例(八) —— UIViewController間轉(zhuǎn)場(chǎng)動(dòng)畫(huà)的實(shí)現(xiàn) (一)
9. 動(dòng)畫(huà)示例(九) —— 一種復(fù)雜加載動(dòng)畫(huà)的實(shí)現(xiàn) (一)

開(kāi)始

首先我們看一個(gè)效果

每個(gè)好一點(diǎn)的iOS應(yīng)用程序都有自定義元素,自定義UI,自定義動(dòng)畫(huà)等。自定義,自定義,自定義!

如果您希望自己的應(yīng)用能夠脫穎而出,那么您必須花時(shí)間添加一些獨(dú)特的功能,這些功能將為您的應(yīng)用提供WOW因素。

在本教程中,您將構(gòu)建一個(gè)自定義text field,在其被輕敲時(shí)可以生成一個(gè)可愛(ài)的小彈性反彈動(dòng)畫(huà)。

在此過(guò)程中,您將使用許多有趣的API:

  • 1)CAShapeLayer
  • 2)CADisplayLink
  • 3)UIView spring animations
  • 4)IBInspectable

下面我們就開(kāi)始了。

新建立一個(gè)工程,該項(xiàng)目基于Single View Application iOS\Application\Single View Application。 它目前在容器視圖中有兩個(gè)text field和一個(gè)按鈕。

你的目標(biāo)是在獲得焦點(diǎn)時(shí)給予彈性彈跳。 你說(shuō),你是如何實(shí)現(xiàn)這一目標(biāo)的?

技術(shù)很簡(jiǎn)單:您將使用四個(gè)控制點(diǎn)視圖和一個(gè)CAShapeLayer,然后使用UIView spring animations為控制點(diǎn)設(shè)置動(dòng)畫(huà)。 當(dāng)它們正在動(dòng)畫(huà)時(shí),你會(huì)在它們的位置周?chē)匦吕L制形狀。

如果這聽(tīng)起來(lái)有點(diǎn)復(fù)雜,請(qǐng)不要擔(dān)心! 它比你想象的容易。


Create a Base Elastic View - 創(chuàng)建一個(gè)彈性視圖

首先,您將創(chuàng)建基本彈性視圖,你將它作為子視圖嵌入到UITextfield中,你將為這個(gè)視圖設(shè)置動(dòng)畫(huà),讓你的控件彈性反彈。

右鍵單擊項(xiàng)目導(dǎo)航器中的ElasticUI組,然后選擇New File ...,然后選擇iOS / Source / Cocoa Touch Class模板。 點(diǎn)擊Next。

調(diào)用類ElasticView,在子類的字段中輸入UIView,并確保語(yǔ)言為Swift。 單擊“下一步”,然后單擊Create以選擇存儲(chǔ)與此新類關(guān)聯(lián)的文件的默認(rèn)位置。

首先,您需要?jiǎng)?chuàng)建四個(gè)控制點(diǎn)視圖和一個(gè)CAShapeLayer。 添加以下代碼,最終得到以下類定義:

import UIKit

class ElasticView: UIView {

  private let topControlPointView = UIView()
  private let leftControlPointView = UIView()
  private let bottomControlPointView = UIView()
  private let rightControlPointView = UIView()
  
  private let elasticShape = CAShapeLayer()
    
  override init(frame: CGRect) {
    super.init(frame: frame)
    setupComponents()
  }
  
  required init(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    setupComponents()
  }

  private func setupComponents() {
    
  }
}

視圖和圖層可以立即創(chuàng)建。setUpComponents()是從所有初始化路徑調(diào)用的設(shè)置方法。 你現(xiàn)在要實(shí)現(xiàn)它。

setupComponents()中添加以下內(nèi)容:

elasticShape.fillColor = backgroundColor?.CGColor
elasticShape.path = UIBezierPath(rect: self.bounds).CGPath
layer.addSublayer(elasticShape)

在這里,您要配置形狀圖層,將其填充顏色設(shè)置為與ElasticView的背景顏色相同,并將路徑填充為與視圖邊界相同的大小。 最后,將其添加到圖層層次結(jié)構(gòu)中。

接下來(lái),在`setupComponents()``的末尾添加以下代碼:

for controlPoint in [topControlPointView, leftControlPointView,
  bottomControlPointView, rightControlPointView] {
  addSubview(controlPoint)
  controlPoint.frame = CGRect(x: 0.0, y: 0.0, width: 5.0, height: 5.0)
  controlPoint.backgroundColor = UIColor.blueColor()
}

這會(huì)將所有四個(gè)控制點(diǎn)添加到您的視圖中。 為了幫助調(diào)試,這也將控制點(diǎn)的背景更改為藍(lán)色,以便在模擬器中很容易看到。 您將在本教程結(jié)束時(shí)刪除它。

您需要將控制點(diǎn)定位在頂部中心,底部中心,左中心和右側(cè)中心。 這使得當(dāng)您做遠(yuǎn)離視圖的動(dòng)畫(huà)時(shí),可以使用它們的定位在CAShapeLayer中繪制新路徑。

你需要經(jīng)常這樣做,所以創(chuàng)建一個(gè)新功能來(lái)做到這一點(diǎn)。 將以下內(nèi)容添加到ElasticView.swift

private func positionControlPoints(){
  topControlPointView.center = CGPoint(x: bounds.midX, y: 0.0)
  leftControlPointView.center = CGPoint(x: 0.0, y: bounds.midY)
  bottomControlPointView.center = CGPoint(x:bounds.midX, y: bounds.maxY)
  rightControlPointView.center = CGPoint(x: bounds.maxX, y: bounds.midY)
}

該函數(shù)將每個(gè)控制點(diǎn)移動(dòng)到視圖邊緣上的正確位置。

現(xiàn)在從setupComponents()的末尾調(diào)用new函數(shù):

positionControlPoints()

在深入了解動(dòng)畫(huà)之前,您將添加一個(gè)視圖,因此您可以看到ElasticView的工作原理。 為此,您將為sb添加新視圖。

打開(kāi)Main.storyboard,將新的UIView拖到視圖控制器的視圖中,并將其Custom Class設(shè)置為ElasticView。 不要擔(dān)心它的位置,只要它在屏幕上,你就能看到發(fā)生了什么。

Build并運(yùn)行你的項(xiàng)目

看那個(gè)! 四個(gè)小藍(lán)方塊 - 這些是您在setupComponents中添加的控制點(diǎn)視圖。

現(xiàn)在,您將使用它們?cè)?code>CAShapeLayer上創(chuàng)建路徑以獲得彈性外觀。


Drawing Shapes with UIBezierPath - 使用UIBezierPath繪制形狀

在深入研究下一系列步驟之前,請(qǐng)考慮如何在2D中繪制內(nèi)容 - 您依賴于繪制線,特別是直線和曲線。 在繪制任何內(nèi)容之前,如果要繪制直線,則需要指定起始位置和結(jié)束位置;如果要繪制更復(fù)雜的內(nèi)容,則需要指定多個(gè)位置。

這些點(diǎn)是CGPoints,您可以在當(dāng)前坐標(biāo)系中指定x和y。

當(dāng)您想要繪制基于矢量的形狀(如正方形,多邊形和復(fù)雜的曲線形狀)時(shí),它會(huì)變得更復(fù)雜一些。

為了模擬彈性效果,您將繪制一個(gè)看起來(lái)像矩形的二次Bézier曲線(quadratic Bézier),但它將為矩形的每一邊都有控制點(diǎn),這樣就可以創(chuàng)建一條曲線來(lái)創(chuàng)建彈性效果。

Bézier曲線以PierreBézier命名,他是法國(guó)工程師,曾在CAD / CAM系統(tǒng)中表示曲線。 看一下二次Bézier曲線的樣子:

藍(lán)色圓圈是您的控制點(diǎn),它們是您之前創(chuàng)建的四個(gè)視圖,紅色圓點(diǎn)是矩形的角。

注意:Apple有一個(gè)針對(duì)UIBezierPath的深入的類參考文檔。 如果您想深入了解如何創(chuàng)建路徑,那么值得一試。

現(xiàn)在是時(shí)候?qū)⒗碚摳吨T實(shí)踐了! 將以下方法添加到ElasticView.swift

 
private func bezierPathForControlPoints()->CGPathRef {
  // 1
  let path = UIBezierPath()

  // 2
  let top = topControlPointView.layer.presentationLayer().position
  let left = leftControlPointView.layer.presentationLayer().position
  let bottom = bottomControlPointView.layer.presentationLayer().position
  let right = rightControlPointView.layer.presentationLayer().position

  let width = frame.size.width
  let height = frame.size.height

  // 3
  path.moveToPoint(CGPointMake(0, 0))
  path.addQuadCurveToPoint(CGPointMake(width, 0), controlPoint: top)
  path.addQuadCurveToPoint(CGPointMake(width, height), controlPoint:right)
  path.addQuadCurveToPoint(CGPointMake(0, height), controlPoint:bottom)
  path.addQuadCurveToPoint(CGPointMake(0, 0), controlPoint: left)

  // 4
  return path.CGPath
}

這個(gè)方法做了很多事,這里進(jìn)行詳細(xì)拆解:

  • 1)創(chuàng)建一個(gè)UIBezierPath來(lái)保持你的形狀。
  • 2)將控制點(diǎn)位置提取為四個(gè)常量。 您使用presentationLayer的原因是在動(dòng)畫(huà)期間獲取視圖的“實(shí)時(shí)”位置。
  • 3)通過(guò)使用控制點(diǎn)從矩形的角到角添加曲線來(lái)創(chuàng)建路徑
  • 4)將路徑作為CGPathRef返回,因?yàn)檫@是形狀層所期望的。

在為控制點(diǎn)設(shè)置動(dòng)畫(huà)時(shí),需要調(diào)用此方法,因?yàn)樗梢宰屇粩嘀匦吕L制新形狀。 你是怎樣做的?

CADisplayLink對(duì)象是一個(gè)計(jì)時(shí)器,允許您的應(yīng)用程序?qū)⒒顒?dòng)與顯示器的刷新率同步。 您可以添加目標(biāo)和在屏幕內(nèi)容更新時(shí)調(diào)用的操作。

這是重新繪制路徑和更新形狀圖層的絕佳機(jī)會(huì)。

首先,每次需要更新時(shí)添加一個(gè)方法來(lái)調(diào)用:

func updateLoop() {
  elasticShape.path = bezierPathForControlPoints()
}

然后,通過(guò)將以下變量添加到ElasticView.swift來(lái)創(chuàng)建顯示鏈接:

private lazy var displayLink : CADisplayLink = {
  let displayLink = CADisplayLink(target: self, selector: Selector("updateLoop"))
  displayLink.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSRunLoopCommonModes)
  return displayLink
}()

這是一個(gè)懶加載變量,意味著在訪問(wèn)它之前不會(huì)創(chuàng)建它。 每次屏幕更新時(shí),它都會(huì)調(diào)用updateLoop()函數(shù)。

您將需要方法來(lái)啟動(dòng)和停止鏈接,因此添加以下內(nèi)容:

private func startUpdateLoop() {
  displayLink.paused = false
}
  
private func stopUpdateLoop() {
  displayLink.paused = true
}

每當(dāng)控制點(diǎn)移動(dòng)時(shí),你已準(zhǔn)備好繪制一條新路徑。 現(xiàn)在,你必須移動(dòng)他們!


UIView Spring Animations - UIView彈性動(dòng)畫(huà)

Apple非常擅長(zhǎng)在每個(gè)iOS版本中添加新功能,spring animations的添加可以輕松提升應(yīng)用程序的WOW因素。

它允許您使用自定義阻尼和速度(amping and velocity)為元素設(shè)置動(dòng)畫(huà),使其更加特殊和有彈性!

將以下方法添加到ElasticView.swift以使這些控制點(diǎn)移動(dòng):

func animateControlPoints() {  
  //1
  let overshootAmount : CGFloat = 10.0
  // 2
  UIView.animateWithDuration(0.25, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 1.5,
    options: nil, animations: {
    // 3
    self.topControlPointView.center.y -= overshootAmount
    self.leftControlPointView.center.x -= overshootAmount
    self.bottomControlPointView.center.y += overshootAmount
    self.rightControlPointView.center.x += overshootAmount
  },
  completion: { _ in
    // 4
    UIView.animateWithDuration(0.45, delay: 0.0, usingSpringWithDamping: 0.15, initialSpringVelocity: 5.5,
      options: nil, animations: {
        // 5
        self.positionControlPoints()
      }, 
      completion: { _ in
        // 6
        self.stopUpdateLoop()
      })
  })
}

以下是逐步細(xì)分:

  • 1)overshootAmount是控制點(diǎn)移動(dòng)的量。
  • 2)在spring animation中包含即將到來(lái)的UI更改,持續(xù)時(shí)間為五分之一秒。如果您不熟悉spring animation但擅長(zhǎng)物理,請(qǐng)查看UIView類參考,了解阻尼和速度變量的詳細(xì)說(shuō)明。對(duì)于我們其余的只要知道這些變量控制著動(dòng)畫(huà)如何反彈。使用數(shù)字來(lái)尋找感覺(jué)正確的配置是很正常的。
  • 3)向上,向左,向下或向右移動(dòng)控制點(diǎn) - 這將是動(dòng)畫(huà)。
  • 4)創(chuàng)建另一個(gè)彈簧動(dòng)畫(huà)以反彈所有內(nèi)容。
  • 5)重置控制點(diǎn)位置 - 這也將是動(dòng)畫(huà)。
  • 6)一旦停止移動(dòng),停止display link。

到目前為止,您尚未調(diào)用animateControlPoints。自定義控件的主要目的是在點(diǎn)擊它時(shí)進(jìn)行動(dòng)畫(huà)處理,因此調(diào)用上述方法的最佳位置是在touchBegan中。

添加以下內(nèi)容:

override func touchesBegan(touches: Set, withEvent event: UIEvent) {
  startUpdateLoop()
  animateControlPoints()
}

Build并運(yùn)行,然后點(diǎn)擊View。


Refactoring and Polishing

現(xiàn)在你已經(jīng)看到了很酷的動(dòng)畫(huà),但你還有一些工作要做,以使你的ElasticView更抽象。

清除的第一個(gè)障礙是overshootAmount。 目前,它的硬編碼值為10,但以編程方式和Interface Builder更改其值非常棒。

Xcode 6.0的一個(gè)新功能是@IBInspectable,這是一種通過(guò)界面構(gòu)建器設(shè)置自定義屬性的好方法。

您將通過(guò)添加overshootAmount作為@IBInspectable屬性來(lái)利用這個(gè)非常棒的新功能,這樣您創(chuàng)建的每個(gè)ElasticView都可以具有不同的值。

將以下變量添加到ElasticView

@IBInspectable var overshootAmount : CGFloat = 10

通過(guò)替換此行來(lái)引用animateControlPoints()中的屬性:

let overshootAmount : CGFloat = 10.0

以及下面這行

let overshootAmount = self.overshootAmount

轉(zhuǎn)到Main.storyboard,單擊ElasticView并選擇Attributes Inspector選項(xiàng)卡。

您會(huì)注意到一個(gè)新選項(xiàng)卡,其中顯示了您的視圖名稱以及一個(gè)名為Overshoot Ainput field

對(duì)于使用@IBInspectable聲明的每個(gè)變量,您將在Interface Builder中看到一個(gè)新的輸入字段,您可以在其中編輯其值。

要查看此操作,請(qǐng)復(fù)制ElasticView,以便最終得到兩個(gè)視圖,并將新視圖放在當(dāng)前視圖上方,就像這樣。

在新視圖中將原始視圖中的Overshoot Amount值更改為20和40。

Build并運(yùn)行。 點(diǎn)按兩個(gè)視圖即可查看差異。 如您所見(jiàn),動(dòng)畫(huà)略有不同,并且取決于您在界面構(gòu)建器中輸入的數(shù)量。

嘗試將值更改為-40而不是40,并查看會(huì)發(fā)生什么。 您可以看到控制點(diǎn)向內(nèi)動(dòng)畫(huà),但背景似乎沒(méi)有變化。

你一定可以解決這個(gè)問(wèn)題吧!

我會(huì)給你一個(gè)線索:你需要在setupComponents方法中改變一些東西。 自己嘗試一下,但是如果你遇到困難,請(qǐng)看看下面的解決方案。

// You have to change the background color of your view after the elasticShape is created, otherwise the view and layer have the same color
backgroundColor = UIColor.clearColor()
clipsToBounds = false

干得好,你終于完成了你的ElasticView

現(xiàn)在您已經(jīng)擁有了ElasticView,您可以將其嵌入到不同的控件中,例如text fields或按鈕。


Making an Elastic UITextfield - 做一個(gè)有彈性的UITextfield

現(xiàn)在您已經(jīng)構(gòu)建了彈性視圖的核心功能,接下來(lái)的任務(wù)是將其嵌入到自定義text fields中。

右鍵單擊項(xiàng)目導(dǎo)航器中的ElasticUI組,然后選擇New File…。 選擇iOS / Source / Cocoa Touch Class模板,然后單擊Next。

調(diào)用類ElasticTextField,在子類字段中輸入UITextfield并確保語(yǔ)言為Swift。 單擊Next,然后單擊Create

打開(kāi)ElasticTextField.swift并用以下內(nèi)容替換其內(nèi)容:

import UIKit

class ElasticTextField: UITextField {
  
  // 1
  var elasticView : ElasticView!
  
  // 2
  @IBInspectable var overshootAmount: CGFloat = 10 {
    didSet {
      elasticView.overshootAmount = overshootAmount
    }
  }
  
  // 3
  override init(frame: CGRect) {
    super.init(frame: frame)
    setupView()
  }
  
  required init(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    setupView()
  }

  // 4
  func setupView() {
    // A
    clipsToBounds = false
    borderStyle = .None

    // B
    elasticView = ElasticView(frame: bounds)
    elasticView.backgroundColor = backgroundColor
    addSubview(elasticView)

    // C
    backgroundColor = UIColor.clearColor()
    
    // D
    elasticView.userInteractionEnabled = false
  }
  
  // 5
  override func touchesBegan(touches: Set, withEvent event: UIEvent) {
    elasticView.touchesBegan(touches, withEvent: event)
  }
}

那里有很多事情發(fā)生!這是一步一步細(xì)分:

  • 1)這是保存ElasticView的A屬性。
  • 2)一個(gè)名為overshootAmountIBInspectable變量,因此您可以通過(guò)Interface Builder更改控件的彈性。它會(huì)覆蓋didSet并只設(shè)置彈性視圖的overshootAmount。
  • 3)該類的標(biāo)準(zhǔn)初始值設(shè)定項(xiàng),因此調(diào)用設(shè)置方法。
  • 4)這是您設(shè)置text field的位置。讓我們進(jìn)一步細(xì)分:
    • A)將clipsToBounds更改為false。這使彈性視圖超出其父級(jí)邊界,并將UITextField的邊框樣式更改為.None以展平控件。
    • B)創(chuàng)建并添加ElasticView作為控件的子視圖。
    • C)更改控件的backgroundColor以使其clear;你這樣做是因?yàn)槟阆M?code>ElasticView決定顏色。
    • D)最后,這會(huì)將ElasticViewuserInteractionEnabled設(shè)置為false。否則,它會(huì)從您的控制中竊取。
    • E)覆蓋touchesBegan并將其轉(zhuǎn)發(fā)到您的ElasticView,以便它可以設(shè)置動(dòng)畫(huà)。

轉(zhuǎn)到Main.storyboard,選擇UITextfield的兩個(gè)實(shí)例,并在Identity Inspector中將它們的類從UITextField更改為ElasticTextField。

此外,請(qǐng)確保刪除為測(cè)試目的而添加的兩個(gè)ElasticView實(shí)例。

Build并運(yùn)行,點(diǎn)按您的textfield,注意它實(shí)際上是如何工作的:

原因是當(dāng)您在代碼中創(chuàng)建ElasticView時(shí),它會(huì)獲得清晰的背景顏色,并將其傳遞給形狀圖層。

要更正此問(wèn)題,只要在視圖上設(shè)置新的背景顏色,就需要一種方式將顏色傳遞到shape layer。


Forwarding the Background Color - 傳遞背景色

因?yàn)橐褂?code>elasticShape作為視圖的主要背景,所以必須覆蓋ElasticView中的backgroundColor。

將以下代碼添加到ElasticView.swift

override var backgroundColor: UIColor? {
  willSet {
    if let newValue = newValue {
      elasticShape.fillColor = newValue.CGColor
      super.backgroundColor = UIColor.clearColor()
    }
  }
}

在設(shè)置值之前,將調(diào)用willSet。 檢查是否已傳遞值,然后將elasticShapefillColor設(shè)置為用戶選擇的顏色。 然后,您調(diào)用super并將其背景顏色設(shè)置為clear

Build并運(yùn)行,你應(yīng)該有一個(gè)可愛(ài)的彈性控制。


Final Tweaks - 最后的調(diào)整

請(qǐng)注意UITextField的占位符文本與左邊緣的接近程度。 它有點(diǎn)舒服,你不覺(jué)得嗎? 你想自己解決這個(gè)問(wèn)題嗎?

看下面的解決方案。

// Add some padding to the text and editing bounds
override func textRectForBounds(bounds: CGRect) -> CGRect {
  return CGRectInset(bounds, 10, 5)
}

override func editingRectForBounds(bounds: CGRect) -> CGRect {
  return CGRectInset(bounds, 10, 5)
}

Removing Debug Information - 刪除調(diào)試信息

打開(kāi)ElasticView.swift并從setupComponents中刪除以下內(nèi)容。

controlPoint.backgroundColor = UIColor.blueColor()

到目前為止就全部完成了。


源碼

1. ElasticView.swift
import UIKit

class ElasticView: UIView {
    
    private let topControlPointView = UIView()
    private let leftControlPointView = UIView()
    private let bottomControlPointView = UIView()
    private let rightControlPointView = UIView()
    
    private let elasticShape = CAShapeLayer()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupComponents()
    }
    
    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupComponents()
    }
    
    private func setupComponents() {
        elasticShape.fillColor = backgroundColor?.CGColor
        elasticShape.path = UIBezierPath(rect: self.bounds).CGPath
        layer.addSublayer(elasticShape)
        
        for controlPoint in [topControlPointView, leftControlPointView,
            bottomControlPointView, rightControlPointView] {
                addSubview(controlPoint)
                controlPoint.frame = CGRect(x: 0.0, y: 0.0, width: 5.0, height: 5.0)
        }
        
        positionControlPoints()
        
        // You have to change the background color of your view after the elasticShape is created, otherwise the view and layer have the same color
        backgroundColor = UIColor.clearColor()
        clipsToBounds = false
    }
    
    private func positionControlPoints(){
        topControlPointView.center = CGPoint(x: bounds.midX, y: 0.0)
        leftControlPointView.center = CGPoint(x: 0.0, y: bounds.midY)
        bottomControlPointView.center = CGPoint(x:bounds.midX, y: bounds.maxY)
        rightControlPointView.center = CGPoint(x: bounds.maxX, y: bounds.midY)
    }
    
    private func bezierPathForControlPoints()->CGPathRef {
        // 1
        let path = UIBezierPath()
        
        // 2
        let top = topControlPointView.layer.presentationLayer().position
        let left = leftControlPointView.layer.presentationLayer().position
        let bottom = bottomControlPointView.layer.presentationLayer().position
        let right = rightControlPointView.layer.presentationLayer().position
        
        let width = frame.size.width
        let height = frame.size.height
        
        // 3
        path.moveToPoint(CGPointMake(0, 0))
        path.addQuadCurveToPoint(CGPointMake(width, 0), controlPoint: top)
        path.addQuadCurveToPoint(CGPointMake(width, height), controlPoint:right)
        path.addQuadCurveToPoint(CGPointMake(0, height), controlPoint:bottom)
        path.addQuadCurveToPoint(CGPointMake(0, 0), controlPoint: left)
        
        // 4
        return path.CGPath
    }
    
    private lazy var displayLink : CADisplayLink = {
        let displayLink = CADisplayLink(target: self, selector: Selector("updateLoop"))
        displayLink.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSRunLoopCommonModes)
        return displayLink
        }()
    
    func updateLoop() {
        elasticShape.path = bezierPathForControlPoints()
    }
    
    private func startUpdateLoop() {
        displayLink.paused = false
    }
    
    private func stopUpdateLoop() {
        displayLink.paused = true
    }
    
    @IBInspectable var overshootAmount : CGFloat = 10
    
    func animateControlPoints() {
        //1
        let overshootAmount = self.overshootAmount
        // 2
        UIView.animateWithDuration(0.25, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 1.5,
            options: nil, animations: {
                // 3
                self.topControlPointView.center.y -= overshootAmount
                self.leftControlPointView.center.x -= overshootAmount
                self.bottomControlPointView.center.y += overshootAmount
                self.rightControlPointView.center.x += overshootAmount
            },
            completion: { _ in
                // 4
                UIView.animateWithDuration(0.45, delay: 0.0, usingSpringWithDamping: 0.15, initialSpringVelocity: 5.5,
                    options: nil, animations: {
                        // 5
                        self.positionControlPoints()
                    }, 
                    completion: { _ in
                        // 6
                        self.stopUpdateLoop()
                })
        })
    }
    
    override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
        startUpdateLoop()
        animateControlPoints()
    }
    
    override var backgroundColor: UIColor? {
        willSet {
            if let newValue = newValue {
                elasticShape.fillColor = newValue.CGColor
                super.backgroundColor = UIColor.clearColor()
            }
        }
    }
}
2. ElasticTextField.swift
import UIKit

class ElasticTextField: UITextField {
    
    // 1
    var elasticView : ElasticView!
    
    // 2
    @IBInspectable var overshootAmount: CGFloat = 10 {
        didSet {
            elasticView.overshootAmount = overshootAmount
        }
    }
    
    // 3
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }
    
    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupView()
    }
    
    // 4
    func setupView() {
        // A
        clipsToBounds = false
        borderStyle = .None
        
        // B
        elasticView = ElasticView(frame: bounds)
        elasticView.backgroundColor = backgroundColor
        addSubview(elasticView)
        
        // C
        backgroundColor = UIColor.clearColor()
        
        // D
        elasticView.userInteractionEnabled = false
    }
    
    // 5
    override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
        elasticView.touchesBegan(touches, withEvent: event)
    }
    
    // Add some padding to the text and editing bounds
    override func textRectForBounds(bounds: CGRect) -> CGRect {
        return CGRectInset(bounds, 10, 5)
    }
    
    override func editingRectForBounds(bounds: CGRect) -> CGRect {
        return CGRectInset(bounds, 10, 5)
    }
}

后記

本篇主要講述了一種彈性動(dòng)畫(huà)的實(shí)現(xiàn),感興趣的給個(gè)贊或者關(guān)注~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容