具有多种动画的菜单弹窗
目的:由于公司项目需求,现需制作一个空调的键位控制的弹窗。
功能:能够自由增删键位的个数,并对键位显示的时候播放多种动画。
首先我们来复习下初中的数学知识:平面直角坐标系和求坐标系中某个点的坐标值。
由于是手工画的,大家将就这看吧。a代表直角三角形的一个锐角,x是横轴坐标,y是纵轴坐标,那么p点的坐标就是(x,y)了。
Tip:其实在android界面中y轴不是朝上的而是朝下的,这个我们等下在讨论。
利用初中知识我们知道:x=r*sin(a) y=r*cos(a) ,那么p=(r*sin(a),r*cos(a))
然后我们把数学函数转换为java代码 :
x=r*Math.sin(Math.toRadians(a))
y=r*Math.cos(Math.toRadians(a))
p=(x,y)
现在有n个按键要均匀分布在圆形上,并且以点A为起点,点A的坐标就是(0,-y)了,我们现在需要算出点BCDE的坐标。
我们先算点B,算点B我们必须先知道圆的半径和角b,假设半径为r。
由于这些点是均匀分布的,所以角b=360/n。
有前面的知识可知 B=(r*Math.sin(Math.toRadians(360/n)),r*Math.cos(Math.toRadians(360/n))),但是由于点B处于第四象限,所以y坐标应该是负值,所以B=(r*Math.sin(Math.toRadians(360/n)), -r*Math.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来实现效果。
通过上面的链接大家可以生成对应的函数。然后我们还需要重写个插值器类。
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()}") } }) }
最后效果