前言
最近遇到一个需求,需要实现一个格子填充的效果
分析
格子的拖动效果
整个 View 的边界判断
二维网格边界的判断
拖动后格子填充时的位置判断
网格的绘制
填充后进行复位
实现
@SuppressLint("ClickableViewAccessibility") class DragGridGroupView : FrameLayout { 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 ) { init() } constructor( context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int ) : super(context, attrs, defStyleAttr, defStyleRes) //二维格子 lateinit var array: Array<Array<View>> private val gridRowCount = 15 //行数量 private val gridColumnCount = 10 //列数量 //上一个 iew private var preView: View? = null //初始位置 private val initPoint: Point = Point(dpToPx(30f), dpToPx(50f)) //格子大小 private val sizePoint: Point = Point(dpToPx(30f), dpToPx(30f)) //按下位置 private val downPoint: PointF = PointF(0f, 0f) //View 位置 private val viewPoint: PointF = PointF(0f, 0f) //是否为按下状态 private var isDown = false //默认格子颜色 private val defaultGridColor = Color.parseColor("#D8D5D7") //滑动时选中的颜色 private val scrollSelectColor = Color.parseColor("#A6D8A9") //选中的颜色 private val selectColor = Color.parseColor("#4FD855") private val paint by lazy { Paint().apply { color = Color.GRAY strokeWidth = dpToPx(1f).toFloat() style = Paint.Style.STROKE pathEffect = dashPathEffect } } private val path = Path() private val dashLength = dpToPx(3f).toFloat() private val dashPathEffect by lazy { DashPathEffect( floatArrayOf(dashLength, dashLength), dashLength ) } val view by lazy { View(context).apply { val layoutParams = LayoutParams(sizePoint.x, sizePoint.y) layoutParams.marginStart = initPoint.x layoutParams.topMargin = initPoint.y this.layoutParams = layoutParams setBackgroundColor(selectColor) } } private val gridLayout by lazy { NotTouchGridLayout(context).apply { val layoutParams = LayoutParams(sizePoint.x * gridColumnCount, sizePoint.y * gridRowCount) layoutParams.marginStart = initPoint.x layoutParams.topMargin = initPoint.y + (sizePoint.y * 2) this.layoutParams = layoutParams setBackgroundColor(Color.BLUE) orientation = GridLayout.HORIZONTAL columnCount = gridColumnCount rowCount = gridRowCount } } private fun init() { addView(gridLayout) addView(view) initData() initDrag() setWillNotDraw(false) } private fun initData() { array = Array(gridRowCount, init = { Array(gridColumnCount, init = { View(context).apply { val layoutParams = LayoutParams(dpToPx(30f), dpToPx(30f)) this.layoutParams = layoutParams setBackgroundColor(defaultGridColor) } }) }) for (i in array.indices) { for (j in 0 until array[i].size) { val v = array[i][j] gridLayout.addView(v) } } } private fun initDrag() { } @SuppressLint("ClickableViewAccessibility") override fun onTouchEvent(event: MotionEvent): Boolean { when (event.action) { MotionEvent.ACTION_DOWN -> { isDown = true invalidate() downPoint.x = event.x downPoint.y = event.y viewPoint.x = view.x viewPoint.y = view.y } MotionEvent.ACTION_MOVE -> { val x = (event.x - downPoint.x) + viewPoint.x val y = (event.y - downPoint.y) + viewPoint.y move(x, y) if (x > gridLayout.x && y > gridLayout.y && x < (gridLayout.width + gridLayout.x) && y < (gridLayout.height + gridLayout.y)) { fillingGrid(x, y) } } MotionEvent.ACTION_UP -> { isDown = false invalidate() val x = (event.x - downPoint.x) + viewPoint.x val y = (event.y - downPoint.y) + viewPoint.y preView?.let { if (x > gridLayout.x && y > gridLayout.y && x < (gridLayout.width + gridLayout.x) && y < (gridLayout.height + gridLayout.y)) { it.tag = 1 setBgColor(it, scrollSelectColor, selectColor) } else { it.tag = 0 setBgColor(it, scrollSelectColor, defaultGridColor) } } val animate = view.animate() animate.x(initPoint.x.toFloat()) animate.y(initPoint.y.toFloat()) animate.start() preView = null } } return true; } private fun setBgColor(view: View, vararg color: Int) { val colorAnim = ObjectAnimator.ofInt(view, "backgroundColor", *color) colorAnim.duration = 500 colorAnim.setEvaluator(ArgbEvaluator()) colorAnim.start() } private fun move(x: Float, y: Float) { //计算右边和下边边界 val right = (width - sizePoint.x) val bottom = (height - sizePoint.y) //内部直接滑动 if (x >= 0 && y >= 0 && x <= right && y <= bottom) { view.x = x view.y = y return } //-------------- 拖动边界判断 if (x < 0 && y < 0) { //左上角 view.x = 0f view.y = 0f } else if (x < 0 && y > bottom) { //左下角 view.x = 0f view.y = bottom.toFloat() } else if (x > right && y < 0) { //右上角 view.x = right.toFloat() view.y = 0f } else if (x > right && y > bottom) { //右下角 view.x = right.toFloat() view.y = bottom.toFloat() } else if ((x > 0 && x < right) && y < 0) { // y越上界 view.x = x view.y = 0f } else if ((x > 0 && x < right) && y > bottom) { // y越下界 view.x = x view.y = bottom.toFloat() } else if ((y > 0 && y < bottom) && x < 0) { // x越左界 view.x = 0f view.y = y } else if ((y > 0 && y < bottom) && x > right) { // x越右界 view.x = right.toFloat() view.y = y } } private fun fillingGrid(x: Float, y: Float) { //计算框内位置,+ 格子的一半,等于中心点距离边上的位置 val nx = x - gridLayout.x + (sizePoint.x / 2) val ny = y - gridLayout.y + (sizePoint.y / 2) //计算索引位置 val i = (nx / sizePoint.x).toInt() val j = (ny / sizePoint.y).toInt() if (j >= gridRowCount || i >= gridColumnCount) return val v = array[j][i] if (preView != null && preView == v) { return } if (v.tag == 1) { return } preView?.run { tag = 0 setBgColor(this, scrollSelectColor, defaultGridColor) } preView = v v.tag = 1 setBgColor(v, defaultGridColor, scrollSelectColor) } /** 等别的 view 绘制完成后,在进行绘制,否则会被覆盖 */ override fun dispatchDraw(canvas: Canvas?) { super.dispatchDraw(canvas) if (!isDown) return val initX = initPoint.x.toFloat() val initY = initPoint.y + (sizePoint.y * 2).toFloat() val gridWidth = gridLayout.width val gridHeight = gridLayout.height for (i in 0..gridRowCount) { val y = (sizePoint.y * i) + initY path.moveTo(initX, y) path.lineTo(initX + gridWidth, y) canvas?.drawPath(path, paint) path.reset() } for (i in 0..gridColumnCount) { val x = (sizePoint.x * i) + initX path.moveTo(x, initY) path.lineTo(x, initY + gridHeight) canvas?.drawPath(path, paint) path.reset() } } private fun dpToPx(dpValue: Float): Int { val scale: Float = resources.displayMetrics.density return (dpValue * scale + 0.5f).toInt() } } class NotTouchGridLayout : GridLayout { constructor(context: Context?) : super(context) constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super( context, attrs, defStyleAttr ) constructor( context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int ) : super(context, attrs, defStyleAttr, defStyleRes) override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { return true } }