diff --git a/app/build.gradle b/app/build.gradle index c166ea605e..3dd73aa451 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -539,6 +539,8 @@ dependencies { if(!gradle.ext.excludeOptionalModules || gradle.ext.enableWechatPay){ implementation(project(":feature:wechat_pay")) } + + implementation "me.xdrop:fuzzywuzzy:${fuzzyVersion}" } File propFile = file('sign.properties') diff --git a/app/src/main/java/com/gh/common/util/DirectUtils.kt b/app/src/main/java/com/gh/common/util/DirectUtils.kt index ed8927b5a7..7ac7efa3a3 100644 --- a/app/src/main/java/com/gh/common/util/DirectUtils.kt +++ b/app/src/main/java/com/gh/common/util/DirectUtils.kt @@ -1534,8 +1534,8 @@ object DirectUtils { if (categoryId.isEmpty()) return val bundle = Bundle() bundle.putString(KEY_TO, CategoryV2Activity::class.java.name) - bundle.putString(KEY_CATEGORY_ID, categoryId) - bundle.putString(KEY_CATEGORY_TITLE, categoryTitle) + bundle.putString(KEY_PAGE_ID, categoryId) + bundle.putString(KEY_PAGE_NAME, categoryTitle) bundle.putString(KEY_ENTRANCE, BaseActivity.mergeEntranceAndPath(entrance, path)) if (exposureEvent != null) bundle.putParcelableArrayList( KEY_EXPOSURE_SOURCE_LIST, diff --git a/app/src/main/java/com/gh/common/util/ViewPagerFragmentHelper.kt b/app/src/main/java/com/gh/common/util/ViewPagerFragmentHelper.kt index 5b6fe53fb9..c8ca215ce3 100644 --- a/app/src/main/java/com/gh/common/util/ViewPagerFragmentHelper.kt +++ b/app/src/main/java/com/gh/common/util/ViewPagerFragmentHelper.kt @@ -199,8 +199,8 @@ object ViewPagerFragmentHelper { // 分类2.0 TYPE_CATEGORY_V2 -> { className = CategoryV2Fragment::class.java.name - bundle.putString(EntranceConsts.KEY_CATEGORY_ID, entity.link) - bundle.putString(EntranceConsts.KEY_CATEGORY_TITLE, entity.text) + bundle.putString(EntranceConsts.KEY_PAGE_ID, entity.link) + bundle.putString(EntranceConsts.KEY_PAGE_NAME, entity.text) bundle.putBoolean(EntranceConsts.KEY_SHOW_DOWNLOAD_MENU, !isTabWrapper) } // 通用内容合集详情页 diff --git a/app/src/main/java/com/gh/common/view/CategoryFilterView.kt b/app/src/main/java/com/gh/common/view/CategoryFilterView.kt index c16c4c31ce..dfff61fdb6 100644 --- a/app/src/main/java/com/gh/common/view/CategoryFilterView.kt +++ b/app/src/main/java/com/gh/common/view/CategoryFilterView.kt @@ -1,38 +1,48 @@ package com.gh.common.view +import android.annotation.SuppressLint import android.content.Context -import android.graphics.Color +import android.content.res.ColorStateList +import android.graphics.Typeface import android.util.AttributeSet import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.LinearLayout +import android.widget.CompoundButton import android.widget.PopupWindow -import android.widget.TextView +import android.widget.RadioButton import androidx.annotation.ColorInt -import androidx.core.content.ContextCompat +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.forEach +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView import com.gh.gamecenter.R -import com.gh.gamecenter.common.utils.dip2px -import com.gh.gamecenter.common.utils.setDrawableEnd -import com.gh.gamecenter.common.utils.toColor +import com.gh.gamecenter.common.utils.* +import com.gh.gamecenter.databinding.LayoutCategoryFilterBinding +import com.gh.gamecenter.databinding.LayoutCategoryFilterSizeBinding +import com.gh.gamecenter.databinding.PopCategoryFilterSizeBinding +import com.gh.gamecenter.databinding.PopCategoryFilterTypeBinding import com.gh.gamecenter.entity.SubjectSettingEntity -import com.google.android.flexbox.FlexboxLayout class CategoryFilterView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 -) : LinearLayout(context, attrs, defStyleAttr) { +) : ConstraintLayout(context, attrs, defStyleAttr) { - private var mTypeTv: TextView - private var mCatalogTv: TextView - private var mSizeTv: TextView - private var mTypeContainer: View - private var mCatalogContainer: View - private var mSizeContainer: View + private val binding: LayoutCategoryFilterBinding - private var mTypeFilterArray = arrayOf(SortType.RECOMMENDED, SortType.NEWEST, SortType.RATING) - private var sizeFilterArray: ArrayList? = null + private val mTypeFilterArray = arrayOf(SortType.RECOMMENDED, SortType.NEWEST, SortType.RATING) + private val sizeFilterArray by lazy { + listOf( + SubjectSettingEntity.Size(min = -1, max = -1, text = "全部大小"), + SubjectSettingEntity.Size(min = -1, max = 100, text = "100M以下"), + SubjectSettingEntity.Size(min = 100, max = 300, text = "100-300M"), + SubjectSettingEntity.Size(min = 300, max = 500, text = "300-500M"), + SubjectSettingEntity.Size(min = 500, max = 1000, text = "500M-1G"), + SubjectSettingEntity.Size(min = 1000, max = -1, text = "1G以上") + ) + } private var mOnCategoryFilterSetupListener: OnCategoryFilterSetupListener? = null private var mOnFilterClickListener: OnFilterClickListener? = null @@ -41,29 +51,28 @@ class CategoryFilterView @JvmOverloads constructor( private var mSizePopupWindow: PopupWindow? = null init { - View.inflate(context, R.layout.layout_category_filter, this) - mTypeTv = findViewById(R.id.type_tv) - mCatalogTv = findViewById(R.id.catalog_tv) - mSizeTv = findViewById(R.id.size_tv) - mTypeContainer = findViewById(R.id.container_type) - mCatalogContainer = findViewById(R.id.container_category) - mSizeContainer = findViewById(R.id.container_size) - mTypeTv.text = mTypeFilterArray[0].value + val inflater = LayoutInflater.from(context) + binding = LayoutCategoryFilterBinding.inflate(inflater, this, true) - mTypeContainer.setOnClickListener { + binding.ivTypeArrow.setImageResource(R.drawable.ic_arrow_down) + binding.ivTypeArrow.imageTintList = + ColorStateList.valueOf(com.gh.gamecenter.common.R.color.text_neutral.toColor(context)) + + binding.ivSizeArrow.setImageResource(R.drawable.ic_arrow_down) + binding.ivSizeArrow.imageTintList = + ColorStateList.valueOf(com.gh.gamecenter.common.R.color.text_neutral.toColor(context)) + + binding.tvType.text = mTypeFilterArray[0].value + + binding.llTypeContainer.setOnClickListener { mOnFilterClickListener?.onTypeClick() - showSelectTypePopupWindow(this, mTypeTv, mTypeTv.text.toString()) + showSelectTypePopupWindow() } - mCatalogContainer.setOnClickListener { - mOnFilterClickListener?.onCategoryClick() - mOnCategoryFilterSetupListener?.onSetupSortCategory() - } - - mSizeContainer.setOnClickListener { + binding.llSizeContainer.setOnClickListener { mOnFilterClickListener?.onSizeClick() - showSelectSizePopupWindow(this, mSizeTv, mSizeTv.text.toString()) + showSelectSizePopupWindow() } } @@ -76,154 +85,133 @@ class CategoryFilterView @JvmOverloads constructor( } fun resetSortSize() { - mSizeTv.text = "全部大小" + binding.tvType.text = R.string.size.toResString() } - private fun toggleHighlightedTextView(targetTextView: TextView, highlightIt: Boolean) { - if (highlightIt) { - targetTextView.background = ContextCompat.getDrawable(targetTextView.context, R.drawable.bg_tag_text) - targetTextView.setTextColor(Color.WHITE) - } else { - targetTextView.background = null - targetTextView.setTextColor(ContextCompat.getColor(targetTextView.context, com.gh.gamecenter.common.R.color.text_757575)) - } - } + private fun showSelectTypePopupWindow() { + binding.tvType.setTextColor(com.gh.gamecenter.common.R.color.text_primary.toColor(context)) - private fun showSelectTypePopupWindow(containerView: View, typeTv: TextView, typeText: String) { - typeTv.setTextColor(com.gh.gamecenter.common.R.color.text_theme.toColor(context)) - typeTv.setDrawableEnd(R.drawable.ic_filter_arrow_up) + binding.ivTypeArrow.setImageResource(R.drawable.ic_arrow_up) + binding.ivTypeArrow.imageTintList = + ColorStateList.valueOf(com.gh.gamecenter.common.R.color.text_primary.toColor(context)) - val inflater = LayoutInflater.from(typeTv.context) - val layout = inflater.inflate(R.layout.layout_filter_size, null) - val windowWidth = typeTv.context.resources.displayMetrics.widthPixels - 80F.dip2px() + val inflater = LayoutInflater.from(context) + val typeBinding = PopCategoryFilterTypeBinding.inflate(inflater, null, false) + val windowWidth = resources.displayMetrics.widthPixels - 80F.dip2px() + val windowHeight = mOnCategoryFilterSetupListener?.getPopHeight() ?: 0 val popupWindow = PopupWindow( - layout, + typeBinding.root, windowWidth, - LayoutParams.WRAP_CONTENT + if (windowHeight == 0) ViewGroup.LayoutParams.WRAP_CONTENT else (windowHeight - height) ).apply { mTypePopupWindow = this } - val flexboxLayout = layout.findViewById(R.id.flexbox) - val backgroundView = layout.findViewById(R.id.background) - backgroundView.setOnClickListener { + typeBinding.root.setOnClickListener { popupWindow.dismiss() } - for (type in mTypeFilterArray) { - val item = inflater.inflate(R.layout.item_filter_size, flexboxLayout, false) + val checkedId = when (binding.tvType.text.toString()) { + "最新" -> R.id.rb_newest + "评分" -> R.id.rb_score + else -> R.id.rb_popular + } - // 单列 3 个,强行设置宽度为屏幕的 1/3 - val width = windowWidth / 3 - val height = item.layoutParams.height + val checkButton = typeBinding.root.findViewById(checkedId) + checkButton.setTypeface(checkButton.typeface, Typeface.BOLD) + checkButton.setTextColor(com.gh.gamecenter.common.R.color.text_primary.toColor(context)) + checkButton.setDrawableEnd(R.drawable.ic_basic_checkmark_circle_fill) - item.layoutParams = ViewGroup.LayoutParams(width, height) - flexboxLayout.addView(item) - - val tv = item.findViewById(R.id.size_tv) - tv.text = type.value - - toggleHighlightedTextView(tv, typeText == type.value) - - tv.tag = type.value - - item.setOnClickListener { - toggleHighlightedTextView(tv, true) - popupWindow.dismiss() - typeTv.text = type.value + val onCheckedChangeListener = CompoundButton.OnCheckedChangeListener { button, isChecked -> + if (isChecked) { + button.setTypeface(button.typeface, Typeface.BOLD) + button.setTextColor(com.gh.gamecenter.common.R.color.text_primary.toColor(context)) + button.setDrawableEnd(R.drawable.ic_basic_checkmark_circle_fill) + val type = when (button.id) { + R.id.rb_newest -> SortType.NEWEST + R.id.rb_score -> SortType.RATING + else -> SortType.RECOMMENDED + } + binding.tvType.text = type.value mOnCategoryFilterSetupListener?.onSetupSortType(type) + popupWindow.dismiss() + } else { + button.setTypeface(button.typeface, Typeface.NORMAL) + button.setTextColor(com.gh.gamecenter.common.R.color.text_secondary.toColor(context)) + button.setDrawableEnd(null) + } + } + typeBinding.rgFilter.forEach { + if (it is RadioButton) { + it.setOnCheckedChangeListener(onCheckedChangeListener) } } popupWindow.setOnDismissListener { - typeTv.setTextColor(com.gh.gamecenter.common.R.color.text_757575.toColor(context)) - typeTv.setDrawableEnd(R.drawable.ic_filter_arrow_down) + binding.tvType.setTextColor(com.gh.gamecenter.common.R.color.text_tertiary.toColor(context)) + binding.ivTypeArrow.setImageResource(R.drawable.ic_arrow_down) + binding.ivTypeArrow.imageTintList = + ColorStateList.valueOf(com.gh.gamecenter.common.R.color.text_neutral.toColor(context)) mTypePopupWindow = null } popupWindow.isTouchable = true popupWindow.isFocusable = true popupWindow.animationStyle = 0 - popupWindow.showAsDropDown(containerView, 0, 0) + popupWindow.showAsDropDown(this, 0, 0) } - private fun showSelectSizePopupWindow(containerView: View, sizeTv: TextView, sizeText: String) { - sizeTv.setTextColor(com.gh.gamecenter.common.R.color.text_theme.toColor(context)) - sizeTv.setDrawableEnd(R.drawable.ic_filter_arrow_up) + private fun showSelectSizePopupWindow() { + binding.tvSize.setTextColor(com.gh.gamecenter.common.R.color.text_primary.toColor(context)) + binding.ivSizeArrow.setImageResource(R.drawable.ic_arrow_up) + binding.ivSizeArrow.imageTintList = + ColorStateList.valueOf(com.gh.gamecenter.common.R.color.text_primary.toColor(context)) - val inflater = LayoutInflater.from(sizeTv.context) - val layout = inflater.inflate(R.layout.layout_filter_size, null) - val windowWidth = sizeTv.context.resources.displayMetrics.widthPixels - 80F.dip2px() + val inflater = LayoutInflater.from(context) + val sizeBinding = PopCategoryFilterSizeBinding.inflate(inflater, null, false) + val windowWidth = context.resources.displayMetrics.widthPixels - 80F.dip2px() + val windowHeight = mOnCategoryFilterSetupListener?.getPopHeight() ?: 0 val popupWindow = PopupWindow( - layout, + sizeBinding.root, windowWidth, - LayoutParams.WRAP_CONTENT + if (windowHeight == 0) ViewGroup.LayoutParams.WRAP_CONTENT else (windowHeight - height) ).apply { mSizePopupWindow = this } - val flexboxLayout = layout.findViewById(R.id.flexbox) - val backgroundView = layout.findViewById(R.id.background) - - sizeFilterArray = if (sizeFilterArray == null) { - getDefaultSizeFilterArray() - } else { - sizeFilterArray?.apply { - if (firstOrNull()?.text != "全部大小") { - add(0, SubjectSettingEntity.Size(min = -1, max = -1, text = "全部大小")) - } - } - } - - backgroundView.setOnClickListener { + sizeBinding.root.setOnClickListener { popupWindow.dismiss() } - - for (size in sizeFilterArray!!) { - val item = inflater.inflate(R.layout.item_filter_size, flexboxLayout, false) - - // 单列 3 个,强行设置宽度为屏幕的 1/3 - val width = windowWidth / 3 - val height = item.layoutParams.height - - item.layoutParams = ViewGroup.LayoutParams(width, height) - flexboxLayout.addView(item) - - val tv = item.findViewById(R.id.size_tv) - tv.text = size.text - - toggleHighlightedTextView(tv, sizeText == size.text) - - tv.tag = size.text - - item.setOnClickListener { - toggleHighlightedTextView(tv, true) - popupWindow.dismiss() - sizeTv.text = size.text - - mOnCategoryFilterSetupListener?.onSetupSortSize(size) - } + sizeBinding.rvSize.setOnClickListener { + // do nothing } + val sizeAdapter = CategoryFilterSizeAdapter { + if (it.min == INVALID_SIZE && it.max == INVALID_SIZE) { + binding.tvSize.text = R.string.size.toResString() + } else { + binding.tvSize.text = it.text + } + popupWindow.dismiss() + mOnCategoryFilterSetupListener?.onSetupSortSize(it) + } + sizeBinding.rvSize.layoutManager = GridLayoutManager(context, 3, RecyclerView.VERTICAL, false) + sizeBinding.rvSize.adapter = sizeAdapter + val selectedPosition = + sizeFilterArray.indexOfFirst { it.text?.contains(binding.tvSize.text.toString()) == true } + + sizeAdapter.setData(sizeFilterArray, selectedPosition) + popupWindow.setOnDismissListener { - sizeTv.setTextColor(com.gh.gamecenter.common.R.color.text_757575.toColor(context)) - sizeTv.setDrawableEnd(R.drawable.ic_filter_arrow_down) + binding.tvSize.setTextColor(com.gh.gamecenter.common.R.color.text_tertiary.toColor(context)) + binding.ivSizeArrow.setImageResource(R.drawable.ic_arrow_down) + binding.ivSizeArrow.imageTintList = + ColorStateList.valueOf(com.gh.gamecenter.common.R.color.text_neutral.toColor(context)) mSizePopupWindow = null } popupWindow.isTouchable = true popupWindow.isFocusable = true popupWindow.animationStyle = 0 - popupWindow.showAsDropDown(containerView, 0, 0) - } - - private fun getDefaultSizeFilterArray(): ArrayList { - return arrayListOf().apply { - add(SubjectSettingEntity.Size(min = -1, max = -1, text = "全部大小")) - add(SubjectSettingEntity.Size(min = -1, max = 100, text = "100M以下")) - add(SubjectSettingEntity.Size(min = 100, max = 300, text = "100-300M")) - add(SubjectSettingEntity.Size(min = 300, max = 500, text = "300-500M")) - add(SubjectSettingEntity.Size(min = 500, max = 1000, text = "500M-1G")) - add(SubjectSettingEntity.Size(min = 1000, max = -1, text = "1G以上")) - } + popupWindow.showAsDropDown(this, 0, 0) } fun setRootBackgroundColor(@ColorInt color: Int) { @@ -231,21 +219,23 @@ class CategoryFilterView @JvmOverloads constructor( } fun setItemTextColor(@ColorInt color: Int) { - mTypeTv.setTextColor(color) - mCatalogTv.setTextColor(color) - mSizeTv.setTextColor(color) + val colorInt = com.gh.gamecenter.common.R.color.text_neutral.toColor(context) + binding.tvType.setTextColor(color) + binding.tvSize.setTextColor(color) + binding.ivTypeArrow.imageTintList = ColorStateList.valueOf(colorInt) + binding.ivSizeArrow.imageTintList = ColorStateList.valueOf(colorInt) } fun updatePopupWindow() { when { mTypePopupWindow != null && mTypePopupWindow!!.isShowing -> { mTypePopupWindow?.dismiss() - showSelectTypePopupWindow(this, mTypeTv, mTypeTv.text.toString()) + showSelectTypePopupWindow() } mSizePopupWindow != null && mSizePopupWindow!!.isShowing -> { mSizePopupWindow?.dismiss() - showSelectSizePopupWindow(this, mSizeTv, mSizeTv.text.toString()) + showSelectSizePopupWindow() } } } @@ -253,18 +243,69 @@ class CategoryFilterView @JvmOverloads constructor( interface OnCategoryFilterSetupListener { fun onSetupSortSize(sortSize: SubjectSettingEntity.Size) fun onSetupSortType(sortType: SortType) - fun onSetupSortCategory() + fun getPopHeight(): Int } interface OnFilterClickListener { - fun onCategoryClick() fun onTypeClick() fun onSizeClick() } enum class SortType(val value: String) { - RECOMMENDED("热门推荐"), - NEWEST("最新上线"), - RATING("最高评分") + RECOMMENDED("热门"), + NEWEST("最新"), + RATING("评分") + } + + class CategoryFilterSizeAdapter(private val itemClick: (SubjectSettingEntity.Size) -> Unit) : + RecyclerView.Adapter() { + + private val dataList = arrayListOf() + private var selectedPosition = 0 + + @SuppressLint("NotifyDataSetChanged") + fun setData(data: List, position: Int) { + dataList.clear() + dataList.addAll(data) + selectedPosition = position + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SizeViewHolder { + return SizeViewHolder(parent.toBinding()) + } + + override fun getItemCount(): Int { + return dataList.size + } + + override fun onBindViewHolder(holder: SizeViewHolder, position: Int) { + val item = dataList[position] + val context = holder.binding.root.context + if (selectedPosition == position) { + holder.binding.root.setTextColor(com.gh.gamecenter.common.R.color.text_theme.toColor(context)) + holder.binding.root.setBackgroundResource(R.drawable.shape_category_filter_size_item_selected) + } else { + holder.binding.root.setTextColor(com.gh.gamecenter.common.R.color.text_secondary.toColor(context)) + holder.binding.root.setBackgroundResource(R.drawable.shape_category_filter_size_item_normal) + } + holder.binding.root.text = item.text + holder.binding.root.setOnClickListener { + if (selectedPosition == holder.bindingAdapterPosition) { + return@setOnClickListener + } + val lastPosition = selectedPosition + selectedPosition = holder.bindingAdapterPosition + notifyItemChanged(lastPosition) + notifyItemChanged(selectedPosition) + itemClick(item) + } + } + + class SizeViewHolder(val binding: LayoutCategoryFilterSizeBinding) : RecyclerView.ViewHolder(binding.root) + } + + companion object { + private const val INVALID_SIZE = -1 } } \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/category2/CategoryDirectoryAdapter.kt b/app/src/main/java/com/gh/gamecenter/category2/CategoryDirectoryAdapter.kt index 6a48be497c..1a9a85b141 100644 --- a/app/src/main/java/com/gh/gamecenter/category2/CategoryDirectoryAdapter.kt +++ b/app/src/main/java/com/gh/gamecenter/category2/CategoryDirectoryAdapter.kt @@ -1,82 +1,89 @@ package com.gh.gamecenter.category2 -import android.content.Context +import android.annotation.SuppressLint import android.view.ViewGroup import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView -import com.gh.gamecenter.common.base.BaseRecyclerViewHolder -import com.gh.gamecenter.common.utils.dip2px +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import com.gh.gamecenter.common.utils.toBinding import com.gh.gamecenter.common.view.GridSpacingItemColorDecoration -import com.gh.gamecenter.common.utils.toColor import com.gh.gamecenter.databinding.CategoryDirectoryItemBinding import com.gh.gamecenter.entity.CategoryEntity -import com.lightgame.adapter.BaseRecyclerAdapter class CategoryDirectoryAdapter( - context: Context, - private val mViewModel: CategoryV2ViewModel, - private var mList: List -) : BaseRecyclerAdapter(context) { + private val listener: SearchCategoryPop.OnSearchCategoryListener +) : RecyclerView.Adapter() { - val width = mContext.resources.displayMetrics.widthPixels * 260 / 360 + private val data = arrayListOf() - fun setListData(list: List) { - mList = list + @SuppressLint("NotifyDataSetChanged") + fun setListData(newData: List) { + data.clear() + data.addAll(newData) notifyDataSetChanged() } - override fun getItemCount() = mList.size + override fun getItemCount() = data.size override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = - CategoryDirectoryItemViewHolder(CategoryDirectoryItemBinding.inflate(mLayoutInflater)) + CategoryDirectoryItemViewHolder(listener, parent.toBinding()) override fun onBindViewHolder(holder: CategoryDirectoryItemViewHolder, position: Int) { - holder.binding.run { - root.layoutParams = root.layoutParams?.apply { - width = mContext.resources.displayMetrics.widthPixels * 260 / 360 - } ?: RecyclerView.LayoutParams(width, RecyclerView.LayoutParams.WRAP_CONTENT) + holder.onBind(position, data[position]) + } - val padTop = if (position == 0) 16F.dip2px() else 24F.dip2px() - root.setPadding(16F.dip2px(), padTop, 16F.dip2px(), 0) + override fun onBindViewHolder(holder: CategoryDirectoryItemViewHolder, position: Int, payloads: MutableList) { + if (payloads.isEmpty()) { + super.onBindViewHolder(holder, position, payloads) + } else { + holder.notifyItemSelectedChanged() + } - val entity = mList[position] - title.text = entity.name - title.setTextColor(com.gh.gamecenter.common.R.color.text_primary.toColor(mContext)) + } - subCategoryRv.run { - if (adapter is SubCategoryAdapter) { - layoutManager = GridLayoutManager(mContext, 3) - adapter = entity.data?.let { - SubCategoryAdapter( - mContext, - mViewModel, - it, - position - ) - } - } else { - layoutManager = GridLayoutManager(mContext, 3) - adapter = entity.data?.let { - SubCategoryAdapter( - mContext, - mViewModel, - it, - position - ) - } - addItemDecoration( - GridSpacingItemColorDecoration( - mContext, - 6, - 6, - com.gh.gamecenter.common.R.color.transparent - ) - ) - } - } + fun notifyItemSelectedChanged(parentId: String) { + val position = data.indexOfFirst { it.id == parentId } + if (position != -1) { + notifyItemChanged(position, "") } } - class CategoryDirectoryItemViewHolder(val binding: CategoryDirectoryItemBinding) : - BaseRecyclerViewHolder(binding.root) + class CategoryDirectoryItemViewHolder( + private val listener: SearchCategoryPop.OnSearchCategoryListener, + val binding: CategoryDirectoryItemBinding + ) : + ViewHolder(binding.root) { + + private val childAdapter by lazy { + SubCategoryAdapter(listener) + } + + fun onBind(position: Int, item: CategoryEntity) { + + val context = binding.root.context + binding.title.text = item.name + + if (binding.subCategoryRv.adapter == null) { + binding.subCategoryRv.layoutManager = object : GridLayoutManager(context, 4) { + override fun canScrollVertically(): Boolean { + return false + } + } + binding.subCategoryRv.adapter = childAdapter + binding.subCategoryRv.addItemDecoration( + GridSpacingItemColorDecoration( + context, + 8, + 8, + com.gh.gamecenter.common.R.color.transparent + ) + ) + } + childAdapter.setData(position, item) + } + + fun notifyItemSelectedChanged() { + childAdapter.notifyItemRangeChanged(0, childAdapter.itemCount, "") + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/category2/CategoryV2Activity.kt b/app/src/main/java/com/gh/gamecenter/category2/CategoryV2Activity.kt index bd1bd83cb2..d23fef9b7d 100644 --- a/app/src/main/java/com/gh/gamecenter/category2/CategoryV2Activity.kt +++ b/app/src/main/java/com/gh/gamecenter/category2/CategoryV2Activity.kt @@ -37,8 +37,8 @@ class CategoryV2Activity : DownloadToolbarActivity() { companion object { fun getIntent(context: Context, catalogId: String, catalogTitle: String, entrance: String): Intent { val bundle = Bundle() - bundle.putString(EntranceConsts.KEY_CATEGORY_ID, catalogId) - bundle.putString(EntranceConsts.KEY_CATEGORY_TITLE, catalogTitle) + bundle.putString(EntranceConsts.KEY_PAGE_ID, catalogId) + bundle.putString(EntranceConsts.KEY_PAGE_NAME, catalogTitle) bundle.putString(EntranceConsts.KEY_ENTRANCE, entrance) return getTargetIntent(context, CategoryV2Activity::class.java, CategoryV2Fragment::class.java, bundle) } diff --git a/app/src/main/java/com/gh/gamecenter/category2/CategoryV2Adapter.kt b/app/src/main/java/com/gh/gamecenter/category2/CategoryV2Adapter.kt index 7d85935313..4f4ca2486b 100644 --- a/app/src/main/java/com/gh/gamecenter/category2/CategoryV2Adapter.kt +++ b/app/src/main/java/com/gh/gamecenter/category2/CategoryV2Adapter.kt @@ -1,6 +1,7 @@ package com.gh.gamecenter.category2 import android.content.Context +import android.graphics.Typeface import android.view.View import android.view.ViewGroup import com.gh.gamecenter.common.base.BaseRecyclerViewHolder @@ -13,11 +14,13 @@ import com.lightgame.adapter.BaseRecyclerAdapter class CategoryV2Adapter( context: Context, - private val mFragment: CategoryV2Fragment, private val mViewModel: CategoryV2ViewModel, private val mList: List ) : BaseRecyclerAdapter(context) { + var selectedPosition = 0 + private set + override fun getItemCount() = mList.size override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = @@ -28,25 +31,35 @@ class CategoryV2Adapter( val catalogEntity = mList[position] catalogName.text = catalogEntity.name recommendTag.goneIf(!catalogEntity.recommended) - if (catalogEntity.name == mViewModel.selectedCategoryName) { + if (position == selectedPosition) { selectedTag.visibility = View.VISIBLE - catalogName.setTextColor(com.gh.gamecenter.common.R.color.text_theme.toColor(mContext)) + catalogName.setTextColor(com.gh.gamecenter.common.R.color.text_primary.toColor(mContext)) + catalogName.setTypeface(null, Typeface.BOLD) root.setBackgroundColor(com.gh.gamecenter.common.R.color.ui_surface.toColor(mContext)) } else { selectedTag.visibility = View.GONE - catalogName.setTextColor(com.gh.gamecenter.common.R.color.text_primary.toColor(mContext)) + catalogName.setTextColor(com.gh.gamecenter.common.R.color.text_tertiary.toColor(mContext)) + catalogName.setTypeface(null, Typeface.NORMAL) root.setBackgroundColor(com.gh.gamecenter.common.R.color.ui_background.toColor(mContext)) } root.setOnClickListener { - if (catalogEntity.name != mViewModel.selectedCategoryName) { - mViewModel.selectedCategoryName = catalogEntity.name - mFragment.changeCategory(position) + if (position != selectedPosition) { mViewModel.logClickSide() - notifyDataSetChanged() + mViewModel.selectSidebarsPosition(position) } } } } + fun selectPosition(newPosition: Int) { + if (selectedPosition == newPosition) { + return + } + val oldSelection = selectedPosition + selectedPosition = newPosition + notifyItemChanged(oldSelection) + notifyItemChanged(newPosition) + } + class CategoryV2ItemViewHolder(val binding: CategoryV2ItemBinding) : BaseRecyclerViewHolder(binding.root) } \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/category2/CategoryV2Fragment.kt b/app/src/main/java/com/gh/gamecenter/category2/CategoryV2Fragment.kt index 81ccaad4eb..10304c9cc7 100644 --- a/app/src/main/java/com/gh/gamecenter/category2/CategoryV2Fragment.kt +++ b/app/src/main/java/com/gh/gamecenter/category2/CategoryV2Fragment.kt @@ -1,14 +1,14 @@ package com.gh.gamecenter.category2 +import android.content.res.ColorStateList +import android.graphics.Typeface import android.os.Bundle +import android.view.Gravity import android.view.MenuItem import android.view.View -import android.view.ViewGroup import androidx.core.os.bundleOf -import androidx.core.view.GravityCompat -import androidx.drawerlayout.widget.DrawerLayout -import androidx.lifecycle.Observer -import androidx.recyclerview.widget.LinearLayoutManager +import androidx.core.widget.TextViewCompat +import androidx.fragment.app.viewModels import com.gh.common.util.LogUtils import com.gh.gamecenter.R import com.gh.gamecenter.SearchActivity @@ -22,35 +22,49 @@ import com.gh.gamecenter.common.view.FixLinearLayoutManager import com.gh.gamecenter.core.utils.PageSwitchDataHelper import com.gh.gamecenter.core.utils.SPUtils import com.gh.gamecenter.databinding.FragmentCategoryBinding -import com.gh.gamecenter.entity.CategoryEntity import com.gh.gamecenter.entity.SidebarsEntity +import com.gh.gamecenter.livedata.EventObserver import com.gh.gamecenter.wrapper.SearchToolbarTabWrapperViewModel class CategoryV2Fragment : LazyFragment() { private var mBinding: FragmentCategoryBinding? = null - private var mViewModel: CategoryV2ViewModel? = null + + private val viewModel by viewModels() + private var mHomeViewModel: SearchToolbarTabWrapperViewModel? = null private var mEntity: SidebarsEntity? = null - private var mSpecialCatalogFragment: SpecialCatalogFragment? = null - private var mCategoryV2ListFragment: CategoryV2ListFragment? = null private var mLastPageDataMap: HashMap? = null - private var mCategoryId: String = "" - private var mCategoryTitle: String = "" + private var pageId: String = "" + private var pageName: String = "" private var mLastSelectedPosition = -1 + private var searchCategoryPop: SearchCategoryPop? = null + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + pageId = arguments?.getString(EntranceConsts.KEY_PAGE_ID) ?: "" + pageName = arguments?.getString(EntranceConsts.KEY_PAGE_NAME) ?: "" + mLastPageDataMap = PageSwitchDataHelper.popLastPageData() savedInstanceState?.run { mLastSelectedPosition = getInt(EntranceConsts.KEY_LAST_SELECTED_POSITION) } + + // 除了这里以外,下面还有一个判断是否为首页 tab 栏的赋值 + var entrance = if (mEntrance.contains("首页")) "首页" else "板块" + + val multiTabNavId = arguments?.getString(EntranceConsts.KEY_MULTI_TAB_NAV_ID, "") ?: "" + if (arguments?.getBoolean(EntranceConsts.KEY_IS_HOME) == true && multiTabNavId.isNotEmpty()) { + mHomeViewModel = + viewModelProviderFromParent(SearchToolbarTabWrapperViewModel.Factory(multiTabNavId, ""), multiTabNavId) + entrance = "首页Tab栏" + } + viewModel.init(entrance) } override fun onSaveInstanceState(outState: Bundle) { - mViewModel?.run { - outState.putInt(EntranceConsts.KEY_LAST_SELECTED_POSITION, selectedCategoryPosition) - } + outState.putInt(EntranceConsts.KEY_LAST_SELECTED_POSITION, viewModel.selectedSidebarsPosition.value ?: 0) super.onSaveInstanceState(outState) } @@ -61,100 +75,40 @@ class CategoryV2Fragment : LazyFragment() { } override fun onFragmentFirstVisible() { - mCategoryId = arguments?.getString(EntranceConsts.KEY_CATEGORY_ID) ?: "" - mCategoryTitle = arguments?.getString(EntranceConsts.KEY_CATEGORY_TITLE) ?: "" - mLastPageDataMap = PageSwitchDataHelper.popLastPageData() - mViewModel = viewModelProviderFromParent(CategoryV2ViewModel.Factory(mCategoryId, mCategoryTitle), mCategoryId) - // 除了这里以外,下面还有一个判断是否为首页 tab 栏的赋值 - mViewModel?.entrance = if (mEntrance.contains("首页")) "首页" else "板块" - - val multiTabNavId = arguments?.getString(EntranceConsts.KEY_MULTI_TAB_NAV_ID, "") ?: "" - if (arguments?.getBoolean(EntranceConsts.KEY_IS_HOME) == true && multiTabNavId.isNotEmpty()) { - mHomeViewModel = viewModelProviderFromParent(SearchToolbarTabWrapperViewModel.Factory(multiTabNavId, ""), multiTabNavId) - mViewModel?.entrance = "首页Tab栏" - } - mViewModel?.logAppearance() + viewModel.logAppearance() super.onFragmentFirstVisible() + + viewModel.loadData(pageId, pageName) } + override fun initRealView() { super.initRealView() initMenu(R.menu.menu_search) - setNavigationTitle(mCategoryTitle) + setNavigationTitle(pageName) mBinding?.run { - val width = resources.displayMetrics.widthPixels * 260 / 360 - drawerLayout.setScrimColor(com.gh.gamecenter.common.R.color.black_alpha_30.toColor()) - // 关闭手势滑动 - drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) - drawerLayout.addDrawerListener(object : DrawerLayout.DrawerListener { - override fun onDrawerStateChanged(newState: Int) { - } - - override fun onDrawerSlide(drawerView: View, slideOffset: Float) { - } - - override fun onDrawerClosed(drawerView: View) { - showGuide() - } - - override fun onDrawerOpened(drawerView: View) { - } - - }) - directoryContainer.layoutParams.width = width - directoryRv.layoutParams.width = width - - // 嵌入在首页时特殊处理 - if (arguments?.getBoolean(EntranceConsts.KEY_IS_HOME) == true) { - root.setBackgroundColor(com.gh.gamecenter.common.R.color.ui_surface.toColor(requireContext())) - root.setPadding(0, 8F.dip2px(), 0, 0) - directoryRv.isNestedScrollingEnabled = false - categoryRv.setBackgroundColor(com.gh.gamecenter.common.R.color.ui_background.toColor(requireContext())) - } + tvMoreCategory.setTextColor(com.gh.gamecenter.common.R.color.text_primary.toColor(requireContext())) + tvTagNumber.typeface = + Typeface.createFromAsset(requireContext().assets, Constants.DIN_FONT_PATH) } - mViewModel?.directoriesLiveData?.observeNonNull(viewLifecycleOwner) { - initDirectoryView(it) - } - - mViewModel?.selectedCountLiveData?.observeNonNull(viewLifecycleOwner) { - mBinding?.run { - if (it == 0) { - confirmTv.text = "确定" - } else { - mViewModel?.run { - if (selectedCategoryName != "全部") { - selectedCategoryName = "全部" - selectedCategoryPosition = if (mEntity?.hasSpecial == true) 1 else 0 - categoryRv.adapter?.notifyDataSetChanged() - mCategoryV2ListFragment?.updateSubCategoryId("all") - } - } - - confirmTv.text = "确定(已选${it})" - } - } - } - - mViewModel?.sidebarsLiveData?.observe(viewLifecycleOwner, Observer { + viewModel.sidebarsLiveData.observe(viewLifecycleOwner) { sidebars -> mBinding?.run { reuseLoading.root.visibility = View.GONE - if (it != null) { + if (sidebars != null) { reuseNoConnection.root.visibility = View.GONE categoryContainer.visibility = View.VISIBLE reuseNoneData.root.visibility = View.GONE - mEntity = it - (mEntity!!.sidebars as ArrayList).run { + mEntity = sidebars + (sidebars.sidebars as ArrayList).run { val allEntity = SidebarsEntity.SidebarEntity(name = "全部", categoryId = "all") - if (mEntity!!.hasSpecial) { + add(0, allEntity) + if (sidebars.hasSpecial) { val specialEntity = SidebarsEntity.SidebarEntity(name = "精选") - add(0, specialEntity) - add(1, allEntity) - } else { - add(0, allEntity) + add(1, specialEntity) } } initView() @@ -165,12 +119,70 @@ class CategoryV2Fragment : LazyFragment() { reuseNoConnection.root.setOnClickListener { reuseNoConnection.root.visibility = View.GONE reuseLoading.root.visibility = View.VISIBLE - mViewModel?.getSidebars() - mViewModel?.getCategoryDirectories() + viewModel.loadData(pageId, pageName) } } } - }) + } + + viewModel.directoriesLiveData.observe(viewLifecycleOwner) { + searchCategoryPop?.setData(it) + } + + viewModel.selectedSubCategories.observe(viewLifecycleOwner) { selectedList -> + searchCategoryPop?.updateCategorySelected(selectedList) + updateMoreCategory(selectedList.size) + } + + viewModel.selectedSidebarsPosition.observe(viewLifecycleOwner) { + mLastSelectedPosition = it + onSelectedPositionChanged(it) + + val adapter = mBinding?.categoryRv?.adapter + if (adapter is CategoryV2Adapter) { + adapter.selectPosition(it) + } + } + + viewModel.notifySubCategorySelected.observe( + viewLifecycleOwner, + EventObserver { + searchCategoryPop?.notifyItemSelectedChanged(it) + }) + } + + private fun createSearchPop(isAutoRequestFocus: Boolean): SearchCategoryPop { + val height = mBinding!!.root.height + return SearchCategoryPop.newInstance(requireContext(), height, isAutoRequestFocus, pageId, pageName).apply { + val data = viewModel.directoriesLiveData.value ?: listOf() + setData(data) + val selectedList = viewModel.selectedSubCategories.value + updateCategorySelected(selectedList) + setOnSearchCategoryListener(object : SearchCategoryPop.OnSearchCategoryListener { + override fun isEnableSelected(): Boolean { + val size = viewModel.selectedSubCategories.value?.size ?: 0 + return size < 5 + } + + override fun onItemSelected(selected: CategoryV2ViewModel.SelectedTags) { + viewModel.addSubCategorySelected(selected) + } + + override fun onItemRemoved(parentId: String, subCategoryId: String) { + viewModel.removeSubCategorySelected(parentId, subCategoryId, "全部游戏") + } + + override fun onResetSelected() { + viewModel.clearSelectedTag() + viewModel.updateGameFiltered() + } + + override fun onSubmit() { + viewModel.logClickDetermine() + } + + }) + } } fun removeGuide() { @@ -181,15 +193,7 @@ class CategoryV2Fragment : LazyFragment() { } private fun showGuide() { - if (!isAdded) return mBinding?.run { - val isShow = SPUtils.getBoolean(Constants.SP_SHOW_CATEGORY_GUIDE) - if (isShow) return - - guideContainer.layoutParams = (guideContainer.layoutParams as ViewGroup.MarginLayoutParams).apply { - val screenWidth = resources.displayMetrics.widthPixels - leftMargin = screenWidth * 66F.dip2px() / 360F.dip2px() - } guideContainer.visibility = View.VISIBLE postDelayedRunnable({ @@ -204,7 +208,7 @@ class CategoryV2Fragment : LazyFragment() { override fun onMenuItemClick(menuItem: MenuItem?) { menuItem?.run { if (itemId == R.id.menu_search) { - LogUtils.uploadSearchGame("access_to_search", mCategoryTitle, "", "") + LogUtils.uploadSearchGame("access_to_search", pageName, "", "") val intent = SearchActivity.getIntent( requireContext(), false, @@ -217,63 +221,64 @@ class CategoryV2Fragment : LazyFragment() { } } - private fun initDirectoryView(list: List) { - mBinding?.run { - mViewModel?.run { - if (directoryRv.adapter != null) { - (directoryRv.adapter as? CategoryDirectoryAdapter)?.setListData(list) - } else { - directoryRv.layoutManager = FixLinearLayoutManager(requireContext()) - directoryRv.adapter = CategoryDirectoryAdapter( - requireContext(), - this, - list - ) - } - } - - resetTv.setOnClickListener { - mViewModel?.logClickReset("全部类别") - confirmTv.text = "确定" - mViewModel?.resetDirectoryList() - mCategoryV2ListFragment?.changeCategoryTab() - } - - confirmTv.setOnClickListener { - mViewModel?.logClickDetermine() - drawerLayout.closeDrawer(GravityCompat.START) - } - } - } - private fun initView() { - if (mEntity?.sidebars?.isNullOrEmpty() == true || mViewModel == null) return + if (mEntity?.sidebars.isNullOrEmpty()) return initSelectedCategory() initCategoryRv() - initContentFragment() + + mBinding?.run { + + vSearchCategory.setOnClickListener { + SensorsBridge.logClassificationSearch(pageId, pageName) + removeGuide() + showSearchPop(true) + } + + vMoreCategory.setOnClickListener { + removeGuide() + showSearchPop(false) + } + } + + val isShow = SPUtils.getBoolean(Constants.SP_SHOW_CATEGORY_GUIDE) + if (!isShow) { + postDelayedRunnable({ + showSearchPop(false) + searchCategoryPop?.setOnDismissListener { + showGuide() + } + }, 200) + } + + } + + private fun showSearchPop(isAutoRequestFocus: Boolean) { + mBinding?.run { + val location = IntArray(2) + vSearchCategory.getLocationOnScreen(location) + + val popTop = location[1] - 8F.dip2px() + searchCategoryPop = createSearchPop(isAutoRequestFocus) + searchCategoryPop?.showAtLocation(vSearchCategory, Gravity.TOP, 0, popTop) + } } private fun initSelectedCategory() { - mEntity?.run { - mViewModel?.run { - if (mLastSelectedPosition != -1) { - selectedCategoryPosition = mLastSelectedPosition - selectedCategoryName = sidebars[mLastSelectedPosition].name - } else { - selectedCategoryPosition = 0 - selectedCategoryName = sidebars[0].name - } - } + if (mLastSelectedPosition != -1) { + viewModel.selectSidebarsPosition(mLastSelectedPosition) + } else { + // 默认选中第 0 个 位置 + viewModel.selectSidebarsPosition(0) } + } private fun initCategoryRv() { mEntity?.run { - mViewModel?.run { + viewModel.run { mBinding?.categoryRv?.layoutManager = FixLinearLayoutManager(requireContext()) mBinding?.categoryRv?.adapter = CategoryV2Adapter( requireContext(), - this@CategoryV2Fragment, this, sidebars ) @@ -281,226 +286,93 @@ class CategoryV2Fragment : LazyFragment() { } } - private fun initContentFragment() { - mEntity?.apply { - mViewModel?.apply { - if (hasSpecial && selectedCategoryPosition == 0) { - initSpecialCatalogFragment() - } else { - initCategoryV2ListFragment() + private fun onSelectedPositionChanged(position: Int) { + mEntity?.run { + viewModel.run { + clearSelectedTag() + childFragmentManager.fragments.find { it.isAdded } + val targetFragment = + if (hasSpecial && position == 1) { + val fragment = childFragmentManager.findFragmentByTag(SpecialCatalogFragment::class.java.name) + ?: SpecialCatalogFragment() + fragment.arguments = bundleOf( + EntranceConsts.KEY_IS_CATEGORY_V2 to true, + EntranceConsts.KEY_CATALOG_ID to pageId, + EntranceConsts.KEY_CATALOG_TITLE to pageName, + EntranceConsts.KEY_EXPOSURE_SOURCE_LIST to arguments?.getParcelableArrayList( + EntranceConsts.KEY_EXPOSURE_SOURCE_LIST + ), + EntranceConsts.KEY_LAST_PAGE_DATA to mLastPageDataMap + ) + fragment - // 第一次点"全部"tab展开全部类别选择框 - // 加延迟是为了防止卡顿 - if (SPUtils.getBoolean(Constants.SP_FIRST_ENTER_CATEGORY_V2, true)) { - SPUtils.setBoolean(Constants.SP_FIRST_ENTER_CATEGORY_V2, false) - mBinding?.drawerLayout?.postDelayed({ - tryCatchInRelease { openDrawer() } - }, 500L) + } else { + val fragment = (childFragmentManager.findFragmentByTag(CategoryV2ListFragment::class.java.name) + ?: CategoryV2ListFragment()) + fragment.arguments = bundleOf( + EntranceConsts.KEY_PAGE_ID to id, + EntranceConsts.KEY_PAGE_NAME to pageName, + EntranceConsts.KEY_EXPOSURE_SOURCE_LIST to arguments?.getParcelableArrayList( + EntranceConsts.KEY_EXPOSURE_SOURCE_LIST + ), + EntranceConsts.KEY_LAST_PAGE_DATA to mLastPageDataMap + ) + fragment } - } - } - } - } - private fun initSpecialCatalogFragment() { - mEntity?.run { - mSpecialCatalogFragment = childFragmentManager - .findFragmentByTag(SpecialCatalogFragment::class.java.name) - as? SpecialCatalogFragment ?: SpecialCatalogFragment() - mSpecialCatalogFragment?.arguments = bundleOf( - EntranceConsts.KEY_IS_CATEGORY_V2 to true, - EntranceConsts.KEY_CATALOG_ID to id, - EntranceConsts.KEY_CATALOG_TITLE to mCategoryTitle, - EntranceConsts.KEY_EXPOSURE_SOURCE_LIST to arguments?.getParcelableArrayList( - EntranceConsts.KEY_EXPOSURE_SOURCE_LIST - ), - EntranceConsts.KEY_LAST_PAGE_DATA to mLastPageDataMap - ) - childFragmentManager - .beginTransaction() - .replace( - R.id.gamesContainer, - mSpecialCatalogFragment!!, - SpecialCatalogFragment::class.java.name - ) - .commitAllowingStateLoss() - } - } - - private fun initCategoryV2ListFragment() { - mEntity?.run { - mViewModel?.run { - mCategoryV2ListFragment = childFragmentManager - .findFragmentByTag(CategoryV2ListFragment::class.java.name) - as? CategoryV2ListFragment ?: CategoryV2ListFragment() - mCategoryV2ListFragment?.arguments = bundleOf( - EntranceConsts.KEY_CATEGORY_ID to id, - EntranceConsts.KEY_SUB_CATEGORY_ID to sidebars[selectedCategoryPosition].categoryId, - EntranceConsts.KEY_CATEGORY_TITLE to mCategoryTitle, - EntranceConsts.KEY_EXPOSURE_SOURCE_LIST to arguments?.getParcelableArrayList( - EntranceConsts.KEY_EXPOSURE_SOURCE_LIST - ), - EntranceConsts.KEY_LAST_PAGE_DATA to mLastPageDataMap - ) childFragmentManager .beginTransaction() - .replace( - R.id.gamesContainer, - mCategoryV2ListFragment!!, - CategoryV2ListFragment::class.java.name - ) + .replace(R.id.gamesContainer, targetFragment, targetFragment::class.java.name) .commitAllowingStateLoss() } } } - fun changeCategory(position: Int) { - mEntity?.run { - mViewModel?.run { - resetDirectoryList() - - if (hasSpecial) { - if (selectedCategoryPosition == 0) { - mCategoryV2ListFragment = childFragmentManager - .findFragmentByTag(CategoryV2ListFragment::class.java.name) - as? CategoryV2ListFragment ?: CategoryV2ListFragment() - mCategoryV2ListFragment?.arguments = bundleOf( - EntranceConsts.KEY_CATEGORY_ID to id, - EntranceConsts.KEY_SUB_CATEGORY_ID to sidebars[position].categoryId, - EntranceConsts.KEY_CATALOG_TITLE to mCategoryTitle, - EntranceConsts.KEY_EXPOSURE_SOURCE_LIST to arguments?.getParcelableArrayList( - EntranceConsts.KEY_EXPOSURE_SOURCE_LIST - ), - EntranceConsts.KEY_LAST_PAGE_DATA to mLastPageDataMap - ) - childFragmentManager - .beginTransaction() - .replace( - R.id.gamesContainer, - mCategoryV2ListFragment!!, - CategoryV2ListFragment::class.java.name - ) - .commitAllowingStateLoss() - } else { - if (position == 0) { - removeGuide() - - mSpecialCatalogFragment = childFragmentManager - .findFragmentByTag(SpecialCatalogFragment::class.java.name) - as? SpecialCatalogFragment ?: SpecialCatalogFragment() - mSpecialCatalogFragment?.arguments = bundleOf( - EntranceConsts.KEY_IS_CATEGORY_V2 to true, - EntranceConsts.KEY_CATALOG_ID to id, - EntranceConsts.KEY_CATALOG_TITLE to mCategoryTitle, - EntranceConsts.KEY_EXPOSURE_SOURCE_LIST to arguments?.getParcelableArrayList( - EntranceConsts.KEY_EXPOSURE_SOURCE_LIST - ), - EntranceConsts.KEY_LAST_PAGE_DATA to mLastPageDataMap - ) - childFragmentManager - .beginTransaction() - .replace( - R.id.gamesContainer, - mSpecialCatalogFragment!!, - SpecialCatalogFragment::class.java.name - ) - .commitAllowingStateLoss() - } else { - if (mCategoryV2ListFragment?.isStateSaved == false) { - mCategoryV2ListFragment?.arguments = bundleOf( - EntranceConsts.KEY_CATEGORY_ID to id, - EntranceConsts.KEY_SUB_CATEGORY_ID to sidebars[position].categoryId, - EntranceConsts.KEY_CATALOG_TITLE to mCategoryTitle, - EntranceConsts.KEY_EXPOSURE_SOURCE_LIST to arguments?.getParcelableArrayList( - EntranceConsts.KEY_EXPOSURE_SOURCE_LIST - ), - EntranceConsts.KEY_LAST_PAGE_DATA to mLastPageDataMap - ) - } - mCategoryV2ListFragment?.changeCategoryTab(sidebars[position].categoryId) - } - } - - // 第一次点"全部"tab展开全部类别选择框 - // 加延迟是为了防止卡顿 - if (position == 1 && SPUtils.getBoolean(Constants.SP_FIRST_ENTER_CATEGORY_V2, true)) { - SPUtils.setBoolean(Constants.SP_FIRST_ENTER_CATEGORY_V2, false) - mBinding?.drawerLayout?.postDelayed({ - tryCatchInRelease { openDrawer() } - }, 200L) - } - } else { - if (mCategoryV2ListFragment?.isStateSaved == false) { - mCategoryV2ListFragment?.arguments = bundleOf( - EntranceConsts.KEY_CATEGORY_ID to id, - EntranceConsts.KEY_SUB_CATEGORY_ID to sidebars[position].categoryId, - EntranceConsts.KEY_CATALOG_TITLE to mCategoryTitle, - EntranceConsts.KEY_EXPOSURE_SOURCE_LIST to arguments?.getParcelableArrayList( - EntranceConsts.KEY_EXPOSURE_SOURCE_LIST - ), - EntranceConsts.KEY_LAST_PAGE_DATA to mLastPageDataMap - ) - } - mCategoryV2ListFragment?.changeCategoryTab(sidebars[position].categoryId) - } - selectedCategoryPosition = position - } - } - } - - fun openDirectoryLayout() { + private fun updateMoreCategory(size: Int) { mBinding?.run { - var i = 0 - mViewModel?.run { - mEntity?.run { - val sidebar = sidebars[selectedCategoryPosition] - if (sidebar.name != "全部") { - directories.forEachIndexed { index, entity -> - if (sidebar.type == "level_one") { - if (sidebar.categoryId == entity.id) { - i = index - return@run - } - } else { - if (sidebar.parentId == entity.id) { - i = index - return@run - } - } - } - } else if (sidebar.name == "全部" && selectedCategoryList.isNotEmpty()) { - i = selectedCategoryList[0].primaryIndex - } + if (size > 0) { + vMoreCategory.setBackgroundResource(R.drawable.bg_more_category_filtered) + TextViewCompat.setCompoundDrawableTintList( + tvMoreCategory, + ColorStateList.valueOf(com.gh.gamecenter.common.R.color.text_theme.toColor(requireContext())) + ) + tvMoreCategory.setTextColor(com.gh.gamecenter.common.R.color.text_theme.toColor(requireContext())) + tvTagNumber.goneIf(false) { + tvTagNumber.text = "$size" } + } else { + vMoreCategory.setBackgroundResource(R.drawable.bg_more_category_default) + TextViewCompat.setCompoundDrawableTintList( + tvMoreCategory, + ColorStateList.valueOf(com.gh.gamecenter.common.R.color.text_primary.toColor(requireContext())) + ) + tvMoreCategory.setTextColor(com.gh.gamecenter.common.R.color.text_primary.toColor(requireContext())) + tvTagNumber.goneIf(true) } - openDrawer() - (directoryRv.layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset(i, 0) - } - } - - private fun openDrawer() { - mBinding?.drawerLayout?.openDrawer(GravityCompat.START) - - mHomeViewModel?.let { - mBinding?.directoryContainer?.setPadding( - 0, - 0, - 0, - requireContext().resources.getDimension(com.gh.gamecenter.common.R.dimen.main_bottom_tab_height).toInt() - it.appBarOffset - ) } } override fun onDarkModeChanged() { super.onDarkModeChanged() getItemMenu(R.id.menu_search)?.setIcon(R.drawable.ic_column_search) - mBinding?.categoryRv?.adapter?.run { - mBinding?.categoryRv?.recycledViewPool?.clear() - notifyItemRangeChanged(0, itemCount) - } - mBinding?.directoryRv?.adapter?.run { - mBinding?.directoryRv?.recycledViewPool?.clear() - notifyItemRangeChanged(0, itemCount) + mBinding?.run { + categoryRv.adapter?.run { + categoryRv.recycledViewPool.clear() + notifyItemRangeChanged(0, itemCount, "") + } + + val selectedTagsSize = viewModel.selectedSubCategories.value?.size ?: 0 + updateMoreCategory(selectedTagsSize) + + context?.let { + ivSearchCategory.imageTintList = + ColorStateList.valueOf(com.gh.gamecenter.common.R.color.text_instance.toColor(it)) + } } } + + companion object { + + private const val SPECIAL_CATEGORY_POSITION = 1 + } } \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/category2/CategoryV2ListAdapter.kt b/app/src/main/java/com/gh/gamecenter/category2/CategoryV2ListAdapter.kt index 42d704d402..5efd6a914a 100644 --- a/app/src/main/java/com/gh/gamecenter/category2/CategoryV2ListAdapter.kt +++ b/app/src/main/java/com/gh/gamecenter/category2/CategoryV2ListAdapter.kt @@ -76,8 +76,15 @@ class CategoryV2ListAdapter( ItemViewType.GAME_NORMAL -> { CategoryGameItemViewHolder(parent.toBinding()) } + else -> { - FooterViewHolder(mLayoutInflater.inflate(com.gh.gamecenter.common.R.layout.refresh_footerview, parent, false)) + FooterViewHolder( + mLayoutInflater.inflate( + com.gh.gamecenter.common.R.layout.refresh_footerview, + parent, + false + ) + ) } } } @@ -94,20 +101,12 @@ class CategoryV2ListAdapter( holder.bindGameItem(gameEntity) holder.initServerType(gameEntity) - val categoryTitle = mCategoryViewModel.categoryTitle - val selectedCategoryName = mCategoryViewModel.selectedCategoryName - val builder = StringBuilder() - mCategoryViewModel.selectedCategoryList.run { - forEachIndexed { index, entity -> - builder.append(entity.name) - if (index != size - 1) { - builder.append("_") - } - } - } - val selectedSubCatalogName = builder.toString() - val sortType = mViewModel.sortType.value - val sortSize = mViewModel.sortSize.text + val categoryTitle = mCategoryViewModel.pageName + val selectedCategoryName = mCategoryViewModel.selectedSidebarsName + val selectedSubCatalogName = + mCategoryViewModel.selectedSubCategories.value?.joinToString("-") { it.category.name ?: "" } + val sortType = mViewModel.gameFiltered.sortType.value + val sortSize = mViewModel.gameFiltered.size.text val exposureSources = ArrayList() if (!mViewModel.exposureSourceList.isNullOrEmpty()) { @@ -172,8 +171,13 @@ class CategoryV2ListAdapter( ) { val trackEvent = JSONObject() try { - trackEvent.put("navigation_bar_name", mCategoryViewModel.selectedCategoryName) - trackEvent.put("game_tag", mCategoryViewModel.selectedCategoryList.map { it.name }) + trackEvent.put("navigation_bar_name", mCategoryViewModel.selectedSidebarsName) + trackEvent.put( + "game_tag", + (mCategoryViewModel.selectedSubCategories.value ?: listOf()) + .map { + it.category.name + }) trackEvent.put("game_status", gameEntity.category) trackEvent.put( "inclusion_size", @@ -266,18 +270,24 @@ class CategoryV2ListAdapter( binding.gameKaifuType.visibility = View.GONE binding.gameKaifuType.text = "" } + serverLabel != null -> { binding.gameKaifuType.visibility = View.VISIBLE binding.gameKaifuType.text = serverLabel.value if (gameEntity.isUseDefaultServerStyle()) { binding.gameKaifuType.background = com.gh.gamecenter.feature.R.drawable.server_label_default_bg.toDrawable(binding.root.context) - binding.gameKaifuType.setTextColor(com.gh.gamecenter.common.R.color.text_secondary.toColor(binding.root.context)) + binding.gameKaifuType.setTextColor( + com.gh.gamecenter.common.R.color.text_secondary.toColor( + binding.root.context + ) + ) } else { binding.gameKaifuType.background = DrawableView.getServerDrawable(serverLabel.color) binding.gameKaifuType.setTextColor(com.gh.gamecenter.common.R.color.white.toColor(binding.root.context)) } } + else -> binding.gameKaifuType.visibility = View.GONE } diff --git a/app/src/main/java/com/gh/gamecenter/category2/CategoryV2ListFragment.kt b/app/src/main/java/com/gh/gamecenter/category2/CategoryV2ListFragment.kt index 22f56f0a20..27dd593a52 100644 --- a/app/src/main/java/com/gh/gamecenter/category2/CategoryV2ListFragment.kt +++ b/app/src/main/java/com/gh/gamecenter/category2/CategoryV2ListFragment.kt @@ -2,9 +2,10 @@ package com.gh.gamecenter.category2 import android.os.Bundle import android.view.View -import android.view.ViewGroup +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import com.ethanhua.skeleton.Skeleton -import com.gh.gamecenter.common.constant.Constants import com.gh.common.exposure.ExposureListener import com.gh.common.util.DialogUtils import com.gh.common.view.CategoryFilterView @@ -13,16 +14,17 @@ import com.gh.common.xapk.XapkUnzipStatus import com.gh.download.DownloadManager import com.gh.gamecenter.R import com.gh.gamecenter.common.baselist.ListFragment +import com.gh.gamecenter.common.constant.Constants import com.gh.gamecenter.common.constant.EntranceConsts -import com.gh.gamecenter.common.utils.* +import com.gh.gamecenter.common.utils.goneIf +import com.gh.gamecenter.common.utils.observeNonNull +import com.gh.gamecenter.common.utils.toColor +import com.gh.gamecenter.common.utils.viewModelProvider import com.gh.gamecenter.databinding.FragmentCategoryListBinding -import com.gh.gamecenter.databinding.LayoutSelectedCategoryBinding -import com.gh.gamecenter.entity.CategoryEntity -import com.gh.gamecenter.feature.entity.GameEntity import com.gh.gamecenter.entity.SubjectSettingEntity import com.gh.gamecenter.eventbus.EBDownloadStatus import com.gh.gamecenter.eventbus.EBPackage -import com.google.android.flexbox.FlexboxLayout +import com.gh.gamecenter.feature.entity.GameEntity import com.lightgame.download.DataWatcher import com.lightgame.download.DownloadEntity import org.greenrobot.eventbus.Subscribe @@ -30,13 +32,18 @@ import org.greenrobot.eventbus.ThreadMode class CategoryV2ListFragment : ListFragment() { - private var mCategoryId: String = "" - private var mSubCategoryId: String = "" - private var mCategoryTitle: String = "" + private var pageId: String = "" + private var pageName: String = "" + private val parentViewModel by viewModels( + ownerProducer = { parentFragment ?: this } + ) + + override fun isAutomaticLoad(): Boolean { + return false + } + private var mAdapter: CategoryV2ListAdapter? = null - private var mSelectedViewList = ArrayList() private var mBinding: FragmentCategoryListBinding? = null - private var mCategoryViewModel: CategoryV2ViewModel? = null private var mLastPageDataMap: HashMap? = null private val mDataWatcher = object : DataWatcher() { override fun onDataChanged(downloadEntity: DownloadEntity) { @@ -52,6 +59,12 @@ class CategoryV2ListFragment : ListFragment } } + private val selectedTagsAdapter by lazy { + SelectedTagsAdapter { + parentViewModel.removeSubCategorySelected(it.parentId, it.category.id, "游戏列表") + } + } + override fun getLayoutId() = 0 override fun getInflatedLayout() = @@ -60,8 +73,7 @@ class CategoryV2ListFragment : ListFragment override fun provideListViewModel() = viewModelProvider( CategoryV2ListViewModel.Factory( - mCategoryId, - mSubCategoryId, + pageId, arguments?.getParcelableArrayList(EntranceConsts.KEY_EXPOSURE_SOURCE_LIST) ) ) @@ -70,12 +82,7 @@ class CategoryV2ListFragment : ListFragment ?: CategoryV2ListAdapter( requireContext(), mListViewModel ?: provideListViewModel(), - mCategoryViewModel ?: viewModelProviderFromParent( - CategoryV2ViewModel.Factory( - mCategoryId, - mCategoryTitle - ), mCategoryId - ), + parentViewModel, mEntrance, mLastPageDataMap ).apply { mAdapter = this } @@ -83,12 +90,10 @@ class CategoryV2ListFragment : ListFragment override fun getItemDecoration() = null override fun onCreate(savedInstanceState: Bundle?) { - mCategoryId = arguments?.getString(EntranceConsts.KEY_CATEGORY_ID) ?: "" - mSubCategoryId = arguments?.getString(EntranceConsts.KEY_SUB_CATEGORY_ID) ?: "" - mCategoryTitle = arguments?.getString(EntranceConsts.KEY_CATEGORY_TITLE) ?: "" + pageId = arguments?.getString(EntranceConsts.KEY_PAGE_ID) ?: "" + pageName = arguments?.getString(EntranceConsts.KEY_PAGE_NAME) ?: "" mLastPageDataMap = arguments?.getSerializable(EntranceConsts.KEY_LAST_PAGE_DATA) as? HashMap - mCategoryViewModel = - viewModelProviderFromParent(CategoryV2ViewModel.Factory(mCategoryId, mCategoryTitle), mCategoryId) + mEntrance = arguments?.getString(EntranceConsts.KEY_ENTRANCE) ?: Constants.ENTRANCE_UNKNOWN super.onCreate(savedInstanceState) @@ -104,19 +109,8 @@ class CategoryV2ListFragment : ListFragment initFilterView() - mListViewModel?.refresh?.observeNonNull(viewLifecycleOwner) { onRefresh() } - mCategoryViewModel?.run { - categoryPositionLiveData.observeNonNull(viewLifecycleOwner) { - directories[it.first].data?.get(it.second)?.run { - mBinding?.selectedCategoryContainer?.visibility = View.VISIBLE - - if (selected) { - addCategory(this) - } else { - removeCategory(this) - } - } - } + mListViewModel?.refresh?.observeNonNull(viewLifecycleOwner) { + onRefresh() } mListRv.addOnScrollListener(ExposureListener(this, provideListAdapter())) @@ -132,19 +126,40 @@ class CategoryV2ListFragment : ListFragment .show() mBinding?.reuseNoneData?.reuseResetLoadTv?.setOnClickListener { - mCategoryViewModel?.run { + // 重试时, + // 清空所有筛选条件 + with(parentViewModel) { logClickReset("游戏列表") - resetDirectoryList() + clearSelectedTag() + // 移除大小限制 + mBinding?.filterContainer?.resetSortSize() + val size = SubjectSettingEntity.Size() + updateGameFiltered(size) + } + + } + + with(parentViewModel) { + gameFiltered.observe(viewLifecycleOwner) { + mListViewModel.updateSortConfig(it) + } + + selectedSubCategories.observe(viewLifecycleOwner) { + updateSelectedTags(it) } - resetSortSize() - changeCategoryTab() -// openDirectoryLayout() } } - private fun resetSortSize() { - mBinding?.filterContainer?.resetSortSize() - mListViewModel?.sortSize = SubjectSettingEntity.Size(min = -1, max = -1, text = "全部大小") + private fun updateSelectedTags(selectedTags: List) { + mBinding?.run { + flTagsContainer.goneIf(selectedTags.isEmpty()) { + if (rvTags.adapter == null) { + rvTags.layoutManager = LinearLayoutManager(requireContext(), RecyclerView.HORIZONTAL, false) + rvTags.adapter = selectedTagsAdapter + } + selectedTagsAdapter.submitList(selectedTags) + } + } } @@ -154,22 +169,19 @@ class CategoryV2ListFragment : ListFragment setOnConfigSetupListener(object : CategoryFilterView.OnCategoryFilterSetupListener { override fun onSetupSortSize(sortSize: SubjectSettingEntity.Size) { - mListViewModel?.updateSortConfig(sortSize = sortSize) + parentViewModel.updateGameFiltered(size = sortSize) } override fun onSetupSortType(sortType: CategoryFilterView.SortType) { - mListViewModel?.updateSortConfig(sortType = sortType) + parentViewModel.updateGameFiltered(sortType = sortType) } - override fun onSetupSortCategory() { - openDirectoryLayout() + override fun getPopHeight(): Int { + return (mBinding?.root?.height ?: 0) - (mBinding?.flTagsContainer?.height ?: 0) } }) setOnFilterClickListener(object : CategoryFilterView.OnFilterClickListener { - override fun onCategoryClick() { - removeGuide() - } override fun onTypeClick() { removeGuide() @@ -183,101 +195,6 @@ class CategoryV2ListFragment : ListFragment } } - fun updateSubCategoryId(id: String) { - mSubCategoryId = id - } - - fun changeCategoryTab(categoryId: String? = null) { - mSelectedViewList.clear() - mBinding?.selectedCategoryContainer?.run { - removeAllViews() - visibility = View.GONE - } - if (categoryId != null) mSubCategoryId = categoryId - mListViewModel?.updateSortConfig(categoryIds = mSubCategoryId) - } - - private fun addCategory(entity: CategoryEntity) { - addCategoryView(entity) - mCategoryViewModel?.selectedCategoryList?.add(entity) - updateCategoryGame() - } - - private fun removeCategory(entity: CategoryEntity) { - mCategoryViewModel?.selectedCategoryList?.run { - if (isEmpty()) return - - removeCategoryView(entity.name ?: "") - remove(entity) - if (size == 0) { - mBinding?.selectedCategoryContainer?.visibility = View.GONE - mListViewModel?.updateSortConfig(categoryIds = mSubCategoryId) - } else { - updateCategoryGame() - } - } - } - - private fun addCategoryView(entity: CategoryEntity) { - val binding = LayoutSelectedCategoryBinding.inflate(layoutInflater).apply { - val params = - FlexboxLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) - params.setMargins(0, 8F.dip2px(), 8F.dip2px(), 0) - params.height = 24F.dip2px() - root.layoutParams = params - - name.text = entity.name - root.setOnClickListener { - removeCategoryAndNotify(entity) - } - } - - mSelectedViewList.add(binding.root) - mBinding?.selectedCategoryContainer?.addView(binding.root) - } - - private fun removeCategoryView(categoryName: String) { - mCategoryViewModel?.selectedCategoryList?.run { - var i = 0 - forEachIndexed { index, categoryEntity -> - if (categoryName == categoryEntity.name) { - i = index - } - } - - if (i < mSelectedViewList.size) { - mBinding?.selectedCategoryContainer?.removeView(mSelectedViewList[i]) - mSelectedViewList.removeAt(i) - } - } - } - - private fun removeCategoryAndNotify(entity: CategoryEntity) { - removeCategory(entity) - entity.selected = false - mCategoryViewModel?.run { - if (selectedCount > 0) { - selectedCount-- - postSelectedCount() - postCategoryDirectoryList() - logClickClassificationDelete(entity.primaryIndex, entity.name ?: "", "游戏列表") - } - } - } - - private fun updateCategoryGame() { - mCategoryViewModel?.selectedCategoryList?.run { - val categoryIds = StringBuilder() - forEachIndexed { index, s -> - categoryIds.append(s.id) - if (index != size - 1) { - categoryIds.append("-") - } - } - mListViewModel?.updateSortConfig(categoryIds = categoryIds.toString()) - } - } - private fun removeGuide() { (parentFragment as? CategoryV2Fragment)?.removeGuide() } @@ -328,10 +245,6 @@ class CategoryV2ListFragment : ListFragment } } - fun openDirectoryLayout() { - (parentFragment as? CategoryV2Fragment)?.openDirectoryLayout() - } - override fun onDarkModeChanged() { super.onDarkModeChanged() mBinding?.filterContainer?.run { diff --git a/app/src/main/java/com/gh/gamecenter/category2/CategoryV2ListViewModel.kt b/app/src/main/java/com/gh/gamecenter/category2/CategoryV2ListViewModel.kt index a1fbcb2c61..4a397d8cc5 100644 --- a/app/src/main/java/com/gh/gamecenter/category2/CategoryV2ListViewModel.kt +++ b/app/src/main/java/com/gh/gamecenter/category2/CategoryV2ListViewModel.kt @@ -4,14 +4,13 @@ import android.app.Application import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.gh.gamecenter.common.entity.ExposureEntity import com.gh.common.exposure.ExposureUtils -import com.gh.gamecenter.core.utils.UrlFilterUtils import com.gh.common.view.CategoryFilterView import com.gh.gamecenter.common.baselist.ListViewModel +import com.gh.gamecenter.common.entity.ExposureEntity import com.gh.gamecenter.common.exposure.ExposureSource +import com.gh.gamecenter.core.utils.UrlFilterUtils import com.gh.gamecenter.feature.entity.GameEntity -import com.gh.gamecenter.entity.SubjectSettingEntity import com.gh.gamecenter.retrofit.RetrofitManager import com.halo.assistant.HaloApp import io.reactivex.Observable @@ -20,14 +19,13 @@ import io.reactivex.Single class CategoryV2ListViewModel( application: Application, val categoryId: String, - var categoryIds: String, var exposureSourceList: List? ) : ListViewModel(application) { val refresh = MutableLiveData() - var sortType = CategoryFilterView.SortType.RECOMMENDED - var sortSize = SubjectSettingEntity.Size() + var gameFiltered = CategoryV2ViewModel.GameFiltered() + private set override fun provideDataObservable(page: Int): Observable>? = null @@ -51,52 +49,36 @@ class CategoryV2ListViewModel( } fun updateSortConfig( - sortSize: SubjectSettingEntity.Size? = null, - sortType: CategoryFilterView.SortType? = null, - categoryIds: String? = null + gameFiltered: CategoryV2ViewModel.GameFiltered ) { - when { - sortSize != null && sortSize != this.sortSize -> { - this.sortSize = sortSize - refresh.postValue(true) - } - - sortType != null && sortType != this.sortType -> { - this.sortType = sortType - refresh.postValue(true) - } - - categoryIds != null && categoryIds != this.categoryIds -> { - this.categoryIds = categoryIds - refresh.postValue(true) - } - } + this.gameFiltered = gameFiltered + refresh.postValue(true) } private fun getFilter(): String? { + return UrlFilterUtils.getFilterQuery( - "category_ids", categoryIds, - "min_size", sortSize.min.toString(), - "max_size", sortSize.max.toString() + "category_ids", gameFiltered.categoryIds.ifBlank { gameFiltered.sidebarCategoryId }, + "min_size", gameFiltered.size.min.toString(), + "max_size", gameFiltered.size.max.toString() ) } private fun getSortType(): String? { - return when (sortType) { + return when (gameFiltered.sortType) { CategoryFilterView.SortType.RECOMMENDED -> "download:-1" CategoryFilterView.SortType.NEWEST -> "publish:-1" CategoryFilterView.SortType.RATING -> "star:-1" } } - class Factory(val categoryId: String, val categoryIds: String, val exposureSourceList: List?) : + class Factory(val categoryId: String, val exposureSourceList: List?) : ViewModelProvider.NewInstanceFactory() { override fun create(modelClass: Class): T { return CategoryV2ListViewModel( HaloApp.getInstance().application, categoryId, - categoryIds, exposureSourceList ) as T } diff --git a/app/src/main/java/com/gh/gamecenter/category2/CategoryV2ViewModel.kt b/app/src/main/java/com/gh/gamecenter/category2/CategoryV2ViewModel.kt index ba6f329bd9..721a4d9723 100644 --- a/app/src/main/java/com/gh/gamecenter/category2/CategoryV2ViewModel.kt +++ b/app/src/main/java/com/gh/gamecenter/category2/CategoryV2ViewModel.kt @@ -1,160 +1,214 @@ package com.gh.gamecenter.category2 -import android.annotation.SuppressLint -import android.app.Application -import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import com.gh.common.util.LogUtils +import com.gh.common.view.CategoryFilterView +import com.gh.gamecenter.common.retrofit.BiResponse +import com.gh.gamecenter.common.utils.singleToMain import com.gh.gamecenter.entity.CategoryEntity import com.gh.gamecenter.entity.SidebarsEntity -import com.gh.gamecenter.common.retrofit.BiResponse +import com.gh.gamecenter.entity.SubjectSettingEntity +import com.gh.gamecenter.livedata.Event import com.gh.gamecenter.retrofit.RetrofitManager -import com.halo.assistant.HaloApp -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.schedulers.Schedulers +import io.reactivex.disposables.CompositeDisposable -class CategoryV2ViewModel( - application: Application, - private val mCategoryId: String, - val categoryTitle: String -) : AndroidViewModel(application) { +class CategoryV2ViewModel : ViewModel() { + private val compositeDisposable = CompositeDisposable() private val api = RetrofitManager.getInstance().api - var sidebarsLiveData = MutableLiveData() - var directories = ArrayList() + var sidebarsLiveData = MutableLiveData() var directoriesLiveData = MutableLiveData>() - var selectedCount = 0 - var selectedCountLiveData = MutableLiveData() - var categoryPositionLiveData = MutableLiveData>() - var selectedCategoryName: String = "" - var selectedCategoryPosition: Int = 0 - var selectedCategoryList = ArrayList() var entrance: String = "" + private var pageId = "" + var pageName = "" + private set - init { - getSidebars() - getCategoryDirectories() + fun init(entrance: String) { + this.entrance = entrance } - @SuppressLint("CheckResult") - fun getSidebars() { - api.getSidebars(mCategoryId) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(object : BiResponse() { - override fun onSuccess(data: SidebarsEntity) { - sidebarsLiveData.postValue(data) + fun loadData(pageId: String, pageName: String) { + this.pageId = pageId + this.pageName = pageName + + val sidebarsObservable = api.getSidebars(pageId) + val directoriesObservable = api.getCategoryDirectories(pageId) + .onErrorReturnItem(listOf()) + sidebarsObservable.zipWith(directoriesObservable) { t1, t2 -> + t1 to t2 + }.compose(singleToMain()) + .subscribe(object : BiResponse>>() { + override fun onSuccess(data: Pair>) { + val (sidebarsData, directories) = data + directoriesLiveData.value = directories + sidebarsLiveData.value = sidebarsData } override fun onFailure(exception: Exception) { super.onFailure(exception) sidebarsLiveData.postValue(null) } - }) + + }).let(compositeDisposable::add) } - @SuppressLint("CheckResult") - fun getCategoryDirectories() { - api.getCategoryDirectories(mCategoryId) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(object : BiResponse>() { - override fun onSuccess(data: List) { - directories = ArrayList(data) - postCategoryDirectoryList() + fun clearSelectedTag() { + logClickReset("全部类别") + val filteredList = _selectedSubCategories.value + if (!filteredList.isNullOrEmpty()) { + _selectedSubCategories.value = mutableListOf() + + val directories = directoriesLiveData.value ?: return + directories.forEach { + it.data?.forEach { child -> + child.selected = false } - }) - } - - fun postSelectedCount() { - selectedCountLiveData.postValue(selectedCount) - } - - fun postCategoryPosition(primaryIndex: Int, subIndex: Int) { - categoryPositionLiveData.postValue(Pair(primaryIndex, subIndex)) - } - - fun postCategoryDirectoryList() { - directoriesLiveData.postValue(directories) - } - - fun resetDirectoryList() { - selectedCount = 0 - selectedCategoryList.clear() - directories.forEach { - it.data?.forEach { entity -> - entity.selected = false } + directoriesLiveData.value = directories } - postSelectedCount() - postCategoryDirectoryList() } fun logAppearance() { - LogUtils.logCategoryV2AppearanceEvent(entrance, categoryTitle) + LogUtils.logCategoryV2AppearanceEvent(entrance, pageName) } fun logClickSide() { - LogUtils.logCategoryV2ClickSideEvent(entrance, categoryTitle, selectedCategoryName, selectedCategoryPosition) + val sidebars = sidebarsLiveData.value?.sidebars + val selectedPosition = selectedSidebarsPosition.value ?: 0 + val selectedCategoryName = sidebars?.getOrNull(selectedPosition)?.name ?: "" + LogUtils.logCategoryV2ClickSideEvent(entrance, pageName, selectedCategoryName, selectedPosition) } - fun logClickClassification(primaryIndex: Int, categoryName: String, position: Int) { + private fun logClickClassification(selected: SelectedTags) { LogUtils.logCategoryV2ClickClassificationEvent( entrance, - categoryTitle, - selectedCategoryName, - directories[primaryIndex].name, - categoryName, - primaryIndex, - position + pageName, + selectedSidebarsName, + selected.parentName, + selected.category.name ?: "", + selected.parentPosition, + selected.position ) } - fun logClickClassificationDelete(primaryIndex: Int, categoryName: String, location: String) { + private fun logClickClassificationDelete(directoryName: String, categoryName: String, location: String) { LogUtils.logCategoryV2ClickClassificationDeleteEvent( entrance, - categoryTitle, - directories[primaryIndex].name, + pageName, + directoryName, categoryName, location ) } fun logClickDetermine() { - val categoryName = StringBuilder() - selectedCategoryList.forEachIndexed { index, s -> - categoryName.append(s.name) - if (index != selectedCategoryList.size - 1) { - categoryName.append("+") - } - } - LogUtils.logCategoryV2ClickDetermineEvent(entrance, categoryTitle, categoryName.toString()) + val categoryNames = selectedSubCategories.value?.joinToString("+") { it.category.name ?: "" } + LogUtils.logCategoryV2ClickDetermineEvent(entrance, pageName, categoryNames ?: "") } fun logClickReset(location: String) { - val categoryName = StringBuilder() - selectedCategoryList.forEachIndexed { index, s -> - categoryName.append(s.name) - if (index != selectedCategoryList.size - 1) { - categoryName.append("+") - } - } - LogUtils.logCategoryV2ClickResetEvent(entrance, categoryTitle, categoryName.toString(), location) + val categoryName = selectedSubCategories.value?.joinToString("+") { it.category.name ?: "" } + LogUtils.logCategoryV2ClickResetEvent(entrance, pageName, categoryName ?: "", location) } - class Factory( - private val categoryId: String, - private val categoryTitle: String - ) : ViewModelProvider.NewInstanceFactory() { - override fun create(modelClass: Class): T { - return CategoryV2ViewModel( - HaloApp.getInstance().application, - categoryId, - categoryTitle - ) as T + private val _notifySubCategorySelected = MutableLiveData>() + val notifySubCategorySelected: LiveData> = _notifySubCategorySelected + + private val _selectedSubCategories = MutableLiveData>() + val selectedSubCategories: LiveData> = _selectedSubCategories + fun addSubCategorySelected(selected: SelectedTags) { + val list = _selectedSubCategories.value + val newData = if (list == null) { + mutableListOf(selected) + } else { + list + selected + } + selected.category.selected = true + _selectedSubCategories.value = newData + + // 当搜索条件发生变化时,侧边栏默认选中 “全部” + selectSidebarsPosition(0, false) + + updateGameFiltered() + _notifySubCategorySelected.value = Event(selected.parentId) + logClickClassification(selected) + } + + fun removeSubCategorySelected(parentId: String, categoryId: String, location: String) { + val list = _selectedSubCategories.value ?: return + val position = list.indexOfFirst { + it.parentId == parentId && categoryId == it.category.id + } + if (position != -1) { + val item = list[position] + + item.category.selected = false + _selectedSubCategories.value = list - item + + updateGameFiltered() + _notifySubCategorySelected.value = Event(parentId) + + logClickClassificationDelete(item.parentName, item.category.name ?: "", location) + } + + } + + private val _selectedSidebarsPosition = MutableLiveData() + val selectedSidebarsPosition: LiveData = _selectedSidebarsPosition + val selectedSidebarsName: String + get() = sidebarsLiveData.value?.sidebars?.getOrNull(selectedSidebarsPosition.value ?: 0)?.name ?: "" + + fun selectSidebarsPosition(position: Int, triggerSearch: Boolean = true) { + val oldPosition = _selectedSidebarsPosition.value ?: INVALID_POSITION + if (position != oldPosition) { + _selectedSidebarsPosition.value = position + // 如果是点击搜索而被动切换到 “全部” tab,则这里不需要更新筛选条件 + if (triggerSearch && position != 1) { + updateGameFiltered() + } } } + + private val _gameFiltered = MutableLiveData() + val gameFiltered: LiveData = _gameFiltered + fun updateGameFiltered( + size: SubjectSettingEntity.Size? = null, + sortType: CategoryFilterView.SortType? = null + ) { + val oldFiltered = _gameFiltered.value + val newSize = size ?: oldFiltered?.size ?: SubjectSettingEntity.Size() + val newSortType = sortType ?: oldFiltered?.sortType ?: CategoryFilterView.SortType.RECOMMENDED + val selectedSidebarPosition = selectedSidebarsPosition.value ?: 0 + val categoryIds = selectedSubCategories.value?.joinToString("-") { it.category.id } ?: "" + val sidebarCategoryId = + sidebarsLiveData.value?.sidebars?.getOrNull(selectedSidebarPosition)?.categoryId ?: "all" + _gameFiltered.value = GameFiltered(newSize, newSortType, categoryIds, sidebarCategoryId) + } + + override fun onCleared() { + super.onCleared() + compositeDisposable.clear() + } + + companion object { + private const val INVALID_POSITION = -1 + } + + data class SelectedTags( + val parentId: String, + val parentName: String, + val parentPosition: Int, + val category: CategoryEntity, + val position: Int + ) + + data class GameFiltered( + val size: SubjectSettingEntity.Size = SubjectSettingEntity.Size(), + val sortType: CategoryFilterView.SortType = CategoryFilterView.SortType.RECOMMENDED, + val categoryIds: String = "", + val sidebarCategoryId: String = "全部" + ) } \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/category2/SearchCategoryPop.kt b/app/src/main/java/com/gh/gamecenter/category2/SearchCategoryPop.kt new file mode 100644 index 0000000000..7383f26d51 --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/category2/SearchCategoryPop.kt @@ -0,0 +1,292 @@ +package com.gh.gamecenter.category2 + +import android.content.Context +import android.graphics.Color +import android.graphics.Typeface +import android.graphics.drawable.ColorDrawable +import android.os.Handler +import android.os.Looper +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.blankj.utilcode.util.KeyboardUtils +import com.gh.gamecenter.R +import com.gh.gamecenter.common.constant.Constants +import com.gh.gamecenter.common.retrofit.BiResponse +import com.gh.gamecenter.common.utils.SensorsBridge +import com.gh.gamecenter.common.utils.goneIf +import com.gh.gamecenter.common.utils.singleToMain +import com.gh.gamecenter.common.utils.toResString +import com.gh.gamecenter.common.view.BugFixedPopupWindow +import com.gh.gamecenter.core.utils.ToastUtils +import com.gh.gamecenter.databinding.PopSearchCategoryBinding +import com.gh.gamecenter.entity.CategoryEntity +import io.reactivex.Single +import io.reactivex.disposables.CompositeDisposable +import me.xdrop.fuzzywuzzy.FuzzySearch + +class SearchCategoryPop( + height: Int, + private val isAutoRequestFocus: Boolean, + private val pageId: String, + private val pageName: String, + private val binding: PopSearchCategoryBinding +) : BugFixedPopupWindow(binding.root, ViewGroup.LayoutParams.MATCH_PARENT, height) { + + private var listener: OnSearchCategoryListener? = null + private val handler = Handler(Looper.getMainLooper()) + private var searchDataList: List? = null + + private val compositeDisposable = CompositeDisposable() + + private var isSearching = false + + private val adapter by lazy { + CategoryDirectoryAdapter(object : OnSearchCategoryListener { + override fun isEnableSelected(): Boolean { + return listener?.isEnableSelected() ?: false + } + + override fun onItemSelected(selected: CategoryV2ViewModel.SelectedTags) { + listener?.onItemSelected(selected) + + } + + override fun onItemRemoved(parentId: String, subCategoryId: String) { + listener?.onItemRemoved(parentId, subCategoryId) + } + + override fun onResetSelected() { + listener?.onResetSelected() + } + + override fun onSubmit() { + listener?.onSubmit() + } + + }) + } + + private val resultAdapter by lazy { + SearchCategoryResultsAdapter { + SensorsBridge.logClassificationSearchReturnClick( + pageId, + pageName, + binding.searchView.searchKey, + it.category.name ?: "" + ) + if (listener?.isEnableSelected() == true) { + clearSearchKey() + listener?.onItemSelected(it) + } else { + ToastUtils.toast(R.string.selected_category_tags_max_toast.toResString()) + } + + } + } + + private val selectedTagAdapter by lazy { + SelectedTagsAdapter { + listener?.onItemRemoved(it.parentId, it.category.id) + } + } + + init { + isOutsideTouchable = true + setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + isFocusable = true + inputMethodMode = INPUT_METHOD_NEEDED + softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING + + initView() + + } + + private fun initView() { + binding.rvCategory.layoutManager = LinearLayoutManager(binding.root.context) + binding.rvCategory.adapter = adapter + + binding.tvSelectedNumber.typeface = + Typeface.createFromAsset(binding.root.context.assets, Constants.DIN_FONT_PATH) + + binding.root.setOnClickListener { + dismiss() + } + + binding.clContent.setOnClickListener { + // 不需要具体实现,只是为了拦截 root 的点击事件 + } + + binding.tvReset.setOnClickListener { + listener?.onResetSelected() + } + + binding.vSubmit.setOnClickListener { + dismiss() + listener?.onSubmit() + } + + binding.searchView.addTextChangedListener { + handler.removeCallbacksAndMessages(null) + val key = it?.toString() ?: "" + if (key.isEmpty()) { + changeToSearching(false) + } else { + handler.postDelayed({ + search(it?.toString() ?: "") + }, SEARCH_DELAY_DURATION) + } + } + + if (!isAutoRequestFocus) { + binding.searchView.setEditTextOnFocusChangeListener { _, hasFocus -> + if (hasFocus) { + SensorsBridge.logClassificationSearch(pageId, pageName) + } + } + } + + } + + fun setData(data: List) { + searchDataList = data + .asSequence() + .mapIndexed { index, parent -> + parent.data?.mapIndexed { childIndex, child -> + CategoryV2ViewModel.SelectedTags(parent.id, parent.name ?: "", index, child, childIndex) + } ?: listOf() + } + .flatten() + .toList() + + adapter.setListData(data) + } + + fun updateCategorySelected(selectedList: List?) { + val size = selectedList?.size ?: 0 + binding.tvSelectedNumber.goneIf(size == 0) { + binding.tvSelectedNumber.text = "$size" + } + + if (binding.rvSelected.adapter == null) { + binding.rvSelected.layoutManager = + LinearLayoutManager(binding.root.context, RecyclerView.HORIZONTAL, false) + binding.rvSelected.adapter = selectedTagAdapter + } + selectedTagAdapter.submitList(selectedList) + binding.rvSelected.goneIf(selectedList.isNullOrEmpty()) + + if (isSearching) { + search(binding.searchView.searchKey) + } + } + + private fun search(key: String) { + Single.create { + val data = searchDataList?.filterNot { item -> item.category.selected } ?: emptyList() + val resultList = FuzzySearch.extractSorted(key, data, { item -> item.category.name ?: "" }, 1) + .map { item -> item.referent } + it.onSuccess(resultList) + }.compose(singleToMain()) + .subscribe(object : BiResponse>() { + override fun onSuccess(data: List) { + val hasResult = data.isNotEmpty() + SensorsBridge.logClassificationSearchReturn(pageId, pageName, key, hasResult) + changeToSearching(true, key, data) + } + + override fun onFailure(exception: Exception) { + SensorsBridge.logClassificationSearchReturn(pageId, pageName, key, false) + changeToSearching(true, key) + } + }).let(compositeDisposable::add) + } + + private fun clearSearchKey() { + binding.searchView.clear() + } + + private fun changeToSearching( + isSearching: Boolean, + key: String = "", + results: List? = null + ) { + this.isSearching = isSearching + binding.rvCategory.goneIf(isSearching) + binding.rvResults.goneIf(results.isNullOrEmpty()) { + if (binding.rvResults.adapter == null) { + binding.rvResults.layoutManager = LinearLayoutManager(binding.rvResults.context) + binding.rvResults.adapter = resultAdapter + } + } + resultAdapter.setData(results ?: listOf(), key) + binding.reuseNoConnection.root.goneIf(!isSearching || !results.isNullOrEmpty()) { + binding.reuseNoConnection.reuseNoneDataTv.text = R.string.no_relevant_content_found.toResString() + binding.reuseNoConnection.reuseNoneDataDescTv.goneIf(false) + binding.reuseNoConnection.reuseNoneDataDescTv.text = R.string.try_a_different_search_term.toResString() + } + } + + fun setOnSearchCategoryListener(listener: OnSearchCategoryListener) { + this.listener = listener + } + + fun notifyItemSelectedChanged(parentId: String) { + adapter.notifyItemSelectedChanged(parentId) + } + + override fun showAtLocation(parent: View?, gravity: Int, x: Int, y: Int) { + super.showAtLocation(parent, gravity, x, y) + if (isAutoRequestFocus) { + binding.searchView.requestFocus() + handler.removeCallbacksAndMessages(null) + // 在某些机型上延时一下才能弹起软键盘 + handler.postDelayed({ + KeyboardUtils.showSoftInput() + }, SHOW_SOFT_INPUT_DELAY) + } + } + + override fun dismiss() { + super.dismiss() + clearSearchKey() + handler.removeCallbacksAndMessages(null) + compositeDisposable.clear() + } + + + companion object { + + private const val SEARCH_DELAY_DURATION = 300L + private const val SHOW_SOFT_INPUT_DELAY = 200L + + fun newInstance( + context: Context, + height: Int, + isAutoRequestFocus: Boolean, + pageId: String, + pageName: String + ): SearchCategoryPop { + val inflater = LayoutInflater.from(context) + val binding = PopSearchCategoryBinding.inflate(inflater) + return SearchCategoryPop(height, isAutoRequestFocus, pageId, pageName, binding) + } + } + + interface OnSearchCategoryListener { + + fun isEnableSelected(): Boolean + + fun onItemSelected(selected: CategoryV2ViewModel.SelectedTags) + + fun onItemRemoved(parentId: String, subCategoryId: String) + + fun onResetSelected() + + fun onSubmit() + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/category2/SearchCategoryResultsAdapter.kt b/app/src/main/java/com/gh/gamecenter/category2/SearchCategoryResultsAdapter.kt new file mode 100644 index 0000000000..e9253b7afb --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/category2/SearchCategoryResultsAdapter.kt @@ -0,0 +1,57 @@ +package com.gh.gamecenter.category2 + +import android.annotation.SuppressLint +import android.text.Spannable +import android.text.SpannableString +import android.text.style.ForegroundColorSpan +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.gh.gamecenter.common.utils.toBinding +import com.gh.gamecenter.common.utils.toColor +import com.gh.gamecenter.databinding.RecyclerSearchCategoryResultBinding + +class SearchCategoryResultsAdapter(val clickListener: (CategoryV2ViewModel.SelectedTags) -> Unit) : + RecyclerView.Adapter() { + private val dataList = arrayListOf() + private var key = "" + + @SuppressLint("NotifyDataSetChanged") + fun setData(data: List, newKey: String) { + key = newKey + dataList.clear() + dataList.addAll(data) + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ResultViewHolder { + return ResultViewHolder(parent.toBinding()) + } + + override fun getItemCount() = dataList.size + + override fun onBindViewHolder(holder: ResultViewHolder, position: Int) { + val item = dataList[position] + val text = item.category.name ?: "" + val spannableString = SpannableString(text) + val highlightColor = com.gh.gamecenter.common.R.color.text_theme.toColor(holder.itemView.context) + text.forEachIndexed { index, char -> + if (key.contains(char)) { + // 需要高亮 + spannableString.setSpan( + ForegroundColorSpan(highlightColor), + index, + index + 1, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + } + holder.binding.root.text = spannableString + holder.itemView.setOnClickListener { + clickListener(item) + } + } + + class ResultViewHolder(val binding: RecyclerSearchCategoryResultBinding) : RecyclerView.ViewHolder(binding.root) { + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/category2/SelectedTagsAdapter.kt b/app/src/main/java/com/gh/gamecenter/category2/SelectedTagsAdapter.kt new file mode 100644 index 0000000000..28abaad805 --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/category2/SelectedTagsAdapter.kt @@ -0,0 +1,48 @@ +package com.gh.gamecenter.category2 + +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.gh.gamecenter.common.utils.toBinding +import com.gh.gamecenter.databinding.RecyclerCategorySelectedTagBinding + +class SelectedTagsAdapter(val click: (CategoryV2ViewModel.SelectedTags) -> Unit) : + ListAdapter( + createDiffUtil() + ) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TagsViewHolder { + return TagsViewHolder(parent.toBinding()) + } + + override fun onBindViewHolder(holder: TagsViewHolder, position: Int) { + val item = getItem(position) + holder.binding.root.setText(item.category.name ?: "") + holder.itemView.setOnClickListener { + click(item) + } + } + + companion object { + + private fun createDiffUtil() = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: CategoryV2ViewModel.SelectedTags, + newItem: CategoryV2ViewModel.SelectedTags + ): Boolean { + return oldItem.category.id == newItem.category.id + } + + override fun areContentsTheSame( + oldItem: CategoryV2ViewModel.SelectedTags, + newItem: CategoryV2ViewModel.SelectedTags + ): Boolean { + return oldItem == newItem + } + + } + } + + class TagsViewHolder(val binding: RecyclerCategorySelectedTagBinding) : RecyclerView.ViewHolder(binding.root) +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/category2/SubCategoryAdapter.kt b/app/src/main/java/com/gh/gamecenter/category2/SubCategoryAdapter.kt index 848f7bb6c7..2cafb599ad 100644 --- a/app/src/main/java/com/gh/gamecenter/category2/SubCategoryAdapter.kt +++ b/app/src/main/java/com/gh/gamecenter/category2/SubCategoryAdapter.kt @@ -1,84 +1,94 @@ package com.gh.gamecenter.category2 -import android.content.Context -import android.view.View +import android.annotation.SuppressLint import android.view.ViewGroup -import com.gh.gamecenter.common.base.BaseRecyclerViewHolder -import com.gh.gamecenter.core.utils.ToastUtils -import com.gh.gamecenter.common.utils.goneIf -import com.gh.gamecenter.common.utils.toColor +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ViewHolder import com.gh.gamecenter.R -import com.gh.gamecenter.common.utils.toDrawable +import com.gh.gamecenter.common.utils.* +import com.gh.gamecenter.core.utils.ToastUtils import com.gh.gamecenter.databinding.SubCategoryItemBinding import com.gh.gamecenter.entity.CategoryEntity -import com.lightgame.adapter.BaseRecyclerAdapter class SubCategoryAdapter( - context: Context, - private val mViewModel: CategoryV2ViewModel, - private val mList: List, - private val mPrimaryIndex: Int -) : BaseRecyclerAdapter(context) { + private val listener: SearchCategoryPop.OnSearchCategoryListener, +) : RecyclerView.Adapter() { - override fun getItemCount() = mList.size + private lateinit var itemData: CategoryEntity + private val data: List + get() = itemData.data ?: emptyList() + + private var directoryPosition = 0 + + @SuppressLint("NotifyDataSetChanged") + fun setData(directoryPosition: Int, newItem: CategoryEntity) { + this.directoryPosition = directoryPosition + itemData = newItem + notifyDataSetChanged() + } + + override fun getItemCount() = data.size override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = - SubCategoryItemViewHolder(SubCategoryItemBinding.inflate(mLayoutInflater)) + SubCategoryItemViewHolder(parent.toBinding()) + + override fun onBindViewHolder(holder: SubCategoryItemViewHolder, position: Int, payloads: MutableList) { + if (payloads.isEmpty()) { + super.onBindViewHolder(holder, position, payloads) + } else { + val item = data[position] + updateSelectedState(item.selected, holder.binding) + } + } override fun onBindViewHolder(holder: SubCategoryItemViewHolder, position: Int) { holder.binding.run { - val categoryEntity = mList[position] - name.text = categoryEntity.name + val categoryEntity = data[position] + tvName.text = categoryEntity.name recommendIv.goneIf(categoryEntity.recommend == false) - if (categoryEntity.selected) { - selectedIv.visibility = View.VISIBLE - container.background = R.drawable.bg_category_selected.toDrawable(mContext) - name.setTextColor(com.gh.gamecenter.common.R.color.text_theme.toColor(mContext)) - } else { - selectedIv.visibility = View.GONE - container.background = com.gh.gamecenter.common.R.drawable.bg_shape_space_radius_8.toDrawable(mContext) - name.setTextColor(com.gh.gamecenter.common.R.color.text_primary.toColor(mContext)) - } + updateSelectedState(categoryEntity.selected, this) root.setOnClickListener { when { - mViewModel.selectedCount >= 5 && !categoryEntity.selected -> ToastUtils.toast("最多只能选择5个类别") + !categoryEntity.selected && !listener.isEnableSelected() -> { + ToastUtils.toast(R.string.selected_category_tags_max_toast.toResString()) + } categoryEntity.selected -> { - categoryEntity.selected = false - selectedIv.visibility = View.GONE - container.background = com.gh.gamecenter.common.R.drawable.bg_shape_space_radius_8.toDrawable(mContext) - name.setTextColor(com.gh.gamecenter.common.R.color.text_primary.toColor(mContext)) - mViewModel.run { - if (selectedCount > 0) { - selectedCount-- - postSelectedCount() - postCategoryPosition(mPrimaryIndex, position) - logClickClassificationDelete(mPrimaryIndex, categoryEntity.name ?: "", "全部类别") - } - } + + listener.onItemRemoved(itemData.id, categoryEntity.id) } !categoryEntity.selected -> { - categoryEntity.selected = true - categoryEntity.primaryIndex = mPrimaryIndex - selectedIv.visibility = View.VISIBLE - container.background = R.drawable.bg_category_selected.toDrawable(mContext) - name.setTextColor(com.gh.gamecenter.common.R.color.text_theme.toColor(mContext)) - mViewModel.run { - if (selectedCount < 5) { - selectedCount++ - logClickClassification(mPrimaryIndex, categoryEntity.name ?: "", position) - postSelectedCount() - postCategoryPosition(mPrimaryIndex, position) - } - } + + listener.onItemSelected( + CategoryV2ViewModel.SelectedTags( + itemData.id, + itemData.name ?: "", + directoryPosition, + categoryEntity, + position + ) + ) } } } } } - class SubCategoryItemViewHolder(val binding: SubCategoryItemBinding) : BaseRecyclerViewHolder(binding.root) + private fun updateSelectedState(isSelected: Boolean, binding: SubCategoryItemBinding) { + with(binding) { + val context = root.context + if (isSelected) { + container.background = R.drawable.bg_category_selected.toDrawable(context) + tvName.setTextColor(com.gh.gamecenter.common.R.color.text_theme.toColor(context)) + } else { + container.background = com.gh.gamecenter.common.R.drawable.bg_shape_space_radius_8.toDrawable(context) + tvName.setTextColor(com.gh.gamecenter.common.R.color.text_secondary.toColor(context)) + } + } + } + + class SubCategoryItemViewHolder(val binding: SubCategoryItemBinding) : ViewHolder(binding.root) } \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/entity/CategoryEntity.kt b/app/src/main/java/com/gh/gamecenter/entity/CategoryEntity.kt index 0d0079753d..dcb371cb92 100644 --- a/app/src/main/java/com/gh/gamecenter/entity/CategoryEntity.kt +++ b/app/src/main/java/com/gh/gamecenter/entity/CategoryEntity.kt @@ -2,16 +2,22 @@ package com.gh.gamecenter.entity import android.os.Parcelable import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize @Parcelize data class CategoryEntity( @SerializedName("_id") - var id: String? = "", + private var _id: String? = null, var icon: String? = "", var name: String? = "", var recommend: Boolean? = false, - var data: List? = null, - var selected: Boolean = false, - var primaryIndex: Int = -1 -) : Parcelable \ No newline at end of file + var data: List? = null +) : Parcelable { + + val id: String + get() = _id ?: "" + + @IgnoredOnParcel + var selected: Boolean = false +} \ No newline at end of file diff --git a/app/src/main/res/drawable-xxhdpi/ic_catalog_selected.webp b/app/src/main/res/drawable-xxhdpi/ic_catalog_selected.webp deleted file mode 100644 index b2cab258b8..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_catalog_selected.webp and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_category_recommend.webp b/app/src/main/res/drawable-xxhdpi/ic_category_recommend.webp index 2ffdbb5408..5071a540bf 100644 Binary files a/app/src/main/res/drawable-xxhdpi/ic_category_recommend.webp and b/app/src/main/res/drawable-xxhdpi/ic_category_recommend.webp differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_category_selected.webp b/app/src/main/res/drawable-xxhdpi/ic_category_selected.webp deleted file mode 100644 index 7146e44db5..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_category_selected.webp and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_catalog_selected.webp b/app/src/main/res/drawable-xxxhdpi/ic_catalog_selected.webp deleted file mode 100644 index 88de08ec0c..0000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_catalog_selected.webp and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/pic_category_guide.webp b/app/src/main/res/drawable-xxxhdpi/pic_category_guide.webp index b229d64c55..1b6e315650 100644 Binary files a/app/src/main/res/drawable-xxxhdpi/pic_category_guide.webp and b/app/src/main/res/drawable-xxxhdpi/pic_category_guide.webp differ diff --git a/app/src/main/res/drawable/bg_category_selected.xml b/app/src/main/res/drawable/bg_category_selected.xml index aea44445d0..8e6d8b555c 100644 --- a/app/src/main/res/drawable/bg_category_selected.xml +++ b/app/src/main/res/drawable/bg_category_selected.xml @@ -2,5 +2,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_category_selected_tag.xml b/app/src/main/res/drawable/bg_category_selected_tag.xml new file mode 100644 index 0000000000..c2dccd3609 --- /dev/null +++ b/app/src/main/res/drawable/bg_category_selected_tag.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_category_sidebar.xml b/app/src/main/res/drawable/bg_category_sidebar.xml new file mode 100644 index 0000000000..af207118d5 --- /dev/null +++ b/app/src/main/res/drawable/bg_category_sidebar.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_more_category_default.xml b/app/src/main/res/drawable/bg_more_category_default.xml new file mode 100644 index 0000000000..cb851b8882 --- /dev/null +++ b/app/src/main/res/drawable/bg_more_category_default.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_more_category_filtered.xml b/app/src/main/res/drawable/bg_more_category_filtered.xml new file mode 100644 index 0000000000..973114a8c5 --- /dev/null +++ b/app/src/main/res/drawable/bg_more_category_filtered.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_search_category_number.xml b/app/src/main/res/drawable/bg_search_category_number.xml new file mode 100644 index 0000000000..739cb3424c --- /dev/null +++ b/app/src/main/res/drawable/bg_search_category_number.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_arrow_down.xml b/app/src/main/res/drawable/ic_arrow_down.xml new file mode 100644 index 0000000000..0597014721 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_down.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_up.xml b/app/src/main/res/drawable/ic_arrow_up.xml new file mode 100644 index 0000000000..a6cb520feb --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_up.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/ic_basic_checkmark_circle_fill.xml b/app/src/main/res/drawable/ic_basic_checkmark_circle_fill.xml new file mode 100644 index 0000000000..fc2e0bf1c0 --- /dev/null +++ b/app/src/main/res/drawable/ic_basic_checkmark_circle_fill.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_basic_classification.xml b/app/src/main/res/drawable/ic_basic_classification.xml new file mode 100644 index 0000000000..09da3cf23c --- /dev/null +++ b/app/src/main/res/drawable/ic_basic_classification.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_clear.xml b/app/src/main/res/drawable/ic_clear.xml new file mode 100644 index 0000000000..2428f4db5a --- /dev/null +++ b/app/src/main/res/drawable/ic_clear.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_x_size_8.xml b/app/src/main/res/drawable/ic_x_size_8.xml new file mode 100644 index 0000000000..504e77ca0f --- /dev/null +++ b/app/src/main/res/drawable/ic_x_size_8.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/app/src/main/res/drawable/shape_category_filter_size_item_normal.xml b/app/src/main/res/drawable/shape_category_filter_size_item_normal.xml new file mode 100644 index 0000000000..c55b59e865 --- /dev/null +++ b/app/src/main/res/drawable/shape_category_filter_size_item_normal.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_category_filter_size_item_selected.xml b/app/src/main/res/drawable/shape_category_filter_size_item_selected.xml new file mode 100644 index 0000000000..ac7cff2052 --- /dev/null +++ b/app/src/main/res/drawable/shape_category_filter_size_item_selected.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_filtered_left.xml b/app/src/main/res/drawable/shape_filtered_left.xml new file mode 100644 index 0000000000..1cc6c51d4b --- /dev/null +++ b/app/src/main/res/drawable/shape_filtered_left.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_filtered_right.xml b/app/src/main/res/drawable/shape_filtered_right.xml new file mode 100644 index 0000000000..cfdde3ec28 --- /dev/null +++ b/app/src/main/res/drawable/shape_filtered_right.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/category_directory_item.xml b/app/src/main/res/layout/category_directory_item.xml index fdd6a6be2f..6b17af8fa2 100644 --- a/app/src/main/res/layout/category_directory_item.xml +++ b/app/src/main/res/layout/category_directory_item.xml @@ -2,20 +2,19 @@ + android:paddingBottom="24dp"> - - - + diff --git a/app/src/main/res/layout/fragment_category.xml b/app/src/main/res/layout/fragment_category.xml index 00d69e3712..8606f7cce5 100644 --- a/app/src/main/res/layout/fragment_category.xml +++ b/app/src/main/res/layout/fragment_category.xml @@ -1,18 +1,93 @@ - - + - + + + + + + + + + + + - + android:layout_marginTop="8dp" + app:topRightRadius="8dp"> + + + + + + - - - - - - - - - - - - - - \ No newline at end of file + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_category_list.xml b/app/src/main/res/layout/fragment_category_list.xml index acf576c614..339650cf84 100644 --- a/app/src/main/res/layout/fragment_category_list.xml +++ b/app/src/main/res/layout/fragment_category_list.xml @@ -1,37 +1,53 @@ + + + + + + + + + - - + android:layout_height="36dp" + android:layout_below="@id/fl_tags_container" /> + android:layout_below="@+id/filterContainer" /> + android:layout_below="@+id/filterContainer"> - + android:layout_width="match_parent" + android:layout_height="36dp"> + android:id="@+id/ll_type_container" + android:layout_width="wrap_content" + android:layout_height="0dp" + android:gravity="center" + android:orientation="horizontal" + android:paddingHorizontal="12dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + android:id="@+id/tv_type" + style="@style/TextCaption1B" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="4dp" + android:gravity="center" + android:text="@string/popular" + android:textColor="@color/text_tertiary" /> + + + + android:id="@+id/ll_size_container" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_marginEnd="4dp" + android:gravity="center" + android:orientation="horizontal" + android:paddingHorizontal="12dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent"> + android:id="@+id/tv_size" + style="@style/TextCaption1B" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/size" + android:textColor="@color/text_tertiary" /> + + - - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_category_filter_size.xml b/app/src/main/res/layout/layout_category_filter_size.xml new file mode 100644 index 0000000000..b9df09289a --- /dev/null +++ b/app/src/main/res/layout/layout_category_filter_size.xml @@ -0,0 +1,9 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/pop_category_filter_size.xml b/app/src/main/res/layout/pop_category_filter_size.xml new file mode 100644 index 0000000000..b9196e3814 --- /dev/null +++ b/app/src/main/res/layout/pop_category_filter_size.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/pop_category_filter_type.xml b/app/src/main/res/layout/pop_category_filter_type.xml new file mode 100644 index 0000000000..597af45f55 --- /dev/null +++ b/app/src/main/res/layout/pop_category_filter_type.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/pop_search_category.xml b/app/src/main/res/layout/pop_search_category.xml new file mode 100644 index 0000000000..dbb3b538f5 --- /dev/null +++ b/app/src/main/res/layout/pop_search_category.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/recycler_category_selected_tag.xml b/app/src/main/res/layout/recycler_category_selected_tag.xml new file mode 100644 index 0000000000..e63a3536cf --- /dev/null +++ b/app/src/main/res/layout/recycler_category_selected_tag.xml @@ -0,0 +1,9 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/recycler_search_category_result.xml b/app/src/main/res/layout/recycler_search_category_result.xml new file mode 100644 index 0000000000..1c91ccf651 --- /dev/null +++ b/app/src/main/res/layout/recycler_search_category_result.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/sub_category_item.xml b/app/src/main/res/layout/sub_category_item.xml index 12edf950c8..a397ec9394 100644 --- a/app/src/main/res/layout/sub_category_item.xml +++ b/app/src/main/res/layout/sub_category_item.xml @@ -1,5 +1,6 @@ - + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="最多五个字" /> - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml index 039dc45dee..ee6176a280 100644 --- a/app/src/main/res/values-night/styles.xml +++ b/app/src/main/res/values-night/styles.xml @@ -180,25 +180,6 @@ horizontal - - - - - - - -