Signal-Android/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/RecyclerViewColorizer.kt

170 wiersze
5.6 KiB
Kotlin

package org.thoughtcrime.securesms.conversation.colors
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.graphics.Rect
import android.view.View
import android.widget.EdgeEffect
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import org.thoughtcrime.securesms.components.RotatableGradientDrawable
/**
* Draws the ChatColors color or gradient following this procedure:
*
* 1. Have the RecyclerView's ItemDecoration#onDraw method, fill the bounds of the RecyclerView with the background color or drawable
* 2. Have each child item draw the bubble shape with the "clear" blend mode to "hole punch" a region within the background already drawn by the RecyclerView
* 3. In the RecyclerView's ItemDecoration#onDrawOver method, draw the gradient with the full bounds of the RecyclerView using the DST_OVER blend mode. This will draw the gradient "underneath" the background rendered in step 1 however will show portions of the gradient in the areas "cleared" by the rendering in step 2
*/
class RecyclerViewColorizer(private val recyclerView: RecyclerView) {
private var topEdgeEffect: EdgeEffect? = null
private var bottomEdgeEffect: EdgeEffect? = null
private fun getLayoutManager(): LinearLayoutManager = recyclerView.layoutManager as LinearLayoutManager
private var useLayer = false
private val noLayerXfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OVER)
private val layerXfermode = PorterDuffXfermode(PorterDuff.Mode.XOR)
private var chatColors: ChatColors? = null
fun setChatColors(chatColors: ChatColors) {
this.chatColors = chatColors
recyclerView.invalidateItemDecorations()
}
private val edgeEffectFactory = object : RecyclerView.EdgeEffectFactory() {
override fun createEdgeEffect(view: RecyclerView, direction: Int): EdgeEffect {
val edgeEffect = super.createEdgeEffect(view, direction)
when (direction) {
DIRECTION_TOP -> topEdgeEffect = edgeEffect
DIRECTION_BOTTOM -> bottomEdgeEffect = edgeEffect
DIRECTION_LEFT -> Unit
DIRECTION_RIGHT -> Unit
}
return edgeEffect
}
}
private val scrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val firstItemPos = getLayoutManager().findFirstVisibleItemPosition()
val lastItemPos = getLayoutManager().findLastVisibleItemPosition()
val itemCount = getLayoutManager().itemCount
val firstVisible = firstItemPos == 0 && itemCount >= 1
val lastVisible = lastItemPos == itemCount - 1 && itemCount >= 1
if (firstVisible || lastVisible || isOverscrolled()) {
useLayer = true
recyclerView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
} else {
useLayer = false
recyclerView.setLayerType(View.LAYER_TYPE_NONE, null)
}
}
}
private val itemDecoration = object : RecyclerView.ItemDecoration() {
private val holePunchPaint = Paint().apply {
xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
color = Color.BLACK
}
private val colorPaint = Paint()
private val outOfBoundsPaint = Paint()
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
outRect.setEmpty()
}
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDraw(c, parent, state)
val colors = chatColors ?: return
if (useLayer) {
c.drawColor(Color.WHITE)
}
for (i in 0 until parent.childCount) {
val child = parent.getChildAt(i)
if (child != null) {
val holder = parent.getChildViewHolder(child)
if (holder is Colorizable) {
holder.getColorizerProjections(parent).use { list ->
list.forEach {
c.drawPath(it.path, holePunchPaint)
}
}
}
}
}
drawShaderMask(c, parent, colors)
}
private fun drawShaderMask(canvas: Canvas, parent: RecyclerView, chatColors: ChatColors) {
if (useLayer) {
colorPaint.xfermode = layerXfermode
} else {
colorPaint.xfermode = noLayerXfermode
}
if (chatColors.isGradient()) {
val mask = chatColors.chatBubbleMask as RotatableGradientDrawable
mask.setXfermode(colorPaint.xfermode)
mask.setBounds(0, 0, parent.width, parent.height)
mask.draw(canvas)
} else {
colorPaint.color = chatColors.asSingleColor()
canvas.drawRect(
0f,
0f,
parent.width.toFloat(),
parent.height.toFloat(),
colorPaint
)
val color = chatColors.asSingleColor()
outOfBoundsPaint.color = color
canvas.drawRect(
0f,
-parent.height.toFloat(),
parent.width.toFloat(),
0f,
outOfBoundsPaint
)
canvas.drawRect(
0f,
parent.height.toFloat(),
parent.width.toFloat(),
parent.height * 2f,
outOfBoundsPaint
)
}
}
}
init {
recyclerView.edgeEffectFactory = edgeEffectFactory
recyclerView.addOnScrollListener(scrollListener)
recyclerView.addItemDecoration(itemDecoration)
}
private fun isOverscrolled(): Boolean {
val topFinished = topEdgeEffect?.isFinished ?: true
val bottomFinished = bottomEdgeEffect?.isFinished ?: true
return !topFinished || !bottomFinished
}
}