前言
哈喽各位好久不见,最近也是五大联赛热播期,也一直有看比赛。 那么经常看比赛的小伙伴肯定都知道足球阵容,像什么433、352、424、4231等等之类的。
那么今天这篇文章就是说去实现一下可视化的阵容图数据哈。
注:因涉及到数据隐私问题,此篇文章仅提供思路,不提供源码和数据。
开工

实现
常见阵容
3行阵容:后卫、中场、前锋
4行阵容:后卫、后腰、前腰、前锋
5行阵容:后卫、后腰、中场、前腰、前锋
业务逻辑
1、守门员位置固定
2、根据阵容摆放球员位置
实现思路
1、足球场使用一张背景图等比例计算宽高
2、每行阵容球员坐标根据背景图区分上下半场在百分比计算
3、每行阵容球员的坐标数据通过工厂抽象出来
代码
坐标代码
kotlin
// 坐标数据接口类
interface FBILineupPosition {
/**
* 获取 球员Y坐标
*/
fun getY(positionLabel: Int): Float
/**
* 获取 守门员Y坐标
*/
fun getGoalkeeperY(): Float = 0.035f
}
kotlin
// 3行阵容坐标数据实现类
class FBILineupPosition03Impl : FBILineupPosition {
override fun getY(positionLabel: Int): Float {
return when (positionLabel) {
0 -> getGoalkeeperY()
1 -> getRearGuardY()
2 -> getMidfieldY()
3 -> getVanguardY()
else -> -1f
}
}
/**
* 获取 后卫Y坐标
*/
private fun getRearGuardY(): Float = 0.235f
/**
* 获取 中场Y坐标
*/
private fun getMidfieldY(): Float = 0.5f
/**
* 获取 前锋Y坐标
*/
private fun getVanguardY(): Float = 0.81f
}
kotlin
// 4行阵容坐标数据实现类
class FBILineupPosition04Impl : FBILineupPosition {
override fun getY(positionLabel: Int): Float {
return when (positionLabel) {
0 -> getGoalkeeperY()
1 -> getRearGuardY()
2 -> getBackWaist()
3 -> getAroundWaist()
4 -> getVanguardY()
else -> -1f
}
}
/**
* 获取 后卫Y坐标
*/
private fun getRearGuardY(): Float = 0.235f
/**
* 获取 后腰Y坐标
*/
private fun getBackWaist(): Float = 0.45f
/**
* 获取 前腰Y坐标
*/
private fun getAroundWaist(): Float = 0.65f
/**
* 获取 前锋Y坐标
*/
private fun getVanguardY(): Float = 0.81f
}
kotlin
// 4行阵容坐标数据实现类
class FBILineupPosition05Impl : FBILineupPosition {
override fun getY(positionLabel: Int): Float {
return when (positionLabel) {
0 -> getGoalkeeperY()
1 -> getRearGuardY()
2 -> getBackWaist()
3 -> getMidfieldY()
4 -> getAroundWaist()
5 -> getVanguardY()
else -> -1f
}
}
/**
* 获取 后卫Y坐标
*/
private fun getRearGuardY(): Float = 0.235f
/**
* 获取 后腰Y坐标
*/
private fun getBackWaist(): Float = 0.35f
/**
* 获取 中场Y坐标
*/
private fun getMidfieldY(): Float = 0.5f
/**
* 获取 前腰Y坐标
*/
private fun getAroundWaist(): Float = 0.75f
/**
* 获取 前锋Y坐标
*/
private fun getVanguardY(): Float = 0.85f
}
球员View
这里我使用LinearLayout实现,因为可以直接平分宽度,不需要我们自己去计算了。
(这里确实是我懒了一点)
kotlin
class FBLineupPlayerView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : LinearLayout(context, attrs) {
private val mData = mutableListOf<...>()
private val mLayoutInflater by lazy { LayoutInflater.from(context) }
init {
this.orientation = HORIZONTAL
}
/**
* 添加 数据
*/
fun addData(itemWidth: Int, data: ...) {
this.mData.add(data)
// childView
val childView = LayoutFbLineupPlayerBinding.inflate(mLayoutInflater, this, false)
// Avatar
Glide or Resource // 自行处理
// Name
childView.tvName.text = ...
// Number
childView.tvNumber.text = ...
// itemWidth
val childWidth = if (itemWidth <= 0) {
WRAP_CONTENT
} else {
itemWidth
}
// AddView
this.addView(childView.root, ViewGroup.LayoutParams(childWidth, WRAP_CONTENT))
}
}
ViewGroup
kotlin
class FBLineupView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ViewGroup(context, attrs, defStyleAttr) {
private var tag = FBLineupView::class.java.simpleName
private var mHalfHeight = 0f
private var mHomeFormation: List<Char>? = null
private var mAwayFormation: List<Char>? = null
private val mHomePlayerViews = SparseArray<FBLineupPlayerView>()
private val mAwayPlayerViews = SparseArray<FBLineupPlayerView>()
private val mScreenSize by lazy { DensityUtil.getScreenSize(context) }
private val mHomeTeamBinding by lazy {
LayoutFbLineupTeamBinding.inflate(LayoutInflater.from(context)).also {
this@FBLineupView.addView(it.root)
}
}
private val mAwayTeamBinding by lazy {
LayoutFbLineupTeamBinding.inflate(LayoutInflater.from(context)).also {
this@FBLineupView.addView(it.root)
}
}
private var mHomeLineupPosition: FBILineupPosition? = null
private var mAwayLineupPosition: FBILineupPosition? = null
private val mBackdrop: Drawable? by lazy {
ContextCompat.getDrawable(context, R.drawable.ic_match_fb_lineup_backdrop)
}
init {
this.background = mBackdrop
// GONE
this.visibility = View.GONE
}
/**
* 设置 数据集
*
* @param data 比赛数据
* @param homeFormation 主队阵型
* @param awayFormation 客队阵型
* @param teamData 队伍信息
*/
fun setData(
data: ...?,
homeFormation: String?,
awayFormation: String?,
teamData: List<...>?
) {
if (TextUtils.isEmpty(homeFormation) || TextUtils.isEmpty(awayFormation)) {
this.visibility = View.GONE
return
}
if (teamData.isNullOrEmpty()) {
this.visibility = View.GONE
return
}
try {
this.visibility = View.VISIBLE
// 处理数据
this.handleData(data, homeFormation, awayFormation)
// 解析阵型 格式:3-5-2 根据-进行分割计算
if (!TextUtils.isEmpty(homeFormation)) {
this.mHomeFormation = homeFormation?.toCharArray()?.toList()?.sorted()
this.mHomeLineupPosition = getLineupPosition(mHomeFormation)
}
if (!TextUtils.isEmpty(awayFormation)) {
this.mAwayFormation = awayFormation?.toCharArray()?.toList()?.sorted()
this.mAwayLineupPosition = getLineupPosition(mAwayFormation)
}
// 主/客
this.addPlayerViews(true, teamData, mHomeFormation, mHomePlayerViews)
this.addPlayerViews(false, teamData, mAwayFormation, mAwayPlayerViews)
} catch (e: Exception) {
e.printStackTrace()
LogUtils.d(tag, "setData() called with: Exception = ${e.message}")
// GONE
this.visibility = View.GONE
}
}
/**
* 处理 数据
*/
private fun handleData(
data: ...?, homeFormation: String?, awayFormation: String?,
) {
if (data == null) {
this.mHomeTeamBinding.root.visibility = View.GONE
this.mAwayTeamBinding.root.visibility = View.GONE
return
}
this.mHomeTeamBinding.root.visibility = View.VISIBLE
this.mAwayTeamBinding.root.visibility = View.VISIBLE
// 主队名称
this.mHomeTeamBinding.tvName.text = ...
// 主队阵型
val finalHomeFormation = homeFormation?.replace("", "-")
?.replaceFirst("-", "")
?.trimEnd('-')
this.mHomeTeamBinding.tvFormation.text = ...
// 主队LOGO
Glide or Resource // 自行处理
// 客队名称
this.mAwayTeamBinding.tvName.text = ...
// 客队阵型
val finalAwayFormation = awayFormation?.replace("", "-")
?.replaceFirst("-", "")
?.trimEnd('-')
this.mAwayTeamBinding.tvFormation.text = ...
// 客队LOGO
Glide or Resource // 自行处理
}
/**
* 清空 数据
*/
fun clearData() {
this.mHomePlayerViews.clear()
this.mAwayPlayerViews.clear()
this.mHomeFormation = null
this.mAwayFormation = null
this.mHomeLineupPosition = null
this.mAwayLineupPosition = null
// RemoveAllViews
this.removeAllViews()
}
/**
* 添加 成员View
*/
private fun addPlayerViews(
isHomeTeam: Boolean = true,
team: List<...>?,
formation: List<Char>?,
cache: SparseArray<FBLineupPlayerView>
) {
if (team == null || team.isEmpty()) {
LogUtils.d(tag, "addPlayerViews() 成员数据为空哦!")
return
}
val maxFormation = if (!formation.isNullOrEmpty()) {
formation.lastOrNull()?.toString()?.toInt() ?: 0
} else {
0
}
val itemWidth = mScreenSize[0] / maxFormation
// forEach
team.forEach { itemData ->
val playerData = if (isHomeTeam) {
itemData.homePlayerList
} else {
itemData.awayPlayerList
}
if (playerData != null) {
val key = (playerData.positionLabel ?: -1)
// Container
val cacheContainer = cache.get(key)
if (cacheContainer == null) {
val playerView = FBLineupPlayerView(context).also {
it.addData(itemWidth, playerData)
cache.put(key, it)
}
this.addView(playerView, LayoutParams(WRAP_CONTENT, WRAP_CONTENT))
} else {
cacheContainer.addData(itemWidth, playerData)
}
}
}
}
/**
* 获取 球员坐标
*
* @param formation 阵型
*
* 出场的行数:
* 3行:守门员,后卫,中场,前锋
* 4行:守门员,后卫,后腰,前腰,前锋
* 5行:守门员,后卫,后腰,中场,前腰,前锋
*/
private fun getLineupPosition(formation: List<Char>?): FBILineupPosition? {
return when (formation?.size) {
3 -> FBILineupPosition03Impl()
4 -> FBILineupPosition04Impl()
5 -> FBILineupPosition05Impl()
else -> null
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
// 测量所有子View
this.measureChildren(widthMeasureSpec, heightMeasureSpec)
// 屏幕高度
val screenWidth = mScreenSize[0]
// 等比例背景宽高
var wRatio = screenWidth / (mBackdrop?.intrinsicWidth?.toFloat() ?: 1f)
if (wRatio == 0f) wRatio = 1f
val height = (mBackdrop?.intrinsicHeight ?: 1) * wRatio
// 计算半场高度
this.mHalfHeight = height / 2f
// 总高度
this.setMeasuredDimension(screenWidth, height.toInt())
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
// 主队
this.onLayoutHome(right)
// 客队
this.onLayoutAway(right)
}
/**
* 主队
*/
private fun onLayoutHome(parentRight: Int) {
try {
// Team
val offset = resources.getDimensionPixelSize(R.dimen.dp_16)
val teamWidth = mHomeTeamBinding.root.measuredWidth
val teamHeight = mHomeTeamBinding.root.measuredHeight
// 定位位置
this.mHomeTeamBinding.root.layout(
offset, offset, (parentRight - teamWidth), (offset + teamHeight)
)
// Player
this.mHomePlayerViews.forEach { index, itemView ->
// 根据位置标签获取坐标位置
val y = mHomeLineupPosition?.getY(index) ?: -1f
// 校验
if (y > -1f) {
// ItemView 宽高
val width = itemView.measuredWidth
val height = itemView.measuredHeight
// ItemView 矩形区域的左、上、右、下
val left = ((parentRight / 2) - (width / 2))
val top = (mHalfHeight * y).toInt()
val right = (parentRight - left)
val bottom = (top + height)
// 定位位置
itemView.layout(left, top, right, bottom)
}
}
} catch (e: Exception) {
e.printStackTrace()
LogUtils.d(tag, "onLayoutHome() called with: Exception = ${e.message}")
}
}
/**
* 客队
*/
private fun onLayoutAway(parentRight: Int) {
try {
// Team
val offset = resources.getDimensionPixelSize(R.dimen.dp_16)
val teamWidth = mAwayTeamBinding.root.measuredWidth
val teamHeight = mAwayTeamBinding.root.measuredHeight
val top = (height - offset - teamHeight)
// 定位位置
this.mAwayTeamBinding.root.layout(
offset, top, (parentRight - teamWidth), (top + teamHeight)
)
// Player
this.mAwayPlayerViews.forEach { index, itemView ->
// 根据位置标签获取坐标位置
val y = 1 - (mAwayLineupPosition?.getY(index) ?: -1f)
// 校验
if (y > -1f) {
itemView.visibility = View.VISIBLE
// ItemView 宽高
val width = itemView.measuredWidth
val height = itemView.measuredHeight
// ItemView 矩形区域的左、上、右、下
val left = ((parentRight / 2) - (width / 2))
val bottom = (mHalfHeight + (mHalfHeight * y)).toInt()
val right = (parentRight - left)
val top = (bottom - height)
// 定位位置
itemView.layout(left, top, right, bottom)
} else {
itemView.visibility = View.GONE
}
}
} catch (e: Exception) {
e.printStackTrace()
LogUtils.d(tag, "onLayoutAway() called with: Exception = ${e.message}")
}
}
}
其实也很简单,就是利用ViewGroup的onLayout方法,然后根据百分比定位一下球员的Y坐标即可
再放几张效果图
3行阵容 | 4行阵容 | 5行阵容 |
---|---|---|
![]() |
![]() |
-我还没见到过- |
到这里就结束了,其实这篇文章只是同样的体育爱好者提供一个实现思路,当然如果有更好的实现方式或方案也希望各位在评论区留言讨论,我秒回复哦~ Bye