笔者从零开始开发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图片裁剪工具封装相关推荐

  1. Android自定义相机拍照、图片裁剪的实现

    原文:Android自定义相机拍照.图片裁剪的实现 最近项目里面又要加一个拍照搜题的功能,也就是用户对着不会做的题目拍一张照片,将照片的文字使用ocr识别出来,再调用题库搜索接口搜索出来展示给用户,类 ...

  2. Android 中自定义View 裁剪扇形图片

    Android 中自定义View 裁剪扇形图片 当需要裁剪图片为扇形区域时,使用Canvas.clipPath(path)方法可以裁剪为扇形区域 ps:此方法会导致绘制图片边缘有锯齿,暂无解决方法(知 ...

  3. Android 自定义拍照,解决图片旋转,拍照参数设置兼容问题

    Android 自定义拍照,解决图片旋转,拍照参数设置兼容问题 参考文章: (1)Android 自定义拍照,解决图片旋转,拍照参数设置兼容问题 (2)https://www.cnblogs.com/ ...

  4. Android 自定义View 圆形圆角图片

    [Android 自定义View 圆形圆角图片] 基于Xfermode 实现 1.概述 在很久以前也写过一个利用Xfermode 实现圆形.圆角图片的(Android 完美实现图片圆角和圆形(对实现进 ...

  5. Android自定义动态布局 — 多图片上传

    Android自定义动态布局 - 多图片上传 本文介绍Android中动态布局添加图片,多图片上传. 项目中效果图:    技术点: 1.动态添加格局中的线条和添加图片的+号 2.多张图片异步上传 首 ...

  6. PHP图片裁剪_图片缩放_PHP生成缩略图

    2019独角兽企业重金招聘Python工程师标准>>> 在制作网页过程中,为了排版整齐美观,对网页中的图片处理成固定大小尺寸的图片,或是要截去图片边角中含有水印的图片,对于图片量多, ...

  7. android 自定义输入法布局,Android自定义输入法使用方法

    Android自定义输入法使用方法 时间:2017-04-21     来源:Android开发学习网 对于Android用户而言,一般都会使用第三方的输入法.可是在输入密码时(尤其是支付相关的密码) ...

  8. android 自定义dialog样式,Android 自定义dialog类

    首先定制style样式 styles.xml 加入自定义样式 @null true true true @color/transparent @color/transparent true 0.6 在 ...

  9. android 自定义相册选择,Android图片选择器,支持拍照、从相册选择、裁剪、自定义主题...

    在很多项目中都会用到图片选择器,比如在选择头像的时候,还会要求选择图片进行裁剪后再进行上传:而有的项目要支持拍照后进行图片裁剪再进行上传.由于Android系统兼容性问题,我们不得不考虑自己实现图片裁 ...

最新文章

  1. 明天面腾讯,我刷了这71道面试题...
  2. PyQt5中使用代码实现嵌套布局
  3. Security Tutorials系列文章第七章:User-Based Authorization(上)
  4. 欧姆龙c200he基本语言,欧姆龙C200HE-CPU42-ZE支持软件操作手册PLC手册 - 广州凌控
  5. 阿里云Linux安装软件镜像源
  6. 大数据架构与技术——(一)大数据概述
  7. 7-2 一行文本的字母数字个数 (10 分)
  8. 04树莓派迅雷远程下载
  9. remove debug symbols to a seperate file
  10. IOT-Studio 物联网应用开发实例
  11. RecyclerView 多条目
  12. sqlserver 与access,excel互相导入导出代码
  13. 数据库技术与应用(SQL Server)——【SQL Server单表查询(二)】
  14. 两万字长文-设计模式总结
  15. 2019中山纪念中学夏令营-Day9[JZOJ](第六次模拟赛)
  16. java 静态方法 实例化_Java系列之:看似简单的问题 静态方法和实例化方法的区别...
  17. 给定一个矩阵m*n,从左上角开始每次只能向右和向下走,最后到右下角的位置共有多少种路径。
  18. 区块链如何赋能数字城市建设?
  19. 达梦“科技自立”:数博会上的国产数据库之光
  20. 【车载开发系列】诊断故障码DTC基本概念与定义

热门文章

  1. 网络通信学习笔记之 ———Socket网络通信
  2. Oracle 12c 的安装步骤教程
  3. 只有程序员才懂的笑话
  4. Openstack 虚拟机云盘扩容
  5. C语言中常用到的几个与系统时间相关的结构体及其说明
  6. 【基础知识】什么是哈希冲突?
  7. 转战物联网·基础篇01-物联网之我见
  8. linux 配置 ftp 服务器以及 ftp 客户端登录
  9. vue 实现电子签名功能 支持生成图片
  10. 《算法导论》学习笔记第一章