[UWP]如何实现UWP平台最佳图片裁剪控件
前几天我写了一个UWP图片裁剪控件ImageCropper(开源地址),自认为算是现阶段UWP社区里最好用的图片裁剪控件了,今天就来分享下我编码的过程。
为什么又要造轮子
因为开发需要,我们需要使用一个图片裁剪控件来编辑用户上传的图片。本着尽量不重复造轮子的原则,我找了下现在UWP生态圈里可用的图片裁剪控件,然后发现一个悲惨的事实:UWP生态圈甚至没有一个体验优秀的图片裁剪控件!
举例来说,就连现在商店里做的比较好的网易云音乐、IT之家以及爱奇艺等应用,他们使用的图片裁剪控件体验也糟糕的一塌糊涂(有认识他们开发人员的大佬,欢迎把我的这篇文章推荐给他们,不怕打脸)。
下图是爱奇艺与IT之家的头像裁剪控件:
那么好吧,我们只好又来造轮子了!
借鉴优秀的前辈
现阶段在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的东西,请大家敬请期待!
这篇文章到此结束,谢谢大家阅读!
[UWP]如何实现UWP平台最佳图片裁剪控件的更多相关文章
- Android开发技巧——定制仿微信图片裁剪控件
拍照--裁剪,或者是选择图片--裁剪,是我们设置头像或上传图片时经常需要的一组操作.上篇讲了Camera的使用,这篇讲一下我对图片裁剪的实现. 背景 下面的需求都来自产品. 裁剪图片要像微信那样,拖动 ...
- SNF开发平台WinForm-Grid表格控件大全
我们在开发系统时,会有很多种控件进行展示,甚至有一些为了方便的一些特殊需求. 那么下面就介绍一些我们在表格控件里常用的方便的控件: 1.Grid表格查询条 Grid表格下拉 3.Grid表格弹框选 ...
- SNF快速开发平台MVC-富文本控件集成了百度开源项目editor
一.效果如下: 二.在框架当中调用代码如下: 1.在js里配置如下: <script type="text/javascript"> var viewModel =fu ...
- UWP开发必备:常用数据列表控件汇总比较
今天是想通过实例将UWP开发常用的数据列表做汇总比较,作为以后项目开发参考.UWP开发必备知识点总结请参照[UWP开发必备以及常用知识点总结]. 本次主要讨论以下控件: GridView:用于显示数据 ...
- UWP &WP8.1 依赖属性和用户控件 依赖属性简单使用 uwp添加UserControl
上面说 附加属性.这章节说依赖属性. 所谓依赖属性.白话讲就是添加一个公开的属性. 同样,依赖属性的用法和附加属性的用法差不多. 依赖属性是具有一个get,set的属性,以及反调函数. 首先是声明依赖 ...
- .NET各大平台数据列表控件绑定原理及比较(WebForm、Winform、WPF)
说说WebForm: 数据列表控件: WebForm 下的列表绑定控件基本就是GridView.DataList.Repeater:当然还有其它DropDownList.ListBox等. 它们的共同 ...
- 基于PhotoView的头像/圆形裁剪控件
常见的图片裁剪有两种,一种是图片固定,裁剪框移动放缩来确定裁剪区域,早期见的比较多,缺点在于不能直接预览裁剪后的效果:还有一种现在比较普遍了,就是裁剪框固定,直接拖动缩放图片,便于预览裁剪结果. 我做 ...
- VisualStudio移动开发(C#、VB.NET)Smobiler开发平台——AlbumView相册控件的使用方式
AlbumView控件 一. 样式一 我们要实现上图中的效果,需要如下的操作: 从工具栏上的“Smobiler Components”拖动一个AlbumView控件到窗体界面上 修改 ...
- Win10 for Phone 裁剪控件
<Page.BottomAppBar> <CommandBar x:Name="appBar"> <AppBarButton Label=" ...
随机推荐
- 使用AsyncTask类实现简单的异步处理操作
AsyncTask: 1.这是一种相比Handler更轻量级的处理异步任务的工具类 2.它和Handler类一样,都是为了不影响主线程(UI)而使用的((注:UI的更新只能在主线程中完成) 3.这个工 ...
- JavaScript: DOM Docunment
Meaning: In browser , we exchange data using JavaScript code with user. We should know that most of ...
- week07 13.3 NewsPipeline之 三News Deduper之 tf_idf 查重
我们运行看结果 安装包sklearn 安装numpy 安装scipy 终于可以啦 我们把安装的包都写在文件里面吧 4行4列 轴对称 只需要看一半就可以 横着看 竖着看都行 数值越接近1 表示越相似 我 ...
- VC6的工程转到VC2010或更高版本出现fatal error C1189编译错误的解决方法
以前也遇到过,当时解决了没写下来,这次正好又遇到了,就顺手写一下吧,别下次又忘记了. 当VC6的工程转到VC2010或更高版本时编译出现如下错误: c:\program files\microsoft ...
- 吴裕雄 python深度学习与实践(7)
import cv2 import numpy as np img = np.mat(np.zeros((,))) cv2.imshow("test",img) cv2.waitK ...
- jQuery之动画
动画相关方法: .hide()..show()..toggle() 参数:null 或 (duration, easing, callblack) .fadeIn..fadeout ..fadeTog ...
- linux nginx 基本用法
nginx -s reload -p <nginx环境目录> -c <指定的配置文件> 其中-p -c 为可选,不写为默认路径和配置 在执行命令之前可通过 nginx -t - ...
- github 访问速度太慢
151.101.112.249 http://global-ssl.fastly.Net 192.30.253.112 http://github.com 在hosts文件添加 另 pip里的pyec ...
- oracle 执行顺序 select查询优化
今天把这几天做的练习复习了一下,不知道自己写得代码执行的效率如何以及要如何提高,于是乎上网开始研究一些材料,现整理如下: 首先,要了解在Oracle中Sql语句运行的机制.以下是sql语句的执行步骤: ...
- 事件委托在ios下面失效
$(document).on("click","目标class",function(){ //安卓下点击可以,ios下面失效 }) 百度了下说是H5新定义的, ...