feat(community): 커뮤니티 전체보기 리스트 그리드 전환 탭을 추가한다

This commit is contained in:
2026-03-06 14:25:26 +09:00
parent d8b2d53747
commit 93b620f4a8
9 changed files with 488 additions and 60 deletions

View File

@@ -9,15 +9,19 @@ import android.os.Looper
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.base.SodaDialog
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivityCreatorCommunityAllBinding
import kr.co.vividnext.sodalive.explorer.profile.creator_community.GetCommunityPostListResponse
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.comment.CreatorCommunityCommentFragment
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.player.CreatorCommunityMediaPlayerManager
import kr.co.vividnext.sodalive.explorer.profile.creator_community.modify.CreatorCommunityModifyActivity
@@ -28,15 +32,64 @@ class CreatorCommunityAllActivity : BaseActivity<ActivityCreatorCommunityAllBind
ActivityCreatorCommunityAllBinding::inflate
) {
companion object {
private const val GRID_SPAN_COUNT = 3
}
private val viewModel: CreatorCommunityAllViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: CreatorCommunityAllAdapter
private lateinit var listAdapter: CreatorCommunityAllAdapter
private lateinit var gridAdapter: CreatorCommunityAllGridAdapter
private lateinit var mediaPlayerManager: CreatorCommunityMediaPlayerManager
private lateinit var imm: InputMethodManager
private var creatorId: Long = 0
private var isListMode = false
private var listAnchorPosition = 0
private var gridAnchorPosition = 0
private var isListEnteredFromGridClick = false
private val handler = Handler(Looper.getMainLooper())
private val gridSpacingPx by lazy { 1.3f.dpToPx().toInt() }
private val gridItemDecoration by lazy {
GridSpacingItemDecoration(
spanCount = GRID_SPAN_COUNT,
spacing = gridSpacingPx,
includeEdge = true
)
}
private val listItemDecoration = object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
outRect.left = 13.3f.dpToPx().toInt()
outRect.right = 13.3f.dpToPx().toInt()
when (parent.getChildAdapterPosition(view)) {
RecyclerView.NO_POSITION -> Unit
0 -> {
outRect.top = 13.3f.dpToPx().toInt()
outRect.bottom = 6.7f.dpToPx().toInt()
}
listAdapter.itemCount - 1 -> {
outRect.top = 6.7f.dpToPx().toInt()
outRect.bottom = 13.3f.dpToPx().toInt()
}
else -> {
outRect.top = 6.7f.dpToPx().toInt()
outRect.bottom = 6.7f.dpToPx().toInt()
}
}
}
}
private val modifyResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
@@ -54,7 +107,9 @@ class CreatorCommunityAllActivity : BaseActivity<ActivityCreatorCommunityAllBind
super.onCreate(savedInstanceState)
mediaPlayerManager = CreatorCommunityMediaPlayerManager(this) {
adapter.updateUI()
if (::listAdapter.isInitialized) {
listAdapter.updateUI()
}
}
creatorId = intent.getLongExtra(Constants.EXTRA_COMMUNITY_CREATOR_ID, 0)
@@ -87,9 +142,29 @@ class CreatorCommunityAllActivity : BaseActivity<ActivityCreatorCommunityAllBind
imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
binding.toolbar.tvBack.setText(R.string.screen_creator_community_all_title)
binding.toolbar.tvBack.setOnClickListener { finish() }
binding.toolbar.tvBack.setOnClickListener {
handleBackNavigation()
}
binding.llTabList.setOnClickListener {
if (isListMode) {
isListEnteredFromGridClick = false
updateTabUi()
} else {
switchToListMode(gridAnchorPosition, fromGridItemClick = false)
}
}
binding.llTabGrid.setOnClickListener {
if (isListMode) {
switchToGridMode(anchorPosition = listAnchorPosition)
}
}
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
handleBackNavigation()
}
})
adapter = CreatorCommunityAllAdapter(
listAdapter = CreatorCommunityAllAdapter(
screenWidth = screenWidth,
onClickLike = { viewModel.communityPostLike(it) },
writeComment = { postId, parentId, comment, isSecret ->
@@ -154,65 +229,25 @@ class CreatorCommunityAllActivity : BaseActivity<ActivityCreatorCommunityAllBind
confirmButtonClick = {
viewModel.purchaseCommunityPost(postId) {
onSuccess(it)
updateGridItem(it)
}
}
).show(screenWidth)
}
)
val recyclerView = binding.rvCreatorCommunity
recyclerView.layoutManager = LinearLayoutManager(
applicationContext,
LinearLayoutManager.VERTICAL,
false
val gridItemSize = ((screenWidth - (gridSpacingPx * (GRID_SPAN_COUNT + 1))) / GRID_SPAN_COUNT)
.coerceAtLeast(1)
gridAdapter = CreatorCommunityAllGridAdapter(
itemSize = gridItemSize,
onClickItem = {
switchToListMode(it, fromGridItemClick = true)
}
)
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
setupRecyclerViews()
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.top = 13.3f.dpToPx().toInt()
outRect.bottom = 6.7f.dpToPx().toInt()
}
adapter.itemCount - 1 -> {
outRect.top = 6.7f.dpToPx().toInt()
outRect.bottom = 13.3f.dpToPx().toInt()
}
else -> {
outRect.top = 6.7f.dpToPx().toInt()
outRect.bottom = 6.7f.dpToPx().toInt()
}
}
}
})
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val lastVisiblePosition = (recyclerView.layoutManager as LinearLayoutManager)
.findLastVisibleItemPosition()
val itemTotalCount = adapter.itemCount - 1
if (itemTotalCount > 0 && lastVisiblePosition == itemTotalCount) {
viewModel.getCommunityPostList()
}
}
})
recyclerView.adapter = adapter
switchToListMode(0, fromGridItemClick = false)
}
@SuppressLint("NotifyDataSetChanged")
@@ -231,11 +266,146 @@ class CreatorCommunityAllActivity : BaseActivity<ActivityCreatorCommunityAllBind
viewModel.communityPostListLiveData.observe(this) {
if (viewModel.page == 2) {
adapter.items.clear()
listAdapter.items.clear()
gridAdapter.items.clear()
}
adapter.items.addAll(it)
adapter.notifyDataSetChanged()
listAdapter.items.addAll(it)
gridAdapter.items.addAll(it)
listAdapter.notifyDataSetChanged()
gridAdapter.notifyDataSetChanged()
}
}
private fun setupRecyclerViews() {
val listRecyclerView = binding.rvCreatorCommunity
listRecyclerView.layoutManager = LinearLayoutManager(
applicationContext,
LinearLayoutManager.VERTICAL,
false
)
listRecyclerView.adapter = listAdapter
listRecyclerView.setHasFixedSize(true)
listRecyclerView.itemAnimator = null
if (listRecyclerView.itemDecorationCount == 0) {
listRecyclerView.addItemDecoration(listItemDecoration)
}
listRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val layoutManager = recyclerView.layoutManager as? LinearLayoutManager ?: return
val firstVisiblePosition = layoutManager.findFirstVisibleItemPosition()
if (firstVisiblePosition != RecyclerView.NO_POSITION) {
listAnchorPosition = firstVisiblePosition
}
val lastVisiblePosition = layoutManager.findLastVisibleItemPosition()
val itemTotalCount = listAdapter.itemCount - 1
if (itemTotalCount > 0 && lastVisiblePosition == itemTotalCount) {
viewModel.getCommunityPostList()
}
}
})
val gridRecyclerView = binding.rvCreatorCommunityGrid
gridRecyclerView.layoutManager = GridLayoutManager(applicationContext, GRID_SPAN_COUNT)
gridRecyclerView.adapter = gridAdapter
gridRecyclerView.setHasFixedSize(true)
gridRecyclerView.itemAnimator = null
if (gridRecyclerView.itemDecorationCount == 0) {
gridRecyclerView.addItemDecoration(gridItemDecoration)
}
gridRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val layoutManager = recyclerView.layoutManager as? GridLayoutManager ?: return
val firstVisiblePosition = layoutManager.findFirstVisibleItemPosition()
if (firstVisiblePosition != RecyclerView.NO_POSITION) {
gridAnchorPosition = firstVisiblePosition
}
val lastVisiblePosition = layoutManager.findLastVisibleItemPosition()
val itemTotalCount = gridAdapter.itemCount - 1
if (itemTotalCount > 0 && lastVisiblePosition == itemTotalCount) {
viewModel.getCommunityPostList()
}
}
})
}
private fun handleBackNavigation() {
if (isListMode && isListEnteredFromGridClick) {
switchToGridMode(anchorPosition = listAnchorPosition)
} else {
finish()
}
}
private fun switchToListMode(position: Int, fromGridItemClick: Boolean) {
val requestedPosition = position.coerceAtLeast(0)
isListMode = true
isListEnteredFromGridClick = fromGridItemClick
listAnchorPosition = requestedPosition
updateTabUi()
binding.rvCreatorCommunity.visibility = View.VISIBLE
binding.rvCreatorCommunityGrid.visibility = View.INVISIBLE
val recyclerView = binding.rvCreatorCommunity
recyclerView.post {
if (listAdapter.itemCount > 0) {
val targetPosition = requestedPosition.coerceIn(0, listAdapter.itemCount - 1)
(recyclerView.layoutManager as? LinearLayoutManager)
?.scrollToPositionWithOffset(targetPosition, 0)
listAnchorPosition = targetPosition
}
}
}
private fun switchToGridMode(anchorPosition: Int = 0) {
isListMode = false
isListEnteredFromGridClick = false
updateTabUi()
if (::mediaPlayerManager.isInitialized) {
mediaPlayerManager.pauseContent()
}
binding.rvCreatorCommunity.visibility = View.INVISIBLE
binding.rvCreatorCommunityGrid.visibility = View.VISIBLE
val recyclerView = binding.rvCreatorCommunityGrid
recyclerView.post {
if (gridAdapter.itemCount > 0) {
val targetPosition = anchorPosition.coerceIn(0, gridAdapter.itemCount - 1)
recyclerView.scrollToPosition(targetPosition)
gridAnchorPosition = targetPosition
}
}
}
private fun updateTabUi() {
if (isListMode) {
binding.ivTabList.setImageResource(R.drawable.ic_community_list_selected)
binding.ivTabGrid.setImageResource(R.drawable.ic_community_grid)
binding.vTabListIndicator.visibility = View.VISIBLE
binding.vTabGridIndicator.visibility = View.INVISIBLE
} else {
binding.ivTabList.setImageResource(R.drawable.ic_community_list)
binding.ivTabGrid.setImageResource(R.drawable.ic_community_grid_selected)
binding.vTabListIndicator.visibility = View.INVISIBLE
binding.vTabGridIndicator.visibility = View.VISIBLE
}
}
private fun updateGridItem(updatedPost: GetCommunityPostListResponse) {
val index = gridAdapter.items.indexOfFirst { it.postId == updatedPost.postId }
if (index >= 0) {
gridAdapter.items[index] = updatedPost
gridAdapter.notifyItemChanged(index)
}
}

View File

@@ -0,0 +1,86 @@
package kr.co.vividnext.sodalive.explorer.profile.creator_community.all
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemCreatorCommunityAllGridBinding
import kr.co.vividnext.sodalive.explorer.profile.creator_community.GetCommunityPostListResponse
import kr.co.vividnext.sodalive.extensions.loadUrl
class CreatorCommunityAllGridAdapter(
private val itemSize: Int,
private val onClickItem: (Int) -> Unit
) : RecyclerView.Adapter<CreatorCommunityAllGridAdapter.ViewHolder>() {
companion object {
private const val CONTENT_PREVIEW_MAX_LENGTH = 24
}
val items = mutableListOf<GetCommunityPostListResponse>()
inner class ViewHolder(
private val binding: ItemCreatorCommunityAllGridBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetCommunityPostListResponse) {
val lp = binding.root.layoutParams
lp.width = itemSize
lp.height = itemSize
binding.root.layoutParams = lp
val isPaidLocked = item.price > 0 && !item.existOrdered
val hasImage = !item.imageUrl.isNullOrBlank()
when {
isPaidLocked -> {
binding.ivGridImage.visibility = View.GONE
binding.tvGridText.visibility = View.GONE
binding.ivGridLock.visibility = View.VISIBLE
}
hasImage -> {
binding.ivGridImage.visibility = View.VISIBLE
binding.tvGridText.visibility = View.GONE
binding.ivGridLock.visibility = View.GONE
binding.ivGridImage.loadUrl(item.imageUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
}
}
else -> {
binding.ivGridImage.visibility = View.GONE
binding.tvGridText.visibility = View.VISIBLE
binding.ivGridLock.visibility = View.GONE
binding.tvGridText.text = item.content
.replace("\n", " ")
.trim()
.take(CONTENT_PREVIEW_MAX_LENGTH)
}
}
binding.root.setOnClickListener {
val position = bindingAdapterPosition
if (position != RecyclerView.NO_POSITION) {
onClickItem(position)
}
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemCreatorCommunityAllGridBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun getItemCount() = items.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 702 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 B

View File

@@ -9,8 +9,91 @@
android:id="@+id/toolbar"
layout="@layout/detail_toolbar" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_creator_community"
<LinearLayout
android:id="@+id/ll_mode_tab"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
android:layout_height="52dp"
android:background="@color/black"
android:baselineAligned="false"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/ll_tab_list"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_tab_list"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:contentDescription="@null"
android:src="@drawable/ic_community_list_selected" />
</FrameLayout>
<View
android:id="@+id/v_tab_list_indicator"
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="#FFFFFF" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_tab_grid"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_tab_grid"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:contentDescription="@null"
android:src="@drawable/ic_community_grid" />
</FrameLayout>
<View
android:id="@+id/v_tab_grid_indicator"
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="#FFFFFF"
android:visibility="invisible" />
</LinearLayout>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#909090" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_creator_community"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_creator_community_grid"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="invisible" />
</FrameLayout>
</LinearLayout>

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_round_corner_5_3_333333"
android:clipToOutline="true"
android:outlineProvider="background">
<ImageView
android:id="@+id/iv_grid_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@null"
android:scaleType="centerCrop"
android:visibility="gone"
tools:src="@drawable/ic_place_holder" />
<TextView
android:id="@+id/tv_grid_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:ellipsize="end"
android:fontFamily="@font/medium"
android:gravity="center"
android:maxLines="3"
android:padding="10dp"
android:textColor="@color/color_bbbbbb"
android:textSize="13.3sp"
android:visibility="gone"
tools:text="텍스트만 있는 게시물" />
<ImageView
android:id="@+id/iv_grid_lock"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:contentDescription="@null"
android:src="@drawable/ic_lock_bb"
android:visibility="gone" />
</FrameLayout>