검색 UI 추가
This commit is contained in:
		| @@ -35,8 +35,8 @@ android { | |||||||
|         applicationId "kr.co.vividnext.sodalive" |         applicationId "kr.co.vividnext.sodalive" | ||||||
|         minSdk 23 |         minSdk 23 | ||||||
|         targetSdk 34 |         targetSdk 34 | ||||||
|         versionCode 154 |         versionCode 156 | ||||||
|         versionName "1.31.1" |         versionName "1.32.0" | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     buildTypes { |     buildTypes { | ||||||
|   | |||||||
| @@ -174,6 +174,8 @@ | |||||||
|         <activity android:name=".audio_content.main.v2.series.origianl_audio_drama.OriginalAudioDramaContentAllActivity" /> |         <activity android:name=".audio_content.main.v2.series.origianl_audio_drama.OriginalAudioDramaContentAllActivity" /> | ||||||
|         <activity android:name=".audio_content.main.v2.series.completed.CompletedSeriesActivity" /> |         <activity android:name=".audio_content.main.v2.series.completed.CompletedSeriesActivity" /> | ||||||
|  |  | ||||||
|  |         <activity android:name=".search.SearchActivity" /> | ||||||
|  |  | ||||||
|         <activity android:name=".mypage.alarm.AlarmListActivity" /> |         <activity android:name=".mypage.alarm.AlarmListActivity" /> | ||||||
|         <activity android:name=".mypage.alarm.AddAlarmActivity" /> |         <activity android:name=".mypage.alarm.AddAlarmActivity" /> | ||||||
|         <activity android:name=".mypage.alarm.select_audio_content.AlarmSelectAudioContentActivity" /> |         <activity android:name=".mypage.alarm.select_audio_content.AlarmSelectAudioContentActivity" /> | ||||||
|   | |||||||
| @@ -46,6 +46,7 @@ import kr.co.vividnext.sodalive.extensions.dpToPx | |||||||
| import kr.co.vividnext.sodalive.live.event_banner.EventBannerAdapter | import kr.co.vividnext.sodalive.live.event_banner.EventBannerAdapter | ||||||
| import kr.co.vividnext.sodalive.main.MainActivity | import kr.co.vividnext.sodalive.main.MainActivity | ||||||
| import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity | import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity | ||||||
|  | import kr.co.vividnext.sodalive.search.SearchActivity | ||||||
| import kr.co.vividnext.sodalive.settings.event.EventDetailActivity | import kr.co.vividnext.sodalive.settings.event.EventDetailActivity | ||||||
| import kr.co.vividnext.sodalive.settings.notice.NoticeDetailActivity | import kr.co.vividnext.sodalive.settings.notice.NoticeDetailActivity | ||||||
| import kr.co.vividnext.sodalive.settings.notification.MemberRole | import kr.co.vividnext.sodalive.settings.notification.MemberRole | ||||||
| @@ -154,6 +155,12 @@ class AudioContentMainTabHomeFragment : BaseFragment<FragmentAudioContentMainTab | |||||||
|         if (SharedPreferenceManager.token.isNotBlank()) { |         if (SharedPreferenceManager.token.isNotBlank()) { | ||||||
|             binding.flSearch.visibility = View.VISIBLE |             binding.flSearch.visibility = View.VISIBLE | ||||||
|             binding.flSearch.setOnClickListener { |             binding.flSearch.setOnClickListener { | ||||||
|  |                 startActivity( | ||||||
|  |                     Intent( | ||||||
|  |                         requireContext(), | ||||||
|  |                         SearchActivity::class.java | ||||||
|  |                     ) | ||||||
|  |                 ) | ||||||
|             } |             } | ||||||
|         } else { |         } else { | ||||||
|             binding.flSearch.visibility = View.GONE |             binding.flSearch.visibility = View.GONE | ||||||
|   | |||||||
| @@ -137,6 +137,9 @@ import kr.co.vividnext.sodalive.mypage.service_center.ServiceCenterViewModel | |||||||
| import kr.co.vividnext.sodalive.network.TokenAuthenticator | import kr.co.vividnext.sodalive.network.TokenAuthenticator | ||||||
| import kr.co.vividnext.sodalive.report.ReportApi | import kr.co.vividnext.sodalive.report.ReportApi | ||||||
| import kr.co.vividnext.sodalive.report.ReportRepository | import kr.co.vividnext.sodalive.report.ReportRepository | ||||||
|  | import kr.co.vividnext.sodalive.search.SearchApi | ||||||
|  | import kr.co.vividnext.sodalive.search.SearchRepository | ||||||
|  | import kr.co.vividnext.sodalive.search.SearchViewModel | ||||||
| import kr.co.vividnext.sodalive.settings.ContentSettingsViewModel | import kr.co.vividnext.sodalive.settings.ContentSettingsViewModel | ||||||
| import kr.co.vividnext.sodalive.settings.SettingsViewModel | import kr.co.vividnext.sodalive.settings.SettingsViewModel | ||||||
| import kr.co.vividnext.sodalive.settings.event.EventApi | import kr.co.vividnext.sodalive.settings.event.EventApi | ||||||
| @@ -229,6 +232,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { | |||||||
|         single { ApiBuilder().build(get(), PlaylistApi::class.java) } |         single { ApiBuilder().build(get(), PlaylistApi::class.java) } | ||||||
|         single { ApiBuilder().build(get(), AuditionApi::class.java) } |         single { ApiBuilder().build(get(), AuditionApi::class.java) } | ||||||
|         single { ApiBuilder().build(get(), AdTrackingApi::class.java) } |         single { ApiBuilder().build(get(), AdTrackingApi::class.java) } | ||||||
|  |         single { ApiBuilder().build(get(), SearchApi::class.java) } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private val viewModelModule = module { |     private val viewModelModule = module { | ||||||
| @@ -323,6 +327,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { | |||||||
|         viewModel { IntroduceCreatorViewModel(get()) } |         viewModel { IntroduceCreatorViewModel(get()) } | ||||||
|         viewModel { CompletedSeriesViewModel(get()) } |         viewModel { CompletedSeriesViewModel(get()) } | ||||||
|         viewModel { AlarmContentAllViewModel(get()) } |         viewModel { AlarmContentAllViewModel(get()) } | ||||||
|  |         viewModel { SearchViewModel(get()) } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private val repositoryModule = module { |     private val repositoryModule = module { | ||||||
| @@ -363,6 +368,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { | |||||||
|         factory { AudioContentMainTabFreeRepository(get()) } |         factory { AudioContentMainTabFreeRepository(get()) } | ||||||
|         factory { OriginalAudioDramaContentAllRepository(get()) } |         factory { OriginalAudioDramaContentAllRepository(get()) } | ||||||
|         factory { AdTrackingRepository(get()) } |         factory { AdTrackingRepository(get()) } | ||||||
|  |         factory { SearchRepository(get()) } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private val moduleList = listOf( |     private val moduleList = listOf( | ||||||
|   | |||||||
| @@ -0,0 +1,556 @@ | |||||||
|  | package kr.co.vividnext.sodalive.search | ||||||
|  |  | ||||||
|  | import android.annotation.SuppressLint | ||||||
|  | import android.app.Service | ||||||
|  | import android.content.Intent | ||||||
|  | import android.graphics.Rect | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.os.Handler | ||||||
|  | import android.os.Looper | ||||||
|  | import android.view.View | ||||||
|  | import android.view.inputmethod.InputMethodManager | ||||||
|  | import android.widget.Toast | ||||||
|  | import androidx.annotation.OptIn | ||||||
|  | import androidx.media3.common.util.UnstableApi | ||||||
|  | import androidx.recyclerview.widget.LinearLayoutManager | ||||||
|  | import androidx.recyclerview.widget.RecyclerView | ||||||
|  | import com.google.android.material.tabs.TabLayout | ||||||
|  | import com.jakewharton.rxbinding4.widget.textChanges | ||||||
|  | import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers | ||||||
|  | import io.reactivex.rxjava3.schedulers.Schedulers | ||||||
|  | import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity | ||||||
|  | import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity | ||||||
|  | import kr.co.vividnext.sodalive.base.BaseActivity | ||||||
|  | import kr.co.vividnext.sodalive.common.Constants | ||||||
|  | import kr.co.vividnext.sodalive.common.LoadingDialog | ||||||
|  | import kr.co.vividnext.sodalive.databinding.ActivitySearchBinding | ||||||
|  | import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity | ||||||
|  | import kr.co.vividnext.sodalive.extensions.dpToPx | ||||||
|  | import org.koin.android.ext.android.inject | ||||||
|  | import java.util.concurrent.TimeUnit | ||||||
|  |  | ||||||
|  | class SearchActivity : BaseActivity<ActivitySearchBinding>(ActivitySearchBinding::inflate) { | ||||||
|  |  | ||||||
|  |     private val viewModel: SearchViewModel by inject() | ||||||
|  |  | ||||||
|  |     private lateinit var imm: InputMethodManager | ||||||
|  |     private lateinit var loadingDialog: LoadingDialog | ||||||
|  |  | ||||||
|  |     private val handler = Handler(Looper.getMainLooper()) | ||||||
|  |  | ||||||
|  |     private lateinit var unifiedChannelAdapter: SearchAdapter | ||||||
|  |     private lateinit var unifiedContentAdapter: SearchAdapter | ||||||
|  |     private lateinit var unifiedSeriesAdapter: SearchAdapter | ||||||
|  |  | ||||||
|  |     private lateinit var channelAdapter: SearchAdapter | ||||||
|  |     private lateinit var contentAdapter: SearchAdapter | ||||||
|  |     private lateinit var seriesAdapter: SearchAdapter | ||||||
|  |  | ||||||
|  |     override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|  |         super.onCreate(savedInstanceState) | ||||||
|  |         imm = getSystemService( | ||||||
|  |             Service.INPUT_METHOD_SERVICE | ||||||
|  |         ) as InputMethodManager | ||||||
|  |  | ||||||
|  |         binding.etSearch.requestFocus() | ||||||
|  |         handler.postDelayed({ | ||||||
|  |             val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager | ||||||
|  |             imm.showSoftInput(binding.etSearch, InputMethodManager.SHOW_IMPLICIT) | ||||||
|  |         }, 500) | ||||||
|  |  | ||||||
|  |         bindData() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onPause() { | ||||||
|  |         hideKeyboard() | ||||||
|  |         super.onPause() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun setupView() { | ||||||
|  |         loadingDialog = LoadingDialog(this, layoutInflater) | ||||||
|  |         binding.ivBack.setOnClickListener { finish() } | ||||||
|  |  | ||||||
|  |         setupTabs() | ||||||
|  |         setupUnifiedView() | ||||||
|  |         setupChannelListView() | ||||||
|  |         setupContentListView() | ||||||
|  |         setupSeriesListView() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun setupChannelListView() { | ||||||
|  |         channelAdapter = SearchAdapter { clickItem(it) } | ||||||
|  |  | ||||||
|  |         binding.rvCreator.layoutManager = LinearLayoutManager( | ||||||
|  |             this, | ||||||
|  |             LinearLayoutManager.VERTICAL, | ||||||
|  |             false | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         binding.rvCreator.addItemDecoration(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() | ||||||
|  |                 outRect.top = 13.3f.dpToPx().toInt() | ||||||
|  |                 outRect.bottom = 13.3f.dpToPx().toInt() | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |         binding.rvCreator.addOnScrollListener(object : RecyclerView.OnScrollListener() { | ||||||
|  |             override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { | ||||||
|  |                 super.onScrolled(recyclerView, dx, dy) | ||||||
|  |  | ||||||
|  |                 val layoutManager = recyclerView.layoutManager as? LinearLayoutManager | ||||||
|  |                 if ( | ||||||
|  |                     layoutManager != null && | ||||||
|  |                     layoutManager | ||||||
|  |                         .findLastCompletelyVisibleItemPosition() == channelAdapter.itemCount - 1 | ||||||
|  |                 ) { | ||||||
|  |                     viewModel.searchCreatorList() | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |         binding.rvCreator.adapter = channelAdapter | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun setupContentListView() { | ||||||
|  |         contentAdapter = SearchAdapter { clickItem(it) } | ||||||
|  |  | ||||||
|  |         binding.rvContent.layoutManager = LinearLayoutManager( | ||||||
|  |             this, | ||||||
|  |             LinearLayoutManager.VERTICAL, | ||||||
|  |             false | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         binding.rvContent.addItemDecoration(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() | ||||||
|  |                 outRect.top = 13.3f.dpToPx().toInt() | ||||||
|  |                 outRect.bottom = 13.3f.dpToPx().toInt() | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |         binding.rvContent.addOnScrollListener(object : RecyclerView.OnScrollListener() { | ||||||
|  |             override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { | ||||||
|  |                 super.onScrolled(recyclerView, dx, dy) | ||||||
|  |  | ||||||
|  |                 val layoutManager = recyclerView.layoutManager as? LinearLayoutManager | ||||||
|  |                 if ( | ||||||
|  |                     layoutManager != null && | ||||||
|  |                     layoutManager | ||||||
|  |                         .findLastCompletelyVisibleItemPosition() == contentAdapter.itemCount - 1 | ||||||
|  |                 ) { | ||||||
|  |                     viewModel.searchContentList() | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |         binding.rvContent.adapter = contentAdapter | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun setupSeriesListView() { | ||||||
|  |         seriesAdapter = SearchAdapter { clickItem(it) } | ||||||
|  |  | ||||||
|  |         binding.rvSeries.layoutManager = LinearLayoutManager( | ||||||
|  |             this, | ||||||
|  |             LinearLayoutManager.VERTICAL, | ||||||
|  |             false | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         binding.rvSeries.addItemDecoration(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() | ||||||
|  |                 outRect.top = 13.3f.dpToPx().toInt() | ||||||
|  |                 outRect.bottom = 13.3f.dpToPx().toInt() | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |         binding.rvSeries.addOnScrollListener(object : RecyclerView.OnScrollListener() { | ||||||
|  |             override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { | ||||||
|  |                 super.onScrolled(recyclerView, dx, dy) | ||||||
|  |  | ||||||
|  |                 val layoutManager = recyclerView.layoutManager as? LinearLayoutManager | ||||||
|  |                 if ( | ||||||
|  |                     layoutManager != null && | ||||||
|  |                     layoutManager | ||||||
|  |                         .findLastCompletelyVisibleItemPosition() == seriesAdapter.itemCount - 1 | ||||||
|  |                 ) { | ||||||
|  |                     viewModel.searchSeriesList() | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |         binding.rvSeries.adapter = seriesAdapter | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun setupUnifiedView() { | ||||||
|  |         unifiedChannelAdapter = SearchAdapter { clickItem(it) } | ||||||
|  |         unifiedContentAdapter = SearchAdapter { clickItem(it) } | ||||||
|  |         unifiedSeriesAdapter = SearchAdapter { clickItem(it) } | ||||||
|  |  | ||||||
|  |         binding.tvMoreCreator.setOnClickListener { | ||||||
|  |             viewModel.changeTab(SearchViewModel.SearchPageTab.CREATOR) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         binding.tvMoreContent.setOnClickListener { | ||||||
|  |             viewModel.changeTab(SearchViewModel.SearchPageTab.CONTENT) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         binding.tvMoreSeries.setOnClickListener { | ||||||
|  |             viewModel.changeTab(SearchViewModel.SearchPageTab.SERIES) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         binding.rvUnifiedCreator.layoutManager = LinearLayoutManager( | ||||||
|  |             this, | ||||||
|  |             LinearLayoutManager.VERTICAL, | ||||||
|  |             false | ||||||
|  |         ) | ||||||
|  |         binding.rvUnifiedCreator.addItemDecoration(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() | ||||||
|  |                 outRect.top = 13.3f.dpToPx().toInt() | ||||||
|  |                 outRect.bottom = 13.3f.dpToPx().toInt() | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |         binding.rvUnifiedCreator.adapter = unifiedChannelAdapter | ||||||
|  |  | ||||||
|  |         binding.rvUnifiedContent.layoutManager = LinearLayoutManager( | ||||||
|  |             this, | ||||||
|  |             LinearLayoutManager.VERTICAL, | ||||||
|  |             false | ||||||
|  |         ) | ||||||
|  |         binding.rvUnifiedContent.addItemDecoration(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() | ||||||
|  |                 outRect.top = 13.3f.dpToPx().toInt() | ||||||
|  |                 outRect.bottom = 13.3f.dpToPx().toInt() | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |         binding.rvUnifiedContent.adapter = unifiedContentAdapter | ||||||
|  |  | ||||||
|  |         binding.rvUnifiedSeries.layoutManager = LinearLayoutManager( | ||||||
|  |             this, | ||||||
|  |             LinearLayoutManager.VERTICAL, | ||||||
|  |             false | ||||||
|  |         ) | ||||||
|  |         binding.rvUnifiedSeries.addItemDecoration(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() | ||||||
|  |                 outRect.top = 13.3f.dpToPx().toInt() | ||||||
|  |                 outRect.bottom = 13.3f.dpToPx().toInt() | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |         binding.rvUnifiedSeries.adapter = unifiedSeriesAdapter | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @OptIn(UnstableApi::class) | ||||||
|  |     private fun clickItem(item: SearchResponseItem) { | ||||||
|  |         hideKeyboard() | ||||||
|  |  | ||||||
|  |         startActivity( | ||||||
|  |             when (item.type) { | ||||||
|  |                 SearchResponseType.CREATOR -> { | ||||||
|  |                     Intent(applicationContext, UserProfileActivity::class.java).apply { | ||||||
|  |                         putExtra(Constants.EXTRA_USER_ID, item.id) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 SearchResponseType.CONTENT -> { | ||||||
|  |                     Intent(applicationContext, AudioContentDetailActivity::class.java).apply { | ||||||
|  |                         putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, item.id) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 SearchResponseType.SERIES -> { | ||||||
|  |                     Intent(applicationContext, SeriesDetailActivity::class.java).apply { | ||||||
|  |                         putExtra(Constants.EXTRA_SERIES_ID, item.id) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun setupTabs() { | ||||||
|  |         val tabs = binding.tabs | ||||||
|  |         val tabTitles = listOf("통합", "채널", "콘텐츠", "시리즈") | ||||||
|  |         for (title in tabTitles) { | ||||||
|  |             tabs.addTab(tabs.newTab().setText(title)) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { | ||||||
|  |             override fun onTabSelected(tab: TabLayout.Tab) { | ||||||
|  |                 val selectedTab = SearchViewModel.SearchPageTab.fromOrdinal(tab.position) | ||||||
|  |                 viewModel.changeTab(selectedTab!!) | ||||||
|  |  | ||||||
|  |                 tab.view.isSelected = true | ||||||
|  |                 hideKeyboard() | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             override fun onTabUnselected(tab: TabLayout.Tab) { | ||||||
|  |                 tab.view.isSelected = false | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             override fun onTabReselected(tab: TabLayout.Tab) { | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @SuppressLint("NotifyDataSetChanged") | ||||||
|  |     private fun bindData() { | ||||||
|  |         compositeDisposable.add( | ||||||
|  |             binding.etSearch.textChanges().skip(1) | ||||||
|  |                 .debounce(500, TimeUnit.MILLISECONDS) | ||||||
|  |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|  |                 .subscribeOn(Schedulers.io()) | ||||||
|  |                 .subscribe { | ||||||
|  |                     channelAdapter.clear() | ||||||
|  |                     contentAdapter.clear() | ||||||
|  |                     seriesAdapter.clear() | ||||||
|  |  | ||||||
|  |                     unifiedChannelAdapter.clear() | ||||||
|  |                     unifiedContentAdapter.clear() | ||||||
|  |                     unifiedSeriesAdapter.clear() | ||||||
|  |  | ||||||
|  |                     viewModel.keyword = it.toString() | ||||||
|  |                     if (it.length >= 2) { | ||||||
|  |                         viewModel.searchUnified() | ||||||
|  |                         binding.tabs.visibility = View.VISIBLE | ||||||
|  |                         binding.tvResultX.visibility = View.GONE | ||||||
|  |                     } else { | ||||||
|  |                         binding.nsSearchUnified.visibility = View.GONE | ||||||
|  |                         binding.rvCreator.visibility = View.GONE | ||||||
|  |                         binding.rvContent.visibility = View.GONE | ||||||
|  |                         binding.rvSeries.visibility = View.GONE | ||||||
|  |                         binding.tabs.visibility = View.GONE | ||||||
|  |                         binding.tvResultX.visibility = View.GONE | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         viewModel.toastLiveData.observe(this) { | ||||||
|  |             it?.let { Toast.makeText(this@SearchActivity, it, Toast.LENGTH_LONG).show() } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         viewModel.isLoading.observe(this) { | ||||||
|  |             if (it) { | ||||||
|  |                 loadingDialog.show(screenWidth, "") | ||||||
|  |             } else { | ||||||
|  |                 loadingDialog.dismiss() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         viewModel.searchUnifiedLiveData.observe(this) { | ||||||
|  |             if ( | ||||||
|  |                 it.creatorList.isEmpty() && | ||||||
|  |                 it.seriesList.isEmpty() && | ||||||
|  |                 it.contentList.isEmpty() | ||||||
|  |             ) { | ||||||
|  |                 binding.tabs.visibility = View.GONE | ||||||
|  |                 hideAllView() | ||||||
|  |                 if (viewModel.keyword.isNotBlank()) { | ||||||
|  |                     binding.tvResultX.visibility = View.VISIBLE | ||||||
|  |                 } else { | ||||||
|  |                     binding.tvResultX.visibility = View.GONE | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 binding.tabs.visibility = View.VISIBLE | ||||||
|  |                 binding.nsSearchUnified.visibility = View.VISIBLE | ||||||
|  |  | ||||||
|  |                 if (it.creatorList.isNotEmpty()) { | ||||||
|  |                     binding.tvCreatorTitle.visibility = View.VISIBLE | ||||||
|  |                     binding.tvMoreCreator.visibility = View.VISIBLE | ||||||
|  |                     binding.rvUnifiedCreator.visibility = View.VISIBLE | ||||||
|  |                     unifiedChannelAdapter.items.addAll(it.creatorList) | ||||||
|  |                     unifiedChannelAdapter.notifyDataSetChanged() | ||||||
|  |                 } else { | ||||||
|  |                     binding.tvCreatorTitle.visibility = View.GONE | ||||||
|  |                     binding.tvMoreCreator.visibility = View.GONE | ||||||
|  |                     binding.rvUnifiedCreator.visibility = View.GONE | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 if (it.contentList.isNotEmpty()) { | ||||||
|  |                     binding.tvContentTitle.visibility = View.VISIBLE | ||||||
|  |                     binding.tvMoreContent.visibility = View.VISIBLE | ||||||
|  |                     binding.rvUnifiedContent.visibility = View.VISIBLE | ||||||
|  |                     unifiedContentAdapter.items.addAll(it.contentList) | ||||||
|  |                     unifiedContentAdapter.notifyDataSetChanged() | ||||||
|  |                 } else { | ||||||
|  |                     binding.tvContentTitle.visibility = View.GONE | ||||||
|  |                     binding.tvMoreContent.visibility = View.GONE | ||||||
|  |                     binding.rvUnifiedContent.visibility = View.GONE | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 if (it.seriesList.isNotEmpty()) { | ||||||
|  |                     binding.tvSeriesTitle.visibility = View.VISIBLE | ||||||
|  |                     binding.tvMoreSeries.visibility = View.VISIBLE | ||||||
|  |                     binding.rvUnifiedSeries.visibility = View.VISIBLE | ||||||
|  |                     unifiedSeriesAdapter.items.addAll(it.seriesList) | ||||||
|  |                     unifiedSeriesAdapter.notifyDataSetChanged() | ||||||
|  |                 } else { | ||||||
|  |                     binding.tvSeriesTitle.visibility = View.GONE | ||||||
|  |                     binding.tvMoreSeries.visibility = View.GONE | ||||||
|  |                     binding.rvUnifiedSeries.visibility = View.GONE | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         viewModel.searchCreatorLiveData.observe(this) { | ||||||
|  |             channelAdapter.items.addAll(it) | ||||||
|  |             channelAdapter.notifyDataSetChanged() | ||||||
|  |  | ||||||
|  |             hideAllView() | ||||||
|  |             if (channelAdapter.items.isEmpty()) { | ||||||
|  |                 if (viewModel.keyword.isNotBlank()) { | ||||||
|  |                     binding.tvResultX.visibility = View.VISIBLE | ||||||
|  |                 } else { | ||||||
|  |                     binding.tvResultX.visibility = View.GONE | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 binding.rvCreator.visibility = View.VISIBLE | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         viewModel.searchContentLiveData.observe(this) { | ||||||
|  |             contentAdapter.items.addAll(it) | ||||||
|  |             contentAdapter.notifyDataSetChanged() | ||||||
|  |  | ||||||
|  |             hideAllView() | ||||||
|  |             if (contentAdapter.items.isEmpty()) { | ||||||
|  |                 if (viewModel.keyword.isNotBlank()) { | ||||||
|  |                     binding.tvResultX.visibility = View.VISIBLE | ||||||
|  |                 } else { | ||||||
|  |                     binding.tvResultX.visibility = View.GONE | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 binding.rvContent.visibility = View.VISIBLE | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         viewModel.searchSeriesLiveData.observe(this) { | ||||||
|  |             seriesAdapter.items.addAll(it) | ||||||
|  |             seriesAdapter.notifyDataSetChanged() | ||||||
|  |  | ||||||
|  |             hideAllView() | ||||||
|  |             if (seriesAdapter.items.isEmpty()) { | ||||||
|  |                 if (viewModel.keyword.isNotBlank()) { | ||||||
|  |                     binding.tvResultX.visibility = View.VISIBLE | ||||||
|  |                 } else { | ||||||
|  |                     binding.tvResultX.visibility = View.GONE | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 binding.rvSeries.visibility = View.VISIBLE | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         viewModel.currentTabLiveData.observe(this) { currentTab -> | ||||||
|  |             hideAllView() | ||||||
|  |  | ||||||
|  |             binding.tabs.getTabAt(currentTab.ordinal)?.select() | ||||||
|  |             when (currentTab) { | ||||||
|  |                 SearchViewModel.SearchPageTab.CREATOR -> { | ||||||
|  |                     if (channelAdapter.items.isEmpty()) { | ||||||
|  |                         viewModel.searchCreatorList() | ||||||
|  |                     } else { | ||||||
|  |                         binding.rvCreator.visibility = View.VISIBLE | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 SearchViewModel.SearchPageTab.CONTENT -> { | ||||||
|  |                     if (contentAdapter.items.isEmpty()) { | ||||||
|  |                         viewModel.searchContentList() | ||||||
|  |                     } else { | ||||||
|  |                         binding.rvContent.visibility = View.VISIBLE | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 SearchViewModel.SearchPageTab.SERIES -> { | ||||||
|  |                     if (seriesAdapter.items.isEmpty()) { | ||||||
|  |                         viewModel.searchSeriesList() | ||||||
|  |                     } else { | ||||||
|  |                         binding.rvSeries.visibility = View.VISIBLE | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 else -> { | ||||||
|  |                     if ( | ||||||
|  |                         unifiedChannelAdapter.items.isEmpty() && | ||||||
|  |                         unifiedContentAdapter.items.isEmpty() && | ||||||
|  |                         unifiedSeriesAdapter.items.isEmpty() | ||||||
|  |                     ) { | ||||||
|  |                         if (viewModel.keyword.isNotBlank()) { | ||||||
|  |                             binding.tvResultX.visibility = View.VISIBLE | ||||||
|  |                         } else { | ||||||
|  |                             binding.tvResultX.visibility = View.GONE | ||||||
|  |                         } | ||||||
|  |                     } else { | ||||||
|  |                         binding.nsSearchUnified.visibility = View.VISIBLE | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun hideAllView() { | ||||||
|  |         binding.nsSearchUnified.visibility = View.GONE | ||||||
|  |         binding.rvCreator.visibility = View.GONE | ||||||
|  |         binding.rvContent.visibility = View.GONE | ||||||
|  |         binding.rvSeries.visibility = View.GONE | ||||||
|  |         binding.tvResultX.visibility = View.GONE | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun hideKeyboard() { | ||||||
|  |         handler.postDelayed({ | ||||||
|  |             imm.hideSoftInputFromWindow( | ||||||
|  |                 window.decorView.applicationWindowToken, | ||||||
|  |                 InputMethodManager.HIDE_NOT_ALWAYS | ||||||
|  |             ) | ||||||
|  |         }, 100) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,97 @@ | |||||||
|  | package kr.co.vividnext.sodalive.search | ||||||
|  |  | ||||||
|  | import android.annotation.SuppressLint | ||||||
|  | import android.content.Context | ||||||
|  | import android.view.LayoutInflater | ||||||
|  | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import androidx.recyclerview.widget.RecyclerView | ||||||
|  | import coil.load | ||||||
|  | import coil.transform.CircleCropTransformation | ||||||
|  | import coil.transform.RoundedCornersTransformation | ||||||
|  | import kr.co.vividnext.sodalive.R | ||||||
|  | import kr.co.vividnext.sodalive.databinding.ItemSearchBinding | ||||||
|  | import kr.co.vividnext.sodalive.extensions.dpToPx | ||||||
|  |  | ||||||
|  | class SearchAdapter( | ||||||
|  |     private val onClickItem: (SearchResponseItem) -> Unit | ||||||
|  | ) : RecyclerView.Adapter<SearchAdapter.ViewHolder>() { | ||||||
|  |     inner class ViewHolder( | ||||||
|  |         private val context: Context, | ||||||
|  |         private val binding: ItemSearchBinding | ||||||
|  |     ) : RecyclerView.ViewHolder(binding.root) { | ||||||
|  |  | ||||||
|  |         @SuppressLint("CheckResult") | ||||||
|  |         fun bind(item: SearchResponseItem) { | ||||||
|  |             binding.ivProfile.visibility = View.GONE | ||||||
|  |             binding.ivContent.visibility = View.GONE | ||||||
|  |             binding.ivSeries.visibility = View.GONE | ||||||
|  |             binding.tvNickname.visibility = View.GONE | ||||||
|  |  | ||||||
|  |             when (item.type) { | ||||||
|  |                 SearchResponseType.CREATOR -> { | ||||||
|  |                     binding.ivProfile.visibility = View.VISIBLE | ||||||
|  |                     binding.ivProfile.load(item.imageUrl) { | ||||||
|  |                         crossfade(true) | ||||||
|  |                         placeholder(R.drawable.bg_placeholder) | ||||||
|  |                         transformations(CircleCropTransformation()) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 SearchResponseType.CONTENT -> { | ||||||
|  |                     binding.ivContent.visibility = View.VISIBLE | ||||||
|  |                     binding.ivContent.load(item.imageUrl) { | ||||||
|  |                         crossfade(true) | ||||||
|  |                         placeholder(R.drawable.bg_placeholder) | ||||||
|  |                         transformations(RoundedCornersTransformation(5.3f.dpToPx())) | ||||||
|  |                     } | ||||||
|  |                     binding.tvNickname.visibility = View.VISIBLE | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 SearchResponseType.SERIES -> { | ||||||
|  |                     binding.ivSeries.visibility = View.VISIBLE | ||||||
|  |                     binding.ivSeries.load(item.imageUrl) { | ||||||
|  |                         crossfade(true) | ||||||
|  |                         placeholder(R.drawable.bg_placeholder) | ||||||
|  |                         transformations(RoundedCornersTransformation(5.3f.dpToPx())) | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     binding.ivSeries.load(item.imageUrl) { | ||||||
|  |                         crossfade(true) | ||||||
|  |                         placeholder(R.drawable.bg_placeholder) | ||||||
|  |                         transformations(RoundedCornersTransformation(5.3f.dpToPx())) | ||||||
|  |                     } | ||||||
|  |                     binding.tvNickname.visibility = View.VISIBLE | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             binding.tvTitle.text = item.title | ||||||
|  |             binding.tvNickname.text = item.nickname | ||||||
|  |  | ||||||
|  |             binding.root.setOnClickListener { onClickItem(item) } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     val items: MutableList<SearchResponseItem> = mutableListOf() | ||||||
|  |  | ||||||
|  |     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( | ||||||
|  |         parent.context, | ||||||
|  |         ItemSearchBinding.inflate( | ||||||
|  |             LayoutInflater.from(parent.context), | ||||||
|  |             parent, | ||||||
|  |             false | ||||||
|  |         ) | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     override fun onBindViewHolder(holder: ViewHolder, position: Int) { | ||||||
|  |         holder.bind(items[position]) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun getItemCount() = items.size | ||||||
|  |  | ||||||
|  |     @SuppressLint("NotifyDataSetChanged") | ||||||
|  |     fun clear() { | ||||||
|  |         items.clear() | ||||||
|  |         notifyDataSetChanged() | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,48 @@ | |||||||
|  | package kr.co.vividnext.sodalive.search | ||||||
|  |  | ||||||
|  | import io.reactivex.rxjava3.core.Single | ||||||
|  | import kr.co.vividnext.sodalive.common.ApiResponse | ||||||
|  | import kr.co.vividnext.sodalive.settings.ContentType | ||||||
|  | import retrofit2.http.GET | ||||||
|  | import retrofit2.http.Header | ||||||
|  | import retrofit2.http.Query | ||||||
|  |  | ||||||
|  | interface SearchApi { | ||||||
|  |     @GET("/search") | ||||||
|  |     fun searchUnified( | ||||||
|  |         @Query("keyword") keyword: String, | ||||||
|  |         @Query("isAdultContentVisible") isAdultContentVisible: Boolean, | ||||||
|  |         @Query("contentType") contentType: ContentType, | ||||||
|  |         @Header("Authorization") authHeader: String | ||||||
|  |     ): Single<ApiResponse<SearchUnifiedResponse>> | ||||||
|  |  | ||||||
|  |     @GET("/search/creators") | ||||||
|  |     fun searchCreatorList( | ||||||
|  |         @Query("keyword") keyword: String, | ||||||
|  |         @Query("isAdultContentVisible") isAdultContentVisible: Boolean, | ||||||
|  |         @Query("contentType") contentType: ContentType, | ||||||
|  |         @Query("page") page: Int, | ||||||
|  |         @Query("size") size: Int, | ||||||
|  |         @Header("Authorization") authHeader: String | ||||||
|  |     ): Single<ApiResponse<SearchResponse>> | ||||||
|  |  | ||||||
|  |     @GET("/search/contents") | ||||||
|  |     fun searchContentList( | ||||||
|  |         @Query("keyword") keyword: String, | ||||||
|  |         @Query("isAdultContentVisible") isAdultContentVisible: Boolean, | ||||||
|  |         @Query("contentType") contentType: ContentType, | ||||||
|  |         @Query("page") page: Int, | ||||||
|  |         @Query("size") size: Int, | ||||||
|  |         @Header("Authorization") authHeader: String | ||||||
|  |     ): Single<ApiResponse<SearchResponse>> | ||||||
|  |  | ||||||
|  |     @GET("/search/series") | ||||||
|  |     fun searchSeriesList( | ||||||
|  |         @Query("keyword") keyword: String, | ||||||
|  |         @Query("isAdultContentVisible") isAdultContentVisible: Boolean, | ||||||
|  |         @Query("contentType") contentType: ContentType, | ||||||
|  |         @Query("page") page: Int, | ||||||
|  |         @Query("size") size: Int, | ||||||
|  |         @Header("Authorization") authHeader: String | ||||||
|  |     ): Single<ApiResponse<SearchResponse>> | ||||||
|  | } | ||||||
| @@ -0,0 +1,58 @@ | |||||||
|  | package kr.co.vividnext.sodalive.search | ||||||
|  |  | ||||||
|  | import kr.co.vividnext.sodalive.common.SharedPreferenceManager | ||||||
|  | import kr.co.vividnext.sodalive.settings.ContentType | ||||||
|  |  | ||||||
|  | class SearchRepository(private val api: SearchApi) { | ||||||
|  |     fun searchUnified( | ||||||
|  |         keyword: String, | ||||||
|  |         token: String | ||||||
|  |     ) = api.searchUnified( | ||||||
|  |         keyword = keyword, | ||||||
|  |         isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible, | ||||||
|  |         contentType = ContentType.values()[SharedPreferenceManager.contentPreference], | ||||||
|  |         authHeader = token | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     fun searchCreatorList( | ||||||
|  |         keyword: String, | ||||||
|  |         page: Int, | ||||||
|  |         size: Int, | ||||||
|  |         token: String | ||||||
|  |     ) = api.searchCreatorList( | ||||||
|  |         keyword = keyword, | ||||||
|  |         isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible, | ||||||
|  |         contentType = ContentType.values()[SharedPreferenceManager.contentPreference], | ||||||
|  |         page = page - 1, | ||||||
|  |         size = size, | ||||||
|  |         authHeader = token | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     fun searchContentList( | ||||||
|  |         keyword: String, | ||||||
|  |         page: Int, | ||||||
|  |         size: Int, | ||||||
|  |         token: String | ||||||
|  |     ) = api.searchContentList( | ||||||
|  |         keyword = keyword, | ||||||
|  |         isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible, | ||||||
|  |         contentType = ContentType.values()[SharedPreferenceManager.contentPreference], | ||||||
|  |         page = page - 1, | ||||||
|  |         size = size, | ||||||
|  |         authHeader = token | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     fun searchSeriesList( | ||||||
|  |         keyword: String, | ||||||
|  |         page: Int, | ||||||
|  |         size: Int, | ||||||
|  |         token: String | ||||||
|  |     ) = api.searchSeriesList( | ||||||
|  |         keyword = keyword, | ||||||
|  |         isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible, | ||||||
|  |         contentType = ContentType.values()[SharedPreferenceManager.contentPreference], | ||||||
|  |         page = page - 1, | ||||||
|  |         size = size, | ||||||
|  |         authHeader = token | ||||||
|  |     ) | ||||||
|  | } | ||||||
| @@ -0,0 +1,33 @@ | |||||||
|  | package kr.co.vividnext.sodalive.search | ||||||
|  |  | ||||||
|  | import androidx.annotation.Keep | ||||||
|  | import com.google.gson.annotations.SerializedName | ||||||
|  |  | ||||||
|  | @Keep | ||||||
|  | data class SearchUnifiedResponse( | ||||||
|  |     @SerializedName("creatorList") val creatorList: List<SearchResponseItem>, | ||||||
|  |     @SerializedName("contentList") val contentList: List<SearchResponseItem>, | ||||||
|  |     @SerializedName("seriesList") val seriesList: List<SearchResponseItem> | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | @Keep | ||||||
|  | data class SearchResponse( | ||||||
|  |     @SerializedName("totalCount") val totalCount: Int, | ||||||
|  |     @SerializedName("items") val items: List<SearchResponseItem> | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | @Keep | ||||||
|  | data class SearchResponseItem( | ||||||
|  |     @SerializedName("id") val id: Long, | ||||||
|  |     @SerializedName("imageUrl") val imageUrl: String, | ||||||
|  |     @SerializedName("title") val title: String, | ||||||
|  |     @SerializedName("nickname") val nickname: String, | ||||||
|  |     @SerializedName("type") val type: SearchResponseType | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | @Keep | ||||||
|  | enum class SearchResponseType { | ||||||
|  |     @SerializedName("CREATOR") CREATOR, | ||||||
|  |     @SerializedName("CONTENT") CONTENT, | ||||||
|  |     @SerializedName("SERIES") SERIES | ||||||
|  | } | ||||||
| @@ -0,0 +1,250 @@ | |||||||
|  | package kr.co.vividnext.sodalive.search | ||||||
|  |  | ||||||
|  | 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 SearchViewModel( | ||||||
|  |     private val repository: SearchRepository | ||||||
|  | ) : BaseViewModel() { | ||||||
|  |     enum class SearchPageTab { | ||||||
|  |         UNIFIED, CREATOR, CONTENT, SERIES; | ||||||
|  |  | ||||||
|  |         companion object { | ||||||
|  |             fun fromOrdinal(ordinal: Int): SearchPageTab? { | ||||||
|  |                 return SearchPageTab.values().getOrNull(ordinal) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     var keyword = "" | ||||||
|  |  | ||||||
|  |     private val _currentTabLiveData = MutableLiveData(SearchPageTab.UNIFIED) | ||||||
|  |     val currentTabLiveData: LiveData<SearchPageTab> | ||||||
|  |         get() = _currentTabLiveData | ||||||
|  |  | ||||||
|  |     private val _toastLiveData = MutableLiveData<String?>() | ||||||
|  |     val toastLiveData: LiveData<String?> | ||||||
|  |         get() = _toastLiveData | ||||||
|  |  | ||||||
|  |     private var _isLoading = MutableLiveData(false) | ||||||
|  |     val isLoading: LiveData<Boolean> | ||||||
|  |         get() = _isLoading | ||||||
|  |  | ||||||
|  |     private var _searchUnifiedLiveData = MutableLiveData<SearchUnifiedResponse>() | ||||||
|  |     val searchUnifiedLiveData: LiveData<SearchUnifiedResponse> | ||||||
|  |         get() = _searchUnifiedLiveData | ||||||
|  |  | ||||||
|  |     private var _searchCreatorLiveData = MutableLiveData<List<SearchResponseItem>>() | ||||||
|  |     val searchCreatorLiveData: LiveData<List<SearchResponseItem>> | ||||||
|  |         get() = _searchCreatorLiveData | ||||||
|  |  | ||||||
|  |     private var _searchContentLiveData = MutableLiveData<List<SearchResponseItem>>() | ||||||
|  |     val searchContentLiveData: LiveData<List<SearchResponseItem>> | ||||||
|  |         get() = _searchContentLiveData | ||||||
|  |  | ||||||
|  |     private var _searchSeriesLiveData = MutableLiveData<List<SearchResponseItem>>() | ||||||
|  |     val searchSeriesLiveData: LiveData<List<SearchResponseItem>> | ||||||
|  |         get() = _searchSeriesLiveData | ||||||
|  |  | ||||||
|  |     private var searchCreatorPage = 1 | ||||||
|  |     private var searchContentPage = 1 | ||||||
|  |     private var searchSeriesPage = 1 | ||||||
|  |  | ||||||
|  |     private var isSearchCreatorLast = false | ||||||
|  |     private var isSearchContentLast = false | ||||||
|  |     private var isSearchSeriesLast = false | ||||||
|  |  | ||||||
|  |     private val size = 20 | ||||||
|  |  | ||||||
|  |     fun changeTab(tab: SearchPageTab) { | ||||||
|  |         _currentTabLiveData.value = tab | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun searchUnified() { | ||||||
|  |         if (!_isLoading.value!!) { | ||||||
|  |             _currentTabLiveData.value = SearchPageTab.UNIFIED | ||||||
|  |  | ||||||
|  |             searchCreatorPage = 1 | ||||||
|  |             searchContentPage = 1 | ||||||
|  |             searchSeriesPage = 1 | ||||||
|  |  | ||||||
|  |             isSearchCreatorLast = false | ||||||
|  |             isSearchContentLast = false | ||||||
|  |             isSearchSeriesLast = false | ||||||
|  |  | ||||||
|  |             _isLoading.value = true | ||||||
|  |             compositeDisposable.add( | ||||||
|  |                 repository.searchUnified( | ||||||
|  |                     keyword = keyword, | ||||||
|  |                     token = "Bearer ${SharedPreferenceManager.token}" | ||||||
|  |                 ) | ||||||
|  |                     .subscribeOn(Schedulers.io()) | ||||||
|  |                     .observeOn(AndroidSchedulers.mainThread()) | ||||||
|  |                     .subscribe( | ||||||
|  |                         { | ||||||
|  |                             _isLoading.value = false | ||||||
|  |                             if (it.success && it.data != null) { | ||||||
|  |                                 _searchUnifiedLiveData.value = it.data!! | ||||||
|  |                             } else { | ||||||
|  |                                 if (it.message != null) { | ||||||
|  |                                     _toastLiveData.value = it.message | ||||||
|  |                                 } else { | ||||||
|  |                                     _toastLiveData | ||||||
|  |                                         .value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                         }, | ||||||
|  |                         { | ||||||
|  |                             _isLoading.value = false | ||||||
|  |                             it.message?.let { message -> Logger.e(message) } | ||||||
|  |                             _toastLiveData | ||||||
|  |                                 .value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." | ||||||
|  |                         } | ||||||
|  |                     ) | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun searchCreatorList() { | ||||||
|  |         if (!_isLoading.value!! && !isSearchCreatorLast) { | ||||||
|  |             _isLoading.value = true | ||||||
|  |             compositeDisposable.add( | ||||||
|  |                 repository.searchCreatorList( | ||||||
|  |                     keyword = keyword, | ||||||
|  |                     page = searchCreatorPage, | ||||||
|  |                     size = size, | ||||||
|  |                     token = "Bearer ${SharedPreferenceManager.token}" | ||||||
|  |                 ) | ||||||
|  |                     .subscribeOn(Schedulers.io()) | ||||||
|  |                     .observeOn(AndroidSchedulers.mainThread()) | ||||||
|  |                     .subscribe( | ||||||
|  |                         { | ||||||
|  |                             _isLoading.value = false | ||||||
|  |                             if (it.success && it.data != null) { | ||||||
|  |                                 searchCreatorPage += 1 | ||||||
|  |  | ||||||
|  |                                 val data = it.data | ||||||
|  |                                 _searchCreatorLiveData.value = data.items | ||||||
|  |  | ||||||
|  |                                 if (data.items.isEmpty()) { | ||||||
|  |                                     isSearchCreatorLast = true | ||||||
|  |                                 } | ||||||
|  |                             } else { | ||||||
|  |                                 if (it.message != null) { | ||||||
|  |                                     _toastLiveData.value = it.message | ||||||
|  |                                 } else { | ||||||
|  |                                     _toastLiveData | ||||||
|  |                                         .value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                         }, | ||||||
|  |                         { | ||||||
|  |                             _isLoading.value = false | ||||||
|  |                             it.message?.let { message -> Logger.e(message) } | ||||||
|  |                             _toastLiveData | ||||||
|  |                                 .value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." | ||||||
|  |                         } | ||||||
|  |                     ) | ||||||
|  |             ) | ||||||
|  |         } else { | ||||||
|  |             _searchCreatorLiveData.value = emptyList() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun searchContentList() { | ||||||
|  |         if (!_isLoading.value!! && !isSearchContentLast) { | ||||||
|  |             _isLoading.value = true | ||||||
|  |             compositeDisposable.add( | ||||||
|  |                 repository.searchContentList( | ||||||
|  |                     keyword = keyword, | ||||||
|  |                     page = searchContentPage, | ||||||
|  |                     size = size, | ||||||
|  |                     token = "Bearer ${SharedPreferenceManager.token}" | ||||||
|  |                 ) | ||||||
|  |                     .subscribeOn(Schedulers.io()) | ||||||
|  |                     .observeOn(AndroidSchedulers.mainThread()) | ||||||
|  |                     .subscribe( | ||||||
|  |                         { | ||||||
|  |                             _isLoading.value = false | ||||||
|  |                             if (it.success && it.data != null) { | ||||||
|  |                                 searchContentPage += 1 | ||||||
|  |  | ||||||
|  |                                 val data = it.data | ||||||
|  |                                 _searchContentLiveData.value = data.items | ||||||
|  |  | ||||||
|  |                                 if (data.items.isEmpty()) { | ||||||
|  |                                     isSearchContentLast = true | ||||||
|  |                                 } | ||||||
|  |                             } else { | ||||||
|  |                                 if (it.message != null) { | ||||||
|  |                                     _toastLiveData.value = it.message | ||||||
|  |                                 } else { | ||||||
|  |                                     _toastLiveData | ||||||
|  |                                         .value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                         }, | ||||||
|  |                         { | ||||||
|  |                             _isLoading.value = false | ||||||
|  |                             it.message?.let { message -> Logger.e(message) } | ||||||
|  |                             _toastLiveData | ||||||
|  |                                 .value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." | ||||||
|  |                         } | ||||||
|  |                     ) | ||||||
|  |             ) | ||||||
|  |         } else { | ||||||
|  |             _searchContentLiveData.value = emptyList() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun searchSeriesList() { | ||||||
|  |         if (!_isLoading.value!! && !isSearchSeriesLast) { | ||||||
|  |             _isLoading.value = true | ||||||
|  |             compositeDisposable.add( | ||||||
|  |                 repository.searchSeriesList( | ||||||
|  |                     keyword = keyword, | ||||||
|  |                     page = searchSeriesPage, | ||||||
|  |                     size = size, | ||||||
|  |                     token = "Bearer ${SharedPreferenceManager.token}" | ||||||
|  |                 ) | ||||||
|  |                     .subscribeOn(Schedulers.io()) | ||||||
|  |                     .observeOn(AndroidSchedulers.mainThread()) | ||||||
|  |                     .subscribe( | ||||||
|  |                         { | ||||||
|  |                             _isLoading.value = false | ||||||
|  |                             if (it.success && it.data != null) { | ||||||
|  |                                 searchSeriesPage += 1 | ||||||
|  |  | ||||||
|  |                                 val data = it.data | ||||||
|  |                                 _searchSeriesLiveData.value = data.items | ||||||
|  |  | ||||||
|  |                                 if (data.items.isEmpty()) { | ||||||
|  |                                     isSearchSeriesLast = true | ||||||
|  |                                 } | ||||||
|  |                             } else { | ||||||
|  |                                 if (it.message != null) { | ||||||
|  |                                     _toastLiveData.value = it.message | ||||||
|  |                                 } else { | ||||||
|  |                                     _toastLiveData | ||||||
|  |                                         .value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                         }, | ||||||
|  |                         { | ||||||
|  |                             _isLoading.value = false | ||||||
|  |                             it.message?.let { message -> Logger.e(message) } | ||||||
|  |                             _toastLiveData | ||||||
|  |                                 .value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." | ||||||
|  |                         } | ||||||
|  |                     ) | ||||||
|  |             ) | ||||||
|  |         } else { | ||||||
|  |             _searchSeriesLiveData.value = emptyList() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										219
									
								
								app/src/main/res/layout/activity_search.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										219
									
								
								app/src/main/res/layout/activity_search.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,219 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||||
|  |     android:layout_width="match_parent" | ||||||
|  |     android:layout_height="match_parent" | ||||||
|  |     android:background="@color/black" | ||||||
|  |     android:orientation="vertical"> | ||||||
|  |  | ||||||
|  |     <LinearLayout | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:layout_marginEnd="13.3dp" | ||||||
|  |         android:gravity="center_vertical" | ||||||
|  |         android:paddingTop="13.3dp"> | ||||||
|  |  | ||||||
|  |         <ImageView | ||||||
|  |             android:id="@+id/iv_back" | ||||||
|  |             android:layout_width="wrap_content" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:contentDescription="@null" | ||||||
|  |             android:padding="13.3dp" | ||||||
|  |             android:src="@drawable/ic_back" /> | ||||||
|  |  | ||||||
|  |         <RelativeLayout | ||||||
|  |             android:layout_width="match_parent" | ||||||
|  |             android:layout_height="50dp" | ||||||
|  |             android:background="@drawable/bg_round_corner_6_7_222222_bbbbbb"> | ||||||
|  |  | ||||||
|  |             <ImageView | ||||||
|  |                 android:layout_width="wrap_content" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:layout_centerVertical="true" | ||||||
|  |                 android:layout_gravity="center_vertical" | ||||||
|  |                 android:layout_marginStart="21.3dp" | ||||||
|  |                 android:contentDescription="@null" | ||||||
|  |                 android:src="@drawable/ic_title_search_black" /> | ||||||
|  |  | ||||||
|  |             <EditText | ||||||
|  |                 android:id="@+id/et_search" | ||||||
|  |                 android:layout_width="match_parent" | ||||||
|  |                 android:layout_height="match_parent" | ||||||
|  |                 android:layout_gravity="center_vertical" | ||||||
|  |                 android:background="@null" | ||||||
|  |                 android:fontFamily="@font/gmarket_sans_medium" | ||||||
|  |                 android:gravity="center_vertical" | ||||||
|  |                 android:hint="검색" | ||||||
|  |                 android:importantForAutofill="no" | ||||||
|  |                 android:inputType="textWebEditText" | ||||||
|  |                 android:paddingHorizontal="54.67dp" | ||||||
|  |                 android:textColor="@color/color_eeeeee" | ||||||
|  |                 android:textColorHint="@color/color_555555" | ||||||
|  |                 android:textCursorDrawable="@drawable/edit_text_cursor" | ||||||
|  |                 android:textSize="13.3sp" /> | ||||||
|  |         </RelativeLayout> | ||||||
|  |     </LinearLayout> | ||||||
|  |  | ||||||
|  |     <com.google.android.material.tabs.TabLayout | ||||||
|  |         android:id="@+id/tabs" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:layout_marginBottom="13.3dp" | ||||||
|  |         android:background="@android:color/transparent" | ||||||
|  |         android:clipToPadding="false" | ||||||
|  |         android:elevation="0dp" | ||||||
|  |         android:paddingHorizontal="13.3dp" | ||||||
|  |         android:visibility="gone" | ||||||
|  |         app:tabGravity="start" | ||||||
|  |         app:tabIndicatorFullWidth="false" | ||||||
|  |         app:tabIndicatorHeight="0dp" | ||||||
|  |         app:tabMinWidth="45dp" | ||||||
|  |         app:tabMode="scrollable" | ||||||
|  |         app:tabPaddingBottom="15dp" | ||||||
|  |         app:tabPaddingTop="15dp" | ||||||
|  |         app:tabSelectedTextColor="@color/color_3bb9f1" | ||||||
|  |         app:tabTextAppearance="@style/ContentMainTabText" | ||||||
|  |         app:tabTextColor="@color/color_bbbbbb" /> | ||||||
|  |  | ||||||
|  |     <androidx.core.widget.NestedScrollView | ||||||
|  |         android:id="@+id/ns_search_unified" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="match_parent" | ||||||
|  |         android:visibility="gone"> | ||||||
|  |  | ||||||
|  |         <LinearLayout | ||||||
|  |             android:layout_width="match_parent" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:orientation="vertical"> | ||||||
|  |  | ||||||
|  |             <TextView | ||||||
|  |                 android:id="@+id/tv_creator_title" | ||||||
|  |                 android:layout_width="wrap_content" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:layout_marginHorizontal="13.3dp" | ||||||
|  |                 android:fontFamily="@font/gmarket_sans_bold" | ||||||
|  |                 android:text="채널" | ||||||
|  |                 android:textColor="@color/color_eeeeee" | ||||||
|  |                 android:textSize="16sp" /> | ||||||
|  |  | ||||||
|  |             <androidx.recyclerview.widget.RecyclerView | ||||||
|  |                 android:id="@+id/rv_unified_creator" | ||||||
|  |                 android:layout_width="match_parent" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:clipToPadding="false" | ||||||
|  |                 android:padding="13.3dp" /> | ||||||
|  |  | ||||||
|  |             <TextView | ||||||
|  |                 android:id="@+id/tv_more_creator" | ||||||
|  |                 android:layout_width="match_parent" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:layout_marginHorizontal="13.3dp" | ||||||
|  |                 android:layout_marginBottom="30dp" | ||||||
|  |                 android:background="@color/color_cc333333" | ||||||
|  |                 android:fontFamily="@font/gmarket_sans_medium" | ||||||
|  |                 android:gravity="center" | ||||||
|  |                 android:paddingVertical="10dp" | ||||||
|  |                 android:text="더보기 >" | ||||||
|  |                 android:textColor="@color/color_777777" | ||||||
|  |                 android:textSize="13.3sp" /> | ||||||
|  |  | ||||||
|  |             <TextView | ||||||
|  |                 android:id="@+id/tv_content_title" | ||||||
|  |                 android:layout_width="wrap_content" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:layout_marginHorizontal="13.3dp" | ||||||
|  |                 android:fontFamily="@font/gmarket_sans_bold" | ||||||
|  |                 android:text="콘텐츠" | ||||||
|  |                 android:textColor="@color/color_eeeeee" | ||||||
|  |                 android:textSize="16sp" /> | ||||||
|  |  | ||||||
|  |             <androidx.recyclerview.widget.RecyclerView | ||||||
|  |                 android:id="@+id/rv_unified_content" | ||||||
|  |                 android:layout_width="match_parent" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:clipToPadding="false" | ||||||
|  |                 android:padding="13.3dp" /> | ||||||
|  |  | ||||||
|  |             <TextView | ||||||
|  |                 android:id="@+id/tv_more_content" | ||||||
|  |                 android:layout_width="match_parent" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:layout_marginHorizontal="13.3dp" | ||||||
|  |                 android:layout_marginBottom="30dp" | ||||||
|  |                 android:background="@color/color_cc333333" | ||||||
|  |                 android:fontFamily="@font/gmarket_sans_medium" | ||||||
|  |                 android:gravity="center" | ||||||
|  |                 android:paddingVertical="10dp" | ||||||
|  |                 android:text="더보기 >" | ||||||
|  |                 android:textColor="@color/color_777777" | ||||||
|  |                 android:textSize="13.3sp" /> | ||||||
|  |  | ||||||
|  |             <TextView | ||||||
|  |                 android:id="@+id/tv_series_title" | ||||||
|  |                 android:layout_width="wrap_content" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:layout_marginHorizontal="13.3dp" | ||||||
|  |                 android:fontFamily="@font/gmarket_sans_bold" | ||||||
|  |                 android:text="시리즈" | ||||||
|  |                 android:textColor="@color/color_eeeeee" | ||||||
|  |                 android:textSize="16sp" /> | ||||||
|  |  | ||||||
|  |             <androidx.recyclerview.widget.RecyclerView | ||||||
|  |                 android:id="@+id/rv_unified_series" | ||||||
|  |                 android:layout_width="match_parent" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:clipToPadding="false" | ||||||
|  |                 android:padding="13.3dp" /> | ||||||
|  |  | ||||||
|  |             <TextView | ||||||
|  |                 android:id="@+id/tv_more_series" | ||||||
|  |                 android:layout_width="match_parent" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:layout_marginHorizontal="13.3dp" | ||||||
|  |                 android:layout_marginBottom="13.3dp" | ||||||
|  |                 android:background="@color/color_cc333333" | ||||||
|  |                 android:fontFamily="@font/gmarket_sans_medium" | ||||||
|  |                 android:gravity="center" | ||||||
|  |                 android:paddingVertical="10dp" | ||||||
|  |                 android:text="더보기 >" | ||||||
|  |                 android:textColor="@color/color_777777" | ||||||
|  |                 android:textSize="13.3sp" /> | ||||||
|  |         </LinearLayout> | ||||||
|  |     </androidx.core.widget.NestedScrollView> | ||||||
|  |  | ||||||
|  |     <androidx.recyclerview.widget.RecyclerView | ||||||
|  |         android:id="@+id/rv_creator" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="match_parent" | ||||||
|  |         android:clipToPadding="false" | ||||||
|  |         android:padding="13.3dp" | ||||||
|  |         android:visibility="gone" /> | ||||||
|  |  | ||||||
|  |     <androidx.recyclerview.widget.RecyclerView | ||||||
|  |         android:id="@+id/rv_content" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="match_parent" | ||||||
|  |         android:clipToPadding="false" | ||||||
|  |         android:padding="13.3dp" | ||||||
|  |         android:visibility="gone" /> | ||||||
|  |  | ||||||
|  |     <androidx.recyclerview.widget.RecyclerView | ||||||
|  |         android:id="@+id/rv_series" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="match_parent" | ||||||
|  |         android:clipToPadding="false" | ||||||
|  |         android:padding="13.3dp" | ||||||
|  |         android:visibility="gone" /> | ||||||
|  |  | ||||||
|  |     <TextView | ||||||
|  |         android:id="@+id/tv_result_x" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:layout_marginVertical="40dp" | ||||||
|  |         android:fontFamily="@font/gmarket_sans_medium" | ||||||
|  |         android:gravity="center" | ||||||
|  |         android:text="검색 결과가 없습니다." | ||||||
|  |         android:textColor="@color/white" | ||||||
|  |         android:textSize="18.3sp" | ||||||
|  |         android:visibility="gone" /> | ||||||
|  | </LinearLayout> | ||||||
							
								
								
									
										57
									
								
								app/src/main/res/layout/item_search.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								app/src/main/res/layout/item_search.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     xmlns:tools="http://schemas.android.com/tools" | ||||||
|  |     android:layout_width="match_parent" | ||||||
|  |     android:layout_height="wrap_content" | ||||||
|  |     android:background="@color/black" | ||||||
|  |     android:gravity="center_vertical" | ||||||
|  |     android:orientation="horizontal"> | ||||||
|  |  | ||||||
|  |     <ImageView | ||||||
|  |         android:id="@+id/iv_profile" | ||||||
|  |         android:layout_width="60dp" | ||||||
|  |         android:layout_height="60dp" | ||||||
|  |         android:contentDescription="@null" | ||||||
|  |         android:visibility="gone" /> | ||||||
|  |  | ||||||
|  |     <ImageView | ||||||
|  |         android:id="@+id/iv_content" | ||||||
|  |         android:layout_width="60dp" | ||||||
|  |         android:layout_height="60dp" | ||||||
|  |         android:contentDescription="@null" | ||||||
|  |         android:visibility="gone" /> | ||||||
|  |  | ||||||
|  |     <ImageView | ||||||
|  |         android:id="@+id/iv_series" | ||||||
|  |         android:layout_width="60dp" | ||||||
|  |         android:layout_height="85dp" | ||||||
|  |         android:contentDescription="@null" | ||||||
|  |         android:visibility="gone" /> | ||||||
|  |  | ||||||
|  |     <LinearLayout | ||||||
|  |         android:layout_width="wrap_content" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:layout_marginStart="13.3dp" | ||||||
|  |         android:orientation="vertical"> | ||||||
|  |  | ||||||
|  |         <TextView | ||||||
|  |             android:id="@+id/tv_title" | ||||||
|  |             android:layout_width="wrap_content" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:fontFamily="@font/gmarket_sans_medium" | ||||||
|  |             android:textColor="@color/color_eeeeee" | ||||||
|  |             android:textSize="13.3sp" | ||||||
|  |             tools:text="slefjeiwok" /> | ||||||
|  |  | ||||||
|  |         <TextView | ||||||
|  |             android:id="@+id/tv_nickname" | ||||||
|  |             android:layout_width="wrap_content" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:layout_marginTop="6.7dp" | ||||||
|  |             android:fontFamily="@font/gmarket_sans_medium" | ||||||
|  |             android:textColor="@color/color_777777" | ||||||
|  |             android:textSize="10sp" | ||||||
|  |             tools:ignore="SmallSp" | ||||||
|  |             tools:text="slefjeiwok" /> | ||||||
|  |     </LinearLayout> | ||||||
|  | </LinearLayout> | ||||||
		Reference in New Issue
	
	Block a user