티스토리 뷰

728x90

2022.04.13 - [안드로이드/코드] - 안드로이드 웹뷰 사용해보기 2 : 웹뷰 적용하기

 

안드로이드 웹뷰 사용해보기 2 : 웹뷰 적용하기

2022.04.11 - [안드로이드/코드] - 안드로이드 웹뷰 사용해보기 1 : 웹 사이트 제작 안드로이드 웹뷰 사용해보기 1 : 웹 사이트 제작 평소에 웹뷰는 앱 내에서 인터넷 보여주는 용도로만 사용하고 그

alanboyce.tistory.com

2편에서 두 번째 화면에 대한 내용 쓰는 걸 까먹긴 했는데 전체적인 활용방식은 똑같아서 수정은 안 하였습니다.
이번 글에 전체 코드가 올라갈 예정입니다!

이번에는 웹뷰의 각종 속성들을 알아보려고 합니다.
모든 속성들을 다 알아볼 순 없고 회사에서 사용했던 것들과 자주 사용되는 속성들 위주로 작성하였습니다.
평소에는 웹뷰 속성이겠거니 하고 넘긴 부분도 공부할 수 있어서 좋았습니다 ㅎㅎ

이번 글은 이전 글과는 달리 코드로만 작성하고자 합니다.
코드 안에 주석을 참조해주세요~

[login.xml]

<!DOCTYPE html>
    <head>
        <meta charset="UTF-8">
        <title>안드로이드 웹뷰 로그인화면</title>
        <link rel="stylesheet" type="text/css" href="style.css">
    </head>
    <body>
        <div class="container">
            <form action="select.html">
                <h3>로그인</h3>
                <div class="inputBox">
                    <span>ID</span>
                    <div class="box">
                        <div class="icon"><ion-icon name="person"></ion-icon></div>
                        <input id="login_id" type="text">
                    </div>
                </div>
                <div class="inputBox">
                    <span>PassWord</span>
                    <div class="box">
                        <div class="icon"><ion-icon name="lock-closed"></ion-icon></div>
                        <input id="login_pw" type="password">
                    </div>
                </div>
                <div class="inputBox">
                    <div class="box">
                        <input type="button" value="로그인" onclick="login()">
                    </div>
                </div> 
            </form>
        </div>
        <script>
            // 로그인 이벤트
            function login(){
                var id = document.getElementById("login_id").value;
                var pw = document.getElementById("login_pw").value;
                console.log("로그인 시도");
                if(id == null || pw == null || id == "" || pw == "") {
                    alert("아이디 또는 패스워드를 입력해주세요.")
                } else {
                    showAndroidToast("id : " + id + " / pw : " + pw);
                    currentLoginInfo(id, pw);
                    window.location = "file:///android_asset/select.html";
                }
            }

            // 앱으로부터 전달받은 id, pw 셋팅
            function setLoginInfo(id, pw) {
                document.getElementById("login_id").value = id;
                document.getElementById("login_pw").value = pw;
            }

            // 로그인한 id, pw 정보를 앱에 전달
            function currentLoginInfo(id, pw) {
                Android.currentLoginInfo(id, pw);
            }

            // 토스트 메시지를 위한 함수
            function showAndroidToast(toast){
                Android.showToast(toast);
            }

        </script>
        <script type="module" src="https://unpkg.com/ionicons@5.5.2/dist/ionicons/ionicons.esm.js"></script>
        <script nomodule src="https://unpkg.com/ionicons@5.5.2/dist/ionicons/ionicons.js"></script>
    </body>
</html>

[login -> style.css]

@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
*
{
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    font-family: 'Poppins', sans-serif;
}
body
{
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    background: #2f363e;
}
.container
{
    position: relative;
    width: 350px;
    min-height: 500px;
    display: flex;
    justify-content: center;
    align-items: center;
    background-color: #2f363e;
    box-shadow: 25px 25px 75px rgba(0, 0, 0, 0.25),
    10px 10px 70px rgba(0, 0, 0, 0.25),
    inset 5px 5px 10px rgba(0, 0, 0, 0.5),
    inset 5px 5px 20px rgba(255, 255, 255, 0.2),
    inset -5px -5px 15px rgba(0, 0, 0, 0.75);
    border-radius: 30px;
    padding: 50px;
}
.container h3
{
    color: #fff;
    font-weight: 600;
    font-size: 2em;
    width: 100%;
    text-align: center;
    margin-bottom: 30px;
    letter-spacing: 2px;
    text-transform: uppercase;
}
.inputBox
{
    position: relative;
    width: 100%;
    margin-bottom: 20px;
}
.inputBox span{
    display: inline-block;
    color: #fff;
    margin-bottom: 10px;
    text-transform: uppercase;
    letter-spacing: 1px;
    font-size: 0.75em;
    border-left: 4px solid #fff;
    padding-left: 4px;
    line-height: 1em;
}
.inputBox .box
{
    display: flex;
}
.inputBox .box .icon
{
    position: relative;
    min-width: 40px;
    height: 40px;
    background-color: #ff2c74;
    display: flex;
    justify-content: center;
    align-items: center;
    border-radius: 50%;
    margin-right: 10px;
    color: #fff;
    font-size: 1.15em;
    box-shadow: 5px 5px 7px rgba(0, 0, 0, 0.25),
    inset 2px 2px 5px rgba(255, 255, 255, 0.25),
    inset -3px -3px 5px rgba(0, 0, 0, 0.5);
}
.inputBox .box input
{
    position: relative;
    width: 100%;
    border: none;
    outline: none;
    padding: 10px 20px;
    border-radius: 30px;
    box-shadow: 5px 5px 7px rgba(0, 0, 0, 0.25),
    inset 2px 2px 5px rgba(255, 255, 255, 0.35),
    inset -3px -3px 5px rgba(0, 0, 0, 0.5);
}
.inputBox .box input[type="button"]
{
    background: #1f83f2;
    box-shadow: 5px 5px 7px rgba(0, 0, 0, 0.25),
    inset 2px 2px 5px rgba(255, 255, 255, 0.25),
    inset -3px -3px 5px rgba(0, 0, 0, 0.5);
    color: #fff;
    cursor: pointer;
    text-transform: uppercase;
    letter-spacing: 2px;
    font-weight: 600;
    margin-top: 10px;
}
.inputBox .box input[type="button"]:hover
{
    filter: brightness(1.1);
}

[select.html]

<!DOCTYPE html>
    <head>
        <meta charset="UTF-8">
        <title>선택</title>
        <link rel="stylesheet" href="style2.css">

    </head>
    <body>
        <div class="container">
            <input type="radio" id="select1" name="select" value="1">
            <input type="radio" id="select2" name="select" value="2">
            <input type="radio" id="select3" name="select" value="3">
            <label id="label1" class="box" for="select1">
                <img src="001.png">
                <span>이상해씨</span>
            </label>
            <br><br>
            <label id="label2" class="box" for="select2">
                <img src="004.png">
                <span>파이리</span>
            </label>
            <br><br>
            <label id="label3" class="box" for="select3">
                <img src="007.png">
                <span>꼬부기</span>
            </label>
        </div>
        <button id="ok" onclick="itemSelected()">선택 완료</button>
        <script>
            // UI를 위한 함수입니다. 중요한 코드는 아닙니다~.
            document.body.addEventListener('change', function(e) {
                let target = e.target;
                var index = 0;
                switch (target.id) {
                    case 'select1':
                        index = 0;
                        break;
                        case 'select2':
                        index = 1;
                        break;
                    case 'select3':
                        index = 2;
                        break;
                }
                var box = document.getElementsByClassName("box");
                var colorList = ["#09c14f", "#ff742a", "#2da2ff"];
                for(var i = 0; i < box.length; i++) {
                    var img = box[i].getElementsByTagName("img");
                    var span = box[i].getElementsByTagName("span");
                    if(i == index) {
                        img[0].style.height = "125%";
                        box[i].style.animationPlayState = "running";
                        box[i].style.filter = "grayscale(0)";
                        span[0].style.color = colorList[i];
                        scrollTo(box[i].offsetTop, box[i].offsetTop /2);
                    } else {
                        img[0].style.height = "100%"
                        box[i].style.animationPlayState = "paused";
                        box[i].style.filter = "grayscale(1)";
                    }
                }
            });

            // 선택 완료 버튼 클릭 이벤트
            function itemSelected() {
                try {
                    var index = document.querySelector("input[name='select']:checked").value;
                    selectCharacter(Number(index));
                } catch (error) {
                    alert("케릭터를 선택해 주세요");
                }

            };

            // 선택한 정보를 앱에게 전달
            function selectCharacter(index) {
                Android.selectCharacter(index);
            };
        </script>
    </body>

</html>

[slect -> style2.css]

*
{
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
body
{
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    background-color: #262626;
    padding: 30px 0 100px 0;
}
#select1, #select2, #select3
{
    width: 0;
    height: 0;
    padding: 0;
    margin: 0;
}
.container
{
    position: relative;
    display: block;
    /*-webkit-box-reflect: below 1px linear-gradient(transparent, transparent, transparent, #0004);*/
}
.container .box
{
    position: relative;
    width: 280px;
    height: 360px;
    margin: 0 20px;
    border-radius: 20px;
    background: linear-gradient(45deg, #09c14f 25%,
    #444 25%, #444 50%, #09c14f 50%, #09c14f 75%,
    #444 75%, #444 100%);
    background-size: 40px 40px;
    filter: grayscale(1);
    animation: animateBg 1s linear infinite;
    animation-play-state: paused;
    transition: filter 1s;
    display: flex;
    justify-content: center;
}
.container .box span
{
    position: absolute;
    bottom: 30px;
    background: white;
    border-radius: 30px;
    padding: 5px 20px;
    border: 2px solid #444;
    font-weight: 700;
}
#label2
{
    background: linear-gradient(135deg, #ff742a 25%,
    #444 25%, #444 50%, #ff742a 50%, #ff742a 75%,
    #444 75%, #444 100%);
    background-size: 40px 40px;
}
#label3
{
    background: linear-gradient(45deg, #2da2ff 25%,
    #444 25%, #444 50%, #2da2ff 50%, #2da2ff 75%,
    #444 75%, #444 100%);
    background-size: 40px 40px;
}
@keyframes animateBg
{
    0%
    {
        background-position: 0;
    }
    100%
    {
        background-position: 40px;
    }
}
.container .box img
{
    position: absolute;
    bottom: 20px;
    height: 100%;
    transition: height 0.5s;
}
#ok
{
    position: fixed;
    bottom: 30px;
    background: white;
    padding: 10px 20px;
    border-radius: 100px;
    border: 3px solid black;
    font-weight: 800;
}

[activity_main.xml]

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <WebView
        android:id="@+id/webView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

[MainActivity.kt]

package com.example.webviewstudy

import android.annotation.SuppressLint
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.view.View
import android.webkit.JsResult
import android.webkit.WebChromeClient
import android.webkit.WebSettings
import android.webkit.WebView
import androidx.appcompat.app.AlertDialog

class MainActivity : AppCompatActivity() {

    private lateinit var webView: WebView

    @SuppressLint("SetJavaScriptEnabled")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        webView = findViewById(R.id.webView)

        webView.apply {
            // WebView 에 불러올 url 주소
            // 방법 1 : 실제 인터넷 주소를 이용하는 방법
            // loadUrl("https://m.naver.com")

            // 방법 2 : html 태그를 이용하는 방법
            // val document = "<!DOCTYPE html><head>웹뷰 테스트</head>" +
            //                "<body><h1>웹뷰 테스트!!</h1>직접 넣는 방식으로 만들어 봤습니다.</body></html>"
            // webView.loadData(document, "text/html; charset=utf-8", "UTF-8")

            // 방법 3 : app > assets 하위에 파일들 생성 후 로드하는 방법
            loadUrl("file:///android_asset/login.html")

            // 앱과 웹에 인터페이스를 이용하여 제어하는 방식
            addJavascriptInterface(
                MyWebAppInterface(this@MainActivity) { moveActivity(it) },
                "Android"
            )

            // 프롬프트, 알럿, 로그 등을 제어, 커스텀할 수 있습니다.
            webChromeClient = MyWebChromeClient(this@MainActivity)
            // 웹뷰 상태 변화에 따라 웹뷰를 제어, 추가 행동 등을 할 수 있습니다.
            webViewClient = MyWebClient()

            // 웹 페이지 로드 전에 캐시와 히스토리를 삭제합니다.
            clearCache(true)
            clearHistory()
        }

        webView.settings.apply {
            // 자바스크립트 사용 허가여부 : false 로 설정하게 되면 javascript 로 작성한 부분이 작동하지 않습니다.
            javaScriptEnabled = true
            // 웹뷰 내 컨텐츠를 줌 기능 가능 여부. 두 개 모두 true 해야 줌이 가능합니다.
            setSupportZoom(true)
            builtInZoomControls = true
            // true 로 설정 시 WebView 에서 제공하는 시스템 컨트롤러가 나옵니다.
            displayZoomControls = false

            // 캐시 모드 설정. Deprecated 된 LOAD_NORMAL 을 제외하고
            // [LOAD_DEFAULT | LOAD_CACHE_ELSE_NETWORK | LOAD_CACHE_ONLY | LOAD_NO_CACHE] 4 종류가 있습니다.
            // LOAD_DEFAULT : 기본 모드, 캐시 사용, 기간 만료 시 네트워크 사용
            // LOAD_CACHE_ELSE_NETWORK : 캐시 기간 만료 시 네트워크 접속
            // LOAD_CACHE_ONLY : 캐시만 사용 (네트워크 사용x)
            // LOAD_NO_CACHE : 캐시 모드 사용 x
            cacheMode = WebSettings.LOAD_NO_CACHE

            // 폰의 system 글꼴 크기에 의해 변하는 것 방지. 글꼴의 크기를 비율(%)로 고정.
            textZoom = 100

            // localStorage 를 사용하여 dom 을 가져올 수 있도록 함
            domStorageEnabled = true
            // WebView 에서 앱에 등록되어있는 이미지 리소스를 사용해야 할 경우 자동으로 로드 여부
            loadsImagesAutomatically = true
            // WebView 를 통해 content Url 에 접근 가능 여부
            allowContentAccess = false
            // 기본 인코딩을 설정
            defaultTextEncodingName = "UTF-8"
            // 미디어 자동 재생 가능 여부
            mediaPlaybackRequiresUserGesture = false
            // javascript 의 window.open() 동작 허용 여부
            javaScriptCanOpenWindowsAutomatically = true
            // 여러개의 윈도우를 사용할 수 있도록 설정
            setSupportMultipleWindows(true)
        }

    }

    // 뒤로 가기 제어
    override fun onBackPressed() {
        // 웹뷰가 뒤로 가기가 가능한지 여부를 체크하여 웹뷰 뒤로가기 또는 액티비티 뒤로가기를 실행합니다.
        if (webView.canGoBack()) {
            webView.goBack()
        } else {
            super.onBackPressed()
        }
    }

    private fun moveActivity(index : Int) {
        // 두 번째 화면으로 이동합니다. 이동 시에 선택한 index 값을 전달합니다.
        val intent = Intent(this, SubActivity::class.java).also {
            it.putExtra(SubActivity.SELECT_KEY, index)
        }
        startActivity(intent)
    }

}
/**
 * 참고한 자료
 * 구글 공식 문서 : https://developer.android.com/guide/webapps/webview?hl=ko
 * 블로그 1 : https://www.blueswt.com/117
 * 블로그 2 : https://kyome.tistory.com/149
 * 블로그 3 : https://seosh817.tistory.com/67
 * */

[activity_sub.xml]

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".SubActivity">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

[SubActivity.kt]

package com.example.webviewstudy

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.ImageView

class SubActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_sub)

        // 메인 화면에서 넘어온 인덱스, 기본 값 1
        val index = intent?.getIntExtra(SELECT_KEY, 1) ?: 1
        val imageView = findViewById<ImageView>(R.id.imageView)

        // index 의 값에 따라 이미지 표시
        imageView.setImageResource(
            when(index){
                1 -> {
                    R.drawable.result1
                }
                2 -> {
                    R.drawable.result2
                }
                else -> {
                    R.drawable.result3
                }
            }
        )

    }

    companion object {
        const val SELECT_KEY = "select_key"
    }
}

[MyPreferenct.kt]

package com.example.webviewstudy

import android.content.Context
import android.content.SharedPreferences

// SharedPreferences 를 간편하게 사용하기 위해 만든 클래스
class MyPreference(context: Context) {
    private val pref : SharedPreferences = context.getSharedPreferences("WebViewTest", Context.MODE_PRIVATE)

    fun getString(key: String, defaultValue: String) : String {
        return pref.getString(key, defaultValue).toString()
    }

    fun setString(key: String, str: String) {
        pref.edit().putString(key, str).apply()
    }

    companion object {
        const val ID = "ID"
        const val PW = "PASS_WORD"
    }
}

[MyWebAppInterface.kt]

package com.example.webviewstudy

import android.content.Context
import android.util.Log
import android.webkit.JavascriptInterface
import android.widget.Toast

// 웹과 앱사이의 인터페이스
class MyWebAppInterface(
    private val mContext: Context,
    private val moveActivity : (Int) -> Unit
) {

    // 토스트 메시지를 표시하기 위한 함수
    @JavascriptInterface
    fun showToast(message: String) {
        Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show()
    }

    // 로그인에 사용한 id, pw 를 저장하기 위한 함수
    @JavascriptInterface
    fun currentLoginInfo(id: String, pw: String) {
        MyPreference(mContext).setString(MyPreference.ID, id)
        MyPreference(mContext).setString(MyPreference.PW, pw)
    }

    // 선택한 캐릭터의 index 를 받아 페이지 이동을 하기 위한 함수
    @JavascriptInterface
    fun selectCharacter(index: Int) {
        moveActivity(index)
    }
}

[MyWebChromeClient.kt]

package com.example.webviewstudy

import android.content.Context
import android.util.Log
import android.webkit.ConsoleMessage
import android.webkit.JsResult
import android.webkit.WebChromeClient
import android.webkit.WebView
import androidx.appcompat.app.AlertDialog

// WebChromeClient Custom
class MyWebChromeClient(private val context: Context) : WebChromeClient() {

    // 웹에서 사용한 콘솔 로그를 앱의 log 로 표기하기 위한 함수
    override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
        Log.e("WebViewLog", "${consoleMessage?.message()}")
        return super.onConsoleMessage(consoleMessage)
    }

    // 웹에서 사용한 알럿을 앱의 알럿으로 표시하기 위한 함수
    override fun onJsAlert(
        view: WebView?,
        url: String?,
        message: String?,
        result: JsResult?
    ): Boolean {
        AlertDialog.Builder(context)
            .setTitle("알림")
            .setMessage(message)
            .setPositiveButton("확인") { dialog, _ ->
                dialog.dismiss()
            }
            .setCancelable(false)
            .show()

        result?.cancel()
        return true
    }
}

[MyWebClient.kt]

package com.example.webviewstudy

import android.webkit.WebView
import android.webkit.WebViewClient

// WebViewClient Custom
class MyWebClient : WebViewClient() {

    // 페이지 로드가 완료 되어 있을 때 처리하기 위한 함수
    override fun onPageFinished(view: WebView?, url: String?) {
        super.onPageFinished(view, url)

        view?: return

        // 주소가 로그인 화면에서만 유효하도록 설정
        if (url?.contains("login.html") == true) {
            val id = MyPreference(view.context).getString(MyPreference.ID, "")
            val pw = MyPreference(view.context).getString(MyPreference.PW, "")

            if (id.isNotEmpty() && pw.isNotEmpty()) {
                // 웹의 setLoginInfo 함수 호출
                view.evaluateJavascript("javascript:setLoginInfo('$id', '$pw');") {}
            }
        }
    }
}

 

 

 

이것으로 웹뷰 관련 글을 마칩니다~

 

 

참고한 사이트

구글 공식 문서 : https://developer.android.com/guide/webapps/webview?hl=ko
블로그 1 : https://www.blueswt.com/117
블로그 2 : https://kyome.tistory.com/149
블로그 3 : https://seosh817.tistory.com/67
유튜브 1 : https://www.youtube.com/watch?v=MnLVEMsbJTI
유튜브 2 : https://www.youtube.com/watch?v=R4rccU8YQjQ

728x90
댓글