안드로이드 자체에서 제공하는 아주 간단한 사진 찍기 및 사진 고르기와 사진 업로드 기능을 합쳐봤습니다.
위 설명은 안드로이드 ui에 대한 기본적인 지식이 있다는 가정 하에 작성합니다.
먼저 카메라와 갤러리, 파일 쓰기, 읽기를 이용하기 위해서 권한을 얻어야 합니다.
?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.myapplication">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-feature
android:name="android.hardware.camera"
android:required="true" />
...
</manifest>
레이아웃 코드입니다.
button_round.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<stroke android:color="@color/gray" android:width="2dp"/>
<solid android:color="@color/white"/>
<corners android:radius="40dp"/>
</shape>
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:id="@+id/activity_main_appbar"
android:layout_width="match_parent"
android:layout_height="50dp"
app:layout_constraintTop_toTopOf="parent"
android:background="@color/lightgreen"
android:elevation="10dp" />
<TextView
android:id="@+id/activity_main_text_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="safe-driving"
android:textColor="@color/black"
android:elevation="11dp"
android:layout_marginStart="20dp"
android:textStyle="bold"
android:textSize="20sp"
app:layout_constraintTop_toTopOf="@id/activity_main_appbar"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="@id/activity_main_appbar" />
<ImageView
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_width="match_parent"
android:layout_height="800dp"
android:background="@drawable/button_round"
android:backgroundTint="@color/orientalBlue"
android:layout_marginTop="300dp"
/>
<ImageView
android:id="@+id/activity_main_image"
android:layout_width="150dp"
android:layout_height="200dp"
android:layout_marginTop="100dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toTopOf="@id/activity_main_button_picture" />
<Button
android:id="@+id/activity_main_button_picture"
app:layout_constraintVertical_chainStyle="packed"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginHorizontal="100dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginTop="300dp"
app:layout_constraintBottom_toTopOf="@id/activity_main_button_upload"
android:text="사진 찍기"
android:background="@drawable/button_round" />
<Button
android:id="@+id/activity_main_button_upload"
android:layout_width="match_parent"
android:layout_height="30dp"
android:layout_marginHorizontal="100dp"
android:elevation="5dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/activity_main_button_picture"
android:text="사진 업로드"
android:gravity="center"
android:background="@drawable/button_round"
android:textColor="@color/black"
android:backgroundTint="@color/white"
android:layout_marginTop="40dp"
app:layout_constraintBottom_toBottomOf="parent"
/>
<Button
android:id="@+id/activity_main_button_gallery"
android:layout_width="match_parent"
android:layout_height="30dp"
android:layout_marginHorizontal="100dp"
android:layout_marginTop="40dp"
android:background="@drawable/button_round"
android:backgroundTint="@color/white"
android:elevation="5dp"
android:gravity="center"
android:text="사진 선택"
android:textColor="@color/black"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/activity_main_button_picture"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
로직 코드입니다.
MainActivity.kt
class MainActivity : AppCompatActivity() {
var bitmap: Bitmap? = null
lateinit var file: File
lateinit var imageView: ImageView
lateinit var img: Bitmap
companion object {
const val TAKE_PICTURE = 1
const val CHOOSE_PICTURE = 2
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
imageView = binding.activityMainImage
// 권한 요청
requestPermission()
binding.activityMainButtonPicture.setOnClickListener {
val intent = Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE)
startActivityForResult(intent, TAKE_PICTURE)
}
binding.activityMainButtonUpload.setOnClickListener {
if (bitmap != null) {
println("token = ${Api.authToken}")
file = convertBitmapToFile(bitmap!!)
val survey = RequestBody.create(MediaType.parse("image/*"), file)
val regex = "[a-zA-Z0-9]".toRegex()
val imageId = regex.find(Env.email)
val multipart = MultipartBody.Part.createFormData("picture", "${imageId?.value}.jpg", survey)
Api.auth.faceCheck(multipart).enqueue(object : retrofit2.Callback<Boolean> {
override fun onResponse(call: Call<Boolean>, response: Response<Boolean>) {
println("response.code: ${response.code()}")
println("response.body: ${response.body()}")
if (response.body() == true) println("인식 성공")
else println("인식 실패")
}
override fun onFailure(call: Call<Boolean>, t: Throwable) {
println("네트워크 에러")
MyApp.shortToast(applicationContext, "네트워크를 확인해주세요.")
}
})
} else {
MyApp.shortToast(this, "사진을 업로드 해주세요.")
}
}
binding.activityMainButtonGallery.setOnClickListener {
val intent = Intent()
intent.type = "image/*"
intent.action = Intent.ACTION_GET_CONTENT
startActivityForResult(intent, CHOOSE_PICTURE)
}
}
fun convertBitmapToFile(bitmap: Bitmap): File {
val newFile = File(applicationContext.filesDir, "picture")
val out = FileOutputStream(newFile)
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out)
return newFile
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
TAKE_PICTURE -> {
if (resultCode == RESULT_OK && data?.hasExtra("data")!!) {
bitmap = data.extras?.get("data") as Bitmap
imageView.setImageBitmap(bitmap)
}
}
CHOOSE_PICTURE -> {
try {
val input = data?.data?.let { contentResolver.openInputStream(it) }
val img: Bitmap = BitmapFactory.decodeStream(input)
input?.close()
bitmap = img
imageView.setImageBitmap(img)
} catch (e: Exception) {
println("이미지 가져오기 실패")
}
}
}
}
private fun requestPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED &&
checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
) {
println("권한 설정 되있음")
} else {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE),
1
)
}
}
}
}
로직 자체는 간단합니다.
startActivityForResult에 상황에 맞는 Intent를 넣어준 후 선택하고 onActivityForResult에서 각 const val 의 값에 의해서 액션을 분기하고 사진을 선택한 것이면 선택한 사진을 data에서 가져와서 bitmap에 넣어주고 사진을 촬영한 것이면 촬영한 사진을 data에서 가져와서 bitmap에 넣어줍니다.
retrofit 코드입니다.
Api.kt
package com.example.myapplication.api
import com.example.myapplication.api.service.AuthService
import com.google.gson.GsonBuilder
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
object Api {
var baseUrl: String = "http://localhost"
var builder = Retrofit.Builder()
var authToken = ""
lateinit var auth: AuthService
fun update(
baseUrl: String = "http://localhost",
authToken: String = ""
) {
val gson = GsonBuilder().setLenient().create()
Api.baseUrl = baseUrl
Api.authToken = authToken
val okHttpClient: OkHttpClient =
OkHttpClient.Builder().addInterceptor(addOkHttpInterceptor(authToken)).build()
val retrofit = builder.baseUrl(baseUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
auth = retrofit.create(AuthService::class.java)
}
private fun addOkHttpInterceptor(authToken: String) = Interceptor { chain ->
val request = chain.request().newBuilder().removeHeader("X-Auth-token")
.addHeader("X-Auth-token", authToken)
chain.proceed(request.build())
}
}
레트로핏 객체를 싱글톤으로 만든 후 레트로핏 객체에 서비스를 등록해줍니다.
AuthService.kt
package com.example.myapplication.api.service
import com.example.myapplication.model.Login
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.Call
import retrofit2.http.*
interface AuthService {
@POST("signup")
@Multipart
fun signup(
@Part("email") email: RequestBody,
@Part("password") password: RequestBody,
@Part profilePic: MultipartBody.Part
): Call<Int>
@POST("login")
fun login(
@Body emailAndPassword: Login
): Call<String>
@GET("user/check")
fun userCheck(): Call<String>
@POST("user/carFaceVerified")
@Multipart
fun faceCheck(@Part picture: MultipartBody.Part): Call<Boolean>
}
레트로핏의 서비스는 인터페이스의 함수로 구현되어 있습니다.
여기서 업로드 함수는 faceCheck 함수를 보시면 됩니다. 이름은 얼굴 인식 어플을 만들었기 때문에 faceCheck라고 되어 있습니다. ㅋㅋ...
- 로직의 순서는 사진 선택 or 사진 촬영 비트맵에 저장
- imageView에 set하기
- bitmap을 outputStream으로 파일로 변환
- multipart의 폼데이터로 만들고 retrofit의 서비스로 전송요청을 보내기
중요) Api의 베이스url 을 Api.update 함수로 원하는 url로 업데이트 해야 원하는 url로 전송 요청을 보낼 수 있습니다.
네트워크 정책 관련 오류가 뜬다면 매니패스트에 인터넷 권한이 활성화 되었는지 확인해주시고 권한이 들어있다면
매니패스트에
android:usesCleartextTraffic="true"
코드를 추가해주세요.
공감과 댓글은 블로거에게 큰 힘이 됩니다.