วิธีแก้ปัญหา UI โดนบังเพราะ On-screen Keyboard

คุณก็เป็นคนหนึ่งที่เจอปัญหา On-screen Keyboard บดบัง UI ที่อยากจะให้ผู้ใช้มองเห็นระหว่างพิมพ์ข้อความใน EditText ใช่มั้ยล่ะ ?

บทความนี้ใช้วิธีเดียวกับ TextInputEditText ของ Material Design

เวลาผู้ใช้กดที่ EditText และ On-screen Keyboard แสดงขึ้นมา ถ้า EditText นั้นอยู่ต่ำกว่าพื้นที่การแสดงผลของ On-screen Keyboard ระบบแอนดรอยด์ก็จะเลื่อน EditText ขึ้นมาให้อยู่เหนือ On-screen Keyboard เพื่อให้ผู้ใช้สามารถเห็นได้ว่าพิมพ์ตัวอักษรอะไรลงไป

ในระหว่างนี้ EditText จะอยู่ในสถานะที่เรียกว่า Focused

แต่สำหรับ UI บางแบบก็อาจจะต้องการให้มี View บางตัวอยู่ต่อท้าย EditText โดยไม่ต้องการให้ On-screen Keyboard แสดงขึ้นมาทับ View ตัวดังกล่าวด้วย

ยกตัวอย่างเช่น EditText ที่มี Button อยู่ข้างล่าง และไม่อยากให้ On-screen Keyboard แสดงขึ้นมาทับ เพราะต้องการให้ผู้ใช้กดปุ่มหลังจากพิมพ์เสร็จได้เลย

ดังนั้นนักพัฒนาอย่างเราก็ต้องหาทางทำให้ Button ไม่ถูก On-screen Keyboard บังระหว่างที่ผู้ใช้กำลังพิมพ์ตัวอักษรใน EditText นั่นเอง

มาทำให้ View ตัวอื่นนอกจาก EditText แสดงอยู่เหนือ On-screen Keyboard กันเถอะ

เวลาผู้ใช้กดที่ EditText เพื่อพิมพ์ข้อมูลใด ๆ ก็ตาม ระบบแอนดรอยด์จะไม่รู้ขนาดของ EditText ตั้งแต่แรก แต่จะเรียก Method ทั้ง 3 ตัวของ EditText เพื่อดูว่าจะต้องใช้พื้นที่เท่าไร แล้วค่อยแสดง On-screen Keyboard ไม่ให้บัง EditText

fun getFocusedRect(r: Rect?)
fun getGlobalVisibleRect(r: Rect?, globalOffset: Point?): Boolean
fun requestRectangleOnScreen(rectangle: Rect?): Boolean

ทั้ง 3 Method นี้จะมีคลาส Rect เป็นตัวกลางสำคัญที่จะบอกพื้นที่ที่ EditText ต้องการเพื่อแสดงบนหน้าจอ

  • getFocusedRect : กำหนดพื้นที่ที่ต้องใช้ในการแสดงสถานะ Focused โดยอ้างอิงจากขนาดของ View
  • getGlobalVisibleRect : กำหนดพื้นที่ในการแสดงของ View โดยอ้างอิงจากพื้นที่หน้าจอที่แอปแสดงผลอยู่
  • requestRectangleOnScreen : สั่งให้ระบบแอนดรอยด์แสดงพื้นที่ที่กำหนดให้เห็นบนหน้าจอ

โดยนักพัฒนาสามารถสร้าง Override Method สำหรับทั้ง 3 Method ใน Custom EditText เพื่อกำหนดค่า Rect เองได้ และเราก็จะคำนวณ Rect ใหม่โดยรวมพื้นที่ของ View ตัวอื่น ๆ ด้วยนั่นเอง

// CustomEditText.kt
class CustomEditText : AppCompatEditText {
    /* ... */

    override fun getFocusedRect(r: Rect?) {
        super.getFocusedRect(r)
        // Re-calculate view area
    }

    override fun getGlobalVisibleRect(r: Rect?, globalOffset: Point?): Boolean {
        val result = super.getGlobalVisibleRect(r, globalOffset)
        // Re-calculate view area
        return result
    }

    override fun requestRectangleOnScreen(rectangle: Rect?): Boolean {
        val result = super.requestRectangleOnScreen(rectangle)
        // Re-calculate view area
        return result
    }
}

แล้วจะคำนวณพื้นที่ของ View ตัวอื่นจาก EditText ได้ยังไง?

ในการทำ EditText ไปใช้งานจริง จะพบว่าเราไม่สามารถรู้ได้เลยว่า EditText จะใช้งานคู่กับ View ตัวไหน ดังนั้นเพื่อให้นำไปใช้งานได้อย่างยืดหยุ่น จึงต้องสร้าง Custom Layout เพื่อให้ EditText ใช้อ้างอิงพื้นที่แทน แล้วข้างใน Custom Layout จะจัด UI แบบไหนก็ได้

ทั้งนี้ก็เพราะว่าใน View ทุกตัวสามารถเขียนคำสั่งเพื่อหา View Group ของ View ตัวนั้นได้ หรือจะเป็น View Group ชั้นนอกที่เกิดจากการสร้าง Nested View Group ก็ได้เช่นกัน

Layout และ ViewGroup ในบทความนี้จะหมายถึงสิ่งเดียวกัน

สร้าง Custom Layout

เพราะเราไม่รู้ว่า EditText จะถูกนำไปใช้กับ View Group แบบไหน และถ้าเป็น Nested View Group จะต้องคำนวณพื้นที่จาก View Group ตัวไหน ดังนั้นเพื่อให้ง่ายต่อการคำนวณ จึงใช้วิธีสร้าง Custom Layout ขึ้นมาเพื่อให้ EditText ใช้อ้างอิงในการคำนวณพื้นที่

เมื่อสร้างขึ้นมาเพื่อใช้อ้างอิงในการคำนวณพื้นที่เท่านั้น จึงสร้าง Custom Layout ขึ้นมาโดยไม่ต้องเพิ่มคำสั่งใด ๆ เข้าไป

// CustomLayout.kt
class CustomLayout : FrameLayout {
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    )
}

ดังนั้นถ้าต้องการให้ Custom EditText คำนวณพื้นที่ของ View ตัวอื่นด้วย ก็ให้ใส่ Custom EditText และ View ตัวอื่น ๆ ไว้ใน Custom Layout นั่นเอง

ค้นหา Custom Layout ผ่าน Custom EditText

เมื่อ Custom EditText อยู่ใน Custom Layout ก็จะสามารถค้นหา Custom Layout ด้วยคำสั่งแบบนี้ได้

// CustomEditText.kt
class CustomEditText : AppCompatEditText {
    /* ... */

    private fun getCustomLayout(): CustomLayout? {
        var viewParent = parent
        while (viewParent is View) {
            if (viewParent is CustomLayout) {
                return viewParent
            }
            viewParent = (viewParent as View).parent
        }
        return null
    }
}

จากคำสั่งดังกล่าว ไม่ว่าข้างใน Custom Layout จะเป็น Nested Custom Layout ก็จะเรียกใช้งานได้เสมอ แต่ถ้าหาไม่เจอก็จะได้เป็น null แทน

คำนวณพื้นที่ใหม่จาก Custom Layout เพื่อใช้แทน Custom EditText

ในคำสั่ง getFocusedRect(...) กับ getGlobalVisibleRect(...) ที่จะส่งคลาส Rect เป็น Parameter เข้ามาให้ ให้คำนวณใหม่โดยกำหนดค่า Bottom ของ Rect ที่ได้จาก Custom Layout แทน

// CustomEditText.kt
class CustomEditText : AppCompatEditText {
    /* ... */
    
    private var parentRect = Rect()

    override fun getFocusedRect(r: Rect?) {
        super.getFocusedRect(r)
        getCustomLayout()?.let { view ->
            view.getFocusedRect(parentRect)
            r?.bottom = parentRect.bottom
        }
    }

    override fun getGlobalVisibleRect(r: Rect?, globalOffset: Point?): Boolean {
        val result = super.getGlobalVisibleRect(r, globalOffset)
        getCustomLayout()?.let { view ->
            view.getGlobalVisibleRect(parentRect, globalOffset)
            r?.bottom = parentRect.bottom
        }
        return result
    }
}

จะเห็นว่าเจ้าของบล็อกได้สร้างคลาส Rect เป็น Global Variable ไว้ โดยเก็บค่าจากคำสั่ง getFocusedRect(...) กับ getGlobalVisibleRect(...) ร่วมกัน

จากนั้นก็จะเอาคลาส Rect ไปใช้งานในคำสั่ง requestRectangleOnScreen ด้วย เพื่อแสดงพื้นที่ทั้งหมดของ Custom Layout ให้อยู่บนหน้าจอ

// CustomEditText.kt
class CustomEditText : AppCompatEditText {
    /* ... */
    
    private var parentRect = Rect()
    
    /* ... */

    override fun requestRectangleOnScreen(rectangle: Rect?): Boolean {
        val result = super.requestRectangleOnScreen(rectangle)
        getFocusableGroupLayout()?.let { view ->
            parentRect.set(
                0,
                0,
                view.width,
                view.height
            )
            view.requestRectangleOnScreen(parentRect, true)
        }
        return result
    }
}

เพียงเท่านี้ก็เป็นอันเสร็จเรียบร้อย ตอนนี้ Custom Layout และ Custom EditText ก็พร้อมนำไปใช้งานแล้ว

ลองจัด UI ใหม่

ข้างใน Custom Layout จะจัดแบบไหนก็ได้ ขอแค่ให้มี Custom EditText อยู่ข้างในก็พอ

<CustomLayout>
    
    <LinearLayout>
        
        <TextView />
        
        <CustomEditText />
        
        <Button />
        
    </LinearLayout>
    
</CustomLayout>

เมื่อลองทดสอบดูก็จะพบว่า Custom Layout เลื่อนมาอยู่เหนือ On-screen Keyboard ให้ด้วยแล้ว

0:00
/

หมดปัญหา View โดน On-screen Keyboard บังแล้ว เย้!

เพียงเท่านี้ก็ไม่ต้องกังวลว่า View ที่สำคัญจะโดน On-screen Keyboard บัง เพียงเพราะอยู่ข้างล่าง EditText แล้ว ซึ่งจะช่วยให้นักพัฒนาสามารถออกแบบ UI สำหรับการแสดง EditText ได้หลากหลายและตรงความต้องการมากขึ้นนั่นเอง

อะไรนะ ไม่อยากสร้างเองหรอ ? มี Library ให้ใช้ด้วยนะ

ในกรณีที่ผู้ที่หลงเข้ามาอ่านไม่อยากสร้าง Custom Layout และ Custom EditText เอง สามารถใช้ Library ได้นะ เจ้าของบล็อกทำไว้ให้เรียบร้อยแล้ว

GitHub - akexorcist/GroupFocusable: Android Custom View for prevent the view behind on-screen keyboard when edit text is focused
Android Custom View for prevent the view behind on-screen keyboard when edit text is focused - GitHub - akexorcist/GroupFocusable: Android Custom View for prevent the view behind on-screen keyboard...

แหล่งข้อมูลอ้างอิง