Kotlin写一个自定义的菜单控件

首先贴一下效果图

思路:菜单控件分两部分,一是点击的子按钮(RecordButton),二是包裹着子按钮的容器(RecordMenu)。

子按钮负责显示文字及背景颜色和点击事件,父容器主要控制子控件的位置和动画显示。

实现:

子按钮,先贴代码

class RecordButton : RelativeLayout {
    /** 控件显示的文本*/
    lateinit var textValue: String
    /** 控件显示的文本字体大小*/
    private var textSize: Float = 18f
    /** 控件显示的文本字体颜色*/
    private var textColor: Int = Color.BLACK
    /** 控件按下时显示的文本字体颜色*/
    private var textColorPress: Int = Color.WHITE
    /** 控件显示的背景颜色*/
    private var backColorNormal: Int = R.drawable.bg_menu_item
    /** 控件按下时显示的背景颜色*/
    private var backColorPress: Int = R.drawable.bg_menu_item_press
    /** 控件是否是主按钮*/
    var isSwitchMain: Boolean = false
    /** 按钮按下时的时间*/
    var pressBtnTime: Long = 0L
    /** 按钮抬起时的时间*/
    var upBtnTime: Long = 0L
    /** 事件是否是点击事件*/
    var isClick: Boolean = false
    /** 点击事件是否打开*/
    var isOpen: Boolean = false
    /** 文本控件*/
    private lateinit var textView: TextView
    /** 监听事件*/
    var onRecordItemClickListener: OnRecordItemClickListener? = null

    constructor(context: Context,
                textValue: String,
                textSize: Float,
                textColor: Int,
                backColorNormal: Int,
                textColorPress: Int,
                backColorPress: Int) : this(context) {
        this.textValue = textValue
        this.textSize = textSize
        this.textColor = textColor
        this.backColorNormal = backColorNormal
        this.isSwitchMain = isSwitchMain
        this.textColorPress = textColorPress
        this.backColorPress = backColorPress
        setBackgroundResource(backColorNormal)

        textView = TextView(context)
        textView.text = textValue
        textView.gravity = CENTER
        textView.setTextColor(textColor)
        textView.textSize = textSize
        var ll = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
        ll.addRule(CENTER_IN_PARENT)
        addView(textView, ll)
    }

    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) {

    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        when (event?.action) {
            MotionEvent.ACTION_DOWN -> {
                pressBtnTime = System.currentTimeMillis()
                setBackgroundResource(backColorPress)
                textView.setTextColor(textColorPress)
                return true
            }
            MotionEvent.ACTION_MOVE -> {
            }
            MotionEvent.ACTION_UP -> {
                upBtnTime = System.currentTimeMillis()
                setBackgroundResource(backColorNormal)
                textView.setTextColor(textColor)
                isClick = (upBtnTime - pressBtnTime) / 1000 < 0.5
            }
        }
        if (isClick) {
            onRecordItemClickListener?.onClick(isSwitchMain, textValue,isOpen)
            isOpen = !isOpen
        }
        return true
    }
}

这里主要用一个RelativeLayout包裹着一个TextView,这么写是为了防止以后扩展,需要添加图片什么的,关于这个样式和显示没什么好说的,主要的就是点击事件,在触摸事件中判断按下和抬起的时间差,如果时间差小于0.5秒则断定为点击。

包裹容器

class RecordMenu : RelativeLayout{
    /** 子按钮半径*/
    private var itemRadius: Int = 0
    /*** 按钮间距*/
    private var itemMargin: Int = 0
    /** 动画时间*/
    private var duration: Long = 0
    /** 字体大小*/
    private var itemFontSize = 18f
    /** 字体正常颜色*/
    private var itemFontColorN = Color.BLACK
    /** 点击时字体颜色*/
    private var itemFontColorP = Color.WHITE
    /** 按钮正常背景*/
    private var itemBackDrawableN = R.drawable.bg_menu_item
    /** 按钮点击背景*/
    private var itemBackDrawableP = R.drawable.bg_menu_item_press
    /** 是否是展开状态*/
    private var isOpen: Boolean = false
    /** 动画是否正在运行*/
    private var isRun: Boolean = false
    /** 子控件监听*/
    private var recordListener = RecordListener()
    /** 上一级的监听事件*/
    var onRecordItemClickListener: OnRecordItemClickListener? = null

    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) {
        init(context, attrs)
    }

    override fun onLayout(change: Boolean, l: Int, t: Int, r: Int, b: Int) {
        /** 画出每个子控件的位置*/
        for (i in 0 until childCount) {
            var recordButton = getChildAt(i) as RecordButton
            var left: Int = 0
            var right: Int = itemRadius * 2
            var top: Int = (childCount - 1) * (itemRadius * 2 + itemMargin) + itemRadius
            var bottom: Int = top + itemRadius * 2
            recordButton.layout(left, top, right, bottom)

        }
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        var width = itemRadius * 2
        var height = (childCount - 1) * (itemRadius * 2 + itemMargin) + itemRadius * 2 + itemRadius
        width += paddingLeft + paddingRight
        height += paddingTop + paddingBottom
        val count = childCount
        for (i in 0 until count) {
            getChildAt(i).measure(width, width)
            if(i == count-1){
                var recordButton = getChildAt(i) as RecordButton
                recordButton.isSwitchMain = true
            }
        }
        setMeasuredDimension(width, height)
    }

    private fun init(context: Context, attrs: AttributeSet?) {
        val typedArray = context.obtainStyledAttributes(attrs, R.styleable.RecordMenu)
        itemRadius = typedArray.getDimension(R.styleable.RecordMenu_itemRadius, 30f).toInt()
        itemMargin = typedArray.getDimension(R.styleable.RecordMenu_itemMargin, 10f).toInt()
        duration = typedArray.getInteger(R.styleable.RecordMenu_animDuration, 2000).toLong()
        itemFontSize = typedArray.getDimension(R.styleable.RecordMenu_itemFontSize,18f)
        itemFontColorN = typedArray.getColor(R.styleable.RecordMenu_itemFontColorN,Color.BLACK)
        itemFontColorP = typedArray.getColor(R.styleable.RecordMenu_itemFontColorP,Color.WHITE)
        itemBackDrawableN = typedArray.getResourceId(R.styleable.RecordMenu_itemBackDrawableN,R.drawable.bg_menu_item)
        itemBackDrawableP = typedArray.getResourceId(R.styleable.RecordMenu_itemBackDrawableP,R.drawable.bg_menu_item_press)
    }

    fun addItemView(textValue: String){
        var recordButton = RecordButton(context,textValue,itemFontSize,itemFontColorN,itemBackDrawableN,itemFontColorP,itemBackDrawableP)
        var l1 = LayoutParams(itemRadius * 2, itemRadius * 2)
        addView(recordButton, l1)
        recordButton.onRecordItemClickListener = recordListener
    }
    fun addItemView(textValue: String,itemBackDrawableN:Int,itemBackDrawableP:Int){
        var recordButton = RecordButton(context,textValue,itemFontSize,itemFontColorN,itemBackDrawableN,itemFontColorP,itemBackDrawableP)
        var l1 = LayoutParams(itemRadius * 2, itemRadius * 2)
        addView(recordButton, l1)
        recordButton.onRecordItemClickListener = recordListener
    }
    inner class RecordListener : OnRecordItemClickListener {
        override fun onClick(isSwitch: Boolean, textValue: String,isOpen1:Boolean) {
            if (!isRun) {
                if (!isOpen) {
                    openMenu()
                } else {
                    closeMenu()
                }
            }
            onRecordItemClickListener?.onClick(isSwitch,textValue,isOpen1)
        }
    }

    /**
     * 展开控件
     */
    fun openMenu() {
        isOpen = true
        isRun = true
        for (i in 0 until childCount) {
            buttonItemOpenAnimation(i, getChildAt(i) as RecordButton)
        }
    }

    /**
     * 关闭控件
     */
    fun closeMenu() {
        isRun = true
        isOpen = false
        for (i in 0 until childCount) {
            buttonItemCloseAnimation(i, getChildAt(i) as RecordButton)
        }
    }

    /**
     * 展开动画
     */
    private fun buttonItemOpenAnimation(index: Int, view: RecordButton) {
        if (!view.isSwitchMain) {
            val propertyAnimator = view.animate().alpha(1f).setInterpolator(OvershootInterpolator()).setDuration(duration / 3)
            propertyAnimator.y((itemRadius * 2 * index + itemMargin * index + itemRadius).toFloat())
            if (isOpen) {
                view.visibility = View.VISIBLE
            }

            propertyAnimator.setListener(object : Animator.AnimatorListener {
                override fun onAnimationRepeat(p0: Animator?) {

                }

                override fun onAnimationCancel(p0: Animator?) {

                }

                override fun onAnimationEnd(p0: Animator?) {
                    if (index == childCount - 2) {
                        isRun = false
                    }
                }

                override fun onAnimationStart(p0: Animator?) {

                }
            })
            propertyAnimator.start()
        }
    }

    /**
     * 关闭动画
     */
    private fun buttonItemCloseAnimation(index: Int, view: RecordButton) {
        if (!view.isSwitchMain) {
            val propertyAnimator = view.animate().alpha(0f).setDuration(duration / 3)
            propertyAnimator.y(((itemRadius * 2 + itemMargin) * (childCount - 1) + itemRadius).toFloat())


            propertyAnimator.setListener(object : Animator.AnimatorListener {
                override fun onAnimationStart(animation: Animator) {}

                override fun onAnimationEnd(animation: Animator) {
                    if (index == childCount - 2) {
                        isRun = false
                    }
                    if (!isOpen) {
                        view.visibility = View.GONE
                    }
                }

                override fun onAnimationCancel(animation: Animator) {}

                override fun onAnimationRepeat(animation: Animator) {}
            })

            propertyAnimator.start()
        }
    }
}

这里面主要就是控制子视图的大小,位置,动画。在onLayout方法中遍历每个子视图,通过layout设置视图位置,这里设置每个子视图都在容器的底部。然后在OnMeasure中设置整个视图的大小,这个根据子视图的大小和个数来计算同时加上内边距。

最后就是通过子视图的点击事件来执行动画,这里用到的是属性动画,用的是系统自带的一个插值器OvershootInterpolator,这个插值器实现的效果就是在线性上先快速的到达终点然后超出然后仔慢慢回到终点,当然不想要这种效果自己可以自定义一个插值器。至于插值器如何用及如何自定义,这里就不在赘述,以后会专门写一篇文章来介绍。

以上就是这个菜单控件的整体实现过程,是不是很简单。。。。

猜你喜欢

转载自blog.csdn.net/android_hdh/article/details/81217372