kopia lustrzana https://github.com/ryukoposting/Signal-Android
298 wiersze
8.7 KiB
Kotlin
298 wiersze
8.7 KiB
Kotlin
package org.thoughtcrime.securesms.scribbles
|
|
|
|
import android.annotation.SuppressLint
|
|
import android.content.Context
|
|
import android.graphics.Canvas
|
|
import android.graphics.Color
|
|
import android.graphics.Paint
|
|
import android.graphics.Rect
|
|
import android.graphics.RectF
|
|
import android.graphics.Typeface
|
|
import android.util.AttributeSet
|
|
import android.view.GestureDetector
|
|
import android.view.HapticFeedbackConstants
|
|
import android.view.MotionEvent
|
|
import android.view.View
|
|
import androidx.annotation.ColorInt
|
|
import androidx.annotation.Dimension
|
|
import androidx.annotation.Px
|
|
import org.thoughtcrime.securesms.util.ViewUtil
|
|
import kotlin.math.abs
|
|
import kotlin.math.ceil
|
|
import kotlin.math.floor
|
|
import kotlin.math.roundToInt
|
|
|
|
class RotationDialView @JvmOverloads constructor(
|
|
context: Context,
|
|
attrs: AttributeSet? = null,
|
|
defStyleAttr: Int = 0
|
|
) : View(context, attrs, defStyleAttr) {
|
|
|
|
private val canvasBounds = Rect()
|
|
private val centerMostIndicatorRect = RectF()
|
|
private val indicatorRect = RectF()
|
|
private val dimensions = Dimensions()
|
|
|
|
private var snapDegrees: Float = 0f
|
|
private var degrees: Float = 0f
|
|
private var isInGesture: Boolean = false
|
|
|
|
private val gestureDetector: GestureDetector = GestureDetector(context, GestureListener())
|
|
|
|
var listener: Listener? = null
|
|
|
|
private val textPaint = Paint().apply {
|
|
isAntiAlias = true
|
|
textSize = ViewUtil.spToPx(15f).toFloat()
|
|
typeface = Typeface.DEFAULT
|
|
color = Colors.textColor
|
|
style = Paint.Style.FILL
|
|
textAlign = Paint.Align.CENTER
|
|
}
|
|
|
|
private val angleIndicatorPaint = Paint().apply {
|
|
isAntiAlias = true
|
|
color = Color.WHITE
|
|
style = Paint.Style.FILL
|
|
}
|
|
|
|
fun setDegrees(degrees: Float) {
|
|
if (degrees != this.degrees) {
|
|
this.degrees = degrees
|
|
this.snapDegrees = calculateSnapDegrees()
|
|
|
|
if (isInGesture) {
|
|
listener?.onDegreeChanged(snapDegrees)
|
|
}
|
|
|
|
invalidate()
|
|
}
|
|
}
|
|
|
|
@SuppressLint("ClickableViewAccessibility")
|
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
if (event.actionIndex != 0) return false
|
|
|
|
isInGesture = gestureDetector.onTouchEvent(event)
|
|
|
|
when (event.actionMasked) {
|
|
MotionEvent.ACTION_DOWN -> listener?.onGestureStart()
|
|
MotionEvent.ACTION_UP -> listener?.onGestureEnd()
|
|
}
|
|
|
|
return isInGesture
|
|
}
|
|
|
|
override fun onDraw(canvas: Canvas) {
|
|
if (isInEditMode) {
|
|
canvas.drawColor(Color.BLACK)
|
|
}
|
|
|
|
canvas.getClipBounds(canvasBounds)
|
|
|
|
val dialDegrees = getDialDegrees(snapDegrees)
|
|
val bottom = canvasBounds.bottom
|
|
val approximateCenterDegree = dialDegrees.roundToInt()
|
|
var currentDegree = approximateCenterDegree
|
|
val fractionalOffset = dialDegrees - approximateCenterDegree
|
|
val dialOffset = dimensions.spaceBetweenAngleIndicators * fractionalOffset
|
|
|
|
val centerX = width / 2f
|
|
centerMostIndicatorRect.set(
|
|
centerX - dimensions.angleIndicatorWidth / 2f,
|
|
bottom.toFloat() - dimensions.majorAngleIndicatorHeight,
|
|
centerX + dimensions.angleIndicatorWidth / 2f,
|
|
bottom.toFloat()
|
|
)
|
|
centerMostIndicatorRect.offset(-dialOffset, 0f)
|
|
|
|
indicatorRect.set(centerMostIndicatorRect)
|
|
|
|
angleIndicatorPaint.color = Colors.colorForOtherDegree(currentDegree)
|
|
indicatorRect.top = bottom.toFloat() - dimensions.getHeightForDegree(currentDegree)
|
|
canvas.drawRect(indicatorRect, angleIndicatorPaint)
|
|
indicatorRect.offset(dimensions.spaceBetweenAngleIndicators.toFloat(), 0f)
|
|
currentDegree += 1
|
|
|
|
while (indicatorRect.left < width && currentDegree <= ceil(MAX_DEGREES)) {
|
|
angleIndicatorPaint.color = Colors.colorForOtherDegree(currentDegree)
|
|
indicatorRect.top = bottom.toFloat() - dimensions.getHeightForDegree(currentDegree)
|
|
canvas.drawRect(indicatorRect, angleIndicatorPaint)
|
|
indicatorRect.offset(dimensions.spaceBetweenAngleIndicators.toFloat(), 0f)
|
|
currentDegree += 1
|
|
}
|
|
|
|
currentDegree = approximateCenterDegree
|
|
indicatorRect.set(centerMostIndicatorRect)
|
|
indicatorRect.offset(-dimensions.spaceBetweenAngleIndicators.toFloat(), 0f)
|
|
currentDegree -= 1
|
|
|
|
while (indicatorRect.left >= 0 && currentDegree >= floor(MIN_DEGRESS)) {
|
|
angleIndicatorPaint.color = Colors.colorForOtherDegree(currentDegree)
|
|
indicatorRect.top = bottom.toFloat() - dimensions.getHeightForDegree(currentDegree)
|
|
canvas.drawRect(indicatorRect, angleIndicatorPaint)
|
|
indicatorRect.offset(-dimensions.spaceBetweenAngleIndicators.toFloat(), 0f)
|
|
currentDegree -= 1
|
|
}
|
|
|
|
centerMostIndicatorRect.offset(dialOffset, 0f)
|
|
angleIndicatorPaint.color = Colors.colorForCenterDegree(approximateCenterDegree)
|
|
canvas.drawRect(centerMostIndicatorRect, angleIndicatorPaint)
|
|
|
|
drawText(canvas)
|
|
}
|
|
|
|
private fun drawText(canvas: Canvas) {
|
|
val approximateDegrees = getDialDegrees(snapDegrees).roundToInt()
|
|
canvas.drawText(
|
|
"$approximateDegrees",
|
|
width / 2f,
|
|
canvasBounds.bottom - textPaint.descent() - dimensions.majorAngleIndicatorHeight - dimensions.textPaddingBottom,
|
|
textPaint
|
|
)
|
|
}
|
|
|
|
private fun getDialDegrees(degrees: Float): Float {
|
|
val alpha: Float = degrees % 360f
|
|
|
|
if (alpha % 90 == 0f) {
|
|
return 0f
|
|
}
|
|
|
|
val beta: Float = floor(alpha / 90f)
|
|
val offset: Float = alpha - beta * 90f
|
|
|
|
return if (offset > 45f) {
|
|
offset - 90f
|
|
} else {
|
|
offset
|
|
}
|
|
}
|
|
|
|
private fun calculateSnapDegrees(): Float {
|
|
return if (isInGesture) {
|
|
val dialDegrees = getDialDegrees(degrees)
|
|
if (dialDegrees.roundToInt() == 0) {
|
|
degrees - dialDegrees
|
|
} else {
|
|
degrees
|
|
}
|
|
} else {
|
|
degrees
|
|
}
|
|
}
|
|
|
|
private inner class GestureListener : GestureDetector.SimpleOnGestureListener() {
|
|
override fun onDown(e: MotionEvent): Boolean {
|
|
return true
|
|
}
|
|
|
|
override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
|
|
val degreeIncrement: Float = distanceX / dimensions.spaceBetweenAngleIndicators
|
|
val prevDialDegrees = getDialDegrees(degrees)
|
|
val newDialDegrees = getDialDegrees(degrees + degreeIncrement)
|
|
|
|
val offEndOfMax = prevDialDegrees >= MAX_DEGREES / 2f && newDialDegrees <= MIN_DEGRESS / 2f
|
|
val offEndOfMin = newDialDegrees >= MAX_DEGREES / 2f && prevDialDegrees <= MIN_DEGRESS / 2f
|
|
|
|
if (prevDialDegrees.roundToInt() != newDialDegrees.roundToInt() && isHapticFeedbackEnabled) {
|
|
performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
|
}
|
|
|
|
when {
|
|
offEndOfMax -> {
|
|
val newIncrement = MAX_DEGREES - prevDialDegrees
|
|
setDegrees(degrees + newIncrement)
|
|
}
|
|
offEndOfMin -> {
|
|
val newIncrement = MAX_DEGREES - abs(prevDialDegrees)
|
|
setDegrees(degrees - newIncrement)
|
|
}
|
|
else -> {
|
|
setDegrees(degrees + degreeIncrement)
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
}
|
|
|
|
private class Dimensions {
|
|
|
|
@Px
|
|
val spaceBetweenAngleIndicators: Int = ViewUtil.dpToPx(Dimensions.spaceBetweenAngleIndicators)
|
|
|
|
@Px
|
|
val angleIndicatorWidth: Int = ViewUtil.dpToPx(Dimensions.angleIndicatorWidth)
|
|
|
|
@Px
|
|
val minorAngleIndicatorHeight: Int = ViewUtil.dpToPx(Dimensions.minorAngleIndicatorHeight)
|
|
|
|
@Px
|
|
val majorAngleIndicatorHeight: Int = ViewUtil.dpToPx(Dimensions.majorAngleIndicatorHeight)
|
|
|
|
@Px
|
|
val textPaddingBottom: Int = ViewUtil.dpToPx(Dimensions.textPaddingBottom)
|
|
|
|
fun getHeightForDegree(degree: Int): Int {
|
|
return if (degree == 0) {
|
|
majorAngleIndicatorHeight
|
|
} else {
|
|
minorAngleIndicatorHeight
|
|
}
|
|
}
|
|
|
|
companion object {
|
|
|
|
@Dimension(unit = Dimension.DP)
|
|
private val spaceBetweenAngleIndicators: Int = 12
|
|
|
|
@Dimension(unit = Dimension.DP)
|
|
private val angleIndicatorWidth: Int = 1
|
|
|
|
@Dimension(unit = Dimension.DP)
|
|
private val minorAngleIndicatorHeight: Int = 12
|
|
|
|
@Dimension(unit = Dimension.DP)
|
|
private val majorAngleIndicatorHeight: Int = 32
|
|
|
|
@Dimension(unit = Dimension.DP)
|
|
private val textPaddingBottom: Int = 8
|
|
}
|
|
}
|
|
|
|
private object Colors {
|
|
@ColorInt
|
|
val textColor: Int = Color.WHITE
|
|
|
|
@ColorInt
|
|
val majorAngleIndicatorColor: Int = 0xFF62E87A.toInt()
|
|
|
|
@ColorInt
|
|
val modFiveIndicatorColor: Int = Color.WHITE
|
|
|
|
@ColorInt
|
|
val minorAngleIndicatorColor: Int = 0x80FFFFFF.toInt()
|
|
|
|
fun colorForCenterDegree(degree: Int) = if (degree == 0) modFiveIndicatorColor else majorAngleIndicatorColor
|
|
|
|
fun colorForOtherDegree(degree: Int): Int {
|
|
return when {
|
|
degree % 5 == 0 -> modFiveIndicatorColor
|
|
else -> minorAngleIndicatorColor
|
|
}
|
|
}
|
|
}
|
|
|
|
companion object {
|
|
private const val MAX_DEGREES: Float = 44.99999f
|
|
private const val MIN_DEGRESS: Float = -44.99999f
|
|
}
|
|
|
|
interface Listener {
|
|
fun onDegreeChanged(degrees: Float)
|
|
fun onGestureStart()
|
|
fun onGestureEnd()
|
|
}
|
|
}
|