Android繪圖軟件開發(2)-圖形的編輯操作實現

引言

圖形并不是畫了就畫了,用戶完全可以在接下來對任一圖形進行后期編輯,而編輯操作包括:選中、平移、縮放、旋轉、拷貝、刪除。本章就想講講怎么去實現這些操作。

開發思路

還是用分類劃分的思路考慮:縱觀這六個編輯操作,“選中”是最特殊的,它是其它操作能夠進行的前置條件(因為必須要選中了某個圖形后才能進行呀,不然系統怎么知道您想要操作哪個圖形)。其次,“平移”、“縮放”、“旋轉”這3個操作的共同之處是都要對畫布上的圖形進行觸摸,進而產生變換。最后,“拷貝”和“刪除”這2個操作非常簡單,只要選中某個圖形,再點擊對應按鈕即可瞬間執行。

選中操作

用戶手指落下后會觸發MotionEvent.ACTION_DOWN事件,該事件對象可以捕獲落點坐標,此時只要遍歷圖形鏈表pelList,逐個計算每個圖形與落點分別在“水平”和“垂直”方向的距離之和,并找出“水平”或“垂直”距離之和最短的那個圖形,該圖形即被選中,如下圖所示,黑點表示落點,實線代表圖形軌跡,最終圓形被選中。



計算距離時,需要知道圖形的位置信息,該信息由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,以它們作為變換參數,更新選中圖形原始的變換矩陣,最后刷新,實現平移,如下圖及下面代碼所示:



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); // 更新平移后路徑所在區域
}

縮放操作

縮放就要稍微復雜點了,但只要稍微想一下咱們平時用智能手機縮放圖片的過程,也是輕而易舉的。其實,縮放過程可以簡單抽象為下圖所示:



其中B1和B2為兩個手指的落點,通過他倆可以算得縮放中心,B1’和B2’是兩個手指移動點。任務很明確啦,我們只需算出縮放中心+縮放比例,即可實現縮放。但問題就是這個縮放比例怎么計算才更合理呢?B1B2是圖形的初始距離,B1’B2’是圖形縮放后的距離,“兩者的比”其實就可以大致表征這個縮放比例,如下所示:



縮放的代碼如下:
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); // 更新平移后路徑所在區域
}

旋轉操作

旋轉就更是復雜了,但萬變不離其宗,我們還是用老方法,把復雜問題抽象成一個簡單的圖分析,如下圖:



C1和C2是落點,C1’和C2’是移動點,O是旋轉中心。歸根究底,我們不外乎就是想要C1OC1’這個角度,有了它就能控制圖形的旋轉。那怎么算這個角度呢?以前上中學的時候有個公式不知道大家還記得不,我們用白話形容一下就是:一個圓,只要知道了一段弧和半徑的長度,就能求得圓心角的弧度值。有了弧度值再做個轉換,自然就能求到度數值了。就像如下所示一樣:



但這里需要注明一點,就是這段弧長是一個近似的長度,是用弦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)”的一種有效的實現方案。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容