[UWP]如何实现UWP平台最佳图片裁剪控件

简介: 原文:[UWP]如何实现UWP平台最佳图片裁剪控件前几天我写了一个UWP图片裁剪控件ImageCropper(开源地址),自认为算是现阶段UWP社区里最好用的图片裁剪控件了,今天就来分享下我编码的过程。
原文: [UWP]如何实现UWP平台最佳图片裁剪控件

前几天我写了一个UWP图片裁剪控件ImageCropper(开源地址),自认为算是现阶段UWP社区里最好用的图片裁剪控件了,今天就来分享下我编码的过程。

为什么又要造轮子

因为开发需要,我们需要使用一个图片裁剪控件来编辑用户上传的图片。本着尽量不重复造轮子的原则,我找了下现在UWP生态圈里可用的图片裁剪控件,然后发现一个悲惨的事实:UWP生态圈甚至没有一个体验优秀的图片裁剪控件!

举例来说,就连现在商店里做的比较好的网易云音乐、IT之家以及爱奇艺等应用,他们使用的图片裁剪控件体验也糟糕的一塌糊涂(有认识他们开发人员的大佬,欢迎把我的这篇文章推荐给他们,不怕打脸)。

下图是爱奇艺与IT之家的头像裁剪控件:

糟糕的图片裁剪体验

那么好吧,我们只好又来造轮子了!

借鉴优秀的前辈

现阶段在Windows平台上,最让我称佩的裁剪图片的应用就是Windows照片了。

Windows照片

它有以下两个优点:

  • 裁剪区域永远显示在视觉中心,突出重点;
  • 操作体验顺畅,触屏操作也能有很好体验。

这次我们就来“抄袭”一下这个系统应用。

如何实现

有了实现目标,接下来就是思考如何编码实现了。

需要哪些属性来控制裁剪区域

分析一下这个控件的组成部分,其实就是由三部分组成的:最下层裁剪源图像,上层控制裁剪区域的四个按钮,以及遮盖在图像上的黑色半透明遮罩层。

所以我定义了下面几个依赖属性来控制界面:

  • SourceImage:类型为WriteableBitmap,控制裁剪图像源;
  • X1,Y1,X2,Y2:这四个double值,控制剪裁区域左上角与右下角两个点坐标;
  • AspectRatio:类型为double值,控制裁剪图像纵横比;
  • MaskArea:类型为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()
    {
        if (ImageTransform == null)
            ImageTransform = new CompositeTransform();
        _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()
    {
        _maskArea.Children.Clear();
        _maskArea.Children.Add(new RectangleGeometry
        {
            Rect = new Rect(-_layoutGrid.Padding.Left, -_layoutGrid.Padding.Top, _layoutGrid.ActualWidth,
                _layoutGrid.ActualHeight)
        });
        _maskArea.Children.Add(new RectangleGeometry {Rect = new Rect(new Point(X1, Y1), new Point(X2, Y2))});
        MaskArea = _maskArea;
        _layoutGrid.Clip = new RectangleGeometry
        {
            Rect = new Rect(0, 0, _layoutGrid.ActualWidth,
                _layoutGrid.ActualHeight)
        };
    }

结尾

到这里,这个控件的所有东西就讲的差不多了,大家有没有觉得还缺了点什么?

对的,它还缺少了裁剪图像源Transform变化时的过渡动画,对于优秀的用户体验来说,这是不可或缺的!

之后我会抽时间补完这部分,并且跟大家讲一点Composition Api的东西,请大家敬请期待!

这篇文章到此结束,谢谢大家阅读!

目录
相关文章
|
11月前
|
数据可视化 图形学 流计算
Unity 操作常用控件(下)
Unity 操作常用控件(下)
|
XML Android开发 数据格式
Android开发中那些你费力写的控件,其实原生都有
实现一个开关的切换,你会怎么做,写一个layout,一半点击为开,一半点击为关,还是两张图片,点一下开,再点一下关?让你实现一个根据用户的输入弹出一个下拉菜单等等,其实都大可没有必要去自己写,本身Android里都有,下面对各个控件,我会一一举例。
179 0
|
测试技术
艾伟:WinForm控件开发总结(二)------使用和调试自定义控件
在上一篇文章里我们创建了一个简单的控件FirstControl,现在我来介绍一下怎么使用和调试自己的控件。我希望将过程写的尽可能的详细,让想学习控件开发的朋友容易上手,高手们见谅。       在同一个solution里添加一个Windows Application工程(在Solution Explorer里右键点击CustomControlSample solution选择Add->New Project…),命名为TestControl。
890 0
|
Windows
[UWP开发]NavigationView基础使用方法
原文:[UWP开发]NavigationView基础使用方法 [UWP开发]NavigationView基础使用方法 NavigationView是秋季创意者更新(16299)引入的新控件,用于生成Windows特色的导航栏。
2308 0
|
Android开发 iOS开发 UED
[UWP]浅谈按钮设计
原文:[UWP]浅谈按钮设计 一时兴起想谈谈UWP按钮的设计。 按钮是UI中最重要的元素之一,可能也是用得最多的交互元素。好的按钮设计可以有效提高用户体验,构造让人眼前一亮的UI。而且按钮通常不会影响布局,小小的按钮无论怎么改也不会对性能有多大影响,所以不少注重细节的设计师最为热衷修改按钮。
1501 0
|
Web App开发 C# Windows
制作一个简单的WPF图片浏览器
原文:制作一个简单的WPF图片浏览器 注:本例选自MSDN样例,并略有改动。先看效果: 这里实现了以下几个功能:1.  对指定文件夹下所有JPG文件进行预览2.  对选定图片进行旋转3.  对选定图片进行灰度处理4.  对选定图片进行裁切处理5.  无限制的恢复功能6. 类似加入购物车的功能以下来看看其实现过程。
983 0
|
前端开发 C#
silverlight,WPF动画终极攻略之番外 3D切换导航篇(Blend 4开发)
原文:silverlight,WPF动画终极攻略之番外 3D切换导航篇(Blend 4开发) 这篇介绍的是3D导航,点击图标,页面360°翻转的效果!有什么不足的欢迎大家指出来。 1.新建一个usercontrol,命名为menu. 2.按照下图设置一下属性。
1241 0
|
C# C++ Windows
WPF中不规则窗体与WindowsFormsHost控件的兼容问题完美解决方案
原文:WPF中不规则窗体与WindowsFormsHost控件的兼容问题完美解决方案          首先先得瑟一下,有关WPF中不规则窗体与WindowsFormsHost控件不兼容的问题,网上给出的解决方案不能满足所有的情况,是有特定条件的,比如  WPF中不规则窗体与WebBrowser控件的兼容问题解决办法。
1309 0