All Downloads are FREE. Search and download functionalities are using the official Maven repository.

main.com.sceyt.chatuikit.presentation.custom_views.AvatarView.kt Maven / Gradle / Ivy

There is a newer version: 1.7.2
Show newest version
package com.sceyt.chatuikit.presentation.custom_views

import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapShader
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Matrix
import android.graphics.Paint
import android.graphics.Path
import android.graphics.Shader
import android.graphics.drawable.Drawable
import android.os.Build
import android.text.Layout
import android.text.StaticLayout
import android.text.TextPaint
import android.util.AttributeSet
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.appcompat.widget.AppCompatImageView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.sceyt.chatuikit.R
import com.sceyt.chatuikit.SceytChatUIKit
import com.sceyt.chatuikit.extensions.getCompatColor
import com.sceyt.chatuikit.extensions.getCompatDrawable
import com.sceyt.chatuikit.extensions.getFirstCharIsEmoji
import com.sceyt.chatuikit.extensions.processEmojiCompat
import com.sceyt.chatuikit.extensions.roundUp
import com.sceyt.chatuikit.presentation.custom_views.AvatarView.DefaultAvatar
import kotlin.math.abs

class AvatarView @JvmOverloads constructor(
        context: Context,
        attrs: AttributeSet? = null,
        defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr) {
    private var name: CharSequence? = null
    private var imageUrl: String? = null
    private val textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG)
    private lateinit var imagePaint: Paint
    private lateinit var backgroundPaint: Paint
    private var textSize = 0
    private var avatarLoadCb: ((loading: Boolean) -> Unit?)? = null

    @ColorInt
    private var avatarBackgroundColor: Int = 0
    private var defaultAvatar: DefaultAvatar? = null

    init {
        var enableRipple = true
        attrs?.let {
            val a = context.obtainStyledAttributes(attrs, R.styleable.AvatarView)
            name = a.getString(R.styleable.AvatarView_sceytUiAvatarFullName) ?: name
            imageUrl = a.getString(R.styleable.AvatarView_sceytUiAvatarImageUrl)
            textSize = a.getDimensionPixelSize(R.styleable.AvatarView_sceytUiAvatarTextSize, textSize)
            avatarBackgroundColor = a.getColor(R.styleable.AvatarView_sceytUiAvatarColor, avatarBackgroundColor)
            val defaultAvatarResId = a.getResourceId(R.styleable.AvatarView_sceytUiAvatarDefaultIcon, 0)
            if (defaultAvatarResId != 0) {
                defaultAvatar = defaultAvatarResId.toDefaultAvatar()
            }
            enableRipple = a.getBoolean(R.styleable.AvatarView_sceytUiAvatarEnableRipple, true)
            a.recycle()
        }
        scaleType = ScaleType.CENTER_CROP
        if (enableRipple) {
            val ripple = context.getCompatDrawable(R.drawable.sceyt_bg_ripple_circle)
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                foreground = ripple
            } else background = ripple
        }
        initPaints()
    }

    private fun initPaints() {
        imagePaint = TextPaint(Paint.ANTI_ALIAS_FLAG)
        backgroundPaint = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
            style = Paint.Style.FILL
        }
    }

    override fun draw(canvas: Canvas) {
        if (visibility != VISIBLE) return
        if (imageUrl.isNullOrBlank()) {
            setImageResource(0)
            val default = defaultAvatar
            if (default == null) {
                val initials = getInitials(name ?: "")
                drawBackgroundColor(canvas, avatarBackgroundColor)
                drawInitials(canvas, initials)
            } else {
                // Id default avatar is DefaultAvatar.Initial then drawDefaultImage
                // will draw the initials and background,
                // otherwise we should tyr to draw the avatarBackgroundColor
                if (default !is DefaultAvatar.Initial)
                    drawBackgroundColor(canvas, avatarBackgroundColor)

                drawDefaultImage(canvas, default)
            }
        }
        super.draw(canvas)
    }

    private fun drawInitials(canvas: Canvas, name: CharSequence) {
        textPaint.textSize = if (textSize > 0) textSize.toFloat() else width * 0.38f
        textPaint.color = Color.WHITE

        val staticLayout = getStaticLayout(name)

        val xPos = (width - staticLayout.width) / 2f
        canvas.save()
        canvas.translate(xPos, (height - staticLayout.height) / 2f)
        staticLayout.draw(canvas)
        canvas.restore()
    }

    private fun drawBackgroundColor(canvas: Canvas, @ColorInt color: Int) {
        if (color == 0) return
        canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), (width / 2).toFloat(), backgroundPaint.apply {
            this.color = color
        })
    }

    private fun getInitials(title: CharSequence): CharSequence {
        if (title.isBlank()) return ""
        val strings = title.trim().split(" ").filter { it.isNotBlank() }
        if (strings.isEmpty()) return ""
        val data = if (isInEditMode)
            Pair(strings[0].take(1), true) else strings[0].getFirstCharIsEmoji()
        val firstChar = data.first
        val isEmoji = data.second
        if (isEmoji)
            return if (isInEditMode)
                firstChar else firstChar.processEmojiCompat() ?: title.take(1)

        val text = if (strings.size > 1) {
            val secondChar = strings[1].getFirstCharIsEmoji().first
            "${firstChar}${secondChar}".uppercase()
        } else firstChar.toString().uppercase()

        return if (isInEditMode) text else text.processEmojiCompat() ?: title.take(1)
    }

    private fun getAvatarRandomColor(initials: CharSequence): Int {
        val colors = if (isInEditMode)
            listOf(1) else SceytChatUIKit.config.defaultAvatarBackgroundColors.getColors(context)
        return colors[abs(initials.hashCode()) % colors.size]
    }

    @Suppress("DEPRECATION")
    private fun getStaticLayout(title: CharSequence): StaticLayout {
        val width = Layout.getDesiredWidth(title, textPaint).roundUp()
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            StaticLayout.Builder.obtain(title, 0, title.length, textPaint, width)
                .setAlignment(Layout.Alignment.ALIGN_NORMAL)
                .setLineSpacing(0f, 1f)
                .setIncludePad(false).build()
        } else StaticLayout(title, textPaint, width, Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false)
    }

    private fun loadAvatarImage(oldImageUrl: String?) {
        if (!imageUrl.isNullOrBlank() && imageUrl != oldImageUrl) {
            avatarLoadCb?.invoke(true)

            Glide.with(context.applicationContext)
                .load(imageUrl)
                .override(width)
                .transition(DrawableTransitionOptions.withCrossFade(100))
                .error(R.drawable.sceyt_bg_circle_gray)
                .placeholder(context.getCompatDrawable(R.drawable.sceyt_bg_circle_gray))
                .listener(com.sceyt.chatuikit.extensions.glideRequestListener {
                    avatarLoadCb?.invoke(false)
                })
                .circleCrop()
                .into(this)
        }
    }

    private fun drawDefaultImage(canvas: Canvas, avatar: DefaultAvatar) {
        when (avatar) {
            is DefaultAvatar.FromBitmap -> {
                drawCircleBitmap(avatar.bitmap, canvas)
            }

            is DefaultAvatar.FromDrawable -> {
                drawCircleDrawable(avatar.drawable, canvas)
            }

            is DefaultAvatar.FromDrawableRes -> {
                val drawable = context.getCompatDrawable(avatar.id) ?: return
                drawCircleDrawable(drawable, canvas)
            }

            is DefaultAvatar.Initial -> {
                val color = if (avatarBackgroundColor == 0)
                    getAvatarRandomColor(avatar.initial) else avatarBackgroundColor
                drawBackgroundColor(canvas, color)
                drawInitials(canvas, avatar.initial)
            }
        }
    }

    private fun drawCircleDrawable(drawable: Drawable, canvas: Canvas) {
        // Get the drawable's width and height
        val drawableWidth = drawable.intrinsicWidth
        val drawableHeight = drawable.intrinsicHeight

        // Find the center and radius for the circle
        val radius = minOf(width, height) / 2f
        val cx = width / 2f
        val cy = height / 2f

        // Create a path for the circular shape
        val path = Path()
        path.addCircle(cx, cy, radius, Path.Direction.CW)

        // Clip the canvas to the circle path
        canvas.save()
        canvas.clipPath(path)

        // Scale and position the drawable within the circle
        val matrix = Matrix()
        matrix.setScale(width / drawableWidth.toFloat(), height / drawableHeight.toFloat())
        // matrix.postTranslate(cx - drawableWidth / 2f, cy - drawableHeight / 2f)
        canvas.concat(matrix)

        // Set the bounds and draw the drawable on the canvas
        drawable.setBounds(0, 0, drawableWidth, drawableHeight)
        drawable.draw(canvas)

        // Restore the canvas to remove the clipping
        canvas.restore()
    }

    private fun drawCircleBitmap(bitmap: Bitmap, canvas: Canvas) {
        updateShader(bitmap)
        val circleCenter = (width) / 2f
        canvas.drawCircle(circleCenter, circleCenter, circleCenter, imagePaint)
    }

    private fun updateShader(bitmap: Bitmap) {
        // Create Shader
        val shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
        // Center Image in Shader
        val matrix = Matrix()
        matrix.setScale(width / bitmap.width.toFloat(), width / bitmap.height.toFloat())
        shader.setLocalMatrix(matrix)
        // Set Shader in Paint
        imagePaint.shader = shader
    }

    fun setImageUrl(url: String?) {
        val oldImageUrl = imageUrl
        imageUrl = url
        invalidate()
        loadAvatarImage(oldImageUrl)
    }

    fun setDefaultAvatar(avatar: DefaultAvatar) {
        defaultAvatar = avatar
        invalidate()
    }

    fun setAvatarImageLoadListener(cb: (Boolean) -> Unit) {
        avatarLoadCb = cb
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val measuredWidth = measuredWidth
        setMeasuredDimension(measuredWidth, measuredWidth)
    }

    sealed class DefaultAvatar {
        data class FromBitmap(val bitmap: Bitmap) : DefaultAvatar()
        data class FromDrawable(val drawable: Drawable) : DefaultAvatar()
        data class FromDrawableRes(@DrawableRes val id: Int) : DefaultAvatar()
        data class Initial(
                val initial: CharSequence
        ) : DefaultAvatar()
    }

    inner class StyleBuilder {
        private var name: CharSequence? = [email protected]
        private var imageUrl: String? = [email protected]
        private var textSize = [email protected]

        @ColorInt
        private var avatarBackgroundColor: Int = [email protected]
        private var defaultAvatar: DefaultAvatar? = [email protected]

        fun setName(name: CharSequence) = apply {
            this.name = name
        }

        fun setImageUrl(url: String?) = apply {
            this.imageUrl = url
        }

        fun setTextSize(size: Int) = apply {
            this.textSize = size
        }

        fun setAvatarBackgroundColor(@ColorInt color: Int) = apply {
            avatarBackgroundColor = color
        }

        fun setAvatarBackgroundColorRes(@ColorRes color: Int) = apply {
            avatarBackgroundColor = if (color != 0)
                context.getCompatColor(color) else 0
        }

        fun setDefaultAvatar(avatar: DefaultAvatar) = apply {
            defaultAvatar = avatar
        }

        fun setDefaultAvatar(@DrawableRes id: Int) = apply {
            defaultAvatar = id.toDefaultAvatar()
        }

        fun setDefaultAvatar(drawable: Drawable?) = apply {
            if (drawable == null) return@apply
            defaultAvatar = DefaultAvatar.FromDrawable(drawable)
        }

        fun setDefaultAvatar(bitmap: Bitmap) = apply {
            defaultAvatar = DefaultAvatar.FromBitmap(bitmap)
        }

        fun build() {
            val oldImageUrl = [email protected]
            [email protected] = name
            [email protected] = imageUrl
            [email protected] = textSize
            [email protected] = avatarBackgroundColor
            [email protected] = defaultAvatar

            invalidate()
            loadAvatarImage(oldImageUrl)
        }
    }

    fun styleBuilder() = StyleBuilder()
}

fun @receiver:DrawableRes Int.toDefaultAvatar(): DefaultAvatar {
    return DefaultAvatar.FromDrawableRes(this)
}






© 2015 - 2024 Weber Informatics LLC | Privacy Policy