일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- Gradle
- Github
- coroutine
- 깃헙
- 유튜브
- 의존성주입
- WebView
- viewpager
- ADB
- 스튜디오
- 웹뷰
- 코루틴
- MVVM
- Retrofit
- error
- RecyclerView
- Android
- 안스
- build
- studio
- 안드로이드
- 레트로핏
- 코틀린
- 에러
- image
- 안드로이드 스튜디오
- Kotlin
- dart
- GIT
- 안드로이드스튜디오
- Today
- Total
코딩하는 일용직 노동자
안드로이드 WebView에서 카메라/사진 갤러리 이미지 업로드 하기 본문
# 들어가며
하이브리드 앱에서 웹뷰로 열린 웹문서에 <input type="file">태그가 있습니다.
사진을 첨부하기 위한 기능입니다. iOS는 앱에서 별도의 처리가 없어도 사진을 선택하면 웹으로 사진이 잘 등록됩니다만, 안드로이드에서는 사진을 선택해도 웹으로 등록이 안됩니다.
이번 포스팅에서는 <input type="file"> 태그에 카메라 or 사진 갤러리를 표시하고 사진촬영이나 이미지 선택후 웹에 이미지를 넘겨주는 처리를 알려드리겠습니다.
우선 WebChromeClient() 를 상속받은 커스텀 클래스를 만들고 아래의 함수를 오버라이드 해줍니다.
class CustomWebChromeClient(val activity: AppCompatActivity) : WebChromeClient() {
var filePathCallbackLollipop: ValueCallback<Array<Uri>>? = null
...
// For Android 5.0+
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
override fun onShowFileChooser(
webView: WebView?,
filePathCallback: ValueCallback<Array<Uri>>?,
fileChooserParams: FileChooserParams
): Boolean {
// Callback 초기화 (중요!)
if (filePathCallbackLollipop != null) {
filePathCallbackLollipop?.onReceiveValue(null)
filePathCallbackLollipop = null
}
filePathCallbackLollipop = filePathCallback
val isCapture = fileChooserParams.isCaptureEnabled
if (activity is IImageHandler) {
activity.takePicture(filePathCallbackLollipop)
}
filePathCallbackLollipop = null
return true
}
}
자! 여기서 중요한 것이 바로 filePathCallback 입니다.
input type 태그가 작동되면 카메라로 사진을 찍든, 이미지를 선택을 하든..
결과 이미지를 filePathCallback 을 통해서 웹으로 돌려주는 것입니다.
input type 태그에서 capture="camera" 가 있는 경우와 없는 경우가 있습니다.
둘의 차이는 카메라 촬영을 할지 안할지를 정하는 옵션입니다.
<input type="file" capture="camera"> // isCaptureEnabled 이 true로 리턴됩니다.
<input type="file"> // isCaptureEnabled 이 false로 리턴됩니다.
이번 포스팅에서는 카메라 촬영도 함께 포함한 예제를 중심으로 진행하겠습니다.
카메라로 사진을 찍는 처리나 이미지를 선택하는 처리는 이미 다양한 자료나 라이브러리들이 있습니다.
여기서는 카메라는 Intent로 호출해서 촬영을 한 후 리턴하는 처리를 보여드리겠습니다.
그리고 이미지를 선택하는 처리는 ImagePicker라는 별도의 라이브러리를 이용하겠습니다.
dependencies {
...
// ImagePicker
implementation 'com.github.nguyenhoanglam:ImagePicker:1.3.3'
}
CustomWebChromeClient의 onShowFileChooser 함수가 호출되었으니 이제 takePicture()를 호출해서 웹뷰가 있는 액티비티로 이벤트를 넘기겠습니다.
여기서는 IImageHandler 를 만들어 이용했습니다.
WebViewActivity 는 IImageHandler를 implements 한 상태이고 커스텀 웹뷰와 CustomWebChromeClient에 activity를 넘겨줬으니 IImageHandler를 이용할 수 있습니다.
interface IImageHandler {
fun takePicture(callBack: ValueCallback<Array<Uri>>?)
fun uploadImageOnPage(resultCode: Int, intent: Intent?)
}
class WebViewActivity : AppCompatActivity(), IImageHandler {
private val CAPTURE_CAMERA_RESULT = 3089
private var filePathCallbackLollipop: ValueCallback<Array<Uri>>? = null
...
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
super.onActivityResult(requestCode, resultCode, intent)
when(requestCode){
CAPTURE_CAMERA_RESULT -> {
onCaptureImageResult(intent)
}
Config.RC_PICK_IMAGES -> {
if (intent != null) {
val images: ArrayList<Image> = intent.getParcelableArrayListExtra(Config.EXTRA_IMAGES)
var data = Intent().apply {
data = Utils.getImageContentUri(this@WebViewActivity, images[0].path)
}
uploadImageOnPage(resultCode, data)
} else {
/**
* 만약 사진촬영이나 선택을 하던중 취소할경우 filePathCallbackLollipop 을 null 로 해줘야
* 웹에서 사진첨부를 눌렀을때 이벤트를 다시 받을 수 있다.
*/
filePathCallbackLollipop?.onReceiveValue(null)
filePathCallbackLollipop = null
}
}
}
}
/**
* <input type="file"> 태그가 호출되면 IImageHandler를 통해 이 함수가 호출된다.
*/
override fun takePicture(callBack: ValueCallback<Array<Uri>>?) {
filePathCallbackLollipop = callBack
showSelectCameraOrImage()
}
/**
* 카메라 / 갤러리 선택 팝업을 표시한다.
*/
private fun showSelectCameraOrImage() {
CameraOrImageSelectDialog(object: CameraOrImageSelectDialog.OnClickSelectListener {
override fun onClickCamera() {
cameraIntent()
}
override fun onClickImage() {
galleryIntent()
}
}).show(supportFragmentManager, "CameraOrImageSelectDialog")
}
/**
* 카메라 작동
*/
private fun cameraIntent() {
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
startActivityForResult(intent, CAPTURE_CAMERA_RESULT)
}
/**
* 이미지 선택
*/
private fun galleryIntent() {
ImagePicker.with(this).run {
setToolbarColor("#FFFFFF")
setStatusBarColor("#FFFFFF")
setToolbarTextColor("#000000")
setToolbarIconColor("#000000")
setProgressBarColor("#FFC300")
setBackgroundColor("#FFFFFF")
setCameraOnly(false)
setMultipleMode(false)
setFolderMode(true)
setShowCamera(false)
setFolderTitle(getString(R.string.select_image))
setDoneTitle(getString(R.string.select))
setKeepScreenOn(true)
start()
}
}
/**
* 카메라 작동후 전달된 인텐트를 받는다.
*/
private fun onCaptureImageResult(data: Intent?) {
if (data == null) {
/**
* 만약 사진촬영이나 선택을 하던중 취소할경우 filePathCallbackLollipop 을 null 로 해줘야
* 웹에서 사진첨부를 눌렀을때 이벤트를 다시 받을 수 있다.
*/
filePathCallbackLollipop?.onReceiveValue(null)
filePathCallbackLollipop = null
return
}
val thumbnail = data.extras!!.get("data") as Bitmap
saveImage(thumbnail)
}
/**
* 비트맵을 로컬에 물리적 이미지 파일로 저장시킨다.
*/
private fun saveImage(bitmap: Bitmap) {
val bytes = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bytes)
// create a directory if it doesn't already exist
val photoDirectory = File(getExternalStorageDirectory().absolutePath + "/cameraphoto/")
if (!photoDirectory.exists()) {
photoDirectory.mkdirs()
}
val imgFile = File(photoDirectory, "${System.currentTimeMillis()}.jpg")
val fo: FileOutputStream
try {
imgFile.createNewFile()
fo = FileOutputStream(imgFile)
fo.write(bytes.toByteArray())
fo.close()
} catch (e: FileNotFoundException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
}
uploadImageOnPage(Activity.RESULT_OK, Intent().apply {
data = imgFile.toUri()
})
}
/**
* 이미지를 웹뷰로 리턴시켜준다.
*/
override fun uploadImageOnPage(resultCode: Int, intent: Intent?) {
if (resultCode == Activity.RESULT_OK) {
if (intent != null) {
filePathCallbackLollipop?.onReceiveValue(
WebChromeClient.FileChooserParams.parseResult(Activity.RESULT_OK, intent)
)
filePathCallbackLollipop = null
}
} else {
/**
* 만약 사진촬영이나 선택을 하던중 취소할경우 filePathCallbackLollipop 을 null 로 해줘야
* 웹에서 사진첨부를 눌렀을때 이벤트를 다시 받을 수 있다.
*/
filePathCallbackLollipop?.onReceiveValue(null)
filePathCallbackLollipop = null
}
}
}
웹뷰액티비티에서 takePicture()가 호출되면 카메라/갤러리 선택 팝업을 보여주겠습니다.
커스텀 팝업을 만들겠습니다.
class CameraOrImageSelectDialog(private val listener: OnClickSelectListener) : DialogFragment() {
interface OnClickSelectListener {
fun onClickCamera()
fun onClickImage()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.dialog_camera_image_select, container, false)
return view
}
override fun onStart() {
super.onStart()
if (dialog != null) {
val width = ViewGroup.LayoutParams.MATCH_PARENT
val height = ViewGroup.LayoutParams.WRAP_CONTENT
dialog!!.window!!.setLayout(width, height)
dialog!!.window!!.setGravity(Gravity.BOTTOM)
dialog!!.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
dialog!!.setCancelable(false)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
llSelectCamera.setOnClickListener {
listener.onClickCamera()
dismiss()
}
llSelectImage.setOnClickListener {
listener.onClickImage()
dismiss()
}
}
}
팝업에서 카메라를 선택하면 cameraIntent() 함수를 호출합니다.
카메라로 사진을 찍으면 onActivityResult() 로 받은후
onCaptureImageResult() 와 saveImage() 함수를 거쳐 이미지가 저장됩니다.
그리고 uploadImageOnPage() 함수에서 웹으로 사진을 리턴해줍니다.
팝업에서 갤러리를 선택하면 galleryIntent() 함수를 호출합니다.
ImagePicker 를 통해 이미지를 선택하면 onActivityResult() 로 받은후
uploadImageOnPage() 함수에서 웹으로 사진을 리턴해줍니다.
# 마치며
촬영된 사진을 파일로 만들거나 이미지를 선택하는 부분은 앞으로 수정될 가능성이 높습니다.
Android 10(Q) 부터는 기존의 저장공간과는 다른 개념으로 바뀌기 때문입니다.
자세한 내용은 여기에서 확인하시기 바랍니다.
'안드로이드' 카테고리의 다른 글
안드로이드 이메일 보내기 소스 (0) | 2020.06.30 |
---|---|
안드로이드 상단 Status Bar 컬러값 변경하기 (0) | 2020.06.25 |
Parcelable을 사용해 데이타 객체 보내기 (2) | 2020.06.09 |
안드로이드 스튜디오 오프라인 모드로 사용하기 (0) | 2020.06.09 |
코틀린 Singleton Pattern (2) | 2020.06.08 |