Elegantly implement vertical SeekBar: without inheriting Seekbar or customizing View

Table of contents

0 Preface

About custom View

1 Implement vertical SeekBar

1.1 XML layout parsing

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.1 Custom style attributes

1.2.2 Customize theme specific attributes

1.3 Coding implementation

1.3.1 Provide external customization

1.3.2 Support dynamic addition and use in code

1.3.3 Layout depends on selfWidth/selfHeight

2 Use vertical SeekBar

3 Summary


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:

  1. The volume or brightness adjustment UI needs to be draggable;
  2. 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

0 Preface

About custom View

1 Implement vertical SeekBar

1.1 XML layout parsing

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.1 Custom style attributes

1.2.2 Customize theme specific attributes

1.3 Coding implementation

1.3.1 Provide external customization

1.3.2 Support dynamic addition and use in code

1.3.3 Layout depends on selfWidth/selfHeight

2 Use vertical SeekBar

3 Summary


Guess you like

Origin blog.csdn.net/Agg_bin/article/details/131676712