好的,中秋節連假第二天,大家是吃烤肉吃的不要不要的阿? 那我們今天主要要做的就是關於登入頁面。今天會用到的就是透過Navigation來導航Fragment之間的移動。以及透過Firebase來登入帳號啦!
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
implementation "android.arch.navigation:navigation-fragment-ktx:1.0.0"
由於Fragment是依賴在Activity上面,而我們的架構上是分AccountActivity跟主要的Activity,所以我們的Navigation也會有兩個,一個是負責AccountActivity內Fragment的跳轉,另一個則負責主要活動的Fragment跳轉。
我們可以把Activity當作一個容器,然後上面會有許多Fragment來顯示,而在預設,Fragment是透過replace來替換的,如果想要用 show或是hide,就要自定義。
我們可以參考這篇文章:https://ithelp.ithome.com.tw/articles/10226275
好的,那接下來我們要到activity_account.xml 的 ConstraintLayout裡面,新增以下
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="start"
app:defaultNavHost="true"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/account_nav" />
★ 我們把這個NavHost用來展示destanation的容器,可以把它想像成最上層,fragment都會是在這個上面來展示,通常會加上app:defaultNaHost="true" 以攔截系統的返回事件。
★app:navGraph="@navigation/account_nav":這個則丟入剛創立好的nav檔案。
既然我們都已經知道Activity有可能會有許多Fragment,並我們的AccountActivity也會有LoginFragment/RegisterFragment/ForgotAccountFragment這三個Fragment,那要怎麼確定彼此之間的關係以及最開始的起始Fragment呢?! 這邊就是Navigation最厲害的地方啦!!
Navigation提供了一個很直觀的畫面,可以讓我們直接透過連連看來設定彼此的跳轉關係。
我們直接去剛剛創立好的 account_nav,並且毫不猶豫地點了Design,然後點選紅色圖示中紅色的標示,這邊會顯示你剛剛新增好的LoginFragment,直接點下去,就會自動幫你新增啦!
當你新增完後,你可以點旁邊的code,來確定是否LoginFragment是此Navigation的起始Fragment。
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/account_nav"
app:startDestination="@id/loginFragment">
<fragment
android:id="@+id/loginFragment"
android:name="com.example.petsmatchingapp.ui.fragment.LoginFragment"
android:label="LoginFragment" />
</navigation>
★我們注意到, app:startDestination 後面目前是loginFragment, 這也很合理,因為目前你只有一個Fragment。同理,之後你若有其他Fragment,只要想要設定該Fragment為初始Fragment,就把這一行的Fragement改成想要起始的即可。
我們總共會有幾個過程
3-1.建立 dimen/string/color ,方便之後的素材
3-2.創立layout
3-3.設計logo
<dimen name="login_banner_height">200dp</dimen>
<dimen name="login_logo_width">120dp</dimen>
<dimen name="login_logo_height">120dp</dimen>
<dimen name="login_logo_chinese_textSize">22sp</dimen>
<dimen name="login_logo_chinese_marginTop">50dp</dimen>
<dimen name="edText_padding">16dp</dimen>
<dimen name="tip_margin_start_end">16dp</dimen>
<dimen name="edText_textSize">16sp</dimen>
<dimen name="hint_word_textSize">14sp</dimen>
<dimen name="tip_margin_top_bottom">35dp</dimen>
<string name="login">登入</string>
<string name="hint_enter_your_email">請輸入信箱</string>
<string name="hint_enter_your_password">請輸入密碼</string>
<string name="login_forgot_account">忘記密碼</string>
<string name="login_don_have_account">還沒有帳號嗎?</string>
<string name="register">註冊</string>
<string name="msg_login_successful">登入成功</string>
<color name="light_pewter_blue">#8ac0b5</color>
<color name="pewter_blue">#56c0b2</color>
一樣是要按照我們之前客製字型方式,我們要先製作Edtext跟button這兩個class
class JFButton(context: Context, attributeSet: AttributeSet): AppCompatButton(context, attributeSet) {
init{
applyFont()
}
private fun applyFont(){
val typeface: Typeface = Typeface.createFromAsset(context.assets,"jfopenhuninn.ttf")
setTypeface(typeface)
}
}
class JFEditText(context: Context, attrs: AttributeSet): AppCompatEditText(context, attrs) {
init{
applyFont()
}
private fun applyFont(){
val typeface: Typeface = Typeface.createFromAsset(context.assets,"jfopenhuninn.ttf")
setTypeface(typeface)
}
}
LoginFragment長這樣
layout就直接貼code,不講解囉!
主要會用到客製化的view,以及客製化button的background,跟設計logo
<?xml version="1.0" encoding="utf-8"?>
<layout
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">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/login"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.fragment.LoginFragment">
<FrameLayout
android:id="@+id/fl_login_fragment"
android:layout_width="match_parent"
android:layout_height="@dimen/login_banner_height"
app:layout_constraintTop_toTopOf="parent"
android:background="@color/light_pewter_blue">
<ImageView
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_gravity="center"
android:src="@drawable/app_logo">
</ImageView>
</FrameLayout>
<com.example.petsmatchingapp.utils.JFTextView
android:id="@+id/tv_login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/login_logo_chinese_textSize"
android:text="@string/login"
app:layout_constraintTop_toBottomOf="@id/fl_login_fragment"
android:gravity="center"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="@dimen/login_logo_chinese_marginTop"
android:textStyle="bold"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tip_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:hint="@string/hint_enter_your_email"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_login"
android:layout_marginTop="@dimen/tip_margin_top_bottom"
android:layout_marginStart="@dimen/tip_margin_start_end"
android:layout_marginEnd="@dimen/tip_margin_start_end">
<com.example.petsmatchingapp.utils.JFEditText
android:id="@+id/ed_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textEmailAddress"
android:padding="@dimen/edText_padding"
android:textSize="@dimen/edText_textSize"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tip_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:hint="@string/hint_enter_your_password"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/tip_email"
android:layout_marginTop="@dimen/tip_margin_top_bottom"
android:layout_marginStart="@dimen/tip_margin_start_end"
android:layout_marginEnd="@dimen/tip_margin_start_end">
<com.example.petsmatchingapp.utils.JFEditText
android:id="@+id/ed_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberPassword"
android:padding="@dimen/edText_padding"
android:textSize="@dimen/edText_textSize"/>
</com.google.android.material.textfield.TextInputLayout>
<com.example.petsmatchingapp.utils.JFTextView
android:id="@+id/tv_forgot_password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="@dimen/tip_margin_start_end"
android:layout_marginRight="@dimen/tip_margin_start_end"
android:textSize="@dimen/hint_word_textSize"
android:text="@string/login_forgot_account"
app:layout_constraintTop_toBottomOf="@id/tip_password"/>
<com.example.petsmatchingapp.utils.JFButton
android:id="@+id/btn_login"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/tv_forgot_password"
android:layout_marginStart="@dimen/tip_margin_start_end"
android:layout_marginEnd="@dimen/tip_margin_start_end"
android:foreground="?attr/selectableItemBackground"
android:text="@string/login"
android:textColor="@color/white"
android:background="@drawable/button_background"
android:layout_marginTop="30dp"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/btn_login"
android:orientation="horizontal"
android:gravity="center"
android:layout_marginTop="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<com.example.petsmatchingapp.utils.JFTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/login_don_have_account"
android:textSize="@dimen/hint_word_textSize"/>
<com.example.petsmatchingapp.utils.JFTextView
android:id="@+id/tv_register"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/register"
android:textStyle="bold"
android:textSize="@dimen/hint_word_textSize"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
我們首先要去 drawable,新建一個 new resource file,在 Root element原本是 selector,我們改成 shape,並命名為- button_background,若是選selector,則可以設定select 控建跟沒按下的樣式。但因為button它自己就有 selectableItemBackground,就是點擊後波紋效果了,所以我們就單純用 shape即可。
button_background 的內容如下
<shape
xmlns:android="http://schemas.android.com/apk/res/android">
//漸層
<gradient
//開始顏色
android:startColor="@color/light_pewter_blue"
//結束顏色
android:endColor="@color/pewter_blue"
//從哪個方向開始漸層,倍數是45開始,上下左右
android:angle="360"
//渲染的type
android:type="linear">
</gradient>
//角度
<corners
android:radius="10dp">
</corners>
</shape>
新增完後,我們直接去想要有這樣效果的 view,把這個 drawable指定到該background即可。
大家可以到這個網頁製作自己喜歡的LOGO https://www.designevo.com/tw/
接下來我們要先搞登入畫面,那就要先回去 Firebase的平台,打開專案後,到左邊的Authentication→sign in method,這邊會有許多登入方法,如第三方登入等.. 我們這次專案先把第一個電子郵件/密碼的選項開啟。
我們首先要拿到edText的資料,並且驗證是否為Empty
//如果回傳 ture,代表格式正確,false則格是錯誤。
private fun validDataForm(): Boolean{
return when{
//check email欄位是否為empty,若為空則用透過snackBar跳出訊息。
TextUtils.isEmpty(binding.edEmail.text.toString().trim())->{
showSnackBar(resources.getString(R.string.hint_enter_your_email),true)
return false
}
//check passwrod欄位是否為empty,若為空則用透過snackBar跳出訊息。
TextUtils.isEmpty(binding.edPassword.text.toString().trim())->{
showSnackBar(resources.getString(R.string.hint_enter_your_password),true)
return false
}
else -> true
}
}
★這邊要確定我們的Fragment是有繼承BaseFragment,我們可以使用snackBar呦!
接下來我們要繼續繼承 View.OnClickListener,直接在LoginFragment後面新增
就會發現它跑出錯誤,因為我們需要在 override onClick這個 funtion。
class LoginFragment : BaseFragment(),View.OnClickListener {
override fun onClick(v: View?) {
TODO("Not yet implemented")
}
}
我們直接在 onClick裡面新增
override fun onClick(v: View?) {
//我們用 when來判斷當今天使用者點擊哪個view,後會有什麼回饋。
when(v){
binding.btnLogin ->{
}
binding.tvForgotPassword ->{
}
binding.tvRegister ->{
}
}
}
你以為onClick結束了嗎? 你還需要在 onCreateView新增以下,它才算是完成
binding.btnLogin.setOnClickListener(this)
binding.tvForgotPassword.setOnClickListener(this)
binding.tvRegister.setOnClickListener(this)
接下來我們需要把當今天 validDataForm() 回傳為 true的時候,把資料註冊到Firebase Auth。而我們Fragment只負責UI,而所以我們要把跟Firebase連接的部分放在 AccountViewModel。
我們先去AccountViewModel,新增以下的funtoin。
//參數傳入 loginFragment,以及 edText拿到的 email跟 password
fun loginWithEmailAndPassword(fragment: LoginFragment, email: String, paw: String){
//透過 FirebaseAuth.getInstance()去拿到實例後,就可以直接點出sign的 funtion了。
FirebaseAuth.getInstance().signInWithEmailAndPassword(email,paw)
//新增 SuccessListener
.addOnSuccessListener{
fragment.loginSuccessful()
}
新增 FailureListener
.addOnFailureListener{
Timber.d("Error while logging cause $it")
fragment.loginFail(it.toString())
}
}
因為我們需要當sign in 成功後,需要跳轉到主要Activity,以及當sign in 失敗後,需要顯示錯誤內容,而UI部分都是在 Fragment,所以我們等等會在 LoginFragment新增成功/失敗的 Funtion,並且在 viewModel的loginWithEmailAndPassword funtion,的參數傳入loginFragment,讓我們可以呼叫loginFragment的成功/失敗 funtion。
接下來要回去LoginFragment,新增成功/失敗的 funtion
//AccountViewModel的 sign in funtion 成功後會呼叫以下的funtion。
fun loginSuccessful(){
hideDialog()
showSnackBar(resources.getString(R.string.msg_login_successful),false)
requireActivity().startActivity(Intent(requireActivity(), MatchingActivity::class.java))
requireActivity().finish()
}
//AccountViewModel的 sign in funtion 失敗後會呼叫以下的 funtion
fun loginFail(message: String){
hideDialog()
showSnackBar(message, true)
}
★別忘了要新增一個 MatchingActivity,讓我們當今天sign in 成功後可以透過 Intent來跳轉到該 Actvitiy。
好的,但是我們的 AccountViewModel的 loginWithEmailAndPassword() 還沒被呼叫到,我們該怎麼呼叫它呢?
歡迎回去昨天的文章看看關於 Koin文章!
如果要呼叫viewModel的話,我們直接在 class 下面新增以下,就可以呼叫囉!
private val accountViewModel: AccountViewModel by sharedViewModel()
接下來我們要在 onClick裡面設定,當今天使用者點擊 login 的按鈕時,應該要進到viewModel的funtion。
//VlidDataForm 回傳 true時,代表資料格式正確。
if(validDataForm()){
//show出 progressBar
showDialog(resources.getString(R.string.please_wait))
val email = binding.edEmail.text.toString().trim()
val paw = binding.edPassword.text.toString().trim()
//進到剛剛建立好的 accountViewModel login funtion
accountViewModel.loginWithEmailAndPassword(this,email,paw)
}
由於在login畫面的時候,我們不應該讓user看到 status欄位,為了提供user更安心,不會被打擾的畫面,所以我們一樣在onCreateView新增以下的code。
由於在login畫面的時候,我們不應該讓user看到 status欄位,為了提供user更安心,不會被打擾的畫面,所以我們一樣在onCreateView新增以下的code。
★這邊要注意,因為我們在這邊是Fragment,所以我們呼叫window的時候前面要先呼叫 requireActivity()。
並且醜醜的ActionBar我們不要,於是我們到 manifest裡面修改 AccountActivity的設定
<activity android:name=".ui.activity.AccountActivity"
//設定 NoActionBar
android:theme="@style/Theme.MaterialComponents.Light.NoActionBar"
//設定畫面只能垂直,不要讓人家水平
android:screenOrientation="portrait"/>
★注意因為我們這邊的theme,因為我們在login_fragment的layout裡面有用到Material的 TextInputLayout,所以我們只能選擇有 Material的theme。
好啦 完成啦!!
接下來明天會是把註冊/忘記密碼頁面完成!!