1、父类
#import <UIKit/UIKit.h>
@interface WPFBaseView : UIView
/** 方块视图 */
@property (nonatomic, weak) UIImageView *boxView;
/** 仿真者 */
@property (nonatomic, strong) UIDynamicAnimator *animator;
@end
#import "WPFBaseView.h"
@implementation WPFBaseView
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
// 设置背景
self.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"BackgroundTile"]];
// 设置方块
UIImageView *boxView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"Box1"]];
boxView.center = CGPointMake(200, 220);
[self addSubview:boxView];
self.boxView = boxView;
// 初始化仿真者
UIDynamicAnimator *animator = [[UIDynamicAnimator alloc] initWithReferenceView:self];
self.animator = animator;
}
return self;
}
#import "WPFBaseView.h"
@interface WPFSnapView : WPFBaseView
@end
#import "WPFSnapView.h"
@implementation WPFSnapView
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 0. 触摸之前要清零之前的吸附事件
[self.animator removeAllBehaviors];
// 1. 获取触摸对象
UITouch *touch = [touches anyObject];
// 2. 获取触摸点
CGPoint loc = [touch locationInView:self];
// 3 添加吸附事件
UISnapBehavior *snap = [[UISnapBehavior alloc] initWithItem:self.boxView snapToPoint:loc];
// 改变震动幅度,0表示振幅最大,1振幅最小
snap.damping = 0.5;
// 4. 将吸附事件添加到仿真者行为中
[self.animator addBehavior:snap];
}
@end
#import "WPFBaseView.h"
@interface WPFPushView : WPFBaseView
@end
#import "WPFPushView.h"
@interface WPFPushView ()
{
UIImageView *_smallView;
UIPushBehavior *_push;
CGPoint _firstPoint;
CGPoint _currentPoint;
}
@end
@implementation WPFPushView
// 重写init 方法
- (instancetype)init {
if (self = [super init]) {
// 1. 添加蓝色view
UIView *blueView = [[UIView alloc] initWithFrame:CGRectMake(150, 300, 20, 20)];
blueView.backgroundColor = [UIColor blueColor];
[self addSubview:blueView];
// 2. 添加图片框,拖拽起点
UIImageView *smallView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"AttachmentPoint_Mask"]];
smallView.hidden = YES;
[self addSubview:smallView];
_smallView = smallView;
// 3. 添加推动行为
UIPushBehavior *push = [[UIPushBehavior alloc] initWithItems:@[self.boxView] mode:UIPushBehaviorModeInstantaneous];
[self.animator addBehavior:push];
_push = push;
// 4. 增加碰撞检测
UICollisionBehavior *collision = [[UICollisionBehavior alloc] initWithItems:@[blueView, self.boxView]];
collision.translatesReferenceBoundsIntoBoundary = YES;
[self.animator addBehavior:collision];
// 5. 添加拖拽手势
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panAction:)];
[self addGestureRecognizer:pan];
}
return self;
}
// 监听开始拖拽的方法
- (void)panAction:(UIPanGestureRecognizer *)pan {
// 如果是刚开始拖拽,则设置起点处的小圆球
if (pan.state == UIGestureRecognizerStateBegan) {
_firstPoint = [pan locationInView:self];
_smallView.center = _firstPoint;
_smallView.hidden = NO;
// 当前拖拽行为正在移动
} else if (pan.state == UIGestureRecognizerStateChanged) {
_currentPoint = [pan locationInView:self];
[self setNeedsDisplay];
// 当前拖拽行为结束
} else if (pan.state == UIGestureRecognizerStateEnded){
// 1. 计算偏移量
CGPoint offset = CGPointMake(_currentPoint.x - _firstPoint.x, _currentPoint.y - _firstPoint.y);
// 2. 计算角度
CGFloat angle = atan2(offset.y, offset.x);
// 3. 计算距离
CGFloat distance = hypot(offset.y, offset.x);
// 4. 设置推动的大小、角度
_push.magnitude = distance;
_push.angle = angle;
// 5. 使单次推行为有效
_push.active = YES;
// 6. 将拖拽的线隐藏
_firstPoint = CGPointZero;
_currentPoint = CGPointZero;
// 2. 将起点的小圆隐藏
_smallView.hidden = YES;
[self setNeedsDisplay];
}
}
- (void)drawRect:(CGRect)rect {
// 1. 开启上下文对象
CGContextRef ctxRef = UIGraphicsGetCurrentContext();
// 2 创建路径对象
CGContextMoveToPoint(ctxRef, _firstPoint.x, _firstPoint.y);
CGContextAddLineToPoint(ctxRef, _currentPoint.x, _currentPoint.y);
// 3. 设置线宽和颜色
CGContextSetLineWidth(ctxRef, 10);
CGContextSetLineJoin(ctxRef, kCGLineJoinRound);
CGContextSetLineCap(ctxRef, kCGLineCapRound);
[[UIColor colorWithRed:arc4random_uniform(256) / 255.0 green:arc4random_uniform(256) / 255.0 blue:arc4random_uniform(256) / 255.0 alpha:1.0] setStroke];
// 4. 渲染
CGContextStrokePath(ctxRef);
}
@end
#import "WPFBaseView.h"
@interface WPFAttachmentView : WPFBaseView
@property (nonatomic, strong) UIAttachmentBehavior *attachment;
@end
#import "WPFAttachmentView.h"
@interface WPFAttachmentView ()
{
// 附着点图片框
UIImageView *_anchorImgView;
// 参考点图片框(boxView 内部)
UIImageView *_offsetImgView;
}
@end
@implementation WPFAttachmentView
- (instancetype)init {
if (self = [super init]) {
// 1. 设置boxView 的中心点
self.boxView.center = CGPointMake(200, 200);
// 2. 添加附着点
CGPoint anchorPoint = CGPointMake(200, 100);
UIOffset offset = UIOffsetMake(20, 20);
// 3. 添加附着行为
UIAttachmentBehavior *attachment = [[UIAttachmentBehavior alloc] initWithItem:self.boxView offsetFromCenter:offset attachedToAnchor:anchorPoint];
[self.animator addBehavior:attachment];
self.attachment = attachment;
// 4. 设置附着点图片(即直杆与被拖拽图片的连接点)
UIImage *image = [UIImage imageNamed:@"AttachmentPoint_Mask"];
UIImageView *anchorImgView = [[UIImageView alloc] initWithImage:image];
anchorImgView.center = anchorPoint;
[self addSubview:anchorImgView];
_anchorImgView = anchorImgView;
// 3. 设置参考点
_offsetImgView = [[UIImageView alloc] initWithImage:image];
CGFloat x = self.boxView.bounds.size.width * 0.5 + offset.horizontal;
CGFloat y = self.boxView.bounds.size.height * 0.5 + offset.vertical;
_offsetImgView.center = CGPointMake(x, y);
[self.boxView addSubview:_offsetImgView];
// 4. 增加拖拽手势
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panAction:)];
[self addGestureRecognizer:pan];
}
return self;
}
// 拖拽的时候会调用的方法
- (void)panAction:(UIPanGestureRecognizer *)pan {
// 1. 获取触摸点
CGPoint loc = [pan locationInView:self];
// 2. 修改附着行为的附着点
_anchorImgView.center = loc;
self.attachment.anchorPoint = loc;
// 3. 进行重绘
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect {
// 1. 获取路径
UIBezierPath *bezierPath = [UIBezierPath bezierPath];
// 2. 划线
[bezierPath moveToPoint:_anchorImgView.center];
CGPoint p = [self convertPoint:_offsetImgView.center fromView:self.boxView];
[bezierPath addLineToPoint:p];
bezierPath.lineWidth = 6;
// 3. 渲染
[bezierPath stroke];
}
@end
#import "WPFBaseView.h"
#import "WPFAttachmentView.h"
@interface WPFSpringView : WPFAttachmentView
@end
#import "WPFSpringView.h"
@implementation WPFSpringView
/*
* KVO键值监听(观察者)->观察者模式
* 如果没有观察者,需要我们自己定时去查看状态,对应的是【轮询】,观察者是替代我们解决轮询的问题。
* 观察者模式的性能并不好,在实际开发中,要慎用!
* 用完观察者最后要释放观察者
* KVO参数说明:
* 1> 观察者,谁来负责"对象"的"键值"变化的观察
* 2> 观察的键值
* 3> 数值发生变化时,通知哪一个数值变化。
* 4> 通常是nil,要传也可以传一个字符串
* 监听方法说明:
* 1> 观察的键值
* 2> 观察的对象
* 3> 数值的新旧内容,取决于定义观察者时的选项
* 4> 定义观察者时设置的上下文
*/
- (instancetype)init {
if (self = [super init]) {
// 振幅
//self.attachment.damping = 1.0f;
// 频率(让线具有弹性)
self.attachment.frequency = 1.0f;
UIGravityBehavior *gravity = [[UIGravityBehavior alloc] initWithItems:@[self.boxView]];
[self.animator addBehavior:gravity];
// 利用KVO监听方块中心点的改变
/**
self.boxView 被监听的对象
observer 监听者
keypath 监听的键值
options 监听什么值
*/
[self.boxView addObserver:self forKeyPath:@"center" options:NSKeyValueObservingOptionNew context:nil];
}
return self;
}
// 监听,当boxView 的中心店改变时就进行冲重绘
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
[self setNeedsDisplay];
}
#warning 销毁的时候要移除监听
- (void)dealloc {
[self.boxView removeObserver:self forKeyPath:@"center"];
}
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
// Drawing code
}
*/
@end
#import "WPFBaseView.h"
@interface WPFCollisionView : WPFBaseView
@end
#import "WPFCollisionView.h"
@interface WPFCollisionView () <UICollisionBehaviorDelegate>
@end
@implementation WPFCollisionView
- (instancetype)init {
if (self = [super init]) {
self.boxView.center = CGPointMake(190, 0);
// 1. 添加重力行为
UIGravityBehavior *gravity = [[UIGravityBehavior alloc] initWithItems:@[self.boxView]];
[self.animator addBehavior:gravity];
// 2. 边缘检测
// 如果把红色view 也加边缘检测,则碰撞后红色View 也会被碰掉,因此要手动添加边界
UICollisionBehavior *collision = [[UICollisionBehavior alloc] initWithItems:@[self.boxView]];
// 让碰撞的行为生效
collision.translatesReferenceBoundsIntoBoundary = YES;
collision.collisionDelegate = self;
// 3. 添加一个红色view
UIView *redView = [[UIView alloc] initWithFrame:CGRectMake(0, 350, 180, 30)];
redView.backgroundColor = [UIColor redColor];
[self addSubview:redView];
// 4. 手动添加边界
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRect:redView.frame];
[collision addBoundaryWithIdentifier:@"redBoundary" forPath:bezierPath];
[self.animator addBehavior:collision];
// 5. 物体的属性行为
UIDynamicItemBehavior *item = [[UIDynamicItemBehavior alloc] initWithItems:@[self.boxView]];
// 设置物体弹性,振幅
item.elasticity = 0.8;
[self.animator addBehavior:item];
}
return self;
}
#pragma mark - UICollisionBehaviorDelegate
// 在碰撞的时候调用
- (void)collisionBehavior:(UICollisionBehavior *)behavior beganContactForItem:(id<UIDynamicItem>)item withBoundaryIdentifier:(id<NSCopying>)identifier atPoint:(CGPoint)p {
NSLog(@"%@", NSStringFromCGPoint(p));
// UIView *view = (UIView *)item;
// view.backgroundColor = [UIColor colorWithRed:arc4random_uniform(256)/255.0 green:arc4random_uniform(256)/255.0 blue:arc4random_uniform(256)/255.0 alpha:1.0];
}
@end
2、视图控制器类
#import <UIKit/UIKit.h>
typedef enum {
kDemoFunctionSnap = 0,
kDemoFunctionPush,
kDemoFunctionAttachment,
kDemoFunctionSpring,
kDemoFunctionCollision
} kDemoFunction;
@interface WPFDemoController : UIViewController
/** 功能类型 */
@property (nonatomic, assign) kDemoFunction function;
@end
#import "WPFDemoController.h"
#import "WPFSnapView.h"
#import "WPFPushView.h"
#import "WPFAttachmentView.h"
#import "WPFSpringView.h"
#import "WPFCollisionView.h"
@interface WPFDemoController ()
@end
@implementation WPFDemoController
- (void)viewDidLoad {
[super viewDidLoad];
WPFBaseView *baseView = nil;
// 根据不同的功能类型选择不同的视图
// 运用了多态
switch (self.function) {
case kDemoFunctionSnap:
baseView = [[WPFSnapView alloc] init];
break;
case kDemoFunctionPush:
baseView = [[WPFPushView alloc] init];
break;
case kDemoFunctionAttachment:
baseView = [[WPFAttachmentView alloc] init];
break;
case kDemoFunctionSpring:
baseView = [[WPFSpringView alloc] init];
break;
case kDemoFunctionCollision:
baseView = [[WPFCollisionView alloc] init];
break;
default:
break;
}
baseView.frame = self.view.bounds;
[self.view addSubview:baseView];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
/*
#pragma mark - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
// Get the new view controller using [segue destinationViewController].
// Pass the selected object to the new view controller.
}
*/
@end
3、调用
#import "ViewController.h"
#import "WPFDemoController.h"
@interface ViewController ()
{
NSArray *_dynamicArr;
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_dynamicArr = @[@"吸附行为", @"推动行为", @"刚性附着行为", @"弹性附着行为", @"碰撞检测"];
self.tableView.tableFooterView = [[UIView alloc] init];
}
// 几组
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
// 几行
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 5;
}
// 每行的具体内容
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// 1. 设置可重用id
NSString *identifier = @"helloCell";
// 2. 根据可重用id 去tableView 的缓存区去找
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
// 3. 如果找不到,就重新实例化一个
if (nil == cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
cell.textLabel.text = _dynamicArr[indexPath.row];
return cell;
}
// 执行代理方法
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
// 1. 实例化一个仿真管理器
WPFDemoController *demoVc = [[WPFDemoController alloc] init];
// 2. 设置标题
demoVc.title = _dynamicArr[indexPath.row];
// 3. 传递功能类型
demoVc.function = (int)indexPath.row;
// 4. 跳转界面
[self.navigationController pushViewController:demoVc animated:YES];
}
@end
demo地址https://github.com/belink-QD/Dynamic