[UWP]如何實現UWP平臺最佳圖片裁剪控制項

来源:https://www.cnblogs.com/hhchaos/archive/2018/11/26/10021952.html
-Advertisement-
Play Games

前幾天我寫了一個UWP圖片裁剪控制項ImageCropper( "開源地址" ),自認為算是現階段UWP社區里最好用的圖片裁剪控制項了,今天就來分享下我編碼的過程。 為什麼又要造輪子 因為開發需要,我們需要使用一個圖片裁剪控制項來編輯用戶上傳的圖片。本著儘量不重覆造輪子的原則,我找了下現在UWP生態圈裡可 ...


前幾天我寫了一個UWP圖片裁剪控制項ImageCropper(開源地址),自認為算是現階段UWP社區里最好用的圖片裁剪控制項了,今天就來分享下我編碼的過程。

為什麼又要造輪子

因為開發需要,我們需要使用一個圖片裁剪控制項來編輯用戶上傳的圖片。本著儘量不重覆造輪子的原則,我找了下現在UWP生態圈裡可用的圖片裁剪控制項,然後發現一個悲慘的事實:UWP生態圈甚至沒有一個體驗優秀的圖片裁剪控制項!

舉例來說,就連現在商店裡做的比較好的網易雲音樂、IT之家以及愛奇藝等應用,他們使用的圖片裁剪控制項體驗也糟糕的一塌糊塗(有認識他們開發人員的大佬,歡迎把我的這篇文章推薦給他們,不怕打臉)。

下圖是愛奇藝與IT之家的頭像裁剪控制項:

糟糕的圖片裁剪體驗

那麼好吧,我們只好又來造輪子了!

借鑒優秀的前輩

現階段在Windows平臺上,最讓我稱佩的裁剪圖片的應用就是Windows照片了。

Windows照片

它有以下兩個優點:

  • 裁剪區域永遠顯示在視覺中心,突出重點;
  • 操作體驗順暢,觸屏操作也能有很好體驗。

這次我們就來“抄襲”一下這個系統應用。

如何實現

有了實現目標,接下來就是思考如何編碼實現了。

需要哪些屬性來控製裁剪區域

分析一下這個控制項的組成部分,其實就是由三部分組成的:最下層裁剪源圖像,上層控製裁剪區域的四個按鈕,以及遮蓋在圖像上的黑色半透明遮罩層。

所以我定義了下麵幾個依賴屬性來控制界面:

  • SourceImage:類型為WriteableBitmap,控製裁剪圖像源;
  • X1,Y1,X2,Y2:這四個double值,控制剪裁區域左上角與右下角兩個點坐標;
  • AspectRatio:類型為double值,控製裁剪圖像縱橫比;

另外還定義了兩個主要的私有屬性用來更新界面佈局:

  • _maskAreaGeometryGroup:類型為GeometryGroup,控制黑色半透明遮罩層;
  • _imageTransform:類型為CompositeTransform,控製裁剪過程中的源圖像變換。

這樣的話,更改裁剪區域只需要修改X1,Y1,X2,Y2這四個值就可以了。

改變大小

另外,如果我們通過拖動圖片來移動選擇區域,同樣是修改X1,Y1,X2,Y2的值(而不是對圖片進行變換,動圖中可能看不出來,源代碼中可以看到)。

拖動圖片

控製裁剪圖像源Transform

在Windows照片應用裁剪圖片控制項中,其體驗良好的一個主要原因就是剪裁區域永遠處於視覺中心,這是通過控製裁剪圖像源在界面上的Transform來完成的。

圖片變換

我們可以看到,裁剪圖像源的變換規則如下:

  • 裁剪區域永遠位於界面中心(使用Uniform規則);
  • 當裁剪區域縮小時,在停止拖動裁剪框控制按鈕時,更新裁剪圖像源的Transform;
  • 當裁剪區域擴大時,實時更新裁剪圖像源的Transform。

限制剪裁區域範圍

另外要註意的是,我們必須保證X1,Y1,X2,Y2取值範圍不超過圖片區域。

這裡有個關於Rect的坑要說明下。一開始我選用的判斷方法是:通過Rect.Contains方法傳入剪裁區域左上角與右下角兩個點坐標,如果均為true,代表剪裁區域範圍合法。但是我發現,在Rect長寬為有小數部分的double值時,如果我把右下角坐標設置為new Point(Rect.X + Rect.Width, Rect.Y + Rect.Height),這個方法會返回錯誤的false值,實在是坑爹!

因此,考慮到使用場景,我為Rect寫了另外一個擴展方法:

    public static bool IsSafePoint(this Rect targetRect, Point point)
    {
        if (point.X - targetRect.X < 0.01)
            return false;
        if (point.X - (targetRect.X + targetRect.Width) > 0.01)
            return false;
        if (point.Y - targetRect.Y < 0.01)
            return false;
        if (point.Y - (targetRect.Y + targetRect.Height) > 0.01)
            return false;
        return true;
    }

核心邏輯代碼

下圖是這個圖片剪裁控制項的核心邏輯:

核心邏輯

其中InitImageLayout方法會在圖片源變化時被調用,它會初始化圖片佈局(通過調用UpdateImageLayout方法)。

    private void InitImageLayout()
    {
        _maxClipRect = new Rect(0, 0, SourceImage.PixelWidth, SourceImage.PixelHeight);
        var maxSelectedRect = new Rect(1, 1, SourceImage.PixelWidth - 2, SourceImage.PixelHeight - 2);
        _currentClipRect = KeepAspectRatio ? maxSelectedRect.GetUniformRect(AspectRatio) : maxSelectedRect;
        UpdateImageLayout();
    }

UpdateImageLayout方法用於初始化控制項或者控制項SizeChanged時,調用此方法更新控制項佈局(通過調用UpdateImageLayoutWithViewport方法)。

    private void UpdateImageLayout()
    {
        var canvasRect = new Rect(0, 0, CanvasWidth, CanvasHeight);
        var uniformSelectedRect = canvasRect.GetUniformRect(_currentClipRect.Width / _currentClipRect.Height);
        UpdateImageLayoutWithViewport(uniformSelectedRect, _currentClipRect);
    }

UpdateImageLayoutWithViewport方法是更新控制項佈局的核心邏輯,它接受兩個參數:viewport和viewportImgRect,其中viewport代表的是實際呈現在你視覺中心的區域,viewportImgRect表示viewport所對應的實際圖片區域(以實際像素大小為單位),代碼將通過這兩個參數更新裁剪圖像源的Transform。

    private void UpdateImageLayoutWithViewport(Rect viewport, Rect viewportImgRect)
    {
        var imageScale = viewport.Width / viewportImgRect.Width;
        _imageTransform.ScaleX = _imageTransform.ScaleY = imageScale;
        _imageTransform.TranslateX = viewport.X - viewportImgRect.X * imageScale;
        _imageTransform.TranslateY = viewport.Y - viewportImgRect.Y * imageScale;
        var selectedRect = _imageTransform.TransformBounds(_currentClipRect);
        _limitedRect = _imageTransform.TransformBounds(_maxClipRect);
        var startPoint = _limitedRect.GetSafePoint(new Point(selectedRect.X, selectedRect.Y));
        var endPoint = _limitedRect.GetSafePoint(new Point(selectedRect.X + selectedRect.Width, selectedRect.Y + selectedRect.Height));
        _changeByCode = true;
        X1 = startPoint.X;
        Y1 = startPoint.Y;
        X2 = endPoint.X;
        Y2 = endPoint.Y;
        _changeByCode = false;
    }

UpdateClipRectWithAspectRatio則在用戶對剪裁區域改變時被調用,其中dragPoint代表用戶操作的哪個按鈕,diffPos代表該按鈕的前後位置差值。

    private void UpdateClipRectWithAspectRatio(DragPoint dragPoint, Point diffPos)
    {
        if (KeepAspectRatio)
        {
            if (Math.Abs(diffPos.X / diffPos.Y) > AspectRatio)
            {
                if (dragPoint == DragPoint.UpperLeft || dragPoint == DragPoint.LowerRight)
                    diffPos.Y = diffPos.X / AspectRatio;
                else
                    diffPos.Y = -diffPos.X / AspectRatio;
            }
            else
            {
                if (dragPoint == DragPoint.UpperLeft || dragPoint == DragPoint.LowerRight)
                    diffPos.X = diffPos.Y * AspectRatio;
                else
                    diffPos.X = -diffPos.Y * AspectRatio;
            }
        }

        var startPoint = new Point(X1, Y1);
        var endPoint = new Point(X2, Y2);
        switch (dragPoint)
        {
            case DragPoint.UpperLeft:
                startPoint.X += diffPos.X;
                startPoint.Y += diffPos.Y;
                break;
            case DragPoint.UpperRight:
                endPoint.X += diffPos.X;
                startPoint.Y += diffPos.Y;
                break;
            case DragPoint.LowerLeft:
                startPoint.X += diffPos.X;
                endPoint.Y += diffPos.Y;
                break;
            case DragPoint.LowerRight:
                endPoint.X += diffPos.X;
                endPoint.Y += diffPos.Y;
                break;
        }

        if (_limitedRect.IsSafePoint(startPoint) && _limitedRect.IsSafePoint(endPoint))
        {
            var canvasRect = new Rect(0, 0, CanvasWidth, CanvasHeight);
            var newRect = new Rect(startPoint, endPoint);
            canvasRect.Union(newRect);
            if (canvasRect.X < 0 || canvasRect.Y < 0 || canvasRect.Width > CanvasWidth ||
                canvasRect.Height > CanvasHeight)
            {
                var inverseImageTransform = _imageTransform.Inverse;
                if (inverseImageTransform != null)
                {
                    var movedRect = inverseImageTransform.TransformBounds(
                        new Rect(startPoint, endPoint));
                    movedRect.Intersect(_maxClipRect);
                    _currentClipRect = movedRect;
                    var oriCanvasRect = new Rect(0, 0, CanvasWidth, CanvasHeight);
                    var viewportRect = oriCanvasRect.GetUniformRect(canvasRect.Width / canvasRect.Height);
                    var viewportImgRect = inverseImageTransform.TransformBounds(canvasRect);
                    UpdateImageLayoutWithViewport(viewportRect, viewportImgRect);
                }
            }
            else
            {
                X1 = startPoint.X;
                Y1 = startPoint.Y;
                X2 = endPoint.X;
                Y2 = endPoint.Y;
            }
        }
    }

UpdateMaskArea方法用來更新遮蓋在裁剪圖像源上的黑色半透明遮罩層,其實就是圖像上覆蓋了一個Path元素,這裡就不細講了,直接貼代碼。

    private void UpdateMaskArea()
    {
        _maskAreaGeometryGroup.Children.Clear();
        _maskAreaGeometryGroup.Children.Add(new RectangleGeometry
        {
            Rect = new Rect(-_layoutGrid.Padding.Left, -_layoutGrid.Padding.Top, _layoutGrid.ActualWidth,
                _layoutGrid.ActualHeight)
        });
        _maskAreaGeometryGroup.Children.Add(new RectangleGeometry {Rect = new Rect(new Point(X1, Y1), new Point(X2, Y2))});
        _layoutGrid.Clip = new RectangleGeometry
        {
            Rect = new Rect(0, 0, _layoutGrid.ActualWidth,
                _layoutGrid.ActualHeight)
        };
    }

結尾

到這裡,這個控制項的所有東西就講的差不多了,大家有沒有覺得還缺了點什麼?

對的,它還缺少了裁剪圖像源Transform變化時的過渡動畫,對於優秀的用戶體驗來說,這是不可或缺的!

之後我會抽時間補完這部分,並且跟大家講一點Composition Api的東西,請大家敬請期待!

這篇文章到此結束,謝謝大家閱讀!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • SSM整合 ssm框架 框架整合 在博客的前面介紹了mybatis,spring,springmvc的使用,那麼這篇博客將介紹將mybatis和spring,springmvc的整合。 <!-- more --> 整合之前,我們需要明白一個點,spring和mybatis之間進行整合不像spring ...
  • 部分基礎知識 1.python常量和變數 2.python基本數據類型 3.python用戶交互 4.python的if迴圈和嵌套 未完........ ...
  • 嗯,這是本人的第一篇隨筆,就從最簡單的單例模式開始,一步一步地記錄自己的成長。 單例模式是最常見的設計模式之一,在項目代碼中幾乎隨處可見。這個設計模式的目的就是為了保證實例只能存在一個。單例模式往下還能再細分為懶漢模式和餓漢模式。下麵逐個來看。 1.餓漢模式 餓漢模式的做法是在類載入的時候就完成實例 ...
  • 我的網站的圖片不想被公開瀏覽、下載、盜鏈怎麼辦?本文主要通過解讀一下ASP.NET Core對於靜態文件的處理方式的相關源碼,來看一下為什麼是wwwroot文件夾,如何修改或新增一個靜態文件夾,為什麼新增的文件夾名字不會被當做controller處理?訪問授權怎麼做? 一、靜態文件夾 所謂靜態文件, ...
  •  寫在前面 上篇文章我們講瞭如在在實際項目開發中使用Git來進行代碼的版本控制,當然介紹的都是比較常用的功能。今天我再帶著大家一起熟悉下一個ORM框架Dapper,實例代碼的演示編寫完成後我會通過Git命令上傳到GitHub上,正好大家可以再次熟悉下Git命令的使用,來鞏固上篇文章的知識。本篇文章 ...
  • 首先使用Nugut安裝NLog, NLog.Extensions.Logging,using NLog.Web,並且加上配置文件 ”nlog.config“,配置文件內容網上都可以百度的到。這是我自己的: 創建表: CREATE TABLE `sys_log` ( `Id` int(11) NOT ...
  • C#我只是一個萌新,由於搞過Java,還是可以看懂C#的 偶然間得到賽車游戲Extreme Drift的源碼 接下來我會花一段時間來解讀,這是一個我學習的過程,記錄在博客 等到我完全解讀之後,我也許會考慮再加入聯機功能等 當然,這個游戲用的是Unity引擎 首先,我先展示一下這個游戲的效果: 選車: ...
  • 非泛型集合的類和介面位於System.Collections命名空間 如:列表、隊列、位數組、哈希表和字典的集合 ArrayList 動態數組 可被單獨索引的對象的有序集合可以使用索引在指定的位置添加和移除項目,動態數組會自動重新調整它的大小允許在列表中進行動態記憶體分配、增加、搜索、排序 Capac ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...