• Android仿QQ消息拖拽效果(二)


    前言

    本文参考辉哥贝塞尔曲线 - QQ消息汽包拖拽,前面我们使用二阶贝塞尔曲线绘制了拖拽圆点效果Android仿QQ消息拖拽效果(一)(二阶贝塞尔曲线使用),这里我们在此基础之上实现仿QQ消息拖拽爆炸效果。

    最终效果

    仿QQ消息拖拽效果

    实现思路

    • 首先,监听需要拖拽view的onTouchListener事件,当手指按下的时候,由于也需要支持在状态栏上拖动,因此需要把拖拽的view添加到windowmanager上才支持【参考android窗口】,而我们自身activity中的view是不支持拖动的,因此,我们可以监听当手指按下的时候,隐藏需要拖动的view,并新创建一个支持拖拽的view(上文自定义的BesselView),同时复制一份原View的BitmapBesselView中绘制出来,并将BesselView添加到WindowManager中即可。
    • 当我们监听到手指移动时,对应ACTION_MOVE事件,我们不断更新BesselView中的移动点的坐标进行重绘即可。
    • 当我们监听到手指抬起时,对应ACTION_UP事件,我们需要根据拖动距离去判断BesselView是恢复原view的位置,还是原地爆炸消失,当是恢复原View的位置时,我们设置一个回弹属性动画回弹到之前位置后,将activity中的view显示出来,同时将BesselViewWindowManager上移除,当是爆炸时,我们设置一个爆炸效果,同样也将其从WindowManager上移除即可。

    相关源码

    • 支持任何View拖拽的辅助类DragViewHelper
    package com.crystal.view.animation
    
    import 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 {
        //目标View
        private lateinit var targetView: View
    
        //监听view消失回调
        private var dragViewDismissListener: DragViewDismissListener? = null
    
        private lateinit var windowManager: WindowManager
        private lateinit var windowManagerLayoutParams: WindowManager.LayoutParams
    
        private lateinit var context: Context
    
        private lateinit var besselView: BesselView
    
    
        //爆炸动画
        private lateinit var bombFrame: FrameLayout
        private lateinit var bombImage: ImageView
    
        /**
         * 绑定View
         */
        fun attachView(context: Context, view: View, listener: DragViewDismissListener?) {
            this.context = context
            this.targetView = view
            this.dragViewDismissListener = listener
            initWindowManager()
            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
                )
            )
            //将自己隐藏,复制一份自己给besselView
            besselView.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 / 2
            bombImage.y = pointF.y - bombImage.height / 2
            val bombAnimation = bombImage.background as AnimationDrawable
            bombAnimation.start()
            //等爆炸动画执行完毕后,通知activity view已消失
            bombImage.postDelayed({
                windowManager.removeView(bombFrame)
                dragViewDismissListener?.onDismiss()
            }, getBombAnimationDuration(bombAnimation))
        }
    
        /**
         * 获取爆炸帧动画总体时长
         */
        private fun getBombAnimationDuration(bombAnimation: AnimationDrawable): Long {
            var totalTime = 0L
            for (i in 0 until bombAnimation.numberOfFrames) {
                totalTime += bombAnimation.getDuration(i)
            }
            return totalTime
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 拖拽效果的BesselView
      较上一篇文章中的有所改动:
      1.事件处理交给DragViewHelper;
      2.按下拖动时,需要复制一份原View的Bitmap;
      3.处理手指抬起后逻辑,并回调对应监听;
    package com.crystal.view.animation
    
    import 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? = 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
        ) {
            paint.color = Color.RED
            paint.isDither = true
            paint.isAntiAlias = true
            fixPointMinRadius = dp2px(2f)
            fixPointMaxRadius = dp2px(7f)
            movePointRadius = dp2px(8f)
        }
    
    
        /**
         * 更新移动点的坐标
         */
        fun updateMovePoint(eventX: Float, eventY: Float) {
            if (movePoint == null) {
                movePoint = PointF()
            }
            movePoint?.x = eventX
            movePoint?.y = eventY
            invalidate()
        }
    
        /**
         * 更新固定点的坐标
         */
        fun updateFixPoint(eventX: Float, eventY: Float) {
            if (fixPoint == null) {
                fixPoint = PointF()
            }
            fixPoint?.x = eventX
            fixPoint?.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!!.x
            val dy = movePoint!!.y - fixPoint!!.y
            return sqrt((dx * dx + dy * dy).toDouble())
        }
    
        /**
         * 计算两个圆心连接中心点坐标 作为二阶贝塞尔曲线的控制点
         */
        private fun getCircleCenterPoint(): PointF {
            val centerX = (movePoint!!.x + fixPoint!!.x) / 2
            val centerY = (movePoint!!.y + fixPoint!!.y) / 2
            return 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? = null
    
    
        fun addBesselViewListener(besselViewListener: BesselViewListener) {
            this.besselViewListener = besselViewListener
        }
    
        interface BesselViewListener {
            //恢复原有view
            fun restore()
    
            //原有view爆炸消失
            fun dismiss(pointF: PointF)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 拖拽计算工具类DragUtils
    package com.crystal.view.animation
    
    import 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.x
           val y = (endPointF.y - startPointF.y) * percent + startPointF.y
           return 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
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    总结

    通过实现QQ消息拖拽效果,体会到WindowManager的重要性,很多效果的实现需要它的配合,探索WindowManager源码势在必行。

    结语

    如果以上文章对您有一点点帮助,希望您不要吝啬的点个赞加个关注,您每一次小小的举动都是我坚持写作的不懈动力!ღ( ´・ᴗ・` )

  • 相关阅读:
    NAT基础:NAT技术原理,静态NAT、动态NAT、NAPT、Easy IP、NAT Server的原理,以及各NAT的配置方法和转换示例。
    2023Linux C/C++全栈开发知识技术合集(基础入门到高级进阶)
    Python 中的 DNS 查找
    spring 单元测试注解
    (高阶) Redis 7 第21讲 IO多路复用模型 完结篇
    积雪草酸肌白蛋白纳米粒|野黄芩苷豆清白蛋白纳米粒|黄芩苷蓖麻蛋白纳米粒(齐岳)
    k8s-svc外界访问pod容器服务-4
    EasyExcel 注解fillForegroundColor
    TCP & UDP
    OpenHarmony轻内核编码规范
  • 原文地址:https://blog.csdn.net/a734474820/article/details/127799862