WaveViewDemo
a demo about trigonometric function
Install / Use
/learn @JiYuwei/WaveViewDemoREADME
WaveViewDemo
a demo about trigonometric function #####基础概念
正弦函数公式:y = Asin(ωx+φ)+k
不知道各位同学还记不记得高中数学里学过的这个函数,它的图形是一条波浪线,它的参数含义如下:
/*
y = Asin(ωx+φ)+k
A表示振幅,使用这个变量来调整波浪的高度
ω表示频率,使用这个变量来调整波浪密集度
φ表示初相,使用这个变量来调整波浪初始位置
k表示高度,使用这个变量来调整波浪在屏幕中y轴的位置。
*/
还有一个参数 T 表示周期,这个参数用来确定函数图像重复的最小单位; T = 2π/ω 为了可以更加直观的理解,这里给出函数图像:

A = 1,ω = 1,φ = 0,k = 0 时,函数图像如上图所示(一个周期)。通过修改这4个参数,我们可以画出任意一条想要的波浪线。
#####CALayer坐标系
CALayer中,坐标系的原点在左上角,也就是说我们屏幕上任意一个视图的坐标系看起来是这样的:

想要获得和预览图中一样的效果,我们的参数需要这样设置:
- A = 视图高度/2
- ω = 2π/视图宽度 (将周期设为宽度,可根据需要自行调整)
- φ = 0
- k = A
第二条波浪线参数只需要将 φ 增加或减少 π ,即可将两条曲线的峰顶与峰底错开,看起来就像这样:
红色区域即为波浪视图的宽高

#####开始封装
理解了基础的概念,下面我们开始封装一个JYWaveView用来显示这个波形;首先我们先创建这样一个页面:

个人页面上方使用一个自定义View作为tableview的headerView,下面是cell。
创建一个JYWaveView类,继承于UIView:
#import <UIKit/UIKit.h>
typedef NS_ENUM(NSInteger, WaveDirectionType){
WaveDirectionTypeFoward = -1, //从左到右
WaveDirectionTypeBackWard = 1 //从右到左
};
@interface JYWaveView : UIView
@property (nonatomic, strong) UIColor *frontColor; //外层波形颜色,默认黑色
@property (nonatomic, strong) UIColor *insideColor; //内层波形颜色,默认灰色
@property (nonatomic, assign) CGFloat frontSpeed; //外层波形移动速度,默认0.01;
@property (nonatomic, assign) CGFloat insideSpeed; //内层波形移动速度,默认0.01 * 1.2;
@property (nonatomic, assign) CGFloat waveOffset; //两层波形初相差值,默认M_PI;
@property (nonatomic, assign) WaveDirectionType directionType; //移动方向,默认从右到左;
@end
我们在头文件中声明一些可以自定义的属性,如波形颜色、移动速度、移动方向等。
接下来是.m文件:
#import "JYWaveView.h"
@implementation JYWaveView
{
CGFloat waveWidth;
CGFloat waveHeight;
CGFloat waveA; // A
CGFloat waveW; // ω
CGFloat offsetF; // φ firstLayer
CGFloat offsetS; // φ secondLayer
CGFloat currentK; // k
CGFloat waveSpeedF; // 外层波形移动速度
CGFloat waveSpeedS; // 内层波形移动速度
WaveDirectionType direction; //移动方向
}
由于并不是所有的属性都支持自定义,为防止出问题,我们在绘图过程中使用私有的成员变量,而不直接使用属性。当外部属性发生改变时,我们可以重写属性的setter方法对相应的成员变量进行修改。
-(void)configWaveProperties
{
_frontColor = [UIColor blackColor];
_insideColor = [UIColor grayColor];
_frontSpeed = 0.01;
_insideSpeed = 0.01 * 1.2;
_waveOffset = M_PI;
_directionType = WaveDirectionTypeBackWard;
}
-(void)createWaves
{
waveWidth = self.frame.size.width;
waveHeight = self.frame.size.height;
waveA = waveHeight / 2;
waveW = (M_PI * 2 / waveWidth) / 1.5;
offsetF = 0;
offsetS = offsetF + _waveOffset;
currentK = waveHeight / 2;
waveSpeedF = _frontSpeed;
waveSpeedS = _insideSpeed;
direction = _directionType;
}
变量初始化完成,下面我们使用CAShapeLayer绘制波浪线。 #####CAShapeLayer CAShapeLayer继承自CALayer,属于CoreAnimation框架,可以使用CALayer的所有属性值,其动画渲染直接提交到手机的GPU当中,相较于view的drawRect方法使用CPU渲染而言,其效率极高,能大大优化内存使用情况。关于CAShaperLayer的详细介绍可以移步 CAShapeLayer简单介绍
首先创建两个waveLayer:
@property(nonatomic,strong)CAShapeLayer *frontWaveLayer;
@property(nonatomic,strong)CAShapeLayer *insideWaveLayer;
_frontWaveLayer = [CAShapeLayer layer];
_frontWaveLayer.fillColor = _frontColor.CGColor; //设置填充颜色
[self.layer addSublayer:_frontWaveLayer];
_insideWaveLayer = [CAShapeLayer layer];
_insideWaveLayer.fillColor = _insideColor.CGColor;
[self.layer insertSublayer:_insideWaveLayer below:_frontWaveLayer]; //将第二个放在第一个下面
然后我们根据正弦函数公式画出两个波形:
-(void)drawCurrentWaveWithLayer:(CAShapeLayer *)waveLayer offset:(CGFloat)offset
{
CGMutablePathRef path = CGPathCreateMutable();
CGFloat y = currentK;
CGPathMoveToPoint(path, nil, 0, y); //将点移动到坐标(0,k)
//以1个像素为单位,[0,视图宽度]为定义域,遍历函数中所有的点,将点连成线
for (NSInteger i = 0; i <= waveWidth; i++) {
y = waveA * sin(waveW * i + offset) + currentK;
CGPathAddLineToPoint(path, nil, i, y);
}
CGPathAddLineToPoint(path, nil, waveWidth, waveHeight); //将函数末尾与视图右下角相连
CGPathAddLineToPoint(path, nil, 0, waveHeight); //连线到视图左下角
CGPathCloseSubpath(path); //将当前点与起点相连并关闭path
waveLayer.path = path; //设置path
CGPathRelease(path);
}
然后我们在初始化方法中调用一下方法,两条波形就画出来了:
[self drawCurrentWaveWithLayer:_frontWaveLayer offset:offsetF];
[self drawCurrentWaveWithLayer:_insideWaveLayer offset:offsetS];
在tableview的headerView中,创建一个JYWaveView的实例:
@property(nonatomic,strong)JYWaveView *doubleWaveView;
-(void)setUpWaveView
{
_doubleWaveView = [[JYWaveView alloc] initWithFrame:CGRectMake(0, self.bounds.size.height - 10, self.bounds.size.width, 10)];
[self addSubview:_doubleWaveView];
}
效果如图:

修改一下波形的颜色:
_doubleWaveView.frontColor = [UIColor whiteColor];
_doubleWaveView.insideColor = [UIColor colorWithRed:0.4 green:0.78 blue:0.68 alpha:1];
-(void)setFrontColor:(UIColor *)frontColor
{
if (_frontColor != frontColor) {
_frontColor = frontColor;
_frontWaveLayer.fillColor = _frontColor.CGColor;
}
}
-(void)setInsideColor:(UIColor *)insideColor
{
if (_insideColor != insideColor) {
_insideColor = insideColor;
_insideWaveLayer.fillColor = _insideColor.CGColor;
}
}

波形画好了,那我们如何让他动起来呢?
#####CADisplayLink
也许你不知道什么是CADisplayLink,但你应该知道NSTimer,CADisplayLink与NSTimer一样也是一个定时器,不过不同的是,这是一个可以和屏幕刷新率相同的频率将内容画到屏幕上的定时器。 使用方式与NSTimer类似,创建一个新的 CADisplayLink 对象,把它添加到一个runloop中,并给它提供一个 target 和selector 在屏幕刷新的时候调用。 关于CADisplayLink的详细介绍可以移步 什么是CADisplayLink
我们让波形动起来的原理就是使用CADisplayLink定时调用一个方法,在这个方法里改变波形的初相(φ)后进行重绘,由于重绘频率非常快(CADisplayLink默认帧率为60fps,即每1/60秒重绘一次)看上去就像波形在以某个速度平滑移动。
在JYWaveView中,创建一个CADisplayLink
@property(nonatomic,strong)CADisplayLink *waveDisplayLink;
_waveDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(refreshCurrentWave:)];
[_waveDisplayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
这里runloop的Mode选择NSRunLoopCommonModes以保证我们滑动屏幕时波形的移动不会被暂停,原因这里不作详细解释,想要了解的同学可以移步 Runloop学习笔记
然后我们实现-refreshCurrentWave:
-(void)refreshCurrentWave:(CADisplayLink *)displayLink
{
offsetF += waveSpeedF * direction; //direction为枚举值,正向为-1,逆向为1,通过改变符号改变曲线的移动方向
offsetS += waveSpeedS * direction;
//将之前创建曲线的方法移到这里
[self drawCurrentWaveWithLayer:_frontWaveLayer offset:offsetF];
[self drawCurrentWaveWithLayer:_insideWaveLayer offset:offsetS];
}
好了,现在波浪线可以动起来了。当然我们还可以对速度和方向进行定制:
_doubleWaveView.frontSpeed = 0.01;
_doubleWaveView.insideSpeed = 0.01; //让两条曲线的速度保持一致
_doubleWaveView.waveOffset = M_PI / 2; //更改两条波形交错的距离
_doubleWaveView.directionType = WaveDirectionTypeFoward; //正向移动
-(void)setFrontSpeed:(CGFloat)frontSpeed
{
if (_frontSpeed != frontSpeed) {
_frontSpeed = frontSpeed;
waveSpeedF = _frontSpeed;
}
}
-(void)setInsideSpeed:(CGFloat)insideSpeed
{
if (_insideSpeed != insideSpeed) {
_insideSpeed = insideSpeed;
waveSpeedS = _insideSpeed;
}
}
-(void)setWaveOffset:(CGFloat)waveOffset
{
if (_waveOffset != waveOffset) {
_waveOffset = waveOffset;
offsetS = offsetF + _waveOffset;
}
}
-(void)setDirectionType:(WaveDirectionType)directionType
{
if (_directionType != directionType) {
_directionType = directionType;
direction = _directionType;
}
}
效果图:

#####针对定时器释放的问题进行优化
我们的波浪线UI已经实现,由于个人页面一般放在TabbarController中与首页平级,不会频繁的创建销毁,照理说不作优化也不会出什么问题。不过还是有必要提一下,因为app中很多地方都会用到定时器,由于定时器与控制器循环引用很容易导致释放不掉的问题,我们用一个例子来具体说明。
在刚才的demo中创建一个新的Controller,在Controller中创建一个tableview,将waveView添加为cell的子视图,看起来像这样:

然后我们使用个人页面中的“波形测试”跳转到这个页面,再从这个页面返回个人页面,重复以上操作数次(先来个50次吧,=。=)
之后我们看看内存及CPU使用情况:


仅仅做了跳转没干别的,CPU占用率爆满,内存也出现了小幅增长(出现内存泄漏),页面滑动卡顿感严重,我们可以使用Instruments测试一下fps:

Why?因为每次回到个人页面时控制器都没有被正确释放,定时器仍然丢在runloop里没拿出来,重复多次后CPU吃不消了。。。那么如何解决这个问题?
有些同学可能会在使用定时器的视图或控制器里这样写:
-(void)dealloc
{
NSLog(@"WaveView dealloc");
[_waveDisplayLink invalidate];
_waveDisplayLink = nil;
}
不过测试表明这样并不能解决问题,因为控制器没有被释放的原因就是dealloc方法没有被正确调用,而导致dealloc不调用的原因就是定时器与控制器之间的循环引用,具体就是这个地方出了问题:
定时器添加到 Runloop 的时候,会被 Runloop 强引用,然后定时器又会有一个对 Target 的强引用(也就是 self )也就是说 NSTimer 强引用了 self ,导致 self 一直不能被释放掉,所以 self 的 dealloc 方法也一直未被执行。
那么我们可以在viewWillDisappear:中释放定时器么?可行,不过会非常麻烦,就拿上面这个例子来说,首先不
Related Skills
node-connect
349.7kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
109.7kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
349.7kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
349.7kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
