iT邦幫忙

2021 iThome 鐵人賽

DAY 7
0
Mobile Development

30天建立寵物約散App-Android新手篇系列 第 7

【Day7】BottomNavigation X ProfileFragment

不知道今年大家中秋節有沒有見到許久沒見到的親戚朋友呢!? 筆者發現年齡越長,大家都會越來越有自己的生活圈,但是,儘管不是很常見面/聯絡~ 但是偶爾一見面,又可以聊的沒有隔閡,我特別喜歡這種氛圍,也希望明年中秋節,大概可以更無憂無慮的一起烤肉,喝喝小米酒。 那麼,抒發情感的話不多說,今天我們主要是要來做下方導航的BottomNavigation,以及讓我們顯示我們目前的登入帳號資訊的ProfileFragment。

一、BottomNavigation 底部導航

先上圖! BottomNavigation長以下

https://ithelp.ithome.com.tw/upload/images/20210922/20138017O4QdGCrth2.jpg

1.直接新增有 Bottom Navigation 的 Activity

有很多時候,我們在新增的時候可以看一下我們需要新增什麼樣的Activity,就可以省下許多時間呦!

★ 注意,因為之前的文章那時候建立 MatchingActivity的時候,是直接建立空白的,所以我們要先把之前的MatchingActivity刪掉

https://ithelp.ithome.com.tw/upload/images/20210922/20138017jx5d9nweaM.png

建立好完,會有

  • 三個Fragment + layout
  • 與三個Fragment先應的viewModel
  • 一個Activity + layout
  • 一個mobile_navigation.xml檔
  • 一個menu

建立好後,如果依照我們之前在LoginFragment的邏輯,當登入成功後,就會到MatchingActivity,你如果登入帳號後,就可以轉到有BottomNavigation的Activity。

BottomNav初始動畫

那接下來我們會有幾個步驟

1.1.把Fragment的viewModel刪掉

因為我們會用sharedViewModel,讓整個MatchingActivity都可以share一樣的資料,所以就不單用某Fragment的viewModel了

★刪掉後也要記得要把Fragment有用到viewModel的一併刪掉喔!

我們先看到MatchingActivity裡面的內容會是

//我們bottomNavigation的view
val navView: BottomNavigationView = findViewById(R.id.nav_view)

//navController,讓我們控制Fragment之間的跳轉
val navController =findNavController(R.id.nav_host_fragment)

//navigationUI使用AppBarConfiguration來控制導航按鈕,而因為BottomNavigation的Fragment彼此沒有層級關係,所以我們這邊傳入id。
val appBarConfiguration =AppBarConfiguration(
setOf(
R.id.navigation_home, R.id.navigation_dashboard, R.id.navigation_notifications
    )
)
setupActionBarWithNavController(navController, appBarConfiguration)
navView.setupWithNavController(navController)

★ AppBarConfiguration裡面的 setOf順序,不會影響bottomNavigation的排序,如果需要更改的話,就要去menu來修改item的排序。

1.2 調整Fragment的位置

新增並替換bottomNavigation的icon
直接去drawable→new→vector asset → clip art,
搜尋 pets跟message跟一個白色的dashboard給我們的bottom Navigation。

修改bottomNavigation的順序+替換icon

因為我們希望使用者滑到最左邊時,可以看到其他使用者新增的資料,會比較符合使用情境?? 吧! 我想..

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

								
    <item
		//id是我們剛剛在activity指定的id
        android:id="@+id/navigation_dashboard"
		//更換我們的icon
        android:icon="@drawable/ic_baseline_dashboard_24"
	    //title是顯示在左上方的ActionBar顯示的title  
		android:title="@string/title_dashboard" />

    <item
        android:id="@+id/navigation_home"
        android:icon="@drawable/ic_baseline_pets_24"
        android:title="@string/title_home" />

    <item
        android:id="@+id/navigation_notifications"
        android:icon="@drawable/ic_baseline_message_24"
        android:title="@string/title_notifications" />

</menu>

2.客製化bottomNavigation的view

看似一切都美好,都可以正常切換頁面後,我們發現! 預設的藍色非常不符合我們的App的主色調,所以我們要進到 activity_matching.xml來修改

我們需要修改的有,背景+字顏色+icon顏色
所以我們要先去新增 drawable,就跟我們之前做button的background一樣!
我們就做個漸層就好,而有些人可能會疑惑,這怎麼感覺跟之前button的內容很像,那就直接用它就好啦? 其實不然,因為button的我們有加上圓角 ><

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">

    <gradient
        android:startColor="@color/light_pewter_blue"
        android:endColor="@color/pewter_blue"
        android:angle="360"
        android:type="linear">
    </gradient>

</shape>

再來回到activity的xml的BottomNavigation的View裡面,我們新增以下

//修改後面的background樣式
android:background="@drawable/bottom_navigation_background"
//修改字的顏色
app:itemTextColor="@color/white"
//修改icon的顏色
app:itemIconTint="@color/white"

3.修改ActionBar

接下來我們要把ActionBar的樣式更改成跟我們AccountActivity一樣的顏色!
我們直接去 values→themes 修改colorPrimary的顏色

<item name="colorPrimary">@color/light_pewter_blue</item>

以下是一其他比較常用的theme name對應圖,可以適時新增/修改主題,讓我們節省時間

https://ithelp.ithome.com.tw/upload/images/20210922/20138017Q9HsazFaHP.png
圖片來源(https://blog.csdn.net/liu1164316159/article/details/52163772)

既然上面都有修改statusBar的顏色了,那我們就直接來修改看看吧!
首先在color新增以下顏色

<color name="status_color">	#007979</color>

再來theme指定成上面的顏色

<item name="android:statusBarColor" tools:targetApi="l">@color/status_color</item>

這樣就完成啦!!

二、ProfileFragment

我們在註冊的時候,我們是把資料放在Firestore,但是有沒有眼尖的小夥伴發現,我們的User裡面,還有user的image這一欄位!? 那我們要在什麼時候新增呢? 設計理念是覺得,盡量減少user在註冊時所消耗的時間,所以我們會在使用者登入後,另外有一個介面可以讓他更新

1.我們要新增一個Fragment,名叫ProfileFragment

2.依照慣例,layout直接貼

dimen

    <dimen name="profile_image_width_height">150dp</dimen>
    <dimen name="profile_image_margin_top">125dp</dimen>
    <dimen name="profile_textSize">16sp</dimen>
    <dimen name="profile_text_margin_top">10dp</dimen>
    <dimen name="button_margin_top_bottom">25dp</dimen>

string

	<string name="toolbar_title_profile">個人頁面</string>
    <string name="edit">修改</string>
    <string name="logout">登出</string>
<?xml version="1.0" encoding="utf-8"?>

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto">





    <androidx.constraintlayout.widget.ConstraintLayout

        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ui.fragment.ProfileFragment">


        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="@dimen/login_banner_height"
            android:background="@color/light_pewter_blue"
            app:layout_constraintTop_toTopOf="parent">

            <androidx.appcompat.widget.Toolbar
                android:id="@+id/toolbar_profile_fragment"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize">

                <com.example.petsmatchingapp.utils.JFTextView
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:textColor="@color/white"
                    android:gravity="center"
                    android:text="@string/toolbar_title_profile"
                    android:textStyle="bold"
                    android:textSize="@dimen/toolbar_textSize">

                </com.example.petsmatchingapp.utils.JFTextView>

            </androidx.appcompat.widget.Toolbar>


        </FrameLayout>

        <FrameLayout
            android:id="@+id/fl_profile_fragment_people_image"
            android:layout_width="@dimen/profile_image_width_height"
            android:layout_height="@dimen/profile_image_width_height"
            android:layout_marginTop="@dimen/profile_image_margin_top"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent">



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

        </FrameLayout>

        <com.example.petsmatchingapp.utils.JFTextView
            android:id="@+id/tv_profile_fragment_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toBottomOf="@id/fl_profile_fragment_people_image"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginTop="20dp"
            android:textStyle="bold"
            android:textSize="16sp"
            tools:text = "王大明"/>

        <com.example.petsmatchingapp.utils.JFTextView
            android:id="@+id/tv_profile_fragment_email"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginTop="@dimen/profile_text_margin_top"
            android:textSize="@dimen/profile_textSize"
            android:textStyle="bold"
            app:layout_constraintTop_toBottomOf="@id/tv_profile_fragment_name"
            tools:text = "sp9rt77w@gmail.com"/>






        <com.example.petsmatchingapp.utils.JFButton
            android:id="@+id/btn_profile_fragment_go_edit"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tv_profile_fragment_email"
            android:layout_marginTop="@dimen/button_margin_top_bottom"
            android:layout_marginStart="@dimen/tip_margin_start_end"
            android:layout_marginEnd="@dimen/tip_margin_start_end"
            android:background="@drawable/button_background"
            android:foreground="?attr/selectableItemBackground"
            android:textColor="@color/white"
            android:text="@string/edit"/>

        <com.example.petsmatchingapp.utils.JFButton
            android:id="@+id/btn_profile_fragment_signout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toBottomOf="@id/btn_profile_fragment_go_edit"
            android:layout_marginTop="@dimen/button_margin_top_bottom"
            android:layout_marginStart="@dimen/tip_margin_start_end"
            android:layout_marginEnd="@dimen/tip_margin_start_end"
            android:background="@drawable/button_background"
            android:foreground="?attr/selectableItemBackground"
            android:textColor="@color/white"
            android:text="@string/logout"/>




    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

3.HomeFragment導航到ProfileFragment

我們要先去 mobile_navigation.xml
我們希望一開始開啟MatchingActivity的時候出現的是dashboard的Fragment,所以我們要先把把
startDestination 修改成以下

app:startDestination="@+id/navigation_dashboard"

新增剛剛建立好的ProfileFragment,並且把HomeFragment連到ProfileFragment

再過來新增menu,並命名為 home_menu,icon的部分一樣透過新增vector asset,搜尋 person

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
        android:id="@+id/navigation_profile"
        android:icon="@drawable/ic_baseline_person_24"
        app:showAsAction="always"
        android:title="@string/navigation_profile">
    </item>

</menu>

跑回HomeFragment,一樣先設定databinding,這邊就不贅述了

我們先在onCreateView新增

//指定Fragment願意新增item到選單
setHasOptionsMenu(true)

再來override以下兩個funtion

override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
//把剛剛我們創建好的menu,傳進去
inflater.inflate(R.menu.home_menu,menu)
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
    when(item.itemId){
			//這邊的id是我們剛剛在home_menu新增的item
      R.id.navigation_profile -> {
				//NavController也要記得宣告喔
        nav.navigate(R.id.action_navigation_home_to_profileFragment)
      }
    }

    return super.onOptionsItemSelected(item)
  }

這樣他就會在在HomeFragment右上角出現剛剛設定的profileIcon,且可以點擊
https://ithelp.ithome.com.tw/upload/images/20210922/20138017uBC4TurDUW.png

4.隱藏ActionBar跟BottomNavigation

但是!! 我們卻發現! 他點進去後竟然是這副德性

https://ithelp.ithome.com.tw/upload/images/20210922/2013801712hEZyo2Hj.png

他保留了ActionBar跟BottomNavigation的View,這是因為這兩個都是跟著Activity的,所以我們要把他們隱藏掉!

我們在ProfileFragment新增以下的funtion,並在 onCreateView呼叫它

private fun dismissActivityActionBarAndBottomNavigationView(){
        val activityInstance = this.activity as MatchingActivity
        activityInstance.supportActionBar?.hide()
        activityInstance.findViewById<BottomNavigationView>(R.id.nav_view).visibility = View.GONE

    }

並且到activity_matching.xml的 ConstranLayout裡面把預設的 Padding刪掉

//把這個刪掉
android:paddingTop="?attr/actionBarSize"

5.新增返回鍵

binding.toolbarProfileFragment.setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
        binding.toolbarProfileFragment.setNavigationOnClickListener { 
            requireActivity().onBackPressed()
        }

6.顯示資料

好了!! 接下來是最重要的,我們要把資料從Firestore的資料叫出來,並且存到livedata,好讓我們可以觀察到有變化
好的,那我們首先要回到AccoutViewModel 新增以下兩個funtion

6.1.拿到現在的userId

fun getCurrentUID(): String?{
        return FirebaseAuth.getInstance().currentUser?.uid
    }

6.2.拿到我們Firestore的資料

fun getUserDetail(){
	//先確認我們的getCurrentUID是否為null
    getCurrentUID()?.let {
    FirebaseFirestore.getInstance().collection(Constant.USER)
        .document(it)
        .get()
        .addOnSuccessListener {
//把得到的result轉成User的object並放到livedata,讓我們可以觀察
   _userDetail.postValue(it.toObject(User::class.java))
           }
        .addOnFailureListener {
//這邊我是用Timber,功能類似Log,但是更加簡潔,有興趣可以查關鍵字
                    Timber.d("Error while getUserDetail cause$it")
                }
        }
    }

這時候會發現我們的 addOnSuccessListener裡面會是紅字,沒關係我們來解決它

6.3一樣在AccountViewModel新增以下

LiveData,可以讓我們儲存數據,也可以讓我們在UI層觀察變化,有興趣的人可以查關鍵字,這邊就不多說了

//這邊我們設成private,目的是不要讓UI層可以修改我們的數據
private val _userDetail = MutableLiveData<User>()

//這邊就是可以讓UI層觀察到的數據,其數據是跟_userDetail同步的
val userDetail: LiveData<User>
get() = _userDetail

接下來一樣在AccountViewModel新增,讓我們初始化時,就呼叫getUserDetail的funtion

init{
getUserDetail()
}

6.4.來觀測數據吧!

回到ProfileFragment
一樣要在class下面呼叫 accountViewModel喔

private val accountViewModel: AccountViewModel by sharedViewModel()
accountViewModel.userDetail.observe( viewLifecycleOwner, Observer {
          
						Constant.loadUserImage(it.image,binding.ivProfileFragmentImage)
            binding.tvProfileFragmentName.setText(it.name)
            binding.tvProfileFragmentEmail.setText(it.email)
            
        })

因為我們會有使用者的圖片,目前還沒有新增,但是之後會教大家怎麼把圖片存到雲端。所以我們需要使用第三方套件 Glide,讓我們可以load網路上的照片!

首先我們要implementation以下

    //Glide
    implementation "com.github.bumptech.glide:glide:$glide_version"
	//這讓我們可以新增圓角等等...
    implementation 'jp.wasabeef:glide-transformations:3.0.1'

並在 project層級新增版本

ext.glide_version = "4.12.0"

接下來就去 Constant來新增 Glide的funtion

這樣可以讓我們以後在別的Fragment要load圖片時,不用再寫重複的Code

//我們只要兩個參數,一個是你要顯示的圖片url跟你要顯示的ImageView
fun loadUserImage(url: Any, v:ImageView){
		//圓角
        val mRequestOptions = RequestOptions.circleCropTransform()
				
        Glide.with(v)
			//傳入你要顯示的圖片url
            .load(url)
			//放入圓角
            .apply(mRequestOptions)
			//顧名思義,當今天還沒load到照片時,要先顯示啥照片,這邊大家可以自己抓網路上的圖片
            .placeholder(R.drawable.placeholder)
			//要顯示在哪個ImageView
            .into(v)
    }

我們可以簡單試試看把一些圖片先直接丟進去,成功如下

https://ithelp.ithome.com.tw/upload/images/20210922/20138017PwY3pzBN8S.png

先說,圖片中的不是本人,也不是楊冪(ㄇㄧˋ),知道是誰的夥伴們,可以在留言欄留言,我會隨機按讚!!

最後在新增 logout的功能就結束啦!!

7.新增logout

先到AccountViewModel新增以下,簡單明瞭不解釋

fun signOut(){
        FirebaseAuth.getInstance().signOut()
    }

並新增 onClickListener,一樣先繼承 View.OnClickListener

override fun onClick(v: View?) {
        when(v){

            binding.btnProfileFragmentSignout ->{
				//登出
                accountViewModel.signOut()
				//轉到AccountActivity
      startActivity(Intent(requireActivity(),AccountActivity::class.java))
                //關掉當前的Activity
				requireActivity().finish()
            }
        }
    }

並且別忘了在 onCreateView新增

binding.btnProfileFragmentSignout.setOnClickListener(this)

看起來一切都美好,但是... 這邊出現問題啦,當我們左上角的返回鍵後!!

https://ithelp.ithome.com.tw/upload/images/20210922/20138017ksya3G8hjp.png

原來是我們剛剛把這兩個都關掉啦!
這邊就新增以下funtion,並在onCreateView呼叫就好!
看起來是有點笨的作法! 有知道其他方法的夥伴們,可以告訴我 plz

private fun showActionBarAndBottomNavigation(){

    if (requireActivity().findViewById<BottomNavigationView>(R.id.nav_view).visibility == View.GONE){
      requireActivity().findViewById<BottomNavigationView>(R.id.nav_view).visibility = View.VISIBLE
    }

    val activityInstance = this.activity as MatchingActivity
    activityInstance.supportActionBar?.show()
  }

大功告成!!
成功如下!!

day7.完成品

明天會做新增註冊資料的頁面!! 請大家期待啦 へけっ


上一篇
【Day6】重設密碼頁面X Firebase Auth
下一篇
【Day8】EditProfileFragment X Storage上傳照片
系列文
30天建立寵物約散App-Android新手篇30

尚未有邦友留言

立即登入留言