Can a SeekBar be vertical? I am not very good at UI design, so how can I make the SeekBar more beautiful, please give me some templates and examples.
Can a SeekBar be vertical? I am not very good at UI design, so how can I make the SeekBar more beautiful, please give me some templates and examples.
Wrap it inside a FrameLayout so that there is no Size issue.
<FrameLayout
android:layout_width="@dimen/_20dp"
android:layout_marginStart="@dimen/_15dp"
android:layout_marginEnd="@dimen/_15dp"
android:layout_height="match_parent"
android:orientation="vertical">
<SeekBar
android:layout_width="150dp"
android:layout_height="30dp"
android:layout_gravity="center"
android:rotation="270" />
</FrameLayout>
I tried in many different ways, but the one which worked for me was. Use Seekbar inside FrameLayout
<FrameLayout
android:id="@+id/VolumeLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@id/MuteButton"
android:layout_below="@id/volumeText"
android:layout_centerInParent="true">
<SeekBar
android:id="@+id/volume"
android:layout_width="500dp"
android:layout_height="60dp"
android:layout_gravity="center"
android:progress="50"
android:secondaryProgress="40"
android:progressDrawable="@drawable/seekbar_volume"
android:secondaryProgressTint="@color/tint_neutral"
android:thumbTint="@color/tint_neutral"
/>
And in Code.
Setup Pre Draw callback on Seekbar, Where you can change the Width and height of the Seekbar I did this part in c#, so Code i used was
var volumeSlider = view.FindViewById<SeekBar>(Resource.Id.home_link_volume);
var volumeFrameLayout = view.FindViewById<FrameLayout>(Resource.Id.linkVolumeFrameLayout);
void OnPreDrawVolume(object sender, ViewTreeObserver.PreDrawEventArgs e)
{
volumeSlider.ViewTreeObserver.PreDraw -= OnPreDrawVolume;
var h = volumeFrameLayout.Height;
volumeSlider.Rotation = 270.0f;
volumeSlider.LayoutParameters.Width = h;
volumeSlider.RequestLayout();
}
volumeSlider.ViewTreeObserver.PreDraw += OnPreDrawVolume;
Here i Add listener to PreDraw Event and when its triggered, I remove the PreDraw so that it doesnt go into Infinite loop.
So when Pre Draw gets executed, I fetch the Height of FrameLayout and assign it to Seekbar. And set the rotation of seekbar to 270. As my seekbar is inside frame Layout and its Gravity is set as Center. I dont need to worry about the Translation. As Seekbar always stay in middle of Frame Layout.
Reason i remove EventHandler is because seekbar.RequestLayout(); Will make this event to be executed again.
You can do it by yourself - it's now so difficult. Here is an example from my project: https://github.com/AlShevelev/WizardCamera
Let start from settings (attrs.xml).
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ExpositionBar">
<attr name="button_icon" format="reference" />
<attr name="button_icon_size" format="dimension" />
<attr name="stroke_width" format="dimension" />
<attr name="stroke_color" format="color" />
<attr name="button_color" format="color" />
<attr name="button_color_pressed" format="color" />
<attr name="min_value" format="float" />
<attr name="max_value" format="float" />
</declare-styleable>
</resources>
Here is a couple of utility functions:
fun <T: Comparable<T>>T.fitInRange(range: Range<T>): T =
when {
this < range.lower -> range.lower
this > range.upper -> range.upper
else -> this
}
fun Float.reduceToRange(rangeFrom: Range<Float>, rangeTo: Range<Float>): Float =
when {
this == rangeFrom.lower -> rangeTo.lower
this == rangeFrom.upper -> rangeTo.upper
else -> {
val placeInRange = (this - rangeFrom.lower) / (rangeFrom.upper - rangeFrom.lower)
((rangeTo.upper - rangeTo.lower) * placeInRange) + rangeTo.lower
}
}
And at last, but not least - a class for vertical seek bar:
class ExpositionBar
@JvmOverloads
constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val drawingRect = RectF(0f, 0f, 0f, 0f)
private val drawingPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val strokeWidth: Float
@ColorInt
private val strokeColor: Int
@ColorInt
private val buttonFillColor: Int
@ColorInt
private val buttonFillColorPressed: Int
private val icon: VectorDrawable
private val valuesRange: Range<Float>
private var centerX = 0f
private var minY = 0f
private var maxY = 0f
private var buttonCenterY = 0f
private var buttonRadiusExt = 0f
private var buttonRadiusInt = 0f
private var buttonMinY = 0f
private var buttonMaxY = 0f
private var buttonCenterBoundsRange = Range(0f, 0f)
private var iconTranslationX = 0f
private var iconTranslationY = 0f
private var isInDragMode = false
private var onValueChangeListener: ((Float) -> Unit)? = null
private var oldOutputValue = Float.MIN_VALUE
init {
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ExpositionBar)
icon = typedArray.getDrawable(R.styleable.ExpositionBar_button_icon) as VectorDrawable
val iconSize = typedArray.getDimensionPixelSize(R.styleable.ExpositionBar_button_icon_size, 0)
icon.setBounds(0, 0, iconSize, iconSize)
strokeWidth = typedArray.getDimensionPixelSize(R.styleable.ExpositionBar_stroke_width, 0).toFloat()
drawingPaint.strokeWidth = strokeWidth
strokeColor = typedArray.getColor(R.styleable.ExpositionBar_stroke_color, Color.WHITE)
buttonFillColor = typedArray.getColor(R.styleable.ExpositionBar_button_color, Color.BLACK)
buttonFillColorPressed = typedArray.getColor(R.styleable.ExpositionBar_button_color_pressed, Color.BLUE)
val minValue = typedArray.getFloat(R.styleable.ExpositionBar_min_value, 0f)
val maxValue = typedArray.getFloat(R.styleable.ExpositionBar_max_value, 0f)
valuesRange = Range(minValue, maxValue)
typedArray.recycle()
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
drawingRect.right = width.toFloat()
drawingRect.bottom = height.toFloat()
buttonCenterY = drawingRect.centerY()
recalculateDrawingValues()
}
override fun onDraw(canvas: Canvas) {
drawingPaint.color = strokeColor
drawingPaint.style = Paint.Style.STROKE
// Draw the center line
canvas.drawLine(centerX, minY, centerX, buttonMinY, drawingPaint)
canvas.drawLine(centerX, buttonMaxY, centerX, maxY, drawingPaint)
// Draw the button
canvas.drawCircle(centerX, buttonCenterY, buttonRadiusExt, drawingPaint)
drawingPaint.style = Paint.Style.FILL
drawingPaint.color = if(isInDragMode) buttonFillColorPressed else buttonFillColor
canvas.drawCircle(centerX, buttonCenterY, buttonRadiusInt, drawingPaint)
// Draw button icon
canvas.translate(iconTranslationX, iconTranslationY)
icon.draw(canvas)
canvas.translate(-iconTranslationX, -iconTranslationY)
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
if(!isEnabled) {
return false
}
when(event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
if(isButtonHit(event.y)){
isInDragMode = true
invalidate()
}
}
MotionEvent.ACTION_MOVE -> {
if(isInDragMode) {
buttonCenterY = event.y.fitInRange(buttonCenterBoundsRange)
recalculateDrawingValues()
invalidate()
val outputValue = buttonCenterY.reduceToRange(buttonCenterBoundsRange, valuesRange)
if (outputValue != oldOutputValue) {
onValueChangeListener?.invoke(outputValue)
oldOutputValue = outputValue
}
}
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL -> {
isInDragMode = false
invalidate()
}
}
return true
}
fun setOnValueChangeListener(listener: ((Float) -> Unit)?) {
onValueChangeListener = listener
}
private fun recalculateDrawingValues() {
centerX = drawingRect.left + drawingRect.width()/2
minY = drawingRect.top
maxY = drawingRect.bottom
buttonRadiusExt = drawingRect.width() / 2 - strokeWidth / 2
buttonRadiusInt = buttonRadiusExt - strokeWidth / 2
buttonMinY = buttonCenterY - buttonRadiusExt
buttonMaxY = buttonCenterY + buttonRadiusExt
val buttonCenterMinY = minY + buttonRadiusExt + strokeWidth / 2
val buttonCenterMaxY = maxY - buttonRadiusExt - strokeWidth / 2
buttonCenterBoundsRange = Range(buttonCenterMinY, buttonCenterMaxY)
iconTranslationX = centerX - icon.bounds.width() / 2
iconTranslationY = buttonCenterY - icon.bounds.height() / 2
}
private fun isButtonHit(y: Float): Boolean {
return y >= buttonMinY && y <= buttonMaxY
}
}
You can use it as shown here:
<com.shevelev.wizard_camera.main_activity.view.widgets.ExpositionBar
android:id="@+id/expositionBar"
android:layout_width="@dimen/mainButtonSize"
android:layout_height="300dp"
android:layout_gravity="end|center_vertical"
android:layout_marginEnd="@dimen/marginNormal"
android:layout_marginBottom="26dp"
app:button_icon = "@drawable/ic_brightness"
app:button_icon_size = "@dimen/toolButtonIconSize"
app:stroke_width = "@dimen/strokeWidthNormal"
app:stroke_color = "@color/mainButtonsForeground"
app:button_color = "@color/mainButtonsBackground"
app:button_color_pressed = "@color/mainButtonsBackgroundPressed"
app:min_value="-100"
app:max_value="100"
/>
Voila!
By using RotateLayout, having vertical SeekBar is a breeze. Just wrap that horrible SeekBar into it and Bob is your uncle:
<com.github.rongi.rotate_layout.layout.RotateLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:angle="-90"
>
<androidx.appcompat.widget.AppCompatSeekBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
</com.github.rongi.rotate_layout.layout.RotateLayout>
Instead of using android:rotation="270" inside of a seek bar, use it inside of a FrameLayout that wraps around it.
<FrameLayout
android:background="@color/gray"
android:layout_width="300dp"
android:layout_height="5dp"
android:layout_marginEnd="-126dp"
android:rotation="270"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
<SeekBar
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</FrameLayout>
To get my frame layout to be 24dp margin right I calculated width -150dp + 24dp because the frame layout is first drawn horizontally and then rotated vertically.