效果大致如上:
示例的Git地址:https://github.com/Goddreamwt/iOSAnimationSample
最近项目中有需要UIView的分解重组动画,网上找了好久,就找到一篇介绍相关效果的文档:重组/分解动画 UIView Refactor/Destruct Animation - 简书
参照作者介绍的gitHub介绍,大致实现了项目中的需求,先分享一下。
原gitHub地址
因为原作者是使用Swift写的,所以要用到OC和Swift混编(我是用OC),分享一下OC和Swift混编设置讲解博客OC项目中使用Swift
下面是OC方式使用
UIViewRefactorAndDestructExtension.swift
注意原gitHub上的UIViewRefactorAndDestructExtension.swift文件在OC里面无法直接调用,做了一下修改。
UIViewRefactorAndDestructExtension.swift文件:把新建一个swift文件,把代码贴到里面即可。也可以直接去git上面下载,链接再文章的最前面。
//
// UIViewRefactorExtension.swift
//
// Created by seedante on 15/11/8.
// Copyright © 2015年 seedante. All rights reserved.
//
import Foundation
import UIKit
enum SDERefactorDirection{
case Horizontal
case Vertical
case Diagonal
case Custom
}
extension CGRect{
var centerPoint: CGPoint{
return CGPoint(x: origin.x + size.width / 2, y: origin.y + size.height / 2)
}
}
extension UIView {
//MARK: Refactor
func refactor(){
refactorWithNewFrame(destinationFrame: nil, piecesRegion: nil, shiningColor: nil)
}
//
func refactorAll(destinationFrame: CGRect, jumpRect: CGRect, shiningColor: UIColor, direction: String, animationTime: TimeInterval, ratio: CGFloat, enableBigRegion: Bool){
var direc: SDERefactorDirection!
switch direction {
case "Horizontal":
direc = SDERefactorDirection.Horizontal
case "Vertical":
direc = SDERefactorDirection.Vertical
case "Diagonal":
direc = SDERefactorDirection.Diagonal
default:
direc = SDERefactorDirection.Custom
}
refactorWithNewFrame(destinationFrame: destinationFrame, piecesRegion: jumpRect, shiningColor: shiningColor, direction: direc, refactorTime: animationTime, pieceRatio: ratio, enableBigRegion: enableBigRegion)
}
func customRefactor(){
//DIY...
}
/**
它是移动动画的替代方法。 注意:自动布局无法正常工作。
- 参数destinationFrame:要更改的框架。 如果没有指定此参数,它将自动重构自身。 我建议你从CGRect更改这个? 使用CGRect时。
- 参数jumpRect:所有片段出现的区域,如果nil是视图的2X帧。
- 参数shiningColor:如果指定此参数,则添加如电焊接灯。
- 参数方向:动画方向
- 参数animationTime:动画的总时间
- 参数比:要查看哪个片段的比例,这里的比例用于宽度和高度。
- 参数enableBigRegion:如果启用它,您将获得2X或4X大小的通用件。 我喜欢这个,它可以减少动画的时间。 我建议只要启用它,如果视图足够大。
**/
func refactorWithNewFrame(destinationFrame: CGRect?, piecesRegion jumpRect: CGRect?, shiningColor: UIColor? = UIColor.clear, direction: SDERefactorDirection = .Horizontal, refactorTime animationTime: TimeInterval = 0.5, pieceRatio ratio: CGFloat = 0.05, enableBigRegion: Bool = false){
guard let _ = self.superview else{
return
}
if direction == .Custom{
customRefactor()
return
}
if destinationFrame != nil{
self.translatesAutoresizingMaskIntoConstraints = false
self.frame = destinationFrame!
}
let fromViewSnapshot = self.snapshotView(afterScreenUpdates: false)
UIView.animate(withDuration: 0.3) {
self.alpha = 0
}
let origin = self.frame.origin
let size = self.frame.size
let pieceWidth: CGFloat = size.width * ratio
let pieceHeight: CGFloat = size.height * ratio
let (column, row) = columnAndrow(size: size, ratio: ratio)
let delayDelta: Double = (direction == .Diagonal) ? animationTime / Double(column + row - 1) : animationTime / Double(column * row)
let piecesRect = filterRect(jumpRect: jumpRect)
var snapshots: [UIView] = []
var ignoreIndexSet:Set<Int> = []
var index = 0
var delay: TimeInterval = 0
var cleanTime: TimeInterval = 0
for y in stride(from: 0, to: size.height, by: pieceHeight) {
for x in stride(from: 0, to: size.width, by: pieceWidth){
index += 1
if ignoreIndexSet.contains(index){
continue
}
let indexOffset = ignoreIndexSet.reduce(0, {
delta, element in
let gap = element < index ? delta + 1 : delta
return gap
})
let (snapshotRegion, addedSet) = snapshotInfo(enableBigRegion: enableBigRegion, index: index, xy: (x, y), widthXheight: (pieceWidth, pieceHeight), columnXrow: (column, row))
let initialFrame = randomRectFrom(sourceRect: piecesRect, regionSize: snapshotRegion.size)
let finalFrame = CGRect(origin: CGPoint(x: (x + origin.x), y: (y + origin.y)), size: snapshotRegion.size)
if addedSet.count > 0{
ignoreIndexSet.formUnion(addedSet)
}
let snapshot = fromViewSnapshot?.resizableSnapshotView(from: snapshotRegion, afterScreenUpdates: false, withCapInsets: .zero)
self.superview!.addSubview(snapshot!)
snapshots.append(snapshot!)
snapshot?.frame = initialFrame
snapshot?.alpha = 0.0
switch direction{
case .Horizontal:
let x = index % column == 0 ? column : index % column
let y = (index - x + column) / column
delay = delayDelta * Double(x * row + y - indexOffset)
case .Vertical:
delay = delayDelta * Double(index - indexOffset)
case .Diagonal:
delay = delayDelta * Double(diagonalIndexFor(index: index, columnXrow: (column, row)))
case .Custom: break
}
let duration: TimeInterval = 0.2 + 0.1 * Double(UInt32(arc4random()) % UInt32(3))
cleanTime = (delay + duration + 0.2 > cleanTime) ? delay + duration + 0.2 : cleanTime
//UIView块动画比Core Animation在这方面表现更好。
let randomScale = CGFloat(UInt32(arc4random()) % UInt32(5)) / 10
snapshot?.transform = CGAffineTransform.identity.translatedBy(x: randomScale, y: randomScale)
UIView.animate(withDuration: 0.1, delay: delay, options: UIViewAnimationOptions.beginFromCurrentState, animations: {
snapshot?.alpha = 1
}, completion: { _ in
UIView.animate(withDuration: duration, delay: 0, options: .curveEaseOut, animations: {
snapshot?.transform = CGAffineTransform.identity
snapshot?.frame = finalFrame
}, completion: nil)
})
//添加重构动画在(抓拍,延迟时间:延迟,持续时间:持续时间,初步框架:初始帧,最后一帧:一帧)
if shiningColor != nil{
addShiningAnimationOn(snapshot: snapshot!, delayTime: delay, shiningColor: shiningColor!)
}
}
}
//不能依赖于dispatch_after,这不能保证确切的执行时间
self.perform(#selector(cleanUp(snapshots:)), with: snapshots, afterDelay: cleanTime)
}
//MARK: 毁坏
func destruct(){
destructWithDirection()
}
func destructAll(direction: String, animationTime: TimeInterval, ratio: CGFloat){
var direc: SDERefactorDirection!
switch direction {
case "Horizontal":
direc = SDERefactorDirection.Horizontal
case "Vertical":
direc = SDERefactorDirection.Vertical
case "Diagonal":
direc = SDERefactorDirection.Diagonal
default:
direc = SDERefactorDirection.Custom
}
destructWithDirection(direction: direc, animationTime: animationTime, pieceRatio: ratio)
}
/**
它是消失动画的替代方案。 在视图中添加一个破坏动画,并从其超视图中删除它。
- 参数方向:动画方向
- 参数animationTime:动画时间
- 参数比:查看比例。 它适用于宽度和高度两者。
*/
func destructWithDirection(direction: SDERefactorDirection = .Diagonal, animationTime: TimeInterval = 0.5, pieceRatio ratio: CGFloat = 0.05){
guard let _ = self.superview else{
return
}
if direction == .Custom{
//DIY
return
}
let fromViewSnapshot = self.snapshotView(afterScreenUpdates: false)
self.alpha = 0
let origin = self.frame.origin
let size = self.frame.size
let pieceWidth = size.width * ratio
let pieceHeight = size.height * ratio
let (column, row) = columnAndrow(size: size, ratio: ratio)
//Here 0.3 is the animation time of single piece
let totalTime: TimeInterval = animationTime - 0.3
let delayDelta: Double = (direction == .Diagonal) ? totalTime / Double(column + row - 1) : totalTime / Double(column * row)
var snapshots: [UIView] = []
var delay: TimeInterval = 0
var windUpTime: TimeInterval = 0
var index = 0
//CGFloat(0).stride(to: size.height, by: pieceHeight)
for y in stride(from: 0, to: size.height, by: pieceHeight) {
for x in stride(from: 0, to: size.width, by: pieceWidth){
index += 1
var regionWidth = pieceWidth
var regionHeight = pieceHeight
if x + regionWidth > size.width{
regionWidth = size.width - x
}
if y + regionHeight > size.height{
regionHeight = size.height - y
}
let snapshotRegion = CGRect(x: x, y: y, width: regionWidth, height: regionHeight)
let snapshot = fromViewSnapshot?.resizableSnapshotView(from: snapshotRegion, afterScreenUpdates: false, withCapInsets: .zero)
let snapshotFrame = CGRect(x: x + origin.x, y: y + origin.y, width: regionWidth, height: regionHeight)
self.superview?.addSubview(snapshot!)
snapshot?.frame = snapshotFrame
snapshots.append(snapshot!)
switch direction{
case .Horizontal:
let x = index % column == 0 ? column : index % column
let y = (index - x + column) / column
delay = delayDelta * Double(x * row + y)
case .Vertical:
delay = delayDelta * Double(index)
case .Diagonal:
delay = delayDelta * Double(diagonalIndexFor(index: index, columnXrow: (column, row)))
case .Custom: break
}
addAnnihilationAnimationaOn(snapshot: snapshot!, delayTime: delay)
windUpTime = (delay + 0.3 > windUpTime) ? delay + 0.3 : windUpTime
}
}
self.perform(#selector(windUp(snapshots:)), with: snapshots, afterDelay: windUpTime + 0.1)
}
//MARK: Private Helper
private func columnAndrow(size: CGSize, ratio: CGFloat) -> (Int, Int){
/*
你不能依赖ceil,比如 Int(ceil(size.width / width)),有时候是以下结果是+1。
*/
var rowCount = 0
for _ in stride(from: 0, to: size.height, by: size.height * ratio){
rowCount += 1
}
var columnCount = 0
for _ in stride(from: 0, to: size.width, by: size.width * ratio){
columnCount += 1
}
return (columnCount, rowCount)
}
private func diagonalIndexFor(index: Int, columnXrow:(Int, Int)) -> Int{
let (column, _) = columnXrow
let x = index % column == 0 ? column : index % column
let y = (index - x + column) / column
let DiagonalIndex = x + y - 1
return DiagonalIndex
}
/*
- 参数jumpRect:指定所有片段的区域
- 返回:jumpRect与屏幕相交,如果不指定jumpRect,则为UIView的2X框。
*/
private func filterRect(jumpRect: CGRect?) -> CGRect{
var piecesRect = self.frame
let screenRect = UIScreen.main.bounds
if jumpRect != nil{
if screenRect.contains(jumpRect!){
piecesRect = jumpRect!
}else{
if !screenRect.intersection(jumpRect!).isNull{
piecesRect = screenRect.intersection(jumpRect!)
}else{
let bigRect = self.frame.insetBy(dx: -self.frame.size.width/2, dy: -self.frame.size.height/2)
piecesRect = screenRect.intersection(bigRect)
}
}
}else{
let bigRect = self.frame.insetBy(dx: -self.frame.size.width/2, dy: -self.frame.size.height/2)
piecesRect = screenRect.intersection(bigRect)
}
return piecesRect
}
private func snapshotInfo(enableBigRegion: Bool, index: Int, xy: (CGFloat, CGFloat), widthXheight: (CGFloat, CGFloat), columnXrow: (Int, Int)) -> (CGRect, Set<Int>){
/*
这个方法怎么办? 集成分区以减少总动画时间。
_______ _______ ____ ____ _______ _______
|__|__| >>> | | or |__| >>> |__| or |__|__| >>> |_____|
|__|__| >>> |_____| |__| |__|
*/
let (pieceWidth, pieceHeight) = widthXheight
let isBigRegion = enableBigRegion ? Int(UInt32(arc4random()) % UInt32(2)) == 1 : false
var regionWidth = isBigRegion ? 2.0 * pieceWidth : pieceWidth
var regionHeith = isBigRegion ? 2.0 * pieceHeight : pieceHeight
let (regionX, regionY) = xy
let size = self.frame.size
if regionX + regionWidth >= size.width{
regionWidth = size.width - regionX
}
if regionY + regionHeith >= size.height{
regionHeith = size.height - regionY
}
let (column, _) = columnXrow
var ignoreIndexSet: Set<Int> = []
if isBigRegion {
if regionX + pieceWidth < size.width{
ignoreIndexSet.insert(index + 1)
}
if regionY + pieceHeight < size.height{
ignoreIndexSet.insert(index + column)
if regionX + pieceWidth < size.width{
ignoreIndexSet.insert(index + column + 1)
}
}
}
let regionOrigin: CGPoint = CGPoint(x: regionX, y: regionY)
let regionSize: CGSize = CGSize(width: regionWidth, height: regionHeith)
let snapshotRegion = CGRect(origin: regionOrigin, size: regionSize)
return (snapshotRegion, ignoreIndexSet)
}
private func randomRectFrom(sourceRect: CGRect, regionSize: CGSize) -> CGRect {
//现在的方法就像它的名字。
let randomX: CGFloat = sourceRect.origin.x + CGFloat(UInt32(arc4random()) % UInt32(sourceRect.size.width))
let randomY: CGFloat = sourceRect.origin.y + CGFloat(UInt32(arc4random()) % UInt32(sourceRect.size.height))
let initialFrame = CGRect(x: randomX, y: randomY, width: regionSize.width, height: regionSize.height)
return initialFrame
}
private func addRefactorAnimationOn(snapshot: UIView, delayTime: TimeInterval, duration: TimeInterval, initialFrame: CGRect, finalFrame: CGRect){
let opaAni = CABasicAnimation(keyPath: "opacity")
opaAni.fromValue = 0
opaAni.toValue = 1
opaAni.duration = 0.1
opaAni.fillMode = kCAFillModeForwards
opaAni.isRemovedOnCompletion = false
opaAni.beginTime = CACurrentMediaTime() + delayTime
snapshot.layer.add(opaAni, forKey: nil)
let moveAni = CABasicAnimation(keyPath: "position")
moveAni.fromValue = NSValue(cgPoint: initialFrame.centerPoint)
moveAni.toValue = NSValue(cgPoint: finalFrame.centerPoint)
moveAni.duration = duration
moveAni.beginTime = CACurrentMediaTime() + delayTime
moveAni.fillMode = kCAFillModeForwards
moveAni.isRemovedOnCompletion = false
snapshot.layer.add(moveAni, forKey: nil)
}
private func addShiningAnimationOn(snapshot: UIView, delayTime: TimeInterval, shiningColor: UIColor, shadowRadius: CGFloat = 10.0){
snapshot.layer.shadowColor = shiningColor.cgColor
snapshot.layer.shadowRadius = shadowRadius
snapshot.layer.shadowPath = UIBezierPath(rect: snapshot.bounds).cgPath
let opaKeyAni = CAKeyframeAnimation(keyPath: "shadowOpacity")
opaKeyAni.values = [0.0, 0.1, 1.0, 0.8, 0.0]
opaKeyAni.keyTimes = [0.0, 0.5, 0.7, 0.99, 1.0]
opaKeyAni.duration = 0.3
opaKeyAni.beginTime = CACurrentMediaTime() + delayTime
snapshot.layer.add(opaKeyAni, forKey: nil)
}
private func addAnnihilationAnimationaOn(snapshot: UIView, delayTime: TimeInterval){
let delta = CGFloat(UInt32(arc4random()) % UInt32(30))
let end = CGPoint(x: snapshot.center.x - delta, y: snapshot.center.y - delta)
UIView.animate(withDuration: 0.3, delay: delayTime, options: .curveEaseInOut, animations: {
snapshot.alpha = 0
snapshot.center = end
}, completion: nil)
}
@objc private func cleanUp(snapshots: [UIView]){
self.alpha = 1
for snapshot in snapshots{
snapshot.removeFromSuperview()
}
}
@objc private func windUp(snapshots: [UIView]){
self.removeFromSuperview()
for snapshot in snapshots{
snapshot.removeFromSuperview()
}
}
}
OC示例:UIViewController.m文件代码
#import "ArchivesViewController.h"
#import "SafeMedication-Swift.h"
@interface ArchivesViewController ()
@property(nonatomic,strong)UIView * testView;
@end
@implementation ArchivesViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor =[UIColor blackColor];
self.testView =[[UIView alloc]initWithFrame:CGRectMake(self.view.frame.size.width/2-75, 250, 150, 150)];
[self.view addSubview:self.testView];
UIImageView * qrCodeImageView =[[UIImageView alloc]initWithFrame:CGRectMake(0, 0, self.testView.frame.size.width, self.testView.frame.size.height)];
qrCodeImageView.image =[UIImage imageNamed:@"qrCode"];
[self.testView addSubview:qrCodeImageView];
UIButton * xiaoShiBtn =[[UIButton alloc]initWithFrame:CGRectMake(VS_Screen_Width/2-50, 500, 100, 30)];
[xiaoShiBtn setTitle:@"重组" forState:UIControlStateNormal];
[xiaoShiBtn setBackgroundColor:VS_ColerSky_Blue];
[xiaoShiBtn addTarget:self action:@selector(xiaoShiBtnClick:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:xiaoShiBtn];
}
-(void)xiaoShiBtnClick:(UIButton *)btn{
[self.testView refactor];
[self.testView refactorAllWithDestinationFrame:CGRectMake(self.testView.frame.origin.x, self.testView.frame.origin.y, self.testView.frame.size.width, self.testView.frame.size.height) jumpRect:CGRectMake(250, 100, self.testView.frame.size.width, self.testView.frame.size.height) shiningColor:VS_ColerSky_Blue direction:@"Horizontal" animationTime:0.4f ratio:0.05f enableBigRegion:NO];
// [self.testView destruct];
// [self.testView destructAllWithDirection:@"Diagonal" animationTime:0.5f ratio:0.05f];
}
Swift演示:TestViewController.swift文件
import UIKit
@objc
class TestViewController: UIViewController {
private lazy var createView: UIView = {
let createView = UIView(frame: CGRect(x: 100, y: 300, width: 100, height: 100))
createView.backgroundColor = UIColor.blue
return createView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.white
view.addSubview(createView)
let but = UIButton(type: .system)
but.frame = CGRect(x: 0, y: 110, width: 50, height: 30)
but.setTitle("消失", for: .normal)
but.addTarget(self, action: #selector(addd(sender:)), for: .touchUpInside)
view.addSubview(but)
let cutbut = UIButton(type: .system)
cutbut.frame = CGRect(x: 100, y: 110, width: 50, height: 30)
cutbut.setTitle("重组", for: .normal)
cutbut.addTarget(self, action: #selector(cutaddd(sender:)), for: .touchUpInside)
view.addSubview(cutbut)
}
func cutaddd(sender: UIButton){
createView.refactor()
createView.refactorWithNewFrame(destinationFrame: nil, piecesRegion: nil, shiningColor: nil, direction: .Horizontal, refactorTime: 0.6, pieceRatio: 0.05, enableBigRegion:false)
}
func addd(sender:UIButton) {
createView.destruct()
createView.destructWithDirection(direction: .Diagonal, animationTime: 0.5, pieceRatio: 0.05)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}