Compare commits

...

11 Commits

Author SHA1 Message Date
c944e529ba Merge branch 'feat/GHZSCY-8177' into 'dev'
feat: 极光认证SDK升级-客户端 https://jira.shanqu.cc/browse/GHZSCY-8176

See merge request halo/android/assistant-android!2220
2025-06-30 14:57:52 +08:00
eaa1a3609f feat: 极光认证SDK升级-客户端 https://jira.shanqu.cc/browse/GHZSCY-8176 2025-06-30 14:57:18 +08:00
2b9478a1b9 Merge branch 'feat/GHZSCY-8124' into 'dev'
feat: 新游开测相关功能优化(第五期)—客户端 https://jira.shanqu.cc/browse/GHZSCY-8124

See merge request halo/android/assistant-android!2218
2025-06-23 10:37:28 +08:00
dbd6e7bb43 feat: 新游开测相关功能优化(第五期)—客户端 https://jira.shanqu.cc/browse/GHZSCY-8124 2025-06-23 10:37:28 +08:00
09a823d4da Merge branch 'feat/GHZSCY-8150' into 'dev'
feat: GameDetailPageTabSelect埋点事件优化-客户端 https://jira.shanqu.cc/browse/GHZSCY-8150

See merge request halo/android/assistant-android!2217
2025-06-20 16:11:16 +08:00
0b49c58884 feat: GameDetailPageTabSelect埋点事件优化-客户端 https://jira.shanqu.cc/browse/GHZSCY-8150 2025-06-20 16:11:16 +08:00
2e93bcc173 Merge branch 'feat/GHZSCY-8041-pr' into 'dev'
feat:游戏搜索结果列表的插入优化—客户端 https://jira.shanqu.cc/browse/GHZSCY-8041

See merge request halo/android/assistant-android!2216
2025-06-20 14:49:04 +08:00
03f6906b46 feat:游戏搜索结果列表的插入优化—客户端 https://jira.shanqu.cc/browse/GHZSCY-8041 2025-06-20 14:49:04 +08:00
0f382baec1 Merge branch 'feat/GHZSCY-8049' into 'dev'
feat: 游戏详情UI优化-客户端 https://jira.shanqu.cc/browse/GHZSCY-8049

See merge request halo/android/assistant-android!2207
2025-05-27 14:39:06 +08:00
62f80bbfdd feat: 游戏详情UI优化-客户端 https://jira.shanqu.cc/browse/GHZSCY-8049 2025-05-27 14:39:06 +08:00
63be9ac221 chore: 版本更新至 5.43.0 2025-05-27 14:04:44 +08:00
30 changed files with 894 additions and 851 deletions

View File

@ -9,7 +9,7 @@ import io.reactivex.Single
class GameSubjectDSPRemoteDataSource(private val api: DspApiService = RetrofitManager.getInstance().dspApiService) {
fun getDspGames(type: String, count: Int): Single<List<GameEntity>> {
fun getDspGames(count: Int): Single<List<GameEntity>> {
val meta = MetaUtil.getMeta()
val request = mapOf(
"device" to mapOf(

View File

@ -0,0 +1,26 @@
package com.gh.gamecenter.entity
import com.gh.gamecenter.servers.gametest2.GameServerTestV2ViewModel
import com.google.gson.annotations.SerializedName
data class GameServerTestDisplaySetting(
@SerializedName("time_text_past")
val timeTextPast: String = RECENT,
@SerializedName("time_text_present")
val timeTextPresent: String = TODAY,
@SerializedName("time_text_future")
val timeTextFuture: String = FUTURE,
@SerializedName("game_category")
val gameCategory: List<String> = listOf(
GameServerTestV2ViewModel.GameCategory.Local.value,
GameServerTestV2ViewModel.GameCategory.Online.value,
GameServerTestV2ViewModel.GameCategory.Welfare.value,
GameServerTestV2ViewModel.GameCategory.Gjonline.value
),
) {
companion object {
const val RECENT = "近期"
const val TODAY = "今天"
const val FUTURE = "预约"
}
}

View File

@ -0,0 +1,33 @@
package com.gh.gamecenter.entity
import com.gh.gamecenter.feature.entity.GameEntity
import com.google.gson.annotations.SerializedName
data class SearchGameUnionEntity(
@SerializedName("type")
private val _type: String? = null,
@SerializedName("link_game")
val linkGame: GameEntity? = null,
@SerializedName("link_wechat_game_cpm_column")
val linkWechatGameCpmColumn: SearchSubjectEntity? = null,
@SerializedName("link_dsp_game_column")
val linkDspGameColumn: SearchSubjectEntity? = null,
@SerializedName("link_wechat_game")
val linkWechatGame: GameEntity? = null,
@SerializedName("link_column")
val linkColum: SearchSubjectEntity? = null,
@SerializedName("link_ad_space")
val linkAdSpace: AdConfig? = null
) {
val type: String
get() = _type ?: ""
companion object {
const val TYPE_GAME = "game"
const val TYPE_WECHAT_GAME_CPM_COLUMN = "wechat_game_cpm_column"
const val TYPE_DSP_GAME_COLUMN = "dsp_game_column"
const val TYPE_WECHAT_GAME = "wechat_game"
const val TYPE_COLUMN = "column"
const val TYPE_AD_SPACE = "ad_space"
}
}

View File

@ -2,8 +2,9 @@ package com.gh.gamecenter.entity
import android.os.Parcelable
import com.gh.common.filter.RegionSettingHelper
import com.gh.gamecenter.entity.SearchGameUnionEntity.Companion.TYPE_DSP_GAME_COLUMN
import com.gh.gamecenter.entity.SearchGameUnionEntity.Companion.TYPE_WECHAT_GAME_CPM_COLUMN
import com.gh.gamecenter.feature.entity.GameEntity
import com.gh.gamecenter.personalhome.home.UserHistoryViewModel
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@ -18,10 +19,6 @@ data class SearchSubjectEntity(
val codeId: String = "", // 广告CODE_ID(本地字段),不为空时为广告专题
@SerializedName("ad_icon_active")
val adIconActive: Boolean = false,
// 本地字段标记是否为微信小游戏CPM专题
var isWGameSubjectCPM: Boolean = false,
// 本地字段标记是否为DSP专题
var isDspSubject: Boolean = false,
val type: String = "",
@SerializedName("column_type")
@ -31,10 +28,11 @@ data class SearchSubjectEntity(
val size: Int = -1, // 专题游戏数量
) : Parcelable {
companion object {
const val TYPE_WECHAT_GAME_CPM_COLUMN = "wechat_game_cpm_column"
const val TYPE_DSP_GAME_COLUMN = "dsp_game_column"
}
val isWGameSubjectCPM: Boolean
get() = type == TYPE_WECHAT_GAME_CPM_COLUMN
val isDspSubject: Boolean
get() = type == TYPE_DSP_GAME_COLUMN
fun getFilterGame() = RegionSettingHelper.filterGame(games)
}

View File

@ -14,6 +14,7 @@ import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.menu.ActionMenuItemView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.doOnNextLayout
import androidx.core.view.isVisible
@ -96,6 +97,7 @@ import io.reactivex.disposables.Disposable
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import retrofit2.HttpException
import splitties.views.horizontalPadding
import java.util.*
class GameDetailWrapperFragment : BaseLazyFragment(), IScrollable {
@ -286,7 +288,7 @@ class GameDetailWrapperFragment : BaseLazyFragment(), IScrollable {
initSkeleton()
binding.reuseNoneData.reuseNoneDataTv.text = "页面不见了"
bodyBinding.tabIndicator.setIndicatorWidth(12)
bodyBinding.tabIndicator.setIndicatorWidth(16)
bodyBinding.viewPager.offscreenPageLimit = 4
binding.expandSpecialDownloadIv.enlargeTouchArea()
@ -357,8 +359,14 @@ class GameDetailWrapperFragment : BaseLazyFragment(), IScrollable {
}
backBtn.setOnClickListener { requireActivity().finish() }
moreMenuItem = actionMenuView.menu.findItem(R.id.menu_more)
downloadMenuItem = actionMenuView.menu.findItem(R.id.menu_download)
downloadMenuItem = actionMenuView.menu.findItem(R.id.menu_download)?.apply {
actionView?.updateLayoutParams<LayoutParams> { width = 40F.dip2px() }
}
downloadMenuItem?.isVisible = Config.isShow()
actionMenuView.findViewById<ActionMenuItemView>(R.id.menu_more)?.run {
updateLayoutParams<LayoutParams> { width = 40F.dip2px() }
horizontalPadding = 8F.dip2px()
}
}
downloadMenuIcon = downloadMenuItem?.actionView?.findViewById(R.id.menu_download_iv)
@ -807,7 +815,10 @@ class GameDetailWrapperFragment : BaseLazyFragment(), IScrollable {
downloadStatus = gameEntity?.downloadStatusChinese ?: "",
gameType = gameEntity?.categoryChinese ?: "",
position = position,
tabContent = tabEntity.name
tabContent = tabEntity.name,
linkType = tabEntity.link?.type ?: "",
linkId = tabEntity.link?.link ?: "",
linkText = tabEntity.link?.text ?: ""
)
val entrance = if (mEntrance.contains("论坛详情")) "论坛" else "游戏"
@ -910,7 +921,7 @@ class GameDetailWrapperFragment : BaseLazyFragment(), IScrollable {
tab.customView = tabItemBinding.root
updateTabStyle(tab, i == bodyBinding.viewPager.currentItem)
tab.view.clipChildren = false
tab.view.setPadding(0, 0, 0, 0)
tab.view.setPadding(0, 0, if (i == tabEntityList.size - 1) 8F.dip2px() else 0, 0)
tab.view.setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_DOWN) {
handleTabTouchEvent(tabEntity?.name ?: "")
@ -937,7 +948,7 @@ class GameDetailWrapperFragment : BaseLazyFragment(), IScrollable {
tab.customView?.findViewById<TextView>(R.id.tab_title)
?.setTypeface(if (isChecked) Typeface.DEFAULT_BOLD else Typeface.DEFAULT)
tab.customView?.findViewById<TextView>(R.id.tab_title)?.setTextColor(
if (isChecked) com.gh.gamecenter.common.R.color.text_primary.toColor(requireContext()) else com.gh.gamecenter.common.R.color.text_tertiary.toColor(
if (isChecked) com.gh.gamecenter.common.R.color.text_primary.toColor(requireContext()) else com.gh.gamecenter.common.R.color.text_secondary.toColor(
requireContext()
)
)

View File

@ -384,7 +384,7 @@ class CustomPageRepository private constructor(
pageInfo.componentPosition
)
)
dspSubjectUseCase.getDspGames(subject.columnType, subject.size.index)
dspSubjectUseCase.getDspGames(subject.size.index)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : BiResponse<List<GameEntity>>() {

View File

@ -4,7 +4,7 @@ import com.gh.gamecenter.SearchActivity.Companion.TRACK_SEARCH_TYPE_INPUT
import com.gh.gamecenter.common.constant.EntranceConsts
import com.gh.gamecenter.common.utils.viewModelProvider
import com.gh.gamecenter.search.SearchGameResultFragment
import com.gh.gamecenter.search.SearchGameResultViewModel
import com.gh.gamecenter.search.viewmodel.SearchGameResultViewModel
import com.halo.assistant.HaloApp
/**
@ -19,7 +19,6 @@ class MiniGameSearchResultFragment : SearchGameResultFragment() {
val factory = SearchGameResultViewModel.Factory(
HaloApp.getInstance().application,
mKey,
true,
MiniGameSearchResultRepository(),
TRACK_SEARCH_TYPE_INPUT,
activity?.intent?.getStringExtra(EntranceConsts.KEY_SOURCE_ENTRANCE) ?: ""

View File

@ -1,45 +1,25 @@
package com.gh.gamecenter.minigame
import com.gh.gamecenter.dsp.data.GameSubjectDSPRemoteDataSource
import com.gh.gamecenter.entity.SearchSubjectEntity
import com.gh.gamecenter.feature.entity.GameEntity
import com.gh.gamecenter.minigame.wechat.WGameSubjectCPMRemoteDataSource
import com.gh.gamecenter.retrofit.RetrofitManager
import com.gh.gamecenter.retrofit.service.ApiService
import com.gh.gamecenter.search.ISearchGameResultRepository
import com.gh.gamecenter.search.SearchItemData
import io.reactivex.Observable
import io.reactivex.Single
class MiniGameSearchResultRepository(
private val api: ApiService = RetrofitManager.getInstance().newApi,
private val mWGameSubjectCPMDataSource: WGameSubjectCPMRemoteDataSource = WGameSubjectCPMRemoteDataSource(),
private val mGameSubjectDSPDataSource: GameSubjectDSPRemoteDataSource = GameSubjectDSPRemoteDataSource(RetrofitManager.getInstance().dspApiService)
private val api: ApiService = RetrofitManager.getInstance().newApi
) : ISearchGameResultRepository {
override fun getSearchGame(
key: String?,
page: Int
): Observable<List<GameEntity>> {
): Observable<List<SearchItemData>> {
return api.getSearchMiniGameList(key, page, 20)
.map { data ->
data.map {
SearchItemData(it, type = "")
}
}
}
override fun getSearchMiniGameCPM(key: String?): Observable<List<GameEntity>> {
return Observable.just(emptyList())
}
override fun getSearchSubject(key: String?, page: Int): Observable<List<SearchSubjectEntity>> {
return Observable.just(emptyList())
}
override fun getWGameCPMGameList(size: Int): Single<MutableList<GameEntity>> {
return mWGameSubjectCPMDataSource.getUserRecommendCPMList(pageSize = size)
}
override fun getDspGameList(
columnType: String,
showDownload: Boolean,
size: Int,
): Single<List<GameEntity>> {
return mGameSubjectDSPDataSource.getDspGames(columnType, size)
}
}

View File

@ -50,6 +50,7 @@ import com.gh.gamecenter.entity.GameColumnCollection;
import com.gh.gamecenter.entity.GameData;
import com.gh.gamecenter.entity.GameDigestEntity;
import com.gh.gamecenter.entity.GameGuidePopupEntity;
import com.gh.gamecenter.entity.GameServerTestDisplaySetting;
import com.gh.gamecenter.entity.GameServerTestTopGame;
import com.gh.gamecenter.entity.GameServerTestV2Entity;
import com.gh.gamecenter.entity.GameVideoInfo;
@ -77,6 +78,7 @@ import com.gh.gamecenter.entity.RatingReplyEntity;
import com.gh.gamecenter.entity.RecommendPopupEntity;
import com.gh.gamecenter.entity.ReserveModifyEntity;
import com.gh.gamecenter.entity.ReserveReminderEntity;
import com.gh.gamecenter.entity.SearchGameUnionEntity;
import com.gh.gamecenter.entity.SearchSubjectEntity;
import com.gh.gamecenter.entity.ServerPublishEntity;
import com.gh.gamecenter.entity.ServerSubscriptionEntity;
@ -326,6 +328,18 @@ public interface ApiService {
@GET
Observable<List<GameEntity>> getSearchGame(@Url String url);
/**
* 搜索游戏(新:整合了 cpm dsp 广告位置排序问题)
*/
@GET("./games:search?view=union")
Observable<List<SearchGameUnionEntity>> getSearchGameUnionData(
@Query("keyword") String keyword,
@Query("page") int page,
@Query("version") String version,
@Query("channel") String channel,
@Query("AD") String ad
);
/**
* 搜索CPM微信小游戏
*/
@ -3092,6 +3106,12 @@ public interface ApiService {
@GET("columns/tests/v2")
Observable<GameServerTestV2Entity> getServerTestV2(@Query("filter") String filter);
/**
* 新游开测-显示配置
*/
@GET("app/column_test_v2/{link_id}/display_setting")
Single<GameServerTestDisplaySetting> getGameServerTestDisplaySetting(@Path("link_id") String linkId);
/**
* 新游开测-详情列表
*/

View File

@ -1,18 +1,7 @@
package com.gh.gamecenter.search
import com.gh.gamecenter.entity.SearchSubjectEntity
import com.gh.gamecenter.feature.entity.GameEntity
import io.reactivex.Observable
import io.reactivex.Single
interface ISearchGameResultRepository {
fun getSearchGame(key: String?, page: Int): Observable<List<GameEntity>>
fun getSearchMiniGameCPM(key: String?): Observable<List<GameEntity>>
fun getSearchSubject(key: String?, page: Int): Observable<List<SearchSubjectEntity>>
fun getWGameCPMGameList(size: Int): Single<MutableList<GameEntity>>
fun getDspGameList(columnType: String, showDownload: Boolean, size: Int): Single<List<GameEntity>>
fun getSearchGame(key: String?, page: Int): Observable<List<SearchItemData>>
}

View File

@ -31,6 +31,7 @@ import com.gh.gamecenter.feature.entity.GameEntity
import com.gh.gamecenter.feature.entity.SettingsEntity
import com.gh.gamecenter.feature.exposure.addExposureHelper
import com.gh.gamecenter.help.HelpAndFeedbackBridge
import com.gh.gamecenter.search.viewmodel.SearchGameResultViewModel
import com.halo.assistant.HaloApp
import com.lightgame.download.DataWatcher
import com.lightgame.download.DownloadEntity
@ -67,7 +68,6 @@ class SearchGameIndexFragment : ListFragment<GameEntity, SearchGameResultViewMod
SearchGameResultViewModel.Factory(
HaloApp.getInstance(),
mKey,
false,
SearchGameResultRepository(),
mType,
activity?.intent?.getStringExtra(EntranceConsts.KEY_SOURCE_ENTRANCE) ?: ""
@ -198,7 +198,6 @@ class SearchGameIndexFragment : ListFragment<GameEntity, SearchGameResultViewMod
this.mType = type
mAdapter?.key = key
mListViewModel?.updateSearchKeyWithType(key, type)
mListViewModel?.clearSearchSubjects()
mListViewModel?.load(LoadType.REFRESH)
}

View File

@ -1,7 +1,6 @@
package com.gh.gamecenter.search
import android.content.Context
import android.util.SparseArray
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -60,6 +59,7 @@ import com.gh.gamecenter.gamedetail.accelerator.dialog.StartingAcceleratorDialog
import com.gh.gamecenter.gamedetail.entity.GameDetailTabEntity
import com.gh.gamecenter.help.HelpAndFeedbackBridge
import com.gh.gamecenter.minigame.MiniGameSearchResultFragment
import com.gh.gamecenter.search.viewmodel.SearchGameResultViewModel
import com.halo.assistant.accelerator.repository.AcceleratorDataHolder
import com.lightgame.download.DownloadEntity
import com.lightgame.utils.Util_System_Keyboard

View File

@ -13,7 +13,6 @@ import android.view.animation.TranslateAnimation
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.gh.common.exposure.DefaultExposureStateChangeListener
import com.gh.common.util.*
@ -41,10 +40,10 @@ import com.gh.gamecenter.eventbus.EBPackage
import com.gh.gamecenter.eventbus.EBStartupAcceleration
import com.gh.gamecenter.feature.entity.GameEntity
import com.gh.gamecenter.feature.exposure.addExposureHelper
import com.gh.gamecenter.feature.entity.VipEntity
import com.gh.gamecenter.help.HelpAndFeedbackBridge
import com.gh.gamecenter.livedata.EventObserver
import com.gh.gamecenter.minigame.MiniGameSearchResultFragment
import com.gh.gamecenter.search.viewmodel.SearchGameResultViewModel
import com.gh.gamecenter.search.viewmodel.SearchTabViewModel
import com.halo.assistant.HaloApp
import com.halo.assistant.accelerator.repository.AcceleratorDataHolder
@ -126,7 +125,6 @@ open class SearchGameResultFragment : ListFragment<GameEntity, SearchGameResultV
SearchGameResultViewModel.Factory(
HaloApp.getInstance(),
mKey,
true,
SearchGameResultRepository(),
mType,
activity?.intent?.getStringExtra(EntranceConsts.KEY_SOURCE_ENTRANCE) ?: "",
@ -444,7 +442,6 @@ open class SearchGameResultFragment : ListFragment<GameEntity, SearchGameResultV
this.mKey = key
mAdapter?.key = key
mAdapter?.clearAdIdSet()
mListViewModel?.clearSearchSubjects()
mListViewModel?.load(LoadType.REFRESH)
}

View File

@ -1,88 +1,218 @@
package com.gh.gamecenter.search
import com.gh.common.constant.Config
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.gh.ad.AdDelegateHelper
import com.gh.common.filter.RegionSettingHelper
import com.gh.common.util.AdHelper
import com.gh.gamecenter.BuildConfig
import com.gh.common.util.PackageUtils
import com.gh.gamecenter.common.retrofit.BiResponse
import com.gh.gamecenter.common.utils.safelyGetInRelease
import com.gh.gamecenter.common.utils.singleToMain
import com.gh.gamecenter.dsp.data.GameSubjectDSPRemoteDataSource
import com.gh.gamecenter.entity.AdConfig
import com.gh.gamecenter.entity.SearchGameUnionEntity.Companion.TYPE_AD_SPACE
import com.gh.gamecenter.entity.SearchGameUnionEntity.Companion.TYPE_COLUMN
import com.gh.gamecenter.entity.SearchGameUnionEntity.Companion.TYPE_DSP_GAME_COLUMN
import com.gh.gamecenter.entity.SearchGameUnionEntity.Companion.TYPE_GAME
import com.gh.gamecenter.entity.SearchGameUnionEntity.Companion.TYPE_WECHAT_GAME
import com.gh.gamecenter.entity.SearchGameUnionEntity.Companion.TYPE_WECHAT_GAME_CPM_COLUMN
import com.gh.gamecenter.entity.SearchSubjectEntity
import com.gh.gamecenter.feature.entity.GameEntity
import com.gh.gamecenter.gamedetail.accelerator.AccelerationDataBase
import com.gh.gamecenter.livedata.Event
import com.gh.gamecenter.minigame.wechat.WGameSubjectCPMRemoteDataSource
import com.gh.gamecenter.retrofit.RetrofitManager
import com.gh.gamecenter.retrofit.service.ApiService
import com.halo.assistant.HaloApp
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import java.net.URLEncoder
import java.util.concurrent.TimeUnit
import io.reactivex.disposables.CompositeDisposable
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import kotlin.random.Random
class SearchGameResultRepository(
private val mApi: ApiService = RetrofitManager.getInstance().api,
private val mNewApi: ApiService = RetrofitManager.getInstance().newApi,
private val mWGameSubjectCPMDataSource: WGameSubjectCPMRemoteDataSource = WGameSubjectCPMRemoteDataSource(),
private val mGameSubjectDSPDataSource: GameSubjectDSPRemoteDataSource = GameSubjectDSPRemoteDataSource(RetrofitManager.getInstance().dspApiService)
private val mGameSubjectDSPDataSource: GameSubjectDSPRemoteDataSource = GameSubjectDSPRemoteDataSource(
RetrofitManager.getInstance().dspApiService
)
) : ISearchGameResultRepository {
private var currentSearchKey: String? = null
private val compositeDisposable = CompositeDisposable()
private var currentMiniGameCPMSearchList: MutableList<GameEntity>? = null
private val _dataUpdateEvent = MutableLiveData<Event<SearchItemData>>()
val dataUpdateEvent: LiveData<Event<SearchItemData>> = _dataUpdateEvent
override fun getSearchGame(
key: String?,
page: Int
): Observable<List<GameEntity>> {
// 可能会有特殊字符,需要 encode 处理
val encodedKey = URLEncoder.encode(key, "utf-8")
return mApi.getSearchGame(
Config.API_HOST
+ "games:search?keyword=" + encodedKey
+ "&view=digest&page=" + page
+ "&channel=" + HaloApp.getInstance().channel
+ "&version=" + BuildConfig.VERSION_NAME
+ "&AD=" + AdHelper.getIdfaString()
)
.map { games ->
// 获取已加速成功过的区服
val gameCanSpeed = games.filter { it.canSpeed }
val gameIds = gameCanSpeed
.map { it.id }
val acctGameList = AccelerationDataBase.instance.accelerationDao().queryAcctGameInfoByGameId(gameIds)
gameCanSpeed.forEach { game ->
game.lastAcctGame = acctGameList.find { game.id == it.gameId }
}
games
}
}
private val adGameOneIdSet = Collections.newSetFromMap(ConcurrentHashMap<String, Boolean>())
override fun getSearchMiniGameCPM(key: String?): Observable<List<GameEntity>> {
val currentMiniGameCPMSearchList = currentMiniGameCPMSearchList
if (key == currentSearchKey && currentMiniGameCPMSearchList != null) {
return Observable.just(currentMiniGameCPMSearchList)
override fun getSearchGame(key: String?, page: Int): Observable<List<SearchItemData>> {
if (page == 1) {
adGameOneIdSet.clear()
}
return mNewApi.getSearchWechatMiniCPMGame(key)
.timeout(5, TimeUnit.SECONDS)
.onErrorReturnItem(emptyList())
.observeOn(AndroidSchedulers.mainThread())
.doOnNext {
this.currentSearchKey = key
this.currentMiniGameCPMSearchList = it
val version = PackageUtils.getGhVersionName()
val channel = HaloApp.getInstance().channel
return mApi.getSearchGameUnionData(key, page, version, channel, AdHelper.getIdfaString())
.map { data ->
data.map {
when (it.type) {
TYPE_GAME -> SearchItemData(it.linkGame, type = it.type)
TYPE_WECHAT_GAME_CPM_COLUMN -> {
it.linkWechatGameCpmColumn?.let(::loadThirdPartyData)
SearchItemData(type = it.type, placeHolderId = it.linkWechatGameCpmColumn?.columnId)
}
TYPE_DSP_GAME_COLUMN -> {
it.linkDspGameColumn?.let(::loadThirdPartyData)
SearchItemData(type = it.type, placeHolderId = it.linkDspGameColumn?.columnId)
}
TYPE_WECHAT_GAME -> SearchItemData(game = it.linkWechatGame, type = it.type)
TYPE_COLUMN -> SearchItemData(subject = it.linkColum, type = it.type)
TYPE_AD_SPACE -> {
val adConfig = it.linkAdSpace
if (adConfig != null) {
val shouldShowAd = AdDelegateHelper.shouldShowGameSearchAd(adConfig)
when {
shouldShowAd && showThirdPartyAd(adConfig) -> {
SearchItemData(
ad = adConfig.thirdPartyAd,
adConfig = it.linkAdSpace,
type = TYPE_AD_SPACE
)
}
shouldShowAd && showOwnerAd(adConfig) -> {
loadOwnerAdData(adConfig)
SearchItemData(type = it.type, placeHolderId = getAdPlaceHolderId(adConfig))
}
else -> SearchItemData(type = it.type, placeHolderId = INVALID_PLACEHOLDER_ID)
}
} else {
SearchItemData(type = it.type, placeHolderId = INVALID_PLACEHOLDER_ID)
}
}
else -> {
// 无效数据
SearchItemData(type = it.type, placeHolderId = INVALID_PLACEHOLDER_ID)
}
}
}
}
}
override fun getSearchSubject(key: String?, page: Int): Observable<List<SearchSubjectEntity>> {
return mApi.getSearchSubject(key, page)
private fun loadThirdPartyData(subject: SearchSubjectEntity) {
if (subject.isWGameSubjectCPM) {
mWGameSubjectCPMDataSource.getUserRecommendCPMList(pageSize = subject.size)
} else {
mGameSubjectDSPDataSource.getDspGames(subject.size)
}.compose(singleToMain())
.subscribe(object : BiResponse<List<GameEntity>>() {
override fun onSuccess(data: List<GameEntity>) {
subject.games = data
_dataUpdateEvent.value = Event(SearchItemData(type = subject.type, subject = subject))
}
}).let(compositeDisposable::add)
}
override fun getWGameCPMGameList(size: Int,): Single<MutableList<GameEntity>> {
return mWGameSubjectCPMDataSource.getUserRecommendCPMList(pageSize = size)
private fun loadOwnerAdData(adConfig: AdConfig?) {
adConfig ?: return
val pageSize = adConfig.ownerAd?.adSource?.gamesIds?.size ?: 20
val codeId = adConfig.ownerAd?.id
val paramsMap = mapOf("page" to "1,$pageSize", "code_id" to codeId)
mNewApi.getAdGames(adConfig.id, paramsMap)
.map { data ->
data.forEach {
it.run {
adIconActive = adConfig.ownerAd?.adSource?.adIconActive ?: false
adSpaceId = adConfig.id
gameAdSourceId = adConfig.ownerAd?.id ?: ""
}
}
data
}
.compose(singleToMain())
.subscribe(object : BiResponse<List<GameEntity>>() {
override fun onSuccess(data: List<GameEntity>) {
val filterGameList = RegionSettingHelper.filterGame(data)
handleOwnerAdGames(adConfig, filterGameList)
}
override fun onFailure(exception: Exception) {
super.onFailure(exception)
handleOwnerAdGames(adConfig, emptyList())
}
}).let(compositeDisposable::add)
}
override fun getDspGameList(
columnType: String,
showDownload: Boolean,
size: Int,
): Single<List<GameEntity>> {
return mGameSubjectDSPDataSource.getDspGames(columnType, size)
private fun handleOwnerAdGames(adConfig: AdConfig, games: List<GameEntity>) {
val showAdColumn = adConfig.ownerAd?.adSource?.displayStyle == "game_zone"
val showOnFailed = adConfig.displayRule.onFailedAction == "show"
when {
games.isNotEmpty() && showAdColumn -> {
SearchItemData(
subject = SearchSubjectEntity(
name = adConfig.ownerAd?.adSource?.title ?: "",
games = games.take(AD_SUBJECT_GAME_MAX_COUNT),
adId = adConfig.id,
codeId = adConfig.ownerAd?.id ?: "",
adIconActive = adConfig.ownerAd?.adSource?.adIconActive ?: false
),
adConfig = adConfig,
type = TYPE_AD_SPACE
)
}
games.isNotEmpty() -> {
var randomGame = games.safelyGetInRelease(Random.nextInt(games.size))
if (games.size > 1 && adGameOneIdSet.contains(randomGame?.id)) {
// 存在重复游戏时重新获取随机游戏
randomGame = games.safelyGetInRelease(Random.nextInt(games.size))
}
randomGame?.id?.let(adGameOneIdSet::add)
SearchItemData(game = randomGame, adConfig = adConfig, type = TYPE_AD_SPACE)
}
showOnFailed && adConfig.thirdPartyAd != null -> {
// 自有广告为空时,显示第三方广告
SearchItemData(ad = adConfig.thirdPartyAd, adConfig = adConfig, type = TYPE_AD_SPACE)
}
else -> null
}?.let {
_dataUpdateEvent.value = Event(it)
}
}
fun clear() {
compositeDisposable.clear()
}
companion object {
private const val AD_SUBJECT_GAME_MAX_COUNT = 8
private const val INVALID_PLACEHOLDER_ID = "Invalid"
fun getAdPlaceHolderId(adConfig: AdConfig?) = "${adConfig?.id}-${adConfig?.position}"
fun showThirdPartyAd(adConfig: AdConfig?) =
(adConfig?.displayRule?.adSource == AdDelegateHelper.AD_TYPE_SDK && adConfig.thirdPartyAd != null) ||
(adConfig?.displayRule?.adSource == AdDelegateHelper.AD_TYPE_OWNER &&
adConfig.ownerAd == null &&
adConfig.thirdPartyAd != null &&
adConfig.displayRule.onFailedAction == "show")
fun showOwnerAd(adConfig: AdConfig?) =
(adConfig?.displayRule?.adSource == AdDelegateHelper.AD_TYPE_OWNER &&
adConfig.ownerAd != null) ||
(adConfig?.displayRule?.adSource == AdDelegateHelper.AD_TYPE_SDK &&
adConfig.thirdPartyAd == null &&
adConfig.ownerAd != null &&
adConfig.displayRule.onFailedAction == "show")
}
}

View File

@ -1,469 +0,0 @@
package com.gh.gamecenter.search
import android.annotation.SuppressLint
import android.app.Application
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.gh.ad.AdDelegateHelper
import com.gh.common.filter.RegionSettingHelper
import com.gh.common.util.PackageHelper
import com.gh.gamecenter.SearchActivity
import com.gh.gamecenter.common.base.GlobalActivityManager
import com.gh.gamecenter.common.baselist.ListViewModel
import com.gh.gamecenter.common.baselist.LoadParams
import com.gh.gamecenter.common.baselist.LoadStatus
import com.gh.gamecenter.common.constant.Constants
import com.gh.gamecenter.common.utils.SensorsBridge
import com.gh.gamecenter.common.utils.safelyGetInRelease
import com.gh.gamecenter.common.utils.singleToMain
import com.gh.gamecenter.common.utils.toArrayList
import com.gh.gamecenter.core.utils.SPUtils
import com.gh.gamecenter.entity.AdConfig
import com.gh.gamecenter.entity.SearchSubjectEntity
import com.gh.gamecenter.feature.entity.GameEntity
import com.gh.gamecenter.livedata.Event
import com.gh.gamecenter.minigame.MiniGameSearchResultRepository
import com.gh.gamecenter.retrofit.RetrofitManager
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import java.util.concurrent.ConcurrentHashMap
import kotlin.random.Random
class SearchGameResultViewModel(
application: Application,
private var mSearchKey: String?,
private var mIsManuallySearch: Boolean,
private val repository: ISearchGameResultRepository,
private var mSearchType: String,
val sourceEntrance: String
) : ListViewModel<GameEntity, SearchItemData>(application) {
private var mPage = 0
private var mSearchSubjects = arrayListOf<SearchSubjectEntity>()
// 游戏广告列表,需要先本地按插入 position 排序,避免后续问题
private var mGameSearchAdList: List<AdConfig>? = null
private var mAdPositionSet: HashSet<Int>? = null
private val mAdGameMap = ConcurrentHashMap<String, List<GameEntity>>()
private var mGameEntityList = arrayListOf<GameEntity>()
fun updateSearchKeyWithType(searchKey: String, searchType: String) {
mSearchKey = searchKey
mSearchType = searchType
}
fun clearSearchSubjects() {
mSearchSubjects.clear()
}
override fun mergeResultLiveData() {
mResultLiveData.addSource(mListLiveData) { list ->
mGameEntityList = ArrayList(list)
decorateListAndPost(list)
}
}
@SuppressLint("CheckResult")
private fun decorateListAndPost(list: MutableList<GameEntity>) {
val itemDataList = ArrayList<SearchItemData>()
val combineGameList = list.toMutableList()
refreshWrongInstallStatus()
repository.getSearchMiniGameCPM(mSearchKey)
.zipWith(repository.getSearchSubject(mSearchKey, mPage)) { cpmGameList, subjectList -> // CPM游戏搜索结果列表合并
for (cpmGame in cpmGameList) {
if (cpmGame.location <= 0 || cpmGame.location > list.size) {
combineGameList.add(cpmGame)
} else {
combineGameList.add(cpmGame.location - 1, cpmGame)
}
}
itemDataList.addAll(
combineGameList.mapIndexed { index, game ->
SearchItemData(game = game, gamePosition = index, isFirst = index == 0)
}
)
subjectList
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ mutableList ->
// 数据源来自于第三方的专题列表,包括 CPM 专题和 DSP 专题
val thirdPartySearchSubjectList = mutableListOf<SearchSubjectEntity>()
for (item in mutableList) {
// 避免同一个位置重复的专题
if (!mSearchSubjects.any {
it.location == item.location && it.columnId == item.columnId
}) {
mSearchSubjects.add(item)
}
}
mSearchSubjects.forEach {
if (it.type == SearchSubjectEntity.TYPE_WECHAT_GAME_CPM_COLUMN) {
thirdPartySearchSubjectList.add(it.apply { isWGameSubjectCPM = true })
} else if (it.type == SearchSubjectEntity.TYPE_DSP_GAME_COLUMN) {
thirdPartySearchSubjectList.add(it.apply { isDspSubject = true })
}
val item = SearchItemData(subject = it)
if (it.location <= 0 || it.location > itemDataList.size) {
itemDataList.add(item)
} else {
itemDataList.add(it.location - 1, item)
}
}
// 处理初始化列表且游戏列表size为0的情况
handleLoadStatusWhenGameListIsEmpty(list, itemDataList)
if (mIsManuallySearch) {
if (mSearchKey == AdDelegateHelper.gameSearchKeyword) {
updateAdConfigAndDecorateList(itemDataList, list)
} else {
AdDelegateHelper.requestAdConfig(false, mSearchKey ?: "") {
updateAdConfigAndDecorateList(itemDataList, list)
}
}
} else {
postResultList(itemDataList, list)
}
if (thirdPartySearchSubjectList.isNotEmpty()) {
decorateListWithThirdPartyData(thirdPartySearchSubjectList, itemDataList, list)
}
}, {
it.printStackTrace()
handleLoadStatusWhenGameListIsEmpty(list, itemDataList)
mResultLiveData.postValue(itemDataList)
})
}
/**
* 请求第三方接口数据来填充专题游戏数据,包含 DSP 专题和 CPM 专题
*/
@SuppressLint("CheckResult")
private fun decorateListWithThirdPartyData(
subjects: List<SearchSubjectEntity>,
itemDataList: ArrayList<SearchItemData>,
list: MutableList<GameEntity>
) {
val subjectList = subjects.filterNot { it.games.isNotEmpty() }
if (subjectList.isEmpty()) return
val subjectSingleList = subjectList.map { subject ->
if (subject.isWGameSubjectCPM) {
repository.getWGameCPMGameList(subject.size)
.onErrorReturnItem(mutableListOf())
.map { subject.columnId to it }
} else {
repository.getDspGameList(subject.columnType, subject.showDownload, subject.size)
.onErrorReturnItem(mutableListOf())
.map { subject.columnId to it }
}
}
Single.zip(subjectSingleList) { it.map { item -> item as Pair<String, MutableList<GameEntity>> } }
.map { dataList ->
for (index in itemDataList.indices) {
val itemData = itemDataList[index]
val subject = itemData.subject ?: continue
if (subject.games.isNotEmpty()) continue
val pair = dataList.firstOrNull { data ->
data.first == subject.columnId
} ?: continue
val newItemData = SearchItemData(
subject = subject.copy(games = pair.second)
)
itemDataList[index] = newItemData
}
itemDataList
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ dataList -> postResultList(dataList, list) }, {})
}
@SuppressLint("CheckResult")
private fun updateAdConfigAndDecorateList(itemDataList: ArrayList<SearchItemData>, list: MutableList<GameEntity>) {
mGameSearchAdList =
AdDelegateHelper.getGameSearchAdList().filter { AdDelegateHelper.shouldShowGameSearchAd(it) }.toArrayList()
.apply { sortBy { it.position } }
val adPositionSet = hashSetOf<Int>()
if (!mGameSearchAdList.isNullOrEmpty()) {
for (config in mGameSearchAdList!!) {
adPositionSet.add(config.position)
}
}
mAdPositionSet = adPositionSet
if (adPositionSet.isNotEmpty()) {
val ownerAdList = arrayListOf<AdConfig>()
val thirdPartyAdList = arrayListOf<AdConfig>()
// 位置已经排过序,若插入位置大于列表的 size 那么直接 break 放弃
for ((index, position) in adPositionSet.withIndex()) {
if (position < itemDataList.size + index + 1) {
val adConfig = mGameSearchAdList!!.safelyGetInRelease(index)
val showThirdPartyAd = adConfig?.displayRule?.adSource == AdDelegateHelper.AD_TYPE_SDK
val showOwnerAd = adConfig?.displayRule?.adSource == AdDelegateHelper.AD_TYPE_OWNER
val showOnFailed = adConfig?.displayRule?.onFailedAction == "show"
if ((showThirdPartyAd && adConfig?.thirdPartyAd != null)
|| (showOwnerAd && adConfig?.ownerAd == null && adConfig?.thirdPartyAd != null && showOnFailed)
) {
thirdPartyAdList.add(adConfig)
} else if ((showOwnerAd && adConfig?.ownerAd != null)
|| (showThirdPartyAd && adConfig?.ownerAd != null && showOnFailed)
) {
ownerAdList.add(adConfig)
}
} else {
break
}
}
if (ownerAdList.isNotEmpty()) {
// 存在自有广告时获取对应广告ID的游戏
val requestSingleList = arrayListOf<Single<List<GameEntity>>>()
ownerAdList.forEach {
// 没有对应广告ID的游戏数据时才添加到请求列表
if (mAdGameMap[it.id] == null) {
requestSingleList.add(createAdGameListSingle(it))
}
}
if (requestSingleList.isNotEmpty()) {
Single.zip(requestSingleList) {}
.compose(singleToMain())
.subscribe({
decorateListWithAd(itemDataList, list)
}, {
decorateListWithAd(itemDataList, list)
})
} else {
decorateListWithAd(itemDataList, list)
}
} else {
decorateListWithThirdPartyAdOnly(itemDataList, thirdPartyAdList, list)
}
} else {
postResultList(itemDataList, list)
}
}
fun refreshWrongInstallStatus() {
if (mGameEntityList.isNotEmpty()) {
PackageHelper.refreshWrongInstallStatus(mGameEntityList)
}
}
private fun decorateListWithThirdPartyAdOnly(
itemDataList: ArrayList<SearchItemData>,
thirdPartyAdList: List<AdConfig>,
list: List<GameEntity>
) {
thirdPartyAdList.forEach {
itemDataList.add(it.position - 1, SearchItemData(ad = it.thirdPartyAd, adConfig = it))
SPUtils.setLong(Constants.SP_LAST_GAME_SEARCH_AD_SHOW_TIME + it.position, System.currentTimeMillis())
}
postResultList(itemDataList, list)
}
private fun decorateListWithAd(
itemDataList: ArrayList<SearchItemData>,
list: List<GameEntity>
) {
val adGameOneIdSet = HashSet<String>() // 展示样式为单个游戏时记录游戏ID避免重复
val decoratedItemDataSize = itemDataList.size
for ((index, position) in mAdPositionSet!!.withIndex()) {
if (position < decoratedItemDataSize + index + 1) {
val adConfig = mGameSearchAdList!!.safelyGetInRelease(index)
val showThirdPartyAd = adConfig?.displayRule?.adSource == AdDelegateHelper.AD_TYPE_SDK
val showOwnerAd = adConfig?.displayRule?.adSource == AdDelegateHelper.AD_TYPE_OWNER
val showOnFailed = adConfig?.displayRule?.onFailedAction == "show"
if ((showThirdPartyAd && adConfig?.thirdPartyAd != null)
|| (showOwnerAd && adConfig?.ownerAd == null && adConfig?.thirdPartyAd != null && showOnFailed)
) {
itemDataList.add(position - 1, SearchItemData(ad = adConfig.thirdPartyAd, adConfig = adConfig))
SPUtils.setLong(
Constants.SP_LAST_GAME_SEARCH_AD_SHOW_TIME + adConfig.position,
System.currentTimeMillis()
)
} else if ((showOwnerAd && adConfig?.ownerAd != null)
|| (showThirdPartyAd && adConfig?.ownerAd != null && showOnFailed)
) {
val gameList = mAdGameMap[adConfig.id]
if (!gameList.isNullOrEmpty()) {
if (adConfig.ownerAd.adSource?.displayStyle == "game_zone") {
// 游戏专题
itemDataList.add(
position - 1,
SearchItemData(
subject = SearchSubjectEntity(
name = adConfig.ownerAd.adSource?.title ?: "",
games = gameList.take(AD_SUBJECT_GAME_MAX_COUNT),
adId = adConfig.id,
codeId = adConfig.ownerAd.id,
adIconActive = adConfig.ownerAd.adSource?.adIconActive ?: false
),
adConfig = adConfig
)
)
} else {
// 单个游戏
var randomGameEntity = gameList.safelyGetInRelease(Random.nextInt(gameList.size))
if (gameList.size != 1 && adGameOneIdSet.contains(randomGameEntity?.id)) {
// 存在重复游戏时重新获取随机游戏
randomGameEntity = gameList.safelyGetInRelease(Random.nextInt(gameList.size))
}
randomGameEntity?.id?.let { adGameOneIdSet.add(it) }
itemDataList.add(
position - 1,
SearchItemData(game = randomGameEntity, adConfig = adConfig)
)
}
SPUtils.setLong(
Constants.SP_LAST_GAME_SEARCH_AD_SHOW_TIME + adConfig.position,
System.currentTimeMillis()
)
} else if (showOnFailed && adConfig.thirdPartyAd != null) {
// 自有广告为空时,显示第三方广告
itemDataList.add(position - 1, SearchItemData(ad = adConfig.thirdPartyAd, adConfig = adConfig))
SPUtils.setLong(
Constants.SP_LAST_GAME_SEARCH_AD_SHOW_TIME + adConfig.position,
System.currentTimeMillis()
)
}
}
} else {
break
}
}
postResultList(itemDataList, list)
}
@SuppressLint("CheckResult")
private fun createAdGameListSingle(adConfig: AdConfig) = Single.create { emitter ->
val pageSize = adConfig.ownerAd?.adSource?.gamesIds?.size ?: 20
val codeId = adConfig.ownerAd?.id
val paramsMap = mapOf("page" to "1,$pageSize", "code_id" to codeId)
RetrofitManager.getInstance().newApi.getAdGames(adConfig.id, paramsMap)
.compose(singleToMain())
.subscribe({ data ->
val filterGameList = RegionSettingHelper.filterGame(data)
filterGameList.forEach {
it.run {
adIconActive = adConfig.ownerAd?.adSource?.adIconActive ?: false
adSpaceId = adConfig.id
gameAdSourceId = adConfig.ownerAd?.id ?: ""
}
}
mAdGameMap[adConfig.id] = filterGameList
emitter.onSuccess(filterGameList)
}, {
mAdGameMap[adConfig.id] = emptyList()
emitter.onSuccess(emptyList<GameEntity>())
})
}
private fun postResultList(resultList: ArrayList<SearchItemData>, list: List<GameEntity>) {
mResultLiveData.postValue(resultList.toMutableList())
if (mPage == 1) {
if (repository is MiniGameSearchResultRepository) {
SensorsBridge.trackMiniGameSearchResultReturn(
GlobalActivityManager.getCurrentPageEntity().pageId,
GlobalActivityManager.getCurrentPageEntity().pageName,
sourceEntrance,
mSearchKey ?: "",
SearchActivity.toTrackSearchType(mSearchType),
list.isNotEmpty()
)
} else {
SensorsBridge.trackGameSearchResultReturn(
GlobalActivityManager.getCurrentPageEntity().pageId,
GlobalActivityManager.getCurrentPageEntity().pageName,
sourceEntrance,
mSearchKey ?: "",
SearchActivity.toTrackSearchType(mSearchType),
list.isNotEmpty()
)
}
}
}
private fun handleLoadStatusWhenGameListIsEmpty(list: List<GameEntity>, itemDataList: ArrayList<SearchItemData>) {
if (mPage == 1 && list.isEmpty()) {
mLoadStatusLiveData.value = if (itemDataList.isEmpty()) LoadStatus.INIT_EMPTY else LoadStatus.INIT_OVER
}
}
override fun loadStatusControl(size: Int) {
if (mCurLoadParams.loadOffset == LoadParams.DEFAULT_OFFSET) { // 初始化列表
if (size == 0) {
// 初始化列表size为0情况放到mergeResultLiveData方法处理
} else if (size == REQUEST_FAILURE_SIZE) {
mLoadStatusLiveData.setValue(LoadStatus.INIT_FAILED)
} else if (size < mOverLimitSize) { // 避免一个屏幕出现两次分页
mLoadStatusLiveData.setValue(LoadStatus.INIT_OVER)
} else {
mLoadStatusLiveData.setValue(LoadStatus.INIT_LOADED)
}
} else {
if (size == REQUEST_FAILURE_SIZE) {
mLoadStatusLiveData.setValue(LoadStatus.LIST_FAILED)
} else if (size == 0) {
mLoadStatusLiveData.setValue(LoadStatus.LIST_OVER)
} else {
mLoadStatusLiveData.setValue(LoadStatus.LIST_LOADED)
}
}
if (size == REQUEST_FAILURE_SIZE) {
mRetryParams = mCurLoadParams
} else {
mRetryParams = null
mCurLoadParams.loadOffset = mCurLoadParams.loadOffset + 1 // 页数 + 1
}
}
override fun provideDataObservable(page: Int): Observable<List<GameEntity>> {
mPage = page
return repository.getSearchGame(mSearchKey, page)
}
private val _refreshVipLoading = MutableLiveData<Event<Boolean>>()
val refreshVipLoading: LiveData<Event<Boolean>> = _refreshVipLoading
fun showRefreshVipLoading(show: Boolean) {
_refreshVipLoading.value = Event(show)
}
class Factory(
private val mApplication: Application,
private val mSearchKey: String?,
private val mIsManuallySearch: Boolean,
private val repository: ISearchGameResultRepository,
private val mSearchType: String,
private val mSourceEntrance: String
) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return SearchGameResultViewModel(
mApplication,
mSearchKey,
mIsManuallySearch,
repository,
mSearchType,
mSourceEntrance
) as T
}
}
companion object {
const val AD_SUBJECT_GAME_MAX_COUNT = 8
}
}

View File

@ -1,20 +1,40 @@
package com.gh.gamecenter.search
import com.gh.gamecenter.entity.AdConfig
import com.gh.gamecenter.entity.SearchGameUnionEntity.Companion.TYPE_AD_SPACE
import com.gh.gamecenter.entity.SearchGameUnionEntity.Companion.TYPE_DSP_GAME_COLUMN
import com.gh.gamecenter.entity.SearchGameUnionEntity.Companion.TYPE_WECHAT_GAME_CPM_COLUMN
import com.gh.gamecenter.entity.SearchSubjectEntity
import com.gh.gamecenter.feature.entity.GameEntity
import com.gh.gamecenter.feature.exposure.ExposureEvent
class SearchItemData(
data class SearchItemData(
val game: GameEntity? = null,
val subject: SearchSubjectEntity? = null,
val ad: AdConfig.ThirdPartyAd? = null,
val adConfig: AdConfig? = null,
val gamePosition: Int = -1,
var gamePosition: Int = -1,
var exposureEventList: ArrayList<ExposureEvent>? = null,
private val isFirst: Boolean = false,
val type: String,
var placeHolderId: String? = null // 如果当前itemBeam还未填充实际数据仅做占位作用placeHolderId会被赋值作为实际数据填充的唯一标识当实际数据填充后placeHolderId置为空
) {
val isShowFirstSetting: Boolean
get() = isFirst && game?.firstSetting != null && !game.firstSetting?.firstPosition.isNullOrBlank()
val isPlaceHolder: Boolean
get() = placeHolderId != null
fun matchPlaceHolder(other: SearchItemData) =
if (type == other.type) {
when (type) {
TYPE_WECHAT_GAME_CPM_COLUMN, TYPE_DSP_GAME_COLUMN -> placeHolderId == other.subject?.columnId
TYPE_AD_SPACE -> placeHolderId == SearchGameResultRepository.getAdPlaceHolderId(other.adConfig)
else -> false
}
} else {
false
}
}

View File

@ -0,0 +1,255 @@
package com.gh.gamecenter.search.viewmodel
import android.app.Application
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.gh.common.util.PackageHelper
import com.gh.gamecenter.SearchActivity
import com.gh.gamecenter.common.base.GlobalActivityManager
import com.gh.gamecenter.common.baselist.ListViewModel
import com.gh.gamecenter.common.baselist.LoadParams
import com.gh.gamecenter.common.baselist.LoadStatus
import com.gh.gamecenter.common.retrofit.Response
import com.gh.gamecenter.common.utils.SensorsBridge
import com.gh.gamecenter.common.utils.observableToMain
import com.gh.gamecenter.core.provider.IHandleGameResponseProvider
import com.gh.gamecenter.entity.SearchGameUnionEntity.Companion.TYPE_GAME
import com.gh.gamecenter.entity.SearchGameUnionEntity.Companion.TYPE_WECHAT_GAME
import com.gh.gamecenter.feature.entity.GameEntity
import com.gh.gamecenter.livedata.Event
import com.gh.gamecenter.livedata.EventObserver
import com.gh.gamecenter.minigame.MiniGameSearchResultRepository
import com.gh.gamecenter.search.ISearchGameResultRepository
import com.gh.gamecenter.search.SearchGameResultRepository
import com.gh.gamecenter.search.SearchItemData
import com.therouter.TheRouter.get
import io.reactivex.Observable
import retrofit2.HttpException
class SearchGameResultViewModel(
application: Application,
private var mSearchKey: String?,
private val repository: ISearchGameResultRepository,
private var mSearchType: String,
val sourceEntrance: String
) : ListViewModel<SearchItemData, SearchItemData>(application) {
private var mPage = 0
private var gameList = arrayListOf<GameEntity>()
private val dataUpdateObserver = EventObserver<SearchItemData> {
notifyItemChanged(it)
}
private var originalDataList = arrayListOf<SearchItemData>()
init {
if (repository is SearchGameResultRepository) {
repository.dataUpdateEvent.observeForever(dataUpdateObserver)
}
}
fun updateSearchKeyWithType(searchKey: String, searchType: String) {
mSearchKey = searchKey
mSearchType = searchType
}
override fun loadData() {
if (mCurLoadParams == null) initLoadParams()
val loadParams = if (mRetryParams != null) mRetryParams else mCurLoadParams
val curStatus = mLoadStatusLiveData.value
if (curStatus != null && curStatus != LoadStatus.INIT_LOADED && curStatus != LoadStatus.LIST_LOADED && curStatus != LoadStatus.INIT) return
if (mCurLoadParams.loadOffset == LoadParams.DEFAULT_OFFSET) {
mLoadStatusLiveData.setValue(LoadStatus.INIT_LOADING)
} else {
mLoadStatusLiveData.setValue(LoadStatus.LIST_LOADING)
}
mPage = loadParams.loadOffset
repository.getSearchGame(mSearchKey, loadParams.loadOffset)
.compose(observableToMain())
.subscribe(object : Response<List<SearchItemData>>() {
override fun onResponse(response: List<SearchItemData>?) {
handleSuccess(response ?: emptyList(), curStatus)
}
override fun onFailure(e: HttpException?) {
e?.let(::handleFailure)
}
})
}
private fun handleSuccess(response: List<SearchItemData>, curStatus: LoadStatus?) {
if (mCurLoadParams.loadOffset == LoadParams.DEFAULT_OFFSET || curStatus == LoadStatus.INIT) {
originalDataList.clear()
}
// 针对游戏的一些操作(过滤隐藏游戏过滤隐藏APK吗增加下载数据)
val handleGameResponse = get(IHandleGameResponseProvider::class.java)
val gameList = handleGameResponse?.handleGameResponse(response, mEntrance) ?: response
originalDataList.addAll(gameList as List<SearchItemData>)
mListLiveData.postValue(originalDataList)
loadStatusControl(response.size)
}
private fun handleFailure(exception: Exception) {
if (exception is HttpException && exception.code() == 404) {
loadStatusControl(0)
mLoadStatusLiveData.value = LoadStatus.INIT_EXCEPTION
mLoadExceptionLiveData.postValue(exception)
} else {
loadStatusControl(REQUEST_FAILURE_SIZE)
if (exception is HttpException) {
mLoadExceptionLiveData.postValue(exception)
}
}
}
override fun mergeResultLiveData() {
mResultLiveData.addSource(mListLiveData) { list ->
val data = arrayListOf<SearchItemData>()
var gamePosition = 0
gameList.clear()
originalDataList.forEach {
if (it.type == TYPE_GAME || it.type == TYPE_WECHAT_GAME) {
it.gamePosition = gamePosition
gamePosition++
it.game?.let(gameList::add)
}
if (!it.isPlaceHolder) {
data.add(it)
}
}
refreshWrongInstallStatus()
if (data.isEmpty()) {
handleLoadStatusWhenGameListIsEmpty(data, list)
} else {
mResultLiveData.postValue(data)
}
if (mPage == 1) {
if (repository is MiniGameSearchResultRepository) {
SensorsBridge.trackMiniGameSearchResultReturn(
GlobalActivityManager.getCurrentPageEntity().pageId,
GlobalActivityManager.getCurrentPageEntity().pageName,
sourceEntrance,
mSearchKey ?: "",
SearchActivity.toTrackSearchType(mSearchType),
list.isNotEmpty()
)
} else {
SensorsBridge.trackGameSearchResultReturn(
GlobalActivityManager.getCurrentPageEntity().pageId,
GlobalActivityManager.getCurrentPageEntity().pageName,
sourceEntrance,
mSearchKey ?: "",
SearchActivity.toTrackSearchType(mSearchType),
list.isNotEmpty()
)
}
}
}
}
private fun notifyItemChanged(newItem: SearchItemData) {
val positionList = arrayListOf<Int>()
originalDataList.forEachIndexed { index, item ->
if (item.matchPlaceHolder(newItem)) {
positionList.add(index)
}
}
positionList.forEach {
originalDataList[it] = newItem
}
val displayItems = originalDataList.filterNot { it.isPlaceHolder }
mResultLiveData.postValue(displayItems)
}
fun refreshWrongInstallStatus() {
if (gameList.isNotEmpty()) {
PackageHelper.refreshWrongInstallStatus(gameList)
}
}
private fun handleLoadStatusWhenGameListIsEmpty(list: List<SearchItemData>, itemDataList: List<SearchItemData>) {
if (mPage == 1 && list.isEmpty()) {
mLoadStatusLiveData.value = if (itemDataList.isEmpty()) LoadStatus.INIT_EMPTY else LoadStatus.INIT_OVER
}
}
override fun loadStatusControl(size: Int) {
if (mCurLoadParams.loadOffset == LoadParams.DEFAULT_OFFSET) { // 初始化列表
if (size == 0) {
// 初始化列表size为0情况放到mergeResultLiveData方法处理
} else if (size == REQUEST_FAILURE_SIZE) {
mLoadStatusLiveData.setValue(LoadStatus.INIT_FAILED)
} else if (size < mOverLimitSize) { // 避免一个屏幕出现两次分页
mLoadStatusLiveData.setValue(LoadStatus.INIT_OVER)
} else {
mLoadStatusLiveData.setValue(LoadStatus.INIT_LOADED)
}
} else {
if (size == REQUEST_FAILURE_SIZE) {
mLoadStatusLiveData.setValue(LoadStatus.LIST_FAILED)
} else if (size == 0) {
mLoadStatusLiveData.setValue(LoadStatus.LIST_OVER)
} else {
mLoadStatusLiveData.setValue(LoadStatus.LIST_LOADED)
}
}
if (size == REQUEST_FAILURE_SIZE) {
mRetryParams = mCurLoadParams
} else {
mRetryParams = null
mCurLoadParams.loadOffset = mCurLoadParams.loadOffset + 1 // 页数 + 1
}
}
override fun provideDataObservable(page: Int): Observable<List<SearchItemData>>? = null
private val _refreshVipLoading = MutableLiveData<Event<Boolean>>()
val refreshVipLoading: LiveData<Event<Boolean>> = _refreshVipLoading
fun showRefreshVipLoading(show: Boolean) {
_refreshVipLoading.value = Event(show)
}
override fun onCleared() {
super.onCleared()
if (repository is SearchGameResultRepository) {
repository.dataUpdateEvent.removeObserver(dataUpdateObserver)
}
}
class Factory(
private val mApplication: Application,
private val mSearchKey: String?,
private val repository: ISearchGameResultRepository,
private val mSearchType: String,
private val mSourceEntrance: String
) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return SearchGameResultViewModel(
mApplication,
mSearchKey,
repository,
mSearchType,
mSourceEntrance
) as T
}
}
}

View File

@ -32,6 +32,7 @@ class GameServerTestV2Fragment : LazyFragment() {
private var mSettingBinding: LayoutGameServerTestV2SettingBinding? = null
private var mCurrentFragment: GameServerTestV2ListFragment? = null
private var mLinkText = ""
private var mLinkId = ""
private var mPageLocation: PageLocation? = null
private var mIsSettingAnimating = false
@ -39,6 +40,7 @@ class GameServerTestV2Fragment : LazyFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mLinkId = arguments?.getString(EntranceConsts.KEY_ID, "none") ?: "none"
mLinkText = requireArguments().getString(EntranceConsts.KEY_NAME, "")
val topTabIndex = requireArguments().getInt(EntranceConsts.KEY_TAB_INDEX, -1)
val bottomTabIndex = requireArguments().getInt(EntranceConsts.KEY_BOTTOM_TAB_INDEX, -1)
@ -64,22 +66,40 @@ class GameServerTestV2Fragment : LazyFragment() {
}
override fun onFragmentFirstVisible() {
mViewModel = viewModelProviderFromParent()
mViewModel = viewModelProviderFromParent(GameServerTestV2ViewModel.Factory(mLinkId))
super.onFragmentFirstVisible()
mViewModel?.run {
mBinding?.filterView?.setItemList(timeFilterList, DEFAULT_TIME_FILTER_INDEX)
selectedTimeFilterLiveData.observe(this@GameServerTestV2Fragment) {
displaySettingLiveData.observe(viewLifecycleOwner) {
mBinding?.filterView?.setItemList(timeFilterList, DEFAULT_TIME_FILTER_INDEX)
mViewModel?.selectedTimeFilterLiveData?.value = mViewModel?.timeFilterList?.getOrNull(DEFAULT_TIME_FILTER_INDEX) ?: ""
mBinding?.filterView?.isVisible = true
mBinding?.optionIv?.setOnClickListener {
showSettingView()
SensorsBridge.trackColumnTestClick(
location = "详情页",
recommendType = mLinkText,
text = "设置",
bottomTab = mPageLocation?.bottomTab,
multiTabName = mPageLocation?.severalTabPageName,
multiTabId = mPageLocation?.severalTabPageId,
position = mPageLocation?.tabPosition,
tabContent = mPageLocation?.tabContent
)
}
mCurrentFragment?.loadData()
if (SPUtils.getBoolean(Constants.SP_SHOW_GAME_SERVER_TEST_V2_SETTING_GUIDE, true)) {
mBaseHandler.post {
showGuide()
}
SPUtils.setBoolean(Constants.SP_SHOW_GAME_SERVER_TEST_V2_SETTING_GUIDE, false)
}
}
selectedTimeFilterLiveData.observe(viewLifecycleOwner) {
mBinding?.filterView?.setCurrentItem(timeFilterList.indexOf(it))
}
}
changeFragment()
if (SPUtils.getBoolean(Constants.SP_SHOW_GAME_SERVER_TEST_V2_SETTING_GUIDE, true)) {
mBaseHandler.post {
showGuide()
}
SPUtils.setBoolean(Constants.SP_SHOW_GAME_SERVER_TEST_V2_SETTING_GUIDE, false)
}
}
override fun initRealView() {
@ -89,7 +109,7 @@ class GameServerTestV2Fragment : LazyFragment() {
filterContainer.setBackgroundColor(com.gh.gamecenter.common.R.color.ui_background.toColor(requireContext()))
filterView.onCheckedAction = { index ->
mViewModel?.let {
it.setSelectedTimeFilter(it.timeFilterList[index])
it.setSelectedTimeFilter(it.timeFilterList.getOrNull(index))
mCurrentFragment?.changeTimeFilter()
SensorsBridge.trackColumnTestClick(
location = "详情页",
@ -100,7 +120,7 @@ class GameServerTestV2Fragment : LazyFragment() {
multiTabId = mPageLocation?.severalTabPageId,
position = mPageLocation?.tabPosition,
tabContent = mPageLocation?.tabContent,
buttonType = it.timeFilterList[index]
buttonType = it.timeFilterList.getOrNull(index) ?: ""
)
}
}
@ -124,19 +144,6 @@ class GameServerTestV2Fragment : LazyFragment() {
)
}
}
optionIv.setOnClickListener {
showSettingView()
SensorsBridge.trackColumnTestClick(
location = "详情页",
recommendType = mLinkText,
text = "设置",
bottomTab = mPageLocation?.bottomTab,
multiTabName = mPageLocation?.severalTabPageName,
multiTabId = mPageLocation?.severalTabPageId,
position = mPageLocation?.tabPosition,
tabContent = mPageLocation?.tabContent
)
}
}
}
@ -190,7 +197,7 @@ class GameServerTestV2Fragment : LazyFragment() {
val isCategoryFilterChanged = selectedCategoryFilterSet != tempCategoryFilterSet
selectedCategoryFilterSet = HashSet(tempCategoryFilterSet)
SPUtils.setStringSet(Constants.SP_GAME_SERVER_TEST_V2_CATEGORY_SET, selectedCategoryFilterSet)
setSelectedTimeFilter(GameServerTestV2ViewModel.TODAY_GAME)
setSelectedTimeFilter(timeFilterList.getOrNull(1))
if (isCategoryFilterChanged) {
mCurrentFragment?.changeCategoryFilter()
}
@ -212,8 +219,8 @@ class GameServerTestV2Fragment : LazyFragment() {
}
val itemList = arrayListOf<TextView>()
val allItem = getItemTextView("全部")
allItem.tag = "全部"
val allItem = getItemTextView(GameServerTestV2ViewModel.GameCategory.All.displayName)
allItem.tag = GameServerTestV2ViewModel.GameCategory.All.displayName
binding.flexbox.addView(allItem)
toggleHighlightedTextView(allItem, mViewModel?.tempCategoryFilterSet.isNullOrEmpty())
allItem.setOnClickListener {
@ -338,7 +345,6 @@ class GameServerTestV2Fragment : LazyFragment() {
}
private fun changeFragment() {
mViewModel?.selectedTimeFilterLiveData?.value = mViewModel?.timeFilterList?.get(DEFAULT_TIME_FILTER_INDEX) ?: ""
mCurrentFragment =
childFragmentManager.findFragmentByTag(GameServerTestV2ViewModel::class.java.name) as? GameServerTestV2ListFragment
?: GameServerTestV2ListFragment()

View File

@ -18,6 +18,7 @@ import com.gh.download.DownloadManager
import com.gh.gamecenter.R
import com.gh.gamecenter.common.baselist.LazyListFragment
import com.gh.gamecenter.common.baselist.ListAdapter
import com.gh.gamecenter.common.baselist.LoadType
import com.gh.gamecenter.common.constant.Constants
import com.gh.gamecenter.common.constant.EntranceConsts
import com.gh.gamecenter.common.exposure.ExposureSource
@ -76,6 +77,12 @@ class GameServerTestV2ListFragment :
)
}
override fun isAutomaticLoad(): Boolean = false
fun loadData() {
mListViewModel.load(LoadType.NORMAL)
}
override fun onCreate(savedInstanceState: Bundle?) {
mGameServerTestV2ViewModel = viewModelProviderFromParent()
super.onCreate(savedInstanceState)
@ -356,7 +363,7 @@ class GameServerTestV2ListFragment :
override fun onChanged(ts: MutableList<GameServerTestV2ListViewModel.ItemData>?) {
super.onChanged(ts)
if (!ts.isNullOrEmpty() && mListViewModel.isLoadFirstPage) {
scrollToTime(GameServerTestV2ViewModel.TimeFilter.TODAY.value)
scrollToTime(GameServerTestV2ViewModel.TimeFilter.Today.value)
mListRv?.postDelayed({
scroll()
mScrollCalculatorHelper.onScrollStateChanged(RecyclerView.SCROLL_STATE_IDLE)

View File

@ -55,14 +55,9 @@ class GameServerTestV2ListViewModel(
mListLiveData.value = null
mLoadStatusLiveData.postValue(LoadStatus.INIT_LOADING)
var categoryFilter = "" //如果选择全部类型,则传空值
mGameServerTestV2ViewModel?.selectedCategoryFilterSet?.forEachIndexed { index, category ->
if (index == 0) {
categoryFilter = mGameServerTestV2ViewModel.getCategoryFilterValue(category).value
} else {
categoryFilter += "-${mGameServerTestV2ViewModel.getCategoryFilterValue(category).value}"
}
}
val categoryFilter = mGameServerTestV2ViewModel?.selectedCategoryFilterSet?.joinToString("-") {
GameServerTestV2ViewModel.GameCategory.fromDisplayName(it).value
} ?: "" //如果选择全部类型,则传空值
Observable.zip(provideDetailObservable(categoryFilter), provideTopGamesObservable()) { t1, t2 ->
mTopGameList = ArrayList(t2).apply {
@ -151,11 +146,11 @@ class GameServerTestV2ListViewModel(
// 置顶游戏固定显示在今天上方,如果今天没有数据则展示在未来上方;如果今天和未来都没有数据,则显示在近期上方
val topGamesReadableDaysOffset = if (mTodayCount != 0) {
GameServerTestV2ViewModel.TimeFilter.TODAY.value
GameServerTestV2ViewModel.TimeFilter.Today.value
} else if (mFutureCount != 0) {
GameServerTestV2ViewModel.TimeFilter.UPCOMING_DAY.value
GameServerTestV2ViewModel.TimeFilter.Future.value
} else {
GameServerTestV2ViewModel.TimeFilter.PAST_DAY.value
GameServerTestV2ViewModel.TimeFilter.Recent.value
}
var isTopGamesAdded = false
@ -179,11 +174,7 @@ class GameServerTestV2ListViewModel(
}
}
val readableDaysOffset = when (slice.timeType) {
"recent" -> GameServerTestV2ViewModel.TimeFilter.PAST_DAY.value
"future" -> GameServerTestV2ViewModel.TimeFilter.UPCOMING_DAY.value
else -> GameServerTestV2ViewModel.TimeFilter.TODAY.value
}
val readableDaysOffset = slice.timeType
if (!mTopGameList.isNullOrEmpty() && readableDaysOffset == topGamesReadableDaysOffset && !isTopGamesAdded) {
subList.add(
@ -335,31 +326,31 @@ class GameServerTestV2ListViewModel(
if (position >= 0) return position
val recentCount =
itemDataList.count { it.readableDaysOffset == GameServerTestV2ViewModel.TimeFilter.PAST_DAY.value }
itemDataList.count { it.readableDaysOffset == GameServerTestV2ViewModel.TimeFilter.Recent.value }
val todayCount =
itemDataList.count { it.readableDaysOffset == GameServerTestV2ViewModel.TimeFilter.TODAY.value }
itemDataList.count { it.readableDaysOffset == GameServerTestV2ViewModel.TimeFilter.Today.value }
when (timeFilter) {
GameServerTestV2ViewModel.TimeFilter.PAST_DAY.value -> {
position = mTimePositionArrayMap.getOrDefault(GameServerTestV2ViewModel.TimeFilter.TODAY.value, -1)
GameServerTestV2ViewModel.TimeFilter.Recent.value -> {
position = mTimePositionArrayMap.getOrDefault(GameServerTestV2ViewModel.TimeFilter.Today.value, -1)
if (position >= 0) return position
position =
mTimePositionArrayMap.getOrDefault(GameServerTestV2ViewModel.TimeFilter.UPCOMING_DAY.value, -1)
mTimePositionArrayMap.getOrDefault(GameServerTestV2ViewModel.TimeFilter.Future.value, -1)
if (position >= 0) return position
}
GameServerTestV2ViewModel.TimeFilter.TODAY.value -> {
GameServerTestV2ViewModel.TimeFilter.Today.value -> {
position =
mTimePositionArrayMap.getOrDefault(GameServerTestV2ViewModel.TimeFilter.UPCOMING_DAY.value, -1)
mTimePositionArrayMap.getOrDefault(GameServerTestV2ViewModel.TimeFilter.Future.value, -1)
if (position >= 0) return position
position =
mTimePositionArrayMap.getOrDefault(GameServerTestV2ViewModel.TimeFilter.PAST_DAY.value, -1)
mTimePositionArrayMap.getOrDefault(GameServerTestV2ViewModel.TimeFilter.Recent.value, -1)
if (position >= 0) return recentCount - 1
}
GameServerTestV2ViewModel.TimeFilter.UPCOMING_DAY.value -> {
position = mTimePositionArrayMap.getOrDefault(GameServerTestV2ViewModel.TimeFilter.TODAY.value, -1)
GameServerTestV2ViewModel.TimeFilter.Future.value -> {
position = mTimePositionArrayMap.getOrDefault(GameServerTestV2ViewModel.TimeFilter.Today.value, -1)
if (position >= 0) return recentCount + todayCount - 1
position =
mTimePositionArrayMap.getOrDefault(GameServerTestV2ViewModel.TimeFilter.PAST_DAY.value, -1)
mTimePositionArrayMap.getOrDefault(GameServerTestV2ViewModel.TimeFilter.Recent.value, -1)
if (position >= 0) recentCount - 1
}
}

View File

@ -1,5 +1,6 @@
package com.gh.gamecenter.servers.gametest2
import android.annotation.SuppressLint
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
@ -7,89 +8,129 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.gh.common.constant.Config
import com.gh.gamecenter.common.constant.Constants
import com.gh.gamecenter.common.utils.singleToMain
import com.gh.gamecenter.core.utils.SPUtils
import com.gh.gamecenter.entity.GameServerTestDisplaySetting
import com.gh.gamecenter.retrofit.RetrofitManager
import com.gh.gamecenter.servers.gametest2.GameServerTestV2ViewModel.GameCategory.*
import com.halo.assistant.HaloApp
class GameServerTestV2ViewModel(application: Application) : AndroidViewModel(application) {
class GameServerTestV2ViewModel(application: Application, private val linkId: String = "none") : AndroidViewModel(application) {
val timeFilterList = arrayListOf(RECENT_GAME, TODAY_GAME, FUTURE_GAME)
val categoryFilterList = arrayListOf("单机游戏", "网络游戏", "福利游戏", "国际服游戏")
val timeFilterList = arrayListOf(
GameServerTestDisplaySetting.RECENT,
GameServerTestDisplaySetting.TODAY,
GameServerTestDisplaySetting.FUTURE
)
val categoryFilterList = arrayListOf(
Local.displayName,
Online.displayName,
Welfare.displayName,
Gjonline.displayName
)
var selectedCategoryFilterSet = SPUtils.getStringSet(Constants.SP_GAME_SERVER_TEST_V2_CATEGORY_SET).toHashSet()
var tempCategoryFilterSet = SPUtils.getStringSet(Constants.SP_GAME_SERVER_TEST_V2_CATEGORY_SET).toHashSet()
var selectedTimeFilterLiveData = MutableLiveData(timeFilterList[GameServerTestV2Fragment.DEFAULT_TIME_FILTER_INDEX])
var selectedTimeFilterLiveData = MutableLiveData<String?>()
var bigImageMode = SPUtils.getBoolean(
Constants.SP_GAME_SERVER_TEST_V2_BIG_IMAGE_MODE,
Config.getSettings() == null || Config.getSettings()?.columnTestV2Setting?.defaultView == "on"
)
fun getCategoryFilterString(): String {
var selectedCategoryFilter = "全部类型"
selectedCategoryFilterSet.forEachIndexed { index, category ->
if (index == 0) {
selectedCategoryFilter = category
} else {
selectedCategoryFilter += "+$category"
}
}
return selectedCategoryFilter
val displaySettingLiveData = MutableLiveData<GameServerTestDisplaySetting>()
init {
loadDisplaySetting()
}
fun getCategoryFilterValue(category: String): GameCategory {
return when (category) {
"单机游戏" -> GameCategory.LOCAL
"网络游戏" -> GameCategory.ONLINE
"福利游戏" -> GameCategory.WELFARE
"国际服游戏" -> GameCategory.GJONLINE
else -> GameCategory.ALL
}
@SuppressLint("CheckResult")
fun loadDisplaySetting() {
RetrofitManager.getInstance().newApi.getGameServerTestDisplaySetting(linkId)
.compose(singleToMain())
.subscribe({ setting ->
timeFilterList.clear()
timeFilterList.addAll(listOf(setting.timeTextPast, setting.timeTextPresent, setting.timeTextFuture))
TimeFilter.Recent.displayName = setting.timeTextPast
TimeFilter.Today.displayName = setting.timeTextPresent
TimeFilter.Future.displayName = setting.timeTextFuture
categoryFilterList.clear()
categoryFilterList.addAll(setting.gameCategory.map { GameCategory.fromValue(it).displayName })
selectedCategoryFilterSet.removeAll { !categoryFilterList.contains(it) }
tempCategoryFilterSet.removeAll { !categoryFilterList.contains(it) }
displaySettingLiveData.postValue(setting)
}, {
it.printStackTrace()
displaySettingLiveData.postValue(GameServerTestDisplaySetting())
})
}
fun getSelectedTimeFilterString(dayFilter: String): String {
return when (dayFilter) {
TimeFilter.PAST_DAY.value -> RECENT_GAME
TimeFilter.TODAY.value -> TODAY_GAME
TimeFilter.UPCOMING_DAY.value -> FUTURE_GAME
else -> TODAY_GAME
}
}
fun getCategoryFilterString() = selectedCategoryFilterSet.joinToString("+").ifEmpty { "全部类型" }
fun getCurrentSelectedTimeFilter(): String {
return when (selectedTimeFilterLiveData.value) {
RECENT_GAME -> TimeFilter.PAST_DAY.value
TODAY_GAME -> TimeFilter.TODAY.value
FUTURE_GAME -> TimeFilter.UPCOMING_DAY.value
else -> TimeFilter.TODAY.value
}
}
fun getSelectedTimeFilterString(dayFilter: String) = TimeFilter.fromValue(dayFilter).displayName
fun getCurrentSelectedTimeFilter() = TimeFilter.fromDisplayName(selectedTimeFilterLiveData.value ?: "").value
fun setSelectedTimeFilter(filter: String?) {
if (filter.isNullOrEmpty()) return
selectedTimeFilterLiveData.value = filter
}
enum class TimeFilter(val value: String) {
PAST_DAY("recent"),
TODAY("today"),
UPCOMING_DAY("future")
sealed class TimeFilter(val value: String, var displayName: String) {
data object Recent : TimeFilter("recent", GameServerTestDisplaySetting.RECENT)
data object Today : TimeFilter("today", GameServerTestDisplaySetting.TODAY)
data object Future : TimeFilter("future", GameServerTestDisplaySetting.FUTURE)
companion object {
fun fromDisplayName(displayName: String): TimeFilter {
return when (displayName) {
Recent.displayName -> Recent
Future.displayName -> Future
else -> Today
}
}
fun fromValue(value: String): TimeFilter {
return when (value) {
Recent.value -> Recent
Future.value -> Future
else -> Today
}
}
}
}
enum class GameCategory(val value: String) {
ALL("all"),
LOCAL("local"),
ONLINE("online"),
WELFARE("welfare"),
GJONLINE("gjonline"),
sealed class GameCategory(val value: String, val displayName: String) {
data object All : GameCategory("all", "全部")
data object Local : GameCategory("local", "单机游戏")
data object Online : GameCategory("online", "网络游戏")
data object Welfare : GameCategory("welfare", "福利游戏")
data object Gjonline : GameCategory("gjonline", "国际服游戏")
companion object {
fun fromDisplayName(displayName: String): GameCategory {
return when (displayName) {
Local.displayName -> Local
Online.displayName -> Online
Welfare.displayName -> Welfare
Gjonline.displayName -> Gjonline
else -> All
}
}
fun fromValue(value: String): GameCategory {
return when (value) {
Local.value -> Local
Online.value -> Online
Welfare.value -> Welfare
Gjonline.value -> Gjonline
else -> All
}
}
}
}
companion object {
const val RECENT_GAME = "近期"
const val TODAY_GAME = "今天"
const val FUTURE_GAME = "预约"
}
class Factory(private val mApplication: Application) :
class Factory(private val linkId: String = "none") :
ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return GameServerTestV2ViewModel(mApplication) as T
return GameServerTestV2ViewModel(HaloApp.getInstance(), linkId) as T
}
}
}

View File

@ -56,9 +56,10 @@
<com.gh.gamecenter.common.view.SegmentedIconFilterView
android:id="@+id/filterView"
android:layout_width="130dp"
android:layout_width="wrap_content"
android:layout_height="28dp"
android:layout_marginStart="16dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

View File

@ -14,7 +14,7 @@
android:layout_width="wrap_content"
android:layout_height="48dp"
android:gravity="center"
android:textColor="@color/text_tertiary"
android:textColor="@color/text_secondary"
android:textSize="@dimen/secondary_title_text_size"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"

View File

@ -33,14 +33,15 @@
<FrameLayout
android:id="@+id/backContainer"
android:layout_width="48dp"
android:layout_width="40dp"
android:layout_height="48dp">
<ImageView
android:id="@+id/backBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_gravity="center_vertical"
android:layout_marginStart="12dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
@ -73,8 +74,8 @@
<com.gh.gamecenter.common.view.TabIndicatorView
android:id="@+id/tabIndicator"
android:layout_width="0dp"
android:layout_height="@dimen/default_tab_indicator_height"
android:layout_marginBottom="10dp"
android:layout_height="4dp"
android:layout_marginBottom="8dp"
app:indicatorColor="@color/primary_theme"
app:disableIndicatorScaling="true"
app:layout_constraintBottom_toBottomOf="parent"
@ -97,6 +98,7 @@
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="end"
android:layout_marginHorizontal="8dp"
android:minWidth="44dp" />
</LinearLayout>

View File

@ -14,10 +14,4 @@
android:title="@string/menu_more"
app:showAsAction="always" />
<item
android:orderInCategory="-1"
android:title=""
app:actionLayout="@layout/layout_menu_inset"
app:showAsAction="always" />
</menu>

View File

@ -7,8 +7,8 @@ ext {
targetSdkVersion = 30 // 升级targetSdkVersion到 34 时需要根据官方文档补全前台服务的权限类型。比如 NDownloadServiceKeepAliveService
// application info (每个大版本之间的 versionCode 增加 20)
versionCode = 1190
versionName = "5.42.0"
versionCode = 1210
versionName = "5.43.0"
applicationId = "com.gh.gamecenter"
applicationIdGat = "com.gh.gamecenter.intl"
@ -143,7 +143,7 @@ ext {
acloudPush = "3.8.8.1"
jpushVersion = "5.4.0"
jverifiationVersion = "3.2.5"
jverifiationVersion = "3.4.2"
honorPushVersion = "7.0.61.303"
volcTlsVersion = "1.1.4"

View File

@ -517,7 +517,10 @@ object SensorsBridge {
downloadStatus: String,
gameType: String,
position: Int,
tabContent: String
tabContent: String,
linkType: String,
linkId: String,
linkText: String,
) {
val json = json {
KEY_GAME_ID to gameId
@ -532,6 +535,9 @@ object SensorsBridge {
KEY_GAME_TYPE to gameType
KEY_POSITION to position
KEY_TAB_CONTENT to tabContent
KEY_LINK_TYPE to linkType
KEY_LINK_ID to linkId
KEY_LINK_TEXT to linkText
}
trackEvent(EVENT_GAME_DETAIL_PAGE_TAB_SELECT, json)

View File

@ -5,10 +5,12 @@ import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.FrameLayout
import androidx.core.view.setPadding
import com.gh.gamecenter.common.R
import com.gh.gamecenter.common.databinding.ViewSegmentedIconFilterBinding
import com.gh.gamecenter.common.utils.DarkModeUtils
import com.gh.gamecenter.common.utils.dip2px
import com.gh.gamecenter.common.utils.toDrawable
class SegmentedIconFilterView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null): FrameLayout(context, attrs) {
private var textSize = 12F
@ -23,15 +25,12 @@ class SegmentedIconFilterView @JvmOverloads constructor(context: Context, attrs:
var onCheckedAction: ((Int) -> Unit)? = null
init {
val view = LayoutInflater.from(context).inflate(R.layout.view_segmented_icon_filter, this, true)
val view = LayoutInflater.from(context).inflate(R.layout.view_segmented_icon_filter, this, false)
binding = ViewSegmentedIconFilterBinding.bind(view)
addView(view)
initView(attrs)
}
fun setContainerBackground(drawable: Drawable?) {
binding.motionLayout.background = drawable
}
private fun initView(attrs: AttributeSet?) {
if (attrs != null) {
val ta = context.obtainStyledAttributes(attrs, R.styleable.SegmentedFilterView)
@ -47,12 +46,8 @@ class SegmentedIconFilterView @JvmOverloads constructor(context: Context, attrs:
ta.recycle()
}
binding.motionLayout.run {
setPadding(containerPadding, containerPadding, containerPadding, containerPadding)
containerBackground?.let {
background = it
}
}
background = containerBackground ?: R.drawable.bg_shape_ui_container_2_radius_999.toDrawable(context)
setPadding(containerPadding)
indicatorBackground?.let {
binding.indicator.background = it
}
@ -129,6 +124,7 @@ class SegmentedIconFilterView @JvmOverloads constructor(context: Context, attrs:
}
fun updateView(isDarkModeOn: Boolean) {
background = containerBackground ?: R.drawable.bg_shape_ui_container_2_radius_999.toDrawable(context)
binding.motionLayout.run {
when (currentIndex) {
0 -> jumpToState(if (isDarkModeOn) R.id.filterDark1 else R.id.filter1)

View File

@ -1,66 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.motion.widget.MotionLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/motionLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
android:layout_height="24dp"
app:layoutDescription="@xml/view_segmented_icon_filter_scene">
<androidx.constraintlayout.motion.widget.MotionLayout
android:id="@+id/motionLayout"
android:layout_width="130dp"
android:layout_height="28dp"
android:background="@drawable/bg_shape_ui_container_2_radius_999"
android:padding="2dp"
app:layoutDescription="@xml/view_segmented_icon_filter_scene">
<View
android:id="@+id/indicator"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/background_shape_white_radius_999" />
<View
android:id="@+id/indicator"
android:layout_width="50dp"
android:layout_height="0dp"
android:background="@drawable/background_shape_white_radius_999" />
<ImageView
android:id="@+id/icon1Iv"
android:layout_width="12dp"
android:layout_height="12dp"
app:srcCompat="@drawable/ic_segmented_icon_filter_dot" />
<ImageView
android:id="@+id/icon1Iv"
android:layout_width="12dp"
android:layout_height="12dp"
app:srcCompat="@drawable/ic_segmented_icon_filter_dot" />
<TextView
android:id="@+id/filter1Tv"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:gravity="center"
android:includeFontPadding="false"
android:text="近期"
android:textSize="@dimen/secondary_size" />
<TextView
android:id="@+id/filter1Tv"
android:layout_width="wrap_content"
android:layout_height="28dp"
android:gravity="center"
android:includeFontPadding="false"
android:text="近期"
android:textSize="@dimen/secondary_size" />
<ImageView
android:id="@+id/icon2Iv"
android:layout_width="12dp"
android:layout_height="12dp"
app:srcCompat="@drawable/ic_segmented_icon_filter_dot" />
<ImageView
android:id="@+id/icon2Iv"
android:layout_width="12dp"
android:layout_height="12dp"
app:srcCompat="@drawable/ic_segmented_icon_filter_dot" />
<TextView
android:id="@+id/filter2Tv"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:gravity="center"
android:includeFontPadding="false"
android:text="今天"
android:textSize="@dimen/secondary_size" />
<TextView
android:id="@+id/filter2Tv"
android:layout_width="wrap_content"
android:layout_height="28dp"
android:gravity="center"
android:includeFontPadding="false"
android:text="今天"
android:textSize="@dimen/secondary_size" />
<ImageView
android:id="@+id/icon3Iv"
android:layout_width="12dp"
android:layout_height="12dp"
app:srcCompat="@drawable/ic_segmented_icon_filter_dot" />
<ImageView
android:id="@+id/icon3Iv"
android:layout_width="12dp"
android:layout_height="12dp"
app:srcCompat="@drawable/ic_segmented_icon_filter_dot" />
<TextView
android:id="@+id/filter3Tv"
android:layout_width="wrap_content"
android:layout_height="28dp"
android:gravity="center"
android:includeFontPadding="false"
android:text="未来"
android:textSize="@dimen/secondary_size" />
</androidx.constraintlayout.motion.widget.MotionLayout>
</FrameLayout>
<TextView
android:id="@+id/filter3Tv"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:gravity="center"
android:includeFontPadding="false"
android:text="未来"
android:textSize="@dimen/secondary_size" />
</androidx.constraintlayout.motion.widget.MotionLayout>

View File

@ -31,9 +31,11 @@
<ConstraintSet android:id="@+id/filter1">
<Constraint
android:id="@+id/indicator"
android:layout_width="50dp"
android:layout_height="24dp"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginEnd="6dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toStartOf="@+id/filter2Tv"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toTopOf="parent" />
@ -64,18 +66,18 @@
android:id="@+id/icon2Iv"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginStart="6dp"
android:alpha="0"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toEndOf="@+id/indicator"
motion:layout_constraintStart_toStartOf="@+id/filter2Tv"
motion:layout_constraintTop_toTopOf="parent" />
<Constraint
android:id="@+id/filter2Tv"
android:layout_width="40dp"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginStart="14dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toEndOf="@+id/indicator"
motion:layout_constraintStart_toEndOf="@+id/filter1Tv"
motion:layout_constraintTop_toTopOf="parent">
<CustomAttribute
motion:attributeName="textColor"
@ -86,18 +88,19 @@
android:id="@+id/icon3Iv"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginStart="6dp"
android:alpha="0"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toEndOf="@+id/filter2Tv"
motion:layout_constraintStart_toStartOf="@+id/filter3Tv"
motion:layout_constraintTop_toTopOf="parent" />
<Constraint
android:id="@+id/filter3Tv"
android:layout_width="40dp"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginStart="-2dp"
android:layout_marginStart="14dp"
android:layout_marginEnd="6dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintStart_toEndOf="@+id/filter2Tv"
motion:layout_constraintTop_toTopOf="parent">
<CustomAttribute
@ -109,9 +112,12 @@
<ConstraintSet android:id="@+id/filter2">
<Constraint
android:id="@+id/indicator"
android:layout_width="50dp"
android:layout_height="24dp"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toStartOf="@+id/filter3Tv"
motion:layout_constraintStart_toEndOf="@+id/filter1Tv"
motion:layout_constraintTop_toTopOf="parent" />
@ -119,17 +125,16 @@
android:id="@+id/icon1Iv"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginStart="6dp"
android:alpha="0"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintStart_toStartOf="@+id/filter1Tv"
motion:layout_constraintTop_toTopOf="parent" />
<Constraint
android:id="@+id/filter1Tv"
android:layout_width="40dp"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginStart="-2dp"
android:layout_marginStart="6dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toTopOf="parent">
@ -142,7 +147,7 @@
android:id="@+id/icon2Iv"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginStart="6dp"
android:layout_marginStart="14dp"
android:alpha="1"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toEndOf="@+id/filter1Tv"
@ -173,11 +178,13 @@
<Constraint
android:id="@+id/filter3Tv"
android:layout_width="40dp"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginEnd="-2dp"
android:layout_marginStart="14dp"
android:layout_marginEnd="6dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintStart_toEndOf="@+id/filter2Tv"
motion:layout_constraintTop_toTopOf="parent">
<CustomAttribute
motion:attributeName="textColor"
@ -188,27 +195,28 @@
<ConstraintSet android:id="@+id/filter3">
<Constraint
android:id="@+id/indicator"
android:layout_width="50dp"
android:layout_height="24dp"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="8dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintStart_toEndOf="@+id/filter2Tv"
motion:layout_constraintTop_toTopOf="parent" />
<Constraint
android:id="@+id/icon1Iv"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginStart="6dp"
android:alpha="0"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintStart_toStartOf="@+id/filter1Tv"
motion:layout_constraintTop_toTopOf="parent" />
<Constraint
android:id="@+id/filter1Tv"
android:layout_width="40dp"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginStart="-2dp"
android:layout_marginStart="6dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toTopOf="parent">
@ -221,17 +229,16 @@
android:id="@+id/icon2Iv"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginStart="6dp"
android:alpha="0"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toEndOf="@+id/filter1Tv"
motion:layout_constraintStart_toStartOf="@+id/filter2Tv"
motion:layout_constraintTop_toTopOf="parent" />
<Constraint
android:id="@+id/filter2Tv"
android:layout_width="40dp"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginStart="-2dp"
android:layout_marginStart="14dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toEndOf="@+id/filter1Tv"
motion:layout_constraintTop_toTopOf="parent">
@ -244,7 +251,7 @@
android:id="@+id/icon3Iv"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginStart="6dp"
android:layout_marginStart="14dp"
android:alpha="1"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toEndOf="@+id/filter2Tv"
@ -255,7 +262,9 @@
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginStart="2dp"
android:layout_marginEnd="6dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintStart_toEndOf="@+id/icon3Iv"
motion:layout_constraintTop_toTopOf="parent">
<CustomAttribute
@ -267,9 +276,11 @@
<ConstraintSet android:id="@+id/filterDark1">
<Constraint
android:id="@+id/indicator"
android:layout_width="50dp"
android:layout_height="24dp"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginEnd="8dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toStartOf="@+id/filter2Tv"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toTopOf="parent" />
@ -300,18 +311,18 @@
android:id="@+id/icon2Iv"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginStart="6dp"
android:alpha="0"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toEndOf="@+id/indicator"
motion:layout_constraintStart_toStartOf="@+id/filter2Tv"
motion:layout_constraintTop_toTopOf="parent" />
<Constraint
android:id="@+id/filter2Tv"
android:layout_width="40dp"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginStart="14dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toEndOf="@+id/indicator"
motion:layout_constraintStart_toEndOf="@+id/filter1Tv"
motion:layout_constraintTop_toTopOf="parent">
<CustomAttribute
motion:attributeName="textColor"
@ -322,18 +333,19 @@
android:id="@+id/icon3Iv"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginStart="6dp"
android:alpha="0"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toEndOf="@+id/filter2Tv"
motion:layout_constraintStart_toStartOf="@+id/filter3Tv"
motion:layout_constraintTop_toTopOf="parent" />
<Constraint
android:id="@+id/filter3Tv"
android:layout_width="40dp"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginStart="-2dp"
android:layout_marginStart="14dp"
android:layout_marginEnd="6dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintStart_toEndOf="@+id/filter2Tv"
motion:layout_constraintTop_toTopOf="parent">
<CustomAttribute
@ -345,9 +357,12 @@
<ConstraintSet android:id="@+id/filterDark2">
<Constraint
android:id="@+id/indicator"
android:layout_width="50dp"
android:layout_height="24dp"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toStartOf="@+id/filter3Tv"
motion:layout_constraintStart_toEndOf="@+id/filter1Tv"
motion:layout_constraintTop_toTopOf="parent" />
@ -355,17 +370,16 @@
android:id="@+id/icon1Iv"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginStart="6dp"
android:alpha="0"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintStart_toStartOf="@+id/filter1Tv"
motion:layout_constraintTop_toTopOf="parent" />
<Constraint
android:id="@+id/filter1Tv"
android:layout_width="40dp"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginStart="-2dp"
android:layout_marginStart="6dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toTopOf="parent">
@ -378,7 +392,7 @@
android:id="@+id/icon2Iv"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginStart="6dp"
android:layout_marginStart="14dp"
android:alpha="1"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toEndOf="@+id/filter1Tv"
@ -401,7 +415,6 @@
android:id="@+id/icon3Iv"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginStart="6dp"
android:alpha="0"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toStartOf="@+id/filter3Tv"
@ -409,11 +422,13 @@
<Constraint
android:id="@+id/filter3Tv"
android:layout_width="40dp"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginEnd="-2dp"
android:layout_marginStart="14dp"
android:layout_marginEnd="6dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintStart_toEndOf="@+id/filter2Tv"
motion:layout_constraintTop_toTopOf="parent">
<CustomAttribute
motion:attributeName="textColor"
@ -424,27 +439,28 @@
<ConstraintSet android:id="@+id/filterDark3">
<Constraint
android:id="@+id/indicator"
android:layout_width="50dp"
android:layout_height="24dp"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="8dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintStart_toEndOf="@+id/filter2Tv"
motion:layout_constraintTop_toTopOf="parent" />
<Constraint
android:id="@+id/icon1Iv"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginStart="6dp"
android:alpha="0"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintStart_toStartOf="@+id/filter1Tv"
motion:layout_constraintTop_toTopOf="parent" />
<Constraint
android:id="@+id/filter1Tv"
android:layout_width="40dp"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginStart="-2dp"
android:layout_marginStart="6dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toTopOf="parent">
@ -457,17 +473,16 @@
android:id="@+id/icon2Iv"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginStart="6dp"
android:alpha="0"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toEndOf="@+id/filter1Tv"
motion:layout_constraintStart_toStartOf="@+id/filter2Tv"
motion:layout_constraintTop_toTopOf="parent" />
<Constraint
android:id="@+id/filter2Tv"
android:layout_width="40dp"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginStart="-2dp"
android:layout_marginStart="14dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toEndOf="@+id/filter1Tv"
motion:layout_constraintTop_toTopOf="parent">
@ -480,7 +495,7 @@
android:id="@+id/icon3Iv"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginStart="6dp"
android:layout_marginStart="14dp"
android:alpha="1"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toEndOf="@+id/filter2Tv"
@ -491,7 +506,9 @@
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginStart="2dp"
android:layout_marginEnd="6dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintStart_toEndOf="@+id/icon3Iv"
motion:layout_constraintTop_toTopOf="parent">
<CustomAttribute