Browse Source

大致完成搜索架子

DoggyZhang 2 weeks ago
parent
commit
2181c83820
26 changed files with 1553 additions and 432 deletions
  1. 24 3
      module/profile/src/main/java/com/adealink/weparty/profile/datasource/remote/SearchHttpService.kt
  2. 115 163
      module/profile/src/main/java/com/adealink/weparty/profile/search/SearchActivity.kt
  3. 55 0
      module/profile/src/main/java/com/adealink/weparty/profile/search/SearchTab.kt
  4. 37 0
      module/profile/src/main/java/com/adealink/weparty/profile/search/TabManager.kt
  5. 36 0
      module/profile/src/main/java/com/adealink/weparty/profile/search/adapter/SearchRoomItemViewBinder.kt
  6. 9 9
      module/profile/src/main/java/com/adealink/weparty/profile/search/adapter/SearchUserItemViewBinder.kt
  7. 52 8
      module/profile/src/main/java/com/adealink/weparty/profile/search/data/SearchData.kt
  8. 165 0
      module/profile/src/main/java/com/adealink/weparty/profile/search/page/SearchAllFragment.kt
  9. 105 0
      module/profile/src/main/java/com/adealink/weparty/profile/search/page/SearchRoomFragment.kt
  10. 160 0
      module/profile/src/main/java/com/adealink/weparty/profile/search/page/SearchUserFragment.kt
  11. 101 0
      module/profile/src/main/java/com/adealink/weparty/profile/search/viewmodel/SearchAllViewModel.kt
  12. 94 0
      module/profile/src/main/java/com/adealink/weparty/profile/search/viewmodel/SearchRoomViewModel.kt
  13. 101 0
      module/profile/src/main/java/com/adealink/weparty/profile/search/viewmodel/SearchUserViewModel.kt
  14. 9 81
      module/profile/src/main/java/com/adealink/weparty/profile/search/viewmodel/SearchViewModel.kt
  15. 8 0
      module/profile/src/main/res/drawable/search_tab_selected_bg.xml
  16. 15 168
      module/profile/src/main/res/layout/activity_user_search.xml
  17. 57 0
      module/profile/src/main/res/layout/fragment_search_all.xml
  18. 57 0
      module/profile/src/main/res/layout/fragment_search_room.xml
  19. 57 0
      module/profile/src/main/res/layout/fragment_search_user.xml
  20. 0 0
      module/profile/src/main/res/layout/layout_profile_search_room_item.xml
  21. 106 0
      module/profile/src/main/res/layout/layout_profile_search_user_item.xml
  22. 35 0
      module/profile/src/main/res/layout/layout_search_history.xml
  23. 32 0
      module/profile/src/main/res/layout/layout_search_result.xml
  24. 34 0
      module/profile/src/main/res/layout/layout_search_tab.xml
  25. 86 0
      module/profile/src/main/res/layout/layout_search_top.xml
  26. 3 0
      module/profile/src/main/res/values/strings.xml

+ 24 - 3
module/profile/src/main/java/com/adealink/weparty/profile/datasource/remote/SearchHttpService.kt

@@ -2,12 +2,33 @@ package com.adealink.weparty.profile.datasource.remote
 
 import com.adealink.frame.base.Rlt
 import com.adealink.frame.network.data.Res
-import com.adealink.weparty.profile.search.data.SearchReq
-import com.adealink.weparty.profile.search.data.SearchRes
+import com.adealink.weparty.profile.search.data.SearchAllReq
+import com.adealink.weparty.profile.search.data.SearchAllRes
+import com.adealink.weparty.profile.search.data.SearchRoomReq
+import com.adealink.weparty.profile.search.data.SearchRoomRes
+import com.adealink.weparty.profile.search.data.SearchUserReq
+import com.adealink.weparty.profile.search.data.SearchUserRes
 import retrofit2.http.Body
 import retrofit2.http.POST
 
 interface SearchHttpService {
+
+    /**
+     * 混合搜索
+     */
+    @POST("search/mixed/list")
+    suspend fun searchAll(@Body req: SearchAllReq): Rlt<Res<SearchAllRes>>
+
+    /**
+     * 搜索用户
+     */
     @POST("playmate/search")
-    suspend fun search(@Body req: SearchReq): Rlt<Res<SearchRes>>
+    suspend fun searchUser(@Body req: SearchUserReq): Rlt<Res<SearchUserRes>>
+
+    /**
+     * 搜索房间
+     */
+    @POST("live/room/list")
+    suspend fun searchRoom(@Body req: SearchRoomReq): Rlt<Res<SearchRoomRes>>
+
 }

+ 115 - 163
module/profile/src/main/java/com/adealink/weparty/profile/search/SearchActivity.kt

@@ -11,90 +11,52 @@ import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.core.view.WindowInsetsCompat
 import androidx.core.view.updateLayoutParams
 import androidx.core.view.updatePadding
-import androidx.lifecycle.MutableLiveData
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
-import com.adealink.frame.base.Rlt
+import androidx.fragment.app.Fragment
+import com.adealink.frame.aab.util.getCompatColor
 import com.adealink.frame.base.fastLazy
 import com.adealink.frame.mvvm.view.viewBinding
 import com.adealink.frame.router.Router
 import com.adealink.frame.router.annotation.RouterUri
-import com.adealink.frame.statistics.BaseStatEvent
 import com.adealink.frame.util.onClick
 import com.adealink.weparty.commonui.BaseActivity
 import com.adealink.weparty.commonui.ext.dp
 import com.adealink.weparty.commonui.ext.fitSystemWindows
 import com.adealink.weparty.commonui.ext.gone
-import com.adealink.weparty.commonui.ext.isSuccess
+import com.adealink.weparty.commonui.ext.hide
 import com.adealink.weparty.commonui.ext.onWindowInsets
 import com.adealink.weparty.commonui.ext.show
+import com.adealink.weparty.commonui.recycleview.adapter.BaseActivityTabFragmentStateAdapter
 import com.adealink.weparty.commonui.recycleview.adapter.MultiTypeListAdapter
-import com.adealink.weparty.commonui.recycleview.itemdecoration.VerticalSpaceItemDecoration
 import com.adealink.weparty.commonui.recycleview.layoutmanager.FlowLayoutManager
-import com.adealink.weparty.commonui.toast.util.showFailedToast
-import com.adealink.weparty.module.playmate.event.PlaymateListEventReporter
-import com.adealink.weparty.module.playmate.event.PlaymateStatEvent
+import com.adealink.weparty.commonui.widget.EmptyFragment
 import com.adealink.weparty.module.profile.Profile
-import com.adealink.weparty.network.data.NoMoreDataError
+import com.adealink.weparty.profile.R
 import com.adealink.weparty.profile.databinding.ActivityUserSearchBinding
+import com.adealink.weparty.profile.databinding.LayoutSearchTabBinding
 import com.adealink.weparty.profile.datasource.local.SearchLocalService
-import com.adealink.weparty.profile.relation.viewmodel.FollowViewModel
 import com.adealink.weparty.profile.search.adapter.SearchHistoryViewBinder
-import com.adealink.weparty.profile.search.adapter.SearchItemViewBinder
 import com.adealink.weparty.profile.search.data.SearchHistoryItemData
-import com.adealink.weparty.profile.search.data.SearchResultItemData
 import com.adealink.weparty.profile.search.viewmodel.SearchViewModel
 import com.adealink.weparty.util.applyImmersive
+import com.google.android.material.tabs.TabLayout
+import com.google.android.material.tabs.TabLayoutMediator
 import com.qmuiteam.qmui.widget.util.QMUIKeyboardHelper
 import com.qmuiteam.qmui.widget.util.QMUIStatusBarHelper
-import com.scwang.smart.refresh.layout.api.RefreshLayout
-import com.scwang.smart.refresh.layout.listener.OnRefreshLoadMoreListener
-import com.adealink.weparty.R as APP_R
 
 @RouterUri(
     path = [Profile.Search.PATH],
     desc = "搜索"
 )
 class SearchActivity : BaseActivity(),
-    SearchItemViewBinder.OnResultClickListener,
     SearchHistoryViewBinder.OnHistoryClickListener {
 
     private val binding by viewBinding(ActivityUserSearchBinding::inflate)
 
     private val historyAdapter by fastLazy { MultiTypeListAdapter<SearchHistoryItemData>() }
     private var historyList = mutableListOf<String>()
-    private val resultAdapter by fastLazy { MultiTypeListAdapter<SearchResultItemData>() }
 
     private val searchViewModel by viewModels<SearchViewModel>()
-    private val followViewModel by viewModels<FollowViewModel>()
-    private var isSearching = MutableLiveData<Boolean>()
-    private var isInputEmpty = MutableLiveData<Boolean>()
-
-    private var exposureReport = object : PlaymateListEventReporter() {
-        override fun getReportEvent(
-            viewHolder: RecyclerView.ViewHolder,
-            position: Int
-        ): BaseStatEvent? {
-            val item = resultAdapter.getItem(position) ?: return null
-            return PlaymateStatEvent(PlaymateStatEvent.Action.EXPOSURE).apply {
-                id to item.data.uid
-            }
-        }
-
-        override fun reportEvent(events: List<BaseStatEvent>) {
-            val reportIds = mutableListOf<String>()
-            for (event in events) {
-                val playmateEvent = (event as? PlaymateStatEvent) ?: continue
-                val idValue = (playmateEvent.id.value as? String) ?: continue
-                reportIds.add(idValue)
-            }
-            if (reportIds.isNotEmpty()) {
-                PlaymateStatEvent(PlaymateStatEvent.Action.EXPOSURE).apply {
-                    ids to reportIds
-                }.send()
-            }
-        }
-    }
+    private lateinit var pageAdapter: PageAdapter
 
     override fun onBeforeCreate() {
         super.onBeforeCreate()
@@ -106,60 +68,37 @@ class SearchActivity : BaseActivity(),
         QMUIStatusBarHelper.setStatusBarLightMode(this)
         setContentView(binding.root)
         applyImmersive { top ->
-            binding.clTop.updateLayoutParams<ConstraintLayout.LayoutParams> {
+            binding.clTop.root.updateLayoutParams<ConstraintLayout.LayoutParams> {
                 topMargin = top
             }
         }
-
-        binding.ivBack.onClick {
+        binding.clTop.ivBack.onClick {
             finish()
         }
-        binding.ivClearInput.onClick {
-            binding.etSearchInput.setText(null)
+        binding.clTop.ivClearInput.onClick {
+            binding.clTop.etSearchInput.setText(null)
         }
 
         historyAdapter.register(SearchHistoryViewBinder(this))
-        binding.rvHistory.layoutManager = FlowLayoutManager(
+        binding.clHistory.rvHistory.layoutManager = FlowLayoutManager(
             FlowLayoutManager.VERTICAL,
             8.dp(),
             10.dp()
         )
-        binding.rvHistory.adapter = historyAdapter
-
-        binding.vRefresh.setEnableRefresh(true)
-        binding.vRefresh.setEnableLoadMore(false)
-        binding.vRefresh.setEnableAutoLoadMore(true)
-        binding.vRefresh.setOnRefreshLoadMoreListener(object : OnRefreshLoadMoreListener {
-            override fun onRefresh(refreshLayout: RefreshLayout) {
-                searchViewModel.reSearch()
-            }
+        binding.clHistory.rvHistory.adapter = historyAdapter
 
-
-            override fun onLoadMore(refreshLayout: RefreshLayout) {
-                searchViewModel.searchMore()
-            }
-        })
-
-        resultAdapter.register(SearchItemViewBinder(this))
-        binding.rvResult.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
-        binding.rvResult.adapter = resultAdapter
-        binding.rvResult.addItemDecoration(
-            VerticalSpaceItemDecoration(20.dp())
-        )
-        exposureReport.register(binding.rvResult)
-
-        binding.etSearchInput.isClickable = true
-        binding.etSearchInput.setOnEditorActionListener(object : OnEditorActionListener {
+        binding.clTop.etSearchInput.isClickable = true
+        binding.clTop.etSearchInput.setOnEditorActionListener(object : OnEditorActionListener {
             override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
                 if (actionId == EditorInfo.IME_ACTION_SEARCH
                     || (actionId == EditorInfo.IME_ACTION_UNSPECIFIED && event != null && event.keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_DOWN)
                 ) {
-                    search(binding.etSearchInput.text?.toString(), false)
+                    search(binding.clTop.etSearchInput.text?.toString(), false)
                 }
                 return true
             }
         })
-        binding.etSearchInput.addTextChangedListener(object : TextWatcher {
+        binding.clTop.etSearchInput.addTextChangedListener(object : TextWatcher {
             override fun beforeTextChanged(
                 s: CharSequence?,
                 start: Int,
@@ -178,16 +117,18 @@ class SearchActivity : BaseActivity(),
 
             override fun afterTextChanged(s: Editable?) {
                 val input = s?.trim()?.toString()
-                isInputEmpty.postValue(input.isNullOrEmpty())
+                searchViewModel.setInput(input)
             }
         })
-        binding.tvSearch.onClick {
-            search(binding.etSearchInput.text?.toString(), false)
+        binding.clTop.tvSearch.onClick {
+            search(binding.clTop.etSearchInput.text?.toString(), false)
         }
 
         fitWindow()
-        QMUIKeyboardHelper.showKeyboard(binding.etSearchInput, 200)
-        binding.etSearchInput.requestFocus()
+        QMUIKeyboardHelper.showKeyboard(binding.clTop.etSearchInput, 200)
+        binding.clTop.etSearchInput.requestFocus()
+
+        initSearchTab()
     }
 
     private fun fitWindow() {
@@ -205,14 +146,71 @@ class SearchActivity : BaseActivity(),
         }
     }
 
+    private fun initSearchTab() {
+        pageAdapter = PageAdapter()
+        binding.clResult.vpContent.offscreenPageLimit = 1
+        binding.clResult.vpContent.adapter = pageAdapter
+        binding.clResult.vpContent.isSaveEnabled = false
+        TabLayoutMediator(
+            binding.clResult.tlTab,
+            binding.clResult.vpContent,
+            true,
+            true
+        ) { tabLayout, position ->
+            tabLayout.setCustomView(R.layout.layout_search_tab)
+            tabLayout.customView?.let { tabView ->
+                val tabViewBinding = LayoutSearchTabBinding.bind(tabView)
+                onConfigureTab(tabViewBinding, position)
+                updateTabView(
+                    tabLayout,
+                    0 == position
+                )
+            }
+        }.attach()
+        binding.clResult.tlTab.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
+
+            override fun onTabSelected(tab: TabLayout.Tab?) {
+                updateTabView(tab, true)
+            }
+
+            override fun onTabUnselected(tab: TabLayout.Tab?) {
+                updateTabView(tab, false)
+            }
+
+            override fun onTabReselected(tab: TabLayout.Tab?) {
+            }
+
+        })
+    }
+
+    private fun onConfigureTab(binding: LayoutSearchTabBinding, position: Int) {
+        binding.tvTab.text = pageAdapter.getTabName(position)
+    }
+
+    private fun updateTabView(
+        tabView: TabLayout.Tab?,
+        isSelected: Boolean
+    ) {
+        tabView?.customView?.let { tabView ->
+            val tabViewBinding = LayoutSearchTabBinding.bind(tabView)
+            if (isSelected) {
+                tabViewBinding.vSelected.show()
+                tabViewBinding.tvTab.setTextColor(getCompatColor(com.adealink.weparty.R.color.color_FF1D2129))
+            } else {
+                tabViewBinding.vSelected.hide()
+                tabViewBinding.tvTab.setTextColor(getCompatColor(com.adealink.weparty.R.color.color_FFC9CDD4))
+            }
+        }
+    }
+
     override fun loadData() {
         super.loadData()
         updateHistoryList(SearchLocalService.getSearchHistory()) { list ->
             if (list.isEmpty()) {
-                binding.clHistory.gone()
+                binding.clHistory.root.gone()
                 return@updateHistoryList
             }
-            binding.clHistory.show()
+            binding.clHistory.root.show()
             historyAdapter.submitList(list.map { SearchHistoryItemData(it) })
         }
     }
@@ -222,13 +220,13 @@ class SearchActivity : BaseActivity(),
         if (keyword.isNullOrEmpty()) {
             return
         }
-        binding.etSearchInput.clearFocus()
-        QMUIKeyboardHelper.hideKeyboard(binding.etSearchInput)
+        binding.clTop.etSearchInput.clearFocus()
+        QMUIKeyboardHelper.hideKeyboard(binding.clTop.etSearchInput)
         hideHistoryList()
 
         //校验非法字符
-        isSearching.postValue(true)
         searchViewModel.search(keyword)
+        binding.clResult.root.show()
 
         if (!fromHistory) {
             addHistoryList(input) { list ->
@@ -260,98 +258,52 @@ class SearchActivity : BaseActivity(),
 
     override fun observeViewModel() {
         super.observeViewModel()
-        isSearching.observe(this) {
-            if (it) {
-                binding.clHistory.gone()
-            }
-        }
-        isInputEmpty.observe(this) {
-            binding.ivClearInput.show(!it)
-            binding.tvSearch.isEnabled = !it
+        searchViewModel.isInputEmpty.observe(this) {
+            binding.clTop.ivClearInput.show(!it)
+            binding.clTop.tvSearch.isEnabled = !it
             if (it) {
-                binding.clSearchResult.gone()
+                binding.clResult.root.gone()
                 if (historyList.isEmpty()) {
-                    binding.clHistory.gone()
+                    binding.clHistory.root.gone()
                 } else {
-                    binding.clHistory.show()
+                    binding.clHistory.root.show()
                 }
             }
         }
-        searchViewModel.searchRltLD.observeWithoutCache(this) { rlt ->
-            binding.vRefresh.finishRefresh()
-            binding.vRefresh.finishLoadMore()
-            binding.vRefresh.setEnableLoadMore(searchViewModel.hasMoreData())
-
-            isSearching.postValue(false)
-            when (rlt) {
-                is Rlt.Failed -> {
-                    if (rlt.error is NoMoreDataError) {
-                        return@observeWithoutCache
-                    }
-                    binding.clSearchResult.show()
-                    binding.rvResult.gone()
-                    binding.vErrorView.show(
-                        errorEmptyResId = APP_R.drawable.common_list_empty_ic,
-                        title = APP_R.string.commonui_list_empty
-                    )
-                }
-
-                is Rlt.Success -> {
-                    binding.clSearchResult.show()
-                    binding.rvResult.show()
-                    binding.vErrorView.gone()
-                }
-            }
-        }
-        searchViewModel.searchResultLD.observeWithoutCache(this) { list ->
-            resultAdapter.submitList(list, true)
-        }
     }
 
     private fun hideHistoryList() {
-        binding.clHistory.gone()
-    }
-
-
-    override fun goProfile(uid: String) {
-        Router.build(this, Profile.UserProfile.PATH)
-            .putExtra(Profile.Common.EXTRA_UID, uid)
-            .start()
+        binding.clHistory.root.gone()
     }
 
-    override fun follow(uid: String) {
-        followViewModel.follow(uid).observe(this) {
-            if (it.isSuccess) {
-                searchViewModel.notifyFollowChanged(uid, true)
-            } else {
-                showFailedToast(it)
-            }
-        }
-    }
-
-    override fun unFollow(uid: String) {
-        followViewModel.unFollow(uid).observe(this) {
-            if (it.isSuccess) {
-                searchViewModel.notifyFollowChanged(uid, false)
-            } else {
-                showFailedToast(it)
-            }
-        }
+    override fun onDestroy() {
+        super.onDestroy()
+        binding.clTop.etSearchInput.clearFocus()
+        QMUIKeyboardHelper.hideKeyboard(binding.clTop.etSearchInput)
     }
 
-
     override fun onClick(
         item: SearchHistoryItemData,
         pos: Int
     ) {
-        binding.etSearchInput.setText(item.keyword)
+        binding.clTop.etSearchInput.setText(item.keyword)
         search(item.keyword, true)
     }
 
-    override fun onDestroy() {
-        super.onDestroy()
-        binding.etSearchInput.clearFocus()
-        QMUIKeyboardHelper.hideKeyboard(binding.etSearchInput)
+    internal inner class PageAdapter : BaseActivityTabFragmentStateAdapter(this) {
+        override fun getTabName(pos: Int): String {
+            return SEARCH_TABS.getOrNull(pos)?.name?.invoke() ?: ""
+        }
+
+        override fun getItemCount(): Int {
+            return SEARCH_TABS.size
+        }
+
+        override fun createFragment(position: Int): Fragment {
+            val tab = SEARCH_TABS.getOrNull(position) ?: return EmptyFragment()
+            return tab.fragmentBuilder.invoke()
+        }
+
     }
 
 }

+ 55 - 0
module/profile/src/main/java/com/adealink/weparty/profile/search/SearchTab.kt

@@ -0,0 +1,55 @@
+package com.adealink.weparty.profile.search
+
+import com.adealink.frame.aab.util.getCompatString
+import com.adealink.weparty.commonui.BaseFragment
+import com.adealink.weparty.profile.R
+import com.adealink.weparty.profile.search.page.SearchAllFragment
+import com.adealink.weparty.profile.search.page.SearchRoomFragment
+import com.adealink.weparty.profile.search.page.SearchUserFragment
+
+enum class SearchTab {
+    ALL,
+    USER,
+    ROOM;
+}
+
+data class Tab(
+    val type: SearchTab,
+    val name: (() -> String),
+    val fragmentBuilder: () -> BaseFragment
+)
+
+val ALL_TAB = Tab(
+    type = SearchTab.ALL,
+    name = {
+        getCompatString(R.string.search_all_tab)
+    },
+    fragmentBuilder = {
+        SearchAllFragment()
+    }
+)
+
+val USER_TAB = Tab(
+    type = SearchTab.USER,
+    name = {
+        getCompatString(R.string.search_user_tab)
+    },
+    fragmentBuilder = {
+        SearchUserFragment()
+    }
+)
+
+val ROOM_TAB = Tab(
+    type = SearchTab.ROOM,
+    name = {
+        getCompatString(R.string.search_room_tab)
+    },
+    fragmentBuilder = {
+        SearchRoomFragment()
+    }
+)
+
+
+val SEARCH_TABS = listOf(
+    ALL_TAB, USER_TAB, ROOM_TAB
+)

+ 37 - 0
module/profile/src/main/java/com/adealink/weparty/profile/search/TabManager.kt

@@ -0,0 +1,37 @@
+package com.adealink.weparty.profile.search
+
+
+interface ITabManager {
+    fun initTabs(tabs: List<Tab>) {}
+
+    fun getTabs(): List<Tab> = arrayListOf()
+
+    fun getCount(): Int = 0
+
+    fun getTab(index: Int): Tab? = null
+
+    fun getTabIndex(tab: Tab): Int = 0
+}
+
+class TabManager : ITabManager {
+    private var tabs = mutableListOf<Tab>()
+
+    override fun initTabs(tabs: List<Tab>) {
+        this.tabs.clear()
+        this.tabs.addAll(tabs)
+    }
+
+    override fun getTabs() = tabs
+
+    override fun getCount(): Int {
+        return tabs.size
+    }
+
+    override fun getTab(index: Int): Tab? {
+        return if (index in tabs.indices) tabs[index] else null
+    }
+
+    override fun getTabIndex(tab: Tab): Int {
+        return tabs.indexOf(tab)
+    }
+}

+ 36 - 0
module/profile/src/main/java/com/adealink/weparty/profile/search/adapter/SearchRoomItemViewBinder.kt

@@ -0,0 +1,36 @@
+package com.adealink.weparty.profile.search.adapter
+
+import android.annotation.SuppressLint
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import com.adealink.frame.util.onClick
+import com.adealink.weparty.commonui.recycleview.adapter.BindingViewHolder
+import com.adealink.weparty.commonui.recycleview.adapter.multitype.ItemViewBinder
+import com.adealink.weparty.profile.databinding.LayoutProfileSearchRoomItemBinding
+import com.adealink.weparty.profile.search.data.SearchRoomResultItemData
+
+class SearchRoomItemViewBinder(val listener: OnRoomResultClickListener) :
+    ItemViewBinder<SearchRoomResultItemData, BindingViewHolder<LayoutProfileSearchRoomItemBinding>>() {
+
+    @SuppressLint("SetTextI18n")
+    override fun onBindViewHolder(
+        holder: BindingViewHolder<LayoutProfileSearchRoomItemBinding>,
+        item: SearchRoomResultItemData,
+    ) {
+        holder.binding.root.onClick {
+            listener.goRoom(item.data.roomId)
+        }
+    }
+
+    override fun onCreateViewHolder(
+        inflater: LayoutInflater,
+        parent: ViewGroup,
+    ): BindingViewHolder<LayoutProfileSearchRoomItemBinding> {
+        return BindingViewHolder(LayoutProfileSearchRoomItemBinding.inflate(inflater, parent, false))
+    }
+
+    interface OnRoomResultClickListener {
+        fun goRoom(roomId: String)
+    }
+
+}

+ 9 - 9
module/profile/src/main/java/com/adealink/weparty/profile/search/adapter/SearchItemViewBinder.kt → module/profile/src/main/java/com/adealink/weparty/profile/search/adapter/SearchUserItemViewBinder.kt

@@ -11,16 +11,16 @@ import com.adealink.weparty.commonui.recycleview.adapter.BindingViewHolder
 import com.adealink.weparty.commonui.recycleview.adapter.multitype.ItemViewBinder
 import com.adealink.weparty.module.profile.ProfileModule
 import com.adealink.weparty.profile.R
-import com.adealink.weparty.profile.databinding.LayoutProfileSearchItemBinding
-import com.adealink.weparty.profile.search.data.SearchResultItemData
+import com.adealink.weparty.profile.databinding.LayoutProfileSearchUserItemBinding
+import com.adealink.weparty.profile.search.data.SearchUserResultItemData
 
-class SearchItemViewBinder(val listener: OnResultClickListener) :
-    ItemViewBinder<SearchResultItemData, BindingViewHolder<LayoutProfileSearchItemBinding>>() {
+class SearchUserItemViewBinder(val listener: OnUserResultClickListener) :
+    ItemViewBinder<SearchUserResultItemData, BindingViewHolder<LayoutProfileSearchUserItemBinding>>() {
 
     @SuppressLint("SetTextI18n")
     override fun onBindViewHolder(
-        holder: BindingViewHolder<LayoutProfileSearchItemBinding>,
-        item: SearchResultItemData,
+        holder: BindingViewHolder<LayoutProfileSearchUserItemBinding>,
+        item: SearchUserResultItemData,
     ) {
         holder.binding.root.onClick {
             listener.goProfile(item.data.uid)
@@ -56,11 +56,11 @@ class SearchItemViewBinder(val listener: OnResultClickListener) :
     override fun onCreateViewHolder(
         inflater: LayoutInflater,
         parent: ViewGroup,
-    ): BindingViewHolder<LayoutProfileSearchItemBinding> {
-        return BindingViewHolder(LayoutProfileSearchItemBinding.inflate(inflater, parent, false))
+    ): BindingViewHolder<LayoutProfileSearchUserItemBinding> {
+        return BindingViewHolder(LayoutProfileSearchUserItemBinding.inflate(inflater, parent, false))
     }
 
-    interface OnResultClickListener {
+    interface OnUserResultClickListener {
         fun goProfile(uid: String)
 
         fun follow(uid: String)

+ 52 - 8
module/profile/src/main/java/com/adealink/weparty/profile/search/data/SearchData.kt

@@ -6,18 +6,43 @@ import com.google.gson.annotations.GsonNullable
 import com.google.gson.annotations.SerializedName
 
 
-data class SearchReq(
+data class SearchAllReq(
+    @SerializedName("keyword") val keyword: String,
+)
+
+data class SearchAllRes(
+    @GsonNullable
+    @SerializedName("playmate") val playmate: List<SearchUserData>?,
+    @GsonNullable
+    @SerializedName("rooms") val rooms: List<SearchRoomData>?,
+)
+
+
+data class SearchUserReq(
+    @SerializedName("keyword") val keyword: String,
+    @SerializedName("page") val page: PageReq
+)
+
+data class SearchUserRes(
+    @SerializedName("list") val list: List<SearchUserData>,
+    @GsonNullable
+    @SerializedName("next") val next: String?
+)
+
+
+data class SearchRoomReq(
     @SerializedName("keyword") val keyword: String,
     @SerializedName("page") val page: PageReq
 )
 
-data class SearchRes(
-    @SerializedName("list") val list: List<SearchData>,
+data class SearchRoomRes(
+    @SerializedName("list") val list: List<SearchRoomData>,
     @GsonNullable
     @SerializedName("next") val next: String?
 )
 
-data class SearchData(
+
+data class SearchUserData(
     @SerializedName("userNo") val uid: String,
     @GsonNullable
     @SerializedName("nickname") val nickName: String? = null,
@@ -38,16 +63,27 @@ data class SearchData(
     @SerializedName("online") val online: Boolean? = null //在线状态
 )
 
-data class SearchResultItemData(
-    val data: SearchData
+data class SearchUserResultItemData(
+    val data: SearchUserData
 ) : BaseListItemData {
     override fun areContentsTheSame(newItem: Any): Boolean {
-        val new = newItem as? SearchResultItemData ?: return false
+        val new = newItem as? SearchUserResultItemData ?: return false
         return data.uid == new.data.uid
                 && data.follow == new.data.follow
     }
 }
 
+data class SearchRoomResultItemData(
+    val data: SearchRoomData
+) : BaseListItemData {
+    override fun areContentsTheSame(newItem: Any): Boolean {
+        val new = newItem as? SearchRoomResultItemData ?: return false
+        return data.roomId == new.data.roomId
+                && data.roomTitle == new.data.roomTitle
+                && data.roomCover == new.data.roomCover
+    }
+}
+
 data class SearchHistoryItemData(
     val keyword: String
 ) : BaseListItemData {
@@ -55,4 +91,12 @@ data class SearchHistoryItemData(
         val new = newItem as? SearchHistoryItemData ?: return false
         return keyword == new.keyword
     }
-}
+}
+
+
+data class SearchRoomData(
+    @SerializedName("roomId") val roomId: String,
+    @SerializedName("roomTitle") val roomTitle: String,
+    @SerializedName("roomCover") val roomCover: String,
+)
+

+ 165 - 0
module/profile/src/main/java/com/adealink/weparty/profile/search/page/SearchAllFragment.kt

@@ -0,0 +1,165 @@
+package com.adealink.weparty.profile.search.page
+
+import androidx.fragment.app.activityViewModels
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.MutableLiveData
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.adealink.frame.base.Rlt
+import com.adealink.frame.base.fastLazy
+import com.adealink.frame.mvvm.view.viewBinding
+import com.adealink.frame.router.Router
+import com.adealink.frame.statistics.BaseStatEvent
+import com.adealink.weparty.commonui.BaseFragment
+import com.adealink.weparty.commonui.ext.dp
+import com.adealink.weparty.commonui.ext.gone
+import com.adealink.weparty.commonui.ext.isSuccess
+import com.adealink.weparty.commonui.ext.show
+import com.adealink.weparty.commonui.recycleview.adapter.MultiTypeListAdapter
+import com.adealink.weparty.commonui.recycleview.itemdecoration.VerticalSpaceItemDecoration
+import com.adealink.weparty.commonui.toast.util.showFailedToast
+import com.adealink.weparty.module.playmate.event.PlaymateListEventReporter
+import com.adealink.weparty.module.playmate.event.PlaymateStatEvent
+import com.adealink.weparty.module.profile.Profile
+import com.adealink.weparty.network.data.NoMoreDataError
+import com.adealink.weparty.profile.R
+import com.adealink.weparty.profile.databinding.FragmentSearchAllBinding
+import com.adealink.weparty.profile.relation.viewmodel.FollowViewModel
+import com.adealink.weparty.profile.search.adapter.SearchUserItemViewBinder
+import com.adealink.weparty.profile.search.data.SearchUserResultItemData
+import com.adealink.weparty.profile.search.viewmodel.SearchAllViewModel
+import com.adealink.weparty.profile.search.viewmodel.SearchViewModel
+import com.scwang.smart.refresh.layout.api.RefreshLayout
+import com.scwang.smart.refresh.layout.listener.OnRefreshLoadMoreListener
+import com.adealink.weparty.R as APP_R
+
+class SearchAllFragment : BaseFragment(R.layout.fragment_search_all),
+    SearchUserItemViewBinder.OnUserResultClickListener {
+
+    private val binding by viewBinding(FragmentSearchAllBinding::bind)
+
+    private val resultAdapter by fastLazy { MultiTypeListAdapter<SearchUserResultItemData>() }
+    private val searchViewModel by activityViewModels<SearchViewModel>()
+    private val searchUserViewModel by viewModels<SearchAllViewModel>()
+    private val followViewModel by activityViewModels<FollowViewModel>()
+    private var isSearching = MutableLiveData<Boolean>()
+    private var isInputEmpty = MutableLiveData<Boolean>()
+
+    private var exposureReport = object : PlaymateListEventReporter() {
+        override fun getReportEvent(
+            viewHolder: RecyclerView.ViewHolder,
+            position: Int
+        ): BaseStatEvent? {
+            val item = resultAdapter.getItem(position) ?: return null
+            return PlaymateStatEvent(PlaymateStatEvent.Action.EXPOSURE).apply {
+                id to item.data.uid
+            }
+        }
+
+        override fun reportEvent(events: List<BaseStatEvent>) {
+            val reportIds = mutableListOf<String>()
+            for (event in events) {
+                val playmateEvent = (event as? PlaymateStatEvent) ?: continue
+                val idValue = (playmateEvent.id.value as? String) ?: continue
+                reportIds.add(idValue)
+            }
+            if (reportIds.isNotEmpty()) {
+                PlaymateStatEvent(PlaymateStatEvent.Action.EXPOSURE).apply {
+                    ids to reportIds
+                }.send()
+            }
+        }
+    }
+
+    override fun initViews() {
+        super.initViews()
+        binding.vRefresh.setEnableRefresh(true)
+        binding.vRefresh.setEnableLoadMore(false)
+        binding.vRefresh.setEnableAutoLoadMore(true)
+        binding.vRefresh.setOnRefreshLoadMoreListener(object : OnRefreshLoadMoreListener {
+            override fun onRefresh(refreshLayout: RefreshLayout) {
+                searchUserViewModel.reSearch()
+            }
+
+
+            override fun onLoadMore(refreshLayout: RefreshLayout) {
+                searchUserViewModel.searchMore()
+            }
+        })
+
+        resultAdapter.register(SearchUserItemViewBinder(this))
+        binding.rvResult.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
+        binding.rvResult.adapter = resultAdapter
+        binding.rvResult.addItemDecoration(
+            VerticalSpaceItemDecoration(20.dp())
+        )
+        exposureReport.register(binding.rvResult)
+    }
+
+    override fun observeViewModel() {
+        super.observeViewModel()
+        searchViewModel.keywordLD.observe(viewLifecycleOwner) {
+            searchUserViewModel.search(it)
+        }
+        searchViewModel.followChangedLD.observeWithoutCache(viewLifecycleOwner) {
+            searchUserViewModel.notifyFollowChanged(it.first, it.second)
+        }
+        searchUserViewModel.searchRltLD.observeWithoutCache(this) { rlt ->
+            binding.vRefresh.finishRefresh()
+            binding.vRefresh.finishLoadMore()
+            binding.vRefresh.setEnableLoadMore(searchUserViewModel.hasMoreData())
+
+            isSearching.postValue(false)
+            when (rlt) {
+                is Rlt.Failed -> {
+                    if (rlt.error is NoMoreDataError) {
+                        return@observeWithoutCache
+                    }
+                    binding.rvResult.show()
+                    binding.rvResult.gone()
+                    binding.vErrorView.show(
+                        errorEmptyResId = APP_R.drawable.common_list_empty_ic,
+                        title = APP_R.string.commonui_list_empty
+                    )
+                }
+
+                is Rlt.Success -> {
+                    binding.rvResult.show()
+                    binding.rvResult.show()
+                    binding.vErrorView.gone()
+                }
+            }
+        }
+        searchUserViewModel.searchResultLD.observeWithoutCache(this) { list ->
+            resultAdapter.submitList(list, true)
+        }
+    }
+
+    override fun goProfile(uid: String) {
+        val act = activity ?: return
+        Router.build(act, Profile.UserProfile.PATH)
+            .putExtra(Profile.Common.EXTRA_UID, uid)
+            .start()
+    }
+
+    override fun follow(uid: String) {
+        followViewModel.follow(uid).observe(this) {
+            if (it.isSuccess) {
+                searchViewModel.notifyFollowChanged(uid, true)
+            } else {
+                showFailedToast(it)
+            }
+        }
+    }
+
+    override fun unFollow(uid: String) {
+        followViewModel.unFollow(uid).observe(this) {
+            if (it.isSuccess) {
+                searchViewModel.notifyFollowChanged(uid, false)
+            } else {
+                showFailedToast(it)
+            }
+        }
+    }
+
+}

+ 105 - 0
module/profile/src/main/java/com/adealink/weparty/profile/search/page/SearchRoomFragment.kt

@@ -0,0 +1,105 @@
+package com.adealink.weparty.profile.search.page
+
+import androidx.fragment.app.activityViewModels
+import androidx.fragment.app.viewModels
+import androidx.recyclerview.widget.GridLayoutManager
+import com.adealink.frame.base.Rlt
+import com.adealink.frame.base.fastLazy
+import com.adealink.frame.mvvm.view.viewBinding
+import com.adealink.frame.router.Router
+import com.adealink.weparty.commonui.BaseFragment
+import com.adealink.weparty.commonui.ext.dp
+import com.adealink.weparty.commonui.ext.gone
+import com.adealink.weparty.commonui.ext.show
+import com.adealink.weparty.commonui.recycleview.adapter.MultiTypeListAdapter
+import com.adealink.weparty.commonui.recycleview.itemdecoration.GridSpacingItemDecoration
+import com.adealink.weparty.module.room.Room
+import com.adealink.weparty.network.data.NoMoreDataError
+import com.adealink.weparty.profile.R
+import com.adealink.weparty.profile.databinding.FragmentSearchRoomBinding
+import com.adealink.weparty.profile.search.adapter.SearchRoomItemViewBinder
+import com.adealink.weparty.profile.search.data.SearchRoomResultItemData
+import com.adealink.weparty.profile.search.viewmodel.SearchRoomViewModel
+import com.adealink.weparty.profile.search.viewmodel.SearchViewModel
+import com.scwang.smart.refresh.layout.api.RefreshLayout
+import com.scwang.smart.refresh.layout.listener.OnRefreshLoadMoreListener
+import com.adealink.weparty.R as APP_R
+
+class SearchRoomFragment : BaseFragment(R.layout.fragment_search_room),
+    SearchRoomItemViewBinder.OnRoomResultClickListener {
+
+    private val binding by viewBinding(FragmentSearchRoomBinding::bind)
+
+    private val resultAdapter by fastLazy { MultiTypeListAdapter<SearchRoomResultItemData>() }
+    private val searchViewModel by activityViewModels<SearchViewModel>()
+    private val searchRoomViewModel by viewModels<SearchRoomViewModel>()
+    override fun initViews() {
+        super.initViews()
+        binding.vRefresh.setEnableRefresh(true)
+        binding.vRefresh.setEnableLoadMore(false)
+        binding.vRefresh.setEnableAutoLoadMore(true)
+        binding.vRefresh.setOnRefreshLoadMoreListener(object : OnRefreshLoadMoreListener {
+            override fun onRefresh(refreshLayout: RefreshLayout) {
+                searchRoomViewModel.reSearch()
+            }
+
+
+            override fun onLoadMore(refreshLayout: RefreshLayout) {
+                searchRoomViewModel.searchMore()
+            }
+        })
+
+        resultAdapter.register(SearchRoomItemViewBinder(this))
+        binding.rvResult.layoutManager = GridLayoutManager(context, 2)
+        binding.rvResult.adapter = resultAdapter
+        binding.rvResult.addItemDecoration(
+            GridSpacingItemDecoration(2, 13.dp(), 16.dp(), false)
+        )
+    }
+
+    override fun observeViewModel() {
+        super.observeViewModel()
+        searchViewModel.keywordLD.observe(viewLifecycleOwner) {
+            searchRoomViewModel.search(it)
+        }
+//        searchViewModel.followChangedLD.observeWithoutCache(viewLifecycleOwner) {
+//            searchUserViewModel.notifyFollowChanged(it.first, it.second)
+//        }
+        searchRoomViewModel.searchRltLD.observeWithoutCache(this) { rlt ->
+            binding.vRefresh.finishRefresh()
+            binding.vRefresh.finishLoadMore()
+            binding.vRefresh.setEnableLoadMore(searchRoomViewModel.hasMoreData())
+
+            when (rlt) {
+                is Rlt.Failed -> {
+                    if (rlt.error is NoMoreDataError) {
+                        return@observeWithoutCache
+                    }
+                    binding.rvResult.show()
+                    binding.rvResult.gone()
+                    binding.vErrorView.show(
+                        errorEmptyResId = APP_R.drawable.common_list_empty_ic,
+                        title = APP_R.string.commonui_list_empty
+                    )
+                }
+
+                is Rlt.Success -> {
+                    binding.rvResult.show()
+                    binding.rvResult.show()
+                    binding.vErrorView.gone()
+                }
+            }
+        }
+        searchRoomViewModel.searchResultLD.observeWithoutCache(this) { list ->
+            resultAdapter.submitList(list, true)
+        }
+    }
+
+    override fun goRoom(roomId: String) {
+        val act = activity ?: return
+        Router.build(act, Room.Room.PATH)
+            .putExtra(Room.Room.EXTRA_ENTER_ROOM_ID, roomId)
+            .start()
+    }
+
+}

+ 160 - 0
module/profile/src/main/java/com/adealink/weparty/profile/search/page/SearchUserFragment.kt

@@ -0,0 +1,160 @@
+package com.adealink.weparty.profile.search.page
+
+import androidx.fragment.app.activityViewModels
+import androidx.fragment.app.viewModels
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.adealink.frame.base.Rlt
+import com.adealink.frame.base.fastLazy
+import com.adealink.frame.mvvm.view.viewBinding
+import com.adealink.frame.router.Router
+import com.adealink.frame.statistics.BaseStatEvent
+import com.adealink.weparty.commonui.BaseFragment
+import com.adealink.weparty.commonui.ext.dp
+import com.adealink.weparty.commonui.ext.gone
+import com.adealink.weparty.commonui.ext.isSuccess
+import com.adealink.weparty.commonui.ext.show
+import com.adealink.weparty.commonui.recycleview.adapter.MultiTypeListAdapter
+import com.adealink.weparty.commonui.recycleview.itemdecoration.VerticalSpaceItemDecoration
+import com.adealink.weparty.commonui.toast.util.showFailedToast
+import com.adealink.weparty.module.playmate.event.PlaymateListEventReporter
+import com.adealink.weparty.module.playmate.event.PlaymateStatEvent
+import com.adealink.weparty.module.profile.Profile
+import com.adealink.weparty.network.data.NoMoreDataError
+import com.adealink.weparty.profile.R
+import com.adealink.weparty.profile.databinding.FragmentSearchUserBinding
+import com.adealink.weparty.profile.relation.viewmodel.FollowViewModel
+import com.adealink.weparty.profile.search.adapter.SearchUserItemViewBinder
+import com.adealink.weparty.profile.search.data.SearchUserResultItemData
+import com.adealink.weparty.profile.search.viewmodel.SearchUserViewModel
+import com.adealink.weparty.profile.search.viewmodel.SearchViewModel
+import com.scwang.smart.refresh.layout.api.RefreshLayout
+import com.scwang.smart.refresh.layout.listener.OnRefreshLoadMoreListener
+import com.adealink.weparty.R as APP_R
+
+class SearchUserFragment : BaseFragment(R.layout.fragment_search_user),
+    SearchUserItemViewBinder.OnUserResultClickListener {
+
+    private val binding by viewBinding(FragmentSearchUserBinding::bind)
+
+    private val resultAdapter by fastLazy { MultiTypeListAdapter<SearchUserResultItemData>() }
+    private val searchViewModel by activityViewModels<SearchViewModel>()
+    private val searchUserViewModel by viewModels<SearchUserViewModel>()
+    private val followViewModel by activityViewModels<FollowViewModel>()
+    private var exposureReport = object : PlaymateListEventReporter() {
+        override fun getReportEvent(
+            viewHolder: RecyclerView.ViewHolder,
+            position: Int
+        ): BaseStatEvent? {
+            val item = resultAdapter.getItem(position) ?: return null
+            return PlaymateStatEvent(PlaymateStatEvent.Action.EXPOSURE).apply {
+                id to item.data.uid
+            }
+        }
+
+        override fun reportEvent(events: List<BaseStatEvent>) {
+            val reportIds = mutableListOf<String>()
+            for (event in events) {
+                val playmateEvent = (event as? PlaymateStatEvent) ?: continue
+                val idValue = (playmateEvent.id.value as? String) ?: continue
+                reportIds.add(idValue)
+            }
+            if (reportIds.isNotEmpty()) {
+                PlaymateStatEvent(PlaymateStatEvent.Action.EXPOSURE).apply {
+                    ids to reportIds
+                }.send()
+            }
+        }
+    }
+
+    override fun initViews() {
+        super.initViews()
+        binding.vRefresh.setEnableRefresh(true)
+        binding.vRefresh.setEnableLoadMore(false)
+        binding.vRefresh.setEnableAutoLoadMore(true)
+        binding.vRefresh.setOnRefreshLoadMoreListener(object : OnRefreshLoadMoreListener {
+            override fun onRefresh(refreshLayout: RefreshLayout) {
+                searchUserViewModel.reSearch()
+            }
+
+
+            override fun onLoadMore(refreshLayout: RefreshLayout) {
+                searchUserViewModel.searchMore()
+            }
+        })
+
+        resultAdapter.register(SearchUserItemViewBinder(this))
+        binding.rvResult.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
+        binding.rvResult.adapter = resultAdapter
+        binding.rvResult.addItemDecoration(
+            VerticalSpaceItemDecoration(20.dp())
+        )
+        exposureReport.register(binding.rvResult)
+    }
+
+    override fun observeViewModel() {
+        super.observeViewModel()
+        searchViewModel.keywordLD.observe(viewLifecycleOwner) {
+            searchUserViewModel.search(it)
+        }
+        searchViewModel.followChangedLD.observeWithoutCache(viewLifecycleOwner) {
+            searchUserViewModel.notifyFollowChanged(it.first, it.second)
+        }
+        searchUserViewModel.searchRltLD.observeWithoutCache(this) { rlt ->
+            binding.vRefresh.finishRefresh()
+            binding.vRefresh.finishLoadMore()
+            binding.vRefresh.setEnableLoadMore(searchUserViewModel.hasMoreData())
+
+            when (rlt) {
+                is Rlt.Failed -> {
+                    if (rlt.error is NoMoreDataError) {
+                        return@observeWithoutCache
+                    }
+                    binding.rvResult.show()
+                    binding.rvResult.gone()
+                    binding.vErrorView.show(
+                        errorEmptyResId = APP_R.drawable.common_list_empty_ic,
+                        title = APP_R.string.commonui_list_empty
+                    )
+                }
+
+                is Rlt.Success -> {
+                    binding.rvResult.show()
+                    binding.rvResult.show()
+                    binding.vErrorView.gone()
+                }
+            }
+        }
+        searchUserViewModel.searchResultLD.observeWithoutCache(this) { list ->
+            resultAdapter.submitList(list, true)
+        }
+    }
+
+    override fun goProfile(uid: String) {
+        val act = activity ?: return
+        Router.build(act, Profile.UserProfile.PATH)
+            .putExtra(Profile.Common.EXTRA_UID, uid)
+            .start()
+    }
+
+    override fun follow(uid: String) {
+        followViewModel.follow(uid).observe(this) {
+            if (it.isSuccess) {
+                searchViewModel.notifyFollowChanged(uid, true)
+            } else {
+                showFailedToast(it)
+            }
+        }
+    }
+
+    override fun unFollow(uid: String) {
+        followViewModel.unFollow(uid).observe(this) {
+            if (it.isSuccess) {
+                searchViewModel.notifyFollowChanged(uid, false)
+            } else {
+                showFailedToast(it)
+            }
+        }
+    }
+
+}

+ 101 - 0
module/profile/src/main/java/com/adealink/weparty/profile/search/viewmodel/SearchAllViewModel.kt

@@ -0,0 +1,101 @@
+package com.adealink.weparty.profile.search.viewmodel
+
+import com.adealink.frame.base.Rlt
+import com.adealink.frame.mvvm.livedata.ExtMutableLiveData
+import com.adealink.frame.mvvm.viewmodel.BaseViewModel
+import com.adealink.frame.network.data.PageReq
+import com.adealink.weparty.App
+import com.adealink.weparty.module.profile.ProfileModule
+import com.adealink.weparty.network.data.NoMoreDataError
+import com.adealink.weparty.profile.datasource.remote.SearchHttpService
+import com.adealink.weparty.profile.search.data.SearchUserReq
+import com.adealink.weparty.profile.search.data.SearchUserResultItemData
+import com.adealink.weparty.util.PageHandler
+import kotlinx.coroutines.launch
+
+class SearchAllViewModel : BaseViewModel() {
+
+    private val searchHttpService by lazy {
+        App.instance.networkService.getHttpService(SearchHttpService::class.java)
+    }
+
+    val searchResultSet = mutableSetOf<String>()
+    val searchResultLD = ExtMutableLiveData<List<SearchUserResultItemData>>()
+    val searchRltLD = ExtMutableLiveData<Rlt<Any>>()
+    private val searchResultList = mutableListOf<SearchUserResultItemData>()
+    private val pageHandler = PageHandler()
+    private var keyword: String? = null
+    fun search(keyword: String?) {
+        pageHandler.reset()
+        this.keyword = keyword
+        searchResultSet.clear()
+        searchResultList.clear()
+        searchMore(keyword)
+    }
+
+    fun reSearch() {
+        search(keyword)
+    }
+
+    fun hasMoreData(): Boolean {
+        return !pageHandler.isEnd
+    }
+
+    fun searchMore(keyword: String? = this@SearchAllViewModel.keyword) {
+        if (pageHandler.isEnd) {
+            searchRltLD.send(Rlt.Failed(NoMoreDataError()))
+            return
+        }
+        viewModelScope.launch {
+            if (keyword.isNullOrEmpty()) {
+                searchRltLD.send(Rlt.Success(Any()))
+                searchResultLD.send(emptyList())
+                return@launch
+            }
+            val rlt = searchHttpService.searchUser(
+                SearchUserReq(
+                    keyword = keyword,
+                    page = PageReq(size = pageHandler.pageSize, next = pageHandler.currentPage)
+                )
+            )
+            when (rlt) {
+                is Rlt.Failed -> {
+                    if (keyword != this@SearchAllViewModel.keyword) {
+                        return@launch
+                    }
+                    searchRltLD.send(rlt)
+                }
+
+                is Rlt.Success -> {
+                    if (keyword != this@SearchAllViewModel.keyword) {
+                        return@launch
+                    }
+                    pageHandler.nextPage(rlt.data.data?.next)
+
+                    val nextList = rlt.data.data?.list?.filter {
+                        !searchResultSet.contains(it.uid)
+                    } ?: emptyList()
+
+                    val nextItemList = nextList.map {
+                        it.online?.let { online ->
+                            ProfileModule.updateUserOnline(it.uid, online)
+                        }
+
+                        SearchUserResultItemData(it)
+                    }
+                    searchResultSet.addAll(nextList.map { it.uid })
+                    searchResultList.addAll(nextItemList)
+                    searchRltLD.send(rlt)
+                    searchResultLD.send(searchResultList)
+                }
+            }
+        }
+    }
+
+    fun notifyFollowChanged(uid: String, follow: Boolean) {
+        viewModelScope.launch {
+            searchResultList.find { it.data.uid == uid }?.data?.follow = follow
+            searchResultLD.send(searchResultList)
+        }
+    }
+}

+ 94 - 0
module/profile/src/main/java/com/adealink/weparty/profile/search/viewmodel/SearchRoomViewModel.kt

@@ -0,0 +1,94 @@
+package com.adealink.weparty.profile.search.viewmodel
+
+import com.adealink.frame.base.Rlt
+import com.adealink.frame.mvvm.livedata.ExtMutableLiveData
+import com.adealink.frame.mvvm.viewmodel.BaseViewModel
+import com.adealink.frame.network.data.PageReq
+import com.adealink.weparty.App
+import com.adealink.weparty.module.profile.ProfileModule
+import com.adealink.weparty.network.data.NoMoreDataError
+import com.adealink.weparty.profile.datasource.remote.SearchHttpService
+import com.adealink.weparty.profile.search.data.SearchRoomReq
+import com.adealink.weparty.profile.search.data.SearchRoomRes
+import com.adealink.weparty.profile.search.data.SearchRoomResultItemData
+import com.adealink.weparty.profile.search.data.SearchUserReq
+import com.adealink.weparty.profile.search.data.SearchUserResultItemData
+import com.adealink.weparty.util.PageHandler
+import kotlinx.coroutines.launch
+
+class SearchRoomViewModel : BaseViewModel() {
+
+    private val searchHttpService by lazy {
+        App.instance.networkService.getHttpService(SearchHttpService::class.java)
+    }
+
+    val searchResultSet = mutableSetOf<String>()
+    val searchResultLD = ExtMutableLiveData<List<SearchRoomResultItemData>>()
+    val searchRltLD = ExtMutableLiveData<Rlt<Any>>()
+    private val searchResultList = mutableListOf<SearchRoomResultItemData>()
+    private val pageHandler = PageHandler()
+    private var keyword: String? = null
+    fun search(keyword: String?) {
+        pageHandler.reset()
+        this.keyword = keyword
+        searchResultSet.clear()
+        searchResultList.clear()
+        searchMore(keyword)
+    }
+
+    fun reSearch() {
+        search(keyword)
+    }
+
+    fun hasMoreData(): Boolean {
+        return !pageHandler.isEnd
+    }
+
+    fun searchMore(keyword: String? = this@SearchRoomViewModel.keyword) {
+        if (pageHandler.isEnd) {
+            searchRltLD.send(Rlt.Failed(NoMoreDataError()))
+            return
+        }
+        viewModelScope.launch {
+            if (keyword.isNullOrEmpty()) {
+                searchRltLD.send(Rlt.Success(Any()))
+                searchResultLD.send(emptyList())
+                return@launch
+            }
+            val rlt = searchHttpService.searchRoom(
+                SearchRoomReq(
+                    keyword = keyword,
+                    page = PageReq(size = pageHandler.pageSize, next = pageHandler.currentPage)
+                )
+            )
+            when (rlt) {
+                is Rlt.Failed -> {
+                    if (keyword != this@SearchRoomViewModel.keyword) {
+                        return@launch
+                    }
+                    searchRltLD.send(rlt)
+                }
+
+                is Rlt.Success -> {
+                    if (keyword != this@SearchRoomViewModel.keyword) {
+                        return@launch
+                    }
+                    pageHandler.nextPage(rlt.data.data?.next)
+
+                    val nextList = rlt.data.data?.list?.filter {
+                        !searchResultSet.contains(it.roomId)
+                    } ?: emptyList()
+
+                    val nextItemList = nextList.map {
+                        SearchRoomResultItemData(it)
+                    }
+                    searchResultSet.addAll(nextList.map { it.roomId })
+                    searchResultList.addAll(nextItemList)
+                    searchRltLD.send(rlt)
+                    searchResultLD.send(searchResultList)
+                }
+            }
+        }
+    }
+
+}

+ 101 - 0
module/profile/src/main/java/com/adealink/weparty/profile/search/viewmodel/SearchUserViewModel.kt

@@ -0,0 +1,101 @@
+package com.adealink.weparty.profile.search.viewmodel
+
+import com.adealink.frame.base.Rlt
+import com.adealink.frame.mvvm.livedata.ExtMutableLiveData
+import com.adealink.frame.mvvm.viewmodel.BaseViewModel
+import com.adealink.frame.network.data.PageReq
+import com.adealink.weparty.App
+import com.adealink.weparty.module.profile.ProfileModule
+import com.adealink.weparty.network.data.NoMoreDataError
+import com.adealink.weparty.profile.datasource.remote.SearchHttpService
+import com.adealink.weparty.profile.search.data.SearchUserReq
+import com.adealink.weparty.profile.search.data.SearchUserResultItemData
+import com.adealink.weparty.util.PageHandler
+import kotlinx.coroutines.launch
+
+class SearchUserViewModel : BaseViewModel() {
+
+    private val searchHttpService by lazy {
+        App.instance.networkService.getHttpService(SearchHttpService::class.java)
+    }
+
+    val searchResultSet = mutableSetOf<String>()
+    val searchResultLD = ExtMutableLiveData<List<SearchUserResultItemData>>()
+    val searchRltLD = ExtMutableLiveData<Rlt<Any>>()
+    private val searchResultList = mutableListOf<SearchUserResultItemData>()
+    private val pageHandler = PageHandler()
+    private var keyword: String? = null
+    fun search(keyword: String?) {
+        pageHandler.reset()
+        this.keyword = keyword
+        searchResultSet.clear()
+        searchResultList.clear()
+        searchMore(keyword)
+    }
+
+    fun reSearch() {
+        search(keyword)
+    }
+
+    fun hasMoreData(): Boolean {
+        return !pageHandler.isEnd
+    }
+
+    fun searchMore(keyword: String? = this@SearchUserViewModel.keyword) {
+        if (pageHandler.isEnd) {
+            searchRltLD.send(Rlt.Failed(NoMoreDataError()))
+            return
+        }
+        viewModelScope.launch {
+            if (keyword.isNullOrEmpty()) {
+                searchRltLD.send(Rlt.Success(Any()))
+                searchResultLD.send(emptyList())
+                return@launch
+            }
+            val rlt = searchHttpService.searchUser(
+                SearchUserReq(
+                    keyword = keyword,
+                    page = PageReq(size = pageHandler.pageSize, next = pageHandler.currentPage)
+                )
+            )
+            when (rlt) {
+                is Rlt.Failed -> {
+                    if (keyword != this@SearchUserViewModel.keyword) {
+                        return@launch
+                    }
+                    searchRltLD.send(rlt)
+                }
+
+                is Rlt.Success -> {
+                    if (keyword != this@SearchUserViewModel.keyword) {
+                        return@launch
+                    }
+                    pageHandler.nextPage(rlt.data.data?.next)
+
+                    val nextList = rlt.data.data?.list?.filter {
+                        !searchResultSet.contains(it.uid)
+                    } ?: emptyList()
+
+                    val nextItemList = nextList.map {
+                        it.online?.let { online ->
+                            ProfileModule.updateUserOnline(it.uid, online)
+                        }
+
+                        SearchUserResultItemData(it)
+                    }
+                    searchResultSet.addAll(nextList.map { it.uid })
+                    searchResultList.addAll(nextItemList)
+                    searchRltLD.send(rlt)
+                    searchResultLD.send(searchResultList)
+                }
+            }
+        }
+    }
+
+    fun notifyFollowChanged(uid: String, follow: Boolean) {
+        viewModelScope.launch {
+            searchResultList.find { it.data.uid == uid }?.data?.follow = follow
+            searchResultLD.send(searchResultList)
+        }
+    }
+}

+ 9 - 81
module/profile/src/main/java/com/adealink/weparty/profile/search/viewmodel/SearchViewModel.kt

@@ -1,101 +1,29 @@
 package com.adealink.weparty.profile.search.viewmodel
 
-import com.adealink.frame.base.Rlt
+import androidx.lifecycle.MutableLiveData
 import com.adealink.frame.mvvm.livedata.ExtMutableLiveData
 import com.adealink.frame.mvvm.viewmodel.BaseViewModel
-import com.adealink.frame.network.data.PageReq
-import com.adealink.weparty.App
-import com.adealink.weparty.module.profile.ProfileModule
-import com.adealink.weparty.network.data.NoMoreDataError
-import com.adealink.weparty.profile.datasource.remote.SearchHttpService
-import com.adealink.weparty.profile.search.data.SearchReq
-import com.adealink.weparty.profile.search.data.SearchResultItemData
-import com.adealink.weparty.util.PageHandler
 import kotlinx.coroutines.launch
 
 class SearchViewModel : BaseViewModel() {
 
-    private val searchHttpService by lazy {
-        App.instance.networkService.getHttpService(SearchHttpService::class.java)
+    val isInputEmpty = ExtMutableLiveData<Boolean>()
+
+    fun setInput(input: String?) {
+        isInputEmpty.send(input.isNullOrEmpty())
     }
 
-    val searchResultSet = mutableSetOf<String>()
-    val searchResultLD = ExtMutableLiveData<List<SearchResultItemData>>()
-    val searchRltLD = ExtMutableLiveData<Rlt<Any>>()
-    private val searchResultList = mutableListOf<SearchResultItemData>()
-    private val pageHandler = PageHandler()
     private var keyword: String? = null
+    val keywordLD = MutableLiveData<String?>()
     fun search(keyword: String?) {
-        pageHandler.reset()
         this.keyword = keyword
-        searchResultSet.clear()
-        searchResultList.clear()
-        searchMore(keyword)
-    }
-
-    fun reSearch() {
-        search(keyword)
-    }
-
-    fun hasMoreData(): Boolean {
-        return !pageHandler.isEnd
+        keywordLD.send(keyword)
     }
 
-    fun searchMore(keyword: String? = this@SearchViewModel.keyword) {
-        if (pageHandler.isEnd) {
-            searchRltLD.send(Rlt.Failed(NoMoreDataError()))
-            return
-        }
-        viewModelScope.launch {
-            if (keyword.isNullOrEmpty()) {
-                searchRltLD.send(Rlt.Success(Any()))
-                searchResultLD.send(emptyList())
-                return@launch
-            }
-            val rlt = searchHttpService.search(
-                SearchReq(
-                    keyword = keyword,
-                    page = PageReq(size = pageHandler.pageSize, next = pageHandler.currentPage)
-                )
-            )
-            when (rlt) {
-                is Rlt.Failed -> {
-                    if (keyword != this@SearchViewModel.keyword) {
-                        return@launch
-                    }
-                    searchRltLD.send(rlt)
-                }
 
-                is Rlt.Success -> {
-                    if (keyword != this@SearchViewModel.keyword) {
-                        return@launch
-                    }
-                    pageHandler.nextPage(rlt.data.data?.next)
-
-                    val nextList = rlt.data.data?.list?.filter {
-                        !searchResultSet.contains(it.uid)
-                    } ?: emptyList()
-
-                    val nextItemList = nextList.map {
-                        it.online?.let { online ->
-                            ProfileModule.updateUserOnline(it.uid, online)
-                        }
-
-                        SearchResultItemData(it)
-                    }
-                    searchResultSet.addAll(nextList.map { it.uid })
-                    searchResultList.addAll(nextItemList)
-                    searchRltLD.send(rlt)
-                    searchResultLD.send(searchResultList)
-                }
-            }
-        }
-    }
+    val followChangedLD = ExtMutableLiveData<Pair<String, Boolean>>()
 
     fun notifyFollowChanged(uid: String, follow: Boolean) {
-        viewModelScope.launch {
-            searchResultList.find { it.data.uid == uid }?.data?.follow = follow
-            searchResultLD.send(searchResultList)
-        }
+        followChangedLD.send(Pair(uid, follow))
     }
 }

+ 8 - 0
module/profile/src/main/res/drawable/search_tab_selected_bg.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <gradient
+        android:endColor="#95E789"
+        android:startColor="#4ED2FE" />
+    <corners android:radius="10dp" />
+</shape>

+ 15 - 168
module/profile/src/main/res/layout/activity_user_search.xml

@@ -6,195 +6,42 @@
     android:layout_height="match_parent"
     android:background="@color/white">
 
-    <androidx.constraintlayout.widget.ConstraintLayout
+    <include
         android:id="@+id/cl_top"
+        layout="@layout/layout_search_top"
         android:layout_width="match_parent"
         android:layout_height="54dp"
-        android:paddingHorizontal="16dp"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent">
-
-        <androidx.appcompat.widget.AppCompatImageView
-            android:id="@+id/iv_back"
-            android:layout_width="24dp"
-            android:layout_height="24dp"
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintTop_toTopOf="parent"
-            app:srcCompat="@drawable/commonui_back_black_ic" />
-
-        <androidx.constraintlayout.widget.ConstraintLayout
-            android:layout_width="0dp"
-            android:layout_height="36dp"
-            android:layout_marginHorizontal="12dp"
-            android:background="@drawable/profile_search_input_bg"
-            android:paddingHorizontal="10dp"
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintEnd_toStartOf="@id/tv_search"
-            app:layout_constraintStart_toEndOf="@id/iv_back"
-            app:layout_constraintTop_toTopOf="parent">
-
-            <androidx.appcompat.widget.AppCompatImageView
-                android:id="@+id/iv_search_input"
-                android:layout_width="18dp"
-                android:layout_height="18dp"
-                app:layout_constraintBottom_toBottomOf="parent"
-                app:layout_constraintStart_toStartOf="parent"
-                app:layout_constraintTop_toTopOf="parent"
-                app:srcCompat="@drawable/profile_search_input_ic" />
-
-            <androidx.appcompat.widget.AppCompatEditText
-                android:id="@+id/et_search_input"
-                android:layout_width="0dp"
-                android:layout_height="0dp"
-                android:layout_marginHorizontal="8dp"
-                android:background="@null"
-                android:gravity="start|center_vertical"
-                android:hint="@string/profile_search_hint"
-                android:imeOptions="actionSearch"
-                android:includeFontPadding="false"
-                android:inputType="text"
-                android:singleLine="true"
-                android:textColor="@color/color_FF1D2129"
-                android:textColorHint="@color/color_FF86909C"
-                android:textSize="12sp"
-                app:layout_constraintBottom_toBottomOf="parent"
-                app:layout_constraintEnd_toStartOf="@id/iv_clear_input"
-                app:layout_constraintStart_toEndOf="@id/iv_search_input"
-                app:layout_constraintTop_toTopOf="parent" />
-
-            <androidx.appcompat.widget.AppCompatImageView
-                android:id="@+id/iv_clear_input"
-                android:layout_width="16dp"
-                android:layout_height="16dp"
-                android:visibility="gone"
-                app:layout_constraintBottom_toBottomOf="parent"
-                app:layout_constraintEnd_toEndOf="parent"
-                app:layout_constraintTop_toTopOf="parent"
-                app:srcCompat="@drawable/common_clear_input_ic"
-                tools:visibility="visible" />
+        app:layout_constraintTop_toTopOf="parent" />
 
-        </androidx.constraintlayout.widget.ConstraintLayout>
-
-        <androidx.appcompat.widget.AppCompatTextView
-            android:id="@+id/tv_search"
-            android:layout_width="wrap_content"
-            android:layout_height="match_parent"
-            android:gravity="center"
-            android:includeFontPadding="false"
-            android:singleLine="true"
-            android:text="@string/common_search"
-            android:textColor="@drawable/profile_search_text_sel"
-            android:textSize="14sp"
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintTop_toTopOf="parent" />
-
-    </androidx.constraintlayout.widget.ConstraintLayout>
-
-
-    <androidx.constraintlayout.widget.ConstraintLayout
+    <include
         android:id="@+id/cl_history"
+        layout="@layout/layout_search_history"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
+        android:layout_height="0dp"
         android:layout_marginTop="12dp"
         android:paddingHorizontal="16dp"
         android:visibility="gone"
+        app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toBottomOf="@id/cl_top"
-        tools:visibility="visible">
+        tools:visibility="visible" />
 
-        <androidx.appcompat.widget.AppCompatTextView
-            android:id="@+id/tv_history"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:fontFamily="@font/poppins_semibold"
-            android:gravity="start"
-            android:includeFontPadding="false"
-            android:singleLine="true"
-            android:text="@string/profile_search_history"
-            android:textColor="@color/color_FF1D2129"
-            android:textSize="14sp"
-            app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintHorizontal_bias="0"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintTop_toTopOf="parent" />
-
-        <androidx.recyclerview.widget.RecyclerView
-            android:id="@+id/rv_history"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="10dp"
-            app:layout_constraintTop_toBottomOf="@id/tv_history"
-            tools:itemCount="3"
-            tools:listitem="@layout/layout_profile_search_history_item" />
-
-    </androidx.constraintlayout.widget.ConstraintLayout>
-
-    <androidx.constraintlayout.widget.ConstraintLayout
-        android:id="@+id/cl_search_result"
-        android:layout_width="0dp"
+    <include
+        android:id="@+id/cl_result"
+        layout="@layout/layout_search_result"
+        android:layout_width="match_parent"
         android:layout_height="0dp"
-        android:layout_marginTop="20dp"
+        android:layout_marginTop="12dp"
         android:paddingHorizontal="16dp"
         android:visibility="gone"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toBottomOf="@id/cl_history"
-        app:layout_goneMarginTop="12dp"
-        tools:visibility="visible">
-
-        <androidx.appcompat.widget.AppCompatTextView
-            android:id="@+id/tv_contact"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:fontFamily="@font/poppins_semibold"
-            android:gravity="start"
-            android:includeFontPadding="false"
-            android:singleLine="true"
-            android:text="@string/profile_search_contact"
-            android:textColor="@color/color_FF1D2129"
-            android:textSize="14sp"
-            app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintHorizontal_bias="0"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintTop_toTopOf="parent" />
-
-        <com.scwang.smart.refresh.layout.SmartRefreshLayout
-            android:id="@+id/v_refresh"
-            android:layout_width="0dp"
-            android:layout_height="0dp"
-            android:layout_marginTop="10dp"
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintTop_toBottomOf="@id/tv_contact">
-
-            <androidx.recyclerview.widget.RecyclerView
-                android:id="@+id/rv_result"
-                style="@style/CommonVerticalFade"
-                android:layout_width="match_parent"
-                android:layout_height="match_parent"
-                tools:listitem="@layout/layout_profile_search_item" />
-
-        </com.scwang.smart.refresh.layout.SmartRefreshLayout>
-
-        <com.adealink.weparty.commonui.widget.CommonEmptyErrorView
-            android:id="@+id/v_error_view"
-            android:layout_width="0dp"
-            android:layout_height="0dp"
-            android:layout_marginTop="10dp"
-            android:visibility="gone"
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintTop_toBottomOf="@id/tv_contact"
-            tools:visibility="visible" />
-
-    </androidx.constraintlayout.widget.ConstraintLayout>
+        app:layout_constraintTop_toBottomOf="@id/cl_top"
+        tools:visibility="visible" />
 
 
 </androidx.constraintlayout.widget.ConstraintLayout>

+ 57 - 0
module/profile/src/main/res/layout/fragment_search_all.xml

@@ -0,0 +1,57 @@
+<?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="match_parent"
+    android:background="@color/white"
+    android:paddingHorizontal="16dp">
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/tv_contact"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:fontFamily="@font/poppins_semibold"
+        android:gravity="start"
+        android:includeFontPadding="false"
+        android:singleLine="true"
+        android:text="@string/profile_search_contact"
+        android:textColor="@color/color_FF1D2129"
+        android:textSize="14sp"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHorizontal_bias="0"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <com.scwang.smart.refresh.layout.SmartRefreshLayout
+        android:id="@+id/v_refresh"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_marginTop="10dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/tv_contact">
+
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/rv_result"
+            style="@style/CommonVerticalFade"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            tools:listitem="@layout/layout_profile_search_user_item" />
+
+    </com.scwang.smart.refresh.layout.SmartRefreshLayout>
+
+    <com.adealink.weparty.commonui.widget.CommonEmptyErrorView
+        android:id="@+id/v_error_view"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_marginTop="10dp"
+        android:visibility="gone"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/tv_contact"
+        tools:visibility="visible" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 57 - 0
module/profile/src/main/res/layout/fragment_search_room.xml

@@ -0,0 +1,57 @@
+<?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="match_parent"
+    android:background="@color/white"
+    android:paddingHorizontal="16dp">
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/tv_contact"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:fontFamily="@font/poppins_semibold"
+        android:gravity="start"
+        android:includeFontPadding="false"
+        android:singleLine="true"
+        android:text="@string/profile_search_contact"
+        android:textColor="@color/color_FF1D2129"
+        android:textSize="14sp"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHorizontal_bias="0"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <com.scwang.smart.refresh.layout.SmartRefreshLayout
+        android:id="@+id/v_refresh"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_marginTop="10dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/tv_contact">
+
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/rv_result"
+            style="@style/CommonVerticalFade"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            tools:listitem="@layout/layout_profile_search_user_item" />
+
+    </com.scwang.smart.refresh.layout.SmartRefreshLayout>
+
+    <com.adealink.weparty.commonui.widget.CommonEmptyErrorView
+        android:id="@+id/v_error_view"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_marginTop="10dp"
+        android:visibility="gone"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/tv_contact"
+        tools:visibility="visible" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 57 - 0
module/profile/src/main/res/layout/fragment_search_user.xml

@@ -0,0 +1,57 @@
+<?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="match_parent"
+    android:background="@color/white"
+    android:paddingHorizontal="16dp">
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/tv_contact"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:fontFamily="@font/poppins_semibold"
+        android:gravity="start"
+        android:includeFontPadding="false"
+        android:singleLine="true"
+        android:text="@string/profile_search_contact"
+        android:textColor="@color/color_FF1D2129"
+        android:textSize="14sp"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHorizontal_bias="0"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <com.scwang.smart.refresh.layout.SmartRefreshLayout
+        android:id="@+id/v_refresh"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_marginTop="10dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/tv_contact">
+
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/rv_result"
+            style="@style/CommonVerticalFade"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            tools:listitem="@layout/layout_profile_search_user_item" />
+
+    </com.scwang.smart.refresh.layout.SmartRefreshLayout>
+
+    <com.adealink.weparty.commonui.widget.CommonEmptyErrorView
+        android:id="@+id/v_error_view"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_marginTop="10dp"
+        android:visibility="gone"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/tv_contact"
+        tools:visibility="visible" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 0 - 0
module/profile/src/main/res/layout/layout_profile_search_item.xml → module/profile/src/main/res/layout/layout_profile_search_room_item.xml


+ 106 - 0
module/profile/src/main/res/layout/layout_profile_search_user_item.xml

@@ -0,0 +1,106 @@
+<?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">
+
+    <com.adealink.weparty.commonui.imageview.AvatarView
+        android:id="@+id/iv_avatar"
+        android:layout_width="40dp"
+        android:layout_height="40dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/tv_name"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="10dp"
+        android:ellipsize="end"
+        android:fontFamily="@font/poppins_semibold"
+        android:gravity="start|center_vertical"
+        android:includeFontPadding="false"
+        android:singleLine="true"
+        android:textColor="@color/color_FF1D2129"
+        android:textSize="14sp"
+        app:layout_constrainedWidth="true"
+        app:layout_constraintBottom_toTopOf="@id/tv_id"
+        app:layout_constraintEnd_toStartOf="@id/v_gender"
+        app:layout_constraintHorizontal_bias="0"
+        app:layout_constraintHorizontal_chainStyle="packed"
+        app:layout_constraintStart_toEndOf="@id/iv_avatar"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_chainStyle="packed"
+        tools:text="UserNameUserNameUserNameUserName" />
+
+    <com.adealink.weparty.module.profile.widget.GenderView
+        android:id="@+id/v_gender"
+        style="@style/CommonGenderView"
+        android:layout_marginStart="4dp"
+        android:layout_marginEnd="24dp"
+        app:layout_constraintBottom_toBottomOf="@id/tv_name"
+        app:layout_constraintEnd_toStartOf="@id/barrier_right"
+        app:layout_constraintStart_toEndOf="@id/tv_name"
+        app:layout_constraintTop_toTopOf="@id/tv_name" />
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/tv_id"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="10dp"
+        android:ellipsize="end"
+        android:gravity="start|center_vertical"
+        android:includeFontPadding="false"
+        android:singleLine="true"
+        android:textColor="@color/color_FF86909C"
+        android:textSize="11sp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHorizontal_bias="0"
+        app:layout_constraintStart_toEndOf="@id/iv_avatar"
+        app:layout_constraintTop_toBottomOf="@id/tv_name"
+        tools:text="ID 12345678" />
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/tv_fans"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="7dp"
+        android:layout_marginEnd="10dp"
+        android:ellipsize="end"
+        android:gravity="start|center_vertical"
+        android:includeFontPadding="false"
+        android:singleLine="true"
+        android:textColor="@color/color_FF86909C"
+        android:textSize="11sp"
+        app:layout_constraintBottom_toBottomOf="@id/tv_id"
+        app:layout_constraintEnd_toEndOf="@id/barrier_right"
+        app:layout_constraintHorizontal_bias="0"
+        app:layout_constraintStart_toEndOf="@id/tv_id"
+        app:layout_constraintTop_toTopOf="@id/tv_id"
+        tools:text="粉丝数 300" />
+
+    <androidx.constraintlayout.widget.Barrier
+        android:id="@+id/barrier_right"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:barrierDirection="start"
+        app:constraint_referenced_ids="btn_follow" />
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/btn_follow"
+        android:layout_width="wrap_content"
+        android:layout_height="22dp"
+        android:gravity="center"
+        android:minWidth="56dp"
+        android:paddingHorizontal="4.5dp"
+        android:text="@string/common_follow"
+        android:textColor="@color/white"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        tools:background="@drawable/profile_follow_btn_bg" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 35 - 0
module/profile/src/main/res/layout/layout_search_history.xml

@@ -0,0 +1,35 @@
+<?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"
+    android:background="@color/white"
+    android:paddingHorizontal="16dp">
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/tv_history"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:fontFamily="@font/poppins_semibold"
+        android:gravity="start"
+        android:includeFontPadding="false"
+        android:singleLine="true"
+        android:text="@string/profile_search_history"
+        android:textColor="@color/color_FF1D2129"
+        android:textSize="14sp"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHorizontal_bias="0"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/rv_history"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="10dp"
+        app:layout_constraintTop_toBottomOf="@id/tv_history"
+        tools:itemCount="3"
+        tools:listitem="@layout/layout_profile_search_history_item" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 32 - 0
module/profile/src/main/res/layout/layout_search_result.xml

@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <com.google.android.material.tabs.TabLayout
+        android:id="@+id/tl_tab"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:layout_constrainedWidth="true"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHorizontal_bias="0"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:tabBackground="@null"
+        app:tabGravity="start"
+        app:tabIndicatorHeight="0dp"
+        app:tabMinWidth="0dp"
+        app:tabPadding="0dp"
+        app:tabRippleColor="@null" />
+
+    <androidx.viewpager2.widget.ViewPager2
+        android:id="@+id/vp_content"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/tl_tab" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 34 - 0
module/profile/src/main/res/layout/layout_search_tab.xml

@@ -0,0 +1,34 @@
+<?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="wrap_content"
+    android:layout_height="wrap_content">
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/tv_tab"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:fontFamily="@font/poppins_semibold"
+        android:includeFontPadding="false"
+        android:textColor="@color/color_FF4E5969"
+        android:textSize="16sp"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        tools:text="Online Play" />
+
+    <View
+        android:id="@+id/v_selected"
+        android:layout_width="23dp"
+        android:layout_height="2dp"
+        android:layout_marginTop="4dp"
+        android:background="@drawable/search_tab_selected_bg"
+        android:visibility="invisible"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/tv_tab"
+        tools:visibility="visible" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 86 - 0
module/profile/src/main/res/layout/layout_search_top.xml

@@ -0,0 +1,86 @@
+<?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="54dp"
+    android:background="@color/white"
+    android:paddingHorizontal="16dp">
+
+    <androidx.appcompat.widget.AppCompatImageView
+        android:id="@+id/iv_back"
+        android:layout_width="24dp"
+        android:layout_height="24dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:srcCompat="@drawable/commonui_back_black_ic" />
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="0dp"
+        android:layout_height="36dp"
+        android:layout_marginHorizontal="12dp"
+        android:background="@drawable/profile_search_input_bg"
+        android:paddingHorizontal="10dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toStartOf="@id/tv_search"
+        app:layout_constraintStart_toEndOf="@id/iv_back"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/iv_search_input"
+            android:layout_width="18dp"
+            android:layout_height="18dp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:srcCompat="@drawable/profile_search_input_ic" />
+
+        <androidx.appcompat.widget.AppCompatEditText
+            android:id="@+id/et_search_input"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:layout_marginHorizontal="8dp"
+            android:background="@null"
+            android:gravity="start|center_vertical"
+            android:hint="@string/profile_search_hint"
+            android:imeOptions="actionSearch"
+            android:includeFontPadding="false"
+            android:inputType="text"
+            android:singleLine="true"
+            android:textColor="@color/color_FF1D2129"
+            android:textColorHint="@color/color_FF86909C"
+            android:textSize="12sp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toStartOf="@id/iv_clear_input"
+            app:layout_constraintStart_toEndOf="@id/iv_search_input"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/iv_clear_input"
+            android:layout_width="16dp"
+            android:layout_height="16dp"
+            android:visibility="gone"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:srcCompat="@drawable/common_clear_input_ic"
+            tools:visibility="visible" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/tv_search"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:gravity="center"
+        android:includeFontPadding="false"
+        android:singleLine="true"
+        android:text="@string/common_search"
+        android:textColor="@drawable/profile_search_text_sel"
+        android:textSize="14sp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 3 - 0
module/profile/src/main/res/values/strings.xml

@@ -89,4 +89,7 @@
     <string name="profile_dynamic_total_comment">%s comments</string>
     <string name="profile_dynamic_edit_talk_hint">Say something…</string>
     <string name="profile_dynamic_video_duration_too_long">Select video under 60s</string>
+    <string name="search_all_tab">Utuh</string>
+    <string name="search_user_tab">User</string>
+    <string name="search_room_tab">Room</string>
 </resources>