Files
yusheng-android/MainModule/src/main/java/com/xscm/modulemain/widget/WheatLayoutManager.kt
2025-11-07 09:22:39 +08:00

640 lines
20 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.xscm.modulemain.widget
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.util.Log
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.annotation.Nullable
import com.google.android.flexbox.FlexboxLayout
import com.hjq.toast.ToastUtils
import com.opensource.svgaplayer.SVGAImageView
import com.opensource.svgaplayer.SVGAParser
import com.opensource.svgaplayer.SVGAVideoEntity
import com.xscm.moduleutil.R
import com.xscm.moduleutil.bean.RoomMessageEvent
import com.xscm.moduleutil.bean.room.RoomPitBean
import com.xscm.moduleutil.widget.CircularImage
import com.xscm.moduleutil.widget.GifAvatarOvalView
import com.xscm.moduleutil.widget.RoomMakeWheatView
import com.xscm.moduleutil.widget.RoomSingSongWheatView
import kotlin.math.roundToInt
/**
* 二卡八显示布局管理器(单例模式,支持预绘制和动态添加)
*/
class WheatLayoutSingManager private constructor(
private val appContext: Context
) {
// 内部根容器(预创建并提前绘制)
private var rootContainer: LinearLayout = LinearLayout(appContext).apply {
orientation = LinearLayout.VERTICAL
layoutParams = FlexboxLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
).apply {
// 添加居中设置
gravity = Gravity.CENTER_HORIZONTAL
}
setLayerType(View.LAYER_TYPE_HARDWARE, null)
}
// 麦位数据
private var pitList: List<RoomPitBean>? = null
// 单麦模式标记
private var isSingleMode = false
private var currentSinglePit = -1
private var singleWheatView: RoomSingSongWheatView? = null
private val multiWheatViews = mutableListOf<RoomSingSongWheatView>()
// 麦位索引映射
private val pitIndexMap = intArrayOf(9, 10, 1, 2, 3, 4, 5, 6, 7, 8)
// 预加载时的尺寸(基于屏幕尺寸预估)
private val preloadWidth: Int by lazy {
appContext.resources.displayMetrics.widthPixels -
appContext.resources.getDimensionPixelSize(R.dimen.dp_5) * 2
}
private val preloadHeight: Int by lazy {
appContext.resources.displayMetrics.heightPixels / 2 // 预估高度为屏幕一半
}
// 初始化时立即执行预绘制
init {
preDrawContainer()
}
/**
* 预绘制容器(核心预加载逻辑)
* 手动触发测量、布局、绘制流程,生成缓存
*/
private fun preDrawContainer() {
// 1. 测量使用预估尺寸AT_MOST模式适配wrap_content
rootContainer.measure(
View.MeasureSpec.makeMeasureSpec(preloadWidth, View.MeasureSpec.AT_MOST),
View.MeasureSpec.makeMeasureSpec(preloadHeight, View.MeasureSpec.AT_MOST)
)
// 2. 布局指定位置左上角为0,0
rootContainer.layout(
0,
0,
rootContainer.measuredWidth,
rootContainer.measuredHeight
)
// 3. 绘制:强制触发绘制,生成缓存
rootContainer.draw(Canvas())
}
/**
* 获取预绘制好的根容器供外部添加到FlexboxLayout
*/
fun getRootContainer(): LinearLayout = rootContainer
/**
* 设置麦位数据并刷新视图
*/
fun setWheatData(pitList: List<RoomPitBean>?) {
this.pitList = pitList
restoreMultiWheat()
// 数据更新后重新预绘制,确保缓存同步
preDrawContainer()
}
/**
* 为麦位视图设置点击事件(确保使用最新的 wheatClickListener
*/
private fun setupViewListeners(wheatView: RoomSingSongWheatView, pitNumber: Int) {
// 头像点击事件
val avatarView = wheatView.mRiv as CircularImage
avatarView.setOnClickListener {
// 直接使用当前的 wheatClickListener可能已被外部设置
wheatClickListener?.onWheatClick(wheatView, pitNumber)
}
// 魅力值点击事件
val charmView = wheatView.mCharmView
charmView.setOnClickListener {
ToastUtils.show("点击了麦位")
wheatClickListener?.onMeilingClick(wheatView, pitNumber)
}
// 整体点击事件
wheatView.setOnClickListener {
wheatClickListener?.onMeilingClick(wheatView, wheatView.pitNumber.toInt())
}
}
/**
* 设置麦位点击监听器(关键修改:设置后为已有视图重新绑定事件)
*/
fun setOnWheatClickListener(@Nullable listener: OnWheatClickListener?) {
this.wheatClickListener = listener
// 为已创建的所有麦位视图重新绑定点击事件(此时 listener 已生效)
multiWheatViews.forEach { view ->
val pitNumber = view.pitNumber.toIntOrNull() ?: return@forEach
setupViewListeners(view, pitNumber)
}
}
/**
* 恢复多麦模式布局
*/
private fun restoreMultiWheat() {
// 清空现有视图(保留缓存逻辑)
val screenWidth = getScreenWidth()
val itemWidth = screenWidth / 4.2
var row = createHorizontalRow()
if (multiWheatViews.size == 10) {
// 复用已有视图,仅更新数据
multiWheatViews.forEachIndexed { i, view ->
if (pitList != null) {
view.setData(pitList!![pitIndexMap[i] - 1])
}
}
} else {
// 创建新视图
for (i in 0 until 10) {
val pitNumber = pitIndexMap[i]
val wheatView = RoomSingSongWheatView(appContext).apply {
this.pitNumber = pitNumber.toString()
if (pitList != null) {
setData(pitList!![pitNumber - 1])
}
}
multiWheatViews.add(wheatView)
var params: LinearLayout.LayoutParams? = null
if (i == 0) {
val fixedHeightInDp = 110 // 固定高度为 100dp
val fixedHeightInPx = dpToPx(fixedHeightInDp) // 调用已有的 dpToPx 方法
// 第一个控件:左边距 86dp右边距 100dp
params = LinearLayout.LayoutParams(itemWidth.toInt(), fixedHeightInPx)
params.rightMargin = dpToPx(50)
} else if (i == 1) {
val fixedHeightInDp = 110 // 固定高度为 100dp
val fixedHeightInPx = dpToPx(fixedHeightInDp) // 调用已有的 dpToPx 方法
// 第二个控件:右边距 86dp
params = LinearLayout.LayoutParams(itemWidth.toInt(), fixedHeightInPx)
} else {
val fixedHeightInDp = 90 // 固定高度为 100dp
val fixedHeightInPx = dpToPx(fixedHeightInDp) // 调用已有的 dpToPx 方法
params = LinearLayout.LayoutParams(itemWidth.toInt() - 30, fixedHeightInPx + 30)
// 其他控件保持原有逻辑
params.setMargins(0, 0, 0, 0) // 不设右边距,由 row padding 控制
}
// val params = getLayoutParams(i, itemWidth.toInt())
wheatView.layoutParams = params
// 设置点击事件
setupViewListeners(wheatView, pitNumber)
row.addView(wheatView)
// 换行逻辑
handleRowBreak(i, row) { newRow -> row = newRow }
}
// 添加最后一行剩余视图
if (row.childCount > 0) {
rootContainer.addView(row)
}
isSingleMode = false
currentSinglePit = -1
}
}
/**
* 恢复PK模式下的多麦布局
*/
fun restoreMultiWheatPk(layoutType: Int) {
try {
if (layoutType == 1) {
rootContainer.removeAllViews()
}
} catch (e: Exception) {
return
}
val pitList = this.pitList ?: return
if (pitList.size < 10) return
val screenWidth = getScreenWidth()
val itemWidth = screenWidth / 8
var row = createHorizontalRow()
// 调整前两个麦位顺序
val (firstPit, secondPit) = when (layoutType) {
1 -> Pair(10, 9)
2 -> Pair(9, 10)
else -> Pair(9, 10)
}
// 添加前两个麦位
addWheatViewItem(row, firstPit, itemWidth * 2, layoutType)
addWheatViewItem(row, secondPit, itemWidth * 2, layoutType)
rootContainer.addView(row)
row = createHorizontalRow()
// 添加剩余8个麦位
for (i in 2 until 10) {
val pitNumber = pitIndexMap[i]
addWheatViewItem(row, pitNumber, itemWidth, layoutType)
if (i > 1 && (i - 2) % 4 == 3) {
rootContainer.addView(row)
row = createHorizontalRow()
}
}
if (row.childCount > 0) {
rootContainer.addView(row)
}
isSingleMode = false
currentSinglePit = -1
// 预绘制更新
preDrawContainer()
}
/**
* 添加单个麦位视图到行布局
*/
private fun addWheatViewItem(
row: LinearLayout,
pitNumber: Int,
itemWidth: Int,
layoutType: Int
) {
val pitList = this.pitList ?: return
val wheatView = RoomSingSongWheatView(appContext).apply {
this.pitNumber = pitNumber.toString()
setData(pitList[pitNumber - 1])
}
val params = when (pitNumber) {
9, 10 -> {
val fixedHeight = appContext.resources.getDimensionPixelSize(R.dimen.dp_90)
if (pitNumber == 9) {
LinearLayout.LayoutParams(itemWidth - 40, fixedHeight).apply {
if (layoutType == 1) {
rightMargin = appContext.resources.getDimensionPixelSize(R.dimen.dp_1)
setMargins(20, -30, -20, 0)
} else {
leftMargin = appContext.resources.getDimensionPixelSize(R.dimen.dp_1)
setMargins(-30, -20, 0, 0)
}
}
} else {
LinearLayout.LayoutParams(itemWidth - 80, fixedHeight).apply {
if (layoutType == 1) {
setMargins(-30, 10, 0, 0)
} else {
setMargins(0, 10, -30, 0)
}
}
}
}
else -> {
val fixedHeight = appContext.resources.getDimensionPixelSize(R.dimen.dp_60)
LinearLayout.LayoutParams(itemWidth + 15, fixedHeight + 20).apply {
setMargins(-20, -20, -20, 0)
}
}
}
wheatView.layoutParams = params
wheatView.setOnClickListener {
wheatClickListener?.let { listener ->
val pitNum = wheatView.pitNumber.toInt()
if (layoutType == 1) {
listener.onWheatClick(wheatView, pitNum)
} else {
listener.onMakeWheatClick(wheatView, pitNum)
}
}
}
row.addView(wheatView)
}
/**
* 创建水平方向的行布局
*/
private fun createHorizontalRow(): LinearLayout {
return LinearLayout(appContext).apply {
orientation = LinearLayout.HORIZONTAL
layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
).apply {
// 添加行内居中
gravity = Gravity.CENTER_HORIZONTAL
}
}
}
/**
* 获取麦位视图的布局参数
*/
private fun getLayoutParams(index: Int, itemWidth: Int): LinearLayout.LayoutParams {
return when (index) {
0 -> {
LinearLayout.LayoutParams((itemWidth * 1.7).toInt(), dpToPx(110)).apply {
// 移除右间距,通过容器居中自动平衡
rightMargin = (getScreenWidth() - ((itemWidth * 1.7).toInt() * 2)) / 2
}
}
1 -> {
LinearLayout.LayoutParams((itemWidth * 1.7).toInt(), dpToPx(110))
}
else -> {
val margin = (getScreenWidth() - (itemWidth * 4)) / 4 / 2
// 统一边距逻辑
LinearLayout.LayoutParams((itemWidth *1.3).toInt() , dpToPx(110)).apply {
setMargins(0, 0, 0, 0)
}
}
}
}
/**
* 处理换行逻辑
*/
private fun handleRowBreak(
index: Int,
currentRow: LinearLayout,
newRowCallback: (LinearLayout) -> Unit
) {
when {
index == 1 -> {
rootContainer.addView(currentRow)
newRowCallback(createHorizontalRow())
}
index > 1 && (index - 2) % 4 == 3 -> {
rootContainer.addView(currentRow)
newRowCallback(createHorizontalRow())
}
}
}
/**
* 创建麦位视图
*/
private fun createWheatView(pitNumber: Int): RoomSingSongWheatView {
val pitList = this.pitList ?: throw IllegalArgumentException("pitList is null")
return RoomSingSongWheatView(appContext).apply {
this.pitNumber = pitNumber.toString()
setData(pitList[pitNumber - 1])
}
}
/**
* 创建申请麦位视图
*/
private fun createRoomMakeWheatView(pitNumber: Int): RoomMakeWheatView {
val pitList = this.pitList ?: throw IllegalArgumentException("pitList is null")
return RoomMakeWheatView(appContext).apply {
this.pitNumber = pitNumber.toString()
setData(pitList[pitNumber - 1])
}
}
/**
* dp转px
*/
private fun dpToPx(dp: Int): Int {
return (dp * appContext.resources.displayMetrics.density).roundToInt()
}
/**
* 获取屏幕宽度
*/
private fun getScreenWidth(): Int {
return appContext.resources.displayMetrics.widthPixels
}
/**
* 更新麦位交换数据
*/
fun setUpData(event: RoomMessageEvent) {
val fromPit = event.text.from_pit_number ?: return
val toPitNumber = event.text.to_pit_number ?: return
val fromWheatView = findWheatViewByPitNumber(fromPit.toInt())
val toWheatView = findWheatViewByPitNumber(toPitNumber.toInt())
if (fromWheatView == null || toWheatView == null) return
// 交换麦位数据
val fromPitBean = fromWheatView.pitBean
val toPitBean = toWheatView.pitBean
val tmpNumber = fromPitBean.pit_number
fromPitBean.pit_number = toPitBean.pit_number
toPitBean.pit_number = tmpNumber
toWheatView.setData(fromPitBean)
fromWheatView.setData(toPitBean)
// 清空原麦位数据
multiWheatViews.forEach { view ->
if (view.pitBean.user_id == event.text.fromUserInfo.user_id.toString() &&
view.pitBean.pit_number != toPitNumber
) {
view.pitBean.apply {
charm = ""
user_id = ""
dress = ""
avatar = ""
nickname = ""
sex = ""
user_code = ""
dress_picture = ""
}
view.setData(view.pitBean)
}
}
// 预绘制更新
preDrawContainer()
}
/**
* 更新单个麦位信息
*/
fun updateSingleWheat(pitBean: RoomPitBean, pitNumber: Int) {
if (pitNumber < 1 || pitNumber > 10) return
if (isSingleMode && currentSinglePit != pitNumber) return
findWheatViewByPitNumber(pitNumber)?.setData(pitBean)
// 预绘制更新
preDrawContainer()
}
/**
* 更新麦位魅力值
*/
fun upDataCharm(pitBean: RoomPitBean, pitNumber: Int) {
if (pitNumber < 1 || pitNumber > 10) return
if (isSingleMode && currentSinglePit != pitNumber) return
findWheatViewByPitNumber(pitNumber)?.setCharm(pitBean.charm)
// 预绘制更新
preDrawContainer()
}
/**
* 根据麦位号查找视图
*/
@Nullable
private fun findWheatViewByPitNumber(pitNumber: Int): RoomSingSongWheatView? {
for (i in 0 until rootContainer.childCount) {
val row = rootContainer.getChildAt(i)
when (row) {
is LinearLayout -> {
for (j in 0 until row.childCount) {
val child = row.getChildAt(j)
if (child is RoomSingSongWheatView && child.pitNumber.toInt() == pitNumber) {
return child
}
}
}
is RoomSingSongWheatView -> {
if (row.pitNumber.toInt() == pitNumber) {
return row
}
}
}
}
return null
}
/**
* 释放资源
*/
fun release() {
try {
// 释放子视图资源
multiWheatViews.forEach { it.releaseResources() }
multiWheatViews.clear()
// 清空容器
rootContainer.removeAllViews()
// 从父布局移除自身
(rootContainer.parent as? ViewGroup)?.removeView(rootContainer)
// 清除绘制缓存
rootContainer.setLayerType(View.LAYER_TYPE_NONE, null)
} catch (e: Exception) {
// 忽略异常
}
// 清空数据引用
pitList = null
singleWheatView = null
wheatClickListener = null
}
/**
* 麦位点击事件接口
*/
interface OnWheatClickListener {
fun onWheatClick(view: RoomSingSongWheatView, pitNumber: Int)
fun onMakeWheatClick(view: RoomSingSongWheatView, pitNumber: Int)
fun onMeilingClick(view: RoomSingSongWheatView, pitNumber: Int)
}
@Nullable
private var wheatClickListener: OnWheatClickListener? = null
/**
* 单例实现
*/
companion object {
@SuppressLint("StaticFieldLeak")
@Volatile
private var instance: WheatLayoutSingManager? = null
/**
* 初始化单例(预加载视图)
* 建议在Application.onCreate()中调用
*/
fun init(context: Context) {
if (instance == null) {
synchronized(WheatLayoutSingManager::class.java) {
if (instance == null) {
instance = WheatLayoutSingManager(context.applicationContext)
}
}
}
}
/**
* 获取单例实例
* 必须先调用init()初始化
*/
fun getInstance(): WheatLayoutSingManager {
return instance
?: throw IllegalStateException("请先调用init()初始化WheatLayoutSingManager")
}
/**
* 销毁单例(退出应用时调用)
*/
fun destroyInstance() {
instance?.release()
instance = null
}
}
private var svgaVideoItem: SVGAVideoEntity? = null
fun bindSvga(view: SVGAImageView, context: Context, assetName: String) {
if (svgaVideoItem == null) {
val parser = SVGAParser(context)
parser.decodeFromAssets(assetName, object : SVGAParser.ParseCompletion {
override fun onComplete(videoItem: SVGAVideoEntity) {
// videoItem 可能为 null按需处理
videoItem.let {
svgaVideoItem = it
view.setVideoItem(it)
view.startAnimation()
}
}
override fun onError() {
// 解析失败的处理
Log.e("SVGA", "decodeFromAssets error: $assetName")
}
})
} else {
view.setVideoItem(svgaVideoItem)
view.startAnimation()
}
}
fun releaseFromParent() {
rootContainer.let { container ->
(container.parent as? ViewGroup)?.removeView(container)
}
}
}