在昨天的Profile頁面中,我們可以看到有照片的訊息,那我們今天主要要來做的就是~ 把手機相簿裡面的照片傳上去Firebase的 Storage,並且要轉換成下次可以download的格式!
長成這樣!
繼承BaseFragment,順便把databinding設定好
dimen
<dimen name="edit_image_margin_top">50dp</dimen>
新增 string
<string name="hint_enter_your_area">請輸入您的居住區域</string>
<string name="toolbar_title_edit_profile">修改資料</string>
<string name="man">男性</string>
<string name="female">女性</string>
<string name="edit_user_detail_successful">修改資料完成!</string>
<string name="update_user_profile_successful">上傳圖片成功!</string>
<string name="hint_select_your_image">請選取照片!</string>
layout一樣直接貼
<?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.EditProfileFragment">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar_edit_profile_fragment"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:background="@color/light_pewter_blue">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@string/toolbar_title_edit_profile"
android:textSize="@dimen/toolbar_textSize"
android:gravity="center"
android:textStyle="bold"
android:textColor="@color/white">
</TextView>
</androidx.appcompat.widget.Toolbar>
<ImageView
android:id="@+id/iv_edit_profile_image"
android:layout_width="@dimen/user_image_width_height"
android:layout_height="@dimen/user_image_width_height"
app:layout_constraintTop_toBottomOf="@id/toolbar_edit_profile_fragment"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="@dimen/edit_image_margin_top">
</ImageView>
<ImageView
android:id="@+id/iv_edit_profile_camera"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_baseline_photo_camera_24"
app:layout_constraintBottom_toBottomOf="@id/iv_edit_profile_image"
app:layout_constraintEnd_toEndOf="@id/iv_edit_profile_image">
</ImageView>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tip_edit_profile_fragment_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginStart="@dimen/tip_margin_start_end"
android:layout_marginEnd="@dimen/tip_margin_start_end"
android:layout_marginTop="@dimen/tip_margin_top_bottom"
app:layout_constraintTop_toBottomOf="@id/iv_edit_profile_camera"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.example.petsmatchingapp.utils.JFEditText
android:id="@+id/ed_edit_profile_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/edText_padding"
android:textSize="@dimen/edText_textSize"
android:hint="@string/hint_enter_your_name"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tip_edit_fragment_area"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginStart="@dimen/tip_margin_start_end"
android:layout_marginEnd="@dimen/tip_margin_start_end"
android:layout_marginTop="@dimen/tip_margin_top_bottom"
app:layout_constraintTop_toBottomOf="@id/tip_edit_profile_fragment_name"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.example.petsmatchingapp.utils.JFEditText
android:id="@+id/ed_edit_area"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/edText_padding"
android:textSize="@dimen/edText_textSize"
android:hint="@string/hint_enter_your_area">
</com.example.petsmatchingapp.utils.JFEditText>
</com.google.android.material.textfield.TextInputLayout>
<RadioGroup
android:id="@+id/radiogp_edit_profile_fragment"
android:layout_width="match_parent"
android:layout_height="40dp"
app:layout_constraintTop_toBottomOf="@id/tip_edit_fragment_area"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginStart="@dimen/tip_margin_start_end"
android:layout_marginEnd="@dimen/tip_margin_start_end"
android:orientation="horizontal"
android:layout_marginTop="20dp">
<com.example.petsmatchingapp.utils.JFRadioButton
android:id="@+id/rb_man"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:button="@null"
android:text="@string/man"
android:layout_marginEnd="3dp"
android:gravity="center"
android:textColor="@color/white"
android:background="@drawable/radio_button_background">
</com.example.petsmatchingapp.utils.JFRadioButton>
<com.example.petsmatchingapp.utils.JFRadioButton
android:id="@+id/rb_female"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:layout_marginEnd="3dp"
android:button="@null"
android:text="@string/female"
android:gravity="center"
android:textColor="@color/white"
android:background="@drawable/radio_button_background">
</com.example.petsmatchingapp.utils.JFRadioButton>
</RadioGroup>
<com.example.petsmatchingapp.utils.JFButton
android:id="@+id/btn_edit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/tip_margin_start_end"
android:layout_marginEnd="@dimen/tip_margin_start_end"
app:layout_constraintTop_toBottomOf="@id/radiogp_edit_profile_fragment"
android:text="@string/edit"
android:background="@drawable/button_background"
android:foreground="?attr/selectableItemBackground"
android:textColor="@android:color/white"
android:layout_marginTop="20dp">
</com.example.petsmatchingapp.utils.JFButton>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
好的,接下來我們會看到有幾個紅字,沒關係,我們來解決它!
先從最簡單的開始,我們發現我們需要一個相機的圖像,所以我們透過new → vector asset,來新增一個camera的圖像。
再過來,我們有一個RadioButton,首先我們透過之前自定義的方式,來新增我們的JFRadioButton!
可能會有人好奇,為什麼我們已經有了Button,為什麼要新增RadioButton,因為RadioButton是單選選項,我們等等也會新增按下按鈕之後的樣式,好讓我們使用者更能夠知道自己現在是按哪一個按鈕!
class JFRadioButton(context: Context, attributeSet: AttributeSet): AppCompatRadioButton(context,attributeSet) {
init {
applyFont()
}
private fun applyFont() {
val typeface: Typeface = Typeface.createFromAsset(context.assets,"jfopenhuninn.ttf")
setTypeface(typeface)
}
}
好的! 接下來我們還有一個需要解決的就是background的問題
我們需要當今天使用者點下某個Radiobutton之後,能夠有樣式的改變,不然我們不知道自己點了什麼就尷尬了~
所以我們就跟之前做button的background一樣,我們要去 new一個drawable,但是這次我們不用 shape,我們在Root element的時候我們選擇 selector
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
//當按鈕被按得時候
<item android:state_checked="true">
<shape android:shape="rectangle">
//圓形角度
<corners android:radius="10dp"/>
//漸層
<gradient android:startColor="@color/light_pewter_blue"
android:endColor="@color/pewter_blue"/>
//邊框顏色
<stroke android:color="@color/status_color"
//邊框寬度
android:width="1dp">
</stroke>
</shape>
</item>
<item android:state_checked="false">
<shape android:shape="rectangle">
<corners android:radius="10dp"/>
<gradient android:startColor="@color/light_pewter_blue"
android:endColor="@color/pewter_blue"/>
</shape>
</item>
</selector>
如果dataclass裡面的資料,後來發現還需要新增某些欄位呢? 那之前的資料怎麼辦呢? 沒關係,我們透過update的功能,就可以不用讓user重新辦帳號了!
直接到 User,並且新增一個欄位,並給他初始值
data class User(
val id: String = "",
val name: String = "",
val email: String = "",
val password: String = "",
val image: String = "",
val gender: String = "",
//新增這行
val area: String = "",
val profileCompleted: Boolean = false
)
跟昨天的文章一樣,我們透過從 Firestore拿到的資料,並且存在livedata,我們去觀察這個livedata是否有資料,如果有的話,就顯示在EditText欄位
首先
private val accountViewModel: AccountViewModel by sharedViewModel()
來 onCreateViwe新增觀察
accountViewModel.userDetail.observe(viewLifecycleOwner, Observer {
//傳入url跟imageView
Constant.loadUserImage(it.image,binding.ivEditProfileImage)
//因為我們初始值是"",所以我們要判定假如它不是初始值才需要顯示
if (it.area != ""){
binding.edEditArea.setText(it.area)
}
binding.edEditProfileName.setText(it.name)
如果userDetail裡面的資料是男生,我們就讓男生的RadioButton亮起來,反之亦然
if (it.gender == Constant.MAN){
binding.rbMan.isChecked = true
}else{
binding.rbFemale.isChecked = true
}
})
發現我們有紅字,所以我們要到 Constant來新增
新增在Constant,可以幫助我們在update資料時,跟拿資料時,不會拼字拼錯。減少一些不必要的錯誤。
const val MAN: String = "man"
const val FEMALE: String = "female"
到 mobile_nav來新增我們剛創好的Fragment,並且連連看從ProfileFragment→EditProfileFragment,及EditProfileFragment→ProfileFragment
再過來回到ProfileFragment,我們需要新增點擊事件! 我們在 onClick裡面新增
binding.btnProfileFragmentGoEdit ->{
findNavController().navigate(R.id.action_profileFragment_to_editProfileFragment)
}
別忘了要在 onCreateView新增
binding.btnProfileFragmentGoEdit.setOnClickListener(this)
好的,那我們接下來就可以看到我們的資料啦,曾先生,以及因為現在area跟image沒有資料,所以我們都是預設值。
接下來我們要用AccountViewModel來update資料,我們一樣到AccountViewModel來新增以下的funtion
//傳入 HashMap,HashMap 可以讓我們根據key來塞入值
fun updateUserDetailToFireStore(mHashMap: HashMap<String,Any>, fragment: EditProfileFragment ){
getCurrentUID()?.let {
FirebaseFirestore.getInstance().collection(Constant.USER)
.document(it)
.update(mHashMap)
.addOnSuccessListener {
fragment.editUserDetailSuccessful()
}
.addOnFailureListener {
fragment.editUserDetailFail(it.toString())
}
}
}
我們發現有幾個紅字,讓我們來解決它,我們回到 EditProfileFragement,並新增以下
fun editUserDetailSuccessful(){
hideDialog()
showSnackBar(resources.getString(R.string.edit_user_detail_successful),false)
//這邊記得要在拿一次UserDetail,不然我們的livedata不會更新,也就不會顯示新的資料
accountViewModel.getUserDetail()
}
fun editUserDetailFail(e: String){
hideDialog()
showSnackBar(e,false)
}
好的! 接下來我們首先要先完成上傳自己相簿的照片
步驟是,首先先把圖片傳到Storage,然後再把它改成改成下載的格式,再把它傳入Firestore
想當然耳,開啟我們相簿是非常隱私的事情,如果我們帥氣的自拍照不需要我們同意就可以讀取,那是多麼恐怖的一件事啊?
邀請權限會是長這樣
我們先去 manifest的新增以下權限
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
那我們還是需要讓user去同意,所以我們設定當今天按下 camera的圖片時,會先確認是否有權限,如果有才會執行去拿相簿,沒有就詢問
private fun checkPermission(){
//check是否有權限,有的話就進相簿
if (ContextCompat.checkSelfPermission(requireActivity(),android.Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED){
resultLauncher.launch(
Intent(Intent.ACTION_PICK,MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
)
}else{
/沒的話就去確認requestPermission
requestPermission()
}
}
我們發現有幾個地方是紅字,所以我們要來解決它
我們先來新增要求權限
private fun requestPermission(){
ActivityCompat.requestPermissions(requireActivity(), arrayOf(android.Manifest.permission.READ_EXTERNAL_STORAGE),Constant.REQUEST_CODE_READ)
}
//這個是值是可以自訂的
const val REQUEST_CODE_READ = 1001
目前onActivityResult已經棄用了,所以我們要更改成,首先我們在 class 下面先新增註冊一個ActivityResult,再到想要發出 Intent的地方,透過這個resultLauncher來啟動Intent。(也就是會在剛剛的checkPermission() 裡面)
//大致上概念是跟onActivityResult一樣,得到的值會是ActivityResult,因為我們Launch的是相簿的Intent,所以我們拿到的是Uri
private val resultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()){ uri ->
if (uri.resultCode == Activity.RESULT_OK){
//一樣透過data.data去拿到值
val selectedUri = uri.data?.data
if (selectedUri != null){
//選完圖片後,直接顯示在我們的畫面上
Constant.loadUserImage(selectedUri,binding.ivEditProfileImage)
//傳入Storage
accountViewModel.saveImageToFireStorage(requireActivity(),this,selectedUri)
}
}
}
確認是否有implement FireStorage
implementation 'com.google.firebase:firebase-storage-ktx'
來到AccountViewModel,新增
//傳入acitivty,讓我們等等可以拿到檔案的的格式
fun saveImageToFireStorage(activity: Activity, fragment: EditProfileFragment,uri: Uri){
//這邊我們要先拿一個 Storage的Reference,並且後面的child我們把它指定成,user_image + 時間 + 檔案的type
val sdf: StorageReference = FirebaseStorage.getInstance().reference.child(Constant.USER_IMAGE + System.currentTimeMillis() + Constant.getFileExtension(activity, uri))
//直接用 putFile就可以上傳檔案了
sdf.putFile(uri)
.addOnSuccessListener {
//上傳成功後可以拿到 UploadTask.TaskSnapshot,我們要把它改成可以下載的Url
it.metadata?.reference?.downloadUrl
//成功後,我們再把它傳入到 fragment,並且一起upda
?.addOnSuccessListener {
fragment.saveImageSuccessful(it)
}
?.addOnFailureListener {
fragment.saveImageFail(it.toString())
}
}
.addOnFailureListener {
fragment.editUserDetailFail(it.toString())
}
}
再過來去 Constant 新增以下
//指定 child的時候,新增敘述讓我們了解是user照片,還是之後會上傳的pet照片。
const val USER_IMAGE: String = "user_image"
//利用 MimeTypeMap去拿出圖片uri的格式
fun getFileExtension(activity: Activity, uri: Uri): String?{
return MimeTypeMap.getSingleton().getExtensionFromMimeType(activity.contentResolver.getType(uri))
回到EditProfileFragment新增以下
fun saveImageSuccessful(uri: Uri){
showSnackBar(resources.getString(R.string.update_user_profile_successful),false)
//把它指定到我們的 mUri
mUri = uri.toString()
}
fun saveImageFail(e: String){
showSnackBar(e,true)
}
我們在class 下方新增以下,讓我們可以賦值,一開始是null,然後再 saveImageSuccessful() 被呼叫時,我們把它賦予新值
var mUri: String? = null
private fun validDataForm(): Boolean{
return when{
TextUtils.isEmpty(binding.edEditProfileName.text.toString().trim()) -> {
showSnackBar(resources.getString(R.string.hint_enter_your_name),true)
return false
}
TextUtils.isEmpty(binding.edEditArea.text.toString().trim()) -> {
showSnackBar(resources.getString(R.string.hint_enter_your_area),true)
return false
}
//我這邊是設定一定要有選取照片,不然不給過,但是實際時可以自己決定要不要有圖片
mUri.isNullOrBlank() && accountViewModel.userDetail.value?.image == "" -> {
showSnackBar(resources.getString(R.string.hint_select_your_image),true)
return false
}
else -> true
}
}
接下來我們來Constant來新增以下,好讓我們不會有寫錯字導致沒有match成功的可能性
const val NAME: String = "name"
const val AREA: String = "area"
const val GENDER: String = "gender"
const val IMAGE: String = "image"
const val PROFILE_COMPLETED: String = "profileCompleted"
回到onClick
override fun onClick(v: View?) {
when(v){
binding.ivEditProfileCamera ->{
//check我們的相簿讀取權限
checkPermission()
}
binding.btnEdit ->{
//確認格式
if (validDataForm()){
showDialog(resources.getString(R.string.please_wait))
//我們透過key對value的方式來更新資料
val mHashMap = HashMap<String,Any>()
mHashMap[Constant.NAME] = binding.edEditProfileName.text.toString().trim()
//確認當今天mUri不是null時才把它放進 HashMap
mUri?.let {
mHashMap[Constant.IMAGE] = it
}
var gender = ""
//判斷user點選的RadioButton
if (binding.rbMan.isChecked){
gender = Constant.MAN
}else{
gender = Constant.FEMALE
}
mHashMap[Constant.GENDER] = gender
//把Profile_completed改成 true
mHashMap[Constant.PROFILE_COMPLETED] = true
mHashMap[Constant.AREA]= binding.edEditArea.text.toString().trim()
accountViewModel.updateUserDetailToFireStore(mHashMap,this)
}
}
}
}
到 onCreateView新增以下
binding.btnEdit.setOnClickListener(this)
binding.ivEditProfileCamera.setOnClickListener(this)
新增返回鍵就結束這一回合!
binding.toolbarEditProfileFragment.setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
binding.toolbarEditProfileFragment.setNavigationOnClickListener {
requireActivity().onBackPressed()
}
成功如下啦!!
慘了,自己打完之後覺得有點害羞0.0
上傳圖片可以先去 Storage確認一下
來看個 FireStore