Android

Android) WebView 정리

가짜 개발자 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() // 브라우저 크롬에 특화
       }

 

반응형