优雅实现垂直SeekBar:不继承Seekbar、不自定义View

目录

0 前言

关于自定义View

1 实现竖直SeekBar

1.1 XML布局解析

1.1.1 套一层FrameLayout

1.1.2 SeekBar去除左右间距

1.1.3 SeekBar高度无法设置

1.1.4 SeekBar背景设置

1.1.5 底部View尺寸和距底部距离不硬编码

1.2 自定义样式属性与主题

1.2.1 自定义样式属性

1.2.2 自定义主题指定属性

1.3 编码实现

1.3.1 对外提供客制化

1.3.2 支持代码中动态添加使用

1.3.3 布局依赖selfWidth/selfHeight

2 使用竖直SeekBar

3 小结


0 前言

        我们知道Android原生不支持垂直的SeekBar,为什么?我想给他们找个的理由,终究是没找到,或许懒惰是程序员的美德吧!好了,下面切入正题:

        实现下面这个UI,你会怎么做?

        下面我们首先来分解下需求:

  1. 音量或亮度调节UI,需要支持可拖动;
  2. 背景、前景、Icon位置、圆角以及UI尺寸可客制化。

        那么,Android的SeekBar支持可拖动,但是水平方向的,如果使用原生则需要将其变为竖直;否则,需要自定义View去绘制这样一个控件。

        另外,UI元素的可客制化要求不能硬编码,这也有好些细节,后面我们会展开讲讲。

关于自定义View

        其实我并不反对自定义View,尽管我们可以自己去画出来这样一个垂直的SeekBar,可是咱能确保自己对View的measure、layout以及draw的处理好过Android原生吗?不会存在性能问题吗?愚以为不见得。且自定义View,可能还费时费力不讨好,UI还原度达不到设计稿上的效果。

        所以,我的思想是尽可能复用Android原生View,或将View进行组合为最佳,其次为切图,是在没招才去自己创建画布绘制View,但一定要兼顾到功能和性能的平衡。比如:我之前通过自定义View实现的VerticalProgress,对功能和性能的平衡就不一定是最佳。

        【自定义View之VerticalProgress_Swuagg的博客

1 实现竖直SeekBar

        实现竖直SeekBar,目前大抵有如下3种方案:

方案一:通过继承View自定义实现,如:继承View自定义实现VerticalSeekBar

方案二:通过继承SeekBar重载方法内部旋转实现,如:继承SeekBar重载方法内部旋转实现

方案三:对于API 11和更高版本,通过rotation属性在XML中指定旋转270°实现,如:使用seekbar的XML属性(android:rotation="270")获得垂直效果

        看标题我们就知道,本文采用的方案三,不继承SeekBar,不自定义View,下面我们通过代码详细看看。

1.1 XML布局解析

        下面先看看实现效果:

        接下来我们再分析下xml布局文件:

<?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>

        首先,我们可以看到未硬编码任何数据,我知道你想拿270反驳我,但这不就是需求设计如此嘛。

1.1.1 套一层FrameLayout

        1)SeekBar旋转270°后会存在尺寸问题,需要套一层FrameLayout,且FrameLayout的宽、高与SeekBar相反,为SeekBar的高、宽,SeekBar也需要指定layout_gravity为center;

       2) 我们发现XML中两者的宽、高都是使用match_parent,是为了让用户客制化尺寸,我们在代码中根据外部使用处的宽、高会进行动态修改。

1.1.2 SeekBar去除左右间距

        SeekBar左右存在默认Padding,设置paddingHorizontal为0未生效,需要设置paddingStart和paddingEnd为0.

1.1.3 SeekBar高度无法设置

        需要同时设置minHeight、minWidth、maxHeight、maxWidth,值为宽与高的最小值,因为未硬编码,所以需在代码中实现。

1.1.4 SeekBar背景设置

        通过自定义属性UISeekBarProgressDrawable,根据主题指定相应drawable,可做到随主题切换。背景文件ui_seekbar_vertical_bg.xml如下:

<?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 底部View尺寸和距底部距离不硬编码

        1)宽度占比一半实现:通过指定宽高为0,设置layout_constraintDimensionRatio="1:1",layout_constraintWidth_percent="0.5",以及左右对齐;

        2)距底部距离,因为未硬编码,所以需在代码中实现。

1.2 自定义样式属性与主题

1.2.1 自定义样式属性

    <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 自定义主题指定属性

    <!--应用Theme主题-->
    <style name="DefaultAppTheme" parent="android:Theme.Black.NoTitleBar.Fullscreen">
        <item name="UISeekBarProgressDrawable">@drawable/ui_seekbar_vertical_bg</item>
    </style>

1.3 编码实现

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 对外提供客制化

        public修饰放开parentView、seekBar以及icon,让应用开发者可自定义相关属性方法。

1.3.2 支持代码中动态添加使用

        提供构造方法constructor(context: Context, width: Int, height: Int),可在代码中动态使用SeekBarVertical。

1.3.3 布局依赖selfWidth/selfHeight

        以selfWidth与selfHeight为基准参考,代码中动态控制SeekBarVertical相关尺寸和位置。

2 使用竖直SeekBar

    <com.metabounds.ui.SeekBarVertical
        android:layout_width="96dp"
        android:layout_height="216dp"/>

        可在xml中指定相关属性值

        app:UISeekBarIcon="@drawable/ic_user_head"
        app:UISeekBarMax="12"
        app:UISeekBarProgress="3"

        也可在代码中指定

binding.seekBar.icon.setBackgroundResource(R.drawable.ic_user_head)
binding.seekBar.seekBar.max = 12
binding.seekBar.seekBar.progress = 3

        代码中动态添加SeekBarVertical

binding.root.addView(SeekBarVertical(this, 96, 216))

3 小结

        本文的实现依赖于Android11及以上的rotation属性,如果要兼容低版本Android,还是建议采用方案1的自定义View或方案2的canvas旋转+平移。


目录

0 前言

关于自定义View

1 实现竖直SeekBar

1.1 XML布局解析

1.1.1 套一层FrameLayout

1.1.2 SeekBar去除左右间距

1.1.3 SeekBar高度无法设置

1.1.4 SeekBar背景设置

1.1.5 底部View尺寸和距底部距离不硬编码

1.2 自定义样式属性与主题

1.2.1 自定义样式属性

1.2.2 自定义主题指定属性

1.3 编码实现

1.3.1 对外提供客制化

1.3.2 支持代码中动态添加使用

1.3.3 布局依赖selfWidth/selfHeight

2 使用竖直SeekBar

3 小结


猜你喜欢

转载自blog.csdn.net/Agg_bin/article/details/131676712