Efficient learning iOS -- Stroke and path animation

Posted by system_critical on Fri, 14 Jan 2022 11:04:36 +0100

This is the animation to complete:

First add the required code. Here, you need to replace the ViewController of the storyboard with

TableViewController, cancel the Under Top Bars and Under Bottom Bars to, and then

Embed in Navigation Controller´╝î

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)
    }
}

(slide to show more)

Then create a file, create a MyRefreshView class, and init needs to pass in frame and UIScrollView, which is used to listen to external pull actions.

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")
    }
}

(slide to show more)

Next, use ShapeLayer and layer to create circular points and aircraft images.

//Declare properties
  let ovalShapeLayer: CAShapeLayer = CAShapeLayer()
  let airplaneLayer: CALayer = CALayer()

(slide to show more)

Then set the relevant attributes. Here, the radius of the circle is set to half of the view height * 0.8. Here, lineDashPattern is the dashed pattern (NSNumbers array) applied when creating the stroked version of the path. The default value is nil. If it is set to [2, 3], the previous line will be cut into one by one. Then the plane is set to the hidden state.

 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

(slide to show more)

Use UIScrollViewDelegate here, then call scrollViewDidScroll and

scrollViewWillEndDragging to monitor the pulling action and height.

   func scrollViewDidScroll(_ scrollView: UIScrollView) {
    }

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

    }

(slide to show more)

Then add refreshVie in viewController. Declare a RefreshView property.

  var refreshView: RefreshView!

Then, set the property in viewDidLoad and add it as a child View.

 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)

(slide to show more)

Rewrite the scrollViewDidScroll and scrollViewWillEndDragging methods in the viewController, then call the methods in refreshView correspondingly and pass in the parameters.

 // 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)
  }

(slide to show more)

Then perform corresponding processing in the scrollViewDidScroll and scrollViewWillEndDragging methods in the refreshView.

Here, you need to judge the progress according to the rolling height, and specify a CGFloat attribute of progress first.

  var progress: CGFloat = 0.0

Calculate the height of upward scrolling in scrollViewDidScroll, then process the size of its own view and compare it with 1 to get the minimum value, and then set the strokeEnd of ovalShapeLayer and the opacity of airplane layer according to the obtained progress.

  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)
  }

(slide to show more)

Next, add animation for ovalShapeLayer and airplaneLayer. Here, change the contentInset of the scrollView to display the view, add the animation of strokeStart and strokeEnd for the ovalShapeLayer, and then add the change of the position around the circle and the change of the picture angle for the airplane layer.

 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)
  }

(slide to show more)

After the animation, you need to adjust the contentInset of scrollView back.

 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
      }
    )
  }

(slide to show more)

Then declare an attribute isRefreshing to determine whether the animation is in progress

 isRefreshing = true

Set it to true in beginRefreshing and false in endRefreshing. Then judge in scrollViewDidScroll that redrawFromProgress will not be called if the animation is being executed.

 if !isRefreshing {
      redrawFromProgress(self.progress)
    }

(slide to show more)

Judge from scrollviewwillenddrawing that if no animation is being executed and progress is greater than or equal to 1, the animation will be executed.

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

(slide to show more)

It's up to the outside world to decide when to end the refresh, so you can use delegate or closure to notify the outside world to start the animation.

Here we declare a closure

    var refreshViewDidRefresh :(() -> Void)?

If it is judged that it is not empty in scrollViewWillEndDragging, call

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

(slide to show more)

Used in ViewController

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

(slide to show more)

So the animation is finished.

Complete code

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)
    }
}


(slide to show more)

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")
    }
}


(slide to show more)

Original link:

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