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地址。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
智慧校园整体解决方案是响应国家教育信息化政策,结合教育改革和技术创新的产物。该方案以物联网、大数据、人工智能和移动互联技术为基础,旨在打造一个安全、高效、互动且环保的教育环境。方案强调从数字化校园向智慧校园的转变,通过自动数据采集、智能分析和按需服务,实现校园业务的智能化管理。 方案的总体设计原则包括应用至上、分层设计和互联互通,确保系统能够满足不同用户角色的需求,并实现数据和资源的整合与共享。框架设计涵盖了校园安全、管理、教学、环境等多个方面,构建了一个全面的校园应用生态系统。这包括智慧安全系统、校园身份识别、智能排课及选课系统、智慧学习系统、精品录播教室方案等,以支持个性化学习和教学评估。 建设内容突出了智慧安全和智慧管理的重要性。智慧安全管理通过分布式录播系统和紧急预案一键启动功能,增强校园安全预警和事件响应能力。智慧管理系统则利用物联网技术,实现人员和设备的智能管理,提高校园运营效率。 智慧教学部分,方案提供了智慧学习系统和精品录播教室方案,支持专业级学习硬件和智能化网络管理,促进个性化学习和教学资源的高效利用。同时,教学质量评估中心和资源应用平台的建设,旨在提升教学评估的科学性和教育资源的共享性。 智慧环境建设则侧重于基于物联网的设备管理,通过智慧教室管理系统实现教室环境的智能控制和能效管理,打造绿色、节能的校园环境。电子班牌和校园信息发布系统的建设,将作为智慧校园的核心和入口,提供教务、一卡通、图书馆等系统的集成信息。 总体而言,智慧校园整体解决方案通过集成先进技术,不仅提升了校园的信息化水平,而且优化了教学和管理流程,为学生、教师和家长提供了更加便捷、个性化的教育体验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值