[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写了另外一个扩展方法: Copy 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方法)。 Copy 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方法)。 Copy 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。 Copy 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代表该按钮的前后位置差值。 Copy 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元素,这里就不细讲了,直接贴代码。 Copy 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的东西,请大家敬请期待! 这篇文章到此结束,谢谢大家阅读! 作者:HHChaos 出处:https://www.cnblogs.com/hhchaos/p/10021952.html 本站使用「署名 4.0 国际」创作共享协议,转载请在文章明显位置注明作者及出处。https://www.cnblogs.com/hhchaos/p/10021952.html
50000+
5万行代码练就真实本领
17年
创办于2008年老牌培训机构
1000+
合作企业
98%
就业率

联系我们

电话咨询

0532-85025005

扫码添加微信