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
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.
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
}}

