前言
本文参考辉哥的Android常见多条件筛选菜单(美团、58),通过自己动手来实现效果,以达到对自定义View技能的进一步提升。
最终效果
实现思路
- 整体为组合View,主要由最上面菜单栏、下方内容部分组成;【内容部分包含菜单内容以及阴影部分】
- 默认状态下下方内容部分应该隐藏;
- 当点击菜单item时,通过对内容部分布局进行垂直方向平移,同时对阴影部分进行透明度变化动画达到下拉效果;
- 当再次点击相同item时,同样对内容部分布局进行垂直方向平移,同时对阴影部分进行透明度变化动画达到隐藏效果,而如果点击不同item时,仅进行内容变化,不进行动画效果;
具体实现
- 定义抽象适配器,用于多种布局适配
package com.crystal.view.animation
import android.view.View
import android.view.ViewGroup
/**
* 用于适配多种布局显示
* on 2022/11/9
*/
abstract class BasePopAdapter {
/**
* 一共多少条
*/
abstract fun getCount(): Int
/**
* 获取当前的tab view
* @param parent 父布局
* @param position 对应position
*/
abstract fun getTabView(parent: ViewGroup, position: Int): View
/**
* 获取当前的内容 view
* @param parent 父布局
* @param position 对应position
*/
abstract fun getMenuContentView(parent: ViewGroup, position: Int): View
/**
* 点击menu展开布局
*/
open fun menuOpen(view: View) {
}
/**
* 点击阴影关闭布局
*/
open fun menuClose(view: View) {
}
}
- 具体适配器,绑定对应布局文件
package com.crystal.view.animation
import android.content.Context
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import com.crystal.view.R
/**
* 具体实现
* on 2022/11/9
*/
class CommonPopAdapter(val context: Context) : BasePopAdapter() {
private val items = mutableListOf("类型", "品牌", "价格", "更多")
override fun getCount(): Int {
return items.size
}
override fun getTabView(parent: ViewGroup, position: Int): View {
val tv = LayoutInflater.from(context)
.inflate(R.layout.layout_item_tab_view, parent, false) as TextView
tv.text = items[position]
tv.setTextColor(Color.BLACK)
return tv
}
override fun getMenuContentView(parent: ViewGroup, position: Int): View {
val tv = LayoutInflater.from(context)
.inflate(R.layout.layout_item_menu_view, parent, false) as TextView
tv.text = items[position]
tv.setTextColor(Color.BLACK)
return tv
}
override fun menuOpen(view: View) {
super.menuOpen(view)
(view as TextView).setTextColor(Color.RED)
}
override fun menuClose(view: View) {
super.menuClose(view)
(view as TextView).setTextColor(Color.BLACK)
}
}
- 适配器对应绑定布局文件
layout_item_tab_view
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:padding="10dp"
android:textColor="@color/black"
android:textSize="16sp">
</TextView>
layout_item_menu_view
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="11111"
android:textColor="@color/black"
android:textSize="16sp">
</TextView>
- 自定义
ListDataPopView
实现多条目菜单选择
package com.crystal.view.animation
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ObjectAnimator
import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.LinearLayout
/**
* 自定义多条目菜单选择View
* on 2022/11/9
*/
class ListDataPopView : LinearLayout {
/**
* 最上方的菜单栏
*/
private lateinit var menuLayout: LinearLayout
/**
* 下方的内容栏【包含菜单内容contentMenuLayout+阴影部分shadowLayout】
*/
private lateinit var contentLayout: FrameLayout
/**
* 菜单内容
*/
private lateinit var contentMenuLayout: FrameLayout
/**
* 最下方的阴影布局
*/
private lateinit var shadowLayout: View
private lateinit var adapter: BasePopAdapter
/**
* 菜单内容的高度
*/
private var contentMenuHeight = 0
constructor(context: Context?) : this(context, null)
constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
context, attrs, defStyleAttr
) {
initLayout()
}
/**
* 添加相关布局
*/
private fun initLayout() {
//设置整体布局方向
orientation = VERTICAL
//最上面的menuView
menuLayout = LinearLayout(context)
menuLayout.setBackgroundColor(Color.parseColor("#999999"))
val menuLayoutParams =
LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
menuLayout.layoutParams = menuLayoutParams
addView(menuLayout)
//下面的contentView和阴影部分用FrameLayout进行包裹
contentLayout = FrameLayout(context)
val contentLayoutParams = LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0)
contentLayoutParams.weight = 1f
contentLayout.layoutParams = contentLayoutParams
addView(contentLayout)
//阴影部分【默认状态不显示】
shadowLayout = View(context)
shadowLayout.alpha = 0f
shadowLayout.setBackgroundColor(Color.parseColor("#999999"))
contentLayout.addView(shadowLayout)
//菜单内容布局
contentMenuLayout = FrameLayout(context)
contentMenuLayout.setBackgroundColor(Color.WHITE)
contentLayout.addView(contentMenuLayout)
//阴影部分布局点击支持隐藏
shadowLayout.setOnClickListener {
if (currentPosition != -1) {
menuClose(currentPosition)
}
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val height = MeasureSpec.getSize(heightMeasureSpec)
if (contentMenuHeight == 0 && height > 0) {
//菜单内容的高度设置为整体高度的0.75
contentMenuHeight = (0.75f * height).toInt()
//设置下方整体内容高度
val contentMenuLayoutParams = contentMenuLayout.layoutParams
contentMenuLayoutParams.height = contentMenuHeight
contentMenuLayout.layoutParams = contentMenuLayoutParams
//一开始菜单布局未显示【将布局平移上去】
contentMenuLayout.translationY = (-contentMenuHeight).toFloat()
}
}
fun setAdapter(adapter: BasePopAdapter) {
this.adapter = adapter
for (position in 0 until adapter.getCount()) {
//添加tabView
val tabView = adapter.getTabView(menuLayout, position)
val tabViewParams = tabView.layoutParams as LayoutParams
tabViewParams.weight = 1f
menuLayout.addView(tabView)
setMenuClickListener(tabView, position)
//添加contentView
val menuView = adapter.getMenuContentView(contentMenuLayout, position)
menuView.visibility = GONE
contentMenuLayout.addView(menuView)
}
}
/**
* 用于记录当前打开的位置
*/
private var currentPosition = -1
/**
* 设置menu点击监听
*/
private fun setMenuClickListener(tabView: View, position: Int) {
tabView.setOnClickListener {
when (currentPosition) {
-1 -> {
//打开当前的tab
menuOpen(position)
}
position -> {
menuClose(position)
}
else -> {
// 将之前打开的隐藏掉
val currentShowMenu = contentMenuLayout.getChildAt(currentPosition)
currentShowMenu.visibility = GONE
adapter.menuClose(menuLayout.getChildAt(currentPosition))
//显示当前的
currentPosition = position
val finalShowMenu = contentMenuLayout.getChildAt(position)
finalShowMenu.visibility = VISIBLE
adapter.menuOpen(tabView)
}
}
}
}
private fun menuOpen(position: Int) {
if (isAnimationExecute) {
return
}
//将内容显示出来
contentMenuLayout.getChildAt(position).visibility = VISIBLE
val contentMenuAnimator =
ObjectAnimator.ofFloat(contentMenuLayout, "translationY", -contentMenuHeight * 1f, 0f)
contentMenuAnimator.duration = 500
contentMenuAnimator.start()
//透明度动画
val alphaAnimator = ObjectAnimator.ofFloat(shadowLayout, "alpha", 0f, 1f)
alphaAnimator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator?) {
super.onAnimationStart(animation)
isAnimationExecute = true
//设置menu字体颜色
adapter.menuOpen(menuLayout.getChildAt(position))
}
override fun onAnimationEnd(animation: Animator?) {
super.onAnimationEnd(animation)
isAnimationExecute = false
currentPosition = position
}
})
alphaAnimator.start()
}
private var isAnimationExecute = false
/**
* 关闭menu
*/
private fun menuClose(position: Int) {
if (isAnimationExecute) {
return
}
val contentMenuAnimator =
ObjectAnimator.ofFloat(contentMenuLayout, "translationY", 0f, -contentMenuHeight * 1f)
contentMenuAnimator.duration = 500
contentMenuAnimator.start()
//透明度动画
val alphaAnimator = ObjectAnimator.ofFloat(shadowLayout, "alpha", 1f, 0f)
alphaAnimator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator?) {
super.onAnimationStart(animation)
isAnimationExecute = true
adapter.menuClose(menuLayout.getChildAt(position))
}
override fun onAnimationEnd(animation: Animator?) {
super.onAnimationEnd(animation)
//将内容隐藏
contentMenuLayout.getChildAt(position).visibility = GONE
currentPosition = -1
isAnimationExecute = false
}
})
alphaAnimator.start()
}
}
总结
通过实现本文效果,不仅了解了适配器模式在实际开发中的重要作用,同时也对自定义ViewGroup
有了进一步的认知,很多情况下我们可以通过代码进行View的添加,而不仅仅通过布局文件来完成。
结语
如果以上文章对您有一点点帮助,希望您不要吝啬的点个赞加个关注,您每一次小小的举动都是我坚持写作的不懈动力!ღ( ´・ᴗ・` )