Как сделать спойлер с помощью View как в Telegram
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Реализация спойлера на Android с использованием View
Создание элемента интерфейса типа "спойлер", который скрывает текст до нажатия (как в Telegram), можно реализовать несколькими способами. Я рассмотрю наиболее практичный и гибкий подход с использованием custom TextView и маскирования текста.
Основная концепция
Спойлер в Telegram работает по принципу:
- Текст отображается "замаскированным" (обычно черными символами на черном фоне, или с применением специального эффекта).
- При клике (или долгом нажатии) маска удаляется, revealing исходный текст.
- Состояние (скрыт/открыт) должно сохраняться для данного сообщения.
Реализация через Custom View (SpollerTextView)
Лучше создать собственный SpollerTextView, наследующий от AppCompatTextView. Это даст полный контроль над отрисовкой и поведением.
1. Класс SpollerTextView
class SpoilerTextView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatTextView(context, attrs, defStyleAttr) {
// Флаг, указывающий, раскрыт ли спойлер
private var isRevealed = false
// Paint для рисования маски (черных прямоугольников)
private val maskPaint = Paint()
// Цвет маски (обычно совпадает с фоном)
private var maskColor = Color.BLACK
// Толщина маски (можно регулировать)
private var maskHeight = 0f
init {
setupAttributes(attrs)
setupPaint()
setupClickListener()
}
private fun setupAttributes(attrs: AttributeSet?) {
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.SpoilerTextView)
maskColor = typedArray.getColor(R.styleable.SpoilerTextView_maskColor, Color.BLACK)
maskHeight = typedArray.getDimension(R.styleable.SpoilerTextView_maskHeight, textSize)
typedArray.recycle()
}
private fun setupPaint() {
maskPaint.color = maskColor
maskPaint.style = Paint.Style.FILL
}
private fun setupClickListener() {
setOnClickListener {
revealSpoiler()
}
}
fun revealSpoiler() {
isRevealed = true
invalidate() // Перерисовать view
}
override fun onDraw(canvas: Canvas) {
// Сначала рисуем текст (базовый метод)
super.onDraw(canvas)
// Если спойлер не раскрыт, рисуем маску поверх текста
if (!isRevealed) {
drawMask(canvas)
}
}
private fun drawMask(canvas: Canvas) {
val lineCount = layout.lineCount
for (i in 0 until lineCount) {
val lineStart = layout.getLineStart(i)
val lineEnd = layout.getLineEnd(i)
// Получаем текст для данной строки
val lineText = text.substring(lineStart, lineEnd)
// Измеряем ширину текста в строке
val lineWidth = layout.getLineWidth(i)
// Получаем координаты линии
val lineTop = layout.getLineTop(i)
val lineBottom = layout.getLineBottom(i)
// Рисуем прямоугольник маски для каждой строки
canvas.drawRect(
0f,
lineTop.toFloat(),
lineWidth,
lineBottom.toFloat(),
maskPaint
)
}
}
}
2. Атрибуты для XML (attrs.xml)
<resources>
<declare-styleable name="SpoilerTextView">
<attr name="maskColor" format="color" />
<attr name="maskHeight" format="dimension" />
</declare-styleable>
</resources>
3. Использование в XML layout
<com.example.app.SpoilerTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Это секретный текст, который скрыт спойлером!"
android:textColor="@color/white"
app:maskColor="@color/black"
app:maskHeight="16dp" />
Альтернативный подход: использование Span для текста
Для более глубокой интеграции с TextView и возможности смешивать обычный текст и спойлеры в одной строке, можно использовать custom Span.
SpoilerSpan класс
class SpoilerSpan : ReplacementSpan() {
private var isRevealed = false
private val maskPaint = Paint()
override fun getSize(
paint: Paint,
text: CharSequence?,
start: Int,
end: Int,
fm: Paint.FontMetricsInt?
): Int {
return paint.measureText(text, start, end).toInt()
}
override fun draw(
canvas: Canvas,
text: CharSequence?,
start: Int,
end: Int,
x: Float,
top: Int,
baseline: Int,
bottom: Int,
paint: Paint
) {
if (!isRevealed) {
// Рисуем маску
maskPaint.color = Color.BLACK
canvas.drawRect(
x,
top.toFloat(),
x + paint.measureText(text, start, end),
bottom.toFloat(),
maskPaint
)
} else {
// Рисуем текст
canvas.drawText(text, start, end, x, baseline.toFloat(), paint)
}
}
fun reveal() {
isRevealed = true
}
}
Применение SpoilerSpan в TextView
val textView = findViewById<TextView>(R.id.text_view)
val spannableString = SpannableString("Это обычный текст и [спойлер].")
val spoilerSpan = SpoilerSpan()
spannableString.setSpan(
spoilerSpan,
20, 28, // Индексы начала и конца спойлера в строке
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
textView.text = spannableString
// Для раскрытия нужно найти span и вызвать reveal()
textView.setOnClickListener {
val spannable = textView.text as Spannable
val spans = spannable.getSpans(0, spannable.length, SpoilerSpan::class.java)
spans.forEach { it.reveal() }
textView.invalidate()
}
Ключевые улучшения и рекомендации
- Анимация раскрытия: Добавить плавное исчезание маски через
AlphaAnimationилиValueAnimator. - Состояние: Сохранять
isRevealedвonSaveInstanceState()и восстанавливать вonRestoreInstanceState(). - Долгое нажатие: Использовать
OnLongClickListenerдля альтернативного взаимодействия. - Кастомный фон: Вместо черных прямоугольников можно использовать градиент или частичную маску.
- Библиотечный подход: Если нужно много спойлеров, создать отдельную библиотеку с поддержкой Jetpack Compose (
SpoilerTextcomposable).
Это решение дает полный контроль над визуальным представлением и поведением, соответствует принципам custom view в Android, и может быть легко адаптировано под конкретные требования дизайна, как в Telegram.