Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions app/src/main/java/com/nextcloud/utils/text/Spans.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger.de>
* SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.utils.text

import android.graphics.drawable.Drawable
import thirdparties.fresco.BetterImageSpan

class Spans {
class MentionChipSpan(drawable: Drawable, verticalAlignment: Int, var id: String, var label: CharSequence?) :
BetterImageSpan(drawable, verticalAlignment) {
override fun equals(o: Any?): Boolean {
if (o === this) {
return true
}
if (o !is MentionChipSpan) {
return false
}
val other = o
if (!other.canEqual(this as Any)) {
return false
}
val thisId: Any? = this.id
val otherId: Any? = other.id
if (if (thisId == null) otherId != null else (thisId != otherId)) {
return false
}
val thisLabel: Any? = this.label
val otherLabel: Any? = other.label

return if (thisLabel == null) otherLabel == null else (thisLabel == otherLabel)
}

protected fun canEqual(other: Any?): Boolean = other is MentionChipSpan

override fun hashCode(): Int {
val prime = 59
var result = 1
val thisId: Any? = this.id
result = result * prime + (if (thisId == null) 43 else thisId.hashCode())
val label: Any? = this.label
return result * prime + (if (label == null) 43 else label.hashCode())
}

override fun toString(): String = "Spans.MentionChipSpan(id=" + this.id + ", label=" + this.label + ")"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ package com.owncloud.android.ui.activities.adapter

import android.content.Context
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.TextPaint
import android.text.TextUtils
import android.text.format.DateFormat
import android.text.format.DateUtils
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
Expand All @@ -27,10 +28,12 @@ import androidx.annotation.DrawableRes
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.chip.ChipDrawable
import com.nextcloud.android.common.ui.theme.utils.ColorRole
import com.nextcloud.client.account.CurrentAccountProvider
import com.nextcloud.common.NextcloudClient
import com.nextcloud.utils.GlideHelper
import com.nextcloud.utils.text.Spans.MentionChipSpan
import com.owncloud.android.MainApp
import com.owncloud.android.R
import com.owncloud.android.databinding.ActivityListItemBinding
Expand All @@ -49,6 +52,7 @@ import com.owncloud.android.utils.theme.ViewThemeUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import thirdparties.fresco.BetterImageSpan
import java.util.Locale
import kotlin.math.floor
import kotlin.math.log
Expand All @@ -62,7 +66,8 @@ open class ActivityListAdapter(
private val isDetailView: Boolean,
private val viewThemeUtils: ViewThemeUtils
) : RecyclerView.Adapter<RecyclerView.ViewHolder>(),
StickyHeaderAdapter {
StickyHeaderAdapter,
DisplayUtils.AvatarGenerationListener {

protected var client: NextcloudClient? = null
val values: MutableList<Any> = mutableListOf()
Expand Down Expand Up @@ -113,9 +118,10 @@ open class ActivityListAdapter(

when {
activity.richSubjectElement.richSubject.isNotEmpty() -> holder.binding.subject.apply {
visibility = View.VISIBLE
movementMethod = LinkMovementMethod.getInstance()
setText(addClickablePart(activity.richSubjectElement), TextView.BufferType.SPANNABLE)
text = addClickablePart(activity.richSubjectElement)
// visibility = View.VISIBLE
// movementMethod = LinkMovementMethod.getInstance()
// setText(addClickablePart(activity.richSubjectElement), TextView.BufferType.SPANNABLE)
}

activity.subject.isNotEmpty() -> holder.binding.subject.apply {
Expand Down Expand Up @@ -176,6 +182,19 @@ open class ActivityListAdapter(
}
}

fun getDrawableForMentionChipSpan(chipResource: Int, text: String): ChipDrawable {
val chip = ChipDrawable.createFromResource(context, chipResource).apply {
setEllipsize(TextUtils.TruncateAt.MIDDLE)
setLayoutDirection(context.getResources().getConfiguration().getLayoutDirection())
setText(text)
setChipIconResource(R.drawable.accent_circle)
}

chip.setBounds(0, 0, chip.getIntrinsicWidth(), chip.getIntrinsicHeight())

return chip
}

private suspend fun nextcloudClient(): NextcloudClient = withContext(Dispatchers.IO) {
OwnCloudClientManagerFactory.getDefaultSingleton()
.getNextcloudClientFor(currentAccountProvider.user.toOwnCloudAccount(), context)
Expand Down Expand Up @@ -224,6 +243,7 @@ open class ActivityListAdapter(
return imageView
}

@Suppress("NestedBlockDepth")
private fun addClickablePart(richElement: RichElement): SpannableStringBuilder {
var text = richElement.richSubject
val ssb = SpannableStringBuilder(text)
Expand All @@ -236,29 +256,57 @@ open class ActivityListAdapter(
}

if (richObject != null) {
val name = richObject.name.orEmpty()
ssb.replace(idx1, idx2, name)
text = ssb.toString()
idx2 = idx1 + name.length

ssb.setSpan(
object : ClickableSpan() {
override fun onClick(widget: View) = activityListInterface.onActivityClicked(richObject)
override fun updateDrawState(ds: TextPaint) {
ds.isUnderlineText = false
}
},
idx1,
idx2,
0
)
ssb.setSpan(StyleSpan(Typeface.BOLD), idx1, idx2, 0)
ssb.setSpan(
ForegroundColorSpan(context.resources.getColor(R.color.text_color)),
idx1,
idx2,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
if ("user".equals(richObject.type)) {
val name = richObject.name

val drawableForChip = getDrawableForMentionChipSpan(R.xml.chip_others, name ?: "")

val mentionChipSpan = MentionChipSpan(
drawableForChip,
BetterImageSpan.ALIGN_CENTER,
richObject.id ?: "",
name
)

if (richObject.id != null) {
DisplayUtils.setAvatar(
currentAccountProvider.user,
richObject.id!!,
name,
this,
context.resources.getDimension(R.dimen.avatar_icon_radius),
context.resources,
drawableForChip,
context
)
}

ssb.setSpan(mentionChipSpan, idx1, idx2, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
} else {
val name = richObject.name.orEmpty()
ssb.replace(idx1, idx2, name)
text = ssb.toString()
idx2 = idx1 + name.length

ssb.setSpan(
object : ClickableSpan() {
override fun onClick(widget: View) = activityListInterface.onActivityClicked(richObject)
override fun updateDrawState(ds: TextPaint) {
ds.isUnderlineText = false
}
},
idx1,
idx2,
0
)
ssb.setSpan(StyleSpan(Typeface.BOLD), idx1, idx2, 0)
ssb.setSpan(
ForegroundColorSpan(context.resources.getColor(R.color.text_color)),
idx1,
idx2,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
idx1 = text.indexOf('{', idx2)
}
Expand Down Expand Up @@ -308,6 +356,12 @@ open class ActivityListAdapter(
override fun isHeader(itemPosition: Int) =
itemPosition in values.indices && getItemViewType(itemPosition) == HEADER_TYPE

override fun avatarGenerated(avatarDrawable: Drawable, callContext: Any) {
(callContext as ChipDrawable).chipIcon = avatarDrawable
}

override fun shouldCallGeneratedCallback(tag: String, callContext: Any): Boolean = true

protected class ActivityViewHolder(val binding: ActivityListItemBinding) :
RecyclerView.ViewHolder(binding.root)

Expand Down
119 changes: 119 additions & 0 deletions app/src/main/java/third_parties/fresco/BetterImageSpan.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* SPDX-FileCopyrightText: 2015-present, Facebook, Inc. and its affiliates.
* SPDX-License-Identifier: MIT
*/

package thirdparties.fresco

import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.text.style.ReplacementSpan
import androidx.annotation.IntDef

/**
* A better implementation of image spans that also supports centering images against the text.
*
* In order to migrate from ImageSpan, replace `new ImageSpan(drawable, alignment)` with
* `new BetterImageSpan(drawable, BetterImageSpan.normalizeAlignment(alignment))`.
*
* There are 2 main differences between BetterImageSpan and ImageSpan:
* 1. Pass in ALIGN_CENTER to center images against the text.
* 2. ALIGN_BOTTOM no longer unnecessarily increases the size of the text:
* DynamicDrawableSpan (ImageSpan's parent) adjusts sizes as if alignment was ALIGN_BASELINE
* which can lead to unnecessary whitespace.
*/
open class BetterImageSpan @JvmOverloads constructor(
val drawable: Drawable,
@param:BetterImageSpanAlignment private val mAlignment: Int = ALIGN_BASELINE
) : ReplacementSpan() {
@Suppress("Detekt.SpreadOperator")
@IntDef(*[ALIGN_BASELINE, ALIGN_BOTTOM, ALIGN_CENTER])
@Retention(AnnotationRetention.SOURCE)
annotation class BetterImageSpanAlignment

private var mWidth = 0
private var mHeight = 0
private var mBounds: Rect? = null
private val mFontMetricsInt = Paint.FontMetricsInt()

init {
updateBounds()
}

/**
* Returns the width of the image span and increases the height if font metrics are available.
*/
override fun getSize(
paint: Paint,
text: CharSequence,
start: Int,
end: Int,
fontMetrics: Paint.FontMetricsInt?
): Int {
updateBounds()
if (fontMetrics == null) {
return mWidth
}
val offsetAbove = getOffsetAboveBaseline(fontMetrics)
val offsetBelow = mHeight + offsetAbove
if (offsetAbove < fontMetrics.ascent) {
fontMetrics.ascent = offsetAbove
}
if (offsetAbove < fontMetrics.top) {
fontMetrics.top = offsetAbove
}
if (offsetBelow > fontMetrics.descent) {
fontMetrics.descent = offsetBelow
}
if (offsetBelow > fontMetrics.bottom) {
fontMetrics.bottom = offsetBelow
}
return mWidth
}

override fun draw(
canvas: Canvas,
text: CharSequence,
start: Int,
end: Int,
x: Float,
top: Int,
y: Int,
bottom: Int,
paint: Paint
) {
paint.getFontMetricsInt(mFontMetricsInt)
val iconTop = y + getOffsetAboveBaseline(mFontMetricsInt)
canvas.translate(x, iconTop.toFloat())
drawable.draw(canvas)
canvas.translate(-x, -iconTop.toFloat())
}

private fun updateBounds() {
mBounds = drawable.bounds
mWidth = mBounds!!.width()
mHeight = mBounds!!.height()
}

private fun getOffsetAboveBaseline(fm: Paint.FontMetricsInt): Int = when (mAlignment) {
ALIGN_BOTTOM -> fm.descent - mHeight

ALIGN_CENTER -> {
val textHeight = fm.descent - fm.ascent
val offset = (textHeight - mHeight) / 2
fm.ascent + offset
}

ALIGN_BASELINE -> -mHeight

else -> -mHeight
}

companion object {
const val ALIGN_BOTTOM = 0
const val ALIGN_BASELINE = 1
const val ALIGN_CENTER = 2
}
}
10 changes: 10 additions & 0 deletions app/src/main/res/drawable/accent_circle.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud Talk - Android Client
Comment thread
tobiasKaminsky marked this conversation as resolved.
~
~ SPDX-FileCopyrightText: 2017-2018 Mario Danic <mario@lovelyhq.com>
~ SPDX-License-Identifier: GPL-3.0-or-later
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/colorPrimary" />
</shape>
6 changes: 4 additions & 2 deletions app/src/main/res/layout/activity_list_item.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@

<TextView
android:id="@+id/subject"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:ellipsize="end"
android:paddingStart="@dimen/activity_icon_layout_right_end_margin"
android:paddingTop="@dimen/standard_padding"
android:paddingBottom="@dimen/standard_margin"
android:paddingEnd="@dimen/zero"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textSize="@dimen/two_line_primary_text_size"
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/dims.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
<dimen name="live_photo_indicator_vertical_padding">3dp</dimen>
<dimen name="live_photo_indicator_horizontal_padding">16dp</dimen>
<dimen name="file_list_item_avatar_icon_radius">10dp</dimen>
<dimen name="avatar_icon_radius">12dp</dimen>
<dimen name="account_action_layout_height">72dp</dimen>
<dimen name="zero">0dp</dimen>
<dimen name="iconized_single_line_item_layout_height">56dp</dimen>
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/res/values/styles.xml
Original file line number Diff line number Diff line change
Expand Up @@ -490,4 +490,8 @@
<item name="android:windowBackground">@android:color/black</item>
<item name="android:colorControlHighlight">@color/primary</item>
</style>

<style name="ChipIncomingTextAppearance" parent="TextAppearance.MaterialComponents.Chip">
<item name="android:textColor">#de000000</item>
</style>
</resources>
Loading
Loading