Android Kotlin仿微信頭像裁剪圖片的方法示例
0.前言
最近突發(fā)了很多事情,又跟康仔跳票了,無可奈何,不好意思了。最近生活上有很多感悟,一個(gè)男人的牛逼就在于平衡工作,學(xué)習(xí)和家庭,這個(gè)點(diǎn)很難把握,既要保證家庭和睦,又要保證自己價(jià)值的實(shí)現(xiàn)從而避免墮入平庸,每個(gè)人的狀況都是不一樣的,沒有什么經(jīng)驗(yàn)是可以照搬的,怎么說呢,不斷摸索吧。
1.分析
整個(gè)效果是仿照微信來做的,效果如圖所示:

整個(gè)效果就是從圖庫(kù)選取一張圖片,并進(jìn)行裁剪,從圖庫(kù)選取沒什么好說的,就說說怎么做的裁剪控件吧,這個(gè)裁剪控件就是ClipImageView,可以看到它有一個(gè)陰影遮罩,一個(gè)透明的框,還有圖片的顯示,以及可以移動(dòng)圖片。
2.代碼
class ClipImageView(context: Context, attributeSet: AttributeSet?) : ImageView(context, attributeSet)
{
private val paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
var clipWidth = 300
set(value)
{
field = value
if (isAttachedToWindow)
{
postInvalidate()
}
}
var clipHeight = 300
set(value)
{
field = value
if (isAttachedToWindow)
{
postInvalidate()
}
}
var minScale = 1.0f
var maxScale = 1.0f
private var rectColor = Color.BLACK
private var lastTouchX = 0F
private var lastTouchY = 0F
private val transMatrix = Matrix()
private var isTouching = false
private var scale = 1.0f
var onsaveClipImageListener: OnSaveClipImageListsner? = null
private val scaleGestureDetectorListener = object : ScaleGestureDetector.SimpleOnScaleGestureListener()
{
override fun onScale(detector: ScaleGestureDetector?): Boolean
{
val curScaleFactor = detector?.scaleFactor ?: 1.0f
var curScale = scale * curScaleFactor
curScale = if (curScale >= 1.0f) Math.min(maxScale, curScale) else Math.max(minScale, curScale)
val scaleFactor = if (curScale > scale) 1 + (curScale - scale) / scale else 1.0f - (scale - curScale) / scale
transMatrix.postScale(scaleFactor, scaleFactor, detector?.focusX
?: 0f, detector?.focusY ?: 0f)
postInvalidate()
scale = curScale
return true
}
override fun onScaleEnd(detector: ScaleGestureDetector?)
{
super.onScaleEnd(detector)
}
}
private var scaleGestureDetector: ScaleGestureDetector
constructor(context: Context) : this(context, null)
init
{
paint.strokeJoin = Paint.Join.ROUND
scaleGestureDetector = ScaleGestureDetector(context, scaleGestureDetectorListener)
if (attributeSet != null)
{
pareseAttributeSet(attributeSet)
}
setBackgroundColor(Color.WHITE)
}
private fun pareseAttributeSet(attributeSet: AttributeSet)
{
val typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.ClipImageView)
clipWidth = typedArray.getDimensionPixelOffset(R.styleable.ClipImageView_clip_width, clipWidth)
clipHeight = typedArray.getDimensionPixelOffset(R.styleable.ClipImageView_clip_width, clipHeight)
rectColor = typedArray.getColor(R.styleable.ClipImageView_rect_color, rectColor)
minScale = typedArray.getFloat(R.styleable.ClipImageView_min_scale, minScale)
maxScale = typedArray.getFloat(R.styleable.ClipImageView_max_scale, maxScale)
typedArray.recycle()
}
override fun layout(l: Int, t: Int, r: Int, b: Int)
{
super.layout(l, t, r, b)
if (clipWidth > measuredWidth)
{
clipWidth = measuredWidth
}
if (clipHeight > measuredHeight)
{
clipHeight = measuredHeight
}
}
override fun onTouchEvent(event: MotionEvent?): Boolean
{
if (event?.pointerCount ?: 1 >= 2)
{
isTouching = false
return scaleGestureDetector.onTouchEvent(event)
}
else
{
when (event?.action)
{
MotionEvent.ACTION_DOWN ->
{
isTouching = true
lastTouchX = event.x
lastTouchY = event.y
}
MotionEvent.ACTION_MOVE ->
{
if (isTouching && event.pointerCount == 1)
{
val offsetX = event.x - lastTouchX
val offsetY = event.y - lastTouchY
transMatrix.postTranslate(offsetX, offsetY)
lastTouchX = event.x
lastTouchY = event.y
postInvalidate()
}
}
MotionEvent.ACTION_UP ->
{
isTouching = false
}
}
return true
}
}
override fun onDraw(canvas: Canvas?)
{
canvas?.let {
val saveState = it.saveCount
it.save()
it.concat(transMatrix)
super.onDraw(canvas)
it.restoreToCount(saveState)
drawMask(it)
drawRect(it)
}
}
private fun drawMask(canvas: Canvas)
{
paint.style = Paint.Style.FILL
paint.color = Color.parseColor("#A0000000")
canvas.drawRect(0.0f, 0.0f, width.toFloat(), (height / 2 - clipHeight / 2).toFloat(), paint)
canvas.drawRect((width / 2 + clipWidth / 2).toFloat(), (height / 2 - clipHeight / 2).toFloat(), width.toFloat(), (height / 2 + clipHeight / 2).toFloat(), paint)
canvas.drawRect(0.0f, (height / 2 + clipHeight / 2).toFloat(), width.toFloat(), height.toFloat(), paint)
canvas.drawRect(0.0f, (height / 2 - clipHeight / 2).toFloat(), (width / 2 - clipWidth / 2).toFloat(), (height / 2 + clipHeight / 2).toFloat(), paint)
}
private fun drawRect(canvas: Canvas)
{
paint.style = Paint.Style.FILL_AND_STROKE
paint.color = rectColor
paint.strokeWidth = 4.0f
val offset = paint.strokeWidth / 2
val left: Float = (width / 2 - clipWidth / 2).toFloat() - offset
val top: Float = (height / 2 - clipHeight / 2).toFloat() - offset
val right: Float = (width / 2 + clipWidth / 2).toFloat() + offset
val bottom: Float = (height / 2 + clipHeight / 2).toFloat() + offset
canvas.drawLine(left, top, right, top, paint)
canvas.drawLine(right, top, right, bottom, paint)
canvas.drawLine(left, bottom, right, bottom, paint)
canvas.drawLine(left, top, left, bottom, paint)
}
interface OnSaveClipImageListsner
{
fun onImageFinishedSav()
}
inner class SaveTask(private val filePath: String) : AsyncTask<Unit, Unit, Unit>()
{
override fun doInBackground(vararg params: Unit?): Unit
{
saveClipImage(filePath)
}
override fun onPostExecute(result: Unit?)
{
super.onPostExecute(result)
onsaveClipImageListener?.onImageFinishedSav()
}
}
fun clipAndSaveImage(filePath: String)
{
SaveTask(filePath).execute()
}
private fun saveClipImage(filePath: String)
{
val clipBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val clipCanvas = Canvas(clipBitmap)
draw(clipCanvas)
try
{
val outputStream = FileOutputStream(filePath)
val bitmap = Bitmap.createBitmap(clipBitmap, width / 2 - clipWidth / 2, height / 2 - clipHeight / 2, clipWidth, clipHeight, transMatrix, true)
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
outputStream.close()
}
catch (e: IOException)
{
e.printStackTrace()
}
}
}
可以發(fā)現(xiàn)這段代碼是繼承自ImageView。
先看代碼段
private fun pareseAttributeSet(attributeSet: AttributeSet)
{
val typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.ClipImageView)
clipWidth = typedArray.getDimensionPixelOffset(R.styleable.ClipImageView_clip_width, clipWidth)
clipHeight = typedArray.getDimensionPixelOffset(R.styleable.ClipImageView_clip_width, clipHeight)
rectColor = typedArray.getColor(R.styleable.ClipImageView_rect_color, rectColor)
minScale = typedArray.getFloat(R.styleable.ClipImageView_min_scale, minScale)
maxScale = typedArray.getFloat(R.styleable.ClipImageView_max_scale, maxScale)
typedArray.recycle()
}
這里解析布局文件的里的屬性,其中clipwidth和clipheight分別代表裁剪框的寬度和高度,minScale和maxScale是最小和最大的縮放程度。
override fun layout(l: Int, t: Int, r: Int, b: Int)
{
super.layout(l, t, r, b)
if (clipWidth > measuredWidth)
{
clipWidth = measuredWidth
}
if (clipHeight > measuredHeight)
{
clipHeight = measuredHeight
}
}
在layout方法里設(shè)置clipWidth和clipHeight,防止設(shè)置值大于控件大小。
drawMask方法和drawRect方法是用來繪制遮罩層和裁剪框的,其中遮罩層就是四個(gè)方形,而裁剪框就是一個(gè)矩形的外框。
private fun drawMask(canvas: Canvas)
{
paint.style = Paint.Style.FILL
paint.color = Color.parseColor("#A0000000")
canvas.drawRect(0.0f, 0.0f, width.toFloat(), (height / 2 - clipHeight / 2).toFloat(), paint)
canvas.drawRect((width / 2 + clipWidth / 2).toFloat(), (height / 2 - clipHeight / 2).toFloat(), width.toFloat(), (height / 2 + clipHeight / 2).toFloat(), paint)
canvas.drawRect(0.0f, (height / 2 + clipHeight / 2).toFloat(), width.toFloat(), height.toFloat(), paint)
canvas.drawRect(0.0f, (height / 2 - clipHeight / 2).toFloat(), (width / 2 - clipWidth / 2).toFloat(), (height / 2 + clipHeight / 2).toFloat(), paint)
}
private fun drawRect(canvas: Canvas)
{
paint.style = Paint.Style.FILL_AND_STROKE
paint.color = rectColor
paint.strokeWidth = 4.0f
val offset = paint.strokeWidth / 2
val left: Float = (width / 2 - clipWidth / 2).toFloat() - offset
val top: Float = (height / 2 - clipHeight / 2).toFloat() - offset
val right: Float = (width / 2 + clipWidth / 2).toFloat() + offset
val bottom: Float = (height / 2 + clipHeight / 2).toFloat() + offset
canvas.drawLine(left, top, right, top, paint)
canvas.drawLine(right, top, right, bottom, paint)
canvas.drawLine(left, bottom, right, bottom, paint)
canvas.drawLine(left, top, left, bottom, paint)
}
接著看如何讓圖片隨手指移動(dòng)和縮放,這里說一下transMatrix,這個(gè)是Matrix類,通過它應(yīng)用到Canvas來實(shí)現(xiàn)縮放和移動(dòng)。
override fun onTouchEvent(event: MotionEvent?): Boolean
{
if (event?.pointerCount ?: 1 >= 2)
{
isTouching = false
return scaleGestureDetector.onTouchEvent(event)
}
else
{
when (event?.action)
{
MotionEvent.ACTION_DOWN ->
{
isTouching = true
lastTouchX = event.x
lastTouchY = event.y
}
MotionEvent.ACTION_MOVE ->
{
if (isTouching && event.pointerCount == 1)
{
val offsetX = event.x - lastTouchX
val offsetY = event.y - lastTouchY
transMatrix.postTranslate(offsetX, offsetY)
lastTouchX = event.x
lastTouchY = event.y
postInvalidate()
}
}
MotionEvent.ACTION_UP ->
{
isTouching = false
}
}
return true
}
}
當(dāng)兩個(gè)手指觸摸時(shí),由移動(dòng)事件有ScaleGestureDetector處理縮放,否則進(jìn)行移動(dòng)。
先看移動(dòng):
將移動(dòng)的距離應(yīng)用到transMatrix,并調(diào)用postInvalidate()重新繪制。
再看縮放處理
private val scaleGestureDetectorListener = object : ScaleGestureDetector.SimpleOnScaleGestureListener()
{
override fun onScale(detector: ScaleGestureDetector?): Boolean
{
val curScaleFactor = detector?.scaleFactor ?: 1.0f
var curScale = scale * curScaleFactor
curScale = if (curScale >= 1.0f) Math.min(maxScale, curScale) else Math.max(minScale, curScale)
val scaleFactor = if (curScale > scale) 1 + (curScale - scale) / scale else 1.0f - (scale - curScale) / scale
transMatrix.postScale(scaleFactor, scaleFactor, detector?.focusX
?: 0f, detector?.focusY ?: 0f)
postInvalidate()
scale = curScale
return true
}
override fun onScaleEnd(detector: ScaleGestureDetector?)
{
super.onScaleEnd(detector)
}
}
在SimpleOnScaleGestureListener的onScale方法處理縮放,將縮放因子應(yīng)用到transMatrix,并調(diào)用postInvalidate()重新繪制。
接下重點(diǎn)就是onDraw方法:
override fun onDraw(canvas: Canvas?)
{
canvas?.let {
val saveState = it.saveCount
it.save()
it.concat(transMatrix)
super.onDraw(canvas)
it.restoreToCount(saveState)
drawMask(it)
drawRect(it)
}
}
先調(diào)用save,保存當(dāng)前畫布狀態(tài),之后應(yīng)用transMatrix,縮放和移動(dòng)畫布,然后調(diào)用ImageView的onDraw()方法,也就是父類的方法,用來繪制圖片,因?yàn)槔L制遮罩層和裁剪框不移動(dòng),所以恢復(fù)畫布狀態(tài)后進(jìn)行繪制。
最后就是裁剪圖片了
inner class SaveTask(private val filePath: String) : AsyncTask<Unit, Unit, Unit>()
{
override fun doInBackground(vararg params: Unit?): Unit
{
saveClipImage(filePath)
}
override fun onPostExecute(result: Unit?)
{
super.onPostExecute(result)
onsaveClipImageListener?.onImageFinishedSav()
}
}
fun clipAndSaveImage(filePath: String)
{
SaveTask(filePath).execute()
}
private fun saveClipImage(filePath: String)
{
val clipBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val clipCanvas = Canvas(clipBitmap)
draw(clipCanvas)
try
{
val outputStream = FileOutputStream(filePath)
val bitmap = Bitmap.createBitmap(clipBitmap, width / 2 - clipWidth / 2, height / 2 - clipHeight / 2, clipWidth, clipHeight, transMatrix, true)
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
outputStream.close()
}
catch (e: IOException)
{
e.printStackTrace()
}
}
可以看到啟動(dòng)了一個(gè)AsyncTask用來裁剪和保存Bitmap,其中saveClipImage就是重新構(gòu)建了一個(gè)畫布,并傳入bitmap,重新調(diào)用draw方法,將數(shù)據(jù)信息保存到bitmap,然后裁剪bitmap并存入文件。
3.源碼地址 GitHub
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Android自定義processor實(shí)現(xiàn)bindView功能的實(shí)例
下面小編就為大家分享一篇Android自定義processor實(shí)現(xiàn)bindView功能的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2017-12-12
Flutter實(shí)現(xiàn)切換應(yīng)用時(shí)隱藏應(yīng)用預(yù)覽
如果您要顯示敏感數(shù)據(jù),例如錢包金額,或者只是當(dāng)?shù)卿洷韱物@示插入的密碼清晰時(shí),當(dāng)您不在應(yīng)用程序中時(shí),您必須隱藏敏感數(shù)據(jù)。本文將利用Flutter實(shí)現(xiàn)切換應(yīng)用時(shí)隱藏應(yīng)用預(yù)覽,需要的可以參考一下2022-06-06
Android視頻/音頻緩存框架AndroidVideoCache(Okhttp)詳解
這篇文章主要為大家詳細(xì)介紹了Android視頻、音頻緩存框架AndroidVideoCache,實(shí)現(xiàn)邊下邊播功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-07-07
Android布局之LinearLayout自定義高亮背景的方法
這篇文章主要介紹了Android布局之LinearLayout自定義高亮背景的方法,實(shí)例分析了Android中LinearLayout布局參數(shù)設(shè)置技巧,需要的朋友可以參考下2016-01-01
ListView實(shí)現(xiàn)聊天列表之處理不同數(shù)據(jù)項(xiàng)
這篇文章主要為大家詳細(xì)介紹了ListView實(shí)現(xiàn)聊天列表之處理不同數(shù)據(jù)項(xiàng),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-11-11
Android的APK應(yīng)用簽名機(jī)制以及讀取簽名的方法
這篇文章主要介紹了Android的APK應(yīng)用簽名機(jī)制以及讀取簽名的方法,這里作者推薦使用Java自帶的API進(jìn)行APK簽名的讀取,需要的朋友可以參考下2016-02-02

