集成涂鸦功能 - iOS

简介

本篇文档描述了会议中涂鸦功能的实现。在集成时,涉及到 JCDoodleJCDoodleManager 这两个类。

JCDoodle 是涂鸦数据的对象,定义了涂鸦数据的属性。JCDoodleManager 负责发送和接收涂鸦数据。涂鸦数据的收发是基于会议功能来实现的,所以确保已集成了会议功能。

数据收发

会议中的成员才能发送和接收涂鸦数据,在 iOS 侧定义了 JCDoodleDelegate protocol,用于 UI 接收涂鸦数据。

接收数据的实现如下:

  1. UI 实现 JCDoodleDelegate protocol 方法

     //ViewController添加JCDoodleDelegate协议
     @interface WhiteBoardViewController () <JCDoodleDelegate>
    
     //接收数据的回调
     - (void)receiveActionType:(JCDoodleActionType)type 
                        doodle:(JCDoodleAction *)doodle 
                    fromSender:(NSString *)userId
     {
    
     }
    
  2. 设置代理

     //必须在加入会议前设置代理,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 的官方文档。

  1. 创建路径,把所有点连成一条路径。
//创建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);
  1. 画路径,需要在 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
  1. 释放路径
//等画完路径,要释放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 不同的设备上,白板上所画轨迹的一致性。这三个平台需处理:

  1. UI 在创建白板(View)时,白板的宽高比都统一为16:9。
  2. 在收发数据时,必须要把轨迹的每一个点都做归一化处理。

具体实现如下:

发送方的实现

发送方在发送轨迹的数据前,先要把基于屏幕坐标的点做归一化处理。

//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...