IT 프로그래밍-Android

[Android] 커스텀 키보드 만들기(2/4) - 영문 키보드 만들기

godsangin 2019. 11. 5. 19:21
반응형

 앞서 말씀드렸듯이 KeyboardView와 Keyboard를 사용하지 않기 위하여 새로운 레이아웃을 정의하고 바인드하는 작업이 필요합니다.

 첫번째로 Layout을 작성해야합니다. 키 패드는 재활용 가능성이 다분하기 때문에 키패드 레이아웃을 따로 작성하였습니다. 

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
                                                   android:layout_width="match_parent"
                                                   android:layout_height="match_parent"
                                                   xmlns:app="http://schemas.android.com/apk/res-auto">
    <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            android:padding="8dp"
            android:text="."/>
    <Button
            android:id="@+id/key_button"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginTop="4dp"
            android:layout_marginBottom="2dp"
            android:layout_marginRight="4dp"
            android:layout_marginLeft="2dp"
            android:duplicateParentState="true"
            android:longClickable="true"
    android:background="@drawable/key_background"/>
    <ImageView
            android:id="@+id/spacial_key"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginTop="4dp"
            android:layout_marginBottom="2dp"
            android:layout_marginRight="4dp"
            android:layout_marginLeft="2dp"
            android:duplicateParentState="true"
            android:longClickable="true"
            android:scaleType="centerInside"
            android:visibility="gone"/>

</androidx.constraintlayout.widget.ConstraintLayout>

<keyboard_item.xml>

 키 패드는 실제로 키 버튼이 들어갈 Button과 Button 오른쪽 위로 롱 클릭 시 작성될 특수문자, DELETE 버튼에 사용될 ImageView로 구성되어있습니다.(레이아웃 이름은 keyboard_item입니다)

그리고 이 키 패드를 include하는 실제 keyboardView를 작성합니다.(레이아웃 이름은 keyboard_action입니다)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent">
    <LinearLayout
            android:id="@+id/numpad_line"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:layout_weight="1"
            android:orientation="horizontal">
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_1"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1">

        </include>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_2"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1">

        </include>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_3"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1">

        </include>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_4"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1">

        </include>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_5"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1">

        </include>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_6"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1">

        </include>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_7"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1">

        </include>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_8"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1">

        </include>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_9"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1">
        </include>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_0"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1">
        </include>
</LinearLayout>
    <LinearLayout
            android:id="@+id/first_line"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:layout_weight="1"
            android:orientation="horizontal">
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_q"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1">

        </include>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_w"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1">

        </include>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_e"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1">

        </include>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_r"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1">

        </include>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_t"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1">

        </include>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_y"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1">

        </include>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_u"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1">

        </include>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_i"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1">

        </include>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_o"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1">

        </include>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_p"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"/>

    </LinearLayout>
    <LinearLayout
            android:id="@+id/second_line"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:layout_weight="1"
            android:paddingLeft="12dp"
            android:paddingRight="12dp"
            android:orientation="horizontal">
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_a"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"/>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_s"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"/>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_d"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"/>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_f"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"/>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_g"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"/>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_h"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"/>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_j"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"/>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_k"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"/>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_l"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"/>
    </LinearLayout>
    <LinearLayout
            android:id="@+id/third_line"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:layout_weight="1"
            android:orientation="horizontal">
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_caps"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1.5"/>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_z"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"/>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_x"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"/>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_c"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"/>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_v"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"/>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_b"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"/>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_n"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"/>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_m"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"/>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_del"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1.7"/>
    </LinearLayout>
    <LinearLayout
            android:id="@+id/fourth_line"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:layout_weight="1"
            android:orientation="horizontal">
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_simbols"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="2"/>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_mode_change"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"/>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_rest"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"/>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_space"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="4"/>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_dot"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"/>
        <include
                layout="@layout/keyboard_item"
                android:id="@+id/action_key_enter"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="2"/>
    </LinearLayout>

</LinearLayout>

<keyboard_action.xml>

위와 같이 4개의 큰 LinearLayout으로 정의되어 있습니다.

다음으로 코드를 작성해 보겠습니다. 각각의 키 패드를 바인드 하기 위한 일반적인 키 패드의 문자열을 정의합니다.

val numpadText = listOf<String>("1","2","3","4","5","6","7","8","9","0")
val firstLineText = listOf<String>("q","w","e","r","t","y","u","i","o","p")
val secondLineText = listOf<String>("a","s","d","f","g","h","j","k","l")
val thirdLineText = listOf<String>("CAPS","z","x","c","v","b","n","m","DEL")
val fourthLineText = listOf<String>("!#1","한/영",",","space",".","Enter")
val myKeysText = ArrayList<List<String>>()
val layoutLines = ArrayList<LinearLayout>()

<KeyboardEnglish.kt>

그런 뒤 싱글톤으로 만들기 위한 newInstance메소드를 작성합니다.

(수정)싱글톤으로 제작한 키보드는 modechange를 실행할 때마다 뷰에 대한 이벤트를 새로 정의하기 때문에 속도 면에서 성능이 크게 저하되는 것을 느낄 수 있었습니다. 그래서 소스가 변경되었습니다.

lateinit var englishLayout: LinearLayout
    var inputConnection:InputConnection? = null
        set(inputConnection){
            field = inputConnection
}
fun init() {
        englishLayout = layoutInflater.inflate(R.layout.keyboard_action, null) as LinearLayout
        vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator

        val config = context.getResources().configuration
        sharedPreferences = context.getSharedPreferences("setting", Context.MODE_PRIVATE)
        val height = sharedPreferences.getInt("keyboardHeight", 150)
        sound = sharedPreferences.getInt("keyboardSound", -1)
        vibrate = sharedPreferences.getInt("keyboardVibrate", -1)

        val numpadLine = englishLayout.findViewById<LinearLayout>(
            R.id.numpad_line
        )
        val firstLine = englishLayout.findViewById<LinearLayout>(
            R.id.first_line
        )
        val secondLine = englishLayout.findViewById<LinearLayout>(
            R.id.second_line
        )
        val thirdLine = englishLayout.findViewById<LinearLayout>(
            R.id.third_line
        )
        val fourthLine = englishLayout.findViewById<LinearLayout>(
            R.id.fourth_line
        )

        if(config.orientation == Configuration.ORIENTATION_LANDSCAPE){
            firstLine.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, (height*0.7).toInt())
            secondLine.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, (height*0.7).toInt())
            thirdLine.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, (height*0.7).toInt())
        }else{
            firstLine.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, height)
            secondLine.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, height)
            thirdLine.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, height)
        }

        myKeysText.clear()
        myKeysText.add(numpadText)
        myKeysText.add(firstLineText)
        myKeysText.add(secondLineText)
        myKeysText.add(thirdLineText)
        myKeysText.add(fourthLineText)

        myLongClickKeysText.clear()
        myLongClickKeysText.add(firstLongClickText)
        myLongClickKeysText.add(secondLongClickText)
        myLongClickKeysText.add(thirdLongClickText)

        layoutLines.clear()
        layoutLines.add(numpadLine)
        layoutLines.add(firstLine)
        layoutLines.add(secondLine)
        layoutLines.add(thirdLine)
        layoutLines.add(fourthLine)

        setLayoutComponents()
    }
    
    fun getLayout():LinearLayout{
        return englishLayout
    }

<KeyboardEnglish.kt>

여기서 주의할 점은 위의 keyboard_action레이아웃과 코드 상의 line들의 문자열의 개수가 같아야한다는 점입니다(ArrayIndexOutofBound Exception이 발생합니다). 키 패드의 높이는 우선 150으로 통일하도록 하겠습니다. 본인의 개발 방향에 맞게 수정하시면 될 것 같습니다.

 

다음으로 setLayoutComponents 메소드입니다. 

private fun setLayoutComponents(){
            for(line in layoutLines.indices){
                val children = layoutLines[line].children.toList()
                val myText = myKeysText[line]
                for(item in children.indices){
                    val actionButton = children[item].findViewById<Button>(R.id.key_button)
                    val spacialKey = children[item].findViewById<ImageView>(R.id.spacial_key)
                    var myOnClickListener:View.OnClickListener? = null
                    when(myText[item]){
                        "space" -> {
                            spacialKey.setImageResource(R.drawable.ic_space_bar)
                            spacialKey.visibility = View.VISIBLE
                            actionButton.visibility = View.GONE
                            myOnClickListener = getSpaceAction()
                            spacialKey.setOnClickListener(myOnClickListener)
                            spacialKey.setOnTouchListener(getOnTouchListener(myOnClickListener))
                            spacialKey.setBackgroundResource(R.drawable.key_background)
                        }
                        "DEL" -> {
                            spacialKey.setImageResource(R.drawable.del)
                            spacialKey.visibility = View.VISIBLE
                            actionButton.visibility = View.GONE
                            myOnClickListener = getDeleteAction()
                            spacialKey.setOnClickListener(myOnClickListener)
                            spacialKey.setOnTouchListener(getOnTouchListener(myOnClickListener))
                        }
                        "CAPS" -> {
                            spacialKey.setImageResource(R.drawable.ic_caps_unlock)
                            spacialKey.visibility = View.VISIBLE
                            actionButton.visibility = View.GONE
                            capsView = spacialKey
                            myOnClickListener = getCapsAction()
                            spacialKey.setOnClickListener(myOnClickListener)
                            spacialKey.setOnTouchListener(getOnTouchListener(myOnClickListener))
                            spacialKey.setBackgroundResource(R.drawable.key_background)
                        }
                        "Enter" -> {
                            spacialKey.setImageResource(R.drawable.ic_enter)
                            spacialKey.visibility = View.VISIBLE
                            actionButton.visibility = View.GONE
                            myOnClickListener = getEnterAction()
                            spacialKey.setOnClickListener(myOnClickListener)
                            spacialKey.setOnTouchListener(getOnTouchListener(myOnClickListener))
                            spacialKey.setBackgroundResource(R.drawable.key_background)
                        }
                        else -> {
                            actionButton.text = myText[item]
                            buttons.add(actionButton)
                            myOnClickListener = getMyClickListener(actionButton)
                            actionButton.setOnTouchListener(getOnTouchListener(myOnClickListener))
                        }
                    }
                    children[item].setOnClickListener(myOnClickListener)
                }
            }
        }

<KeyboardEnglish.kt>

layout레벨의 모든 LinearLayout을 순회하며 문자열을 button에 바인드해줍니다. 이 때 특수한 키워드는 필요한 방향에 맞게 수정합니다. (UI작업) 여기서 else문은 일반적인 키 패드 작업을 의미합니다.

 

마지막으로 각 버튼을 눌렀을 때 발생하는 이벤트를 반환하는 getMyClickListener를 작성합니다.

private fun getMyClickListener(actionButton:Button):View.OnClickListener{
            val clickListener = (View.OnClickListener {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                    inputConnection.requestCursorUpdates(InputConnection.CURSOR_UPDATE_IMMEDIATE)
                }
                playVibrate()
                val cursorcs:CharSequence? =  inputConnection.getSelectedText(InputConnection.GET_TEXT_WITH_STYLES)
                if(cursorcs != null && cursorcs.length >= 2){

                    val eventTime = SystemClock.uptimeMillis()
                    inputConnection.finishComposingText()
                    inputConnection.sendKeyEvent(KeyEvent(eventTime, eventTime,
                        KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL, 0, 0, 0, 0,
                        KeyEvent.FLAG_SOFT_KEYBOARD))
                    inputConnection.sendKeyEvent(KeyEvent(SystemClock.uptimeMillis(), eventTime,
                        KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL, 0, 0, 0, 0,
                        KeyEvent.FLAG_SOFT_KEYBOARD))
                    inputConnection.sendKeyEvent(KeyEvent(eventTime, eventTime,
                        KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_LEFT, 0, 0, 0, 0,
                        KeyEvent.FLAG_SOFT_KEYBOARD))
                    inputConnection.sendKeyEvent(KeyEvent(SystemClock.uptimeMillis(), eventTime,
                        KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DPAD_LEFT, 0, 0, 0, 0,
                        KeyEvent.FLAG_SOFT_KEYBOARD))

                }
                else{
                    when (actionButton.text.toString()) {
                        "한/영" -> {
                            keyboardInterationListener.modechange(1)
                        }
                        "!#1" -> {
                            keyboardInterationListener.modechange(2)
                        }
                        else -> {
                            playClick(
                                actionButton.text.toString().toCharArray().get(
                                    0
                                ).toInt()
                            )
                            inputConnection.commitText(actionButton.text,1)
                        }
                    }
                }
            })
            actionButton.setOnClickListener(clickListener)
            return clickListener
        }

<KeyboardEnglish.kt>

여기서 만약 현재 선택중인 블럭이 존재한다면 해당 블럭을 삭제하고 텍스트를 입력할 수 있도록 합니다. button의 text가 한/영 또는 특수문자일 경우에는 이전에 작성했던 keyboardInteractionListener의 modechange를 호출합니다.

그 이외의 경우에는 button의 텍스트 자체를 inputConnection을 통하여 commit합니다.

영어 키보드의 핵심 기능은 여기까지이고 특수기능, 전체 코드를 확인하고 싶으시다면 댓글을 남겨주시기 바랍니다(여기있는 코드만 복사/붙혀넣기 하면 오류가 많을거예요 ㅠㅠ). 회사측이랑 이야기해보고 깃허브 URL을 첨부하도록 하겠습니다..!

 

영어 키보드의 경우에는 버튼의 텍스트를 바로 commit하면 되겠지만 여러분이 알고 계시는 한글 키보드는 어떨까요 ??

한글은 text가 완성이 되려면 몇 번이 될지 모르는 클릭을 해야합니다. 다음시간에는 한글 키보드 작성을 위한 오토마타 생성 편으로 찾아뵙겠습니다.