← Назад к вопросам

Как сделать спойлер с помощью View как в Telegram

2.0 Middle🔥 101 комментариев
#Android компоненты#UI и вёрстка

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Реализация спойлера на Android с использованием View

Создание элемента интерфейса типа "спойлер", который скрывает текст до нажатия (как в Telegram), можно реализовать несколькими способами. Я рассмотрю наиболее практичный и гибкий подход с использованием custom TextView и маскирования текста.

Основная концепция

Спойлер в Telegram работает по принципу:

  1. Текст отображается "замаскированным" (обычно черными символами на черном фоне, или с применением специального эффекта).
  2. При клике (или долгом нажатии) маска удаляется, revealing исходный текст.
  3. Состояние (скрыт/открыт) должно сохраняться для данного сообщения.

Реализация через 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()
}

Ключевые улучшения и рекомендации

  1. Анимация раскрытия: Добавить плавное исчезание маски через AlphaAnimation или ValueAnimator.
  2. Состояние: Сохранять isRevealed в onSaveInstanceState() и восстанавливать в onRestoreInstanceState().
  3. Долгое нажатие: Использовать OnLongClickListener для альтернативного взаимодействия.
  4. Кастомный фон: Вместо черных прямоугольников можно использовать градиент или частичную маску.
  5. Библиотечный подход: Если нужно много спойлеров, создать отдельную библиотеку с поддержкой Jetpack Compose (SpoilerText composable).

Это решение дает полный контроль над визуальным представлением и поведением, соответствует принципам custom view в Android, и может быть легко адаптировано под конкретные требования дизайна, как в Telegram.