iT邦幫忙

2022 iThome 鐵人賽

DAY 4
1

在前兩篇學會MDC Components的Button和Text Field,所以已經可以來實作一個「登錄頁面」。

介紹的內容:

  • 使用 IDE版本 : Android Studio Chipmunk | 2021.2.1 Canary 7
  • 使用 MDC Android Components
  • 實作介紹
    • Add the XML(提供完整程式碼)
    • Add input validation 顯示 error提示訊息的邏輯判斷(提供完整程式碼)
    • Add a trailing icon 設定顯示密碼、屏蔽密碼的邏輯

開始前需要特別說明

因為接下來的程式碼會全部使用ViewBinding取得對應view的 ID引用。
Kotlin 有個非常便捷的特性,無需再使用 findViewById() 對應view的 ID引用
build.gradle (Module: MDCSubject.app) 新增

但為什麼這麼方便我還要使用ViewBinding,因為官方已經對Kotlin Android Extensions 已提出棄用,通常棄用也不是不能用還是可以用到官方說不能用為止。

過去使用Kotlin Android Extensions的經驗,使用上必須注意class上面的import所引用的layout是否正確,因為有可能import到不正確的layout,這個IDE不會提示錯誤,必須靠人來檢查,所以建議使用ViewBinding。

ViewBinding是什麼

XML layout 自動生成Binding class,對view的 ID 引用,在class 的 onCreate 時透過使用生成的class.inflate(layoutInflater),且每個binging class還包含一個 getRoot() 方法,為相應layout的root的view提供直接引用

設定啟用:build.gradle (Module: MDCSubject.app)

buildFeatures{
	viewBinding = true
}

使用 MDC Android Components

https://ithelp.ithome.com.tw/upload/images/20220917/20144469lVKEWPPvqB.png


實作介紹

Add the XML

1. 新增兩個Textfield,分別是輸入名字、密碼

  • 每個欄位由一個TextInputLayout元素和一個TextInputEditText子欄位組成,文字欄位中的提示文字在android:hint屬性中指定提示文字,提示文字是當點擊時會自動縮小往上跑。
    https://ithelp.ithome.com.tw/upload/images/20220917/20144469ivsGzqXMTH.png
<com.google.android.material.textfield.TextInputLayout
        android:id="@+id/textInput_username"
        style="@style/Widget.Material3.TextInputLayout.FilledBox"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="@string/login_hint_username">

        <com.google.android.material.textfield.TextInputEditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

    </com.google.android.material.textfield.TextInputLayout>

		<!--password-->
    <com.google.android.material.textfield.TextInputLayout
        android:id="@+id/textInput_password"
        style="@style/Widget.Material3.TextInputLayout.FilledBox"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="@string/login_hint_password">

        <com.google.android.material.textfield.TextInputEditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>

    </com.google.android.material.textfield.TextInputLayout> 

2. trailing icon 設定顯示密碼、屏蔽密碼

  • 在密碼TextInputEditText元素上將android:inputType屬性設定為“textPassword”。 可讓密碼欄位中隱藏輸入文字。
    <com.google.android.material.textfield.TextInputLayout
        ...
                app:endIconMode="password_toggle"
        ...>
    
            <com.google.android.material.textfield.TextInputEditText
               ...
                android:inputType="textPassword" />
    
        </com.google.android.material.textfield.TextInputLayout> 
    

3. 密碼欄位設定counter限制用戶可輸入文字長度

<com.google.android.material.textfield.TextInputLayout
    ...
    app:counterEnabled="true"
    app:counterMaxLength="8">
    ...
</com.google.android.material.textfield.TextInputLayout>

4. 名字、密碼的欄位設定用戶為輸入時提示Errors訊息

  • 在Password TextInputLayout元素上將app:errorEnabled屬性設定為true。 這將為文字欄位下方的錯誤訊息新增的錯提示。

    <com.google.android.material.textfield.TextInputLayout
        ...
        app:errorEnabled="true">
        ...
    </com.google.android.material.textfield.TextInputLayout>
    

5. Button (Filled button)

<Button
   style="@style/Widget.Material3.Button"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:text="Filled button" />

完整程式碼

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/img_person"
        android:layout_width="100dp"
        android:layout_height="100dp"
        app:layout_constraintBottom_toTopOf="@id/username_input_text"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:srcCompat="@drawable/ic_person" />

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/login_title"
        app:layout_constraintBottom_toTopOf="@+id/username_input_text"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/img_person" />

    <com.google.android.material.textfield.TextInputLayout
        android:id="@+id/username_input_text"
        style="@style/Widget.Material3.TextInputLayout.FilledBox"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginHorizontal="50dp"
        android:layout_marginVertical="30dp"
        android:hint="@string/login_hint_username"
        app:errorEnabled="true"
        app:layout_constraintBottom_toTopOf="@id/password_input_text"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tv_title">

        <com.google.android.material.textfield.TextInputEditText
            android:id="@+id/username_edit_text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

    </com.google.android.material.textfield.TextInputLayout>

    <com.google.android.material.textfield.TextInputLayout
        android:id="@+id/password_input_text"
        style="@style/Widget.Material3.TextInputLayout.FilledBox"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginHorizontal="50dp"
        android:hint="@string/login_hint_password"
        app:counterEnabled="true"
        app:counterMaxLength="8"
        app:endIconMode="password_toggle"
        app:endIconDrawable="@mipmap/icon_closed_eye"
        app:errorEnabled="true"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/username_input_text">

        <com.google.android.material.textfield.TextInputEditText
            android:id="@+id/password_edit_text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:inputType="textPassword"/>

    </com.google.android.material.textfield.TextInputLayout>


    <Button
        android:id="@+id/btn_login"
        style="@style/Widget.Material3.Button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginVertical="20dp"
        android:text="@string/button_text_login"
        app:layout_constraintEnd_toEndOf="@id/password_input_text"
        app:layout_constraintTop_toBottomOf="@id/password_input_text" />

</androidx.constraintlayout.widget.ConstraintLayout>

Add input validation 顯示 error提示訊息的邏輯判斷

點擊按鈕判斷是否顯示Error提示情境

實現邏輯:

當用戶在未輸入時點擊登入按鈕,此時顯示Error提示訊息警示,用戶有輸入時透過doOnTextChange 動態偵測用戶輸入關閉Error警示提示。

判斷邏輯:

  • UserName、Password皆輸入完成
  • UserName、Password皆未輸入完成
  • 只有輸入UserName,Password未輸入
  • 只有輸入Password,UserName未輸入

https://ithelp.ithome.com.tw/upload/images/20220917/201444692L6eV83Ck6.png

TextInputLayout的ID.error方法設定自定義的提示訊息,清除error提示訊息時直接設定null

// Set error text
passwordInputText.error = getString(R.string.error)

// Clear error text
passwordInputText.error = null

doOnTextChange 動態偵測用戶輸入關閉error 警示提示,這個方法點進去看源碼實作其實是封裝TextWatcher的實作方法。

// Get input text
val passwordEditText = binding?.passwordEditText

passwordEditText.editText?.doOnTextChanged { text, _, _, _ ->
    // Respond to input text change
}

完整程式碼

override fun onResume() {
        super.onResume()

        usernameEditText?.doOnTextChanged { _, _, _, count ->
            if (count > 0) {
                usernameInputText?.error = null
            }
        }

        passwordEditText?.doOnTextChanged { text, _, _, _ ->
            if ( text != null && text.length >= 8) {
                passwordInputText?.error = null
            }
        }
}

private fun inputTextValid() {
    val btnLogin = binding?.btnLogin
    val usernameEditText = binding?.usernameEditText
    val usernameInputText = binding?.usernameInputText
    val passwordEditText = binding?.passwordEditText
    val passwordInputText = binding?.passwordInputText

    btnLogin?.setOnClickListener {
           if (isUserNameValid(usernameEditText?.text) && isPasswordValid(passwordEditText?.text)) {
                // 都有輸入
                usernameInputText?.error = null
                passwordInputText?.error = null
                (activity as NavigationHost).navigateTo(ProductGridFragment(), false)
            } else {
                when {
                    isUserNameValid(usernameEditText?.text) -> {
                        // 只有輸入UserName
                        passwordInputText?.error = "請輸入長度8碼的密碼"
                    }
                    isPasswordValid(passwordEditText?.text) -> {
                        // 只有輸入Password
                        usernameInputText?.error = "請輸入最少一個字的名字"
                    }
                    else -> {
                        // 都沒有輸入
                        usernameInputText?.error = "請輸入最少一個字的名字"
                        passwordInputText?.error = "請輸入長度8碼的密碼"
                    }
                }
            }
        }
}

// 判斷Password欄位是否有輸入指定長度
private fun isPasswordValid(text: Editable?): Boolean {
    return text != null && text.length >= 8
}

// 判斷UserNamex欄位文字不為null
private fun isUserNameValid(text: Editable?): Boolean {
    return text != null && text.isNotEmpty()
}

Add a trailing icon 設定顯示密碼、屏蔽密碼的邏輯

https://ithelp.ithome.com.tw/upload/images/20220917/20144469XBRuRNsW7C.png

官方預設的顯示密碼、屏蔽密碼的icon切換很奇怪,所以稍微自己改一下,讓眼睛打開時密碼明文,閉起來時密碼密文顯示
https://ithelp.ithome.com.tw/upload/images/20220918/20144469jvbIRe2rUY.png

1. 先新增自定義的閉眼icon** app:endIconDrawable="自定義"

    <com.google.android.material.textfield.TextInputLayout
        ...
                app:endIconMode="password_toggle"
                app:endIconDrawable="@mipmap/icon_closed_eye"
        ...>

            <com.google.android.material.textfield.TextInputEditText
               ...
                android:inputType="textPassword" />

        </com.google.android.material.textfield.TextInputLayout> 

2.自定義icon點擊判斷密碼明文和密文的切換,以下說明

  • 是否預設顯示密碼明文:預設是密文輸入,如需求是密碼欄位指定要明文輸入時可指定InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD,和指定顯示密碼的icon 的設定passwordInputText?.endIconDrawable
  • 點擊icon判斷密碼明文和密文的切換: 透過setEndIconOnClickListener監聽點擊icon檢查PasswordTransformationMethod方法是否密文轉換,後續判斷getTransformation方法回傳明文為null張開眼睛icon,密文時閉眼icon,passwordInputText設定endIconDrawable讓icon進行切換。
  private fun passwordEyeVisibility() {

//        // 預設顯示密碼明文
//        passwordEditText?.inputType = InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
//        passwordInputText?.endIconDrawable = context?.let { AppCompatResources.getDrawable(it, R.drawable.ic_open_eye) }

        passwordInputText?.setEndIconOnClickListener {

            // 檢查是否有 PasswordTransformationMethod 密碼轉換方法
            val hasPasswordTransformation = passwordEditText?.transformationMethod is PasswordTransformationMethod
            if (hasPasswordTransformation) {
                passwordEditText?.transformationMethod = null
            } else {
                passwordEditText?.transformationMethod = PasswordTransformationMethod.getInstance()
            }

            // 取得密碼文字來設定字定義 endIconDrawable
            setCustomizeEndIcon(passwordEditText)
        }
}

private fun setCustomizeEndIcon(editText: EditText?) {
		// getTransformation方法回傳明文為null張開眼睛icon,密文時閉眼icon
        val passwordSource = editText?.transformationMethod?.getTransformation(editText.text, editText)
				
        if (passwordSource == null) {
            // 密碼明文
            passwordInputText?.endIconDrawable = context?.let {
                AppCompatResources.getDrawable(it, R.drawable.ic_open_eye)
            }
        } else {
            // 密碼密文
            passwordInputText?.endIconDrawable = context?.let {
                AppCompatResources.getDrawable(it, R.mipmap.icon_closed_eye)
            }
        }
}

這樣就完成一般的登入頁面的檢核邏輯囉~
歡迎下載程式碼

感謝您看到這邊/images/emoticon/emoticon08.gif
明天就來介紹常用的 Icon buttons。

參考資料
官方提供練習和跟著做 MDC-101 Android:Material Components (MDC) Basics (Kotlin)


上一篇
Day03 使用 Text fields
下一篇
Day05 使用M3 Icon button
系列文
Kotlin 實踐 Material Design 懶人包30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言