【Day9】AddInvitationFragment(上)

好的,接下来我们要新增邀约的Fragment,好让使用者可以上去PO出自己的邀约,以及让不同的使用者可以看到目前有的邀约。那我们开始吧! 今天会完成上传图片/选单

页面长相长这样!

Day9.first

1.新增Invitation的DataClass

data class Invitation(
        val id: String = "",
        val user_id: String = "",
        val pet_image: String = "",
        val pet_type: String = "",
        val pet_type_description: String = "",
        val area: String = "",
        val date_place: String = "",
        val date_time: String = "",
        val note: String = ""
)

2.新增AddInvitationFragment

一样继承 BaseFragment以及databinding

3.layout

按照惯例先dimen

    <dimen name="image_height">250dp</dimen>
    <dimen name="icon_camera_margin_bottom_end">10dp</dimen>
    <dimen name="add_invitation_input_layout_margin_top_bottom"

string

	<string name="toolbar_title_add_invitation">新增邀约</string>
    <string name="hint_enter_pet_type">请选择宠物种类</string>
    <string name="hint_enter_pet_type_description">请输入宠物品种</string>
    <string name="hint_enter_date_place">请输入邀约地点</string>
    <string name="hint_enter_date_time">请输入邀约时间</string>
    <string name="hint_enter_date_note">请输入注意事项(如宠物害怕物品..)</string>
    <string name="update_pet_image_successful">上传图片成功!</string>
    <string name="hint_enter_area">请输入邀约区域</string>

layout

为了达到视觉效果统一,layout大部分会是用material.textfield.TextInputLayout来包。然後因为这次的填写的内容比较多,所以我们会用Scroll View来包。

<?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:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ui.fragment.AddInvitationFragment">


        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar_add_invitation_fragment"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="@color/light_pewter_blue"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent">

            <TextView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:gravity="center"
                android:text="@string/toolbar_title_add_invitation"
                android:textColor="@color/white"
                android:textSize="@dimen/toolbar_textSize"
                android:textStyle="bold">

            </TextView>

        </androidx.appcompat.widget.Toolbar>

        <ScrollView
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:orientation="vertical"
            android:fillViewport="true"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintTop_toBottomOf="@id/toolbar_add_invitation_fragment">

            <androidx.constraintlayout.widget.ConstraintLayout
                android:layout_width="match_parent"
                android:layout_height="0dp">


                <FrameLayout
                    android:id="@+id/fl_add_invitation_image"
                    android:layout_width="match_parent"
                    android:layout_height="@dimen/image_height"
                    android:background="@color/grey_light"
                    app:layout_constraintTop_toTopOf="parent">

                    <ImageView
                        android:id="@+id/iv_add_invitation_pet_image"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:scaleType="fitXY">

                    </ImageView>

                    <ImageView
                        android:id="@+id/iv_add_invitation_camera"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_gravity="bottom|end"
                        android:layout_marginEnd="@dimen/icon_camera_margin_bottom_end"
                        android:layout_marginBottom="@dimen/icon_camera_margin_bottom_end"
                        android:src="@drawable/ic_baseline_photo_camera_24">

                    </ImageView>

                </FrameLayout>


                <com.google.android.material.textfield.TextInputLayout
                    android:id="@+id/tip_add_invitation_fragment_spinner_pet_type"
                    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/fl_add_invitation_image"
                    style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu">

                    <AutoCompleteTextView
                        android:id="@+id/spinner_pet_type"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:inputType="none"
                        android:hint="@string/hint_enter_pet_type"
                        android:textSize="@dimen/edText_textSize"
                        android:padding="@dimen/edText_padding"/>

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


                <com.google.android.material.textfield.TextInputLayout
                    android:id="@+id/tip_add_invitation_fragment_pet_type_description"
                    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/add_invitation_input_layout_margin_top_bottom"
                    app:layout_constraintTop_toBottomOf="@id/tip_add_invitation_fragment_spinner_pet_type"
                    style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">

                    <com.example.petsmatchingapp.utils.JFEditText
                        android:id="@+id/ed_add_invitation_pet_type_description"
                        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_pet_type_description"/>

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


                <com.google.android.material.textfield.TextInputLayout
                    android:id="@+id/tip_add_invitation_fragment_spinner_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/add_invitation_input_layout_margin_top_bottom"
                    app:layout_constraintTop_toBottomOf="@id/tip_add_invitation_fragment_pet_type_description"
                    style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu">

                    <AutoCompleteTextView
                        android:id="@+id/spinner_area"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:inputType="none"
                        android:hint="@string/hint_enter_area"
                        android:textSize="@dimen/edText_textSize"
                        android:padding="@dimen/edText_padding"/>

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


                <com.google.android.material.textfield.TextInputLayout
                    android:id="@+id/tip_add_invitation_fragment_date_place"
                    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/add_invitation_input_layout_margin_top_bottom"
                    app:layout_constraintTop_toBottomOf="@id/tip_add_invitation_fragment_spinner_area"
                    style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">

                    <com.example.petsmatchingapp.utils.JFEditText
                        android:id="@+id/ed_add_invitation_date_place"
                        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_date_place"/>

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

                <com.google.android.material.textfield.TextInputLayout
                    android:id="@+id/tip_add_invitation_fragment_date_time"
                    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/add_invitation_input_layout_margin_top_bottom"
                    app:layout_constraintTop_toBottomOf="@id/tip_add_invitation_fragment_date_place"
                    style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">

                    <com.example.petsmatchingapp.utils.JFEditText
                        android:id="@+id/ed_add_invitation_date_time"
                        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_date_time"/>

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

                <com.google.android.material.textfield.TextInputLayout
                    android:id="@+id/tip_add_invitation_fragment_note"
                    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/add_invitation_input_layout_margin_top_bottom"
                    app:layout_constraintTop_toBottomOf="@id/tip_add_invitation_fragment_date_time"
                    style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">

                    <com.example.petsmatchingapp.utils.JFEditText
                        android:id="@+id/ed_add_invitation_note"
                        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_date_note"/>

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


                <com.example.petsmatchingapp.utils.JFButton
                    android:id="@+id/btn_add_invitation_fragment_submit"
                    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/tip_add_invitation_fragment_note"
                    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/submit"/>


            </androidx.constraintlayout.widget.ConstraintLayout>


        </ScrollView>

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

4.上传照片

知道我们架构的小夥伴们都知道,我们主要有分两个viewModel,一个是负责我们的帐号相关的AccountViewModel,另外一个则是主要放Mathcing资料的viewModel,所以我们接下来前置工作就是要新增viewModel,跟设定koin

一样先建立MatchingViewModel,并继承viewModel,别忘了去MyApp的startKoin funtion新增

val viewModelModule = module {
            viewModel { AccountViewModel() }
			//新增这个
            viewModel { MatchingViewModel() }
        }

4.1.新增权限check

虽然权限我们之前在ProfileFragment设定过了,但是也有可能那时候使用者拒绝,所以我们在这边还要再问一次!

private fun checkPermission(){
		//确认是否有权限,如果有权限则开启相簿
        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()
        }
    }

这边出现红字,我们来新增要求权限

private fun requestPermission(){
        ActivityCompat.requestPermissions(requireActivity(), arrayOf(android.Manifest.permission.READ_EXTERNAL_STORAGE),Constant.REQUEST_CODE_READ)
    }

如果没有跟到前面几个文章的小夥伴们,也要记得在manifest新增权限喔!

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

4.2.拿到Result,并传去viewModel

private val resultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()){ uri ->
        if (uri.resultCode == Activity.RESULT_OK){

			//一样透过 data.data去拿uri
            val selectedUri = uri.data?.data
            if (selectedUri != null){
				//有result的时候,就先把他用Glide显示进去。
                Constant.loadPetImage(selectedUri,binding.ivAddInvitationPetImage)
				//再来传去storage
       matchingViewModel.saveImageToFireStorage(requireActivity(),this,selectedUri)

            }
        }
    }

因为我们在loadUserImage的时候,我们为了让层次更加明显,以及笔者在做update使用者图片时,刚好是中秋节,为了让人比月娇,所以我们把它改成圆角,但是宠物就不需要了,我们希望宠物邀约的时候,能更无死角,更全面性的看,所以我们要修改一下 Glide的地方,一样去Constant新增以下

我们只要把圆角拿掉就好

fun loadPetImage(url: Any, v:ImageView){

        Glide.with(v)
            .load(url)
            .placeholder(R.drawable.placeholder)
            .into(v)
    }

别忘了要先宣告matchingViewModel喔!

我们在传送邀约的时候,也会需要po出这个讯息的人的ID(这也是为什麽Invitation这个dataClass会需要有 user_id的资讯),这样我们对这个邀约的操控性就可以更高,譬如说之後我们可能不希望在所有约散的Fragment时,看到自己的PO出的讯息等...

private val matchingViewModel: MatchingViewModel by sharedViewModel()
private val accountViewModel: AccountViewModel by sharedViewModel()

4.3.上传至Storage并回传可下载的uri

到 MatchingViewModel新增上传到 Storage的funtion啦!

原则上跟上传user的头贴一样,我们只要把child名称改变就好,以便之後我们好辨识

//传入的fragment就要改成AddInvitationFragment
fun saveImageToFireStorage(activity: Activity, fragment: AddInvitationFragment, uri: Uri){

        val sdf: StorageReference = FirebaseStorage.getInstance().reference.child(Constant.PET_IMAGE + "_" + System.currentTimeMillis() + "_" + Constant.getFileExtension(activity, uri))
        sdf.putFile(uri)
            .addOnSuccessListener {
								//当上传资料成功时,我们在新增Successful的listener,并且把它改成 downloadUri,并传回fragment!	
                it.metadata?.reference?.downloadUrl
                    ?.addOnSuccessListener {
                        fragment.saveImageSuccessful(it)
                    }
                    ?.addOnFailureListener {
                        fragment.saveImageFail(it.toString())
                    }

            }
            .addOnFailureListener {
                fragment.saveImageFail(it.toString())
            }

    }

接下来我们发现红字,我们来解决它,一样回Fragment来新增以下,且因为我们要让我们的 mUri 存活在整个class,所以我们在class新增一个 null的 mUri

private var mUri: String? = null
fun saveImageSuccessful(uri: Uri){
        mUri = uri.toString()
    }

    fun saveImageFail(e:String){
        showSnackBar(e,true)
    }

4.4.onClick事件

一样先继承 View.OnClickListener,并新增check权限的funtion

override fun onClick(v: View?) {
        when(v){
            binding.ivAddInvitationCamera -> {
                checkPermission()
            }
        }
    }

别忘了在 onCreateView新增

binding.ivAddInvitationCamera.setOnClickListener(this)

5.Spinner下拉式选单

我们这边一样透过外面在包一个 inputLayout,这样就可以保证我们的layout长相都一样。其中也有很多有趣的设计,可以看一下 https://material.io/components/text-fields/android

5.1.绑定adapter

我们先做一个 String List,先在class 先延迟初始化

private lateinit var petList: List<String>
private lateinit var areaList: List<String>

再来onCreateView赋值

petList = listOf("狗", "猫", "兔子", "鸟","猪","鱼","其它")
areaList = listOf("基隆市","台北市","新北市","桃园市","新竹市","新竹县","苗栗县","彰化县","云林县","南投县","台中市","嘉义市","嘉义县","台南市",
            "高雄市","屏东县","宜兰县","花莲县","台东县","澎湖县","金门县","连江县","其它")

★贴心小提醒,选单要记得添加其它呦,要不然没有看到适合自己的人就会觉得被排挤!!

private fun setSpinner(){
				
        val petAdapter = ArrayAdapter(requireContext(),R.layout.spinner_list_item,petList)
        binding.spinnerPetType.setAdapter(petAdapter)
        
        val areaAdapter = ArrayAdapter(requireContext(),R.layout.spinner_list_item,areaList)
        binding.spinnerArea.setAdapter(areaAdapter)

    }

ArrayAdapter传入

  • context
  • 等等会做的text的Layout
  • 选单资料(list)

再来新增 layout,并命名为 spinner_list_item

<?xml version="1.0" encoding="utf-8"?>

	<TextView app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="@dimen/edText_padding"
    android:ellipsize="end"
    android:maxLines="1"
    android:textSize="@dimen/edText_textSize"
    android:textAppearance="?attr/textAppearanceSubtitle1"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto" />

★注意外面不用包layout喔,自己在做的时候就有手溅,自己乱加,结果就导致不行

取得资料
我们先在class下新增

	private var selectedPetType: String? = null
    private var selectedArea: String? = null

新增setOnItemClickListener就可以拿到 view,position,id 等资料罗! 但我们这边只要 position就好了!

binding.spinnerPetType.setOnItemClickListener { _, _, position, _ ->
               
		        selectedPetType = petList[position]
               
        }

        binding.spinnerArea.setOnItemClickListener { _, _, position, _ ->

		         selectedArea = areaList[position]
		  
        }

好的,今天就先告一段落啦!! 目前还没有弄Navigation导航到这个Fragment,我相信有认真学习的夥伴们一定可以自己做出来,所以我们就先给大家看一下小成果吧!!

大家也可以自己Log看看有没有成功拿到资料!

day9.finish


<<:  [DAY-10] 人才密度最大化 留任测试

>>:  不只懂 Vue 语法:请说明 keep-alive 以及 is 属性的作用?

Day18:SwiftUI Picker

前言 SwiftUI Picker 可以让我们自行选择项目, 也可以选择不同的呈现方式, 这篇文章来...

企划实现(3)

企划发想过程 第一步 寻找需求 在一个企划的发想初期最先要做的事是找到市场,当你找到了市场才能继续做...

网路是怎样连接的(四)DNS

思考重点 如何利用DNS协议查找相应的IP位置? 服务器是怎麽存储那麽多相应的域名消息? DNS是使...

会员管理网站实作篇 - (以律师谘询平台为例子) part 1

前言 最後四篇篇幅我们以实作一个会员网站为例子,想做这个主题原因在於会员网站在 WP 中算是个少见的...

Day14 - 解析看板文章及显示

该来处理搜寻结果了。 在前几天的内容中,当我完成搜寻时,都会使用parseBoardArticle方...