1.0.12: PTT-кнопка в стиле Zello (градиент, тень, глянец, ободок)

This commit is contained in:
ua1zbe
2026-08-21 11:39:46 +03:00
parent 7342dddd0b
commit 587d46578e

View File

@@ -3,15 +3,18 @@ package ru.ua1zbe.svxremote.ui
import android.content.Context import android.content.Context
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.graphics.LinearGradient
import android.graphics.Paint import android.graphics.Paint
import android.graphics.Path import android.graphics.Path
import android.graphics.Shader
import android.util.AttributeSet import android.util.AttributeSet
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import ru.ua1zbe.svxremote.R import ru.ua1zbe.svxremote.R
/** /**
* Большая круглая кнопка PTT. Удерживание — передача. * Большая круглая кнопка PTT в стиле Zello: градиентная заливка с глянцем,
* мягкая тень (плавающая кнопка) и тонкий ободок.
* Состояния: idle (синяя), нажата (красная), отключена (серая). * Состояния: idle (синяя), нажата (красная), отключена (серая).
*/ */
class PttButton @JvmOverloads constructor( class PttButton @JvmOverloads constructor(
@@ -19,8 +22,11 @@ class PttButton @JvmOverloads constructor(
attrs: AttributeSet? = null attrs: AttributeSet? = null
) : View(context, attrs) { ) : View(context, attrs) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG) private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val ringPaint = Paint(Paint.ANTI_ALIAS_FLAG) private val ringPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val glossPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val micPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val clip = Path()
private var radius = 0f private var radius = 0f
private var centerX = 0f private var centerX = 0f
@@ -31,7 +37,6 @@ class PttButton @JvmOverloads constructor(
set(value) { set(value) {
field = value field = value
if (!value) setPressed(false) if (!value) setPressed(false)
refreshColors()
invalidate() invalidate()
} }
@@ -50,22 +55,28 @@ class PttButton @JvmOverloads constructor(
} }
init { init {
fillPaint.style = Paint.Style.FILL
fillPaint.setShadowLayer(dp(8).toFloat(), 0f, dp(4).toFloat(), Color.argb(80, 0, 0, 0))
ringPaint.style = Paint.Style.STROKE ringPaint.style = Paint.Style.STROKE
ringPaint.strokeWidth = dp(3).toFloat() ringPaint.strokeWidth = dp(2).toFloat()
ringPaint.color = Color.parseColor("#3D444D") glossPaint.style = Paint.Style.FILL
paint.style = Paint.Style.FILL micPaint.style = Paint.Style.FILL
refreshColors() micPaint.color = Color.WHITE
isClickable = true isClickable = true
isFocusable = true isFocusable = true
} }
private fun refreshColors() { /** (верхний цвет, нижний цвет) градиента заливки. */
paint.color = when { private fun fillColors(): Pair<Int, Int> = when {
!enabled2 -> Color.parseColor("#484F58") !enabled2 -> 0x565E68 to 0x3C424B
isPressed -> Color.parseColor("#F85149") isPressed -> 0xFF8A80 to 0xDB3A30
else -> Color.parseColor("#1F6FEB") else -> 0x4A9BFF to 0x1561D6
} }
invalidate()
private fun ringColor(): Int = when {
!enabled2 -> 0x6B727B
isPressed -> 0xFFA79F
else -> 0x6FB0FF
} }
private fun dp(v: Int): Int = (v * resources.displayMetrics.density).toInt() private fun dp(v: Int): Int = (v * resources.displayMetrics.density).toInt()
@@ -74,32 +85,51 @@ class PttButton @JvmOverloads constructor(
super.onSizeChanged(w, h, oldw, oldh) super.onSizeChanged(w, h, oldw, oldh)
centerX = w / 2f centerX = w / 2f
centerY = h / 2f centerY = h / 2f
radius = minOf(w, h) / 2f - dp(16) radius = minOf(w, h) / 2f - dp(18)
refreshColors()
} }
override fun onDraw(canvas: Canvas) { override fun onDraw(canvas: Canvas) {
super.onDraw(canvas) super.onDraw(canvas)
val r = radius / (1f + if (isPressed) 0.06f else 0f) val r = radius / (1f + if (isPressed) 0.06f else 0f)
canvas.drawCircle(centerX, centerY, r, paint) val cx = centerX
canvas.drawCircle(centerX, centerY, r, ringPaint) val cy = centerY
val (top, bottom) = fillColors()
// Иконка микрофона (как icons8: капсула + чашка + ножка + основание) // Заливка с вертикальным градиентом (глянец)
val micPaint = Paint(Paint.ANTI_ALIAS_FLAG) fillPaint.shader = LinearGradient(
micPaint.color = Color.WHITE cx, cy - r, cx, cy + r, top, bottom, Shader.TileMode.CLAMP
micPaint.style = Paint.Style.FILL )
canvas.drawCircle(cx, cy, r, fillPaint)
val x = centerX // Тонкий ободок по краю
val y = centerY ringPaint.color = ringColor()
val s = r * 0.5f canvas.drawCircle(cx, cy, r - dp(1).toFloat(), ringPaint)
// Верхний глянец (белое затухание сверху)
glossPaint.shader = LinearGradient(
cx, cy - r, cx, cy + r * 0.18f,
Color.argb(70, 255, 255, 255), Color.TRANSPARENT, Shader.TileMode.CLAMP
)
clip.reset()
clip.addCircle(cx, cy, r, Path.Direction.CW)
canvas.save()
canvas.clipPath(clip)
canvas.drawRect(cx - r, cy - r, cx + r, cy + r, glossPaint)
canvas.restore()
glossPaint.shader = null
drawMic(canvas, cx, cy, r * 0.5f)
}
/** Иконка микрофона (капсула + держатель + ножка + основание). */
private fun drawMic(canvas: Canvas, x: Float, y: Float, s: Float) {
// корпус-капсула // корпус-капсула
val halfW = s * 0.34f val halfW = s * 0.34f
canvas.drawRoundRect( canvas.drawRoundRect(
x - halfW, y - s * 0.85f, x + halfW, y + s * 0.10f, x - halfW, y - s * 0.85f, x + halfW, y + s * 0.10f,
halfW, halfW, micPaint halfW, halfW, micPaint
) )
// чашка-держатель (U-образная, заполненная) // чашка-держатель (U-образная)
val cup = Path() val cup = Path()
cup.fillType = Path.FillType.EVEN_ODD cup.fillType = Path.FillType.EVEN_ODD
cup.moveTo(x - s * 0.52f, y + s * 0.06f) cup.moveTo(x - s * 0.52f, y + s * 0.06f)