Skip to content

Instantly share code, notes, and snippets.

@hi-manshu
Created February 17, 2021 15:12
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hi-manshu/1effbcb736daa8cf1441f7f9b34d7ce5 to your computer and use it in GitHub Desktop.
Save hi-manshu/1effbcb736daa8cf1441f7f9b34d7ce5 to your computer and use it in GitHub Desktop.
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PointF
import android.util.AttributeSet
import android.util.Log
import android.view.View
class BezierView : View {
private var mainPaint: Paint? = null
private var shadowPaint: Paint? = null
private var mainPath: Path? = null
private var shadowPath: Path? = null
private lateinit var outerArray: Array<PointF>
private lateinit var innerArray: Array<PointF>
private lateinit var progressArray: Array<PointF>
private var width = 0f
private var height = 0f
private var bezierOuterWidth = 0f
private var bezierOuterHeight = 0f
private var bezierInnerWidth = 0f
private var bezierInnerHeight = 0f
private val shadowHeight = dipf(context, 35) // this height will change bottomnavigation bg height
var color = 0
set(value) {
field = value
mainPaint?.color = field
invalidate()
}
var shadowColor = 0
set(value) {
field = value
shadowPaint?.setShadowLayer(dipf(context, 2), 0f, 0f, shadowColor)
invalidate()
}
var bezierX = 0f
set(value) {
if (value == field)
return
field = value
Log.e("BezierX", field.toString());
invalidate()
}
var progress = 0f
set(value) {
if (value == field)
return
field = value
progressArray[1].x = bezierX - bezierInnerWidth / 2
progressArray[2].x = bezierX - bezierInnerWidth / 4
progressArray[3].x = bezierX - bezierInnerWidth / 4
progressArray[4].x = bezierX
progressArray[5].x = bezierX + bezierInnerWidth / 4
progressArray[6].x = bezierX + bezierInnerWidth / 4
progressArray[7].x = bezierX + bezierInnerWidth / 2
for (i in 2..6) {
if (progress <= 1f) {//convert to outer
progressArray[i].y = calculate(innerArray[i].y, outerArray[i].y)
} else {
progressArray[i].y = calculate(outerArray[i].y, innerArray[i].y)
}
}
if (field == 2f)
field = 0f
invalidate()
}
var waveHeight = 7
set(value) {
if (value == field)
return
if (value < 7) {
field = 7
invalidate()
} else {
field = value
invalidate()
}
}
@SuppressLint("NewApi")
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
initializeViews()
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
initializeViews()
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
initializeViews()
}
constructor(context: Context) : super(context) {
initializeViews()
}
private fun initializeViews() {
setWillNotDraw(false)
mainPath = Path()
shadowPath = Path()
outerArray = Array(11) { PointF() }
innerArray = Array(11) { PointF() }
progressArray = Array(11) { PointF() }
mainPaint = Paint(Paint.ANTI_ALIAS_FLAG)
mainPaint?.apply {
strokeWidth = 0f
isAntiAlias = true
style = Paint.Style.STROKE
color = this@BezierView.color
}
shadowPaint = Paint(Paint.ANTI_ALIAS_FLAG)
shadowPaint?.apply {
isAntiAlias = true
setShadowLayer(dipf(context, 4), 0f, 0f, shadowColor)
}
color = color
shadowColor = shadowColor
setLayerType(LAYER_TYPE_SOFTWARE, shadowPaint)
}
@SuppressLint("DrawAllocation")
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
width = MeasureSpec.getSize(widthMeasureSpec).toFloat()
height = MeasureSpec.getSize(heightMeasureSpec).toFloat()
bezierOuterWidth = dipf(context, 72)
bezierOuterHeight = dipf(context, 0)
bezierInnerWidth = dipf(context, 124)
bezierInnerHeight = dipf(context, 0) // this will change curve shape
val extra = shadowHeight
outerArray[0] = PointF(0f, bezierOuterHeight + extra)
outerArray[1] = PointF((bezierX - bezierOuterWidth / 2), bezierOuterHeight + extra)
outerArray[2] = PointF(bezierX - bezierOuterWidth / 4, bezierOuterHeight + extra)
outerArray[3] = PointF(bezierX - bezierOuterWidth / 4, extra)
outerArray[4] = PointF(bezierX, extra)
outerArray[5] = PointF(bezierX + bezierOuterWidth / 4, extra)
outerArray[6] = PointF(bezierX + bezierOuterWidth / 4, bezierOuterHeight + extra)
outerArray[7] = PointF(bezierX + bezierOuterWidth / 2, bezierOuterHeight + extra)
outerArray[8] = PointF(width, bezierOuterHeight + extra)
outerArray[9] = PointF(width, height)
outerArray[10] = PointF(0f, height)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
mainPath!!.reset()
shadowPath!!.reset()
if (progress == 0f) {
drawInner(canvas, true)
drawInner(canvas, false)
} else {
drawProgress(canvas, true)
drawProgress(canvas, false)
}
}
private fun drawInner(canvas: Canvas, isShadow: Boolean) {
val paint = if (isShadow) shadowPaint else mainPaint
val path = if (isShadow) shadowPath else mainPath
paint?.apply {
strokeWidth = 2f
isAntiAlias = true
style = Paint.Style.FILL
color = this@BezierView.color
}
calculateInner()
path!!.lineTo(innerArray[0].x, innerArray[0].y)
path.lineTo(innerArray[1].x, innerArray[1].y)
path.cubicTo(innerArray[2].x, innerArray[2].y, innerArray[3].x, innerArray[3].y, innerArray[4].x, innerArray[4].y)
path.cubicTo(innerArray[5].x, innerArray[5].y, innerArray[6].x, innerArray[6].y, innerArray[7].x, innerArray[7].y)
path.lineTo(innerArray[8].x, innerArray[8].y)
path.lineTo(innerArray[9].x, innerArray[9].y)
path.lineTo(innerArray[10].x, innerArray[10].y)
progressArray = innerArray.clone()
canvas.drawPath(path, paint!!)
}
private fun calculateInner() {
val extra = shadowHeight
innerArray[0] = PointF(0f, bezierInnerHeight + extra)
innerArray[1] = PointF((bezierX - bezierInnerWidth / 2), bezierInnerHeight + extra)
innerArray[2] = PointF(bezierX - bezierInnerWidth / 4, bezierInnerHeight + extra)
innerArray[3] = PointF(bezierX - bezierInnerWidth / 4, (height - extra) / waveHeight)
innerArray[4] = PointF(bezierX, (height - extra) / waveHeight)
innerArray[5] = PointF(bezierX + bezierInnerWidth / 4, (height - extra) / waveHeight)
innerArray[6] = PointF(bezierX + bezierInnerWidth / 4, bezierInnerHeight + extra)
innerArray[7] = PointF(bezierX + bezierInnerWidth / 2, bezierInnerHeight + extra)
innerArray[8] = PointF(width, bezierInnerHeight + extra)
innerArray[9] = PointF(width, height)
innerArray[10] = PointF(0f, height)
}
private fun drawProgress(canvas: Canvas, isShadow: Boolean) {
val paint = if (isShadow) shadowPaint else mainPaint
val path = if (isShadow) shadowPath else mainPath
path!!.lineTo(progressArray[0].x, progressArray[0].y)
path.lineTo(progressArray[1].x, progressArray[1].y)
path.cubicTo(progressArray[2].x, progressArray[2].y, progressArray[3].x, progressArray[3].y, progressArray[4].x, progressArray[4].y)
path.cubicTo(progressArray[5].x, progressArray[5].y, progressArray[6].x, progressArray[6].y, progressArray[7].x, progressArray[7].y)
path.lineTo(progressArray[8].x, progressArray[8].y)
path.lineTo(progressArray[9].x, progressArray[9].y)
path.lineTo(progressArray[10].x, progressArray[10].y)
canvas.drawPath(path, paint!!)
}
private fun calculate(start: Float, end: Float): Float {
var p = progress
if (p > 1f)
p = progress - 1f
if (p in 0.9f..1f)
calculateInner()
return (p * (end - start)) + start
}
}
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<View
android:id="@+id/v_circle"
android:layout_width="48dp"
android:layout_height="40dp" />
<LinearLayout
android:id="@+id/fl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:elevation="6dp"
android:orientation="vertical"
android:paddingTop="12dp"
android:paddingStart="12dp"
android:paddingEnd="12dp"
tools:targetApi="lollipop">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:elevation="6dp">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/iconBackground"
android:layout_width="40dp"
android:visibility="gone"
app:shapeAppearance="@style/circleImageView"
android:layout_height="40dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:background="@color/colorPrimary" />
<com.koel.app.base.bottom.CellImageView
android:id="@+id/iv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/ic_baseline_account_circle_24"
app:icon_imageview_size="24dp"
android:padding="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_count"
android:layout_width="wrap_content"
android:layout_height="16dp"
android:layout_gravity="top|end|right"
android:layout_marginTop="13dp"
android:gravity="center"
android:includeFontPadding="false"
android:minWidth="16dp"
android:textDirection="ltr"
android:textSize="8dp"
android:visibility="gone"
tools:ignore="RtlHardcoded,SpUsage" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:ellipsize="end"
android:gravity="center_horizontal"
android:includeFontPadding="false"
android:maxLines="1"
android:text="test"
android:visibility="gone" />
</LinearLayout>
</merge>
import android.animation.ValueAnimator
import android.content.Context
import android.util.AttributeSet
import android.util.Log
import androidx.appcompat.widget.AppCompatImageView
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import com.koel.app.R
import kotlin.math.ceil
internal class CellImageView : AppCompatImageView {
var isBitmap = false
set(value) {
field = value
draw()
}
var useColor = true
set(value) {
field = value
draw()
}
var resource = 0
set(value) {
field = value
draw()
}
var color = 0
set(value) {
field = value
draw()
}
var size = dip(context, 24)
set(value) {
field = value
requestLayout()
}
private var actionBackgroundAlpha = false
private var changeSize = true
private var fitImage = false
private var colorAnimator: ValueAnimator? = null
private var allowDraw = false
constructor(context: Context) : super(context) {
initializeView()
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
setAttributeFromXml(context, attrs)
initializeView()
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
setAttributeFromXml(context, attrs)
initializeView()
}
private fun setAttributeFromXml(context: Context, attrs: AttributeSet) {
val a = context.theme.obtainStyledAttributes(attrs, R.styleable.CellImageView, 0, 0)
try {
a.apply {
isBitmap = getBoolean(R.styleable.CellImageView_icon_imageview_isBitmap, isBitmap)
useColor = getBoolean(R.styleable.CellImageView_icon_imageview_useColor, useColor)
resource = getResourceId(R.styleable.CellImageView_icon_imageview_resource, resource)
color = getColor(R.styleable.CellImageView_icon_imageview_color, color)
size = getDimensionPixelSize(R.styleable.CellImageView_icon_imageview_size, size)
actionBackgroundAlpha = getBoolean(R.styleable.CellImageView_icon_imageview_actionBackgroundAlpha, actionBackgroundAlpha)
changeSize = getBoolean(R.styleable.CellImageView_icon_imageview_changeSize, changeSize)
fitImage = getBoolean(R.styleable.CellImageView_icon_imageview_fitImage, fitImage)
}
} finally {
a.recycle()
}
}
private fun initializeView() {
allowDraw = true
draw()
}
private fun draw() {
if (!allowDraw)
return
if (resource == 0)
return
if (isBitmap) {
try {
val drawable = if (color == 0) context.getDrawableCompat(resource) else DrawableHelper.changeColorDrawableRes(context, resource, color)
setImageDrawable(drawable)
} catch (e: Exception) {
e.printStackTrace()
}
return
}
if (useColor && color == 0)
return
val c = if (useColor) color else -2
try {
setImageDrawable(DrawableHelper.changeColorDrawableVector(context, resource, c))
} catch (e: Exception) {
e.printStackTrace()
}
}
fun changeColorByAnim(newColor: Int, d: Long = 250L) {
if (color == 0) {
color = newColor
return
}
val lastColor = color
colorAnimator?.cancel()
colorAnimator = ValueAnimator.ofFloat(0f, 1f)
colorAnimator?.apply {
duration = d
interpolator = FastOutSlowInInterpolator()
addUpdateListener { animation ->
val f = animation.animatedFraction
color = ColorHelper.mixTwoColors(newColor, lastColor, f)
}
start()
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
Log.e("heightMeasureSpec", heightMeasureSpec.toString() + "")
if (fitImage) {
val d = drawable
if (d != null) {
val width = MeasureSpec.getSize(widthMeasureSpec)
val height = ceil((width.toFloat() * d.intrinsicHeight.toFloat() / d.intrinsicWidth).toDouble()).toInt()
setMeasuredDimension(width, height)
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
return
}
if (isBitmap || !changeSize) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
return
}
val newSize = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY)
super.onMeasure(newSize, newSize)
}
}
import android.animation.ValueAnimator
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.RippleDrawable
import android.os.Build
import android.util.AttributeSet
import android.util.Log
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.widget.RelativeLayout
import androidx.core.view.ViewCompat
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import com.koel.app.R
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.bottom_nav_item.view.*
@Suppress("unused")
class CustomBottomNavigationIcon : RelativeLayout, LayoutContainer {
companion object {
const val EMPTY_VALUE = "empty"
}
var defaultIconColor = 0
set(value) {
field = value
if (allowDraw)
iv.color = if (!isEnabledCell) defaultIconColor else selectedIconColor
}
var selectedIconColor = Color.parseColor("#00C957")
set(value) {
field = value
if (allowDraw) {
iv.color = if (isEnabledCell) selectedIconColor else defaultIconColor
}
}
var circleColor = 0
set(value) {
field = value
if (allowDraw)
isEnabledCell = isEnabledCell
}
var icon = 0
set(value) {
field = value
if (allowDraw)
iv.resource = value
}
var iconText = ""
set(value) {
field = value
if (allowDraw)
tv.text = value
}
var iconTextColor = 0
set(value) {
field = value
if (allowDraw) {
if (!isEnabledCell) tv.setTextColor(iconTextColor) else tv.setTextColor(
selectedIconTextColor
)
}
}
var selectedIconTextColor = 0
set(value) {
field = value
if (allowDraw)
if (isEnabledCell) tv.setTextColor(selectedIconTextColor) else tv.setTextColor(
iconTextColor
)
}
var iconTextTypeface: Typeface? = null
set(value) {
field = value
if (allowDraw && field != null)
tv.typeface = field
}
var iconTextSize = 10f
set(value) {
field = value
if (allowDraw) {
tv.setTextSize(TypedValue.COMPLEX_UNIT_PX, field)
}
}
var count: String? = EMPTY_VALUE
set(value) {
field = value
if (allowDraw) {
if (count != null && count == EMPTY_VALUE) {
tv_count.text = ""
tv_count.visibility = View.INVISIBLE
} else {
if (count != null && count?.length ?: 0 >= 3) {
field = count?.substring(0, 1) + ".."
}
tv_count.text = count
tv_count.visibility = View.VISIBLE
val scale = if (count?.isEmpty() == true) 0.5f else 1f
tv_count.scaleX = scale
tv_count.scaleY = scale
}
}
}
private var iconSize = dip(context, 48)
set(value) {
field = value
if (allowDraw) {
iv.size = value
iv.pivotX = iconSize / 2f
iv.pivotY = iconSize / 2f
}
}
var countTextColor = 0
set(value) {
field = value
if (allowDraw)
tv_count.setTextColor(field)
}
var countBackgroundColor = 0
set(value) {
field = value
if (allowDraw) {
val d = GradientDrawable()
d.setColor(field)
d.shape = GradientDrawable.OVAL
ViewCompat.setBackground(tv_count, d)
}
}
var countTypeface: Typeface? = null
set(value) {
field = value
if (allowDraw && field != null)
tv_count.typeface = field
}
var rippleColor = 0
set(value) {
field = value
if (allowDraw) {
isEnabledCell = isEnabledCell
}
}
var isFromLeft = false
var duration = 0L
private var progress = 0f
set(value) {
field = value
Log.e("TAG", "height is ${containerView.layoutParams.height} ${dip(context, 18)}")
fl.y = (1f - progress) * dip(context, 18) + dip(context, -2)
iv.color = if (progress == 1f) selectedIconColor else iconTextColor
tv.setTextColor(if (progress == 1f) selectedIconTextColor else defaultIconColor)
val scale = (1f - progress) * (-0.2f) + 1f
iv.scaleX = scale
iv.scaleY = scale
/*val d = GradientDrawable()
d.setColor(circleColor)
d.shape = GradientDrawable.OVAL
ViewCompat.setBackground(v_circle, d)
ViewCompat.setElevation(v_circle, if (progress > 0.7f) dipf(context, progress * 4f) else 0f)
val m = dip(context, 24)
v_circle.x = (1f - progress) * (if (isFromLeft) -m else m) + ((measuredWidth - dip(context, 48)) / 2f)
v_circle.y = (1f - progress) * measuredHeight + dip(context, 6)*/
}
var isEnabledCell = false
set(value) {
field = value
val d = GradientDrawable()
d.setColor(circleColor)
d.shape = GradientDrawable.OVAL
if (Build.VERSION.SDK_INT >= 21 && !isEnabledCell) {
fl.background = RippleDrawable(ColorStateList.valueOf(rippleColor), null, d)
} else {
fl.runAfterDelay(200) {
fl.setBackgroundColor(Color.TRANSPARENT)
}
}
}
var onClickListener: () -> Unit = {}
set(value) {
field = value
fl?.setOnClickListener {
onClickListener()
}
}
override lateinit var containerView: View
private var allowDraw = false
constructor(context: Context) : super(context) {
initializeView()
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
setAttributeFromXml(context, attrs)
initializeView()
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
setAttributeFromXml(context, attrs)
initializeView()
}
@Suppress("UNUSED_PARAMETER")
private fun setAttributeFromXml(context: Context, attrs: AttributeSet) {
}
private fun initializeView() {
allowDraw = true
containerView =
LayoutInflater.from(context).inflate(R.layout.bottom_nav_item, this)
draw()
}
private fun draw() {
if (!allowDraw)
return
icon = icon
count = count
iconSize = iconSize
iconTextTypeface = iconTextTypeface
iconTextColor = iconTextColor
selectedIconTextColor = selectedIconTextColor
iconTextSize = iconTextSize
countTextColor = countTextColor
countBackgroundColor = countBackgroundColor
countTypeface = countTypeface
rippleColor = rippleColor
onClickListener = onClickListener
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
progress = progress
}
fun disableCell() {
if (isEnabledCell)
animateProgress(false)
isEnabledCell = false
}
fun enableCell(isAnimate: Boolean = true) {
if (!isEnabledCell) {
animateProgress(true, isAnimate)
}
isEnabledCell = true
}
private fun animateProgress(enableCell: Boolean, isAnimate: Boolean = true) {
val d = if (enableCell) duration else 250
val anim = ValueAnimator.ofFloat(0f, 1f)
anim.apply {
startDelay = if (enableCell) d / 4 else 0L
duration = if (isAnimate) d else 1L
interpolator = FastOutSlowInInterpolator()
addUpdateListener {
val f = it.animatedFraction
progress = if (enableCell)
f
else
1f - f
}
start()
}
}
fun showBackground() {
iconBackground.visibility = View.VISIBLE
}
fun hideBackground() {
iconBackground.visibility = View.GONE
}
}
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Color
import android.graphics.Typeface
import android.os.Build
import android.util.AttributeSet
import android.util.LayoutDirection
import android.util.Log
import android.view.Gravity
import android.widget.FrameLayout
import android.widget.LinearLayout
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import com.koel.app.R
import kotlin.math.abs
internal typealias IBottomNavigationListener = (model: KoelNavigation.Model) -> Unit
@Suppress("MemberVisibilityCanBePrivate")
class KoelNavigation : FrameLayout {
var models = ArrayList<Model>()
var cells = ArrayList<CustomBottomNavigationIcon>()
private set
private var callListenerWhenIsSelected = false
private var selectedId = -1
private var onClickedListener: IBottomNavigationListener = {}
private var onShowListener: IBottomNavigationListener = {}
private var onReselectListener: IBottomNavigationListener = {}
private var heightCell = 0
private var isAnimating = false
var defaultIconColor = Color.parseColor("#757575")
set(value) {
field = value
updateAllIfAllowDraw()
}
var selectedIconColor = Color.parseColor("#00C957")
set(value) {
field = value
updateAllIfAllowDraw()
}
var backgroundBottomColor = Color.parseColor("#FF5733")
set(value) {
field = value
updateAllIfAllowDraw()
}
var circleColor = Color.parseColor("#4D73F6")
set(value) {
field = value
updateAllIfAllowDraw()
}
private var shadowColor = -0x454546
var countTextColor = Color.parseColor("#9281c1")
set(value) {
field = value
updateAllIfAllowDraw()
}
var countBackgroundColor = Color.parseColor("#ff0000")
set(value) {
field = value
updateAllIfAllowDraw()
}
var countTypeface: Typeface? = null
set(value) {
field = value
updateAllIfAllowDraw()
}
var iconTextColor = Color.parseColor("#003F87")
set(value) {
field = value
updateAllIfAllowDraw()
}
var selectedIconTextColor = Color.parseColor("#003F87")
set(value) {
field = value
updateAllIfAllowDraw()
}
var iconTextTypeface: Typeface? = null
set(value) {
field = value
updateAllIfAllowDraw()
}
var iconTextSize = 10f
set(value) {
field = value
updateAllIfAllowDraw()
}
var waveHeight = 7
set(value) {
field = value
updateAllIfAllowDraw()
}
private var rippleColor = Color.parseColor("#757575")
private var allowDraw = false
@Suppress("PrivatePropertyName")
private lateinit var ll_cells: LinearLayout
private lateinit var bezierView: BezierView
init {
heightCell = dip(context, 90) // bottom navigation height
}
constructor(context: Context) : super(context) {
initializeViews()
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
setAttributeFromXml(context, attrs)
initializeViews()
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
setAttributeFromXml(context, attrs)
initializeViews()
}
private fun setAttributeFromXml(context: Context, attrs: AttributeSet) {
val a = context.theme.obtainStyledAttributes(attrs, R.styleable.KoelNavigation, 0, 0)
try {
a.apply {
defaultIconColor = getColor(R.styleable.KoelNavigation_ss_defaultIconColor, defaultIconColor)
selectedIconColor = getColor(R.styleable.KoelNavigation_ss_selectedIconColor, selectedIconColor)
backgroundBottomColor = getColor(R.styleable.KoelNavigation_ss_backgroundBottomColor, backgroundBottomColor)
circleColor = getColor(R.styleable.KoelNavigation_ss_circleColor, circleColor)
countTextColor = getColor(R.styleable.KoelNavigation_ss_countTextColor, countTextColor)
countBackgroundColor = getColor(R.styleable.KoelNavigation_ss_countBackgroundColor, countBackgroundColor)
rippleColor = getColor(R.styleable.KoelNavigation_ss_rippleColor, rippleColor)
shadowColor = getColor(R.styleable.KoelNavigation_ss_shadowColor, shadowColor)
iconTextColor = getColor(R.styleable.KoelNavigation_ss_iconTextColor, iconTextColor)
selectedIconTextColor = getColor(R.styleable.KoelNavigation_ss_selectedIconTextColor, selectedIconTextColor)
iconTextSize = getDimensionPixelSize(R.styleable.KoelNavigation_ss_iconTextSize, dip(context, iconTextSize.toInt())).toFloat()
waveHeight = getInteger(R.styleable.KoelNavigation_ss_waveHeight, waveHeight)
val iconTexttypeface = getString(R.styleable.KoelNavigation_ss_iconTextTypeface)
if (iconTexttypeface != null && iconTexttypeface.isNotEmpty())
iconTextTypeface = Typeface.createFromAsset(context.assets, iconTexttypeface)
val typeface = getString(R.styleable.KoelNavigation_ss_countTypeface)
if (typeface != null && typeface.isNotEmpty())
countTypeface = Typeface.createFromAsset(context.assets, typeface)
}
} finally {
a.recycle()
}
}
private fun initializeViews() {
ll_cells = LinearLayout(context)
ll_cells.apply {
val params = LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, heightCell)
params.gravity = Gravity.BOTTOM
layoutParams = params
orientation = LinearLayout.HORIZONTAL
clipChildren = false
clipToPadding = false
}
bezierView = BezierView(context)
bezierView.apply {
layoutParams = LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, (heightCell))
color = backgroundBottomColor
shadowColor = this@KoelNavigation.shadowColor
}
bezierView.waveHeight = waveHeight
addView(bezierView)
addView(ll_cells)
allowDraw = true
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
if (selectedId == -1) {
bezierView.bezierX = measuredWidth + dipf(context, 72)
}
if (selectedId != -1) {
show(selectedId, false)
}
}
fun add(model: Model) {
val cell = CustomBottomNavigationIcon(context)
cell.apply {
val params = LinearLayout.LayoutParams(0, heightCell, 1f)
layoutParams = params
icon = model.icon
count = model.count
defaultIconColor = this@KoelNavigation.defaultIconColor
selectedIconColor = this@KoelNavigation.selectedIconColor
iconTextTypeface = this@KoelNavigation.iconTextTypeface
iconTextColor = this@KoelNavigation.iconTextColor
selectedIconTextColor = this@KoelNavigation.selectedIconTextColor
iconTextSize = this@KoelNavigation.iconTextSize
countTextColor = this@KoelNavigation.countTextColor
countBackgroundColor = this@KoelNavigation.countBackgroundColor
backgroundBottomColor = this@KoelNavigation.backgroundBottomColor
countTypeface = this@KoelNavigation.countTypeface
rippleColor = this@KoelNavigation.rippleColor
onClickListener = {
if (isShowing(model.id)){
onReselectListener(model)}
if (!cell.isEnabledCell && !isAnimating) {
show(model.id)
onClickedListener(model)
} else {
if (callListenerWhenIsSelected) {
onClickedListener(model)
}
}
}
disableCell()
ll_cells.addView(this)
}
cells.add(cell)
models.add(model)
}
private fun updateAllIfAllowDraw() {
if (!allowDraw)
return
cells.forEach {
it.defaultIconColor = defaultIconColor
it.selectedIconColor = selectedIconColor
it.circleColor = circleColor
it.countTextColor = countTextColor
it.countBackgroundColor = countBackgroundColor
it.countTypeface = countTypeface
}
bezierView.color = backgroundBottomColor
}
private fun anim(cell: CustomBottomNavigationIcon, id: Int, enableAnimation: Boolean = true) {
isAnimating = true
val pos = getModelPosition(id)
val nowPos = getModelPosition(selectedId)
val nPos = if (nowPos < 0) 0 else nowPos
val dif = abs(pos - nPos)
val d = (dif) * 100L + 150L
val animDuration = if (enableAnimation) d else 1L
val animInterpolator = FastOutSlowInInterpolator()
val anim = ValueAnimator.ofFloat(0f, 1f)
anim.apply {
duration = animDuration
interpolator = animInterpolator
val beforeX = bezierView.bezierX
addUpdateListener {
val f = it.animatedFraction
val newX = cell.x + (cell.measuredWidth / 2)
if (newX > beforeX)
bezierView.bezierX = f * (newX - beforeX) + beforeX
else
bezierView.bezierX = beforeX - f * (beforeX - newX)
if (f == 1f)
isAnimating = false
}
start()
}
if (abs(pos - nowPos) > 1) {
val progressAnim = ValueAnimator.ofFloat(0f, 1f)
progressAnim.apply {
duration = animDuration
interpolator = animInterpolator
addUpdateListener {
val f = it.animatedFraction
bezierView.progress = f * 2f
}
start()
}
}
cell.isFromLeft = pos > nowPos
cells.forEach {
it.duration = d
}
}
fun show(id: Int, enableAnimation: Boolean = true) {
for (i in models.indices) {
val model = models[i]
val cell = cells[i]
if (model.id == id) {
anim(cell, id, enableAnimation)
cell.enableCell()
cell.showBackground()
onShowListener(model)
} else {
cell.hideBackground()
cell.disableCell()
}
}
selectedId = id
}
fun isShowing(id: Int): Boolean {
return selectedId == id
}
fun getModelById(id: Int): Model? {
models.forEach {
if (it.id == id)
return it
}
return null
}
fun getCellById(id: Int): CustomBottomNavigationIcon? {
return cells[getModelPosition(id)]
}
fun getModelPosition(id: Int): Int {
for (i in models.indices) {
val item = models[i]
if (item.id == id)
return i
}
return -1
}
fun setCount(id: Int, count: String) {
val model = getModelById(id) ?: return
val pos = getModelPosition(id)
model.count = count
cells[pos].count = count
}
fun clearCount(id: Int) {
val model = getModelById(id) ?: return
val pos = getModelPosition(id)
model.count = CustomBottomNavigationIcon.EMPTY_VALUE
cells[pos].count = CustomBottomNavigationIcon.EMPTY_VALUE
}
fun clearAllCounts() {
models.forEach {
clearCount(it.id)
}
}
fun setOnShowListener(listener: IBottomNavigationListener) {
onShowListener = listener
}
fun setOnClickMenuListener(listener: IBottomNavigationListener) {
onClickedListener = listener
}
fun setOnReselectListener(listener: IBottomNavigationListener) {
onReselectListener = listener
}
class Model(var id: Int, var icon: Int, var text: String) {
var count: String = CustomBottomNavigationIcon.EMPTY_VALUE
}
}
private fun getDP(context: Context) = context.resources.displayMetrics.density
internal fun dipf(context: Context, f: Float) = f * getDP(context)
internal fun dipf(context: Context, i: Int) = i * getDP(context)
internal fun dip(context: Context, i: Int) = (i * getDP(context)).toInt()
internal fun toDP(context: Context,value: Int): Int = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value.toFloat(),context.resources.displayMetrics).toInt()
internal fun toPx(context: Context,value: Float): Float = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, value,context.resources.displayMetrics)
internal object DrawableHelper {
fun changeColorDrawableVector(c: Context?, resDrawable: Int, color: Int): Drawable? {
if (c == null)
return null
val d = VectorDrawableCompat.create(c.resources, resDrawable, null) ?: return null
d.mutate()
if (color != -2) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
d.colorFilter = BlendModeColorFilter(color, BlendMode.SRC_IN)
} else {
d.setColorFilter(color, PorterDuff.Mode.SRC_IN)
}
}
return d
}
fun changeColorDrawableRes(c: Context?, resDrawable: Int, color: Int): Drawable? {
if (c == null)
return null
val d = ContextCompat.getDrawable(c, resDrawable) ?: return null
d.mutate()
if (color != -2) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
d.colorFilter = BlendModeColorFilter(color, BlendMode.SRC_IN)
} else {
d.setColorFilter(color, PorterDuff.Mode.SRC_IN)
}
}
return d
}
}
internal object ColorHelper {
fun mixTwoColors(color1: Int, color2: Int, amount: Float): Int {
val alphaChannel = 24
val redChannel = 16
val greenChannel = 8
val inverseAmount = 1.0f - amount
val a =
((color1 shr alphaChannel and 0xff).toFloat() * amount + (color2 shr alphaChannel and 0xff).toFloat() * inverseAmount).toInt() and 0xff
val r =
((color1 shr redChannel and 0xff).toFloat() * amount + (color2 shr redChannel and 0xff).toFloat() * inverseAmount).toInt() and 0xff
val g =
((color1 shr greenChannel and 0xff).toFloat() * amount + (color2 shr greenChannel and 0xff).toFloat() * inverseAmount).toInt() and 0xff
val b =
((color1 and 0xff).toFloat() * amount + (color2 and 0xff).toFloat() * inverseAmount).toInt() and 0xff
return a shl alphaChannel or (r shl redChannel) or (g shl greenChannel) or b
}
}
internal fun Context.getDrawableCompat(res: Int) = ContextCompat.getDrawable(this, res)
internal inline fun <T : View?> T.runAfterDelay(delay: Long, crossinline f: T.() -> Unit) {
this?.postDelayed({
try {
f()
} catch (e: Exception) {
}
}, delay)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment