일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- NavHost
- Jetpack
- 안드로이드
- XML
- DiffUtil
- 리사이클러뷰
- ListAdapter
- 플레이스토어
- 로그인
- 뷰
- 파이어베이스
- Authentication
- Android
- Kotlin
- cleanarchitecture
- UiState
- Flow
- 알고리즘
- 클린아키텍처
- 코딩테스트
- Build variants
- coroutine
- 컴포즈
- 회원가입
- 코틀린
- sharedFlow
- MVVM
- NavController
- Compose
- 커스텀뷰
- Today
- Total
Grusie 안드로이드 개발 기술 블로그
[Android] 커스텀 이미지 크롭 기능 만들기 본문
이미지 크롭 기능을 구현해야 할 일이 있었다.
라이브러리를 사용하려고 이곳 저곳 둘러봤으나, 원하는 디자인을 전부 만족시킬 만한 라이브러리가 보이지 않았다.
만약 있다고 하더라도, 언젠가 고치기 위해선 뷰는 가능하면 라이브러리를 사용하지 않는 것이 좋겠다는 생각이 들어 직접 만들게 되었다.
커스텀 뷰
class ImageCropView(context: Context, attrs: AttributeSet) : ConstraintLayout(context, attrs) {
private var bitmap: Bitmap? = null
private var cropRect: RectF = RectF(100f, 100f, 400f, 400f)
private var lastTouchX: Float = 0f
private var lastTouchY: Float = 0f
private val borderSize = 50f
private val paint = Paint().apply {
color = context.getColor(R.color.white_FFFFFF)
style = Paint.Style.STROKE
strokeWidth = 1.dpToPx(context).toFloat()
}
private val imageView: ImageView
private val minHeight = 100.dpToPx(context).toFloat()
private val minWidth = 100.dpToPx(context).toFloat()
이미지 크롭을 위한 커스텀 뷰이다. 하나씩 설명하며 내려갈 예정이다.
우선 이미지뷰와, 크롭을 위한 사각형이 들어가야 하기에, ConstraintLayout을 확장하여 만들었고,
넘겨받을/넘겨줄 bitmap, 크롭을 하기 위한 사각형 cropRect, 마지막 터치 좌표, borderSize<-말이 borderSize이지, 그냥 확대 할 때 사용하는 터치 영역이다. 나머지는 보면 알 것 같다.
이미지뷰 세팅
init {
imageView = ImageView(context).apply {
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
scaleType = ImageView.ScaleType.CENTER_CROP
}
addView(imageView)
}
fun loadImageFromUrl(url: String) {
Glide.with(context)
.asBitmap()
.load(url)
.into(object : CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
bitmap = resource
imageView.setImageBitmap(bitmap)
invalidate()
}
override fun onLoadCleared(placeholder: android.graphics.drawable.Drawable?) {
}
})
}
기본 이미지 뷰를, 삽입해주며, 속성으로 MATCH_PARENT를 주고, scale_type을 지정해줬다.
loadImageFromUrl 함수에서 글라이드를 사용해 갤러리에서 받아온 url을, BitMap으로 전환해서 저장해준다.
OnDraw(), dispatchDraw()
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
}
override fun dispatchDraw(canvas: Canvas) {
super.dispatchDraw(canvas)
canvas.drawRect(cropRect, paint)
}
뷰 그룹 내에서 사각형을 그리는 것이기에, dispatchDraw에서 호출해준다.
OnTouchEvent
override fun onTouchEvent(event: MotionEvent?): Boolean {
event?.let {
val touchX = event.x
val touchY = event.y
when (it.action) {
MotionEvent.ACTION_DOWN -> {
lastTouchX = touchX
lastTouchY = touchY
if (cropRect.contains(touchX, touchY)) {
return true
}
}
MotionEvent.ACTION_MOVE -> {
val offsetX = touchX - lastTouchX
val offsetY = touchY - lastTouchY
if (lastTouchX in cropRect.left..cropRect.left + borderSize) {
resizeHeight(offsetY, touchX, touchY)
resizeLeft(offsetX, touchX, touchY)
return true
} else if (lastTouchX in cropRect.right - borderSize..cropRect.right) {
resizeHeight(offsetY, touchX, touchY)
resizeRight(offsetX, touchX, touchY)
return true
} else if (lastTouchY in cropRect.top..cropRect.top + borderSize) {
resizeTop(offsetY, touchX, touchY)
return true
} else if (lastTouchY in cropRect.bottom - borderSize..cropRect.bottom) {
resizeBottom(offsetY, touchX, touchY)
return true
}
if (cropRect.contains(lastTouchX, lastTouchY)) {
val newLeft = cropRect.left + offsetX
val newTop = cropRect.top + offsetY
val newRight = cropRect.right + offsetX
val newBottom = cropRect.bottom + offsetY
val adjustedLeft = newLeft.coerceAtLeast(0f)
val adjustedTop = newTop.coerceAtLeast(0f)
if (newRight <= imageView.width && newBottom <= imageView.height) {
cropRect.offsetTo(adjustedLeft, adjustedTop)
invalidate()
lastTouchX = touchX
lastTouchY = touchY
}
return true
}
}
MotionEvent.ACTION_UP -> {
return true
}
}
}
return super.onTouchEvent(event)
}
터치를 통해, cropRect의 크기 및 위치를 조정하기 위한 터치 이벤트 리스너이다.
이전 터치값과 현재 터치값을 비교하여 offset을 만들고, border사이즈 내에 존재한다면, 상하좌우에 맞게 크기를 조정하도록 구현하였다.
만약 그렇지 않지만, 사각형 내에는 존재한다면, 사각형의 offset을 변경해주는 형태로 진행하였다.
사각형이 이미지뷰 밖으로 넘어갈 경우, Bitmap생성 시 에러가 발생하기에, 경계선을 잘 지정해주어야 한다.
사이즈 조절 함수
private fun resizeRight(changeRight: Float, touchX: Float? = null, touchY: Float? = null) {
val newRight = cropRect.right + changeRight
val newWidth = cropRect.width() + changeRight
if (newWidth in minWidth..imageView.width.toFloat()) {
cropRect.right = newRight
}
invalidate()
if (touchX != null && touchY != null) {
lastTouchX = touchX
lastTouchY = touchY
}
}
사이즈 조절함수는 이렇게 구현되어 있다.
경계를 잘 정하고, 변경 값을 가지고 left, right, bottom, top을 각각 조절한다.
이미지 크롭
fun getCroppedBitmap(): Bitmap? {
bitmap?.let {
val imageViewWidth = imageView.width
val imageViewHeight = imageView.height
try {
val left = (cropRect.left * it.width / imageViewWidth).toInt()
val top = (cropRect.top * it.height / imageViewHeight).toInt()
val right = (cropRect.right * it.width / imageViewWidth).toInt()
val bottom = (cropRect.bottom * it.height / imageViewHeight).toInt()
return Bitmap.createBitmap(it, left, top, right - left, bottom - top)
} catch (e: Exception) {
Log.e("confirm getCroppedBitmap Error", "${e.message}")
return null
}
}
return null
}
이미지를 크롭하는 함수이다. imageView의 크기에 따라 비트맵 비율이 변경 될 것이기에, 비율 처리를 해두었다.
또한, createBitmap시 너비 값이 0이하가 나올 경우 에러를 발생하기에, try-catch문을 사용하였다.
옵션
fun resizeWithRatio(ratio: String) {
var left = 0f
var right = 0f
var top = 0f
var bottom = 0f
when (ratio) {
RATIO_1_1 -> {
left = 50f
right = 650f
top = 50f
bottom = 650f
}
RATIO_3_4 -> {
left = 50f
right = 650f
top = 50f
bottom = 850f
}
RATIO_4_3 -> {
left = 50f
right = 850f
top = 50f
bottom = 650f
}
RATIO_FILL -> {
left = 0f
right = imageView.width.toFloat()
top = 0f
bottom = imageView.height.toFloat()
}
else -> {
}
}
cropRect.left = left.coerceAtLeast(0f)
cropRect.right = right.coerceAtMost(imageView.width.toFloat())
cropRect.bottom = bottom.coerceAtMost(imageView.height.toFloat())
cropRect.top = top.coerceAtLeast(0f)
invalidate()
}
crop 사각형을 비율별로 크기를 지정하는 함수이다.
순서대로 1:1, 3:4, 4:3, 화면 맞춤으로 되어있다. 그냥 비율에 맞춰서 스태틱하게 만들어져 있다.
혹시나 기기사이즈로 인해, 화면을 벗어날 수도 있기에, 마찬가지로 경계를 잘 지정해서 화면을 그려주면 된다.
XML
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="ratio"
type="String" />
</data>
...
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/cl_title">
<com.example.memories.ui.common.customview.ImageCropView
android:id="@+id/imageCropView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginBottom="19dp"
app:layout_constraintBottom_toTopOf="@id/cl_resize_crop_view"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0"
tools:layout_editor_absoluteX="0dp" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_resize_crop_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="48dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_rect_1_1"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginEnd="55dp"
android:foreground="?android:attr/selectableItemBackground"
app:layout_constraintBottom_toBottomOf="@id/cl_rect_3_4"
app:layout_constraintEnd_toStartOf="@id/cl_rect_3_4"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/iv_rect_1_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="6dp"
android:scaleType="centerCrop"
android:src="@drawable/ic_rect_1_1"
app:layout_constraintBottom_toTopOf="@id/tv_rect_1_1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="@{ratio == `ratio_1_1` ? @color/green_05FFD2 : @color/black_000000}" />
<TextView
android:id="@+id/tv_rect_1_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/image_crop_ratio_1_1"
android:textColor="@color/black_0E0E12"
android:textSize="13sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
...
커스텀 크롭 뷰를 넣어주고, 하단에 옵션인 크롭 사각형을 비율 별로 나타낼 수 있는 기능을 추가하였다.
커스텀 크롭 뷰에 전부 넣을수도 있었겠지만, 범용성을 위해 따로 분리하였다.
Activity
val uri = intent.getStringExtra(EXTRA_CROP_IMAGES) ?: ""
binding.run {
imageCropView.loadImageFromUrl(uri)
clRect11.setOnSingleClickListener {
imageCropView.resizeWithRatio(ImageCropView.RATIO_1_1)
ratio = ImageCropView.RATIO_1_1
}
clRect34.setOnSingleClickListener {
imageCropView.resizeWithRatio(ImageCropView.RATIO_3_4)
ratio = ImageCropView.RATIO_3_4
}
clRect43.setOnSingleClickListener {
imageCropView.resizeWithRatio(ImageCropView.RATIO_4_3)
ratio = ImageCropView.RATIO_4_3
}
clRectFill.setOnSingleClickListener {
imageCropView.resizeWithRatio(ImageCropView.RATIO_FILL)
ratio = ImageCropView.RATIO_FILL
}
tvCancel.setOnSingleClickListener {
finish()
}
tvComplete.setOnSingleClickListener {
val croppedBitmap = imageCropView.getCroppedBitmap()
croppedBitmap?.let { imageCropView.loadImageFromUrl(it) }
}
}
처음 들어올 때, intent에서 부터 받아온, 이미지 uri를 통해, 크롭뷰에 이미지를 띄우고, 각 크롭뷰 사이즈 조절 버튼에 함수를 지정해준다.
완료버튼을 누를 시, 현재는 임시로, 크롭뷰 기존 이미지 화면에 자른 화면을 띄우도록 구현해 두었다.
결과물
참고
https://blog.naver.com/sangrime/220693902411
[안드로이드] 이미지 자르기, 선택하기 (Android Image Cropper)
Github에서 개발중인 좋은 라이브러리를 소개합니다. 오픈소스를 잘 활...
blog.naver.com
https://velog.io/@jinny_0422/Android%EB%B6%88%EB%9F%AC%EC%98%A8ImageCrop%ED%95%98%EA%B8%B0
[Android] 불러온 Image Crop하기
불러온 Image를 Crop 하는 기능을 정리해 놓는다. Crop기능을 직접 구현하시는 분들도 있지만 나는 라이브러리를 사용할거지요옹
velog.io
https://github.com/ArthurHub/Android-Image-Cropper
GitHub - ArthurHub/Android-Image-Cropper: Image Cropping Library for Android, optimized for Camera / Gallery.
Image Cropping Library for Android, optimized for Camera / Gallery. - ArthurHub/Android-Image-Cropper
github.com
후기
커스텀뷰를 많이 사용해보지 않아서 역시 어려웠지만, 그래도 하루만에 끝내서 다행이다.
디자인과 기능에 대한 정의가 아직 확실하지 않아서 만들면서 힘들었다. 다중선택 시 어떻게 화면을 띄울것인지 라던지, 이미지 크롭 뷰의 모서리 모양은 어떻게 할 것인지 등 제대로 정해지지 않았으며, 그걸 구현하는 것도 막막하긴 하다.
그래도 일단 크롭 자체를 성공했다는 점에서 아주 만족스럽다.
라이브러리들이 있는만큼 너무 어려울것이라고 생각했는데, 나쁘지 않았다.
뷰에 관련된 라이브러리들은 가능한 사용하지 말고 직접 만드는 게 커스텀에 용이 한 것 같다.
'안드로이드 개발 > 뷰' 카테고리의 다른 글
[Android] 크롭 이미지 DiffUtil 연동 (선택 / 수정 / 삭제) (0) | 2024.06.12 |
---|---|
[Android] 커스텀 데이트 피커 만들기(Number Picker) (0) | 2024.05.17 |
[Android] 이미지 축소 확대, 회전 커스텀 뷰 만들기 (feat. 터치 이벤트 종류, 각도 함수) (0) | 2024.04.24 |
[Android] BottomSheetDialogFragment 사용하기 (+ 둥근 모서리) (0) | 2024.04.18 |
[Android] RecyclerView, ItemDecoration으로 마진 조절하기 (0) | 2024.04.11 |