美文网首页Android
具有多种动画的菜单弹窗

具有多种动画的菜单弹窗

作者: lucasDev | 来源:发表于2019-03-15 11:23 被阅读0次

    目的:由于公司项目需求,现需制作一个空调的键位控制的弹窗。

    功能:能够自由增删键位的个数,并对键位显示的时候播放多种动画。

    image image.gif

        首先我们来复习下初中的数学知识:平面直角坐标系和求坐标系中某个点的坐标值。
    
    image image.gif

    由于是手工画的,大家将就这看吧。a代表直角三角形的一个锐角,x是横轴坐标,y是纵轴坐标,那么p点的坐标就是(x,y)了。

    Tip:其实在android界面中y轴不是朝上的而是朝下的,这个我们等下在讨论。

    利用初中知识我们知道:x=rsin(a) y=rcos(a) ,那么p=(rsin(a),rcos(a))

    然后我们把数学函数转换为java代码 :

    x=r*Math.sin(Math.toRadians(a))

    y=r*Math.cos(Math.toRadians(a))

    p=(x,y)

    image image.gif

    现在有n个按键要均匀分布在圆形上,并且以点A为起点,点A的坐标就是(0,-y)了,我们现在需要算出点BCDE的坐标。

    我们先算点B,算点B我们必须先知道圆的半径和角b,假设半径为r。

    由于这些点是均匀分布的,所以角b=360/n。

    有前面的知识可知 B=(rMath.sin(Math.toRadians(360/n)),rMath.cos(Math.toRadians(360/n))),但是由于点B处于第四象限,所以y坐标应该是负值,所以B=(rMath.sin(Math.toRadians(360/n)), -rMath.cos(Math.toRadians(360/n)))。

    现在我们继续看点C的坐标,需先求角c,c=2b-90=2b%90,不知道大家能否理解。

    以此类推 d=3b%90 ,e=4d%90...,这样我们就能算出剩余的点坐标了。

    现在我们已经有了各个点的坐标那么只需将各个点从原点(0,0)移动到各自的位置上即可。下面我就用代码的方式把前面的结论用代码的方式表达出来。

    package com.mmy.kotlinsample.popup
    
    import android.animation.AnimatorSet
    import android.animation.ObjectAnimator
    import android.app.Activity
    import android.content.Context
    import android.graphics.Point
    import android.graphics.drawable.ColorDrawable
    import android.util.Log
    import android.view.Gravity
    import android.view.LayoutInflater
    import android.view.ViewGroup
    import android.widget.ImageView
    import android.widget.PopupWindow
    import android.widget.RelativeLayout
    import com.mmy.menupopup.R
    
    
    /**
     * @file       ControlPopup.kt
     * @brief      描述
     * @author     lucas
     * @date       2018/5/8 0008
     * @version    V1.0
     * @par        Copyright (c):
     * @par History:
     *             version: zsr, 2017-09-23
     */
    class ControlPopup constructor(val context: Context, val array: IntArray) : PopupWindow() {
        val r = 400.0//半径
        val pints = ArrayList<Point>()//用户存储每个点的坐标
        var degrees: Double = 0.0
        val location = kotlin.IntArray(2)
        var mRootView: RelativeLayout? = null
        val set = AnimatorSet()
    
        init {
            width = ViewGroup.LayoutParams.MATCH_PARENT
            height = ViewGroup.LayoutParams.MATCH_PARENT
            setBackgroundDrawable(ColorDrawable(context.resources.getColor(android.R.color.transparent)))
            isOutsideTouchable = true
    
            //第一个按键固定在正下方
            val element = Point(0, r.toInt())
            element.direction(true, false)
            pints.add(element)
            //算出每个角的度数
            degrees = 360 / array.size.toDouble()
            //算出剩下的图标的坐标
            for (i in 1 until array.size) {
                //夹角度数
                val d = degrees * i
                var point: Point? = null
                //判断是在那个象限,确定方向
                val radians = Math.toRadians(d % 90)
                when (d.toInt()) {
                    in 0..90 -> {//第四象限 x,-y
    //                    Log.d("popup","90:${d % 90},x:${Math.sin(d % 90) * r},y:${Math.cos(d % 90) * r}")
                        point = Point((Math.sin(radians) * r).toInt(), (Math.cos(radians) * r).toInt())
                        point.direction(true, false)
                    }
                    in 90..180 -> {//第一象限 x,y
                        point = Point((Math.cos(radians) * r).toInt(), (Math.sin(radians) * r).toInt())
                        point.direction(true, true)
                    }
                    in 180..270 -> {//第二象限-x,y
                        point = Point((Math.sin(radians) * r).toInt(), (Math.cos(radians) * r).toInt())
                        point.direction(false, true)
                    }
                    else -> {//第三象限-x,-y
                        point = Point((Math.cos(radians) * r).toInt(), (Math.sin(radians) * r).toInt())
                        point.direction(false, false)
                    }
                }
                if (point != null)
                    pints.add(point)
            }
    
            mRootView = LayoutInflater.from(context).inflate(R.layout.popup_control, null, false) as RelativeLayout
            contentView = mRootView
            mRootView?.setOnClickListener { dismiss() }
            //添加按键
            array.forEach {
                val imageView = ImageView(context)
                imageView.setImageResource(it)
                val params = RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
                params.addRule(RelativeLayout.CENTER_IN_PARENT)
                imageView.layoutParams = params
                mRootView?.addView(imageView)
            }
            //开始播放动画
            openAnim()
        }
    
        private fun closeAnim() {
            for (i in 0 until pints.size) {
                pints[i].tostring()
                val childAt = mRootView?.getChildAt(i)
                //x轴平移
                val animatorX = ObjectAnimator.ofFloat(childAt, "translationX", 0.0f, 0.0f)
                //y轴平移
                val animatorY = ObjectAnimator.ofFloat(childAt, "translationY", 0.0f, 0.0f)
                set.play(animatorX).with(animatorY)
                set.duration = 1000
                set.start()
            }
        }
    
        private fun openAnim() {
            for (i in 0 until pints.size) {
                pints[i].tostring()
                val childAt = mRootView?.getChildAt(i)
                //x轴平移
                val animatorX = ObjectAnimator.ofFloat(childAt, "translationX", 0.0f, pints[i].x.toFloat())
                //y轴平移
                val animatorY = ObjectAnimator.ofFloat(childAt, "translationY", 0.0f, pints[i].y.toFloat())
                set.play(animatorX).with(animatorY)
                set.duration = 1000
                set.start()
            }
        }
    
        //显示弹窗
        fun show(activity: Activity){
            showAtLocation(activity.findViewById(android.R.id.content),Gravity.CENTER,0,0)
            openAnim()
        }
    
        //关闭弹窗
        fun close(){
            closeAnim()
            dismiss()
        }
    
    
        /**
         * 给pint扩展个方向的方法
         * Tip:当y的值为正数就是在上方,反之在下方
         *      x值为正数就在右侧,反之左侧
         */
        fun Point.direction(xDirection: Boolean, yDirection: Boolean) {
            if (!xDirection)
                this.x = 0 - this.x
            if (yDirection)
                this.y = 0 - this.y
        }
    
        fun Point.tostring() {
            Log.d("popup", "x:${this.x},y:${this.y}")
        }
    }
    

    以上代码只是完成了按键的添加和按键的平移动画,看似比较呆板,现在我们来给动画加点灵魂。

    首先我们了解下弹性动画,通过插值器interpolator来实现效果。

    Interpolator

    image image.gif

    通过上面的链接大家可以生成对应的函数。然后我们还需要重写个插值器类。

    package com.mmy.menupopup
    
    import android.view.animation.Interpolator
    
    /**
     * @file       SpringScaleInterpolator.kt
     * @brief      描述
     * @author     lucas
     * @date       2018/5/10 0010
     * @version    V1.0
     * @par        Copyright (c):
     * @par History:
     *             version: zsr, 2017-09-23
     */
    class SpringScaleInterpolator(val factor:Float) :Interpolator{
        override fun getInterpolation(input: Float): Float {
            return (Math.pow(2.0, (-10 * input).toDouble()) * Math.sin((input - factor / 4) * (2 * Math.PI) / factor) + 1).toFloat()
        }
    }
    

    通过参数factor来调整动画的弹性效果。并把其添加至动画中。

    private fun openAnim() {
        for (i in 0 until pints.size) {
            pints[i].tostring()
            val childAt = mRootView?.getChildAt(i)
            //x轴平移
            val animatorX = ObjectAnimator.ofFloat(childAt, "translationX", 0.0f, pints[i].x.toFloat())
            //y轴平移
            val animatorY = ObjectAnimator.ofFloat(childAt, "translationY", 0.0f, pints[i].y.toFloat())
            set.play(animatorX).with(animatorY)
            set.duration = 1000
            set.interpolator = SpringScaleInterpolator(0.4f)
            set.start()
        }
    }
    

    这样我们的动画就具有部分灵魂了。

    我们还可以给每个按键在显示的时候添加一定的延时,这样按键的显示会具有一定的先后顺序,这样看起来也比较舒服。

    private fun openAnim() {
        for (i in 0 until pints.size) {
            pints[i].tostring()
            mHandler.postDelayed({
                val childAt = mRootView?.getChildAt(i)
                //x轴平移
                val animatorX = ObjectAnimator.ofFloat(childAt, "translationX", 0.0f, pints[i].x.toFloat())
                //y轴平移
                val animatorY = ObjectAnimator.ofFloat(childAt, "translationY", 0.0f, pints[i].y.toFloat())
                val set = AnimatorSet()
                set.play(animatorX).with(animatorY)
                set.duration = duration
                set.interpolator = SpringScaleInterpolator(0.4f)
                set.start()
            }, i * 50L)
        }
    }
    

    下面我们继续给中间的图片添加一个类似5.0的转场动画效果。

    private fun openAnim() {
        //将目标控件自动到原点
        val midView = mRootView?.findViewById<View>(R.id.v_mid_icon)
        val targetLocation = kotlin.IntArray(2)
        targetView.getLocationOnScreen(targetLocation)
        midView?.viewTreeObserver?.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
            override fun onGlobalLayout() {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                    midView.viewTreeObserver?.removeOnGlobalLayoutListener(this)
                }
                val midLocation = kotlin.IntArray(2)
                midView.getLocationOnScreen(midLocation)
                //x轴平移
                val animatorX = ObjectAnimator.ofFloat(midView, "translationX", (targetLocation[0].toFloat()-midLocation[0].toFloat()), .0f)
                //y轴平移
                val animatorY = ObjectAnimator.ofFloat(midView, "translationY", (targetLocation[1].toFloat()-midLocation[1].toFloat()), .0f)
                //缩放动画
                val scaleAnimatorX = ObjectAnimator.ofFloat(midView, "scaleX", 1.0f, 1.5f)
                val scaleAnimatorY = ObjectAnimator.ofFloat(midView, "scaleY", 1.0f, 1.5f)
                val set = AnimatorSet()
                set.play(animatorX).with(animatorY).with(scaleAnimatorX).with(scaleAnimatorY)
                set.duration = targetDuration
                set.addListener(object :Animator.AnimatorListener{
                    override fun onAnimationRepeat(p0: Animator?) {
                    }
    
                    override fun onAnimationEnd(p0: Animator?) {
                        //当目标图片移动结束后开始释放其他按键
                        for (i in 0 until pints.size) {
                            pints[i].tostring()
                            mHandler.postDelayed({
                                val childAt = mContainer?.getChildAt(i)
                                childAt?.visibility=View.VISIBLE
                                //x轴平移
                                val animatorX = ObjectAnimator.ofFloat(childAt, "translationX", 0.0f, pints[i].x.toFloat())
                                //y轴平移
                                val animatorY = ObjectAnimator.ofFloat(childAt, "translationY", 0.0f, pints[i].y.toFloat())
                                val set = AnimatorSet()
                                set.play(animatorX).with(animatorY)
                                set.duration = duration
                                set.interpolator = SpringScaleInterpolator(0.4f)
                                set.start()
                            }, i * delay)
                        }
                    }
    
                    override fun onAnimationCancel(p0: Animator?) {
                    }
    
                    override fun onAnimationStart(p0: Animator?) {
                    }
    
                })
                set.start()
                Log.d("lucas", "xxx:${midLocation[0].toFloat()},yyy:${midLocation[1].toFloat()}")
            }
        })
    
    }
    

    最后效果

    image image.gif

    Github地址

    相关文章

      网友评论

        本文标题:具有多种动画的菜单弹窗

        本文链接:https://www.haomeiwen.com/subject/foklmqtx.html