Introduction:- Reusable component always increase the speed of development. So, i'm going to explain a component that displays image in the circular shape that's written in Kotlin.
For display user profile pic we always need a circular image view. Let's dive in the code snippet.
First of all, you need to add it in your layout file: activity_main
<LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"android:layout_width="match_parent"android:layout_height="match_parent"android:clickable="true"android:focusable="true"android:orientation="vertical"><com.demo.CircularImageViewandroid:id="@+id/fragment_tv_user_pic"android:layout_width="60dp"android:layout_height="60dp"android:layout_gravity="center_horizontal"android:layout_marginTop="22dp"app:civ_border_color="@color/colorWhite"app:civ_border_width="2dp"app:matProg_barColor="@color/colorAccent"app:matProg_barWidth="2dp"app:matProg_circleRadius="60dp"app:matProg_progressIndeterminate="true"/></LinearLayout>
Example-1:
If you want display short form of the name in this component. Use the following code snippet.
val civUserPic = findViewById<CircularImageView>(R.id.fragment_tv_user_pic)civUserPic.createTextPaint("#2CB044", 60, true)civUserPic.setText("S.K")
Output:
Example-2:
If you want to display image from res folder in this component. Use the following code snippet.
civUserPic.setImageDrawable(ContextCompat.getDrawable(this,R.drawable.images))civUserPic.setBorderColor(R.color.colorPrimary)
Output:
Example-3:
If you want to display image from the server and you have full path of image use following steps. You can use any image loader lib like here i'm using Picasso
civUserPic.loaderState(true)civUserPic.invalidate()Picasso.with(this).load("image_url").into(civUserPic)
Output:
So, time to view component.
values/attrs:-
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ProgressWheel">
<attr name="matProg_progressIndeterminate" format="boolean"/>
<attr name="matProg_barColor" format="color"/>
<attr name="matProg_rimColor" format="color"/>
<attr name="matProg_rimWidth" format="dimension"/>
<attr name="matProg_spinSpeed" format="float"/>
<attr name="matProg_barSpinCycleTime" format="integer"/>
<attr name="matProg_circleRadius" format="dimension"/>
<attr name="matProg_fillRadius" format="boolean"/>
<attr name="matProg_barWidth" format="dimension"/>
<attr name="matProg_linearProgress" format="boolean"/>
<attr name="matProg_visible" format="boolean"/>
</declare-styleable>
<declare-styleable name="CircularImageView">
<attr name="civ_border_width" format="dimension" />
<attr name="civ_border_color" format="color" />
<attr name="civ_border_overlay" format="boolean" />
<attr name="civ_fad_overlay" format="boolean" />
<attr name="civ_fill_color" format="color" />
</declare-styleable>
</resources>
CircularImageView.Kt:-
class CircularImageView : AppCompatImageView {private val mDrawableRect = RectF()private val mBorderRect = RectF()private val mShaderMatrix = Matrix()private val mBitmapPaint = Paint()private val mBorderPaint = Paint()private val mFillPaint = Paint()private var mBorderColor = DEFAULT_BORDER_COLORprivate var mBorderWidth = DEFAULT_BORDER_WIDTHprivate var mFillColor = DEFAULT_FILL_COLORprivate var mBitmap: Bitmap? = nullprivate var mBitmapShader: BitmapShader? = nullprivate var mBitmapWidth: Int = 0private var mBitmapHeight: Int = 0private var mDrawableRadius: Float = 0.toFloat()private var mBorderRadius: Float = 0.toFloat()private var mColorFilter: ColorFilter? = nullprivate var mReady: Boolean = falseprivate var mSetupPending: Boolean = falseprivate var mBorderOverlay: Boolean = falseprivate val barLength = 16//Sizes (with defaults in DP)private var circleRadius = 28private var barWidth = 4private var rimWidth = 4private var fillRadius = falseprivate var timeStartGrowing = 0.0private var barSpinCycleTime = 460.0private var barExtraLength = 0fprivate var barGrowingFromFront = trueprivate var pausedTimeWithoutGrowing: Long = 0//Colors (with defaults)private var barColor = -0x56000000private var rimColor = 0x00FFFFFF//Paintsprivate val barPaint = Paint()private val rimPaint = Paint()//Rectanglesprivate var circleBounds = RectF()//Animation//The amount of degrees per secondprivate var spinSpeed = 230.0f// The last time the spinner was animatedprivate var lastTimeAnimated: Long = 0private var linearProgress: Boolean = falsevar isProgressVisible: Boolean = falseprivate var mProgress = 0.0fprivate var mTargetProgress = 0.0fprivate var isSpinning = falseprivate var callback: ProgressCallback? = nullprivate var mFadEnable: Boolean=falseprivate var fadPaint: Paint? = nullprivate var fad: Int = 0private var textPaint: Paint? = nullprivate var text: String? = nullprivate var isBorderRequired: Boolean = falseconstructor(context: Context) : super(context) {init()}constructor(context: Context, attrs: AttributeSet) : this(context, attrs, 0) {parseAttributes(context.obtainStyledAttributes(attrs,R.styleable.ProgressWheel))}constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) {val a = context.obtainStyledAttributes(attrs, R.styleable.CircularImageView, defStyle, 0)mBorderWidth = a.getDimensionPixelSize(R.styleable.CircularImageView_civ_border_width, DEFAULT_BORDER_WIDTH)mBorderColor = a.getColor(R.styleable.CircularImageView_civ_border_color, DEFAULT_BORDER_COLOR)mBorderOverlay = a.getBoolean(R.styleable.CircularImageView_civ_border_overlay, DEFAULT_BORDER_OVERLAY)mFadEnable = a.getBoolean(R.styleable.CircularImageView_civ_fad_overlay, DEFAULT_BORDER_OVERLAY)mFillColor = a.getColor(R.styleable.CircularImageView_civ_fill_color, DEFAULT_FILL_COLOR)a.recycle()init()}private fun init() {super.setScaleType(SCALE_TYPE)mReady = trueif (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 (mBitmap != null) {//draw imageif (mFillColor != Color.TRANSPARENT) {canvas.drawCircle(width / 2.0f, height / 2.0f, mDrawableRadius, mFillPaint)}canvas.drawCircle(width / 2.0f, height / 2.0f, mDrawableRadius, mBitmapPaint)if (mBorderWidth != 0) {canvas.drawCircle(width / 2.0f, height / 2.0f, mBorderRadius, mBorderPaint)}isProgressVisible = false} else if (text != null && !text!!.isEmpty() && textPaint != null) {if (isBorderRequired && mBorderWidth != 0) {canvas.drawCircle(width / 2.0f, height / 2.0f, mBorderRadius, mBorderPaint)}canvas.drawText(text!!, width / 2.0f - textPaint!!.measureText(text) / 2, height / 2.0f - (textPaint!!.descent() + textPaint!!.ascent()) / 2, textPaint!!)}if (mFadEnable && fadPaint != null) {fadPaint!!.alpha = fadcanvas.drawCircle(width / 2.0f, height / 2.0f, mDrawableRadius, fadPaint!!)}//draw progress barif (isProgressVisible) {drawProgressBar(canvas)}}fun loaderState(state: Boolean) {isProgressVisible = state}private fun drawProgressBar(canvas: Canvas) {canvas.drawArc(circleBounds, 360f, 360f, false, rimPaint)var mustInvalidate = falseif (isSpinning) {//Draw the spinning barmustInvalidate = trueval deltaTime = SystemClock.uptimeMillis() - lastTimeAnimatedval deltaNormalized = deltaTime * spinSpeed / 1000.0fupdateBarLength(deltaTime)mProgress += deltaNormalizedif (mProgress > 360) {mProgress -= 360f// A full turn has been completed// we run the callback with -1 in case we want to// do something, like changing the colorrunCallback(-1.0f)}lastTimeAnimated = SystemClock.uptimeMillis()var from = mProgress - 90var length = barLength + barExtraLengthif (isInEditMode) {from = 0flength = 135f}canvas.drawArc(circleBounds, from, length, false,barPaint)} else {val oldProgress = mProgressif (mProgress != mTargetProgress) {//We smoothly increase the progress barmustInvalidate = trueval deltaTime = (SystemClock.uptimeMillis() - lastTimeAnimated).toFloat() / 1000val deltaNormalized = deltaTime * spinSpeedmProgress = Math.min(mProgress + deltaNormalized, mTargetProgress)lastTimeAnimated = SystemClock.uptimeMillis()}if (oldProgress != mProgress) {runCallback()}var offset = 0.0fvar progress = mProgressif (!linearProgress) {val factor = 2.0foffset = (1.0f - Math.pow((1.0f - mProgress / 360.0f).toDouble(), (2.0f * factor).toDouble())).toFloat() * 360.0fprogress = (1.0f - Math.pow((1.0f - mProgress / 360.0f).toDouble(), factor.toDouble())).toFloat() * 360.0f}if (isInEditMode) {progress = 360f}canvas.drawArc(circleBounds, offset - 90, progress, false, barPaint)}if (mustInvalidate) {invalidate()}}override fun onSizeChanged(w: Int, h: Int, oldW: Int, oldH: Int) {super.onSizeChanged(w, h, oldW, oldH)setUpBounds(w, h)setupPaints()setup()}override fun setImageBitmap(bm: Bitmap) {super.setImageBitmap(bm)mBitmap = bmsetup()}override fun setImageDrawable(drawable: Drawable?) {super.setImageDrawable(drawable)mBitmap = getBitmapFromDrawable(drawable)setup()}override fun setImageResource(@DrawableRes resId: Int) {super.setImageResource(resId)mBitmap = getBitmapFromDrawable(drawable)setup()}override fun setImageURI(uri: Uri?) {super.setImageURI(uri)mBitmap = if (uri != null) getBitmapFromDrawable(drawable) else nullsetup()}override fun setColorFilter(cf: ColorFilter) {if (cf === mColorFilter) {return}mColorFilter = cfmBitmapPaint.colorFilter = mColorFilterinvalidate()}private fun getBitmapFromDrawable(drawable: Drawable?): Bitmap? {if (drawable == null) {return null}if (drawable is BitmapDrawable) {return drawable.bitmap}try {val bitmap: Bitmapif (drawable is ColorDrawable) {bitmap = Bitmap.createBitmap(COLOR_DRAWABLE_DIMENSION, COLOR_DRAWABLE_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) {Log.d("error", e.toString())return null}}private fun setup() {if (!mReady) {mSetupPending = truereturn}if (width == 0 && height == 0) {return}mBorderPaint.style = Paint.Style.STROKEmBorderPaint.isAntiAlias = truemBorderPaint.color = mBorderColormBorderPaint.strokeWidth = mBorderWidth.toFloat()mFillPaint.style = Paint.Style.FILLmFillPaint.isAntiAlias = truemFillPaint.color = mFillColormBorderRect.set(0f, 0f, width.toFloat(), height.toFloat())mBorderRadius = Math.min((mBorderRect.height() - mBorderWidth) / 2.0f, (mBorderRect.width() - mBorderWidth) / 2.0f)mDrawableRect.set(mBorderRect)if (!mBorderOverlay) {mDrawableRect.inset(mBorderWidth.toFloat(), mBorderWidth.toFloat())}mDrawableRadius = Math.min(mDrawableRect.height() / 2.0f, mDrawableRect.width() / 2.0f)if (mBitmap == null) {invalidate()return}mBitmapShader = BitmapShader(mBitmap!!, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)mBitmapPaint.isAntiAlias = truemBitmapPaint.shader = mBitmapShadermBitmapHeight = mBitmap!!.heightmBitmapWidth = mBitmap!!.widthupdateShaderMatrix()invalidate()}fun setBorderColor(color: Int) {mBorderColor = colormBorderPaint.color = mBorderColorinvalidate()}private fun updateShaderMatrix() {val scale: Floatvar dx = 0fvar dy = 0fmShaderMatrix.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)}fun createFadPaint(color: String) {fadPaint = Paint()fadPaint!!.style = Paint.Style.FILLfadPaint!!.isAntiAlias = truefadPaint!!.color = Color.parseColor(color)}fun createTextPaint(color: String, fontSize: Int, isBorderRequired: Boolean) {textPaint = Paint()textPaint!!.style = Paint.Style.FILLtextPaint!!.isAntiAlias = truetextPaint!!.color = Color.parseColor(color)textPaint!!.textSize = fontSize.toFloat()this.isBorderRequired = isBorderRequired}fun setText(text: String) {this.text = textmBitmap = nullLog.i(">>>", "" + text)invalidate()}fun setFad(fad: Int) {this.fad = fadLog.i(">>>", "" + fad)invalidate()}interface ProgressCallback {fun onProgressUpdate(progress: Float)}private fun parseAttributes(a: TypedArray) {// We transform the default values from DIP to pixelsval metrics = context.resources.displayMetricsbarWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, barWidth.toFloat(), metrics).toInt()rimWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, rimWidth.toFloat(), metrics).toInt()circleRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, circleRadius.toFloat(), metrics).toInt()circleRadius = a.getDimension(R.styleable.ProgressWheel_matProg_circleRadius, circleRadius.toFloat()).toInt()fillRadius = a.getBoolean(R.styleable.ProgressWheel_matProg_fillRadius, false)barWidth = a.getDimension(R.styleable.ProgressWheel_matProg_barWidth, barWidth.toFloat()).toInt()rimWidth = a.getDimension(R.styleable.ProgressWheel_matProg_rimWidth, rimWidth.toFloat()).toInt()val baseSpinSpeed = a.getFloat(R.styleable.ProgressWheel_matProg_spinSpeed, spinSpeed / 360.0f)spinSpeed = baseSpinSpeed * 360barSpinCycleTime = a.getInt(R.styleable.ProgressWheel_matProg_barSpinCycleTime, barSpinCycleTime.toInt()).toDouble()barColor = a.getColor(R.styleable.ProgressWheel_matProg_barColor, barColor)rimColor = a.getColor(R.styleable.ProgressWheel_matProg_rimColor, rimColor)isProgressVisible = a.getBoolean(R.styleable.ProgressWheel_matProg_visible, false)linearProgress = a.getBoolean(R.styleable.ProgressWheel_matProg_linearProgress, false)if (a.getBoolean(R.styleable.ProgressWheel_matProg_progressIndeterminate, false)) {spin()}// Recyclea.recycle()}private fun spin() {lastTimeAnimated = SystemClock.uptimeMillis()isSpinning = trueinvalidate()}override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {super.onMeasure(widthMeasureSpec, heightMeasureSpec)val viewWidth = circleRadius + this.paddingLeft + this.paddingRightval viewHeight = circleRadius + this.paddingTop + this.paddingBottomval widthMode = View.MeasureSpec.getMode(widthMeasureSpec)val widthSize = View.MeasureSpec.getSize(widthMeasureSpec)val heightMode = View.MeasureSpec.getMode(heightMeasureSpec)val heightSize = View.MeasureSpec.getSize(heightMeasureSpec)val width: Intval height: Int//Measure Widthif (widthMode == View.MeasureSpec.EXACTLY) {//Must be this sizewidth = widthSize} else if (widthMode == View.MeasureSpec.AT_MOST) {//Can't be bigger than...width = Math.min(viewWidth, widthSize)} else {//Be whatever you wantwidth = viewWidth}//Measure Heightif (heightMode == View.MeasureSpec.EXACTLY || widthMode == View.MeasureSpec.EXACTLY) {//Must be this sizeheight = heightSize} else if (heightMode == View.MeasureSpec.AT_MOST) {//Can't be bigger than...height = Math.min(viewHeight, heightSize)} else {//Be whatever you wantheight = viewHeight}setMeasuredDimension(width, height)}private fun setUpBounds(layoutWidth: Int, layoutHeight: Int) {val paddingTop = paddingTopval paddingBottom = paddingBottomval paddingLeft = paddingLeftval paddingRight = paddingRightif (!fillRadius) {// Width should equal to Height, find the min value to setup the circleval minValue = Math.min(layoutWidth - paddingLeft - paddingRight,layoutHeight - paddingBottom - paddingTop)val circleDiameter = Math.min(minValue, circleRadius * 2 - barWidth * 2)// Calc the Offset if needed for centering the wheel in the available spaceval xOffset = (layoutWidth - paddingLeft - paddingRight - circleDiameter) / 2 + paddingLeftval yOffset = (layoutHeight - paddingTop - paddingBottom - circleDiameter) / 2 + paddingTopcircleBounds = RectF((xOffset + barWidth).toFloat(),(yOffset + barWidth).toFloat(),(xOffset + circleDiameter - barWidth).toFloat(),(yOffset + circleDiameter - barWidth).toFloat())} else {circleBounds = RectF((paddingLeft + barWidth).toFloat(),(paddingTop + barWidth).toFloat(),(layoutWidth - paddingRight - barWidth).toFloat(),(layoutHeight - paddingBottom - barWidth).toFloat())}}private fun setupPaints() {barPaint.color = barColorbarPaint.isAntiAlias = truebarPaint.style = Paint.Style.STROKEbarPaint.strokeWidth = barWidth.toFloat()rimPaint.color = rimColorrimPaint.isAntiAlias = truerimPaint.style = Paint.Style.STROKErimPaint.strokeWidth = rimWidth.toFloat()}fun setCallback(progressCallback: ProgressCallback) {callback = progressCallbackif (!isSpinning) {runCallback()}}private fun runCallback() {if (callback != null) {val normalizedProgress = Math.round(mProgress * 100 / 360.0f).toFloat() / 100callback!!.onProgressUpdate(normalizedProgress)}}private fun runCallback(value: Float) {if (callback != null) {callback!!.onProgressUpdate(value)}}override fun onVisibilityChanged(changedView: View, visibility: Int) {super.onVisibilityChanged(changedView, visibility)if (visibility == View.VISIBLE) {lastTimeAnimated = SystemClock.uptimeMillis()}}private fun updateBarLength(deltaTimeInMilliSeconds: Long) {if (pausedTimeWithoutGrowing >= 200) {timeStartGrowing += deltaTimeInMilliSeconds.toDouble()if (timeStartGrowing > barSpinCycleTime) {// We completed a size change cycle// (growing or shrinking)timeStartGrowing -= barSpinCycleTimepausedTimeWithoutGrowing = 0barGrowingFromFront = !barGrowingFromFront}val distance = Math.cos((timeStartGrowing / barSpinCycleTime + 1) * Math.PI).toFloat() / 2 + 0.5fval destLength = 270.toFloat() - barLengthif (barGrowingFromFront) {barExtraLength = distance * destLength} else {val newLength = destLength * (1 - distance)mProgress += barExtraLength - newLengthbarExtraLength = newLength}} else {pausedTimeWithoutGrowing += deltaTimeInMilliSeconds}}fun stopSpinning() {isSpinning = falsemProgress = 0.0fmTargetProgress = 0.0finvalidate()}public override fun onSaveInstanceState(): Parcelable? {val superState = super.onSaveInstanceState()val ss = WheelSavedState(superState)// We save everything that can be changed at runtimess.mProgress = this.mProgressss.mTargetProgress = this.mTargetProgressss.isSpinning = this.isSpinningss.spinSpeed = this.spinSpeedss.barWidth = this.barWidthss.barColor = this.barColorss.rimWidth = this.rimWidthss.rimColor = this.rimColorss.circleRadius = this.circleRadiusss.linearProgress = this.linearProgressss.fillRadius = this.fillRadiusreturn ss}public override fun onRestoreInstanceState(state: Parcelable) {if (state !is WheelSavedState) {super.onRestoreInstanceState(state)return}super.onRestoreInstanceState(state.superState)this.mProgress = state.mProgressthis.mTargetProgress = state.mTargetProgressthis.isSpinning = state.isSpinningthis.spinSpeed = state.spinSpeedthis.barWidth = state.barWidththis.barColor = state.barColorthis.rimWidth = state.rimWidththis.rimColor = state.rimColorthis.circleRadius = state.circleRadiusthis.linearProgress = state.linearProgressthis.fillRadius = state.fillRadiusthis.lastTimeAnimated = SystemClock.uptimeMillis()}internal class WheelSavedState : View.BaseSavedState {var mProgress: Float = 0.toFloat()var mTargetProgress: Float = 0.toFloat()var isSpinning: Boolean = falsevar spinSpeed: Float = 0.toFloat()var barWidth: Int = 0var barColor: Int = 0var rimWidth: Int = 0var rimColor: Int = 0var circleRadius: Int = 0var linearProgress: Boolean = falsevar fillRadius: Boolean = falseconstructor(superState: Parcelable) : super(superState) {}private constructor(`in`: Parcel) : super(`in`) {this.mProgress = `in`.readFloat()this.mTargetProgress = `in`.readFloat()this.isSpinning = `in`.readByte().toInt() != 0this.spinSpeed = `in`.readFloat()this.barWidth = `in`.readInt()this.barColor = `in`.readInt()this.rimWidth = `in`.readInt()this.rimColor = `in`.readInt()this.circleRadius = `in`.readInt()this.linearProgress = `in`.readByte().toInt() != 0this.fillRadius = `in`.readByte().toInt() != 0}override fun writeToParcel(out: Parcel, flags: Int) {super.writeToParcel(out, flags)out.writeFloat(this.mProgress)out.writeFloat(this.mTargetProgress)out.writeByte((if (isSpinning) 1 else 0).toByte())out.writeFloat(this.spinSpeed)out.writeInt(this.barWidth)out.writeInt(this.barColor)out.writeInt(this.rimWidth)out.writeInt(this.rimColor)out.writeInt(this.circleRadius)out.writeByte((if (linearProgress) 1 else 0).toByte())out.writeByte((if (fillRadius) 1 else 0).toByte())}companion object {//required field that makes Parcelables from a Parcelval CREATOR: Parcelable.Creator<WheelSavedState> = object : Parcelable.Creator<WheelSavedState> {override fun createFromParcel(`in`: Parcel): WheelSavedState {return WheelSavedState(`in`)}override fun newArray(size: Int): Array<WheelSavedState?> {return arrayOfNulls(size)}}}}companion object {private val SCALE_TYPE = ImageView.ScaleType.CENTER_CROPprivate val BITMAP_CONFIG = Bitmap.Config.ARGB_8888private val COLOR_DRAWABLE_DIMENSION = 2private val DEFAULT_BORDER_WIDTH = 0private val DEFAULT_BORDER_COLOR = Color.BLACKprivate val DEFAULT_FILL_COLOR = Color.TRANSPARENTprivate val DEFAULT_BORDER_OVERLAY = false}}
Let me know if you facing issue to use it.