240411 TIL - 당근마켓 비슷한 앱 만들기

DoDoBest

·

2024. 4. 11. 22:59

당근마켓 비슷한 앱 만들기

내일배움캠프 앱개발 숙련주차 개인 과제로 당근마켓과 비슷한 간단한 앱 만들기를 구현했습니다.

 

https://github.com/DoTheBestMayB/NBC-AppleMarket

 

GitHub - DoTheBestMayB/NBC-AppleMarket: 내일배움캠프 - 앱개발 숙련 개인 과제 - 당근마켓

내일배움캠프 - 앱개발 숙련 개인 과제 - 당근마켓. Contribute to DoTheBestMayB/NBC-AppleMarket development by creating an account on GitHub.

github.com

 

 

당근마켓 매너온도 안내 문구 만들기

 

당근마켓에서 매너온도를 누르면 나오는 안내 팝업은 사각형의 TextView 위에 삼각형 View가 존재하는 형태로 되어 있습니다.

 

 

 

당근마켓 안내 문구의 컬러 값은 #202123 이며 @color/guidance_color로 지정했습니다. 삼각형 vector를 만듭니다.

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:height="10dp"
    android:width="10dp"
    android:viewportHeight="100"
    android:viewportWidth="100" >
    <group
        android:name="triableGroup">
        <path
            android:name="triangle"
            android:fillColor="@color/guidance_color"
            android:pathData="m 50,0 l 50,100 -100,0 z" />
    </group>
</vector>

 

그러면 삼각형 백터를 담은 ImageView와 TextView를 이용해서 다음과 같이 만들 수 있습니다.

 

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/root"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <ImageView
        android:id="@+id/v_bubble"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="12dp"
        android:src="@drawable/traingle"
        app:layout_constraintBottom_toTopOf="@id/tv_guidance"
        app:layout_constraintEnd_toEndOf="@id/tv_guidance"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tv_guidance"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/guidance_border"
        android:maxEms="15"
        android:padding="12dp"
        android:text="@string/temper_guidance"
        android:textColor="@color/white"
        android:textSize="18sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/v_bubble" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

 

 

 

 

https://stackoverflow.com/a/31704460/11722881

 

Making a triangle shape using XML definitions?

Is there a way that I can specify a triangle shape in an XML file? <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="triangle"> <stroke

stackoverflow.com

 

 

ConstraintLayout에서 Include 사용시 주의사항

 

layout_width, layout_height을 명시하지 않으면 제약에 상관없이 View가 왼쪽 위로 이동합니다.

 

<include
    android:id="@+id/v_guidance"
    layout="@layout/bubble_guidance"
    app:layout_constraintTop_toBottomOf="@id/tv_temper_info"
    app:layout_constraintEnd_toEndOf="@id/tv_temper_info" />

 

 

그래서 아래와 같이 width와 height을 명시해야 합니다.

 

<include
    android:id="@+id/v_guidance"
    layout="@layout/bubble_guidance"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_constraintTop_toBottomOf="@id/tv_temper_info"
    app:layout_constraintEnd_toEndOf="@id/tv_temper_info" />

 

 

https://stackoverflow.com/a/51807639/11722881

 

How to include constraint layout to another constraint layout and set constraint between each

I'm using constraintLyout v 1.0.1. I would like to include in my xml a sub ConstraintLayout corresponding to a part of my global layout (which itself is a ConstraintLayout). I split the layout in two

stackoverflow.com

 

View 외 영역 클릭시 Visibility 변경하기

 

매너온도 View 외 영역을 클릭하거나 드래그할 경우, 매너온도 창이 보이지 않도록 구현하고 싶었습니다. 매너온도 View는 ConstraintLayout -> ScrollView -> ConstraintLayout -> include(ConstraintLayout) 와 같은 hierarchy로 구성되어 있습니다.

 

매너온도 View의 visibility를 변경하기 위해 부모 Layout에 touch event가 발생하면 Visilibity를 GONE으로 변경하는 방법을 선택했습니다. Layout에 touch event를 주기 위해 아래 3가지 속성을 추가했습니다.

 

android:clickable="true"
android:focusable="true"
android:focusableInTouchMode="true"

 

 

최상위 ConstraintLayout에 설정할 경우, ScrollView를 스크롤할 때 매너온도 View가 사라지지 않았습니다.

매너온도 View의 바로 상위 ConstraintLayout에 설정할 경우, ConstraintLayout의 내용이 ScrollView 전체 영역을 차지할만큼 크지 않을 경우, 하단의 빈공간을 터치할 때 매너온도 View가 사라지지 않았습니다.

 

그래서 ScollView에 설정했습니다. 다만, ScrollView는 스크롤이 가능하기 때문인지 setOnClickListener는 동작하지 않았고, 그래서 setOnTouchListener를 사용했습니다.

매너온도 창도 ScrollView 영역에 포함됩니다. 매너온도 창을 터치해서 스크롤하는 경우에는 매너온도 창이 사리지지 않도록 하기 위해 매너온도 창에 setOnClickListener를 추가해 visibility를 VISIBLE로 설정했습니다.

 

with(binding) {
    // 스크롤뷰를 클릭하면 매너 온도 안내 팝업이 사라지도록 구현
    sv.setOnTouchListener { v, event ->
        if (event.action == MotionEvent.ACTION_DOWN) {
            vGuidance.root.visibility = View.GONE
        }
        false // 클릭되었을 때, 매너 온도 안내 팝업을 사라지도록 하는 것이 목표 이므로, 터치 이벤트가 child로 타고 가도록 false 처리
    }

    // 스크롤뷰에 등록한 터치 리스너로 인해, 매너 온도 안내 팝업을 클릭하면 사라지고 있는데, 이것을 방지하기 위한 코드
    vGuidance.root.setOnClickListener {
        it.visibility = View.VISIBLE
    }

}

 

 

 

 

RecyclerView payload 사용시 주의사항

RecyclerView에서 데이터의 일부 파라미터만 변경된 경우, 해당 파라미터 값에 해당하는 UI만 변경하기 위해 payload를 사용할 수 있습니다.

이때, 해당 UI만 변경하고 넘어가서는 안 됩니다. bind 함수에서 setOnClickListener와 같은 람다 블록에서 전달 받은 데이터를 이용하는 코드가 있다면, 해당 리스너도 다시 정의해야 합니다. 그렇지 않으면 해당 람다 블록에 포획된 데이터와 payload에 의해 업데이트 된 UI 데이터가 일치하지 않게 됩니다.

 

처음에는 기존 데이터와 새로운 데이터가 좋아요라는 파라미터가 다른 경우, updateLike라는 함수를 호출해 좋아요 숫자 textView만 변경되도록 구현했습니다. 그 결과 setListener 람다 블록에서 반환하는 데이터와 최신 데이터 간의 좋아요 숫자가 일치하지 않는 문제가 발생했습니다.

 

class ProductAdapter(
    private val onClickListener: ProductOnClickListener,
) : ListAdapter<Product, ProductAdapter.ViewHolder>(diffCallback) {

    inner class ViewHolder(private val binding: ItemProductOverviewBinding) :
        RecyclerView.ViewHolder(binding.root) {

        fun bind(product: Product) = with(binding) {
            setData(product)
            setVisibility(product)
            setListener(product)
        }

        ...

        fun updateLikeFilled(product: Product) {
            val id = if (LikeManager.checkLike(LoggedUserManager.getUserInfo(), product)) {
                R.drawable.like_fill
            } else {
                R.drawable.like
            }
            binding.ivLike.setImageResource(id)
        }

        ...

        private fun setListener(product: Product) {
            binding.root.setOnClickListener {
                onClickListener.onClick(product)
            }
            binding.root.setOnLongClickListener {
                onClickListener.onLongClick(product)
                return@setOnLongClickListener true
            }
        }

        fun updateLike(count: Int) {
            binding.tvLike.text = count.toString()
        }
    }

    ...

    override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
        if (payloads.isEmpty()) {
            super.onBindViewHolder(holder, position, payloads)
            return
        }
        val product = getItem(position)
        for (payloadLists in payloads) {
            for (payload in payloadLists as List<*>) {
                when (payload) {
                    ProductChangePayload.LIKE -> holder.updateLike(product.like)
                    ProductChangePayload.LIKED_FILLED -> holder.updateLikeFilled(product)
                }
            }
        }

    }

    companion object {
        private val diffCallback = object : DiffUtil.ItemCallback<Product>() {
            override fun areItemsTheSame(oldItem: Product, newItem: Product): Boolean {
                return oldItem.uuid == newItem.uuid
            }

            override fun areContentsTheSame(oldItem: Product, newItem: Product): Boolean {
                return oldItem == newItem
            }

            override fun getChangePayload(oldItem: Product, newItem: Product): Any {
                val changes = mutableListOf<ProductChangePayload>()

                if (oldItem.like != newItem.like) {
                    changes.add(ProductChangePayload.LIKE)
                    changes.add(ProductChangePayload.LIKED_FILLED)
                }

                return changes
            }
        }
    }
}

 

따라서 다음과 같이 listener를 다시 등록하여 람다 블록에 포획된 데이터를 최신화하도록 코드를 수정했습니다.

 

...
    override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
        if (payloads.isEmpty()) {
            super.onBindViewHolder(holder, position, payloads)
            return
        }
        val product = getItem(position)
        for (payloadLists in payloads) {
            for (payload in payloadLists as List<*>) {
                when (payload) {
                    ProductChangePayload.LIKE -> holder.updateLike(product.like)
                    ProductChangePayload.LIKED_FILLED -> holder.updateLikeFilled(product)
                    ProductChangePayload.LISTENER -> holder.setListener(product)
                }
            }
        }

    }

    companion object {
        private val diffCallback = object : DiffUtil.ItemCallback<Product>() {
            ...

            override fun getChangePayload(oldItem: Product, newItem: Product): Any {
                val changes = mutableListOf<ProductChangePayload>()

                if (oldItem.like != newItem.like) {
                    changes.add(ProductChangePayload.LIKE)
                    changes.add(ProductChangePayload.LIKED_FILLED)
                    changes.add(ProductChangePayload.LISTENER)
                }

                return changes
            }
        }
    }
}