하이브리드 앱 갤러리 호출 - haibeulideu aeb gaelleoli hochul

본 포스팅은 모바일 웹으로 구현했을 경우 사용 가능한 소스입니다.

혹시 하이브리드앱이나 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) 부터는 기존의 저장공간과는 다른 개념으로 바뀌기 때문입니다.
자세한 내용은 여기에서 확인하시기 바랍니다.

Toplist

최신 우편물

태그