本文最后更新于 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)
}
}