feat(character-gallery): 갤러리 탭 UI/페이징 및 API 연동, DI 적용
- API: CharacterApi에 이미지 리스트 API 추가(characterId, page, size) - VM: 페이징(loadInitial/loadNext), 요청 중복 방지, 마지막 페이지 판단, 누적 리스트 관리 - UI: ProgressBar(배경 #37474F/진행 #3BB9F1, radius 999dp, 비활성) + 좌/우 텍스트 구성 - Grid 3열 + 2dp 간격, item 4:5 비율, 잠금/구매 버튼 UI 적용 - UX: tv_ratio_right에서 ownedCount만 #FDD453로 강조(white 대비)
This commit is contained in:
@@ -2,10 +2,12 @@ package kr.co.vividnext.sodalive.chat.character
|
|||||||
|
|
||||||
import io.reactivex.rxjava3.core.Single
|
import io.reactivex.rxjava3.core.Single
|
||||||
import kr.co.vividnext.sodalive.chat.character.detail.detail.CharacterDetailResponse
|
import kr.co.vividnext.sodalive.chat.character.detail.detail.CharacterDetailResponse
|
||||||
|
import kr.co.vividnext.sodalive.chat.character.detail.gallery.CharacterImageListResponse
|
||||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
import retrofit2.http.Header
|
import retrofit2.http.Header
|
||||||
import retrofit2.http.Path
|
import retrofit2.http.Path
|
||||||
|
import retrofit2.http.Query
|
||||||
|
|
||||||
interface CharacterApi {
|
interface CharacterApi {
|
||||||
@GET("/api/chat/character/main")
|
@GET("/api/chat/character/main")
|
||||||
@@ -18,4 +20,12 @@ interface CharacterApi {
|
|||||||
@Header("Authorization") authHeader: String,
|
@Header("Authorization") authHeader: String,
|
||||||
@Path("characterId") characterId: Long
|
@Path("characterId") characterId: Long
|
||||||
): Single<ApiResponse<CharacterDetailResponse>>
|
): Single<ApiResponse<CharacterDetailResponse>>
|
||||||
|
|
||||||
|
@GET("/api/chat/character/image/list")
|
||||||
|
fun getCharacterImageList(
|
||||||
|
@Header("Authorization") authHeader: String,
|
||||||
|
@Query("characterId") characterId: Long,
|
||||||
|
@Query("page") page: Int,
|
||||||
|
@Query("size") size: Int
|
||||||
|
): Single<ApiResponse<CharacterImageListResponse>>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package kr.co.vividnext.sodalive.chat.character.detail.gallery
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import kr.co.vividnext.sodalive.databinding.ItemCharacterGalleryBinding
|
||||||
|
|
||||||
|
class CharacterGalleryAdapter(
|
||||||
|
private var items: List<CharacterImageListItemResponse> = emptyList()
|
||||||
|
) : RecyclerView.Adapter<CharacterGalleryAdapter.ViewHolder>() {
|
||||||
|
|
||||||
|
inner class ViewHolder(
|
||||||
|
private val binding: ItemCharacterGalleryBinding
|
||||||
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
fun bind(item: CharacterImageListItemResponse) {
|
||||||
|
Glide.with(binding.ivImage)
|
||||||
|
.load(item.imageUrl)
|
||||||
|
.into(binding.ivImage)
|
||||||
|
|
||||||
|
if (item.isOwned) {
|
||||||
|
binding.llLock.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
binding.llLock.visibility = View.VISIBLE
|
||||||
|
binding.tvPrice.text = item.imagePriceCan.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val binding =
|
||||||
|
ItemCharacterGalleryBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
|
return ViewHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
holder.bind(items[position])
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = items.size
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
fun submitItems(newItems: List<CharacterImageListItemResponse>) {
|
||||||
|
items = newItems
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,103 @@
|
|||||||
package kr.co.vividnext.sodalive.chat.character.detail.gallery
|
package kr.co.vividnext.sodalive.chat.character.detail.gallery
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.core.graphics.toColorInt
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import kr.co.vividnext.sodalive.base.BaseFragment
|
import kr.co.vividnext.sodalive.base.BaseFragment
|
||||||
|
import kr.co.vividnext.sodalive.chat.character.detail.CharacterDetailActivity.Companion.EXTRA_CHARACTER_ID
|
||||||
|
import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration
|
||||||
import kr.co.vividnext.sodalive.databinding.FragmentCharacterGalleryBinding
|
import kr.co.vividnext.sodalive.databinding.FragmentCharacterGalleryBinding
|
||||||
|
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 캐릭터 상세 - 갤러리 탭 (빈 화면)
|
* 캐릭터 상세 - 갤러리 탭
|
||||||
*/
|
*/
|
||||||
class CharacterGalleryFragment : BaseFragment<FragmentCharacterGalleryBinding>(
|
class CharacterGalleryFragment : BaseFragment<FragmentCharacterGalleryBinding>(
|
||||||
FragmentCharacterGalleryBinding::inflate
|
FragmentCharacterGalleryBinding::inflate
|
||||||
) {
|
) {
|
||||||
|
private val viewModel: CharacterGalleryViewModel by viewModel()
|
||||||
|
private val adapter = CharacterGalleryAdapter()
|
||||||
|
|
||||||
|
private val characterId: Long by lazy {
|
||||||
|
arguments?.getLong("arg_character_id")
|
||||||
|
?: requireActivity().intent.getLongExtra(EXTRA_CHARACTER_ID, 0L)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
// 추후 갤러리 콘텐츠 추가 예정
|
setupRecyclerView()
|
||||||
|
observeState()
|
||||||
|
viewModel.loadInitial(characterId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupRecyclerView() {
|
||||||
|
val layoutManager = GridLayoutManager(requireContext(), 3)
|
||||||
|
binding.rvGallery.layoutManager = layoutManager
|
||||||
|
if (binding.rvGallery.itemDecorationCount == 0) {
|
||||||
|
binding.rvGallery.addItemDecoration(
|
||||||
|
GridSpacingItemDecoration(3, 2f.dpToPx().toInt(), true)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
binding.rvGallery.adapter = adapter
|
||||||
|
|
||||||
|
binding.rvGallery.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
|
super.onScrolled(recyclerView, dx, dy)
|
||||||
|
if (dy <= 0) return
|
||||||
|
val totalItemCount = layoutManager.itemCount
|
||||||
|
val lastVisible = layoutManager.findLastVisibleItemPosition()
|
||||||
|
if (lastVisible >= totalItemCount - 6) {
|
||||||
|
viewModel.loadNext()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
|
private fun observeState() {
|
||||||
|
viewModel.uiState.observe(viewLifecycleOwner) { state ->
|
||||||
|
binding.tvEmptyGallery.visibility = if (state.items.isEmpty() && !state.isLoading) {
|
||||||
|
View.VISIBLE
|
||||||
|
} else {
|
||||||
|
View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.items.isNotEmpty() && !state.isLoading) {
|
||||||
|
binding.rvGallery.visibility = View.VISIBLE
|
||||||
|
binding.clRatio.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
val percent = (state.ratio * 100).toInt()
|
||||||
|
// 좌측 라벨은 고정("보유중"), 우측은 보유/전체 개수(ownedCount만 강조 색상 적용)
|
||||||
|
binding.tvRatioLeft.text = "$percent% 보유중"
|
||||||
|
|
||||||
|
val ownedStr = state.ownedCount.toString()
|
||||||
|
val totalStr = state.totalCount.toString()
|
||||||
|
val fullText = "$ownedStr / ${totalStr}개"
|
||||||
|
val spannable = android.text.SpannableString(fullText)
|
||||||
|
val ownedColor = "#FDD453".toColorInt()
|
||||||
|
spannable.setSpan(
|
||||||
|
android.text.style.ForegroundColorSpan(ownedColor),
|
||||||
|
/* start */ 0,
|
||||||
|
/* end */ ownedStr.length,
|
||||||
|
android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
|
)
|
||||||
|
// 나머지는 TextView의 기본 색상(white)을 사용
|
||||||
|
binding.tvRatioRight.text = spannable
|
||||||
|
|
||||||
|
// 슬라이더(ProgressBar) 값 설정: 0~100
|
||||||
|
binding.progressRatio.progress = percent
|
||||||
|
|
||||||
|
adapter.submitItems(state.items)
|
||||||
|
} else {
|
||||||
|
binding.rvGallery.visibility = View.VISIBLE
|
||||||
|
binding.clRatio.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
state.error?.let { showToast(it) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package kr.co.vividnext.sodalive.chat.character.detail.gallery
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.chat.character.CharacterApi
|
||||||
|
|
||||||
|
class CharacterGalleryRepository(
|
||||||
|
private val characterApi: CharacterApi
|
||||||
|
) {
|
||||||
|
fun getCharacterImageList(token: String, characterId: Long, page: Int, size: Int) =
|
||||||
|
characterApi.getCharacterImageList(authHeader = token, characterId = characterId, page = page, size = size)
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package kr.co.vividnext.sodalive.chat.character.detail.gallery
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import com.orhanobut.logger.Logger
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
|
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||||
|
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||||
|
|
||||||
|
class CharacterGalleryViewModel(
|
||||||
|
private val repository: CharacterGalleryRepository
|
||||||
|
) : BaseViewModel() {
|
||||||
|
|
||||||
|
data class UiState(
|
||||||
|
val totalCount: Long = 0L,
|
||||||
|
val ownedCount: Long = 0L,
|
||||||
|
val ratio: Float = 0f, // 0.0 ~ 1.0
|
||||||
|
val items: List<CharacterImageListItemResponse> = emptyList(),
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val error: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
private val _uiState = MutableLiveData(UiState())
|
||||||
|
val uiState: LiveData<UiState> get() = _uiState
|
||||||
|
|
||||||
|
private var characterId: Long = 0L
|
||||||
|
private var currentPage: Int = 0
|
||||||
|
private val pageSize: Int = 20
|
||||||
|
private var isLastPage: Boolean = false
|
||||||
|
private var isRequesting: Boolean = false
|
||||||
|
private val accumulatedItems = mutableListOf<CharacterImageListItemResponse>()
|
||||||
|
|
||||||
|
fun loadInitial(characterId: Long) {
|
||||||
|
// 상태 초기화
|
||||||
|
this.characterId = characterId
|
||||||
|
currentPage = 0
|
||||||
|
isLastPage = false
|
||||||
|
isRequesting = false
|
||||||
|
accumulatedItems.clear()
|
||||||
|
_uiState.value = UiState(isLoading = true)
|
||||||
|
request(page = currentPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadNext() {
|
||||||
|
if (isRequesting || isLastPage) return
|
||||||
|
request(page = currentPage + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun request(page: Int) {
|
||||||
|
val token = "Bearer ${SharedPreferenceManager.token}"
|
||||||
|
isRequesting = true
|
||||||
|
compositeDisposable.add(
|
||||||
|
repository.getCharacterImageList(token = token, characterId = characterId, page = page, size = pageSize)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe({ response ->
|
||||||
|
val success = response.success
|
||||||
|
val data = response.data
|
||||||
|
if (success && data != null) {
|
||||||
|
// 누적 처리
|
||||||
|
val newItems = data.items
|
||||||
|
if (page == 0) accumulatedItems.clear()
|
||||||
|
accumulatedItems.addAll(newItems)
|
||||||
|
|
||||||
|
val total = data.totalCount
|
||||||
|
val owned = data.ownedCount
|
||||||
|
val ratio = if (total > 0) owned.toFloat() / total.toFloat() else 0f
|
||||||
|
|
||||||
|
// 마지막 페이지 판단: 새 항목이 pageSize보다 적거나, 누적 >= total
|
||||||
|
isLastPage = newItems.size < pageSize || accumulatedItems.size.toLong() >= total
|
||||||
|
currentPage = page
|
||||||
|
|
||||||
|
_uiState.value = UiState(
|
||||||
|
totalCount = total,
|
||||||
|
ownedCount = owned,
|
||||||
|
ratio = ratio,
|
||||||
|
items = accumulatedItems.toList(),
|
||||||
|
isLoading = false,
|
||||||
|
error = null
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
_uiState.value = _uiState.value?.copy(
|
||||||
|
isLoading = false,
|
||||||
|
error = response.message ?: "갤러리 정보를 불러오지 못했습니다."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
isRequesting = false
|
||||||
|
}, { throwable ->
|
||||||
|
Logger.e(throwable, throwable.message ?: "")
|
||||||
|
_uiState.value = _uiState.value?.copy(
|
||||||
|
isLoading = false,
|
||||||
|
error = "네트워크 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."
|
||||||
|
)
|
||||||
|
isRequesting = false
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package kr.co.vividnext.sodalive.chat.character.detail.gallery
|
||||||
|
|
||||||
|
import androidx.annotation.Keep
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
data class CharacterImageListItemResponse(
|
||||||
|
@SerializedName("id") val id: Long,
|
||||||
|
@SerializedName("imageUrl") val imageUrl: String,
|
||||||
|
@SerializedName("isOwned") val isOwned: Boolean,
|
||||||
|
@SerializedName("imagePriceCan") val imagePriceCan: Long
|
||||||
|
)
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
data class CharacterImageListResponse(
|
||||||
|
@SerializedName("totalCount") val totalCount: Long,
|
||||||
|
@SerializedName("ownedCount") val ownedCount: Long,
|
||||||
|
@SerializedName("items") val items: List<CharacterImageListItemResponse>
|
||||||
|
)
|
||||||
@@ -71,6 +71,8 @@ import kr.co.vividnext.sodalive.chat.character.comment.CharacterCommentApi
|
|||||||
import kr.co.vividnext.sodalive.chat.character.comment.CharacterCommentRepository
|
import kr.co.vividnext.sodalive.chat.character.comment.CharacterCommentRepository
|
||||||
import kr.co.vividnext.sodalive.chat.character.detail.detail.CharacterDetailRepository
|
import kr.co.vividnext.sodalive.chat.character.detail.detail.CharacterDetailRepository
|
||||||
import kr.co.vividnext.sodalive.chat.character.detail.detail.CharacterDetailViewModel
|
import kr.co.vividnext.sodalive.chat.character.detail.detail.CharacterDetailViewModel
|
||||||
|
import kr.co.vividnext.sodalive.chat.character.detail.gallery.CharacterGalleryRepository
|
||||||
|
import kr.co.vividnext.sodalive.chat.character.detail.gallery.CharacterGalleryViewModel
|
||||||
import kr.co.vividnext.sodalive.chat.talk.TalkApi
|
import kr.co.vividnext.sodalive.chat.talk.TalkApi
|
||||||
import kr.co.vividnext.sodalive.chat.talk.TalkTabRepository
|
import kr.co.vividnext.sodalive.chat.talk.TalkTabRepository
|
||||||
import kr.co.vividnext.sodalive.chat.talk.TalkTabViewModel
|
import kr.co.vividnext.sodalive.chat.talk.TalkTabViewModel
|
||||||
@@ -358,6 +360,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
|
|||||||
viewModel { HomeViewModel(get(), get()) }
|
viewModel { HomeViewModel(get(), get()) }
|
||||||
viewModel { CharacterTabViewModel(get()) }
|
viewModel { CharacterTabViewModel(get()) }
|
||||||
viewModel { CharacterDetailViewModel(get()) }
|
viewModel { CharacterDetailViewModel(get()) }
|
||||||
|
viewModel { CharacterGalleryViewModel(get()) }
|
||||||
viewModel { TalkTabViewModel(get()) }
|
viewModel { TalkTabViewModel(get()) }
|
||||||
viewModel { kr.co.vividnext.sodalive.chat.character.comment.CharacterCommentListViewModel(get()) }
|
viewModel { kr.co.vividnext.sodalive.chat.character.comment.CharacterCommentListViewModel(get()) }
|
||||||
viewModel { kr.co.vividnext.sodalive.chat.character.comment.CharacterCommentReplyViewModel(get()) }
|
viewModel { kr.co.vividnext.sodalive.chat.character.comment.CharacterCommentReplyViewModel(get()) }
|
||||||
@@ -407,6 +410,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
|
|||||||
factory { HomeRepository(get()) }
|
factory { HomeRepository(get()) }
|
||||||
factory { CharacterTabRepository(get()) }
|
factory { CharacterTabRepository(get()) }
|
||||||
factory { CharacterDetailRepository(get(), get()) }
|
factory { CharacterDetailRepository(get(), get()) }
|
||||||
|
factory { CharacterGalleryRepository(get()) }
|
||||||
factory { TalkTabRepository(get()) }
|
factory { TalkTabRepository(get()) }
|
||||||
factory { CharacterCommentRepository(get()) }
|
factory { CharacterCommentRepository(get()) }
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
app/src/main/res/drawable-mdpi/ic_new_lock.png
Normal file
BIN
app/src/main/res/drawable-mdpi/ic_new_lock.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 359 B |
6
app/src/main/res/drawable/bg_buy_button.xml
Normal file
6
app/src/main/res/drawable/bg_buy_button.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||||
|
<solid android:color="#B5E7FA" />
|
||||||
|
<corners android:radius="30dp" />
|
||||||
|
<stroke android:width="1dp" android:color="#3BB9F1" />
|
||||||
|
</shape>
|
||||||
17
app/src/main/res/drawable/bg_gallery_progress.xml
Normal file
17
app/src/main/res/drawable/bg_gallery_progress.xml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:id="@android:id/background">
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<corners android:radius="999dp" />
|
||||||
|
<solid android:color="#37474F" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item android:id="@android:id/progress">
|
||||||
|
<clip>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<corners android:radius="999dp" />
|
||||||
|
<solid android:color="#3BB9F1" />
|
||||||
|
</shape>
|
||||||
|
</clip>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
||||||
@@ -1,17 +1,87 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout 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"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="@color/color_131313"
|
android:background="@color/color_131313"
|
||||||
android:padding="24dp">
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<!-- 상단: 레이블(좌/우) + 슬라이더(ProgressBar) -->
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/cl_ratio"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progress_ratio"
|
||||||
|
style="@android:style/Widget.ProgressBar.Horizontal"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="10dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:clickable="false"
|
||||||
|
android:enabled="false"
|
||||||
|
android:focusable="false"
|
||||||
|
android:indeterminate="false"
|
||||||
|
android:max="100"
|
||||||
|
android:progress="0"
|
||||||
|
android:progressDrawable="@drawable/bg_gallery_progress"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/tv_ratio_left" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_ratio_left"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="@font/pretendard_bold"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="18sp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="40% 보유중" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_ratio_right"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="@font/pretendard_regular"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="16sp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="0 / 0개" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<!-- RecyclerView: 3열 그리드 -->
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/rv_gallery"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:overScrollMode="never"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||||
|
app:spanCount="3" />
|
||||||
|
|
||||||
|
<!-- 빈 상태 안내 -->
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tv_empty_gallery"
|
android:id="@+id/tv_empty_gallery"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="0dp"
|
||||||
android:text="갤러리 콘텐츠가 없습니다."
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:fontFamily="@font/pretendard_bold"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="갤러리가 비어있습니다"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="16sp"
|
android:textSize="20sp"
|
||||||
android:layout_gravity="center" />
|
android:visibility="gone" />
|
||||||
|
|
||||||
</FrameLayout>
|
</LinearLayout>
|
||||||
|
|||||||
66
app/src/main/res/layout/item_character_gallery.xml
Normal file
66
app/src/main/res/layout/item_character_gallery.xml
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<?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"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<!-- 이미지 컨테이너: 4:5 비율(800:1000) -->
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/iv_image"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
app:layout_constraintDimensionRatio="4:5"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/ll_lock"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/iv_image"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/iv_image"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/iv_image"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/iv_image">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
android:src="@drawable/ic_new_lock" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/btn_buy"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="10dp"
|
||||||
|
android:background="@drawable/bg_buy_button"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:paddingHorizontal="10dp"
|
||||||
|
android:paddingVertical="3dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="16dp"
|
||||||
|
android:layout_height="16dp"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
android:src="@drawable/ic_can" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_price"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="@font/pretendard_bold"
|
||||||
|
android:textColor="#263238"
|
||||||
|
android:textSize="16sp"
|
||||||
|
tools:text="0" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
Reference in New Issue
Block a user