集成涂鸦控件

1.Windows涂鸦控件简介

JusTalk Cloud 涂鸦功能用于一对一音视频通话或会议中的信息共享,可实现多人在一个共享的白板或图片上的涂鸦数据互通和展示,并且支持 Windows、iOS 和 Android 三个平台的数据互通。

涂鸦功能模块可分为涂鸦数据的构造、涂鸦数据的传输、涂鸦数据的解析和涂鸦数据的界面展示。涂鸦数据的构造、传输和解析使用 SDK 接口来实现,界面展示则使用 C# 平台的接口来实现。在做涂鸦相关操作前请先确认你已经初始化SDK和完成登录

2.涂鸦动作介绍

涂鸦由一系列动作构成(Action),客户端本地每完成一个动作都需要调用发送涂鸦接口发送包含此动作的涂鸦给其他接收端,接收端会根据收到的涂鸦解析出相应的动作做出界面上的处理。每个涂鸦对象只能包含一个涂鸦动作(Action)。比如PC端执行”画“这一动作,然后将带有“画”这个动作的涂鸦发送给手机端,手机端收到后解析这个涂鸦,然后得到”画“的动作,再把画的内容展示到界面上。

常用的涂鸦动作 相应代码(数值)
开始涂鸦 EN_MTC_DOODLE_ACTION_START (5)
停止涂鸦 EN_MTC_DOODLE_ACTION_STOP (6)
EN_MTC_DOODLE_ACTION_DRAW(0)
消除 EN_MTC_DOODLE_ACTION_ERASE(1)
撤销 EN_MTC_DOODLE_ACTION_UNDO(3)
清除全部 EN_MTC_DOODLE_ACTION_CLEAN(2)

3. 实现涂鸦步骤

  • 初始化涂鸦

    首先需要初始化涂鸦接口,和注册涂鸦接收监听接口。和登录操作一样界面也需要继承涂鸦接收监听接口:JCDoodleApi.JCReceiveDoodleListener

    JCDoodleApi.Instance.initialize();
    JCDoodleApi.Instance.registerJCReceiveDoodleListener(this);
    

    然后定义涂鸦对象,和涂鸦构造器对象。

    private JCDoodle jcDoodle;
    private JCDoodle.Builder jcDoodle_builder;
    
  • 创建涂鸦

    接下来就可以创建涂鸦了(先以创建一个开始涂鸦的动作为例)

    jcDoodle_builder = new JCDoodle.Builder((uint)EN_MTC_DOODLE_ACTION_TYPE.EN_MTC_DOODLE_ACTION_START);
    jcDoodle = jcDoodle_builder.build();
    
  • 发送涂鸦

    创建完包含开始涂鸦动作的涂鸦对象后,我们需要调用发送涂鸦接口把这一动作告诉其他客户端(手机端等)

    JCDoodleApi.Instance.sendDoodle(jcDoodle);
    
  • 接收涂鸦

    其他端收到涂鸦后SDK会调用界面的回调函数onReceiveDoodle(JCDoodle doodle, string fromUserId)
    doodle: 收到的涂鸦对象
    fromUserId:发送此涂鸦的用户ID

    函数的结构代码如下:

    public void onReceiveDoodle(JCDoodle doodle, string fromUserId)
            {
                if(doodle.ActionType == (uint)EN_MTC_DOODLE_ACTION_TYPE.EN_MTC_DOODLE_ACTION_START)
                {
                    //收到开始涂鸦的动作后,在界面做出更新
                }
                else if (doodle.ActionType == (uint)EN_MTC_DOODLE_ACTION_TYPE.EN_MTC_DOODLE_ACTION_DRAW)
                {
                    //收到画的动作后,在界面做出更新
                }
                else if (doodle.ActionType == (uint)EN_MTC_DOODLE_ACTION_TYPE.EN_MTC_DOODLE_ACTION_CLEAN)
                {
                    //收到清除动作后,在界面做出更新
                }
                else if (doodle.ActionType == (uint)EN_MTC_DOODLE_ACTION_TYPE.EN_MTC_DOODLE_ACTION_UNDO)
                {
                    //收到撤销动作后,在界面做出更新
                }
            }
    
  • 坐标系转换

    画布是涂鸦内容显示的区域。为统一不同窗口分辨率,将进行坐标系转换。

    • 窗口坐标系:系统自带的坐标系,以像素为单位,原点一般为左上角
    • 逻辑坐标系:SDK中使用的坐标系,以归一化比例,原点为中心点,长宽比一般为16:9(长宽比例可自定义)

      为了保证 Windows、Andorid、iOS 三个平台互通时显示的位置一致,三个平台使用的长宽比必须一致,Windows 和手机端一样是以竖的方式来展现。

      图片的显示区域可能不是16:9,但是可以把区域延伸想像成16:9。

      如下图所示,在两种不同分辨率下的坐标系示意:

    窗口坐标转逻辑坐标,参考如下:

      //pointX 和 pointY 为点击屏幕时产生的点,转换出来的 x 和 y 是传入涂鸦SDK的点
      //假设 kDefaultWidth 为涂鸦展示区域的宽度, kDefaultHeight = kDefaultWidth * 16 / 9
      double x = (double) (pointX * 2 / kDefaultWidth - 1.0);
      double y = (double) (pointY * 2 / kDefaultHeight - 1.0);
    

    逻辑坐标转窗口坐标,参考如下:

      //fX 和 fY 为数据解析出来的逻辑坐标的点
      //转成 窗口的Point
      double pointX = (double) ((fX + 1.0) * kDefaultWidth / 2);
      double pointY = (double) ((fY + 1.0) * kDefaultHeight / 2);
    

    涂鸦传输控件传输的涂鸦对象都是使用的逻辑坐标,所以在每次发送和接收涂鸦对象的时候必须先转换坐标。

4.涂鸦相应属性和常用函数接口

涂鸦相关属性或函数 说明
string UserID 涂鸦用户ID
uint ActionType 涂鸦的动作类型
float PaintStrokeWidth 涂鸦画笔宽度
Color PaintColor 涂鸦颜色
uint Session 涂鸦暂存区
uint PageId 涂鸦页面ID
PointCollection PointList 每一笔涂鸦的点的集合
string DoodleContent 涂鸦内容(用于发送)
void AddCoordinate(Point point) 给涂鸦点的集合加点

5. 实现涂鸦“画”的动作UI代码实例解析

//需要先在UI界面添加Canvas画板对象,此例中画板对象名为MarkCanvas
//此控件继承WPF用户控件,和涂鸦接收监听接口
public partial class WhiteboardControl : UserControl, JCDoodleApi.JCReceiveDoodleListener
    {
        //定义画笔颜色
        private readonly Color kDefaultPenColor = Colors.Blue;
        //定义画笔宽度
        private readonly double kDefaultPenThicknessRatio = 5.0;

        private JCDoodle jcDoodle;
        private JCDoodle.Builder jcDoodle_builder;
        //定义画一笔的点的集合
        private PointCollection drawingpath;
        //定义起始点
        private Point startPoint;
        //UI 初始化
        public WhiteboardControl()
        {
            this.Resources.MergedDictionaries.Add(ResourceManager.SharedDictionary);
            InitializeComponent();
            //初始化JCDoodleApi
            JCDoodleApi.Instance.initialize();
            //注册涂鸦监听接口
            JCDoodleApi.Instance.registerJCReceiveDoodleListener(this);
        }
        //画板上鼠标按下事件
        private void DoodleImage_MouseDown(object sender, MouseButtonEventArgs e)
        {
            //获取鼠标按下的点的坐标
            Point point = e.GetPosition(sender as IInputElement);
            startPoint = point;
            //开始画线
            StartDrawPath(point);
            if (PageMouseDown != null)
            {
                PageMouseDown(0, sender, e);
            }
            e.Handled = true;
        }
        //画板上鼠标抬起事件
        private void DoodleImage_MouseUp(object sender, MouseButtonEventArgs e)
        {
            //停止画线
            StopDrawPath();
            if (PageMouseUp != null)
            {
                PageMouseUp(0, sender, e);
            }
            e.Handled = true;
        }
        //画板上鼠标移动事件
        private void DoodleImage_MouseMove(object sender, MouseEventArgs e)
        {
            if (e.LeftButton == MouseButtonState.Pressed)
            {
                // 返回指针相对于Canvas的位置  
                Point point = e.GetPosition(MarkCanvas);

                if (drawingpath.Count == 0 && drawingpath != null)
                {
                    // 加入起始点  
                    drawingpath.Add(new Point(this.startPoint.X, this.startPoint.Y));
                }
                else
                {
                    // 加入移动过程中的point  
                    drawingpath.Add(point);
                }

                // 去重复点  
                var disList = drawingpath.Distinct().ToList();
                var count = disList.Count(); // 总点数  
                if (point != this.startPoint && this.startPoint != null)
                {
                    var l = new Line();
                    Color color = kDefaultPenColor;
                    SolidColorBrush scb = new SolidColorBrush(color);
                    l.Stroke = scb;
                    l.StrokeThickness = kDefaultPenThicknessRatio;
                    if (count < 2)
                        return;
                    l.X1 = disList[count - 2].X;  // count-2  保证 line的起始点为点集合中的倒数第二个点。  
                    l.Y1 = disList[count - 2].Y;
                    // 终点X,Y 为当前point的X,Y  
                    l.X2 = point.X;
                    l.Y2 = point.Y;
                    //转换坐标系
                    Point p = ToLogicPoint(point);
                    //doodlebuider加入点
                    jcDoodle_builder.AddCoordinate(p);
                    //画板上加入点
                    MarkCanvas.Children.Add(l);
                }
            } 
        }
        //逻辑坐标点集转换成窗口坐标点集
        private PointCollection FromLogicPoints(PointCollection points)
        {
            PointCollection imagePoints = new PointCollection();
            foreach (Point point in points)
            {
                imagePoints.Add(FromLogicPoint(point));
            }
            return imagePoints;
        }
        //窗口坐标点转换成逻辑坐标点
        private Point ToLogicPoint(Point point)
        {
            double X = (point.X * 2) / MarkCanvas.Width - 1.0;
            double Y = (point.Y * 2) / (MarkCanvas.Height) - 1.0;
            return new Point(X, Y);
        }
        //逻辑坐标点转换成窗口坐标点
        private Point FromLogicPoint(Point point)
        {
            double X = (point.X + 1.0) * MarkCanvas.ActualWidth / 2;
            double Y = (point.Y + 1.0) * (MarkCanvas.ActualWidth * 16 / 9) / 2;
            return new Point(X, Y);
        }
        //开始画线的动作
        private void StartDrawPath(Point point)
        {
            //创建新的doodle_builder
            jcDoodle_builder = new JCDoodle.Builder((uint)EN_MTC_DOODLE_ACTION_TYPE.EN_MTC_DOODLE_ACTION_DRAW);
            //定义doodle的相关属性
            jcDoodle_builder.PageId = 0;
            jcDoodle_builder.PaintColor = kDefaultPenColor;
            jcDoodle_builder.PaintStrokeWidth = (float)kDefaultPenThicknessRatio;
            Point p = ToLogicPoint(point);
            //doodle_builder 加入起始点
            jcDoodle_builder.AddCoordinate(p);
            //本地点的集合加入点
            drawingpath = new PointCollection();
            drawingpath.Add(point);
        }
        //停止画线
        private void StopDrawPath()
        {
            if (jcDoodle_builder == null)
            {
                return;
            }
            //根据doodle_builder创建doodle对象
            jcDoodle = jcDoodle_builder.build();
            //发送此doodle给其他端
            JCDoodleApi.Instance.sendDoodle(jcDoodle);
        }
        //回调函数收到doodle后界面的反馈
        public void onReceiveDoodle(JCDoodle doodle, string fromUserId)
        {
            ...
            //判断如果接收到的doodle是画的动作
            else if (doodle.ActionType == (uint)EN_MTC_DOODLE_ACTION_TYPE.EN_MTC_DOODLE_ACTION_DRAW)
            {    //获取doodle的属性
                PointCollection pc = doodle.PointList;
                float paintwidth = doodle.PaintStrokeWidth;
                Color paintcolor = doodle.PaintColor;
                //调用本地画线函数
                RenderDrawPath(pc, paintwidth, paintcolor);
            }
            ...
        }
        //本地画线函数
        private void RenderDrawPath(PointCollection pc, float paintwidth, Color paintcolor)
        {
            //转换点坐标
            PointCollection pcNormal = FromLogicPoints(pc);
            var disList = pcNormal.Distinct().ToList();
            var count = disList.Count(); // 总点数  
            Color color = kDefaultPenColor;
            SolidColorBrush scb = new SolidColorBrush(color);

            if (count < 2)
                return;
            else
            {
                for (int i = 0; i < (count-1); i++)
                {
                    var l = new Line();
                    l.Stroke = scb;
                    l.StrokeThickness = kDefaultPenThicknessRatio;
                    l.X1 = disList[i].X;
                    l.Y1 = disList[i].Y;
                    l.X2 = disList[i + 1].X;
                    l.Y2 = disList[i + 1].Y;
                    //画板上加点
                    MarkCanvas.Children.Add(l);
                }
            }
        }

    }