仿QQ未读消息数拖拽效果

174 阅读2分钟

效果图:

a68189d288413a9ec6228111fb190a7e.gif

实现思路

drag.png

如上图,可以把拖拽时的视图分成三个,圆a、圆b和点ABCD通过贝塞尔曲线围起来的区域,两圆心连线的中点E当作AD和BC的贝塞尔控制点

代码:

import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import kotlin.math.absoluteValue
import kotlin.math.min
import kotlin.math.pow
import kotlin.math.sqrt

/**
 * 仿qq拖拽效果
 */
class QQDragView(context: Context, attributeSet: AttributeSet?, defStyleAttr : Int) : View(context, attributeSet, defStyleAttr){
    constructor(context: Context) : this(context,null,0)
    constructor(context: Context, attributeSet: AttributeSet) : this(context,attributeSet,0)
    var mainPaint = Paint()
    var bacPaint = Paint()
    var dragPaint = Paint()

    var mainPath = Path()
    var bacPath = Path()
    var dragPath = Path()

    var viewWidth = 0f
    var viewHeight = 0f
    var mainRadius = 0f
    /**
     * 最长距离
     */
    var maxLength = 0f

    private var mainRectF = RectF()
    private var bacRectF = RectF()
    private var pointX = 0f
    private var pointY = 0f

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val width = min(measure(widthMeasureSpec,true),measure(heightMeasureSpec,false))
        viewWidth = width.toFloat()
        viewHeight = viewWidth
        mainRadius = viewWidth / 30
        pointX = viewWidth / 2
        pointY = viewHeight / 2
        maxLength = viewWidth * 2 / 5

        setMeasuredDimension(width,width)
    }

    init {
        mainPaint.color = Color.BLACK
        mainPaint.strokeWidth = 1f
        mainPaint.style = Paint.Style.FILL
        mainPaint.isAntiAlias = true

        bacPaint.color = Color.BLACK
        bacPaint.strokeWidth = 1f
        bacPaint.style = Paint.Style.FILL
        bacPaint.isAntiAlias = true

        dragPaint.color = Color.BLACK
        dragPaint.strokeWidth = 1f
        dragPaint.style = Paint.Style.FILL
        dragPaint.isAntiAlias = true
    }

    /**
     * 是否拽出
     */
    var isOutDrag = false

    /**
     * 已经清除
     */
    var isClear = false
    override fun onDraw(canvas: Canvas?) {
        canvas?.let {
            if(isClear){
                return
            }
            //圆心距离
            val distance = getDistance(viewWidth / 2,viewHeight / 2,pointX,pointY)
            if(!isOutDrag && !isOutDistance(distance)){//没有超过拖拽距离
                val r = mainRadius - mainRadius * distance / maxLength
                bacPath.reset()
                bacRectF.left = viewWidth / 2 - r
                bacRectF.right = viewWidth / 2 + r
                bacRectF.top = viewHeight / 2 - r
                bacRectF.bottom = viewHeight / 2 + r
                bacPath.addOval(bacRectF,Path.Direction.CW)

                drawDragPart(canvas,distance,r)
                it.drawPath(bacPath, bacPaint)
            }else{
                isOutDrag = true
            }

            mainPath.reset()
            mainRectF.left = pointX - mainRadius
            mainRectF.right = pointX + mainRadius
            mainRectF.top = pointY - mainRadius
            mainRectF.bottom = pointY + mainRadius
            mainPath.addOval(mainRectF,Path.Direction.CW)
            it.drawPath(mainPath, mainPaint)
        }
    }

    /**
     * 绘制拖拽部分
     */
    private fun drawDragPart(canvas: Canvas,distance: Float,radius:Float) {
        if(distance > 0){
            dragPath.reset()
            //计算两个圆四个点的坐标
            var bac_x1 = if(viewWidth / 2 > pointX) viewWidth / 2 - radius * (viewWidth / 2 - pointY).absoluteValue / distance
            else viewWidth / 2 + radius * (viewWidth / 2 - pointY).absoluteValue / distance
            var bac_y1 = if(viewWidth / 2 > pointY) viewHeight / 2 + radius * (viewHeight / 2 - pointX).absoluteValue / distance
            else viewHeight / 2 - radius * (viewHeight / 2 - pointX).absoluteValue / distance

            var bac_x2 = if(viewWidth / 2 > pointX) viewWidth / 2 + radius * (viewWidth / 2 - pointY).absoluteValue / distance
            else viewWidth / 2 - radius * (viewWidth / 2 - pointY).absoluteValue / distance
            var bac_y2 = if(viewWidth / 2 > pointY) viewHeight / 2 - radius * (viewHeight / 2 - pointX).absoluteValue / distance
            else viewHeight / 2 + radius * (viewHeight / 2 - pointX).absoluteValue / distance

            var main_x1 = if(viewWidth / 2 > pointX) pointX - mainRadius * (viewWidth / 2 - pointY).absoluteValue / distance
            else pointX + mainRadius * (viewWidth / 2 - pointY).absoluteValue / distance
            var main_y1 = if(viewWidth / 2 > pointY) pointY + mainRadius * (viewHeight / 2 - pointX).absoluteValue / distance
            else pointY - mainRadius * (viewHeight / 2 - pointX).absoluteValue / distance

            var main_x2 = if(viewWidth / 2 > pointX) pointX + mainRadius * (viewWidth / 2 - pointY).absoluteValue / distance
            else pointX - mainRadius * (viewWidth / 2 - pointY).absoluteValue / distance
            var main_y2 = if(viewWidth / 2 > pointY) pointY - mainRadius * (viewHeight / 2 - pointX).absoluteValue / distance
            else pointY + mainRadius * (viewHeight / 2 - pointX).absoluteValue / distance

            //以两个圆心连线的中点当作贝塞尔曲线的控制点
            var centerX = (viewWidth / 2 + pointX) / 2
            var centerY = (viewHeight / 2 + pointY) / 2

            //连接各点形成闭环
            dragPath.moveTo(bac_x1,bac_y1)
            dragPath.lineTo(bac_x2,bac_y2)
            dragPath.quadTo(centerX,centerY,main_x2,main_y2)
            dragPath.lineTo(main_x1,main_y1)
            dragPath.quadTo(centerX,centerY,bac_x1,bac_y1)
            dragPath.close()

            canvas.drawPath(dragPath,dragPaint)
        }
    }

    /**
     * 测距
     */
    private fun getDistance(x1 : Float,y1 : Float,x2 : Float,y2 : Float) : Float{
        return sqrt(((x1 - x2).pow(2) + (y1 - y2).pow(2)))
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        event?.let {
            if(it.action == MotionEvent.ACTION_MOVE){
                pointX = event.x
                pointY = event.y
                invalidate()
            }else if(it.action == MotionEvent.ACTION_UP || it.action == MotionEvent.ACTION_CANCEL){
                if(!isOutDrag){//没有拽出来
                    /*pointX = viewWidth / 2
                    pointY = viewHeight / 2
                    invalidate()*/
                }else{//拽出来了
                    //圆心距离
                    val distance = getDistance(viewWidth / 2,viewHeight / 2,pointX,pointY)
                    if(isOutDistance(distance)){//超过了拖拽距离
                        isClear = true
                        clearListener?.onClear()
                    }else{
                        isOutDrag = false
                    }
                }
                pointX = viewWidth / 2
                pointY = viewHeight / 2
                invalidate()
            }
        }
        return true
    }
    interface ClearListener{
        fun onClear()
    }
    private var clearListener : ClearListener? = null
    fun setOnClearListener(clearListener : ClearListener){
        this.clearListener = clearListener
    }

    /**
     * 是否超过距离
     */
    private fun isOutDistance(distance: Float) : Boolean{
        return distance > maxLength / 2
    }

    private fun measure( measureSpec :Int,  isWidth : Boolean) : Int{
        var result = 0
        val mode = MeasureSpec.getMode(measureSpec);
        val size = MeasureSpec.getSize(measureSpec);
        val padding = if(isWidth)  getPaddingLeft() + getPaddingRight()
        else getPaddingTop() + getPaddingBottom()
        if (mode == MeasureSpec.EXACTLY) {
            result = size;
        } else {
            result = if(isWidth) getSuggestedMinimumWidth()
            else getSuggestedMinimumHeight();
            result += padding;
            if (mode == MeasureSpec.AT_MOST) {
                if (isWidth) {
                    result = Math.max(result, size);
                } else {
                    result = Math.min(result, size);
                }
            }
        }
        return result
    }
}