Skip to content
Merged
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
191 changes: 85 additions & 106 deletions app/src/main/java/com/owncloud/android/ui/AvatarGroupLayout.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@
* Nextcloud Android client application
*
* SPDX-FileCopyrightText: 2021-2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.owncloud.android.ui

import android.content.Context
import android.content.res.Resources
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.widget.ImageView
import android.widget.RelativeLayout
import androidx.annotation.Px
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.updateLayoutParams
import com.nextcloud.android.common.ui.theme.utils.ColorRole
import com.nextcloud.client.account.User
import com.nextcloud.utils.GlideHelper.loadCircularBitmapIntoImageView
Expand All @@ -24,28 +24,29 @@ import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.lib.resources.shares.ShareType
import com.owncloud.android.lib.resources.shares.ShareeUser
import com.owncloud.android.utils.DisplayUtils
import com.owncloud.android.utils.DisplayUtils.AvatarGenerationListener
import com.owncloud.android.utils.theme.ViewThemeUtils
import kotlin.math.min

@Suppress("MagicNumber")
class AvatarGroupLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
) : RelativeLayout(context, attrs, defStyleAttr, defStyleRes),
AvatarGenerationListener {
private val borderDrawable = ContextCompat.getDrawable(context, R.drawable.round_bgnd)
DisplayUtils.AvatarGenerationListener {
private val borderDrawable = checkNotNull(ContextCompat.getDrawable(context, R.drawable.round_bgnd)).apply {
setTint(ContextCompat.getColor(context, R.color.bg_default))
}

@Px
private val avatarSize: Int = DisplayUtils.convertDpToPixel(40f, context)
private val avatarSize = DisplayUtils.convertDpToPixel(AVATAR_SIZE_DP, context)

@Px
private val avatarBorderSize: Int = DisplayUtils.convertDpToPixel(2f, context)
private val avatarBorderSize = DisplayUtils.convertDpToPixel(AVATAR_BORDER_DP, context)

@Px
private val overlapPx: Int = DisplayUtils.convertDpToPixel(24f, context)
private val overlapPx = DisplayUtils.convertDpToPixel(AVATAR_OVERLAP_DP, context)

private val avatarRadius = resources.getDimension(R.dimen.list_item_avatar_icon_radius)

var boundFileId: Long? = null
set(value) {
Expand All @@ -57,19 +58,12 @@ class AvatarGroupLayout @JvmOverloads constructor(

private var displayedSharees: List<ShareeUser>? = null

init {
checkNotNull(borderDrawable)
DrawableCompat.setTint(borderDrawable, ContextCompat.getColor(context, R.color.bg_default))
}

@Suppress("LongMethod", "TooGenericExceptionCaught")
fun setAvatars(user: User, sharees: List<ShareeUser>, viewThemeUtils: ViewThemeUtils) {
if (sharees == displayedSharees) {
return
}
displayedSharees = sharees

val context = getContext()
removeAllViews()

if (sharees.isEmpty()) {
Expand All @@ -78,108 +72,79 @@ class AvatarGroupLayout @JvmOverloads constructor(
}
visibility = VISIBLE

var avatarLayoutParams: LayoutParams?
val shareeSize = min(sharees.size, MAX_AVATAR_COUNT)
val resources = context.resources
val avatarRadius = resources.getDimension(R.dimen.list_item_avatar_icon_radius)
val avatarCount = sharees.size.coerceAtMost(MAX_AVATAR_COUNT)
val hasOverflow = sharees.size > MAX_AVATAR_COUNT
val serverName = user.accountName.substringAfterLast('@')
var sharee: ShareeUser

var avatarCount = 0
while (avatarCount < shareeSize) {
avatarLayoutParams = LayoutParams(avatarSize, avatarSize).apply {
setMargins(0, 0, avatarCount * overlapPx, 0)
addRule(ALIGN_PARENT_RIGHT)
}
repeat(avatarCount) { index ->
val avatar = addAvatarView(index)

val avatar = ImageView(context).apply {
layoutParams = avatarLayoutParams
setPadding(avatarBorderSize, avatarBorderSize, avatarBorderSize, avatarBorderSize)
scaleType = ImageView.ScaleType.CENTER_INSIDE
background = borderDrawable
}

addView(avatar)
avatar.requestLayout()

if (avatarCount == 0 && sharees.size > MAX_AVATAR_COUNT) {
if (index == 0 && hasOverflow) {
avatar.setImageResource(R.drawable.ic_people)
viewThemeUtils.platform.tintDrawable(context, avatar.drawable, ColorRole.ON_SURFACE)
} else {
sharee = sharees[avatarCount]
when (sharee.shareType) {
ShareType.GROUP, ShareType.EMAIL, ShareType.ROOM, ShareType.CIRCLE ->
viewThemeUtils.files.createAvatar(
sharee.shareType,
avatar,
context
)

ShareType.FEDERATED, ShareType.FEDERATED_GROUP -> showFederatedShareAvatar(
context,
sharee.userId!!,
avatarRadius,
resources,
avatar,
viewThemeUtils
)

else -> {
avatar.tag = "${sharee.userId}@$serverName"
DisplayUtils.setAvatar(
user,
sharee.userId!!,
sharee.displayName,
this,
avatarRadius,
resources,
avatar,
context,
avatarBorderSize
)
}
}
bindSharee(avatar, sharees[index], user, serverName, viewThemeUtils)
}
avatarCount++
}

// Recalculate container size based on avatar count
val size = overlapPx * (avatarCount - 1) + avatarSize
val rememberParam = layoutParams
rememberParam.width = size
layoutParams = rememberParam
updateLayoutParams { width = overlapPx * (avatarCount - 1) + avatarSize }
}

@Suppress("TooGenericExceptionCaught")
private fun showFederatedShareAvatar(
context: Context,
user: String,
avatarRadius: Float,
resources: Resources,
private fun addAvatarView(index: Int): ImageView {
val avatar = ImageView(context).apply {
layoutParams = LayoutParams(avatarSize, avatarSize).apply {
setMargins(0, 0, index * overlapPx, 0)
addRule(ALIGN_PARENT_RIGHT)
}
setPadding(avatarBorderSize, avatarBorderSize, avatarBorderSize, avatarBorderSize)
scaleType = ImageView.ScaleType.CENTER_INSIDE
background = borderDrawable
}

addView(avatar)
avatar.requestLayout()
return avatar
}

private fun bindSharee(
avatar: ImageView,
sharee: ShareeUser,
user: User,
serverName: String,
viewThemeUtils: ViewThemeUtils
) {
val split = user.split("@")
val userId = split.getOrNull(0) ?: user
val server = split.getOrNull(1)

val url = if (server != null) {
"https://$server/index.php/avatar/$userId/${resources.getInteger(R.integer.file_avatar_px)}"
} else {
// fallback: no federated server, maybe use local avatar
null
when (sharee.shareType) {
ShareType.GROUP, ShareType.EMAIL, ShareType.ROOM, ShareType.CIRCLE, ShareType.PUBLIC_LINK ->
viewThemeUtils.files.createAvatar(sharee.shareType, avatar, context)

ShareType.FEDERATED, ShareType.FEDERATED_GROUP ->
showFederatedShareAvatar(checkNotNull(sharee.userId), avatar, viewThemeUtils)

else -> {
val userId = checkNotNull(sharee.userId)
avatar.tag = "$userId@$serverName"
DisplayUtils.setAvatar(
user,
userId,
sharee.displayName,
this,
avatarRadius,
resources,
avatar,
context,
avatarBorderSize
)
}
}
}

val placeholder: Drawable = try {
TextDrawable.createAvatarByUserId(userId, avatarRadius)
} catch (e: Exception) {
Log_OC.e(TAG, "Error calculating RGB value for active account icon.", e)
viewThemeUtils.platform.colorDrawable(
ResourcesCompat
.getDrawable(resources, R.drawable.account_circle_white, null)!!,
ContextCompat.getColor(context, R.color.black)
)
private fun showFederatedShareAvatar(federatedId: String, avatar: ImageView, viewThemeUtils: ViewThemeUtils) {
val parts = federatedId.split("@")
val userId = parts.first()
val url = parts.getOrNull(1)?.let { server ->
"https://$server/index.php/avatar/$userId/${resources.getInteger(R.integer.file_avatar_px)}"
}
val placeholder = createFederatedPlaceholder(userId, viewThemeUtils)

avatar.tag = null
if (url != null) {
Expand All @@ -189,15 +154,29 @@ class AvatarGroupLayout @JvmOverloads constructor(
}
}

override fun avatarGenerated(avatarDrawable: Drawable?, callContext: Any) {
(callContext as ImageView).setImageDrawable(avatarDrawable)
@Suppress("TooGenericExceptionCaught")
private fun createFederatedPlaceholder(userId: String, viewThemeUtils: ViewThemeUtils): Drawable = try {
TextDrawable.createAvatarByUserId(userId, avatarRadius)
} catch (e: Exception) {
Log_OC.e(TAG, "Error calculating RGB value for active account icon.", e)
viewThemeUtils.platform.colorDrawable(
checkNotNull(ResourcesCompat.getDrawable(resources, R.drawable.account_circle_white, null)),
ContextCompat.getColor(context, R.color.black)
)
}

override fun avatarGenerated(avatarDrawable: Drawable?, callContext: Any?) {
(callContext as? ImageView)?.setImageDrawable(avatarDrawable)
}

override fun shouldCallGeneratedCallback(tag: String?, callContext: Any): Boolean =
(callContext as ImageView).tag == tag
override fun shouldCallGeneratedCallback(tag: String?, callContext: Any?): Boolean =
(callContext as? ImageView)?.let { it.tag == tag } ?: false

companion object {
private val TAG: String = AvatarGroupLayout::class.java.simpleName
private const val MAX_AVATAR_COUNT = 3
private const val AVATAR_SIZE_DP = 40f
private const val AVATAR_BORDER_DP = 2f
private const val AVATAR_OVERLAP_DP = 24f
}
}
Loading
Loading