前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >高效学习iOS —— Stroke和路径动画

高效学习iOS —— Stroke和路径动画

作者头像
CC老师
发布2022-01-14 15:09:55
1.8K0
发布2022-01-14 15:09:55
举报

这是要完成的动画:

先添加需要的代码,这里需要将storyboard的ViewController换成

TableViewController,将Under Top Bars 和 Under Bottom Bars 取消到,然后为其

Embed in Navigation Controller,

代码语言:javascript
复制
import UIKit

func delay(seconds: Double, completion: @escaping ()-> Void) {
  DispatchQueue.main.asyncAfter(deadline: .now() + seconds, execute: completion)
}

class ViewController: UITableViewController {
    let packItems = ["Ice cream money", "Great weather", "Beach ball", "Swimsuit for him", "Swimsuit for her", "Beach games", "Ironing board", "Cocktail mood", "Sunglasses", "Flip flops", "Spare flip flops"]

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
        self.title = "try"
        self.navigationController?.navigationBar.titleTextAttributes = [.foregroundColor: UIColor.white]
        self.tableView.rowHeight = 64.0
        self.view.backgroundColor = UIColor(red: 0.0, green: 154.0/255.0, blue: 222.0/255.0, alpha: 1.0)

    }

    // MARK: Table View methods
    override func numberOfSections(in tableView: UITableView) -> Int {
      return 1
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
      return 11
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
      let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as UITableViewCell
      cell.accessoryType = .none
      cell.textLabel!.text = packItems[indexPath.row]
      cell.imageView!.image = UIImage(named: "summericons_100px_\(indexPath.row).png")
      return cell
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
      tableView.deselectRow(at: indexPath, animated: true)
    }
}

(滑动显示更多)

然后创建一个文件,创建一个MyRefreshView类并且init需要传进来frame和UIScrollView,UIScrollView用来监听外界的拉取动作。

代码语言:javascript
复制
class MyRefreshView: UIView, UIScrollViewDelegate {
    var scrollView: UIScrollView

    init(frame: CGRect, scrollView: UIScrollView) {
        self.scrollView = scrollView
        super.init(frame: frame)

        //add the background image
        let imgView = UIImageView(image: UIImage(named: "refresh-view-bg.png"))
        imgView.frame = bounds
        imgView.contentMode = .scaleAspectFill
        imgView.clipsToBounds = true
        addSubview(imgView)

    }

    required init(coder aDecoder: NSCoder) {
      fatalError("init(coder:) has not been implemented")
    }
}

(滑动显示更多)

接下来使用ShapeLayer 和 layer来创建圆形的点和 飞机图片。

代码语言:javascript
复制
//声明属性
  let ovalShapeLayer: CAShapeLayer = CAShapeLayer()
  let airplaneLayer: CALayer = CALayer()

(滑动显示更多)

然后设置好相关属性,这里圆的半径设置为view高度 * 0.8 的一半,这里lineDashPattern是创建路径的描边版本时应用的虚线图案(NSNumbers数组)。默认为nil,设置为[2, 3]之后就会把之前的一条线切割成一条一条的了。然后这里飞机先设置为隐藏的状态。

代码语言:javascript
复制
 ovalShapeLayer.strokeColor = UIColor.white.cgColor
    ovalShapeLayer.fillColor = UIColor.clear.cgColor
    ovalShapeLayer.lineWidth = 4.0
    ovalShapeLayer.lineDashPattern = [2, 3]

    let refreshRadius = frame.size.height/2 * 0.8

    ovalShapeLayer.path = UIBezierPath(ovalIn: CGRect(
      x: frame.size.width/2 - refreshRadius,
      y: frame.size.height/2 - refreshRadius,
      width: 2 * refreshRadius,
      height: 2 * refreshRadius)
      ).cgPath

    layer.addSublayer(ovalShapeLayer)

    let airplaneImage = UIImage(named: "airplane.png")!
    airplaneLayer.contents = airplaneImage.cgImage
    airplaneLayer.bounds = CGRect(x: 0.0, y: 0.0,
                                  width: airplaneImage.size.width,
                                  height: airplaneImage.size.height)

    airplaneLayer.position = CGPoint(
      x: frame.size.width/2 + frame.size.height/2 * 0.8,
      y: frame.size.height/2)

    layer.addSublayer(airplaneLayer)

    airplaneLayer.opacity = 0.0

(滑动显示更多)

这里使用UIScrollViewDelegate,然后调用scrollViewDidScroll和

scrollViewWillEndDragging来监听拉取的动作以及高度。

代码语言:javascript
复制
   func scrollViewDidScroll(_ scrollView: UIScrollView) {
    }

    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {

    }

(滑动显示更多)

然后在viewController里面添加refreshVie。声明一个RefreshView属性。

代码语言:javascript
复制
  var refreshView: RefreshView!

然后,在viewDidLoad里面设置好属性并且添加为子View。

代码语言:javascript
复制
 let refreshRect = CGRect(x: 0.0, y: -kRefreshViewHeight, width: view.frame.size.width, height: kRefreshViewHeight)
    refreshView = RefreshView(frame: refreshRect, scrollView: self.tableView)
    refreshView.delegate = self
    view.addSubview(refreshView)

(滑动显示更多)

在viewController里面重写scrollViewDidScroll 和 scrollViewWillEndDragging方法,然后在里面相对应调用refreshView里面的方法,并且把参数传进去。

代码语言:javascript
复制
 // MARK: Scroll view methods
  override func scrollViewDidScroll(_ scrollView: UIScrollView) {
    refreshView.scrollViewDidScroll(scrollView)
  }

  override func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    refreshView.scrollViewWillEndDragging(scrollView, withVelocity: velocity, targetContentOffset: targetContentOffset)
  }

(滑动显示更多)

然后在refreshView里面的scrollViewDidScroll 和 scrollViewWillEndDragging方法进行对应的处理。

这里需要根据滚动的高度来进行判断进度,先声明一个progress的CGFloat属性。

代码语言:javascript
复制
  var progress: CGFloat = 0.0

在scrollViewDidScroll 里面算出向上滚动的高度,然后处理本身view的大小和1比较取最小值,然后根据得到的progress设置ovalShapeLayer的strokeEnd和airplaneLayer的opacity。

代码语言:javascript
复制
  func scrollViewDidScroll(_ scrollView: UIScrollView) {
    let offsetY = max(-(scrollView.contentOffset.y + scrollView.contentInset.top), 0.0)
    progress = min(max(offsetY / frame.size.height, 0.0), 1.0)

    redrawFromProgress(self.progress)

  }
   func redrawFromProgress(_ progress: CGFloat) {
    ovalShapeLayer.strokeEnd = progress
    airplaneLayer.opacity = Float(progress)
  }

(滑动显示更多)

接下来再为ovalShapeLayer和airplaneLayer添加动画。这里改变了scrollView的contentInset来显示这个view,再为ovalShapeLayer添加上strokeStart和strokeEnd的动画,然后为airplaneLayer添加上绕圆的位置的变化以及图片角度的变化。

代码语言:javascript
复制
 func beginRefreshing() {    
    UIView.animate(withDuration: 0.3) {
      var newInsets = self.scrollView.contentInset
      newInsets.top += self.frame.size.height
      self.scrollView.contentInset = newInsets
    }

    let strokeStartAnimation = CABasicAnimation(keyPath: "strokeStart")
    strokeStartAnimation.fromValue = -0.5
    strokeStartAnimation.toValue = 1.0

    let strokeEndAnimation = CABasicAnimation(keyPath: "strokeEnd")
    strokeEndAnimation.fromValue = 0.0
    strokeEndAnimation.toValue = 1.0

    let strokeAnimationGroup = CAAnimationGroup()
    strokeAnimationGroup.duration = 1.5
    strokeAnimationGroup.repeatDuration = 5.0
    strokeAnimationGroup.animations = [strokeStartAnimation, strokeEndAnimation]
    ovalShapeLayer.add(strokeAnimationGroup, forKey: nil)

    let flightAnimation = CAKeyframeAnimation(keyPath: "position")
    flightAnimation.path = ovalShapeLayer.path
    flightAnimation.calculationMode = .paced

    let airplaneOrientationAnimation = CABasicAnimation(keyPath: "transform.rotation")
    airplaneOrientationAnimation.fromValue = 0
    airplaneOrientationAnimation.toValue = 2.0 * .pi

    let flightAnimationGroup = CAAnimationGroup()
    flightAnimationGroup.duration = 1.5
    flightAnimationGroup.repeatDuration = 5.0
    flightAnimationGroup.animations = [flightAnimation, airplaneOrientationAnimation]
    airplaneLayer.add(flightAnimationGroup, forKey: nil)
  }

(滑动显示更多)

动画结束后需要把scrollView的contentInset调整回去。

代码语言:javascript
复制
 func endRefreshing() {
        UIView.animate(withDuration: 0.3, delay:0.0, options: .curveEaseOut,
      animations: {
        var newInsets = self.scrollView.contentInset
        newInsets.top -= self.frame.size.height
        self.scrollView.contentInset = newInsets
      },
      completion: {_ in
        //finished
      }
    )
  }

(滑动显示更多)

然后声明一个属性 isRefreshing来判断是否正在动画

代码语言:javascript
复制
 isRefreshing = true

在beginRefreshing里面设为true,在endRefreshing设为false。然后在scrollViewDidScroll里面判断,如果正在执行动画就不调用redrawFromProgress。

代码语言:javascript
复制
 if !isRefreshing {
      redrawFromProgress(self.progress)
    }

(滑动显示更多)

再scrollViewWillEndDragging里面判断,如果没有正在执行动画并且progress大于等于1,那么就执行动画。

代码语言:javascript
复制
  func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    if !isRefreshing && self.progress >= 1.0 {
      beginRefreshing()
    }
  }

(滑动显示更多)

这里由外界决定什么时候结束刷新,那么就可以用delegate 或者 闭包来通知外界开始动画了。

这里声明一个闭包

代码语言:javascript
复制
    var refreshViewDidRefresh :(() -> Void)?

在scrollViewWillEndDragging里面判断不为空则调用

代码语言:javascript
复制
  func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        if !isRefreshing && self.progress >= 1.0 {
            if (refreshViewDidRefresh != nil) {
                refreshViewDidRefresh!()
            }
            beginRefreshing()
        }
    }

(滑动显示更多)

在ViewController 里面使用

代码语言:javascript
复制
  func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        if !isRefreshing && self.progress >= 1.0 {
            if (refreshViewDidRefresh != nil) {
                refreshViewDidRefresh!()
            }
            beginRefreshing()
        }
    }

(滑动显示更多)

这样动画就完成啦。

完整代码

代码语言:javascript
复制
import UIKit

func delay(seconds: Double, completion: @escaping ()-> Void) {
  DispatchQueue.main.asyncAfter(deadline: .now() + seconds, execute: completion)
}
let kRefreshViewHeight: CGFloat = 110.0

class ViewController: UITableViewController {

    let packItems = ["Ice cream money", "Great weather", "Beach ball", "Swimsuit for him", "Swimsuit for her", "Beach games", "Ironing board", "Cocktail mood", "Sunglasses", "Flip flops", "Spare flip flops"]
    var refreshView: MyRefreshView!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        self.view.backgroundColor = .clear
        self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
        self.title = "try"
        self.navigationController?.navigationBar.titleTextAttributes = [.foregroundColor: UIColor.white]

        self.tableView.rowHeight = 64.0
        self.view.backgroundColor = UIColor(red: 0.0, green: 154.0/255.0, blue: 222.0/255.0, alpha: 1.0)
        let refreshRect = CGRect(x: 0.0, y: -kRefreshViewHeight, width: view.frame.size.width, height: kRefreshViewHeight)
        refreshView = MyRefreshView(frame: refreshRect, scrollView: self.tableView)

        refreshView.refreshViewDidRefresh = { [weak self] in
            delay(seconds: 4) {
                self?.refreshView.endRefreshing()
            }
        }
        view.addSubview(refreshView)
    }

    // MARK: Table View methods
    override func numberOfSections(in tableView: UITableView) -> Int {
      return 1
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
      return 11
    }

    override func scrollViewDidScroll(_ scrollView: UIScrollView) {
      refreshView.scrollViewDidScroll(scrollView)
    }

    override func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
      refreshView.scrollViewWillEndDragging(scrollView, withVelocity: velocity, targetContentOffset: targetContentOffset)
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
      let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as UITableViewCell
      cell.accessoryType = .none
      cell.textLabel!.text = packItems[indexPath.row]
      cell.imageView!.image = UIImage(named: "summericons_100px_\(indexPath.row).png")
      return cell
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
      tableView.deselectRow(at: indexPath, animated: true)
    }
}


(滑动显示更多)

代码语言:javascript
复制
import UIKit
import QuartzCore
protocol MyRefreshViewDelegate: class {
    func refreshViewDidRefresh(_ refreshView: MyRefreshView)
}
class MyRefreshView: UIView, UIScrollViewDelegate {
    var scrollView: UIScrollView
    weak var delegate: MyRefreshViewDelegate?
    let ovalShapeLayer: CAShapeLayer = CAShapeLayer()
    let airplaneLayer: CALayer = CALayer()
    var progress: CGFloat = 0.0
    var isRefreshing = false
    var refreshViewDidRefresh :(() -> Void)?

    init(frame: CGRect, scrollView: UIScrollView) {
        self.scrollView = scrollView
        super.init(frame: frame)

        //add the background image
        let imgView = UIImageView(image: UIImage(named: "refresh-view-bg.png"))
        imgView.frame = bounds
        imgView.contentMode = .scaleAspectFill
        imgView.clipsToBounds = true
        addSubview(imgView)

        ovalShapeLayer.strokeColor = UIColor.white.cgColor
        ovalShapeLayer.fillColor = UIColor.clear.cgColor
        ovalShapeLayer.lineWidth = 4.0
        ovalShapeLayer.lineDashPattern = [2, 3]

        let refreshRadius = frame.size.height/2 * 0.8

        ovalShapeLayer.path = UIBezierPath(ovalIn: CGRect(
            x: frame.size.width/2 - refreshRadius,
            y: frame.size.height/2 - refreshRadius,
            width: 2 * refreshRadius,
            height: 2 * refreshRadius)
        ).cgPath

        layer.addSublayer(ovalShapeLayer)

        let airplaneImage = UIImage(named: "airplane.png")!
        airplaneLayer.contents = airplaneImage.cgImage
        airplaneLayer.bounds = CGRect(x: 0.0, y: 0.0,
                                      width: airplaneImage.size.width,
                                      height: airplaneImage.size.height)

        airplaneLayer.position = CGPoint(
            x: frame.size.width/2 + frame.size.height/2 * 0.8,
            y: frame.size.height/2)

        layer.addSublayer(airplaneLayer)

        airplaneLayer.opacity = 0.0

    }

    // MARK: Scroll View Delegate methods

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let offsetY = max(-(scrollView.contentOffset.y + scrollView.contentInset.top), 0.0)
        progress = min(max(offsetY / frame.size.height, 0.0), 1.0)

        if !isRefreshing {
            redrawFromProgress(self.progress)
        }
    }

    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        if !isRefreshing && self.progress >= 1.0 {
            if (refreshViewDidRefresh != nil) {
                refreshViewDidRefresh!()
            }
            beginRefreshing()
        }
    }

    func redrawFromProgress(_ progress: CGFloat) {
        ovalShapeLayer.strokeEnd = progress
        airplaneLayer.opacity = Float(progress)
    }

    func beginRefreshing() {
        isRefreshing = true

        UIView.animate(withDuration: 0.3) {
            var newInsets = self.scrollView.contentInset
            newInsets.top += self.frame.size.height
            self.scrollView.contentInset = newInsets
        }

        let strokeStartAnimation = CABasicAnimation(keyPath: "strokeStart")
        strokeStartAnimation.fromValue = -0.5
        strokeStartAnimation.toValue = 1.0

        let strokeEndAnimation = CABasicAnimation(keyPath: "strokeEnd")
        strokeEndAnimation.fromValue = 0.0
        strokeEndAnimation.toValue = 1.0

        let strokeAnimationGroup = CAAnimationGroup()
        strokeAnimationGroup.duration = 1.5
        strokeAnimationGroup.repeatDuration = 5.0
        strokeAnimationGroup.animations = [strokeStartAnimation, strokeEndAnimation]
        ovalShapeLayer.add(strokeAnimationGroup, forKey: nil)

        let flightAnimation = CAKeyframeAnimation(keyPath: "position")
        flightAnimation.path = ovalShapeLayer.path
        flightAnimation.calculationMode = .paced

        let airplaneOrientationAnimation = CABasicAnimation(keyPath: "transform.rotation")
        airplaneOrientationAnimation.fromValue = 0
        airplaneOrientationAnimation.toValue = 2.0 * .pi

        let flightAnimationGroup = CAAnimationGroup()
        flightAnimationGroup.duration = 1.5
        flightAnimationGroup.repeatDuration = 5.0
        flightAnimationGroup.animations = [flightAnimation, airplaneOrientationAnimation]
        airplaneLayer.add(flightAnimationGroup, forKey: nil)
    }

    func endRefreshing() {

        isRefreshing = false

        UIView.animate(withDuration: 0.3, delay:0.0, options: .curveEaseOut,
                       animations: {
            var newInsets = self.scrollView.contentInset
            newInsets.top -= self.frame.size.height
            self.scrollView.contentInset = newInsets
        },
                       completion: {_ in
            //finished
        }
        )
    }

    required init(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}


(滑动显示更多)

原文链接:

https://blog.csdn.net/LinShunIos/article/details/121290126

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-12-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 HelloCoder全栈小集 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档