ABOUT ME

-

  • Android) WebView 정리
    Android 2021. 7. 8. 12:19


    WebView

    • 웹 탐색과 웹 브라우저는 Android, iOS, PC 모두 가진 기능입니다.
    • WebView는 웹 브라우저를 구성하는 HTML과 같은 요소들을 받아들여 이를 브라우저와 동일한 형식으로 해석해서 표현해주는 뷰입니다.
    • 그래서 WebView는 PC의 서버에서 response 한 웹 파일을 받아서 Android에서도 똑같이 보여주고 다룰 수 있고, 디바이스 상관없이 정보 공유가 가능한 하이브리드 앱을 쉽게 구현할 수 있도록 도와줍니다.

     

    URL 웹 페이지 요청

    • 단순히 WebView를 참조하고 loadUrl로 웹 브라우저를 실행하여 해당 url을 로드하는 방법입니다.
    • loadUrl 메소드를 사용해서 해당 주소에 요청을 보내고, 응답받은 html 파일을 사용하여 웹 화면을 표시합니다.
    • WebView에 WebViewClient를 제공함으로써 웹 페이지를 WebView에 띄울 수 있게 됩니다.
     binding.webView.apply {
                webViewClient = WebViewClient()
                loadUrl("https://www.google.com/")
            }

     

     

    내부 html 요청

    • 개발자가 가진 html 파일을 표시하려면, 이 파일이 assets 폴더 안에 존재해야 합니다.
    • assets은 res폴더 처럼 리소스를 모아두는 곳인데, res에서 분류한 타입 외의 html, css, js 등을 사용할 때에 사용하는 폴더입니다.
    • 아래와 같이 assets 폴더 안에 html 파일을 넣고 loadUrl 메소드를 사용하여 불러옵니다.

     

    webView.loadUrl("file:///android_asset/www/index.html")

     

     

    html을 parsing하여 보여주기

    • 일반적으로 사용하는 loadUrl() 메소드로 html parsing 하여 필요한 부분만 보여줄 수 없습니다.
    • 이러한 경우 loadData 메소드나 loadDataWithBaseURL 메소드를 이용하면 됩니다.
    • loadData(String data, String mimeType, String encoding) -> data : encoding으로 작성된 data는 html 코드를 string 형태로 넣어서 사용할 수 있습니다. mimeType : 문서의 다양성을 알려주기 위함으로 "text/html" 등의 값을 가질 수 있습니다. encoding : data가 어떤 형태의 encoding으로 작성되었는지 알려줍니다.
    • 하지만, loadData 메소드는 network 상의 content를 WebView에 보여줄 수 없습니다. 즉 html 내용 중 상대 경로로 있는 스타일이나 이미지 등을 못 가져오는 경우가 있습니다.
    • 예를 들어 <link="stylesheet" href="/Content/Css/main.css?2016011909href="/Content/Css/main.css? 2016011909" /> 이런 css가 있다고 하면 /Content 폴더가 어디 경로를 기점인지 알 수가 없어 loadDataWithBaseUrl을 사용하여 해결합니다.
    • loadDataWithURL(String baseUrl, String data, String mimeType, String encoding, String histroyUrl) -> baseUrl : 상대 경로를 해결하기 위해 사용됨. historyUrl : histroy entry로 사용됨.
    loadData(url, "text/html; charset=utf-8", "UTF-8")
    loadDataWithBaseURL(null, url, "text/html; charset=utf-8", "UTF-8", null)

     

    java script 연동 (앱 -> 웹뷰 JavaScript 호출)

    • 웹 브라우저는 java script를 사용합니다.
    • 단순히 받아온 코드를 해석하는 것은 문제가 없을지 몰라도, 화면에 표시되는 웹 페이지와 안드로이드 코드가 유기적으로 동작하기 어려울 수 있습니다.
    • 하지만, 이를 지원해주는 api가 존재합니다.
    • loadUrl 메소드에 java script의 함수명을 적어주어, 정의된 함수를 실행시킬 수 있습니다.
    • 앱의 동작에 연동하여 java script의 어떠한 동작을 하도록 이벤트를 걸 때 사용하면 좋습니다.
    webView.loadUrl("javascript:alerthello()")

     

    WebView와 앱 통신  (웹뷰 -> 앱 코드 호출)

    • WebView는 보안상의 이유로 디바이스 자원을 100% 사용할 수 없습니다. 보통 안드로이드는 디바이스 유일 값으로 Android Id를 사용하는데 디바이스 영역으로 웹뷰에서 바로 접근하여 추출할 수 없습니다.
    • 하지만, 네이티브에서는 해당 값을 자유롭게 추출 가능합니다. 이와 같은 경우 웹뷰와 네이티브의 연동을 통해 값을 전달합니다.
    • 본 연동방식은 안드로이드 OS 4.1 JELLY_BEAN 이하 디바이스에서는 보안상 문제가 있어, 안드로이드 OS 4.2 이상에서 사용을 권장하고 있습니다. 안드로이드 OS 4.1 이하에서는 @JavaScriptinterface 어노테이션이 동작하지 않습니다.

     

    JavaScript에서 네이티브 메소드 호출

    • JavaScript에서 네이티브 코드 호출 -> 네이티브 코드가 JavaScriptInterface 어노테이션이 지정되었는지 확인하고 지정되어 있으면 네이티브 메소드를 실행 -> 네이티브 메소드의 실행결과를 반환.

     

    네이티브에서 JavaScript 호출

    • evaluteJavascript를 통해 JavaScript 호출 -> 실행결과를 반환.

     

    웹뷰에서 앱 코드를 호출하는 예제를 보겠습니다.

    • assets 폴더에 html 파일을 만들어줍니다. 안드로이드에서 선언한 함수를 호출할 수 있습니다.
    <input type="button" value="WebView Test" onClick="showAndroidToast('Connection Success!')"/>
    <script type="text/javascript">
        function showAndroidToast(toast) {
            Android.showToast(toast);
        }
    
    </script>
    • @JavascriptInterface 함수 생성.
    webview.settings.javaScriptEnabled = true
    webview.addJavascriptInterface(WebAppInterface(this), "Android")
    webview.loadUrl("file:///android_asset/www/index.html")
    
    /** Instantiate the interface and set the context  */
    class WebAppInterface(private val mContext: Context) {
    	/** Show a toast from the web page  */
        @JavascriptInterface
        fun showToast(toast: String) =
            Toast.makeText(mContext, toast, Toast.LENGTH_SHORT).show()
    }

     

    Full Screen Issue

    • HTML video 태그 혹은 비디오 전체 화면 버튼 자체가 보이지 않을 때, 또한 전체화면 모드에서 가로모드로 플레이되지 않을 때.
    더보기

     binding.webView.settings.apply {
                javaScriptEnabled = true
                setAppCacheEnabled(true)
                pluginState = WebSettings.PluginState.ON
            }
            binding.webView.webChromeClient = object : WebChromeClient() {
                override fun onShowCustomView(view: View?, callback: CustomViewCallback?) {
                    super.onShowCustomView(
                        view,
                        callback
                    )
                    binding.webView.isVisible = false
                }

                override fun onHideCustomView() {
                    super.onHideCustomView()
                    binding.webView.isVisible = true

                }
            }
            binding.webView.webChromeClient = FullscreenableChromeClient(this)
            val html = getHTML(videoId)
            binding.webView.loadData(html, "text/html", "UTF-8")
        }

        fun getHTML(videoId: String): String {
            return ("<iframe class=\"youtube-player\" style=\"border: 0; width: 100%; height: 100%;padding:0px; margin:0px\" id=\"ytplayer\" type=\"text/html\" src=\"https://www.youtube.com/embed/$videoId?theme=dark&autohide=2&modestbranding=1&showinfo=0&autoplay=1&rel=0&enablejsapi=1\" frameborder=\"0\" allowfullscreen autobuffer controls onclick=\"this.play()\">\n</iframe>\n")
        }

     

     

    import android.app.Activity
    import android.content.Context
    import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
    import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
    import android.os.Build
    import android.view.MotionEvent
    import android.view.View
    import android.view.ViewGroup
    import android.view.WindowManager
    import android.webkit.WebChromeClient
    import android.widget.FrameLayout
    import androidx.core.content.ContextCompat

    class FullscreenableChromeClient(activity: Activity) : WebChromeClient() {
        private var mActivity: Activity? = null

        private var mCustomView: View? = null
        private var mCustomViewCallback: WebChromeClient.CustomViewCallback? = null
        private var mOriginalOrientation: Int = 0
        private var mFullscreenContainer: FrameLayout? = null

        companion object {
            private val COVER_SCREEN_PARAMS = FrameLayout.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT
            )
        }

        init {
            this.mActivity = activity
        }

        override fun onShowCustomView(
            view: View,
            callback: WebChromeClient.CustomViewCallback
        ) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
                if (mCustomView != null) {
                    callback.onCustomViewHidden()
                    return
                }
                this.mActivity?.requestedOrientation =
                    SCREEN_ORIENTATION_LANDSCAPE
                mOriginalOrientation = mActivity!!.requestedOrientation

                val decor =
                    mActivity?.window?.decorView as FrameLayout
                mFullscreenContainer =
                    FullscreenHolder(mActivity!!)
                mFullscreenContainer!!.addView(
                    view,
                    COVER_SCREEN_PARAMS
                )
                decor.addView(
                    mFullscreenContainer,
                    COVER_SCREEN_PARAMS
                )
                mCustomView = view
                setFullscreen(true)
                mCustomViewCallback = callback
            }
            super.onShowCustomView(view, callback)
        }

        override fun onShowCustomView(
            view: View,
            requestedOrientation: Int,
            callback: WebChromeClient.CustomViewCallback
        ) {
            this.onShowCustomView(view, callback)
        }

        override fun onHideCustomView() {
            if (mCustomView == null) {
                return
            }
            setFullscreen(false)
            val decor =
                mActivity!!.window.decorView as FrameLayout
            decor.removeView(mFullscreenContainer)
            mFullscreenContainer = null
            mCustomView = null
            mCustomViewCallback!!.onCustomViewHidden()

            mActivity?.requestedOrientation = SCREEN_ORIENTATION_PORTRAIT
        }

        private fun setFullscreen(enabled: Boolean) {
            val win = mActivity!!.window
            val winParams = win.attributes
            val bits =
                WindowManager.LayoutParams.FLAG_FULLSCREEN
            if (enabled) {
                winParams.flags = winParams.flags or bits
            } else {
                winParams.flags =
                    winParams.flags and bits.inv()
                if (mCustomView != null) {
                    mCustomView!!.systemUiVisibility =
                        View.SYSTEM_UI_FLAG_VISIBLE
                }
            }
            win.attributes = winParams
        }

        private class FullscreenHolder(ctx: Context) :
            FrameLayout(ctx) { init {
            setBackgroundColor(
                ContextCompat.getColor(
                    ctx,
                    android.R.color.black
                )
            )
        }

            override fun onTouchEvent(evt: MotionEvent): Boolean {
                return true
            }
        }
    }

     

    WebView Settings

    binding.webView.settings.apply {
                setSupportMultipleWindows(false) // 새창 띄우기 허용 
                setSupportZoom(false) // 화면 확대 허용 
                javaScriptEnabled = true // 자바스크립트 허용 
                javaScriptCanOpenWindowsAutomatically = false // 자바스크립트 새창 띄우기 허용 
                loadWithOverviewMode = true // html의 컨텐츠가 웹뷰보다 클 경우 스크린 크기에 맞게 조정 
                useWideViewPort = true // html의 viewport 메타 태그 지원 
                builtInZoomControls = false // 기본적으로 줌 기능이 되지 않습니다.
                displayZoomControls = false // 화면 확대/축소 허용 
                layoutAlgorithm = WebSettings.LayoutAlgorithm.SINGLE_COLUMN // 컨텐츠 사이즈 맞추기 
                cacheMode = WebSettings.LOAD_NO_CACHE // 브라우저 캐쉬 허용 
                domStorageEnabled = true // 로컬 저장 허용 
                databaseEnabled = true
                /**
                 * * This request has been blocked; the content must be served over HTTPS
                 * * https 에서 이미지가 표시 안되는 오류를 해결하기 위한 처리
                 * */
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                    mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
                }
            }
     binding.webView.apply {
      		webViewClient = WebViewClient()     // WebView 위젯을 사용해 여러 기능 사용
            webChromeClient = WebChromeClient() // 브라우저 크롬에 특화
           }

     

    반응형

    댓글

Designed by Me.