SmashKs/OneShoot

View on GitHub
presentation/src/main/java/smash/ks/com/oneshoot/widgets/customize/selectable/SelectableAreaView.kt

Summary

Maintainability
F
3 days
Test Coverage
/*
 * Copyright (C) 2018 The Smash Ks Open Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package smash.ks.com.oneshoot.widgets.customize.selectable

import android.content.Context
import android.graphics.Canvas
import android.graphics.Color.WHITE
import android.graphics.Paint
import android.graphics.Paint.Style.FILL
import android.graphics.Paint.Style.STROKE
import android.graphics.PointF
import android.graphics.PorterDuff.Mode.DST_OUT
import android.graphics.PorterDuffXfermode
import android.graphics.RectF
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.MotionEvent.ACTION_DOWN
import android.view.MotionEvent.ACTION_MOVE
import android.view.MotionEvent.ACTION_UP
import android.widget.FrameLayout
import smash.ks.com.oneshoot.widgets.customize.selectable.SelectableAreaView.AnglePoint.LB
import smash.ks.com.oneshoot.widgets.customize.selectable.SelectableAreaView.AnglePoint.LT
import smash.ks.com.oneshoot.widgets.customize.selectable.SelectableAreaView.AnglePoint.RB
import smash.ks.com.oneshoot.widgets.customize.selectable.SelectableAreaView.AnglePoint.RT

/**
 * A customize view for selecting a rectangle area. It has four angles which be able to
 * let us control and adjust the size and the position of the rectangle.
 */
class SelectableAreaView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
    companion object {
        const val DEFAULT_GAP = 300f
        const val DEFAULT_STROKE_WIDTH = 2f
        const val DEFAULT_STROKE_HALF_WIDTH = 1f
        const val DEFAULT_ANGLE_WIDTH = 15f
        const val DEFAULT_TOUCH_RANGE = DEFAULT_ANGLE_WIDTH * 3
        const val DEFAULT_POSITION = 50f

        private const val DIRECT_LEFT = 0b0001
        private const val DIRECT_RIGHT = 0b0010
        private const val DIRECT_TOP = 0b0100
        private const val DIRECT_BOTTOM = 0b1000

        private const val FLOAT_ZERO = 0f

        private const val OUTER_ARGB_ALPHA = 150
        private const val TOUCHABLE_RANGE_TIMES = 4

        private const val SHIFT = 8

        private const val BLACK = 0xff
    }

    var selectedAreaCallback: ((x: Int, y: Int, width: Int, height: Int) -> Unit)? = null
        set(value) {
            val x = fourAngles[0].coordination.x.toInt()
            val y = fourAngles[0].coordination.y.toInt()
            val width = fourAngles[1].coordination.x.toInt() - x
            val height = fourAngles[1].coordination.y.toInt() - y

            value?.invoke(x, y, width, height)
            field = value
        }
    private var isTouchAngle = false
    private var isTouchInside = false
    private val paintRect by lazy {
        Paint().apply {
            setARGB(OUTER_ARGB_ALPHA, BLACK, BLACK, BLACK)
            isAntiAlias = true
            style = STROKE
            strokeWidth = DEFAULT_STROKE_WIDTH
        }
    }
    private val paintOuterRect by lazy {
        Paint().apply {
            setARGB(OUTER_ARGB_ALPHA, 0, 0, 0)
            isAntiAlias = true
            style = FILL
        }
    }
    private val paintInnerRect by lazy {
        Paint().apply {
            color = WHITE  // This is interacted area so any color is fine.
            isAntiAlias = true
            style = FILL
            xfermode = PorterDuffXfermode(DST_OUT)
        }
    }
    private val paintAngles by lazy {
        Paint().apply {
            setARGB(OUTER_ARGB_ALPHA, BLACK, BLACK, BLACK)
            isAntiAlias = true
        }
    }
    private val leftTopPoint by lazy {
        LT(false,
           PointF(DEFAULT_POSITION, DEFAULT_POSITION),
           DIRECT_LEFT or DIRECT_TOP)
    }
    private val rightBottomPoint by lazy {
        RB(false,
           PointF(DEFAULT_GAP + DEFAULT_POSITION, DEFAULT_GAP + DEFAULT_POSITION),
           DIRECT_RIGHT or DIRECT_BOTTOM)
    }
    private val leftBottomPoint by lazy {
        LB(false,
           PointF(DEFAULT_POSITION, DEFAULT_GAP + DEFAULT_POSITION),
           DIRECT_LEFT or DIRECT_BOTTOM)
    }
    private val rightTopPoint by lazy {
        RT(false,
           PointF(DEFAULT_GAP + DEFAULT_POSITION, DEFAULT_POSITION),
           DIRECT_RIGHT or DIRECT_TOP)
    }
    private val fourAngles by lazy { listOf(leftTopPoint, rightBottomPoint, leftBottomPoint, rightTopPoint) }
    /** This canvas rect object. */
    private val wholeRectangle by lazy { RectF(FLOAT_ZERO, FLOAT_ZERO, width.toFloat(), height.toFloat()) }
    /** Each angles' offset when touching the screen inside the rectangle. */
    private val listOffset by lazy { listOf(PointF(), PointF(), PointF(), PointF()) }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            ACTION_DOWN -> {
                // Four angles' area.
                fourAngles.forEach searching@{
                    val rangeLeft = it.coordination.x - DEFAULT_TOUCH_RANGE * TOUCHABLE_RANGE_TIMES
                    val rangeRight = it.coordination.x + DEFAULT_TOUCH_RANGE
                    val rangeTop = it.coordination.y - DEFAULT_TOUCH_RANGE
                    val rangeBottom = it.coordination.y + DEFAULT_TOUCH_RANGE

                    if (event.x in rangeLeft..rangeRight && event.y in rangeTop..rangeBottom) {
                        it.isSelected = true
                        isTouchAngle = true
                        return@searching
                    }
                }
                // Inside the rectangle excepting the overlapping of the four angles' area.
                if (!isTouchAngle &&
                    event.x in leftTopPoint.coordination.x..rightBottomPoint.coordination.x &&
                    event.y in leftTopPoint.coordination.y..rightBottomPoint.coordination.y) {
                    // Keeping the each offsets for each angles.
                    listOffset.zip(fourAngles).forEach { (offset, angles) ->
                        offset.x = event.x - angles.coordination.x
                        offset.y = event.y - angles.coordination.y
                    }

                    isTouchInside = true
                }
            }
            ACTION_MOVE -> {
                when {
                    isTouchAngle -> {
                        fourAngles.find(AnglePoint::isSelected)?.apply {
                            // Change the touching rectangle angle.
                            coordination.x = event.x
                            coordination.y = event.y
                            // Change others related rectangle angle.
                            fourAngles.forEach {
                                when (direction and it.direction) {
                                    DIRECT_LEFT, DIRECT_RIGHT -> it.coordination.x = event.x
                                    DIRECT_TOP, DIRECT_BOTTOM -> it.coordination.y = event.y
                                }
                            }
                        }
                    }
                    isTouchInside -> {
                        // Recalculate for the each angles position.
                        fourAngles.zip(listOffset).forEach { (angle, offset) ->
                            angle.coordination.x = event.x - offset.x
                            angle.coordination.y = event.y - offset.y
                        }
                    }
                }
                invalidate()
            }
            ACTION_UP -> {
                if (isTouchAngle || isTouchInside) {
                    val x = fourAngles[0].coordination.x.toInt()
                    val y = fourAngles[0].coordination.y.toInt()
                    val width = fourAngles[1].coordination.x.toInt() - x
                    val height = fourAngles[1].coordination.y.toInt() - y

                    selectedAreaCallback?.invoke(x, y, width, height)
                }
                fourAngles.find(AnglePoint::isSelected)?.isSelected = false
                isTouchAngle = false
                isTouchInside = false
            }
        }

        return true
    }

    override fun onDraw(canvas: Canvas) {
        canvas.apply {
            // Create a new canvas for doing interaction layer.
            saveLayer(wholeRectangle, null).apply {
                // All background rectangle (DST layer).
                drawRect(wholeRectangle, paintOuterRect)
                // Inner transport of selection area rectangle (SRC layer).
                drawRect(leftTopPoint.coordination.x,
                         leftTopPoint.coordination.y,
                         rightBottomPoint.coordination.x,
                         rightBottomPoint.coordination.y,
                         paintInnerRect)
            }.apply(::restoreToCount)
            // Selection Area Rectangle.
            drawRect(leftTopPoint.coordination.x,
                     leftTopPoint.coordination.y,
                     rightBottomPoint.coordination.x,
                     rightBottomPoint.coordination.y,
                     paintRect)
            // Four Angles
            fourAngles.forEach {
                // Create a new canvas for doing interaction layer.
                saveLayer(wholeRectangle, null).apply {
                    when (it) {
                        is LT -> {
                            // All background rectangle (DST layer).
                            drawRect(it.coordination.x + DEFAULT_STROKE_HALF_WIDTH,
                                     it.coordination.y + DEFAULT_STROKE_HALF_WIDTH,
                                     it.coordination.x + DEFAULT_STROKE_HALF_WIDTH + DEFAULT_TOUCH_RANGE,
                                     it.coordination.y + DEFAULT_STROKE_HALF_WIDTH + DEFAULT_TOUCH_RANGE,
                                     paintAngles)
                            // Inner transport of selection area rectangle (SRC layer).
                            drawRect(it.coordination.x + SHIFT,
                                     it.coordination.y + SHIFT,
                                     it.coordination.x + DEFAULT_TOUCH_RANGE + SHIFT,
                                     it.coordination.y + DEFAULT_TOUCH_RANGE + SHIFT,
                                     paintInnerRect)
                        }
                        is RB -> {
                            // All background rectangle (DST layer).
                            drawRect(it.coordination.x - DEFAULT_STROKE_HALF_WIDTH - DEFAULT_TOUCH_RANGE,
                                     it.coordination.y - DEFAULT_STROKE_HALF_WIDTH - DEFAULT_TOUCH_RANGE,
                                     it.coordination.x - DEFAULT_STROKE_HALF_WIDTH,
                                     it.coordination.y - DEFAULT_STROKE_HALF_WIDTH,
                                     paintAngles)
                            // Inner transport of selection area rectangle (SRC layer).
                            drawRect(it.coordination.x - DEFAULT_TOUCH_RANGE - SHIFT,
                                     it.coordination.y - DEFAULT_TOUCH_RANGE - SHIFT,
                                     it.coordination.x - SHIFT,
                                     it.coordination.y - SHIFT,
                                     paintInnerRect)
                        }
                        is LB -> {
                            // All background rectangle (DST layer).
                            drawRect(it.coordination.x + DEFAULT_STROKE_HALF_WIDTH,
                                     it.coordination.y - DEFAULT_STROKE_HALF_WIDTH - DEFAULT_TOUCH_RANGE,
                                     it.coordination.x + DEFAULT_STROKE_HALF_WIDTH + DEFAULT_TOUCH_RANGE,
                                     it.coordination.y - DEFAULT_STROKE_HALF_WIDTH,
                                     paintAngles)
                            // Inner transport of selection area rectangle (SRC layer).
                            drawRect(it.coordination.x + SHIFT,
                                     it.coordination.y - DEFAULT_TOUCH_RANGE - SHIFT,
                                     it.coordination.x + DEFAULT_TOUCH_RANGE + SHIFT,
                                     it.coordination.y - SHIFT,
                                     paintInnerRect)
                        }
                        is RT -> {
                            // All background rectangle (DST layer).
                            drawRect(it.coordination.x - DEFAULT_STROKE_HALF_WIDTH - DEFAULT_TOUCH_RANGE,
                                     it.coordination.y + DEFAULT_STROKE_HALF_WIDTH,
                                     it.coordination.x - DEFAULT_STROKE_HALF_WIDTH,
                                     it.coordination.y + DEFAULT_STROKE_HALF_WIDTH + DEFAULT_TOUCH_RANGE,
                                     paintAngles)
                            // Inner transport of selection area rectangle (SRC layer).
                            drawRect(it.coordination.x - DEFAULT_STROKE_HALF_WIDTH - DEFAULT_TOUCH_RANGE - SHIFT,
                                     it.coordination.y + DEFAULT_STROKE_HALF_WIDTH + SHIFT,
                                     it.coordination.x - DEFAULT_STROKE_HALF_WIDTH - SHIFT,
                                     it.coordination.y + DEFAULT_STROKE_HALF_WIDTH + DEFAULT_TOUCH_RANGE + SHIFT,
                                     paintInnerRect)
                        }
                    }
                }.apply(::restoreToCount)
            }
        }
    }

    /**
     * The angle point's properties of a rectangle.
     *
     * @property isSelected check myself is clicked or selected.
     * @property coordination the coordination.
     * @property direction the point's position. Ref: [DIRECT_LEFT], [DIRECT_RIGHT], [DIRECT_TOP], [DIRECT_BOTTOM].
     *                     Using bit to keep the position is easy to check by code.
     */
    sealed class AnglePoint(
        var isSelected: Boolean,
        var coordination: PointF,
        var direction: Int
    ) {
        /**
         * The left top point of a rectangle.
         */
        class LT(
            isSelected: Boolean,
            coordination: PointF,
            direction: Int
        ) : AnglePoint(isSelected, coordination, direction)

        /**
         * The left bottom point of a rectangle.
         */
        class LB(
            isSelected: Boolean,
            coordination: PointF, direction: Int
        ) : AnglePoint(isSelected, coordination, direction)

        /**
         * The right top point of a rectangle.
         */
        class RT(
            isSelected: Boolean,
            coordination: PointF, direction: Int
        ) : AnglePoint(isSelected, coordination, direction)

        /**
         * The right bottom point of a rectangle.
         */
        class RB(
            isSelected: Boolean,
            coordination: PointF, direction: Int
        ) : AnglePoint(isSelected, coordination, direction)
    }
}