android 自定义图片裁剪,Android图片裁剪工具封装
笔者从零开始开发Android,而且是跳过java直接使用kotlin开发,这其中的好处是可以避开java这门传统语言诸多的潜规则,难处是相比资深Android开发者少了许多可以现用的工具库。比如Android对图片的支持就非常开放,换言之就是非常依赖一个成熟的工具库(比如Glide),(相比web里标签就安全易用很多)。
包括本文将实现的工具在内,笔者目前也收集了整整2个成熟好用的图片相关工具类,一个就是Glide,另一个是Subsampling Scale Image View,并且膨胀地以为不再需要其他任何图片相关的工具库了。这个想法差点被动摇的一次就是现在需要加一个图片裁剪功能了,一想到网上现有的那些酷炫的工具库,就担心起要不要用和用哪个的问题,但是当笔者看了这篇文章——How We Created uCrop,就对引入第三方图片裁剪库更加抵触了,思来想去自己内心的想法都是:
为了【传一张图片然后确定参数挖出一张新图片来】这样一个需求而依赖【别人开发的自己无法控制或灵活定制的】库是不值得的。
本文将纯依赖SubsamplingScaleImageView来实现一个支持缩放和旋转的图片裁剪组件,SubsamplingScaleImageView帮助我完成了其中图片缩放、旋转、拖拽的底层触摸交互以及图片的内存管理工作,这让我可以专心的利用这些动作来确定一件事:我需要裁剪出一张图片如何旋转/缩放后的哪个部分。然后就可以根据这些信息从原图创建出需要的裁剪图。
依靠SubsamplingScaleImageView已经具备的能力,这个图片裁剪组件的kotlin代码目前包含非关键代码在内也只有300行出头。
实现过程总览
纵观整个图片裁剪工具,其实现分这么几步:
自定义View的基本结构和布局
拖拽位置边界的保护
旋转能力和执行裁剪
自定义View
首先给这个图片裁剪工具起个难听的名字叫做YmageCutterView,其布局基于ConstraintLayout中间放一个指定宽高的矩形作为裁剪框,四周是四个半透明矩形作为遮罩,底下是原图。设想就是缩放和拖拽原图,再以中间裁剪框作为边界裁剪出目标图。像这样:
android:layout_height="match_parent"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="#333333">
android:id="@+id/ymage_cutter_origin"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_width="0dp"
android:layout_height="0dp"
android:background="#99000000"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/ymage_cutter_frame" />
android:id="@+id/ymage_cutter_mask_right"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="#99000000"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/ymage_cutter_frame"
app:layout_constraintEnd_toEndOf="parent"/>
android:id="@+id/ymage_cutter_mask_top"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="#99000000"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/ymage_cutter_frame"
app:layout_constraintStart_toEndOf="@id/ymage_cutter_mask_left"
app:layout_constraintEnd_toStartOf="@id/ymage_cutter_mask_right"/>
android:id="@+id/ymage_cutter_mask_bottom"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="#99000000"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/ymage_cutter_frame"
app:layout_constraintStart_toEndOf="@id/ymage_cutter_mask_left"
app:layout_constraintEnd_toStartOf="@id/ymage_cutter_mask_right"/>
android:id="@+id/ymage_cutter_frame"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/ymage_cutter_mask_left"
app:layout_constraintEnd_toStartOf="@id/ymage_cutter_mask_right"
app:layout_constraintWidth_percent="0.7"
app:layout_constraintWidth_max="300dp"
app:layout_constraintDimensionRatio="1:1"/>
然后为了使裁剪尺寸支持自定义,需要设置一个自定义参数叫ratio,用来传入需要裁剪图片的宽高比,比如支持这么使用:
android:layout_width="match_parent"
android:layout_height="match_parent"
app:ratio="4:3"/>
上面这样中间的裁剪框就是个4:3的矩形。至于用户通过拖拽实时更改这个裁剪框尺寸这个需求,本文还未去实现,讲真其使用场景也不多,后续再考虑补足。由于使用的是ConstraintLayout,只需要将传入的ratio属性设置给中间裁剪框的layoutParams.dimensionRatio就完成裁剪图片宽高比的更新了。
拖拽位置边界的保护
SubsamplingScaleImageView本身已经实现了较完整的触摸交互,包括拖拽和缩放,在它的基础上我们先加一个边界保护:
originIV.setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
inDrag = false
inCheck = false
lazyCheck?.cancel()
lazyCheck = null
lazyCheck = Timer().schedule(100) {
if (!inDrag && !inCheck) {
inCheck = true
resolvePositionCheck()
}
}
}
MotionEvent.ACTION_DOWN -> {
inDrag = true
}
}
return@setOnTouchListener false
}
以上代码做了这么几件事情:
手指按下时指示正在拖拽
手指抬起后100毫秒内若未再次按下,则执行边界检查
边界检查实现如下:
private fun resolvePositionCheck() {
val center = originIV.center ?: return
val x: Float
val y: Float
if (originIV.orientation == rotation0 || originIV.orientation == rotation180) {
val left = frameV.width/2/originIV.scale
val top = frameV.height/2/originIV.scale
val right = left + (originIV.sWidth*originIV.scale - frameV.width)/originIV.scale
val bottom = top + (originIV.sHeight*originIV.scale - frameV.height)/originIV.scale
limitRect.set(left, top, right, bottom)
x = if (center.x < limitRect.left) {
if (originIV.sWidth*originIV.scale < frameV.width) {
if (center.x < limitRect.right) {
limitRect.right
} else {
center.x
}
} else {
limitRect.left
}
} else if (center.x > limitRect.right) {
if (originIV.sWidth*originIV.scale < frameV.width) {
limitRect.left
} else {
limitRect.right
}
} else {
center.x
}
y = if (center.y < limitRect.top) {
if (originIV.sHeight*originIV.scale < frameV.height) {
if (center.y < limitRect.bottom) {
limitRect.bottom
} else {
center.y
}
} else {
limitRect.top
}
} else if (center.y > limitRect.bottom) {
if (originIV.sHeight*originIV.scale < frameV.height) {
limitRect.top
} else {
limitRect.bottom
}
} else {
center.y
}
} else {
val left = frameV.width/2/originIV.scale
val top = frameV.height/2/originIV.scale
val right = left + (originIV.sHeight*originIV.scale - frameV.width)/originIV.scale
val bottom = top + (originIV.sWidth*originIV.scale - frameV.height)/originIV.scale
limitRect.set(left, top, right, bottom)
x = if (center.x < limitRect.left) {
if (originIV.sHeight*originIV.scale < frameV.width) {
if (center.x < limitRect.right) {
limitRect.right
} else {
center.x
}
} else {
limitRect.left
}
} else if (center.x > limitRect.right) {
if (originIV.sHeight*originIV.scale < frameV.width) {
limitRect.left
} else {
limitRect.right
}
} else {
center.x
}
y = if (center.y < limitRect.top) {
if (originIV.sWidth*originIV.scale < frameV.height) {
if (center.y < limitRect.bottom) {
limitRect.bottom
} else {
center.y
}
} else {
limitRect.top
}
} else if (center.y > limitRect.bottom) {
if (originIV.sWidth*originIV.scale < frameV.height) {
limitRect.top
} else {
limitRect.bottom
}
} else {
center.y
}
}
if (x != center.x || y != center.y) {
GlobalScope.launch(Dispatchers.Main) {
originIV.animateCenter(PointF(x, y))
?.withDuration(100)
?.withEasing(SubsamplingScaleImageView.EASE_OUT_QUAD)
?.withInterruptible(false)
?.start()
}
}
}
算是这个裁剪组件里最长的一个方法了,比较不美观的套了很多条件检查值得去优化,不过做的事情描述起来很简单,就是如果原图没有完全包含在裁剪框中则按最近路径移动进来。比如安卓微信里的头像裁剪,图片就可以任意移出裁剪框,笔者觉得这样不妥。
旋转能力和执行裁剪
图片的旋转只需设置SubsamplingScaleImageView的orientation即可,麻烦的是不同旋转角度下原图的宽高要区分处理,也就是说,0度和180度下原图的宽就是宽,高就是高,但90度和270度下原图的宽是高而高是宽。这在上文边界保护和下文执行裁剪时都要加以区分处理。
执行裁剪作为一个方法提供给外界调用,实现如下:
fun shutter(): Bitmap? {
if (resultBitmap?.isRecycled == false) {
resultBitmap?.recycle()
}
val center = originIV.center ?: return null
val cut = Rect(
(center.x - frameV.width/2/originIV.scale).toInt(),
(center.y - frameV.height/2/originIV.scale).toInt(),
(center.x + frameV.width/2/originIV.scale).toInt(),
(center.y + frameV.height/2/originIV.scale).toInt()
)
val bitmap = BitmapFactory.decodeFile(src)
val matrix = Matrix()
matrix.postRotate(originIV.orientation.toFloat())
val rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
resultBitmap = Bitmap.createBitmap(rotatedBitmap, Math.max(cut.left, 0), Math.max(cut.top, 0), Math.min(cut.right-cut.left, rotatedBitmap.width), Math.min(cut.bottom-cut.top, rotatedBitmap.height), null, true)
if (bitmap != resultBitmap && !bitmap.isRecycled) {
bitmap.recycle()
}
if (rotatedBitmap != resultBitmap && !rotatedBitmap.isRecycled) {
rotatedBitmap.recycle()
}
return resultBitmap
}
此方法做的事情就是,先将原图根据当前的旋转角度旋转得到源bitmap,再根据当前原图的缩放和位置确定裁剪图片的起点坐标(x,y)以及宽高,调用createBitmap挖出裁剪图,最后返回裁剪后的目标bitmap。
总结
至此这个图片裁剪组件已经完成,说白了是以自定义View的形式对SubsamplingScaleImageView进行扩展和二次封装,使用起来需要把这个View放到自己的Activity中,目前支持传入ratio属性设置裁剪图的宽高比,并提供了这么几个方法供调用:
reset() 重设图片的旋转和缩放
rotate(degree: Int) 执行旋转,角度必须为 0, 90, 180, 270的其中一个
shutter(): Bitmap? 按快门方法,执行裁剪并返回bitmap
最后再次膜拜SubsamplingScaleImageView并附上本文项目Github地址。
android 自定义图片裁剪,Android图片裁剪工具封装相关推荐
- Android自定义相机拍照、图片裁剪的实现
原文:Android自定义相机拍照.图片裁剪的实现 最近项目里面又要加一个拍照搜题的功能,也就是用户对着不会做的题目拍一张照片,将照片的文字使用ocr识别出来,再调用题库搜索接口搜索出来展示给用户,类 ...
- Android 中自定义View 裁剪扇形图片
Android 中自定义View 裁剪扇形图片 当需要裁剪图片为扇形区域时,使用Canvas.clipPath(path)方法可以裁剪为扇形区域 ps:此方法会导致绘制图片边缘有锯齿,暂无解决方法(知 ...
- Android 自定义拍照,解决图片旋转,拍照参数设置兼容问题
Android 自定义拍照,解决图片旋转,拍照参数设置兼容问题 参考文章: (1)Android 自定义拍照,解决图片旋转,拍照参数设置兼容问题 (2)https://www.cnblogs.com/ ...
- Android 自定义View 圆形圆角图片
[Android 自定义View 圆形圆角图片] 基于Xfermode 实现 1.概述 在很久以前也写过一个利用Xfermode 实现圆形.圆角图片的(Android 完美实现图片圆角和圆形(对实现进 ...
- Android自定义动态布局 — 多图片上传
Android自定义动态布局 - 多图片上传 本文介绍Android中动态布局添加图片,多图片上传. 项目中效果图: 技术点: 1.动态添加格局中的线条和添加图片的+号 2.多张图片异步上传 首 ...
- PHP图片裁剪_图片缩放_PHP生成缩略图
2019独角兽企业重金招聘Python工程师标准>>> 在制作网页过程中,为了排版整齐美观,对网页中的图片处理成固定大小尺寸的图片,或是要截去图片边角中含有水印的图片,对于图片量多, ...
- android 自定义输入法布局,Android自定义输入法使用方法
Android自定义输入法使用方法 时间:2017-04-21 来源:Android开发学习网 对于Android用户而言,一般都会使用第三方的输入法.可是在输入密码时(尤其是支付相关的密码) ...
- android 自定义dialog样式,Android 自定义dialog类
首先定制style样式 styles.xml 加入自定义样式 @null true true true @color/transparent @color/transparent true 0.6 在 ...
- android 自定义相册选择,Android图片选择器,支持拍照、从相册选择、裁剪、自定义主题...
在很多项目中都会用到图片选择器,比如在选择头像的时候,还会要求选择图片进行裁剪后再进行上传:而有的项目要支持拍照后进行图片裁剪再进行上传.由于Android系统兼容性问题,我们不得不考虑自己实现图片裁 ...
最新文章
- 明天面腾讯,我刷了这71道面试题...
- PyQt5中使用代码实现嵌套布局
- Security Tutorials系列文章第七章:User-Based Authorization(上)
- 欧姆龙c200he基本语言,欧姆龙C200HE-CPU42-ZE支持软件操作手册PLC手册 - 广州凌控
- 阿里云Linux安装软件镜像源
- 大数据架构与技术——(一)大数据概述
- 7-2 一行文本的字母数字个数 (10 分)
- 04树莓派迅雷远程下载
- remove debug symbols to a seperate file
- IOT-Studio 物联网应用开发实例
- RecyclerView 多条目
- sqlserver 与access,excel互相导入导出代码
- 数据库技术与应用(SQL Server)——【SQL Server单表查询(二)】
- 两万字长文-设计模式总结
- 2019中山纪念中学夏令营-Day9[JZOJ](第六次模拟赛)
- java 静态方法 实例化_Java系列之:看似简单的问题 静态方法和实例化方法的区别...
- 给定一个矩阵m*n,从左上角开始每次只能向右和向下走,最后到右下角的位置共有多少种路径。
- 区块链如何赋能数字城市建设?
- 达梦“科技自立”:数博会上的国产数据库之光
- 【车载开发系列】诊断故障码DTC基本概念与定义