Android-连点器
本文最后更新于 92 天前,其中的信息可能已经有所发展或是发生改变。

连点器悬浮面板全部附加于Activity的DecorView实现。具体分为:点位连点器和路径连点器点位连点器使用的While+Thread实现轮询点位分发路径连点器使用的是状态机点位分发

1. 点位连点器

1.1 单击、双击、长按

class GmTouchSpotView : LinearLayout {
    constructor(context: Context) : super(context) {
        init()
    }

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
        init()
    }

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
            context,
            attrs,
            defStyleAttr
    ) {
        init()
    }

    constructor(
            context: Context,
            attrs: AttributeSet?,
            defStyleAttr: Int,
            defStyleRes: Int
    ) : super(context, attrs, defStyleAttr, defStyleRes) {
        init()
    }

    private fun init() {
        layoutParams = ViewGroup.LayoutParams(-2, -2)
        Ext.getLayoutView("gamehelper_point", this)
        clipChildren = false
        clipToPadding = false
        setWillNotDraw(false)

        GmLifecycleUtils.getTopActivity()?.window?.decorView?.apply {
            screenMin = measuredWidth.coerceAtMost(measuredHeight)
            screenMax = measuredWidth.coerceAtLeast(measuredHeight)
        }

        translationX = x
        translationY = y

        post {
            if (spotType == SPOT_START) {
                for (listener in moveListener) {
                    listener.onSizeChange(measuredWidth)
                }
            }
        }
    }

    private var screenMin: Int = 0
    private var screenMax: Int = 0
    private var x = 0f
    private var y = 0f
    private var moveListener: ArrayList<GmSpotMoveListener> = arrayListOf()
    private var spotType: Int = SPOT_START

    constructor (
            context: Context,
            x: Float,
            y: Float,
            @SpotType spotType: Int,
            moveListener: GmSpotMoveListener? = null
    ) : super(
            context
    ) {
        this.x = x
        this.y = y
        this.spotType = spotType
        moveListener?.let {
            this.moveListener.add(it)
        }
        init()
    }

    fun addMoveListener(moveListener: GmSpotMoveListener? = null) {
        moveListener?.let {
            this.moveListener.add(it)
        }
    }

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        moveListener.clear()
    }

    constructor (
            context: Context,
            x: Float,
            y: Float,
            moveListener: GmSpotMoveListener? = null
    ) : super(
            context
    ) {
        this.x = x
        this.y = y
        moveListener?.let {
            this.moveListener.add(it)
        }
        init()
    }

    // 起点坐标
    private var lastX = 0f
    private var lastY = 0f

    fun updateSpotCoordinate(newX: Float, newY: Float) {
        x = newX
        y = newY
        translationX = x
        translationY = y
        for (listener in moveListener) {
            listener.onMove(newX, newY, spotType)
        }
    }

    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent): Boolean {
        if (config?.playStatus == true) {
            return super.onTouchEvent(event)
        }

        val action: Int = event.action
        val realX: Float
        if (action == MotionEvent.ACTION_DOWN) {
            lastX = event.rawX
            lastY = event.rawY
        } else if (action == MotionEvent.ACTION_UP) {
            // 更新坐标
            for (listener in moveListener) {
                listener.onMoveEnd(x, y)
            }
        } else if (action == MotionEvent.ACTION_MOVE) {
            val rawX: Float = event.rawX
            val rawY: Float = event.rawY
            val dx: Float = rawX - this.lastX
            val dy: Float = rawY - this.lastY

            val curX = x + dx
            val curY = y + dy

            val h: Int
            if (resources.configuration.orientation == 1) {
                realX = max(0f, min(curX, (screenMin - width) * 1.0F))
                h = screenMax
            } else {
                realX = max(0f, min(curX, (screenMax - width) * 1.0F))
                h = screenMin
            }
            val realY = max(0.0f, min(curY, (h - height) * 1.0f))

            translationX = realX
            translationY = realY
            for (listener in moveListener) {
                listener.onMove(realX, realY, spotType)
            }
            x = realX
            y = realY

            lastX = rawX
            lastY = rawY
        }
        return true
    }

    private var orientation = resources.configuration.orientation
    override fun onConfigurationChanged(newConfig: Configuration?) {
        super.onConfigurationChanged(newConfig)
        newConfig?.let { config ->
            if (orientation != config.orientation) {
                orientation = config.orientation
                val screenW = DisplayUtils.getScreenWidth(context)
                val screenH = DisplayUtils.getScreenHeight(context)
                val xRatio = x / screenH
                x = (screenW - measuredWidth) * xRatio
                val yRatio = y / screenW
                y = (screenH - measuredHeight) * yRatio

                translationX = x
                translationY = y
                for (listener in moveListener) {
                    listener.onMove(x, y, spotType)
                }
            }
        }
    }

    fun add(gmTouchSpotView: GmTouchSpotView) {
        GmLifecycleUtils.getTopActivity()?.window?.decorView?.let { decorView ->
            (decorView as ViewGroup).apply {
                clipChildren = false
                addView(gmTouchSpotView)
            }
        }
    }

    fun hide() {
        visibility = View.INVISIBLE
    }

    fun remove() {
        GmLifecycleUtils.getTopActivity()?.window?.decorView?.let { decorView ->
            (decorView as ViewGroup).removeView(this)
        }
    }

    fun show() {
        visibility = View.VISIBLE
    }

    private var config: GmSpaceLinkerConfig? = null
    fun setConfig(config: GmSpaceLinkerConfig) {
        this.config = config
    }
}

1.2 滑动

滑动需要两个点位并且需要一段连接线,如下

class GmTouchSwipeSpotView : FrameLayout, GmSpotMoveListener {
    constructor(context: Context) : super(context) {
        init()
    }

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
        init()
    }

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context, attrs, defStyleAttr
    ) {
        init()
    }

    constructor(
        context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int
    ) : super(context, attrs, defStyleAttr, defStyleRes) {
        init()
    }

    private fun init() {
//        layoutParams = ViewGroup.LayoutParams(-2,-2)
        setWillNotDraw(false)
        isClickable = false
        isFocusable = false
        setFocusableInTouchMode(false)
        setClipChildren(false)

        mArrowBitmap =
            BitmapFactory.decodeResource(Ext.resource.resource, Ext.resource.getDrawableId("gamehelper_slide_arrow"))?.apply {
                mArrowHeight = height
                mArrowWidth = width
            }
    }

    private var mArrowWidth: Int = 0
    private var mArrowHeight: Int = 0
    private var mArrowBitmap: Bitmap? = null

    private var x = 0f
    private var y = 0f
    private var x2 = 0f
    private var y2 = 0f

    constructor (context: Context, x: Float, y: Float, x2: Float, y2: Float) : super(context) {
        this.x = x
        this.y = y
        this.x2 = x2
        this.y2 = y2
        init()
    }

    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        return false
    }

    private var pointSize = -1
    private var animationX = 0f
    private var animationY = 0f
    override fun draw(canvas: Canvas) {
        try {
            super.draw(canvas)
            if (pointSize == -1) return
            canvas.apply {
                val halfSize = pointSize / 2f
                drawLine(x + halfSize, y + halfSize, x2 + halfSize, y2 + halfSize, paint)
                drawArrow(
                    canvas,
                    x + halfSize,
                    y + halfSize,
                    if (isRunStatus) animationX + halfSize else x2 + halfSize,
                    if (isRunStatus) animationY + halfSize else y2 + halfSize,
                    mArrowWidth * 1.0f / 2,
                    mArrowHeight * 1.0f / 2
                )
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    private val paint = Paint().apply {
        color = Color.argb(125, 0, 0, 0)
        strokeWidth = 14f.dpToPx(context).toFloat()
        style = Paint.Style.FILL
    }

    fun add(gmTouchSpotView: GmTouchSwipeSpotView) {
        GmLifecycleUtils.getTopActivity()?.window?.decorView?.let { decorView ->
            (decorView as ViewGroup).apply {
                clipChildren = false
                addView(gmTouchSpotView)
            }
        }
    }

    private fun drawArrow(
        canvas: Canvas,
        fromX: Float,
        fromY: Float,
        toX: Float,
        toY: Float,
        width: Float,
        height: Float
    ) {

        val juli =
            sqrt(((toX - fromX) * (toX - fromX) + (toY - fromY) * (toY - fromY)).toDouble()).toFloat()
        if (juli <= (width * 4)) {
            return
        }
        val juliX = toX - fromX
        val juliY = toY - fromY

        val dianX = juliX / (if (isRunStatus) 1 else 2) + fromX - (width / juli * juliX)
        val dianY = juliY / (if (isRunStatus) 1 else 2) + fromY - (width / juli * juliY)
        val arrowStartX = dianX + (height / juli * juliY)
        val arrowStartY = dianY - (height / juli * juliX)
        val matrix = Matrix()
        matrix.postRotate(getAngle(fromX, fromY, toX, toY))
        matrix.postTranslate(arrowStartX, arrowStartY)
        canvas.drawBitmap(mArrowBitmap!!, matrix, (if (isRunStatus) arrowBitmapPaint else null))
    }

    private fun getAngle(fromX: Float, fromY: Float, toX: Float, toY: Float): Float {
        var angle = 0f
        if (toX != fromX && toY != fromY) {
            angle =
                (atan(abs(((toY - fromY) * 1.0f / (toX - fromX)).toDouble())) * 180 / Math.PI).toFloat()
        }
        if (toX > fromX && toY < fromY) {  //第一象限
            angle = 360 - angle
        } else if (toX > fromX && toY > fromY) { //第四象限
        } else if (toX < fromX && toY > fromY) { //第三象限
            angle = 180 - angle
        } else if (toX < fromX && toY < fromY) { //第二象限
            angle = 180 + angle
        } else if (toX == fromX && toY < fromY) {
            angle = 270f
        } else if (toX == fromX && toY > fromY) {
            angle = 90f
        } else if (toX < fromX && toY == fromY) {
            angle = 180f
        } else if (toX > fromX && toY == fromY) {
            angle = 0f
        }
        return angle
    }

    override fun onMove(newX: Float, newY: Float, @SpotType type: Int) {
        if (type == SPOT_START) {
            x = newX
            y = newY
        } else {
            x2 = newX
            y2 = newY
        }
        postInvalidate()
    }

    override fun onSizeChange(spotSize: Int) {
        pointSize = spotSize
        postInvalidate()
    }

    fun createListener(): GmSpotMoveListener {
        return this
    }

    fun hide() {
        visibility = View.INVISIBLE
    }

    fun remove() {
        GmLifecycleUtils.getTopActivity()?.window?.decorView?.let { decorView ->
            (decorView as ViewGroup).removeView(this)
        }
    }

    fun show() {
        visibility = View.VISIBLE
    }

    private var config: GmSpaceLinkerConfig? = null
    fun setConfig(config: GmSpaceLinkerConfig) {
        this.config = config
    }

    private val arrowBitmapPaint = Paint()
    private var isRunStatus = false
    private var arrowAnimation: ValueAnimator? = null

    fun moveUpdate(newX: Float,newY: Float) {
        if(config?.playStatus == true) {
            isRunStatus = true
            val f = (newX - x) / (x2 - x)
            arrowBitmapPaint.colorFilter = ColorMatrixColorFilter(ColorMatrix().apply {
                // 1 ~ 2
//            val scale = if (f <= 0.5) 1f + 2f * f else 2 - 2 * (f - 0.5f)
                // 1 ~ 3
//            val scale = if (f <= 0.5) 1f + 4 * f else 3 - 4 * (f - 0.5f)
                // 1 ~ 5
                val scale = if (f <= 0.5) 1f + 8 * f else 5 - 8 * (f - 0.5f)

                setScale(scale, scale, scale, 1.0f)
            })
            arrowBitmapPaint.alpha =
                    if (f <= 0.5) (f * 255 * 2).toInt() else ((1 - (f - 0.5) * 2) * 255).toInt()
            animationX = newX
            animationY = newY
            postInvalidate()
        } else {
            resetArrowPosition()
        }
    }

    fun startArrowAnimation(slideDuration: Long) {
        if (arrowAnimation == null) {
            arrowAnimation = GmAnimationUtil.fraction(duration = slideDuration) { f ->
                if (f != 1f) {
                    animationX = x + (x2 - x) * f
                    animationY = y + (y2 - y) * f
                    isRunStatus = true
                    arrowBitmapPaint.colorFilter = ColorMatrixColorFilter(ColorMatrix().apply {
                        // 1 ~ 2
//            val scale = if (f <= 0.5) 1f + 2f * f else 2 - 2 * (f - 0.5f)
                        // 1 ~ 3
//            val scale = if (f <= 0.5) 1f + 4 * f else 3 - 4 * (f - 0.5f)
                        // 1 ~ 5
                        val scale = if (f <= 0.5) 1f + 8 * f else 5 - 8 * (f - 0.5f)

                        setScale(scale, scale, scale, 1.0f)
                    })
                    arrowBitmapPaint.alpha =
                        if (f <= 0.5) (f * 255 * 2).toInt() else ((1 - (f - 0.5) * 2) * 255).toInt()
                } else { // 执行完了 将箭头重置
                    isRunStatus = false
                }
                postInvalidate()
            }
        } else {
            arrowAnimation?.setDuration(slideDuration)
        }
        arrowAnimation?.start()
    }

    fun resetArrowPosition() {
        arrowAnimation?.cancel()
        isRunStatus = false
        postInvalidate()
    }

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        arrowAnimation?.cancel()
    }
}

1.3 事件分发

mThread = Thread {
	while (config.playStatus && !Thread.currentThread().isInterrupted) {
		try {
			for ((index, recordBean) in config.recordBeanList.withIndex()) {
				Thread.sleep(wrapSpeedTime(gapTime))
				if (recordBean.type == 4) {
					playSwipe(config, recordBean)
				} else {
					playNormal(config, recordBean)
				}
			}
		} catch (e: InterruptedException) {
			e.printStackTrace()
			break
		} catch (e: Exception) {
			e.printStackTrace()
		}
	}
}

private fun dispatchEvent(event: MotionEvent, call: (() -> Unit)? = null) {
	Handler(Looper.getMainLooper()).post {
		event.source = 4098
		try {
			GmLifecycleUtils.getTopActivity()?.window?.decorView?.apply {
				dispatchTouchEvent(event)
			}
			call?.invoke()
		} catch (e: Exception) {
			e.printStackTrace()
		}
	}
}

2. 路径连点器

2.1 路径绘制与渲染

通过设置Activity的setWindowCallback获取事件进行绘制路径,绘制类GmPathRecordSpotView暴露handleTouchEvent(event: MotionEvent绘制时传入的是setWindowCallback获取的事件,渲染时传入的是绘制时保存的事件并通过状态机的方式来循环处理不同的事件逻辑。

class GmPathRecordSpotView(context: Context, val listener: GmPathRecordStatusListener) :
    FrameLayout(context) {
    private val path = Path()
    private val paint = Paint().apply {
        color = Color.argb(125, 0, 0, 0)
        strokeWidth = 14f.dpToPx(context).toFloat()
        style = Paint.Style.STROKE
        strokeJoin = Paint.Join.ROUND
        strokeCap = Paint.Cap.ROUND
        isAntiAlias = true
    }

    init {
        setWillNotDraw(false)
        isClickable = false
        isFocusable = false
        isFocusableInTouchMode = false
        clipChildren = false
    }

    private val pathRecordList: ArrayList<GmPathRecordModel> = arrayListOf()
    private var cacheRecordList: ArrayList<GmPathRecordModel> = arrayListOf()
    private var index: Int = 0

    private var lastX = 0f
    private var lastY = 0f

    private fun isOverMinSlidingDistance(f: Float, f2: Float): Boolean {
        return f.pow(2.0f) + f2.pow(2.0f) >= (30f).pow(2.0f)
    }

    // 录制路径
    fun handleWindowTouchEvent(event: MotionEvent) {
        if (type == 0) {
            handleTouchEvent(event)
        }
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        return false
    }

    private fun handleTouchEvent(event: MotionEvent) {
        val action = event.action
        if (action != MotionEvent.ACTION_DOWN) {
            if (action != MotionEvent.ACTION_UP) {
                if (action == MotionEvent.ACTION_CANCEL) {
                    ">>>>>>>>>>>>>>>>>>>>>>>> MotionEvent.ACTION_CANCEL ".logHelper()
                    pathRecordList[index - 1].let { record ->
                        record.coordinates.apply {
                            if (size > 1) {
                                lastX = event.x
                                lastY = event.y
                                record.tagFollow = "follow_${index}"
                                addPointView(record.tagFollow, index, true)
                            }
                            add(
                                GmPathRecordModel.PathRecordCoordinateBean(
                                    event.x,
                                    event.y,
                                    MotionEvent.ACTION_UP
                                )
                            )
                            val curTime = System.currentTimeMillis()
                            this[this.size - 1].nextGap = curTime - lastTime
                            lastTime = curTime
                        }
                    }
                } else if (action == MotionEvent.ACTION_MOVE) {
                    val curX = event.x
                    val curY = event.y
                    val dx: Float = curX - lastX
                    val dy: Float = curY - lastY

                    if (isOverMinSlidingDistance(dx, dy)) {
                        pathRecordList[index - 1].coordinates.apply {
                            add(
                                GmPathRecordModel.PathRecordCoordinateBean(
                                    curX,
                                    curY,
                                    MotionEvent.ACTION_MOVE
                                )
                            )

                            val curTime = System.currentTimeMillis()
                            this[this.size - 1].nextGap = curTime - lastTime
                            lastTime = curTime
                        }
                    }
                }
            } else {
                pathRecordList[index - 1].let { record ->
                    record.coordinates.apply {
                        if (size > 1) {
                            lastX = event.x
                            lastY = event.y
                            record.tagFollow = "follow_${index}"
                            addPointView(record.tagFollow, index, true)
                        }
                        add(
                            GmPathRecordModel.PathRecordCoordinateBean(
                                event.x,
                                event.y,
                                MotionEvent.ACTION_UP
                            )
                        )
                        val curTime = System.currentTimeMillis()
                        this[this.size - 1].nextGap = curTime - lastTime
                        lastTime = curTime
                    }
                }
            }
        } else {
            lastX = event.x
            lastY = event.y

            val curTime = System.currentTimeMillis()

            val model = GmPathRecordModel(++index)
            model.coordinates.add(
                GmPathRecordModel.PathRecordCoordinateBean(
                    lastX,
                    lastY,
                    MotionEvent.ACTION_DOWN
                )
            )
            model.startTime = curTime
            model.tag = "$index"

            if (pathRecordList.isNotEmpty()) {
                pathRecordList[pathRecordList.size - 1].apply {
                    totalTime = curTime - this.startTime
                    if (type == 1) {
                        canShow = false
                    }
                }
            }

            model.coordinates[model.coordinates.size - 1].nextGap = curTime - lastTime

            pathRecordList.add(model)
            lastTime = curTime

            addPointView(model.tag, index, false)
        }
        postInvalidate()
    }

    private val spotMap: ArrayMap<String, View> = ArrayMap()
    private fun dismissPointViewByTag(tag: String, index: Int) {
        if (spotMap.containsKey(tag)) {
            spotMap[tag]?.apply {
                visibility = View.INVISIBLE
            }
        }
    }


    private fun showPointViewByTag(tag: String) {
        if (spotMap.containsKey(tag)) {
            spotMap[tag]?.apply {
                visibility = View.VISIBLE
                handlerSpotAnimation(this, tag)
            }
        }
    }

    private fun dismissAllPointView() {
        for (spotView in spotMap) {
            spotView.value.visibility = View.INVISIBLE
        }
    }

    private fun showAllPointView() {
        for (spotView in spotMap) {
            spotView.value.visibility = View.VISIBLE
        }
    }

    private fun removeAllPointView() {
        for (spotView in spotMap) {
            val view = spotView.value
            if (view.parent != null) {
                (view.parent as ViewGroup).removeView(view)
            }
        }
        spotMap.clear()
    }

    private var pointViewSize = 40f.dpToPx(context)
    private var isCalcPointViewSize = false
    private fun addPointView(tag: String, index: Int, isUp: Boolean) {
        if (spotMap.containsKey(tag)) {
            showPointViewByTag(tag)
            return
        }
        getLayoutView("gamehelper_point")
            .let { pointView ->
                if(!isCalcPointViewSize) {
                    isCalcPointViewSize = true
                    pointView.post {
                        pointViewSize = pointView.measuredWidth
                    }
                }
                (getWidgetView(pointView, "num") as TextView).text = "$index"
                val size = 40f.dpToPx(context)
                pointView.translationX = lastX - size / 2
                pointView.translationY = lastY - size / 2
                spotMap[tag] = pointView

                this.addView(pointView, ViewGroup.LayoutParams(-2, -2))

                handlerSpotAnimation(pointView, tag)
            }
    }

    private fun handlerSpotAnimation(view: View, tag: String) {
        var duration = 150L
        val isTagFollow = tag.contains("follow_")
        var isSwipe = if (isTagFollow) true else spotMap.containsKey("follow_$tag")
        try {
            var totalTime = cacheRecordList[index - 1].totalTime
            var endTime = 0L
            if ((index + 1) < cacheRecordList.size) {
                endTime = cacheRecordList[index].coordinates[0].nextGap
            } else {
                endTime = GmPathRecordManager.pathRecordListModel?.endRemainTime ?: 150
            }
            totalTime -= endTime
//            if ((totalTime + endTime) >= duration * 2) {
//            } else {
//                duration = ((totalTime + endTime)) / 2
//            }

            if (totalTime < duration) {
                duration = totalTime
            }

            if (isTagFollow) {
                GmAnimationUtil.scaleBrightness(
                    (getWidgetView(view, "point") as ImageView),
                    isRunning,
                    // 150 * 2 放大和缩小
                    duration = if (endTime > 300) 300 else endTime,
                    scaleParams = floatArrayOf(
                        1.0f,
                        1.2f,
                        1.0f
                    ),
                    brightNessParams = floatArrayOf(1.0f, 2.0f, 1.0f),
                )
            } else {
                handler.postDelayed({
                    GmAnimationUtil.scaleBrightness(
                        (getWidgetView(view, "point") as ImageView),
                        isRunning,
                        duration = duration,
                        scaleParams = floatArrayOf(0.8f, 1.0f),
                        brightNessParams = floatArrayOf(2.0f, 1.0f),
                    )
                }, if(isSwipe) totalTime else totalTime - duration)
                GmAnimationUtil.scaleBrightness(
                    (getWidgetView(view, "point") as ImageView),
                    isRunning,
                    duration = duration,
                    scaleParams = floatArrayOf(1.0f, 0.8f),
                    brightNessParams = floatArrayOf(1.0f, 2.0f),
                )
            }
        } catch (e: Exception) {
        }
    }

    @Volatile
    private var isRunning = false

    fun pause() {
        isRunning = false
        handler.removeCallbacksAndMessages(null)
        sleepTime = sleepRemainTime
    }

    fun resume() {
        isRunning = true
        sleepStartTime = SystemClock.elapsedRealtime()
        if (runnableType == 0) {
            handler.post(countDownRunnable)
        } else if (runnableType == 1) {
            handler.post(remainCountDownRunnable)
        } else if (runnableType == 2) {
            handler.post(loopFinishCountDownRunnable)
        }
    }

    private val handler = Handler(Looper.getMainLooper())

    // 0 设置路径  1 播放路径 2 隐藏路径
    private var lastTime = 0L
    private var startTime = 0L
    private var type = -1


    fun hidePath() {
        setType(2)
    }

    fun stopPlay() {
        isRunning = false
        hidePath()
    }

    fun stopRecord() {
        hidePath()
        val endRemainTime = System.currentTimeMillis() - lastTime
        GmPathRecordManager.pathRecordListModel = GmPathRecordList().apply {
            isPathRecord = true
            recordModelList = ArrayList(pathRecordList)
            this.endRemainTime = endRemainTime
            val curTime = System.currentTimeMillis()
            if (pathRecordList.isNotEmpty()) {
                // 记录最后一个路径的总时间
                pathRecordList[pathRecordList.size - 1].apply {
                    totalTime = curTime - this.startTime
                }
            }
            totalTime = curTime - startTime
            ">>>>>>>>>> 结束录制 总时长:$totalTime".logHelper()
            name = "默认方案${GmSpaceRuleSpUtils.getAllRecordList().size + 1}"
        }
    }


    // OUT_LOOP, INNER_LOOP, REVERSE_ANIMATION, ANIMATION, LOGIC
    private val OUT_LOOP = 0
    private val INNER_LOOP = 1
    private val REVERSE_ANIMATION = 2
    private val ANIMATION = 3
    private val LOGIC = 4


    private var currentStep = OUT_LOOP
    private var outLoopIndex = 0
    private var innerLoopIndex = 0
    private var curloopCount = 1
    private var sleepTime = 0L
    private var sleepStartTime = 0L

    private var runnableType = 0

    // 睡眠剩余时间
    private var sleepRemainTime = 0L
    private val countDownRunnable: Runnable = object : Runnable {
        override fun run() {
            runnableType = 0
            val dur = SystemClock.elapsedRealtime() - sleepStartTime
            sleepRemainTime = sleepTime - dur
            if (sleepRemainTime <= 0) {
                currentStep = LOGIC
                exeRecord()
                return
            }
            handler.postDelayed(this, 1)
        }
    }

    private val remainCountDownRunnable: Runnable = object : Runnable {
        override fun run() {
            runnableType = 1
            val dur = SystemClock.elapsedRealtime() - sleepStartTime
            sleepRemainTime = sleepTime - dur
//            ">>>>>>>>>>>>>>>>> remainCountDownRunnable $sleepTime".logHelper()
            if (sleepRemainTime <= 0) {
                pathRecordList.clear()
                dismissAllPointView()
                listener.onRunPathIndex(outLoopIndex + 1)
                currentStep = INNER_LOOP
                exeRecord()
                return
            }
            handler.postDelayed(this, 1)
        }
    }

    private val loopFinishCountDownRunnable: Runnable = object : Runnable {
        override fun run() {
            runnableType = 2
            val dur = SystemClock.elapsedRealtime() - sleepStartTime
            sleepRemainTime = sleepTime - dur
//            ">>>>>>>>>>>>>>>>> loopFinishCountDownRunnable $sleepTime".logHelper()
            if (sleepRemainTime <= 0) {
                curloopCount = 1
                listener.playFinish()
                return
            }
            handler.postDelayed(this, 1)
        }
    }

    private fun exeRecord() {
        if (!isRunning)
            return
        when (currentStep) {
            OUT_LOOP -> {
                if (outLoopIndex >= cacheRecordList.size) {
                    // 是否需要重新开始
                    outLoopIndex = 0
                    val loopCount = GmPathRecordManager.pathRecordListModel?.loopCount ?: 0
                    val loopType = GmPathRecordManager.pathRecordListModel?.loopType ?: 0
                    if (loopType == 0) {
                        index = 0
                        isFromStart = false
                        sleepStartTime = SystemClock.elapsedRealtime()
                        sleepTime = GmPathRecordManager.pathRecordListModel?.endRemainTime ?: 0
                        handler.post(remainCountDownRunnable)
                    } else {
                        if (curloopCount >= loopCount) {
                            sleepStartTime = SystemClock.elapsedRealtime()
                            sleepTime = GmPathRecordManager.pathRecordListModel?.endRemainTime ?: 0
                            handler.post(loopFinishCountDownRunnable)
                            return
                        }
                        index = 0
                        isFromStart = false
                        curloopCount += 1
                        listener.playRoundFinish(curloopCount)

                        sleepStartTime = SystemClock.elapsedRealtime()
                        sleepTime = GmPathRecordManager.pathRecordListModel?.endRemainTime ?: 0
                        handler.post(remainCountDownRunnable)
                    }
                } else {
                    listener.onRunPathIndex(outLoopIndex + 1)
                    currentStep = INNER_LOOP
                    exeRecord()
                }
            }

            INNER_LOOP -> {
                val size = cacheRecordList[outLoopIndex].coordinates.size
                val recordModel = cacheRecordList[outLoopIndex]
                if (innerLoopIndex < size) {
                    val coordinateBean = recordModel.coordinates[innerLoopIndex]
                    sleepTime = coordinateBean.nextGap
                    val gapTime = GmPathRecordManager.pathRecordListModel?.gapTime ?: 300
                    if (innerLoopIndex == 0) { // 处理每段的开头
                        if (outLoopIndex == 0 && !isFromStart) {
                            sleepTime += gapTime
                            listener.playSleep(sleepTime)
                        } else {
                            listener.playSleep(sleepTime)
                        }
                    }

                    // 延迟执行 分发逻辑  以及动画
                    sleepStartTime = SystemClock.elapsedRealtime()
                    handler.post(countDownRunnable)
                } else {
                    // 移除point

                    // 下一条路径
                    innerLoopIndex = 0
                    currentStep = OUT_LOOP
                    outLoopIndex += 1
                    exeRecord()
                }
            }

            REVERSE_ANIMATION -> {

            }

            ANIMATION -> {

            }

            LOGIC -> {
//                cacheRecordList[outLoopIndex].totalTime

                val coordinateBean = cacheRecordList[outLoopIndex].coordinates[innerLoopIndex]

                listener.playNotify()
                // 添加point

                val uptimeMillis = SystemClock.uptimeMillis()
                val motionEvent = MotionEvent.obtain(
                    uptimeMillis,
                    uptimeMillis,
                    coordinateBean.action,
                    coordinateBean.x,
                    coordinateBean.y,
                    0
                )
                motionEvent.source = 4098
                if (context is Activity) {
                    (context as Activity).apply {
                        runOnUiThread {
                            window.decorView.dispatchTouchEvent(
                                motionEvent
                            )
                        }
                    }
                }
                handleTouchEvent(motionEvent)
                innerLoopIndex += 1
                currentStep = INNER_LOOP
                exeRecord()
            }
        }
    }

    fun setType(type: Int) {
        this.type = type
        index = 0
        if (type == 0) {
            pathRecordList.clear()
            cacheRecordList.clear()
            startTime = System.currentTimeMillis()
            lastTime = startTime
        }
        if (type == 1) {
            isRunning = true
            isFromStart = true
            curloopCount = 1
            currentStep = OUT_LOOP
            if (cacheRecordList.isEmpty()) {
                cacheRecordList = ArrayList(pathRecordList)
            }
            pathRecordList.clear()

            exeRecord()
        }
        dismissAllPointView()
        postInvalidate()
    }

    private var isFromStart = true
    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        mSingleThreadExecutor.shutdownNow()
        handler.removeCallbacksAndMessages(null)
    }

    private var mSingleThreadExecutor = Executors.newSingleThreadExecutor()

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        // 0 处理MotionEvent  1 绘制 2 隐藏
        if (type == 2) {
            return
        }
        val drawPathModel: ArrayList<GmPathRecordModel> = ArrayList(pathRecordList)
        for ((index, pathModel) in drawPathModel.withIndex()) {
            if (pathModel.canShow) {
                drawLinePath(pathModel, canvas)
            } else {
                dismissPointViewByTag(pathModel.tag, index)
                dismissPointViewByTag(pathModel.tagFollow, index)
            }
        }
    }

    private fun drawLinePath(pathModel: GmPathRecordModel, canvas: Canvas) {
        val size = pathModel.coordinates.size
        if (size > 0) {
            val firstCoordinateBean = pathModel.coordinates[0]
            path.reset()
            path.moveTo(firstCoordinateBean.x, firstCoordinateBean.y)
            if (size > 1) {
                for (i in 1 until pathModel.coordinates.size) {
                    val lastCoordinateBean = pathModel.coordinates[i - 1]
                    val coordinateBean = pathModel.coordinates[i]
                    val x = lastCoordinateBean.x
                    val y = lastCoordinateBean.y
                    path.quadTo(x, y, (coordinateBean.x + x) / 2.0f, (coordinateBean.y + y) / 2.0f)
                }
            }
            canvas.drawPath(path, paint)
        }
    }

    fun setPathModel(pathRecordList: ArrayList<GmPathRecordModel>) {
        cacheRecordList = ArrayList(pathRecordList)
    }
}
本文链接:https://iichen.cn/?p=793
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇