How to rewrite my ItemDecoration for supporting reverseLayout?

27 Views Asked by At

I recently found an application on github In this application there is a list of words and they are divided into categories by the first letter. Also, when the user starts scrolling through the list, the letter is scrolled along with the recyclerview.

Here is application: https://github.com/hdralexandru/Wordphabet

Here's how it looks and works

I would like to make this application support reverseLayout, but I tried and failed. The result should be similar as in the Telegram application when you view the comments and the image is moved by scrolling the recycler view.

enter image description here

The code of Item Decoration:

class StickyLetterDecoration(
context: Context,
words: List<String> = ListsProvider.WORDS) : RecyclerView.ItemDecoration() {

private val textPaint: TextPaint
private val positionToInitialsMap: Map<Int, String>
private val relativeDrawingPosition: Point<Float>

private val itemPadding: Float

init {
    val viewTextSize = context.resources.getDimensionPixelSize(R.dimen.word_text_size)
    positionToInitialsMap = ListsProvider.buildMapWithIndexes(words)
    textPaint = TextPaint(ANTI_ALIAS_FLAG).apply {
        alpha = 255 //Totally visible
        typeface = Typeface.create(Typeface.DEFAULT_BOLD, Typeface.ITALIC) // Bold text for better visibility
        textSize = viewTextSize * SCALING_FACTOR
        color = context.resources.getColor(R.color.initials_color)
        textAlign = Paint.Align.CENTER
    }
    val itemViewPadding = context.resources.getDimensionPixelOffset(R.dimen.word_padding)
    relativeDrawingPosition = Point(0f, 0f).apply {
        val rawPixelsMargin = context.resources.getDimensionPixelSize(R.dimen.word_left_margin)
        /* Since we also use textAlign = CENTER, x will be
         * center of our text
         */
        x = rawPixelsMargin / 2f
        /*
         * When using canvas.draw(text, x, y, paint), y represents the BASELINE,
         * not the center, like in x
         */
        val baseLine = (itemViewPadding * 2 + viewTextSize) / 2 + textPaint.textSize / 2
        y = baseLine
    }
    itemPadding = itemViewPadding + (textPaint.textSize - viewTextSize)/2
}


override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
    var prevHeaderY = Float.MAX_VALUE
    var prevFoundPosition =
        NO_POSITION

    for (initialsPosition in (parent.size - 1) downTo 0) {
        /*
         * Iterate over each item, in reverse order, to be able to
         * `push` the first item upwards
         */
        val currentChild = parent.getChildAt(initialsPosition)
        if (childOutsideParent(currentChild, parent)) continue

        val childPosition: Int = parent.getChildAdapterPosition(currentChild)
        positionToInitialsMap[childPosition]?.let {
            /*
             * If the position of the current child is a key in our map it means
             * that this is the first letter of our set of words. We should draw the
             * initial on the screen
             */
            val yDrawingPosition = (currentChild.top + currentChild.translationY + relativeDrawingPosition.y)
                .coerceAtLeast(relativeDrawingPosition.y)
                .coerceAtMost(prevHeaderY - relativeDrawingPosition.y - itemPadding)
            canvas.drawText(it, relativeDrawingPosition.x, yDrawingPosition, textPaint)
            prevFoundPosition = childPosition
            prevHeaderY = yDrawingPosition
        }
    }

    /**
     * If no header word was found, it means that
     * the first word of our category and the first word of NEXT category
     * are not visible on screen. For this, we get the current position of the adapter + 1
     * and will later print the first value smaller than this
     */
    prevFoundPosition = if (prevFoundPosition != NO_POSITION) prevFoundPosition else
        parent.getChildAdapterPosition(parent[0]) + 1

    for (initialsPosition in positionToInitialsMap.keys.reversed()) {
        if (initialsPosition < prevFoundPosition) {
            /**
             * this is the header item of our category
             */
            positionToInitialsMap[initialsPosition]?.let {
                val yDrawingPosition = (prevHeaderY - textPaint.textSize - itemPadding)
                    .coerceAtMost(relativeDrawingPosition.y)
                canvas.drawText(it, relativeDrawingPosition.x, yDrawingPosition, textPaint)
            }
            break
        }
    }



/**
 * Since we iterate over the full map, if a child (possible header) is outside
 * the recyclerView, we shouldn't continue do things, because they won't be visible
 * on screen.
 *
 * Even if the RecyclerView only creates as many items as it needs (and not a full list),
 * we don't know for sure if there are as many items as they fit on the screen or + (1 or 2) extra items
 */
private fun childOutsideParent(childView: View, parent: RecyclerView): Boolean {
    return childView.bottom < 0
            || (childView.top + childView.translationY.toInt() > parent.height)

}

fun buildMapWithIndexes(words: List<String> = WORDS) = words
    .mapIndexed { index, string -> index to string[0].toUpperCase().toString() }
    .distinctBy { it.second }
    .toMap()

companion object {
    private const val SCALING_FACTOR = 1.5F
    private const val NO_POSITION = -1
}}
0

There are 0 best solutions below