Skip to content

Instantly share code, notes, and snippets.

@harshvishu
Last active November 8, 2019 07:38
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save harshvishu/3acf963c9f35c1a0830cdba54659d49c to your computer and use it in GitHub Desktop.
Save harshvishu/3acf963c9f35c1a0830cdba54659d49c to your computer and use it in GitHub Desktop.
CircleImageView Kotlin
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleImageView">
<attr name="civ_border_width" format="dimension" />
<attr name="civ_border_color" format="color" />
<attr name="civ_border_overlay" format="boolean" />
<attr name="civ_fill_color" format="color" />
<attr name="civ_circle_background_color" format="color" />
</declare-styleable>
</resources>
package com.example.android
import android.content.Context
import android.graphics.*
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.support.annotation.ColorInt
import android.support.annotation.ColorRes
import android.support.annotation.DrawableRes
import android.support.annotation.RequiresApi
import android.util.AttributeSet
import android.view.View
import android.view.ViewOutlineProvider
import android.widget.ImageView
import com.example.android.R
class CircleImageView : android.support.v7.widget.AppCompatImageView {
private val mDrawableRect = RectF()
private val mBorderRect = RectF()
private val mShaderMatrix = Matrix()
private val mBitmapPaint = Paint()
private val mBorderPaint = Paint()
private val mCircleBackgroundPaint = Paint()
private var mBorderColor = DEFAULT_BORDER_COLOR
private var mBorderWidth = DEFAULT_BORDER_WIDTH
private var mCircleBackgroundColor = DEFAULT_CIRCLE_BACKGROUND_COLOR
private var mBitmap: Bitmap? = null
private var mBitmapShader: BitmapShader? = null
private var mBitmapWidth: Int = 0
private var mBitmapHeight: Int = 0
private var mDrawableRadius: Float = 0.toFloat()
private var mBorderRadius: Float = 0.toFloat()
private var mColorFilter: ColorFilter? = null
private var mReady: Boolean = false
private var mSetupPending: Boolean = false
private var mBorderOverlay: Boolean = false
var isDisableCircularTransformation: Boolean = false
set(disableCircularTransformation) {
if (isDisableCircularTransformation == disableCircularTransformation) {
return
}
field = disableCircularTransformation
initializeBitmap()
}
var borderColor: Int
get() = mBorderColor
set(@ColorInt borderColor) {
if (borderColor == mBorderColor) {
return
}
mBorderColor = borderColor
mBorderPaint.color = mBorderColor
invalidate()
}
var circleBackgroundColor: Int
get() = mCircleBackgroundColor
set(@ColorInt circleBackgroundColor) {
if (circleBackgroundColor == mCircleBackgroundColor) {
return
}
mCircleBackgroundColor = circleBackgroundColor
mCircleBackgroundPaint.color = circleBackgroundColor
invalidate()
}
/**
* Return the color drawn behind the circle-shaped drawable.
*
* @return The color drawn behind the drawable
*/
/**
* Set a color to be drawn behind the circle-shaped drawable. Note that
* this has no effect if the drawable is opaque or no drawable is set.
*/
var fillColor: Int
@Deprecated("Use {@link #getCircleBackgroundColor()} instead.")
get() = circleBackgroundColor
@Deprecated("Use {@link #setCircleBackgroundColor(int)} instead.")
set(@ColorInt fillColor) {
circleBackgroundColor = fillColor
}
var borderWidth: Int
get() = mBorderWidth
set(borderWidth) {
if (borderWidth == mBorderWidth) {
return
}
mBorderWidth = borderWidth
setup()
}
var isBorderOverlay: Boolean
get() = mBorderOverlay
set(borderOverlay) {
if (borderOverlay == mBorderOverlay) {
return
}
mBorderOverlay = borderOverlay
setup()
}
constructor(context: Context) : super(context) {
init()
}
@JvmOverloads constructor(context: Context, attrs: AttributeSet, defStyle: Int = 0) : super(context, attrs, defStyle) {
val a = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView, defStyle, 0)
mBorderWidth = a.getDimensionPixelSize(R.styleable.CircleImageView_civ_border_width, DEFAULT_BORDER_WIDTH)
mBorderColor = a.getColor(R.styleable.CircleImageView_civ_border_color, DEFAULT_BORDER_COLOR)
mBorderOverlay = a.getBoolean(R.styleable.CircleImageView_civ_border_overlay, DEFAULT_BORDER_OVERLAY)
// Look for deprecated civ_fill_color if civ_circle_background_color is not set
if (a.hasValue(R.styleable.CircleImageView_civ_circle_background_color)) {
mCircleBackgroundColor = a.getColor(R.styleable.CircleImageView_civ_circle_background_color,
DEFAULT_CIRCLE_BACKGROUND_COLOR)
} else if (a.hasValue(R.styleable.CircleImageView_civ_fill_color)) {
mCircleBackgroundColor = a.getColor(R.styleable.CircleImageView_civ_fill_color,
DEFAULT_CIRCLE_BACKGROUND_COLOR)
}
a.recycle()
init()
}
private fun init() {
super.setScaleType(SCALE_TYPE)
mReady = true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
outlineProvider = OutlineProvider()
}
if (mSetupPending) {
setup()
mSetupPending = false
}
}
override fun getScaleType(): ImageView.ScaleType {
return SCALE_TYPE
}
override fun setScaleType(scaleType: ImageView.ScaleType) {
if (scaleType != SCALE_TYPE) {
throw IllegalArgumentException(String.format("ScaleType %s not supported.", scaleType))
}
}
override fun setAdjustViewBounds(adjustViewBounds: Boolean) {
if (adjustViewBounds) {
throw IllegalArgumentException("adjustViewBounds not supported.")
}
}
override fun onDraw(canvas: Canvas) {
if (isDisableCircularTransformation) {
super.onDraw(canvas)
return
}
if (mBitmap == null) {
return
}
if (mCircleBackgroundColor != Color.TRANSPARENT) {
canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mCircleBackgroundPaint)
}
canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mBitmapPaint)
if (mBorderWidth > 0) {
canvas.drawCircle(mBorderRect.centerX(), mBorderRect.centerY(), mBorderRadius, mBorderPaint)
}
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
setup()
}
override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
super.setPadding(left, top, right, bottom)
setup()
}
override fun setPaddingRelative(start: Int, top: Int, end: Int, bottom: Int) {
super.setPaddingRelative(start, top, end, bottom)
setup()
}
@Suppress("MemberVisibilityCanBePrivate")
@Deprecated("Use {@link #setBorderColor(int)} instead", ReplaceWith("borderColor = context.resources.getColor(borderColorRes)"))
fun setBorderColorResource(@ColorRes borderColorRes: Int) {
borderColor = context.resources.getColor(borderColorRes)
}
@Suppress("MemberVisibilityCanBePrivate")
fun setCircleBackgroundColorResource(@ColorRes circleBackgroundRes: Int) {
circleBackgroundColor = context.resources.getColor(circleBackgroundRes)
}
/**
* Set a color to be drawn behind the circle-shaped drawable. Note that
* this has no effect if the drawable is opaque or no drawable is set.
*
* @param fillColorRes The color resource to be resolved to a color and
* drawn behind the drawable
*/
@Deprecated("Use {@link #setCircleBackgroundColorResource(int)} instead.", ReplaceWith("setCircleBackgroundColorResource(fillColorRes)"))
fun setFillColorResource(@ColorRes fillColorRes: Int) {
setCircleBackgroundColorResource(fillColorRes)
}
override fun setImageBitmap(bm: Bitmap) {
super.setImageBitmap(bm)
initializeBitmap()
}
override fun setImageDrawable(drawable: Drawable?) {
super.setImageDrawable(drawable)
initializeBitmap()
}
override fun setImageResource(@DrawableRes resId: Int) {
super.setImageResource(resId)
initializeBitmap()
}
override fun setImageURI(uri: Uri?) {
super.setImageURI(uri)
initializeBitmap()
}
override fun setColorFilter(cf: ColorFilter) {
if (cf === mColorFilter) {
return
}
mColorFilter = cf
applyColorFilter()
invalidate()
}
override fun getColorFilter(): ColorFilter? {
return mColorFilter
}
private fun applyColorFilter() {
mBitmapPaint.colorFilter = mColorFilter
}
private fun getBitmapFromDrawable(drawable: Drawable?): Bitmap? {
if (drawable == null) {
return null
}
if (drawable is BitmapDrawable) {
return drawable.bitmap
}
try {
val bitmap: Bitmap
if (drawable is ColorDrawable) {
bitmap = Bitmap.createBitmap(COLORDRAWABLE_DIMENSION, COLORDRAWABLE_DIMENSION, BITMAP_CONFIG)
} else {
bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, BITMAP_CONFIG)
}
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
return bitmap
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
private fun initializeBitmap() {
mBitmap = if (isDisableCircularTransformation) {
null
} else {
getBitmapFromDrawable(drawable)
}
setup()
}
private fun setup() {
if (!mReady) {
mSetupPending = true
return
}
if (width == 0 && height == 0) {
return
}
if (mBitmap == null) {
invalidate()
return
}
mBitmapShader = BitmapShader(mBitmap!!, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
mBitmapPaint.isAntiAlias = true
mBitmapPaint.shader = mBitmapShader
mBorderPaint.style = Paint.Style.STROKE
mBorderPaint.isAntiAlias = true
mBorderPaint.color = mBorderColor
mBorderPaint.strokeWidth = mBorderWidth.toFloat()
mCircleBackgroundPaint.style = Paint.Style.FILL
mCircleBackgroundPaint.isAntiAlias = true
mCircleBackgroundPaint.color = mCircleBackgroundColor
mBitmapHeight = mBitmap!!.height
mBitmapWidth = mBitmap!!.width
mBorderRect.set(calculateBounds())
mBorderRadius = Math.min((mBorderRect.height() - mBorderWidth) / 2.0f, (mBorderRect.width() - mBorderWidth) / 2.0f)
mDrawableRect.set(mBorderRect)
if (!mBorderOverlay && mBorderWidth > 0) {
mDrawableRect.inset(mBorderWidth - 1.0f, mBorderWidth - 1.0f)
}
mDrawableRadius = Math.min(mDrawableRect.height() / 2.0f, mDrawableRect.width() / 2.0f)
applyColorFilter()
updateShaderMatrix()
invalidate()
}
private fun calculateBounds(): RectF {
val availableWidth = width - paddingLeft - paddingRight
val availableHeight = height - paddingTop - paddingBottom
val sideLength = Math.min(availableWidth, availableHeight)
val left = paddingLeft + (availableWidth - sideLength) / 2f
val top = paddingTop + (availableHeight - sideLength) / 2f
return RectF(left, top, left + sideLength, top + sideLength)
}
private fun updateShaderMatrix() {
val scale: Float
var dx = 0f
var dy = 0f
mShaderMatrix.set(null)
if (mBitmapWidth * mDrawableRect.height() > mDrawableRect.width() * mBitmapHeight) {
scale = mDrawableRect.height() / mBitmapHeight.toFloat()
dx = (mDrawableRect.width() - mBitmapWidth * scale) * 0.5f
} else {
scale = mDrawableRect.width() / mBitmapWidth.toFloat()
dy = (mDrawableRect.height() - mBitmapHeight * scale) * 0.5f
}
mShaderMatrix.setScale(scale, scale)
mShaderMatrix.postTranslate((dx + 0.5f).toInt() + mDrawableRect.left, (dy + 0.5f).toInt() + mDrawableRect.top)
mBitmapShader!!.setLocalMatrix(mShaderMatrix)
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private inner class OutlineProvider : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
val bounds = Rect()
mBorderRect.roundOut(bounds)
outline.setRoundRect(bounds, bounds.width() / 2.0f)
}
}
companion object {
private val SCALE_TYPE = ImageView.ScaleType.CENTER_CROP
private val BITMAP_CONFIG = Bitmap.Config.ARGB_8888
private const val COLORDRAWABLE_DIMENSION = 2
private const val DEFAULT_BORDER_WIDTH = 0
private const val DEFAULT_BORDER_COLOR = Color.BLACK
private const val DEFAULT_CIRCLE_BACKGROUND_COLOR = Color.TRANSPARENT
private const val DEFAULT_BORDER_OVERLAY = false
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment