640 lines
20 KiB
Kotlin
640 lines
20 KiB
Kotlin
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)
|
||
}
|
||
}
|
||
|
||
} |