携手创造,一起成长!这是我参加「日新计划 8 月更文挑战」的第 1 天,点击检查活动详情


背景

因项目需求变更,公司内的工厂测验程序重写,之前的接触屏测验已不契合项目需求,遂对比了某米和某为的接触屏测验作用,个人觉得某米的作用不错,尽管终究没有被采纳,但是这不妨碍咱们实现一下某米的接触屏测验。或许因机型不同,打开某米的接触屏测验的办法也不尽相同,读者请自行百度相应机型的办法。

Android-自定义View-仿某米的触摸屏测试

某米的接触屏测验如上图所示,咱们简略剖析一下:

  • 屏幕四周、笔直居中和水平居中有制作单元格,接触后会重绘色彩。
  • 屏幕两个对角线有类似于管道的图画,此图画重绘只能从制作 X 号的地方开端,一直到管道对端的 X 号地方停止,假如期间手指接触超出管道的规模即失利。
  • 手指接触在单元格与管道内的区域滑动时,屏幕会显现滑动轨道,假如超出区域,则轨道消失。
  • 一切单元格与两条管道重绘完毕则测验完结。

制作单元格

思路如下:以左上角的单元格为起点,核算出一切单元格的坐标保存起来,终究在 onDraw 办法中遍历单元格组,依据坐标进行制作。

首要界说单元格的基准宽高与终究宽高变量:

private var itemWidthBasic = 90
private var itemHeightBasic = 80
private var itemWidth = -1F
private var itemHeight = -1F

其次界说自界说 View 的宽高变量:

private var viewWidth: Int = -1
private var viewHeight: Int = -1

终究界说单元格在宽高方向上的数量变量:

private var widthCount = -1
private var heightCount = -1

界说制作单元格的画笔:

private val boxPaint by lazy {
    Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.GRAY
        style = Paint.Style.STROKE
        strokeWidth = 2F
    }
}

界说 TouchRectF 实体,对 RectF 包装一层,添加 isReDrawable 变量,单元格被接触重绘后符号为 True:

data class TouchRectF(val rectF: RectF, var isReDrawable: Boolean = false) {
    fun reset() {
        isReDrawable = false
    }
}

界说保存单元格坐标的容器,其中包括屏幕上下左右以及笔直与水平居中的坐标容器:

// 屏幕左边单元格的坐标容器
private val leftRectFList = mutableListOf<TouchRectF>()
// 屏幕顶部单元格的坐标容器
private val topRectFList = mutableListOf<TouchRectF>()
// 屏幕右侧单元格的坐标容器
private val rightRectFList = mutableListOf<TouchRectF>()
// 屏幕底部单元格的坐标容器
private val bottomRectFList = mutableListOf<TouchRectF>()
// 屏幕水平居中单元格的坐标容器
private val centerHorizontalRectFList = mutableListOf<TouchRectF>()
// 屏幕笔直居中单元格的坐标容器
private val centerVerticalRectFList = mutableListOf<TouchRectF>()

选择在 onLayout 办法中核算一切单元格的坐标并获取 View 的宽高:

override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
    super.onLayout(changed, left, top, right, bottom)
    // 保存 View 的宽高
    viewWidth = width
    viewHeight = height
    computeRectF()
}

下图显现了一切单元格地点的规模:

Android-自定义View-仿某米的触摸屏测试

computeRectF 办法中核算单元格的宽高、数量及坐标:

  1. 首要以单元格的基准宽高核算单元格宽高方向上的数量
  2. 其次以单元格宽高方向上的数量核算单元格的终究宽高
  3. 铲除之前核算的成果

依据上面单元格规模示意图:

  • 核算并保存左边单元格的坐标,不包括头和尾,去掉与顶部和底部堆叠的单元格
  • 核算并保存顶部单元格的坐标
  • 核算并保存右侧单元格的坐标,不包括头和尾,去掉与顶部和底部堆叠的单元格
  • 核算并保存底部单元格的坐标
  • 核算并保存水平居中单元格的坐标,不包括头和尾,去掉与左边和右侧堆叠的单元格
  • 核算并保存笔直居中单元格的坐标,不包括头和尾,去掉与顶部和底部堆叠的单元格,且去掉与水平居中堆叠的单元格
private fun computeRectF() {
    // 以单元格的基准宽高核算单元格宽高方向上的数量
    widthCount = viewWidth / itemWidthBasic
    heightCount = viewHeight / itemHeightBasic
    // 以单元格宽高方向上的数量再核算单元格的终究宽高
    itemWidth = viewWidth.toFloat() / widthCount
    itemHeight = viewHeight.toFloat() / heightCount
    // 清空之前核算的成果
    leftRectFList.clear()
    topRectFList.clear()
    rightRectFList.clear()
    bottomRectFList.clear()
    centerHorizontalRectFList.clear()
    centerVerticalRectFList.clear()
    // 核算并保存屏幕左边单元格的坐标, 不包括头和尾, 去掉与顶部和底部堆叠的单元格
    for (i in 1 until heightCount - 1) {
        val rectF = RectF(0F, itemHeight * i, itemWidth, itemHeight * (i + 1))
        leftRectFList.add(TouchRectF(rectF))
    }
    // 核算并保存屏幕顶部单元格的坐标
    for (i in 0 until widthCount) {
        val rectF = RectF(itemWidth * i, 0F, itemWidth * (i + 1), itemHeight)
        topRectFList.add(TouchRectF(rectF))
    }
    // 核算并保存屏幕右侧单元格的坐标, 不包括头和尾, 去掉与顶部和底部堆叠的单元格
    for (i in 1 until heightCount - 1) {
        val rectF = RectF(
            viewWidth - itemWidth,
            itemHeight * i,
            viewWidth.toFloat(),
            itemHeight * (i + 1)
        )
        rightRectFList.add(TouchRectF(rectF))
    }
    // 核算并保存屏幕底部单元格的坐标
    for (i in 0 until widthCount) {
        val rectF = RectF(
            itemWidth * i,
            viewHeight - itemHeight,
            itemWidth * (i + 1),
            viewHeight.toFloat()
        )
        bottomRectFList.add(TouchRectF(rectF))
    }
    // 核算并保存屏幕水平居中单元格的坐标, 不包括头和尾, 去掉与左边和右侧堆叠的单元格
    val centerHIndex = heightCount / 2
    for (i in 1 until widthCount - 1) {
        val rectF = RectF(
            itemWidth * i,
            itemHeight * centerHIndex,
            itemWidth * (i + 1),
            itemHeight * (centerHIndex + 1)
        )
        centerHorizontalRectFList.add(TouchRectF(rectF))
    }
    // 核算并保存屏幕笔直居中单元格的坐标, 不包括头和尾, 去掉与顶部和底部堆叠的单元格, 且去掉与水平居中堆叠的单元格
    val centerVIndex = widthCount / 2
    val skipIndex: Int = centerHIndex
    for (i in 1 until heightCount - 1) {
        // 越过与横轴穿插的部分
        if (i == skipIndex) {
            continue
        }
        val rectF = RectF(
            itemWidth * centerVIndex,
            itemHeight * i,
            itemWidth * (centerVIndex + 1),
            itemHeight * (i + 1)
        )
        centerVerticalRectFList.add(TouchRectF(rectF))
    }
}

接下来在 onDraw 中制作单元格:

override fun onDraw(canvas: Canvas) {
    // 单元格数量为 -1 时回来
    if (widthCount == -1 || heightCount == -1) {
        return
    }
    // 清空画布
    canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR)
    canvas.drawColor(Color.WHITE)
    // 制作水平方向的单元格
    drawHorizontalBox(canvas)
    // 制作笔直方向的单元格
    drawVerticalBox(canvas)
}
private fun drawHorizontalBox(canvas: Canvas) {
    for (rectF in topRectFList) {
        drawBox(rectF, canvas)
    }
    for (rectF in centerHorizontalRectFList) {
        drawBox(rectF, canvas)
    }
    for (rectF in bottomRectFList) {
        drawBox(rectF, canvas)
    }
}
private fun drawVerticalBox(canvas: Canvas) {
    for (rectF in leftRectFList) {
        drawBox(rectF, canvas)
    }
    for (rectF in centerVerticalRectFList) {
        drawBox(rectF, canvas)
    }
    for (rectF in rightRectFList) {
        drawBox(rectF, canvas)
    }
}
private fun drawBox(rectF: TouchRectF, canvas: Canvas) {
    canvas.drawRect(rectF.rectF, boxPaint)
}
  1. 首要做参数校验与画布清空
  2. 然后制作水平方向的单元格,在 drawHorizontalBox 办法中遍历水平方向的单元格坐标容器,再调用 drawBox 办法传入坐标制作单元格
  3. 终究制作笔直方向的单元格,在 drawVerticalBox 办法中遍历笔直方向的单元格坐标容器,再调用 drawBox 办法传入坐标制作单元格
  4. drawBox 办法中制作单元格

作用如下:

Android-自定义View-仿某米的触摸屏测试

制作穿插管道

上面制作单元格比较简略一些,现在要制作的两个管道相对杂乱一些,本文为了简略,没有彻底仿照某米接触屏测验中管道的 UI 作用。

思路:经过 Path 衔接对角两个单元格的极点组成管道,由于 Path 闭合后,单元格的两个极点会衔接成直线,这儿两个极点的衔接使用二阶贝赛尔曲线制作一个 View 显现规模之外的弧线,这样看起来管道没有起止点,且 Path 也可以闭合,一起也方便判别接触点是否在管道内。

界说管道 Path 和 Region:

/**
 * /
 */
private val positiveCrossPath = TouchPath()
private val positiveCrossRegion = Region()
/**
 * \
 */
private val reverseCrossPath = TouchPath()
private val reverseCrossRegion = Region()

computeRectF 办法中核算管道 Path 途径:

  • 重置 Path 途径
  • 核算正向管道 Path,以左下角单元格为起点,右上角单元格为结尾制作 Path。
  • 核算反向管道 Path,以左上角单元格为起点,右下角单元格为结尾制作 Path。
  • 下面代码中的注释阐明的更多一些。
private fun computeRectF() {
    // 省掉核算单元格的代码
    // 重置 Path
    positiveCrossPath.path.reset()
    reverseCrossPath.path.reset()
    // PositiveCross
    // 获取左下角单元格坐标
    val lbRectF = bottomRectFList.first().rectF
    // 获取右上角单元格坐标
    val rtRectF = topRectFList.last().rectF
    with(positiveCrossPath.path) {
        // 移动 Path 至左下角单元格的左上角极点
        moveTo(lbRectF.left, lbRectF.top)
        // 衔接直线至右上角单元格的左上角极点
        lineTo(rtRectF.left, rtRectF.top)
        // 以右上角单元格的右上角极点坐标为基准核算屏幕外一点为控制点, 右上角单元格的右下角极点为完毕点制作二阶贝赛尔曲线
        quadTo(
            rtRectF.right + itemWidth,
            rtRectF.top - itemHeight,
            rtRectF.right,
            rtRectF.bottom
        )
        // 衔接直线至左下角单元格的右下角极点
        lineTo(lbRectF.right, lbRectF.bottom)
        // 以左下角单元格的左下角极点坐标为基准核算屏幕外一点为控制点, 左下角单元格的左上角极点为完毕点制作二阶贝赛尔曲线
        quadTo(
            lbRectF.left - itemWidth,
            lbRectF.bottom + itemHeight,
            lbRectF.left,
            lbRectF.top
        )
        // 闭合 Path
        close()
    }
    // 核算正向管道 Path 区域
    val positiveCrossRectF = RectF()
    positiveCrossPath.path.computeBounds(positiveCrossRectF, true)
    positiveCrossRegion.setPath(positiveCrossPath.path, positiveCrossRectF.toRegion())
    // ReverseCross
    // 获取左上角单元格坐标
    val ltRectF = topRectFList.first().rectF
    // 获取右下角单元格坐标
    val rbRectF = bottomRectFList.last().rectF
    with(reverseCrossPath.path) {
        // 移动 Path 至左上角单元格的右上角极点
        moveTo(ltRectF.right, ltRectF.top)
        // 衔接直线只右下角单元格的右上角极点
        lineTo(rbRectF.right, rbRectF.top)
        // 以右下角单元格的右下角极点坐标为基准核算屏幕外一点为控制点, 右下角单元格的左下角极点为完毕点制作二阶贝赛尔曲线
        quadTo(
            rbRectF.right + itemWidth,
            rbRectF.bottom + itemHeight,
            rbRectF.left,
            rbRectF.bottom
        )
        // 衔接直线至左上角单元格的左下角极点
        lineTo(ltRectF.left, ltRectF.bottom)
        // 以左上角单元格的左下角极点坐标为基准核算屏幕外一点为控制点, 左上角单元格的右上角极点为完毕点制作二阶贝赛尔曲线
        quadTo(
            ltRectF.left - itemWidth,
            ltRectF.top - itemHeight,
            ltRectF.right,
            ltRectF.top
        )
        // 闭合 Path
        close()
    }
    // 核算反向管道 Path 区域
    val reverseCrossRectF = RectF()
    reverseCrossPath.path.computeBounds(reverseCrossRectF, true)
    reverseCrossRegion.setPath(reverseCrossPath.path, reverseCrossRectF.toRegion())
}

接下来在 onDraw 办法中制作管道:

override fun onDraw(canvas: Canvas) {
    // 省掉制作单元格代码
    drawPositiveCross(canvas)
    drawReverseCross(canvas)
}
private fun drawReverseCross(canvas: Canvas) {
    canvas.drawPath(reverseCrossPath.path, boxPaint)
}
private fun drawPositiveCross(canvas: Canvas) {
    canvas.drawPath(positiveCrossPath.path, boxPaint)
}

作用如下:

Android-自定义View-仿某米的触摸屏测试

重绘单元格

单元格与管道现已制作好了,下面咱们先开端重绘单元格。

大体思路:在手指接触屏幕的时分判别当时接触的屏幕坐标是否在单元格内,是的话则重绘,不然不重绘。

首要界说重绘单元格的画笔:

private val fillPaint by lazy {
    Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.GREEN
        style = Paint.Style.FILL
    }
}

由于单元格与单元格、单元格与管道之间有堆叠的部分,突出显现堆叠部分哪块单元格没有被重绘,重绘单元格时改动单元格边框的色彩,所以需求界说重绘单元格的画笔:

private val redrawBoxPaint by lazy {
    Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.YELLOW
        style = Paint.Style.STROKE
        strokeWidth = 3F
    }
}

咱们重写 onTouchEvent 办法,在此办法中处理接触事情:

override fun onTouchEvent(event: MotionEvent): Boolean {
    val x = event.x
    val y = event.y
    when (event.actionMasked) {
        MotionEvent.ACTION_DOWN -> {
            // 依据当时坐标查找可重绘的单元格
            findReDrawableBox(x, y)
            // 重绘 View
            invalidate()
        }
    }
    return true
}
// 查找可重绘的单元格
private fun findReDrawableBox(x: Float, y: Float) {
    val touchRectF = (leftRectFList.find { it.rectF.contains(x, y) }
        ?: topRectFList.find { it.rectF.contains(x, y) }
        ?: rightRectFList.find { it.rectF.contains(x, y) }
        ?: bottomRectFList.find { it.rectF.contains(x, y) }
        ?: centerHorizontalRectFList.find { it.rectF.contains(x, y) }
        ?: centerVerticalRectFList.find { it.rectF.contains(x, y) })
    if (touchRectF != null) {
        // 符号可重绘的单元格
        markBoxReDrawable(touchRectF)
    }
}
// 符号可重绘的单元格
private fun markBoxReDrawable(rectF: TouchRectF) {
    if (!rectF.isReDrawable) {
        rectF.isReDrawable = true
    }
}

onTouchEvent 办法中,咱们监听 ACTION_DOWN 事情,依据当时接触屏幕的坐标查找可重绘的单元格,假如查找到匹配的单元格且此单元格现在还没有被重绘,则符号此单元格为可重绘的。

接下来,咱们重构制作单元格的 drawBox 办法来重绘单元格:

// 重构 drawBox 办法,添加剧绘代码
private fun drawBox(rectF: TouchRectF, canvas: Canvas) {
    // 判别当时单元格是否现已符号为可重绘
    if (rectF.isReDrawable) {
        // 重绘单元格
        canvas.drawRect(rectF.rectF, redrawBoxPaint)
        canvas.drawRect(rectF.rectF, fillPaint)
    } else {
        canvas.drawRect(rectF.rectF, boxPaint)
    }
}

制作轨道线

一个个方格点击不太实际,因此添加手指滑动重绘单元格,一起添加手指滑动轨道线制作。

界说轨道线 Path:

private val linePath = Path()

界说轨道线画笔:

private val linePaint by lazy {
    Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.BLUE
        style = Paint.Style.STROKE
        strokeWidth = 8F
        strokeJoin = Paint.Join.ROUND
        strokeCap = Paint.Cap.ROUND
    }
}

接下来,需求修改 onTouchEvent 办法添加对滑动重绘单元格和制作轨道线的支持:

override fun onTouchEvent(event: MotionEvent): Boolean {
    val x = event.x
    val y = event.y
    when (event.actionMasked) {
        MotionEvent.ACTION_DOWN -> {
            // 清空轨道线 Path
            linePath.reset()
            // 移动轨道线起点至点击坐标
            linePath.moveTo(x, y)
            // 依据当时坐标查找可重绘的单元格
            findReDrawableBox(x, y)
        }
        MotionEvent.ACTION_MOVE -> {
            // 判别当时坐标是否在单元格和管道区域内
            if (isInTouchableRegion(x, y)) {
                if (linePath.isEmpty) {
                    // 假如被重置了,先移动起点至当时坐标
                    linePath.moveTo(x, y)
                } else {
                    // 没有被重置,衔接直线至当时坐标
                    linePath.lineTo(x, y)
                }
                // 依据当时坐标查找可重绘的单元格
                findReDrawableBox(x, y)
            } else {
                // 清空轨道线 Path
                linePath.reset()
            }
            // 重绘View
            invalidate()
        }
        MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
            // 清空轨道线 Path
            linePath.reset()
            // 重绘View
            invalidate()
        }
    }
    return true
}
// 判别当时坐标是否在单元格和管道区域内
private fun isInTouchableRegion(x: Float, y: Float): Boolean {
    return leftRectFList.any { it.rectF.contains(x, y) } ||
    		topRectFList.any { it.rectF.contains(x, y) } ||
    		rightRectFList.any { it.rectF.contains(x, y) } ||
    		bottomRectFList.any { it.rectF.contains(x, y) } ||
    		centerHorizontalRectFList.any { it.rectF.contains(x, y) } ||
    		centerVerticalRectFList.any { it.rectF.contains(x, y) } ||
    		positiveCrossRegion.contains(x.toInt(), y.toInt()) ||
    		reverseCrossRegion.contains(x.toInt(), y.toInt())

首要在 ACTION_DOWN 中清空轨道线 Path,并移动轨道线起点至当时坐标。

然后在 ACTION_MOVE 中先判别当时坐标是否在单元格和管道区域内,假如不在区域内,不制作轨道线,则重置轨道线 Path;不然再判别轨道线 Path 是否为空,为空以为现已被重置,先移动轨道线起点至当时坐标,不然以为没有被重置,衔接直线至当时坐标;终究重绘 View。

接下来修改 onDraw 方格,添加轨道线的制作:

override fun onDraw(canvas: Canvas) {
    // 省掉制作单元格和管道代码
    // 制作轨道线
    drawTrackLine(canvas)
}
private fun drawTrackLine(canvas: Canvas) {
    // 判别轨道线 Path 是否为空
    if (linePath.isEmpty) {
        return
    }
    // 制作轨道线
    canvas.drawPath(linePath, linePaint)
}

作用如下:

Android-自定义View-仿某米的触摸屏测试

重绘穿插管道

在某米的接触屏测验中,笔者发现有以下几个条件需求注意:

  1. 假如滑动期间超出管道的规模以为无效。
  2. 只能从管道一端开端接触,即从管道中间接触视为无效。
  3. 假如从管道一端开端,不是经过管道抵达另一端以为无效,即开端时是从管道一端开端,期间经过沿单元格滑动抵达另一端。

以上几个问题中,第一个问题上面制作轨道线时现已解决,下面咱们解决其他几个问题。

解决思路:

  • 问题2:判别轨道线的起点坐标是否在四个极点单元格区域内。
  • 问题3:判别轨道线上一切点的坐标是否在管道区域内。

首要界说 PathMeasure 变量,用于获取轨道线 Path 上各点的坐标:

private val linePathMeasure = PathMeasure()

onTouchEvent 办法中的 ACTION_MOVE 分支中添加 findReDrawableCross 重绘管道逻辑:

override fun onTouchEvent(event: MotionEvent): Boolean {
    when (event.actionMasked) {
        // 省掉 ACTION_DOWN
        MotionEvent.ACTION_MOVE -> {
            // 判别当时坐标是否在单元格和管道区域内
            if (isInTouchableRegion(x, y)) {
                // 省掉之前代码
                // 依据当时坐标查找可重绘的单元格
                findReDrawableBox(x, y)
                // 新增重绘管道逻辑
                findReDrawableCross()
            }
        }
        // 省掉 ACTION_UP、ACTION_CANCEL
    }
    return true
}

findReDrawableCross 办法代码较多且略微杂乱一些,下面我把代码拆开逐步剖析。

轨道线途径测量及校验

  1. 首要校验轨道线 Path 是否为空,为空则回来。
  2. 把轨道线 Path 设置给途径测量器。
  3. 获取轨道线 Path 的起点与结尾的坐标并校验坐标合法性。
private fun findReDrawableCross() {
    // 轨道线 Path 为空回来
    if (linePath.isEmpty) {
        return
    }
    // 把轨道线 Path 设置给途径测量器
    linePathMeasure.setPath(linePath, false)
    // 界说起点与结尾坐标数组
    val startPoint = FloatArray(2)
    val endPoint = FloatArray(2)
    // 获取 Path 长度
    val linePathLength = linePathMeasure.length
    // 核算起点与结尾坐标
    linePathMeasure.getPosTan(0F, startPoint, null)
    linePathMeasure.getPosTan(linePathLength, endPoint, null)
    // 校验起点坐标
    val startX = startPoint[0]
    val startY = startPoint[1]
    if (startX == 0F || startY == 0F) {
        return
    }
    // 校验结尾坐标
    val endX = endPoint[0]
    val endY = endPoint[1]
    if (endX == 0F || endY == 0F) {
        return
    }
}

重绘正向管道

  1. 获取正向管道两头的单元格。
  2. 判别轨道线的起点与结尾坐标是否都在管道两头的单元格区域内。
  3. 遍历轨道线,判别轨道线上点的坐标是否在管道区域内。
  4. 符号正向管道可重绘。
private fun findReDrawableCross() {
    // 省掉校验
    // 获取正向管道两头的单元格
    val lbRectF = bottomRectFList.first().rectF
    val rtRectF = topRectFList.last().rectF
    // 判别轨道线的起点与结尾坐标是否都在管道两头的单元格区域内
    if (((lbRectF.contains(startX, startY) && rtRectF.contains(endX, endY)) ||
         (lbRectF.contains(endX, endY) && rtRectF.contains(startX, startY)))
       ) {
        // 界说 mark 变量为 true
        var mark = true
        // 遍历轨道线
        for (i in 1 until linePathLength.toInt()) {
            // 获取轨道线上点的坐标
            val point = FloatArray(2)
            val posTan = linePathMeasure.getPosTan(i.toFloat(), point, null)
            if (!posTan) {
                mark = false
                break
            }
            // 坐标校验
            val x = point[0]
            val y = point[1]
            if (x == 0F || y == 0F) {
                mark = false
                break
            }
            // 判别轨道线上点的坐标是否在管道区域内
            if (!positiveCrossRegion.contains(x.toInt(), y.toInt())) {
                mark = false
                break
            }
        }
        if (mark) {
            // 符号正向管道可重绘
            markPositiveCrossReDrawable()
        }
    }
}
// 符号正向管道可重绘
private fun markPositiveCrossReDrawable() {
    if (!positiveCrossPath.isReDrawable) {
        positiveCrossPath.isReDrawable = true
    }
}

重绘反向管道

反向管道的重绘逻辑与正向管道相同:

  1. 获取反向管道两头的单元格。
  2. 判别轨道线的起点与结尾坐标是否都在管道两头的单元格区域内。
  3. 遍历轨道线,判别轨道线上点的坐标是否在管道区域内。
  4. 符号反向管道可重绘。
private fun findReDrawableCross() {
    // 省掉校验
    // 获取反向管道两头的单元格
    val ltRectF = topRectFList.first().rectF
    val rbRectF = bottomRectFList.last().rectF
    // 判别轨道线的起点与结尾坐标是否都在管道两头的单元格区域内
    if (((ltRectF.contains(startX, startY) && rbRectF.contains(endX, endY)) ||
         (ltRectF.contains(endX, endY) && rbRectF.contains(startX, startY)))
       ) {
        // 界说 mark 变量为 true
        var mark = true
        // 遍历轨道线
        for (i in 1 until linePathLength.toInt()) {
            // 获取轨道线上点的坐标
            val point = FloatArray(2)
            val posTan = linePathMeasure.getPosTan(i.toFloat(), point, null)
            if (!posTan) {
                mark = false
                break
            }
            // 坐标校验
            val x = point[0]
            val y = point[1]
            if (x == 0F || y == 0F) {
                mark = false
                break
            }
            // 判别轨道线上点的坐标是否在管道区域内
            if (!reverseCrossRegion.contains(x.toInt(), y.toInt())) {
                mark = false
                break
            }
        }
        if (mark) {
            // 符号反向管道可重绘
            markReverseCrossReDrawable()
        }
    }
}
// 符号反向管道可重绘
private fun markReverseCrossReDrawable() {
    if (!reverseCrossPath.isReDrawable) {
        reverseCrossPath.isReDrawable = true
    }
}

重绘穿插管道的作用如下:

Android-自定义View-仿某米的触摸屏测试

测验完结

终究咱们还剩余接触屏测验完结的判别以及对外供给测验完结的回调,而且测验完结后不再制作轨道线。

界说测验完结的回调:

interface TouchPassListener {
    fun onTouchPass()
}

界说是否测验完结变量与测验完结回调变量:

private var isPassed = false
private var mTouchPassListener: TouchPassListener? = null

新增 isTouchPass 办法,在此办法中判别一切单元格和管道是否都被符号为可重绘的:

private fun isTouchPass(): Boolean {
    return leftRectFList.all { it.isReDrawable } &&
    		topRectFList.all { it.isReDrawable } &&
    		rightRectFList.all { it.isReDrawable } &&
    		bottomRectFList.all { it.isReDrawable } &&
    		centerHorizontalRectFList.all { it.isReDrawable } &&
    		centerVerticalRectFList.all { it.isReDrawable } &&
    		positiveCrossPath.isReDrawable &&
    		reverseCrossPath.isReDrawable
}

然后在符号单元格和管道为可重绘的办法中调用 isTouchPass 办法即可:

private fun markBoxReDrawable(rectF: TouchRectF) {
    if (!rectF.isReDrawable) {
        rectF.isReDrawable = true
        if (isTouchPass()) {
            touchPass()
        }
    }
}
private fun markPositiveCrossReDrawable() {
    if (!positiveCrossPath.isReDrawable) {
        positiveCrossPath.isReDrawable = true
        if (isTouchPass()) {
            touchPass()
        }
    }
}
private fun markReverseCrossReDrawable() {
    if (!reverseCrossPath.isReDrawable) {
        reverseCrossPath.isReDrawable = true
        if (isTouchPass()) {
            touchPass()
        }
    }
}
private fun touchPass() {
    isPassed = true
    mTouchPassListener?.onTouchPass()
}

终究作用如下:

Android-自定义View-仿某米的触摸屏测试