Table of contents
1.1.1 Set a layer of FrameLayout
1.1.2 SeekBar removes left and right spacing
1.1.3 SeekBar height cannot be set
1.1.4 SeekBar background setting
1.1.5 The bottom View size and distance from the bottom are not hard-coded
1.2 Custom style attributes and themes
1.2.2 Customize theme specific attributes
1.3.1 Provide external customization
1.3.2 Support dynamic addition and use in code
1.3.3 Layout depends on selfWidth/selfHeight
0 Preface
We know that Android does not natively support vertical SeekBar. Why? I wanted to find a reason for them, but I couldn't find one. Maybe laziness is a virtue of programmers! Okay, let’s get to the point:
What would you do to implement the following UI?
Let’s first break down the requirements:
- The volume or brightness adjustment UI needs to be draggable;
- The background, foreground, Icon position, rounded corners and UI size can be customized.
Well, Android's SeekBar supports draggability, but it is horizontal. If you use native, you need to change it to vertical; otherwise, you need to customize the View to draw such a control.
In addition, the customizability requirements of UI elements cannot be hard-coded. There are also many details, which we will discuss later.
About custom View
In fact, I am not opposed to customizing the View. Although we can draw such a vertical SeekBar ourselves, can we ensure that we can handle the measure, layout and draw of the View better than Android native? Will there be no performance issues? It is foolish to think that it is not possible. And customizing View may be time-consuming and laborious, and the UI restoration cannot achieve the effect of the design draft.
Therefore, my idea is to reuse Android's native View as much as possible, or to combine the View for the best result. The second is to cut the picture. I only create the canvas to draw the View myself when I have no choice, but I must take into account the balance between function and performance. . For example: the VerticalProgress I previously implemented through a custom View may not necessarily have the best balance between functionality and performance.
[ Customized View VerticalProgress_Swuagg's blog ]
1 Implement vertical SeekBar
To implement a vertical SeekBar, there are currently three options:
Option 1: Customize the implementation by inheriting View, such as: inheriting View custom implementation of VerticalSeekBar
Option 2: Implement internal rotation by inheriting the SeekBar overloaded method, such as: inheriting the internal rotation of the SeekBar overloaded method
Option 3: For API 11 and higher versions, specify rotation 270° in XML through the rotation attribute. For example: use the XML attribute of seekbar (android:rotation="270") to obtain a vertical effect.
As we can see from the title, the third solution adopted in this article does not inherit SeekBar and does not customize View. Let’s take a closer look through the code below.
1.1 XML layout parsing
Let’s take a look at the implementation effect:
Next we analyze the xml layout file:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/parent"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<SeekBar
android:id="@+id/seekbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:progressDrawable="?attr/UISeekBarProgressDrawable"
android:rotation="270"
android:thumb="@null" />
</FrameLayout>
<View
android:id="@+id/icon"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintWidth_percent="0.5" />
</androidx.constraintlayout.widget.ConstraintLayout>
First of all, we can see that no data is hard-coded. I know you want to refute me with 270, but isn’t this the requirement design?
1.1.1 Set a layer of FrameLayout
1) There will be size problems after the SeekBar is rotated 270°, and a layer of FrameLayout needs to be covered. The width and height of the FrameLayout are opposite to those of the SeekBar, which are the height and width of the SeekBar. The SeekBar also needs to specify layout_gravity as center;
2) We found that match_parent is used for both width and height in XML. This is to allow users to customize the size. We will dynamically modify the width and height in the code according to the external use location.
1.1.2 SeekBar removes left and right spacing
There is default Padding around SeekBar. Setting paddingHorizontal to 0 does not take effect. You need to set paddingStart and paddingEnd to 0.
1.1.3 SeekBar height cannot be set
You need to set minHeight, minWidth, maxHeight, and maxWidth at the same time. The value is the minimum value of width and height. Since it is not hard-coded, it needs to be implemented in the code.
1.1.4 SeekBar background setting
By customizing the attribute UISeekBarProgressDrawable and specifying the corresponding drawable according to the theme, you can switch with the theme. The background file ui_seekbar_vertical_bg.xml is as follows:
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape android:shape="rectangle">
<corners android:radius="32dp" />
<stroke
android:width="2dp"
android:color="#3A4266" />
<solid android:color="#323A60" />
</shape>
</item>
<item android:id="@android:id/progress">
<clip>
<shape android:shape="rectangle">
<corners android:radius="32dp" />
<solid android:color="#6E779C" />
</shape>
</clip>
</item>
</layer-list>
1.1.5 The bottom View size and distance from the bottom are not hard-coded
1) Implement half width ratio: by specifying width and height as 0, setting layout_constraintDimensionRatio="1:1", layout_constraintWidth_percent="0.5", and aligning left and right;
2) The distance from the bottom, because it is not hard-coded, needs to be implemented in the code.
1.2 Custom style attributes and themes
1.2.1 Custom style attributes
<declare-styleable name="SeekBarVertical">
<attr name="UISeekBarProgress" format="integer" />
<attr name="UISeekBarMax" format="integer" />
<attr name="UISeekBarIcon" format="reference" />
<attr name="UISeekBarProgressDrawable" format="reference" />
</declare-styleable>
1.2.2 Customize theme specific attributes
<!--应用Theme主题-->
<style name="DefaultAppTheme" parent="android:Theme.Black.NoTitleBar.Fullscreen">
<item name="UISeekBarProgressDrawable">@drawable/ui_seekbar_vertical_bg</item>
</style>
1.3 Coding implementation
package com.agg.ui
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import android.widget.SeekBar
import androidx.constraintlayout.widget.ConstraintLayout
/**
* Description:
* CreateDate: 2023/7/11 14:58
* Author: agg
*/
class SeekBarVertical : ConstraintLayout {
lateinit var parentView: ConstraintLayout
lateinit var seekBar: SeekBar
lateinit var icon: View
private var selfWidth: Int = -1
private var selfHeight: Int = -1
private var seekBarMinMaxValue: Int = -1
private var selfProgress: Int = -1
private var selfMax: Int = -1
private var iconDrawable: Drawable? = null
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyle: Int = 0) : super(
context, attrs, defStyle
) {
attrs?.let { initAttrs(context, attrs) }
initView()
}
/**
* 代码中动态添加时使用。如:
*
* binding.root.addView(SeekBarVertical(this,192,432))
*/
constructor(context: Context, width: Int, height: Int) : super(context) {
selfWidth = width
selfHeight = height
seekBarMinMaxValue = if (selfHeight < selfWidth) selfHeight else selfWidth
initView()
}
@SuppressLint("ResourceType")
private fun initAttrs(context: Context, attrs: AttributeSet) {
// 获取Android原生宽、高属性
context.obtainStyledAttributes(
attrs, intArrayOf(android.R.attr.layout_width, android.R.attr.layout_height)
).apply {
selfWidth = getDimensionPixelSize(0, -1)
selfHeight = getDimensionPixelSize(1, -1)
seekBarMinMaxValue = if (selfHeight < selfWidth) selfHeight else selfWidth
recycle()
}
// 获取SeekBarVertical自定义属性
context.obtainStyledAttributes(
attrs, R.styleable.SeekBarVertical
).apply {
selfProgress = getInt(R.styleable.SeekBarVertical_UISeekBarProgress, -1)
selfMax = getInt(R.styleable.SeekBarVertical_UISeekBarMax, -1)
iconDrawable = getDrawable(R.styleable.SeekBarVertical_UISeekBarIcon)
recycle()
}
}
@SuppressLint("NewApi")
private fun initView() {
// 未设置宽高则直接返回
if (selfWidth < 0 || selfHeight < 0) return
parentView = View.inflate(context, R.layout.ui_seekbar_vertical, this) as ConstraintLayout
// 设置控件整体宽、高
(parentView.findViewById<ConstraintLayout>(R.id.parent).layoutParams as LayoutParams).apply {
width = selfWidth
height = selfHeight
}
// 设置SeekBar宽、高,以及progress和max值
seekBar = parentView.findViewById<SeekBar>(R.id.seekbar).apply {
(layoutParams as FrameLayout.LayoutParams).apply {
width = selfHeight
height = selfWidth
}
minHeight = seekBarMinMaxValue
maxHeight = seekBarMinMaxValue
minWidth = seekBarMinMaxValue
maxWidth = seekBarMinMaxValue
if (selfProgress >= 0) progress = selfProgress
if (selfMax >= 0) max = selfMax
}
// 设置icon距离底部位置,以及icon背景
icon = parentView.findViewById<View>(R.id.icon).apply {
(layoutParams as LayoutParams).bottomMargin = seekBarMinMaxValue / 4
iconDrawable?.let { setBackgroundDrawable(it) }
}
}
}
1.3.1 Provide external customization
The public modification releases parentView, seekBar and icon, allowing application developers to customize related attributes and methods.
1.3.2 Support dynamic addition and use in code
The constructor method constructor(context: Context, width: Int, height: Int) is provided to dynamically use SeekBarVertical in the code.
1.3.3 Layout depends on selfWidth/selfHeight
Using selfWidth and selfHeight as the base reference, the code dynamically controls the size and position of SeekBarVertical.
2 Use vertical SeekBar
<com.metabounds.ui.SeekBarVertical
android:layout_width="96dp"
android:layout_height="216dp"/>
Related attribute values can be specified in xml
app:UISeekBarIcon="@drawable/ic_user_head"
app:UISeekBarMax="12"
app:UISeekBarProgress="3"
You can also specify it in the code
binding.seekBar.icon.setBackgroundResource(R.drawable.ic_user_head)
binding.seekBar.seekBar.max = 12
binding.seekBar.seekBar.progress = 3
Dynamically add SeekBarVertical in code
binding.root.addView(SeekBarVertical(this, 96, 216))
3 Summary
The implementation of this article relies on the rotation attribute of Android 11 and above. If you want to be compatible with lower versions of Android, it is recommended to use the custom View of Solution 1 or the canvas rotation + translation of Solution 2.
Table of contents
1.1.1 Set a layer of FrameLayout
1.1.2 SeekBar removes left and right spacing
1.1.3 SeekBar height cannot be set
1.1.4 SeekBar background setting
1.1.5 The bottom View size and distance from the bottom are not hard-coded
1.2 Custom style attributes and themes
1.2.2 Customize theme specific attributes
1.3.1 Provide external customization
1.3.2 Support dynamic addition and use in code
1.3.3 Layout depends on selfWidth/selfHeight