<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="line">
<stroke
android:width="2dp"
android:color="#000000"
android:dashWidth="6dp"
android:dashGap="4dp" />
</shape>
import android.graphics.DashPathEffect
import android.graphics.Paint
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.RectShape
import android.view.View
import androidx.core.content.ContextCompat
fun View.setSolidLine(
color: Int,
thickness: Float = 4f
) {
val shape = ShapeDrawable(RectShape()).apply {
paint.color = color
paint.style = Paint.Style.STROKE
paint.strokeWidth = thickness
}
background = shape
}
fun View.setDottedLine(
color: Int,
thickness: Float = 4f,
dashWidth: Float = 15f,
dashGap: Float = 10f
) {
val shape = ShapeDrawable(RectShape()).apply {
paint.color = color
paint.style = Paint.Style.STROKE
paint.strokeWidth = thickness
paint.pathEffect = DashPathEffect(floatArrayOf(dashWidth, dashGap), 0f)
}
background = shape
}
// Solid line
myView.setSolidLine(
color = ContextCompat.getColor(this, android.R.color.black),
thickness = 6f
)
// Dotted line
myView.setDottedLine(
color = ContextCompat.getColor(this, android.R.color.holo_red_dark),
thickness = 6f,
dashWidth = 20f,
dashGap = 10f
)
private fun updateMarker(
context: Context,
sb: SeekBar,
rlMarker: View,
message: String,
) {
val tvProgress = rlMarker.findViewById<View>(R.id.tvProgress) as TextView
val vArrow: View = rlMarker.findViewById(R.id.vArrow)
val width = (sb.width - sb.paddingLeft - sb.paddingRight)
val isRTL = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL
val thumbPos = if (isRTL) {
// RTL → calculate from right
sb.width - sb.paddingRight -
(width * sb.progress / sb.max) -
Math.round(convertDpToPixel(10f, context))
} else {
// LTR (default)
sb.paddingLeft +
(width * sb.progress / sb.max) +
Math.round(convertDpToPixel(10f, context))
}
tvProgress.text = message
tvProgress.post {
val display =
(context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay
val deviceDisplay = Point()
display.getSize(deviceDisplay)
if (isRTL) {
// vArrow follows thumb in RTL
vArrow.x = (thumbPos - sb.thumbOffset).toFloat()
// tvProgress RTL alignment
if ((thumbPos + tvProgress.width / 2 + sb.paddingRight > deviceDisplay.x)) {
// tvProgress is beyond right bound
tvProgress.x = vArrow.x - 20
} else if ((thumbPos - tvProgress.width / 2 - sb.paddingLeft < 0)) {
// tvProgress is beyond left bound
tvProgress.x = vArrow.x - tvProgress.width + 20 + vArrow.width
} else {
// in between
tvProgress.x = thumbPos - tvProgress.width / 2f
}
} else {
// ===== Existing English (LTR) logic =====
vArrow.x = (thumbPos - sb.thumbOffset).toFloat()
if ((thumbPos - tvProgress.width / 2 - sb.paddingLeft < 0)) {
tvProgress.x = vArrow.x - 20
} else if ((thumbPos + tvProgress.width / 2 + sb.paddingRight > deviceDisplay.x)) {
tvProgress.x = vArrow.x - tvProgress.width + 20 + vArrow.width
} else {
tvProgress.x = thumbPos - tvProgress.width / 2f
}
}
}
}
tvProgress.text = message
tvProgress.post {
val display = (context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay
val deviceDisplay = Point()
display.getSize(deviceDisplay)
val screenWidth = deviceDisplay.x.toFloat()
// Keep arrow exactly under thumb, but clamp it so the arrow view stays inside screen
val rawArrowX = (thumbPos - sb.thumbOffset).toFloat()
val arrowX = rawArrowX.coerceIn(0f, screenWidth - vArrow.width.toFloat())
vArrow.x = arrowX
// Desired center position for tvProgress (centered above thumb)
val desiredTvX = thumbPos - tvProgress.width / 2f
// Define safe bounds for tvProgress so it never goes off-screen.
// Using seekbar paddings so tooltip doesn't overlap seekbar gutters.
val minTvX = sb.paddingLeft.toFloat()
val maxTvX = screenWidth - sb.paddingRight - tvProgress.width.toFloat()
// If tooltip is wider than screen, just pin at 0
val finalTvX = if (tvProgress.width >= screenWidth) {
0f
} else {
desiredTvX.coerceIn(minTvX, maxTvX)
}
tvProgress.x = finalTvX
}
----
tvProgress.text = message
tvProgress.post {
val display = (context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay
val deviceDisplay = Point()
display.getSize(deviceDisplay)
val screenWidth = deviceDisplay.x.toFloat()
// Get the thumb bounds (accurate position of thumb)
val thumbBounds = sb.thumb.bounds
val thumbCenterX = sb.x + sb.paddingLeft + thumbBounds.exactCenterX()
// Arrow should be centered exactly below thumb
val rawArrowX = thumbCenterX - vArrow.width / 2f
val arrowX = rawArrowX.coerceIn(0f, screenWidth - vArrow.width.toFloat())
vArrow.x = arrowX
// Tooltip (tvProgress) should be centered above thumb
val desiredTvX = thumbCenterX - tvProgress.width / 2f
val minTvX = sb.paddingLeft.toFloat()
val maxTvX = screenWidth - sb.paddingRight - tvProgress.width.toFloat()
val finalTvX = if (tvProgress.width >= screenWidth) {
0f
} else {
desiredTvX.coerceIn(minTvX, maxTvX)
}
tvProgress.x = finalTvX
}
LTR ----------------
tvProgress.text = message
tvProgress.post {
val display = (context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay
val deviceDisplay = Point()
display.getSize(deviceDisplay)
val screenWidth = deviceDisplay.x.toFloat()
// Get the thumb's exact center on screen
val thumbBounds = sb.thumb.bounds
val thumbCenterX = sb.x + sb.paddingLeft + thumbBounds.exactCenterX()
// Arrow: place its center exactly under thumb
val rawArrowX = thumbCenterX - vArrow.width / 2f
val arrowX = rawArrowX.coerceIn(0f, screenWidth - vArrow.width.toFloat())
vArrow.x = arrowX
// Tooltip: center above thumb
val desiredTvX = thumbCenterX - tvProgress.width / 2f
val minTvX = sb.paddingLeft.toFloat()
val maxTvX = screenWidth - sb.paddingRight - tvProgress.width.toFloat()
val finalTvX = if (tvProgress.width >= screenWidth) {
0f
} else {
desiredTvX.coerceIn(minTvX, maxTvX)
}
tvProgress.x = finalTvX
}
LTR-1------------------------------------
tvProgress.text = message
tvProgress.post {
val display = (context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay
val deviceDisplay = Point()
display.getSize(deviceDisplay)
val screenWidth = deviceDisplay.x.toFloat()
// Thumb's exact center (absolute screen X)
val thumbBounds = sb.thumb.bounds
val thumbCenterX = sb.x + sb.paddingLeft + thumbBounds.exactCenterX()
// --- Arrow ---
val rawArrowX = thumbCenterX - vArrow.width / 2f
val arrowX = rawArrowX.coerceIn(0f, screenWidth - vArrow.width.toFloat())
vArrow.x = arrowX
// --- Tooltip ---
// try to center above thumb
val desiredTvX = thumbCenterX - tvProgress.width / 2f
// safe boundaries so tooltip never escapes
val minTvX = 0f
val maxTvX = screenWidth - tvProgress.width.toFloat()
val finalTvX = desiredTvX.coerceIn(minTvX, maxTvX)
tvProgress.x = finalTvX
}
LTR-2------------------------
tvProgress.text = message
tvProgress.post {
val display = (context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay
val deviceDisplay = Point()
display.getSize(deviceDisplay)
val screenWidth = deviceDisplay.x.toFloat()
// Thumb center X
val thumbBounds = sb.thumb.bounds
val thumbCenterX = sb.x + sb.paddingLeft + thumbBounds.exactCenterX()
// --- Tooltip (tvProgress) ---
val desiredTvX = thumbCenterX - tvProgress.width / 2f
val minTvX = 0f
val maxTvX = screenWidth - tvProgress.width.toFloat()
val finalTvX = desiredTvX.coerceIn(minTvX, maxTvX)
tvProgress.x = finalTvX
// --- Arrow (vArrow) ---
// Arrow should ideally be centered under thumb,
// but if tvProgress is clamped, adjust arrow to stay inside tooltip
var arrowX = thumbCenterX - vArrow.width / 2f
if (arrowX < finalTvX) {
// Clamp arrow inside left edge of tooltip
arrowX = finalTvX
} else if (arrowX + vArrow.width > finalTvX + tvProgress.width) {
// Clamp arrow inside right edge of tooltip
arrowX = finalTvX + tvProgress.width - vArrow.width
}
vArrow.x = arrowX
}
object LocaleHelper {
fun wrap(context: Context, language: String): Context {
val locale = Locale(language)
Locale.setDefault(locale)
val config = Configuration(context.resources.configuration)
config.setLocale(locale)
return context.createConfigurationContext(config)
}
}
override fun onAttach(context: Context) {
super.onAttach(LocaleHelper.wrap(context, "en"))
}
import android.graphics.*
import android.graphics.drawable.Drawable
class LineDrawable(
private val color: Int,
private val thickness: Float,
private val dashWidth: Float? = null,
private val dashGap: Float? = null
) : Drawable() {
private val paint = Paint().apply {
isAntiAlias = true
style = Paint.Style.STROKE
strokeWidth = thickness
this.color = this@LineDrawable.color
if (dashWidth != null && dashGap != null) {
pathEffect = DashPathEffect(floatArrayOf(dashWidth, dashGap), 0f)
}
}
override fun draw(canvas: Canvas) {
val centerY = bounds.exactCenterY()
canvas.drawLine(bounds.left.toFloat(), centerY, bounds.right.toFloat(), centerY, paint)
}
override fun setAlpha(alpha: Int) { paint.alpha = alpha }
override fun setColorFilter(colorFilter: ColorFilter?) { paint.colorFilter = colorFilter }
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
}
About the author
Experienced software engineer with demonstrated history of working in telecommunications industry along with many other sectors like education, e-commerce, healthcare etc. Skilled in Android SDK, Kotlin, SQLite, Java, MVP, MVVM,REST, JSON JavaScript. Strong engineering professional with a bachelor of engineering focused in computer engineering. Solid understanding of the full mobile development life cycle.
Comments
Join the discussionNo comments yet. Be the first to comment.