本文参考辉哥贝塞尔曲线 - QQ消息汽包拖拽,前面我们使用二阶贝塞尔曲线绘制了拖拽圆点效果Android仿QQ消息拖拽效果(一)(二阶贝塞尔曲线使用),这里我们在此基础之上实现仿QQ消息拖拽爆炸效果。
onTouchListener
事件,当手指按下的时候,由于也需要支持在状态栏上拖动
,因此需要把拖拽的view添加到windowmanager
上才支持【参考android窗口】,而我们自身activity中的view是不支持拖动的,因此,我们可以监听当手指按下的时候,隐藏需要拖动的view,并新创建一个支持拖拽的view
(上文自定义的BesselView
),同时复制一份原View的Bitmap
在BesselView
中绘制出来,并将BesselView
添加到WindowManager
中即可。ACTION_MOVE
事件,我们不断更新BesselView
中的移动点的坐标进行重绘即可。ACTION_UP
事件,我们需要根据拖动距离去判断BesselView
是恢复原view的位置,还是原地爆炸消失,当是恢复原View的位置时,我们设置一个回弹属性动画回弹到之前位置后,将activity中的view
显示出来,同时将BesselView
从WindowManager
上移除,当是爆炸时,我们设置一个爆炸效果,同样也将其从WindowManager
上移除即可。DragViewHelper
package com.crystal.view.animationimport android.content.Context
import android.graphics.Bitmap
import android.graphics.PixelFormatProto
import android.graphics.PointF
import android.graphics.drawable.AnimationDrawable
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.FrameLayout
import android.widget.ImageView
import com.crystal.view.R/*** 支持任何View拖拽的辅助类* on 2022/11/10*/
class DragViewHelper : BesselView.BesselViewListener {//目标Viewprivate lateinit var targetView: View//监听view消失回调private var dragViewDismissListener: DragViewDismissListener? = nullprivate lateinit var windowManager: WindowManagerprivate lateinit var windowManagerLayoutParams: WindowManager.LayoutParamsprivate lateinit var context: Contextprivate lateinit var besselView: BesselView//爆炸动画private lateinit var bombFrame: FrameLayoutprivate lateinit var bombImage: ImageView/*** 绑定View*/fun attachView(context: Context, view: View, listener: DragViewDismissListener?) {this.context = contextthis.targetView = viewthis.dragViewDismissListener = listenerinitWindowManager()initDragViewBombView()addTargetViewOnTouchListener()}/*** 初始化WindowManager*/private fun initWindowManager() {windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager//设置拖动时windowManager透明同时导航栏不变成黑色windowManagerLayoutParams = WindowManager.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.TYPE_APPLICATION,WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN, PixelFormatProto.TRANSPARENT)}private fun initDragViewBombView() {besselView = BesselView(context)besselView.addBesselViewListener(this)bombFrame = FrameLayout(context)bombImage = ImageView(context)bombImage.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT)bombFrame.addView(bombImage)}//给view设置onTouchListener监听,用于监听手指拖动private fun addTargetViewOnTouchListener() {targetView.setOnTouchListener { _, event ->when (event.action) {MotionEvent.ACTION_DOWN -> {handleActionDown()}MotionEvent.ACTION_MOVE -> {handleActionMove(event)}MotionEvent.ACTION_UP -> {//手指松开时,要处理回弹或者消失besselView.handleActionUp()}}true}}/*** 处理按下事件*/private fun handleActionDown() {//由于需要支持导航栏拖动,所以需要添加推动的View在WindowManger上windowManager.addView(besselView, windowManagerLayoutParams)//初始化贝塞尔固定点,需要保证固定点圆心位于view的中心位置val centerTargetLocation = IntArray(2)//获取的位置为View左上角坐标targetView.getLocationOnScreen(centerTargetLocation)//设置拖拽view的固定位置为目标view的中心位置,但高度不包含状态栏高度,这点需要注意besselView.updateFixPoint(centerTargetLocation[0] + targetView.width / 2f,centerTargetLocation[1] + targetView.height / 2f - DragUtils.getStatusBarHeight(context))//将自己隐藏,复制一份自己给besselViewbesselView.setDragBitmap(getDragBitmap())targetView.visibility = View.INVISIBLE}private fun handleActionMove(event: MotionEvent) {//手指移动时,改变besselView的坐标besselView.updateMovePoint(event.rawX,event.rawY - DragUtils.getStatusBarHeight(context))}/*** 用于监听View拖动消失*/interface DragViewDismissListener {fun onDismiss()}/*** 获取targetView对应的bitmap*/private fun getDragBitmap(): Bitmap {targetView.buildDrawingCache()return targetView.drawingCache}override fun restore() {//重置为初始状态windowManager.removeView(besselView)targetView.visibility = View.VISIBLE}override fun dismiss(pointF: PointF) {//将view消失windowManager.removeView(besselView)windowManager.addView(bombFrame, windowManagerLayoutParams)bombImage.setBackgroundResource(R.drawable.anim_bubble_pop)//设置爆炸图片位置【左上角显示image】bombImage.x = pointF.x - bombImage.width / 2bombImage.y = pointF.y - bombImage.height / 2val bombAnimation = bombImage.background as AnimationDrawablebombAnimation.start()//等爆炸动画执行完毕后,通知activity view已消失bombImage.postDelayed({windowManager.removeView(bombFrame)dragViewDismissListener?.onDismiss()}, getBombAnimationDuration(bombAnimation))}/*** 获取爆炸帧动画总体时长*/private fun getBombAnimationDuration(bombAnimation: AnimationDrawable): Long {var totalTime = 0Lfor (i in 0 until bombAnimation.numberOfFrames) {totalTime += bombAnimation.getDuration(i)}return totalTime}
}
BesselView
package com.crystal.view.animationimport android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ObjectAnimator
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.util.TypedValue
import android.view.View
import android.view.animation.OvershootInterpolator
import kotlin.math.sqrt/*** 仿qq消息拖拽效果【二阶贝塞尔曲线学习】* on 2022/11/10*/
class BesselView : View {//画笔工具private val paint = Paint()//固定点private var fixPoint: PointF? = null//跟随手指移动点private var movePoint: PointF? = null//固定点半径【当移动点距离远时,会逐渐变小】private var fixPointRadius = 0f//固定点半径最小值private var fixPointMinRadius = 0f//固定圆半径最大值private var fixPointMaxRadius = 0f//移动点半径private var movePointRadius = 0f//将拖动的View复制一份进行绘制private var dragBitmap: Bitmap? = nullconstructor(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) {paint.color = Color.REDpaint.isDither = truepaint.isAntiAlias = truefixPointMinRadius = dp2px(2f)fixPointMaxRadius = dp2px(7f)movePointRadius = dp2px(8f)}/*** 更新移动点的坐标*/fun updateMovePoint(eventX: Float, eventY: Float) {if (movePoint == null) {movePoint = PointF()}movePoint?.x = eventXmovePoint?.y = eventYinvalidate()}/*** 更新固定点的坐标*/fun updateFixPoint(eventX: Float, eventY: Float) {if (fixPoint == null) {fixPoint = PointF()}fixPoint?.x = eventXfixPoint?.y = eventY}/*** 设置拖动的View对应的Bitmap*/fun setDragBitmap(dragBitmap: Bitmap) {this.dragBitmap = dragBitmap}override fun onDraw(canvas: Canvas) {if (fixPoint == null || movePoint == null) {return}//绘制移动点canvas.drawCircle(movePoint!!.x, movePoint!!.y, movePointRadius, paint)fixPointRadius = (fixPointMaxRadius - getPointCenterDistance() / 24f).toFloat()//绘制固定点和贝塞尔曲线【当距离过大时,不绘制贝塞尔曲线和固定点】if (fixPointRadius > fixPointMinRadius) {canvas.drawCircle(fixPoint!!.x, fixPoint!!.y, fixPointRadius, paint)drawBesselLine(canvas)}if (dragBitmap != null) {canvas.drawBitmap(dragBitmap!!,movePoint!!.x - dragBitmap!!.width / 2f,movePoint!!.y - dragBitmap!!.height / 2f,null)}}/*** 绘制二阶贝塞尔曲线*/private fun drawBesselLine(canvas: Canvas) {//分别计算角a的sin值和cos值val sina = (movePoint!!.y - fixPoint!!.y) / getPointCenterDistance()val cosa = (movePoint!!.x - fixPoint!!.x) / getPointCenterDistance()//求出p0点坐标val p0 = PointF((fixPoint!!.x + fixPointRadius * sina).toFloat(),(fixPoint!!.y - fixPointRadius * cosa).toFloat())//求出p2点坐标val p2 = PointF((fixPoint!!.x - fixPointRadius * sina).toFloat(),(fixPoint!!.y + fixPointRadius * cosa).toFloat())//求出p1点坐标val p1 = PointF((movePoint!!.x + movePointRadius * sina).toFloat(),(movePoint!!.y - movePointRadius * cosa).toFloat())//求出p3点坐标val p3 = PointF((movePoint!!.x - movePointRadius * sina).toFloat(),(movePoint!!.y + movePointRadius * cosa).toFloat())//绘制贝塞尔曲线val path = Path()path.moveTo(p0.x, p0.y)path.quadTo(getCircleCenterPoint().x, getCircleCenterPoint().y, p1.x, p1.y)path.lineTo(p3.x, p3.y)path.quadTo(getCircleCenterPoint().x, getCircleCenterPoint().y, p2.x, p2.y)path.close()canvas.drawPath(path, paint)}/*** dp 转 px*/private fun dp2px(dp: Float): Float {return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.displayMetrics)}/*** 计算两点距离*/private fun getPointCenterDistance(): Double {val dx = movePoint!!.x - fixPoint!!.xval dy = movePoint!!.y - fixPoint!!.yreturn sqrt((dx * dx + dy * dy).toDouble())}/*** 计算两个圆心连接中心点坐标 作为二阶贝塞尔曲线的控制点*/private fun getCircleCenterPoint(): PointF {val centerX = (movePoint!!.x + fixPoint!!.x) / 2val centerY = (movePoint!!.y + fixPoint!!.y) / 2return PointF(centerX, centerY)}fun handleActionUp() {if (fixPointRadius > fixPointMinRadius) {//进行回弹动画,移动到fixPoint位置val valueAnimator = ObjectAnimator.ofFloat(1f)valueAnimator.duration = 200//设置差值器,在结束后回弹效果valueAnimator.interpolator = OvershootInterpolator(3f)valueAnimator.addUpdateListener {val percent = it.animatedValue as Float//回弹起始点为移动点坐标,终点为固定点坐标,计算结果为动画变化过程中拖动点val dragPoint = DragUtils.getDragPointByPercent(movePoint!!, fixPoint!!, percent)updateMovePoint(dragPoint.x, dragPoint.y)}valueAnimator.start()valueAnimator.addListener(object : AnimatorListenerAdapter() {override fun onAnimationEnd(animation: Animator?) {//回弹动画结束后,需要把原有位置的view显示出来besselViewListener?.restore()}})} else {//进行爆炸效果besselViewListener?.dismiss(movePoint!!)}}private var besselViewListener: BesselViewListener? = nullfun addBesselViewListener(besselViewListener: BesselViewListener) {this.besselViewListener = besselViewListener}interface BesselViewListener {//恢复原有viewfun restore()//原有view爆炸消失fun dismiss(pointF: PointF)}
}
DragUtils
package com.crystal.view.animationimport android.content.Context
import android.graphics.PointF/**
* 拖拽计算工具类
* on 2022/11/10
*/
object DragUtils {/*** 用于计算拖拽时位置*/fun getDragPointByPercent(startPointF: PointF, endPointF: PointF, percent: Float): PointF {val x = (endPointF.x - startPointF.x) * percent + startPointF.xval y = (endPointF.y - startPointF.y) * percent + startPointF.yreturn PointF(x, y)}/*** 获取状态栏高度*/fun getStatusBarHeight(context: Context): Int {val resourceId = context.resources.getIdentifier("status_bar_height", "dimen", "android")if (resourceId > 0) {return context.resources.getDimensionPixelSize(resourceId)}return 0}
}
通过实现QQ消息拖拽效果,体会到WindowManager
的重要性,很多效果的实现需要它的配合,探索WindowManager
源码势在必行。
如果以上文章对您有一点点帮助,希望您不要吝啬的点个赞加个关注,您每一次小小的举动都是我坚持写作的不懈动力!ღ( ´・ᴗ・` )
下一篇:1. 云计算简介