Android绘图软件开发(2)-图形的编辑操作实现

引言

图形并不是画了就画了,用户完全可以在接下来对任一图形进行后期编辑,而编辑操作包括:选中、平移、缩放、旋转、拷贝、删除。本章就想讲讲怎么去实现这些操作。

开发思路

还是用分类划分的思路考虑:纵观这六个编辑操作,“选中”是最特殊的,它是其它操作能够进行的前置条件(因为必须要选中了某个图形后才能进行呀,不然系统怎么知道您想要操作哪个图形)。其次,“平移”、“缩放”、“旋转”这3个操作的共同之处是都要对画布上的图形进行触摸,进而产生变换。最后,“拷贝”和“删除”这2个操作非常简单,只要选中某个图形,再点击对应按钮即可瞬间执行。

选中操作

用户手指落下后会触发MotionEvent.ACTION_DOWN事件,该事件对象可以捕获落点坐标,此时只要遍历图形链表pelList,逐个计算每个图形与落点分别在“水平”和“垂直”方向的距离之和,并找出“水平”或“垂直”距离之和最短的那个图形,该图形即被选中,如下图所示,黑点表示落点,实线代表图形轨迹,最终圆形被选中。

select

计算距离时,需要知道图形的位置信息,该信息由pel对象的region提供,通过调用region.getBounds()即可获得图形的包围盒,上图中的虚线即表示包围盒。代码大体如下所示。

ListIterator<Pel> pelIterator = pelList.listIterator(); // 获取pelList对应的迭代器头结点
while (pelIterator.hasNext())
{
    Pel pel = pelIterator.next();
    Rect rect=(pel.region).getBounds(); //获取图形包围盒

    //计算落点距离图形左、右边界的距离和
    float leftDis=Math.abs(rect.left-downPoint.x);
    float rightDis=Math.abs(rect.right-downPoint.x);
    float horizontalDis=leftDis+rightDis;

    //计算落点距离图形上、下边界的距离和
    float topDis=Math.abs(rect.top-downPoint.y);
    float bottomDis=Math.abs(rect.bottom-downPoint.y);
    float verticalDis=topDis+bottomDis;

    //判断落点与该图形的距离和是否更小,若是进一步判断落点是否在图形内部或附近,若太远则仍然不算
    if((horizontalDis < minHorizontalDis || verticalDis < minVerticalDis) && (horizontalDis < rect.width() + 5 && verticalDis < rect.height() + 5))
    {
        selectedPel=pel; //选中该图形
        minHorizontalDis=horizontalDis;
        minVerticalDis=verticalDis;
    }
}

变换矩阵

在讲图形的变换操作(即平移、缩放、旋转)之前,首先要引入“变换矩阵”这个概念,若要详细说明那太多了,所以简单起见大家只用知道:这个matrix存储了某一图形距离它初始形态的水平偏移量、垂直偏移量、缩放系数、旋转角度、倾斜角度等参数信息,当要发生变换时,只用把图形初始形态的坐标和这个变换矩阵做前乘、后乘、相加等运算,即可得到新图形的形态。

所以,表面上看似是在对图形进行变换,但实质是对pel.path在中封装的Matrix matrix进行变换,我们的任务是需要获取这个matrix,然后调用Matrix类提供的各种方法去变换该矩阵即可。下面是获取matrix的代码。

public Matrix getMatrix(Pel pel)
{
    Matrix matrix = new Matrix();
    PathMeasure pathMeasure = new PathMeasure(pel.path, true); // 必须先将Path封装成PathMeasure
    pathMeasure.getMatrix(pathMeasure.getLength(),matrix, PathMeasure.POSITION_MATRIX_FLAG & PathMeasure.TANGENT_MATRIX_FLAG);

    return matrix;
}

平移操作

平移很简单,由于选中的时候记录了落点坐标,而移动的时候又会实时捕获手指当前所在的坐标,通过这两个坐标很容易计算出偏移距离dx和dy,以它们作为变换参数,更新选中图形原始的变换矩阵,最后刷新,实现平移,如下图及下面代码所示:

translatedragt

if (mode == DRAG)// 平移操作
{
    dx = curPoint.x - downPoint.x;//计算距离
    dy = curPoint.y - downPoint.y;

    // 对选中图元施行平移变换
    transMatrix.set(savedMatrix);
    transMatrix.postTranslate(dx, dy); // 作用于平移变换因子

    (selectedPel.path).set(savedPel.path);
    (selectedPel.path).transform(transMatrix); // 作用于图元
    (selectedPel.region).setPath(selectedPel.path, clipRegion); // 更新平移后路径所在区域
}

缩放操作

缩放就要稍微复杂点了,但只要稍微想一下咱们平时用智能手机缩放图片的过程,也是轻而易举的。其实,缩放过程可以简单抽象为下图所示:

zoom其中B1和B2为两个手指的落点,通过他俩可以算得缩放中心,B1’和B2’是两个手指移动点。任务很明确啦,我们只需算出缩放中心+缩放比例,即可实现缩放。但问题就是这个缩放比例怎么计算才更合理呢?B1B2是图形的初始距离,B1’B2’是图形缩放后的距离,“两者的比”其实就可以大致表征这个缩放比例,如下所示:

zoomt

缩放的代码如下:

if (mode == ZOOM)// 缩放操作
{
    float scale = newDist / oriDist;

    transMatrix.set(savedMatrix);
    transMatrix.postScale(scale, scale, centerPoint.x,centerPoint.y); // 作用于缩放变换因子

    (selectedPel.path).set(savedPel.path);
    (selectedPel.path).transform(transMatrix); // 作用于图元
    (selectedPel.region).setPath(selectedPel.path, clipRegion); // 更新平移后路径所在区域
}

旋转操作

旋转就更是复杂了,但万变不离其宗,我们还是用老方法,把复杂问题抽象成一个简单的图分析,如下图:

rotate

C1和C2是落点,C1’和C2’是移动点,O是旋转中心。归根究底,我们不外乎就是想要C1OC1’这个角度,有了它就能控制图形的旋转。那怎么算这个角度呢?以前上中学的时候有个公式不知道大家还记得不,我们用白话形容一下就是:一个圆,只要知道了一段弧和半径的长度,就能求得圆心角的弧度值。有了弧度值再做个转换,自然就能求到度数值了。就像如下所示一样:

rotatet

但这里需要注明一点,就是这段弧长是一个近似的长度,是用弦C1C1’去近似模拟的,因为真正的弧长基本不可能计算出来,也没有必要算出来,就算算出来了也会很慢,导致旋转过程的不流畅。下面是代码:

if (mode == ROTATE)// 旋转操作
{
    transMatrix.set(savedMatrix);
    transMatrix.setRotate(getDegree(),centerPoint.x,centerPoint.y);

    (selectedPel.path).set(savedPel.path);
    (selectedPel.path).transform(transMatrix); // 作用于图元
    (selectedPel.region).setPath(selectedPel.path, clipRegion); // 更新平移后路径所在区域
}
// 计算旋转角度
private float getDegree()
{
    // 获得两次down下时的距离
    float x=curPoint.x-downPoint.x;
    float y=curPoint.y-downPoint.y;

    float arc=FloatMath.sqrt(x * x + y * y);//弧长
    float radius=getRadius();//半径

    return (arc/radius)*(180/3.14f);
}
// 计算两个触摸点之间的距离
private float distance()
{
    float x = curPoint.x - secPoint.x;
    float y = curPoint.y - secPoint.y;
    return FloatMath.sqrt(x * x + y * y) / 2;
}

拷贝操作

选中某个图形后,点击拷贝按钮,就复制出了一个一模一样的图形在画布上。这个功能的实现非常简单,只需要根据当前选中的图形selectedPel的信息,去构造一个完全相同的另一图形对象,并将其添加至pelList尾部,刷新画布即可,这里就不贴代码了。

删除操作

选中某个图形后,点击删除按钮,画布上的这个图形就不复存在了。这个功能也非常简单,唯一需要注意的是不能删除selectedPel就了事了,而要在这之前,先遍历pelList,根据这个selectedPel引用找到它在pelList中的位置,并删除,最后刷新画布。同样比较简单,这里不贴代码了。

结语

本章就快要到一段落了,不知道复杂的编辑操作有没有变得简单一些呢?但这里似乎还是有一个交互上的问题没有解决:

选中图形以后可以平移、缩放和旋转,我们分别用了DRAG、ZOOM、ROTATE来标志状态,但其中平移操作是单指操作,而缩放和旋转都是多指操作,平移倒是很好和其它二位区分,但缩放和旋转就很难区分了,都是先落下两只手指再进行操作,且他俩的操作非常相似,您觉得这里应该如何去设置一个“临界条件”来作为两操作的分水岭,从而自然而人性化地完成交互呢?我这里也有一个自己写的方案,效果还不错,也很符合人的思维。如果实在没有头绪的话,可以去我的GitHub下载源码参考。

今天就先到这里啦,下一章讲解一下绘图软件(不,宏观说应该是数字编辑软件)中“撤销(undo)”和“重做(redo)”的一种有效的实现方案。

Leave a Comment.