结合《A Guide To iOS Animation》该书学习iOS动画。
贝塞尔曲线扫盲
该文章通熟易懂,解释了“贝塞尔曲线”这个词。
“小球由 弧AB,弧BC,弧CD,弧DA 组成。
其中 弧AB 是一条由 A,B 加两个控制点 C1,C2 一共四个点绘制的三次贝塞尔曲线。其他弧线段同理。”
“代码中的 (CGContextRef)ctx 字面意思是指上下文,你可以理解为一块全局的画布。也就是说,一旦在某个地方改了画布的一些属性,之后任何地方使用该画布属性都是改了之后的。比如上面在 //1 中把线条样式改成了虚线,那么在下文 //2 中如果不恢复成连续的直线,那么画出来的依然是虚线样式。”
CGFloat dash[] = {5.0, 5.0};
CGContextSetLineDash(ctx, 0.0, dash, 2); //1,设置边框正方形虚线
.....
CGContextSetLineDash(ctx, 0, NULL, 0); //2,设置圆形边框为实线
...
CGFloat dash2[] = {2.0, 2.0};
CGContextSetLineDash(ctx, 0.0, dash2, 2);//设置Ac1c2Bc3c4Cc5c6Dc7c8这个多边形的边框虚线
绘制部分
-(void)drawInContext:(CGContextRef)ctx{
//A-C1、B-C2... 的距离,当设置为正方形边长的1/3.6倍时,画出来的圆弧完美贴合圆形
CGFloat offset = self.outsideRect.size.width / 3.6;
//A.B.C.D实际需要移动的距离.系数为滑块偏离中点0.5的绝对值再乘以2.当滑到两端的时候,movedDistance为最大值:「外接矩形宽度的1/6」.
CGFloat movedDistance = (self.outsideRect.size.width * 1 / 6) * fabs(self.progress-0.5)*2;
//方便下方计算各点坐标,先算出外接矩形的中心点坐标
CGPoint rectCenter = CGPointMake(self.outsideRect.origin.x + self.outsideRect.size.width/2 , self.outsideRect.origin.y + self.outsideRect.size.height/2);
CGPoint pointA = CGPointMake(rectCenter.x ,self.outsideRect.origin.y + movedDistance);
CGPoint pointB = CGPointMake(self.movePoint == POINT_D ? rectCenter.x + self.outsideRect.size.width/2 : rectCenter.x + self.outsideRect.size.width/2 + movedDistance*2 ,rectCenter.y);
CGPoint pointC = CGPointMake(rectCenter.x ,rectCenter.y + self.outsideRect.size.height/2 - movedDistance);
CGPoint pointD = CGPointMake(self.movePoint == POINT_D ? self.outsideRect.origin.x - movedDistance*2 : self.outsideRect.origin.x, rectCenter.y);
//.......
}
“offset 指的是 A-C1,B-C2... 的距离,当该值设置为正方形边长的 1/3.6 倍时,画出来的圆弧近似贴合 1/4 圆。为什么是 3.6 ?这里 有一篇文章。文章里三阶贝塞尔曲线拟合 1/4 圆的时候最佳参数 h=0.552, 表示的意义是:当正方形边长的 1/2 为 1 ( 即正方形边长为 2) 时, offset 等于 0.552 就能使圆弧近似贴近 1/4 圆。所以比例系数为 1/0.552 ,即正方形边长和 offset 的比例系数为:2/0.552 = 3.623。近似于 3.6。其实还有种更直观的近似方法:如果圆心为 O,OC1, OC2 就一定是三等分点,也就是夹角为 30°,那么 AC1 (也就是 offset )就等于 1/2的边长 * tan30° 。
顺便解释两个地方.“1.为什么要*2? 因为为了让 D 点区别于 A 点,让 D 移动距离比 A 多,你完全可以 3 ,2.5,但是不能移动太远。2.为什么要是1/6?这个 1/6 也是自己定的。你可以让 A 移动 1/7 ,1/10 都可以,但是最好不要太靠近 1/2,这时 A 点就移到中点了,形变的样子就不好看了。”
http://kittenyang.com/drawablebubble/
摘录来自: 杨骑滔(KittenYang). “A GUIDE TO IOS ANIMATION”。 iBooks.
未读气泡拖拽消失效果
“这个交互中,难点在于如何绘制贝塞尔曲线。而线又是由点组成的。所以最终归根结底我们还是要找个关键点的坐标。一图胜千言。下面,我绘制了一幅分析图,这样一来问题就转化成了一个高中数学求点坐标的题目了。”
“在panGesture 中的UIPanGestureStateChanged中去实时重绘贝塞尔曲线”
- (void)displayLinkAction {
x1 = backView.center.x;
y1 = backView.center.y;
x2 = self.frontView.center.x;
y2 = self.frontView.center.y;
centerDistance = sqrtf((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
if (centerDistance == 0) {
cosDigree = 1;
sinDigree = 0;
}else {
cosDigree = (y2 - y1) / centerDistance;
sinDigree = (x2 - x1) / centerDistance;
}
//centerDistance / self.viscosity 拖拽过程中,原先圆球半径渐渐变小,
//这里减去一个两球圆心距离与系数viscosity的比
r1 = oldBackViewFrame.size.width / 2 - centerDistance / self.viscosity;
pointA = CGPointMake(x1 - r1 * cosDigree, y1 + r1 * sinDigree); // A
pointB = CGPointMake(x1 + r1 * cosDigree, y1 - r1 * sinDigree); // B
pointD = CGPointMake(x2 - r2 * cosDigree, y2 + r2 * sinDigree); // D
pointC = CGPointMake(x2 + r2 * cosDigree, y2 - r2 * sinDigree); // C
pointO = CGPointMake(pointA.x + (centerDistance / 2) * sinDigree,
pointA.y + (centerDistance / 2) * cosDigree);
pointP = CGPointMake(pointB.x + (centerDistance / 2) * sinDigree,
pointB.y + (centerDistance / 2) * cosDigree);
[self drawRect];
}
在拖拽过程中,等移动完成后,会移除上一次 layer [shapeLayer removeFromSuperlayer];手势每次开始移动,在displayLinkAction方法中重新绘制贝塞尔曲线。
- (void)handleDragGesture:(UIPanGestureRecognizer *)ges {
CGPoint dragPoint = [ges locationInView:self.containerView];
if (ges.state == UIGestureRecognizerStateBegan) {
backView.hidden = NO;
fillColorForCute = self.bubbleColor;
[self RemoveAniamtionLikeGameCenterBubble];
}else if (ges.state == UIGestureRecognizerStateChanged) {
self.frontView.center = dragPoint;
if (r1 <= 6) {
fillColorForCute = [UIColor clearColor];
backView.hidden = YES;
[shapeLayer removeFromSuperlayer];
}
[self displayLinkAction];
}else if (ges.state == UIGestureRecognizerStateEnded ||
ges.state == UIGestureRecognizerStateCancelled ||
ges.state == UIGestureRecognizerStateFailed) {
if (r1 <= 6) {
[UIView animateWithDuration:3.0f
animations:^{
if (self.backgroundColor == [UIColor whiteColor]) {
self.backgroundColor = [UIColor blackColor];
}
else {
self.backgroundColor = [UIColor whiteColor];
}
}
completion:^(BOOL finished) {
if (finished) {
self.frontView.layer.opacity = 1;
backView.hidden = YES;
fillColorForCute = [UIColor clearColor];
[shapeLayer removeFromSuperlayer];
self.frontView.center = oldBackViewCenter;
[self AddAniamtionLikeGameCenterBubble];
}
}
];
}else {
backView.hidden = YES;
fillColorForCute = [UIColor clearColor];
[shapeLayer removeFromSuperlayer];//移除
[UIView animateWithDuration:0.5
delay:0.0f
usingSpringWithDamping:0.4f
initialSpringVelocity:0.0f
options:UIViewAnimationOptionCurveEaseInOut
animations:^{
self.frontView.center = oldBackViewCenter;
} completion:^(BOOL finished) {
if (finished) {
[self AddAniamtionLikeGameCenterBubble];
}
}];
}
}
}
效果
QQ中未读气泡拖拽消失的实现分析