tvOS视差按钮的ObjC实现

2023-10-07 20:30
文章标签 实现 按钮 tvos objc 视差

本文主要是介绍tvOS视差按钮的ObjC实现,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

介绍

苹果在最新发布的Apple TV里引入了有趣的图标设计
具体说来 图标由2-5个分层图层构成 在图标被选中的时候 图标内每个图层进行不同幅度的位移 从而形成视觉上具有深度距离感的视差效果 图标构成和效果可以见视频: 


这种效果很适合用于多媒体类应用 例如图书或者电影封面 让封面变得立体生动 然而这种效果目前只能在Apple TV的tvOS里见到
所以 如何在iOS上做出同样的效果呢?现在就让我们一起研究下视差按钮的实现原理 并且自己实现一个吧 ^_^ 

原理

假设我们已经有了以下图片:(你可以从下载链接下载已经分层的四张图片)
四张已经分层的图片四张已经分层的图片
基于这四张图片 我们该怎么对其进行变换来达到tvOS的视差效果呢?
重新观察上文中苹果官方的例子视频 我们可以得出以下结论: 

1. 总图层在旋转 但不同于一般在屏幕平面上的旋转 而是相对屏幕具有一定夹角的旋转

如果不太了解这种旋转是怎么发生的 我们可以看一张有关 CATransform3D 的图:
CATransform3DMakeRotation的XYZ轴CATransform3DMakeRotation的XYZ轴
我们常用的 CGRect 有 X 和 Y 两个位置参数 而 CATransform3D 可以理解为在日常的两个轴以外加了 Z 轴 方向为从手机上表面竖直向下 如图
这么一来 我们日常所见的在屏幕平面上的旋转(比如屏幕旋转)其实是绕 Z 轴旋转
而绕 X 和 Y 轴的旋转 便是让 CALayer 具有相对屏幕具有一定夹角的旋转 具体表现就是如同tvOS按钮一样有远近效果(其实在透视效果上还是有些不一样 后面会提怎么解决)
CATransform3DMakeRotation 就是这么通过旋转角度定义的:

1
2
3
4
5
6
CATransform3D CATransform3DMakeRotation (
CGFloat angle, //绕着向量(x,y,z)旋转的角度
CGFloat x, //x轴分量
CGFloat y, //y轴分量
CGFloat z //z轴分量
);

 

2.除去旋转 每个图层都在进行不同半径的圆周移动

为什么有移动?
透视是创造三维深度感觉的关键 而透视效果最直白的话来说 就是“近大远小”
让我们来看个例子吧 :-)
近大远小的说明近大远小的说明
如果只有总图层自转 分图层不进行移动 那么整个按钮虽然有自转效果 但是看起来还是平的
如果要保证有三维效果 就要有视差 即近大远小 让远处的图层移动的距离很小 近处的图层移动距离很大(大家可以自行想象同样速度远处近处的汽车 看起来移动的距离也不一样)
因此 就要令分图层进行圆周移动 离我们近的图层 圆周半径要更大些 保证看起来移动的距离更大
我们简单地用 Principle 做了一个原型 大体效果应该是这个样子的 中间的圆点是移动的轴心
简陋的圆周移动效果简陋的圆周移动效果

3.总的图层也会移动

看到我们刚才那个简陋的效果了没?你有可能会想 为什么看起来和tvOS差别那么大?
原因是 tvOS实现的效果 整个按钮并没有明显地移动 而是近似于固定在某个位置的 这么一来 就要求我们在分图层移动的时候 总图层叠加一项反方向的移动 保证按钮固定住
于是我们又用 Principle 做了一个原型 大体效果应该是这个样子的
还是很简陋的圆周移动效果还是很简陋的圆周移动效果

4.高光的移动方向恰好相反

高光就是我们在tvOS的图标上看到的白色反光 这个部分其实很简单:
用PS画一个白色的圆 加上模糊效果 就是一个 高光图层
让图层在移动的时候于其他图层方向相反 即让图层叠加之后的效果为 高光永远在离我们最近的地方 这里说起来会有点困惑 但是用代码实现的时候就自然明白了 ^_^

实现

注:实现部分限于篇幅 不可能将所有代码都粘贴出来 只是在几个关键的地方粘贴出来加以说明
完整代码见 https://github.com/JustinFincher/JZtvOSParallaxButton

1.按钮基本

按照我们的计划 这个按钮默认并没有三维效果 就是很多UIImage叠加起来 只有当我们长按的时候 才会有各种旋转和移动
这里动画方式分为两种 第一种是自动动画 首先会移动和旋转到一个特定的角度 然后便开始周期移动了 第二种是手动动画 按钮会跟随手指Drag进行旋转和移动
让我们先新建一个UIButton吧 :-)

1
2
3
4
5
6
7
8
9
10
11
//JZParallaxButton.h
# import <UIKit/UIKit.h>

typedef NS_ENUM(NSInteger, RotateMethodType)
{
RotateMethodTypeAutoRotate = 0, //自动动画
RotateMethodTypeWithFinger, //跟随手指
RotateMethodTypeWithFingerReverse, //跟随手指 但反向
};
@interface JZParallaxButton : UIButton
@end

 

1
2
3
4
5
6
//JZParallaxButton.m
# define LongPressInterval 0.5 //自动动画状态下的长按判断时间
@interface UIButton ()<UIGestureRecognizerDelegate>
@end
@implementation JZParallaxButton
@end

写一个自己的init方法 然后在里面加入长按的手势判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//JZParallaxButton.m
- (instancetype)initButtonWithCGRect:(CGRect)RectInfo
WithLayerArray:(NSMutableArray *)ArrayOfLayer
WithRoundCornerEnabled:(BOOL)isRoundCorner
WithCornerRadiusifEnabled:(CGFloat)Radius
WithRotationFrames:(int)Frames
WithRotationInterval:(CGFloat)Interval
{
self = [super initWithFrame:RectInfo];

//...省略...

UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(selfLongPressed:)];
longPress.delegate = self;
longPress.minimumPressDuration = LongPressInterval;
//self就是UIButton了 所以可以对self add
[self addGestureRecognizer:longPress];

return self;
}

 

1
2
3
4
5
6
7
8
//JZParallaxButton.m
//长按会触发的方法
- (void)selfLongPressed:(UILongPressGestureRecognizer *)sender
{
CGPoint PressedPoint = [sender locationInView:self];
NSLog(@"%F , %F",PressedPoint.x,PressedPoint.y);
//可以读取当前按下的位置
}

2.层级关系和逻辑判断

我们的按钮在实现后应有以下层级:
层级关系层级关系

1
2
3
4
5
6
7
8
9
ParallaxButton:UIButton  //我们建立的UIButton SubClass
|-BoundsView:UIView //总视图
|--LayerImageView1:UIImageView //分视图 是总视图的SubView
|--LayerImageView2:UIImageView
|--LayerImageView3:UIImageView
|--LayerImageView4:UIImageView
|--LayerImageView5:UIImageView
|-- ....
|--LayerImageViewX:UIImageView

 

有的同学有可能会觉得 为什么需要总视图这个 BoundsView 呢 直接将所有的UIImageView都划归为UIButton的SubView不就好了么?
实验过直接将UIImageView添加到UIButton为SubView后 我有一个相对合理的解释:
我们之前分析原理的时候 说明其实是只有总图层(即 BoundsView )在旋转的 分图层只需处理移动问题
如果去除了总图层 就只能让每个分图层(即 LayerImageView )在移动的同时都旋转 这势必带来一个问题 那就是会有“厚度”的感觉
让我们实验下 如果层级关系如下图 会是什么结果
错误的显示效果错误的显示效果
可以看到 这里的图层移动方式和原型里的效果已经很接近了 但是因为分图层移动半径不一 并且没有总图层进行约束 导致分图层的显示区域不在一个长方形里 看起来像是有厚度了一样 整个按钮实际看起来并没有tvOS按钮里的那种轻盈感
因此 需要有总图层进行约束 即将分图层添加为总图层的SubView 并设置总图层Layer的MasksToBounds为YES 这时 所有分图层的可见区域都受限制于总图层 无论怎么旋转和移动都不会出现厚度感了
我们现在将视图层级需要的一些变量写出来 然后再实现一些逻辑判断的代码 比如长按后需要做什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//JZParallaxButton.h
# import <UIKit/UIKit.h>

typedef NS_ENUM(NSInteger, ParallaxMethodType)
{
ParallaxMethodTypeLinear = 0,
ParallaxMethodTypeEaseIn,
ParallaxMethodTypeEaseOut,
};

@interface JZParallaxButton : UIButton

//数组用于记录当前Button包含的所有ImageLayer 即分图层
@property (nonatomic,strong) NSMutableArray *LayerArray;

//button圆角
@property (nonatomic,assign) BOOL RoundCornerEnabled;

//button圆角
@property (nonatomic,assign) CGFloat RoundCornerRadius;

//是否在Parallax
@property (nonatomic,assign) BOOL isParallax;

@property (nonatomic,assign) int RotationAllSteps;
@property (nonatomic,assign) CGFloat RotationInterval;

@property (nonatomic,assign) CGFloat ScaleBase;
@property (nonatomic,assign) CGFloat ScaleAddition;

@property (nonatomic,assign) ParallaxMethodType ParallaxMethod;
@property (nonatomic,assign) RotateMethodType RotateMethod;

- (instancetype)initButtonWithCGRect:(CGRect)RectInfo
WithLayerArray:(NSMutableArray *)ArrayOfLayer
WithRoundCornerEnabled:(BOOL)isRoundCorner
WithCornerRadiusifEnabled:(CGFloat)Radius
WithRotationFrames:(int)Frames
WithRotationInterval:(CGFloat)Interval;

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
//JZParallaxButton.m

# define RotateParameter 0.5 //用于调整旋转幅度
# define SpotlightOutRange 20.0f //用于高光距离中心的长度
# define zPositionMax 800 //Core Layer变换时摄像机默认z轴位置

# define BoundsVieTranslation 50 //总图层的移动幅度
# define LayerVieTranslation 20 //分图层的移动幅度
# define LongPressInterval 0.5 //自动动画状态下的长按判断时间

@interface UIButton ()<UIGestureRecognizerDelegate>

@property (nonatomic,assign) int RotationNowStep; //记录动画的当前状态
@property (nonatomic,weak)NSTimer *RotationTimer; //动画计时器
@property (nonatomic,strong) UIImageView *SpotLightView; //高光图层
@property (nonatomic,strong) UIView *BoundsView; //总图层
@property (nonatomic,assign) CGPoint TouchPointInSelf; //手指按下的时候在Button内部 的位置 用于Button设为跟随手指的时候
@property (nonatomic,assign) BOOL hasPreformedBeginAnimation; //判断是否在进行动画 防止动画未表演完就触发下一个动作 造成错位
@end

@implementation JZParallaxButton
//省略 @synthesize ...

- (instancetype)initButtonWithCGRect:(CGRect)RectInfo
WithLayerArray:(NSMutableArray *)ArrayOfLayer
WithRoundCornerEnabled:(BOOL)isRoundCorner
WithCornerRadiusifEnabled:(CGFloat)Radius
WithRotationFrames:(int)Frames
WithRotationInterval:(CGFloat)Interval
{
//省略之前的代码....
LayerArray = [[NSMutableArray alloc] initWithCapacity:[ArrayOfLayer count]];

BoundsView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)];
BoundsView.layer.masksToBounds = YES;
BoundsView.layer.shouldRasterize = TRUE;
BoundsView.layer.rasterizationScale = [[UIScreen mainScreen] scale];
if (self.RoundCornerEnabled)
{
BoundsView.layer.cornerRadius = self.RoundCornerRadius;
}
[self addSubview:BoundsView];


for (int i = 0; i < [ArrayOfLayer count]; i++)
{
UIImageView *LayerImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)];
UIImage *LayerImage = [ArrayOfLayer objectAtIndex:i];
[LayerImageView setImage:LayerImage];
LayerImageView.layer.shouldRasterize = TRUE;
LayerImageView.layer.rasterizationScale = [[UIScreen mainScreen] scale];

//从下往上添加
[BoundsView addSubview:LayerImageView];
[LayerArray addObject:LayerImageView];

//如果把所有分图层都加完了
if (i == [ArrayOfLayer count] - 1)
{
//在最上层添加高光分图层
SpotLightView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, self.frame.size.width,self.frame.size.height)];

NSString *bundlePath = [[NSBundle bundleForClass:[JZParallaxButton class]]
pathForResource:@"JZParallaxButton" ofType:@"bundle"];
NSBundle *bundle = [NSBundle bundleWithPath:bundlePath];

SpotLightView.image = [UIImage imageNamed:@"Spotlight" inBundle:bundle compatibleWithTraitCollection:nil];
SpotLightView.contentMode = UIViewContentModeScaleAspectFit;
SpotLightView.layer.masksToBounds = YES;
[BoundsView addSubview:SpotLightView];
SpotLightView.layer.zPosition = zPositionMax;
[self bringSubviewToFront:SpotLightView];
SpotLightView.alpha = 0.0;
[LayerArray addObject:SpotLightView];
}
}
- (void)selfLongPressed:(UILongPressGestureRecognizer *)sender
{
CGPoint PressedPoint = [sender locationInView:self];
//NSLog(@"%F , %F",PressedPoint.x,PressedPoint.y);
self.TouchPointInSelf = PressedPoint;

if(sender.state == UIGestureRecognizerStateBegan)
{
//NSLog(@"Long Press");

转载于:https://www.cnblogs.com/DMDD/p/4996746.html

这篇关于tvOS视差按钮的ObjC实现的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/160230

相关文章

C++使用栈实现括号匹配的代码详解

《C++使用栈实现括号匹配的代码详解》在编程中,括号匹配是一个常见问题,尤其是在处理数学表达式、编译器解析等任务时,栈是一种非常适合处理此类问题的数据结构,能够精确地管理括号的匹配问题,本文将通过C+... 目录引言问题描述代码讲解代码解析栈的状态表示测试总结引言在编程中,括号匹配是一个常见问题,尤其是在

Java实现检查多个时间段是否有重合

《Java实现检查多个时间段是否有重合》这篇文章主要为大家详细介绍了如何使用Java实现检查多个时间段是否有重合,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录流程概述步骤详解China编程步骤1:定义时间段类步骤2:添加时间段步骤3:检查时间段是否有重合步骤4:输出结果示例代码结语作

使用C++实现链表元素的反转

《使用C++实现链表元素的反转》反转链表是链表操作中一个经典的问题,也是面试中常见的考题,本文将从思路到实现一步步地讲解如何实现链表的反转,帮助初学者理解这一操作,我们将使用C++代码演示具体实现,同... 目录问题定义思路分析代码实现带头节点的链表代码讲解其他实现方式时间和空间复杂度分析总结问题定义给定

Java覆盖第三方jar包中的某一个类的实现方法

《Java覆盖第三方jar包中的某一个类的实现方法》在我们日常的开发中,经常需要使用第三方的jar包,有时候我们会发现第三方的jar包中的某一个类有问题,或者我们需要定制化修改其中的逻辑,那么应该如何... 目录一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理一、需求描述需求描述如下:需要在

如何使用Java实现请求deepseek

《如何使用Java实现请求deepseek》这篇文章主要为大家详细介绍了如何使用Java实现请求deepseek功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1.deepseek的api创建2.Java实现请求deepseek2.1 pom文件2.2 json转化文件2.2

python使用fastapi实现多语言国际化的操作指南

《python使用fastapi实现多语言国际化的操作指南》本文介绍了使用Python和FastAPI实现多语言国际化的操作指南,包括多语言架构技术栈、翻译管理、前端本地化、语言切换机制以及常见陷阱和... 目录多语言国际化实现指南项目多语言架构技术栈目录结构翻译工作流1. 翻译数据存储2. 翻译生成脚本

如何通过Python实现一个消息队列

《如何通过Python实现一个消息队列》这篇文章主要为大家详细介绍了如何通过Python实现一个简单的消息队列,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录如何通过 python 实现消息队列如何把 http 请求放在队列中执行1. 使用 queue.Queue 和 reque

Python如何实现PDF隐私信息检测

《Python如何实现PDF隐私信息检测》随着越来越多的个人信息以电子形式存储和传输,确保这些信息的安全至关重要,本文将介绍如何使用Python检测PDF文件中的隐私信息,需要的可以参考下... 目录项目背景技术栈代码解析功能说明运行结php果在当今,数据隐私保护变得尤为重要。随着越来越多的个人信息以电子形

使用 sql-research-assistant进行 SQL 数据库研究的实战指南(代码实现演示)

《使用sql-research-assistant进行SQL数据库研究的实战指南(代码实现演示)》本文介绍了sql-research-assistant工具,该工具基于LangChain框架,集... 目录技术背景介绍核心原理解析代码实现演示安装和配置项目集成LangSmith 配置(可选)启动服务应用场景

使用Python快速实现链接转word文档

《使用Python快速实现链接转word文档》这篇文章主要为大家详细介绍了如何使用Python快速实现链接转word文档功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 演示代码展示from newspaper import Articlefrom docx import