Tusky-App-Android/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt

141 lines
4.9 KiB
Kotlin

package com.keylesspalace.tusky.components.compose.view
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.graphics.Point
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import com.keylesspalace.tusky.entity.Attachment
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.min
class FocusIndicatorView
@JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var focus: Attachment.Focus? = null
private var imageSize: Point? = null
private var circleRadius: Float? = null
fun setImageSize(width: Int, height: Int) {
this.imageSize = Point(width, height)
if (focus != null) {
invalidate()
}
}
fun setFocus(focus: Attachment.Focus) {
this.focus = focus
if (imageSize != null) {
invalidate()
}
}
// Assumes setFocus called first
fun getFocus(): Attachment.Focus {
return focus!!
}
// This needs to be consistent every time it is consulted over the lifetime of the object,
// so base it on the view width/height whenever the first access occurs.
private fun getCircleRadius(): Float {
val circleRadius = this.circleRadius
if (circleRadius != null) {
return circleRadius
}
val newCircleRadius = min(this.width, this.height).toFloat() / 4.0f
this.circleRadius = newCircleRadius
return newCircleRadius
}
// Remember focus uses -1..1 y-down coordinates (so focus value should be negated for y)
private fun axisToFocus(value: Float, innerLimit: Int, outerLimit: Int): Float {
val offset = (outerLimit - innerLimit) / 2 // Assume image is centered in widget frame
val result = (value - offset) / innerLimit.toFloat() * 2.0f - 1.0f // To range -1..1
return min(1.0f, max(-1.0f, result)) // Clamp
}
private fun axisFromFocus(value: Float, innerLimit: Int, outerLimit: Int): Float {
val offset = (outerLimit - innerLimit) / 2
return offset.toFloat() + ((value + 1.0f) / 2.0f) * innerLimit.toFloat() // From range -1..1
}
@SuppressLint(
"ClickableViewAccessibility"
) // Android Studio wants us to implement PerformClick for accessibility, but that unfortunately cannot be made meaningful for this widget.
override fun onTouchEvent(event: MotionEvent): Boolean {
if (event.actionMasked == MotionEvent.ACTION_CANCEL) {
return false
}
val imageSize = this.imageSize ?: return false
// Convert touch xy to point inside image
focus = Attachment.Focus(axisToFocus(event.x, imageSize.x, this.width), -axisToFocus(event.y, imageSize.y, this.height))
invalidate()
return true
}
private val transparentDarkGray = 0x40000000
private val strokeWidth = 4.0f * this.resources.displayMetrics.density
private val curtainPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val curtainPath = Path()
init {
curtainPaint.color = transparentDarkGray
curtainPaint.style = Paint.Style.FILL
strokePaint.style = Paint.Style.STROKE
strokePaint.strokeWidth = strokeWidth
strokePaint.color = Color.WHITE
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val imageSize = this.imageSize
val focus = this.focus
if (imageSize != null && focus != null) {
val x = axisFromFocus(focus.x, imageSize.x, this.width)
val y = axisFromFocus(-focus.y, imageSize.y, this.height)
val circleRadius = getCircleRadius()
curtainPath.reset() // Draw a flood fill with a hole cut out of it
curtainPath.fillType = Path.FillType.WINDING
curtainPath.addRect(
0.0f,
0.0f,
this.width.toFloat(),
this.height.toFloat(),
Path.Direction.CW
)
curtainPath.addCircle(x, y, circleRadius, Path.Direction.CCW)
canvas.drawPath(curtainPath, curtainPaint)
canvas.drawCircle(x, y, circleRadius, strokePaint) // Draw white circle
canvas.drawCircle(x, y, strokeWidth / 2.0f, strokePaint) // Draw white dot
}
}
// Give a "safe" height based on currently set image size. Assume imageSize is set and height>width already checked
fun maxAttractiveHeight(): Int {
val height = this.imageSize!!.y
val circleRadius = getCircleRadius()
// Give us enough space for the image, plus on each side half a focus indicator circle, plus a strokeWidth
return ceil(height.toFloat() + circleRadius * 2.0f + strokeWidth).toInt()
}
}