目录
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,你会怎么做?
下面我们首先来分解下需求:
- 音量或亮度调节UI,需要支持可拖动;
- 背景、前景、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 小结