集成涂鸦功能 - iOS
简介
本篇文档描述了会议中涂鸦功能的实现。在集成时,涉及到 JCDoodle
和 JCDoodleManager
这两个类。
JCDoodle
是涂鸦数据的对象,定义了涂鸦数据的属性。JCDoodleManager
负责发送和接收涂鸦数据。涂鸦数据的收发是基于会议功能来实现的,所以确保已集成了会议功能。
数据收发
会议中的成员才能发送和接收涂鸦数据,在 iOS 侧定义了 JCDoodleDelegate protocol
,用于 UI 接收涂鸦数据。
接收数据的实现如下:
UI 实现
JCDoodleDelegate protocol
方法//ViewController添加JCDoodleDelegate协议 @interface WhiteBoardViewController () <JCDoodleDelegate> //接收数据的回调 - (void)receiveActionType:(JCDoodleActionType)type doodle:(JCDoodleAction *)doodle fromSender:(NSString *)userId { }
设置代理
//必须在加入会议前设置代理,self是实现JCDoodleDelegate的ViewController对象 [JCDoodleManager setDelegate:self];
涂鸦数据的发送,请看以下具体场景的使用。
发起涂鸦
会议中的某个成员(发起方)点击发起涂鸦的按钮,然后会议中其他成员(接收方)的会议界面显示白板,可以在白板上开始涂鸦。具体实现如下:
发起方的实现
发起方在点击按钮后,调用发起涂鸦的发送接口,UI 显示白板。
//发起涂鸦,content可以传自定义的字符串
[JCDoodleManager startDoodleWithContent:nil];
//UI显示白板
//todo...
接收方的实现
接收收到发起涂鸦的回调后,UI 显示白板。
- (void)receiveActionType:(JCDoodleActionType)type
doodle:(JCDoodleAction *)doodle
fromSender:(NSString *)userId
{
//收到发起涂鸦的动作
if (type == JCDoodleActionStart) {
//发起涂鸦时带了自定义的字符串,获取该字符串
NSString *userDefined = doodle.userDefined;
//UI显示白板
//todo...
}
}
结束涂鸦
当发起涂鸦的成员(发起方)点击结束涂鸦的按钮,然后会议中其他成员(接收方)的会议界面隐藏白板。具体实现如下:
发起方的实现
发起方在点击结束涂鸦按钮后,调用结束涂鸦的发送接口,UI 隐藏白板。
//结束涂鸦
[JCDoodleManager stopDoodle];
//UI隐藏白板
//todo...
接收方的实现
接收方收到结束涂鸦的回调后,UI 隐藏白板。
- (void)receiveActionType:(JCDoodleActionType)type
doodle:(JCDoodleAction *)doodle
fromSender:(NSString *)userId
{
//收到结束涂鸦的动作
if (type == JCDoodleActionStop) {
//UI隐藏白板
//todo...
}
}
画轨迹
发起涂鸦后,某个成员(发送方)选择一个画笔的颜色和宽度在白板上画了一条曲线,然后会议中所有成员(接收方)的白板上出现同样的曲线。这个过程分为涂鸦数据的收发和 UI 展示。
发送方的实现
发送方把画的曲线转换成特定格式的数据,然后发送数据,具体实现如下:
//创建数据对象
JCDoodleAction *_doodleDraw = [[JCDoodleAction alloc] init];
//设置数据对象的类型为画曲线
_doodleDraw.actionType = JCDoodleActionDraw;
//设置曲线的颜色
_doodleDraw.brushColor = [UIColor redColor];
//设置曲线的宽度
_doodleDraw.brushWidth = 1.0;
//设置画曲线的成员,以便UI管理这些轨迹
_doodleDraw.userId = [JCApiManager getOwnUserId];
//把在手机屏幕上划过的每一个点加到数据对象中,参数x,y必须是归一化处理后的数据。
//归一化处理查看后面的说明
[_doodleDraw addPointWithPositionX:x positionY:y];
//发送数据
[JCDoodleManager sendDoodleAction:_doodleDraw];
接收方的实现
接收方收到数据后,获取对应的值,具体实现如下:
- (void)receiveActionType:(JCDoodleActionType)type
doodle:(JCDoodleAction *)doodle
fromSender:(NSString *)userI
{
//收到画曲线的动作
if (type == JCDoodleActionDraw) {
//获取颜色
UIColor * color = doodle.brushColor;
//获取宽度
CGFloat width = doodle.brushWidth;
//获取点的数组
NSArray *points = doodle.pathPoints;
//遍历数组,数组内的每一个对象都是NSArray,表示一个点。
for (NSArray *point in points) {
//一个点(NSArray)内包含了3个值
//第一个值是和上一个点的时间间隔
int x = [[point objectAtIndex:0] intValue];
//第二个值是点的x坐标
CGFloat x = [[point objectAtIndex:1] floatValue];
//第三个值是点的y坐标
CGFloat y = [[point objectAtIndex:2] floatValue];
//注:这里得到的点不能直接画到白板上,要先转化。具体查看归一化处理的说明
}
}
}
UI 展示
下面简单描述画曲线的实现过程,更详细画图的实现请查阅 iOS 的官方文档。
- 创建路径,把所有点连成一条路径。
//创建path
CGMutablePathRef path = CGPathCreateMutable();
//加曲线的起始点(x, y) 到path中
CGPathMoveToPoint(path, NULL, x, y);
//把曲线划过的点(toX, toY)和上一个点(x, y)连起来,这条曲线的路径就出来了
CGPathAddQuadCurveToPoint(path, NULL, x, y, (x + toX) / 2, (y + toY) / 2);
- 画路径,需要在 UIView 上来实现。
@implementation DoodleDrawView
//重写 UIView 的 draw 方法
- (void)drawRect:(CGRect)rect
{
//_cacheImage 是 一个 UIImage 对象,用来缓存路径
if (_cacheImage) {
//把_cacheImage显示到UIView上
[_cacheImage drawInRect:self.bounds];
}
}
//传路径、宽度和颜色,把路径画到_cacheImage上
- (void)drawPath:(CGPathRef)path
lineWidth:(CGFloat)width
lineColor:(UIColor *)color
{
//设置画的区域
UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0.0);
[_cacheImage drawInRect:self.bounds];
//设置path的宽度和颜色等属性
CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapRound);
CGContextSetLineJoin(UIGraphicsGetCurrentContext(), kCGLineJoinRound);
CGContextSetLineWidth(UIGraphicsGetCurrentContext(), width);
CGContextSetStrokeColorWithColor(UIGraphicsGetCurrentContext(), color.CGColor);
//把path画到_cacheImage上
CGContextBeginPath(UIGraphicsGetCurrentContext());
CGContextAddPath(UIGraphicsGetCurrentContext(), path);
CGContextStrokePath(UIGraphicsGetCurrentContext());
_cacheImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
//刷新UIView
[self setNeedsDisplay];
}
@end
- 释放路径
//等画完路径,要释放path
CGPathRelease(path);
清除轨迹
发起涂鸦后,某个成员(发送方)点击清除所有轨迹的按钮,然后会议中所有成员(接收方)的白板上清除所有轨迹。
发送方的实现
发起方点击按钮后,调用清除涂鸦的发送接口,UI 清除轨迹。
//清除涂鸦,如果有多页的涂鸦,PageNumber传对应的页码
[JCDoodleManager cleanDoodleWithPageNumber:0];
//UIView清除轨迹
//todo...
接收方的实现
接收方收到清除涂鸦的回调后,UI 清除轨迹。
- (void)receiveActionType:(JCDoodleActionType)type
doodle:(JCDoodleAction *)doodle
fromSender:(NSString *)userId
{
//收到清除涂鸦的动作
if (type == JCDoodleActionClean) {
//UIView清除轨迹
//todo...
}
}
UI 清除轨迹
清除轨迹需要在 UIView 上来实现。
@implementation DoodleDrawView
//清除所有路径
- (void)cleanAllPath
{
if (_cacheImage) {
//释放缓存路径的_cacheImage
_cacheImage = nil;
}
//刷新UIView
[self setNeedsDisplay];
}
@end
归一化处理
为了保证在 iOS、Andorid 和 PC 不同的设备上,白板上所画轨迹的一致性。这三个平台需处理:
- UI 在创建白板(View)时,白板的宽高比都统一为16:9。
- 在收发数据时,必须要把轨迹的每一个点都做归一化处理。
具体实现如下:
发送方的实现
发送方在发送轨迹的数据前,先要把基于屏幕坐标的点做归一化处理。
//point是白板View上轨迹的每一个点,ViewWidth和ViewHeight是白板的实际宽高
//x,y是归一化处理后的值
CGFloat x = 2 * point.x / ViewWidth - 1.0;
CGFloat y = 2 * point.y / ViewHeight - 1.0;
//再加到涂鸦的数据对象中
[_doodleDraw addPointWithPositionX:x positionY:y];
接收方的实现
接收方在收到数据后,先把归一化的值转换成基于屏幕坐标的点。
//x,y是收到数据后获取出来的值
CGFloat x = [[point objectAtIndex:1] floatValue];
CGFloat y = [[point objectAtIndex:2] floatValue];
//normalX,normalY 是屏幕坐标的点
CGFloat normalX = (x + 1.0) * ViewWidth / 2;
CGFloat normalY = (y + 1.0) * ViewHeight / 2;
//再加到path中
CGPathMoveToPoint(path, NULL, normalX, normalY);
//todo...