PopupViewHelper.kt

package androidx.emoji2.emojipicker

import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.View.OnClickListener
import android.view.ViewGroup
import android.view.accessibility.AccessibilityEvent
import android.widget.GridLayout
import androidx.core.content.ContextCompat

/*
 * Copyright 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

internal class PopupViewHelper(private val context: Context) {
    companion object {
        private enum class Layout { FLAT, SQUARE, SQUARE_WITH_SKIN_TONE_CIRCLE }

        private const val SQUARE_LAYOUT_VARIANT_COUNT = 26

        // Set of emojis that use the square layout without skin tone.
        private val SQUARE_LAYOUT_EMOJI_NO_SKIN_TONE = setOf("👪")

        private val SKIN_TONE_COLOR_RES_IDS = listOf(
            R.color.light_skin_tone,
            R.color.medium_light_skin_tone,
            R.color.medium_skin_tone,
            R.color.medium_dark_skin_tone,
            R.color.dark_skin_tone
        )

        /**
         * Square variant layout template with skin tone.
         * 0 : a place holder
         * -5: light skin tone circle
         * -4: medium-light skin tone circle
         * -3: medium skin tone circle
         * -2: medium-dark skin tone circle
         * -1: dark skin tone circle
         * Positive number is the index + 1 in the variant array
         */
        private val SQUARE_LAYOUT_WITH_SKIN_TONES_TEMPLATE = arrayOf(
            intArrayOf(0, 0, -5, -4, -3, -2, -1),
            intArrayOf(0, -5, 2, 3, 4, 5, 6),
            intArrayOf(0, -4, 7, 8, 9, 10, 11),
            intArrayOf(0, -3, 12, 13, 14, 15, 16),
            intArrayOf(0, -2, 17, 18, 19, 20, 21),
            intArrayOf(1, -1, 22, 23, 24, 25, 26)
        )

        /**
         * Square variant layout template without skin tone.
         * 0 : a place holder
         * Positive number is the index + 1 in the variant array
         */
        private val SQUARE_LAYOUT_TEMPLATE = arrayOf(
            intArrayOf(0, 2, 3, 4, 5, 6),
            intArrayOf(0, 7, 8, 9, 10, 11),
            intArrayOf(0, 12, 13, 14, 15, 16),
            intArrayOf(0, 17, 18, 19, 20, 21),
            intArrayOf(1, 22, 23, 24, 25, 26)
        )
    }

    fun fillPopupView(
        popupView: GridLayout,
        layoutInflater: LayoutInflater,
        gridWidth: Int,
        gridHeight: Int,
        variants: List<String>,
        clickListener: OnClickListener
    ): GridLayout {
        val gridTemplate = getGridTemplate(variants)
        popupView
            .apply {
                columnCount = gridTemplate.column
                rowCount = gridTemplate.row
                orientation = GridLayout.HORIZONTAL
            }
        gridTemplate.template.flatMap { it.asIterable() }.forEach {
            val gridCell = when (it) {
                in 1..variants.size -> (layoutInflater.inflate(
                    R.layout.emoji_view_holder,
                    null,
                    false
                ) as ViewGroup).apply {
                    findViewById<EmojiView>(R.id.emoji_view).apply {
                        emoji = variants[it - 1]
                        setOnClickListener(clickListener)
                        if (it == 1) {
                            // Hover on the first emoji in the popup
                            popupView.post {
                                sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER)
                            }
                        }
                    }
                }

                0 -> layoutInflater
                    .inflate(R.layout.emoji_view_holder, null, false)

                else -> SkinToneCircleView(context).apply {
                    paint = Paint().apply {
                        color = ContextCompat.getColor(context, SKIN_TONE_COLOR_RES_IDS[it + 5])
                        style = Paint.Style.FILL
                    }
                }
            }
            popupView.addView(gridCell)
            gridCell.layoutParams.width = gridWidth
            gridCell.layoutParams.height = gridHeight
        }
        return popupView
    }

    private fun getGridTemplate(variants: List<String>): GridTemplate {
        val layout =
            if (variants.size == SQUARE_LAYOUT_VARIANT_COUNT)
                if (SQUARE_LAYOUT_EMOJI_NO_SKIN_TONE.contains(variants[0]))
                    Layout.SQUARE
                else Layout.SQUARE_WITH_SKIN_TONE_CIRCLE
            else Layout.FLAT
        val template = when (layout) {
            Layout.SQUARE -> SQUARE_LAYOUT_TEMPLATE
            Layout.SQUARE_WITH_SKIN_TONE_CIRCLE -> SQUARE_LAYOUT_WITH_SKIN_TONES_TEMPLATE
            Layout.FLAT -> arrayOf(variants.indices.map { it + 1 }.toIntArray())
        }
        val column = when (layout) {
            Layout.SQUARE, Layout.SQUARE_WITH_SKIN_TONE_CIRCLE -> template[0].size
            Layout.FLAT -> minOf(6, template[0].size)
        }
        val row = when (layout) {
            Layout.SQUARE, Layout.SQUARE_WITH_SKIN_TONE_CIRCLE -> template.size
            Layout.FLAT -> variants.size / column + if (variants.size % column == 0) 0 else 1
        }

        return GridTemplate(template, row, column)
    }

    private data class GridTemplate(
        val template: Array<IntArray>,
        val row: Int,
        val column: Int
    )
}

internal class SkinToneCircleView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null
) : View(context, attrs) {
    private val radius = resources.getDimension(R.dimen.emoji_picker_skin_tone_circle_radius)
    var paint: Paint? = null

    override fun draw(canvas: Canvas?) {
        super.draw(canvas)
        canvas?.apply {
            paint?.let { drawCircle(width / 2f, height / 2f, radius, it) }
        }
    }
}