往期文章
效果图
可以根据传入数组长度控制设备视图的数量(不过一般控制在6个左右不会发生重叠现象,如果传入数量过多会排的很密集o(╯□╰)o)
每一组设备视图出现的位置不是固定的,是随机生成的位置。
每一组设备视图都不会与中间的苹果标志发生碰撞
实现过程
图层架构
首先整个组件由一个布局View组成根View,用来组件中的雷达扩散View,与设备View。
其次雷达扩散View居中放置在布局View中,其宽高等于布局View的宽高除以雷达扩散扩散放大的倍数。这里我们设置的雷达扩散View扩散倍数是1.6,所以雷达扩散View的宽度和高度等于(R雷 = R布/1.6),
iconView同时也放置在布局视图的中心。
最后设备View围绕着iconView,旋转排列。
整体图层构架如下
雷达扩散效果
雷达扩散效果主要是通过缩放视图大小以及修改视图背景颜色来实现的。所以这里我们需要两个动画,一个用来实现视图大小的缩放,一个用来实现视图背景颜色的变化。
这里我们使用CABasicAnimation来实现大小的缩放,scaleFactor为缩放因子也就是我们视图需要扩散的倍数。实现代码如下
//涟漪组合动画
-(CABasicAnimation*) scaleAnimation{
CABasicAnimation* scaleAnimation = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
scaleAnimation.fromValue = @(0.1);
scaleAnimation.toValue = @(self.scaleFactor);
return scaleAnimation;
}
CAKeyframeAnimationl来实现背景颜色的关键帧渐变,这里我们设置了四个插值,当然你可以设置更多的插值来使背景颜色改变更自然。
- (CAKeyframeAnimation*) backgroudColorAnimation {
CAKeyframeAnimation *backgroundColorAnimation = [CAKeyframeAnimation animation];
backgroundColorAnimation.keyPath = @"backgroundColor";
backgroundColorAnimation.values = @[(__bridge id)ColorWithAlpha(83, 150, 230, 0.5).CGColor,
(__bridge id)ColorWithAlpha(172, 206, 245, 0.5).CGColor,
(__bridge id)ColorWithAlpha(196, 221, 248, 0.5).CGColor,
(__bridge id)ColorWithAlpha(227, 239, 251, 0).CGColor,
];
backgroundColorAnimation.keyTimes = @[@0.3,@0.6,@0.9,@1];
return backgroundColorAnimation;
}
最后我们将这两个动画效果组合成我们想要的扩散动画组,并添加到layer层上执行动画。
- (CAAnimationGroup*) rippleAnimation{
CAAnimationGroup* animationGroup = [CAAnimationGroup animation];
animationGroup.duration = animationDuration;
animationGroup.repeatCount = MAXFLOAT;
animationGroup.animations = @[[self scaleAnimation],[self backgroudColorAnimation]];
animationGroup.removedOnCompletion = NO;
animationGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault];
return animationGroup;
}
这里可以看到我们的扩散效果已经成功的添加到动画layer上面了。
为了实现多layer扩散效果我们还需要使用CAReplicatorLayer对当期动画图层进行复制,并设置间隔执行时间,从而完成我们的雷达扩散效果,这里我们加上原本动画图层和两个复制图层一共设置三个图层。并设置每个图层间隔1秒执行,设置代码如下
//使用CAReplicatorLayer对该图层进行复制
- (CAReplicatorLayer*) replicatorLayer:(CGRect) rect {
CAReplicatorLayer* replicatorLayer = [[CAReplicatorLayer alloc] init];
replicatorLayer.instanceCount = rippleCount;
replicatorLayer.instanceDelay = rippleDuration;
[replicatorLayer addSublayer:[self rippleBaseAnimationLayer:rect]];
return replicatorLayer;
}
//生成一个扩散动画图层
- (CALayer *) rippleBaseAnimationLayer:(CGRect) rect {
CALayer* baseLayer = [CALayer layer];
baseLayer.frame = CGRectMake(0, 0, rect.size.width, rect.size.height);
baseLayer.cornerRadius = rect.size.height / 2;
[baseLayer addAnimation:[self rippleAnimation] forKey:@"rippleAnimationGroup"];
return baseLayer;
}
这里可以看到我们的动画图层已经被复制两份,并且三个图层之间按照间隔一秒的时间执行动画
设备视图
设备视图设置比较简单,分为文本区和icon区域,整体大小固定。
这里为了让我们的设备视图被添加的时候,会像泡泡一样从里面冒出来,我们需要给设备视图添加一个弹簧缩放效果。 在IOS9之后,我们不需要自己再去写弹簧效果了,直接使用CASpringAnimation就可以给我们的组件的各种属性(scale,x,y等)添加弹簧效果了,还可以设置弹簧效果的物理属性,例如刚性,初始速度,振幅等,具体代码如下。
- (CABasicAnimation*) bubleAnimation {
CASpringAnimation* animation = [CASpringAnimation animationWithKeyPath:@"transform.scale"];
//起始值
animation.fromValue = @(0);
//结束值
animation.toValue = @(1);
//动画持续时间
animation.duration = 1;
return animation;
}
布局
现在我们将雷达扩散View添加到布局View的中心位置。因为之前我们设置了雷达扩散View的宽高等于布局View的宽高除以缩放因子,这样就保证了扩散动画不会超出布局Viewa的frame。
设备View我们按照以布局View为中点,设备View围绕中点进行旋转排列,其角度如下图所示中点右侧是0度角,中点下方是90度角,中点左侧是180度角,中点上方是270度角。
确定好坐标系之后,我们按照以布局View的center为圆心,设备View的高度0.5再加上iconView的高度0.5和3倍的间隙gap组成一个最小半径,确保设备View不会超出布局View的frame。
为了让设备View呈现不规则的排列,我需要在最小半径的基础上加上一个随机值,让设备View距离中点的距离不总是相同的。同时我们也需要保证设备View和iconView不会发生碰撞以及在布局View的frame之内。
因为最小半径是iconView的和设备View的外切状态下得到的,这样确保设备View不会撞上iconVie,接着判断设备View的左上定点和右上定点是否在布局View中,如果不在布局View中,则使用最小半径确保设备View在布局View的frame之内。实现代码如下
//根据基本半径生成随机半径,并且判断生成的新半径是否在矩形范围内
- (CGPoint) randomRadius:(CGPoint)center andWithAngle:(CGFloat) angle andWithRadius:(CGFloat)radius {
CGFloat baseR = radius;
//随机半径
radius = baseR+arc4random()%(6*gap);
CGPoint point = [self calcCircleCoordinateWithCenter:center andWithAngle:angle andWithRadius:radius];
if (![self isInRect:point]) {
radius = baseR;
point = [self calcCircleCoordinateWithCenter:center andWithAngle:angle andWithRadius:radius];
}
return point;
}
接下来我们需要得到每个设备View自身所在的角度。我们使用一个圆周360除以设备View的总数,比如我们现在需要布局5个设View,就使用 360 / 5 ,将一个圆周分为5等分。假设第一个设备View所在的角度是0度,那么第二个是72度,第三个是144度,第四个是216度,第五个是288度。
有了半径,中心点,对应的角度这三个因素,我们就可以算出每个设备View的center。
根据简单的三角函数公式
centerX = R * cos(α * π/180) centerY = R * sin(α * π/180)
现在我们将所推导的数学公式转化为OC代码。
- (CGPoint)calcCircleCoordinateWithCenter:(CGPoint)center andWithAngle:(CGFloat) angle andWithRadius:(CGFloat)radius{
CGFloat x2 = radius * cosf(angle * M_PI/180);
CGFloat y2 = radius * sinf(angle * M_PI/180);
return CGPointMake(center.x+x2, center.y + y2);
}
这样一来我们的设备视图就能按照相等的角度差,围绕着圆心排列。并且距离圆心的距离不总是相等的。
可这样看上去设备View出现的规律性依旧很强,index0总是在0度角。所以这里我们让第一个设备View的角度不从0开始。让开始角从360度中随机生成一个角度。
CGFloat startAngle = arc4random() % 360;
这样一运行,看上去就舒服多了
Demo
最后贴上Demo地址,喜欢的同学可以给我点一颗星。
如果你有更好的实现方案,欢迎给我留言一起讨论。我会及时回复