package app.fedilab.android.helper /* * This file is a part of Fedilab * * This program is free software; you can redistribute it and/or modify it under the terms of the * GNU General Public License as published by the Free Software Foundation; either version 3 of the * License, or (at your option) any later version. * * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General * Public License for more details. * * You should have received a copy of the GNU General Public License along with Fedilab; if not, * see . */ import android.content.Context import android.content.res.Resources import android.graphics.* import android.view.View import androidx.core.content.res.ResourcesCompat import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.RecyclerView import app.fedilab.android.R import app.fedilab.android.helper.RecyclerViewThreadLines.LineInfo import app.fedilab.android.client.entities.api.Context as StatusContext class RecyclerViewThreadLines(context: Context, private val lineInfoList: List) : DividerItemDecoration(context, VERTICAL) { private val lineColors = threadLineColors.map { ResourcesCompat.getColor(context.resources, it, context.theme) } private val dashPathEffect = DashPathEffect(floatArrayOf(3.dpToPx, 3.dpToPx), 0F) private val borderColor = lineColors[0] private val commonPaint = Paint().apply { isDither = false strokeWidth = 1.5F.dpToPx strokeCap = Paint.Cap.BUTT strokeJoin = Paint.Join.MITER color = borderColor } private val maxLevel = lineColors.size private val fontScale = PreferenceManager.getDefaultSharedPreferences(context).getFloat(context.getString(R.string.SET_FONT_SCALE), 1.1f).toInt() private val baseMargin: Int = 6.dpToPx.toInt() private val margin: Int = baseMargin * fontScale override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { val position = parent.getChildAdapterPosition(view) if (position < 0 || position >= lineInfoList.size) return val level = lineInfoList[position].level val startMargin = margin * level + margin * fontScale if (parent.layoutDirection == View.LAYOUT_DIRECTION_LTR) outRect.left = startMargin else outRect.right = startMargin } override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { val childCount = parent.childCount for (i in 0 until childCount) { val view = parent.getChildAt(i) val position = parent.getChildAdapterPosition(view) if (position < 0 || position >= lineInfoList.size) return val lineInfo = lineInfoList[position] val level = lineInfo.level for (j in 0..level) { val lineMargin = margin * j.coerceAtLeast(1) + 3.dpToPx val lineStart = if (parent.layoutDirection == View.LAYOUT_DIRECTION_LTR) lineMargin else c.width - lineMargin var lineTop: Float = (view.top - baseMargin).toFloat() val paint = Paint(commonPaint) paint.color = if (j > 0) lineColors[j - 1] else Color.GRAY // draw lines for below statuses if (j != level && lineInfo.lines.contains(j)) c.drawLine(lineStart, lineTop, lineStart, view.bottom.toFloat(), paint) // draw vertical line for current statuses if (j == level && position != 0) { // top the line starts at the middle of the above status if (i > 0) lineTop -= parent.getChildAt(i - 1).height / 2 - 1 // '- 1' is to prevent overlapping with above horizontal line // bottom of the line ends at the middle of the current status var lineBottom = view.bottom.toFloat() - view.height / 2 // if below status has a full line for current level, extend the line to the bottom if (position < lineInfoList.lastIndex && lineInfoList[position + 1].lines.contains(level) && j != maxLevel) lineBottom = view.bottom.toFloat() // if level is max, use a dashed line if (j == maxLevel) paint.pathEffect = dashPathEffect c.drawLine(lineStart, lineTop, lineStart, lineBottom, paint) } // draw horizontal line for current statuses if (j == level) { lineTop = view.bottom.toFloat() - view.height / 2 val lineEnd = lineStart + margin * 2 c.drawLine(lineStart - 1, lineTop, lineEnd, lineTop, paint) // 'lineStart - 1' is to properly connect with the vertical line } } } } data class LineInfo(var level: Int, var lines: List) private val Int.dpToPx: Float get() = this * Resources.getSystem().displayMetrics.density private val Float.dpToPx: Float get() = this * Resources.getSystem().displayMetrics.density companion object { val threadLineColors = listOf(R.color.decoration_1, R.color.decoration_2, R.color.decoration_3, R.color.decoration_4, R.color.decoration_5) } } fun getThreadDecorationInfo(statusContext: StatusContext): MutableList { val lineInfoList = mutableListOf() repeat(statusContext.ancestors.size) { lineInfoList.add(LineInfo(0, listOf(0))) } lineInfoList.add(LineInfo(0, listOf(0))) val descendantsLineInfoList = List(statusContext.descendants.size) { LineInfo(0, listOf()) } for (i in statusContext.descendants.indices) { statusContext.descendants[i].let { status -> var level = 0 if (status.in_reply_to_id != null) { var replyToId: String? = status.in_reply_to_id while (replyToId != null && level < RecyclerViewThreadLines.threadLineColors.size) { level += 1 replyToId = statusContext.descendants.firstOrNull { it.id == replyToId }?.in_reply_to_id } } descendantsLineInfoList[i].level = level } } for (i in descendantsLineInfoList.indices.reversed()) { val lines: MutableList = mutableListOf() val lineInfo = descendantsLineInfoList[i] lines.add(lineInfo.level) if (i < descendantsLineInfoList.lastIndex) { val belowLineInfo = descendantsLineInfoList[i + 1] lines.addAll(belowLineInfo.lines.filter { it < lineInfo.level }) } descendantsLineInfoList[i].lines = lines } lineInfoList.addAll(descendantsLineInfoList) return lineInfoList }