본 포스팅은 모바일 웹으로 구현했을 경우 사용 가능한 소스입니다.
혹시 하이브리드앱이나 webview 로 호출한 페이지에서 카메라나 사진첩을 연동해야 하시는 분은 아래 링크를 참고하시면 되겠습니다.
android webview 에서 카메라 호출 및 사진첩(갤러리) 호출하여 이미지 파일 업로드 하기
html5 의 속성을 이용하여 스마트폰의 카메라와 연결하는 방법이다.
스마트폰(모바일 디바이스)의 카메라와 연결하여 사진이나 동영상을 찍고 찍은 데이터를 javascript 를 이용하여 접근 및 제어가 가능하다.
//mobilehtml5.org/
여기서 확인해보면 미디어 수준에서 접근하는 방법과 스트림 수준에서 접근하는 방법이 있다.
아래는 위 주소의 내용을 일부 발췌한 이미지이다.
미디어 수준에서 접근하는 방법을 많은
브라우저에서 지원하는 것을 볼 수 있다.
그래서 미디어 수준에서 접근하는 방법을 사용했다.
카메라 연결하는 소스
| <script> $(function(){ $('#camera').change(function(e){ $('#pic').attr('src', URL.createObjectURL(e.target.files[0])); }); });
</script> <input type="file" id="camera" name="camera" capture="camera" accept="image/*" />
<br/> <img id="pic" style="width:100%;" />
| cs
|
동영상 촬영 연결하는 소스
| <script> $(function(){ $('#camcorder').change(function(e){ $('#mov').attr('src', URL.createObjectURL(e.target.files[0])); }); }); </script> <input type="file" id="camcorder" name="camcorder" capture="camcorder" accept="video/*" />
<br/> <video id="mov" width="100%" autoplay="yes" controls="true" ></video>
| cs
|
두 소스의 설명
4# : e.target.files[0] 는 파일 객체이다. URL.createObjectURL 은 파일 객체 또는 Blob 객체의 URL 를 생성한다. 생성된 URL은 바로 img 태그나 video 태그에 넣어주면 확인이 가능하다.
| <input type="file" id="camera" name="camera">
| cs
|
이렇게만 작성한다면 갤러리(사진첩)도 볼 수 있다...........
일반적으로 사용 후에 URL.revokeObjectURL() 을 호출하지 않아도 브라우저가 unloaded 될 때 자동으로 해제된다. 필요시에 따라 설정하면 될 것 같다.
URL.createObjectURL()의 데스크탑과 모바일의 브라우저별 호환성은 다음과 같다.
ps. file 업로드는 기존 방식과 다른 점이 없었다. 필자는 spring CommonsMultipartResolver 를 이용하여 모바일에서도 파일을 업로드했다.
참고 :
//mobilehtml5.org/
//developer.mozilla.org/ko/docs/Web/API/URL/createObjectURL
코딩하는 일용직 노동자
안드로이드
안드로이드 WebView에서 카메라/사진 갤러리 이미지 업로드 하기
bacass 2020. 6. 22. 10:49
# 들어가며
하이브리드 앱에서 웹뷰로 열린 웹문서에 <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() 함수에서 웹으로 사진을 리턴해줍니다.
ImagePicker를 이용해 업로드한 사진을 선택한다.
선택한 이미지와 촬영한 사진이 정상적으로 업로드 되었다.
# 마치며
촬영된 사진을 파일로 만들거나 이미지를 선택하는 부분은 앞으로 수정될 가능성이 높습니다.
Android 10(Q) 부터는 기존의 저장공간과는 다른 개념으로 바뀌기 때문입니다.
자세한 내용은 여기에서 확인하시기 바랍니다.